Skip to content

Commit 6a3fb46

Browse files
committed
Implement ALTER TABLE ... DROP CONSTRAINT for foreign keys
1 parent 5d79372 commit 6a3fb46

4 files changed

Lines changed: 176 additions & 4 deletions

File tree

tests/WP_SQLite_Driver_Metadata_Tests.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,4 +1715,71 @@ public function testInformationSchemaAlterTableDropForeignKeys(): void {
17151715
$result
17161716
);
17171717
}
1718+
1719+
public function testInformationSchemaAlterTableDropConstraint(): void {
1720+
$this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(255))' );
1721+
$this->assertQuery(
1722+
'CREATE TABLE t2 (
1723+
id INT,
1724+
CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES t1 (id)
1725+
)'
1726+
);
1727+
1728+
$this->assertQuery( 'ALTER TABLE t2 DROP CONSTRAINT fk1' );
1729+
1730+
// INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
1731+
$result = $this->assertQuery( "SELECT * FROM information_schema.table_constraints WHERE table_name = 't2'" );
1732+
$this->assertCount( 0, $result );
1733+
1734+
// INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
1735+
$result = $this->assertQuery( "SELECT * FROM information_schema.referential_constraints WHERE table_name = 't2'" );
1736+
$this->assertCount( 0, $result );
1737+
1738+
// INFORMATION_SCHEMA.KEY_COLUMN_USAGE
1739+
$result = $this->assertQuery( "SELECT * FROM information_schema.key_column_usage WHERE table_name = 't2'" );
1740+
$this->assertCount( 0, $result );
1741+
1742+
// SHOW CREATE TABLE
1743+
$result = $this->assertQuery( 'SHOW CREATE TABLE t2' );
1744+
$this->assertEquals(
1745+
array(
1746+
(object) array(
1747+
'Create Table' => implode(
1748+
"\n",
1749+
array(
1750+
'CREATE TABLE `t2` (',
1751+
' `id` int DEFAULT NULL',
1752+
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
1753+
)
1754+
),
1755+
),
1756+
),
1757+
$result
1758+
);
1759+
}
1760+
1761+
public function testInformationSchemaAlterTableDropMissingConstraint(): void {
1762+
$this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY)' );
1763+
1764+
$this->expectException( WP_SQLite_Driver_Exception::class );
1765+
$this->expectExceptionMessage( "SQLSTATE[HY000]: General error: 3940 Constraint 'cnst' does not exist." );
1766+
$this->expectExceptionCode( 'HY000' );
1767+
$this->assertQuery( 'ALTER TABLE t2 DROP CONSTRAINT cnst' );
1768+
}
1769+
1770+
public function testInformationSchemaAlterTableDropConstraintWithAmbiguousName(): void {
1771+
$this->assertQuery( 'CREATE TABLE t1 (id INT PRIMARY KEY, name VARCHAR(255))' );
1772+
$this->assertQuery(
1773+
'CREATE TABLE t2 (
1774+
id INT,
1775+
CONSTRAINT cnst UNIQUE (id),
1776+
CONSTRAINT cnst FOREIGN KEY (id) REFERENCES t1 (id)
1777+
)'
1778+
);
1779+
1780+
$this->expectException( WP_SQLite_Driver_Exception::class );
1781+
$this->expectExceptionMessage( "SQLSTATE[HY000]: General error: 3939 Table has multiple constraints with the name 'cnst'. Please use constraint specific 'DROP' clause." );
1782+
$this->expectExceptionCode( 'HY000' );
1783+
$this->assertQuery( 'ALTER TABLE t2 DROP CONSTRAINT cnst' );
1784+
}
17181785
}

wp-includes/sqlite-ast/class-wp-sqlite-driver.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4827,6 +4827,22 @@ private function convert_information_schema_exception( WP_SQLite_Information_Sch
48274827
),
48284828
'42000'
48294829
);
4830+
case WP_SQLite_Information_Schema_Exception::TYPE_CONSTRAINT_DOES_NOT_EXIST:
4831+
return $this->new_driver_exception(
4832+
sprintf(
4833+
"SQLSTATE[HY000]: General error: 3940 Constraint '%s' does not exist.",
4834+
$e->get_data()['name']
4835+
),
4836+
'HY000'
4837+
);
4838+
case WP_SQLite_Information_Schema_Exception::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME:
4839+
return $this->new_driver_exception(
4840+
sprintf(
4841+
"SQLSTATE[HY000]: General error: 3939 Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.",
4842+
$e->get_data()['name']
4843+
),
4844+
'HY000'
4845+
);
48304846
default:
48314847
return $e;
48324848
}

wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,13 @@ public function record_alter_table( WP_Parser_Node $node ): void {
640640

641641
// DROP
642642
if ( WP_MySQL_Lexer::DROP_SYMBOL === $first_token->id ) {
643+
// DROP CONSTRAINT
644+
if ( $action->has_child_token( WP_MySQL_Lexer::CONSTRAINT_SYMBOL ) ) {
645+
$name = $this->get_value( $action->get_first_child_node( 'identifier' ) );
646+
$this->record_drop_constraint( $table_is_temporary, $table_name, $name );
647+
continue;
648+
}
649+
643650
// DROP FOREIGN KEY
644651
if ( $action->has_child_token( WP_MySQL_Lexer::FOREIGN_SYMBOL ) ) {
645652
$field_identifier = $action->get_first_child_node( 'fieldIdentifier' );
@@ -1218,6 +1225,52 @@ private function record_add_constraint(
12181225
}
12191226
}
12201227

1228+
/**
1229+
* Analyze DROP CONSTRAINT statement and record data in the information schema.
1230+
*
1231+
* @param bool $table_is_temporary Whether the table is temporary.
1232+
* @param string $table_name The table name.
1233+
* @param string $name The constraint name.
1234+
*/
1235+
private function record_drop_constraint(
1236+
bool $table_is_temporary,
1237+
string $table_name,
1238+
string $name
1239+
): void {
1240+
$constraint_types = $this->connection->query(
1241+
sprintf(
1242+
'SELECT constraint_type FROM %s WHERE table_schema = ? AND table_name = ? AND constraint_name = ?',
1243+
$this->connection->quote_identifier( $this->get_table_name( $table_is_temporary, 'table_constraints' ) )
1244+
),
1245+
array(
1246+
$this->db_name,
1247+
$table_name,
1248+
$name,
1249+
)
1250+
)->fetchAll(
1251+
PDO::FETCH_COLUMN // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
1252+
);
1253+
1254+
if ( 0 === count( $constraint_types ) ) {
1255+
throw WP_SQLite_Information_Schema_Exception::constraint_does_not_exist( $name );
1256+
}
1257+
1258+
// MySQL doesn't allow a generic DELETE CONSTRAINT clause when the target
1259+
// is ambiguous, i.e., when multiple constraints with the same name exist.
1260+
if ( count( $constraint_types ) > 1 ) {
1261+
throw WP_SQLite_Information_Schema_Exception::multiple_constraints_with_name( $name );
1262+
}
1263+
1264+
$constraint_type = $constraint_types[0];
1265+
if ( 'FOREIGN KEY' === $constraint_type ) {
1266+
$this->record_drop_foreign_key( $table_is_temporary, $table_name, $name );
1267+
} else {
1268+
throw new \Exception(
1269+
"DROP CONSTRAINT for constraint type '$constraint_type' is not supported."
1270+
);
1271+
}
1272+
}
1273+
12211274
/**
12221275
* Analyze DROP FOREIGN KEY statement and record data in the information schema.
12231276
*

wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
*/
99
class WP_SQLite_Information_Schema_Exception extends Exception {
1010
// Information schema exception types.
11-
const TYPE_DUPLICATE_TABLE_NAME = 'duplicate-table-name';
12-
const TYPE_DUPLICATE_COLUMN_NAME = 'duplicate-column-name';
13-
const TYPE_DUPLICATE_KEY_NAME = 'duplicate-key-name';
14-
const TYPE_KEY_COLUMN_NOT_FOUND = 'key-column-not-found';
11+
const TYPE_DUPLICATE_TABLE_NAME = 'duplicate-table-name';
12+
const TYPE_DUPLICATE_COLUMN_NAME = 'duplicate-column-name';
13+
const TYPE_DUPLICATE_KEY_NAME = 'duplicate-key-name';
14+
const TYPE_KEY_COLUMN_NOT_FOUND = 'key-column-not-found';
15+
const TYPE_CONSTRAINT_DOES_NOT_EXIST = 'constraint-does-not-exist';
16+
const TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME = 'multiple-constraints-with-name';
1517

1618
/**
1719
* The exception type.
@@ -106,11 +108,45 @@ public static function duplicate_key_name( string $key_name ): WP_SQLite_Informa
106108
);
107109
}
108110

111+
/**
112+
* Create a key column not found exception.
113+
*
114+
* @param string $column_name The name of the affected column.
115+
* @return self The exception instance.
116+
*/
109117
public static function key_column_not_found( string $column_name ): WP_SQLite_Information_Schema_Exception {
110118
return new self(
111119
self::TYPE_KEY_COLUMN_NOT_FOUND,
112120
sprintf( "Key column '%s' doesn't exist in table.", $column_name ),
113121
array( 'column_name' => $column_name )
114122
);
115123
}
124+
125+
/**
126+
* Create a constraint does not exist exception.
127+
*
128+
* @param string $name The name of the affected constraint.
129+
* @return self The exception instance.
130+
*/
131+
public static function constraint_does_not_exist( string $name ): WP_SQLite_Information_Schema_Exception {
132+
return new self(
133+
self::TYPE_CONSTRAINT_DOES_NOT_EXIST,
134+
sprintf( "Constraint '%s' does not exist.", $name ),
135+
array( 'name' => $name )
136+
);
137+
}
138+
139+
/**
140+
* Create a multiple constraints with name exception.
141+
*
142+
* @param string $name The name of the affected constraint.
143+
* @return self The exception instance.
144+
*/
145+
public static function multiple_constraints_with_name( string $name ): WP_SQLite_Information_Schema_Exception {
146+
return new self(
147+
self::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME,
148+
sprintf( "Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", $name ),
149+
array( 'name' => $name )
150+
);
151+
}
116152
}

0 commit comments

Comments
 (0)