Skip to content

Commit eb3146a

Browse files
authored
Add support for NO_AUTO_VALUE_ON_ZERO SQL mode (#366)
## Summary Emulates MySQL's `NO_AUTO_VALUE_ON_ZERO` SQL mode behavior on SQLite. MySQL treats a literal `0` in an `AUTO_INCREMENT` column the same as `NULL` and generates the next sequence value. This can be suppressed by the `NO_AUTO_VALUE_ON_ZERO` SQL mode, which is **not** part of MySQL's default modes. Some legacy WordPress versions rely on the default behavior — e.g. WP 1.0's `post.php` emits: ```sql INSERT INTO wp_posts (ID, post_author, post_title, post_status) VALUES ('0', '1', 'Hello', 'publish'); ``` On MySQL this stores `ID = 1`. The current SQLite driver stored `ID = 0`, which fails with legacy WP. ## Scope - **Affects:** `INSERT` / `REPLACE` value lists (including `VALUES`, `SET`, and `SELECT` forms). - **Does not affect:** `UPDATE` (and the UPDATE half of `INSERT ... ON DUPLICATE KEY UPDATE`). MySQL never auto-generates `AUTO_INCREMENT` values on UPDATE — zeros are stored literally. The default `sql_mode` was already correct — `NO_AUTO_VALUE_ON_ZERO` is absent, matching MySQL 8.0 defaults. ## Tests - `testDefaultSqlModeDoesNotIncludeNoAutoValueOnZero` — asserts the default modes. - `testAutoIncrementZeroAdvancesSequenceByDefault` — `0`, `'0'`, and `NULL` all generate new ids. - `testAutoIncrementZeroAdvancesSequenceForAllInsertShapes` — covers `INSERT ... SET`, `INSERT ... SELECT`, and `REPLACE`. - `testNoAutoValueOnZeroSqlMode` — with the mode on, literal `0` is stored as-is; only `NULL` 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 eb3146a

2 files changed

Lines changed: 101 additions & 7 deletions

File tree

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

Lines changed: 30 additions & 7 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 = str_contains( $column['EXTRA'], 'auto_increment' );
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 ) );
@@ -6206,7 +6229,7 @@ private function get_sqlite_create_table_statement(
62066229
if (
62076230
'INTEGER' === $type
62086231
&& 'PRI' === $column['COLUMN_KEY']
6209-
&& 'auto_increment' !== $column['EXTRA']
6232+
&& ! str_contains( $column['EXTRA'], 'auto_increment' )
62106233
&& count( $grouped_constraints['PRIMARY'] ) === 1
62116234
) {
62126235
$type = 'INT';
@@ -6223,7 +6246,7 @@ private function get_sqlite_create_table_statement(
62236246
if ( 'NO' === $column['IS_NULLABLE'] ) {
62246247
$query .= ' NOT NULL';
62256248
}
6226-
if ( 'auto_increment' === $column['EXTRA'] ) {
6249+
if ( str_contains( $column['EXTRA'], 'auto_increment' ) ) {
62276250
$has_autoincrement = true;
62286251
$query .= ' PRIMARY KEY AUTOINCREMENT';
62296252
}
@@ -6534,7 +6557,7 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str
65346557
// Nullable "timestamp" columns dump NULL explicitly.
65356558
$sql .= ' NULL';
65366559
}
6537-
if ( 'auto_increment' === $column['EXTRA'] ) {
6560+
if ( str_contains( $column['EXTRA'], 'auto_increment' ) ) {
65386561
$has_auto_increment = true;
65396562
$sql .= ' AUTO_INCREMENT';
65406563
}

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)