Skip to content

bun:sqlite Database.query() returns undefined when the database is reached through any indirection #10290

Closed

Description

@proggeramlug

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: works

bun 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: works

Same 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

  1. proggeramlug commented on Sep 15, 2026

    @proggeramlug
    ContributorAuthor

    Narrowed considerably — it is the call site, not the receiver, not the property

    Follow-up probe, same process, one database instance:

    const db: any = new Database(":memory:")
    function* gen(): any { return db }
    const ind: any = runGen(gen)        // ind === db  →  true
    
    typeof ind.query                    // bun: "function"   perry: "function"   ← property IS present
    const f = ind.query; typeof f       // bun: "function"   perry: "function"
    f.call(ind, "select * from t")      // bun: object       perry: undefined
    ind["query"]("select * from t")     // bun: object       perry: undefined
    ind.query("select * from t")        // bun: object       perry: undefined
    db.query("select * from t")         // bun: object       perry: object       ← same object, works

    So: the receiver is the same object (ind === db), the property exists and is callable, and the call still yields undefined — 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_query in the module, for the direct call. The indirect ones never reach it; they go through the bound-method path (js_class_method_bind via database_method_name_static) and that path returns undefined for this method.

    Which methods are affected

    Same object, all through the indirect binding:

    method bun perry
    query object undefined
    run object undefined
    prepare object object
    exec ok ok
    filename ":memory:" ":memory:"
    transaction function function
    serialize function function

    prepare and query are worth comparing directly, because they are the same underlying function: the codegen table maps both to js_bun_sqlite_database_query, and that runtime function is a one-line forward to js_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

    • Not the class filter selection in lower_call/native_module_dispatch.rs. Both passes require sig.module == normalized, so a cross-module entry (mysql2, pg) is never chosen for a bun:sqlite receiver.
    • Not a missing property. typeof reports "function" and the value can be extracted and .called.
    • Not receiver identity. ind === db.
    • Not bun:sqlite support generally. safeIntegers, all, iterate, values all exist and work on a directly-obtained statement.

    Still open, and the thing to fix

    Why the bound-method invoke path yields undefined for query and run while prepare, exec and the accessors work. Start at bind_static_handle_method / database_method_name_static in crates/perry-stdlib/src/sqlite/dispatch.rs and follow what invoking that bound method actually calls, comparing the prepare and query arms.

    Note the separate property-visibility divergence in the original report is on the statement, not the database: typeof stmt.safeIntegers is "undefined" under perry though the call works. Opposite symptom, possibly the same machinery.

  2. proggeramlug commented on Sep 15, 2026

    @proggeramlug
    ContributorAuthor

    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_lookup in native_module_registry.rs maps only "sqlite", not "bun:sqlite", and nm_dispatch_sqlite matches only ("sqlite", _) with a _ => arm that returns undefined. 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_METHODS in native_module/callable_exports.rs contains open, close, exec, prepare, function, aggregate, … and omits query, 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:

                    bun                                          perry
    ind.query       name="query"  len=1  src="query(query) {…"   name="query"  len=0  src="function () { [native code] }"
    ind.prepare     name="prepare" len=2                         name="prepare" len=0  [native code]
    same fn twice?  true                                         false
    prototype own property names                                 bun: [constructor, handle, inTransaction,
                                                                       loadExtension, serialize, fileControl,
                                                                       close, clearQueryCache, run, prepare,
                                                                       query, transaction, exec]
                                                                 perry: []
    

    Two facts fall out, and they explain why both patches missed:

    1. The prototype is empty under perry. So the attach path is never reached for this object, and editing its method list cannot matter. Bun exposes thirteen own names on the prototype; perry exposes none.
    2. Each property read mints a fresh function (ind.query === ind.query is false under perry, true under bun). So the value comes from the per-receiver bound-closure path — js_class_method_bind in object/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 undefined for query and run while prepare, exec and the accessors work. The next step is to follow what invoking that bound closure calls, and diff the prepare and query arms there. Note also that identity differing from bun (f === f false) is its own small divergence worth fixing alongside.

  3. added a commit that references this issue on Sep 15, 2026
    7debed5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions