From 6316ef592ecceb4354e668b10430db1fab301290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Thu, 22 Oct 2026 14:08:07 +0200 Subject: [PATCH] Fix ALTER TABLE ADD with a parenthesized column list "ALTER TABLE t ADD [COLUMN] (a INT, b INT)" failed with a TypeError. The column name in a "columnDefinition" node is a "fieldIdentifier", not an "identifier", so the name lookup returned null. The list can also hold indexes and constraints, e.g. "ADD (a INT, INDEX (a))". These were skipped when the list had a column, and only the first one was recorded when it had none. Record all list items, columns first, like CREATE TABLE does. --- ...s-wp-sqlite-information-schema-builder.php | 11 +- .../tests/WP_MySQL_On_SQLite_Tests.php | 187 ++++++++++++++++++ 2 files changed, 194 insertions(+), 4 deletions(-) diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php index 9cd5c3dfd..d1fd1b7a3 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php @@ -665,12 +665,15 @@ public function record_alter_table( WP_Parser_Node $node ): void { // ADD if ( WP_MySQL_Lexer::ADD_SYMBOL === $first_token->id ) { // ADD [COLUMN] (...[, ...]) - $column_definitions = $action->get_descendant_nodes( 'columnDefinition' ); - if ( count( $column_definitions ) > 0 ) { - foreach ( $column_definitions as $column_definition ) { - $name = $this->get_value( $column_definition->get_first_child_node( 'identifier' ) ); + $table_element_list = $action->get_first_child_node( 'tableElementList' ); + if ( null !== $table_element_list ) { + foreach ( $table_element_list->get_descendant_nodes( 'columnDefinition' ) as $column_definition ) { + $name = $this->get_value( $column_definition->get_first_child_node( 'fieldIdentifier' ) ); $this->record_add_column( $table_is_temporary, $table_name, $name, $column_definition ); } + foreach ( $table_element_list->get_descendant_nodes( 'tableConstraintDef' ) as $constraint ) { + $this->record_add_constraint_or_index( $table_is_temporary, $table_name, $constraint ); + } continue; } diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php index d404a0b44..d3fcac24c 100644 --- a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php @@ -1778,6 +1778,193 @@ public function testAlterTableAddNotNullVarcharColumn() { ); } + public function testAlterTableAddColumnList(): void { + $this->assertQuery( 'CREATE TABLE t (id INT)' ); + + // ADD (...) syntax. + $this->assertQuery( "ALTER TABLE t ADD (a INT, b VARCHAR(10) NOT NULL DEFAULT 'x')" ); + + // ADD COLUMN (...) syntax. + $this->assertQuery( 'ALTER TABLE t ADD COLUMN (c INT)' ); + + $this->assertQuery( 'SHOW COLUMNS FROM t' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'a', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'b', + 'Type' => 'varchar(10)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => 'x', + 'Extra' => '', + ), + (object) array( + 'Field' => 'c', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $this->last_result + ); + + $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertEquals( + implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `id` int DEFAULT NULL,', + ' `a` int DEFAULT NULL,', + " `b` varchar(10) NOT NULL DEFAULT 'x',", + ' `c` int DEFAULT NULL', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + $this->last_result[0]->{'Create Table'} + ); + + $this->assertQuery( 'INSERT INTO t (id, a, c) VALUES (1, 2, 3)' ); + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( + array( + (object) array( + 'id' => '1', + 'a' => '2', + 'b' => 'x', + 'c' => '3', + ), + ), + $this->last_result + ); + } + + public function testAlterTableAddColumnListWithConstraints(): void { + $this->assertQuery( 'CREATE TABLE t (id INT)' ); + $this->assertQuery( + 'ALTER TABLE t ADD ( + a INT AUTO_INCREMENT PRIMARY KEY, + b INT UNIQUE, + INDEX idx_c (c), + c INT, + CONSTRAINT chk_c CHECK (c > 0) + )' + ); + + $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertEquals( + implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `id` int DEFAULT NULL,', + ' `a` int NOT NULL AUTO_INCREMENT,', + ' `b` int DEFAULT NULL,', + ' `c` int DEFAULT NULL,', + ' PRIMARY KEY (`a`),', + ' UNIQUE KEY `b` (`b`),', + ' KEY `idx_c` (`c`),', + ' CONSTRAINT `chk_c` CHECK (c > 0)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + $this->last_result[0]->{'Create Table'} + ); + + // Insert valid data. + $this->assertQuery( 'INSERT INTO t (c) VALUES (1)' ); + $this->assertQuery( 'SELECT * FROM t' ); + $this->assertEquals( + array( + (object) array( + 'id' => null, + 'a' => '1', + 'b' => null, + 'c' => '1', + ), + ), + $this->last_result + ); + + // Insert invalid data. + $this->assertQueryError( + 'INSERT INTO t (c) VALUES (0)', + 'SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: chk_c' + ); + } + + public function testAlterTableAddIndexList(): void { + $this->assertQuery( 'CREATE TABLE t (a INT, b INT)' ); + $this->assertQuery( 'ALTER TABLE t ADD (INDEX idx_a (a), UNIQUE KEY uk_b (b))' ); + + $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertEquals( + implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `a` int DEFAULT NULL,', + ' `b` int DEFAULT NULL,', + ' UNIQUE KEY `uk_b` (`b`),', + ' KEY `idx_a` (`a`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + $this->last_result[0]->{'Create Table'} + ); + } + + public function testAlterTableAddColumnListErrors(): void { + $this->assertQuery( 'CREATE TABLE t (id INT)' ); + + $this->assertQueryError( + 'ALTER TABLE t ADD (a INT, id INT)', + "SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'id'" + ); + $this->assertQueryError( + 'ALTER TABLE t ADD (a INT, a INT)', + "SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'a'" + ); + $this->assertQueryError( + 'ALTER TABLE t ADD (a INT, INDEX (b))', + "SQLSTATE[42000]: Syntax error or access violation: 1072 Key column 'b' doesn't exist in table" + ); + + // The table is unchanged. + $this->assertQuery( 'SHOW COLUMNS FROM t' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $this->last_result + ); + } + public function testColumnWithOnUpdate() { // CREATE TABLE with ON UPDATE $this->assertQuery(