@@ -1057,6 +1057,109 @@ jobs:
10571057 open(path, 'w').write(src.replace(old, new, 1))
10581058 print('patched CHECK TABLE missing-table check')
10591059
1060+ # 12. Turso doesn't propagate VALUES row-column aliases (column1,
1061+ # column2, ...) through `INSERT ... SELECT ... FROM (VALUES ...)`
1062+ # subqueries — outer reference to `column1` fails with
1063+ # "no such column". Replace the VALUES subquery with a
1064+ # UNION ALL of SELECTs carrying explicit `expr AS columnN`
1065+ # aliases (single-row case collapses to a single SELECT).
1066+ path = 'src/sqlite/class-wp-pdo-mysql-on-sqlite.php'
1067+ src = open(path).read()
1068+ old = (
1069+ "\t\tif ( 'insertFromConstructor' === $node->rule_name ) {\n"
1070+ "\t\t\t// VALUES (...)\n"
1071+ "\t\t\t$insert_values = $node->get_first_child_node( 'insertValues' );\n"
1072+ "\t\t\t$from = $this->translate( $insert_values );\n"
1073+ "\n"
1074+ "\t\t\t/**\n"
1075+ "\t\t\t * The automatic \"columnN\" naming for VALUES lists is supported only\n"
1076+ "\t\t\t * from SQLite 3.33.0. For older versions, we need to emulate it by\n"
1077+ "\t\t\t * prepending a dummy VALUES list header via the UNION ALL operator:\n"
1078+ "\t\t\t *\n"
1079+ "\t\t\t * SELECT\n"
1080+ "\t\t\t * NULL AS `column1`, NULL AS `column2`, ... WHERE FALSE\n"
1081+ "\t\t\t * UNION ALL\n"
1082+ "\t\t\t * VALUES (value1, value2, ...)\n"
1083+ "\t\t\t */\n"
1084+ "\t\t\t$is_values_naming_supported = version_compare( $this->get_sqlite_version(), '3.33.0', '>=' );\n"
1085+ "\t\t\tif ( ! $is_values_naming_supported ) {\n"
1086+ "\t\t\t\t$values_list = $insert_values->get_first_child_node( 'valueList' );\n"
1087+ "\t\t\t\t$values = $values_list->get_first_child_node( 'values' );\n"
1088+ "\t\t\t\t$value_count = (\n"
1089+ "\t\t\t\t\tcount( $values->get_child_nodes( 'expr' ) )\n"
1090+ "\t\t\t\t\t+ count( $values->get_child_nodes( WP_MySQL_Lexer::DEFAULT_SYMBOL ) )\n"
1091+ "\t\t\t\t);\n"
1092+ "\n"
1093+ "\t\t\t\t$columns_list = '';\n"
1094+ "\t\t\t\tfor ( $i = 1; $i <= $value_count; $i++ ) {\n"
1095+ "\t\t\t\t\t$columns_list .= $i > 1 ? ', ' : '';\n"
1096+ "\t\t\t\t\t$columns_list .= 'NULL AS ' . $this->quote_sqlite_identifier( 'column' . $i );\n"
1097+ "\t\t\t\t}\n"
1098+ "\t\t\t\t$from = 'SELECT ' . $columns_list . ' WHERE FALSE UNION ALL ' . $from;\n"
1099+ "\t\t\t}\n"
1100+ "\t\t}"
1101+ )
1102+ new = (
1103+ "\t\tif ( 'insertFromConstructor' === $node->rule_name ) {\n"
1104+ "\t\t\t// Build a UNION ALL of SELECT rows with explicit `expr AS columnN`\n"
1105+ "\t\t\t// aliases. Turso doesn't propagate implicit VALUES column names\n"
1106+ "\t\t\t// through INSERT...SELECT subqueries.\n"
1107+ "\t\t\t$insert_values = $node->get_first_child_node( 'insertValues' );\n"
1108+ "\t\t\t$value_list = $insert_values->get_first_child_node( 'valueList' );\n"
1109+ "\t\t\t$values_nodes = $value_list->get_child_nodes( 'values' );\n"
1110+ "\t\t\t$select_rows = array();\n"
1111+ "\t\t\tforeach ( $values_nodes as $values_node ) {\n"
1112+ "\t\t\t\t$position = 0;\n"
1113+ "\t\t\t\t$row_parts = array();\n"
1114+ "\t\t\t\tforeach ( $values_node->get_children() as $child ) {\n"
1115+ "\t\t\t\t\tif ( $child instanceof WP_Parser_Node && 'expr' === $child->rule_name ) {\n"
1116+ "\t\t\t\t\t\t$expr_sql = $this->translate( $child );\n"
1117+ "\t\t\t\t\t\t$row_parts[] = $expr_sql . ' AS ' . $this->quote_sqlite_identifier( 'column' . ( $position + 1 ) );\n"
1118+ "\t\t\t\t\t\t$position++;\n"
1119+ "\t\t\t\t\t} elseif ( $child instanceof WP_Parser_Token && WP_MySQL_Lexer::DEFAULT_SYMBOL === $child->id ) {\n"
1120+ "\t\t\t\t\t\t$row_parts[] = 'NULL AS ' . $this->quote_sqlite_identifier( 'column' . ( $position + 1 ) );\n"
1121+ "\t\t\t\t\t\t$position++;\n"
1122+ "\t\t\t\t\t}\n"
1123+ "\t\t\t\t}\n"
1124+ "\t\t\t\t$select_rows[] = 'SELECT ' . implode( ', ', $row_parts );\n"
1125+ "\t\t\t}\n"
1126+ "\t\t\t$from = implode( ' UNION ALL ', $select_rows );\n"
1127+ "\t\t}"
1128+ )
1129+ assert old in src, 'insertFromConstructor VALUES block not found'
1130+ open(path, 'w').write(src.replace(old, new, 1))
1131+ print('patched insertFromConstructor to emit SELECT-with-aliases form')
1132+
1133+ # 13. Update Translation_Tests expectations to match the new
1134+ # SELECT-AS-columnN form emitted by insertFromConstructor.
1135+ path = 'tests/WP_SQLite_Driver_Translation_Tests.php'
1136+ src = open(path).read()
1137+ # Match `FROM (VALUES ( v1 [, v2 ...] ) [, ( ... ) ...])`
1138+ # where values are simple literals (no nested parens or strings
1139+ # containing parens — good enough for all hits in this file).
1140+ values_pattern = re.compile(
1141+ r"\(VALUES "
1142+ r"\( ([^()]+) \)"
1143+ r"((?: , \( [^()]+ \))*)"
1144+ r"\)"
1145+ )
1146+ def rewrite_values(m):
1147+ first_row = m.group(1).strip()
1148+ extra_rows_text = m.group(2)
1149+ rows = [first_row]
1150+ for row_match in re.finditer(r"\( ([^()]+) \)", extra_rows_text):
1151+ rows.append(row_match.group(1).strip())
1152+ selects = []
1153+ for row in rows:
1154+ values = [v.strip() for v in row.split(',')]
1155+ parts = [f"{v} AS `column{i+1}`" for i, v in enumerate(values)]
1156+ selects.append("SELECT " + ", ".join(parts))
1157+ return "(" + " UNION ALL ".join(selects) + ")"
1158+ new_src, n = values_pattern.subn(rewrite_values, src)
1159+ assert n >= 10, f'expected 10+ VALUES expectations, matched {n}'
1160+ open(path, 'w').write(new_src)
1161+ print(f'patched {n} Translation_Tests VALUES expectations to SELECT form')
1162+
10601163 # 11. 6 Translation_Tests assert on the exact SQL emitted by
10611164 # sync_column_key_info. Our EXISTS rewrite (needed because
10621165 # Turso doesn't implement SQLite's "bare column alongside
0 commit comments