diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index 7b07a7a6..f8db1623 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -893,4 +893,989 @@ 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 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( + '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 + ); + } + + 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 + ); + } + + 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 + ); + } + + 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 t DROP PRIMARY KEY' ); + + // INFORMATION_SCHEMA.TABLE_CONSTRAINTS + $result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't'" ); + $this->assertCount( 0, $result ); + + // 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 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 t' ); + $this->assertEquals( + array( + (object) array( + 'Create Table' => implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `id` int DEFAULT NULL,', + ' `name` text DEFAULT NULL', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + ), + ), + $result + ); + } + + 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)' ); + + $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/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 ); + } } diff --git a/tests/WP_SQLite_Driver_Translation_Tests.php b/tests/WP_SQLite_Driver_Translation_Tests.php index 53725179..0c23e64d 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", ) ); } @@ -256,9 +258,12 @@ 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", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -279,6 +284,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 +305,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", ) ); } @@ -328,9 +335,12 @@ 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", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -352,9 +362,12 @@ 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", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't1' ORDER BY constraint_name", ) ); @@ -374,9 +387,12 @@ 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", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't2' ORDER BY constraint_name", ) ); @@ -398,9 +414,12 @@ 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", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't3' ORDER BY constraint_name", ) ); } @@ -425,15 +444,20 @@ 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", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -463,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`)' @@ -470,9 +496,12 @@ 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", + "SELECT * FROM `_wp_sqlite_mysql_information_schema_referential_constraints` WHERE constraint_schema = 'wp' AND table_name = 't' ORDER BY constraint_name", ) ); } @@ -541,6 +570,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 +600,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 +630,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 +660,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 +696,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 +728,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 +764,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 +799,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 +834,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 +856,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 +878,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 +908,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 +934,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 +960,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 +982,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 +1006,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 +1032,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 +1056,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 +1080,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 +1106,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 +1126,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 +1154,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 +1176,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 +1202,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 +1228,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 +1252,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 +1274,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 +1294,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..93f2549c 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -4094,7 +4094,43 @@ private function get_sqlite_create_table_statement( $grouped_constraints[ $name ][ $seq ] = $constraint; } - // 4. Generate CREATE TABLE statement columns. + // 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 = ? AND referenced_column_name IS NOT NULL', + $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'], + ); + } + } + + // 5. Generate CREATE TABLE statement columns. $rows = array(); $on_update_queries = array(); $has_autoincrement = false; @@ -4171,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 ); @@ -4230,7 +4266,42 @@ function ( $column ) { } } - // 6. Compose the CREATE TABLE statement. + // 7. 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 ) + ); + + // 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'; + } + $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; + } + + // 8. Compose the CREATE TABLE statement. $create_table_query = sprintf( "CREATE %sTABLE %s (\n", $table_is_temporary ? 'TEMPORARY ' : '', @@ -4312,7 +4383,43 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str $grouped_constraints[ $name ][ $seq ] = $constraint; } - // 4. Generate CREATE TABLE statement columns. + // 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 = ? AND referenced_column_name IS NOT NULL', + $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'], + ); + } + } + + // 5. Generate CREATE TABLE statement columns. $rows = array(); foreach ( $column_info as $column ) { $sql = ' '; @@ -4356,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]; @@ -4413,7 +4520,31 @@ function ( $column ) { $rows[] = $sql; } - // 5. Compose the CREATE TABLE statement. + // 7. 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; + } + + // 8. Compose the CREATE TABLE statement. $collation = $table_info['TABLE_COLLATION']; $charset = substr( $collation, 0, strpos( $collation, '_' ) ); @@ -4707,6 +4838,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 8baf9071..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 @@ -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) + ", ); /** @@ -479,34 +510,53 @@ 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, + $index_data['index_name'] ?? null + ); + // 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; } @@ -591,6 +641,28 @@ 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 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' ); + $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 ) { @@ -1100,12 +1172,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 +1204,172 @@ 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, $index_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 + ); + } + } + + /** + * 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 ( '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( + "DROP CONSTRAINT for constraint type '$constraint_type' is not supported." + ); + } + } + + /** + * 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. + * + * @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, + + // Remove only FOREIGN KEY records; not PRIMARY/UNIQUE KEY data. + 'REFERENCED_TABLE_SCHEMA' => $this->db_name, + ) + ); } /** @@ -1369,7 +1591,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 +1604,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 +1614,160 @@ 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|null The referential constraint data as stored in information schema. + */ + 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. + $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. + * @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, + ?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 && ! $is_primary && ! $is_unique ) { + return array(); + } + + // 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(); + } + + // 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_parts = array(); + foreach ( $node->get_descendant_nodes( 'keyPart' ) as $key_part ) { + $key_parts[] = $key_part->get_first_child_node( '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, + 'constraint_name' => $name, + 'table_schema' => $this->db_name, + 'table_name' => $table_name, + 'column_name' => $column_name, + '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' => $referenced_columns ? $this->get_value( $referenced_columns[ $i ] ) : null, + ); + } + return $rows; + } + /** * Update column info from constraint data in the statistics table. * @@ -2034,16 +2410,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' ) ); + } + + // 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/FOREIGN KEY/UNIQUE constraints. + // TODO: Handle CHECK constraints. return null; } @@ -2060,10 +2474,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 +2696,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. * @@ -2403,7 +2867,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( 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 ) + ); + } }