@@ -1500,12 +1500,6 @@ private function execute_select_statement( WP_Parser_Node $node ): void {
15001500 * @throws WP_SQLite_Driver_Exception When the query execution fails.
15011501 */
15021502 private function execute_insert_or_replace_statement ( WP_Parser_Node $ node ): void {
1503- // Check if strict mode is disabled.
1504- $ is_non_strict_mode = (
1505- ! $ this ->is_sql_mode_active ( 'STRICT_TRANS_TABLES ' )
1506- && ! $ this ->is_sql_mode_active ( 'STRICT_ALL_TABLES ' )
1507- );
1508-
15091503 $ parts = array ();
15101504 foreach ( $ node ->get_children () as $ child ) {
15111505 $ is_token = $ child instanceof WP_MySQL_Token;
@@ -1527,8 +1521,7 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
15271521 // Translate "UPDATE IGNORE" to "UPDATE OR IGNORE".
15281522 $ parts [] = 'OR IGNORE ' ;
15291523 } elseif (
1530- $ is_non_strict_mode
1531- && $ is_node
1524+ $ is_node
15321525 && (
15331526 'insertFromConstructor ' === $ child ->rule_name
15341527 || 'insertQueryExpression ' === $ child ->rule_name
@@ -1537,17 +1530,7 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
15371530 ) {
15381531 $ table_ref = $ node ->get_first_child_node ( 'tableRef ' );
15391532 $ table_name = $ this ->unquote_sqlite_identifier ( $ this ->translate ( $ table_ref ) );
1540- $ parts [] = $ this ->translate_insert_or_replace_body_in_non_strict_mode ( $ table_name , $ child );
1541- } elseif ( $ is_node && 'updateList ' === $ child ->rule_name ) {
1542- // Convert "SET c1 = v1, c2 = v2, ... to "(c1, c2, ...) VALUES (v1, v2, ...)".
1543- $ columns = array ();
1544- $ values = array ();
1545- foreach ( $ child ->get_child_nodes ( 'updateElement ' ) as $ update_element ) {
1546- $ column_ref = $ update_element ->get_first_child_node ( 'columnRef ' );
1547- $ columns [] = $ this ->translate ( $ column_ref );
1548- $ values [] = $ this ->translate ( $ update_element ->get_first_child_node ( 'expr ' ) );
1549- }
1550- $ parts [] = '( ' . implode ( ', ' , $ columns ) . ') VALUES ( ' . implode ( ', ' , $ values ) . ') ' ;
1533+ $ parts [] = $ this ->translate_insert_or_replace_body ( $ table_name , $ child );
15511534 } else {
15521535 $ parts [] = $ this ->translate ( $ child );
15531536 }
@@ -4423,13 +4406,19 @@ private function translate_show_like_or_where_condition( WP_Parser_Node $like_or
44234406 * @param WP_Parser_Node $node The "insertQueryExpression" or "insertValues" AST node.
44244407 * @return string The translated INSERT query body.
44254408 */
4426- private function translate_insert_or_replace_body_in_non_strict_mode (
4409+ private function translate_insert_or_replace_body (
44274410 string $ table_name ,
44284411 WP_Parser_Node $ node
44294412 ): string {
44304413 // This method is always used with the main database.
44314414 $ database = $ this ->get_saved_db_name ( $ this ->main_db_name );
44324415
4416+ // Check if strict mode is enabled.
4417+ $ is_strict_mode = (
4418+ $ this ->is_sql_mode_active ( 'STRICT_TRANS_TABLES ' )
4419+ || $ this ->is_sql_mode_active ( 'STRICT_ALL_TABLES ' )
4420+ );
4421+
44334422 // Get column metadata for the target table from the information schema.
44344423 $ is_temporary = $ this ->information_schema_builder ->temporary_table_exists ( $ table_name );
44354424 $ columns_table = $ this ->information_schema_builder ->get_table_name ( $ is_temporary , 'columns ' );
@@ -4472,16 +4461,24 @@ private function translate_insert_or_replace_body_in_non_strict_mode(
44724461 // Prepare a helper map of columns that are included in the INSERT list.
44734462 $ insert_map = array_combine ( $ insert_list , $ insert_list );
44744463
4475- // Filter out omitted columns that will get a value from the SQLite engine.
4476- // That is, nullable columns, columns with defaults, and generated columns.
4464+ /*
4465+ * Filter out columns that were omitted in the INSERT list:
4466+ * 1. In strict mode, filter out all omitted columns.
4467+ * 2. In non-strict mode, filter out omitted columns that will get a
4468+ * value from the SQLite engine. That is, nullable columns, columns
4469+ * with defaults, and generated columns.
4470+ */
44774471 $ columns = array_values (
44784472 array_filter (
44794473 $ columns ,
4480- function ( $ column ) use ( $ insert_map ) {
4474+ function ( $ column ) use ( $ is_strict_mode , $ insert_map ) {
44814475 $ is_omitted = ! isset ( $ insert_map [ $ column ['COLUMN_NAME ' ] ] );
44824476 if ( ! $ is_omitted ) {
44834477 return true ;
44844478 }
4479+ if ( $ is_strict_mode ) {
4480+ return false ;
4481+ }
44854482 $ is_nullable = 'YES ' === $ column ['IS_NULLABLE ' ];
44864483 $ has_default = $ column ['COLUMN_DEFAULT ' ];
44874484 $ is_generated = str_contains ( $ column ['EXTRA ' ], 'auto_increment ' );
@@ -4526,13 +4523,18 @@ function ( $column ) use ( $insert_map ) {
45264523 }
45274524 $ fragment .= ') ' ;
45284525
4529- // Compose a wrapper SELECT statement emulating IMPLICIT DEFAULT values.
4526+ // Compose a wrapper SELECT statement emulating MySQL-like type casting,
4527+ // and, in non-strict mode, IMPLICIT DEFAULT values for omitted columns.
45304528 $ fragment .= ' SELECT ' ;
45314529 foreach ( $ columns as $ i => $ column ) {
45324530 $ is_omitted = ! isset ( $ insert_map [ $ column ['COLUMN_NAME ' ] ] );
45334531 $ fragment .= $ i > 0 ? ', ' : '' ;
45344532 if ( $ is_omitted ) {
45354533 /*
4534+ * This path only applies to non-strict mode. In strict mode,
4535+ * omitted columns get no IMPLICIT DEFAULT values, and they were
4536+ * previously filtered out from the columns list.
4537+ *
45364538 * When a column is omitted from the INSERT list, we need to use
45374539 * an IMPLICIT DEFAULT value. Note that at this point, all omitted
45384540 * columns that will not get an implicit default are filtered out.
@@ -4546,7 +4548,7 @@ function ( $column ) use ( $insert_map ) {
45464548 $ identifier = $ this ->quote_sqlite_identifier ( $ select_list [ $ position ] );
45474549 $ fragment .= sprintf (
45484550 '%s AS %s ' ,
4549- $ this ->cast_value_in_non_strict_mode ( $ column ['DATA_TYPE ' ], $ identifier ),
4551+ $ this ->cast_value_for_insert_or_update ( $ column ['DATA_TYPE ' ], $ identifier ),
45504552 $ identifier
45514553 );
45524554 }
@@ -4640,7 +4642,7 @@ private function translate_update_list_in_non_strict_mode( string $table_name, W
46404642 }
46414643
46424644 // Apply type casting.
4643- $ value = $ this ->cast_value_in_non_strict_mode ( $ data_type , $ value );
4645+ $ value = $ this ->cast_value_for_insert_or_update ( $ data_type , $ value );
46444646
46454647 // If the column is NOT NULL, a NULL value resolves to implicit default.
46464648 $ implicit_default = self ::DATA_TYPE_IMPLICIT_DEFAULT_MAP [ $ data_type ] ?? null ;
@@ -4944,23 +4946,27 @@ private function create_table_reference_map( WP_Parser_Node $node ): array {
49444946 }
49454947
49464948 /**
4947- * Emulate MySQL type casting for INSERT or UPDATE value in non-strict mode .
4949+ * Emulate MySQL type casting for INSERT or UPDATE values .
49484950 *
49494951 * @param string $mysql_data_type The MySQL data type.
49504952 * @param string $translated_value The original translated value.
49514953 * @return string The translated value.
49524954 */
4953- private function cast_value_in_non_strict_mode (
4955+ private function cast_value_for_insert_or_update (
49544956 string $ mysql_data_type ,
49554957 string $ translated_value
49564958 ): string {
4957- $ sqlite_data_type = self ::DATA_TYPE_STRING_MAP [ $ mysql_data_type ];
4959+ // TODO: This is also a good place to implement checks for maximum column
4960+ // lengths with truncating or bailing out depending on the SQL mode.
4961+
4962+ // Check if strict mode is enabled.
4963+ $ is_strict_mode = (
4964+ $ this ->is_sql_mode_active ( 'STRICT_TRANS_TABLES ' )
4965+ || $ this ->is_sql_mode_active ( 'STRICT_ALL_TABLES ' )
4966+ );
49584967
4959- // Get and quote the IMPLICIT DEFAULT value.
4960- $ implicit_default = self ::DATA_TYPE_IMPLICIT_DEFAULT_MAP [ $ mysql_data_type ] ?? null ;
4961- $ quoted_implicit_default = null === $ implicit_default
4962- ? 'NULL '
4963- : $ this ->connection ->quote ( $ implicit_default );
4968+ $ mysql_data_type = strtolower ( $ mysql_data_type );
4969+ $ sqlite_data_type = self ::DATA_TYPE_STRING_MAP [ $ mysql_data_type ];
49644970
49654971 /*
49664972 * In MySQL, when saving a value via INSERT or UPDATE in non-strict mode,
@@ -4997,18 +5003,37 @@ private function cast_value_in_non_strict_mode(
49975003 $ function_call = sprintf ( "STRFTIME('%%Y', %s) " , $ translated_value );
49985004 }
49995005
5000- // When the function call evaluates to NULL (invalid date/time),
5001- // we need to fallback to the IMPLICIT DEFAULT value.
5006+ // In strict mode, invalid date/time values are rejected.
5007+ // In non-strict mode, they get an IMPLICIT DEFAULT value.
5008+ if ( $ is_strict_mode ) {
5009+ $ fallback = sprintf ( "THROW('Incorrect datetime value: ''' || %s || '''') " , $ translated_value );
5010+ } else {
5011+ $ implicit_default = self ::DATA_TYPE_IMPLICIT_DEFAULT_MAP [ $ mysql_data_type ] ?? null ;
5012+ $ fallback = null === $ implicit_default
5013+ ? 'NULL '
5014+ : $ this ->connection ->quote ( $ implicit_default );
5015+ }
50025016 return sprintf (
50035017 'IIF(%s IS NULL, NULL, COALESCE(%s, %s)) ' ,
50045018 $ translated_value ,
50055019 $ function_call ,
5006- $ quoted_implicit_default
5020+ $ fallback
50075021 );
50085022 default :
5009- // For all other data types, use SQLite-native CAST expression.
5010- $ mysql_data_type = strtolower ( $ mysql_data_type );
5011- return sprintf ( 'CAST(%s AS %s) ' , $ translated_value , $ sqlite_data_type );
5023+ /*
5024+ * For all other data types, cast to the SQLite types as follows:
5025+ * 1. In strict mode, cast only values for TEXT and BLOB columns.
5026+ * Numeric types accept string notation in SQLite as well.
5027+ * 2. In non-strict mode, cast all values.
5028+ *
5029+ * TODO: While close to MySQL behavior, this does't exactly match
5030+ * all special cases. We may improve this further to accept
5031+ * BLOBs for numeric types, and other special behaviors.
5032+ */
5033+ if ( ! $ is_strict_mode || 'TEXT ' === $ sqlite_data_type || 'BLOB ' === $ sqlite_data_type ) {
5034+ return sprintf ( 'CAST(%s AS %s) ' , $ translated_value , $ sqlite_data_type );
5035+ }
5036+ return $ translated_value ;
50125037 }
50135038 }
50145039
0 commit comments