Repository navigation
bun:sqlite Database.query() returns undefined when the database is reached through any indirection #10290
Description
Parent: #10107 (OpenCode v1.18.30 native bring-up).
Summary
Database.query() from bun:sqlite returns undefined when the database is reached through any indirection — a Map, a function return, a generator — instead of returning a statement. Called directly on a local it works. prepare() on the same receiver works through the same indirections, so this is specific to query.
Silently returning undefined is the damaging part: the caller fails later, on whatever it does with the statement, with no hint that the database call was the problem.
Repro
import { Database } from "bun:sqlite"
const db: any = new Database(":memory:")
db.run("create table t (a integer, b text)")
db.run("insert into t values (1, 'x')")
console.log("direct ", typeof db.query("select * from t"))
const ctx = new Map<string, any>([["native", db]])
console.log("via Map ", typeof ctx.get("native").query("select * from t"))
function getNative(): any { return db }
console.log("via fn ", typeof getNative().query("select * from t"))
function* gen(): any { return db }
function runGen(g: any) { const it = g(); let r = it.next(); while (!r.done) r = it.next(r.value); return r.value }
const viaGen: any = runGen(gen)
console.log("via gen ", typeof viaGen.query("select * from t"))
console.log("prepare ", typeof viaGen.prepare("select * from t")) // control: worksbun 1.3.14 — every line prints object.
perry 0.5.1571 (--platform bun):
direct object
via Map undefined
via fn undefined
via gen undefined
prepare object ← the control passes, so the receiver itself is fine
Second divergence in the same area
The statement's methods are callable but invisible as properties:
const stmt: any = db.prepare("select * from t")
typeof stmt.safeIntegers // bun: "function" perry: "undefined"
stmt.safeIntegers(true) // both: worksSame for a statement obtained from db.query(...). So a direct call dispatches, but typeof, feature detection and destructuring all see nothing. Worth fixing together since both look like the native dispatch table being consulted only on the direct-call shape.
Why it matters
This is what OpenCode's models command hits. packages/core/src/database/sqlite.bun.ts does exactly the failing shape — the database arrives from an effect service through Effect.gen, and then:
const native = (yield* Sqlite.Native) as Database
const statement = native.query(query)
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))native.query(query) is undefined, so the next line fails with TypeError: Cannot read properties of undefined (reading 'safeIntegers') and the command exits 1.
This path became reachable only after #10281 / PR #10283 taught the resolver to honor the bun export condition. OpenCode maps #sqlite, #db, #pty and #fff by condition:
"#sqlite": { "bun": "./src/database/sqlite.bun.ts", "node": "./src/database/sqlite.node.ts", "default": "./src/database/sqlite.bun.ts" }Before that fix perry resolved the node variant and never exercised bun:sqlite; the official bun binary has always run the bun variant. So this is a pre-existing gap that the resolver fix uncovered rather than caused — but it does mean models regressed from working to failing on that lineage, which is worth knowing when reading the bring-up status.
Activity
proggeramlug commentedon Sep 15, 2026
Narrowed considerably — it is the call site, not the receiver, not the property
Follow-up probe, same process, one database instance:
So: the receiver is the same object (
ind === db), the property exists and is callable, and the call still yieldsundefined— while the identical call through the directly-known local works. The only difference is what the compiler knows about the variable at the call site.The emitted IR shows why they differ in kind: there is exactly one
call i64 @js_bun_sqlite_database_queryin the module, for the direct call. The indirect ones never reach it; they go through the bound-method path (js_class_method_bindviadatabase_method_name_static) and that path returnsundefinedfor this method.Which methods are affected
Same object, all through the indirect binding:
queryrunprepareexecfilename":memory:"":memory:"transactionserializeprepareandqueryare worth comparing directly, because they are the same underlying function: the codegen table maps both tojs_bun_sqlite_database_query, and that runtime function is a one-line forward tojs_node_sqlite_database_sync_prepare. Identical table entries, identical implementation, and only one of them survives the bound-method path. Whatever differs is in that path, not in the sqlite code.Ruled out
lower_call/native_module_dispatch.rs. Both passes requiresig.module == normalized, so a cross-module entry (mysql2, pg) is never chosen for abun:sqlitereceiver.typeofreports"function"and the value can be extracted and.called.ind === db.bun:sqlitesupport generally.safeIntegers,all,iterate,valuesall exist and work on a directly-obtained statement.Still open, and the thing to fix
Why the bound-method invoke path yields
undefinedforqueryandrunwhileprepare,execand the accessors work. Start atbind_static_handle_method/database_method_name_staticincrates/perry-stdlib/src/sqlite/dispatch.rsand follow what invoking that bound method actually calls, comparing theprepareandqueryarms.Note the separate property-visibility divergence in the original report is on the statement, not the database:
typeof stmt.safeIntegersis"undefined"under perry though the call works. Opposite symptom, possibly the same machinery.proggeramlug commentedon Sep 15, 2026
Two hypotheses tested and killed, plus the runtime's own answer
Both of these looked right from the source and both were wrong when built and run. Recording them so nobody repeats them.
Wrong 1 — the module bucket.
nm_dispatch_lookupinnative_module_registry.rsmaps only"sqlite", not"bun:sqlite", andnm_dispatch_sqlitematches only("sqlite", _)with a_ =>arm that returnsundefined. That looks exactly like a silent-undefined fallthrough. Adding"bun:sqlite"to both changed nothing — behaviour byte-identical.Wrong 2 — the prototype method list.
SQLITE_DATABASE_SYNC_PROTOTYPE_METHODSinnative_module/callable_exports.rscontainsopen, close, exec, prepare, function, aggregate, …and omitsquery,run,transaction,serialize,deserialize— which matches the failing set exactly. Adding the five changed nothing either.What the runtime actually reports. Probing the function behind the property:
Two facts fall out, and they explain why both patches missed:
ind.query === ind.queryisfalseunder perry,trueunder bun). So the value comes from the per-receiver bound-closure path —js_class_method_bindinobject/native_module.rs— not from a prototype method and not from the dispatch bucket.So the live path is: property read mints a bound closure carrying the method name, and invoking that closure yields
undefinedforqueryandrunwhileprepare,execand the accessors work. The next step is to follow what invoking that bound closure calls, and diff theprepareandqueryarms there. Note also that identity differing from bun (f === ffalse) is its own small divergence worth fixing alongside.fix(stdlib): let bun:sqlite query/run/transaction reach the Database …