Skip to content

Commit 68992c0

Browse files
committed
Add support for CHECK constraints
1 parent 842d581 commit 68992c0

File tree

3 files changed

+326
-20
lines changed

3 files changed

+326
-20
lines changed

tests/WP_SQLite_Driver_Tests.php

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8976,4 +8976,176 @@ public function testColumnInfoWithZeroRowsPhpBug(): void {
89768976
$column_info[0]
89778977
);
89788978
}
8979+
8980+
public function testCheckConstraints(): void {
8981+
$this->assertQuery(
8982+
"CREATE TABLE t (
8983+
id INT NOT NULL CHECK (id > 0),
8984+
name VARCHAR(255) NOT NULL CHECK (name != ''),
8985+
score DOUBLE NOT NULL CHECK (score > 0 AND score < 100),
8986+
data JSON CHECK (json_valid(data)),
8987+
start_timestamp TIMESTAMP NOT NULL,
8988+
end_timestamp TIMESTAMP NOT NULL,
8989+
CONSTRAINT c1 CHECK (id < 10),
8990+
CONSTRAINT c2 CHECK (start_timestamp < end_timestamp),
8991+
CONSTRAINT c3 CHECK (length(data) < 20)
8992+
)"
8993+
);
8994+
8995+
// Valid data.
8996+
$this->assertQuery(
8997+
"INSERT INTO t (id, name, score, start_timestamp, end_timestamp, data)
8998+
VALUES (1, 'test', 50, '2025-01-01 12:00:00', '2025-01-02 12:00:00', '{\"key\":\"value\"}')
8999+
"
9000+
);
9001+
9002+
// Invalid ID.
9003+
$exception = null;
9004+
try {
9005+
$this->assertQuery(
9006+
"INSERT INTO t (id, name, score, start_timestamp, end_timestamp, data)
9007+
VALUES (0, 'test', 50, '2025-01-01 12:00:00', '2025-01-02 12:00:00', '{\"key\":\"value\"}')
9008+
"
9009+
);
9010+
} catch ( WP_SQLite_Driver_Exception $e ) {
9011+
$exception = $e;
9012+
}
9013+
$this->assertNotNull( $exception );
9014+
$this->assertSame(
9015+
'SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: t_chk_1',
9016+
$exception->getMessage()
9017+
);
9018+
9019+
// Invalid name.
9020+
$exception = null;
9021+
try {
9022+
$this->assertQuery(
9023+
"INSERT INTO t (id, name, score, start_timestamp, end_timestamp, data)
9024+
VALUES (1, '', 50, '2025-01-01 12:00:00', '2025-01-02 12:00:00', '{\"key\":\"value\"}')
9025+
"
9026+
);
9027+
} catch ( WP_SQLite_Driver_Exception $e ) {
9028+
$exception = $e;
9029+
}
9030+
$this->assertNotNull( $exception );
9031+
$this->assertSame(
9032+
'SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: t_chk_2',
9033+
$exception->getMessage()
9034+
);
9035+
9036+
// Invalid score.
9037+
$exception = null;
9038+
try {
9039+
$this->assertQuery(
9040+
"INSERT INTO t (id, name, score, start_timestamp, end_timestamp, data)
9041+
VALUES (1, 'test', 100, '2025-01-01 12:00:00', '2025-01-02 12:00:00', '{\"key\":\"value\"}')
9042+
"
9043+
);
9044+
} catch ( WP_SQLite_Driver_Exception $e ) {
9045+
$exception = $e;
9046+
}
9047+
$this->assertNotNull( $exception );
9048+
$this->assertSame(
9049+
'SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: t_chk_3',
9050+
$exception->getMessage()
9051+
);
9052+
9053+
// Invalid data.
9054+
$exception = null;
9055+
try {
9056+
$this->assertQuery(
9057+
"INSERT INTO t (id, name, score, start_timestamp, end_timestamp, data)
9058+
VALUES (1, 'test', 50, '2025-01-01 12:00:00', '2025-01-02 12:00:00', 'invalid JSON')
9059+
"
9060+
);
9061+
} catch ( WP_SQLite_Driver_Exception $e ) {
9062+
$exception = $e;
9063+
}
9064+
$this->assertNotNull( $exception );
9065+
$this->assertSame(
9066+
'SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: t_chk_4',
9067+
$exception->getMessage()
9068+
);
9069+
9070+
// Invalid c1.
9071+
$exception = null;
9072+
try {
9073+
$this->assertQuery(
9074+
"INSERT INTO t (id, name, score, start_timestamp, end_timestamp, data)
9075+
VALUES (11, 'test', 50, '2025-01-01 12:00:00', '2025-01-02 12:00:00', '{\"key\":\"value\"}')
9076+
"
9077+
);
9078+
} catch ( WP_SQLite_Driver_Exception $e ) {
9079+
$exception = $e;
9080+
}
9081+
$this->assertNotNull( $exception );
9082+
$this->assertSame(
9083+
'SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: c1',
9084+
$exception->getMessage()
9085+
);
9086+
9087+
// Invalid c2.
9088+
$exception = null;
9089+
try {
9090+
$this->assertQuery(
9091+
"INSERT INTO t (id, name, score, start_timestamp, end_timestamp, data)
9092+
VALUES (1, 'test', 50, '2025-01-02 12:00:00', '2025-01-01 12:00:00', '{\"key\":\"value\"}')
9093+
"
9094+
);
9095+
} catch ( WP_SQLite_Driver_Exception $e ) {
9096+
$exception = $e;
9097+
}
9098+
$this->assertNotNull( $exception );
9099+
$this->assertSame(
9100+
'SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: c2',
9101+
$exception->getMessage()
9102+
);
9103+
9104+
// Invalid c3.
9105+
$exception = null;
9106+
try {
9107+
$this->assertQuery(
9108+
"INSERT INTO t (id, name, score, start_timestamp, end_timestamp, data)
9109+
VALUES (1, 'test', 50, '2025-01-01 12:00:00', '2025-01-02 12:00:00', '{\"key\":\"a-very-long-value\"}')
9110+
"
9111+
);
9112+
} catch ( WP_SQLite_Driver_Exception $e ) {
9113+
$exception = $e;
9114+
}
9115+
$this->assertNotNull( $exception );
9116+
$this->assertSame(
9117+
'SQLSTATE[23000]: Integrity constraint violation: 19 CHECK constraint failed: c3',
9118+
$exception->getMessage()
9119+
);
9120+
9121+
// SHOW CREATE TABLE
9122+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
9123+
$this->assertCount( 1, $result );
9124+
$this->assertEquals(
9125+
implode(
9126+
"\n",
9127+
array(
9128+
'CREATE TABLE `t` (',
9129+
' `id` int NOT NULL,',
9130+
' `name` varchar(255) NOT NULL,',
9131+
' `score` double NOT NULL,',
9132+
' `data` json DEFAULT NULL,',
9133+
' `start_timestamp` timestamp NOT NULL,',
9134+
' `end_timestamp` timestamp NOT NULL,',
9135+
9136+
// The of the check expressions below is not 100% matching MySQL,
9137+
// because in MySQL the expressions are parsed and normalized.
9138+
' CONSTRAINT `c1` CHECK ( id < 10 ),',
9139+
' CONSTRAINT `c2` CHECK ( start_timestamp < end_timestamp ),',
9140+
' CONSTRAINT `c3` CHECK ( length ( data ) < 20 ),',
9141+
' CONSTRAINT `t_chk_1` CHECK ( id > 0 ),',
9142+
" CONSTRAINT `t_chk_2` CHECK ( name != '' ),",
9143+
' CONSTRAINT `t_chk_3` CHECK ( score > 0 AND score < 100 ),',
9144+
' CONSTRAINT `t_chk_4` CHECK ( json_valid ( data ) )',
9145+
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
9146+
)
9147+
),
9148+
$result[0]->{'Create Table'}
9149+
);
9150+
}
89799151
}

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

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4739,7 +4739,26 @@ private function get_sqlite_create_table_statement(
47394739
}
47404740
}
47414741

4742-
// 5. Generate CREATE TABLE statement columns.
4742+
// 5. Get CHECK constraint info.
4743+
$table_constraints_table = $this->information_schema_builder
4744+
->get_table_name( $table_is_temporary, 'table_constraints' );
4745+
$check_constraints_table = $this->information_schema_builder
4746+
->get_table_name( $table_is_temporary, 'check_constraints' );
4747+
$check_constraints_info = $this->execute_sqlite_query(
4748+
sprintf(
4749+
'SELECT tc.*, cc.check_clause
4750+
FROM %s tc
4751+
JOIN %s cc ON cc.constraint_name = tc.constraint_name
4752+
WHERE tc.constraint_schema = ?
4753+
AND tc.table_name = ?
4754+
ORDER BY tc.constraint_name',
4755+
$this->quote_sqlite_identifier( $table_constraints_table ),
4756+
$this->quote_sqlite_identifier( $check_constraints_table )
4757+
),
4758+
array( $this->db_name, $table_name )
4759+
)->fetchAll( PDO::FETCH_ASSOC );
4760+
4761+
// 6. Generate CREATE TABLE statement columns.
47434762
$rows = array();
47444763
$on_update_queries = array();
47454764
$has_autoincrement = false;
@@ -4875,7 +4894,7 @@ function ( $column ) {
48754894
}
48764895
}
48774896

4878-
// 7. Add foreign key constraints.
4897+
// 8. Add foreign key constraints.
48794898
foreach ( $referential_constraints_info as $referential_constraint ) {
48804899
$column_names = array();
48814900
$referenced_column_names = array();
@@ -4910,7 +4929,26 @@ function ( $column ) {
49104929
$rows[] = $query;
49114930
}
49124931

4913-
// 8. Compose the CREATE TABLE statement.
4932+
// 9. Add CHECK constraints.
4933+
foreach ( $check_constraints_info as $check_constraint ) {
4934+
if ( 'NO' === $check_constraint['ENFORCED'] ) {
4935+
continue;
4936+
}
4937+
4938+
// Translate the check clause from MySQL to SQLite.
4939+
$ast = $this->create_parser( 'SELECT ' . $check_constraint['CHECK_CLAUSE'] )->parse();
4940+
$expr = $ast->get_first_descendant_node( 'selectItem' )->get_first_child_node();
4941+
$check_clause = $this->translate( $expr );
4942+
4943+
$sql = sprintf(
4944+
' CONSTRAINT %s CHECK %s',
4945+
$this->quote_sqlite_identifier( $check_constraint['CONSTRAINT_NAME'] ),
4946+
$check_clause
4947+
);
4948+
$rows[] = $sql;
4949+
}
4950+
4951+
// 10. Compose the CREATE TABLE statement.
49144952
$create_table_query = sprintf(
49154953
"CREATE %sTABLE %s (\n",
49164954
$table_is_temporary ? 'TEMPORARY ' : '',
@@ -5028,7 +5066,26 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str
50285066
}
50295067
}
50305068

5031-
// 5. Generate CREATE TABLE statement columns.
5069+
// 5. Get CHECK constraint info.
5070+
$table_constraints_table = $this->information_schema_builder
5071+
->get_table_name( $table_is_temporary, 'table_constraints' );
5072+
$check_constraints_table = $this->information_schema_builder
5073+
->get_table_name( $table_is_temporary, 'check_constraints' );
5074+
$check_constraints_info = $this->execute_sqlite_query(
5075+
sprintf(
5076+
'SELECT tc.*, cc.check_clause
5077+
FROM %s tc
5078+
JOIN %s cc ON cc.constraint_name = tc.constraint_name
5079+
WHERE tc.constraint_schema = ?
5080+
AND tc.table_name = ?
5081+
ORDER BY tc.constraint_name',
5082+
$this->quote_sqlite_identifier( $table_constraints_table ),
5083+
$this->quote_sqlite_identifier( $check_constraints_table )
5084+
),
5085+
array( $this->db_name, $table_name )
5086+
)->fetchAll( PDO::FETCH_ASSOC );
5087+
5088+
// 6. Generate CREATE TABLE statement columns.
50325089
$rows = array();
50335090
foreach ( $column_info as $column ) {
50345091
$sql = ' ';
@@ -5072,7 +5129,7 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str
50725129
$rows[] = $sql;
50735130
}
50745131

5075-
// 6. Generate CREATE TABLE statement constraints, collect indexes.
5132+
// 7. Generate CREATE TABLE statement constraints, collect indexes.
50765133
foreach ( $grouped_constraints as $constraint ) {
50775134
ksort( $constraint );
50785135
$info = $constraint[1];
@@ -5129,7 +5186,7 @@ function ( $column ) {
51295186
$rows[] = $sql;
51305187
}
51315188

5132-
// 7. Add foreign key constraints.
5189+
// 8. Add foreign key constraints.
51335190
foreach ( $referential_constraints_info as $referential_constraint ) {
51345191
$column_names = array();
51355192
$referenced_column_names = array();
@@ -5153,7 +5210,18 @@ function ( $column ) {
51535210
$rows[] = $sql;
51545211
}
51555212

5156-
// 8. Compose the CREATE TABLE statement.
5213+
// 9. Add CHECK constraints.
5214+
foreach ( $check_constraints_info as $check_constraint ) {
5215+
$sql = sprintf(
5216+
' CONSTRAINT %s CHECK %s%s',
5217+
$this->quote_mysql_identifier( $check_constraint['CONSTRAINT_NAME'] ),
5218+
$check_constraint['CHECK_CLAUSE'],
5219+
'NO' === $check_constraint['ENFORCED'] ? ' /*!80016 NOT ENFORCED */' : ''
5220+
);
5221+
$rows[] = $sql;
5222+
}
5223+
5224+
// 10. Compose the CREATE TABLE statement.
51575225
$collation = $table_info['TABLE_COLLATION'];
51585226
$charset = substr( $collation, 0, strpos( $collation, '_' ) );
51595227

0 commit comments

Comments
 (0)