Skip to content
This repository was archived by the owner on Jun 2, 2025. It is now read-only.

Commit 25675be

Browse files
committed
Implement type casting for INSERT and UPDATE in non-strict mode
1 parent 885e5cd commit 25675be

File tree

2 files changed

+173
-3
lines changed

2 files changed

+173
-3
lines changed

tests/WP_SQLite_Driver_Tests.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4543,6 +4543,113 @@ public function testNonStrictModeWithReplaceStatement(): void {
45434543
$this->assertSame( 'blue', $result[0]->color );
45444544
}
45454545

4546+
public function testNonStrictModeTypeCasting(): void {
4547+
$this->assertQuery(
4548+
"CREATE TABLE t (
4549+
col_int INT,
4550+
col_float FLOAT,
4551+
col_double DOUBLE,
4552+
col_decimal DECIMAL,
4553+
col_char CHAR(255),
4554+
col_varchar VARCHAR(255),
4555+
col_text TEXT,
4556+
col_bool BOOL,
4557+
col_bit BIT,
4558+
col_binary BINARY(255),
4559+
col_varbinary VARBINARY(255),
4560+
col_blob BLOB,
4561+
col_date DATE,
4562+
col_time TIME,
4563+
col_datetime DATETIME,
4564+
col_timestamp TIMESTAMP,
4565+
col_year YEAR,
4566+
col_enum ENUM('a', 'b', 'c'),
4567+
col_set SET('a', 'b', 'c'),
4568+
col_json JSON
4569+
)"
4570+
);
4571+
4572+
// Set non-strict mode.
4573+
$this->assertQuery( "SET SESSION sql_mode = ''" );
4574+
4575+
// INSERT.
4576+
$this->assertQuery(
4577+
"INSERT INTO t VALUES ('', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '')"
4578+
);
4579+
4580+
$result = $this->assertQuery( 'SELECT * FROM t' );
4581+
$this->assertCount( 1, $result );
4582+
$this->assertSame( '0', $result[0]->col_int );
4583+
$this->assertSame( PHP_VERSION_ID < 80100 ? '0.0' : '0', $result[0]->col_float );
4584+
$this->assertSame( PHP_VERSION_ID < 80100 ? '0.0' : '0', $result[0]->col_double );
4585+
$this->assertSame( PHP_VERSION_ID < 80100 ? '0.0' : '0', $result[0]->col_decimal );
4586+
$this->assertSame( '', $result[0]->col_char );
4587+
$this->assertSame( '', $result[0]->col_varchar );
4588+
$this->assertSame( '', $result[0]->col_text );
4589+
$this->assertSame( '0', $result[0]->col_bool );
4590+
$this->assertSame( '0', $result[0]->col_bit );
4591+
$this->assertSame( '0', $result[0]->col_binary ); // TODO: Should save ''.
4592+
$this->assertSame( '', $result[0]->col_varbinary );
4593+
$this->assertSame( '', $result[0]->col_blob );
4594+
$this->assertSame( '0000-00-00', $result[0]->col_date );
4595+
$this->assertSame( '00:00:00', $result[0]->col_time );
4596+
$this->assertSame( '0000-00-00 00:00:00', $result[0]->col_datetime );
4597+
$this->assertSame( '0000-00-00 00:00:00', $result[0]->col_timestamp );
4598+
$this->assertSame( '0000', $result[0]->col_year );
4599+
$this->assertSame( '', $result[0]->col_enum );
4600+
$this->assertSame( '', $result[0]->col_set );
4601+
$this->assertSame( '', $result[0]->col_json ); // TODO: This should not be allowed.
4602+
4603+
// UPDATE.
4604+
$this->assertQuery(
4605+
"UPDATE t SET
4606+
col_int = '',
4607+
col_float = '',
4608+
col_double = '',
4609+
col_decimal = '',
4610+
col_char = '',
4611+
col_varchar = '',
4612+
col_text = '',
4613+
col_bool = '',
4614+
col_bit = '',
4615+
col_binary = '',
4616+
col_varbinary = '',
4617+
col_blob = '',
4618+
col_date = '',
4619+
col_time = '',
4620+
col_datetime = '',
4621+
col_timestamp = '',
4622+
col_year = '',
4623+
col_enum = '',
4624+
col_set = '',
4625+
col_json = ''
4626+
"
4627+
);
4628+
4629+
$result = $this->assertQuery( 'SELECT * FROM t' );
4630+
$this->assertCount( 1, $result );
4631+
$this->assertSame( '0', $result[0]->col_int );
4632+
$this->assertSame( PHP_VERSION_ID < 80100 ? '0.0' : '0', $result[0]->col_float );
4633+
$this->assertSame( PHP_VERSION_ID < 80100 ? '0.0' : '0', $result[0]->col_double );
4634+
$this->assertSame( PHP_VERSION_ID < 80100 ? '0.0' : '0', $result[0]->col_decimal );
4635+
$this->assertSame( '', $result[0]->col_char );
4636+
$this->assertSame( '', $result[0]->col_varchar );
4637+
$this->assertSame( '', $result[0]->col_text );
4638+
$this->assertSame( '0', $result[0]->col_bool );
4639+
$this->assertSame( '0', $result[0]->col_bit );
4640+
$this->assertSame( '0', $result[0]->col_binary ); // TODO: Should save ''.
4641+
$this->assertSame( '', $result[0]->col_varbinary );
4642+
$this->assertSame( '', $result[0]->col_blob );
4643+
$this->assertSame( '0000-00-00', $result[0]->col_date );
4644+
$this->assertSame( '00:00:00', $result[0]->col_time );
4645+
$this->assertSame( '0000-00-00 00:00:00', $result[0]->col_datetime );
4646+
$this->assertSame( '0000-00-00 00:00:00', $result[0]->col_timestamp );
4647+
$this->assertSame( '0000', $result[0]->col_year );
4648+
$this->assertSame( '', $result[0]->col_enum );
4649+
$this->assertSame( '', $result[0]->col_set );
4650+
$this->assertSame( '', $result[0]->col_json ); // TODO: This should not be allowed.
4651+
}
4652+
45464653
public function testSessionSqlModes(): void {
45474654
// Syntax: "sql_mode" ("@@sql_mode" for SELECT)
45484655
$this->assertQuery( 'SET sql_mode = "ERROR_FOR_DIVISION_BY_ZERO"' );

wp-includes/sqlite-ast/class-wp-sqlite-driver.php

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3018,9 +3018,14 @@ private function translate_insert_or_replace_body_in_non_strict_mode(
30183018
}
30193019
$fragment .= null === $default ? 'NULL' : $this->connection->quote( $default );
30203020
} else {
3021-
// When a column value is included, we can use it without change.
3022-
$position = array_search( $column['COLUMN_NAME'], $insert_list, true );
3023-
$fragment .= $this->quote_sqlite_identifier( $select_list[ $position ] );
3021+
// When a column value is included, we need to apply type casting.
3022+
$position = array_search( $column['COLUMN_NAME'], $insert_list, true );
3023+
$identifier = $this->quote_sqlite_identifier( $select_list[ $position ] );
3024+
$fragment .= sprintf(
3025+
'%s AS %s',
3026+
$this->cast_value_in_non_strict_mode( $column['DATA_TYPE'], $identifier ),
3027+
$identifier
3028+
);
30243029
}
30253030
}
30263031

@@ -3095,6 +3100,9 @@ private function translate_update_list_in_non_strict_mode( string $table_name, W
30953100
$value = $this->translate( $expr );
30963101
}
30973102

3103+
// Apply type casting.
3104+
$value = $this->cast_value_in_non_strict_mode( $data_type, $value );
3105+
30983106
// If the column is NOT NULL, a NULL value resolves to implicit default.
30993107
$implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $data_type ] ?? null;
31003108
if ( ! $is_nullable && null !== $implicit_default ) {
@@ -3110,6 +3118,61 @@ private function translate_update_list_in_non_strict_mode( string $table_name, W
31103118
return $fragment;
31113119
}
31123120

3121+
/**
3122+
* Emulate MySQL type casting for INSERT or UPDATE value in non-strict mode.
3123+
*
3124+
* @param string $mysql_data_type The MySQL data type.
3125+
* @param string $translated_value The original translated value.
3126+
* @return string The translated value.
3127+
*/
3128+
private function cast_value_in_non_strict_mode(
3129+
string $mysql_data_type,
3130+
string $translated_value
3131+
): string {
3132+
$sqlite_data_type = self::DATA_TYPE_STRING_MAP[ $mysql_data_type ];
3133+
3134+
// Get and quote the IMPLICIT DEFAULT value.
3135+
$implicit_default = self::DATA_TYPE_IMPLICIT_DEFAULT_MAP[ $mysql_data_type ] ?? null;
3136+
$quoted_implicit_default = null === $implicit_default
3137+
? 'NULL'
3138+
: $this->connection->quote( $implicit_default );
3139+
3140+
/*
3141+
* In MySQL, when saving a value via INSERT or UPDATE in non-strict mode,
3142+
* 1. MySQL attempts to cast the value to the target column data type.
3143+
* 2. When casting can't be done, MySQL saves an IMPLICIT DEFAULT.
3144+
*/
3145+
switch ( $mysql_data_type ) {
3146+
case 'date':
3147+
case 'time':
3148+
case 'datetime':
3149+
case 'timestamp':
3150+
case 'year':
3151+
if ( 'date' === $mysql_data_type ) {
3152+
$function_call = sprintf( 'DATE(%s)', $translated_value );
3153+
} elseif ( 'time' === $mysql_data_type ) {
3154+
$function_call = sprintf( 'TIME(%s)', $translated_value );
3155+
} elseif ( 'datetime' === $mysql_data_type || 'timestamp' === $mysql_data_type ) {
3156+
$function_call = sprintf( 'DATETIME(%s)', $translated_value );
3157+
} elseif ( 'year' === $mysql_data_type ) {
3158+
$function_call = sprintf( "STRFTIME('%%Y', %s)", $translated_value );
3159+
}
3160+
3161+
// When the function call evaluates to NULL (invalid date/time),
3162+
// we need to fallback to the IMPLICIT DEFAULT value.
3163+
return sprintf(
3164+
'IIF(%s IS NULL, NULL, COALESCE(%s, %s))',
3165+
$translated_value,
3166+
$function_call,
3167+
$quoted_implicit_default
3168+
);
3169+
default:
3170+
// For all other data types, use SQLite-native CAST expression.
3171+
$mysql_data_type = strtolower( $mysql_data_type );
3172+
return sprintf( 'CAST(%s AS %s)', $translated_value, $sqlite_data_type );
3173+
}
3174+
}
3175+
31133176
/**
31143177
* Generate a SQLite CREATE TABLE statement from information schema data.
31153178
*

0 commit comments

Comments
 (0)