Skip to content

Commit 34dc1cd

Browse files
committed
Rewrite VALUES as SELECT-AS-columnN in insertFromConstructor
Turso rejects the outer reference to columnN in INSERT ... SELECT ... FROM (VALUES (...)) subqueries even with an explicit UNION-ALL header prefix. Work around this by emitting the VALUES list as a UNION ALL of single-row SELECTs with explicit `expr AS columnN` aliases — Turso handles these correctly. Update Translation_Tests INSERT/REPLACE expectations to match the new SQL form via regex patch at CI time. Targets 6 columnN runtime errors (testNonStrictModeTypeCasting, testColumnInfoForDateAndTimeDataTypes, testCastValuesOnInsert and variants).
1 parent e3122aa commit 34dc1cd

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

.github/workflows/phpunit-tests-turso.yml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)