@@ -1407,134 +1407,6 @@ jobs:
14071407 open(path, 'w').write(src)
14081408 print('patched UPDATE multi-table to rowid-IN subquery')
14091409
1410- # 17. For single-row INSERT ... VALUES without DEFAULT tokens,
1411- # inline each value expression directly into the wrapping
1412- # SELECT instead of using a FROM (VALUES ...) subquery with
1413- # columnN aliases. Turso can't resolve the outer columnN
1414- # reference through an INSERT...SELECT...FROM subquery even
1415- # when the aliases are explicit. The inline form bypasses
1416- # the columnN indirection entirely and is equivalent SQL on
1417- # real SQLite.
1418- src = open(path).read()
1419-
1420- # Step 1: intercept select_list initialization for single-row
1421- # VALUES without DEFAULT, set select_list to pre-translated
1422- # expressions (wrapped in parens so CAST composes correctly),
1423- # and set $inline_values = true.
1424- old_init = (
1425- "\t\t} else {\n"
1426- "\t\t\t// When inserting from a VALUES list, SQLite uses a \"columnN\" naming.\n"
1427- "\t\t\t// This also applies to the SET syntax, which is converted to VALUES.\n"
1428- "\t\t\tforeach ( array_keys( $insert_list ) as $position ) {\n"
1429- "\t\t\t\t$select_list[] = 'column' . ( $position + 1 );\n"
1430- "\t\t\t}\n"
1431- "\t\t}"
1432- )
1433- new_init = (
1434- "\t\t} else {\n"
1435- "\t\t\t$inline_values = false;\n"
1436- "\t\t\tif ( 'insertFromConstructor' === $node->rule_name ) {\n"
1437- "\t\t\t\t$insert_values = $node->get_first_child_node( 'insertValues' );\n"
1438- "\t\t\t\t$value_list = $insert_values->get_first_child_node( 'valueList' );\n"
1439- "\t\t\t\t$values_nodes = $value_list->get_child_nodes( 'values' );\n"
1440- "\t\t\t\t$has_default = count( $values_nodes ) === 1\n"
1441- "\t\t\t\t\t&& count( $values_nodes[0]->get_child_nodes( WP_MySQL_Lexer::DEFAULT_SYMBOL ) ) > 0;\n"
1442- "\t\t\t\tif ( count( $values_nodes ) === 1 && ! $has_default ) {\n"
1443- "\t\t\t\t\t$inline_values = true;\n"
1444- "\t\t\t\t\tforeach ( $values_nodes[0]->get_child_nodes( 'expr' ) as $expr_node ) {\n"
1445- "\t\t\t\t\t\t$select_list[] = '(' . $this->translate( $expr_node ) . ')';\n"
1446- "\t\t\t\t\t}\n"
1447- "\t\t\t\t}\n"
1448- "\t\t\t}\n"
1449- "\t\t\tif ( ! $inline_values ) {\n"
1450- "\t\t\t\t// When inserting from a VALUES list, SQLite uses a \"columnN\" naming.\n"
1451- "\t\t\t\t// This also applies to the SET syntax, which is converted to VALUES.\n"
1452- "\t\t\t\tforeach ( array_keys( $insert_list ) as $position ) {\n"
1453- "\t\t\t\t\t$select_list[] = 'column' . ( $position + 1 );\n"
1454- "\t\t\t\t}\n"
1455- "\t\t\t}\n"
1456- "\t\t}"
1457- )
1458- assert old_init in src, 'select_list else-branch not found'
1459- src = src.replace(old_init, new_init, 1)
1460-
1461- # Step 2: CAST loop — use raw expression when $inline_values.
1462- old_cast = (
1463- "\t\t\t\t$position = array_search( $column['COLUMN_NAME'], $insert_list, true );\n"
1464- "\t\t\t\t$identifier = $this->quote_sqlite_identifier( $select_list[ $position ] );\n"
1465- "\t\t\t\t$value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier );\n"
1466- )
1467- new_cast = (
1468- "\t\t\t\t$position = array_search( $column['COLUMN_NAME'], $insert_list, true );\n"
1469- "\t\t\t\t$identifier = ( isset( $inline_values ) && $inline_values )\n"
1470- "\t\t\t\t\t? $select_list[ $position ]\n"
1471- "\t\t\t\t\t: $this->quote_sqlite_identifier( $select_list[ $position ] );\n"
1472- "\t\t\t\t$value = $this->cast_value_for_saving( $column['DATA_TYPE'], $identifier );\n"
1473- )
1474- assert old_cast in src, 'CAST identifier lookup not found'
1475- src = src.replace(old_cast, new_cast, 1)
1476-
1477- # Step 3: FROM wrapping — skip for inline case.
1478- old_from_block = (
1479- "\t\t// Wrap the original insert VALUES, SELECT, or SET list in a FROM clause.\n"
1480- "\t\tif ( 'insertFromConstructor' === $node->rule_name ) {\n"
1481- "\t\t\t// VALUES (...)\n"
1482- "\t\t\t$insert_values = $node->get_first_child_node( 'insertValues' );\n"
1483- "\t\t\t$from = $this->translate( $insert_values );\n"
1484- )
1485- new_from_block = (
1486- "\t\t// Wrap the original insert VALUES, SELECT, or SET list in a FROM clause.\n"
1487- "\t\tif ( isset( $inline_values ) && $inline_values ) {\n"
1488- "\t\t\t$from = null;\n"
1489- "\t\t} elseif ( 'insertFromConstructor' === $node->rule_name ) {\n"
1490- "\t\t\t// VALUES (...)\n"
1491- "\t\t\t$insert_values = $node->get_first_child_node( 'insertValues' );\n"
1492- "\t\t\t$from = $this->translate( $insert_values );\n"
1493- )
1494- assert old_from_block in src, 'FROM wrapping block not found'
1495- src = src.replace(old_from_block, new_from_block, 1)
1496-
1497- # Step 4: final emission — omit FROM when $from is null.
1498- old_emit = "\t\t$fragment .= ' FROM (' . $from . ') WHERE true';"
1499- new_emit = (
1500- "\t\tif ( null === $from ) {\n"
1501- "\t\t\t$fragment .= ' WHERE true';\n"
1502- "\t\t} else {\n"
1503- "\t\t\t$fragment .= ' FROM (' . $from . ') WHERE true';\n"
1504- "\t\t}"
1505- )
1506- assert old_emit in src, 'final FROM emission not found'
1507- src = src.replace(old_emit, new_emit, 1)
1508- open(path, 'w').write(src)
1509- print('patched single-row VALUES to inline into outer SELECT (no FROM subquery)')
1510-
1511- # Update Translation_Tests INSERT/REPLACE expectations for the inline form.
1512- import re
1513- path_tt = 'tests/WP_SQLite_Driver_Translation_Tests.php'
1514- src_tt = open(path_tt).read()
1515- srow_pat = re.compile(
1516- r"SELECT (?P<cols>`column\d+`(?:, `column\d+`)*) FROM \(VALUES \( (?P<vals>[^()]+) \)\) WHERE true"
1517- )
1518- def rewrite_single_row(m):
1519- cols = re.findall(r"`column\d+`", m.group('cols'))
1520- vals = [v.strip() for v in m.group('vals').split(',')]
1521- if len(cols) != len(vals):
1522- return m.group(0)
1523- return "SELECT " + ", ".join(f"({v})" for v in vals) + " WHERE true"
1524- src_tt2, n1 = srow_pat.subn(rewrite_single_row, src_tt)
1525- scast_pat = re.compile(
1526- r"SELECT (?P<casts>CAST\(`column\d+` AS \w+\)(?:, CAST\(`column\d+` AS \w+\))*) FROM \(VALUES \( (?P<vals>[^()]+) \)\) WHERE true"
1527- )
1528- def rewrite_cast_row(m):
1529- types = re.findall(r"CAST\(`column\d+` AS (\w+)\)", m.group('casts'))
1530- vals = [v.strip() for v in m.group('vals').split(',')]
1531- if len(types) != len(vals):
1532- return m.group(0)
1533- return "SELECT " + ", ".join(f"CAST(({v}) AS {t})" for v, t in zip(vals, types)) + " WHERE true"
1534- src_tt2, n2 = scast_pat.subn(rewrite_cast_row, src_tt2)
1535- open(path_tt, 'w').write(src_tt2)
1536- print(f'patched Translation_Tests VALUES expectations: {n1} plain + {n2} CAST')
1537-
15381410 # 14. Update Translation_Tests::testHexadecimalLiterals to match
15391411 # the hex-literal alias force patch (which needs to stay so
15401412 # Turso doesn't mangle x'417a' into 17a' at runtime).
0 commit comments