From eddd73635416609a70c725c6d2da28a1b3dafb87 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Thu, 6 Nov 2025 15:43:06 +0100 Subject: [PATCH] Fix `INSERT ... SELECT ...` edge case in non-strict mode --- tests/WP_SQLite_Driver_Tests.php | 36 +++++++++++++++++++ .../sqlite-ast/class-wp-sqlite-driver.php | 18 +++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index ecd06361..5786f9c0 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -10526,6 +10526,42 @@ public function testCastValuesOnInsertInNonStrictMode(): void { $this->assertQuery( 'DROP TABLE t' ); } + public function testCastNotNullValuesOnInsert(): void { + $this->assertQuery( 'CREATE TABLE t (value INT NOT NULL)' ); + + // Strict mode: + $this->assertQueryError( 'INSERT INTO t VALUES (NULL)', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + $this->assertQueryError( 'INSERT INTO t SET value = NULL', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + $this->assertQueryError( 'INSERT INTO t SELECT NULL', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + $this->assertQueryError( 'INSERT INTO t VALUES ((SELECT NULL))', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + + // Non-strict mode: + $this->assertQuery( "SET SESSION sql_mode = ''" ); + $this->assertQueryError( 'INSERT INTO t VALUES (NULL)', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + $this->assertQueryError( 'INSERT INTO t SET value = NULL', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + $this->assertQuery( 'INSERT INTO t SELECT NULL' ); + $this->assertSame( '0', $this->assertQuery( 'SELECT * FROM t' )[0]->value ); + $this->assertQueryError( 'INSERT INTO t VALUES ((SELECT NULL))', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + } + + public function testCastNotNullValuesOnUpdate(): void { + $this->assertQuery( 'CREATE TABLE t (value INT NOT NULL)' ); + $this->assertQuery( 'INSERT INTO t VALUES (1)' ); + + // Strict mode: + $this->assertQueryError( 'UPDATE t SET value = NULL', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + $this->assertQueryError( 'UPDATE t SET value = (SELECT NULL)', 'SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: t.value' ); + + // Non-strict mode: + $this->assertQuery( "SET SESSION sql_mode = ''" ); + $this->assertQuery( 'UPDATE t SET value = NULL' ); + $this->assertSame( '0', $this->assertQuery( 'SELECT * FROM t' )[0]->value ); + + $this->assertQuery( 'UPDATE t SET value = (SELECT NULL)' ); + $this->assertSame( '0', $this->assertQuery( 'SELECT * FROM t' )[0]->value ); + $this->assertQuery( 'DROP TABLE t' ); + } + public function testCastValuesOnDuplicateKeyUpdate(): void { $this->assertQuery( 'CREATE TABLE t (value TEXT UNIQUE)' ); $this->assertQuery( "INSERT INTO t VALUES ('test')" ); diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 0504baa8..41a61a7e 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -4580,7 +4580,23 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { // When a column value is included, we need to apply type casting. $position = array_search( $column['COLUMN_NAME'], $insert_list, true ); $identifier = $this->quote_sqlite_identifier( $select_list[ $position ] ); - $fragment .= $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier ); + $value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier ); + + /* + * In MySQL non-STRICT mode, when inserting from a SELECT query: + * + * When a column is declared as NOT NULL, inserting a NULL value + * saves an IMPLICIT DEFAULT value instead. This behavior only + * applies to the INSERT ... SELECT syntax (not VALUES or SET). + */ + $is_insert_from_select = 'insertQueryExpression' === $node->rule_name; + if ( ! $is_strict_mode && $is_insert_from_select && 'NO' === $column['IS_NULLABLE'] ) { + $implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $column['DATA_TYPE'] ] ?? null; + if ( null !== $implicit_default ) { + $value = sprintf( 'COALESCE(%s, %s)', $value, $this->connection->quote( $implicit_default ) ); + } + } + $fragment .= $value; } }