Skip to content

Commit 909fd69

Browse files
committed
Inline single-row INSERT VALUES into outer SELECT (correct order)
Previous attempt had an ordering bug where select_list wasn't populated until after the CAST loop had already used it. Reorganize so the inline detection and expression pre-translation happen inside the initial select_list-population step (before the CAST loop). Adds an $inline_values flag threaded through CAST identifier lookup (raw expression vs identifier quoting) and the FROM wrap step (null $from means emit "WHERE true" without FROM subquery). Targets 6 columnN runtime errors (testNonStrictModeTypeCasting, testColumnInfoForDateAndTimeDataTypes, testCastValues* variants).
1 parent 976d86f commit 909fd69

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,134 @@ jobs:
13471347
open(path, 'w').write(src)
13481348
print('patched UPDATE multi-table to rowid-IN subquery')
13491349
1350+
# 17. For single-row INSERT ... VALUES without DEFAULT tokens,
1351+
# inline each value expression directly into the wrapping
1352+
# SELECT instead of using a FROM (VALUES ...) subquery with
1353+
# columnN aliases. Turso can't resolve the outer columnN
1354+
# reference through an INSERT...SELECT...FROM subquery even
1355+
# when the aliases are explicit. The inline form bypasses
1356+
# the columnN indirection entirely and is equivalent SQL on
1357+
# real SQLite.
1358+
src = open(path).read()
1359+
1360+
# Step 1: intercept select_list initialization for single-row
1361+
# VALUES without DEFAULT, set select_list to pre-translated
1362+
# expressions (wrapped in parens so CAST composes correctly),
1363+
# and set $inline_values = true.
1364+
old_init = (
1365+
"\t\t} else {\n"
1366+
"\t\t\t// When inserting from a VALUES list, SQLite uses a \"columnN\" naming.\n"
1367+
"\t\t\t// This also applies to the SET syntax, which is converted to VALUES.\n"
1368+
"\t\t\tforeach ( array_keys( $insert_list ) as $position ) {\n"
1369+
"\t\t\t\t$select_list[] = 'column' . ( $position + 1 );\n"
1370+
"\t\t\t}\n"
1371+
"\t\t}"
1372+
)
1373+
new_init = (
1374+
"\t\t} else {\n"
1375+
"\t\t\t$inline_values = false;\n"
1376+
"\t\t\tif ( 'insertFromConstructor' === $node->rule_name ) {\n"
1377+
"\t\t\t\t$insert_values = $node->get_first_child_node( 'insertValues' );\n"
1378+
"\t\t\t\t$value_list = $insert_values->get_first_child_node( 'valueList' );\n"
1379+
"\t\t\t\t$values_nodes = $value_list->get_child_nodes( 'values' );\n"
1380+
"\t\t\t\t$has_default = count( $values_nodes ) === 1\n"
1381+
"\t\t\t\t\t&& count( $values_nodes[0]->get_child_nodes( WP_MySQL_Lexer::DEFAULT_SYMBOL ) ) > 0;\n"
1382+
"\t\t\t\tif ( count( $values_nodes ) === 1 && ! $has_default ) {\n"
1383+
"\t\t\t\t\t$inline_values = true;\n"
1384+
"\t\t\t\t\tforeach ( $values_nodes[0]->get_child_nodes( 'expr' ) as $expr_node ) {\n"
1385+
"\t\t\t\t\t\t$select_list[] = '(' . $this->translate( $expr_node ) . ')';\n"
1386+
"\t\t\t\t\t}\n"
1387+
"\t\t\t\t}\n"
1388+
"\t\t\t}\n"
1389+
"\t\t\tif ( ! $inline_values ) {\n"
1390+
"\t\t\t\t// When inserting from a VALUES list, SQLite uses a \"columnN\" naming.\n"
1391+
"\t\t\t\t// This also applies to the SET syntax, which is converted to VALUES.\n"
1392+
"\t\t\t\tforeach ( array_keys( $insert_list ) as $position ) {\n"
1393+
"\t\t\t\t\t$select_list[] = 'column' . ( $position + 1 );\n"
1394+
"\t\t\t\t}\n"
1395+
"\t\t\t}\n"
1396+
"\t\t}"
1397+
)
1398+
assert old_init in src, 'select_list else-branch not found'
1399+
src = src.replace(old_init, new_init, 1)
1400+
1401+
# Step 2: CAST loop — use raw expression when $inline_values.
1402+
old_cast = (
1403+
"\t\t\t\t$position = array_search( $column['COLUMN_NAME'], $insert_list, true );\n"
1404+
"\t\t\t\t$identifier = $this->quote_sqlite_identifier( $select_list[ $position ] );\n"
1405+
"\t\t\t\t$value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier );\n"
1406+
)
1407+
new_cast = (
1408+
"\t\t\t\t$position = array_search( $column['COLUMN_NAME'], $insert_list, true );\n"
1409+
"\t\t\t\t$identifier = ( isset( $inline_values ) && $inline_values )\n"
1410+
"\t\t\t\t\t? $select_list[ $position ]\n"
1411+
"\t\t\t\t\t: $this->quote_sqlite_identifier( $select_list[ $position ] );\n"
1412+
"\t\t\t\t$value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier );\n"
1413+
)
1414+
assert old_cast in src, 'CAST identifier lookup not found'
1415+
src = src.replace(old_cast, new_cast, 1)
1416+
1417+
# Step 3: FROM wrapping — skip for inline case.
1418+
old_from_block = (
1419+
"\t\t// Wrap the original insert VALUES, SELECT, or SET list in a FROM clause.\n"
1420+
"\t\tif ( 'insertFromConstructor' === $node->rule_name ) {\n"
1421+
"\t\t\t// VALUES (...)\n"
1422+
"\t\t\t$insert_values = $node->get_first_child_node( 'insertValues' );\n"
1423+
"\t\t\t$from = $this->translate( $insert_values );\n"
1424+
)
1425+
new_from_block = (
1426+
"\t\t// Wrap the original insert VALUES, SELECT, or SET list in a FROM clause.\n"
1427+
"\t\tif ( isset( $inline_values ) && $inline_values ) {\n"
1428+
"\t\t\t$from = null;\n"
1429+
"\t\t} elseif ( 'insertFromConstructor' === $node->rule_name ) {\n"
1430+
"\t\t\t// VALUES (...)\n"
1431+
"\t\t\t$insert_values = $node->get_first_child_node( 'insertValues' );\n"
1432+
"\t\t\t$from = $this->translate( $insert_values );\n"
1433+
)
1434+
assert old_from_block in src, 'FROM wrapping block not found'
1435+
src = src.replace(old_from_block, new_from_block, 1)
1436+
1437+
# Step 4: final emission — omit FROM when $from is null.
1438+
old_emit = "\t\t$fragment .= ' FROM (' . $from . ') WHERE true';"
1439+
new_emit = (
1440+
"\t\tif ( null === $from ) {\n"
1441+
"\t\t\t$fragment .= ' WHERE true';\n"
1442+
"\t\t} else {\n"
1443+
"\t\t\t$fragment .= ' FROM (' . $from . ') WHERE true';\n"
1444+
"\t\t}"
1445+
)
1446+
assert old_emit in src, 'final FROM emission not found'
1447+
src = src.replace(old_emit, new_emit, 1)
1448+
open(path, 'w').write(src)
1449+
print('patched single-row VALUES to inline into outer SELECT (no FROM subquery)')
1450+
1451+
# Update Translation_Tests INSERT/REPLACE expectations for the inline form.
1452+
import re
1453+
path_tt = 'tests/WP_SQLite_Driver_Translation_Tests.php'
1454+
src_tt = open(path_tt).read()
1455+
srow_pat = re.compile(
1456+
r"SELECT (?P<cols>`column\d+`(?:, `column\d+`)*) FROM \(VALUES \( (?P<vals>[^()]+) \)\) WHERE true"
1457+
)
1458+
def rewrite_single_row(m):
1459+
cols = re.findall(r"`column\d+`", m.group('cols'))
1460+
vals = [v.strip() for v in m.group('vals').split(',')]
1461+
if len(cols) != len(vals):
1462+
return m.group(0)
1463+
return "SELECT " + ", ".join(f"({v})" for v in vals) + " WHERE true"
1464+
src_tt2, n1 = srow_pat.subn(rewrite_single_row, src_tt)
1465+
scast_pat = re.compile(
1466+
r"SELECT (?P<casts>CAST\(`column\d+` AS \w+\)(?:, CAST\(`column\d+` AS \w+\))*) FROM \(VALUES \( (?P<vals>[^()]+) \)\) WHERE true"
1467+
)
1468+
def rewrite_cast_row(m):
1469+
types = re.findall(r"CAST\(`column\d+` AS (\w+)\)", m.group('casts'))
1470+
vals = [v.strip() for v in m.group('vals').split(',')]
1471+
if len(types) != len(vals):
1472+
return m.group(0)
1473+
return "SELECT " + ", ".join(f"CAST(({v}) AS {t})" for v, t in zip(vals, types)) + " WHERE true"
1474+
src_tt2, n2 = scast_pat.subn(rewrite_cast_row, src_tt2)
1475+
open(path_tt, 'w').write(src_tt2)
1476+
print(f'patched Translation_Tests VALUES expectations: {n1} plain + {n2} CAST')
1477+
13501478
# 14. Update Translation_Tests::testHexadecimalLiterals to match
13511479
# the hex-literal alias force patch (which needs to stay so
13521480
# Turso doesn't mangle x'417a' into 17a' at runtime).

0 commit comments

Comments
 (0)