Skip to content

Commit 9cb9ede

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 3a3baf7 commit 9cb9ede

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
@@ -5132,19 +5132,42 @@ function ( $column ) use ( $is_strict_mode, $insert_map ) {
51325132
$fragment .= null === $default ? 'NULL' : $this->quote_sqlite_value( $default );
51335133
} else {
51345134
// When a column value is included, we need to apply type casting.
5135-
$position = array_search( $column['COLUMN_NAME'], $insert_list, true );
5136-
$identifier = $this->quote_sqlite_identifier( $select_list[ $position ] );
5137-
$value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier );
5135+
$position = array_search( $column['COLUMN_NAME'], $insert_list, true );
5136+
$identifier = $this->quote_sqlite_identifier( $select_list[ $position ] );
5137+
$value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier );
5138+
$is_auto_increment = 'auto_increment' === $column['EXTRA'];
5139+
5140+
/*
5141+
* In MySQL, inserting 0 into an AUTO_INCREMENT column increments
5142+
* the sequence, unless the NO_AUTO_VALUE_ON_ZERO SQL mode is set.
5143+
*
5144+
* In SQLite, we need to rewrite 0 to NULL to advance the sequence.
5145+
* The value is cast to INTEGER before the comparison, because
5146+
* SQLite treats values of different types as unequal (0 != '0').
5147+
*
5148+
* See: https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sqlmode_no_auto_value_on_zero
5149+
*/
5150+
if ( $is_auto_increment && ! $this->is_sql_mode_active( 'NO_AUTO_VALUE_ON_ZERO' ) ) {
5151+
$value = sprintf( 'NULLIF(CAST(%s AS INTEGER), 0)', $value );
5152+
}
51385153

51395154
/*
51405155
* In MySQL non-STRICT mode, when inserting from a SELECT query:
51415156
*
51425157
* When a column is declared as NOT NULL, inserting a NULL value
51435158
* saves an IMPLICIT DEFAULT value instead. This behavior only
51445159
* applies to the INSERT ... SELECT syntax (not VALUES or SET).
5160+
*
5161+
* AUTO_INCREMENT columns are excluded. A NULL value advances
5162+
* the sequence regardless of the column's nullability.
51455163
*/
51465164
$is_insert_from_select = 'insertQueryExpression' === $node->rule_name;
5147-
if ( ! $is_strict_mode && $is_insert_from_select && 'NO' === $column['IS_NULLABLE'] ) {
5165+
if (
5166+
! $is_strict_mode
5167+
&& ! $is_auto_increment
5168+
&& $is_insert_from_select
5169+
&& 'NO' === $column['IS_NULLABLE']
5170+
) {
51485171
$implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $column['DATA_TYPE'] ] ?? null;
51495172
if ( null !== $implicit_default ) {
51505173
$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
@@ -2671,6 +2671,77 @@ public function testDateFunctionsOnZeroDates() {
26712671
$this->assertEquals( 0, $results[0]->d );
26722672
}
26732673

2674+
public function testDefaultSqlModeDoesNotIncludeNoAutoValueOnZero() {
2675+
$this->assertQuery( 'SELECT @@sql_mode AS mode;' );
2676+
$results = $this->engine->get_query_results();
2677+
$this->assertCount( 1, $results );
2678+
$this->assertStringNotContainsString( 'NO_AUTO_VALUE_ON_ZERO', strtoupper( $results[0]->mode ) );
2679+
}
2680+
2681+
public function testAutoIncrementZeroAdvancesSequenceByDefault() {
2682+
// Default SQL modes do not include NO_AUTO_VALUE_ON_ZERO.
2683+
// Values like 0 and '0' should behave like NULL and advance the sequence.
2684+
$this->assertQuery(
2685+
"INSERT INTO _options (ID, option_name, option_value) VALUES (0, 'a', '1');"
2686+
);
2687+
$this->assertQuery(
2688+
"INSERT INTO _options (ID, option_name, option_value) VALUES ('0', 'b', '2');"
2689+
);
2690+
$this->assertQuery(
2691+
"INSERT INTO _options (ID, option_name, option_value) VALUES (NULL, 'c', '3');"
2692+
);
2693+
2694+
$this->assertQuery( 'SELECT ID, option_name FROM _options ORDER BY ID;' );
2695+
$results = $this->engine->get_query_results();
2696+
$this->assertCount( 3, $results );
2697+
$this->assertEquals( 1, $results[0]->ID );
2698+
$this->assertEquals( 'a', $results[0]->option_name );
2699+
$this->assertEquals( 2, $results[1]->ID );
2700+
$this->assertEquals( 'b', $results[1]->option_name );
2701+
$this->assertEquals( 3, $results[2]->ID );
2702+
$this->assertEquals( 'c', $results[2]->option_name );
2703+
}
2704+
2705+
public function testAutoIncrementZeroAdvancesSequenceForAllInsertShapes() {
2706+
// INSERT ... SET
2707+
$this->assertQuery( "INSERT INTO _options SET ID = 0, option_name = 'set', option_value = '1';" );
2708+
2709+
// INSERT ... SELECT
2710+
$this->assertQuery( "INSERT INTO _options (ID, option_name, option_value) SELECT 0, 'select', '2';" );
2711+
2712+
// REPLACE ... VALUES
2713+
$this->assertQuery( "REPLACE INTO _options (ID, option_name, option_value) VALUES ('0', 'replace', '3');" );
2714+
2715+
$this->assertQuery( 'SELECT ID, option_name FROM _options ORDER BY ID;' );
2716+
$results = $this->engine->get_query_results();
2717+
$this->assertCount( 3, $results );
2718+
$this->assertEquals( 1, $results[0]->ID );
2719+
$this->assertEquals( 2, $results[1]->ID );
2720+
$this->assertEquals( 3, $results[2]->ID );
2721+
}
2722+
2723+
public function testNoAutoValueOnZeroSqlMode() {
2724+
$this->assertQuery( "SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'" );
2725+
2726+
// Literal 0 and '0' are stored as-is. Only NULL generates a value.
2727+
$this->assertQuery(
2728+
"INSERT INTO _options (ID, option_name, option_value) VALUES (0, 'a', '1');"
2729+
);
2730+
2731+
$this->assertQuery( "SELECT ID FROM _options WHERE option_name = 'a';" );
2732+
$results = $this->engine->get_query_results();
2733+
$this->assertCount( 1, $results );
2734+
$this->assertEquals( 0, $results[0]->ID );
2735+
2736+
$this->assertQuery(
2737+
"INSERT INTO _options (ID, option_name, option_value) VALUES (NULL, 'b', '2');"
2738+
);
2739+
$this->assertQuery( "SELECT ID FROM _options WHERE option_name = 'b';" );
2740+
$results = $this->engine->get_query_results();
2741+
$this->assertCount( 1, $results );
2742+
$this->assertEquals( 1, $results[0]->ID );
2743+
}
2744+
26742745
public function testCaseInsensitiveSelect() {
26752746
$this->assertQuery(
26762747
"CREATE TABLE _tmp_table (

0 commit comments

Comments
 (0)