From 6b89473b10109c372305f59584535f9c56b4bca1 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Mon, 8 Sep 2025 17:00:06 +0200 Subject: [PATCH 01/10] Implement FOREIGN KEY contraints for CREATE TABLE --- tests/WP_SQLite_Driver_Metadata_Tests.php | 422 ++++++++++++++++++ tests/WP_SQLite_Driver_Translation_Tests.php | 39 ++ .../sqlite-ast/class-wp-sqlite-driver.php | 120 +++++ ...s-wp-sqlite-information-schema-builder.php | 305 +++++++++++-- 4 files changed, 858 insertions(+), 28 deletions(-) diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index 7b07a7a6..1d727fef 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -893,4 +893,426 @@ public function testInformationSchemaTableConstraintsDropIndex(): void { $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); $this->assertEquals( array(), $result ); } + + public function testInformationSchemaForeignKeys(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( + 'CREATE TABLE t2 ( + id INT, + FOREIGN KEY (id) REFERENCES t1 (id), + FOREIGN KEY idx_name (id) REFERENCES t1 (id), + CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES t1 (id), + CONSTRAINT fk2 FOREIGN KEY idx_name (id) REFERENCES t1 (id), + CONSTRAINT fk3 FOREIGN KEY (id) REFERENCES t1 (id) ON DELETE CASCADE, + CONSTRAINT fk4 FOREIGN KEY (id) REFERENCES t1 (id) ON UPDATE CASCADE, + CONSTRAINT fk5 FOREIGN KEY (id) REFERENCES t1 (id) ON DELETE CASCADE ON UPDATE CASCADE + )' + ); + + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_2', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk1', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk2', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk3', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk4', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk5', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'NO ACTION', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_2', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'NO ACTION', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk1', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'NO ACTION', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk2', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'NO ACTION', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk3', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'CASCADE', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk4', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'CASCADE', + 'DELETE_RULE' => 'NO ACTION', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk5', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'CASCADE', + 'DELETE_RULE' => 'CASCADE', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + ), + $result + ); + + // INFORMATION_SCHEMA.KEY_COLUMN_USAGE + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_2', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk1', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk2', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk3', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk4', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk5', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + ), + $result + ); + + // SHOW CREATE TABLE + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL,', + ' CONSTRAINT `fk1` FOREIGN KEY (`id`) REFERENCES `t1` (`id`),', + ' CONSTRAINT `fk2` FOREIGN KEY (`id`) REFERENCES `t1` (`id`),', + ' CONSTRAINT `fk3` FOREIGN KEY (`id`) REFERENCES `t1` (`id`) ON DELETE CASCADE,', + ' CONSTRAINT `fk4` FOREIGN KEY (`id`) REFERENCES `t1` (`id`) ON UPDATE CASCADE,', + ' CONSTRAINT `fk5` FOREIGN KEY (`id`) REFERENCES `t1` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,', + ' CONSTRAINT `t2_ibfk_1` FOREIGN KEY (`id`) REFERENCES `t1` (`id`),', + ' CONSTRAINT `t2_ibfk_2` FOREIGN KEY (`id`) REFERENCES `t1` (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + } + + public function testInformationSchemaForeignKeysWithMultipleColumns(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT, name VARCHAR(255))' ); + $this->assertQuery( + 'CREATE TABLE t2 ( + id INT, + name VARCHAR(255), + FOREIGN KEY (id, name) REFERENCES t1 (id, name) + )' + ); + + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => null, + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'NO ACTION', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + ), + $result + ); + + // INFORMATION_SCHEMA.KEY_COLUMN_USAGE + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'name', + 'ORDINAL_POSITION' => '2', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '2', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'name', + ), + ), + $result + ); + + // SHOW CREATE TABLE + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL,', + ' `name` varchar(255) DEFAULT NULL,', + ' CONSTRAINT `t2_ibfk_1` FOREIGN KEY (`id`, `name`) REFERENCES `t1` (`id`, `name`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + } } diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php index 53725179..23597977 100644 --- a/tests/WP_SQLite_Driver_Translation_Tests.php +++ b/tests/WP_SQLite_Driver_Translation_Tests.php @@ -213,6 +213,7 @@ public function testCreateTable(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -236,6 +237,7 @@ public function testCreateTableWithMultipleColumns(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -259,6 +261,7 @@ public function testCreateTableWithBasicConstraints(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -279,6 +282,7 @@ public function testCreateTableWithEngine(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -299,6 +303,7 @@ public function testCreateTableWithCollate(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -331,6 +336,7 @@ public function testCreateTableWithPrimaryKey(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -355,6 +361,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't1'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't1' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't1' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't1' ORDER BY constraint_name", ) ); @@ -377,6 +384,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't2'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't2' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't2' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't2' ORDER BY constraint_name", ) ); @@ -401,6 +409,7 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't3'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't3' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't3' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't3' ORDER BY constraint_name", ) ); } @@ -434,6 +443,7 @@ public function testCreateTableWithInlineUniqueIndexes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -473,6 +483,7 @@ public function testCreateTableWithStandaloneUniqueIndexes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -541,6 +552,7 @@ public function testAlterTableAddColumn(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -570,6 +582,7 @@ public function testAlterTableAddColumnWithNotNull(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -599,6 +612,7 @@ public function testAlterTableAddColumnWithDefault(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -628,6 +642,7 @@ public function testAlterTableAddColumnWithNotNullAndDefault(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -663,6 +678,7 @@ public function testAlterTableAddMultipleColumns(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -694,6 +710,7 @@ public function testAlterTableDropColumn(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -729,6 +746,7 @@ public function testAlterTableDropMultipleColumns(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -763,6 +781,7 @@ public function testAlterTableAddAndDropColumns(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -797,6 +816,7 @@ public function testAlterTableDropAndAddSingleColumn(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -818,6 +838,7 @@ public function testBitDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -839,6 +860,7 @@ public function testBooleanDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -868,6 +890,7 @@ public function testIntegerDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -893,6 +916,7 @@ public function testFloatDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -918,6 +942,7 @@ public function testDecimalTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -939,6 +964,7 @@ public function testCharDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -962,6 +988,7 @@ public function testVarcharDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -987,6 +1014,7 @@ public function testNationalCharDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1010,6 +1038,7 @@ public function testNcharVarcharDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1033,6 +1062,7 @@ public function testNationalVarcharDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1058,6 +1088,7 @@ public function testTextDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1077,6 +1108,7 @@ public function testEnumDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1104,6 +1136,7 @@ public function testDateAndTimeDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1125,6 +1158,7 @@ public function testBinaryDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1150,6 +1184,7 @@ public function testBlobDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1175,6 +1210,7 @@ public function testBasicSpatialDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1198,6 +1234,7 @@ public function testMultiObjectSpatialDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1219,6 +1256,7 @@ public function testGeometryCollectionDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -1238,6 +1276,7 @@ public function testSerialDataTypes(): void { "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index a907bafc..a168a00d 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -4094,6 +4094,42 @@ private function get_sqlite_create_table_statement( $grouped_constraints[ $name ][ $seq ] = $constraint; } + // 4. Get foreign key info. + $referential_constraints_table = $this->information_schema_builder + ->get_table_name( $table_is_temporary, 'referential_constraints' ); + $referential_constraints_info = $this->execute_sqlite_query( + sprintf( + 'SELECT * FROM %s WHERE constraint_schema = ? AND table_name = ? ORDER BY constraint_name', + $this->quote_sqlite_identifier( $referential_constraints_table ) + ), + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + $key_column_usage_map = array(); + if ( count( $referential_constraints_info ) > 0 ) { + $key_column_usage_table = $this->information_schema_builder + ->get_table_name( $table_is_temporary, 'key_column_usage' ); + $key_column_usage_info = $this->execute_sqlite_query( + sprintf( + 'SELECT * FROM %s WHERE table_schema = ? AND table_name = ?', + $this->quote_sqlite_identifier( $key_column_usage_table ) + ), + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + $key_column_usage_map = array(); + foreach ( $key_column_usage_info as $key_column_usage ) { + $constraint_name = $key_column_usage['CONSTRAINT_NAME']; + if ( ! isset( $key_column_usage_map[ $constraint_name ] ) ) { + $key_column_usage_map[ $constraint_name ] = array(); + } + $key_column_usage_map[ $constraint_name ][] = array( + $key_column_usage['COLUMN_NAME'], + $key_column_usage['REFERENCED_COLUMN_NAME'], + ); + } + } + // 4. Generate CREATE TABLE statement columns. $rows = array(); $on_update_queries = array(); @@ -4230,6 +4266,30 @@ function ( $column ) { } } + // Add foreign key constraints. + foreach ( $referential_constraints_info as $referential_constraint ) { + $column_names = array(); + $referenced_column_names = array(); + foreach ( $key_column_usage_map[ $referential_constraint['CONSTRAINT_NAME'] ] as $info ) { + $column_names[] = $this->quote_sqlite_identifier( $info[0] ); + $referenced_column_names[] = $this->quote_sqlite_identifier( $info[1] ); + } + $query = sprintf( + ' CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)', + $this->quote_sqlite_identifier( $referential_constraint['CONSTRAINT_NAME'] ), + implode( ', ', $column_names ), + $this->quote_sqlite_identifier( $referential_constraint['REFERENCED_TABLE_NAME'] ), + implode( ', ', $referenced_column_names ) + ); + if ( 'NO ACTION' !== $referential_constraint['DELETE_RULE'] ) { + $query .= sprintf( ' ON DELETE %s', $referential_constraint['DELETE_RULE'] ); + } + if ( 'NO ACTION' !== $referential_constraint['UPDATE_RULE'] ) { + $query .= sprintf( ' ON UPDATE %s', $referential_constraint['UPDATE_RULE'] ); + } + $rows[] = $query; + } + // 6. Compose the CREATE TABLE statement. $create_table_query = sprintf( "CREATE %sTABLE %s (\n", @@ -4312,6 +4372,42 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str $grouped_constraints[ $name ][ $seq ] = $constraint; } + // Get foreign key info. + $referential_constraints_table = $this->information_schema_builder + ->get_table_name( $table_is_temporary, 'referential_constraints' ); + $referential_constraints_info = $this->execute_sqlite_query( + sprintf( + 'SELECT * FROM %s WHERE constraint_schema = ? AND table_name = ? ORDER BY constraint_name', + $this->quote_sqlite_identifier( $referential_constraints_table ) + ), + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + $key_column_usage_map = array(); + if ( count( $referential_constraints_info ) > 0 ) { + $key_column_usage_table = $this->information_schema_builder + ->get_table_name( $table_is_temporary, 'key_column_usage' ); + $key_column_usage_info = $this->execute_sqlite_query( + sprintf( + 'SELECT * FROM %s WHERE table_schema = ? AND table_name = ?', + $this->quote_sqlite_identifier( $key_column_usage_table ) + ), + array( $this->db_name, $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + $key_column_usage_map = array(); + foreach ( $key_column_usage_info as $key_column_usage ) { + $constraint_name = $key_column_usage['CONSTRAINT_NAME']; + if ( ! isset( $key_column_usage_map[ $constraint_name ] ) ) { + $key_column_usage_map[ $constraint_name ] = array(); + } + $key_column_usage_map[ $constraint_name ][] = array( + $key_column_usage['COLUMN_NAME'], + $key_column_usage['REFERENCED_COLUMN_NAME'], + ); + } + } + // 4. Generate CREATE TABLE statement columns. $rows = array(); foreach ( $column_info as $column ) { @@ -4413,6 +4509,30 @@ function ( $column ) { $rows[] = $sql; } + // Add foreign key constraints. + foreach ( $referential_constraints_info as $referential_constraint ) { + $column_names = array(); + $referenced_column_names = array(); + foreach ( $key_column_usage_map[ $referential_constraint['CONSTRAINT_NAME'] ] as $info ) { + $column_names[] = $this->quote_mysql_identifier( $info[0] ); + $referenced_column_names[] = $this->quote_mysql_identifier( $info[1] ); + } + $sql = sprintf( + ' CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)', + $this->quote_mysql_identifier( $referential_constraint['CONSTRAINT_NAME'] ), + implode( ', ', $column_names ), + $this->quote_mysql_identifier( $referential_constraint['REFERENCED_TABLE_NAME'] ), + implode( ', ', $referenced_column_names ) + ); + if ( 'NO ACTION' !== $referential_constraint['DELETE_RULE'] ) { + $sql .= sprintf( ' ON DELETE %s', $referential_constraint['DELETE_RULE'] ); + } + if ( 'NO ACTION' !== $referential_constraint['UPDATE_RULE'] ) { + $sql .= sprintf( ' ON UPDATE %s', $referential_constraint['UPDATE_RULE'] ); + } + $rows[] = $sql; + } + // 5. Compose the CREATE TABLE statement. $collation = $table_info['TABLE_COLLATION']; $charset = substr( $collation, 0, strpos( $collation, '_' ) ); diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php index 8baf9071..79e8dde5 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -26,13 +26,11 @@ class WP_SQLite_Information_Schema_Builder { * TODO (not yet implemented): * - VIEWS * - CHECK_CONSTRAINTS - * - KEY_COLUMN_USAGE (foreign keys) - * - REFERENTIAL_CONSTRAINTS (foreign keys) * - TRIGGERS */ const INFORMATION_SCHEMA_TABLE_DEFINITIONS = array( // INFORMATION_SCHEMA.SCHEMATA - 'schemata' => " + 'schemata' => " CATALOG_NAME TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' SCHEMA_NAME TEXT NOT NULL COLLATE NOCASE, -- database name DEFAULT_CHARACTER_SET_NAME TEXT NOT NULL COLLATE NOCASE, -- default character set @@ -43,7 +41,7 @@ class WP_SQLite_Information_Schema_Builder { ", // INFORMATION_SCHEMA.TABLES - 'tables' => " + 'tables' => " TABLE_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' TABLE_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- database name TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name @@ -70,7 +68,7 @@ class WP_SQLite_Information_Schema_Builder { ", // INFORMATION_SCHEMA.COLUMNS - 'columns' => " + 'columns' => " TABLE_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' TABLE_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- database name TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name @@ -97,7 +95,7 @@ class WP_SQLite_Information_Schema_Builder { ", // INFORMATION_SCHEMA.STATISTICS (indexes) - 'statistics' => " + 'statistics' => " TABLE_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' TABLE_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- database name TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name @@ -121,7 +119,7 @@ class WP_SQLite_Information_Schema_Builder { ", // INFORMATION_SCHEMA.TABLE_CONSTRAINTS - 'table_constraints' => " + 'table_constraints' => " CONSTRAINT_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' CONSTRAINT_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- constraint database name CONSTRAINT_NAME TEXT NOT NULL COLLATE NOCASE, -- constraint name @@ -137,6 +135,39 @@ class WP_SQLite_Information_Schema_Builder { PRIMARY KEY (TABLE_SCHEMA, TABLE_NAME, CONSTRAINT_TYPE, CONSTRAINT_NAME), UNIQUE (CONSTRAINT_SCHEMA, TABLE_NAME, CONSTRAINT_TYPE, CONSTRAINT_NAME) ", + + // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + 'referential_constraints' => " + CONSTRAINT_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' + CONSTRAINT_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- constraint database name + CONSTRAINT_NAME TEXT NOT NULL COLLATE NOCASE, -- constraint name + UNIQUE_CONSTRAINT_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' + UNIQUE_CONSTRAINT_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- referenced unique constraint database name + UNIQUE_CONSTRAINT_NAME TEXT COLLATE NOCASE, -- referenced unique constraint name or NULL + MATCH_OPTION TEXT NOT NULL COLLATE NOCASE DEFAULT 'NONE', -- always 'NONE' + UPDATE_RULE TEXT NOT NULL COLLATE NOCASE, -- 'CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT', 'NO ACTION' + DELETE_RULE TEXT NOT NULL COLLATE NOCASE, -- 'CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT', 'NO ACTION' + TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name + REFERENCED_TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- referenced table name + PRIMARY KEY (CONSTRAINT_SCHEMA, CONSTRAINT_NAME) + ", + + // INFORMATION_SCHEMA.KEY_COLUMN_USAGE + 'key_column_usage' => " + CONSTRAINT_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' + CONSTRAINT_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- constraint database name + CONSTRAINT_NAME TEXT NOT NULL COLLATE NOCASE, -- constraint name + TABLE_CATALOG TEXT NOT NULL DEFAULT 'def' COLLATE NOCASE, -- always 'def' + TABLE_SCHEMA TEXT NOT NULL COLLATE NOCASE, -- table database name + TABLE_NAME TEXT NOT NULL COLLATE NOCASE, -- table name + COLUMN_NAME TEXT NOT NULL COLLATE NOCASE, -- column name + ORDINAL_POSITION INTEGER NOT NULL, -- column position + POSITION_IN_UNIQUE_CONSTRAINT INTEGER, -- column position in referenced unique constraint + REFERENCED_TABLE_SCHEMA TEXT COLLATE NOCASE, -- referenced table database name + REFERENCED_TABLE_NAME TEXT COLLATE NOCASE, -- referenced table name + REFERENCED_COLUMN_NAME TEXT COLLATE NOCASE, -- referenced column name + UNIQUE (CONSTRAINT_SCHEMA, CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_SCHEMA) + ", ); /** @@ -1100,12 +1131,9 @@ private function record_add_constraint( $keyword = $keyword->get_first_child_token(); } - // FOREIGN KEY and CHECK constraints are not supported yet. - if ( - WP_MySQL_Lexer::FOREIGN_SYMBOL === $keyword->id - || WP_MySQL_Lexer::CHECK_SYMBOL === $keyword->id - ) { - throw new \Exception( 'FOREIGN KEY and CHECK constraints are not supported yet.' ); + // CHECK constraints are not supported yet. + if ( WP_MySQL_Lexer::CHECK_SYMBOL === $keyword->id ) { + throw new \Exception( 'CHECK constraints are not supported yet.' ); } // PRIMARY KEY and UNIQUE require an index. @@ -1135,19 +1163,32 @@ private function record_add_constraint( $index_name = null; } - // Save table constraint data. - $constraint_data = $this->extract_table_constraint_data( - $node, - $table_name, - $index_name - ); + // Extract constraint data. + $constraint_data = $this->extract_table_constraint_data( $node, $table_name, $index_name ); + $referential_constraint_data = $this->extract_referential_constraint_data( $node, $table_name ); + $key_column_usage_data = $this->extract_key_column_usage_data( $node, $table_name ); + // Save constraint data. if ( null !== $constraint_data ) { $this->insert_values( $this->get_table_name( $table_is_temporary, 'table_constraints' ), $constraint_data ); } + + if ( null !== $referential_constraint_data ) { + $this->insert_values( + $this->get_table_name( $table_is_temporary, 'referential_constraints' ), + $referential_constraint_data + ); + } + + foreach ( $key_column_usage_data as $key_column_usage_item ) { + $this->insert_values( + $this->get_table_name( $table_is_temporary, 'key_column_usage' ), + $key_column_usage_item + ); + } } /** @@ -1369,7 +1410,7 @@ private function extract_index_statistics_data( * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. * @param string $table_name The table name. * @param string $column_name The column name. - * @return array Table constraint data for the information schema. + * @return array|null Table constraint data for the information schema. */ public function extract_table_constraint_data( WP_Parser_Node $node, @@ -1382,7 +1423,7 @@ public function extract_table_constraint_data( } // Index name always takes precedence over constraint name. - $name = $index_name ?? $this->get_table_constraint_name( $node ); + $name = $index_name ?? $this->get_table_constraint_name( $node, $table_name ); return array( 'table_schema' => $this->db_name, 'table_name' => $table_name, @@ -1392,6 +1433,126 @@ public function extract_table_constraint_data( ); } + /** + * Extract referential constraint data from the "tableConstraintDef" AST node. + * + * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @param string $table_name The table name. + * @return array The referential constraint data as stored in information schema. + */ + private function extract_referential_constraint_data( WP_Parser_Node $node, string $table_name ): array { + // Referenced table name. + $references = $node->get_first_child_node( 'references' ); + $referenced_table = $references->get_first_child_node( 'tableRef' ); + $referenced_identifiers = $referenced_table->get_descendant_nodes( 'identifier' ); + $referenced_table_name = $this->get_value( end( $referenced_identifiers ) ); + + // Referenced column names. + $reference_parts = $references->get_first_child_node( 'identifierListWithParentheses' ) + ->get_first_child_node( 'identifierList' ) + ->get_child_nodes( 'identifier' ); + + // ON UPDATE and ON DELETE both use the "deleteOption" node. + $actions = $this->get_foreign_key_actions( $references ); + $on_update = $actions['on_update']; + $on_delete = $actions['on_delete']; + + // Find PRIMARY and UNIQUE constraints in the referenced table. + $table_is_temporary = false; + $statistics_table_name = $this->get_table_name( $table_is_temporary, 'statistics' ); + $statistics = $this->connection->query( + ' + SELECT index_name, column_name + FROM ' . $this->connection->quote_identifier( $statistics_table_name ) . " + WHERE table_schema = ? + AND table_name = ? + AND non_unique = 0 + ORDER BY index_name = 'PRIMARY' DESC, index_name, seq_in_index + ", + array( $this->db_name, $referenced_table_name ) + )->fetchAll( + PDO::FETCH_ASSOC // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO + ); + + // Group index columns to a map. + $index_columns_map = array(); + foreach ( $statistics as $statistics_item ) { + $index_columns_map[ $statistics_item['INDEX_NAME'] ][] = $statistics_item['COLUMN_NAME']; + } + + // Find which index includes referenced column names as a prefix. + $unique_constraint_name = null; + foreach ( $index_columns_map as $index_name => $index_columns ) { + $is_prefix = true; + foreach ( $reference_parts as $i => $reference_part ) { + if ( $index_columns[ $i ] !== $this->get_value( $reference_part ) ) { + $is_prefix = false; + break; + } + } + if ( $is_prefix ) { + $unique_constraint_name = $index_name; + break; + } + } + + $name = $this->get_table_constraint_name( $node, $table_name ); + return array( + 'constraint_schema' => $this->db_name, + 'constraint_name' => $name, + 'unique_constraint_schema' => $this->db_name, + 'unique_constraint_name' => $unique_constraint_name, + 'update_rule' => $on_update, + 'delete_rule' => $on_delete, + 'table_name' => $table_name, + 'referenced_table_name' => $referenced_table_name, + ); + } + + /** + * Extract key column usage data from the "tableConstraintDef" AST node. + * + * @param WP_Parser_Node $node The "tableConstraintDef" AST node. + * @param string $table_name The table name. + * @return array The key column usage data as stored in information schema. + */ + private function extract_key_column_usage_data( WP_Parser_Node $node, string $table_name ): array { + // Referenced table name. + $references = $node->get_first_child_node( 'references' ); + $referenced_table = $references->get_first_child_node( 'tableRef' ); + $referenced_identifiers = $referenced_table->get_descendant_nodes( 'identifier' ); + $referenced_table_schema = count( $referenced_identifiers ) > 1 + ? $this->get_value( $referenced_identifiers[0] ) + : $this->db_name; + $referenced_table_name = $this->get_value( end( $referenced_identifiers ) ); + + $name = $this->get_table_constraint_name( $node, $table_name ); + + $key_parts = $node->get_first_descendant_node( 'keyList' )->get_child_nodes( 'keyPart' ); + $reference_parts = $references->get_first_child_node( 'identifierListWithParentheses' ) + ->get_first_child_node( 'identifierList' ) + ->get_child_nodes( 'identifier' ); + + $rows = array(); + foreach ( $key_parts as $i => $key_part ) { + $column_name = $this->get_value( $key_part->get_first_child_node( 'identifier' ) ); + + $rows[] = array( + 'constraint_schema' => $this->db_name, + 'constraint_name' => $name, + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'column_name' => $column_name, + 'ordinal_position' => $i + 1, + 'position_in_unique_constraint' => $i + 1, + 'referenced_table_schema' => $referenced_table_schema, + 'referenced_table_name' => $referenced_table_name, + 'referenced_column_name' => $this->get_value( $reference_parts[ $i ] ), + ); + } + return $rows; + } + /** * Update column info from constraint data in the statistics table. * @@ -2034,16 +2195,54 @@ private function get_column_generation_expression( WP_Parser_Node $node ): strin /** * Extract table constraint name from the "tableConstraintDef" or "columnDefinition" AST node. * - * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. - * @return string|null The table constraint name. + * @param WP_Parser_Node $node The "tableConstraintDef" or "columnDefinition" AST node. + * @param string $table_name The table name. + * @return string|null The table constraint name. */ - public function get_table_constraint_name( WP_Parser_Node $node ): ?string { + public function get_table_constraint_name( WP_Parser_Node $node, string $table_name ): ?string { $name_node = $node->get_first_child_node( 'constraintName' ); if ( null !== $name_node ) { - return $this->get_value( $name_node ); + return $this->get_value( $name_node->get_first_child_node( 'identifier' ) ); } - // TODO: Handle CHECK/FOREIGN KEY/UNIQUE constraints. + // FOREIGN KEY constraint without a name gets a generated name. + if ( $node->get_first_descendant_node( 'references' ) ) { + // Get the highest existing name in format "_ibfk_". + $existing_names = $this->connection->query( + sprintf( + "SELECT DISTINCT constraint_name + FROM %s + WHERE table_schema = ? + AND table_name = ? + AND (constraint_name LIKE ? ESCAPE '\\')", + $this->connection->quote_identifier( + $this->get_table_name( + $this->temporary_table_exists( $table_name ), + 'table_constraints' + ) + ) + ), + array( + $this->db_name, + $table_name, + str_replace( array( '_', '%' ), array( '\\_', '\\%' ), $table_name ) . '\\_ibfk\\_%', + ) + )->fetchAll( + PDO::FETCH_COLUMN // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO + ); + + $last_name_index = 0; + foreach ( $existing_names as $existing_name ) { + $parts = explode( '_', $existing_name ); + $last_part = end( $parts ); + if ( strlen( $last_part ) === strspn( $last_part, '0123456789' ) ) { + $last_name_index = (int) max( $last_name_index, (int) $last_part ); + } + } + return $table_name . '_ibfk_' . ( $last_name_index + 1 ); + } + + // TODO: Handle CHECK constraints. return null; } @@ -2060,10 +2259,10 @@ private function get_table_constraint_type( WP_Parser_Node $node ): ?string { if ( $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ) ) { return 'UNIQUE'; } - if ( $node->get_first_descendant_token( WP_MySQL_Lexer::FOREIGN_SYMBOL ) ) { + if ( $node->get_first_descendant_node( 'references' ) ) { return 'FOREIGN KEY'; } - if ( $node->get_first_descendant_token( WP_MySQL_Lexer::CHECK_SYMBOL ) ) { + if ( $node->get_first_descendant_node( 'checkConstraint' ) ) { return 'CHECK'; } return null; @@ -2282,6 +2481,56 @@ private function get_index_column_sub_part( return $value; } + /** + * Extract foreign key UPDATE and DELETE actions from the "references" AST node. + * + * @param WP_Parser_Node $node The "references" AST node. + * @return array The foreign key actions as stored in information schema. + */ + private function get_foreign_key_actions( WP_Parser_Node $node ): array { + $children = $node->get_children(); + + // ON UPDATE and ON DELETE both use the "deleteOption" node. + $update_option = null; + $delete_option = null; + foreach ( $children as $i => $child ) { + if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::UPDATE_SYMBOL === $child->id ) { + $update_option = $children[ $i + 1 ]; + } elseif ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::DELETE_SYMBOL === $child->id ) { + $delete_option = $children[ $i + 1 ]; + } + } + + $result = array( + 'on_update' => 'NO ACTION', + 'on_delete' => 'NO ACTION', + ); + foreach ( array( 'on_update', 'on_delete' ) as $action ) { + $option = 'on_update' === $action ? $update_option : $delete_option; + if ( null === $option ) { + continue; + } + + $tokens = $option->get_descendant_tokens(); + $token1_id = isset( $tokens[0] ) ? $tokens[0]->id : null; + $token2_id = isset( $tokens[1] ) ? $tokens[1]->id : null; + if ( WP_MySQL_Lexer::NO_SYMBOL === $token1_id ) { + $result[ $action ] = 'NO ACTION'; + } elseif ( WP_MySQL_Lexer::RESTRICT_SYMBOL === $token1_id ) { + $result[ $action ] = 'RESTRICT'; + } elseif ( WP_MySQL_Lexer::CASCADE_SYMBOL === $token1_id ) { + $result[ $action ] = 'CASCADE'; + } elseif ( WP_MySQL_Lexer::SET_SYMBOL === $token1_id && WP_MySQL_Lexer::NULL_SYMBOL === $token2_id ) { + $result[ $action ] = 'SET NULL'; + } elseif ( WP_MySQL_Lexer::SET_SYMBOL === $token1_id && WP_MySQL_Lexer::DEFAULT_SYMBOL === $token2_id ) { + $result[ $action ] = 'SET DEFAULT'; + } else { + throw new \Exception( sprintf( 'Unsupported foreign key action: %s', $option->get_value() ) ); + } + } + return $result; + } + /** * Determine whether the column data type is a spatial data type. * From e9d17de67d2dd440ad959eaf6f15c6272526b186 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Tue, 9 Sep 2025 10:28:12 +0200 Subject: [PATCH 02/10] Implement inline REFERENCES clause in column definition --- tests/WP_SQLite_Driver_Metadata_Tests.php | 129 ++++++++++++++++++ ...s-wp-sqlite-information-schema-builder.php | 70 +++++++--- 2 files changed, 183 insertions(+), 16 deletions(-) diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index 1d727fef..d9d29bd9 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -1211,6 +1211,135 @@ public function testInformationSchemaForeignKeys(): void { ); } + public function testInformationSchemaInlineForeignKeys(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT, name VARCHAR(255))' ); + $this->assertQuery( + 'CREATE TABLE t2 ( + id INT, + t1_id INT REFERENCES t1 (id), + t1_name VARCHAR(255) REFERENCES t1 (name) ON DELETE CASCADE + )' + ); + + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_2', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => null, + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'NO ACTION', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_2', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => null, + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'CASCADE', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + ), + $result + ); + + // INFORMATION_SCHEMA.KEY_COLUMN_USAGE + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 't1_id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_2', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 't1_name', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'name', + ), + ), + $result + ); + + // SHOW CREATE TABLE + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL,', + ' `t1_id` int DEFAULT NULL,', + ' `t1_name` varchar(255) DEFAULT NULL,', + ' CONSTRAINT `t2_ibfk_1` FOREIGN KEY (`t1_id`) REFERENCES `t1` (`id`),', + ' CONSTRAINT `t2_ibfk_2` FOREIGN KEY (`t1_name`) REFERENCES `t1` (`name`) ON DELETE CASCADE', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + } + public function testInformationSchemaForeignKeysWithMultipleColumns(): void { $this->assertQuery( 'CREATE TABLE t1 (id INT, name VARCHAR(255))' ); $this->assertQuery( diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php index 79e8dde5..3befe370 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -510,34 +510,52 @@ public function record_create_table( WP_Parser_Node $node ): void { throw $e; } - // Inline column constraints and indexes. - $index_data = $this->extract_column_statistics_data( + // Extract inline column constraints and indexes. + $index_data = $this->extract_column_statistics_data( $table_name, $column_name, $column_node, 'YES' === $column_data['is_nullable'] ); + $constraint_data = $this->extract_table_constraint_data( + $column_node, + $table_name, + $index_data['index_name'] ?? null + ); + $referential_constraint_data = $this->extract_referential_constraint_data( + $column_node, + $table_name + ); + $key_column_usage_data = $this->extract_key_column_usage_data( + $column_node, + $table_name + ); + // Save inline column constraints and indexes. if ( null !== $index_data ) { $this->insert_values( $this->get_table_name( $table_is_temporary, 'statistics' ), $index_data ); } - - // Save constraint data. - $constraint_data = $this->extract_table_constraint_data( - $column_node, - $table_name, - $index_data['index_name'] ?? null - ); - if ( null !== $constraint_data ) { $this->insert_values( $this->get_table_name( $table_is_temporary, 'table_constraints' ), $constraint_data ); } + if ( null !== $referential_constraint_data ) { + $this->insert_values( + $this->get_table_name( $table_is_temporary, 'referential_constraints' ), + $referential_constraint_data + ); + } + foreach ( $key_column_usage_data as $key_column_usage_item ) { + $this->insert_values( + $this->get_table_name( $table_is_temporary, 'key_column_usage' ), + $key_column_usage_item + ); + } $column_position += 1; } @@ -1438,11 +1456,15 @@ public function extract_table_constraint_data( * * @param WP_Parser_Node $node The "tableConstraintDef" AST node. * @param string $table_name The table name. - * @return array The referential constraint data as stored in information schema. + * @return array|null The referential constraint data as stored in information schema. */ - private function extract_referential_constraint_data( WP_Parser_Node $node, string $table_name ): array { + private function extract_referential_constraint_data( WP_Parser_Node $node, string $table_name ): ?array { + $references = $node->get_first_descendant_node( 'references' ); + if ( null === $references ) { + return null; + } + // Referenced table name. - $references = $node->get_first_child_node( 'references' ); $referenced_table = $references->get_first_child_node( 'tableRef' ); $referenced_identifiers = $referenced_table->get_descendant_nodes( 'identifier' ); $referenced_table_name = $this->get_value( end( $referenced_identifiers ) ); @@ -1517,8 +1539,12 @@ private function extract_referential_constraint_data( WP_Parser_Node $node, stri * @return array The key column usage data as stored in information schema. */ private function extract_key_column_usage_data( WP_Parser_Node $node, string $table_name ): array { + $references = $node->get_first_descendant_node( 'references' ); + if ( null === $references ) { + return array(); + } + // Referenced table name. - $references = $node->get_first_child_node( 'references' ); $referenced_table = $references->get_first_child_node( 'tableRef' ); $referenced_identifiers = $referenced_table->get_descendant_nodes( 'identifier' ); $referenced_table_schema = count( $referenced_identifiers ) > 1 @@ -1528,14 +1554,26 @@ private function extract_key_column_usage_data( WP_Parser_Node $node, string $ta $name = $this->get_table_constraint_name( $node, $table_name ); - $key_parts = $node->get_first_descendant_node( 'keyList' )->get_child_nodes( 'keyPart' ); + if ( 'columnDefinition' === $node->rule_name ) { + $identifiers = $node + ->get_first_descendant_node( 'fieldIdentifier' ) + ->get_descendant_nodes( 'identifier' ); + $key_parts = array( end( $identifiers ) ); + } else { + $key_list = $node->get_first_descendant_node( 'keyList' ); + $key_parts = array(); + foreach ( $key_list->get_child_nodes( 'keyPart' ) as $key_part ) { + $key_parts[] = $key_part->get_first_child_node( 'identifier' ); + } + } + $reference_parts = $references->get_first_child_node( 'identifierListWithParentheses' ) ->get_first_child_node( 'identifierList' ) ->get_child_nodes( 'identifier' ); $rows = array(); foreach ( $key_parts as $i => $key_part ) { - $column_name = $this->get_value( $key_part->get_first_child_node( 'identifier' ) ); + $column_name = $this->get_value( $key_part ); $rows[] = array( 'constraint_schema' => $this->db_name, From 026a6023aca594d6dd28a69bbc7df3a2783a7600 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Tue, 9 Sep 2025 10:39:52 +0200 Subject: [PATCH 03/10] Add test for ALTER TABLE with adding a FOREIGN KEY contraint --- tests/WP_SQLite_Driver_Metadata_Tests.php | 123 ++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index d9d29bd9..b8403f2f 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -1444,4 +1444,127 @@ public function testInformationSchemaForeignKeysWithMultipleColumns(): void { $result ); } + + public function testInformationSchemaAlterTableAddForeignKeys(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(255))' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT)' ); + $this->assertQuery( 'ALTER TABLE t2 ADD FOREIGN KEY (id) REFERENCES t1 (id)' ); + $this->assertQuery( 'ALTER TABLE t2 ADD CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES t1 (id) ON DELETE CASCADE' ); + + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk1', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'CONSTRAINT_TYPE' => 'FOREIGN KEY', + 'ENFORCED' => 'YES', + ), + ), + $result + ); + + // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'NO ACTION', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk1', + 'UNIQUE_CONSTRAINT_CATALOG' => 'def', + 'UNIQUE_CONSTRAINT_SCHEMA' => 'wp', + 'UNIQUE_CONSTRAINT_NAME' => 'PRIMARY', + 'MATCH_OPTION' => 'NONE', + 'UPDATE_RULE' => 'NO ACTION', + 'DELETE_RULE' => 'CASCADE', + 'TABLE_NAME' => 't2', + 'REFERENCED_TABLE_NAME' => 't1', + ), + ), + $result + ); + + // INFORMATION_SCHEMA.KEY_COLUMN_USAGE + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertEquals( + array( + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 't2_ibfk_1', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + (object) array( + 'CONSTRAINT_CATALOG' => 'def', + 'CONSTRAINT_SCHEMA' => 'wp', + 'CONSTRAINT_NAME' => 'fk1', + 'TABLE_CATALOG' => 'def', + 'TABLE_SCHEMA' => 'wp', + 'TABLE_NAME' => 't2', + 'COLUMN_NAME' => 'id', + 'ORDINAL_POSITION' => '1', + 'POSITION_IN_UNIQUE_CONSTRAINT' => '1', + 'REFERENCED_TABLE_SCHEMA' => 'wp', + 'REFERENCED_TABLE_NAME' => 't1', + 'REFERENCED_COLUMN_NAME' => 'id', + ), + ), + $result + ); + + // SHOW CREATE TABLE + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL,', + ' CONSTRAINT `fk1` FOREIGN KEY (`id`) REFERENCES `t1` (`id`) ON DELETE CASCADE,', + ' CONSTRAINT `t2_ibfk_1` FOREIGN KEY (`id`) REFERENCES `t1` (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + } } From 5d793729aaa2184fc094dd40ea88bb2dca8bf917 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Tue, 9 Sep 2025 10:55:16 +0200 Subject: [PATCH 04/10] Implement ALTER TABLE ... DROP FOREIGN KEY --- tests/WP_SQLite_Driver_Metadata_Tests.php | 148 ++++++++++++++++++ ...s-wp-sqlite-information-schema-builder.php | 49 ++++++ 2 files changed, 197 insertions(+) diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index b8403f2f..130f657f 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -1567,4 +1567,152 @@ public function testInformationSchemaAlterTableAddForeignKeys(): void { $result ); } + + public function testInformationSchemaAlterTableDropForeignKeys(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(255))' ); + $this->assertQuery( + 'CREATE TABLE t2 ( + id INT, + t1_id INT REFERENCES t1 (id), + FOREIGN KEY (t1_id) REFERENCES t1 (id), + CONSTRAINT fk1 FOREIGN KEY (t1_id) REFERENCES t1 (id) ON DELETE CASCADE + )' + ); + + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertCount( 3, $result ); + + // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertCount( 3, $result ); + + // INFORMATION_SCHEMA.KEY_COLUMN_USAGE + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertCount( 3, $result ); + + // SHOW CREATE TABLE + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL,', + ' `t1_id` int DEFAULT NULL,', + ' CONSTRAINT `fk1` FOREIGN KEY (`t1_id`) REFERENCES `t1` (`id`) ON DELETE CASCADE,', + ' CONSTRAINT `t2_ibfk_1` FOREIGN KEY (`t1_id`) REFERENCES `t1` (`id`),', + ' CONSTRAINT `t2_ibfk_2` FOREIGN KEY (`t1_id`) REFERENCES `t1` (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + + // DROP the first foreign key. + $this->assertQuery( 'ALTER TABLE t2 DROP FOREIGN KEY t2_ibfk_1' ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertCount( 2, $result ); + $this->assertEquals( 't2_ibfk_2', $result[0]->CONSTRAINT_NAME ); + $this->assertEquals( 'fk1', $result[1]->CONSTRAINT_NAME ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertCount( 2, $result ); + $this->assertEquals( 't2_ibfk_2', $result[0]->CONSTRAINT_NAME ); + $this->assertEquals( 'fk1', $result[1]->CONSTRAINT_NAME ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertCount( 2, $result ); + $this->assertEquals( 't2_ibfk_2', $result[0]->CONSTRAINT_NAME ); + $this->assertEquals( 'fk1', $result[1]->CONSTRAINT_NAME ); + + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL,', + ' `t1_id` int DEFAULT NULL,', + ' CONSTRAINT `fk1` FOREIGN KEY (`t1_id`) REFERENCES `t1` (`id`) ON DELETE CASCADE,', + ' CONSTRAINT `t2_ibfk_2` FOREIGN KEY (`t1_id`) REFERENCES `t1` (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + + // DROP the second foreign key. + $this->assertQuery( 'ALTER TABLE t2 DROP FOREIGN KEY t2_ibfk_2' ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'fk1', $result[0]->CONSTRAINT_NAME ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'fk1', $result[0]->CONSTRAINT_NAME ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'fk1', $result[0]->CONSTRAINT_NAME ); + + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL,', + ' `t1_id` int DEFAULT NULL,', + ' CONSTRAINT `fk1` FOREIGN KEY (`t1_id`) REFERENCES `t1` (`id`) ON DELETE CASCADE', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + + // DROP the third foreign key. + $this->assertQuery( 'ALTER TABLE t2 DROP FOREIGN KEY fk1' ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertCount( 0, $result ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertCount( 0, $result ); + + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertCount( 0, $result ); + + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL,', + ' `t1_id` int DEFAULT NULL', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + } } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php index 3befe370..106300bb 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -640,6 +640,15 @@ public function record_alter_table( WP_Parser_Node $node ): void { // DROP if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) { + // DROP FOREIGN KEY + if ( $action->has_child_token( WP_MySQL_Lexer::FOREIGN_SYMBOL ) ) { + $field_identifier = $action->get_first_child_node( 'fieldIdentifier' ); + $identifiers = $field_identifier->get_descendant_nodes( 'identifier' ); + $name = $this->get_value( end( $identifiers ) ); + $this->record_drop_foreign_key( $table_is_temporary, $table_name, $name ); + continue; + } + // DROP [COLUMN] $column_ref = $action->get_first_child_node( 'fieldIdentifier' ); if ( null !== $column_ref ) { @@ -1209,6 +1218,46 @@ private function record_add_constraint( } } + /** + * Analyze DROP FOREIGN KEY statement and record data in the information schema. + * + * @param bool $table_is_temporary Whether the table is temporary. + * @param string $table_name The table name. + * @param string $name The foreign key name. + */ + private function record_drop_foreign_key( + bool $table_is_temporary, + string $table_name, + string $name + ): void { + $this->delete_values( + $this->get_table_name( $table_is_temporary, 'table_constraints' ), + array( + 'TABLE_SCHEMA' => $this->db_name, + 'TABLE_NAME' => $table_name, + 'CONSTRAINT_NAME' => $name, + ) + ); + + $this->delete_values( + $this->get_table_name( $table_is_temporary, 'referential_constraints' ), + array( + 'CONSTRAINT_SCHEMA' => $this->db_name, + 'TABLE_NAME' => $table_name, + 'CONSTRAINT_NAME' => $name, + ) + ); + + $this->delete_values( + $this->get_table_name( $table_is_temporary, 'key_column_usage' ), + array( + 'TABLE_SCHEMA' => $this->db_name, + 'TABLE_NAME' => $table_name, + 'CONSTRAINT_NAME' => $name, + ) + ); + } + /** * Analyze "columnDefinition" or "fieldDefinition" AST node and extract column data. * From 6a3fb46e0afb89ed0b233adfb7c9d55efb0957d7 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Wed, 10 Sep 2025 10:47:14 +0200 Subject: [PATCH 05/10] Implement ALTER TABLE ... DROP CONSTRAINT for foreign keys --- tests/WP_SQLite_Driver_Metadata_Tests.php | 67 +++++++++++++++++++ .../sqlite-ast/class-wp-sqlite-driver.php | 16 +++++ ...s-wp-sqlite-information-schema-builder.php | 53 +++++++++++++++ ...wp-sqlite-information-schema-exception.php | 44 ++++++++++-- 4 files changed, 176 insertions(+), 4 deletions(-) diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index 130f657f..a01a6b21 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -1715,4 +1715,71 @@ public function testInformationSchemaAlterTableDropForeignKeys(): void { $result ); } + + public function testInformationSchemaAlterTableDropConstraint(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(255))' ); + $this->assertQuery( + 'CREATE TABLE t2 ( + id INT, + CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES t1 (id) + )' + ); + + $this->assertQuery( 'ALTER TABLE t2 DROP CONSTRAINT fk1' ); + + // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $this->assertCount( 0, $result ); + + // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $this->assertCount( 0, $result ); + + // INFORMATION_SCHEMA.KEY_COLUMN_USAGE + $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $this->assertCount( 0, $result ); + + // SHOW CREATE TABLE + $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t2` (', + ' `id` int DEFAULT NULL', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + } + + public function testInformationSchemaAlterTableDropMissingConstraint(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + + $this->expectException( WP_SQLite_Driver_Exception::class ); + $this->expectExceptionMessage( "SQLSTATE[HY000]: General error: 3940 Constraint 'cnst' does not exist." ); + $this->expectExceptionCode( 'HY000' ); + $this->assertQuery( 'ALTER TABLE t2 DROP CONSTRAINT cnst' ); + } + + public function testInformationSchemaAlterTableDropConstraintWithAmbiguousName(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(255))' ); + $this->assertQuery( + 'CREATE TABLE t2 ( + id INT, + CONSTRAINT cnst UNIQUE (id), + CONSTRAINT cnst FOREIGN KEY (id) REFERENCES t1 (id) + )' + ); + + $this->expectException( WP_SQLite_Driver_Exception::class ); + $this->expectExceptionMessage( "SQLSTATE[HY000]: General error: 3939 Table has multiple constraints with the name 'cnst'. Please use constraint specific 'DROP' clause." ); + $this->expectExceptionCode( 'HY000' ); + $this->assertQuery( 'ALTER TABLE t2 DROP CONSTRAINT cnst' ); + } } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index a168a00d..6a6baa43 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -4827,6 +4827,22 @@ private function convert_information_schema_exception( WP_SQLite_Information_Sch ), '42000' ); + case WP_SQLite_Information_Schema_Exception::TYPE_CONSTRAINT_DOES_NOT_EXIST: + return $this->new_driver_exception( + sprintf( + "SQLSTATE[HY000]: General error: 3940 Constraint '%s' does not exist.", + $e->get_data()['name'] + ), + 'HY000' + ); + case WP_SQLite_Information_Schema_Exception::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME: + return $this->new_driver_exception( + sprintf( + "SQLSTATE[HY000]: General error: 3939 Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", + $e->get_data()['name'] + ), + 'HY000' + ); default: return $e; } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php index 106300bb..03c71842 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -640,6 +640,13 @@ public function record_alter_table( WP_Parser_Node $node ): void { // DROP if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) { + // DROP CONSTRAINT + if ( $action->has_child_token( WP_MySQL_Lexer::CONSTRAINT_SYMBOL ) ) { + $name = $this->get_value( $action->get_first_child_node( 'identifier' ) ); + $this->record_drop_constraint( $table_is_temporary, $table_name, $name ); + continue; + } + // DROP FOREIGN KEY if ( $action->has_child_token( WP_MySQL_Lexer::FOREIGN_SYMBOL ) ) { $field_identifier = $action->get_first_child_node( 'fieldIdentifier' ); @@ -1218,6 +1225,52 @@ private function record_add_constraint( } } + /** + * Analyze DROP CONSTRAINT statement and record data in the information schema. + * + * @param bool $table_is_temporary Whether the table is temporary. + * @param string $table_name The table name. + * @param string $name The constraint name. + */ + private function record_drop_constraint( + bool $table_is_temporary, + string $table_name, + string $name + ): void { + $constraint_types = $this->connection->query( + sprintf( + 'SELECT constraint_type FROM %s WHERE table_schema = ? AND table_name = ? AND constraint_name = ?', + $this->connection->quote_identifier( $this->get_table_name( $table_is_temporary, 'table_constraints' ) ) + ), + array( + $this->db_name, + $table_name, + $name, + ) + )->fetchAll( + PDO::FETCH_COLUMN // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO + ); + + if ( 0 === count( $constraint_types ) ) { + throw WP_SQLite_Information_Schema_Exception::constraint_does_not_exist( $name ); + } + + // MySQL doesn't allow a generic DELETE CONSTRAINT clause when the target + // is ambiguous, i.e., when multiple constraints with the same name exist. + if ( count( $constraint_types ) > 1 ) { + throw WP_SQLite_Information_Schema_Exception::multiple_constraints_with_name( $name ); + } + + $constraint_type = $constraint_types[0]; + if ( 'FOREIGN KEY' === $constraint_type ) { + $this->record_drop_foreign_key( $table_is_temporary, $table_name, $name ); + } else { + throw new \Exception( + "DROP CONSTRAINT for constraint type '$constraint_type' is not supported." + ); + } + } + /** * Analyze DROP FOREIGN KEY statement and record data in the information schema. * diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php index 361a7854..93a9ab88 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php @@ -8,10 +8,12 @@ */ class WP_SQLite_Information_Schema_Exception extends Exception { // Information schema exception types. - const TYPE_DUPLICATE_TABLE_NAME = 'duplicate-table-name'; - const TYPE_DUPLICATE_COLUMN_NAME = 'duplicate-column-name'; - const TYPE_DUPLICATE_KEY_NAME = 'duplicate-key-name'; - const TYPE_KEY_COLUMN_NOT_FOUND = 'key-column-not-found'; + const TYPE_DUPLICATE_TABLE_NAME = 'duplicate-table-name'; + const TYPE_DUPLICATE_COLUMN_NAME = 'duplicate-column-name'; + const TYPE_DUPLICATE_KEY_NAME = 'duplicate-key-name'; + const TYPE_KEY_COLUMN_NOT_FOUND = 'key-column-not-found'; + const TYPE_CONSTRAINT_DOES_NOT_EXIST = 'constraint-does-not-exist'; + const TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME = 'multiple-constraints-with-name'; /** * The exception type. @@ -106,6 +108,12 @@ public static function duplicate_key_name( string $key_name ): WP_SQLite_Informa ); } + /** + * Create a key column not found exception. + * + * @param string $column_name The name of the affected column. + * @return self The exception instance. + */ public static function key_column_not_found( string $column_name ): WP_SQLite_Information_Schema_Exception { return new self( self::TYPE_KEY_COLUMN_NOT_FOUND, @@ -113,4 +121,32 @@ public static function key_column_not_found( string $column_name ): WP_SQLite_In array( 'column_name' => $column_name ) ); } + + /** + * Create a constraint does not exist exception. + * + * @param string $name The name of the affected constraint. + * @return self The exception instance. + */ + public static function constraint_does_not_exist( string $name ): WP_SQLite_Information_Schema_Exception { + return new self( + self::TYPE_CONSTRAINT_DOES_NOT_EXIST, + sprintf( "Constraint '%s' does not exist.", $name ), + array( 'name' => $name ) + ); + } + + /** + * Create a multiple constraints with name exception. + * + * @param string $name The name of the affected constraint. + * @return self The exception instance. + */ + public static function multiple_constraints_with_name( string $name ): WP_SQLite_Information_Schema_Exception { + return new self( + self::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME, + sprintf( "Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", $name ), + array( 'name' => $name ) + ); + } } From 26a1564b5153558d7d2c5deee875196802993cd5 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Wed, 10 Sep 2025 15:56:28 +0200 Subject: [PATCH 06/10] Store key column usage data also for PRIMARY and UNIQUE key records --- tests/WP_SQLite_Driver_Translation_Tests.php | 18 +++++ .../sqlite-ast/class-wp-sqlite-driver.php | 4 +- ...s-wp-sqlite-information-schema-builder.php | 73 ++++++++++++------- 3 files changed, 68 insertions(+), 27 deletions(-) diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php index 23597977..0c23e64d 100644 --- a/tests/WP_SQLite_Driver_Translation_Tests.php +++ b/tests/WP_SQLite_Driver_Translation_Tests.php @@ -258,6 +258,8 @@ public function testCreateTableWithBasicConstraints(): void { . " VALUES ('wp', 't', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't', 'wp', 'PRIMARY', 'PRIMARY KEY')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'PRIMARY', 'wp', 't', 'id', 1, null, null, null, null)", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -333,6 +335,8 @@ public function testCreateTableWithPrimaryKey(): void { . " VALUES ('wp', 't', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't', 'wp', 'PRIMARY', 'PRIMARY KEY')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'PRIMARY', 'wp', 't', 'id', 1, null, null, null, null)", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -358,6 +362,8 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void { . " VALUES ('wp', 't1', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't1', 'wp', 'PRIMARY', 'PRIMARY KEY')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'PRIMARY', 'wp', 't1', 'id', 1, null, null, null, null)", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't1'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't1' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't1' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -381,6 +387,8 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void { . " VALUES ('wp', 't2', 0, 'wp', 'PRIMARY', 1, 'id', 'A', 0, null, null, '', 'BTREE', '', '', 'YES', null)", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't2', 'wp', 'PRIMARY', 'PRIMARY KEY')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'PRIMARY', 'wp', 't2', 'id', 1, null, null, null, null)", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't2'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't2' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't2' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -406,6 +414,8 @@ public function testCreateTableWithPrimaryKeyAndAutoincrement(): void { "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't3'", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't3', 'wp', 'PRIMARY', 'PRIMARY KEY')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'PRIMARY', 'wp', 't3', 'id', 1, null, null, null, null)", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't3'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't3' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't3' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -434,12 +444,16 @@ public function testCreateTableWithInlineUniqueIndexes(): void { . " VALUES ('wp', 't', 0, 'wp', 'id', 1, 'id', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't', 'wp', 'id', 'UNIQUE')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'id', 'wp', 't', 'id', 1, null, null, null, null)", 'INSERT INTO `_wp_sqlite_mysql_information_schema_columns` (`table_schema`, `table_name`, `column_name`, `ordinal_position`, `column_default`, `is_nullable`, `data_type`, `character_maximum_length`, `character_octet_length`, `numeric_precision`, `numeric_scale`, `datetime_precision`, `character_set_name`, `collation_name`, `column_type`, `column_key`, `extra`, `privileges`, `column_comment`, `generation_expression`, `srs_id`)' . " VALUES ('wp', 't', 'name', 2, null, 'YES', 'text', 65535, 65535, null, null, null, 'utf8mb4', 'utf8mb4_0900_ai_ci', 'text', 'UNI', '', 'select,insert,update,references', '', '', null)", 'INSERT INTO `_wp_sqlite_mysql_information_schema_statistics` (`table_schema`, `table_name`, `non_unique`, `index_schema`, `index_name`, `seq_in_index`, `column_name`, `collation`, `cardinality`, `sub_part`, `packed`, `nullable`, `index_type`, `comment`, `index_comment`, `is_visible`, `expression`)' . " VALUES ('wp', 't', 0, 'wp', 'name', 1, 'name', 'A', 0, null, null, 'YES', 'BTREE', '', '', 'YES', null)", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't', 'wp', 'name', 'UNIQUE')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'name', 'wp', 't', 'name', 1, null, null, null, null)", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", @@ -473,6 +487,8 @@ public function testCreateTableWithStandaloneUniqueIndexes(): void { "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't', 'wp', 'id', 'UNIQUE')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'id', 'wp', 't', 'id', 1, null, null, null, null)", "SELECT column_name, data_type, is_nullable, character_maximum_length FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' AND column_name IN ('name')", "SELECT DISTINCT index_name FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' AND (index_name = 'name' OR index_name LIKE 'name\_%' ESCAPE '\\')", 'INSERT INTO `_wp_sqlite_mysql_information_schema_statistics` (`table_schema`, `table_name`, `non_unique`, `index_schema`, `index_name`, `seq_in_index`, `column_name`, `collation`, `cardinality`, `sub_part`, `packed`, `nullable`, `index_type`, `comment`, `index_comment`, `is_visible`, `expression`)' @@ -480,6 +496,8 @@ public function testCreateTableWithStandaloneUniqueIndexes(): void { "UPDATE `_wp_sqlite_mysql_information_schema_columns` AS c SET (column_key, is_nullable) = ( SELECT CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI' WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI' WHEN MAX(s.seq_in_index = 1) THEN 'MUL' ELSE '' END, CASE WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO' ELSE c.is_nullable END FROM `_wp_sqlite_mysql_information_schema_statistics` AS s WHERE s.table_schema = c.table_schema AND s.table_name = c.table_name AND s.column_name = c.column_name ) WHERE c.table_schema = 'wp' AND c.table_name = 't'", 'INSERT INTO `_wp_sqlite_mysql_information_schema_table_constraints` (`table_schema`, `table_name`, `constraint_schema`, `constraint_name`, `constraint_type`)' . " VALUES ('wp', 't', 'wp', 'name', 'UNIQUE')", + 'INSERT INTO `_wp_sqlite_mysql_information_schema_key_column_usage` (`constraint_schema`, `constraint_name`, `table_schema`, `table_name`, `column_name`, `ordinal_position`, `position_in_unique_constraint`, `referenced_table_schema`, `referenced_table_name`, `referenced_column_name`)' + . " VALUES ('wp', 'name', 'wp', 't', 'name', 1, null, null, null, null)", "SELECT * FROM `_wp_sqlite_mysql_information_schema_tables` WHERE table_type = 'BASE TABLE' AND table_schema = 'wp' AND table_name = 't'", "SELECT * FROM `_wp_sqlite_mysql_information_schema_columns` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY ordinal_position", "SELECT * FROM `_wp_sqlite_mysql_information_schema_statistics` WHERE table_schema = 'wp' AND table_name = 't' ORDER BY INDEX_NAME = 'PRIMARY' DESC, NON_UNIQUE = '0' DESC, INDEX_TYPE = 'SPATIAL' DESC, INDEX_TYPE = 'BTREE' DESC, INDEX_TYPE = 'FULLTEXT' DESC, ROWID, SEQ_IN_INDEX", diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 6a6baa43..be09b30e 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -4111,7 +4111,7 @@ private function get_sqlite_create_table_statement( ->get_table_name( $table_is_temporary, 'key_column_usage' ); $key_column_usage_info = $this->execute_sqlite_query( sprintf( - 'SELECT * FROM %s WHERE table_schema = ? AND table_name = ?', + 'SELECT * FROM %s WHERE table_schema = ? AND table_name = ? AND referenced_column_name IS NOT NULL', $this->quote_sqlite_identifier( $key_column_usage_table ) ), array( $this->db_name, $table_name ) @@ -4389,7 +4389,7 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str ->get_table_name( $table_is_temporary, 'key_column_usage' ); $key_column_usage_info = $this->execute_sqlite_query( sprintf( - 'SELECT * FROM %s WHERE table_schema = ? AND table_name = ?', + 'SELECT * FROM %s WHERE table_schema = ? AND table_name = ? AND referenced_column_name IS NOT NULL', $this->quote_sqlite_identifier( $key_column_usage_table ) ), array( $this->db_name, $table_name ) diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php index 03c71842..cc34334e 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -528,7 +528,8 @@ public function record_create_table( WP_Parser_Node $node ): void { ); $key_column_usage_data = $this->extract_key_column_usage_data( $column_node, - $table_name + $table_name, + $index_data['index_name'] ?? null ); // Save inline column constraints and indexes. @@ -1200,7 +1201,7 @@ private function record_add_constraint( // Extract constraint data. $constraint_data = $this->extract_table_constraint_data( $node, $table_name, $index_name ); $referential_constraint_data = $this->extract_referential_constraint_data( $node, $table_name ); - $key_column_usage_data = $this->extract_key_column_usage_data( $node, $table_name ); + $key_column_usage_data = $this->extract_key_column_usage_data( $node, $table_name, $index_name ); // Save constraint data. if ( null !== $constraint_data ) { @@ -1304,9 +1305,12 @@ private function record_drop_foreign_key( $this->delete_values( $this->get_table_name( $table_is_temporary, 'key_column_usage' ), array( - 'TABLE_SCHEMA' => $this->db_name, - 'TABLE_NAME' => $table_name, - 'CONSTRAINT_NAME' => $name, + 'TABLE_SCHEMA' => $this->db_name, + 'TABLE_NAME' => $table_name, + 'CONSTRAINT_NAME' => $name, + + // Remove only FOREIGN KEY records; not PRIMARY/UNIQUE KEY data. + 'REFERENCED_TABLE_SCHEMA' => $this->db_name, ) ); } @@ -1638,44 +1642,58 @@ private function extract_referential_constraint_data( WP_Parser_Node $node, stri * * @param WP_Parser_Node $node The "tableConstraintDef" AST node. * @param string $table_name The table name. + * @param string $index_name The index name, when the constraint uses an index. * @return array The key column usage data as stored in information schema. */ - private function extract_key_column_usage_data( WP_Parser_Node $node, string $table_name ): array { + private function extract_key_column_usage_data( + WP_Parser_Node $node, + string $table_name, + ?string $index_name = null + ): array { + $is_primary = $node->get_first_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ); + $is_unique = $node->get_first_descendant_token( WP_MySQL_Lexer::UNIQUE_SYMBOL ); $references = $node->get_first_descendant_node( 'references' ); - if ( null === $references ) { + if ( null === $references && ! $is_primary && ! $is_unique ) { return array(); } - // Referenced table name. - $referenced_table = $references->get_first_child_node( 'tableRef' ); - $referenced_identifiers = $referenced_table->get_descendant_nodes( 'identifier' ); - $referenced_table_schema = count( $referenced_identifiers ) > 1 - ? $this->get_value( $referenced_identifiers[0] ) - : $this->db_name; - $referenced_table_name = $this->get_value( end( $referenced_identifiers ) ); + // Referenced table name and column names. + if ( $references ) { + $referenced_table = $references->get_first_child_node( 'tableRef' ); + $referenced_identifiers = $referenced_table->get_descendant_nodes( 'identifier' ); + $referenced_table_schema = count( $referenced_identifiers ) > 1 + ? $this->get_value( $referenced_identifiers[0] ) + : $this->db_name; + $referenced_table_name = $this->get_value( end( $referenced_identifiers ) ); + $referenced_columns = $references->get_first_child_node( 'identifierListWithParentheses' ) + ->get_first_child_node( 'identifierList' ) + ->get_child_nodes( 'identifier' ); + } else { + $referenced_table_schema = null; + $referenced_table_name = null; + $referenced_columns = array(); + } - $name = $this->get_table_constraint_name( $node, $table_name ); + // Constraint name. + $name = $index_name ?? $this->get_table_constraint_name( $node, $table_name ); + // Key parts. if ( 'columnDefinition' === $node->rule_name ) { $identifiers = $node ->get_first_descendant_node( 'fieldIdentifier' ) ->get_descendant_nodes( 'identifier' ); $key_parts = array( end( $identifiers ) ); } else { - $key_list = $node->get_first_descendant_node( 'keyList' ); $key_parts = array(); - foreach ( $key_list->get_child_nodes( 'keyPart' ) as $key_part ) { + foreach ( $node->get_descendant_nodes( 'keyPart' ) as $key_part ) { $key_parts[] = $key_part->get_first_child_node( 'identifier' ); } } - $reference_parts = $references->get_first_child_node( 'identifierListWithParentheses' ) - ->get_first_child_node( 'identifierList' ) - ->get_child_nodes( 'identifier' ); - $rows = array(); foreach ( $key_parts as $i => $key_part ) { $column_name = $this->get_value( $key_part ); + $position = $i + 1; $rows[] = array( 'constraint_schema' => $this->db_name, @@ -1683,11 +1701,11 @@ private function extract_key_column_usage_data( WP_Parser_Node $node, string $ta 'table_schema' => $this->db_name, 'table_name' => $table_name, 'column_name' => $column_name, - 'ordinal_position' => $i + 1, - 'position_in_unique_constraint' => $i + 1, + 'ordinal_position' => $position, + 'position_in_unique_constraint' => $references ? $position : null, 'referenced_table_schema' => $referenced_table_schema, 'referenced_table_name' => $referenced_table_name, - 'referenced_column_name' => $this->get_value( $reference_parts[ $i ] ), + 'referenced_column_name' => $referenced_columns ? $this->get_value( $referenced_columns[ $i ] ) : null, ); } return $rows; @@ -2792,7 +2810,12 @@ private function update_values( string $table_name, array $data, array $where ): private function delete_values( string $table_name, array $where ): void { $where_statements = array(); foreach ( $where as $column => $value ) { - $where_statements[] = $this->connection->quote_identifier( $column ) . ' = ?'; + if ( null === $value ) { + $where_statements[] = $this->connection->quote_identifier( $column ) . ' IS NULL'; + unset( $where[ $column ] ); + } else { + $where_statements[] = $this->connection->quote_identifier( $column ) . ' = ?'; + } } $this->connection->query( From 2f93dbc2f649ca837524efdc0cb2f6c3099822d8 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Wed, 10 Sep 2025 16:11:06 +0200 Subject: [PATCH 07/10] Implement DROP PRIMARY KEY and DROP CONSTRAINT for primary and unique keys --- tests/WP_SQLite_Driver_Metadata_Tests.php | 130 +++++++++++++++--- ...s-wp-sqlite-information-schema-builder.php | 59 +++++++- 2 files changed, 171 insertions(+), 18 deletions(-) diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index a01a6b21..f8db1623 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -1716,39 +1716,80 @@ public function testInformationSchemaAlterTableDropForeignKeys(): void { ); } - public function testInformationSchemaAlterTableDropConstraint(): void { - $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(255))' ); - $this->assertQuery( - 'CREATE TABLE t2 ( - id INT, - CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES t1 (id) - )' + public function testInformationSchemaAlterTableDropPrimaryKey(): void { + $this->assertQuery( 'CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))' ); + + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `id` int NOT NULL,', + ' `name` varchar(255) DEFAULT NULL,', + ' PRIMARY KEY (`id`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result ); - $this->assertQuery( 'ALTER TABLE t2 DROP CONSTRAINT fk1' ); + $this->assertQuery( 'ALTER TABLE t DROP PRIMARY KEY' ); - // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS - $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); $this->assertCount( 0, $result ); - // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS - $result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + // INFORMATION_SCHEMA.STATISTICS + $result = $this->assertQuery( "SELECT * FROM information_schema.statistics WHERE table_name = 't'" ); $this->assertCount( 0, $result ); - // INFORMATION_SCHEMA.KEY_COLUMN_USAGE - $result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + // SHOW CREATE TABLE + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `id` int NOT NULL,', + ' `name` varchar(255) DEFAULT NULL', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + } + + public function testInformationSchemaAlterTableDropUniqueKey(): void { + $this->assertQuery( 'CREATE TABLE t (id INT, name TEXT, CONSTRAINT c UNIQUE (name))' ); + $this->assertQuery( 'ALTER TABLE t DROP INDEX c' ); + + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + + // INFORMATION_SCHEMA.STATISTICS + $result = $this->assertQuery( "SELECT * FROM information_schema.statistics WHERE table_name = 't'" ); $this->assertCount( 0, $result ); // SHOW CREATE TABLE - $result = $this->assertQuery( 'SHOW CREATE TABLE t2' ); + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); $this->assertEquals( array( (object) array( 'Create Table' => implode( "\n", array( - 'CREATE TABLE `t2` (', - ' `id` int DEFAULT NULL', + 'CREATE TABLE `t` (', + ' `id` int DEFAULT NULL,', + ' `name` text DEFAULT NULL', ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', ) ), @@ -1758,6 +1799,61 @@ public function testInformationSchemaAlterTableDropConstraint(): void { ); } + public function testInformationSchemaAlterTableDropConstraint(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(255))' ); + $this->assertQuery( + 'CREATE TABLE t2 ( + id INT PRIMARY KEY, + name VARCHAR(255), + CONSTRAINT fk FOREIGN KEY (id) REFERENCES t1 (id), + CONSTRAINT name_unique UNIQUE (name) + )' + ); + + // Check the constraint records. + $table_constraints = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $referential_constraints = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $key_column_usage = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $statistics = $this->assertQuery( "SELECT * FROM information_schema.statistics WHERE table_name = 't2'" ); + $this->assertCount( 3, $table_constraints ); + $this->assertCount( 1, $referential_constraints ); + $this->assertCount( 3, $key_column_usage ); + $this->assertCount( 2, $statistics ); + + // Drop the primary key constraint. + $this->assertQuery( 'ALTER TABLE t2 DROP PRIMARY KEY' ); + $table_constraints = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $referential_constraints = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $key_column_usage = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $statistics = $this->assertQuery( "SELECT * FROM information_schema.statistics WHERE table_name = 't2'" ); + $this->assertCount( 2, $table_constraints ); + $this->assertCount( 1, $referential_constraints ); + $this->assertCount( 2, $key_column_usage ); + $this->assertCount( 1, $statistics ); + + // Drop the unique key constraint. + $this->assertQuery( 'ALTER TABLE t2 DROP CONSTRAINT name_unique' ); + $table_constraints = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $referential_constraints = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $key_column_usage = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $statistics = $this->assertQuery( "SELECT * FROM information_schema.statistics WHERE table_name = 't2'" ); + $this->assertCount( 1, $table_constraints ); + $this->assertCount( 1, $referential_constraints ); + $this->assertCount( 1, $key_column_usage ); + $this->assertCount( 0, $statistics ); + + // Drop the foreign key constraint. + $this->assertQuery( 'ALTER TABLE t2 DROP FOREIGN KEY fk' ); + $table_constraints = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" ); + $referential_constraints = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" ); + $key_column_usage = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" ); + $statistics = $this->assertQuery( "SELECT * FROM information_schema.statistics WHERE table_name = 't2'" ); + $this->assertCount( 0, $table_constraints ); + $this->assertCount( 0, $referential_constraints ); + $this->assertCount( 0, $key_column_usage ); + $this->assertCount( 0, $statistics ); + } + public function testInformationSchemaAlterTableDropMissingConstraint(): void { $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php index cc34334e..404f03ed 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -648,6 +648,12 @@ public function record_alter_table( WP_Parser_Node $node ): void { continue; } + // DROP PRIMARY KEY + if ( $action->has_child_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { + $this->record_drop_key( $table_is_temporary, $table_name, 'PRIMARY' ); + continue; + } + // DROP FOREIGN KEY if ( $action->has_child_token( WP_MySQL_Lexer::FOREIGN_SYMBOL ) ) { $field_identifier = $action->get_first_child_node( 'fieldIdentifier' ); @@ -1263,7 +1269,11 @@ private function record_drop_constraint( } $constraint_type = $constraint_types[0]; - if ( 'FOREIGN KEY' === $constraint_type ) { + if ( 'PRIMARY KEY' === $constraint_type ) { + $this->record_drop_key( $table_is_temporary, $table_name, 'PRIMARY' ); + } elseif ( 'UNIQUE' === $constraint_type ) { + $this->record_drop_key( $table_is_temporary, $table_name, $name ); + } elseif ( 'FOREIGN KEY' === $constraint_type ) { $this->record_drop_foreign_key( $table_is_temporary, $table_name, $name ); } else { throw new \Exception( @@ -1272,6 +1282,53 @@ private function record_drop_constraint( } } + /** + * Analyze DROP PRIMARY KEY or DROP UNIQUE statement and record data + * in the information schema. + * + * @param bool $table_is_temporary Whether the table is temporary. + * @param string $table_name The table name. + * @param mixed $name The constraint name. + */ + private function record_drop_key( + bool $table_is_temporary, + string $table_name, + string $name + ): void { + $this->delete_values( + $this->get_table_name( $table_is_temporary, 'table_constraints' ), + array( + 'TABLE_SCHEMA' => $this->db_name, + 'TABLE_NAME' => $table_name, + 'CONSTRAINT_NAME' => $name, + ) + ); + + $this->delete_values( + $this->get_table_name( $table_is_temporary, 'statistics' ), + array( + 'TABLE_SCHEMA' => $this->db_name, + 'TABLE_NAME' => $table_name, + 'INDEX_NAME' => $name, + ) + ); + + $this->delete_values( + $this->get_table_name( $table_is_temporary, 'key_column_usage' ), + array( + 'TABLE_SCHEMA' => $this->db_name, + 'TABLE_NAME' => $table_name, + 'CONSTRAINT_NAME' => $name, + + // Remove only PRIMARY/UNIQUE key records; not FOREIGN KEY data. + 'REFERENCED_TABLE_SCHEMA' => null, + ) + ); + + // Sync column info from constraint data. + $this->sync_column_key_info( $table_is_temporary, $table_name ); + } + /** * Analyze DROP FOREIGN KEY statement and record data in the information schema. * From 1b951194abf3ec65408e08cd49cbbc5279180d28 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Wed, 10 Sep 2025 17:13:51 +0200 Subject: [PATCH 08/10] Add FOREIGN KEY tests --- tests/WP_SQLite_Driver_Tests.php | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index 915248df..d4333bbd 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -6581,4 +6581,116 @@ public function testRollbackNonExistentTransactionSavepoint(): void { $this->expectExceptionMessage( 'no such savepoint: sp1' ); $this->assertQuery( 'ROLLBACK TO SAVEPOINT sp1' ); } + + public function testForeignKeyOnUpdateNoAction(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT, FOREIGN KEY (id) REFERENCES t1 (id) ON UPDATE NO ACTION)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->expectException( 'WP_SQLite_Driver_Exception' ); + $this->expectExceptionMessage( 'SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed' ); + $this->assertQuery( 'UPDATE t1 SET id = 2 WHERE id = 1' ); + } + + public function testForeignKeyOnUpdateRestrict(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT, FOREIGN KEY (id) REFERENCES t1 (id) ON UPDATE RESTRICT)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->expectException( 'WP_SQLite_Driver_Exception' ); + $this->expectExceptionMessage( 'SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed' ); + $this->assertQuery( 'UPDATE t1 SET id = 2 WHERE id = 1' ); + } + + public function testForeignKeyOnUpdateCascade(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT, FOREIGN KEY (id) REFERENCES t1 (id) ON UPDATE CASCADE)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->assertQuery( 'UPDATE t1 SET id = 2 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM t2' ); + $this->assertEquals( array( (object) array( 'id' => '2' ) ), $result ); + } + + public function testForeignKeyOnUpdateSetNull(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT, FOREIGN KEY (id) REFERENCES t1 (id) ON UPDATE SET NULL)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->assertQuery( 'UPDATE t1 SET id = 2 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM t2' ); + $this->assertEquals( array( (object) array( 'id' => null ) ), $result ); + } + + public function testForeignKeyOnUpdateSetDefault(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT DEFAULT 0, FOREIGN KEY (id) REFERENCES t1 (id) ON UPDATE SET DEFAULT)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (0)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->assertQuery( 'UPDATE t1 SET id = 2 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM t2' ); + $this->assertEquals( array( (object) array( 'id' => '0' ) ), $result ); + } + + public function testForeignKeyOnDeleteNoAction(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT, FOREIGN KEY (id) REFERENCES t1 (id) ON DELETE NO ACTION)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->expectException( 'WP_SQLite_Driver_Exception' ); + $this->expectExceptionMessage( 'SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed' ); + $this->assertQuery( 'DELETE FROM t1 WHERE id = 1' ); + } + + public function testForeignKeyOnDeleteRestrict(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT, FOREIGN KEY (id) REFERENCES t1 (id) ON DELETE RESTRICT)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->expectException( 'WP_SQLite_Driver_Exception' ); + $this->expectExceptionMessage( 'SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed' ); + $this->assertQuery( 'DELETE FROM t1 WHERE id = 1' ); + } + + public function testForeignKeyOnDeleteCascade(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT, FOREIGN KEY (id) REFERENCES t1 (id) ON DELETE CASCADE)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->assertQuery( 'DELETE FROM t1 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM t2' ); + $this->assertEquals( array(), $result ); + } + + public function testForeignKeyOnDeleteSetNull(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT, FOREIGN KEY (id) REFERENCES t1 (id) ON DELETE SET NULL)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->assertQuery( 'DELETE FROM t1 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM t2' ); + $this->assertEquals( array( (object) array( 'id' => null ) ), $result ); + } + + public function testForeignKeyOnDeleteSetDefault(): void { + $this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE t2 (id INT DEFAULT 0, FOREIGN KEY (id) REFERENCES t1 (id) ON DELETE SET DEFAULT)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (0)' ); + $this->assertQuery( 'INSERT INTO t1 (id) VALUES (1)' ); + $this->assertQuery( 'INSERT INTO t2 (id) VALUES (1)' ); + + $this->assertQuery( 'DELETE FROM t1 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM t2' ); + $this->assertEquals( array( (object) array( 'id' => '0' ) ), $result ); + } } From a3e908ca000f210deae1057d5f6a7a152db8dc15 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Thu, 11 Sep 2025 16:35:43 +0200 Subject: [PATCH 09/10] Make MySQL's "NO ACTION" in ON DELETE/UPDATE be "RESTRICT" in SQLite --- .../sqlite-ast/class-wp-sqlite-driver.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index be09b30e..f3c6c7ed 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -4281,12 +4281,23 @@ function ( $column ) { $this->quote_sqlite_identifier( $referential_constraint['REFERENCED_TABLE_NAME'] ), implode( ', ', $referenced_column_names ) ); - if ( 'NO ACTION' !== $referential_constraint['DELETE_RULE'] ) { - $query .= sprintf( ' ON DELETE %s', $referential_constraint['DELETE_RULE'] ); + + // ON DELETE + $delete_rule = $referential_constraint['DELETE_RULE']; + if ( 'NO ACTION' === $delete_rule ) { + // In MySQL, NO ACTION is equivalent to RESTRICT with InnoDB. + $delete_rule = 'RESTRICT'; } - if ( 'NO ACTION' !== $referential_constraint['UPDATE_RULE'] ) { - $query .= sprintf( ' ON UPDATE %s', $referential_constraint['UPDATE_RULE'] ); + $query .= sprintf( ' ON DELETE %s', $delete_rule ); + + // ON UPDATE + $update_rule = $referential_constraint['UPDATE_RULE']; + if ( 'NO ACTION' === $update_rule ) { + // In MySQL, NO ACTION is equivalent to RESTRICT with InnoDB. + $update_rule = 'RESTRICT'; } + $query .= sprintf( ' ON UPDATE %s', $update_rule ); + $rows[] = $query; } From 14be86c34a6a48a599f955401c98359094ba6aa1 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Thu, 11 Sep 2025 16:36:54 +0200 Subject: [PATCH 10/10] Fix comments --- .../sqlite-ast/class-wp-sqlite-driver.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index f3c6c7ed..93f2549c 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -4130,7 +4130,7 @@ private function get_sqlite_create_table_statement( } } - // 4. Generate CREATE TABLE statement columns. + // 5. Generate CREATE TABLE statement columns. $rows = array(); $on_update_queries = array(); $has_autoincrement = false; @@ -4207,7 +4207,7 @@ private function get_sqlite_create_table_statement( } } - // 5. Generate CREATE TABLE statement constraints, collect indexes. + // 6. Generate CREATE TABLE statement constraints, collect indexes. $create_index_queries = array(); foreach ( $grouped_constraints as $constraint ) { ksort( $constraint ); @@ -4266,7 +4266,7 @@ function ( $column ) { } } - // Add foreign key constraints. + // 7. Add foreign key constraints. foreach ( $referential_constraints_info as $referential_constraint ) { $column_names = array(); $referenced_column_names = array(); @@ -4301,7 +4301,7 @@ function ( $column ) { $rows[] = $query; } - // 6. Compose the CREATE TABLE statement. + // 8. Compose the CREATE TABLE statement. $create_table_query = sprintf( "CREATE %sTABLE %s (\n", $table_is_temporary ? 'TEMPORARY ' : '', @@ -4383,7 +4383,7 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str $grouped_constraints[ $name ][ $seq ] = $constraint; } - // Get foreign key info. + // 4. Get foreign key info. $referential_constraints_table = $this->information_schema_builder ->get_table_name( $table_is_temporary, 'referential_constraints' ); $referential_constraints_info = $this->execute_sqlite_query( @@ -4419,7 +4419,7 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str } } - // 4. Generate CREATE TABLE statement columns. + // 5. Generate CREATE TABLE statement columns. $rows = array(); foreach ( $column_info as $column ) { $sql = ' '; @@ -4463,7 +4463,7 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str $rows[] = $sql; } - // 4. Generate CREATE TABLE statement constraints, collect indexes. + // 6. Generate CREATE TABLE statement constraints, collect indexes. foreach ( $grouped_constraints as $constraint ) { ksort( $constraint ); $info = $constraint[1]; @@ -4520,7 +4520,7 @@ function ( $column ) { $rows[] = $sql; } - // Add foreign key constraints. + // 7. Add foreign key constraints. foreach ( $referential_constraints_info as $referential_constraint ) { $column_names = array(); $referenced_column_names = array(); @@ -4544,7 +4544,7 @@ function ( $column ) { $rows[] = $sql; } - // 5. Compose the CREATE TABLE statement. + // 8. Compose the CREATE TABLE statement. $collation = $table_info['TABLE_COLLATION']; $charset = substr( $collation, 0, strpos( $collation, '_' ) );