Skip to content

Commit 740ef5d

Browse files
committed
Apply INSERT type casting also in STRICT SQL mode
1 parent a8d1e57 commit 740ef5d

File tree

3 files changed

+220
-41
lines changed

3 files changed

+220
-41
lines changed

tests/WP_SQLite_Driver_Tests.php

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function setUp(): void {
2828
"CREATE TABLE _dates (
2929
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
3030
option_name TEXT NOT NULL default '',
31-
option_value DATE NOT NULL
31+
option_value DATETIME NOT NULL
3232
);"
3333
);
3434
}
@@ -2342,6 +2342,8 @@ public function testOnDuplicateUpdate() {
23422342
}
23432343

23442344
public function testTruncatesInvalidDates() {
2345+
$this->assertQuery( "SET sql_mode = ''" );
2346+
23452347
$this->assertQuery( "INSERT INTO _dates (option_value) VALUES ('2022-01-01 14:24:12');" );
23462348
$this->assertQuery( "INSERT INTO _dates (option_value) VALUES ('2022-31-01 14:24:12');" );
23472349

@@ -10053,4 +10055,152 @@ public function testEmptyColumnMeta(): void {
1005310055
$this->assertSame( 0, $this->engine->get_last_column_count() );
1005410056
$this->assertSame( array(), $this->engine->get_last_column_meta() );
1005510057
}
10058+
10059+
public function testCastValuesOnInsert(): void {
10060+
// INTEGER
10061+
$this->assertQuery( 'CREATE TABLE t (value INT)' );
10062+
$this->assertQuery( 'INSERT INTO t VALUES (NULL)' );
10063+
$this->assertQuery( 'INSERT INTO t VALUES (FALSE)' );
10064+
$this->assertQuery( 'INSERT INTO t VALUES (TRUE)' );
10065+
$this->assertQuery( 'INSERT INTO t VALUES (2)' );
10066+
$this->assertQuery( "INSERT INTO t VALUES ('3')" );
10067+
10068+
$result = $this->assertQuery( 'SELECT * FROM t' );
10069+
$this->assertSame( null, $result[0]->value );
10070+
$this->assertSame( '0', $result[1]->value );
10071+
$this->assertSame( '1', $result[2]->value );
10072+
$this->assertSame( '2', $result[3]->value );
10073+
$this->assertSame( '3', $result[4]->value );
10074+
$this->assertQuery( 'DROP TABLE t' );
10075+
10076+
// FLOAT
10077+
$this->assertQuery( 'CREATE TABLE t (value FLOAT)' );
10078+
$this->assertQuery( 'INSERT INTO t VALUES (NULL)' );
10079+
$this->assertQuery( 'INSERT INTO t VALUES (FALSE)' );
10080+
$this->assertQuery( 'INSERT INTO t VALUES (TRUE)' );
10081+
$this->assertQuery( 'INSERT INTO t VALUES (2.34)' );
10082+
$this->assertQuery( "INSERT INTO t VALUES ('3.45')" );
10083+
10084+
$result = $this->assertQuery( 'SELECT * FROM t' );
10085+
$this->assertSame( null, $result[0]->value );
10086+
$this->assertSame( '0', $result[1]->value );
10087+
$this->assertSame( '1', $result[2]->value );
10088+
$this->assertSame( '2.34', $result[3]->value );
10089+
$this->assertSame( '3.45', $result[4]->value );
10090+
$this->assertQuery( 'DROP TABLE t' );
10091+
10092+
// STRING
10093+
$this->assertQuery( 'CREATE TABLE t (value TEXT)' );
10094+
$this->assertQuery( 'INSERT INTO t VALUES (NULL)' );
10095+
$this->assertQuery( 'INSERT INTO t VALUES (FALSE)' );
10096+
$this->assertQuery( 'INSERT INTO t VALUES (TRUE)' );
10097+
$this->assertQuery( "INSERT INTO t VALUES ('a')" );
10098+
$this->assertQuery( 'INSERT INTO t VALUES (0x62)' );
10099+
$this->assertQuery( "INSERT INTO t VALUES (x'63')" );
10100+
$this->assertQuery( 'INSERT INTO t VALUES (123)' );
10101+
10102+
$result = $this->assertQuery( 'SELECT * FROM t' );
10103+
$this->assertSame( null, $result[0]->value );
10104+
$this->assertSame( '0', $result[1]->value );
10105+
$this->assertSame( '1', $result[2]->value );
10106+
$this->assertSame( 'a', $result[3]->value );
10107+
$this->assertSame( 'b', $result[4]->value );
10108+
$this->assertSame( 'c', $result[5]->value );
10109+
$this->assertSame( '123', $result[6]->value );
10110+
$this->assertQuery( 'DROP TABLE t' );
10111+
10112+
// BLOB
10113+
$this->assertQuery( 'CREATE TABLE t (value BLOB)' );
10114+
$this->assertQuery( 'INSERT INTO t VALUES (NULL)' );
10115+
$this->assertQuery( 'INSERT INTO t VALUES (FALSE)' );
10116+
$this->assertQuery( 'INSERT INTO t VALUES (TRUE)' );
10117+
$this->assertQuery( 'INSERT INTO t VALUES (0)' );
10118+
$this->assertQuery( 'INSERT INTO t VALUES (123)' );
10119+
$this->assertQuery( 'INSERT INTO t VALUES (123.456)' );
10120+
$this->assertQuery( "INSERT INTO t VALUES ('a')" );
10121+
$this->assertQuery( 'INSERT INTO t VALUES (0x62)' );
10122+
$this->assertQuery( "INSERT INTO t VALUES (x'63')" );
10123+
10124+
$result = $this->assertQuery( 'SELECT * FROM t' );
10125+
$this->assertSame( null, $result[0]->value );
10126+
$this->assertSame( '0', $result[1]->value );
10127+
$this->assertSame( '1', $result[2]->value );
10128+
$this->assertSame( '0', $result[3]->value );
10129+
$this->assertSame( '123', $result[4]->value );
10130+
$this->assertSame( '123.456', $result[5]->value );
10131+
$this->assertSame( 'a', $result[6]->value );
10132+
$this->assertSame( 'b', $result[7]->value );
10133+
$this->assertSame( 'c', $result[8]->value );
10134+
$this->assertQuery( 'DROP TABLE t' );
10135+
10136+
// DATE
10137+
$this->assertQuery( 'CREATE TABLE t (value DATE)' );
10138+
$this->assertQuery( 'INSERT INTO t VALUES (NULL)' );
10139+
$this->assertQuery( 'INSERT INTO t VALUES (FALSE)' );
10140+
$this->assertQuery( 'INSERT INTO t VALUES (TRUE)' );
10141+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23')" );
10142+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23 18:30:00')" );
10143+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23 18:30:00.123456')" );
10144+
10145+
$result = $this->assertQuery( 'SELECT * FROM t' );
10146+
$this->assertSame( null, $result[0]->value );
10147+
//$this->assertSame( '0', $result[1]->value );
10148+
//$this->assertSame( '1', $result[2]->value );
10149+
$this->assertSame( '2025-10-23', $result[3]->value );
10150+
$this->assertSame( '2025-10-23', $result[4]->value );
10151+
$this->assertSame( '2025-10-23', $result[5]->value );
10152+
$this->assertQuery( 'DROP TABLE t' );
10153+
10154+
// TIME
10155+
$this->assertQuery( 'CREATE TABLE t (value TIME)' );
10156+
$this->assertQuery( 'INSERT INTO t VALUES (NULL)' );
10157+
$this->assertQuery( 'INSERT INTO t VALUES (FALSE)' );
10158+
$this->assertQuery( 'INSERT INTO t VALUES (TRUE)' );
10159+
$this->assertQuery( "INSERT INTO t VALUES ('18:30:00')" );
10160+
$this->assertQuery( "INSERT INTO t VALUES ('18:30:00.123456')" );
10161+
10162+
$result = $this->assertQuery( 'SELECT * FROM t' );
10163+
$this->assertSame( null, $result[0]->value );
10164+
//$this->assertSame( '0', $result[1]->value );
10165+
//$this->assertSame( '1', $result[2]->value );
10166+
$this->assertSame( '18:30:00', $result[3]->value );
10167+
$this->assertSame( '18:30:00', $result[4]->value );
10168+
$this->assertQuery( 'DROP TABLE t' );
10169+
10170+
// DATETIME
10171+
$this->assertQuery( 'CREATE TABLE t (value DATETIME)' );
10172+
$this->assertQuery( 'INSERT INTO t VALUES (NULL)' );
10173+
$this->assertQuery( 'INSERT INTO t VALUES (FALSE)' );
10174+
$this->assertQuery( 'INSERT INTO t VALUES (TRUE)' );
10175+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23')" );
10176+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23 18:30:00')" );
10177+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23 18:30:00.123456')" );
10178+
10179+
$result = $this->assertQuery( 'SELECT * FROM t' );
10180+
$this->assertSame( null, $result[0]->value );
10181+
//$this->assertSame( '0', $result[1]->value );
10182+
//$this->assertSame( '1', $result[2]->value );
10183+
$this->assertSame( '2025-10-23 00:00:00', $result[3]->value );
10184+
$this->assertSame( '2025-10-23 18:30:00', $result[4]->value );
10185+
$this->assertSame( '2025-10-23 18:30:00', $result[5]->value );
10186+
$this->assertQuery( 'DROP TABLE t' );
10187+
10188+
// TIMESTAMP
10189+
$this->assertQuery( 'CREATE TABLE t (value TIMESTAMP)' );
10190+
$this->assertQuery( 'INSERT INTO t VALUES (NULL)' );
10191+
$this->assertQuery( 'INSERT INTO t VALUES (FALSE)' );
10192+
$this->assertQuery( 'INSERT INTO t VALUES (TRUE)' );
10193+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23')" );
10194+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23 18:30:00')" );
10195+
$this->assertQuery( "INSERT INTO t VALUES ('2025-10-23 18:30:00.123456')" );
10196+
10197+
$result = $this->assertQuery( 'SELECT * FROM t' );
10198+
$this->assertSame( null, $result[0]->value );
10199+
//$this->assertSame( '0', $result[1]->value );
10200+
//$this->assertSame( '1', $result[2]->value );
10201+
$this->assertSame( '2025-10-23 00:00:00', $result[3]->value );
10202+
$this->assertSame( '2025-10-23 18:30:00', $result[4]->value );
10203+
$this->assertSame( '2025-10-23 18:30:00', $result[5]->value );
10204+
$this->assertQuery( 'DROP TABLE t' );
10205+
}
1005610206
}

tests/WP_SQLite_Driver_Translation_Tests.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public function testSelect(): void {
9494
}
9595

9696
public function testInsert(): void {
97+
$this->driver->query( 'CREATE TABLE t (c INT)' );
98+
9799
$this->assertQuery(
98100
'INSERT INTO `t` ( `c` ) VALUES ( 1 )',
99101
'INSERT INTO t (c) VALUES (1)'
@@ -121,6 +123,8 @@ public function testInsert(): void {
121123
}
122124

123125
public function testReplace(): void {
126+
$this->driver->query( 'CREATE TABLE t (c INT)' );
127+
124128
$this->assertQuery(
125129
'REPLACE INTO `t` ( `c` ) VALUES ( 1 )',
126130
'REPLACE INTO t (c) VALUES (1)'

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

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,12 +1500,6 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
15001500
* @throws WP_SQLite_Driver_Exception When the query execution fails.
15011501
*/
15021502
private function execute_insert_or_replace_statement( WP_Parser_Node $node ): void {
1503-
// Check if strict mode is disabled.
1504-
$is_non_strict_mode = (
1505-
! $this->is_sql_mode_active( 'STRICT_TRANS_TABLES' )
1506-
&& ! $this->is_sql_mode_active( 'STRICT_ALL_TABLES' )
1507-
);
1508-
15091503
$parts = array();
15101504
foreach ( $node->get_children() as $child ) {
15111505
$is_token = $child instanceof WP_MySQL_Token;
@@ -1527,8 +1521,7 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
15271521
// Translate "UPDATE IGNORE" to "UPDATE OR IGNORE".
15281522
$parts[] = 'OR IGNORE';
15291523
} elseif (
1530-
$is_non_strict_mode
1531-
&& $is_node
1524+
$is_node
15321525
&& (
15331526
'insertFromConstructor' === $child->rule_name
15341527
|| 'insertQueryExpression' === $child->rule_name
@@ -1537,17 +1530,7 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
15371530
) {
15381531
$table_ref = $node->get_first_child_node( 'tableRef' );
15391532
$table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) );
1540-
$parts[] = $this->translate_insert_or_replace_body_in_non_strict_mode( $table_name, $child );
1541-
} elseif ( $is_node && 'updateList' === $child->rule_name ) {
1542-
// Convert "SET c1 = v1, c2 = v2, ... to "(c1, c2, ...) VALUES (v1, v2, ...)".
1543-
$columns = array();
1544-
$values = array();
1545-
foreach ( $child->get_child_nodes( 'updateElement' ) as $update_element ) {
1546-
$column_ref = $update_element->get_first_child_node( 'columnRef' );
1547-
$columns[] = $this->translate( $column_ref );
1548-
$values[] = $this->translate( $update_element->get_first_child_node( 'expr' ) );
1549-
}
1550-
$parts[] = '(' . implode( ', ', $columns ) . ') VALUES (' . implode( ', ', $values ) . ')';
1533+
$parts[] = $this->translate_insert_or_replace_body( $table_name, $child );
15511534
} else {
15521535
$parts[] = $this->translate( $child );
15531536
}
@@ -4423,13 +4406,19 @@ private function translate_show_like_or_where_condition( WP_Parser_Node $like_or
44234406
* @param WP_Parser_Node $node The "insertQueryExpression" or "insertValues" AST node.
44244407
* @return string The translated INSERT query body.
44254408
*/
4426-
private function translate_insert_or_replace_body_in_non_strict_mode(
4409+
private function translate_insert_or_replace_body(
44274410
string $table_name,
44284411
WP_Parser_Node $node
44294412
): string {
44304413
// This method is always used with the main database.
44314414
$database = $this->get_saved_db_name( $this->main_db_name );
44324415

4416+
// Check if strict mode is enabled.
4417+
$is_strict_mode = (
4418+
$this->is_sql_mode_active( 'STRICT_TRANS_TABLES' )
4419+
|| $this->is_sql_mode_active( 'STRICT_ALL_TABLES' )
4420+
);
4421+
44334422
// Get column metadata for the target table from the information schema.
44344423
$is_temporary = $this->information_schema_builder->temporary_table_exists( $table_name );
44354424
$columns_table = $this->information_schema_builder->get_table_name( $is_temporary, 'columns' );
@@ -4472,16 +4461,24 @@ private function translate_insert_or_replace_body_in_non_strict_mode(
44724461
// Prepare a helper map of columns that are included in the INSERT list.
44734462
$insert_map = array_combine( $insert_list, $insert_list );
44744463

4475-
// Filter out omitted columns that will get a value from the SQLite engine.
4476-
// That is, nullable columns, columns with defaults, and generated columns.
4464+
/*
4465+
* Filter out columns that were omitted in the INSERT list:
4466+
* 1. In strict mode, filter out all omitted columns.
4467+
* 2. In non-strict mode, filter out omitted columns that will get a
4468+
* value from the SQLite engine. That is, nullable columns, columns
4469+
* with defaults, and generated columns.
4470+
*/
44774471
$columns = array_values(
44784472
array_filter(
44794473
$columns,
4480-
function ( $column ) use ( $insert_map ) {
4474+
function ( $column ) use ( $is_strict_mode, $insert_map ) {
44814475
$is_omitted = ! isset( $insert_map[ $column['COLUMN_NAME'] ] );
44824476
if ( ! $is_omitted ) {
44834477
return true;
44844478
}
4479+
if ( $is_strict_mode ) {
4480+
return false;
4481+
}
44854482
$is_nullable = 'YES' === $column['IS_NULLABLE'];
44864483
$has_default = $column['COLUMN_DEFAULT'];
44874484
$is_generated = str_contains( $column['EXTRA'], 'auto_increment' );
@@ -4526,13 +4523,18 @@ function ( $column ) use ( $insert_map ) {
45264523
}
45274524
$fragment .= ')';
45284525

4529-
// Compose a wrapper SELECT statement emulating IMPLICIT DEFAULT values.
4526+
// Compose a wrapper SELECT statement emulating MySQL-like type casting,
4527+
// and, in non-strict mode, IMPLICIT DEFAULT values for omitted columns.
45304528
$fragment .= ' SELECT ';
45314529
foreach ( $columns as $i => $column ) {
45324530
$is_omitted = ! isset( $insert_map[ $column['COLUMN_NAME'] ] );
45334531
$fragment .= $i > 0 ? ', ' : '';
45344532
if ( $is_omitted ) {
45354533
/*
4534+
* This path only applies to non-strict mode. In strict mode,
4535+
* omitted columns get no IMPLICIT DEFAULT values, and they were
4536+
* previously filtered out from the columns list.
4537+
*
45364538
* When a column is omitted from the INSERT list, we need to use
45374539
* an IMPLICIT DEFAULT value. Note that at this point, all omitted
45384540
* columns that will not get an implicit default are filtered out.
@@ -4546,7 +4548,7 @@ function ( $column ) use ( $insert_map ) {
45464548
$identifier = $this->quote_sqlite_identifier( $select_list[ $position ] );
45474549
$fragment .= sprintf(
45484550
'%s AS %s',
4549-
$this->cast_value_in_non_strict_mode( $column['DATA_TYPE'], $identifier ),
4551+
$this->cast_value_for_insert_or_update( $column['DATA_TYPE'], $identifier ),
45504552
$identifier
45514553
);
45524554
}
@@ -4640,7 +4642,7 @@ private function translate_update_list_in_non_strict_mode( string $table_name, W
46404642
}
46414643

46424644
// Apply type casting.
4643-
$value = $this->cast_value_in_non_strict_mode( $data_type, $value );
4645+
$value = $this->cast_value_for_insert_or_update( $data_type, $value );
46444646

46454647
// If the column is NOT NULL, a NULL value resolves to implicit default.
46464648
$implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $data_type ] ?? null;
@@ -4944,23 +4946,27 @@ private function create_table_reference_map( WP_Parser_Node $node ): array {
49444946
}
49454947

49464948
/**
4947-
* Emulate MySQL type casting for INSERT or UPDATE value in non-strict mode.
4949+
* Emulate MySQL type casting for INSERT or UPDATE values.
49484950
*
49494951
* @param string $mysql_data_type The MySQL data type.
49504952
* @param string $translated_value The original translated value.
49514953
* @return string The translated value.
49524954
*/
4953-
private function cast_value_in_non_strict_mode(
4955+
private function cast_value_for_insert_or_update(
49544956
string $mysql_data_type,
49554957
string $translated_value
49564958
): string {
4957-
$sqlite_data_type = self::DATA_TYPE_STRING_MAP[ $mysql_data_type ];
4959+
// TODO: This is also a good place to implement checks for maximum column
4960+
// lengths with truncating or bailing out depending on the SQL mode.
4961+
4962+
// Check if strict mode is enabled.
4963+
$is_strict_mode = (
4964+
$this->is_sql_mode_active( 'STRICT_TRANS_TABLES' )
4965+
|| $this->is_sql_mode_active( 'STRICT_ALL_TABLES' )
4966+
);
49584967

4959-
// Get and quote the IMPLICIT DEFAULT value.
4960-
$implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $mysql_data_type ] ?? null;
4961-
$quoted_implicit_default = null === $implicit_default
4962-
? 'NULL'
4963-
: $this->connection->quote( $implicit_default );
4968+
$mysql_data_type = strtolower( $mysql_data_type );
4969+
$sqlite_data_type = self::DATA_TYPE_STRING_MAP[ $mysql_data_type ];
49644970

49654971
/*
49664972
* In MySQL, when saving a value via INSERT or UPDATE in non-strict mode,
@@ -4997,18 +5003,37 @@ private function cast_value_in_non_strict_mode(
49975003
$function_call = sprintf( "STRFTIME('%%Y', %s)", $translated_value );
49985004
}
49995005

5000-
// When the function call evaluates to NULL (invalid date/time),
5001-
// we need to fallback to the IMPLICIT DEFAULT value.
5006+
// In strict mode, invalid date/time values are rejected.
5007+
// In non-strict mode, they get an IMPLICIT DEFAULT value.
5008+
if ( $is_strict_mode ) {
5009+
$fallback = sprintf( "THROW('Incorrect datetime value: ''' || %s || '''')", $translated_value );
5010+
} else {
5011+
$implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $mysql_data_type ] ?? null;
5012+
$fallback = null === $implicit_default
5013+
? 'NULL'
5014+
: $this->connection->quote( $implicit_default );
5015+
}
50025016
return sprintf(
50035017
'IIF(%s IS NULL, NULL, COALESCE(%s, %s))',
50045018
$translated_value,
50055019
$function_call,
5006-
$quoted_implicit_default
5020+
$fallback
50075021
);
50085022
default:
5009-
// For all other data types, use SQLite-native CAST expression.
5010-
$mysql_data_type = strtolower( $mysql_data_type );
5011-
return sprintf( 'CAST(%s AS %s)', $translated_value, $sqlite_data_type );
5023+
/*
5024+
* For all other data types, cast to the SQLite types as follows:
5025+
* 1. In strict mode, cast only values for TEXT and BLOB columns.
5026+
* Numeric types accept string notation in SQLite as well.
5027+
* 2. In non-strict mode, cast all values.
5028+
*
5029+
* TODO: While close to MySQL behavior, this does't exactly match
5030+
* all special cases. We may improve this further to accept
5031+
* BLOBs for numeric types, and other special behaviors.
5032+
*/
5033+
if ( ! $is_strict_mode || 'TEXT' === $sqlite_data_type || 'BLOB' === $sqlite_data_type ) {
5034+
return sprintf( 'CAST(%s AS %s)', $translated_value, $sqlite_data_type );
5035+
}
5036+
return $translated_value;
50125037
}
50135038
}
50145039

0 commit comments

Comments
 (0)