MDL-29314 enforce index size limits
authorPetr Skoda <commits@skodak.org>
Mon, 12 Sep 2011 09:49:29 +0000 (11:49 +0200)
committerPetr Skoda <commits@skodak.org>
Fri, 16 Sep 2011 06:45:06 +0000 (08:45 +0200)
lib/ddl/simpletest/testddl.php
lib/ddl/sql_generator.php
lib/xmldb/xmldb_index.php
lib/xmldb/xmldb_object.php

index 2d3f56d..66b8e7b 100644 (file)
@@ -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() {
index 059c620..0717e9a 100644 (file)
@@ -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';
index 11ed9c7..655d7fb 100644 (file)
@@ -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).
index 5802110..4fd59ac 100644 (file)
@@ -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;
+    }
 }