Skip to content

Commit 8cc100c

Browse files
committed
Fix driver rendering bugs (B/C/D) and add utf8mb4_bin collation
Driver (applied at CI time): - Strip outer parens from PRAGMA-reported DEFAULT expressions so the reconstructor's typed checks recognize CURRENT_TIMESTAMP / 1 + 2 instead of falling through to quote_mysql_utf8_string_literal. - Normalize Turso's tokenized signed-number defaults ("- 1.23" -> "-1.23") so is_numeric() accepts them. - Force an explicit alias for hex-literal SELECT items (x'417a') since Turso's implicit column naming mangles them. Turso: - Alias common MySQL collations (utf8mb4_bin, utf8mb4_0900_ai_ci, etc.) to Binary/NoCase before CollationSeq::from_str rejects them.
1 parent 61e2bb7 commit 8cc100c

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

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

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

Comments
 (0)