@@ -215,6 +215,74 @@ class WP_SQLite_Driver {
215215 '%y ' => '%y ' ,
216216 );
217217
218+ /**
219+ * A map of MySQL data types to implicit default values for non-strict mode.
220+ *
221+ * In MySQL, when STRICT_TRANS_TABLES and STRICT_ALL_TABLES modes are disabled,
222+ * columns get IMPLICIT DEFAULT values that are used under some circumstances.
223+ *
224+ * See:
225+ * https://dev.mysql.com/doc/refman/8.4/en/data-type-defaults.html#data-type-defaults-implicit
226+ */
227+ const DATA_TYPE_IMPLICIT_DEFAULT_MAP = array (
228+ // Numeric data types:
229+ 'bit ' => '0 ' ,
230+ 'bool ' => '0 ' ,
231+ 'boolean ' => '0 ' ,
232+ 'tinyint ' => '0 ' ,
233+ 'smallint ' => '0 ' ,
234+ 'mediumint ' => '0 ' ,
235+ 'int ' => '0 ' ,
236+ 'integer ' => '0 ' ,
237+ 'bigint ' => '0 ' ,
238+ 'float ' => '0 ' ,
239+ 'double ' => '0 ' ,
240+ 'real ' => '0 ' ,
241+ 'decimal ' => '0 ' ,
242+ 'dec ' => '0 ' ,
243+ 'fixed ' => '0 ' ,
244+ 'numeric ' => '0 ' ,
245+
246+ // String data types:
247+ 'char ' => '' ,
248+ 'varchar ' => '' ,
249+ 'nchar ' => '' ,
250+ 'nvarchar ' => '' ,
251+ 'tinytext ' => '' ,
252+ 'text ' => '' ,
253+ 'mediumtext ' => '' ,
254+ 'longtext ' => '' ,
255+ 'enum ' => '' , // TODO: Implement (first enum value).
256+ 'set ' => '' ,
257+ 'json ' => 'null ' , // String value 'null' (valid JSON)
258+
259+ // Date and time data types:
260+ 'date ' => '0000-00-00 ' ,
261+ 'time ' => '00:00:00 ' ,
262+ 'datetime ' => '0000-00-00 00:00:00 ' ,
263+ 'timestamp ' => '0000-00-00 00:00:00 ' ,
264+ 'year ' => '0000 ' ,
265+
266+ // Binary data types:
267+ 'binary ' => '' ,
268+ 'varbinary ' => '' ,
269+ 'tinyblob ' => '' ,
270+ 'blob ' => '' ,
271+ 'mediumblob ' => '' ,
272+ 'longblob ' => '' ,
273+
274+ // Spatial data types (no implicit defaults):
275+ 'geometry ' => null ,
276+ 'point ' => null ,
277+ 'linestring ' => null ,
278+ 'polygon ' => null ,
279+ 'multipoint ' => null ,
280+ 'multilinestring ' => null ,
281+ 'multipolygon ' => null ,
282+ 'geomcollection ' => null ,
283+ 'geometrycollection ' => null ,
284+ );
285+
218286 /**
219287 * The SQLite engine version.
220288 *
@@ -912,6 +980,14 @@ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): vo
912980 if ( $ child instanceof WP_MySQL_Token && WP_MySQL_Lexer::IGNORE_SYMBOL === $ child ->id ) {
913981 // Translate "UPDATE IGNORE" to "UPDATE OR IGNORE".
914982 $ parts [] = 'OR IGNORE ' ;
983+ } elseif (
984+ $ child instanceof WP_Parser_Node &&
985+ ( 'insertFromConstructor ' === $ child ->rule_name || 'insertQueryExpression ' === $ child ->rule_name )
986+ ) {
987+ // TODO: Use this only in non-strict mode.
988+ $ table_ref = $ node ->get_first_child_node ( 'tableRef ' );
989+ $ table_name = $ this ->unquote_sqlite_identifier ( $ this ->translate ( $ table_ref ) );
990+ $ parts [] = $ this ->translate_insert_or_replace_body_in_non_strict_mode ( $ table_name , $ child );
915991 } else {
916992 $ parts [] = $ this ->translate ( $ child );
917993 }
@@ -2592,6 +2668,139 @@ private function translate_show_like_or_where_condition( WP_Parser_Node $like_or
25922668 return '' ;
25932669 }
25942670
2671+ /**
2672+ * Translate INSERT body, emulating MySQL implicit defaults in non-strict mode.
2673+ *
2674+ * In MySQL, the behavior of INSERT and UPDATE statements depends on whether
2675+ * the STRICT_TRANS_TABLES (InnoDB) or STRICT_ALL_TABLES SQL mode is enabled.
2676+ *
2677+ * By default, STRICT_TRANS_TABLES is enabled, which makes the InnoDB table
2678+ * behavior correspond to the natural behavior of SQLite tables. However,
2679+ * some applications, including WordPress, disable strict mode altogether.
2680+ *
2681+ * The strict SQL modes can be set per session, and can be changed at runtime.
2682+ * In SQLite, we can emulate this using the knowledge of the table structure:
2683+ * 1. Explicitly passed INSERT statement values are used without change.
2684+ * 2. Values omitted from the INSERT statement are replaced with the column
2685+ * DEFAULT or an IMPLICIT DEFAULT value based on their data type.
2686+ *
2687+ * Here's a summary of the strict vs. non-strict behaviors in MySQL:
2688+ *
2689+ * When STRICT_TRANS_TABLES or STRICT_ALL_TABLES is enabled:
2690+ * 1. NULL + NO DEFAULT: No value saves NULL, NULL saves NULL, DEFAULT saves NULL.
2691+ * 2. NULL + DEFAULT: No value saves DEFAULT, NULL saves NULL, DEFAULT saves DEFAULT.
2692+ * 3. NOT NULL + NO DEFAULT: No value is rejected, NULL is rejected, DEFAULT is rejected.
2693+ * 4. NOT NULL + DEFAULT: No value saves DEFAULT, NULL is rejected, DEFAULT saves DEFAULT.
2694+ *
2695+ * When STRICT_TRANS_TABLES and STRICT_ALL_TABLES are disabled:
2696+ * 1. NULL + NO DEFAULT: No value saves NULL, NULL saves NULL, DEFAULT saves NULL.
2697+ * 2. NULL + DEFAULT: No value saves DEFAULT, NULL saves NULL, DEFAULT saves DEFAULT.
2698+ * 3. NOT NULL + NO DEFAULT: No value saves IMPLICIT DEFAULT.
2699+ * NULL is rejected on INSERT, but saves IMPLICIT DEFAULT on UPDATE.
2700+ * DEFAULT saves IMPLICIT DEFAULT.
2701+ * 4. NOT NULL + DEFAULT: No value saves DEFAULT.
2702+ * NULL is rejected on INSERT, but saves IMPLICIT DEFAULT on UPDATE.
2703+ * DEFAULT saves DEFAULT.
2704+ *
2705+ * For more information about IMPLICIT DEFAULT values in MySQL, see:
2706+ * https://dev.mysql.com/doc/refman/8.4/en/data-type-defaults.html#data-type-defaults-implicit
2707+ *
2708+ * @param string $table_name The name of the target table.
2709+ * @param WP_Parser_Node $node The "insertQueryExpression" or "insertValues" AST node.
2710+ * @return string The translated INSERT query body.
2711+ */
2712+ private function translate_insert_or_replace_body_in_non_strict_mode (
2713+ string $ table_name ,
2714+ WP_Parser_Node $ node
2715+ ): string {
2716+ // 1. Get column metadata from information schema.
2717+ $ is_temporary = $ this ->information_schema_builder ->temporary_table_exists ( $ table_name );
2718+ $ columns_table = $ this ->information_schema_builder ->get_table_name ( $ is_temporary , 'columns ' );
2719+ $ columns = $ this ->execute_sqlite_query (
2720+ "
2721+ SELECT column_name, is_nullable, column_default, data_type, extra
2722+ FROM $ columns_table
2723+ WHERE table_schema = ?
2724+ AND table_name = ?
2725+ ORDER BY ordinal_position
2726+ " ,
2727+ array ( $ this ->db_name , $ table_name )
2728+ )->fetchAll ( PDO ::FETCH_ASSOC );
2729+
2730+ // 2. Get the list of fields explicitly defined in the INSERT statement.
2731+ $ insert_list = array ();
2732+ $ fields_node = $ node ->get_first_child_node ( 'fields ' );
2733+ if ( $ fields_node ) {
2734+ // This is the optional "INSERT INTO ... (field1, field2, ...)" list.
2735+ foreach ( $ fields_node ->get_child_nodes () as $ field ) {
2736+ $ insert_list [] = $ this ->unquote_sqlite_identifier ( $ this ->translate ( $ field ) );
2737+ }
2738+ } else {
2739+ // When no explicit field list is provided, all columns are required.
2740+ foreach ( array_column ( $ columns , 'COLUMN_NAME ' ) as $ column_name ) {
2741+ $ insert_list [] = $ column_name ;
2742+ }
2743+ }
2744+
2745+ // 3. Get the list of column names returned by VALUES or SELECT clause.
2746+ $ select_list = array ();
2747+ if ( 'insertQueryExpression ' === $ node ->rule_name ) {
2748+ // When inserting from a SELECT query, we don't know the column names.
2749+ // Let's wrap the query with a SELECT (...) LIMIT 0 to get obtain them.
2750+ $ expr = $ node ->get_first_child_node ( 'queryExpressionOrParens ' );
2751+ $ stmt = $ this ->execute_sqlite_query (
2752+ 'SELECT * FROM ( ' . $ this ->translate ( $ expr ) . ') LIMIT 1 '
2753+ );
2754+ $ stmt ->execute ();
2755+
2756+ for ( $ i = 0 ; $ i < $ stmt ->columnCount (); $ i ++ ) {
2757+ $ select_list [] = $ stmt ->getColumnMeta ( $ i )['name ' ];
2758+ }
2759+ } else {
2760+ // When inserting from a VALUES list, SQLite uses "columnN" naming.
2761+ foreach ( array_keys ( $ insert_list ) as $ position ) {
2762+ $ select_list [] = 'column ' . ( $ position + 1 );
2763+ }
2764+ }
2765+
2766+ // 4. Compose a new INSERT field list with all columns from the table.
2767+ $ fragment = '( ' ;
2768+ foreach ( $ columns as $ i => $ column ) {
2769+ $ fragment .= $ i > 0 ? ', ' : '' ;
2770+ $ fragment .= $ this ->quote_sqlite_identifier ( $ column ['COLUMN_NAME ' ] );
2771+ }
2772+ $ fragment .= ') ' ;
2773+
2774+ // 5. Compose a wrapper SELECT statement emulating IMPLICIT DEFAULT values.
2775+ $ fragment .= ' SELECT ' ;
2776+ foreach ( $ columns as $ i => $ column ) {
2777+ $ is_omitted = ! in_array ( $ column ['COLUMN_NAME ' ], $ insert_list , true );
2778+ $ fragment .= $ i > 0 ? ', ' : '' ;
2779+ if ( $ is_omitted ) {
2780+ // When a column value is omitted from the INSERT statement, we
2781+ // need to use the DEFAULT value or the IMPLICIT DEFAULT value.
2782+ $ is_auto_inc = str_contains ( $ column ['EXTRA ' ], 'auto_increment ' );
2783+ $ is_nullable = 'YES ' === $ column ['IS_NULLABLE ' ];
2784+ $ default = $ column ['COLUMN_DEFAULT ' ];
2785+ if ( null === $ default && ! $ is_nullable && ! $ is_auto_inc ) {
2786+ $ default = self ::DATA_TYPE_IMPLICIT_DEFAULT_MAP [ $ column ['DATA_TYPE ' ] ] ?? null ;
2787+ }
2788+ $ fragment .= null === $ default ? 'NULL ' : $ this ->pdo ->quote ( $ default );
2789+ } else {
2790+ // When a colum value is included, we can use it without change.
2791+ $ position = array_search ( $ column ['COLUMN_NAME ' ], $ insert_list , true );
2792+ $ fragment .= $ this ->quote_sqlite_identifier ( $ select_list [ $ position ] );
2793+ }
2794+ }
2795+
2796+ // 6. Wrap the original insert VALUES or SELECT expression in a FROM clause.
2797+ $ values = 'insertFromConstructor ' === $ node ->rule_name
2798+ ? $ node ->get_first_child_node ( 'insertValues ' )
2799+ : $ node ->get_first_child_node ( 'queryExpressionOrParens ' );
2800+ $ fragment .= ' FROM ( ' . $ this ->translate ( $ values ) . ') WHERE true ' ;
2801+
2802+ return $ fragment ;
2803+ }
25952804
25962805 /**
25972806 * Generate a SQLite CREATE TABLE statement from information schema data.
0 commit comments