From 35a7acb4590f60473457651afdd30e4bc7d3b717 Mon Sep 17 00:00:00 2001 From: "Dominik Schilling (ocean90)" Date: Fri, 27 May 2016 11:56:20 +0000 Subject: [PATCH] Database: Normalize index definitions in `dbDelta()`. `dbDelta()` compares the index definitions against the result of `SHOW INDEX FROM $table_name`. This requires a specific format so indices are not unnecessarily re-created. This format wasn't ensured, until now. * Parse the raw index definition to extract the type, name and columns so a normalized definition can be built (#20263, #34873). * Standardize on uppercase types (#34871) and on 'KEY'. 'INDEX' is only a synonym for 'KEY'. * Escape index names with backticks (#20263). * Normalize columns: Ignore ASC and DESC definitions (#34959), remove whitespaces (#34869) and escape column names with backticks (#20263). * Add backticks to all index change queries (#20263). Props ocean90, pento, kurtpayne. Fixes #20263, #34869, #34871, #34873, #34959. git-svn-id: https://develop.svn.wordpress.org/trunk@37583 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/upgrade.php | 109 ++++++++++-- tests/phpunit/tests/dbdelta.php | 267 +++++++++++++++++++++++++++++- 2 files changed, 364 insertions(+), 12 deletions(-) diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index c025d81b76..0df44a80ed 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -2184,10 +2184,11 @@ function dbDelta( $queries = '', $execute = true ) { $flds = explode("\n", $qryline); // For every field line specified in the query. - foreach ($flds as $fld) { + foreach ( $flds as $fld ) { + $fld = trim( $fld, " \t\n\r\0\x0B," ); // Default trim characters, plus ','. // Extract the field name. - preg_match("|^([^ ]*)|", trim($fld), $fvals); + preg_match( '|^([^ ]*)|', $fld, $fvals ); $fieldname = trim( $fvals[1], '`' ); $fieldname_lowercased = strtolower( $fieldname ); @@ -2202,14 +2203,98 @@ function dbDelta( $queries = '', $execute = true ) { case 'key': case 'spatial': $validfield = false; - $indices[] = trim(trim($fld), ", \n"); + + /* + * Normalize the index definition. + * + * This is done so the definition can be compared against the result of a + * `SHOW INDEX FROM $table_name` query which returns the current table + * index information. + */ + + // Extract type, name and columns from the definition. + preg_match( + '/^' + . '(?P' // 1) Type of the index. + . 'PRIMARY\s+KEY|(?:UNIQUE|FULLTEXT|SPATIAL)\s+(?:KEY|INDEX)|KEY|INDEX' + . ')' + . '\s+' // Followed by at least one white space character. + . '(?:' // Name of the index. Optional if type is PRIMARY KEY. + . '`?' // Name can be escaped with a backtick. + . '(?P' // 2) Name of the index. + . '(?:[0-9a-zA-Z$_-]|[\xC2-\xDF][\x80-\xBF])+' + . ')' + . '`?' // Name can be escaped with a backtick. + . '\s+' // Followed by at least one white space character. + . ')*' + . '\(' // Opening bracket for the columns. + . '(?P' + . '.+?' // 3) Column names, index prefixes, and orders. + . ')' + . '\)' // Closing bracket for the columns. + . '$/im', + $fld, + $index_matches + ); + + // Uppercase the index type and normalize space characters. + $index_type = strtoupper( preg_replace( '/\s+/', ' ', trim( $index_matches['index_type'] ) ) ); + + // 'INDEX' is a synonym for 'KEY', standardize on 'KEY'. + $index_type = str_replace( 'INDEX', 'KEY', $index_type ); + + // Escape the index name with backticks. An index for a primary key has no name. + $index_name = ( 'PRIMARY KEY' === $index_type ) ? '' : '`' . $index_matches['index_name'] . '`'; + + // Parse the columns. Multiple columns are separated by a comma. + $index_columns = array_map( 'trim', explode( ',', $index_matches['index_columns'] ) ); + + // Normalize columns. + foreach ( $index_columns as &$index_column ) { + // Extract column name and number of indexed characters (sub_part). + preg_match( + '/' + . '`?' // Name can be escaped with a backtick. + . '(?P' // 1) Name of the column. + . '(?:[0-9a-zA-Z$_-]|[\xC2-\xDF][\x80-\xBF])+' + . ')' + . '`?' // Name can be escaped with a backtick. + . '(?:' // Optional sub part. + . '\s*' // Optional white space character between name and opening bracket. + . '\(' // Opening bracket for the sub part. + . '\s*' // Optional white space character after opening bracket. + . '(?P' + . '\d+' // 2) Number of indexed characters. + . ')' + . '\s*' // Optional white space character before closing bracket. + . '\)' // Closing bracket for the sub part. + . ')?' + . '/', + $index_column, + $index_column_matches + ); + + // Escape the column name with backticks. + $index_column = '`' . $index_column_matches['column_name'] . '`'; + + // Append the optional sup part with the number of indexed characters. + if ( isset( $index_column_matches['sub_part'] ) ) { + $index_column .= '(' . $index_column_matches['sub_part'] . ')'; + } + } + + // Build the normalized index definition and add it to the list of indices. + $indices[] = "{$index_type} {$index_name} (" . implode( ',', $index_columns ) . ")"; + + // Destroy no longer needed variables. + unset( $index_column, $index_column_matches, $index_matches, $index_type, $index_name, $index_columns ); + break; } - $fld = trim( $fld ); // If it's a valid field, add it to the field array. if ( $validfield ) { - $cfields[ $fieldname_lowercased ] = trim( $fld, ", \n" ); + $cfields[ $fieldname_lowercased ] = $fld; } } @@ -2243,7 +2328,7 @@ function dbDelta( $queries = '', $execute = true ) { if ( $do_change ) { // Add a query to change the column type. - $cqueries[] = "ALTER TABLE {$table} CHANGE COLUMN {$tablefield->Field} " . $cfields[ $tablefield_field_lowercased ]; + $cqueries[] = "ALTER TABLE {$table} CHANGE COLUMN `{$tablefield->Field}` " . $cfields[ $tablefield_field_lowercased ]; $for_update[$table.'.'.$tablefield->Field] = "Changed type of {$table}.{$tablefield->Field} from {$tablefield->Type} to {$fieldtype}"; } } @@ -2253,7 +2338,7 @@ function dbDelta( $queries = '', $execute = true ) { $default_value = $matches[1]; if ($tablefield->Default != $default_value) { // Add a query to change the column's default value - $cqueries[] = "ALTER TABLE {$table} ALTER COLUMN {$tablefield->Field} SET DEFAULT '{$default_value}'"; + $cqueries[] = "ALTER TABLE {$table} ALTER COLUMN `{$tablefield->Field}` SET DEFAULT '{$default_value}'"; $for_update[$table.'.'.$tablefield->Field] = "Changed default value of {$table}.{$tablefield->Field} from {$tablefield->Default} to {$default_value}"; } } @@ -2306,17 +2391,19 @@ function dbDelta( $queries = '', $execute = true ) { $index_string .= 'SPATIAL '; } $index_string .= 'KEY '; - if ($index_name != 'PRIMARY') { - $index_string .= $index_name; + if ( 'PRIMARY' !== $index_name ) { + $index_string .= '`' . $index_name . '`'; } $index_columns = ''; // For each column in the index. foreach ($index_data['columns'] as $column_data) { - if ($index_columns != '') $index_columns .= ','; + if ( $index_columns != '' ) { + $index_columns .= ','; + } // Add the field to the column list string. - $index_columns .= $column_data['fieldname']; + $index_columns .= '`' . $column_data['fieldname'] . '`'; if ($column_data['subpart'] != '') { $index_columns .= '('.$column_data['subpart'].')'; } diff --git a/tests/phpunit/tests/dbdelta.php b/tests/phpunit/tests/dbdelta.php index 9f293417db..6ef3ef0f02 100644 --- a/tests/phpunit/tests/dbdelta.php +++ b/tests/phpunit/tests/dbdelta.php @@ -531,9 +531,274 @@ class Tests_dbDelta extends WP_UnitTestCase { $this->assertSame( array( "{$wpdb->prefix}spatial_index_test.spatial_value2" => "Added column {$wpdb->prefix}spatial_index_test.spatial_value2", - "Added index {$wpdb->prefix}spatial_index_test SPATIAL KEY spatial_key2 (spatial_value2)" + "Added index {$wpdb->prefix}spatial_index_test SPATIAL KEY `spatial_key2` (`spatial_value2`)" ), $updates ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}spatial_index_test" ); } + + /** + * @ticket 20263 + */ + function test_query_with_backticks_does_not_cause_a_query_to_alter_all_columns_and_indices_to_run_even_if_none_have_changed() { + global $wpdb; + + $schema = " + CREATE TABLE {$wpdb->prefix}dbdelta_test2 ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `references` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `compound_key` (`id`,`references`) + ) + "; + + $wpdb->query( $schema ); + + $updates = dbDelta( $schema ); + + $table_indices = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->prefix}dbdelta_test2" ); + $compound_key_index = wp_list_filter( $table_indices, array( 'Key_name' => 'compound_key' ) ); + + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}dbdelta_test2" ); + + $this->assertCount( 2, $compound_key_index ); + $this->assertEmpty( $updates ); + } + + /** + * @ticket 20263 + */ + function test_index_with_a_reserved_keyword_can_be_created() { + global $wpdb; + + $updates = dbDelta( + " + CREATE TABLE {$wpdb->prefix}dbdelta_test ( + id bigint(20) NOT NULL AUTO_INCREMENT, + column_1 varchar(255) NOT NULL, + column_2 text, + column_3 blob, + `references` varchar(255) NOT NULL, + PRIMARY KEY (id), + KEY key_1 (column_1), + KEY compound_key (id , column_1), + KEY compound_key2 (id,`references`), + FULLTEXT KEY fulltext_key (column_1) + ) ENGINE=MyISAM + " + ); + + $table_indices = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->prefix}dbdelta_test" ); + + $this->assertCount( 2, wp_list_filter( $table_indices, array( 'Key_name' => 'compound_key2' ) , 'AND' ) ); + + $this->assertSame( + array( + "{$wpdb->prefix}dbdelta_test.references" => "Added column {$wpdb->prefix}dbdelta_test.references", + 0 => "Added index {$wpdb->prefix}dbdelta_test KEY `compound_key2` (`id`,`references`)", + ), + $updates + ); + } + + /** + * @ticket 20263 + */ + function test_wp_get_db_schema_does_no_alter_queries_on_existing_install() { + $updates = dbDelta( wp_get_db_schema() ); + + $this->assertEmpty( $updates ); + } + + /** + * @ticket 20263 + */ + function test_key_and_index_and_fulltext_key_and_fulltext_index_and_unique_key_and_unique_index_indicies() { + global $wpdb; + + $schema = " + CREATE TABLE {$wpdb->prefix}dbdelta_test ( + id bigint(20) NOT NULL AUTO_INCREMENT, + column_1 varchar(255) NOT NULL, + column_2 text, + column_3 blob, + PRIMARY KEY (id), + KEY key_1 (column_1), + KEY compound_key (id,column_1), + FULLTEXT KEY fulltext_key (column_1), + INDEX key_2 (column_1), + UNIQUE KEY key_3 (column_1), + UNIQUE INDEX key_4 (column_1), + FULLTEXT INDEX key_5 (column_1), + ) ENGINE=MyISAM + "; + + $creates = dbDelta( $schema ); + $this->assertSame( + array( + 0 => "Added index {$wpdb->prefix}dbdelta_test KEY `key_2` (`column_1`)", + 1 => "Added index {$wpdb->prefix}dbdelta_test UNIQUE KEY `key_3` (`column_1`)", + 2 => "Added index {$wpdb->prefix}dbdelta_test UNIQUE KEY `key_4` (`column_1`)", + 3 => "Added index {$wpdb->prefix}dbdelta_test FULLTEXT KEY `key_5` (`column_1`)", + ), + $creates + ); + + $updates = dbDelta( $schema ); + $this->assertEmpty( $updates ); + } + + /** + * @ticket 20263 + */ + function test_index_and_key_are_synonyms_and_do_not_recreate_indices() { + global $wpdb; + + $updates = dbDelta( + " + CREATE TABLE {$wpdb->prefix}dbdelta_test ( + id bigint(20) NOT NULL AUTO_INCREMENT, + column_1 varchar(255) NOT NULL, + column_2 text, + column_3 blob, + PRIMARY KEY (id), + INDEX key_1 (column_1), + INDEX compound_key (id,column_1), + FULLTEXT INDEX fulltext_key (column_1) + ) ENGINE=MyISAM + " + ); + + $this->assertEmpty( $updates ); + } + + /** + * @ticket 20263 + */ + function test_indices_with_prefix_limits_are_created_and_do_not_recreate_indices() { + global $wpdb; + + $schema = " + CREATE TABLE {$wpdb->prefix}dbdelta_test ( + id bigint(20) NOT NULL AUTO_INCREMENT, + column_1 varchar(255) NOT NULL, + column_2 text, + column_3 blob, + PRIMARY KEY (id), + KEY key_1 (column_1), + KEY compound_key (id,column_1), + FULLTEXT KEY fulltext_key (column_1), + KEY key_2 (column_1(10)), + KEY key_3 (column_2(100),column_1(10)), + ) ENGINE=MyISAM + "; + + $creates = dbDelta( $schema ); + $this->assertSame( + array( + 0 => "Added index {$wpdb->prefix}dbdelta_test KEY `key_2` (`column_1`(10))", + 1 => "Added index {$wpdb->prefix}dbdelta_test KEY `key_3` (`column_2`(100),`column_1`(10))", + ), + $creates + ); + + $updates = dbDelta( $schema ); + $this->assertEmpty( $updates ); + } + + /** + * @ticket 34959 + */ + function test_index_col_names_with_order_do_not_recreate_indices() { + global $wpdb; + + $updates = dbDelta( + " + CREATE TABLE {$wpdb->prefix}dbdelta_test ( + id bigint(20) NOT NULL AUTO_INCREMENT, + column_1 varchar(255) NOT NULL, + column_2 text, + column_3 blob, + PRIMARY KEY (id), + KEY key_1 (column_1 DESC), + KEY compound_key (id,column_1 ASC), + FULLTEXT KEY fulltext_key (column_1) + ) ENGINE=MyISAM + " + ); + + $this->assertEmpty( $updates ); + } + + /** + * @ticket 34873 + */ + function test_primary_key_with_single_space_does_not_recreate_index() { + global $wpdb; + + $updates = dbDelta( + " + CREATE TABLE {$wpdb->prefix}dbdelta_test ( + id bigint(20) NOT NULL AUTO_INCREMENT, + column_1 varchar(255) NOT NULL, + column_2 text, + column_3 blob, + PRIMARY KEY (id), + KEY key_1 (column_1), + KEY compound_key (id,column_1), + FULLTEXT KEY fulltext_key (column_1) + ) ENGINE=MyISAM + " + ); + + $this->assertEmpty( $updates ); + } + + /** + * @ticket 34869 + */ + function test_index_definitions_with_spaces_do_not_recreate_indices() { + global $wpdb; + + $updates = dbDelta( + " + CREATE TABLE {$wpdb->prefix}dbdelta_test ( + id bigint(20) NOT NULL AUTO_INCREMENT, + column_1 varchar(255) NOT NULL, + column_2 text, + column_3 blob, + PRIMARY KEY (id), + KEY key_1 ( column_1), + KEY compound_key (id, column_1), + FULLTEXT KEY fulltext_key (column_1) + ) ENGINE=MyISAM + " + ); + + $this->assertEmpty( $updates ); + } + + /** + * @ticket 34871 + */ + function test_index_types_are_not_case_sensitive_and_do_not_recreate_indices() { + global $wpdb; + + $updates = dbDelta( + " + CREATE TABLE {$wpdb->prefix}dbdelta_test ( + id bigint(20) NOT NULL AUTO_INCREMENT, + column_1 varchar(255) NOT NULL, + column_2 text, + column_3 blob, + PRIMARY KEY (id), + key key_1 (column_1), + key compound_key (id,column_1), + FULLTEXT KEY fulltext_key (column_1) + ) ENGINE=MyISAM + " + ); + + $this->assertEmpty( $updates ); + } }