@@ -420,6 +420,133 @@ jobs:
420420 - name: Install gdb
421421 run: sudo apt-get install -y --no-install-recommends gdb
422422
423+ # These patches to the driver work around Turso behaviours that the SQL
424+ # layer in this repo currently assumes. Each is a localised rewrite so
425+ # the driver still produces correct behaviour when run against Turso;
426+ # they are not behaviour changes for real SQLite.
427+ - name: Patch driver for Turso compatibility
428+ working-directory: packages/mysql-on-sqlite
429+ run: |
430+ python3 - <<'PY'
431+ import re
432+
433+ # 1. Turso doesn't expose `sqlite_temp_master`. The driver queries it
434+ # to decide whether to use temp-schema tables, but Turso doesn't
435+ # support TEMP tables at all, so the predicate is always false.
436+ path = 'src/sqlite/class-wp-sqlite-information-schema-builder.php'
437+ src = open(path).read()
438+ old = (
439+ "\tpublic function temporary_table_exists( string $table_name ): bool {\n"
440+ "\t\t/*\n"
441+ "\t\t * We could search in the \"{$this->temporary_table_prefix}tables\" table,\n"
442+ "\t\t * but it may not exist yet, so using \"sqlite_temp_master\" is simpler.\n"
443+ "\t\t */\n"
444+ "\t\t$stmt = $this->connection->query(\n"
445+ "\t\t\t\"SELECT 1 FROM sqlite_temp_master WHERE type = 'table' AND name = ?\",\n"
446+ "\t\t\tarray( $table_name )\n"
447+ "\t\t);\n"
448+ "\t\treturn $stmt->fetchColumn() === '1';\n"
449+ "\t}"
450+ )
451+ new = (
452+ "\tpublic function temporary_table_exists( string $table_name ): bool {\n"
453+ "\t\t// Turso compatibility: it does not support TEMP tables or\n"
454+ "\t\t// expose sqlite_temp_master, so this is always false.\n"
455+ "\t\treturn false;\n"
456+ "\t}"
457+ )
458+ assert old in src, 'temporary_table_exists body not found'
459+ src = src.replace(old, new, 1)
460+
461+ # 2. Turso's UPDATE statement doesn't accept row-value assignment from
462+ # a subquery (`SET (a, b) = (SELECT ...)`). Rewrite the single query
463+ # in sync_column_key_info as two correlated subqueries, one per
464+ # target column.
465+ old = (
466+ "\t\t$this->connection->query(\n"
467+ "\t\t\t'\n"
468+ "\t\t\t\tUPDATE ' . $this->connection->quote_identifier( $columns_table_name ) . \" AS c\n"
469+ "\t\t\t\tSET (column_key, is_nullable) = (\n"
470+ "\t\t\t\t\tSELECT\n"
471+ "\t\t\t\t\t\tCASE\n"
472+ "\t\t\t\t\t\t\tWHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI'\n"
473+ "\t\t\t\t\t\t\tWHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI'\n"
474+ "\t\t\t\t\t\t\tWHEN MAX(s.seq_in_index = 1) THEN 'MUL'\n"
475+ "\t\t\t\t\t\t\tELSE ''\n"
476+ "\t\t\t\t\t\tEND,\n"
477+ "\t\t\t\t\t\tCASE\n"
478+ "\t\t\t\t\t\t\tWHEN MAX(s.index_name = 'PRIMARY') THEN 'NO'\n"
479+ "\t\t\t\t\t\t\tELSE c.is_nullable\n"
480+ "\t\t\t\t\t\tEND\n"
481+ "\t\t\t\t\tFROM \" . $this->connection->quote_identifier( $statistics_table_name ) . ' AS s\n"
482+ "\t\t\t\t\tWHERE s.table_schema = c.table_schema\n"
483+ "\t\t\t\t\tAND s.table_name = c.table_name\n"
484+ "\t\t\t\t\tAND s.column_name = c.column_name\n"
485+ "\t\t\t\t)\n"
486+ "\t\t\t WHERE c.table_schema = ?\n"
487+ "\t\t\t AND c.table_name = ?\n"
488+ "\t\t\t',\n"
489+ "\t\t\tarray( self::SAVED_DATABASE_NAME, $table_name )\n"
490+ "\t\t);"
491+ )
492+ assert old in src, 'sync_column_key_info UPDATE not found'
493+ new = ''' $columns_table = $this->connection->quote_identifier( $columns_table_name );
494+ $statistics_table = $this->connection->quote_identifier( $statistics_table_name );
495+ $this->connection->query(
496+ "
497+ UPDATE $columns_table AS c
498+ SET column_key = (
499+ SELECT
500+ CASE
501+ WHEN MAX(s.index_name = 'PRIMARY') THEN 'PRI'
502+ WHEN MAX(s.non_unique = 0 AND s.seq_in_index = 1) THEN 'UNI'
503+ WHEN MAX(s.seq_in_index = 1) THEN 'MUL'
504+ ELSE ''
505+ END
506+ FROM $statistics_table AS s
507+ WHERE s.table_schema = c.table_schema
508+ AND s.table_name = c.table_name
509+ AND s.column_name = c.column_name
510+ ),
511+ is_nullable = (
512+ SELECT
513+ CASE
514+ WHEN MAX(s.index_name = 'PRIMARY') THEN 'NO'
515+ ELSE c.is_nullable
516+ END
517+ FROM $statistics_table AS s
518+ WHERE s.table_schema = c.table_schema
519+ AND s.table_name = c.table_name
520+ AND s.column_name = c.column_name
521+ )
522+ WHERE c.table_schema = ?
523+ AND c.table_name = ?
524+ ",
525+ array( self::SAVED_DATABASE_NAME, $table_name )
526+ );'''
527+ src = src.replace(old, new, 1)
528+ open(path, 'w').write(src)
529+ print('patched information-schema-builder.php')
530+
531+ # 3. wp_die polyfill: 21 tests hit an error path in the driver that
532+ # calls wp_die(). Real WordPress provides it; the unit-test bootstrap
533+ # does not. Add a minimal polyfill.
534+ path = 'tests/bootstrap.php'
535+ src = open(path).read()
536+ marker = "if ( ! function_exists( 'do_action' ) ) {"
537+ inject = (
538+ "if ( ! function_exists( 'wp_die' ) ) {\n"
539+ "\tfunction wp_die( $message = '', $title = '', $args = array() ) {\n"
540+ "\t\tthrow new \\RuntimeException( is_string( $message ) ? $message : 'wp_die' );\n"
541+ "\t}\n"
542+ "}\n\n"
543+ )
544+ assert marker in src
545+ src = src.replace(marker, inject + marker, 1)
546+ open(path, 'w').write(src)
547+ print('added wp_die polyfill to bootstrap.php')
548+ PY
549+
423550 - name: Capture failing SQL from the first failing driver test
424551 continue-on-error: true
425552 env:
0 commit comments