Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5132,19 +5132,42 @@ 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:
*
* 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 ) );
Expand Down
71 changes: 71 additions & 0 deletions packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Loading