diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php index 34171ac7..799a6669 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php @@ -5132,9 +5132,24 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { $fragment .= null === $default ? 'NULL' : $this->quote_sqlite_value( $default ); } else { // 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 ] ); - $value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier ); + $position = array_search( $column['COLUMN_NAME'], $insert_list, true ); + $identifier = $this->quote_sqlite_identifier( $select_list[ $position ] ); + $value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier ); + $is_auto_increment = 'auto_increment' === $column['EXTRA']; + + /* + * In MySQL, inserting 0 into an AUTO_INCREMENT column increments + * the sequence, unless the NO_AUTO_VALUE_ON_ZERO SQL mode is set. + * + * In SQLite, we need to rewrite 0 to NULL to advance the sequence. + * The value is cast to INTEGER before the comparison, because + * SQLite treats values of different types as unequal (0 != '0'). + * + * See: https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sqlmode_no_auto_value_on_zero + */ + if ( $is_auto_increment && ! $this->is_sql_mode_active( 'NO_AUTO_VALUE_ON_ZERO' ) ) { + $value = sprintf( 'NULLIF(CAST(%s AS INTEGER), 0)', $value ); + } /* * In MySQL non-STRICT mode, when inserting from a SELECT query: @@ -5142,9 +5157,17 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) { * 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). + * + * AUTO_INCREMENT columns are excluded. A NULL value advances + * the sequence regardless of the column's nullability. */ $is_insert_from_select = 'insertQueryExpression' === $node->rule_name; - if ( ! $is_strict_mode && $is_insert_from_select && 'NO' === $column['IS_NULLABLE'] ) { + if ( + ! $is_strict_mode + && ! $is_auto_increment + && $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->quote_sqlite_value( $implicit_default ) ); diff --git a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php index 4daf7ca7..73b4166a 100644 --- a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php @@ -2671,6 +2671,77 @@ public function testDateFunctionsOnZeroDates() { $this->assertEquals( 0, $results[0]->d ); } + public function testDefaultSqlModeDoesNotIncludeNoAutoValueOnZero() { + $this->assertQuery( 'SELECT @@sql_mode AS mode;' ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertStringNotContainsString( 'NO_AUTO_VALUE_ON_ZERO', strtoupper( $results[0]->mode ) ); + } + + public function testAutoIncrementZeroAdvancesSequenceByDefault() { + // Default SQL modes do not include NO_AUTO_VALUE_ON_ZERO. + // Values like 0 and '0' should behave like NULL and advance the sequence. + $this->assertQuery( + "INSERT INTO _options (ID, option_name, option_value) VALUES (0, 'a', '1');" + ); + $this->assertQuery( + "INSERT INTO _options (ID, option_name, option_value) VALUES ('0', 'b', '2');" + ); + $this->assertQuery( + "INSERT INTO _options (ID, option_name, option_value) VALUES (NULL, 'c', '3');" + ); + + $this->assertQuery( 'SELECT ID, option_name FROM _options ORDER BY ID;' ); + $results = $this->engine->get_query_results(); + $this->assertCount( 3, $results ); + $this->assertEquals( 1, $results[0]->ID ); + $this->assertEquals( 'a', $results[0]->option_name ); + $this->assertEquals( 2, $results[1]->ID ); + $this->assertEquals( 'b', $results[1]->option_name ); + $this->assertEquals( 3, $results[2]->ID ); + $this->assertEquals( 'c', $results[2]->option_name ); + } + + public function testAutoIncrementZeroAdvancesSequenceForAllInsertShapes() { + // INSERT ... SET + $this->assertQuery( "INSERT INTO _options SET ID = 0, option_name = 'set', option_value = '1';" ); + + // INSERT ... SELECT + $this->assertQuery( "INSERT INTO _options (ID, option_name, option_value) SELECT 0, 'select', '2';" ); + + // REPLACE ... VALUES + $this->assertQuery( "REPLACE INTO _options (ID, option_name, option_value) VALUES ('0', 'replace', '3');" ); + + $this->assertQuery( 'SELECT ID, option_name FROM _options ORDER BY ID;' ); + $results = $this->engine->get_query_results(); + $this->assertCount( 3, $results ); + $this->assertEquals( 1, $results[0]->ID ); + $this->assertEquals( 2, $results[1]->ID ); + $this->assertEquals( 3, $results[2]->ID ); + } + + public function testNoAutoValueOnZeroSqlMode() { + $this->assertQuery( "SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'" ); + + // Literal 0 and '0' are stored as-is. Only NULL generates a value. + $this->assertQuery( + "INSERT INTO _options (ID, option_name, option_value) VALUES (0, 'a', '1');" + ); + + $this->assertQuery( "SELECT ID FROM _options WHERE option_name = 'a';" ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( 0, $results[0]->ID ); + + $this->assertQuery( + "INSERT INTO _options (ID, option_name, option_value) VALUES (NULL, 'b', '2');" + ); + $this->assertQuery( "SELECT ID FROM _options WHERE option_name = 'b';" ); + $results = $this->engine->get_query_results(); + $this->assertCount( 1, $results ); + $this->assertEquals( 1, $results[0]->ID ); + } + public function testCaseInsensitiveSelect() { $this->assertQuery( "CREATE TABLE _tmp_table (