Skip to content

Commit 72c5ea6

Browse files
committed
Handle NO_AUTO_VALUE_ON_ZERO in the INSERT translator
By default, MySQL treats a literal 0 in an AUTO_INCREMENT column the same as NULL and generates the next sequence value. This behavior is suppressed by the NO_AUTO_VALUE_ON_ZERO SQL mode, which is not part of the default modes. To emulate this on SQLite, rewrite the value to NULL via NULLIF(CAST(... AS INTEGER), 0). The explicit CAST is required because SQLite compares storage classes strictly, so NULLIF('0', 0) returns '0', not NULL — and WordPress 1.0 emits the string form. Also exclude AUTO_INCREMENT columns from the non-strict IMPLICIT DEFAULT COALESCE wrapping, so a NULL (original or rewritten from 0) always advances the sequence. See: https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sqlmode_no_auto_value_on_zero
1 parent de8f9b0 commit 72c5ea6

2 files changed

Lines changed: 98 additions & 4 deletions

File tree

packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5351,19 +5351,42 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) {
53515351
$fragment .= null === $default ? 'NULL' : $this->quote_sqlite_value( $default );
53525352
} else {
53535353
// When a column value is included, we need to apply type casting.
5354-
$position = array_search( $column['COLUMN_NAME'], $insert_list, true );
5355-
$identifier = $this->quote_sqlite_identifier( $select_list[ $position ] );
5356-
$value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier );
5354+
$position = array_search( $column['COLUMN_NAME'], $insert_list, true );
5355+
$identifier = $this->quote_sqlite_identifier( $select_list[ $position ] );
5356+
$value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier );
5357+
$is_auto_increment = 'auto_increment' === $column['EXTRA'];
5358+
5359+
/*
5360+
* In MySQL, inserting 0 into an AUTO_INCREMENT column increments
5361+
* the sequence, unless the NO_AUTO_VALUE_ON_ZERO SQL mode is set.
5362+
*
5363+
* In SQLite, we need to rewrite 0 to NULL to advance the sequence.
5364+
* The value is cast to INTEGER before the comparison, because
5365+
* SQLite treats values of different types as unequal (0 != '0').
5366+
*
5367+
* See: https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sqlmode_no_auto_value_on_zero
5368+
*/
5369+
if ( $is_auto_increment && ! $this->is_sql_mode_active( 'NO_AUTO_VALUE_ON_ZERO' ) ) {
5370+
$value = sprintf( 'NULLIF(CAST(%s AS INTEGER), 0)', $value );
5371+
}
53575372

53585373
/*
53595374
* In MySQL non-STRICT mode, when inserting from a SELECT query:
53605375
*
53615376
* When a column is declared as NOT NULL, inserting a NULL value
53625377
* saves an IMPLICIT DEFAULT value instead. This behavior only
53635378
* applies to the INSERT ... SELECT syntax (not VALUES or SET).
5379+
*
5380+
* AUTO_INCREMENT columns are excluded. A NULL value advances
5381+
* the sequence regardless of the column's nullability.
53645382
*/
53655383
$is_insert_from_select = 'insertQueryExpression' === $node->rule_name;
5366-
if ( ! $is_strict_mode && $is_insert_from_select && 'NO' === $column['IS_NULLABLE'] ) {
5384+
if (
5385+
! $is_strict_mode
5386+
&& ! $is_auto_increment
5387+
&& $is_insert_from_select
5388+
&& 'NO' === $column['IS_NULLABLE']
5389+
) {
53675390
$implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $column['DATA_TYPE'] ] ?? null;
53685391
if ( null !== $implicit_default ) {
53695392
$value = sprintf( 'COALESCE(%s, %s)', $value, $this->quote_sqlite_value( $implicit_default ) );

packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2774,6 +2774,77 @@ public function testDateFunctionsOnZeroDates() {
27742774
$this->assertEquals( 0, $results[0]->d );
27752775
}
27762776

2777+
public function testDefaultSqlModeDoesNotIncludeNoAutoValueOnZero() {
2778+
$this->assertQuery( 'SELECT @@sql_mode AS mode;' );
2779+
$results = $this->engine->get_query_results();
2780+
$this->assertCount( 1, $results );
2781+
$this->assertStringNotContainsString( 'NO_AUTO_VALUE_ON_ZERO', strtoupper( $results[0]->mode ) );
2782+
}
2783+
2784+
public function testAutoIncrementZeroAdvancesSequenceByDefault() {
2785+
// Default SQL modes do not include NO_AUTO_VALUE_ON_ZERO.
2786+
// Values like 0 and '0' should behave like NULL and advance the sequence.
2787+
$this->assertQuery(
2788+
"INSERT INTO _options (ID, option_name, option_value) VALUES (0, 'a', '1');"
2789+
);
2790+
$this->assertQuery(
2791+
"INSERT INTO _options (ID, option_name, option_value) VALUES ('0', 'b', '2');"
2792+
);
2793+
$this->assertQuery(
2794+
"INSERT INTO _options (ID, option_name, option_value) VALUES (NULL, 'c', '3');"
2795+
);
2796+
2797+
$this->assertQuery( 'SELECT ID, option_name FROM _options ORDER BY ID;' );
2798+
$results = $this->engine->get_query_results();
2799+
$this->assertCount( 3, $results );
2800+
$this->assertEquals( 1, $results[0]->ID );
2801+
$this->assertEquals( 'a', $results[0]->option_name );
2802+
$this->assertEquals( 2, $results[1]->ID );
2803+
$this->assertEquals( 'b', $results[1]->option_name );
2804+
$this->assertEquals( 3, $results[2]->ID );
2805+
$this->assertEquals( 'c', $results[2]->option_name );
2806+
}
2807+
2808+
public function testAutoIncrementZeroAdvancesSequenceForAllInsertShapes() {
2809+
// INSERT ... SET
2810+
$this->assertQuery( "INSERT INTO _options SET ID = 0, option_name = 'set', option_value = '1';" );
2811+
2812+
// INSERT ... SELECT
2813+
$this->assertQuery( "INSERT INTO _options (ID, option_name, option_value) SELECT 0, 'select', '2';" );
2814+
2815+
// REPLACE ... VALUES
2816+
$this->assertQuery( "REPLACE INTO _options (ID, option_name, option_value) VALUES ('0', 'replace', '3');" );
2817+
2818+
$this->assertQuery( 'SELECT ID, option_name FROM _options ORDER BY ID;' );
2819+
$results = $this->engine->get_query_results();
2820+
$this->assertCount( 3, $results );
2821+
$this->assertEquals( 1, $results[0]->ID );
2822+
$this->assertEquals( 2, $results[1]->ID );
2823+
$this->assertEquals( 3, $results[2]->ID );
2824+
}
2825+
2826+
public function testNoAutoValueOnZeroSqlMode() {
2827+
$this->assertQuery( "SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'" );
2828+
2829+
// Literal 0 and '0' are stored as-is. Only NULL generates a value.
2830+
$this->assertQuery(
2831+
"INSERT INTO _options (ID, option_name, option_value) VALUES (0, 'a', '1');"
2832+
);
2833+
2834+
$this->assertQuery( "SELECT ID FROM _options WHERE option_name = 'a';" );
2835+
$results = $this->engine->get_query_results();
2836+
$this->assertCount( 1, $results );
2837+
$this->assertEquals( 0, $results[0]->ID );
2838+
2839+
$this->assertQuery(
2840+
"INSERT INTO _options (ID, option_name, option_value) VALUES (NULL, 'b', '2');"
2841+
);
2842+
$this->assertQuery( "SELECT ID FROM _options WHERE option_name = 'b';" );
2843+
$results = $this->engine->get_query_results();
2844+
$this->assertCount( 1, $results );
2845+
$this->assertEquals( 1, $results[0]->ID );
2846+
}
2847+
27772848
public function testCaseInsensitiveSelect() {
27782849
$this->assertQuery(
27792850
"CREATE TABLE _tmp_table (

0 commit comments

Comments
 (0)