@@ -368,6 +368,42 @@ jobs:
368368 print('patched function-name case (register + resolve)')
369369 PY_FN_CASE
370370
371+ # Turso's CollationSeq is a closed enum of three built-in collations
372+ # (Binary/NoCase/Rtrim). Driver-emitted SQL references MySQL
373+ # collations like `utf8mb4_bin` (byte-compare) and `utf8mb4_0900_ai_ci`
374+ # (case-insensitive). Map these to the closest built-in at lookup
375+ # time before Turso's EnumString rejects the name.
376+ python3 - <<'PY_COLLATION'
377+ p = 'core/translate/collate.rs'
378+ s = open(p).read()
379+ old = (
380+ " pub fn new(collation: &str) -> crate::Result<Self> {\n"
381+ " CollationSeq::from_str(collation).map_err(|_| {\n"
382+ " crate::LimboError::ParseError(format!(\"no such collation sequence: {collation}\"))\n"
383+ " })\n"
384+ " }\n"
385+ )
386+ new = (
387+ " pub fn new(collation: &str) -> crate::Result<Self> {\n"
388+ " // Alias common MySQL collation names to the nearest\n"
389+ " // Turso built-in before strum rejects them.\n"
390+ " let lower = collation.to_ascii_lowercase();\n"
391+ " let alias = match lower.as_str() {\n"
392+ " \"utf8mb4_bin\" | \"utf8_bin\" | \"ascii_bin\" | \"latin1_bin\" => \"Binary\",\n"
393+ " \"utf8mb4_0900_ai_ci\" | \"utf8mb4_general_ci\" | \"utf8_general_ci\"\n"
394+ " | \"latin1_general_ci\" | \"latin1_swedish_ci\" => \"NoCase\",\n"
395+ " _ => collation,\n"
396+ " };\n"
397+ " CollationSeq::from_str(alias).map_err(|_| {\n"
398+ " crate::LimboError::ParseError(format!(\"no such collation sequence: {collation}\"))\n"
399+ " })\n"
400+ " }\n"
401+ )
402+ assert old in s, 'CollationSeq::new body not found'
403+ open(p, 'w').write(s.replace(old, new, 1))
404+ print('patched CollationSeq to alias MySQL collations')
405+ PY_COLLATION
406+
371407 echo '--- Patched stub! macro ---'
372408 sed -n '/macro_rules! stub/,/^}$/p' sqlite3/src/lib.rs
373409
@@ -855,6 +891,89 @@ jobs:
855891 src = src.replace(marker, inject + marker, 1)
856892 open(path, 'w').write(src)
857893 print('added wp_die polyfill to bootstrap.php')
894+
895+ # 6. Turso's PRAGMA table_xinfo preserves outer parens on
896+ # `DEFAULT (expr)` columns (real SQLite strips them). That defeats
897+ # the reconstructor's typed checks and falls through to
898+ # quote_mysql_utf8_string_literal, so SHOW CREATE TABLE emits
899+ # DEFAULT '(CURRENT_TIMESTAMP)' / DEFAULT '(1 + 2)' instead of
900+ # DEFAULT CURRENT_TIMESTAMP / DEFAULT '1 + 2'. Strip outer parens
901+ # at the top of generate_column_default so the typed checks see
902+ # the bare expression.
903+ path = 'src/sqlite/class-wp-sqlite-information-schema-reconstructor.php'
904+ src = open(path).read()
905+ old = (
906+ "\tprivate function generate_column_default( string $mysql_type, ?string $default_value ): ?string {\n"
907+ "\t\tif ( null === $default_value || '' === $default_value ) {\n"
908+ "\t\t\treturn null;\n"
909+ "\t\t}\n"
910+ )
911+ new = (
912+ "\tprivate function generate_column_default( string $mysql_type, ?string $default_value ): ?string {\n"
913+ "\t\tif ( null === $default_value || '' === $default_value ) {\n"
914+ "\t\t\treturn null;\n"
915+ "\t\t}\n"
916+ "\t\tif ( strlen( $default_value ) >= 2 && '(' === $default_value[0] && ')' === substr( $default_value, -1 ) ) {\n"
917+ "\t\t\t$default_value = trim( substr( $default_value, 1, -1 ) );\n"
918+ "\t\t}\n"
919+ )
920+ assert old in src, 'generate_column_default prologue not found'
921+ src = src.replace(old, new, 1)
922+ open(path, 'w').write(src)
923+ print('patched reconstructor default-value paren stripping')
924+
925+ # 7. Turso's PRAGMA table_xinfo returns signed numeric literals with
926+ # a space between sign and digits ("- 1.23", "+ 1.23") — real
927+ # SQLite returns "-1.23". Our is_numeric() check rejects the
928+ # spaced form; collapse the gap so the literal is recognised.
929+ src = open(path).read()
930+ old = (
931+ "\t\t// Numeric literals. E.g.: 123, 1.23, -1.23, 1e3, 1.2e-3\n"
932+ "\t\tif ( is_numeric( $no_underscore_default_value ) ) {\n"
933+ "\t\t\treturn $no_underscore_default_value;\n"
934+ "\t\t}\n"
935+ )
936+ new = (
937+ "\t\t// Numeric literals. E.g.: 123, 1.23, -1.23, 1e3, 1.2e-3\n"
938+ "\t\t$normalised_numeric = preg_replace( '/^([+-])\\s+/', '$1', $no_underscore_default_value );\n"
939+ "\t\tif ( is_numeric( $normalised_numeric ) ) {\n"
940+ "\t\t\treturn $normalised_numeric;\n"
941+ "\t\t}\n"
942+ )
943+ assert old in src, 'generate_column_default numeric block not found'
944+ src = src.replace(old, new, 1)
945+ open(path, 'w').write(src)
946+ print('patched reconstructor signed-numeric spacing')
947+
948+ # 8. Turso mangles implicit column names for hex literals like
949+ # x'417a' (drops the "x'" prefix). Force an explicit alias for
950+ # hex-literal SELECT items so the column name matches the source
951+ # text, as MySQL/SQLite produce.
952+ path = 'src/sqlite/class-wp-pdo-mysql-on-sqlite.php'
953+ src = open(path).read()
954+ old = (
955+ "\t\t$raw_alias = substr( $this->last_mysql_query, $node->get_start(), $node->get_length() );\n"
956+ "\t\t$alias = $this->quote_sqlite_identifier( $raw_alias );\n"
957+ "\t\tif ( $alias === $item || $raw_alias === $item ) {\n"
958+ "\t\t\t// For the simple case of selecting only columns (\"SELECT id FROM t\"),\n"
959+ "\t\t\t// let's avoid unnecessary aliases (\"SELECT `id` AS `id` FROM t\").\n"
960+ "\t\t\treturn $item;\n"
961+ "\t\t}\n"
962+ )
963+ new = (
964+ "\t\t$raw_alias = substr( $this->last_mysql_query, $node->get_start(), $node->get_length() );\n"
965+ "\t\t$alias = $this->quote_sqlite_identifier( $raw_alias );\n"
966+ "\t\t$is_hex_literal = ( 0 === strncasecmp( $item, \"x'\", 2 ) );\n"
967+ "\t\tif ( ! $is_hex_literal && ( $alias === $item || $raw_alias === $item ) ) {\n"
968+ "\t\t\t// For the simple case of selecting only columns (\"SELECT id FROM t\"),\n"
969+ "\t\t\t// let's avoid unnecessary aliases (\"SELECT `id` AS `id` FROM t\").\n"
970+ "\t\t\treturn $item;\n"
971+ "\t\t}\n"
972+ )
973+ assert old in src, 'translate_select_item block not found'
974+ src = src.replace(old, new, 1)
975+ open(path, 'w').write(src)
976+ print('patched hex-literal alias force under Turso')
858977 PY
859978
860979 - name : Capture failing SQL from the first failing driver test
0 commit comments