Skip to content

Commit 257e916

Browse files
committed
Implement emulation of MySQL non-strict mode for UPDATE queries
1 parent 52769f0 commit 257e916

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

tests/WP_SQLite_Driver_Tests.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4079,6 +4079,14 @@ public function testNonStrictSqlModeNullWithDefault(): void {
40794079
$result = $this->assertQuery( 'SELECT * FROM t2' );
40804080
$this->assertCount( 1, $result );
40814081
$this->assertNull( $result[0]->value );
4082+
4083+
// NULL value saves NULL on UPDATE:
4084+
$this->assertQuery( "CREATE TABLE t3 (id INT, value TEXT NULL DEFAULT 'd')" );
4085+
$this->assertQuery( "INSERT INTO t3 (id, value) VALUES (1, 'initial-value')" );
4086+
$this->assertQuery( 'UPDATE t3 SET value = NULL WHERE id = 1' );
4087+
$result = $this->assertQuery( 'SELECT * FROM t3' );
4088+
$this->assertCount( 1, $result );
4089+
$this->assertNull( $result[0]->value );
40824090
}
40834091

40844092
public function testNonStrictSqlModeNotNullWithoutDefault(): void {
@@ -4102,6 +4110,14 @@ public function testNonStrictSqlModeNotNullWithoutDefault(): void {
41024110
'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t2.value',
41034111
$exception->getMessage()
41044112
);
4113+
4114+
// NULL value saves IMPLICIT DEFAULT on UPDATE:
4115+
$this->assertQuery( 'CREATE TABLE t3 (id INT, value TEXT NOT NULL)' );
4116+
$this->assertQuery( "INSERT INTO t3 (id, value) VALUES (1, 'initial-value')" );
4117+
$this->assertQuery( 'UPDATE t3 SET value = NULL WHERE id = 1' );
4118+
$result = $this->assertQuery( 'SELECT * FROM t3' );
4119+
$this->assertCount( 1, $result );
4120+
$this->assertSame( '', $result[0]->value );
41054121
}
41064122

41074123
public function testNonStrictSqlModeNotNullWithDefault(): void {
@@ -4125,5 +4141,13 @@ public function testNonStrictSqlModeNotNullWithDefault(): void {
41254141
'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t2.value',
41264142
$exception->getMessage()
41274143
);
4144+
4145+
// NULL value saves IMPLICIT DEFAULT on UPDATE:
4146+
$this->assertQuery( 'CREATE TABLE t3 (id INT, value TEXT NOT NULL DEFAULT "d")' );
4147+
$this->assertQuery( "INSERT INTO t3 (id, value) VALUES (1, 'initial-value')" );
4148+
$this->assertQuery( 'UPDATE t3 SET value = NULL WHERE id = 1' );
4149+
$result = $this->assertQuery( 'SELECT * FROM t3' );
4150+
$this->assertCount( 1, $result );
4151+
$this->assertSame( '', $result[0]->value );
41284152
}
41294153
}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,11 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
10371037
if ( $child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $child->id ) {
10381038
// Translate "UPDATE IGNORE" to "UPDATE OR IGNORE".
10391039
$parts[] = 'OR IGNORE';
1040+
} elseif ( $child instanceof WP_Parser_Node && 'updateList' === $child->rule_name ) {
1041+
$table_ref = $node->get_first_child_node( 'tableReferenceList' )->get_first_child_node( 'tableReference' );
1042+
$table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) );
1043+
// TODO: Use this only in non-strict mode.
1044+
$parts[] = $this->translate_update_list_in_non_strict_mode( $table_name, $child );
10401045
} else {
10411046
$parts[] = $this->translate( $child );
10421047
}
@@ -2802,6 +2807,70 @@ private function translate_insert_or_replace_body_in_non_strict_mode(
28022807
return $fragment;
28032808
}
28042809

2810+
/**
2811+
* Translate UPDATE list, emulating MySQL implicit defaults in non-strict mode.
2812+
*
2813+
* In MySQL, the behavior of INSERT and UPDATE statements depends on whether
2814+
* the STRICT_TRANS_TABLES (InnoDB) or STRICT_ALL_TABLES SQL mode is enabled.
2815+
*
2816+
* When the strict mode is not enabled, executing an UPDATE statement that
2817+
* sets a NOT NULL column value to NULL saves an IMPLICIT DEFAULT instead.
2818+
*
2819+
* @param string $table_name The name of the target table.
2820+
* @param WP_Parser_Node $node The "updateList" AST node.
2821+
* @return string The translated UPDATE list.
2822+
*/
2823+
private function translate_update_list_in_non_strict_mode( string $table_name, WP_Parser_Node $node ): string {
2824+
// 1. Get column metadata from information schema.
2825+
$is_temporary = $this->information_schema_builder->temporary_table_exists( $table_name );
2826+
$columns_table = $this->information_schema_builder->get_table_name( $is_temporary, 'columns' );
2827+
$columns = $this->execute_sqlite_query(
2828+
"
2829+
SELECT column_name, is_nullable, data_type, column_default
2830+
FROM $columns_table
2831+
WHERE table_schema = ?
2832+
AND table_name = ?
2833+
",
2834+
array( $this->db_name, $table_name )
2835+
)->fetchAll( PDO::FETCH_ASSOC );
2836+
$column_map = array_combine( array_column( $columns, 'COLUMN_NAME' ), $columns );
2837+
2838+
// 2. Translate UPDATE list, emulating implicit defaults for NULLs values.
2839+
$fragment = '';
2840+
foreach ( $node->get_child_nodes() as $i => $update_element ) {
2841+
$column_ref = $update_element->get_first_child_node( 'columnRef' );
2842+
$expr = $update_element->get_first_child_node( 'expr' );
2843+
2844+
// Get column info.
2845+
$column_name = $this->unquote_sqlite_identifier( $this->translate( $column_ref ) );
2846+
$column_info = $column_map[ $column_name ];
2847+
$data_type = $column_info['DATA_TYPE'];
2848+
$is_nullable = 'YES' === $column_info['IS_NULLABLE'];
2849+
$default = $column_info['COLUMN_DEFAULT'];
2850+
2851+
// Get the UPDATE value. It's either an expression or a DEFAULT keyword.
2852+
if ( null === $expr ) {
2853+
// Emulate "column = DEFAULT".
2854+
$value = null === $default ? 'NULL' : $this->pdo->quote( $default );
2855+
} else {
2856+
$value = $this->translate( $expr );
2857+
}
2858+
2859+
// If the column is NOT NULL, a NULL value resolves to implicit default.
2860+
$implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $data_type ] ?? null;
2861+
if ( ! $is_nullable && null !== $implicit_default ) {
2862+
$value = sprintf( 'COALESCE(%s, %s)', $value, $this->pdo->quote( $implicit_default ) );
2863+
}
2864+
2865+
// Compose the UPDATE list item.
2866+
$fragment .= $i > 0 ? ', ' : '';
2867+
$fragment .= $this->translate( $column_ref );
2868+
$fragment .= ' = ';
2869+
$fragment .= $value;
2870+
}
2871+
return $fragment;
2872+
}
2873+
28052874
/**
28062875
* Generate a SQLite CREATE TABLE statement from information schema data.
28072876
*

0 commit comments

Comments
 (0)