Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
138 commits
Select commit Hold shift + click to select a range
b375d21
Add PHPUnit tests workflow for Turso DB
JanJakes Apr 23, 2026
07bf43a
Make Turso preload sanity check non-fatal
JanJakes Apr 23, 2026
47c9341
Trigger CI re-run
JanJakes Apr 23, 2026
3460fa3
Patch Turso stub macro so pdo_sqlite can connect
JanJakes Apr 23, 2026
6cfd531
Extend Turso smoke test to narrow down PHPUnit segfault
JanJakes Apr 23, 2026
6f994f1
Add PHPUnit startup diagnostics to isolate segfault
JanJakes Apr 23, 2026
2555d7c
Remove PHPUnit startup diagnostics
JanJakes Apr 23, 2026
6f1670c
Stage-by-stage reproduction of test setUp() to isolate segfault
JanJakes Apr 23, 2026
310d9b9
Diagnose createFunction with different callables
JanJakes Apr 23, 2026
4c1e3da
Run createFunction diagnostic under gdb
JanJakes Apr 23, 2026
3bae638
Add compat shim for pdo_sqlite's non-_v2 function calls
JanJakes Apr 23, 2026
9c0b24d
Patch sqlite3_column_* to return defaults when no row
JanJakes Apr 23, 2026
9eb972b
Run PHPUnit under gdb to capture segfault backtrace
JanJakes Apr 23, 2026
441d211
Add sqlite3_snprintf / sqlite3_mprintf to compat shim
JanJakes Apr 23, 2026
0e7338a
Fail the job when PHPUnit crashes or tests fail
JanJakes Apr 23, 2026
50ff4b7
Patch driver to include failing SQL in exception messages
JanJakes Apr 23, 2026
c327e29
Revert driver SQL-logging patch; use standalone diagnostic instead
JanJakes Apr 23, 2026
073bbbd
Install diagnostic query logger on driver's own connection
JanJakes Apr 23, 2026
59d0225
Print full SQL of last 3 queries in diagnostic
JanJakes Apr 23, 2026
43b7af5
Replace shim's printf with a SQLite-compatible formatter
JanJakes Apr 23, 2026
ba7dbbb
Patch driver to work around three Turso compatibility issues
JanJakes Apr 23, 2026
325615a
Fix YAML tab indentation in Turso driver patch step
JanJakes Apr 23, 2026
1416186
Drop UPDATE rewrite patch — caused PHPUnit to hang
JanJakes Apr 23, 2026
e7be7dc
Bisect: disable wp_die polyfill
JanJakes Apr 23, 2026
874e7d3
Keep only wp_die polyfill among driver patches
JanJakes Apr 23, 2026
af06534
Reinstate temporary_table_exists patch + timeout-kill on shutdown hang
JanJakes Apr 23, 2026
e91c99a
Drop temporary_table_exists patch — Turso deadlocks during run, not s…
JanJakes Apr 23, 2026
8c01165
Derive PHPUnit pass/fail from JUnit XML, not process exit status
JanJakes Apr 23, 2026
10501ae
Patch sqlite3_finalize to avoid deadlock, re-enable temp_master patch
JanJakes Apr 23, 2026
651fad3
Simpler sqlite3_finalize patch: try_lock only, keep stmt_run_to_compl…
JanJakes Apr 23, 2026
a07b6bd
Revert try_lock and temporary_table_exists patches
JanJakes Apr 23, 2026
0497aff
Switch to Turso main (pinned) with TEMP-table support
JanJakes Apr 23, 2026
bf2ae06
Bump PHPUnit timeout 180s → 600s (Turso main exercises more paths)
JanJakes Apr 23, 2026
9d1487f
PHPUnit --debug so we see which test is hanging
JanJakes Apr 23, 2026
98e9025
Skip testAlterTableAddColumnWithNotNull (Turso hangs on ALTER flow)
JanJakes Apr 23, 2026
507f2b8
Skip all ALTER TABLE translation tests (each hangs Turso)
JanJakes Apr 23, 2026
9ad62cd
Skip WP_SQLite_Driver_Translation_Tests (multiple hangs)
JanJakes Apr 23, 2026
7729ad5
Increase timeout to 600s (MySQL test-suite lexer test is slow)
JanJakes Apr 24, 2026
356b1af
Skip MySQL server-suite lexer test (10+ min under LD_PRELOAD)
JanJakes Apr 24, 2026
25cc97b
Also skip WP_MySQL_Server_Suite_Parser_Tests (same CSV loop)
JanJakes Apr 24, 2026
04aecc7
Patch Turso to look up scalar functions case-insensitively
JanJakes Apr 24, 2026
6d1ae96
Retry sync_column_key_info UPDATE rewrite on Turso main
JanJakes Apr 24, 2026
61afdce
Wrap UPDATE subqueries in IFNULL to fix NOT NULL violations
JanJakes Apr 24, 2026
b9e534d
Catch-and-ignore PRAGMA foreign_key_check (Turso not implemented)
JanJakes Apr 24, 2026
bd8c2cc
Strip 'Runtime error: ' and ' (19)' from Turso error messages
JanJakes Apr 24, 2026
dc21d2f
Simplify Runtime-error stripping (regex lookbehind didn't match)
JanJakes Apr 24, 2026
c02d5e5
Swallow 'sqlite_sequence may not be modified' (Turso restriction)
JanJakes Apr 24, 2026
6031c5c
Normalize multi-column UNIQUE error format from Turso
JanJakes Apr 24, 2026
b9a36b2
Expand Turso's custom-function slots from 32 to 64
JanJakes Apr 24, 2026
a618745
Skip testFromBase64Function/testToBase64Function (hang in UDF dispatch)
JanJakes Apr 24, 2026
da141f1
Probe: unskip all tests to see where hangs remain
JanJakes Apr 24, 2026
77f1e10
Diagnostic: actually CALL high-slot UDFs to test bridge dispatch
JanJakes Apr 24, 2026
d3fa66f
Diagnostic: reproduce testFromBase64Function setup in isolation
JanJakes Apr 24, 2026
cb3240d
Fix bootstrap path in FROM_BASE64 diagnostic
JanJakes Apr 24, 2026
3aea2e7
Diagnostic: test UPPERCASE UDF call to verify case-insensitive patch
JanJakes Apr 24, 2026
7c38d50
Diagnostic: drop [e] UDF pollution to reproduce FROM_BASE64 cleanly
JanJakes Apr 24, 2026
78e997b
Skip UAF in Turso's slot-reuse destroy path
JanJakes Apr 24, 2026
993ffa4
Debug: run only testFromBase64Function to isolate
JanJakes Apr 24, 2026
7cbee1f
Add dedicated gdb step for FROM_BASE64; restore main skip regex
JanJakes Apr 24, 2026
2c6b9f4
Fix Turso TextValue::free / Blob::free fat-pointer corruption
JanJakes Apr 24, 2026
22a5745
Fix types.rs path in Turso patch (relative to turso/ working dir)
JanJakes Apr 24, 2026
4c4f491
Unskip base64 tests; only CSV-driven server-suite tests remain skipped
JanJakes Apr 24, 2026
1d34101
Re-skip Translation_Tests; add isolation probes for testReconstructTable
JanJakes Apr 24, 2026
9eee5c4
Add bisection probes for testReconstructTable hang source
JanJakes Apr 24, 2026
9088f60
Add full-run probe with gdb watchdog to capture hang stack
JanJakes Apr 24, 2026
3f3e53a
Trim bisection probes; keep full-main reproducer with better gdb watc…
JanJakes Apr 24, 2026
61e2bb7
Fix re-entrant deadlock in Turso's sqlite3_finalize
JanJakes Apr 24, 2026
8cc100c
Fix driver rendering bugs (B/C/D) and add utf8mb4_bin collation
JanJakes Apr 24, 2026
66df0f3
Rewrite sync_column_key_info as EXISTS-based (fixes Bug A / 6-7 tests)
JanJakes Apr 24, 2026
70393ec
Fix VALUES-aliasing and CHECK TABLE under Turso
JanJakes Apr 24, 2026
7f630ea
Allow DELETE on sqlite_sequence in Turso
JanJakes Apr 24, 2026
f4dee46
Remove debugging probe and gdb diagnostic steps
JanJakes Apr 24, 2026
5cd7538
Patch Translation_Tests expectations to match EXISTS rewrite
JanJakes Apr 24, 2026
173d9d5
Revert VALUES-aliasing fallback patch (ineffective and regresses tests)
JanJakes Apr 24, 2026
bfe3f31
Match SQLite: strip outer parens from PRAGMA table_info defaults
JanJakes Apr 24, 2026
e3122aa
Revert PRAGMA paren-strip patch (causes segfault at testReconstructTa…
JanJakes Apr 24, 2026
34dc1cd
Rewrite VALUES as SELECT-AS-columnN in insertFromConstructor
JanJakes Apr 24, 2026
d09ee65
Align testHexadecimalLiterals expectation with hex-alias force patch
JanJakes Apr 24, 2026
e7cf0cf
Add missing re import for VALUES rewrite patch
JanJakes Apr 24, 2026
a74dd39
Revert VALUES-to-SELECT rewrite (persistent segfault in reconstructor)
JanJakes Apr 24, 2026
1d462a5
Try PRAGMA paren-strip again with simpler Rust form
JanJakes Apr 24, 2026
7e880ca
Permanently revert PRAGMA paren-strip (confirmed segfault trigger)
JanJakes Apr 24, 2026
7ced8d3
Preserve original SQL text in CREATE TRIGGER sqlite_master.sql
JanJakes Apr 24, 2026
0a8ae2c
Restore column-name casing in Turso error messages
JanJakes Apr 24, 2026
c0772d3
Scope implicit column collation to direct refs (fixes NOCASE bleed)
JanJakes Apr 24, 2026
fffa8fc
Emit DEFAULT without parens for simple identifiers
JanJakes Apr 24, 2026
976d86f
Translate multi-table UPDATE to rowid-IN subquery under Turso
JanJakes Apr 24, 2026
909fd69
Inline single-row INSERT VALUES into outer SELECT (correct order)
JanJakes Apr 24, 2026
403d319
Wrap AUTO_INCREMENT correlated subquery in MAX aggregate
JanJakes Apr 24, 2026
aaa380c
Revert "Inline single-row INSERT VALUES into outer SELECT (correct or…
JanJakes Apr 25, 2026
403888b
Extend DEFAULT now() handling + update UPDATE Translation_Tests expec…
JanJakes Apr 25, 2026
67718c6
Revert "Wrap AUTO_INCREMENT correlated subquery in MAX aggregate"
JanJakes Apr 25, 2026
03c705a
Patch driver AUTO_INCREMENT lookup to a JOIN form under Turso
JanJakes Apr 25, 2026
6f1f8a2
Limit AUTO_INCREMENT JOIN rewrite to the SHOW TABLE STATUS path
JanJakes Apr 25, 2026
c138e01
Add CI probe for Turso columnN binding shapes
JanJakes Apr 25, 2026
ce22919
Extend columnN probe + parse stdout when JUnit not flushed
JanJakes Apr 25, 2026
ba6b01b
Flatten YEAR cast nested subquery for Turso
JanJakes Apr 25, 2026
835c983
Restore --debug for PHPUnit run under Turso
JanJakes Apr 25, 2026
b23d78a
Adapt testCreateTableWithDefaultExpressions PRAGMA assertion for Turso
JanJakes Apr 25, 2026
c96a268
Also patch result[2] PRAGMA assertion in testCreateTableWithDefaultEx…
JanJakes Apr 25, 2026
0760375
Patch result[3] PRAGMA assertion (Turso double-wraps `||`)
JanJakes Apr 25, 2026
e727c21
Add CI probe for Turso TEMP table I/O failure shapes
JanJakes Apr 25, 2026
b5a2d2c
Add CI probe for Turso correlated-subquery-in-derived-table bug
JanJakes Apr 25, 2026
5fe9618
Tighter probe + force temp_store=MEMORY under Turso
JanJakes Apr 25, 2026
fcb7399
Scope temp_store=MEMORY workaround to the failing temp-table tests
JanJakes Apr 25, 2026
656f12e
Revert temp_store=MEMORY patch — it also crashes testReconstructTable
JanJakes Apr 25, 2026
78066d7
Add length() perturbation to translate_table_ref AUTO_INCREMENT subquery
JanJakes Apr 25, 2026
472d4c4
Revert length() perturbation of AUTO_INCREMENT correlated subquery
JanJakes Apr 25, 2026
ac53791
Patch Turso effective_temp_store to always return Memory
JanJakes Apr 25, 2026
d69d148
Try Turso pager patch: treat 0-byte page read as empty (SQLite-compat)
JanJakes Apr 25, 2026
605874b
Revert "Try Turso pager patch: treat 0-byte page read as empty (SQLit…
JanJakes Apr 25, 2026
440205b
Revert "Patch Turso effective_temp_store to always return Memory"
JanJakes Apr 25, 2026
bbcfe74
ci: retrigger to check testReconstructTable flakiness
JanJakes Apr 25, 2026
647ed4d
Run the 3 failing tests in separate PHP processes under Turso
JanJakes Apr 25, 2026
5f879cf
Revert "Run the 3 failing tests in separate PHP processes under Turso"
JanJakes Apr 25, 2026
a39d432
Clear stale FUNC_SLOTS on sqlite3_close
JanJakes Apr 25, 2026
6db5e33
Revert "Clear stale FUNC_SLOTS on sqlite3_close"
JanJakes Apr 25, 2026
286a045
Add CI probe for PDO-churn state buildup vs the 3 failing tests
JanJakes Apr 25, 2026
96c7ba0
Extend churn probe with UDF registration
JanJakes Apr 25, 2026
ce6b58e
Add CI probe — run only the 3 failing tests in isolation
JanJakes Apr 25, 2026
2295388
Probe correlated subquery against REAL sqlite_sequence too
JanJakes Apr 25, 2026
36f8e18
Replace isolation probe with SQL-capture probe
JanJakes Apr 25, 2026
03f042f
Use Turso RUST_LOG=trace to capture every prepared SQL in probe
JanJakes Apr 25, 2026
7406405
Probe: include the test class's setUp tables before testCreateTempora…
JanJakes Apr 25, 2026
08a4715
Lift probe head cap + add hard timeout
JanJakes Apr 25, 2026
ebf5459
Patch Turso translate_drop_table sqlite_sequence db mismatch
JanJakes Apr 25, 2026
08ca947
ci: retrigger to verify root-cause fix and confirm testReconstructTab…
JanJakes Apr 25, 2026
23f2179
Probe testTemporaryTableHasPriority — find its specific failing SQL
JanJakes Apr 25, 2026
2ef2068
ci: retrigger to verify 590/596 baseline after all probes
JanJakes Apr 25, 2026
98ee736
Patch Turso hash-join planner for correlated subqueries
JanJakes Apr 25, 2026
42cf49a
Probe: isolate testTemporaryTableHasPriority with querying-only filter
JanJakes Apr 25, 2026
8c56b0a
Patch Turso translate_drop_table to use correct db schema for indices
JanJakes Apr 25, 2026
504ce28
Fix borrow in drop_indices patch — iterate Vec by reference
JanJakes Apr 25, 2026
d4ad384
ci: retrigger to verify drop-indices fix past testReconstructTable flake
JanJakes Apr 25, 2026
11f0d4e
ci: print skipped/incomplete test names in JUnit summary group
JanJakes Apr 25, 2026
74ab2f8
Extract Turso source patches to .github/turso-patches/
JanJakes Apr 26, 2026
876920e
Bump Turso pin 375f5d5 -> 5e371c2 to pull in 83 trunk commits
JanJakes Apr 27, 2026
075450a
Retire driver UPDATE-FROM workaround now that Turso trunk supports it
JanJakes Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/turso-patches/01-stub-macro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""
Neutralize Turso's stub! macro (sqlite3/src/lib.rs).

Many SQLite C API functions are stubbed out via `stub!()`, which expands
to `todo!("X is not implemented")`. pdo_sqlite hits one during PDO
construction (sqlite3_set_authorizer) and the panic aborts the PHP
process. Rewrite the body to return a zeroed value of the function's
return type (0 / SQLITE_OK for ints, NULL for pointers) instead.
"""

import sys

PATH = 'sqlite3/src/lib.rs'
OLD = 'todo!("{} is not implemented", stringify!($fn));'
NEW = 'return unsafe { std::mem::zeroed() };'

with open(PATH) as f:
src = f.read()
if OLD not in src:
sys.exit(f'{PATH}: stub! todo!() body not found')
n = src.count(OLD)
src = src.replace(OLD, NEW)
with open(PATH, 'w') as f:
f.write(src)
print(f'patched stub! macro ({n} occurrence{"s" if n != 1 else ""})')
53 changes: 53 additions & 0 deletions .github/turso-patches/02-column-functions-null-row.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
sqlite3_column_* — early-return when no row is present (sqlite3/src/lib.rs).

The sqlite3_column_* functions `.expect()` that a row is present, but
pdo_sqlite legitimately calls them on statements that have not yet
stepped to SQLITE_ROW (e.g. for column metadata). Replace the expect
with an early return of the type's "null" value, matching SQLite's
actual behaviour.
"""

import re
import sys

PATH = 'sqlite3/src/lib.rs'

DEFAULTS = {
'sqlite3_column_type': 'SQLITE_NULL',
'sqlite3_column_int': '0',
'sqlite3_column_int64': '0',
'sqlite3_column_double': '0.0',
'sqlite3_column_blob': 'std::ptr::null()',
'sqlite3_column_bytes': '0',
'sqlite3_column_text': 'std::ptr::null()',
}

PATTERN = re.compile(
r'(pub unsafe extern "C" fn (sqlite3_column_\w+)\([^)]*\)[^{]*\{)'
r'((?:[^{}]|\{[^{}]*\})*?)'
r'(let row = stmt\s*\.stmt\s*\.row\(\)\s*'
r'\.expect\("Function should only be called after `SQLITE_ROW`"\);)',
re.DOTALL,
)


def repl(m):
header, name, body, _ = m.group(1), m.group(2), m.group(3), m.group(4)
default = DEFAULTS.get(name, '0')
guarded = (
f'let row = match stmt.stmt.row() {{ '
f'Some(r) => r, None => return {default} }};'
)
return header + body + guarded


with open(PATH) as f:
src = f.read()
src, n = PATTERN.subn(repl, src)
if n == 0:
sys.exit(f'{PATH}: no sqlite3_column_* expect-row blocks matched')
with open(PATH, 'w') as f:
f.write(src)
print(f'patched {n} sqlite3_column_* functions')
73 changes: 73 additions & 0 deletions .github/turso-patches/03-text-blob-free-types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
TextValue::free / Blob::free — rebuild the original slice box (extensions/core/src/types.rs).

TextValue/Blob `free` reconstructs `Box<u8>` from a pointer that was
originally `Box<str>` / `Box<[u8]>` (fat pointers, length lost in the
cast). This corrupts the heap when custom UDFs return text/blob values.
Fix both frees to use the stored length and rebuild the correct slice
box.
"""

import re
import sys

PATH = 'extensions/core/src/types.rs'

OLD_TEXT = (
' #[cfg(feature = "core_only")]\n'
' fn free(self) {\n'
' if !self.text.is_null() {\n'
' let _ = unsafe { Box::from_raw(self.text as *mut u8) };\n'
' }\n'
' }\n'
)
NEW_TEXT = (
' #[cfg(feature = "core_only")]\n'
' fn free(self) {\n'
' if !self.text.is_null() && self.len > 0 {\n'
' unsafe {\n'
' let slice = std::slice::from_raw_parts_mut(\n'
' self.text as *mut u8, self.len as usize);\n'
' let _ = Box::from_raw(slice as *mut [u8]);\n'
' }\n'
' }\n'
' }\n'
)

with open(PATH) as f:
s = f.read()
if OLD_TEXT not in s:
sys.exit(f'{PATH}: TextValue::free not found')
s = s.replace(OLD_TEXT, NEW_TEXT, 1)

# Blob::free uses the same pattern.
blob_pat = re.compile(
r'(impl Blob \{\n(?:[^}]|\{[^}]*\})*?)'
r'( #\[cfg\(feature = "core_only"\)\]\n'
r' fn free\(self\) \{\n'
r' if !self\.data\.is_null\(\) \{\n'
r' let _ = unsafe \{ Box::from_raw\(self\.data as \*mut u8\) \};\n'
r' \}\n'
r' \}\n)'
)
m = blob_pat.search(s)
if m:
new_blob = (
' #[cfg(feature = "core_only")]\n'
' fn free(self) {\n'
' if !self.data.is_null() && self.size > 0 {\n'
' unsafe {\n'
' let slice = std::slice::from_raw_parts_mut(\n'
' self.data as *mut u8, self.size as usize);\n'
' let _ = Box::from_raw(slice as *mut [u8]);\n'
' }\n'
' }\n'
' }\n'
)
s = s[:m.start(2)] + new_blob + s[m.end(2):]
print('patched Blob::free')

with open(PATH, 'w') as f:
f.write(s)
print('patched TextValue::free')
46 changes: 46 additions & 0 deletions .github/turso-patches/04-create-function-v2-skip-old-destroy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
create_function_v2 — don't fire the old destroy callback on slot reuse (sqlite3/src/lib.rs).

Turso's create_function_v2 invokes the previous FuncSlot's destroy
callback when re-registering a UDF with the same name. In practice
(PHPUnit) this means:
- setUp #1 opens PDO A, registers 44 UDFs, each with a destroy
callback + p_app pointing to A.
- tearDown closes PDO A — Turso's sqlite3_close doesn't clear those
FuncSlots.
- setUp #2 opens PDO B, re-registers the same 44 names. Turso invokes
the OLD destroy callback with the now-dangling A p_app, which trips
pdo_sqlite and hangs the process.

Comment the destroy invocation out; the callbacks still fire at real
PDO-destruction time from the PHP side.
"""

import sys

PATH = 'sqlite3/src/lib.rs'
OLD = (
' // Reuse existing slot — invoke old destroy callback on old user data.\n'
' if let Some(old) = slots[id].take() {\n'
' if old.destroy != 0 {\n'
' let old_destroy: unsafe extern "C" fn(*mut ffi::c_void) =\n'
' std::mem::transmute(old.destroy);\n'
' old_destroy(old.p_app as *mut ffi::c_void);\n'
' }\n'
' }\n'
)
NEW = (
" // Don't invoke the old destroy callback here — in PDO\n"
" // usage the previous slot's p_app often belongs to a db\n"
' // that has already been closed, so the callback UAFs.\n'
' let _ = slots[id].take();\n'
)

with open(PATH) as f:
s = f.read()
if OLD not in s:
sys.exit(f'{PATH}: slot destroy block not found')
with open(PATH, 'w') as f:
f.write(s.replace(OLD, NEW, 1))
print('patched slot-reuse destroy invocation')
109 changes: 109 additions & 0 deletions .github/turso-patches/05-finalize-try-lock-on-gc-reentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
sqlite3_finalize / stmt_run_to_completion — try_lock to dodge GC re-entry deadlock.

sqlite3_finalize uses a non-reentrant std::sync::Mutex on the db. PHP's
cycle GC can fire a PDO statement destructor while another statement's
sqlite3_step is in progress (i.e., from inside a UDF callback whose
bridge re-enters PHP). The outer step holds the mutex, the inner
finalize blocks on it, and we deadlock.

Fix: use try_lock; on contention, skip the stmt_list unlink and the
drain. The list is only traversed by sqlite3_next_stmt (which
pdo_sqlite doesn't call) and dropped on sqlite3_close, so a stale entry
is harmless; the stmt's own Box is still freed below.
"""

import sys

PATH = 'sqlite3/src/lib.rs'

OLD_FINALIZE = (
' if !stmt_ref.db.is_null() {\n'
' let db = &mut *stmt_ref.db;\n'
' let mut db_inner = db.inner.lock().unwrap();\n'
'\n'
' if db_inner.stmt_list == stmt {\n'
' db_inner.stmt_list = stmt_ref.next;\n'
' } else {\n'
' let mut current = db_inner.stmt_list;\n'
' while !current.is_null() {\n'
' let current_ref = &mut *current;\n'
' if current_ref.next == stmt {\n'
' current_ref.next = stmt_ref.next;\n'
' break;\n'
' }\n'
' current = current_ref.next;\n'
' }\n'
' }\n'
' }\n'
)
NEW_FINALIZE = (
' if !stmt_ref.db.is_null() {\n'
' let db = &mut *stmt_ref.db;\n'
' // try_lock to avoid deadlock when finalize is invoked\n'
' // re-entrantly (GC destructor during UDF callback).\n'
' if let Ok(mut db_inner) = db.inner.try_lock() {\n'
' if db_inner.stmt_list == stmt {\n'
' db_inner.stmt_list = stmt_ref.next;\n'
' } else {\n'
' let mut current = db_inner.stmt_list;\n'
' while !current.is_null() {\n'
' let current_ref = &mut *current;\n'
' if current_ref.next == stmt {\n'
' current_ref.next = stmt_ref.next;\n'
' break;\n'
' }\n'
' current = current_ref.next;\n'
' }\n'
' }\n'
' }\n'
' }\n'
)

OLD_DRAIN = (
'unsafe fn stmt_run_to_completion(stmt: *mut sqlite3_stmt) -> ffi::c_int {\n'
' let stmt_ref = &mut *stmt;\n'
' while stmt_ref.stmt.execution_state().is_running() {\n'
' let result = sqlite3_step(stmt);\n'
' if result != SQLITE_DONE && result != SQLITE_ROW {\n'
' return result;\n'
' }\n'
' }\n'
' SQLITE_OK\n'
'}\n'
)
NEW_DRAIN = (
'unsafe fn stmt_run_to_completion(stmt: *mut sqlite3_stmt) -> ffi::c_int {\n'
' let stmt_ref = &mut *stmt;\n'
" // Skip drain if we can't acquire the db mutex: we're\n"
" // re-entering from a UDF callback's GC destructor, and\n"
' // sqlite3_step would block forever. The stmt will be\n'
' // freed anyway by the caller.\n'
' if !stmt_ref.db.is_null() {\n'
' let db = &*stmt_ref.db;\n'
' if db.inner.try_lock().is_err() {\n'
' return SQLITE_OK;\n'
' }\n'
' }\n'
' while stmt_ref.stmt.execution_state().is_running() {\n'
' let result = sqlite3_step(stmt);\n'
' if result != SQLITE_DONE && result != SQLITE_ROW {\n'
' return result;\n'
' }\n'
' }\n'
' SQLITE_OK\n'
'}\n'
)

with open(PATH) as f:
s = f.read()
if OLD_FINALIZE not in s:
sys.exit(f'{PATH}: sqlite3_finalize stmt_list block not found')
s = s.replace(OLD_FINALIZE, NEW_FINALIZE, 1)
if OLD_DRAIN not in s:
sys.exit(f'{PATH}: stmt_run_to_completion block not found')
s = s.replace(OLD_DRAIN, NEW_DRAIN, 1)
with open(PATH, 'w') as f:
f.write(s)
print('patched sqlite3_finalize + stmt_run_to_completion for GC re-entry')
47 changes: 47 additions & 0 deletions .github/turso-patches/06-max-custom-funcs-32-to-64.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
Bump MAX_CUSTOM_FUNCS from 32 to 64 (sqlite3/src/lib.rs).

Turso's custom-function registry is capped at 32 pre-generated bridge
trampolines; the driver registers 44 UDFs, so the last 12 silently fail.
Bump to 64 by adding 32 more func_bridge!/FUNC_BRIDGES entries.
"""

import re
import sys

PATH = 'sqlite3/src/lib.rs'

with open(PATH) as f:
s = f.read()

OLD_MAX = 'const MAX_CUSTOM_FUNCS: usize = 32;'
NEW_MAX = 'const MAX_CUSTOM_FUNCS: usize = 64;'
if OLD_MAX not in s:
sys.exit(f'{PATH}: MAX_CUSTOM_FUNCS not found')
s = s.replace(OLD_MAX, NEW_MAX, 1)

# Inject 32 more func_bridge! declarations after func_bridge_31.
bridge_marker = 'func_bridge!(31, func_bridge_31);\n'
if bridge_marker not in s:
sys.exit(f'{PATH}: func_bridge_31 marker not found')
extra_bridges = ''.join(
f'func_bridge!({i}, func_bridge_{i});\n' for i in range(32, 64)
)
s = s.replace(bridge_marker, bridge_marker + extra_bridges, 1)

# Extend the FUNC_BRIDGES array: find the closing `];` of the static
# and inject the extra entries before it.
pat = re.compile(
r'(static FUNC_BRIDGES: \[ScalarFunction; MAX_CUSTOM_FUNCS\] = \[\n'
r'(?:\s*func_bridge_\d+,\n)+)(\];\n)'
)
m = pat.search(s)
if m is None:
sys.exit(f'{PATH}: FUNC_BRIDGES array not found')
extra_entries = ''.join(f' func_bridge_{i},\n' for i in range(32, 64))
s = s[:m.start(2)] + extra_entries + s[m.start(2):]

with open(PATH, 'w') as f:
f.write(s)
print('patched MAX_CUSTOM_FUNCS 32 -> 64')
36 changes: 36 additions & 0 deletions .github/turso-patches/07-function-name-case-insensitive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""
Function-name lookup case-insensitivity (core/connection.rs + core/ext/mod.rs).

SQLite looks up function names case-insensitively, but Turso's extension
registry stores names as-is and connection.rs looks them up with
HashMap::get directly. The driver's translator emits e.g. THROW(...)
uppercase, so 32 tests fail with "no such function: THROW" even though
we registered "throw". Normalise to lowercase at both register and
lookup sites.
"""

import sys

CONN = 'core/connection.rs'
EXT = 'core/ext/mod.rs'

with open(CONN) as f:
s = f.read()
old = 'self.functions.get(name).cloned()'
new = 'self.functions.get(&name.to_lowercase()).cloned()'
if old not in s:
sys.exit(f'{CONN}: resolve_function lookup not found')
with open(CONN, 'w') as f:
f.write(s.replace(old, new, 1))

with open(EXT) as f:
s = f.read()
old = '(*ext_ctx.syms).functions.insert(\n name_str.clone(),'
new = '(*ext_ctx.syms).functions.insert(\n name_str.to_lowercase(),'
if old not in s:
sys.exit(f'{EXT}: register_scalar_function insert not found')
with open(EXT, 'w') as f:
f.write(s.replace(old, new, 1))

print('patched function-name case (register + resolve)')
Loading
Loading