From b2cfdcf6979eecb98a24459d81228acbab6a66f9 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Mon, 12 Sep 2011 11:49:29 +0200 Subject: [PATCH] MDL-29314 enforce index size limits --- lib/ddl/simpletest/testddl.php | 109 +++++++++++++++++++++++++++++++++ lib/ddl/sql_generator.php | 3 + lib/xmldb/xmldb_index.php | 85 +++++++++++++++++++++++++ lib/xmldb/xmldb_object.php | 13 ++++ 4 files changed, 210 insertions(+) diff --git a/lib/ddl/simpletest/testddl.php b/lib/ddl/simpletest/testddl.php index 2d3f56df401..66b8e7b3f58 100644 --- a/lib/ddl/simpletest/testddl.php +++ b/lib/ddl/simpletest/testddl.php @@ -1487,6 +1487,115 @@ class ddl_test extends UnitTestCase { $this->assertTrue(count($reserved) > 1); } + public function test_index_max_bytes() { + $DB = $this->tdb; + $dbman = $DB->get_manager(); + + $maxstr = ''; + for($i=0; $i<255; $i++) { + $maxstr .= '言'; // random long string that should fix exactly the limit for one char column + } + + $table = new xmldb_table('testtable'); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('name', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_index('name', XMLDB_INDEX_NOTUNIQUE, array('name')); + + // Drop if exists + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + $dbman->create_table($table); + $tablename = $table->getName(); + $this->tables[$tablename] = $table; + + $rec = new stdClass(); + $rec->name = $maxstr; + + $id = $DB->insert_record($tablename, $rec); + $this->assertTrue(!empty($id)); + + $rec = $DB->get_record($tablename, array('id'=>$id)); + $this->assertIdentical($rec->name, $maxstr); + + $dbman->drop_table($table); + + + $table = new xmldb_table('testtable'); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('name', XMLDB_TYPE_CHAR, 255+1, null, XMLDB_NOTNULL, null); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_index('name', XMLDB_INDEX_NOTUNIQUE, array('name')); + + try { + $dbman->create_table($table); + $this->assertTrue(false); + } catch (Exception $e) { + $this->assertTrue($e instanceof coding_exception); + } + } + + public function test_index_composed_max_bytes() { + $DB = $this->tdb; + $dbman = $DB->get_manager(); + + $maxstr = ''; + for($i=0; $i<200; $i++) { + $maxstr .= '言'; + } + $reststr = ''; + for($i=0; $i<133; $i++) { + $reststr .= '言'; + } + + $table = new xmldb_table('testtable'); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('name1', XMLDB_TYPE_CHAR, 200, null, XMLDB_NOTNULL, null); + $table->add_field('name2', XMLDB_TYPE_CHAR, 133, null, XMLDB_NOTNULL, null); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_index('name1-name2', XMLDB_INDEX_NOTUNIQUE, array('name1','name2')); + + // Drop if exists + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + $dbman->create_table($table); + $tablename = $table->getName(); + $this->tables[$tablename] = $table; + + $rec = new stdClass(); + $rec->name1 = $maxstr; + $rec->name2 = $reststr; + + $id = $DB->insert_record($tablename, $rec); + $this->assertTrue(!empty($id)); + + $rec = $DB->get_record($tablename, array('id'=>$id)); + $this->assertIdentical($rec->name1, $maxstr); + $this->assertIdentical($rec->name2, $reststr); + + + $table = new xmldb_table('testtable'); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('name1', XMLDB_TYPE_CHAR, 201, null, XMLDB_NOTNULL, null); + $table->add_field('name2', XMLDB_TYPE_CHAR, 133, null, XMLDB_NOTNULL, null); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_index('name1-name2', XMLDB_INDEX_NOTUNIQUE, array('name1','name2')); + + // Drop if exists + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + + try { + $dbman->create_table($table); + $this->assertTrue(false); + } catch (Exception $e) { + $this->assertTrue($e instanceof coding_exception); + } + } + // Following methods are not supported == Do not test /* public function testRenameIndex() { diff --git a/lib/ddl/sql_generator.php b/lib/ddl/sql_generator.php index 059c62016c8..0717e9a1bf0 100644 --- a/lib/ddl/sql_generator.php +++ b/lib/ddl/sql_generator.php @@ -348,6 +348,9 @@ abstract class sql_generator { * needed to create it (in array) */ public function getCreateIndexSQL($xmldb_table, $xmldb_index) { + if ($error = $xmldb_index->validateDefinition($xmldb_table)) { + throw new coding_exception($error); + } $unique = ''; $suffix = 'ix'; diff --git a/lib/xmldb/xmldb_index.php b/lib/xmldb/xmldb_index.php index 11ed9c738f0..655d7fb1e91 100644 --- a/lib/xmldb/xmldb_index.php +++ b/lib/xmldb/xmldb_index.php @@ -31,6 +31,22 @@ class xmldb_index extends xmldb_object { var $unique; var $fields; + /** + * Note: + * - MySQL: MyISAM has a limit of 1000 bytes for any key including composed, InnoDB has limit 3500 bytes. + * + * @const max length of composed indexes, one utf-8 char is 3 bytes in the worst case + */ + const INDEX_COMPOSED_MAX_BYTES = 999; + + /** + * Note: + * - MySQL: InnoDB limits size of index on single column to 767bytes (256 chars) + * + * @const single column index length limit, one utf-8 char is 3 bytes in the worst case + */ + const INDEX_MAX_BYTES = 765; + /** * Creates one new xmldb_index */ @@ -270,6 +286,75 @@ class xmldb_index extends xmldb_object { return $o; } + + /** + * Validates the index restrictions. + * + * The error message should not be localised because it is intended for developers, + * end users and admins should never see these problems! + * + * @param xmldb_table $xmldb_table optional when object is table + * @return string null if ok, error message if problem found + */ + function validateDefinition(xmldb_table $xmldb_table=null) { + if (!$xmldb_table) { + return 'Invalid xmldb_index->validateDefinition() call, $xmldb_table si required.'; + } + + $total = 0; + foreach ($this->getFields() as $fieldname) { + if (!$field = $xmldb_table->getField($fieldname)) { + // argh, we do not have the fields loaded yet, this should not happen during install + continue; + } + + switch ($field->getType()) { + case XMLDB_TYPE_INTEGER: + $total += 8; // big int + break; + + case XMLDB_TYPE_NUMBER: + $total += 12; // this is just a guess + break; + + case XMLDB_TYPE_FLOAT: + $total += 8; // double precision + break; + + case XMLDB_TYPE_CHAR: + if ($field->getLength() > self::INDEX_MAX_BYTES / 3) { + return 'Invalid index definition in table {'.$xmldb_table->getName(). '}: XMLDB_TYPE_CHAR field "'.$field->getName().'" can not be indexed because it is too long.' + .' Limit is '.(self::INDEX_MAX_BYTES/3).' chars.'; + } + $total += ($field->getLength() * 3); // the most complex utf-8 chars have 3 bytes + break; + + case XMLDB_TYPE_TEXT: + return 'Invalid index definition in table {'.$xmldb_table->getName(). '}: XMLDB_TYPE_TEXT field "'.$field->getName().'" can not be indexed'; + break; + + case XMLDB_TYPE_BINARY: + return 'Invalid index definition in table {'.$xmldb_table->getName(). '}: XMLDB_TYPE_BINARY field "'.$field->getName().'" can not be indexed'; + break; + + case XMLDB_TYPE_DATETIME: + $total += 8; // this is just a guess + break; + + case XMLDB_TYPE_TIMESTAMP: + $total += 8; // this is just a guess + break; + } + } + + if ($total > self::INDEX_COMPOSED_MAX_BYTES) { + return 'Invalid index definition in table {'.$xmldb_table->getName(). '}: the composed index on fields "'.implode(',', $this->getFields()).'" is too long.' + .' Limit is '.self::INDEX_COMPOSED_MAX_BYTES.' bytes / '.(self::INDEX_COMPOSED_MAX_BYTES/3).' chars.'; + } + + return null; + } + } /// TODO: Delete for 2.1 (deprecated in 2.0). diff --git a/lib/xmldb/xmldb_object.php b/lib/xmldb/xmldb_object.php index 5802110757a..4fd59acbee5 100644 --- a/lib/xmldb/xmldb_object.php +++ b/lib/xmldb/xmldb_object.php @@ -478,4 +478,17 @@ class xmldb_object { return $arr; } + + /** + * Validates the definition of objects and returns error message. + * + * The error message should not be localised because it is intended for developers, + * end users and admins should never see these problems! + * + * @param xmldb_table $xmldb_table optional when object is table + * @return string null if ok, error message if problem found + */ + function validateDefinition(xmldb_table $xmldb_table=null) { + return null; + } }