diff --git a/.cargo/config.toml b/.cargo/config.toml index aebf957f4d..c1f27947fa 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -48,3 +48,10 @@ rustflags = ["-C", "force-unwind-tables=yes", "-Z", "use-sync-unwind=yes"] # rather than silent — but it is still a mistake). [target.'cfg(target_arch = "x86_64")'] rustflags = ["-C", "force-unwind-tables=yes", "-Z", "use-sync-unwind=yes", "-C", "force-frame-pointers=yes"] + +# The bundled SQLite is built as node's is in this one respect: node compiles +# it with SQLITE_DEFAULT_MEMSTATUS=0 (`PRAGMA compile_options` on node 26), +# so every sqlite3_malloc skips the global memory-statistics mutex and +# counters. Nothing in perry reads SQLite's memory statistics. +[env] +LIBSQLITE3_FLAGS = "-DSQLITE_DEFAULT_MEMSTATUS=0" diff --git a/changelog.d/PENDING-11919-cb-sqlite.md b/changelog.d/PENDING-11919-cb-sqlite.md new file mode 100644 index 0000000000..cd44086520 --- /dev/null +++ b/changelog.d/PENDING-11919-cb-sqlite.md @@ -0,0 +1 @@ +node:sqlite `DatabaseSync`, `StatementSync`, `StatementSyncIterator`, `SQLTagStore`, `Session` and `db.limits` are now ordinary JavaScript objects that own GC-finalized native payloads, with node's own accessors, prototypes, `instanceof`, `Object.keys` and `JSON.stringify`. SQLite now calls user-defined functions, aggregates and the authorizer back through the database's traced owner edge instead of id registries and root scanners: a throw inside a callback is rethrown as the exact value after SQLite returns (never unwound through SQLite frames), the first throw wins, a callback may re-enter the same database (nested queries, `exec` from a UDF) without deadlocking, and `close()` inside a running callback is deferred until the outer call returns, which then reports the closed database. `close()` / `open()` keep the same object; statements of an earlier open report "statement has been finalized". A database whose UDF captures it is collected without `close()`. Fixed alongside: `%IteratorPrototype%` carries `Symbol.toStringTag` "Iterator", and a native payload's JS state may hold more fields than its inline slots. diff --git a/changelog.d/PENDING-11919-native-callback-path.md b/changelog.d/PENDING-11919-native-callback-path.md new file mode 100644 index 0000000000..93a21607b2 --- /dev/null +++ b/changelog.d/PENDING-11919-native-callback-path.md @@ -0,0 +1,5 @@ +Reduce native payload callback overhead by reading the callback array through a traced slot on the stable cell, reusing an owner link across statement steps, and avoiding owner rooting on a normal native-call completion. Keep callback exception capture, the first pending throw, thread checks, deferred close, and child OpenSerial checks. Callback-array replacement and clearing synchronize the cell edge through the existing state setters. New runtime witnesses cover slot relocation, marking, growth, replacement, clearing, and its exact malloc-parent write barrier, including child-process sabotage. + +Callback families alone carry a traced array slot and a native catch-token pointer after the unchanged legacy header. NativeCallGuard::call lazily captures one savepoint for a native call, refreshes root depths per trampoline, and re-arms a trampoline-local jump target for every callback. Existing enter/finish callers retain ordinary per-callback capture. Prepare exposes its validated owner link without another receiver lookup. New sabotage witnesses cover direct data-only array reads, forwarding, cached root refresh, new.target relocation, nested token restoration and catch-frame cleanup. + +A callback refreshes only its runtime handle depth (trampolines add only handle scopes), calls a compiler-emitted body directly, checks its owner once (`call_callback`: open, this thread, nothing pending) and reads the slot on an inlined hot path; the scalar trampoline converts allocation-free arguments in one pass. Payload method dispatch drops two redundant descriptor scans (an ordinary object's keys carry its attributes). Payload instances are born in their call site's recorded final shape (own keys, JS-state key, own accessors) holding the family's shared accessor pairs, and their JS state in its recorded shape. node:sqlite statements own the `sqlite3_stmt` their `prepare()` compiled, as node's do: the authorizer runs once per `prepare()` (it ran again on every run), `expandedSQL` and `columns()` read the compiled statement, and the connection's close or the statement's drop finalizes it. diff --git a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv index 645df24139..4b7dba6482 100644 --- a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv @@ -505,12 +505,20 @@ js_bun_plugin Reenters js_bun_spawn Reenters js_bun_sql_new Reenters js_bun_sqlite_database_call Reenters +js_bun_sqlite_database_close Reenters js_bun_sqlite_database_filename Reenters +js_bun_sqlite_database_load_extension Reenters js_bun_sqlite_database_new Reenters js_bun_sqlite_database_query Reenters js_bun_sqlite_database_run Reenters +js_bun_sqlite_database_serialize Reenters js_bun_sqlite_database_transaction Reenters +js_bun_sqlite_is_database_handle Reenters +js_bun_sqlite_is_statement_handle Reenters +js_bun_sqlite_statement_all Reenters js_bun_sqlite_statement_finalize Reenters +js_bun_sqlite_statement_get Reenters +js_bun_sqlite_statement_run Reenters js_bun_sqlite_statement_safe_integers Reenters js_bun_sqlite_statement_values Reenters js_bun_stderr Reenters @@ -1857,61 +1865,13 @@ js_node_inspector_url Reenters js_node_inspector_wait_for_debugger Reenters js_node_setheaders_entries_json Reenters js_node_sqlite_backup Reenters -js_node_sqlite_database_sync_aggregate Reenters -js_node_sqlite_database_sync_apply_changeset Reenters js_node_sqlite_database_sync_call Reenters -js_node_sqlite_database_sync_close Reenters -js_node_sqlite_database_sync_create_session Reenters -js_node_sqlite_database_sync_create_tag_store Reenters -js_node_sqlite_database_sync_deserialize Reenters -js_node_sqlite_database_sync_dispose Reenters -js_node_sqlite_database_sync_enable_defensive Reenters -js_node_sqlite_database_sync_enable_load_extension Reenters -js_node_sqlite_database_sync_exec Reenters -js_node_sqlite_database_sync_function Reenters -js_node_sqlite_database_sync_is_open Reenters -js_node_sqlite_database_sync_is_transaction Reenters -js_node_sqlite_database_sync_limits Reenters -js_node_sqlite_database_sync_load_extension Reenters -js_node_sqlite_database_sync_location Reenters js_node_sqlite_database_sync_new Reenters -js_node_sqlite_database_sync_open Reenters -js_node_sqlite_database_sync_prepare Reenters -js_node_sqlite_database_sync_serialize Reenters -js_node_sqlite_database_sync_set_authorizer Reenters -js_node_sqlite_is_database_sync_handle Reenters -js_node_sqlite_is_limits_handle Reenters -js_node_sqlite_is_session_handle Reenters -js_node_sqlite_is_statement_sync_handle Reenters -js_node_sqlite_is_tag_store_handle Reenters js_node_sqlite_native_dispatch Reenters js_node_sqlite_session_call Reenters -js_node_sqlite_session_changeset Reenters -js_node_sqlite_session_close Reenters -js_node_sqlite_session_dispose Reenters js_node_sqlite_session_new Reenters -js_node_sqlite_session_patchset Reenters -js_node_sqlite_sql_tag_store_all Reenters -js_node_sqlite_sql_tag_store_capacity Reenters -js_node_sqlite_sql_tag_store_clear Reenters -js_node_sqlite_sql_tag_store_db Reenters -js_node_sqlite_sql_tag_store_get Reenters -js_node_sqlite_sql_tag_store_iterate Reenters -js_node_sqlite_sql_tag_store_run Reenters -js_node_sqlite_sql_tag_store_size Reenters -js_node_sqlite_statement_sync_all Reenters js_node_sqlite_statement_sync_call Reenters -js_node_sqlite_statement_sync_columns Reenters -js_node_sqlite_statement_sync_expanded_sql Reenters -js_node_sqlite_statement_sync_get Reenters -js_node_sqlite_statement_sync_iterate Reenters js_node_sqlite_statement_sync_new Reenters -js_node_sqlite_statement_sync_run Reenters -js_node_sqlite_statement_sync_set_allow_bare_named_parameters Reenters -js_node_sqlite_statement_sync_set_allow_unknown_named_parameters Reenters -js_node_sqlite_statement_sync_set_read_bigints Reenters -js_node_sqlite_statement_sync_set_return_arrays Reenters -js_node_sqlite_statement_sync_source_sql Reenters js_node_stream_add_abort_signal Reenters js_node_stream_compose Reenters js_node_stream_duplex_from_options Reenters diff --git a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv index 00ba8e4201..c06c341f61 100644 --- a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv +++ b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv @@ -505,12 +505,20 @@ js_bun_plugin Reenters js_bun_spawn Reenters js_bun_sql_new Reenters js_bun_sqlite_database_call ThrowOnly +js_bun_sqlite_database_close Reenters js_bun_sqlite_database_filename Reenters +js_bun_sqlite_database_load_extension Reenters js_bun_sqlite_database_new Reenters js_bun_sqlite_database_query Reenters js_bun_sqlite_database_run Reenters +js_bun_sqlite_database_serialize Reenters js_bun_sqlite_database_transaction ThrowOnly +js_bun_sqlite_is_database_handle Reenters +js_bun_sqlite_is_statement_handle Reenters +js_bun_sqlite_statement_all Reenters js_bun_sqlite_statement_finalize Reenters +js_bun_sqlite_statement_get Reenters +js_bun_sqlite_statement_run Reenters js_bun_sqlite_statement_safe_integers Reenters js_bun_sqlite_statement_values Reenters js_bun_stderr Reenters @@ -1857,61 +1865,13 @@ js_node_inspector_url AllocOnly js_node_inspector_wait_for_debugger ThrowOnly js_node_setheaders_entries_json Reenters js_node_sqlite_backup Reenters -js_node_sqlite_database_sync_aggregate Reenters -js_node_sqlite_database_sync_apply_changeset Reenters js_node_sqlite_database_sync_call ThrowOnly -js_node_sqlite_database_sync_close Reenters -js_node_sqlite_database_sync_create_session Reenters -js_node_sqlite_database_sync_create_tag_store Reenters -js_node_sqlite_database_sync_deserialize Reenters -js_node_sqlite_database_sync_dispose Reenters -js_node_sqlite_database_sync_enable_defensive Reenters -js_node_sqlite_database_sync_enable_load_extension Reenters -js_node_sqlite_database_sync_exec Reenters -js_node_sqlite_database_sync_function Reenters -js_node_sqlite_database_sync_is_open Reenters -js_node_sqlite_database_sync_is_transaction Reenters -js_node_sqlite_database_sync_limits Reenters -js_node_sqlite_database_sync_load_extension Reenters -js_node_sqlite_database_sync_location Reenters js_node_sqlite_database_sync_new Reenters -js_node_sqlite_database_sync_open Reenters -js_node_sqlite_database_sync_prepare Reenters -js_node_sqlite_database_sync_serialize Reenters -js_node_sqlite_database_sync_set_authorizer Reenters -js_node_sqlite_is_database_sync_handle Reenters -js_node_sqlite_is_limits_handle Reenters -js_node_sqlite_is_session_handle Reenters -js_node_sqlite_is_statement_sync_handle Reenters -js_node_sqlite_is_tag_store_handle Reenters js_node_sqlite_native_dispatch Reenters js_node_sqlite_session_call ThrowOnly -js_node_sqlite_session_changeset Reenters -js_node_sqlite_session_close Reenters -js_node_sqlite_session_dispose Reenters js_node_sqlite_session_new ThrowOnly -js_node_sqlite_session_patchset Reenters -js_node_sqlite_sql_tag_store_all Reenters -js_node_sqlite_sql_tag_store_capacity Reenters -js_node_sqlite_sql_tag_store_clear Reenters -js_node_sqlite_sql_tag_store_db Reenters -js_node_sqlite_sql_tag_store_get Reenters -js_node_sqlite_sql_tag_store_iterate Reenters -js_node_sqlite_sql_tag_store_run Reenters -js_node_sqlite_sql_tag_store_size Reenters -js_node_sqlite_statement_sync_all Reenters js_node_sqlite_statement_sync_call ThrowOnly -js_node_sqlite_statement_sync_columns Reenters -js_node_sqlite_statement_sync_expanded_sql Reenters -js_node_sqlite_statement_sync_get Reenters -js_node_sqlite_statement_sync_iterate Reenters js_node_sqlite_statement_sync_new ThrowOnly -js_node_sqlite_statement_sync_run Reenters -js_node_sqlite_statement_sync_set_allow_bare_named_parameters Reenters -js_node_sqlite_statement_sync_set_allow_unknown_named_parameters Reenters -js_node_sqlite_statement_sync_set_read_bigints Reenters -js_node_sqlite_statement_sync_set_return_arrays Reenters -js_node_sqlite_statement_sync_source_sql Reenters js_node_stream_add_abort_signal Reenters js_node_stream_compose Reenters js_node_stream_duplex_from_options Reenters diff --git a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv index c28b969992..2912140093 100644 --- a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv @@ -505,12 +505,20 @@ js_bun_plugin Reenters js_bun_spawn Reenters js_bun_sql_new Reenters js_bun_sqlite_database_call Reenters +js_bun_sqlite_database_close Reenters js_bun_sqlite_database_filename Reenters +js_bun_sqlite_database_load_extension Reenters js_bun_sqlite_database_new Reenters js_bun_sqlite_database_query Reenters js_bun_sqlite_database_run Reenters +js_bun_sqlite_database_serialize Reenters js_bun_sqlite_database_transaction Reenters +js_bun_sqlite_is_database_handle Reenters +js_bun_sqlite_is_statement_handle Reenters +js_bun_sqlite_statement_all Reenters js_bun_sqlite_statement_finalize Reenters +js_bun_sqlite_statement_get Reenters +js_bun_sqlite_statement_run Reenters js_bun_sqlite_statement_safe_integers Reenters js_bun_sqlite_statement_values Reenters js_bun_stderr Reenters @@ -1857,61 +1865,13 @@ js_node_inspector_url Reenters js_node_inspector_wait_for_debugger Reenters js_node_setheaders_entries_json Reenters js_node_sqlite_backup Reenters -js_node_sqlite_database_sync_aggregate Reenters -js_node_sqlite_database_sync_apply_changeset Reenters js_node_sqlite_database_sync_call Reenters -js_node_sqlite_database_sync_close Reenters -js_node_sqlite_database_sync_create_session Reenters -js_node_sqlite_database_sync_create_tag_store Reenters -js_node_sqlite_database_sync_deserialize Reenters -js_node_sqlite_database_sync_dispose Reenters -js_node_sqlite_database_sync_enable_defensive Reenters -js_node_sqlite_database_sync_enable_load_extension Reenters -js_node_sqlite_database_sync_exec Reenters -js_node_sqlite_database_sync_function Reenters -js_node_sqlite_database_sync_is_open Reenters -js_node_sqlite_database_sync_is_transaction Reenters -js_node_sqlite_database_sync_limits Reenters -js_node_sqlite_database_sync_load_extension Reenters -js_node_sqlite_database_sync_location Reenters js_node_sqlite_database_sync_new Reenters -js_node_sqlite_database_sync_open Reenters -js_node_sqlite_database_sync_prepare Reenters -js_node_sqlite_database_sync_serialize Reenters -js_node_sqlite_database_sync_set_authorizer Reenters -js_node_sqlite_is_database_sync_handle Reenters -js_node_sqlite_is_limits_handle Reenters -js_node_sqlite_is_session_handle Reenters -js_node_sqlite_is_statement_sync_handle Reenters -js_node_sqlite_is_tag_store_handle Reenters js_node_sqlite_native_dispatch Reenters js_node_sqlite_session_call Reenters -js_node_sqlite_session_changeset Reenters -js_node_sqlite_session_close Reenters -js_node_sqlite_session_dispose Reenters js_node_sqlite_session_new Reenters -js_node_sqlite_session_patchset Reenters -js_node_sqlite_sql_tag_store_all Reenters -js_node_sqlite_sql_tag_store_capacity Reenters -js_node_sqlite_sql_tag_store_clear Reenters -js_node_sqlite_sql_tag_store_db Reenters -js_node_sqlite_sql_tag_store_get Reenters -js_node_sqlite_sql_tag_store_iterate Reenters -js_node_sqlite_sql_tag_store_run Reenters -js_node_sqlite_sql_tag_store_size Reenters -js_node_sqlite_statement_sync_all Reenters js_node_sqlite_statement_sync_call Reenters -js_node_sqlite_statement_sync_columns Reenters -js_node_sqlite_statement_sync_expanded_sql Reenters -js_node_sqlite_statement_sync_get Reenters -js_node_sqlite_statement_sync_iterate Reenters js_node_sqlite_statement_sync_new Reenters -js_node_sqlite_statement_sync_run Reenters -js_node_sqlite_statement_sync_set_allow_bare_named_parameters Reenters -js_node_sqlite_statement_sync_set_allow_unknown_named_parameters Reenters -js_node_sqlite_statement_sync_set_read_bigints Reenters -js_node_sqlite_statement_sync_set_return_arrays Reenters -js_node_sqlite_statement_sync_source_sql Reenters js_node_stream_add_abort_signal Reenters js_node_stream_compose Reenters js_node_stream_duplex_from_options Reenters diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs index a937ff8f74..5bedddb7ac 100644 --- a/crates/perry-codegen/src/lower_call/builtin.rs +++ b/crates/perry-codegen/src/lower_call/builtin.rs @@ -759,6 +759,7 @@ pub(super) fn lower_builtin_new<'a>( } // node:sqlite DatabaseSync — keep full NaN-boxed values for path and // options so the runtime can preserve Node-shaped validation errors. + // The result is an ordinary object (#11919). "DatabaseSync" => { // #6986: `path_value` was held in a bare SSA register across // `options_value`'s lowering (an arbitrary user expression). @@ -774,12 +775,11 @@ pub(super) fn lower_builtin_new<'a>( None => undef(), }; let blk = ctx.block(); - let handle = blk.call( - I64, + Ok(Some(blk.call( + DOUBLE, "js_node_sqlite_database_sync_new", &[(DOUBLE, &path_value), (DOUBLE, &options_value)], - ); - Ok(Some(nanbox_pointer_inline(blk, &handle))) + ))) } "StatementSync" => { // #6986: `arg0` was held in a bare SSA register across `arg1`'s @@ -796,12 +796,11 @@ pub(super) fn lower_builtin_new<'a>( None => undef(), }; let blk = ctx.block(); - let handle = blk.call( - I64, + Ok(Some(blk.call( + DOUBLE, "js_node_sqlite_statement_sync_new", &[(DOUBLE, &arg0), (DOUBLE, &arg1)], - ); - Ok(Some(nanbox_pointer_inline(blk, &handle))) + ))) } "Session" => { // #6986: `arg0` was held in a bare SSA register across `arg1`'s @@ -818,12 +817,11 @@ pub(super) fn lower_builtin_new<'a>( None => undef(), }; let blk = ctx.block(); - let handle = blk.call( - I64, + Ok(Some(blk.call( + DOUBLE, "js_node_sqlite_session_new", &[(DOUBLE, &arg0), (DOUBLE, &arg1)], - ); - Ok(Some(nanbox_pointer_inline(blk, &handle))) + ))) } // async_hooks.AsyncLocalStorage — `new AsyncLocalStorage()` produces a // real handle so `.run(store, cb)` / `.getStore()` / `.enterWith(store)` diff --git a/crates/perry-codegen/src/lower_call/native_table/databases.rs b/crates/perry-codegen/src/lower_call/native_table/databases.rs index 1f95b01884..66be937acd 100644 --- a/crates/perry-codegen/src/lower_call/native_table/databases.rs +++ b/crates/perry-codegen/src/lower_call/native_table/databases.rs @@ -131,7 +131,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "close", class_filter: Some("Database"), - runtime: "js_node_sqlite_database_sync_close", + runtime: "js_bun_sqlite_database_close", args: &[], ret: NR_I32, }, @@ -140,7 +140,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "serialize", class_filter: Some("Database"), - runtime: "js_node_sqlite_database_sync_serialize", + runtime: "js_bun_sqlite_database_serialize", args: &[NA_F64], ret: NR_GCPTR, }, @@ -149,7 +149,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "loadExtension", class_filter: Some("Database"), - runtime: "js_node_sqlite_database_sync_load_extension", + runtime: "js_bun_sqlite_database_load_extension", args: &[NA_F64], ret: NR_I32, }, @@ -167,7 +167,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "run", class_filter: Some("Statement"), - runtime: "js_node_sqlite_statement_sync_run", + runtime: "js_bun_sqlite_statement_run", args: &[NA_VARARGS], ret: NR_GCPTR, }, @@ -176,7 +176,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "get", class_filter: Some("Statement"), - runtime: "js_node_sqlite_statement_sync_get", + runtime: "js_bun_sqlite_statement_get", args: &[NA_VARARGS], ret: NR_F64, }, @@ -185,7 +185,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "all", class_filter: Some("Statement"), - runtime: "js_node_sqlite_statement_sync_all", + runtime: "js_bun_sqlite_statement_all", args: &[NA_VARARGS], ret: NR_GCPTR, }, @@ -217,6 +217,9 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ ret: NR_VOID, }, // ========== node:sqlite ========== + // DatabaseSync, StatementSync, StatementSyncIterator, SQLTagStore and + // Session are ordinary objects (#11919): every receiver method resolves + // from the family prototype. Only the call forms and `backup` stay here. NativeModSig { module: "sqlite", has_receiver: false, @@ -224,7 +227,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ class_filter: None, runtime: "js_node_sqlite_database_sync_call", args: &[NA_F64, NA_F64], - ret: NR_HANDLE_ID, + ret: NR_F64, }, NativeModSig { module: "sqlite", @@ -233,7 +236,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ class_filter: None, runtime: "js_node_sqlite_session_call", args: &[NA_F64, NA_F64], - ret: NR_HANDLE_ID, + ret: NR_F64, }, NativeModSig { module: "sqlite", @@ -242,7 +245,7 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ class_filter: None, runtime: "js_node_sqlite_statement_sync_call", args: &[NA_F64, NA_F64], - ret: NR_HANDLE_ID, + ret: NR_F64, }, NativeModSig { module: "sqlite", @@ -253,409 +256,4 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[ args: &[NA_F64, NA_F64, NA_F64], ret: NR_PROMISE, }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "open", - class_filter: None, - runtime: "js_node_sqlite_database_sync_open", - args: &[], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "close", - class_filter: None, - runtime: "js_node_sqlite_database_sync_close", - args: &[], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "__perry_dispose__", - class_filter: None, - runtime: "js_node_sqlite_database_sync_dispose", - args: &[], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "@@__perry_wk_dispose", - class_filter: None, - runtime: "js_node_sqlite_database_sync_dispose", - args: &[], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "exec", - class_filter: None, - runtime: "js_node_sqlite_database_sync_exec", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "prepare", - class_filter: None, - runtime: "js_node_sqlite_database_sync_prepare", - args: &[NA_F64, NA_F64], - ret: NR_HANDLE_ID, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "serialize", - class_filter: None, - runtime: "js_node_sqlite_database_sync_serialize", - args: &[NA_F64], - ret: NR_GCPTR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "deserialize", - class_filter: None, - runtime: "js_node_sqlite_database_sync_deserialize", - args: &[NA_F64], - ret: NR_VOID, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "function", - class_filter: Some("DatabaseSync"), - runtime: "js_node_sqlite_database_sync_function", - args: &[NA_F64, NA_F64, NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "aggregate", - class_filter: Some("DatabaseSync"), - runtime: "js_node_sqlite_database_sync_aggregate", - args: &[NA_F64, NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "enableDefensive", - class_filter: Some("DatabaseSync"), - runtime: "js_node_sqlite_database_sync_enable_defensive", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "setAuthorizer", - class_filter: Some("DatabaseSync"), - runtime: "js_node_sqlite_database_sync_set_authorizer", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "createTagStore", - class_filter: Some("DatabaseSync"), - runtime: "js_node_sqlite_database_sync_create_tag_store", - args: &[NA_F64], - ret: NR_HANDLE_ID, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "createSession", - class_filter: Some("DatabaseSync"), - runtime: "js_node_sqlite_database_sync_create_session", - args: &[NA_F64], - ret: NR_HANDLE_ID, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "applyChangeset", - class_filter: Some("DatabaseSync"), - runtime: "js_node_sqlite_database_sync_apply_changeset", - args: &[NA_F64, NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "enableLoadExtension", - class_filter: None, - runtime: "js_node_sqlite_database_sync_enable_load_extension", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "loadExtension", - class_filter: None, - runtime: "js_node_sqlite_database_sync_load_extension", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "location", - class_filter: None, - runtime: "js_node_sqlite_database_sync_location", - args: &[NA_F64], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "isOpen", - class_filter: None, - runtime: "js_node_sqlite_database_sync_is_open", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "isTransaction", - class_filter: None, - runtime: "js_node_sqlite_database_sync_is_transaction", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "limits", - class_filter: None, - runtime: "js_node_sqlite_database_sync_limits", - args: &[], - ret: NR_HANDLE_ID, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "changeset", - class_filter: Some("Session"), - runtime: "js_node_sqlite_session_changeset", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "patchset", - class_filter: Some("Session"), - runtime: "js_node_sqlite_session_patchset", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "close", - class_filter: Some("Session"), - runtime: "js_node_sqlite_session_close", - args: &[], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "__perry_dispose__", - class_filter: Some("Session"), - runtime: "js_node_sqlite_session_dispose", - args: &[], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "@@__perry_wk_dispose", - class_filter: Some("Session"), - runtime: "js_node_sqlite_session_dispose", - args: &[], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "run", - class_filter: Some("SQLTagStore"), - runtime: "js_node_sqlite_sql_tag_store_run", - args: &[NA_VARARGS], - ret: NR_GCPTR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "get", - class_filter: Some("SQLTagStore"), - runtime: "js_node_sqlite_sql_tag_store_get", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "all", - class_filter: Some("SQLTagStore"), - runtime: "js_node_sqlite_sql_tag_store_all", - args: &[NA_VARARGS], - ret: NR_GCPTR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "iterate", - class_filter: Some("SQLTagStore"), - runtime: "js_node_sqlite_sql_tag_store_iterate", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "clear", - class_filter: Some("SQLTagStore"), - runtime: "js_node_sqlite_sql_tag_store_clear", - args: &[], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "size", - class_filter: Some("SQLTagStore"), - runtime: "js_node_sqlite_sql_tag_store_size", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "capacity", - class_filter: Some("SQLTagStore"), - runtime: "js_node_sqlite_sql_tag_store_capacity", - args: &[], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "db", - class_filter: Some("SQLTagStore"), - runtime: "js_node_sqlite_sql_tag_store_db", - args: &[], - ret: NR_HANDLE_ID, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "run", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_run", - args: &[NA_VARARGS], - ret: NR_GCPTR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "get", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_get", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "all", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_all", - args: &[NA_VARARGS], - ret: NR_GCPTR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "iterate", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_iterate", - args: &[NA_VARARGS], - ret: NR_F64, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "columns", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_columns", - args: &[], - ret: NR_GCPTR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "setReadBigInts", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_set_read_bigints", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "setReturnArrays", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_set_return_arrays", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "setAllowBareNamedParameters", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_set_allow_bare_named_parameters", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "setAllowUnknownNamedParameters", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_set_allow_unknown_named_parameters", - args: &[NA_F64], - ret: NR_I32, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "sourceSQL", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_source_sql", - args: &[], - ret: NR_STR, - }, - NativeModSig { - module: "sqlite", - has_receiver: true, - method: "expandedSQL", - class_filter: None, - runtime: "js_node_sqlite_statement_sync_expanded_sql", - args: &[], - ret: NR_STR, - }, ]; diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs index 9bfe09aac8..4119224953 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs @@ -31,118 +31,35 @@ pub(crate) fn declare_data_stores(module: &mut LlModule) { &[I64, DOUBLE], ); module.declare_function("js_bun_sqlite_statement_finalize", VOID, &[I64]); + module.declare_function("js_bun_sqlite_database_close", I32, &[I64]); + module.declare_function("js_bun_sqlite_database_serialize", I64, &[I64, DOUBLE]); + module.declare_function("js_bun_sqlite_database_load_extension", I32, &[I64, DOUBLE]); + module.declare_function("js_bun_sqlite_statement_run", I64, &[I64, I64]); + module.declare_function("js_bun_sqlite_statement_get", DOUBLE, &[I64, I64]); + module.declare_function("js_bun_sqlite_statement_all", I64, &[I64, I64]); module.declare_function("js_node_sqlite_backup", I64, &[DOUBLE, DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_database_sync_call", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_database_sync_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_database_sync_open", I32, &[I64]); - module.declare_function("js_node_sqlite_database_sync_close", I32, &[I64]); - module.declare_function("js_node_sqlite_database_sync_dispose", I32, &[I64]); - module.declare_function("js_node_sqlite_database_sync_exec", I32, &[I64, DOUBLE]); module.declare_function( - "js_node_sqlite_database_sync_prepare", - I64, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_function", - I32, - &[I64, DOUBLE, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_aggregate", - I32, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_enable_defensive", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_set_authorizer", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_create_tag_store", - I64, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_create_session", - I64, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_apply_changeset", + "js_node_sqlite_database_sync_call", DOUBLE, - &[I64, DOUBLE, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_enable_load_extension", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_database_sync_load_extension", - I32, - &[I64, DOUBLE], + &[DOUBLE, DOUBLE], ); module.declare_function( - "js_node_sqlite_database_sync_location", + "js_node_sqlite_database_sync_new", DOUBLE, - &[I64, DOUBLE], + &[DOUBLE, DOUBLE], ); - module.declare_function("js_node_sqlite_database_sync_is_open", DOUBLE, &[I64]); module.declare_function( - "js_node_sqlite_database_sync_is_transaction", + "js_node_sqlite_statement_sync_call", DOUBLE, - &[I64], + &[DOUBLE, DOUBLE], ); - module.declare_function("js_node_sqlite_database_sync_limits", I64, &[I64]); - module.declare_function("js_node_sqlite_statement_sync_call", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_statement_sync_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_statement_sync_run", I64, &[I64, I64]); - module.declare_function("js_node_sqlite_statement_sync_get", DOUBLE, &[I64, I64]); - module.declare_function("js_node_sqlite_statement_sync_all", I64, &[I64, I64]); - module.declare_function("js_node_sqlite_statement_sync_iterate", DOUBLE, &[I64, I64]); - module.declare_function("js_node_sqlite_statement_sync_columns", I64, &[I64]); module.declare_function( - "js_node_sqlite_statement_sync_set_read_bigints", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_statement_sync_set_return_arrays", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_statement_sync_set_allow_bare_named_parameters", - I32, - &[I64, DOUBLE], - ); - module.declare_function( - "js_node_sqlite_statement_sync_set_allow_unknown_named_parameters", - I32, - &[I64, DOUBLE], + "js_node_sqlite_statement_sync_new", + DOUBLE, + &[DOUBLE, DOUBLE], ); - module.declare_function("js_node_sqlite_statement_sync_source_sql", I64, &[I64]); - module.declare_function("js_node_sqlite_statement_sync_expanded_sql", I64, &[I64]); - module.declare_function("js_node_sqlite_sql_tag_store_run", I64, &[I64, I64]); - module.declare_function("js_node_sqlite_sql_tag_store_get", DOUBLE, &[I64, I64]); - module.declare_function("js_node_sqlite_sql_tag_store_all", I64, &[I64, I64]); - module.declare_function("js_node_sqlite_sql_tag_store_iterate", DOUBLE, &[I64, I64]); - module.declare_function("js_node_sqlite_sql_tag_store_clear", I32, &[I64]); - module.declare_function("js_node_sqlite_sql_tag_store_size", DOUBLE, &[I64]); - module.declare_function("js_node_sqlite_sql_tag_store_capacity", DOUBLE, &[I64]); - module.declare_function("js_node_sqlite_sql_tag_store_db", I64, &[I64]); - module.declare_function("js_node_sqlite_session_call", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_session_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function("js_node_sqlite_session_changeset", I64, &[I64]); - module.declare_function("js_node_sqlite_session_patchset", I64, &[I64]); - module.declare_function("js_node_sqlite_session_close", I32, &[I64]); - module.declare_function("js_node_sqlite_session_dispose", I32, &[I64]); + module.declare_function("js_node_sqlite_session_call", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_node_sqlite_session_new", DOUBLE, &[DOUBLE, DOUBLE]); // ========== OS ========== module.declare_function("js_os_cpus", I64, &[]); diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index 6d9d9cdf62..1d1951c598 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -504,12 +504,20 @@ js_bun_server_stop f64 i64,f64 js_bun_spawn f64 f64,f64 js_bun_sql_new f64 f64,f64 js_bun_sqlite_database_call i64 f64,f64 +js_bun_sqlite_database_close i32s i64 js_bun_sqlite_database_filename ptr i64 +js_bun_sqlite_database_load_extension i32s i64,f64 js_bun_sqlite_database_new i64 f64,f64 js_bun_sqlite_database_query i64 i64,f64 js_bun_sqlite_database_run ptr i64,f64,ptr +js_bun_sqlite_database_serialize ptr i64,f64 js_bun_sqlite_database_transaction ptr i64,f64 +js_bun_sqlite_is_database_handle i32s i64 +js_bun_sqlite_is_statement_handle i32s i64 +js_bun_sqlite_statement_all ptr i64,ptr js_bun_sqlite_statement_finalize void i64 +js_bun_sqlite_statement_get f64 i64,ptr +js_bun_sqlite_statement_run ptr i64,ptr js_bun_sqlite_statement_safe_integers f64 i64,f64 js_bun_sqlite_statement_values ptr i64,ptr js_bun_stderr f64 @@ -2329,61 +2337,13 @@ js_node_inspector_url f64 js_node_inspector_wait_for_debugger f64 js_node_setheaders_entries_json ptr f64 js_node_sqlite_backup ptr f64,f64,f64 -js_node_sqlite_database_sync_aggregate i32s i64,f64,f64 -js_node_sqlite_database_sync_apply_changeset f64 i64,f64,f64 -js_node_sqlite_database_sync_call i64 f64,f64 -js_node_sqlite_database_sync_close i32s i64 -js_node_sqlite_database_sync_create_session i64 i64,f64 -js_node_sqlite_database_sync_create_tag_store i64 i64,f64 -js_node_sqlite_database_sync_deserialize i32s i64,f64 -js_node_sqlite_database_sync_dispose i32s i64 -js_node_sqlite_database_sync_enable_defensive i32s i64,f64 -js_node_sqlite_database_sync_enable_load_extension i32s i64,f64 -js_node_sqlite_database_sync_exec i32s i64,f64 -js_node_sqlite_database_sync_function i32s i64,f64,f64,f64 -js_node_sqlite_database_sync_is_open f64 i64 -js_node_sqlite_database_sync_is_transaction f64 i64 -js_node_sqlite_database_sync_limits i64 i64 -js_node_sqlite_database_sync_load_extension i32s i64,f64 -js_node_sqlite_database_sync_location f64 i64,f64 -js_node_sqlite_database_sync_new i64 f64,f64 -js_node_sqlite_database_sync_open i32s i64 -js_node_sqlite_database_sync_prepare i64 i64,f64,f64 -js_node_sqlite_database_sync_serialize ptr i64,f64 -js_node_sqlite_database_sync_set_authorizer i32s i64,f64 -js_node_sqlite_is_database_sync_handle i32s i64 -js_node_sqlite_is_limits_handle i32s i64 -js_node_sqlite_is_session_handle i32s i64 -js_node_sqlite_is_statement_sync_handle i32s i64 -js_node_sqlite_is_tag_store_handle i32s i64 +js_node_sqlite_database_sync_call f64 f64,f64 +js_node_sqlite_database_sync_new f64 f64,f64 js_node_sqlite_native_dispatch f64 ptr,usize,ptr,usize,i32s -js_node_sqlite_session_call i64 f64,f64 -js_node_sqlite_session_changeset ptr i64 -js_node_sqlite_session_close i32s i64 -js_node_sqlite_session_dispose i32s i64 -js_node_sqlite_session_new i64 f64,f64 -js_node_sqlite_session_patchset ptr i64 -js_node_sqlite_sql_tag_store_all ptr i64,ptr -js_node_sqlite_sql_tag_store_capacity f64 i64 -js_node_sqlite_sql_tag_store_clear i32s i64 -js_node_sqlite_sql_tag_store_db i64 i64 -js_node_sqlite_sql_tag_store_get f64 i64,ptr -js_node_sqlite_sql_tag_store_iterate f64 i64,ptr -js_node_sqlite_sql_tag_store_run ptr i64,ptr -js_node_sqlite_sql_tag_store_size f64 i64 -js_node_sqlite_statement_sync_all ptr i64,ptr -js_node_sqlite_statement_sync_call i64 f64,f64 -js_node_sqlite_statement_sync_columns ptr i64 -js_node_sqlite_statement_sync_expanded_sql ptr i64 -js_node_sqlite_statement_sync_get f64 i64,ptr -js_node_sqlite_statement_sync_iterate f64 i64,ptr -js_node_sqlite_statement_sync_new i64 f64,f64 -js_node_sqlite_statement_sync_run ptr i64,ptr -js_node_sqlite_statement_sync_set_allow_bare_named_parameters i32s i64,f64 -js_node_sqlite_statement_sync_set_allow_unknown_named_parameters i32s i64,f64 -js_node_sqlite_statement_sync_set_read_bigints i32s i64,f64 -js_node_sqlite_statement_sync_set_return_arrays i32s i64,f64 -js_node_sqlite_statement_sync_source_sql ptr i64 +js_node_sqlite_session_call f64 f64,f64 +js_node_sqlite_session_new f64 f64,f64 +js_node_sqlite_statement_sync_call f64 f64,f64 +js_node_sqlite_statement_sync_new f64 f64,f64 js_node_stream_add_abort_signal f64 f64,f64 js_node_stream_compose f64 ptr js_node_stream_duplex_from_options f64 f64,f64 diff --git a/crates/perry-hir/src/destructuring/var_decl/native_new.rs b/crates/perry-hir/src/destructuring/var_decl/native_new.rs index 571b37511c..d9f0469f61 100644 --- a/crates/perry-hir/src/destructuring/var_decl/native_new.rs +++ b/crates/perry-hir/src/destructuring/var_decl/native_new.rs @@ -252,8 +252,6 @@ pub(crate) fn register_native_from_new_and_calls( Some("PoolConnection") } ("better-sqlite3", "prepare") => Some("Statement"), - ("sqlite", "prepare") => Some("StatementSync"), - ("sqlite", "createSession") => Some("Session"), _ => None, }; if let Some(class_name) = returns_handle { @@ -336,7 +334,9 @@ fn native_instance_for_new_ident( let module_name = if let Some((m, method)) = ctx.lookup_native_module(local_name) { match (m, method) { ("url", Some("URL" | "URLSearchParams")) - | ("util", Some("TextEncoder" | "TextDecoder")) => None, + | ("util", Some("TextEncoder" | "TextDecoder")) + // node:sqlite objects are ordinary objects with prototypes (#11919). + | ("sqlite", Some("DatabaseSync" | "StatementSync" | "Session")) => None, _ => Some(m.to_string()), } } else if user_class_defined { @@ -393,7 +393,6 @@ fn native_instance_for_new_member( | ("net" | "node:net", "BlockList" | "SocketAddress") | ("dns" | "dns/promises", "Resolver") | ("vm", "SourceTextModule" | "SyntheticModule") - | ("sqlite", "DatabaseSync") ) || (module_name == "stream" && STREAM_CTOR_NAMES.contains(&class_name)); if !is_known_native_class { diff --git a/crates/perry-hir/src/js_transform/local_natives.rs b/crates/perry-hir/src/js_transform/local_natives.rs index d23875a0af..d6a1f79e6c 100644 --- a/crates/perry-hir/src/js_transform/local_natives.rs +++ b/crates/perry-hir/src/js_transform/local_natives.rs @@ -657,9 +657,6 @@ pub fn fix_class_field_expr( pub fn chained_native_class(module: &str, prior_method: &str) -> Option<&'static str> { match (module, prior_method) { ("better-sqlite3", "prepare") => Some("Statement"), - ("sqlite", "prepare") => Some("StatementSync"), - ("sqlite", "createTagStore") => Some("SQLTagStore"), - ("sqlite", "createSession") => Some("Session"), ("mysql2", "getConnection") | ("mysql2/promise", "getConnection") => Some("PoolConnection"), ("pg", "connect") => Some("PoolClient"), _ => None, @@ -1386,15 +1383,6 @@ pub fn detect_native_instance_creation_with_context( ("better-sqlite3", "Database", "prepare") => { Some((module.clone(), "Statement".to_string())) } - ("sqlite", "DatabaseSync", "prepare") => { - Some((module.clone(), "StatementSync".to_string())) - } - ("sqlite", "DatabaseSync", "createTagStore") => { - Some((module.clone(), "SQLTagStore".to_string())) - } - ("sqlite", "DatabaseSync", "createSession") => { - Some((module.clone(), "Session".to_string())) - } ("bun:sqlite", "Database", "query" | "prepare") => { Some((module.clone(), "Statement".to_string())) } @@ -1422,15 +1410,6 @@ pub fn detect_native_instance_creation_with_context( ("better-sqlite3", "Database", "prepare") => { Some((module.clone(), "Statement".to_string())) } - ("sqlite", "DatabaseSync", "prepare") => { - Some((module.clone(), "StatementSync".to_string())) - } - ("sqlite", "DatabaseSync", "createTagStore") => { - Some((module.clone(), "SQLTagStore".to_string())) - } - ("sqlite", "DatabaseSync", "createSession") => { - Some((module.clone(), "Session".to_string())) - } ("bun:sqlite", "Database", "query" | "prepare") => { Some((module.clone(), "Statement".to_string())) } @@ -1442,14 +1421,10 @@ pub fn detect_native_instance_creation_with_context( None } Expr::New { class_name, .. } => { - // new Database(...) → better-sqlite3 Database instance - // new DatabaseSync(...) → node:sqlite DatabaseSync instance - // (#3183); both reuse the rusqlite backend, tagged under - // distinct modules so NativeModSig dispatch routes correctly. + // new Database(...) → better-sqlite3 Database instance. A + // node:sqlite DatabaseSync is an ordinary object (#11919). match class_name.as_str() { "Database" => Some(("better-sqlite3".to_string(), "Database".to_string())), - "DatabaseSync" => Some(("sqlite".to_string(), "DatabaseSync".to_string())), - "StatementSync" => Some(("sqlite".to_string(), "StatementSync".to_string())), "BunSqliteDatabase" => Some(("bun:sqlite".to_string(), "Database".to_string())), "Transpiler" => Some(("bun".to_string(), "Transpiler".to_string())), _ => None, diff --git a/crates/perry-hir/src/lower/expr_call/static_and_instance.rs b/crates/perry-hir/src/lower/expr_call/static_and_instance.rs index 34246dd8a5..f710668585 100644 --- a/crates/perry-hir/src/lower/expr_call/static_and_instance.rs +++ b/crates/perry-hir/src/lower/expr_call/static_and_instance.rs @@ -511,9 +511,6 @@ pub(super) fn try_static_method_and_instance( let chained_class: Option<&'static str> = match (module.as_str(), prior_method.as_str()) { ("better-sqlite3", "prepare") => Some("Statement"), - ("sqlite", "prepare") => Some("StatementSync"), - ("sqlite", "createTagStore") => Some("SQLTagStore"), - ("sqlite", "createSession") => Some("Session"), ("mysql2", "getConnection") | ("mysql2/promise", "getConnection") => { Some("PoolConnection") } diff --git a/crates/perry-hir/src/lower/expr_member.rs b/crates/perry-hir/src/lower/expr_member.rs index 8cf3ba4e83..24a50d67d3 100644 --- a/crates/perry-hir/src/lower/expr_member.rs +++ b/crates/perry-hir/src/lower/expr_member.rs @@ -1559,82 +1559,6 @@ fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Re object: Box::new(object_expr), property: property_name, }); - } else if module_name == "sqlite" - && class_name == "DatabaseSync" - && matches!( - property_name.as_str(), - "open" - | "close" - | "exec" - | "prepare" - | "function" - | "aggregate" - | "enableDefensive" - | "setAuthorizer" - | "createTagStore" - | "createSession" - | "applyChangeset" - | "enableLoadExtension" - | "loadExtension" - | "location" - ) - { - // `node:sqlite` DatabaseSync methods are callable fields. - // Bare reads like `typeof db.close` must not invoke the - // lifecycle method as a zero-arg getter. - let object_expr = lower_expr(ctx, &member.obj)?; - return Ok(Expr::PropertyGet { - byte_offset: 0, - object: Box::new(object_expr), - property: property_name, - }); - } else if module_name == "sqlite" - && class_name == "Session" - && matches!(property_name.as_str(), "changeset" | "patchset" | "close") - { - let object_expr = lower_expr(ctx, &member.obj)?; - return Ok(Expr::PropertyGet { - byte_offset: 0, - object: Box::new(object_expr), - property: property_name, - }); - } else if module_name == "sqlite" - && class_name == "SQLTagStore" - && matches!( - property_name.as_str(), - "run" | "get" | "all" | "iterate" | "clear" - ) - { - let object_expr = lower_expr(ctx, &member.obj)?; - return Ok(Expr::PropertyGet { - byte_offset: 0, - object: Box::new(object_expr), - property: property_name, - }); - } else if module_name == "sqlite" - && class_name == "StatementSync" - && matches!( - property_name.as_str(), - "run" - | "get" - | "all" - | "iterate" - | "columns" - | "setReadBigInts" - | "setReturnArrays" - | "setAllowBareNamedParameters" - | "setAllowUnknownNamedParameters" - ) - { - // `node:sqlite` StatementSync methods are callable fields. - // Getter properties such as `sourceSQL` and `expandedSQL` - // keep the native getter path below. - let object_expr = lower_expr(ctx, &member.obj)?; - return Ok(Expr::PropertyGet { - byte_offset: 0, - object: Box::new(object_expr), - property: property_name, - }); } else if module_name == "Headers" && is_headers_method_name(&property_name) { // A bare Fetch Headers method read (`headers.entries`) is a // function value, not a zero-arg native call. The call form diff --git a/crates/perry-hir/src/lower/expr_member/native_dispatch.rs b/crates/perry-hir/src/lower/expr_member/native_dispatch.rs index f9f4e999dd..9232839f20 100644 --- a/crates/perry-hir/src/lower/expr_member/native_dispatch.rs +++ b/crates/perry-hir/src/lower/expr_member/native_dispatch.rs @@ -143,8 +143,8 @@ pub(crate) fn is_native_dispatch_member(module: &str, class: &str, prop: &str) - // the dispatching fallback. They have no observed user-own-property // surface, so preserve that: dispatch any member not handled by the // PropertyGet arms above. - "dns" | "dns/promises" | "dgram" | "inspector" | "inspector/promises" | "sqlite" - | "url" | "worker_threads" | "util" | "sys" | "console" | "Headers" => true, + "dns" | "dns/promises" | "dgram" | "inspector" | "inspector/promises" | "url" + | "worker_threads" | "util" | "sys" | "console" | "Headers" => true, // Any other module (e.g. a mis-tagged `debug` createDebug value): a bare // member read is an own-property GET, never an invoking dispatch. _ => false, diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index 3f4d2c2ce7..51e0679669 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -701,7 +701,20 @@ pub(crate) fn lower_module_decl( // HANDLE_*_DISPATCH; don't register them as // typed native instances or property reads can // be routed through native-class method lowering. + // node:sqlite objects are ordinary objects with + // prototypes (#11919), whatever the local alias. + let sqlite_export = ctx + .lookup_native_module(class_name) + .and_then(|(m, export)| (m == "sqlite").then_some(export)) + .flatten(); let module_name = match (class_name, module_name.as_deref()) { + _ if matches!( + sqlite_export, + Some("DatabaseSync" | "StatementSync" | "Session") + ) => + { + None + } ("StringDecoder", Some("string_decoder")) | ("Recoverable" | "REPLServer", Some("repl")) | ( @@ -1070,12 +1083,6 @@ pub(crate) fn lower_module_decl( "inspector" | "inspector/promises", "Session" ) - | ( - "sqlite", - "DatabaseSync" - | "Session" - | "StatementSync" - ) ); if is_known_native_class { ctx.register_native_instance( @@ -1130,15 +1137,6 @@ pub(crate) fn lower_module_decl( ("better-sqlite3", "prepare") => { Some("Statement") } - ("sqlite", "prepare") => { - Some("StatementSync") - } - ("sqlite", "createTagStore") => { - Some("SQLTagStore") - } - ("sqlite", "createSession") => { - Some("Session") - } _ => None, }; if let Some(class_name) = returns_handle { diff --git a/crates/perry-runtime/src/array/iter_object.rs b/crates/perry-runtime/src/array/iter_object.rs index 12637d5e52..0dc9a44d40 100644 --- a/crates/perry-runtime/src/array/iter_object.rs +++ b/crates/perry-runtime/src/array/iter_object.rs @@ -43,11 +43,6 @@ const ITER_RESULT_CACHE_FIELD: u32 = 5; const KIND_VALUES: i32 = 0; const KIND_KEYS: i32 = 1; const KIND_ENTRIES: i32 = 2; -/// Values iterator with Node's `node:sqlite` result protocol (#6561): -/// exhaustion and `return()` yield `{ done: true, value: null }` (the -/// array iterator yields `value: undefined`), and `return()` terminates -/// the iterator. Produced only by `StatementSync.prototype.iterate()`. -const KIND_VALUES_NULL_DONE: i32 = 3; /// Values iterator over a live Arguments exotic object. Unlike an Array /// iterator this reads `length` and each indexed property from the Arguments /// object on every step, so mutations made before exhaustion are observable. @@ -121,37 +116,6 @@ pub fn array_values_iter(arr_f64: f64) -> f64 { unsafe { alloc_iterator(arr_ptr, KIND_VALUES) } } -/// Values iterator whose done-result carries `value: null` and whose -/// `return()` terminates it — the `node:sqlite` `iterate()` protocol -/// (#6561). See [`KIND_VALUES_NULL_DONE`]. -pub fn array_values_iter_null_done( - arr_f64: f64, - iteration_epoch: &std::sync::atomic::AtomicU64, - epoch: u64, -) -> f64 { - let arr_ptr = unbox_array_ptr(arr_f64); - if arr_ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let obj = js_object_alloc(ARRAY_ITERATOR_CLASS_ID, 6); - js_object_set_field( - obj, - 0, - JSValue::from_bits(js_nanbox_pointer(arr_ptr as i64).to_bits()), - ); - js_object_set_field(obj, 1, JSValue::number(0.0)); - js_object_set_field(obj, 2, JSValue::number(KIND_VALUES_NULL_DONE as f64)); - js_object_set_field( - obj, - 3, - JSValue::pointer(iteration_epoch as *const _ as *const u8), - ); - js_object_set_field(obj, 4, JSValue::number(epoch as f64)); - js_object_set_field(obj, ITER_RESULT_CACHE_FIELD, JSValue::undefined()); - crate::object::attach_iterator_prototype(obj, ARRAY_ITERATOR_CLASS_ID); - js_nanbox_pointer(obj as i64) -} - /// `arr.keys()` iterator — yields each index `0..length`. pub fn array_keys_iter(arr_f64: f64) -> f64 { let arr_ptr = unbox_array_ptr(arr_f64); @@ -623,7 +587,7 @@ pub extern "C" fn js_array_entries_iter_obj(arr: *const ArrayHeader) -> i64 { // the keys array's address. Both constructors now come from the single // `crate::iter_result` implementation, which shares one keys array (and so one // shape) per key order per thread and allocates only the result object. -use crate::iter_result::{make_iter_result, make_sqlite_iter_result}; +use crate::iter_result::make_iter_result; unsafe fn make_pair_array(idx: u32, value: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); @@ -690,26 +654,10 @@ unsafe fn dispatch_array_iterator_method_inner( let iter_h = scope.root_nanbox_f64(js_nanbox_pointer(iter_obj as i64)); let iter_obj = || js_nanbox_get_pointer(iter_h.get_nanbox_f64()) as *mut ObjectHeader; - // Field 2: iterator kind — read up front so the exhausted paths can pick - // the kind's done-value (`null` for KIND_VALUES_NULL_DONE, `undefined` - // otherwise). + // Field 2: iterator kind. let kind = f64::from_bits(js_object_get_field(iter_obj(), 2).bits()) as i32; - let done_value = || { - if kind == KIND_VALUES_NULL_DONE { - JSValue::null() - } else { - JSValue::undefined() - } - }; - // `node:sqlite`'s iterator yields `{ done, value }`; every other kind - // yields `{ value, done }`. The key order is observable through - // `Object.keys`/`JSON.stringify`, so it picks the shared keys array (and - // therefore the shape) the result is built with. - let result_order = if kind == KIND_VALUES_NULL_DONE { - crate::iter_result::IterResultOrder::DoneValue - } else { - crate::iter_result::IterResultOrder::ValueDone - }; + let done_value = JSValue::undefined; + let result_order = crate::iter_result::IterResultOrder::ValueDone; match method_name { "next" => { if honor_override { @@ -720,26 +668,10 @@ unsafe fn dispatch_array_iterator_method_inner( return result; } } - if kind == KIND_VALUES_NULL_DONE { - let epoch_ptr = js_nanbox_get_pointer(f64::from_bits( - js_object_get_field(iter_obj(), 3).bits(), - )) as *const std::sync::atomic::AtomicU64; - let expected = f64::from_bits(js_object_get_field(iter_obj(), 4).bits()) as u64; - if epoch_ptr.is_null() - || (*epoch_ptr).load(std::sync::atomic::Ordering::Relaxed) != expected - { - crate::fs::validate::throw_error_with_code( - "Statement iterator has been invalidated", - "ERR_INVALID_STATE", - ); - } - } // Field 0: backing array pointer (NaN-boxed). let backing_field = js_object_get_field(iter_obj(), 0); let backing_f64 = f64::from_bits(backing_field.bits()); - // Iterators clear their backing array at exhaustion. A completed - // SQLite statement iterator is also permanently closed; calling - // `StatementSync::iterate()` again creates a separate iterator. + // Iterators clear their backing array at exhaustion. if JSValue::from_bits(backing_f64.to_bits()).is_undefined() { return crate::iter_result::emit_iter_result_cached( &scope, @@ -806,7 +738,7 @@ unsafe fn dispatch_array_iterator_method_inner( let elem_h = scope.root_nanbox_f64(elem); let value = match kind { - KIND_VALUES | KIND_VALUES_NULL_DONE | KIND_ARGUMENTS_VALUES | KIND_PROXY_VALUES => { + KIND_VALUES | KIND_ARGUMENTS_VALUES | KIND_PROXY_VALUES => { JSValue::from_bits(elem_h.get_nanbox_u64()) } KIND_KEYS => JSValue::number(idx as f64), @@ -834,71 +766,7 @@ unsafe fn dispatch_array_iterator_method_inner( // `return`/`throw` are part of the iterator spec; Node's array // iterator inherits them from %IteratorPrototype%. Return a // `{ value: undefined, done: true }` shape for early-exit code. - // KIND_VALUES_NULL_DONE (`node:sqlite` iterate()) additionally - // TERMINATES the iterator on `return()` — a later `.next()` stays - // `{ done: true, value: null }` — matching Node's sqlite iterator. - "return" | "throw" => { - if kind == KIND_VALUES_NULL_DONE { - js_object_set_field(iter_obj(), 0, JSValue::undefined()); - } - if kind == KIND_VALUES_NULL_DONE { - make_sqlite_iter_result(done_value(), true) - } else { - make_iter_result(done_value(), true) - } - } + "return" | "throw" => make_iter_result(done_value(), true), _ => f64::from_bits(TAG_UNDEFINED), } } - -#[cfg(test)] -mod sqlite_iterator_tests { - use super::*; - use std::sync::atomic::AtomicU64; - - unsafe fn result_fields(result: f64) -> (u64, u64) { - let result = js_nanbox_get_pointer(result) as *mut ObjectHeader; - ( - js_object_get_field(result, 0).bits(), - js_object_get_field(result, 1).bits(), - ) - } - - #[test] - fn sqlite_iterator_stays_exhausted_after_fused_for_of_drain() { - let _serialized = crate::array::test_serialize(); - let epoch = AtomicU64::new(0); - let rows = crate::array::js_array_push_f64(crate::array::js_array_alloc(1), 7.0); - let scope = crate::gc::RuntimeHandleScope::new(); - let iter_h = scope.root_nanbox_f64(array_values_iter_null_done( - js_nanbox_pointer(rows as i64), - &epoch, - 0, - )); - let iter = || js_nanbox_get_pointer(iter_h.get_nanbox_f64()) as *mut ObjectHeader; - - unsafe { - // Model the optimized `for...of` driver: one yielded row followed - // by its terminal advance. - let first = dispatch_array_iterator_method_emit(iter(), "next", true, true); - assert_eq!( - result_fields(first), - (crate::value::TAG_FALSE, 7.0f64.to_bits()) - ); - let done = dispatch_array_iterator_method_emit(iter(), "next", true, true); - assert_eq!( - result_fields(done), - (crate::value::TAG_TRUE, crate::value::TAG_NULL) - ); - - // The same iterator must remain closed when user code calls - // `.next()` after the loop. Resetting its cursor used to return - // the first row again here. - let after = dispatch_array_iterator_method(iter(), "next"); - assert_eq!( - result_fields(after), - (crate::value::TAG_TRUE, crate::value::TAG_NULL) - ); - } - } -} diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index d2eef889ba..107c03431c 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -201,8 +201,8 @@ pub use self::iter_methods::{ }; pub use self::iter_object::{ arguments_values_iter, array_entries_iter, array_keys_iter, array_values_iter, - array_values_iter_null_done, dispatch_array_iterator_method, js_array_entries_iter_obj, - js_array_keys_iter_obj, js_array_values_iter_obj, ARRAY_ITERATOR_CLASS_ID, + dispatch_array_iterator_method, js_array_entries_iter_obj, js_array_keys_iter_obj, + js_array_values_iter_obj, ARRAY_ITERATOR_CLASS_ID, }; pub(crate) use self::iter_object::{ dispatch_array_iterator_method_builtin, dispatch_array_iterator_method_emit, diff --git a/crates/perry-runtime/src/exception.rs b/crates/perry-runtime/src/exception.rs index 659f27abbd..4e95b2885c 100644 --- a/crates/perry-runtime/src/exception.rs +++ b/crates/perry-runtime/src/exception.rs @@ -42,6 +42,8 @@ unsafe impl crate::zeroed_cache::ZeroEmpty for JmpBuf {} #[cfg(any(test, not(perry_native_stack_maps)))] pub(crate) use savepoints::catch_subsystem_used; +mod native_call; +pub(crate) use native_call::{catch_native_callback, NativeCatch}; mod savepoints; use savepoints::CatchSavepoint; pub(crate) use savepoints::{catch_subsystem, note_catch_subsystem_used, CatchStack}; @@ -70,10 +72,11 @@ const MAX_TRY_DEPTH: usize = 1024; /// innermost `try`-containing generated frame, which is exactly this entry /// (handler-stack order mirrors stack order, and an entry above it would /// have been popped or would itself be the throw target). -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] enum HandlerKind { Setjmp, Unwind, + NativeInactive, } struct ExceptionState { @@ -470,6 +473,12 @@ pub extern "C-unwind" fn js_throw(value: f64) -> ! { crate::closure::reset_throw_not_callable_counter(); let depth = (*s).try_depth - 1; + if (*s).handler_kinds[depth] == HandlerKind::NativeInactive { + // A callback arms its own trampoline. Outside it, jumping to a + // previous trampoline would cross native frames into dead storage. + eprintln!("perry: JavaScript throw outside native callback trap"); + std::process::abort(); + } // Apply the deferred context restores of async-context scopes // (`AsyncLocalStorage#run`/`#exit`, `runInAsyncScope`) whose normal // restore code this longjmp skips (#788). Pure thread-local state @@ -487,6 +496,7 @@ pub extern "C-unwind" fn js_throw(value: f64) -> ! { match (*s).handler_kinds[depth] { HandlerKind::Setjmp => (*s).jump_buffers[depth].as_mut_ptr(), HandlerKind::Unwind => std::ptr::null_mut(), + HandlerKind::NativeInactive => unreachable!(), } }); // WASI (#11378): there is no exception transport yet — setjmp/longjmp diff --git a/crates/perry-runtime/src/exception/native_call.rs b/crates/perry-runtime/src/exception/native_call.rs new file mode 100644 index 0000000000..37b92b0f87 --- /dev/null +++ b/crates/perry-runtime/src/exception/native_call.rs @@ -0,0 +1,164 @@ +//! Reuse a lazily captured savepoint during one native call. Jump targets +//! remain trampoline-local; between callbacks the handler is disarmed. + +use super::*; + +pub(crate) struct NativeCatch { + /// This thread's exception state, once a callback captured the handler + /// (null until then). Stable for the thread's life. + state: *mut ExceptionState, + /// The handler slot the first callback pushed. + depth: usize, + #[cfg(test)] + captures: u32, + _thread: std::marker::PhantomData>, +} + +impl NativeCatch { + #[inline] + pub(crate) fn new() -> Self { + Self { + state: std::ptr::null_mut(), + depth: 0, + #[cfg(test)] + captures: 0, + _thread: std::marker::PhantomData, + } + } + + #[cfg(test)] + pub(crate) fn captures(&self) -> u32 { + self.captures + } + + pub(crate) fn finish(&mut self) { + let s = std::mem::replace(&mut self.state, std::ptr::null_mut()); + if s.is_null() { + return; + } + let depth = self.depth; + unsafe { + assert_eq!((*s).try_depth, depth + 1, "unbalanced native callback trap"); + assert_eq!((*s).handler_kinds[depth], HandlerKind::NativeInactive); + #[cfg(test)] + if crate::native_payload::callback_sabotage("catch_pop") { + return; + } + (*s).try_depth = depth; + } + } +} + +/// `catch` is null for a legacy entry, or points at the current native call's +/// stack token. All pointer values are already rooted by the caller. +pub(crate) unsafe fn catch_native_callback( + catch: *mut NativeCatch, + callee: f64, + this: f64, + args: &[f64], +) -> Result { + // Keep the ordinary full savepoint for entries not using guard.call. + if catch.is_null() { + return catch_js_throw(|| { + crate::closure::native_call_value_this( + callee, + crate::closure::JsThis::from_f64(this), + args.as_ptr(), + args.len(), + ) + }); + } + let s = (*catch).state; + let (s, depth) = if s.is_null() { + // Capture here, after the first callback's argument roots exist. + // No callback means no capture or handler stack mutation. + try_push_with_kind(HandlerKind::NativeInactive); + let s = with_exception_state(|s| s); + let depth = (*s).try_depth - 1; + (*catch).state = s; + (*catch).depth = depth; + #[cfg(test)] + { + (*catch).captures += 1; + } + (s, depth) + } else { + let depth = (*catch).depth; + assert_eq!((*s).try_depth, depth + 1, "unbalanced callback entry"); + // Root scopes may differ in each trampoline's argument conversion. + (&mut (*s).savepoints) + .get_unchecked_mut(depth) + .assume_init_mut() + .refresh_native_roots(); + (s, depth) + }; + *(&mut (*s).handler_kinds).get_unchecked_mut(depth) = HandlerKind::Setjmp; + let env = (&mut (*s).jump_buffers) + .get_unchecked_mut(depth) + .as_mut_ptr(); + // POD context avoids generic FnOnce/Option transport on every callback. + struct Invocation { + callee: f64, + this: f64, + args: *const f64, + len: usize, + result: f64, + } + unsafe extern "C" fn invoke(raw: *mut core::ffi::c_void) { + let ctx = &mut *(raw as *mut Invocation); + let this = crate::closure::JsThis::from_f64(ctx.this); + // A function whose body the compiler emitted (every arrow or + // function a program registers) enters that body directly; any + // other callee takes the generic value call. + ctx.result = match compiled_body(ctx.callee) { + Some((closure, info)) => { + crate::closure::call_compiled_body_this(closure, info, this, ctx.args, ctx.len) + } + None => crate::closure::native_call_value_this(ctx.callee, this, ctx.args, ctx.len), + }; + } + let mut ctx = Invocation { + callee, + this, + args: args.as_ptr(), + len: args.len(), + result: 0.0, + }; + let rc = perry_sjlj_try(env.cast(), invoke, (&raw mut ctx).cast()); + assert_eq!((*s).try_depth, depth + 1, "unbalanced callback return"); + *(&mut (*s).handler_kinds).get_unchecked_mut(depth) = HandlerKind::NativeInactive; + if rc == 0 { + Ok(ctx.result) + } else { + let err = js_get_exception(); + js_clear_exception(); + Err(err) + } +} + +/// The closure `callee` names and its body when the compiler emitted that +/// body (`FN_COMPILED_BODY`): none of the exotic callees the generic value +/// call tests first (class refs, proxies, bound native exports, no-op-backed +/// built-ins, class objects) carries it. +#[inline] +unsafe fn compiled_body( + callee: f64, +) -> Option<( + *const crate::closure::ClosureHeader, + &'static crate::closure::JsFunctionInfo, +)> { + let bits = callee.to_bits(); + if bits & crate::value::TAG_MASK != crate::value::POINTER_TAG { + return None; + } + let ptr = (bits & crate::value::POINTER_MASK) as usize; + let header = crate::value::addr_class::try_read_gc_header(ptr)?; + if header.obj_type != crate::gc::GC_TYPE_CLOSURE + || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + { + return None; + } + let closure = ptr as *const crate::closure::ClosureHeader; + let info = (*closure).info.as_ref()?; + (info.flags & crate::codegen_abi::FN_COMPILED_BODY != 0).then_some((closure, info)) +} diff --git a/crates/perry-runtime/src/exception/savepoints.rs b/crates/perry-runtime/src/exception/savepoints.rs index 8893d5fea8..a264b96243 100644 --- a/crates/perry-runtime/src/exception/savepoints.rs +++ b/crates/perry-runtime/src/exception/savepoints.rs @@ -115,7 +115,7 @@ macro_rules! catch_savepoints { ($($(#[$attr:meta])* $name:ident: $ty:ty, capture: $capture:path, restore: $restore:path, latch: $latch:expr, idle: $idle:expr;)*) => { - #[derive(Clone, Copy)] + #[derive(Clone, Copy, Debug, PartialEq)] pub(super) struct CatchSavepoint { $($(#[$attr])* $name: $ty,)* } @@ -129,6 +129,22 @@ macro_rules! catch_savepoints { } } + /// A native call's handler is captured at its first callback and + /// reused by the rest. Only runtime handle scopes differ between + /// its callbacks (each trampoline roots its own converted + /// arguments); every other managed stack is back at the + /// native-call baseline whenever a callback starts. + #[inline] + pub(super) fn refresh_native_roots(&mut self) { + #[cfg(test)] + if crate::native_payload::callback_sabotage("catch_refresh") { + return; + } + self.runtime_handles = crate::gc::runtime_handle_stack_savepoint(); + #[cfg(test)] + assert_eq!(*self, Self::capture(), "native helper changed managed catch state"); + } + pub(super) fn restore(self) { $($(#[$attr])* $restore(self.$name);)* } @@ -261,6 +277,10 @@ pub(super) fn scan_pending_trap_roots_mut( // SAFETY: every slot below `try_depth` was written by `capture()` in // `try_push_with_kind` before `try_depth` advanced past it. let entry = unsafe { entry.assume_init_mut() }; + #[cfg(test)] + if crate::native_payload::callback_sabotage("catch_new_target_trace") { + continue; + } visitor.visit_nanbox_u64_slot(&mut entry.new_target); } } diff --git a/crates/perry-runtime/src/gc/layout_slot_visit.rs b/crates/perry-runtime/src/gc/layout_slot_visit.rs index e7dff74521..e55a7d8b1d 100644 --- a/crates/perry-runtime/src/gc/layout_slot_visit.rs +++ b/crates/perry-runtime/src/gc/layout_slot_visit.rs @@ -552,8 +552,17 @@ unsafe fn visit_gc_rewrite_slot_descriptors_with( let cell = user_ptr as *mut crate::native_handle::NativeHandleHeader; // One enumerator serves mark, relocation and dirty-slot rescan. // Released cells keep tracing their owner; only finalized cells stop. - if (*cell).finalized == 0 && (*cell).owner != 0 { - visit(fixed_slot(&mut (*cell).owner as *mut u64)); + if (*cell).finalized == 0 { + if (*cell).owner != 0 { + visit(fixed_slot(&mut (*cell).owner as *mut u64)); + } + #[cfg(test)] + if crate::native_payload::callback_sabotage("callback_trace") { + return; + } + if let Some(slot) = crate::native_payload::callback_slot_address(cell) { + visit(fixed_slot(slot)); + } } } GcRewriteDescriptorKind::Leaf => {} diff --git a/crates/perry-runtime/src/gc/tests/copying/latch.rs b/crates/perry-runtime/src/gc/tests/copying/latch.rs index d7985ca33f..a03eb0daec 100644 --- a/crates/perry-runtime/src/gc/tests/copying/latch.rs +++ b/crates/perry-runtime/src/gc/tests/copying/latch.rs @@ -347,6 +347,7 @@ fn pin_object_non_young_call_sites_are_never_young() { 0x11919, &LATCH_PROBE_VTABLE, "latch callback probe", + false, ); let cell_header = header_from_user_ptr(cell as *const u8); assert_eq!((*cell_header).gc_flags & GC_FLAG_ARENA, 0); diff --git a/crates/perry-runtime/src/gc/tests/native_payload_callbacks.rs b/crates/perry-runtime/src/gc/tests/native_payload_callbacks.rs index 00894d2e7d..fe95eb1de5 100644 --- a/crates/perry-runtime/src/gc/tests/native_payload_callbacks.rs +++ b/crates/perry-runtime/src/gc/tests/native_payload_callbacks.rs @@ -392,11 +392,13 @@ fn t9_wrong_thread_link_owner_never_throws() { let value = owner(); let link = np::owner_link(value, &FAMILY).unwrap(); let result = std::thread::spawn(move || { - crate::exception::catch_js_throw(|| unsafe { np::link_owner(link) }) + crate::exception::catch_js_throw(|| unsafe { + (np::link_owner(link), np::enter_link(link).err()) + }) }) .join() .unwrap(); - assert_eq!(result, Ok(None)); + assert_eq!(result, Ok((None, Some(PayloadMiss::Closed)))); assert_eq!( unsafe { np::link_owner(link) }.map(f64::to_bits), Some(value.to_bits()) @@ -493,10 +495,439 @@ fn t12_caught_nested_throw_leaves_busy_zero() { assert_eq!(crate::exception::current_try_depth(), depth); } +#[test] +fn callback_cell_slot_moves_grows_replaces_and_clears() { + let _guard = CopyingNurseryTestGuard::new(1); + let _reset = Reset::new(); + let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + let cb = scope.root_nanbox_f64(closure(crate::fn_info!(returns, 0))); + np::set_callback(value.get_nanbox_f64(), &FAMILY, 0, cb.get_nanbox_f64()); + let link = np::owner_link(value.get_nanbox_f64(), &FAMILY).unwrap(); + let before = unsafe { *np::callback_slot_address(cell(link)).unwrap() }; + let trace = collect_minor_trace(GcTriggerKind::MallocCount); + assert!(trace.copying_nursery.eligible); + let after = np::callbacks(value.get_nanbox_f64(), &FAMILY).to_bits(); + assert_ne!(before, after, "fixture must move the callbacks array"); + assert_eq!( + unsafe { *np::callback_slot_address(cell(link)).unwrap() }, + after + ); + assert_eq!( + unsafe { np::callback_from_link(link, 0) }.to_bits(), + cb.get_nanbox_f64().to_bits() + ); + np::set_callback(value.get_nanbox_f64(), &FAMILY, 4096, cb.get_nanbox_f64()); + assert_eq!( + unsafe { np::callback_from_link(link, 4096) }.to_bits(), + cb.get_nanbox_f64().to_bits() + ); + let replacement = scope.root_nanbox_f64(crate::value::js_nanbox_pointer( + crate::array::js_array_alloc(0) as i64, + )); + np::state_set( + value.get_nanbox_f64(), + &FAMILY, + b"callbacks", + replacement.get_nanbox_f64(), + ); + assert_eq!( + unsafe { np::callback_from_link(link, 0) }.to_bits(), + TAG_UNDEFINED + ); + np::set_callback(value.get_nanbox_f64(), &FAMILY, 0, cb.get_nanbox_f64()); + crate::state_key_memo!(static MEMO_TEST_CALLBACKS); + np::state_set_memo( + value.get_nanbox_f64(), + &FAMILY, + b"callbacks", + f64::from_bits(TAG_UNDEFINED), + &MEMO_TEST_CALLBACKS, + ); + assert_eq!( + unsafe { np::callback_from_link(link, 0) }.to_bits(), + TAG_UNDEFINED + ); +} + +#[test] +fn callback_cell_slot_marks_its_array_without_another_edge() { + let _guard = CopyingNurseryTestGuard::new(0); + let _reset = Reset::new(); + // The guard isolates scanner registration, but realm towers from earlier + // libtest cases still live on this thread. Preserve their production + // roots during this mark walk; none points at this callbacks array. + gc_register_named_mutable_root_scanner( + "object_cache", + crate::object::scan_object_cache_roots_mut, + ); + let _no_stack = ConservativeScanDisabledGuard::new(); + let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + np::set_callback( + value.get_nanbox_f64(), + &FAMILY, + 0, + closure(crate::fn_info!(returns, 0)), + ); + let link = np::owner_link(value.get_nanbox_f64(), &FAMILY).unwrap(); + let array_addr = + unsafe { *np::callback_slot_address(cell(link)).unwrap() } as usize & POINTER_MASK as usize; + // Remove the duplicate state edge without using the synchronizing API: + // only the cell slot may keep this array alive in this fixture. + let state = np::js_state(value.get_nanbox_f64(), &FAMILY, false); + let key = crate::string::intern_ascii_literal(b"callbacks"); + crate::object::js_object_set_field_by_name( + (state.to_bits() & POINTER_MASK) as *mut crate::object::ObjectHeader, + key, + f64::from_bits(TAG_UNDEFINED), + ); + clear_marks(); + clear_mark_seeds(); + let valid_ptrs = build_valid_pointer_set(); + mark_mutable_registered_roots(&valid_ptrs); + drain_incremental_mark_barrier_seeds(&valid_ptrs); + assert_ne!( + unsafe { (*header_from_user_ptr(array_addr as *const u8)).gc_flags } & GC_FLAG_MARKED, + 0, + "the cell's callback slot must mark the array" + ); +} + +#[test] +fn callback_cell_slot_barrier_is_exact_for_malloc_parent() { + let _guard = CopyingNurseryTestGuard::new(1); + let _reset = Reset::new(); + let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + let cb = closure(crate::fn_info!(returns, 0)); + np::set_callback(value.get_nanbox_f64(), &FAMILY, 0, cb); + let link = np::owner_link(value.get_nanbox_f64(), &FAMILY).unwrap(); + let _ = barrier::remembered_dirty_snapshot(); + reset_remembered_set(); + let replacement = crate::value::js_nanbox_pointer(crate::array::js_array_alloc(0) as i64); + np::state_set(value.get_nanbox_f64(), &FAMILY, b"callbacks", replacement); + let header = unsafe { header_from_user_ptr(cell(link) as *const u8) } as usize; + let snapshot = barrier::remembered_dirty_snapshot(); + assert!( + snapshot + .external_dirty_entries + .iter() + .any(|&(_, h)| h == header) + || snapshot.fallback_headers.contains(&header), + "callback slot must remember its malloc parent" + ); +} + +#[test] +fn runtime_callback_slots_never_call_an_accessor() { + let _guard = CopyingNurseryTestGuard::new(0); + let _reset = Reset::new(); + let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + let cb = scope.root_nanbox_f64(closure(crate::fn_info!(returns, 0))); + np::set_callback(value.get_nanbox_f64(), &FAMILY, 1, cb.get_nanbox_f64()); + let arr = scope.root_nanbox_f64(np::callbacks(value.get_nanbox_f64(), &FAMILY)); + let desc = scope.root_raw_mut_ptr(crate::object::js_object_alloc_null_proto(0, 1)); + let get = crate::string::intern_ascii_literal(b"get"); + crate::object::js_object_set_field_by_name(desc.get_raw_mut_ptr(), get, cb.get_nanbox_f64()); + let key = scope.root_string_ptr(crate::string::intern_ascii_literal(b"0")); + crate::object::js_object_define_property( + arr.get_nanbox_f64(), + crate::value::js_nanbox_string(key.get_raw_const_ptr::() as i64), + crate::value::js_nanbox_pointer( + desc.get_raw_mut_ptr::() as i64 + ), + ); + let link = np::owner_link(value.get_nanbox_f64(), &FAMILY).unwrap(); + CALLS.store(0, Ordering::SeqCst); + let result = crate::exception::catch_js_throw(|| unsafe { np::callback_from_link(link, 0) }); + assert_eq!(result.map(f64::to_bits), Ok(TAG_UNDEFINED)); + assert_eq!( + CALLS.load(Ordering::SeqCst), + 0, + "callback lookup must never run JS" + ); + np::state_set( + value.get_nanbox_f64(), + &FAMILY, + b"callbacks", + crate::value::js_nanbox_pointer( + desc.get_raw_mut_ptr::() as i64 + ), + ); + assert_eq!( + unsafe { np::callback_from_link(link, 0) }.to_bits(), + TAG_UNDEFINED, + "non-array storage is not a callback array" + ); +} + +extern "C" fn clears_new_target_and_throws( + _: *const crate::closure::ClosureHeader, + _: crate::closure::JsThis, +) -> f64 { + crate::object::js_new_target_set(f64::from_bits(TAG_UNDEFINED)); + crate::exception::js_throw(43.0) +} + +#[test] +fn native_call_reuses_catch_refreshes_roots_and_pops() { + let _guard = CopyingNurseryTestGuard::new(0); + let _reset = Reset::new(); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + let returns = scope.root_nanbox_f64(closure(crate::fn_info!(returns, 0))); + let throws = scope.root_nanbox_f64(closure(crate::fn_info!(throws, 1))); + let link = np::owner_link(value.get_nanbox_f64(), &FAMILY).unwrap(); + let depth = crate::exception::current_try_depth(); + let guard = unsafe { np::enter_link(link) }.unwrap(); + let ((), end) = unsafe { + guard.call(|| { + assert_eq!( + np::call_from_link(link, returns.get_nanbox_f64(), &[]), + Ok(19.0) + ); + assert_eq!(crate::exception::current_try_depth(), depth + 1); + // The second trampoline has argument-conversion roots absent from the + // first. A throw must retain them instead of restoring the old depth. + // Two roots: the first callback's own capture (inside its pending-slot + // scope) sits one root above the guard, so one root here would + // coincide with it and hide a missing refresh. + let extra = RuntimeHandleScope::new(); + let object = extra.root_raw_mut_ptr(crate::object::js_object_alloc(0, 0)); + let _second = extra.root_raw_mut_ptr(crate::object::js_object_alloc(0, 0)); + let handles = crate::gc::runtime_handle_stack_savepoint(); + assert_eq!( + np::call_from_link(link, throws.get_nanbox_f64(), &[43.0]), + Err(()) + ); + assert_eq!(crate::gc::runtime_handle_stack_savepoint(), handles); + assert!(!object + .get_raw_mut_ptr::() + .is_null()); + let cached = &*(*(cell(link) as *mut np::NativeCallbackCell)).catch; + assert_eq!( + cached.captures(), + 1, + "many callbacks share one full savepoint" + ); + }) + }; + assert_eq!(end, Err(CallEnd::Threw(43.0))); + assert_eq!(crate::exception::current_try_depth(), depth); + assert_eq!(unsafe { (*cell(link)).busy }, 0); + assert!(unsafe { + (*(cell(link) as *mut np::NativeCallbackCell)) + .catch + .is_null() + }); + // Native calls with no callback never push a handler. + let guard = unsafe { np::enter_link(link) }.unwrap(); + assert_eq!( + unsafe { guard.call(|| crate::exception::current_try_depth()) }, + (depth, Ok(())) + ); +} + +#[test] +fn native_call_cached_new_target_moves_before_later_throw() { + let _guard = CopyingNurseryTestGuard::new(1); + let _reset = Reset::new(); + let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + gc_register_named_mutable_root_scanner("exception", crate::exception::scan_exception_roots_mut); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + let returns = scope.root_nanbox_f64(closure(crate::fn_info!(returns, 0))); + let throws = scope.root_nanbox_f64(closure(crate::fn_info!(clears_new_target_and_throws, 0))); + let target = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 0)); + let before = crate::value::js_nanbox_pointer( + target.get_raw_mut_ptr::() as i64, + ); + let old_target = scope.root_nanbox_f64(crate::object::js_new_target_set(before)); + let link = np::owner_link(value.get_nanbox_f64(), &FAMILY).unwrap(); + let guard = unsafe { np::enter_link(link) }.unwrap(); + let (_, end) = unsafe { + guard.call(|| { + assert_eq!( + np::call_from_link(link, returns.get_nanbox_f64(), &[]), + Ok(19.0) + ); + let trace = collect_minor_trace(GcTriggerKind::MallocCount); + assert!(trace.copying_nursery.eligible); + let moved = crate::value::js_nanbox_pointer( + target.get_raw_mut_ptr::() as i64, + ); + assert_ne!(before.to_bits(), moved.to_bits()); + // The live new.target cell need not be registered in this fixture: + // restore it to its correct moved value before the second callback. + crate::object::js_new_target_set(moved); + assert_eq!( + np::call_from_link(link, throws.get_nanbox_f64(), &[]), + Err(()) + ); + assert_eq!( + crate::object::js_new_target_get().to_bits(), + moved.to_bits() + ); + }) + }; + crate::object::js_new_target_set(old_target.get_nanbox_f64()); + assert_eq!(end, Err(CallEnd::Threw(43.0))); +} + +extern "C" fn reenters_cached_then_closes( + _: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(this.as_f64()); + let cb = scope.root_nanbox_f64(closure(crate::fn_info!(returns, 0))); + let guard = np::enter(value.get_nanbox_f64(), &FAMILY).unwrap(); + let link = guard.owner_link(); + let (_, end) = unsafe { + guard.call(|| { + assert_eq!((*cell(link)).busy, 2); + assert_eq!(np::call_from_link(link, cb.get_nanbox_f64(), &[]), Ok(19.0)); + assert_eq!( + np::close(value.get_nanbox_f64(), &FAMILY), + CloseOutcome::Deferred + ); + assert_eq!(DROPS.load(Ordering::SeqCst), 0); + }) + }; + assert_eq!(end, Err(CallEnd::Closed)); + assert_eq!(unsafe { (*cell(link)).busy }, 1); + assert_eq!(DROPS.load(Ordering::SeqCst), 0); + 23.0 +} + +#[test] +fn native_call_reentrant_catch_restores_outer_then_defers_close() { + let _guard = CopyingNurseryTestGuard::new(0); + let _reset = Reset::new(); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + let cb = scope.root_nanbox_f64(closure(crate::fn_info!(reenters_cached_then_closes, 0))); + let guard = np::enter(value.get_nanbox_f64(), &FAMILY).unwrap(); + let link = guard.owner_link(); + let depth = crate::exception::current_try_depth(); + let (result, end) = unsafe { + guard.call(|| { + let result = np::call_from_native( + value.get_nanbox_f64(), + cb.get_nanbox_f64(), + value.get_nanbox_f64(), + &[], + ); + assert_eq!(crate::exception::current_try_depth(), depth + 1); + let outer_ptr = (*(cell(link) as *mut np::NativeCallbackCell)).catch; + assert!( + !outer_ptr.is_null(), + "nested call must restore the outer catch token" + ); + let outer = &*outer_ptr; + assert_eq!(outer.captures(), 1); + result + }) + }; + assert_eq!(result, Ok(23.0)); + assert_eq!(end, Err(CallEnd::Closed)); + assert_eq!(crate::exception::current_try_depth(), depth); + assert_eq!(unsafe { (*cell(link)).busy }, 0); + assert_eq!(DROPS.load(Ordering::SeqCst), 1); +} + +#[test] +fn callback_cell_slot_resolves_array_growth_before_gc_rewrite() { + let _guard = CopyingNurseryTestGuard::new(0); + let _reset = Reset::new(); + let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + let cb = scope.root_nanbox_f64(closure(crate::fn_info!(returns, 0))); + np::set_callback(value.get_nanbox_f64(), &FAMILY, 0, cb.get_nanbox_f64()); + let link = np::owner_link(value.get_nanbox_f64(), &FAMILY).unwrap(); + let original = np::callbacks(value.get_nanbox_f64(), &FAMILY); + let arr = + crate::JSValue::from_bits(original.to_bits()).as_pointer::(); + // Model an array alias growing its storage without a JS-state setter. + let grown = scope.root_raw_mut_ptr(unsafe { + crate::array::js_array_set_f64_extend(arr as *mut _, 4096, cb.get_nanbox_f64()) + }); + assert_ne!( + original.to_bits() & POINTER_MASK, + grown.get_raw_mut_ptr::() as u64 + ); + assert_eq!( + unsafe { *np::callback_slot_address(cell(link)).unwrap() }, + original.to_bits() + ); + assert_eq!( + unsafe { np::callback_from_link(link, 4096) }.to_bits(), + cb.get_nanbox_f64().to_bits() + ); +} + +#[test] +fn callback_cell_slot_reads_sparse_own_data() { + let _guard = CopyingNurseryTestGuard::new(0); + let _reset = Reset::new(); + let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(owner()); + let cb = scope.root_nanbox_f64(closure(crate::fn_info!(returns, 0))); + let index = 1_048_576; + np::set_callback(value.get_nanbox_f64(), &FAMILY, index, cb.get_nanbox_f64()); + let link = np::owner_link(value.get_nanbox_f64(), &FAMILY).unwrap(); + let array = np::callbacks(value.get_nanbox_f64(), &FAMILY); + let arr = crate::JSValue::from_bits(array.to_bits()).as_pointer::(); + assert!( + unsafe { (*arr).capacity } <= index, + "fixture must take the sparse path" + ); + assert_eq!( + unsafe { np::callback_from_link(link, index) }.to_bits(), + cb.get_nanbox_f64().to_bits() + ); + assert_eq!( + unsafe { np::callback_from_link(link, index - 1) }.to_bits(), + TAG_UNDEFINED + ); +} + #[test] fn every_sabotage_makes_its_runtime_witness_red() { let exe = std::env::current_exe().unwrap(); for (fault, witness) in [ + ( + "callback_sparse_key", + "callback_cell_slot_reads_sparse_own_data", + ), + ( + "callback_forwarding", + "callback_cell_slot_resolves_array_growth_before_gc_rewrite", + ), + ( + "catch_reentry", + "native_call_reentrant_catch_restores_outer_then_defers_close", + ), + ( + "catch_refresh", + "native_call_reuses_catch_refreshes_roots_and_pops", + ), + ( + "catch_pop", + "native_call_reuses_catch_refreshes_roots_and_pops", + ), + ( + "catch_new_target_trace", + "native_call_cached_new_target_moves_before_later_throw", + ), ( "latch", "gc::tests::copying::latch::callback_cell_ref_does_not_arm_the_young_pin_latch", @@ -515,6 +946,26 @@ fn every_sabotage_makes_its_runtime_witness_red() { "t2_cell_ref_keeps_owner_and_callback_through_full_gc", ), ("barrier", "t3_owner_store_has_a_remembered_barrier"), + ( + "callback_data_read", + "runtime_callback_slots_never_call_an_accessor", + ), + ( + "callback_trace", + "callback_cell_slot_moves_grows_replaces_and_clears", + ), + ( + "callback_trace", + "callback_cell_slot_marks_its_array_without_another_edge", + ), + ( + "callback_sync", + "callback_cell_slot_moves_grows_replaces_and_clears", + ), + ( + "callback_barrier", + "callback_cell_slot_barrier_is_exact_for_malloc_parent", + ), ( "catch", "t4_t5_throw_identity_first_throw_wins_and_c_returns", diff --git a/crates/perry-runtime/src/native_class_ids.rs b/crates/perry-runtime/src/native_class_ids.rs index e076d3d05f..419c9961ce 100644 --- a/crates/perry-runtime/src/native_class_ids.rs +++ b/crates/perry-runtime/src/native_class_ids.rs @@ -60,6 +60,14 @@ pub const CRYPTO_HMAC: u32 = 0xFFFF_2413; pub const CRYPTO_CIPHERIV: u32 = 0xFFFF_2414; pub const CRYPTO_DECIPHERIV: u32 = 0xFFFF_2415; pub const DOMAIN: u32 = 0xFFFF_2416; +// node:sqlite (#11919): DatabaseSync, StatementSync, StatementSyncIterator, +// SQLTagStore, Session and the `db.limits` object. +pub const SQLITE_DATABASE_SYNC: u32 = 0xFFFF_2417; +pub const SQLITE_STATEMENT_SYNC: u32 = 0xFFFF_2418; +pub const SQLITE_STATEMENT_ITERATOR: u32 = 0xFFFF_2419; +pub const SQLITE_TAG_STORE: u32 = 0xFFFF_241A; +pub const SQLITE_SESSION: u32 = 0xFFFF_241B; +pub const SQLITE_LIMITS: u32 = 0xFFFF_241C; /// #10926: `AsyncResource` is native-backed too, but keeps its LEGACY /// `0xFFFF_0079`, which `instanceof` and `class_registry::parent_static` @@ -73,7 +81,7 @@ pub const ASYNC_LOCAL_STORAGE_LEGACY: u32 = 0xFFFF_0078; /// family between them carries a `native_state` word the far side of a /// `postMessage` could not reconstruct. const NATIVE_BACKED_FIRST: u32 = ABORT_CONTROLLER; -const NATIVE_BACKED_LAST: u32 = DOMAIN; +const NATIVE_BACKED_LAST: u32 = SQLITE_LIMITS; /// Class ids whose instances are ordinary objects carrying native state that /// cannot cross a thread boundary (#340/#341). @@ -128,6 +136,12 @@ const ALL: &[u32] = &[ CRYPTO_CIPHERIV, CRYPTO_DECIPHERIV, DOMAIN, + SQLITE_DATABASE_SYNC, + SQLITE_STATEMENT_SYNC, + SQLITE_STATEMENT_ITERATOR, + SQLITE_TAG_STORE, + SQLITE_SESSION, + SQLITE_LIMITS, ]; /// Strictly ascending ⟹ no two families share an id, and the block stays @@ -189,6 +203,12 @@ mod tests { CRYPTO_CIPHERIV, CRYPTO_DECIPHERIV, DOMAIN, + SQLITE_DATABASE_SYNC, + SQLITE_STATEMENT_SYNC, + SQLITE_STATEMENT_ITERATOR, + SQLITE_TAG_STORE, + SQLITE_SESSION, + SQLITE_LIMITS, ASYNC_LOCAL_STORAGE_LEGACY, ASYNC_RESOURCE_LEGACY, ] { diff --git a/crates/perry-runtime/src/native_handle.rs b/crates/perry-runtime/src/native_handle.rs index 12da2290a2..2ac7e4089e 100644 --- a/crates/perry-runtime/src/native_handle.rs +++ b/crates/perry-runtime/src/native_handle.rs @@ -1,7 +1,7 @@ //! GC-managed opaque native handles for external native-library bindings. //! //! A native handle is a Perry heap value whose payload contains only native -//! metadata and a raw resource pointer, plus an optional traced owner slot for +//! metadata and a raw resource pointer, plus optional traced owner and callbacks slots for //! callback payloads. The resource pointer is not a Perry heap edge and //! finalizers must be basic native cleanup callbacks only. @@ -68,21 +68,24 @@ pub struct NativeHandleHeader { pub external_bytes: u64, } -const LEGACY_CELL_SIZE: usize = if cfg!(target_pointer_width = "64") { +const CELL_SIZE: usize = if cfg!(target_pointer_width = "64") { 136 } else if std::mem::align_of::() == 8 { 128 } else { 124 }; -const _: () = assert!(std::mem::size_of::() == LEGACY_CELL_SIZE); +const _: () = assert!(std::mem::size_of::() == CELL_SIZE); +#[inline] pub(crate) fn current_thread_id() -> u64 { // Cached per thread: a Rust-payload method checks it on every call, and // hashing `std::thread::current().id()` clones an `Arc` and runs SipHash. std::thread_local! { static CURRENT_THREAD_ID: std::cell::Cell = const { std::cell::Cell::new(0) }; } + #[cold] + #[inline(never)] fn compute() -> u64 { let mut hasher = DefaultHasher::new(); std::thread::current().id().hash(&mut hasher); @@ -217,6 +220,7 @@ unsafe fn native_handle_new( finalizer: *mut c_void, debug_name_ptr: *const u8, debug_name_len: i64, + callbacks: bool, ) -> f64 { let ptr_value = resource_ptr as *mut c_void; let stored_ownership = if ptr_value.is_null() { @@ -225,7 +229,11 @@ unsafe fn native_handle_new( ownership }; let handle = crate::gc::gc_malloc( - std::mem::size_of::(), + if callbacks { + std::mem::size_of::() + } else { + std::mem::size_of::() + }, crate::gc::GC_TYPE_NATIVE_HANDLE, ) as *mut NativeHandleHeader; (*handle).magic = NATIVE_HANDLE_MAGIC; @@ -254,6 +262,11 @@ unsafe fn native_handle_new( ) as _; (*handle).busy = 0; (*handle).flags = 0; + if callbacks { + (*handle).flags |= crate::native_payload::CALLBACK_STORAGE; + (*(handle as *mut crate::native_payload::NativeCallbackCell)).callbacks = 0; + (*(handle as *mut crate::native_payload::NativeCallbackCell)).catch = std::ptr::null_mut(); + } (*handle)._pad1 = Default::default(); (*handle).external_bytes = 0; f64::from_bits(crate::value::JSValue::pointer(handle as *const u8).bits()) @@ -377,6 +390,7 @@ pub(crate) unsafe fn native_handle_new_rust_payload( type_id: u64, vtable: &'static crate::native_payload::PayloadVTable, debug_name: &str, + callbacks: bool, ) -> *mut NativeHandleHeader { runtime_main_thread_id(); let word = vtable as *const crate::native_payload::PayloadVTable as *mut c_void; @@ -389,6 +403,7 @@ pub(crate) unsafe fn native_handle_new_rust_payload( word, debug_name.as_ptr(), debug_name.len() as i64, + callbacks, ); let cell = crate::value::JSValue::from_bits(value.to_bits()).as_pointer::() as *mut NativeHandleHeader; @@ -558,6 +573,7 @@ pub extern "C" fn js_native_handle_new_owned( finalizer, debug_name_ptr, debug_name_len, + false, ) } } @@ -583,6 +599,7 @@ pub extern "C" fn js_native_handle_new_borrowed( ptr::null_mut(), debug_name_ptr, debug_name_len, + false, ) } } @@ -916,7 +933,8 @@ mod tests { assert_eq!((*gc).obj_type, crate::gc::GC_TYPE_NATIVE_HANDLE); assert!(!crate::gc::gc_type_is_pointer_free((*gc).obj_type)); assert_eq!((*handle).owner, 0); - assert_eq!(std::mem::size_of::(), LEGACY_CELL_SIZE); + assert_eq!((*handle).flags & crate::native_payload::CALLBACK_STORAGE, 0); + assert_eq!(std::mem::size_of::(), CELL_SIZE); assert!(!crate::gc::gc_type_is_movable((*gc).obj_type)); } } diff --git a/crates/perry-runtime/src/native_payload.rs b/crates/perry-runtime/src/native_payload.rs index fdbc40296f..e7bfaadb81 100644 --- a/crates/perry-runtime/src/native_payload.rs +++ b/crates/perry-runtime/src/native_payload.rs @@ -51,6 +51,15 @@ use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use crate::native_handle::NativeHandleHeader; use crate::object::ObjectHeader; +#[path = "native_payload_birth.rs"] +mod birth; +pub use birth::{BirthMemo, OwnAccessor}; +#[path = "native_payload_slots.rs"] +mod callback_slots; +pub use callback_slots::{call_callback, callback_at, callback_from_link}; +pub(crate) use callback_slots::{callback_slot_address, NativeCallbackCell}; +use callback_slots::{store_callbacks, sync_callbacks}; + /// A family: one class id, one payload type, one prototype per realm. pub struct NativePayloadFamily { /// The family's id from `native_class_ids.rs`. @@ -123,6 +132,35 @@ impl PayloadPrototype { ); } + /// Install a builtin method under a well-known symbol (`Symbol.dispose`): + /// `well_known` is the symbol's short name, `name` the function's `.name`. + pub fn symbol_method( + &mut self, + well_known: &str, + name: &str, + info: *const crate::closure::JsFunctionInfo, + arity: u32, + ) { + let symbol = crate::symbol::well_known_symbol(well_known); + if symbol.is_null() { + return; + } + let closure = crate::closure::js_closure_alloc(info, 0); + if closure.is_null() { + return; + } + crate::object::native_module::set_bound_native_closure_name(closure, name); + crate::object::native_module::set_builtin_closure_length(closure as usize, arity); + crate::object::native_module::set_builtin_closure_non_constructable(closure as usize); + unsafe { + crate::symbol::js_object_set_symbol_property( + crate::value::js_nanbox_pointer(self.proto as i64), + f64::from_bits(crate::value::JSValue::pointer(symbol as *const u8).bits()), + crate::value::js_nanbox_pointer(closure as i64), + ); + } + } + /// Set the prototype's own `[[Prototype]]`. pub fn inherit(&mut self, parent: f64) { if crate::value::JSValue::from_bits(parent.to_bits()).is_pointer() { @@ -194,7 +232,7 @@ crate::perry_thread_local! { } const _: () = assert!( - crate::native_class_ids::DOMAIN - crate::native_class_ids::WEB_BUILTIN_BLOCK_START + crate::native_class_ids::SQLITE_LIMITS - crate::native_class_ids::WEB_BUILTIN_BLOCK_START < PROTOTYPE_SLOTS as u32, "a native-payload family id outgrew the prototype slot array" ); @@ -247,12 +285,10 @@ fn family_prototype(family: &NativePayloadFamily) -> *mut ObjectHeader { // Raw locals stay stable across the allocating installs below, exactly as // the timer and iterator prototypes do (#7251). let _no_move = crate::gc::GcSuppressScope::new(); - let proto = crate::object::js_object_alloc(0, 0); - if proto.is_null() { - return proto; - } - let mut builder = PayloadPrototype { proto }; - (family.install_prototype)(&mut builder); + // Resolve the constructor first. Creating a module export runs that + // module's attach hook, which may materialize this very prototype (so the + // export carries `prototype` before any instance exists); adopt the one + // it published instead of building a second. let constructor = match family.constructor_export { Some((module, export)) => crate::object::bound_native_callable_export_value(module, export), None => { @@ -272,6 +308,16 @@ fn family_prototype(family: &NativePayloadFamily) -> *mut ObjectHeader { } } }; + let existing = PAYLOAD_PROTOTYPES.with(|slots| slots[index].load(Ordering::Acquire)); + if existing != 0 { + return existing as *mut ObjectHeader; + } + let proto = crate::object::js_object_alloc(0, 0); + if proto.is_null() { + return proto; + } + let mut builder = PayloadPrototype { proto }; + (family.install_prototype)(&mut builder); if crate::value::JSValue::from_bits(constructor.to_bits()).is_pointer() { let constructor_ptr = crate::value::js_nanbox_get_pointer(constructor) as usize; crate::closure::closure_set_dynamic_prop( @@ -414,6 +460,7 @@ pub fn alloc( plain_vtable::(), external_bytes, own, + own.len(), proto, ) } @@ -435,6 +482,7 @@ pub fn alloc_stream( hooked_vtable::(), external_bytes, own, + own.len(), proto, ) } @@ -448,11 +496,31 @@ pub fn prototype_value(family: &'static NativePayloadFamily) -> f64 { crate::value::js_nanbox_pointer(proto as i64) } +/// [`alloc`] with the instance's JS state built at birth: `state` lists its +/// fields in order, and `accessors` the own accessors the instance is born +/// with (after its own data keys). The state object has a null prototype, so +/// storing its fields can run no inherited setter. Every birth at one call +/// site has the same keys, so the site's `memo` records the final shapes its +/// first birth reached and later births are allocated directly in them +/// (`native_payload_birth.rs`). Declare one memo per call site with +/// [`birth_memo!`](crate::birth_memo). +pub fn alloc_with_state( + family: &'static NativePayloadFamily, + payload: T, + external_bytes: usize, + own: &[(&[u8], f64)], + state: &[(&[u8], f64)], + accessors: &[OwnAccessor], + memo: &'static BirthMemo, +) -> f64 { + birth::alloc_born(family, payload, external_bytes, own, state, accessors, memo) +} + /// Allocate an ordinary instance with its permanent cell but no resource. /// The first attach establishes the family's payload layout and drop thunk. pub fn alloc_closed(family: &'static NativePayloadFamily, own: &[(&[u8], f64)]) -> f64 { let proto = family_prototype(family); - alloc_cell::<()>(family, None, plain_vtable::<()>(), 0, own, proto) + alloc_cell::<()>(family, None, plain_vtable::<()>(), 0, own, own.len(), proto) } /// Allocate a payload-family instance linked to an already-materialized @@ -470,16 +538,20 @@ pub fn alloc_with_prototype( plain_vtable::(), external_bytes, own, + own.len(), proto, ) } +/// `slots`: the instance's live inline slots, at least `own.len()` (a birth +/// that adds keys after `own` sizes for them, so none spills). fn alloc_cell( family: &'static NativePayloadFamily, payload: Option, vtable: &'static PayloadVTable, external_bytes: usize, own: &[(&[u8], f64)], + slots: usize, proto: *mut ObjectHeader, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); @@ -490,7 +562,7 @@ fn alloc_cell( if proto.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - let obj = unsafe { born_instance(family.class_id, proto, own.len() as u32) }; + let obj = unsafe { born_instance(family.class_id, proto, slots as u32) }; if obj.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } @@ -586,6 +658,7 @@ fn attach_rooted( }, vtable, family.name, + family.links_owner, ) }; let word = crate::value::JSValue::pointer(cell as *const u8).bits(); @@ -604,6 +677,9 @@ fn attach_rooted( let owner = crate::value::js_nanbox_pointer(obj as i64).to_bits(); // GC_STORE_AUDIT(BARRIERED): malloc cell -> nursery owner. (*cell).owner = owner; + if let Some(state) = raw_js_state(obj) { + store_callbacks(cell, raw_field_memo(state, b"callbacks", &CALLBACKS_MEMO)); + } #[cfg(test)] if callback_sabotage("barrier") { return; @@ -814,6 +890,9 @@ pub(crate) const CLOSING: u8 = 2; /// The cell's `finalizer` word names a static [`PayloadVTable`] (every Rust /// payload cell), not a bare C finalizer (a plain native handle). pub(crate) const VTABLE_WORD: u8 = 4; +/// The owner's JS state already holds the `pendingException` slot. +pub(crate) const PENDING_SLOT: u8 = 8; +pub(crate) const CALLBACK_STORAGE: u8 = 16; /// The non-finalized cell states. Finalized cells answer PayloadMiss::Closed. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -993,6 +1072,7 @@ pub fn owner_link(value: f64, family: &NativePayloadFamily) -> Result Option { let cell = link.0 as *mut NativeHandleHeader; #[cfg(test)] @@ -1057,12 +1137,28 @@ pub struct NativeCallGuard { } pub fn enter(value: f64, family: &NativePayloadFamily) -> Result { - let link = owner_link(value, family)?; - unsafe { + if !family.links_owner { + return Err(PayloadMiss::Foreign); + } + let cell = payload_cell(value, family.class_id)?; + unsafe { enter_link(OwnerLink(cell as usize)) } +} + +/// Enter through a previously validated, stable owner link. Each entry still +/// checks OPEN and thread affinity; keep the owner rooted for the entire call. +/// +/// # Safety +/// As [`link_owner`]; `link` must remain live until the returned guard finishes. +#[inline] +pub unsafe fn enter_link(link: OwnerLink) -> Result { + { let cell = link.0 as *mut NativeHandleHeader; if crate::native_handle::rust_payload_ptr_on_owner_thread(cell).is_null() { return Err(PayloadMiss::Closed); } + if (*cell).owner == 0 { + return Err(PayloadMiss::Foreign); + } #[cfg(test)] if callback_sabotage("reentry") && (*cell).busy != 0 { return Err(PayloadMiss::Closed); @@ -1086,16 +1182,56 @@ pub enum CallEnd { } impl NativeCallGuard { + /// The validated link, usable while the rooted receiver keeps its cell alive. + #[inline] + pub fn owner_link(&self) -> OwnerLink { + self.link + } + + /// Run one native call and finish before its result can be converted. + /// + /// # Safety + /// `f` may run only native code and non-throwing native data helpers. + /// It may call JS only through the native callback protocol. No payload + /// borrow or lock may span it. Keep the receiver rooted throughout. + /// Between callbacks, `f` must not change managed stack state other than + /// GC root scopes; callback argument conversion may add those scopes. + #[inline] + pub unsafe fn call(self, f: impl FnOnce() -> R) -> (R, Result<(), CallEnd>) { + let cell = self.link.0 as *mut NativeHandleHeader; + let mut catch = crate::exception::NativeCatch::new(); + if (*cell).flags & CALLBACK_STORAGE == 0 { + let value = f(); + return (value, self.finish()); + } + let extended = cell as *mut NativeCallbackCell; + let previous = (*extended).catch; + (*extended).catch = &mut catch; + let value = f(); + catch.finish(); + #[cfg(test)] + let previous = if callback_sabotage("catch_reentry") { + std::ptr::null_mut() + } else { + previous + }; + (*extended).catch = previous; + (value, self.finish()) + } + /// Decrement busy, take the first pending exception, then complete a /// deferred close at zero. Return the exception to throw outside C frames. pub fn finish(self) -> Result<(), CallEnd> { unsafe { let cell = self.link.0 as *mut NativeHandleHeader; - let scope = crate::gc::RuntimeHandleScope::new(); - let owner = scope.root_nanbox_f64(f64::from_bits((*cell).owner)); assert_ne!((*cell).busy, 0, "unbalanced native call"); (*cell).busy -= 1; let closing = (*cell).flags & CLOSING != 0; + if (*cell).flags & (PENDING | CLOSING) == 0 { + return Ok(()); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let owner = scope.root_nanbox_f64(f64::from_bits((*cell).owner)); let result = if (*cell).flags & PENDING != 0 { let state = root_pointer::( &scope, @@ -1138,12 +1274,101 @@ fn root_pointer<'a, T>( /// run JS while a native trampoline is preparing or parking an exception. fn state_field(state: &crate::gc::RuntimeHandle<'_>, key: &[u8]) -> f64 { state.with_mut_ptr::(|obj| unsafe { - state_key_index(obj, key).map_or_else(undefined, |i| { - f64::from_bits(crate::object::js_object_get_field(obj, i as u32).bits()) - }) + state_key_index(obj, key).map_or_else(undefined, |i| state_slot_get(obj, i)) }) } +/// [`state_field`] through a per-site [`StateKeyMemo`]. +fn state_field_memo( + state: &crate::gc::RuntimeHandle<'_>, + key: &[u8], + memo: &'static StateKeyMemo, +) -> f64 { + state.with_mut_ptr::(|obj| unsafe { + state_key_index_memo(obj, key, memo).map_or_else(undefined, |i| state_slot_get(obj, i)) + }) +} + +/// The slot a JS-state key had under one ShapeId, remembered per call site +/// and per agent (ShapeIds are per agent), exactly like an emitted `o.key` +/// site's compact word: one ShapeId compare, then one load. A miss walks the +/// keys and re-primes. Declare one with [`state_key_memo!`]. +pub type StateKeyMemo = std::thread::LocalKey>; + +/// Declare a [`StateKeyMemo`]: `state_key_memo!(static MEMO_DB);`. +#[macro_export] +macro_rules! state_key_memo { + ($vis:vis static $name:ident) => { + ::std::thread_local! { + $vis static $name: ::std::cell::Cell = const { ::std::cell::Cell::new(0) }; + } + }; +} + +unsafe fn state_key_index_memo( + obj: *const ObjectHeader, + name: &[u8], + memo: &'static StateKeyMemo, +) -> Option { + let shape = crate::object::shapes::object_shape_stamp(obj); + if shape != 0 { + let word = memo.with(|cell| cell.get()); + if (word >> 32) as u32 == shape { + return Some((word as u32) as usize); + } + } + let i = state_key_index(obj, name)?; + if shape != 0 { + memo.with(|cell| cell.set(((shape as u64) << 32) | i as u64)); + } + Some(i) +} + +/// Read a field of a live object through a memo. Never allocates, so no +/// handle scope is needed around it. +unsafe fn raw_field_memo(obj: *mut ObjectHeader, key: &[u8], memo: &'static StateKeyMemo) -> f64 { + state_key_index_memo(obj, key, memo).map_or_else(undefined, |i| state_slot_get(obj, i)) +} + +/// The JS state object of `obj` if it has one (no allocation). +unsafe fn raw_js_state(obj: *mut ObjectHeader) -> Option<*mut ObjectHeader> { + let state = raw_field_memo(obj, JS_STATE_KEY, &JS_STATE_MEMO); + let js = crate::value::JSValue::from_bits(state.to_bits()); + js.is_pointer() + .then(|| js.as_pointer::() as *mut ObjectHeader) +} + +state_key_memo!(static JS_STATE_MEMO); +state_key_memo!(static CALLBACKS_MEMO); +state_key_memo!(static PENDING_MEMO); + +/// A state field past the inline slots lives in the object's overflow +/// storage (a state grows by definition, one key at a time). +unsafe fn state_slot_get(obj: *mut ObjectHeader, i: usize) -> f64 { + // This is a live runtime-owned object, and the key lookup already proved + // the slot index. Resolve its inline bound once; the shared helper also + // handles overflow and the invalid null-pointer sentinel. + let live = crate::object::object_live_slot_count(obj); + f64::from_bits(crate::object::object_field_at_with_live(obj, i as u32, live).bits()) +} + +unsafe fn state_slot_set(obj: *mut ObjectHeader, i: usize, value: f64) { + if i < state_inline_limit(obj) { + // The ordinary indexed store includes the exact-slot barrier. + crate::object::js_object_set_field( + obj, + i as u32, + crate::JSValue::from_bits(value.to_bits()), + ); + } else { + crate::object::overflow_set(obj as usize, i, value.to_bits()); + } +} + +unsafe fn state_inline_limit(obj: *mut ObjectHeader) -> usize { + (crate::object::object_live_slot_count(obj) as usize).max(crate::object::INLINE_SLOT_FLOOR) +} + unsafe fn state_key_index(obj: *const ObjectHeader, name: &[u8]) -> Option { let keys = crate::object::object_keys(obj); let (slots, len) = keys.dense_slots(); @@ -1164,12 +1389,7 @@ fn set_state_field( let value = scope.root_nanbox_f64(value); if state.with_mut_ptr::(|obj| unsafe { if let Some(i) = state_key_index(obj, key) { - // The ordinary indexed store includes the exact-slot barrier. - crate::object::js_object_set_field( - obj, - i as u32, - crate::JSValue::from_bits(value.get_nanbox_f64().to_bits()), - ); + state_slot_set(obj, i, value.get_nanbox_f64()); true } else { false @@ -1195,9 +1415,21 @@ pub fn callbacks(owner: f64, family: &NativePayloadFamily) -> f64 { if !is_instance(owner, family) { return undefined(); } + if let Some(obj) = instance_of(owner, family.class_id) { + unsafe { + if let Some(state) = raw_js_state(obj) { + let existing = raw_field_memo(state, b"callbacks", &CALLBACKS_MEMO); + if crate::JSValue::from_bits(existing.to_bits()).is_pointer() { + return existing; + } + } + } + } let scope = crate::gc::RuntimeHandleScope::new(); - let state = root_pointer::(&scope, js_state(owner, family, true)); - let existing = state_field(&state, b"callbacks"); + let owner = scope.root_nanbox_f64(owner); + let state = + root_pointer::(&scope, js_state(owner.get_nanbox_f64(), family, true)); + let existing = state_field_memo(&state, b"callbacks", &CALLBACKS_MEMO); if crate::JSValue::from_bits(existing.to_bits()).is_pointer() { return existing; } @@ -1206,6 +1438,15 @@ pub fn callbacks(owner: f64, family: &NativePayloadFamily) -> f64 { crate::value::js_nanbox_pointer(p as i64) }); set_state_field(&scope, &state, b"callbacks", value); + // Re-read the array after the state store, which may collect. + sync_callbacks( + owner.get_nanbox_f64(), + family, + b"callbacks", + array.with_mut_ptr::(|p| { + crate::value::js_nanbox_pointer(p as i64) + }), + ); array .with_mut_ptr::(|p| crate::value::js_nanbox_pointer(p as i64)) } @@ -1235,6 +1476,14 @@ pub fn set_callback(owner: f64, family: &NativePayloadFamily, index: u32, f: f64 crate::value::js_nanbox_pointer(p as i64) }), ); + sync_callbacks( + owner.get_nanbox_f64(), + family, + b"callbacks", + updated.with_mut_ptr::(|p| { + crate::value::js_nanbox_pointer(p as i64) + }), + ); } /// Call JS under a catch, park the first throw on the owner and return an error @@ -1277,11 +1526,7 @@ pub unsafe fn call_from_native( let args_roots: Vec<_> = args.iter().map(|&v| scope.root_nanbox_f64(v)).collect(); // Pre-create the pending field before running JS. The throw landing only // writes an existing slot: it must not allocate with an unrooted exception. - let state = root_pointer::( - &scope, - js_state_for_class(owner.get_nanbox_f64(), class_id, true), - ); - set_state_field(&scope, &state, b"pendingException", undefined()); + ensure_pending_slot(cell, owner.get_nanbox_f64(), class_id); let args: Vec<_> = args_roots.iter().map(|v| v.get_nanbox_f64()).collect(); #[cfg(test)] if callback_sabotage("catch") { @@ -1292,14 +1537,12 @@ pub unsafe fn call_from_native( args.len(), )); } - match crate::exception::catch_js_throw(|| { - crate::closure::native_call_value_this( - callee.get_nanbox_f64(), - crate::closure::JsThis::from_f64(this.get_nanbox_f64()), - args.as_ptr(), - args.len(), - ) - }) { + match crate::exception::catch_native_callback( + callback_slots::catch_from_cell(cell), + callee.get_nanbox_f64(), + this.get_nanbox_f64(), + &args, + ) { Ok(value) => Ok(value), Err(err) => { // JS may have moved the owner. Re-read it through its handle; @@ -1310,6 +1553,103 @@ pub unsafe fn call_from_native( } } +/// The throw landing must not allocate with an unrooted exception, so the +/// pending slot exists before JS runs. While PENDING is clear it holds +/// undefined (finish and the landing keep that invariant): create it once +/// per owner and remember that on the cell. +unsafe fn ensure_pending_slot(cell: *mut NativeHandleHeader, owner: f64, class_id: u32) { + if (*cell).flags & PENDING_SLOT != 0 { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let owner = scope.root_nanbox_f64(owner); + let state = root_pointer::( + &scope, + js_state_for_class(owner.get_nanbox_f64(), class_id, true), + ); + if state.with_mut_ptr::(|obj| { + state_key_index_memo(obj, b"pendingException", &PENDING_MEMO).is_none() + }) { + set_state_field(&scope, &state, b"pendingException", undefined()); + } + (*cell).flags |= PENDING_SLOT; +} + +/// [`call_from_native`] for an S trampoline that holds the userdata's link +/// and proved it with [`link_owner`] on entry. The cell's state is +/// re-checked on every call (an earlier callback may have closed the owner +/// or parked an exception); its thread cannot change, so the trampoline's +/// proof stands. The owner is re-read from the cell (the GC rewrites the +/// slot). The caller keeps `callee` and every pointer in `args` rooted, or +/// reads them after its last allocation. +/// +/// # Safety +/// As [`call_from_native`]; `link` passed [`link_owner`] on this thread in +/// the calling trampoline. +#[inline] +pub unsafe fn call_from_link(link: OwnerLink, callee: f64, args: &[f64]) -> Result { + let cell = link.0 as *mut NativeHandleHeader; + if !cell_open(cell) || ((*cell).flags & PENDING != 0 && !pending_sabotage()) { + return Err(()); + } + if (*cell).flags & PENDING_SLOT == 0 { + return first_call_from_link(link, callee, args); + } + call_open_link(cell, callee, args) +} + +/// The owner's first callback: create its pending slot (see +/// [`ensure_pending_slot`]), then call. +#[cold] +#[inline(never)] +unsafe fn first_call_from_link(link: OwnerLink, callee: f64, args: &[f64]) -> Result { + let cell = link.0 as *mut NativeHandleHeader; + let scope = crate::gc::RuntimeHandleScope::new(); + let callee_root = scope.root_nanbox_f64(callee); + let roots: Vec<_> = args.iter().map(|v| scope.root_nanbox_f64(*v)).collect(); + ensure_pending_slot(cell, f64::from_bits((*cell).owner), (*cell).type_id as u32); + let args: Vec = roots.iter().map(|v| v.get_nanbox_f64()).collect(); + call_from_link(link, callee_root.get_nanbox_f64(), &args) +} + +/// [`call_from_link`] once the cell is open, nothing is pending and the +/// pending slot exists. +unsafe fn call_open_link( + cell: *mut NativeHandleHeader, + callee: f64, + args: &[f64], +) -> Result { + let this = undefined(); + #[cfg(test)] + if callback_sabotage("catch") { + return Ok(crate::closure::native_call_value_this( + callee, + crate::closure::JsThis::from_f64(this), + args.as_ptr(), + args.len(), + )); + } + match crate::exception::catch_native_callback( + callback_slots::catch_from_cell(cell), + callee, + this, + args, + ) { + Ok(value) => Ok(value), + Err(err) => { + let _ = set_pending_exception(f64::from_bits((*cell).owner), err); + Err(()) + } + } +} + +/// The open-cell half of [`link_owner`] (payload installed, neither closing +/// nor finalized), for a cell whose thread is already proven. +#[inline] +unsafe fn cell_open(cell: *mut NativeHandleHeader) -> bool { + (*cell).finalized == 0 && (*cell).flags & CLOSING == 0 && !(*cell).resource_ptr.is_null() +} + /// Park a trampoline validation failure without throwing through C. Returns /// Err when the receiver is invalid/finalized/wrong-thread or an earlier throw /// is pending. Accepts CLOSING while a guard still owns the native call. @@ -1492,16 +1832,14 @@ pub(crate) unsafe fn try_payload_method_fast_dispatch( { return None; } - let name_str = std::str::from_utf8(name).ok()?; + // Both are ordinary objects, whose keys carry every key's attributes + // (charter step 3): the instance owning no key of this name has no own + // descriptor for it, and the prototype's key entry says whether it is an + // accessor. if crate::object::dictionary::is_dictionary(obj) - || crate::object::descriptor_state::may_have_descriptor_entry(obj as usize, name_str, true) || shape_key_index(obj, name).is_some() || crate::object::dictionary::is_dictionary(proto) - || crate::object::descriptor_state::may_have_descriptor_entry( - proto as usize, - name_str, - true, - ) + || crate::object::key_attrs::object_key_is_accessor(proto, name) { return None; } @@ -1539,13 +1877,19 @@ fn js_state_for_class(value: f64, class_id: u32, create: bool) -> f64 { let Some(obj) = instance_of(value, class_id) else { return undefined; }; + if let Some(state) = unsafe { raw_js_state(obj) } { + return crate::value::js_nanbox_pointer(state as i64); + } + if !create { + return undefined; + } let scope = crate::gc::RuntimeHandleScope::new(); let obj = scope.root_raw_mut_ptr(obj); - let existing = state_field(&obj, JS_STATE_KEY); + let existing = state_field_memo(&obj, JS_STATE_KEY, &JS_STATE_MEMO); if crate::value::JSValue::from_bits(existing.to_bits()).is_pointer() || !create { return existing; } - let state = crate::object::js_object_alloc(0, 4); + let state = crate::object::js_object_alloc(0, 6); if state.is_null() { return undefined; } @@ -1571,6 +1915,142 @@ fn set_own( }); } +/// Read the field `key` of an instance's JS state (`undefined` when the +/// instance has no state or no such field). Never runs JS. +pub fn state_get(value: f64, family: &NativePayloadFamily, key: &[u8]) -> f64 { + let state = js_state(value, family, false); + if !crate::value::JSValue::from_bits(state.to_bits()).is_pointer() { + return undefined(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let state = root_pointer::(&scope, state); + state_field(&state, key) +} + +/// [`state_get`] through a per-site [`StateKeyMemo`] (hot keys). +pub fn state_get_memo( + value: f64, + family: &NativePayloadFamily, + key: &[u8], + memo: &'static StateKeyMemo, +) -> f64 { + let Some(obj) = instance_of(value, family.class_id) else { + return undefined(); + }; + unsafe { raw_js_state(obj).map_or_else(undefined, |state| raw_field_memo(state, key, memo)) } +} + +/// [`state_set`] through a per-site [`StateKeyMemo`] (hot keys). +pub fn state_set_memo( + value: f64, + family: &NativePayloadFamily, + key: &[u8], + v: f64, + memo: &'static StateKeyMemo, +) { + let scope = crate::gc::RuntimeHandleScope::new(); + let v = scope.root_nanbox_f64(v); + let owner = scope.root_nanbox_f64(value); + let state = js_state(owner.get_nanbox_f64(), family, true); + if !crate::value::JSValue::from_bits(state.to_bits()).is_pointer() { + return; + } + let state = root_pointer::(&scope, state); + let slot = state + .with_mut_ptr::(|obj| unsafe { state_key_index_memo(obj, key, memo) }); + match slot { + Some(i) => state.with_mut_ptr::(|obj| unsafe { + state_slot_set(obj, i, v.get_nanbox_f64()) + }), + None => set_state_field(&scope, &state, key, v.get_nanbox_f64()), + } + sync_callbacks(owner.get_nanbox_f64(), family, key, v.get_nanbox_f64()); +} + +/// Store `v` as the field `key` of an instance's JS state (created on +/// demand). An own data slot is defined directly; no setter can run. +pub fn state_set(value: f64, family: &NativePayloadFamily, key: &[u8], v: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let v = scope.root_nanbox_f64(v); + let owner = scope.root_nanbox_f64(value); + let state = js_state(owner.get_nanbox_f64(), family, true); + if !crate::value::JSValue::from_bits(state.to_bits()).is_pointer() { + return; + } + let state = root_pointer::(&scope, state); + set_state_field(&scope, &state, key, v.get_nanbox_f64()); + sync_callbacks(owner.get_nanbox_f64(), family, key, v.get_nanbox_f64()); +} + +/// Define an own accessor on an instance, for families whose node objects +/// carry own (not prototype) getters such as `db.isOpen`. The getter and +/// setter are per-realm singleton closures of `get` / `set`. +pub fn define_own_accessor( + value: f64, + name: &str, + get: *const crate::closure::JsFunctionInfo, + set: Option<*const crate::closure::JsFunctionInfo>, + enumerable: bool, + configurable: bool, +) { + let Some(obj) = any_object(value) else { + return; + }; + // The key is an interned literal and the closures are realm singletons; + // keep the heap still across the shape transition and the pair install. + let _no_move = crate::gc::GcSuppressScope::new(); + let getter = crate::closure::js_closure_alloc_singleton(get); + let getter_bits = crate::value::js_nanbox_pointer(getter as i64).to_bits(); + let setter_bits = set.map_or(0, |info| { + let setter = crate::closure::js_closure_alloc_singleton(info); + crate::value::js_nanbox_pointer(setter as i64).to_bits() + }); + let key = crate::string::intern_ascii_literal(name.as_bytes()); + unsafe { + crate::object::install_own_builtin_accessor( + obj, + key, + name, + getter_bits, + setter_bits, + crate::object::PropertyAttrs::new(true, enumerable, configurable), + ); + } +} + +/// `%IteratorPrototype%` of this realm, for a family prototype to inherit. +pub fn iterator_prototype() -> f64 { + crate::object::iterator_prototypes::ensure_iterator_prototypes(); + let ptr = crate::object::iterator_prototypes::ITERATOR_PROTOTYPE_PTR.load(Ordering::Acquire); + if ptr == 0 { + undefined() + } else { + crate::value::js_nanbox_pointer(ptr) + } +} + +/// A `{ done, value }` iterator result in node:sqlite's key order. +pub fn iter_result_done_value(done: bool, value: f64) -> f64 { + unsafe { + crate::iter_result::make_sqlite_iter_result( + crate::value::JSValue::from_bits(value.to_bits()), + done, + ) + } +} + +/// The family's per-realm prototype, materialized now. A family whose +/// constructor is a module export calls this when the export is created, so +/// `Export.prototype` carries the methods before any instance exists. +pub fn prototype(family: &NativePayloadFamily) -> f64 { + let proto = family_prototype(family); + if proto.is_null() { + undefined() + } else { + crate::value::js_nanbox_pointer(proto as i64) + } +} + /// The family whose class id is `class_id`, for `instanceof` against a module /// export. Families register nothing at runtime: this is the static list of /// node constructors that answer by class id. @@ -1584,6 +2064,9 @@ pub(crate) fn export_class_id(module: &str, export: &str) -> Option { ("async_hooks", "AsyncLocalStorage") => ids::ASYNC_LOCAL_STORAGE_LEGACY, ("async_hooks", "AsyncResource") => ids::ASYNC_RESOURCE_LEGACY, ("domain", "Domain") => ids::DOMAIN, + ("sqlite", "DatabaseSync") => ids::SQLITE_DATABASE_SYNC, + ("sqlite", "StatementSync") => ids::SQLITE_STATEMENT_SYNC, + ("sqlite", "Session") => ids::SQLITE_SESSION, _ => return None, }) } diff --git a/crates/perry-runtime/src/native_payload_birth.rs b/crates/perry-runtime/src/native_payload_birth.rs new file mode 100644 index 0000000000..81dde2d310 --- /dev/null +++ b/crates/perry-runtime/src/native_payload_birth.rs @@ -0,0 +1,347 @@ +//! An instance born in its construction's final shape. +//! +//! Every instance an [`alloc_with_state`](super::alloc_with_state) call site +//! makes gets the same own keys in the same order with the same attributes: +//! the site's own data keys, the hidden JS-state key, then the family's own +//! accessors (node's per-instance getters such as `StatementSync`'s +//! `sourceSQL`). Its JS state object, a null-prototype object, likewise gets +//! the site's state keys. So both shapes are facts of the call site. The +//! site's first birth in an agent builds them key by key through the ordinary +//! define paths and records the two ShapeIds the objects reached in the site's +//! [`BirthMemo`]; every later birth allocates each object directly in its +//! recorded shape and fills the slots: no key-add transition, no by-name +//! store, no accessor install. Each record is re-validated on every use +//! (`shapes::shape_is_filled_birth` and its attributed form), and ShapeIds +//! are never reused, so a record that no longer names those facts simply +//! stops replaying. + +use super::*; + +/// One own accessor an instance is born with. The getter and setter are the +/// per-realm singleton closures of `get` / `set`. +#[derive(Clone, Copy)] +pub struct OwnAccessor { + pub name: &'static str, + pub get: *const crate::closure::JsFunctionInfo, + pub set: Option<*const crate::closure::JsFunctionInfo>, + pub enumerable: bool, + pub configurable: bool, +} + +/// A call site's recorded final shapes: the instance's ShapeId in the high +/// half, its JS state's in the low half (0 = none recorded). Per agent, like +/// ShapeIds. Declare one with [`birth_memo!`](crate::birth_memo). +pub type BirthMemo = std::thread::LocalKey>; + +/// Declare a [`BirthMemo`]: `birth_memo!(static STMT_BIRTH);`. +#[macro_export] +macro_rules! birth_memo { + ($vis:vis static $name:ident) => { + ::std::thread_local! { + $vis static $name: ::std::cell::Cell = const { ::std::cell::Cell::new(0) }; + } + }; +} + +/// The site's instance with its JS state, born in the recorded final shapes +/// when the memo still names them, else built key by key and recorded. +pub(super) fn alloc_born( + family: &'static NativePayloadFamily, + payload: T, + external_bytes: usize, + own: &[(&[u8], f64)], + state: &[(&[u8], f64)], + accessors: &[OwnAccessor], + memo: &'static BirthMemo, +) -> f64 { + let proto = family_prototype(family); + if proto.is_null() { + return undefined(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let values: Vec<_> = own + .iter() + .chain(state) + .map(|&(_, value)| scope.root_nanbox_f64(value)) + .collect(); + let (own_values, state_values) = values.split_at(own.len()); + let recorded = memo.with(std::cell::Cell::get); + if recorded != 0 { + if let Some(obj) = + unsafe { born_final(family, recorded, own_values, state_values, accessors) } + { + let obj = scope.root_raw_mut_ptr(obj); + attach_rooted( + &obj, + family, + Some(payload), + plain_vtable::(), + external_bytes, + ); + return obj.with_mut_ptr::(|obj| { + crate::value::js_nanbox_pointer(obj as i64) + }); + } + } + // The site's first birth (or a record that no longer replays): the + // ordinary sequence, then record what it reached. + // Sized to its keys, so the recorded shape's live slots are its keys. + let state_obj = crate::object::js_object_alloc_null_proto(0, state.len() as u32); + if state_obj.is_null() { + return undefined(); + } + let state_obj = scope.root_raw_mut_ptr(state_obj); + for (&(key, _), value) in state.iter().zip(state_values) { + set_own(&scope, &state_obj, key, value.get_nanbox_f64()); + } + let state_value = + state_obj.with_mut_ptr::(|o| crate::value::js_nanbox_pointer(o as i64)); + let mut all: Vec<(&[u8], f64)> = own + .iter() + .zip(own_values) + .map(|(&(key, _), value)| (key, value.get_nanbox_f64())) + .collect(); + all.push((JS_STATE_KEY, state_value)); + let slots = all.len() + accessors.len(); + let value = scope.root_nanbox_f64(alloc_cell( + family, + Some(payload), + plain_vtable::(), + external_bytes, + &all, + slots, + proto, + )); + for accessor in accessors { + define_own_accessor( + value.get_nanbox_f64(), + accessor.name, + accessor.get, + accessor.set, + accessor.enumerable, + accessor.configurable, + ); + } + let obj = crate::JSValue::from_bits(value.get_nanbox_u64()).as_pointer::(); + let state_obj = state_obj.with_mut_ptr::(|o| o); + unsafe { + let instance = crate::object::shapes::object_shape_stamp(obj); + let state_shape = crate::object::shapes::object_shape_stamp(state_obj); + // Every later birth validates the record before it replays it. + memo.with(|m| m.set(u64::from(instance) << 32 | u64::from(state_shape))); + } + value.get_nanbox_f64() +} + +/// The prototype identity a family instance's shape names: its class-default +/// link to the family prototype (`born_instance`'s birth record). +unsafe fn family_proto_id(proto: *mut ObjectHeader) -> Option { + let meta = (*proto).meta; + (!meta.is_null()).then(|| (*meta).proto_serial) +} + +/// An unpublished null-prototype object, as `js_object_alloc_null_proto` +/// initializes one before publishing its birth shape. +unsafe fn unpublished_state(count: u32) -> *mut ObjectHeader { + let obj = crate::object::object_alloc_unpublished(0, count); + let gc = crate::gc::header_from_trusted_user_ptr(obj.cast()).cast_mut(); + (*gc)._reserved |= crate::gc::OBJ_FLAG_NULL_PROTO; + obj +} + +/// Stamp a fresh, unobserved object with `id`. +unsafe fn stamp(obj: *mut ObjectHeader, id: u32) { + if crate::arena::pointer_in_nursery(obj as usize) { + // GC_STORE_AUDIT(POINTER_FREE): a ShapeId, never a heap reference. + (*obj).parent_class_id = id; + } else { + crate::object::shapes::stamp_object_shape_id_with_carrier_note(obj, id); + } +} + +/// The family's shared accessor pairs, one per `accessors` entry in order: +/// a runtime-only array in its prototype's JS state (per realm, traced and +/// moved with the prototype; JS never reaches it, so a pair stays reachable +/// from JS only through an accessor slot), so every instance a birth replays holds the same pairs, as +/// every instance holds the same getter and setter closures. `None` when the +/// prototype's pairs are another accessor list's (that site then makes its +/// own pairs). +unsafe fn shared_pairs<'s>( + scope: &'s crate::gc::RuntimeHandleScope, + proto: *mut ObjectHeader, + accessors: &[OwnAccessor], +) -> Option> { + let held = proto_pairs(proto); + if crate::JSValue::from_bits(held.to_bits()).is_pointer() { + return pairs_match(held, accessors).then(|| scope.root_nanbox_f64(held)); + } + let proto = scope.root_raw_mut_ptr(proto); + let pairs = scope.root_raw_mut_ptr(crate::array::js_array_alloc(accessors.len() as u32)); + for accessor in accessors { + let get = crate::closure::js_closure_alloc_singleton(accessor.get); + let get = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(get as i64)); + let set = accessor.set.map_or(0, |info| { + let set = crate::closure::js_closure_alloc_singleton(info); + crate::value::js_nanbox_pointer(set as i64).to_bits() + }); + let pair = crate::object::accessor_pair::pair_new(crate::object::accessor_pair::Accessor { + get: get.get_nanbox_u64(), + set, + ..Default::default() + }); + let next = crate::array::js_array_push( + pairs.get_raw_mut_ptr(), + crate::JSValue::from_bits(crate::value::js_nanbox_pointer(pair as i64).to_bits()), + ); + pairs.set_raw_mut_ptr(next); + } + let mut state = proto + .with_mut_ptr::(|p| raw_field_memo(p, JS_STATE_KEY, &PROTO_STATE_MEMO)); + if !crate::JSValue::from_bits(state.to_bits()).is_pointer() { + let fresh = crate::object::js_object_alloc_null_proto(0, 1); + state = crate::value::js_nanbox_pointer(fresh as i64); + set_state_field(scope, &proto, JS_STATE_KEY, state); + state = proto.with_mut_ptr::(|p| { + raw_field_memo(p, JS_STATE_KEY, &PROTO_STATE_MEMO) + }); + } + let state = root_pointer::(scope, state); + let value = pairs.with_mut_ptr::(|a| { + crate::value::js_nanbox_pointer(a as i64) + }); + set_state_field(scope, &state, OWN_PAIRS_KEY, value); + Some(scope.root_nanbox_f64(value)) +} + +const OWN_PAIRS_KEY: &[u8] = b"ownAccessorPairs"; +crate::state_key_memo!(static PROTO_STATE_MEMO); +crate::state_key_memo!(static OWN_PAIRS_MEMO); + +/// The pairs array a prototype's JS state holds (`undefined` when none). +unsafe fn proto_pairs(proto: *mut ObjectHeader) -> f64 { + let state = raw_field_memo(proto, JS_STATE_KEY, &PROTO_STATE_MEMO); + let js = crate::JSValue::from_bits(state.to_bits()); + if !js.is_pointer() { + return undefined(); + } + raw_field_memo( + js.as_pointer::() as *mut ObjectHeader, + OWN_PAIRS_KEY, + &OWN_PAIRS_MEMO, + ) +} + +/// Do `pairs` hold exactly `accessors`' functions, in order? +unsafe fn pairs_match(pairs: f64, accessors: &[OwnAccessor]) -> bool { + let arr = crate::JSValue::from_bits(pairs.to_bits()).as_pointer::(); + if (*arr).length as usize != accessors.len() || (*arr).capacity < (*arr).length { + return false; + } + let info_of = |bits: u64| -> *const crate::closure::JsFunctionInfo { + if bits == 0 { + return std::ptr::null(); + } + let closure = (bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; + (*closure).info + }; + let words = crate::array::array_elements_ptr(arr); + accessors.iter().enumerate().all(|(i, accessor)| { + crate::object::accessor_pair::pair_of_value(*words.add(i)).is_some_and(|pair| { + info_of(pair.get) == accessor.get + && info_of(pair.set) == accessor.set.unwrap_or(std::ptr::null()) + }) + }) +} + +/// Do the recorded shape's lanes hold any value in slots `0..count` (no +/// typed lane), so a newborn's slots take plain initializing stores? +fn any_lanes(id: u32, count: usize) -> bool { + let rep = crate::object::shapes::shape_rep_by_id(id); + (0..count as u32).all(|slot| { + crate::object::field_rep::slot_rep(rep, slot) == crate::object::field_rep::REP_ANY + }) +} + +/// Initialize slot `i` of an unobserved newborn stamped with an all-`Any` +/// shape: the canonical store's string alias and barrier work; the shape's +/// rep is what traces the slot. +unsafe fn init_slot(obj: *mut ObjectHeader, i: usize, bits: u64) { + let fields = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::gc::runtime_store_jsvalue_slot_layout_deferred( + obj as usize, + fields.add(i) as usize, + i, + bits, + ); +} + +/// Both objects in the recorded shapes, or `None` (nothing reachable was +/// made) when a record no longer names them. Each object is stamped and +/// filled before anything else allocates. +unsafe fn born_final( + family: &'static NativePayloadFamily, + recorded: u64, + own: &[crate::gc::RuntimeHandle<'_>], + state: &[crate::gc::RuntimeHandle<'_>], + accessors: &[OwnAccessor], +) -> Option<*mut ObjectHeader> { + let scope = crate::gc::RuntimeHandleScope::new(); + let (instance_id, state_id) = ((recorded >> 32) as u32, recorded as u32); + let pairs = if accessors.is_empty() { + None + } else { + Some(shared_pairs(&scope, family_prototype(family), accessors)?) + }; + let state_obj = unpublished_state(state.len() as u32); + let kind = crate::object::shapes::store_kind::receiver_ordinary_kind(state_obj); + if !crate::object::shapes::shape_is_filled_birth( + state_id, + crate::object::shapes::PROTO_ID_NULL, + state.len() as u32, + kind, + ) || !any_lanes(state_id, state.len()) + { + return None; + } + stamp(state_obj, state_id); + for (i, value) in state.iter().enumerate() { + init_slot(state_obj, i, value.get_nanbox_u64()); + } + let state_obj = scope.root_raw_mut_ptr(state_obj); + let count = own.len() + 1 + accessors.len(); + let obj = crate::object::object_alloc_unpublished(family.class_id, count as u32); + let proto_id = family_proto_id(family_prototype(family))?; + let kind = crate::object::shapes::store_kind::receiver_ordinary_kind(obj); + if !crate::object::shapes::shape_is_attributed_filled_birth( + instance_id, + proto_id, + count as u32, + kind, + ) || !any_lanes(instance_id, count) + { + return None; + } + if !accessors.is_empty() { + // The accessors were born in the recorded keys with their + // attributes: the object's descriptor bit is initialized with them, + // as a builtin install leaves it (no table entry, no process gate). + let gc = crate::gc::header_from_trusted_user_ptr(obj.cast()).cast_mut(); + (*gc)._reserved |= crate::gc::OBJ_FLAG_HAS_DESCRIPTORS; + } + stamp(obj, instance_id); + for (i, value) in own.iter().enumerate() { + init_slot(obj, i, value.get_nanbox_u64()); + } + let state_bits = state_obj + .with_mut_ptr::(|o| crate::value::js_nanbox_pointer(o as i64).to_bits()); + init_slot(obj, own.len(), state_bits); + if let Some(pairs) = pairs { + let arr = crate::JSValue::from_bits(pairs.get_nanbox_u64()) + .as_pointer::(); + let words = crate::array::array_elements_ptr(arr); + for i in 0..accessors.len() { + init_slot(obj, own.len() + 1 + i, *words.add(i)); + } + } + Some(obj) +} diff --git a/crates/perry-runtime/src/native_payload_slots.rs b/crates/perry-runtime/src/native_payload_slots.rs new file mode 100644 index 0000000000..49941aab83 --- /dev/null +++ b/crates/perry-runtime/src/native_payload_slots.rs @@ -0,0 +1,215 @@ +//! Traced callback-array access on the stable payload cell. + +use super::*; + +/// Only callback families pay for this traced extension. The legacy/N header +/// and its layout remain unchanged; CALLBACK_STORAGE proves the allocation. +#[repr(C)] +pub(crate) struct NativeCallbackCell { + pub header: NativeHandleHeader, + pub callbacks: u64, + pub catch: *mut crate::exception::NativeCatch, +} + +#[inline] +pub(crate) unsafe fn callback_slot_address(cell: *mut NativeHandleHeader) -> Option<*mut u64> { + ((*cell).flags & CALLBACK_STORAGE != 0) + .then(|| &raw mut (*(cell as *mut NativeCallbackCell)).callbacks) +} + +/// The callback registered at `index` in the owner's callbacks array +/// (`undefined` when absent). Never allocates. +pub fn callback_at(owner: f64, family: &NativePayloadFamily, index: u32) -> f64 { + let Ok(cell) = payload_cell(owner, family.class_id) else { + return undefined(); + }; + unsafe { + if callback_slot_address(cell).is_some() { + callback_slot(cell, index) + } else { + let obj = instance_of(owner, family.class_id).unwrap(); + raw_js_state(obj).map_or_else(undefined, |state| { + let array = raw_field_memo(state, b"callbacks", &CALLBACKS_MEMO); + if !is_callback_array(crate::JSValue::from_bits(array.to_bits())) { + return undefined(); + } + callback_array_slot(array.to_bits(), index) + }) + } + } +} + +/// Read a callback through stable userdata, after its last allocating conversion. +/// Never allocates; the slot is rewritten by moving GC. Whether the owner +/// may still be called is [`call_from_link`]'s check, made at the call. +/// +/// # Safety +/// `link` passed [`link_owner`] on this thread in the calling trampoline. +/// The caller roots pointer arguments until the JS call. +#[inline] +pub unsafe fn callback_from_link(link: OwnerLink, index: u32) -> f64 { + callback_slot(link.0 as *mut NativeHandleHeader, index) +} + +/// Call the callback at `index` of the owner `link` names: the whole JS side +/// of an S trampoline that needs nothing else from the owner. One check that +/// the owner may run JS (open, on this thread, nothing pending) answers +/// `Err` with no JS otherwise; then the slot is read and called under the +/// native call's catch, as [`call_from_link`] calls. The caller keeps every +/// pointer in `args` rooted, or converted them after its last allocation. +/// +/// # Safety +/// `link` names a live cell, owned by its C resource. +#[inline] +pub unsafe fn call_callback(link: OwnerLink, index: u32, args: &[f64]) -> Result { + link_owner(link).ok_or(())?; + let cell = link.0 as *mut NativeHandleHeader; + if (*cell).flags & PENDING != 0 && !pending_sabotage() { + return Err(()); + } + let callee = callback_slot(cell, index); + if (*cell).flags & PENDING_SLOT == 0 { + return first_call_from_link(link, callee, args); + } + call_open_link(cell, callee, args) +} + +#[inline(always)] +unsafe fn callback_slot(cell: *mut NativeHandleHeader, index: u32) -> f64 { + let Some(slot) = callback_slot_address(cell) else { + return undefined(); + }; + callback_array_slot(*slot, index) +} + +#[inline(always)] +unsafe fn callback_array_slot(bits: u64, index: u32) -> f64 { + let array = crate::JSValue::from_bits(bits); + if !array.is_pointer() { + return undefined(); + } + let arr = array.as_pointer::(); + // The traced slot proves a real array, so ordinary reads need one + // header bit; growth may leave a forwarding stub before the next GC + // rewrite, and an index past the dense storage is a sparse own key. + let header = crate::gc::header_from_trusted_user_ptr(arr.cast()); + #[cfg(test)] + let test_path = + callback_sabotage("callback_forwarding") || callback_sabotage("callback_data_read"); + #[cfg(not(test))] + let test_path = false; + if test_path + || (*header).gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + || index >= (*arr).capacity + { + return callback_array_slot_slow(arr, index); + } + dense_callback(arr, index) +} + +#[inline(always)] +unsafe fn dense_callback(arr: *const crate::array::ArrayHeader, index: u32) -> f64 { + if index >= (*arr).length { + return undefined(); + } + let bits = *crate::array::array_elements_ptr(arr).add(index as usize); + if bits == crate::value::TAG_HOLE { + undefined() + } else { + f64::from_bits(bits) + } +} + +#[cold] +#[inline(never)] +unsafe fn callback_array_slot_slow(mut arr: *const crate::array::ArrayHeader, index: u32) -> f64 { + let header = crate::gc::header_from_trusted_user_ptr(arr.cast()); + let forwarded = (*header).gc_flags & crate::gc::GC_FLAG_FORWARDED != 0; + #[cfg(test)] + let forwarded = forwarded && !callback_sabotage("callback_forwarding"); + if forwarded { + arr = crate::array::clean_arr_ptr(arr); + if arr.is_null() { + return undefined(); + } + } + #[cfg(test)] + if callback_sabotage("callback_data_read") { + return f64::from_bits(crate::array::js_array_get(arr, index).bits()); + } + if index >= (*arr).length { + return undefined(); + } + if index >= (*arr).capacity { + return sparse_callback_slot(arr, index); + } + dense_callback(arr, index) +} + +pub(super) unsafe fn store_callbacks(cell: *mut NativeHandleHeader, value: f64) { + // GC_STORE_AUDIT(BARRIERED): exact malloc-parent edge, just like owner. + #[cfg(test)] + if callback_sabotage("callback_sync") { + return; + } + let value = crate::JSValue::from_bits(value.to_bits()); + let bits = if is_callback_array(value) { + value.bits() + } else { + crate::value::TAG_UNDEFINED + }; + let Some(slot) = callback_slot_address(cell) else { + return; + }; + *slot = bits; + #[cfg(test)] + if callback_sabotage("callback_barrier") { + return; + } + crate::gc::runtime_write_barrier_external_slot(cell as usize, slot as usize, bits); +} + +pub(super) fn sync_callbacks(owner: f64, family: &NativePayloadFamily, key: &[u8], value: f64) { + if key == b"callbacks" { + if let Ok(cell) = payload_cell(owner, family.class_id) { + unsafe { store_callbacks(cell, value) }; + } + } +} + +#[inline] +unsafe fn is_callback_array(value: crate::JSValue) -> bool { + value.is_pointer() + && crate::value::addr_class::try_read_gc_header(value.as_pointer::() as usize) + .is_some_and(|h| h.obj_type == crate::gc::GC_TYPE_ARRAY) +} + +pub(super) unsafe fn catch_from_cell( + cell: *mut NativeHandleHeader, +) -> *mut crate::exception::NativeCatch { + if (*cell).flags & CALLBACK_STORAGE == 0 { + return std::ptr::null_mut(); + } + (*(cell as *mut NativeCallbackCell)).catch +} + +#[cold] +unsafe fn sparse_callback_slot(arr: *const crate::array::ArrayHeader, mut index: u32) -> f64 { + let mut digits = [0u8; 10]; + let mut start = digits.len(); + loop { + start -= 1; + digits[start] = b'0' + (index % 10) as u8; + index /= 10; + if index == 0 { + break; + } + } + #[cfg(test)] + if callback_sabotage("callback_sparse_key") { + digits[start] = b'x'; + } + // Every byte above is ASCII. No heap or GC allocation. + let key = std::str::from_utf8_unchecked(&digits[start..]); + crate::array::array_named_property_get_by_name(arr, key).unwrap_or_else(undefined) +} diff --git a/crates/perry-runtime/src/object/iterator_prototypes.rs b/crates/perry-runtime/src/object/iterator_prototypes.rs index 51b24f1c1b..5fe494f304 100644 --- a/crates/perry-runtime/src/object/iterator_prototypes.rs +++ b/crates/perry-runtime/src/object/iterator_prototypes.rs @@ -355,6 +355,10 @@ fn build_iterator_prototypes() { return; } install_symbol_iterator(shared); + // `Iterator.prototype[Symbol.toStringTag]` is "Iterator": an iterator + // whose own prototype carries no tag (node:sqlite's + // StatementSyncIterator) prints as `[object Iterator]`. + set_to_string_tag(shared, "Iterator"); let array_proto = build_family_proto( crate::fn_info!(array_iterator_next_thunk, 1; with_declared(0), with_flags(crate::closure::FN_BUILTIN)), diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 4e4ba145bf..a4eacdd62c 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -61,7 +61,9 @@ pub use alloc::{ }; #[cfg(feature = "regex-engine")] pub(crate) use alloc_basic::object_alloc_plain_born; -pub(crate) use alloc_basic::{object_alloc_born, object_alloc_filled_birth, object_alloc_plain}; +pub(crate) use alloc_basic::{ + object_alloc_born, object_alloc_filled_birth, object_alloc_plain, object_alloc_unpublished, +}; #[allow(unused_imports)] pub(crate) use alloc_plain::mark_object_plain_ordinary; pub use assign::*; @@ -227,7 +229,9 @@ pub(crate) mod own_override; mod own_override_builtin_install_tests; #[cfg(test)] mod own_override_push_tests; -pub(crate) use object_ops::{ensure_key_in_keys_array, install_builtin_getter}; +pub(crate) use object_ops::{ + ensure_key_in_keys_array, install_builtin_getter, install_own_builtin_accessor, +}; mod object_ops_frozen; mod polymorphic_index; #[cfg(test)] diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 258ef43e49..6688f544dc 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -54,8 +54,7 @@ pub(crate) use callable_exports::{ prune_dead_builtin_closure_metadata_owners_young, scan_builtin_closure_metadata_roots_mut, scan_tls_derived_prototype_roots_mut, set_bound_native_closure_metadata, set_bound_native_closure_name, set_builtin_closure_length, - set_builtin_closure_non_constructable, sqlite_session_constructor_value, - sqlite_statement_sync_constructor_value, timers_promises_parent_namespace, + set_builtin_closure_non_constructable, timers_promises_parent_namespace, tls_constructor_prototype_is_instance_of, util_inspect_default_options_value, zlib_codes_object, }; @@ -98,8 +97,6 @@ crate::perry_thread_local! { /// process-global atomic, which kept an object/string written by an /// exited thread as a dangling, unrooted address. static BUFFER_POOL_SIZE_BITS: Cell = const { Cell::new(65536f64.to_bits()) }; - pub(crate) static SQLITE_STATEMENT_SYNC_CONSTRUCTOR_VALUE: Cell = const { Cell::new(0) }; - pub(crate) static SQLITE_SESSION_CONSTRUCTOR_VALUE: Cell = const { Cell::new(0) }; pub(crate) static UTIL_INSPECT_DEFAULT_OPTIONS: Cell = const { Cell::new(0) }; pub(crate) static UTIL_INSPECT_STYLES: Cell = const { Cell::new(0) }; pub(crate) static UTIL_INSPECT_COLORS: Cell = const { Cell::new(0) }; @@ -284,20 +281,6 @@ pub fn scan_native_callable_export_roots_mut(visitor: &mut crate::gc::RuntimeRoo visitor.visit_nanbox_u64_slot(&mut value_bits); slot.set(value_bits); }); - SQLITE_STATEMENT_SYNC_CONSTRUCTOR_VALUE.with(|slot| { - let mut value_bits = slot.get(); - if value_bits != 0 { - visitor.visit_nanbox_u64_slot(&mut value_bits); - slot.set(value_bits); - } - }); - SQLITE_SESSION_CONSTRUCTOR_VALUE.with(|slot| { - let mut value_bits = slot.get(); - if value_bits != 0 { - visitor.visit_nanbox_u64_slot(&mut value_bits); - slot.set(value_bits); - } - }); UTIL_INSPECT_DEFAULT_OPTIONS.with(|slot| { let mut value_bits = slot.get(); if value_bits != 0 { diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index 2c88f1d2dc..9207f48acf 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -345,60 +345,6 @@ pub(crate) fn is_cluster_emitter_method(prop: &str) -> bool { ) } -extern "C" fn sqlite_statement_sync_constructor_thunk( - _closure: *const crate::closure::ClosureHeader, - _this: crate::closure::JsThis, -) -> f64 { - crate::fs::validate::throw_error_with_code("Illegal constructor", "ERR_ILLEGAL_CONSTRUCTOR") -} - -extern "C" fn sqlite_session_constructor_thunk( - _closure: *const crate::closure::ClosureHeader, - _this: crate::closure::JsThis, -) -> f64 { - crate::fs::validate::throw_error_with_code("Illegal constructor", "ERR_ILLEGAL_CONSTRUCTOR") -} - -pub(crate) fn sqlite_statement_sync_constructor_value() -> f64 { - SQLITE_STATEMENT_SYNC_CONSTRUCTOR_VALUE.with(|slot| { - let cached = slot.get(); - if cached != 0 { - return f64::from_bits(cached); - } - - let func_ptr = - crate::fn_info!(sqlite_statement_sync_constructor_thunk, 0; with_declared(0)); - let closure = crate::closure::js_closure_alloc_singleton(func_ptr); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - set_bound_native_closure_name(closure, "StatementSync"); - let value = crate::value::js_nanbox_pointer(closure as i64); - slot.set(value.to_bits()); - value - }) -} - -pub(crate) fn sqlite_session_constructor_value() -> f64 { - SQLITE_SESSION_CONSTRUCTOR_VALUE.with(|slot| { - let cached = slot.get(); - if cached != 0 { - return f64::from_bits(cached); - } - - let func_ptr = crate::fn_info!(sqlite_session_constructor_thunk, 0; with_declared(0)); - let closure = crate::closure::js_closure_alloc_singleton(func_ptr); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - set_bound_native_closure_name(closure, "Session"); - let value = crate::value::js_nanbox_pointer(closure as i64); - attach_sqlite_session_prototype(value); - slot.set(value.to_bits()); - value - }) -} - fn native_callable_export_display_name<'a>(module: &str, prop: &'a str) -> &'a str { if module == "fs" { match prop { @@ -687,25 +633,6 @@ const BUFFER_PROTOTYPE_METHODS: &[&str] = &[ "utf8Write", ]; -const SQLITE_DATABASE_SYNC_PROTOTYPE_METHODS: &[&str] = &[ - "open", - "close", - "exec", - "prepare", - "function", - "aggregate", - "enableDefensive", - "setAuthorizer", - "createTagStore", - "createSession", - "applyChangeset", - "enableLoadExtension", - "loadExtension", - "location", -]; - -const SQLITE_SESSION_PROTOTYPE_METHODS: &[&str] = &["changeset", "patchset", "close"]; - const ASSERT_PROTOTYPE_METHODS: &[&str] = &[ "fail", "ok", @@ -806,170 +733,6 @@ fn attach_assert_prototype(constructor_value: f64) { ); } -extern "C" fn sqlite_database_sync_prototype_method_thunk( - closure: *const crate::closure::ClosureHeader, - this: crate::closure::JsThis, - arg0: f64, - arg1: f64, - arg2: f64, -) -> f64 { - unsafe { - let method_name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; - let method_name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; - let receiver = this.as_f64(); - let args = [arg0, arg1, arg2]; - crate::object::js_native_call_method( - receiver, - method_name_ptr, - method_name_len, - args.as_ptr(), - args.len(), - ) - } -} - -fn attach_sqlite_database_sync_prototype(constructor_value: f64) { - let constructor_js = JSValue::from_bits(constructor_value.to_bits()); - if !constructor_js.is_pointer() { - return; - } - let closure = constructor_js.as_pointer::() as usize; - if closure == 0 { - return; - } - - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return; - } - - let constructor = "constructor"; - let constructor_key = - crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); - super::define_builtin_data_property( - proto, - constructor_key, - constructor_value, - constructor.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - let func_ptr = - crate::fn_info!(sqlite_database_sync_prototype_method_thunk, 3; with_declared(3)); - for method in SQLITE_DATABASE_SYNC_PROTOTYPE_METHODS { - let leaked: &'static [u8] = method.as_bytes().to_vec().leak(); - let method_closure = crate::closure::js_closure_alloc(func_ptr, 2); - if method_closure.is_null() { - continue; - } - crate::closure::js_closure_set_capture_ptr(method_closure, 0, leaked.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(method_closure, 1, leaked.len() as i64); - set_bound_native_closure_name(method_closure, method); - set_builtin_closure_length(method_closure as usize, 0); - let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); - let method_value = crate::value::js_nanbox_pointer(method_closure as i64); - super::define_builtin_data_property( - proto, - key, - method_value, - (*method).to_string(), - super::PropertyAttrs::new(true, false, true), - ); - } - - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(closure, "prototype", proto_value); - super::set_builtin_property_attrs( - closure, - "prototype".to_string(), - super::PropertyAttrs::new(true, false, false), - ); -} - -fn attach_sqlite_session_prototype(constructor_value: f64) { - let constructor_js = JSValue::from_bits(constructor_value.to_bits()); - if !constructor_js.is_pointer() { - return; - } - let closure = constructor_js.as_pointer::() as usize; - if closure == 0 { - return; - } - - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return; - } - - let func_ptr = - crate::fn_info!(sqlite_database_sync_prototype_method_thunk, 3; with_declared(3)); - for method in SQLITE_SESSION_PROTOTYPE_METHODS { - let leaked: &'static [u8] = method.as_bytes().to_vec().leak(); - let method_closure = crate::closure::js_closure_alloc(func_ptr, 2); - if method_closure.is_null() { - continue; - } - crate::closure::js_closure_set_capture_ptr(method_closure, 0, leaked.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(method_closure, 1, leaked.len() as i64); - set_bound_native_closure_name(method_closure, method); - set_builtin_closure_length(method_closure as usize, 0); - let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); - let method_value = crate::value::js_nanbox_pointer(method_closure as i64); - super::define_builtin_data_property( - proto, - key, - method_value, - (*method).to_string(), - super::PropertyAttrs::new(true, true, true), - ); - } - - let dispose_method = "@@__perry_wk_dispose"; - let dispose_leaked: &'static [u8] = dispose_method.as_bytes().to_vec().leak(); - let dispose_closure = crate::closure::js_closure_alloc(func_ptr, 2); - if !dispose_closure.is_null() { - crate::closure::js_closure_set_capture_ptr( - dispose_closure, - 0, - dispose_leaked.as_ptr() as i64, - ); - crate::closure::js_closure_set_capture_ptr(dispose_closure, 1, dispose_leaked.len() as i64); - set_bound_native_closure_name(dispose_closure, "[Symbol.dispose]"); - set_builtin_closure_length(dispose_closure as usize, 0); - let dispose_value = crate::value::js_nanbox_pointer(dispose_closure as i64); - let dispose_sym = crate::symbol::well_known_symbol("dispose"); - if !dispose_sym.is_null() { - let dispose_sym_value = crate::value::js_nanbox_pointer(dispose_sym as i64); - unsafe { - crate::symbol::js_object_set_symbol_property( - crate::value::js_nanbox_pointer(proto as i64), - dispose_sym_value, - dispose_value, - ); - } - } - } - - let constructor = "constructor"; - let constructor_key = - crate::string::js_string_from_bytes(constructor.as_ptr(), constructor.len() as u32); - super::define_builtin_data_property( - proto, - constructor_key, - constructor_value, - constructor.to_string(), - super::PropertyAttrs::new(true, false, true), - ); - - let proto_value = crate::value::js_nanbox_pointer(proto as i64); - crate::closure::closure_set_dynamic_prop(closure, "prototype", proto_value); - super::set_builtin_property_attrs( - closure, - "prototype".to_string(), - super::PropertyAttrs::new(true, false, false), - ); -} - fn attach_crypto_key_object_shape(closure_addr: usize, constructor_value: f64) { let from_value = bound_native_callable_export_value("crypto.KeyObject", "from"); crate::closure::closure_set_dynamic_prop(closure_addr, "from", from_value); @@ -1626,11 +1389,22 @@ pub(crate) unsafe fn nm_attach_sqlite( mut value: f64, _closure_addr: usize, ) -> f64 { - if property_name == "DatabaseSync" { - attach_sqlite_database_sync_prototype(value); - } - if property_name == "Session" { - attach_sqlite_session_prototype(value); + // `DatabaseSync` / `StatementSync` / `Session` are native-payload + // families (#11919): materialize the family prototype now, so the export + // carries its `prototype` (with the methods) before any instance exists. + // The family installs it on this very export value. + if matches!(property_name, "DatabaseSync" | "StatementSync" | "Session") { + let ptr = crate::value::JS_NATIVE_SQLITE_DISPATCH.load(std::sync::atomic::Ordering::SeqCst); + if !ptr.is_null() { + let dispatch: crate::value::JsNativeSqliteDispatchFn = std::mem::transmute(ptr); + dispatch( + property_name.as_ptr(), + property_name.len(), + std::ptr::null(), + 0, + crate::value::NATIVE_SQLITE_DISPATCH_PROTOTYPE, + ); + } } value } diff --git a/crates/perry-runtime/src/object/native_module/constants.rs b/crates/perry-runtime/src/object/native_module/constants.rs index 59f303b0ca..637450abcf 100644 --- a/crates/perry-runtime/src/object/native_module/constants.rs +++ b/crates/perry-runtime/src/object/native_module/constants.rs @@ -520,8 +520,6 @@ pub(crate) unsafe fn get_native_module_constant( }, "sqlite" => match property { "constants" => Some(create_sub_namespace("sqlite.constants")), - "Session" => Some(sqlite_session_constructor_value()), - "StatementSync" => Some(sqlite_statement_sync_constructor_value()), _ => None, }, "sqlite.constants" => sqlite_const(property), diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index edab6b5187..9aba3db63e 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -53,8 +53,9 @@ pub(crate) use prototype::get_prototype_of_resolved; // `instanceof.rs` definition (preserves the pre-split resolution). pub(crate) use keys_array::{ ensure_key_in_keys_array, ensure_key_in_keys_array_for_value, - ensure_key_in_keys_array_with_entry, install_builtin_getter, own_key_present, - own_key_present_via_index, own_property_present, own_property_present_via_index, + ensure_key_in_keys_array_with_entry, install_builtin_getter, install_own_builtin_accessor, + own_key_present, own_key_present_via_index, own_property_present, + own_property_present_via_index, }; /// Helper: extract object pointer from NaN-boxed f64. Returns null on failure. diff --git a/crates/perry-runtime/src/object/object_ops/keys_array.rs b/crates/perry-runtime/src/object/object_ops/keys_array.rs index 909f37ffcf..c6794941cf 100644 --- a/crates/perry-runtime/src/object/object_ops/keys_array.rs +++ b/crates/perry-runtime/src/object/object_ops/keys_array.rs @@ -720,6 +720,38 @@ pub(crate) unsafe fn install_builtin_getter(proto: *mut ObjectHeader, key: &str, ); } +/// Install a built-in accessor as an OWN property of a freshly allocated +/// ordinary object (node's per-instance getters such as `db.isOpen`): the key +/// is claimed with the accessor's attributes and the pair lives in the key's +/// slot, exactly as [`install_builtin_getter`] does for a prototype. The +/// caller keeps the heap still (`GcSuppressScope`) and roots the object. +pub(crate) unsafe fn install_own_builtin_accessor( + obj: *mut ObjectHeader, + key_str: *const crate::StringHeader, + key: &str, + getter_bits: u64, + setter_bits: u64, + attrs: PropertyAttrs, +) { + if obj.is_null() || (obj as usize) < 0x10000 || key_str.is_null() { + return; + } + let entry = crate::object::key_attrs::AttrsEdit::Data(&[], attrs.bits).apply( + crate::object::key_attrs::AttrsEdit::Accessor(&[], getter_bits != 0, setter_bits != 0) + .apply(0), + ); + ensure_key_in_keys_array_with_entry(obj, key_str, entry); + set_builtin_accessor_descriptor( + obj as usize, + key.to_string(), + AccessorDescriptor { + get: getter_bits, + set: setter_bits, + }, + attrs, + ); +} + /// A builtin getter's attributes: writable is N/A for an accessor; /// enumerable=false, configurable=true. const BUILTIN_GETTER_ATTRS: PropertyAttrs = PropertyAttrs::new(true, false, true); diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index 976cab0fe7..99f1934b8e 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -4711,6 +4711,36 @@ pub(crate) fn shape_is_filled_birth( proto_id: u64, count: u32, kind: ShapeObjectKind, +) -> bool { + filled_birth(id, proto_id, count, kind, false) +} + +/// [`shape_is_filled_birth`] for a construction whose recorded keys may +/// carry attributes: own accessors (or other non-default attributes) born +/// with the object, as a native payload instance's own getters are. The +/// keys' entries are those the construction's first run installed, since a +/// ShapeId never comes to name other keys. Installing an accessor's +/// functions mints a semantic generation (`transition_object_shape_accessor_replaced`), +/// so the recorded shape may carry one: it retires caches trained on OTHER +/// ids, and every object born on this id holds the same accessor functions +/// (the construction's realm singletons) as the object that minted it. +#[inline] +pub(crate) fn shape_is_attributed_filled_birth( + id: u32, + proto_id: u64, + count: u32, + kind: ShapeObjectKind, +) -> bool { + filled_birth(id, proto_id, count, kind, true) +} + +#[inline] +fn filled_birth( + id: u32, + proto_id: u64, + count: u32, + kind: ShapeObjectKind, + attributed: bool, ) -> bool { let Some(record) = ShapeSlab::agent_record_present(id) else { return false; @@ -4723,9 +4753,9 @@ pub(crate) fn shape_is_filled_birth( && kind.is_ordinary_layout() && r.logical_key_count == count && r.live_inline_slot_count == count - && r.semantic_generation == 0 + && (attributed || r.semantic_generation == 0) && r.hole_count == 0 - && r.summary() == 0 + && (attributed || r.summary() == 0) && r.special_constfn_mask() == 0 && r.keys != 0 } diff --git a/crates/perry-runtime/src/value/handle.rs b/crates/perry-runtime/src/value/handle.rs index a49680eb7a..3c0d83496c 100644 --- a/crates/perry-runtime/src/value/handle.rs +++ b/crates/perry-runtime/src/value/handle.rs @@ -100,6 +100,10 @@ pub extern "C" fn js_set_native_events_dispatch(func: JsNativeQuerystringDispatc /// Set the node:sqlite module dispatcher. Registered by perry-stdlib at /// startup so captured and dynamic-imported sqlite exports reach stdlib. +/// `construct` argument of the sqlite dispatch hook: materialize the +/// export's native-payload prototype (no arguments, no instance). +pub const NATIVE_SQLITE_DISPATCH_PROTOTYPE: i32 = 2; + #[no_mangle] pub extern "C" fn js_set_native_sqlite_dispatch(func: JsNativeSqliteDispatchFn) { JS_NATIVE_SQLITE_DISPATCH.store(func as *mut (), Ordering::SeqCst); diff --git a/crates/perry-runtime/src/value/mod.rs b/crates/perry-runtime/src/value/mod.rs index b452bd0f87..7e467ab3ac 100644 --- a/crates/perry-runtime/src/value/mod.rs +++ b/crates/perry-runtime/src/value/mod.rs @@ -104,7 +104,7 @@ pub use handle::{ js_set_native_http_dispatch, js_set_native_module_js_loader, js_set_native_net_dispatch, js_set_native_querystring_dispatch, js_set_native_sqlite_dispatch, js_set_native_tls_dispatch, js_set_native_webcrypto_dispatch, js_set_native_zlib_dispatch, js_set_new_from_handle_v8, - native_module_try_js_property, + native_module_try_js_property, NATIVE_SQLITE_DISPATCH_PROTOTYPE, }; // ----- Basic NaN-box pack / unpack FFI ----- diff --git a/crates/perry-stdlib/src/bun_sql.rs b/crates/perry-stdlib/src/bun_sql.rs index 6f733c600e..8e40183d56 100644 --- a/crates/perry-stdlib/src/bun_sql.rs +++ b/crates/perry-stdlib/src/bun_sql.rs @@ -102,10 +102,10 @@ unsafe fn execute_sqlite_query(db_handle: Handle, strings_value: f64, params_val let params_ptr = template_array(params.get_nanbox_f64()) .expect("the rooted tagged-template parameter array remains an array"); let rows = js_call_catching(|| { - let rows = js_node_sqlite_statement_sync_all(statement, params_ptr); + let rows = js_bun_sqlite_statement_all(statement, params_ptr); f64::from_bits(JSValue::array_ptr(rows).bits()) }); - finalize_node_sqlite_statement_handle(statement); + finalize_bun_sqlite_statement_handle(statement); match rows { Ok(rows) => rows, Err(error) => js_throw(error), @@ -138,7 +138,7 @@ fn transaction_sql(nested: bool, success: bool) -> &'static str { unsafe fn finish_transaction(db_handle: Handle, nested: bool, success: bool) { let sql = transaction_sql(nested, success); - with_open_node_connection(db_handle, |conn| { + with_open_bun_connection(db_handle, |conn| { if let Err((message, code)) = node_sqlite_exec_batch(conn, sql) { throw_sqlite_error_ext(&message, code); } @@ -193,14 +193,14 @@ extern "C" fn bun_sql_begin( let callback = scope.root_nanbox_f64(callback_value); let client = scope.root_nanbox_f64(captured_client(closure)); let db_handle = captured_handle(closure); - let nested = with_open_node_connection(db_handle, |conn| !conn.is_autocommit()); + let nested = with_open_bun_connection(db_handle, |conn| !conn.is_autocommit()); let begin = if nested { "SAVEPOINT perry_bun_sql" } else { "BEGIN" }; let started = js_call_catching(|| { - with_open_node_connection(db_handle, |conn| { + with_open_bun_connection(db_handle, |conn| { if let Err((message, code)) = node_sqlite_exec_batch(conn, begin) { throw_sqlite_error_ext(&message, code); } @@ -321,7 +321,7 @@ extern "C" fn bun_sql_close( let onclose = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, METHOD_ONCLOSE_CAPTURE)); match js_call_catching(|| { - js_node_sqlite_database_sync_close(db_handle); + js_bun_sqlite_database_close(db_handle); undefined_f64() }) { Ok(_) => match call_onclose( diff --git a/crates/perry-stdlib/src/common/dispatch/init.rs b/crates/perry-stdlib/src/common/dispatch/init.rs index 92d163ad72..1adaba795d 100644 --- a/crates/perry-stdlib/src/common/dispatch/init.rs +++ b/crates/perry-stdlib/src/common/dispatch/init.rs @@ -3,11 +3,9 @@ use crate::common::feature_hooks::{HandleArm, Hook, PropertySetArm}; // Slots for the optional-feature arms of the three hubs in this file; see // `method_dispatch.rs` for the scheme. -static SET_SQLITE: Hook = Hook::empty(); static SET_HTTP_SERVER_STATUS: Hook = Hook::empty(); static SET_HTTP_AGENT: Hook = Hook::empty(); static SET_HTTP_SERVER_SOCKET: Hook = Hook::empty(); -static OWN_NAMES_SQLITE: Hook = Hook::empty(); static PROTOTYPE_CRYPTO: Hook = Hook::empty(); /// `try_arm!` for the property-set hub, whose arms answer "claimed". @@ -43,8 +41,6 @@ pub unsafe extern "C" fn js_handle_property_set_dispatch( let _ = handle; let _ = value; - try_set_arm!(SET_SQLITE, handle, property_name, value); - if crate::common::net_method_values::dispatch_property_set(handle, property_name, value) { return; } @@ -70,7 +66,6 @@ pub unsafe extern "C" fn js_handle_property_set_dispatch( #[no_mangle] pub unsafe extern "C" fn js_handle_own_property_names_dispatch(handle: i64) -> f64 { - try_arm!(OWN_NAMES_SQLITE, handle); if crate::string_decoder::is_string_decoder_handle(handle) { return crate::string_decoder::string_decoder_own_property_names(handle); } @@ -394,8 +389,6 @@ pub(super) unsafe fn install_external_zlib_registrations() { #[cfg(feature = "database-sqlite")] pub(super) unsafe fn install_sqlite_registrations() { perry_runtime::js_set_native_sqlite_dispatch(crate::sqlite::js_node_sqlite_native_dispatch); - SET_SQLITE.set(set_sqlite); - OWN_NAMES_SQLITE.set(own_names_sqlite); } #[cfg(feature = "external-http-server-pump")] @@ -467,14 +460,6 @@ pub(super) unsafe fn install_streams_registrations() { } } -#[cfg(feature = "database-sqlite")] -unsafe fn set_sqlite(handle: i64, property_name: &str, value: f64) -> bool { - if crate::sqlite::dispatch_node_sqlite_limits_set(handle, property_name, value) { - return true; - } - false -} - #[cfg(feature = "external-http-server-pump")] unsafe fn set_http_server_status(handle: i64, property_name: &str, value: f64) -> bool { if matches!( @@ -576,14 +561,6 @@ unsafe fn set_http_server_socket(handle: i64, property_name: &str, value: f64) - false } -#[cfg(feature = "database-sqlite")] -unsafe fn own_names_sqlite(handle: i64) -> Option { - if let Some(names) = crate::sqlite::dispatch_node_sqlite_own_property_names(handle) { - return Some(names); - } - None -} - #[cfg(feature = "crypto")] unsafe fn prototype_crypto(handle: i64) -> Option { if crate::common::handle::with_handle::(handle, |_| true) diff --git a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs index 785015af24..91e859f2ad 100644 --- a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs @@ -360,103 +360,17 @@ unsafe fn arm_nodemailer(handle: i64, method_name: &str, args: &[f64]) -> Option #[cfg(feature = "database-sqlite")] unsafe fn arm_node_sqlite(handle: i64, method_name: &str, args: &[f64]) -> Option { - // node:sqlite DatabaseSync handle. Keep this before the better-sqlite3 - // SQLite fallbacks because method names like prepare/exec/close overlap - // but the lifecycle/error semantics are intentionally different. - if matches!( - method_name, - "open" - | "close" - | "exec" - | "prepare" - | "serialize" - | "deserialize" - // `function`/`aggregate`/`enableDefensive`/`setAuthorizer` were - // missing from this gate (#6561): an any-typed - // `db.function(...)` / `db.aggregate(...)` fell through the - // tower and silently no-op'd, so the SQL function was never - // registered and the next query failed with - // "no such function: ". - | "function" - | "aggregate" - | "enableDefensive" - | "setAuthorizer" - | "createTagStore" - | "createSession" - | "applyChangeset" - | "enableLoadExtension" - | "loadExtension" - | "location" - // #10290: `query`/`run`/`transaction` are the bun:sqlite `Database` - // surface. `dispatch_node_sqlite_database_method` has always - // implemented all three (the same registry backs both bindings), - // but they were missing from THIS gate, so they never reached it. - // Codegen's static NATIVE_MODULE_TABLE entry covers `db.query(...)` - // only while the receiver's class is provable; the moment the - // database reaches a call site through any indirection — a field, - // an interface-typed parameter, a generator return, Drizzle's - // driver object — the call falls to this dynamic tower and - // silently returned `undefined`. `db.query(sql)` answering - // `undefined` is how `opencode models` dies. - | "query" - | "run" - | "transaction" - | "__perry_dispose__" - | "@@__perry_wk_dispose" - ) { - if let Some(result) = - crate::sqlite::dispatch_node_sqlite_database_method(handle, method_name, &args) - { - return Some(result); - } - } - - // node:sqlite SQLTagStore handle. Keep this before StatementSync because - // the query execution method names overlap but tag stores consume tagged - // template arguments and bind them positionally. - if matches!(method_name, "run" | "get" | "all" | "iterate" | "clear") { - if let Some(result) = - crate::sqlite::dispatch_node_sqlite_tag_store_method(handle, method_name, &args) - { - return Some(result); - } - } - - // node:sqlite StatementSync handle. Keep this before the better-sqlite3 - // statement fallback because run/get/all overlap but Node's parameter and - // result semantics are different. - if matches!( - method_name, - "run" - | "get" - | "all" - | "iterate" - | "columns" - | "setReadBigInts" - | "setReturnArrays" - | "setAllowBareNamedParameters" - | "setAllowUnknownNamedParameters" - ) { - if let Some(result) = - crate::sqlite::dispatch_node_sqlite_statement_method(handle, method_name, &args) - { - return Some(result); - } - } - - // node:sqlite Session handle. This follows DatabaseSync dispatch because - // `close` overlaps and the database lifecycle rules should win for DBs. - if matches!( - method_name, - "changeset" | "patchset" | "close" | "__perry_dispose__" | "@@__perry_wk_dispose" - ) { - if let Some(result) = - crate::sqlite::dispatch_node_sqlite_session_method(handle, method_name, &args) - { - return Some(result); - } + // bun:sqlite `Database` / `Statement` registry handles (also Bun.SQL's + // SQLite adapter). Keep these before the better-sqlite3 fallbacks: + // prepare/exec/close/run/get/all overlap with different semantics. + // node:sqlite objects never reach this tower: they are ordinary objects + // whose methods resolve from their prototypes. + if let Some(result) = + crate::sqlite::dispatch_bun_sqlite_database_method(handle, method_name, args) + { + return Some(result); } - None + crate::sqlite::dispatch_bun_sqlite_statement_method(handle, method_name, args) } #[cfg(feature = "crypto")] diff --git a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs index 8dd82ce288..01c32b276f 100644 --- a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs @@ -298,32 +298,10 @@ unsafe fn prop_http_agent(handle: i64, property_name: &str) -> Option { #[cfg(feature = "database-sqlite")] unsafe fn prop_sqlite(handle: i64, property_name: &str) -> Option { - { - if let Some(v) = - crate::sqlite::dispatch_node_sqlite_database_property(handle, property_name) - { - return Some(v); - } - if let Some(v) = - crate::sqlite::dispatch_node_sqlite_tag_store_property(handle, property_name) - { - return Some(v); - } - if let Some(v) = - crate::sqlite::dispatch_node_sqlite_statement_property(handle, property_name) - { - return Some(v); - } - if let Some(v) = crate::sqlite::dispatch_node_sqlite_limits_property(handle, property_name) - { - return Some(v); - } - if let Some(v) = crate::sqlite::dispatch_node_sqlite_session_property(handle, property_name) - { - return Some(v); - } + if let Some(v) = crate::sqlite::dispatch_bun_sqlite_database_property(handle, property_name) { + return Some(v); } - None + crate::sqlite::dispatch_bun_sqlite_statement_property(handle, property_name) } #[cfg(feature = "external-http-server-pump")] diff --git a/crates/perry-stdlib/src/runtime_thread_exit_tests/stdlib_misc_tests.rs b/crates/perry-stdlib/src/runtime_thread_exit_tests/stdlib_misc_tests.rs index 43afe2aa4d..60034ae6a8 100644 --- a/crates/perry-stdlib/src/runtime_thread_exit_tests/stdlib_misc_tests.rs +++ b/crates/perry-stdlib/src/runtime_thread_exit_tests/stdlib_misc_tests.rs @@ -56,50 +56,6 @@ fn thread_exit_releases_the_threads_tls_server_listener_and_once_entries() { ); } -#[cfg(feature = "database-sqlite")] -#[test] -fn thread_exit_releases_the_threads_node_sqlite_callbacks_and_authorizer() { - use crate::sqlite::thread_exit_probe as sq; - let (function, aggregate, state, db, present) = std::thread::spawn(|| { - let callbacks: Vec = (0..4) - .map(|_| perry_runtime::value::js_nanbox_pointer(closure_here())) - .collect(); - let function = sq::register_function_for_test(callbacks[0]); - let aggregate = sq::register_aggregate_for_test(callbacks[1]); - let state = sq::register_aggregate_state_for_test(callbacks[2]); - let db = sq::register_db_with_authorizer_for_test(callbacks[3]); - let present = sq::function_registered(function) - && sq::aggregate_registered(aggregate) - && sq::aggregate_state_registered(state) - && sq::authorizer(db).is_some(); - (function, aggregate, state, db, present) - }) - .join() - .unwrap(); - assert!( - present, - "the node:sqlite entries must exist while their thread lives" - ); - assert!( - !sq::function_registered(function), - "a dead thread's db.function() callback is still scanned" - ); - assert!( - !sq::aggregate_registered(aggregate), - "a dead thread's db.aggregate() callbacks are still scanned" - ); - assert!( - !sq::aggregate_state_registered(state), - "a dead thread's running aggregate state is still scanned" - ); - assert!( - sq::authorizer(db).is_none(), - "a dead thread's authorizer closure outlived its heap in HANDLES" - ); - sq::free_boxes_for_test(function, aggregate, state); - crate::common::drop_handle(db); -} - #[cfg(feature = "crypto")] #[test] fn thread_exit_releases_the_threads_crypto_key_entries() { diff --git a/crates/perry-stdlib/src/sqlite.rs b/crates/perry-stdlib/src/sqlite.rs index 94371b632c..5c6ab43a28 100644 --- a/crates/perry-stdlib/src/sqlite.rs +++ b/crates/perry-stdlib/src/sqlite.rs @@ -1,24 +1,32 @@ -//! SQLite module (better-sqlite3 compatible) +//! SQLite modules: `node:sqlite`, `bun:sqlite` / `Bun.SQL` and the stdlib +//! copy of `better-sqlite3`. //! -//! Native implementation of the 'better-sqlite3' npm package using rusqlite. -//! Provides synchronous SQLite database operations. - -use crate::common::{for_each_handle_mut_of, Handle}; +//! `node:sqlite` (`DatabaseSync`, `StatementSync`, `StatementSyncIterator`, +//! `SQLTagStore`, `Session`, `db.limits`) is ordinary objects that own a +//! native payload (#11919, `docs/native-payload-pattern.md`): see +//! `database_sync.rs`, `statement_sync.rs`, `sqlite_callbacks.rs`, +//! `tag_store.rs` and `session.rs`. `bun:sqlite` and `Bun.SQL` still use the +//! registry handles in `node_db.rs` / `bun.rs`; they register no JS callbacks. + +use crate::common::Handle; use rusqlite::Connection; -use std::collections::{HashMap, HashSet, VecDeque}; -use std::sync::atomic::{AtomicBool, AtomicU64}; -use std::sync::{Mutex, OnceLock}; +use std::collections::HashSet; +use std::sync::atomic::AtomicBool; +use std::sync::Mutex; mod backup; mod better; mod bind; mod bun; mod connection; +mod database_sync; mod dispatch; mod node_db; -mod node_stmt_session; -mod node_tag_store; mod options; +mod session; +mod sqlite_callbacks; +mod statement_sync; +mod tag_store; // Re-export every moved item (pub and pub(crate)) back into the `sqlite` // module namespace so existing intra-crate paths (`crate::sqlite::Foo`) @@ -27,22 +35,23 @@ pub(crate) use backup::*; pub(crate) use bind::*; pub(crate) use bun::*; pub(crate) use connection::*; +pub(crate) use database_sync::*; pub(crate) use dispatch::*; pub(crate) use node_db::*; -pub(crate) use node_stmt_session::*; -pub(crate) use node_tag_store::*; pub(crate) use options::*; +pub(crate) use session::*; +pub(crate) use sqlite_callbacks::*; +pub(crate) use statement_sync::*; +pub(crate) use tag_store::*; /// SQLite database handle pub struct SqliteDbHandle { pub conn: Mutex, } -/// Node `node:sqlite` DatabaseSync handle. -/// -/// Kept separate from `SqliteDbHandle` so the historical better-sqlite3 -/// close/exec/prepare behavior remains unchanged. -pub struct NodeSqliteDbHandle { +/// `bun:sqlite` `Database` / `Bun.SQL` handle (registry-backed; no JS +/// callbacks can be registered on it). +pub struct BunSqliteDbHandle { pub conn: Mutex>, pub path: String, pub read_only: bool, @@ -58,124 +67,40 @@ pub struct NodeSqliteDbHandle { pub allow_load_extension: bool, pub enable_load_extension: AtomicBool, pub defensive: AtomicBool, - pub authorizer_callback: Mutex>, pub initial_limits: [Option; NODE_SQLITE_LIMIT_COUNT], - pub limits_handle: Mutex>, - pub sessions: Mutex>, pub statements: Mutex>, } -pub struct NodeSqliteLimitsHandle { - pub db_handle: Handle, -} - -pub struct NodeSqliteSessionHandle { - pub db_handle: Handle, - pub session: Mutex>, -} - -pub struct NodeSqliteTagStoreHandle { - pub db_handle: Handle, - pub capacity: usize, - pub cache: Mutex, -} - -pub struct NodeSqliteTagStoreCache { - statements: HashMap, - recency: VecDeque, -} - -impl NodeSqliteTagStoreCache { - pub(crate) fn new() -> Self { - Self { - statements: HashMap::new(), - recency: VecDeque::new(), - } - } - - pub(crate) fn touch(&mut self, sql: &str) { - self.recency.retain(|cached| cached != sql); - self.recency.push_back(sql.to_string()); - } - - pub(crate) fn get(&mut self, sql: &str) -> Option { - let handle = *self.statements.get(sql)?; - self.touch(sql); - Some(handle) - } - - pub(crate) fn remove(&mut self, sql: &str) -> Option { - self.recency.retain(|cached| cached != sql); - self.statements.remove(sql) - } - - pub(crate) fn put(&mut self, sql: String, handle: Handle, capacity: usize) -> Vec { - let mut finalized = Vec::new(); - if capacity == 0 { - finalized.push(handle); - return finalized; - } - - if let Some(previous) = self.statements.insert(sql.clone(), handle) { - finalized.push(previous); - } - self.touch(&sql); - - while self.statements.len() > capacity { - let Some(oldest) = self.recency.pop_front() else { - break; - }; - if let Some(evicted) = self.statements.remove(&oldest) { - finalized.push(evicted); - } - } - finalized - } - - pub(crate) fn clear(&mut self) -> Vec { - self.recency.clear(); - self.statements.drain().map(|(_, handle)| handle).collect() - } - - pub(crate) fn len(&self) -> usize { - self.statements.len() - } -} - -pub struct NodeSqliteStmtHandle { +/// `bun:sqlite` `Statement` handle. +pub struct BunSqliteStmtHandle { pub db_handle: Handle, pub sql: String, pub finalized: AtomicBool, - pub iteration_epoch: AtomicU64, pub read_bigints: AtomicBool, pub return_arrays: AtomicBool, pub allow_bare_named_parameters: AtomicBool, pub allow_unknown_named_parameters: AtomicBool, - pub expanded_sql: Mutex, -} - -pub(crate) struct NodeSqliteStmtOptions { - read_bigints: bool, - return_arrays: bool, - allow_bare_named_parameters: bool, - allow_unknown_named_parameters: bool, } -pub(crate) struct NodeSqliteCustomFunction { - callback: f64, - use_bigint_arguments: bool, -} - -pub(crate) struct NodeSqliteCustomAggregate { - start: f64, - step: f64, - result: Option, - inverse: Option, - use_bigint_arguments: bool, +impl BunSqliteStmtHandle { + pub(crate) fn flags(&self) -> StmtFlags { + use std::sync::atomic::Ordering::Relaxed; + StmtFlags { + read_bigints: self.read_bigints.load(Relaxed), + return_arrays: self.return_arrays.load(Relaxed), + allow_bare_named_parameters: self.allow_bare_named_parameters.load(Relaxed), + allow_unknown_named_parameters: self.allow_unknown_named_parameters.load(Relaxed), + } + } } -pub(crate) struct NodeSqliteAggregateState { - state: f64, +/// A statement's row/bind options, plain data. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct StmtFlags { + pub(crate) read_bigints: bool, + pub(crate) return_arrays: bool, + pub(crate) allow_bare_named_parameters: bool, + pub(crate) allow_unknown_named_parameters: bool, } #[derive(Clone)] @@ -223,353 +148,6 @@ pub(crate) const TAG_NULL_BITS: u64 = 0x7FFC_0000_0000_0002; pub(crate) const JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991; pub(crate) const JS_SAFE_INTEGER_MIN: i64 = -9_007_199_254_740_991; -pub(crate) static NODE_SQLITE_CUSTOM_FUNCTIONS: OnceLock>> = OnceLock::new(); -pub(crate) static NODE_SQLITE_CUSTOM_AGGREGATES: OnceLock>> = OnceLock::new(); -pub(crate) static NODE_SQLITE_ACTIVE_AGGREGATES: OnceLock>> = OnceLock::new(); - -thread_local! { - // The mutable-root scanner registry is thread-local, so this latch must be too. - static NODE_SQLITE_GC_SCANNER: std::cell::Cell = const { std::cell::Cell::new(false) }; -} - -pub(crate) fn node_sqlite_custom_functions() -> &'static Mutex> { - NODE_SQLITE_CUSTOM_FUNCTIONS.get_or_init(|| Mutex::new(HashSet::new())) -} - -pub(crate) fn node_sqlite_custom_aggregates() -> &'static Mutex> { - NODE_SQLITE_CUSTOM_AGGREGATES.get_or_init(|| Mutex::new(HashSet::new())) -} - -pub(crate) fn node_sqlite_active_aggregates() -> &'static Mutex> { - NODE_SQLITE_ACTIVE_AGGREGATES.get_or_init(|| Mutex::new(HashSet::new())) -} - -pub(crate) fn ensure_node_sqlite_gc_scanner_registered() { - ensure_node_sqlite_thread_exit_hooks_registered(); - NODE_SQLITE_GC_SCANNER.with(|registered| { - if registered.get() { - return; - } - perry_runtime::gc::gc_register_mutable_root_scanner_named( - "stdlib:node_sqlite", - scan_node_sqlite_roots_mut, - ); - registered.set(true); - }); -} - -/// #11471: the three node:sqlite sets and `NodeSqliteDbHandle::authorizer_callback` -/// hold JS values of the thread that called `db.function()` / `db.aggregate()` / -/// `db.setAuthorizer()`. Registered before any of them is filled: every insert -/// path runs `ensure_node_sqlite_gc_scanner_registered` first. -fn ensure_node_sqlite_thread_exit_hooks_registered() { - static REGISTER: std::sync::Once = std::sync::Once::new(); - REGISTER.call_once(|| { - perry_runtime::arena::thread_exit::register_thread_exit_range_hook( - release_node_sqlite_callbacks_in_freed_ranges, - ); - crate::common::handle::register_handle_payload_releaser::( - release_node_sqlite_authorizer_in_freed_ranges, - ); - }); -} - -/// Thread-exit hook (#11471). The boxes named by the three sets are owned by -/// SQLite (freed by xDestroy / xFinal, which tolerate an already-unregistered -/// pointer), so this never frees them: it unregisters every box holding a -/// value in the dying thread's arena (so no surviving thread's scanner visits -/// it) and overwrites those values with `undefined`, so a late SQLite call -/// throws "value is not a function" instead of jumping into reused memory. -fn release_node_sqlite_callbacks_in_freed_ranges( - freed: &perry_runtime::arena::thread_exit::FreedRanges, -) { - fn scrub(slot: &mut f64, freed: &perry_runtime::arena::thread_exit::FreedRanges) -> bool { - if freed.holds_value(*slot) { - *slot = f64::from_bits(TAG_UNDEFINED_BITS); - true - } else { - false - } - } - if let Some(functions) = NODE_SQLITE_CUSTOM_FUNCTIONS.get() { - let mut functions = functions.lock().unwrap_or_else(|p| p.into_inner()); - functions.retain(|raw| { - let func = *raw as *mut NodeSqliteCustomFunction; - // SAFETY: a registered box stays allocated until xDestroy, which - // unregisters it under this lock first. - func.is_null() || !scrub(unsafe { &mut (*func).callback }, freed) - }); - } - if let Some(aggregates) = NODE_SQLITE_CUSTOM_AGGREGATES.get() { - let mut aggregates = aggregates.lock().unwrap_or_else(|p| p.into_inner()); - aggregates.retain(|raw| { - let aggregate = *raw as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - return true; - } - // SAFETY: as above (xDestroy unregisters under this lock). - let aggregate = unsafe { &mut *aggregate }; - let mut dead = scrub(&mut aggregate.start, freed); - dead |= scrub(&mut aggregate.step, freed); - if let Some(result) = aggregate.result.as_mut() { - dead |= scrub(result, freed); - } - if let Some(inverse) = aggregate.inverse.as_mut() { - dead |= scrub(inverse, freed); - } - !dead - }); - } - if let Some(states) = NODE_SQLITE_ACTIVE_AGGREGATES.get() { - let mut states = states.lock().unwrap_or_else(|p| p.into_inner()); - states.retain(|raw| { - let state = *raw as *mut NodeSqliteAggregateState; - // SAFETY: xFinal unregisters under this lock before freeing. - state.is_null() || !scrub(unsafe { &mut (*state).state }, freed) - }); - } -} - -/// `HANDLES` payload releaser (#11471): clear a DatabaseSync's authorizer when -/// the closure lives in the dying thread's arena. The authorizer trampoline -/// reads `None` as "allow" (`SQLITE_OK`), so the connection stays usable. The -/// database itself is kept: it holds no other GC value. -fn release_node_sqlite_authorizer_in_freed_ranges( - db: &mut NodeSqliteDbHandle, - freed: &perry_runtime::arena::thread_exit::FreedRanges, -) -> bool { - let mut callback = db - .authorizer_callback - .lock() - .unwrap_or_else(|p| p.into_inner()); - if callback.is_some_and(|value| freed.holds_value(value)) { - *callback = None; - } - false -} - -#[cfg(test)] -pub(crate) mod thread_exit_probe { - //! #11471 test probes over the node:sqlite callback tables. - use super::*; - - /// Register a scalar-function box exactly as `db.function()` does, minus - /// the `sqlite3_create_function_v2` call. Returns the box address. - pub(crate) fn register_function_for_test(callback: f64) -> usize { - let info = Box::into_raw(Box::new(NodeSqliteCustomFunction { - callback, - use_bigint_arguments: false, - })); - register_node_sqlite_custom_function(info); - info as usize - } - - /// Register an aggregate box as `db.aggregate()` does. Returns its address. - pub(crate) fn register_aggregate_for_test(step: f64) -> usize { - let aggregate = Box::into_raw(Box::new(NodeSqliteCustomAggregate { - start: f64::from_bits(TAG_NULL_BITS), - step, - result: None, - inverse: None, - use_bigint_arguments: false, - })); - register_node_sqlite_custom_aggregate(aggregate); - aggregate as usize - } - - /// Register a running aggregate state as xStep does. Returns its address. - pub(crate) fn register_aggregate_state_for_test(state: f64) -> usize { - ensure_node_sqlite_gc_scanner_registered(); - let state = Box::into_raw(Box::new(NodeSqliteAggregateState { state })); - register_node_sqlite_aggregate_state(state); - state as usize - } - - /// A closed DatabaseSync whose authorizer is `callback`, registered in - /// `HANDLES` as `new DatabaseSync()` + `db.setAuthorizer()` leave it. - pub(crate) fn register_db_with_authorizer_for_test(callback: f64) -> Handle { - ensure_node_sqlite_gc_scanner_registered(); - crate::common::register_handle(NodeSqliteDbHandle { - conn: Mutex::new(None), - path: String::new(), - read_only: false, - read_write: true, - create: true, - enable_foreign_keys: true, - enable_dqs: false, - timeout_ms: 0, - read_bigints: false, - return_arrays: false, - allow_bare_named_parameters: true, - allow_unknown_named_parameters: false, - allow_load_extension: false, - enable_load_extension: AtomicBool::new(false), - defensive: AtomicBool::new(true), - authorizer_callback: Mutex::new(Some(callback)), - initial_limits: [None; NODE_SQLITE_LIMIT_COUNT], - limits_handle: Mutex::new(None), - sessions: Mutex::new(HashSet::new()), - statements: Mutex::new(HashSet::new()), - }) - } - - pub(crate) fn function_registered(addr: usize) -> bool { - node_sqlite_custom_functions() - .lock() - .unwrap() - .contains(&addr) - } - - pub(crate) fn aggregate_registered(addr: usize) -> bool { - node_sqlite_custom_aggregates() - .lock() - .unwrap() - .contains(&addr) - } - - pub(crate) fn aggregate_state_registered(addr: usize) -> bool { - node_sqlite_active_aggregates() - .lock() - .unwrap() - .contains(&addr) - } - - pub(crate) fn authorizer(db: Handle) -> Option { - crate::common::with_handle::(db, |db| { - *db.authorizer_callback.lock().unwrap() - }) - .flatten() - } - - /// Free the probe boxes (SQLite's xDestroy / xFinal would in real use). - pub(crate) fn free_boxes_for_test(function: usize, aggregate: usize, state: usize) { - unsafe { - unregister_node_sqlite_custom_function(function as *mut NodeSqliteCustomFunction); - drop(Box::from_raw(function as *mut NodeSqliteCustomFunction)); - unregister_node_sqlite_custom_aggregate(aggregate as *mut NodeSqliteCustomAggregate); - drop(Box::from_raw(aggregate as *mut NodeSqliteCustomAggregate)); - unregister_node_sqlite_aggregate_state(state as *mut NodeSqliteAggregateState); - drop(Box::from_raw(state as *mut NodeSqliteAggregateState)); - } - } -} - -pub(crate) fn scan_node_sqlite_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRootVisitor<'_>) { - { - let functions = node_sqlite_custom_functions() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - for raw in functions.iter() { - let func = *raw as *mut NodeSqliteCustomFunction; - if !func.is_null() { - unsafe { - visitor.visit_nanbox_f64_slot(&mut (*func).callback); - } - } - } - } - { - let aggregates = node_sqlite_custom_aggregates() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - for raw in aggregates.iter() { - let aggregate = *raw as *mut NodeSqliteCustomAggregate; - if !aggregate.is_null() { - unsafe { - visitor.visit_nanbox_f64_slot(&mut (*aggregate).start); - visitor.visit_nanbox_f64_slot(&mut (*aggregate).step); - if let Some(result) = (*aggregate).result.as_mut() { - visitor.visit_nanbox_f64_slot(result); - } - if let Some(inverse) = (*aggregate).inverse.as_mut() { - visitor.visit_nanbox_f64_slot(inverse); - } - } - } - } - } - { - let states = node_sqlite_active_aggregates() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - for raw in states.iter() { - let state = *raw as *mut NodeSqliteAggregateState; - if !state.is_null() { - unsafe { - visitor.visit_nanbox_f64_slot(&mut (*state).state); - } - } - } - } - for_each_handle_mut_of::(|db| { - if let Ok(mut callback) = db.authorizer_callback.lock() { - if let Some(value) = callback.as_mut() { - visitor.visit_nanbox_f64_slot(value); - } - } - }); -} - -pub(crate) fn register_node_sqlite_custom_function(ptr: *mut NodeSqliteCustomFunction) { - ensure_node_sqlite_gc_scanner_registered(); - if !ptr.is_null() { - node_sqlite_custom_functions() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .insert(ptr as usize); - } -} - -pub(crate) fn unregister_node_sqlite_custom_function(ptr: *mut NodeSqliteCustomFunction) -> bool { - if !ptr.is_null() { - return node_sqlite_custom_functions() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .remove(&(ptr as usize)); - } - false -} - -pub(crate) fn register_node_sqlite_custom_aggregate(ptr: *mut NodeSqliteCustomAggregate) { - ensure_node_sqlite_gc_scanner_registered(); - if !ptr.is_null() { - node_sqlite_custom_aggregates() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .insert(ptr as usize); - } -} - -pub(crate) fn unregister_node_sqlite_custom_aggregate(ptr: *mut NodeSqliteCustomAggregate) -> bool { - if !ptr.is_null() { - return node_sqlite_custom_aggregates() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .remove(&(ptr as usize)); - } - false -} - -pub(crate) fn register_node_sqlite_aggregate_state(ptr: *mut NodeSqliteAggregateState) { - ensure_node_sqlite_thread_exit_hooks_registered(); - if !ptr.is_null() { - node_sqlite_active_aggregates() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .insert(ptr as usize); - } -} - -pub(crate) fn unregister_node_sqlite_aggregate_state(ptr: *mut NodeSqliteAggregateState) -> bool { - if !ptr.is_null() { - return node_sqlite_active_aggregates() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .remove(&(ptr as usize)); - } - false -} - /// SQLite statement handle pub struct SqliteStmtHandle { pub sql: String, diff --git a/crates/perry-stdlib/src/sqlite/backup.rs b/crates/perry-stdlib/src/sqlite/backup.rs index e60b1ddaf5..3d29a5b4ef 100644 --- a/crates/perry-stdlib/src/sqlite/backup.rs +++ b/crates/perry-stdlib/src/sqlite/backup.rs @@ -1,10 +1,8 @@ use super::*; -use crate::common::{get_handle, Handle}; use perry_runtime::{ - buffer::is_registered_buffer, - closure::{js_closure_call1, ClosureHeader}, - js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, js_object_set_field_by_name, - js_string_from_bytes, JSValue, ObjectHeader, StringHeader, + buffer::is_registered_buffer, js_get_string_pointer_unified, js_nanbox_pointer, + js_object_alloc, js_object_set_field_by_name, js_string_from_bytes, JSValue, ObjectHeader, + StringHeader, }; use rusqlite::{ffi, Connection, OpenFlags}; use std::ffi::{CStr, CString}; @@ -13,7 +11,7 @@ pub(crate) struct NodeSqliteBackupOptions { source: String, target: String, rate: i32, - progress: Option<*const ClosureHeader>, + pub(crate) progress: Option, } impl Default for NodeSqliteBackupOptions { @@ -28,9 +26,9 @@ impl Default for NodeSqliteBackupOptions { } pub(crate) struct NodeSqliteBackupError { - message: String, - errcode: Option, - errstr: Option, + pub(crate) message: String, + pub(crate) errcode: Option, + pub(crate) errstr: Option, } pub(crate) fn sqlite_errstr(code: i32) -> String { @@ -287,52 +285,38 @@ pub(crate) unsafe fn parse_node_sqlite_backup_options( options.rate = int32_option(options_value, "rate", options.rate); options.source = string_option(options_value, "source", Some("main")).unwrap(); options.target = string_option(options_value, "target", Some("main")).unwrap(); - options.progress = function_option(options_value, "progress").and_then(closure_ptr_from_value); + options.progress = function_option(options_value, "progress"); options } -pub(crate) unsafe fn database_handle_from_backup_source(value: f64) -> Handle { - let js = value_from_f64(value); - if !js.is_pointer() { - throw_type("The \"sourceDb\" argument must be an object."); - } - let handle = raw_addr_from_value(value) as Handle; - if get_handle::(handle).is_none() { - throw_type("The \"sourceDb\" argument must be an instance of DatabaseSync."); - } - handle -} - -pub(crate) unsafe fn call_backup_progress( - progress: *const ClosureHeader, - total_pages: i32, - remaining_pages: i32, -) { - let info = js_object_alloc(0, 2); +/// The `{ totalPages, remainingPages }` argument of a backup progress call. +pub(crate) unsafe fn backup_progress_info(total_pages: i32, remaining_pages: i32) -> f64 { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let info = scope.root_raw_mut_ptr(js_object_alloc(0, 2)); let total_key = js_string_from_bytes(b"totalPages".as_ptr(), "totalPages".len() as u32); - let remaining_key = - js_string_from_bytes(b"remainingPages".as_ptr(), "remainingPages".len() as u32); js_object_set_field_by_name( - info, + info.get_raw_mut_ptr::(), total_key, f64::from_bits(JSValue::number(total_pages as f64).bits()), ); + let remaining_key = + js_string_from_bytes(b"remainingPages".as_ptr(), "remainingPages".len() as u32); js_object_set_field_by_name( - info, + info.get_raw_mut_ptr::(), remaining_key, f64::from_bits(JSValue::number(remaining_pages as f64).bits()), ); - js_closure_call1( - progress, - perry_runtime::closure::plain_call_receiver(), - f64::from_bits(JSValue::object_ptr(info as *mut u8).bits()), - ); + f64::from_bits(JSValue::object_ptr(info.get_raw_mut_ptr::() as *mut u8).bits()) } +/// Copy `source` into the database at `path`. `progress(total, remaining)` +/// runs between steps; returning `false` stops the copy (its exception is +/// pending on the source database). pub(crate) unsafe fn perform_node_sqlite_backup( - source_conn: &Connection, + source: *mut ffi::sqlite3, path: &str, options: &NodeSqliteBackupOptions, + mut progress: impl FnMut(i32, i32) -> bool, ) -> Result { let destination = Connection::open_with_flags( resolve_sqlite_path(path), @@ -357,7 +341,7 @@ pub(crate) unsafe fn perform_node_sqlite_backup( let backup = ffi::sqlite3_backup_init( destination.handle(), target_name.as_ptr(), - source_conn.handle(), + source, source_name.as_ptr(), ); if backup.is_null() { @@ -373,10 +357,11 @@ pub(crate) unsafe fn perform_node_sqlite_backup( total_pages = ffi::sqlite3_backup_pagecount(backup); let remaining_pages = ffi::sqlite3_backup_remaining(backup); - if remaining_pages != 0 { - if let Some(progress) = options.progress { - call_backup_progress(progress, total_pages, remaining_pages); - } + if remaining_pages != 0 + && options.progress.is_some() + && !progress(total_pages, remaining_pages) + { + break; } if rc == ffi::SQLITE_DONE { diff --git a/crates/perry-stdlib/src/sqlite/bind.rs b/crates/perry-stdlib/src/sqlite/bind.rs index 4d7b65578d..aba35a9811 100644 --- a/crates/perry-stdlib/src/sqlite/bind.rs +++ b/crates/perry-stdlib/src/sqlite/bind.rs @@ -1,8 +1,6 @@ use super::*; -use crate::common::{get_handle, Handle}; use perry_runtime::{ buffer::{is_any_array_buffer, is_data_view, is_registered_buffer}, - closure::js_closure_call_array, js_array_alloc, js_array_get, js_array_length, js_array_push, js_get_string_pointer_unified, js_object_alloc_null_proto, js_object_get_field_by_name, js_object_set_field, js_object_set_keys, js_string_from_bytes, ArrayHeader, BigIntHeader, JSValue, ObjectHeader, @@ -12,7 +10,6 @@ use rusqlite::{ffi, types::Value as SqliteValue, Connection}; use std::collections::HashMap; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_int, c_void}; -use std::sync::atomic::Ordering; /// Convert SQLite value to JSValue pub(crate) unsafe fn sqlite_value_to_jsvalue(value: &SqliteValue) -> JSValue { @@ -81,11 +78,22 @@ pub(crate) unsafe fn sqlite_c_string_value(ptr: *const c_char) -> JSValue { } pub(crate) unsafe fn sqlite_error_message(conn: &Connection) -> String { - CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) + sqlite_errmsg_raw(conn.handle()) +} + +pub(crate) unsafe fn sqlite_errmsg_raw(db: *mut ffi::sqlite3) -> String { + CStr::from_ptr(ffi::sqlite3_errmsg(db)) .to_string_lossy() .into_owned() } +/// Throw `ERR_SQLITE_ERROR` for a raw connection's current error state. +pub(crate) unsafe fn throw_sqlite_error_from_db(db: *mut ffi::sqlite3) -> ! { + let errcode = ffi::sqlite3_extended_errcode(db); + let message = sqlite_errmsg_raw(db); + throw_sqlite_error_ext(&message, errcode) +} + pub(crate) unsafe fn prepare_node_raw_statement(conn: &Connection, sql: &str) -> RawNodeStatement { let c_sql = CString::new(sql) .unwrap_or_else(|_| throw_sqlite_error("SQL string must not contain null bytes")); @@ -103,20 +111,15 @@ pub(crate) unsafe fn prepare_node_raw_statement(conn: &Connection, sql: &str) -> RawNodeStatement { ptr: raw } } -pub(crate) unsafe fn update_node_expanded_sql( - stmt: &NodeSqliteStmtHandle, - raw_stmt: *mut ffi::sqlite3_stmt, -) { +/// `sqlite3_expanded_sql` as an owned string (empty when unavailable). +pub(crate) unsafe fn expanded_sql_of(raw_stmt: *mut ffi::sqlite3_stmt) -> String { let expanded = ffi::sqlite3_expanded_sql(raw_stmt); - let text = if expanded.is_null() { + if expanded.is_null() { String::new() } else { let text = CStr::from_ptr(expanded).to_string_lossy().into_owned(); ffi::sqlite3_free(expanded.cast::()); text - }; - if let Ok(mut cached) = stmt.expanded_sql.lock() { - *cached = text; } } @@ -134,26 +137,51 @@ pub(crate) fn bigint_to_i64(ptr: *const BigIntHeader) -> Option { } } -pub(crate) unsafe fn node_sqlite_bind_error(conn: &Connection, rc: c_int) { - if rc != ffi::SQLITE_OK { - throw_sqlite_error_from_conn(conn); +/// Why a parameter could not be bound, as plain data: the caller finalizes +/// its statement before throwing ([`BindError::throw`]). +pub(crate) enum BindError { + Type(String), + ArgValue(String), + InvalidState(String), + Sqlite { message: String, code: i32 }, +} + +impl BindError { + pub(crate) unsafe fn throw(self) -> ! { + match self { + BindError::Type(message) => throw_type(&message), + BindError::ArgValue(message) => throw_arg_value(&message), + BindError::InvalidState(message) => throw_invalid_state(&message), + BindError::Sqlite { message, code } => throw_sqlite_error_ext(&message, code), + } + } +} + +unsafe fn bind_rc(db: *mut ffi::sqlite3, rc: c_int) -> Result<(), BindError> { + if rc == ffi::SQLITE_OK { + Ok(()) + } else { + Err(BindError::Sqlite { + message: sqlite_errmsg_raw(db), + code: ffi::sqlite3_extended_errcode(db), + }) } } pub(crate) unsafe fn bind_node_sqlite_value( - conn: &Connection, + db: *mut ffi::sqlite3, raw_stmt: *mut ffi::sqlite3_stmt, index: c_int, value: f64, -) { +) -> Result<(), BindError> { let js = value_from_f64(value); let rc = if js.is_null() { ffi::sqlite3_bind_null(raw_stmt, index) } else if js.is_undefined() || js.is_bool() { - throw_type(&format!( + return Err(BindError::Type(format!( "Provided value cannot be bound to SQLite parameter {}.", index - )); + ))); } else if js.is_any_string() { let ptr = js_get_string_pointer_unified(value) as *const StringHeader; if ptr.is_null() { @@ -173,7 +201,9 @@ pub(crate) unsafe fn bind_node_sqlite_value( ffi::sqlite3_bind_double(raw_stmt, index, js.as_int32() as f64) } else if js.is_bigint() { let Some(value) = bigint_to_i64(js.as_bigint_ptr()) else { - throw_arg_value("BigInt value is too large to bind."); + return Err(BindError::ArgValue( + "BigInt value is too large to bind.".to_string(), + )); }; ffi::sqlite3_bind_int64(raw_stmt, index, value) } else if js.is_number() { @@ -198,13 +228,13 @@ pub(crate) unsafe fn bind_node_sqlite_value( if let Some(status) = status { status } else { - throw_type(&format!( + return Err(BindError::Type(format!( "Provided value cannot be bound to SQLite parameter {}.", index - )); + ))); } }; - node_sqlite_bind_error(conn, rc); + bind_rc(db, rc) } pub(crate) unsafe fn node_args_from_array(args_arr: *const ArrayHeader) -> Vec { @@ -251,11 +281,11 @@ pub(crate) fn has_sqlite_parameter_prefix(name: &str) -> bool { } pub(crate) unsafe fn bind_node_sqlite_params( - stmt: &NodeSqliteStmtHandle, - conn: &Connection, + flags: StmtFlags, + db: *mut ffi::sqlite3, raw_stmt: *mut ffi::sqlite3_stmt, args_arr: *const ArrayHeader, -) { +) -> Result<(), BindError> { let args = node_args_from_array(args_arr); let mut positional_start = 0usize; let mut named_params: Option = None; @@ -285,8 +315,8 @@ pub(crate) unsafe fn bind_node_sqlite_params( } if let Some(named_value) = named_params { - let allow_bare = stmt.allow_bare_named_parameters.load(Ordering::Relaxed); - let allow_unknown = stmt.allow_unknown_named_parameters.load(Ordering::Relaxed); + let allow_bare = flags.allow_bare_named_parameters; + let allow_unknown = flags.allow_unknown_named_parameters; if !closure_ptr_from_value(named_value).is_some() { let keys = perry_runtime::object::js_object_keys_value(named_value); let key_count = js_array_length(keys); @@ -299,10 +329,10 @@ pub(crate) unsafe fn bind_node_sqlite_params( if allow_bare { if let Some(fulls) = bare_names.get(&bare) { if fulls.len() > 1 { - throw_invalid_state(&format!( + return Err(BindError::InvalidState(format!( "Cannot create bare named parameter '{}' because of conflicting names '{}' and '{}'.", bare, fulls[0], fulls[1] - )); + ))); } } } @@ -320,11 +350,14 @@ pub(crate) unsafe fn bind_node_sqlite_params( if allow_unknown { continue; } - throw_invalid_state(&format!("Unknown named parameter '{}'", key)); + return Err(BindError::InvalidState(format!( + "Unknown named parameter '{}'", + key + ))); }; let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); let value = js_object_get_field_by_name(obj, key_ptr); - bind_node_sqlite_value(conn, raw_stmt, index, f64_from_jsvalue(value)); + bind_node_sqlite_value(db, raw_stmt, index, f64_from_jsvalue(value))?; } } } @@ -344,24 +377,29 @@ pub(crate) unsafe fn bind_node_sqlite_params( // Node raises ERR_SQLITE_ERROR with errcode 25 (SQLITE_RANGE) when // more anonymous values are supplied than the statement has // anonymous parameters (#6561). - throw_sqlite_error_ext("column index out of range", ffi::SQLITE_RANGE); + return Err(BindError::Sqlite { + message: "column index out of range".to_string(), + code: ffi::SQLITE_RANGE, + }); } for (offset, index) in positional_indices.into_iter().enumerate() { if let Some(value) = args.get(positional_start + offset).copied() { - bind_node_sqlite_value(conn, raw_stmt, index, value); + bind_node_sqlite_value(db, raw_stmt, index, value)?; } } + Ok(()) } pub(crate) unsafe fn bind_node_sqlite_positional_params( - conn: &Connection, + db: *mut ffi::sqlite3, raw_stmt: *mut ffi::sqlite3_stmt, values: &[f64], -) { +) -> Result<(), BindError> { let param_count = ffi::sqlite3_bind_parameter_count(raw_stmt).max(0) as usize; for (offset, value) in values.iter().take(param_count).enumerate() { - bind_node_sqlite_value(conn, raw_stmt, (offset + 1) as c_int, *value); + bind_node_sqlite_value(db, raw_stmt, (offset + 1) as c_int, *value)?; } + Ok(()) } pub(crate) unsafe fn node_sqlite_integer_value(value: i64, read_bigints: bool) -> JSValue { @@ -479,82 +517,6 @@ pub(crate) unsafe fn node_sqlite_closure_arity(callback: f64) -> c_int { perry_runtime::closure::closure_arity(closure).unwrap_or(0) as c_int } -pub(crate) unsafe fn node_sqlite_call_closure(callback: f64, args: &[f64]) -> f64 { - let Some(closure) = closure_ptr_from_value(callback) else { - throw_plain_type("value is not a function"); - }; - js_closure_call_array( - closure as i64, - perry_runtime::closure::plain_call_receiver(), - if args.is_empty() { - std::ptr::null() - } else { - args.as_ptr() - }, - args.len() as i64, - ) -} - -pub(crate) unsafe fn node_sqlite_value_arg( - value: *mut ffi::sqlite3_value, - use_bigints: bool, -) -> JSValue { - if value.is_null() { - return JSValue::null(); - } - match ffi::sqlite3_value_type(value) { - ffi::SQLITE_NULL => JSValue::null(), - ffi::SQLITE_INTEGER => { - node_sqlite_integer_value(ffi::sqlite3_value_int64(value), use_bigints) - } - ffi::SQLITE_FLOAT => JSValue::number(ffi::sqlite3_value_double(value)), - ffi::SQLITE_TEXT => { - let ptr = ffi::sqlite3_value_text(value); - if ptr.is_null() { - return JSValue::null(); - } - let len = ffi::sqlite3_value_bytes(value) as usize; - let bytes = std::slice::from_raw_parts(ptr, len); - JSValue::string_ptr(js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32)) - } - ffi::SQLITE_BLOB => { - let len = ffi::sqlite3_value_bytes(value) as usize; - let ptr = ffi::sqlite3_value_blob(value); - let input = if len > 0 && !ptr.is_null() { - std::slice::from_raw_parts(ptr as *const u8, len) - } else { - &[] - }; - JSValue::from_bits( - perry_runtime::buffer::bytes::from_slice( - perry_runtime::buffer::bytes::Brand::Uint8Array, - input, - ) - .to_bits(), - ) - } - _ => JSValue::null(), - } -} - -pub(crate) unsafe fn node_sqlite_callback_args( - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, - use_bigints: bool, -) -> Vec { - let argc = argc.max(0) as usize; - let mut args = Vec::with_capacity(argc); - for index in 0..argc { - let value = if argv.is_null() { - std::ptr::null_mut() - } else { - *argv.add(index) - }; - args.push(f64_from_jsvalue(node_sqlite_value_arg(value, use_bigints))); - } - args -} - pub(crate) unsafe fn node_sqlite_blob_like_bytes(value: f64) -> Option> { let raw = raw_addr_from_value(value); if raw < 0x1000 { @@ -614,170 +576,6 @@ pub(crate) unsafe fn node_sqlite_result_value(ctx: *mut ffi::sqlite3_context, va } } -pub(crate) unsafe extern "C" fn node_sqlite_scalar_callback( - ctx: *mut ffi::sqlite3_context, - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, -) { - let info = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomFunction; - if info.is_null() { - sqlite_result_error(ctx, "SQLite function is not available"); - return; - } - let args = node_sqlite_callback_args(argc, argv, (*info).use_bigint_arguments); - let result = node_sqlite_call_closure((*info).callback, &args); - node_sqlite_result_value(ctx, result); -} - -pub(crate) unsafe extern "C" fn node_sqlite_scalar_destroy(data: *mut c_void) { - let info = data as *mut NodeSqliteCustomFunction; - unregister_node_sqlite_custom_function(info); - if !info.is_null() { - drop(Box::from_raw(info)); - } -} - -pub(crate) unsafe fn node_sqlite_aggregate_start(aggregate: &NodeSqliteCustomAggregate) -> f64 { - if closure_ptr_from_value(aggregate.start).is_some() { - node_sqlite_call_closure(aggregate.start, &[]) - } else { - aggregate.start - } -} - -pub(crate) unsafe fn node_sqlite_aggregate_state( - ctx: *mut ffi::sqlite3_context, - aggregate: &NodeSqliteCustomAggregate, - create: bool, -) -> Option<*mut NodeSqliteAggregateState> { - let slot = ffi::sqlite3_aggregate_context( - ctx, - if create { - std::mem::size_of::<*mut NodeSqliteAggregateState>() as c_int - } else { - 0 - }, - ) as *mut *mut NodeSqliteAggregateState; - if slot.is_null() { - if create { - ffi::sqlite3_result_error_nomem(ctx); - } - return None; - } - if (*slot).is_null() && create { - let initial = node_sqlite_aggregate_start(aggregate); - perry_runtime::gc::js_write_barrier_root_nanbox(initial.to_bits()); - let state = Box::into_raw(Box::new(NodeSqliteAggregateState { state: initial })); - register_node_sqlite_aggregate_state(state); - *slot = state; - } - if (*slot).is_null() { - None - } else { - Some(*slot) - } -} - -pub(crate) unsafe fn node_sqlite_aggregate_apply( - ctx: *mut ffi::sqlite3_context, - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, - callback: f64, -) { - let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - sqlite_result_error(ctx, "SQLite aggregate is not available"); - return; - } - let Some(state) = node_sqlite_aggregate_state(ctx, &*aggregate, true) else { - return; - }; - let mut args = Vec::with_capacity(argc.max(0) as usize + 1); - args.push((*state).state); - args.extend(node_sqlite_callback_args( - argc, - argv, - (*aggregate).use_bigint_arguments, - )); - let next = node_sqlite_call_closure(callback, &args); - perry_runtime::gc::js_write_barrier_root_nanbox(next.to_bits()); - (*state).state = next; -} - -pub(crate) unsafe extern "C" fn node_sqlite_aggregate_step( - ctx: *mut ffi::sqlite3_context, - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, -) { - let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - sqlite_result_error(ctx, "SQLite aggregate is not available"); - return; - } - node_sqlite_aggregate_apply(ctx, argc, argv, (*aggregate).step); -} - -pub(crate) unsafe extern "C" fn node_sqlite_aggregate_inverse( - ctx: *mut ffi::sqlite3_context, - argc: c_int, - argv: *mut *mut ffi::sqlite3_value, -) { - let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - sqlite_result_error(ctx, "SQLite aggregate is not available"); - return; - } - let Some(inverse) = (*aggregate).inverse else { - sqlite_result_error(ctx, "SQLite aggregate inverse is not available"); - return; - }; - node_sqlite_aggregate_apply(ctx, argc, argv, inverse); -} - -pub(crate) unsafe fn node_sqlite_aggregate_emit(ctx: *mut ffi::sqlite3_context, finalize: bool) { - let aggregate = ffi::sqlite3_user_data(ctx) as *mut NodeSqliteCustomAggregate; - if aggregate.is_null() { - sqlite_result_error(ctx, "SQLite aggregate is not available"); - return; - } - let Some(state) = node_sqlite_aggregate_state(ctx, &*aggregate, true) else { - return; - }; - let value = if finalize && (*aggregate).inverse.is_some() { - (*state).state - } else if let Some(result) = (*aggregate).result { - node_sqlite_call_closure(result, &[(*state).state]) - } else { - (*state).state - }; - node_sqlite_result_value(ctx, value); - if finalize { - let slot = ffi::sqlite3_aggregate_context(ctx, 0) as *mut *mut NodeSqliteAggregateState; - if !slot.is_null() && !(*slot).is_null() { - let state_ptr = *slot; - unregister_node_sqlite_aggregate_state(state_ptr); - drop(Box::from_raw(state_ptr)); - *slot = std::ptr::null_mut(); - } - } -} - -pub(crate) unsafe extern "C" fn node_sqlite_aggregate_final(ctx: *mut ffi::sqlite3_context) { - node_sqlite_aggregate_emit(ctx, true); -} - -pub(crate) unsafe extern "C" fn node_sqlite_aggregate_value(ctx: *mut ffi::sqlite3_context) { - node_sqlite_aggregate_emit(ctx, false); -} - -pub(crate) unsafe extern "C" fn node_sqlite_aggregate_destroy(data: *mut c_void) { - let aggregate = data as *mut NodeSqliteCustomAggregate; - unregister_node_sqlite_custom_aggregate(aggregate); - if !aggregate.is_null() { - drop(Box::from_raw(aggregate)); - } -} - pub(crate) unsafe fn set_object_keys_from_names(obj: *mut ObjectHeader, names: &[String]) { let mut keys = js_array_alloc(names.len() as u32); for name in names { @@ -800,19 +598,19 @@ pub(crate) unsafe fn make_null_proto_object( } pub(crate) unsafe fn node_sqlite_row_value( - stmt: &NodeSqliteStmtHandle, + flags: StmtFlags, raw_stmt: *mut ffi::sqlite3_stmt, ) -> JSValue { - node_sqlite_row_value_with_mode(stmt, raw_stmt, stmt.return_arrays.load(Ordering::Relaxed)) + node_sqlite_row_value_with_mode(flags, raw_stmt, flags.return_arrays) } pub(crate) unsafe fn node_sqlite_row_value_with_mode( - stmt: &NodeSqliteStmtHandle, + flags: StmtFlags, raw_stmt: *mut ffi::sqlite3_stmt, return_arrays: bool, ) -> JSValue { let column_count = ffi::sqlite3_column_count(raw_stmt); - let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); + let read_bigints = flags.read_bigints; if return_arrays { let mut arr = js_array_alloc(column_count as u32); for index in 0..column_count { @@ -836,81 +634,6 @@ pub(crate) unsafe fn node_sqlite_row_value_with_mode( JSValue::object_ptr(make_null_proto_object(&names, &values) as *mut u8) } -pub(crate) unsafe fn with_node_sqlite_statement( - stmt_handle: Handle, - params_arr: *const ArrayHeader, - action: F, -) -> R -where - F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, -{ - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - if stmt.finalized.load(Ordering::Relaxed) { - throw_invalid_state("statement has been finalized"); - } - stmt.iteration_epoch.fetch_add(1, Ordering::Relaxed); - let db = get_handle::(stmt.db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_ptr = { - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if let Some(conn) = conn_guard.as_ref() { - conn as *const Connection - } else { - drop(conn_guard); - throw_invalid_state("database is not open"); - } - }; - let conn = &*conn_ptr; - let raw = prepare_node_raw_statement(conn, &stmt.sql); - let raw_ptr = raw.ptr; - bind_node_sqlite_params(stmt, conn, raw_ptr, params_arr); - update_node_expanded_sql(stmt, raw_ptr); - let result = action(conn, stmt, raw_ptr); - drop(raw); - result -} - -pub(crate) unsafe fn with_node_sqlite_statement_positional( - stmt_handle: Handle, - values: &[f64], - action: F, -) -> R -where - F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, -{ - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - if stmt.finalized.load(Ordering::Relaxed) { - throw_invalid_state("statement has been finalized"); - } - let db = get_handle::(stmt.db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_ptr = { - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if let Some(conn) = conn_guard.as_ref() { - conn as *const Connection - } else { - drop(conn_guard); - throw_invalid_state("database is not open"); - } - }; - let conn = &*conn_ptr; - let raw = prepare_node_raw_statement(conn, &stmt.sql); - let raw_ptr = raw.ptr; - bind_node_sqlite_positional_params(conn, raw_ptr, values); - update_node_expanded_sql(stmt, raw_ptr); - let result = action(conn, stmt, raw_ptr); - drop(raw); - result -} - /// Build packed keys (null-separated) and a shape_id from column names. pub(crate) fn build_packed_keys(column_names: &[String]) -> (Vec, u32) { let mut packed = Vec::new(); diff --git a/crates/perry-stdlib/src/sqlite/bun.rs b/crates/perry-stdlib/src/sqlite/bun.rs index de168f4f10..9a474889b2 100644 --- a/crates/perry-stdlib/src/sqlite/bun.rs +++ b/crates/perry-stdlib/src/sqlite/bun.rs @@ -57,7 +57,7 @@ pub unsafe extern "C" fn js_bun_sqlite_database_new(path_value: f64, options_val options.enable_foreign_keys = false; options.allow_extension = true; options.defensive = false; - register_node_sqlite_database(path, options, "bun:sqlite") + register_bun_sqlite_database(path, options, "bun:sqlite") } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -127,12 +127,12 @@ pub(crate) unsafe fn open_bun_sqlite_database( options.enable_foreign_keys = false; options.allow_extension = true; options.defensive = false; - register_node_sqlite_database(path, options, "bun") + register_bun_sqlite_database(path, options, "bun") } #[no_mangle] pub unsafe extern "C" fn js_bun_sqlite_database_query(db_handle: Handle, sql_value: f64) -> Handle { - js_node_sqlite_database_sync_prepare(db_handle, sql_value, undefined_f64()) + bun_sqlite_database_prepare(db_handle, sql_value) } #[no_mangle] @@ -142,14 +142,14 @@ pub unsafe extern "C" fn js_bun_sqlite_database_run( params: *const ArrayHeader, ) -> *mut ObjectHeader { let statement = js_bun_sqlite_database_query(db_handle, sql_value); - let result = js_node_sqlite_statement_sync_run(statement, params); - finalize_node_sqlite_statement_handle(statement); + let result = js_bun_sqlite_statement_run(statement, params); + finalize_bun_sqlite_statement_handle(statement); result } #[no_mangle] pub unsafe extern "C" fn js_bun_sqlite_database_filename(db_handle: Handle) -> *mut StringHeader { - let db = get_handle::(db_handle) + let db = get_handle::(db_handle) .unwrap_or_else(|| throw_invalid_state("database is not open")); js_string_from_bytes(db.path.as_ptr(), db.path.len() as u32) } @@ -159,7 +159,7 @@ pub unsafe extern "C" fn js_bun_sqlite_statement_values( stmt_handle: Handle, params: *const ArrayHeader, ) -> *mut ArrayHeader { - with_node_sqlite_statement(stmt_handle, params, |conn, stmt, raw_stmt| { + with_bun_sqlite_statement(stmt_handle, params, |conn, flags, raw_stmt| { let scope = perry_runtime::gc::RuntimeHandleScope::new(); let rows = js_array_alloc(0); let rows_handle = scope.root_raw_mut_ptr(rows); @@ -167,7 +167,7 @@ pub unsafe extern "C" fn js_bun_sqlite_statement_values( loop { match ffi::sqlite3_step(raw_stmt) { ffi::SQLITE_ROW => { - let row = node_sqlite_row_value_with_mode(stmt, raw_stmt, true); + let row = node_sqlite_row_value_with_mode(flags, raw_stmt, true); row_handle.set_nanbox_u64(row.bits()); let rows = js_array_push( rows_handle.get_raw_mut_ptr(), @@ -188,7 +188,7 @@ pub unsafe extern "C" fn js_bun_sqlite_statement_safe_integers( stmt_handle: Handle, enabled_value: f64, ) -> f64 { - let stmt = get_handle::(stmt_handle) + let stmt = get_handle::(stmt_handle) .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); if stmt.finalized.load(Ordering::Relaxed) { throw_invalid_state("statement has been finalized"); @@ -204,7 +204,7 @@ pub unsafe extern "C" fn js_bun_sqlite_statement_safe_integers( #[no_mangle] pub unsafe extern "C" fn js_bun_sqlite_statement_finalize(stmt_handle: Handle) { - finalize_node_sqlite_statement_handle(stmt_handle); + finalize_bun_sqlite_statement_handle(stmt_handle); } unsafe extern "C" fn bun_sqlite_transaction_wrapper( @@ -219,13 +219,13 @@ unsafe extern "C" fn bun_sqlite_transaction_wrapper( let args: Vec = (0..arg_count) .map(|index| f64_from_jsvalue(js_array_get(rest, index))) .collect(); - let nested = with_open_node_connection(db_handle, |conn| !conn.is_autocommit()); + let nested = with_open_bun_connection(db_handle, |conn| !conn.is_autocommit()); let begin = if nested { "SAVEPOINT `bun:sqlite transaction`" } else { "BEGIN" }; - with_open_node_connection(db_handle, |conn| { + with_open_bun_connection(db_handle, |conn| { if let Err((message, code)) = node_sqlite_exec_batch(conn, begin) { throw_sqlite_error_ext(&message, code); } @@ -249,7 +249,7 @@ unsafe extern "C" fn bun_sqlite_transaction_wrapper( } else { "COMMIT" }; - with_open_node_connection(db_handle, |conn| { + with_open_bun_connection(db_handle, |conn| { if let Err((message, code)) = node_sqlite_exec_batch(conn, finish) { throw_sqlite_error_ext(&message, code); } @@ -262,7 +262,7 @@ unsafe extern "C" fn bun_sqlite_transaction_wrapper( } else { "ROLLBACK" }; - with_open_node_connection(db_handle, |conn| { + with_open_bun_connection(db_handle, |conn| { let _ = node_sqlite_exec_batch(conn, rollback); }); perry_runtime::exception::js_throw(error) diff --git a/crates/perry-stdlib/src/sqlite/connection.rs b/crates/perry-stdlib/src/sqlite/connection.rs index 7ad83b8eca..0cddbe6a0e 100644 --- a/crates/perry-stdlib/src/sqlite/connection.rs +++ b/crates/perry-stdlib/src/sqlite/connection.rs @@ -1,11 +1,10 @@ use super::*; use crate::common::{get_handle, Handle}; -use rusqlite::{ffi, limits::Limit, Connection, OpenFlags}; -use std::ffi::CStr; +use rusqlite::{limits::Limit, Connection, OpenFlags}; use std::sync::atomic::Ordering; use std::time::Duration; -pub(crate) fn open_node_sqlite_connection(db: &NodeSqliteDbHandle) -> rusqlite::Result { +pub(crate) fn open_node_sqlite_connection(db: &BunSqliteDbHandle) -> rusqlite::Result { let mut flags = OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX; if db.read_only { flags |= OpenFlags::SQLITE_OPEN_READ_ONLY; @@ -58,25 +57,6 @@ pub(crate) fn open_node_sqlite_connection(db: &NodeSqliteDbHandle) -> rusqlite:: Ok(conn) } -pub(crate) unsafe fn configure_node_sqlite_load_extension( - conn: &Connection, - enable: bool, -) -> Result<(), String> { - let mut current = 0; - let rc = ffi::sqlite3_db_config( - conn.handle(), - ffi::SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, - if enable { 1 } else { 0 }, - &mut current, - ); - if rc == ffi::SQLITE_OK { - return Ok(()); - } - Err(CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned()) -} - pub(crate) unsafe fn with_sqlite_connection(db_handle: Handle, f: F) -> Option where F: FnOnce(&Connection) -> R, @@ -86,7 +66,7 @@ where return Some(f(&conn)); } } - if let Some(db) = get_handle::(db_handle) { + if let Some(db) = get_handle::(db_handle) { if let Ok(conn) = db.conn.lock() { if let Some(conn) = conn.as_ref() { return Some(f(conn)); @@ -96,11 +76,11 @@ where None } -pub(crate) unsafe fn with_open_node_connection(db_handle: Handle, f: F) -> R +pub(crate) unsafe fn with_open_bun_connection(db_handle: Handle, f: F) -> R where F: FnOnce(&Connection) -> R, { - let db = get_handle::(db_handle) + let db = get_handle::(db_handle) .unwrap_or_else(|| throw_invalid_state("database is not open")); let conn_ptr = { let conn = db @@ -117,8 +97,8 @@ where f(&*conn_ptr) } -pub(crate) unsafe fn ensure_open_node_database(db_handle: Handle) { - let db = get_handle::(db_handle) +pub(crate) unsafe fn ensure_open_bun_database(db_handle: Handle) { + let db = get_handle::(db_handle) .unwrap_or_else(|| throw_invalid_state("database is not open")); let conn = db .conn @@ -130,26 +110,7 @@ pub(crate) unsafe fn ensure_open_node_database(db_handle: Handle) { } } -pub(crate) unsafe fn delete_node_sqlite_sessions(db: &NodeSqliteDbHandle) { - let handles: Vec = db - .sessions - .lock() - .map(|mut sessions| sessions.drain().collect()) - .unwrap_or_default(); - - for handle in handles { - let Some(session_handle) = get_handle::(handle) else { - continue; - }; - if let Ok(mut session) = session_handle.session.lock() { - if let Some(raw) = session.take() { - ffi::sqlite3session_delete(raw as *mut ffi::sqlite3_session); - } - } - } -} - -pub(crate) unsafe fn finalize_node_sqlite_statements(db: &NodeSqliteDbHandle) { +pub(crate) unsafe fn finalize_bun_sqlite_statements(db: &BunSqliteDbHandle) { let handles: Vec = db .statements .lock() @@ -157,20 +118,18 @@ pub(crate) unsafe fn finalize_node_sqlite_statements(db: &NodeSqliteDbHandle) { .unwrap_or_default(); for handle in handles { - if let Some(stmt) = get_handle::(handle) { + if let Some(stmt) = get_handle::(handle) { stmt.finalized.store(true, Ordering::Relaxed); - stmt.iteration_epoch.fetch_add(1, Ordering::Relaxed); } } } -pub(crate) unsafe fn finalize_node_sqlite_statement_handle(stmt_handle: Handle) { - let Some(stmt) = get_handle::(stmt_handle) else { +pub(crate) unsafe fn finalize_bun_sqlite_statement_handle(stmt_handle: Handle) { + let Some(stmt) = get_handle::(stmt_handle) else { return; }; stmt.finalized.store(true, Ordering::Relaxed); - stmt.iteration_epoch.fetch_add(1, Ordering::Relaxed); - if let Some(db) = get_handle::(stmt.db_handle) { + if let Some(db) = get_handle::(stmt.db_handle) { if let Ok(mut statements) = db.statements.lock() { statements.remove(&stmt_handle); } diff --git a/crates/perry-stdlib/src/sqlite/database_sync.rs b/crates/perry-stdlib/src/sqlite/database_sync.rs new file mode 100644 index 0000000000..fccd2c1438 --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/database_sync.rs @@ -0,0 +1,1444 @@ +//! `node:sqlite` `DatabaseSync` and its `db.limits` object: ordinary objects +//! that own a native payload (#11919, `docs/native-payload-pattern.md`). +//! +//! `DatabaseSync` is a class-S family: SQLite calls the authorizer, scalar +//! functions and aggregates back synchronously. Each registration stores its +//! JS function in the payload's `callbacks` array (`set_callback`) and hands +//! SQLite a [`CallbackSite`](perry_runtime::native_payload::CallbackSite) as +//! userdata; the trampolines in `sqlite_callbacks.rs` find the owner through +//! the cell's traced owner edge. Every C call that can call back runs between +//! `native_payload::enter` and `finish`, with no payload borrow held, and a +//! JS throw inside SQLite is parked and rethrown after the C call returns. +//! +//! The payload exists only while the database is open: `close()` releases it +//! (deferred while a callback is running) and `open()` attaches a new one to +//! the same object. The configuration a reopen needs lives in the JS state. + +use super::*; +use perry_runtime::closure::{ClosureHeader, JsThis}; +use perry_runtime::gc::{RuntimeHandle, RuntimeHandleScope}; +use perry_runtime::native_class_ids::{SQLITE_DATABASE_SYNC, SQLITE_LIMITS}; +use perry_runtime::native_payload::{ + self, AttachMiss, CallEnd, CallbackSites, Lifecycle, NativePayloadFamily, OpenSerial, + PayloadMiss, PayloadPrototype, +}; +use perry_runtime::{ + buffer::BufferHeader, js_array_alloc, js_array_get, js_array_length, js_array_push_f64, + js_nanbox_pointer, js_promise_rejected, js_promise_resolved, js_string_from_bytes, ArrayHeader, + JSValue, Promise, +}; +use rusqlite::ffi; +use std::cell::Cell; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use std::rc::Rc; + +perry_runtime::state_key_memo!(static MEMO_DB); +perry_runtime::state_key_memo!(static MEMO_LIMITS); + +macro_rules! builtin { + ($body:path, $n:tt) => { + perry_runtime::fn_info!($body, $n; with_declared($n), with_flags(perry_runtime::closure::FN_BUILTIN)) + }; +} + +pub(crate) static DB_FAMILY: NativePayloadFamily = NativePayloadFamily { + class_id: SQLITE_DATABASE_SYNC, + links_owner: true, + name: "DatabaseSync", + constructor_export: Some(("sqlite", "DatabaseSync")), + constructor_length: 1, + install_prototype: install_db_prototype, +}; + +pub(crate) static LIMITS_FAMILY: NativePayloadFamily = NativePayloadFamily { + class_id: SQLITE_LIMITS, + links_owner: false, + name: "", + constructor_export: None, + constructor_length: 0, + install_prototype: install_limits_prototype, +}; + +/// What `open()` needs to (re)open the connection. Plain data; persisted in +/// the JS state while the database is closed. +#[derive(Clone)] +pub(crate) struct DbConfig { + pub(crate) path: String, + pub(crate) opts: NodeSqliteOptions, + pub(crate) enable_load_extension: bool, +} + +impl DbConfig { + pub(crate) fn stmt_flags(&self) -> StmtFlags { + StmtFlags { + read_bigints: self.opts.read_bigints, + return_arrays: self.opts.return_arrays, + allow_bare_named_parameters: self.opts.allow_bare_named_parameters, + allow_unknown_named_parameters: self.opts.allow_unknown_named_parameters, + } + } + + fn bits(&self) -> u32 { + let o = &self.opts; + [ + o.open, + o.read_only, + o.read_write, + o.create, + o.enable_foreign_keys, + o.enable_dqs, + o.read_bigints, + o.return_arrays, + o.allow_bare_named_parameters, + o.allow_unknown_named_parameters, + o.allow_extension, + o.defensive, + self.enable_load_extension, + ] + .iter() + .enumerate() + .fold(0, |bits, (i, on)| bits | (u32::from(*on) << i)) + } + + fn from_bits(path: String, bits: u32, timeout_ms: i32, limits: [Option; 11]) -> Self { + let on = |i: u32| bits & (1 << i) != 0; + DbConfig { + path, + opts: NodeSqliteOptions { + open: on(0), + read_only: on(1), + read_write: on(2), + create: on(3), + enable_foreign_keys: on(4), + enable_dqs: on(5), + timeout_ms, + read_bigints: on(6), + return_arrays: on(7), + allow_bare_named_parameters: on(8), + allow_unknown_named_parameters: on(9), + allow_extension: on(10), + defensive: on(11), + initial_limits: limits, + }, + enable_load_extension: on(12), + } + } +} + +/// A child C resource the connection must delete before it closes. +pub(crate) struct SessionCell { + raw: std::cell::Cell<*mut ffi::sqlite3_session>, +} + +impl SessionCell { + pub(crate) fn new(raw: *mut ffi::sqlite3_session) -> Rc { + Rc::new(SessionCell { + raw: std::cell::Cell::new(raw), + }) + } + + pub(crate) fn raw(&self) -> *mut ffi::sqlite3_session { + self.raw.get() + } + + /// Delete the session now (idempotent). Plain C; no callbacks. + pub(crate) fn delete(&self) { + let raw = self.raw.replace(std::ptr::null_mut()); + if !raw.is_null() { + unsafe { ffi::sqlite3session_delete(raw) }; + } + } +} + +/// The open connection. The C resource is released in `Drop` before the +/// callback sites are freed (fields drop after the body). +pub(crate) struct NodeDb { + pub(crate) raw: *mut ffi::sqlite3, + pub(crate) sessions: Vec>, + sites: CallbackSites, + auth_site: *mut c_void, + next_callback: u32, + pub(crate) agg_free: Vec, + pub(crate) agg_len: u32, + pub(crate) serial: OpenSerial, + pub(crate) cfg: DbConfig, + /// Shared with this open's statements: true until the connection + /// closes. A statement's compiled `sqlite3_stmt` is finalized by its own + /// drop while this holds, and by the connection's close otherwise. + pub(crate) live: Rc>, +} + +extern "C" { + // In the bundled SQLite; libsqlite3-sys does not bind it. + fn sqlite3_close_v2(db: *mut ffi::sqlite3) -> c_int; +} + +impl Drop for NodeDb { + fn drop(&mut self) { + // From here the connection finalizes every statement; a statement + // dropped later must not touch its (finalized) `sqlite3_stmt`. + self.live.set(false); + unsafe { + for session in self.sessions.drain(..) { + session.delete(); + } + // A statement still on the connection belongs to an outer call + // whose callback closed the database (deferred close); finalize + // it here, while the sites it may call are still alive. Its own + // frame sees the database closed and leaves it alone. + loop { + let stmt = ffi::sqlite3_next_stmt(self.raw, std::ptr::null_mut()); + if stmt.is_null() { + break; + } + ffi::sqlite3_finalize(stmt); + } + sqlite3_close_v2(self.raw); + } + #[cfg(test)] + test_counters::DB_DROPS.with(|c| c.set(c.get() + 1)); + } +} + +#[cfg(test)] +pub(crate) mod test_counters { + thread_local! { + pub(crate) static DB_DROPS: std::cell::Cell = const { std::cell::Cell::new(0) }; + } +} + +/// Native memory a connection keeps (page cache, schema, VDBE scratch); a +/// conservative estimate for GC pacing. +const DB_EXTERNAL_BYTES: usize = 64 * 1024; + +pub(crate) fn throw_illegal_invocation() -> ! { + throw_plain_type("Illegal invocation") +} + +/// `this`'s open connection, throwing node's errors otherwise. Do not hold +/// the reference across a C call that can call back, an allocation or JS. +pub(crate) unsafe fn db_payload<'a>(this: f64) -> &'a mut NodeDb { + match native_payload::payload_mut::(this, &DB_FAMILY) { + Ok(db) => db, + Err(PayloadMiss::Closed) => throw_invalid_state("database is not open"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + } +} + +pub(crate) fn db_is_open(db: f64) -> bool { + matches!( + native_payload::lifecycle(db, &DB_FAMILY), + Ok(Lifecycle::Open) + ) +} + +/// Run one C call that can call back into JS, between `enter` and +/// `finish`. `f` must be plain C: it may not throw, convert results or call +/// JS other than through SQLite's callbacks. +pub(crate) unsafe fn guarded(db: f64, f: impl FnOnce() -> R) -> (R, Result<(), CallEnd>) { + let guard = match native_payload::enter(db, &DB_FAMILY) { + Ok(guard) => guard, + Err(PayloadMiss::Closed) => throw_invalid_state("database is not open"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + }; + guard.call(f) +} + +/// Throw what a guarded call ended with, outside every SQLite frame. +pub(crate) unsafe fn throw_call_end(end: CallEnd) -> ! { + match end { + CallEnd::Threw(value) => perry_runtime::exception::js_throw(value), + CallEnd::Closed => throw_invalid_state("database is not open"), + } +} + +/// An `Error` value with a node error code, built without throwing. +pub(crate) fn error_value_with_code(message: &str, code: &'static str) -> f64 { + let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); + perry_runtime::node_submodules::register_error_code_pub(msg, code); + let err = perry_runtime::error::js_error_new_with_message(msg); + js_nanbox_pointer(err as i64) +} + +/// A SQLite error captured inside a guard (plain data). +pub(crate) struct CapturedError { + pub(crate) message: String, + pub(crate) code: i32, +} + +pub(crate) unsafe fn capture_error(db: *mut ffi::sqlite3) -> CapturedError { + CapturedError { + message: sqlite_errmsg_raw(db), + code: ffi::sqlite3_extended_errcode(db), + } +} + +pub(crate) unsafe fn throw_captured(error: CapturedError) -> ! { + throw_sqlite_error_ext(&error.message, error.code) +} + +// ---- configuration in the JS state ---------------------------------------- + +/// The configuration lives in one JS-state field, `cfg`: +/// `[path, flag bits, timeout, limits array | undefined]`. +unsafe fn store_config(db: f64, cfg: &DbConfig) { + let scope = RuntimeHandleScope::new(); + let db = scope.root_nanbox_f64(db); + let record = scope.root_raw_mut_ptr(js_array_alloc(4)); + let push = |value: f64| { + let next = js_array_push_f64(record.get_raw_mut_ptr(), value); + record.set_raw_mut_ptr(next); + }; + push(f64_from_jsvalue(string_value(&cfg.path))); + push(f64_from_jsvalue(JSValue::number(cfg.bits() as f64))); + push(f64_from_jsvalue(JSValue::number( + cfg.opts.timeout_ms as f64, + ))); + if cfg.opts.initial_limits.iter().any(Option::is_some) { + let limits = scope.root_raw_mut_ptr(js_array_alloc(NODE_SQLITE_LIMIT_COUNT as u32)); + for limit in cfg.opts.initial_limits { + let value = f64_from_jsvalue(JSValue::number(limit.map_or(-1.0, f64::from))); + let next = js_array_push_f64(limits.get_raw_mut_ptr(), value); + limits.set_raw_mut_ptr(next); + } + push(js_nanbox_pointer( + limits.get_raw_mut_ptr::() as i64 + )); + } else { + push(undefined_f64()); + } + let record = js_nanbox_pointer(record.get_raw_mut_ptr::() as i64); + native_payload::state_set(db.get_nanbox_f64(), &DB_FAMILY, b"cfg", record); +} + +/// Re-state the flag bits after `enableDefensive` / `enableLoadExtension`. +unsafe fn store_config_bits(db: f64, cfg: &DbConfig) { + let record = native_payload::state_get(db, &DB_FAMILY, b"cfg"); + if value_from_f64(record).is_pointer() { + perry_runtime::array::js_array_set_f64( + raw_addr_from_value(record) as *mut ArrayHeader, + 1, + f64_from_jsvalue(JSValue::number(cfg.bits() as f64)), + ); + } +} + +fn number_of(value: f64) -> f64 { + let js = value_from_f64(value); + if js.is_int32() { + js.as_int32() as f64 + } else if js.is_number() { + js.as_number() + } else { + 0.0 + } +} + +unsafe fn load_config(db: f64) -> DbConfig { + let record = native_payload::state_get(db, &DB_FAMILY, b"cfg"); + let arr = raw_addr_from_value(record) as *const ArrayHeader; + let field = |i: u32| { + if arr.is_null() || i >= js_array_length(arr) { + undefined_f64() + } else { + f64_from_jsvalue(js_array_get(arr, i)) + } + }; + let path = string_key_from_js_value(value_from_f64(field(0))).unwrap_or_default(); + let bits = number_of(field(1)) as u32; + let timeout = number_of(field(2)) as i32; + let mut limits = [None; NODE_SQLITE_LIMIT_COUNT]; + let stored = field(3); + if value_from_f64(stored).is_pointer() { + let larr = raw_addr_from_value(stored) as *const ArrayHeader; + for (i, slot) in limits.iter_mut().enumerate() { + if (i as u32) < js_array_length(larr) { + let value = number_of(f64_from_jsvalue(js_array_get(larr, i as u32))); + if value >= 0.0 { + *slot = Some(value as i32); + } + } + } + } + DbConfig::from_bits(path, bits, timeout, limits) +} + +// ---- opening ----------------------------------------------------------- + +/// Open the C connection for `cfg` (rusqlite's flags, extended result codes +/// and default busy timeout, then node's pragmas and limits). +unsafe fn open_raw(cfg: &DbConfig) -> Result<*mut ffi::sqlite3, NodeSqliteBackupError> { + let mut flags = ffi::SQLITE_OPEN_URI | ffi::SQLITE_OPEN_NOMUTEX | ffi::SQLITE_OPEN_EXRESCODE; + if cfg.opts.read_only { + flags |= ffi::SQLITE_OPEN_READONLY; + } + if cfg.opts.read_write { + flags |= ffi::SQLITE_OPEN_READWRITE; + } + if cfg.opts.create { + flags |= ffi::SQLITE_OPEN_CREATE; + } + let path = if cfg.path == ":memory:" { + cfg.path.clone() + } else { + resolve_sqlite_path(&cfg.path) + }; + let c_path = CString::new(path.clone()).map_err(|_| NodeSqliteBackupError { + message: "path must not contain null bytes".to_string(), + errcode: None, + errstr: None, + })?; + let mut db: *mut ffi::sqlite3 = std::ptr::null_mut(); + let rc = ffi::sqlite3_open_v2(c_path.as_ptr(), &mut db, flags, std::ptr::null()); + if rc != ffi::SQLITE_OK { + let errstr = sqlite_errstr(rc); + let message = if db.is_null() { + path + } else { + let msg = sqlite_errmsg_raw(db); + ffi::sqlite3_close(db); + if rc & 0xff == ffi::SQLITE_CANTOPEN { + format!("{msg}: {path}") + } else { + msg + } + }; + return Err(NodeSqliteBackupError { + message, + errcode: Some(rc), + errstr: Some(errstr), + }); + } + let fail = |db: *mut ffi::sqlite3, rc: c_int| { + let error = NodeSqliteBackupError { + message: sqlite_errmsg_raw(db), + errcode: Some(rc), + errstr: Some(sqlite_errstr(rc)), + }; + ffi::sqlite3_close(db); + error + }; + let rc = ffi::sqlite3_busy_timeout(db, 5000); + if rc != ffi::SQLITE_OK { + return Err(fail(db, rc)); + } + if cfg.opts.timeout_ms > 0 { + let rc = ffi::sqlite3_busy_timeout(db, cfg.opts.timeout_ms); + if rc != ffi::SQLITE_OK { + return Err(fail(db, rc)); + } + } + let pragma = if cfg.opts.enable_foreign_keys { + c"PRAGMA foreign_keys = ON" + } else { + c"PRAGMA foreign_keys = OFF" + }; + let rc = ffi::sqlite3_exec( + db, + pragma.as_ptr(), + None, + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + if rc != ffi::SQLITE_OK { + return Err(fail(db, ffi::sqlite3_extended_errcode(db))); + } + for (idx, value) in cfg.opts.initial_limits.iter().enumerate() { + if let Some(value) = value { + ffi::sqlite3_limit(db, limit_id(idx), *value); + } + } + Ok(db) +} + +fn limit_id(idx: usize) -> c_int { + [ + ffi::SQLITE_LIMIT_LENGTH, + ffi::SQLITE_LIMIT_SQL_LENGTH, + ffi::SQLITE_LIMIT_COLUMN, + ffi::SQLITE_LIMIT_EXPR_DEPTH, + ffi::SQLITE_LIMIT_COMPOUND_SELECT, + ffi::SQLITE_LIMIT_VDBE_OP, + ffi::SQLITE_LIMIT_FUNCTION_ARG, + ffi::SQLITE_LIMIT_ATTACHED, + ffi::SQLITE_LIMIT_LIKE_PATTERN_LENGTH, + ffi::SQLITE_LIMIT_VARIABLE_NUMBER, + ffi::SQLITE_LIMIT_TRIGGER_DEPTH, + ][idx] +} + +/// CLOSED -> OPEN: open the connection and attach it to the same object. +unsafe fn open_db(this: f64, cfg: DbConfig) { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this); + match native_payload::lifecycle(this.get_nanbox_f64(), &DB_FAMILY) { + Ok(Lifecycle::Closed) => {} + Ok(Lifecycle::Open) => throw_invalid_state("database is already open"), + Ok(Lifecycle::Closing) => throw_invalid_state("database is closing"), + Err(PayloadMiss::Closed) => throw_invalid_state("database is not open"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + } + let raw = match open_raw(&cfg) { + Ok(raw) => raw, + Err(error) => perry_runtime::exception::js_throw(sqlite_error_value(error)), + }; + let configured = configure_node_sqlite_defensive(raw, cfg.opts.defensive) + .and_then(|_| configure_node_sqlite_dqs(raw, cfg.opts.enable_dqs)) + .and_then(|_| configure_node_sqlite_load_extension(raw, cfg.enable_load_extension)); + if let Err(message) = configured { + ffi::sqlite3_close(raw); + throw_sqlite_error(&message); + } + let payload = NodeDb { + raw, + sessions: Vec::new(), + sites: CallbackSites::new(), + auth_site: std::ptr::null_mut(), + next_callback: 1, + agg_free: Vec::new(), + agg_len: 0, + serial: native_payload::next_open_serial(), + cfg, + live: Rc::new(Cell::new(true)), + }; + match native_payload::attach( + this.get_nanbox_f64(), + &DB_FAMILY, + payload, + DB_EXTERNAL_BYTES, + ) { + Ok(()) => {} + Err(AttachMiss::Open) => throw_invalid_state("database is already open"), + Err(AttachMiss::Closing) => throw_invalid_state("database is closing"), + Err(AttachMiss::Finalized | AttachMiss::Foreign) => { + throw_invalid_state("database is not open") + } + } +} + +// ---- construction ------------------------------------------------------ + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_call(_path: f64, _options: f64) -> f64 { + throw_construct_required() +} + +/// `new DatabaseSync(path, options)`. +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_database_sync_new( + path_value: f64, + options_value: f64, +) -> f64 { + let path = node_sqlite_database_path(path_value); + let options = parse_node_sqlite_options(options_value); + let open = options.open; + let cfg = DbConfig { + path, + enable_load_extension: options.allow_extension, + opts: options, + }; + let scope = RuntimeHandleScope::new(); + let db = scope.root_nanbox_f64(native_payload::alloc_closed(&DB_FAMILY, &[])); + let getter = |name: &str, info| { + native_payload::define_own_accessor(db.get_nanbox_f64(), name, info, None, true, false) + }; + getter("isOpen", builtin!(db_is_open_getter, 0)); + getter("isTransaction", builtin!(db_is_transaction_getter, 0)); + getter("limits", builtin!(db_limits_getter, 0)); + let type_symbol = + perry_runtime::symbol::js_symbol_for(f64_from_jsvalue(string_value("sqlite-type"))); + let type_symbol = scope.root_nanbox_f64(type_symbol); + perry_runtime::symbol::js_object_set_symbol_property( + db.get_nanbox_f64(), + type_symbol.get_nanbox_f64(), + f64_from_jsvalue(string_value("node:sqlite")), + ); + store_config(db.get_nanbox_f64(), &cfg); + if open { + open_db(db.get_nanbox_f64(), cfg); + } + db.get_nanbox_f64() +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_native_dispatch( + method_name_ptr: *const u8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, + construct: i32, +) -> f64 { + let method_name = if method_name_ptr.is_null() || method_name_len == 0 { + "" + } else { + std::str::from_utf8_unchecked(std::slice::from_raw_parts(method_name_ptr, method_name_len)) + }; + let arg = |index: usize| -> f64 { + if index < args_len && !args_ptr.is_null() { + *args_ptr.add(index) + } else { + undefined_f64() + } + }; + if construct == perry_runtime::value::NATIVE_SQLITE_DISPATCH_PROTOTYPE { + return match method_name { + "DatabaseSync" => native_payload::prototype(&DB_FAMILY), + "StatementSync" => native_payload::prototype(&STMT_FAMILY), + "Session" => native_payload::prototype(&SESSION_FAMILY), + _ => undefined_f64(), + }; + } + match (method_name, construct != 0) { + ("DatabaseSync", true) => js_node_sqlite_database_sync_new(arg(0), arg(1)), + ("DatabaseSync", false) => js_node_sqlite_database_sync_call(arg(0), arg(1)), + ("Session" | "StatementSync", _) => throw_illegal_constructor(), + ("backup", _) => js_nanbox_pointer(js_node_sqlite_backup(arg(0), arg(1), arg(2)) as i64), + _ => undefined_f64(), + } +} + +/// Validate the `DatabaseSync` `path` argument per Node: a string or `file:` URL, +/// neither of which may contain null +/// bytes — with Node's exact `ERR_INVALID_ARG_TYPE` message (#6561). +pub(crate) unsafe fn node_sqlite_database_path(value: f64) -> String { + const PATH_TYPE_MSG: &str = + "The \"path\" argument must be a string, Uint8Array, or URL without null bytes."; + let js = value_from_f64(value); + let path = if js.is_any_string() { + let ptr = perry_runtime::js_get_string_pointer_unified(value) + as *const perry_runtime::StringHeader; + string_from_header(ptr).unwrap_or_else(|| throw_type(PATH_TYPE_MSG)) + } else if let Some(bytes) = node_sqlite_path_bytes(value) { + if bytes.contains(&0) { + throw_type(PATH_TYPE_MSG); + } + String::from_utf8_lossy(&bytes).into_owned() + } else if js.is_pointer() { + // URL object — accept `file:` URLs, decoding the percent-encoded + // pathname like Node's `fileURLToPath`. + let protocol = string_from_jsvalue(object_field(value, "protocol")).unwrap_or_default(); + let pathname = string_from_jsvalue(object_field(value, "pathname")).unwrap_or_default(); + if protocol != "file:" || pathname.is_empty() { + throw_type(PATH_TYPE_MSG); + } + percent_decode_pathname(&pathname) + } else { + throw_type(PATH_TYPE_MSG); + }; + if path.as_bytes().contains(&0) { + throw_type(PATH_TYPE_MSG); + } + path +} + +unsafe fn node_sqlite_path_bytes(value: f64) -> Option> { + use perry_runtime::buffer::{is_registered_buffer, is_uint8array_buffer}; + let raw = raw_addr_from_value(value); + if raw < 0x1000 { + return None; + } + if (is_registered_buffer(raw) && is_uint8array_buffer(raw)) + || perry_runtime::typedarray::lookup_typed_array_kind(raw) + == Some(perry_runtime::typedarray::KIND_UINT8) + { + return perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(raw as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }); + } + None +} + +// ---- prototype ----------------------------------------------------------- + +fn install_db_prototype(proto: &mut PayloadPrototype) { + proto.method("open", builtin!(db_open_thunk, 0), 0); + proto.method("close", builtin!(db_close_thunk, 0), 0); + proto.method("exec", builtin!(db_exec_thunk, 1), 1); + proto.method("prepare", builtin!(db_prepare_thunk, 2), 1); + proto.method("serialize", builtin!(db_serialize_thunk, 1), 0); + proto.method("deserialize", builtin!(db_deserialize_thunk, 1), 1); + proto.method("function", builtin!(db_function_thunk, 3), 2); + proto.method("aggregate", builtin!(db_aggregate_thunk, 2), 2); + proto.method("enableDefensive", builtin!(db_enable_defensive_thunk, 1), 1); + proto.method("setAuthorizer", builtin!(db_set_authorizer_thunk, 1), 1); + proto.method("createTagStore", builtin!(db_create_tag_store_thunk, 1), 0); + proto.method("createSession", builtin!(db_create_session_thunk, 1), 0); + proto.method("applyChangeset", builtin!(db_apply_changeset_thunk, 2), 1); + proto.method( + "enableLoadExtension", + builtin!(db_enable_load_extension_thunk, 1), + 1, + ); + proto.method("loadExtension", builtin!(db_load_extension_thunk, 1), 1); + proto.method("location", builtin!(db_location_thunk, 1), 0); + proto.symbol_method( + "dispose", + "[Symbol.dispose]", + builtin!(db_dispose_thunk, 0), + 0, + ); +} + +extern "C" fn db_open_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let this = this.as_f64(); + if !native_payload::is_instance(this, &DB_FAMILY) { + throw_illegal_invocation(); + } + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this); + let cfg = load_config(this.get_nanbox_f64()); + open_db(this.get_nanbox_f64(), cfg); + } + undefined_f64() +} + +/// Release the connection now, or after the outermost running C call. +unsafe fn close_db(this: f64) { + native_payload::close(this, &DB_FAMILY); + // node drops every registration with the connection; a reopen starts + // with none. + native_payload::state_set(this, &DB_FAMILY, b"callbacks", undefined_f64()); + native_payload::state_set(this, &DB_FAMILY, b"aggStates", undefined_f64()); +} + +extern "C" fn db_close_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let this = this.as_f64(); + match native_payload::lifecycle(this, &DB_FAMILY) { + Ok(Lifecycle::Open) => close_db(this), + Ok(_) | Err(PayloadMiss::Closed) => throw_invalid_state("database is not open"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + } + } + undefined_f64() +} + +extern "C" fn db_dispose_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let this = this.as_f64(); + if db_is_open(this) { + close_db(this); + } + } + undefined_f64() +} + +extern "C" fn db_is_open_getter(_c: *const ClosureHeader, this: JsThis) -> f64 { + let this = this.as_f64(); + if !native_payload::is_instance(this, &DB_FAMILY) { + throw_illegal_invocation(); + } + bool_f64(db_is_open(this)) +} + +extern "C" fn db_is_transaction_getter(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let raw = db_payload(this.as_f64()).raw; + bool_f64(ffi::sqlite3_get_autocommit(raw) == 0) + } +} + +extern "C" fn db_exec_thunk(_c: *const ClosureHeader, this: JsThis, sql: f64) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + let raw = db_payload(this.get_nanbox_f64()).raw; + let sql = string_from_value(sql, "sql"); + let Ok(c_sql) = CString::new(sql) else { + throw_sqlite_error_ext("SQL string must not contain null bytes", ffi::SQLITE_MISUSE); + }; + let (error, end) = guarded(this.get_nanbox_f64(), || exec_raw(raw, &c_sql)); + if let Err(end) = end { + throw_call_end(end); + } + if let Some(error) = error { + throw_captured(error); + } + } + undefined_f64() +} + +/// `sqlite3_exec` capturing its error as plain data (callable in a guard). +pub(crate) unsafe fn exec_raw(raw: *mut ffi::sqlite3, sql: &CStr) -> Option { + let mut error_message: *mut c_char = std::ptr::null_mut(); + let rc = ffi::sqlite3_exec( + raw, + sql.as_ptr(), + None, + std::ptr::null_mut(), + &mut error_message, + ); + if rc == ffi::SQLITE_OK { + return None; + } + let code = ffi::sqlite3_extended_errcode(raw); + let message = if error_message.is_null() { + sqlite_errmsg_raw(raw) + } else { + let message = CStr::from_ptr(error_message).to_string_lossy().into_owned(); + ffi::sqlite3_free(error_message.cast()); + message + }; + Some(CapturedError { message, code }) +} + +/// Statement options over the database defaults (`prepare(sql, options)`). +unsafe fn parse_statement_options(defaults: StmtFlags, options_value: f64) -> StmtFlags { + let js = value_from_f64(options_value); + if js.is_undefined() { + return defaults; + } + if js.is_null() || !is_object_like(options_value) { + throw_type("The \"options\" argument must be an object."); + } + StmtFlags { + read_bigints: bool_option(options_value, "readBigInts", defaults.read_bigints), + return_arrays: bool_option(options_value, "returnArrays", defaults.return_arrays), + allow_bare_named_parameters: bool_option( + options_value, + "allowBareNamedParameters", + defaults.allow_bare_named_parameters, + ), + allow_unknown_named_parameters: bool_option( + options_value, + "allowUnknownNamedParameters", + defaults.allow_unknown_named_parameters, + ), + } +} + +extern "C" fn db_prepare_thunk( + _c: *const ClosureHeader, + this: JsThis, + sql_value: f64, + options_value: f64, +) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + let defaults = db_payload(this.get_nanbox_f64()).cfg.stmt_flags(); + if !value_from_f64(sql_value).is_any_string() { + throw_type("The \"sql\" argument must be of type string"); + } + let sql = string_from_value(sql_value, "sql"); + let flags = parse_statement_options(defaults, options_value); + let Ok(c_sql) = CString::new(sql) else { + throw_sqlite_error("SQL string must not contain null bytes"); + }; + let raw = db_payload(this.get_nanbox_f64()).raw; + // Compiled once, as node does: the statement owns it from here. + let stmt = prepare_guarded(&this, raw, &c_sql); + let open = db_payload(this.get_nanbox_f64()); + let (serial, live) = (open.serial, open.live.clone()); + new_statement(&this, serial, live, c_sql, stmt, flags) + } +} + +/// Compile `sql` on the open connection, under a guard (the authorizer +/// runs here). Throws node's error after cleaning up. +pub(crate) unsafe fn prepare_guarded( + db: &RuntimeHandle<'_>, + raw: *mut ffi::sqlite3, + sql: &CStr, +) -> *mut ffi::sqlite3_stmt { + prepare_guarded_with_link(db, raw, sql).0 +} + +pub(crate) unsafe fn prepare_guarded_with_link( + db: &RuntimeHandle<'_>, + raw: *mut ffi::sqlite3, + sql: &CStr, +) -> (*mut ffi::sqlite3_stmt, native_payload::OwnerLink) { + let guard = match native_payload::enter(db.get_nanbox_f64(), &DB_FAMILY) { + Ok(guard) => guard, + Err(PayloadMiss::Closed) => throw_invalid_state("database is not open"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + }; + let link = guard.owner_link(); + let ((stmt, error), end) = guard.call(|| { + let mut stmt = std::ptr::null_mut(); + let rc = ffi::sqlite3_prepare_v2(raw, sql.as_ptr(), -1, &mut stmt, std::ptr::null_mut()); + let error = (rc != ffi::SQLITE_OK).then(|| capture_error(raw)); + (stmt, error) + }); + // A deferred close released inside the call finalized every statement + // of the connection, this one included. + if let Err(end) = end { + if !stmt.is_null() && db_is_open(db.get_nanbox_f64()) { + ffi::sqlite3_finalize(stmt); + } + throw_call_end(end); + } + if let Some(error) = error { + if !stmt.is_null() { + ffi::sqlite3_finalize(stmt); + } + throw_captured(error); + } + (stmt, link) +} + +extern "C" fn db_serialize_thunk(_c: *const ClosureHeader, this: JsThis, schema: f64) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + db_payload(this.get_nanbox_f64()); + let schema = if value_from_f64(schema).is_undefined() { + "main".to_string() + } else { + string_from_value(schema, "attachedDb") + }; + let schema = CString::new(schema) + .unwrap_or_else(|_| throw_type("The \"attachedDb\" argument must be a string")); + let raw = db_payload(this.get_nanbox_f64()).raw; + js_nanbox_pointer(sqlite_serialize_to_buffer(raw, &schema) as i64) + } +} + +extern "C" fn db_deserialize_thunk(_c: *const ClosureHeader, this: JsThis, image: f64) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + db_payload(this.get_nanbox_f64()); + let bytes = sqlite_image_bytes(image); + let raw = db_payload(this.get_nanbox_f64()).raw; + sqlite_deserialize_main(raw, &bytes); + } + undefined_f64() +} + +/// Take `count` consecutive callback indices and a userdata site for the +/// first. The payload borrow ends before any JS-heap allocation. +unsafe fn reserve_callbacks( + this: f64, + count: u32, + use_bigint_arguments: bool, +) -> (u32, *mut c_void) { + let link = native_payload::owner_link(this, &DB_FAMILY) + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + let db = db_payload(this); + let base = db.next_callback; + db.next_callback = base + .checked_add(count) + .filter(|next| *next < SITE_BIGINT_ARGS) + .unwrap_or_else(|| throw_invalid_state("too many registered functions")); + let flag = if use_bigint_arguments { + SITE_BIGINT_ARGS + } else { + 0 + }; + let site = db.sites.site(link, base | flag); + (base, site) +} + +extern "C" fn db_function_thunk( + _c: *const ClosureHeader, + this: JsThis, + name_value: f64, + options_or_function_value: f64, + function_value: f64, +) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + db_payload(this.get_nanbox_f64()); + let name = sqlite_function_name(string_from_value(name_value, "name")); + let (options_value, callback) = + if closure_ptr_from_value(options_or_function_value).is_some() { + (undefined_f64(), options_or_function_value) + } else { + let options_js = value_from_f64(options_or_function_value); + if options_js.is_undefined() && value_from_f64(function_value).is_undefined() { + node_sqlite_function_arg(options_or_function_value, "function"); + } + if options_js.is_null() + || options_js.is_undefined() + || !is_object_like(options_or_function_value) + { + throw_type("The \"options\" argument must be an object."); + } + ( + options_or_function_value, + node_sqlite_function_arg(function_value, "function"), + ) + }; + let callback = scope.root_nanbox_f64(callback); + let use_bigint_arguments = + node_sqlite_bool_option_exact(options_value, "useBigIntArguments", false); + let varargs = node_sqlite_bool_option_exact(options_value, "varargs", false); + let deterministic = node_sqlite_bool_option_exact(options_value, "deterministic", false); + let direct_only = node_sqlite_bool_option_exact(options_value, "directOnly", false); + let argc = if varargs { + -1 + } else { + node_sqlite_closure_arity(callback.get_nanbox_f64()) + }; + let mut text_rep = ffi::SQLITE_UTF8; + if deterministic { + text_rep |= ffi::SQLITE_DETERMINISTIC; + } + if direct_only { + text_rep |= ffi::SQLITE_DIRECTONLY; + } + let (index, site) = reserve_callbacks(this.get_nanbox_f64(), 1, use_bigint_arguments); + native_payload::set_callback( + this.get_nanbox_f64(), + &DB_FAMILY, + index, + callback.get_nanbox_f64(), + ); + let raw = db_payload(this.get_nanbox_f64()).raw; + let rc = ffi::sqlite3_create_function_v2( + raw, + name.as_ptr(), + argc, + text_rep, + site, + Some(node_sqlite_scalar_trampoline), + None, + None, + None, + ); + if rc != ffi::SQLITE_OK { + throw_sqlite_error_from_db(raw); + } + } + undefined_f64() +} + +extern "C" fn db_aggregate_thunk( + _c: *const ClosureHeader, + this: JsThis, + name_value: f64, + options_value: f64, +) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + db_payload(this.get_nanbox_f64()); + let name = sqlite_function_name(string_from_value(name_value, "name")); + if value_from_f64(options_value).is_null() || !is_object_like(options_value) { + throw_plain_type("The \"options\" argument must be an object."); + } + let options = scope.root_nanbox_f64(options_value); + let start = object_field(options.get_nanbox_f64(), "start"); + if start.is_undefined() { + throw_type("The \"options.start\" argument must be a function or a primitive value."); + } + let start = scope.root_nanbox_u64(start.bits()); + let step = node_sqlite_optional_callback_option(options.get_nanbox_f64(), "step", true) + .unwrap_or_else(|| throw_type("The \"options.step\" argument must be a function.")); + let step = scope.root_nanbox_f64(step); + let result = + node_sqlite_optional_callback_option(options.get_nanbox_f64(), "result", false) + .unwrap_or_else(undefined_f64); + let result = scope.root_nanbox_f64(result); + let inverse = + node_sqlite_optional_callback_option(options.get_nanbox_f64(), "inverse", true) + .unwrap_or_else(undefined_f64); + let inverse = scope.root_nanbox_f64(inverse); + let has_inverse = !value_from_f64(inverse.get_nanbox_f64()).is_undefined(); + let use_bigint_arguments = + node_sqlite_bool_option_exact(options.get_nanbox_f64(), "useBigIntArguments", false); + let varargs = node_sqlite_bool_option_exact(options.get_nanbox_f64(), "varargs", false); + let direct_only = + node_sqlite_bool_option_exact(options.get_nanbox_f64(), "directOnly", false); + let argc = if varargs { + -1 + } else { + node_sqlite_closure_arity(step.get_nanbox_f64()).saturating_sub(1) + }; + let mut text_rep = ffi::SQLITE_UTF8; + if direct_only { + text_rep |= ffi::SQLITE_DIRECTONLY; + } + let (base, site) = + reserve_callbacks(this.get_nanbox_f64(), AGG_SLOTS, use_bigint_arguments); + // Each store may collect: read every value from its handle. + let owner = this.get_nanbox_f64(); + native_payload::set_callback( + owner, + &DB_FAMILY, + base + AGG_START, + f64::from_bits(start.get_nanbox_u64()), + ); + let owner = this.get_nanbox_f64(); + native_payload::set_callback(owner, &DB_FAMILY, base + AGG_STEP, step.get_nanbox_f64()); + let owner = this.get_nanbox_f64(); + native_payload::set_callback( + owner, + &DB_FAMILY, + base + AGG_RESULT, + result.get_nanbox_f64(), + ); + let owner = this.get_nanbox_f64(); + native_payload::set_callback( + owner, + &DB_FAMILY, + base + AGG_INVERSE, + inverse.get_nanbox_f64(), + ); + let raw = db_payload(this.get_nanbox_f64()).raw; + let rc = ffi::sqlite3_create_window_function( + raw, + name.as_ptr(), + argc, + text_rep, + site, + Some(node_sqlite_aggregate_step), + Some(node_sqlite_aggregate_final), + if has_inverse { + Some(node_sqlite_aggregate_value) + } else { + None + }, + if has_inverse { + Some(node_sqlite_aggregate_inverse) + } else { + None + }, + None, + ); + if rc != ffi::SQLITE_OK { + throw_sqlite_error_from_db(raw); + } + } + undefined_f64() +} + +extern "C" fn db_set_authorizer_thunk( + _c: *const ClosureHeader, + this: JsThis, + callback_value: f64, +) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + db_payload(this.get_nanbox_f64()); + let js = value_from_f64(callback_value); + let enable = if js.is_null() { + false + } else { + if closure_ptr_from_value(callback_value).is_none() { + throw_type("The \"callback\" argument must be a function or null."); + } + true + }; + native_payload::set_callback( + this.get_nanbox_f64(), + &DB_FAMILY, + AUTHORIZER_INDEX, + if enable { + callback_value + } else { + undefined_f64() + }, + ); + let link = native_payload::owner_link(this.get_nanbox_f64(), &DB_FAMILY) + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + let db = db_payload(this.get_nanbox_f64()); + if db.auth_site.is_null() { + db.auth_site = db.sites.site(link, AUTHORIZER_INDEX); + } + let (raw, site) = (db.raw, db.auth_site); + let rc = ffi::sqlite3_set_authorizer( + raw, + if enable { + Some(node_sqlite_authorizer_trampoline) + } else { + None + }, + if enable { site } else { std::ptr::null_mut() }, + ); + if rc != ffi::SQLITE_OK { + throw_sqlite_error_from_db(raw); + } + } + undefined_f64() +} + +extern "C" fn db_enable_defensive_thunk( + _c: *const ClosureHeader, + this: JsThis, + active: f64, +) -> f64 { + unsafe { + let js = value_from_f64(active); + if !js.is_bool() { + throw_type("The \"active\" argument must be a boolean."); + } + let active = js.as_bool(); + let raw = db_payload(this.as_f64()).raw; + if let Err(message) = configure_node_sqlite_defensive(raw, active) { + throw_sqlite_error(&message); + } + let db = db_payload(this.as_f64()); + db.cfg.opts.defensive = active; + let cfg = db.cfg.clone(); + store_config_bits(this.as_f64(), &cfg); + } + undefined_f64() +} + +extern "C" fn db_enable_load_extension_thunk( + _c: *const ClosureHeader, + this: JsThis, + allow_value: f64, +) -> f64 { + unsafe { + let js = value_from_f64(allow_value); + if !js.is_bool() { + throw_type("The \"allow\" argument must be a boolean"); + } + let allow = js.as_bool(); + let this = this.as_f64(); + if !native_payload::is_instance(this, &DB_FAMILY) { + throw_illegal_invocation(); + } + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this); + let mut cfg = match native_payload::payload_mut::(this.get_nanbox_f64(), &DB_FAMILY) + { + Ok(db) => db.cfg.clone(), + Err(_) => load_config(this.get_nanbox_f64()), + }; + if allow && !cfg.opts.allow_extension { + throw_invalid_state( + "Cannot enable extension loading because it was disabled at database creation.", + ); + } + if let Ok(db) = native_payload::payload_mut::(this.get_nanbox_f64(), &DB_FAMILY) { + if let Err(message) = configure_node_sqlite_load_extension(db.raw, allow) { + throw_sqlite_error(&message); + } + db.cfg.enable_load_extension = allow; + } + cfg.enable_load_extension = allow; + store_config_bits(this.get_nanbox_f64(), &cfg); + } + undefined_f64() +} + +extern "C" fn db_load_extension_thunk(_c: *const ClosureHeader, this: JsThis, path: f64) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + let db = db_payload(this.get_nanbox_f64()); + if !db.cfg.opts.allow_extension || !db.cfg.enable_load_extension { + throw_invalid_state("extension loading is not allowed"); + } + let path = string_from_value(path, "path"); + let c_path = CString::new(path) + .unwrap_or_else(|_| throw_type("The \"path\" argument must not contain null bytes")); + let raw = db_payload(this.get_nanbox_f64()).raw; + sqlite_load_extension_or_throw(raw, &c_path); + } + undefined_f64() +} + +extern "C" fn db_location_thunk(_c: *const ClosureHeader, this: JsThis, db_name: f64) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + db_payload(this.get_nanbox_f64()); + let db_name = if value_from_f64(db_name).is_undefined() { + "main".to_string() + } else { + string_from_value(db_name, "dbName") + }; + let c_name = CString::new(db_name) + .unwrap_or_else(|_| throw_type("The \"dbName\" argument must not contain null bytes")); + let raw = db_payload(this.get_nanbox_f64()).raw; + let filename = ffi::sqlite3_db_filename(raw, c_name.as_ptr()); + if filename.is_null() { + return null_f64(); + } + let filename = CStr::from_ptr(filename).to_str().unwrap_or(""); + if filename.is_empty() { + null_f64() + } else { + f64_from_jsvalue(string_value(filename)) + } + } +} + +extern "C" fn db_create_tag_store_thunk(_c: *const ClosureHeader, this: JsThis, max: f64) -> f64 { + unsafe { new_tag_store(this.as_f64(), node_sqlite_tag_store_capacity(max)) } +} + +extern "C" fn db_create_session_thunk(_c: *const ClosureHeader, this: JsThis, options: f64) -> f64 { + unsafe { new_session(this.as_f64(), options) } +} + +extern "C" fn db_apply_changeset_thunk( + _c: *const ClosureHeader, + this: JsThis, + changeset: f64, + options: f64, +) -> f64 { + unsafe { apply_changeset(this.as_f64(), changeset, options) } +} + +// ---- db.limits ----------------------------------------------------------- + +fn install_limits_prototype(_proto: &mut PayloadPrototype) {} + +extern "C" fn db_limits_getter(_c: *const ClosureHeader, this: JsThis) -> f64 { + { + let this = this.as_f64(); + if !native_payload::is_instance(this, &DB_FAMILY) { + throw_illegal_invocation(); + } + let existing = native_payload::state_get_memo(this, &DB_FAMILY, b"limits", &MEMO_LIMITS); + if value_from_f64(existing).is_pointer() { + return existing; + } + let scope = RuntimeHandleScope::new(); + let db = scope.root_nanbox_f64(this); + let limits = scope.root_nanbox_f64(native_payload::alloc(&LIMITS_FAMILY, (), 0, &[])); + native_payload::state_set_memo( + limits.get_nanbox_f64(), + &LIMITS_FAMILY, + b"db", + db.get_nanbox_f64(), + &MEMO_DB, + ); + for (name, get, set) in limit_accessors() { + native_payload::define_own_accessor( + limits.get_nanbox_f64(), + name, + get, + Some(set), + true, + false, + ); + } + native_payload::state_set_memo( + db.get_nanbox_f64(), + &DB_FAMILY, + b"limits", + limits.get_nanbox_f64(), + &MEMO_LIMITS, + ); + limits.get_nanbox_f64() + } +} + +unsafe fn limit_get(this: f64, idx: usize) -> f64 { + if !native_payload::is_instance(this, &LIMITS_FAMILY) { + throw_illegal_invocation(); + } + let db = native_payload::state_get_memo(this, &LIMITS_FAMILY, b"db", &MEMO_DB); + let raw = db_payload(db).raw; + f64_from_jsvalue(JSValue::number( + ffi::sqlite3_limit(raw, limit_id(idx), -1) as f64 + )) +} + +unsafe fn limit_set(this: f64, idx: usize, name: &str, value: f64) -> f64 { + if !native_payload::is_instance(this, &LIMITS_FAMILY) { + throw_illegal_invocation(); + } + let db = native_payload::state_get_memo(this, &LIMITS_FAMILY, b"db", &MEMO_DB); + db_payload(db); + let new_value = non_negative_i32_value(value_from_f64(value), name, true); + let raw = db_payload(db).raw; + ffi::sqlite3_limit(raw, limit_id(idx), new_value); + undefined_f64() +} + +macro_rules! limit_thunks { + ($( $idx:expr, $name:literal, $get:ident, $set:ident; )*) => { + $( + extern "C" fn $get(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { limit_get(this.as_f64(), $idx) } + } + extern "C" fn $set(_c: *const ClosureHeader, this: JsThis, value: f64) -> f64 { + unsafe { limit_set(this.as_f64(), $idx, $name, value) } + } + )* + fn limit_accessors() -> Vec<( + &'static str, + *const perry_runtime::closure::JsFunctionInfo, + *const perry_runtime::closure::JsFunctionInfo, + )> { + vec![$( ($name, builtin!($get, 0), builtin!($set, 1)), )*] + } + }; +} + +limit_thunks! { + 0, "length", limit_get_length, limit_set_length; + 1, "sqlLength", limit_get_sql_length, limit_set_sql_length; + 2, "column", limit_get_column, limit_set_column; + 3, "exprDepth", limit_get_expr_depth, limit_set_expr_depth; + 4, "compoundSelect", limit_get_compound_select, limit_set_compound_select; + 5, "vdbeOp", limit_get_vdbe_op, limit_set_vdbe_op; + 6, "functionArg", limit_get_function_arg, limit_set_function_arg; + 7, "attach", limit_get_attach, limit_set_attach; + 8, "likePatternLength", limit_get_like_pattern_length, limit_set_like_pattern_length; + 9, "variableNumber", limit_get_variable_number, limit_set_variable_number; + 10, "triggerDepth", limit_get_trigger_depth, limit_set_trigger_depth; +} + +// ---- sqlite.backup() ------------------------------------------------------- + +/// `backup(sourceDb, path, options)`: a promise for the page count. The +/// progress callback runs between steps through `call_from_native` inside +/// one guard, so a throw or a `close()` from it ends the backup cleanly. +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_backup( + source_db_value: f64, + path_value: f64, + options_value: f64, +) -> *mut Promise { + if !value_from_f64(source_db_value).is_pointer() { + throw_type("The \"sourceDb\" argument must be an object."); + } + if !native_payload::is_instance(source_db_value, &DB_FAMILY) { + throw_type("The \"sourceDb\" argument must be an instance of DatabaseSync."); + } + let scope = RuntimeHandleScope::new(); + let db = scope.root_nanbox_f64(source_db_value); + db_payload(db.get_nanbox_f64()); + let path = path_like_from_value(path_value, "path"); + let options = parse_node_sqlite_backup_options(options_value); + let progress = options.progress.map(|p| scope.root_nanbox_f64(p)); + let raw = db_payload(db.get_nanbox_f64()).raw; + let (result, end) = guarded(db.get_nanbox_f64(), || { + perform_node_sqlite_backup(raw, &path, &options, |total, remaining| { + let Some(progress) = &progress else { + return true; + }; + let info = backup_progress_info(total, remaining); + native_payload::call_from_native( + db.get_nanbox_f64(), + progress.get_nanbox_f64(), + undefined_f64(), + &[info], + ) + .is_ok() + }) + }); + match end { + Err(CallEnd::Threw(value)) => return js_promise_rejected(value), + Err(CallEnd::Closed) => { + return js_promise_rejected(error_value_with_code( + "database is not open", + "ERR_INVALID_STATE", + )) + } + Ok(()) => {} + } + match result { + Ok(total_pages) => { + js_promise_resolved(f64::from_bits(JSValue::number(total_pages as f64).bits())) + } + Err(error) => js_promise_rejected(sqlite_error_value(error)), + } +} diff --git a/crates/perry-stdlib/src/sqlite/dispatch.rs b/crates/perry-stdlib/src/sqlite/dispatch.rs index 98c0b5a4c7..a3286882ec 100644 --- a/crates/perry-stdlib/src/sqlite/dispatch.rs +++ b/crates/perry-stdlib/src/sqlite/dispatch.rs @@ -1,9 +1,10 @@ +//! Handle method/property dispatch for the registry-backed `bun:sqlite` +//! `Database` and `Statement` handles. `node:sqlite` objects are ordinary +//! objects whose methods live on their prototypes. + use super::*; use crate::common::{get_handle, Handle}; -use perry_runtime::{ - closure::ClosureHeader, js_array_alloc, js_array_push_f64, js_nanbox_pointer, - js_string_from_bytes, ArrayHeader, JSValue, -}; +use perry_runtime::{js_array_alloc, js_array_push_f64, js_nanbox_pointer, ArrayHeader, JSValue}; /// `js_class_method_bind` retains the name pointer in the closure, so make a /// non-static forwarded property slice impossible to pass accidentally. @@ -20,7 +21,6 @@ unsafe fn bind_static_handle_method(handle: Handle, method: &'static [u8]) -> f6 fn database_method_name_static(property: &str) -> Option<&'static [u8]> { match property { - "open" => Some(b"open"), "close" => Some(b"close"), "exec" => Some(b"exec"), "prepare" => Some(b"prepare"), @@ -29,95 +29,60 @@ fn database_method_name_static(property: &str) -> Option<&'static [u8]> { "transaction" => Some(b"transaction"), "serialize" => Some(b"serialize"), "deserialize" => Some(b"deserialize"), - "function" => Some(b"function"), - "aggregate" => Some(b"aggregate"), - "enableDefensive" => Some(b"enableDefensive"), - "setAuthorizer" => Some(b"setAuthorizer"), - "createTagStore" => Some(b"createTagStore"), - "createSession" => Some(b"createSession"), - "applyChangeset" => Some(b"applyChangeset"), - "enableLoadExtension" => Some(b"enableLoadExtension"), "loadExtension" => Some(b"loadExtension"), - "location" => Some(b"location"), "__perry_dispose__" => Some(b"__perry_dispose__"), "@@__perry_wk_dispose" => Some(b"@@__perry_wk_dispose"), _ => None, } } -fn tag_store_method_name_static(property: &str) -> Option<&'static [u8]> { +fn statement_method_name_static(property: &str) -> Option<&'static [u8]> { match property { "run" => Some(b"run"), "get" => Some(b"get"), "all" => Some(b"all"), "values" => Some(b"values"), + "iterate" => Some(b"iterate"), + "columns" => Some(b"columns"), "safeIntegers" => Some(b"safeIntegers"), "finalize" => Some(b"finalize"), - "iterate" => Some(b"iterate"), - "clear" => Some(b"clear"), - _ => None, - } -} - -fn session_method_name_static(property: &str) -> Option<&'static [u8]> { - match property { - "changeset" => Some(b"changeset"), - "patchset" => Some(b"patchset"), - "close" => Some(b"close"), - "__perry_dispose__" => Some(b"__perry_dispose__"), - "@@__perry_wk_dispose" => Some(b"@@__perry_wk_dispose"), _ => None, } } -fn statement_method_name_static(property: &str) -> Option<&'static [u8]> { - match property { - "run" => Some(b"run"), - "get" => Some(b"get"), - "all" => Some(b"all"), - "iterate" => Some(b"iterate"), - "columns" => Some(b"columns"), - "setReadBigInts" => Some(b"setReadBigInts"), - "setReturnArrays" => Some(b"setReturnArrays"), - "setAllowBareNamedParameters" => Some(b"setAllowBareNamedParameters"), - "setAllowUnknownNamedParameters" => Some(b"setAllowUnknownNamedParameters"), - _ => None, +pub(crate) unsafe fn packed_args_array(args: &[f64]) -> *mut ArrayHeader { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let arr = scope.root_raw_mut_ptr(js_array_alloc(args.len() as u32)); + for value in args { + let next = js_array_push_f64(arr.get_raw_mut_ptr(), *value); + arr.set_raw_mut_ptr(next); } + arr.get_raw_mut_ptr::() } -pub unsafe fn dispatch_node_sqlite_database_method( +pub unsafe fn dispatch_bun_sqlite_database_method( handle: Handle, method: &str, args: &[f64], ) -> Option { - if js_node_sqlite_is_database_sync_handle(handle) == 0 { + if js_bun_sqlite_is_database_handle(handle) == 0 { return None; } let arg0 = args.first().copied().unwrap_or_else(undefined_f64); - let arg1 = args.get(1).copied().unwrap_or_else(undefined_f64); - let arg2 = args.get(2).copied().unwrap_or_else(undefined_f64); match method { - "open" => { - js_node_sqlite_database_sync_open(handle); - Some(undefined_f64()) - } "close" => { - js_node_sqlite_database_sync_close(handle); + js_bun_sqlite_database_close(handle); Some(undefined_f64()) } "__perry_dispose__" | "@@__perry_wk_dispose" => { - js_node_sqlite_database_sync_dispose(handle); + bun_sqlite_database_dispose(handle); Some(undefined_f64()) } "exec" => { - js_node_sqlite_database_sync_exec(handle, arg0); + bun_sqlite_database_exec(handle, arg0); Some(undefined_f64()) } - "prepare" => { - let stmt = js_node_sqlite_database_sync_prepare(handle, arg0, arg1); - Some(js_nanbox_pointer(stmt)) - } - "query" => Some(js_nanbox_pointer(js_bun_sqlite_database_query( + "prepare" | "query" => Some(js_nanbox_pointer(js_bun_sqlite_database_query( handle, arg0, ))), "run" => { @@ -130,69 +95,33 @@ pub unsafe fn dispatch_node_sqlite_database_method( js_bun_sqlite_database_transaction(handle, arg0) as i64, )), "serialize" => Some(js_nanbox_pointer( - js_node_sqlite_database_sync_serialize(handle, arg0) as i64, + js_bun_sqlite_database_serialize(handle, arg0) as i64, )), "deserialize" => { - js_node_sqlite_database_sync_deserialize(handle, arg0); - Some(undefined_f64()) - } - "function" => { - js_node_sqlite_database_sync_function(handle, arg0, arg1, arg2); - Some(undefined_f64()) - } - "aggregate" => { - js_node_sqlite_database_sync_aggregate(handle, arg0, arg1); - Some(undefined_f64()) - } - "enableDefensive" => { - js_node_sqlite_database_sync_enable_defensive(handle, arg0); - Some(undefined_f64()) - } - "setAuthorizer" => { - js_node_sqlite_database_sync_set_authorizer(handle, arg0); - Some(undefined_f64()) - } - "createTagStore" => { - let store = js_node_sqlite_database_sync_create_tag_store(handle, arg0); - Some(js_nanbox_pointer(store)) - } - "createSession" => { - let session = js_node_sqlite_database_sync_create_session(handle, arg0); - Some(js_nanbox_pointer(session)) - } - "applyChangeset" => Some(js_node_sqlite_database_sync_apply_changeset( - handle, arg0, arg1, - )), - "enableLoadExtension" => { - js_node_sqlite_database_sync_enable_load_extension(handle, arg0); + bun_sqlite_database_deserialize(handle, arg0); Some(undefined_f64()) } "loadExtension" => { - js_node_sqlite_database_sync_load_extension(handle, arg0); + js_bun_sqlite_database_load_extension(handle, arg0); Some(undefined_f64()) } - "location" => Some(js_node_sqlite_database_sync_location(handle, arg0)), _ => None, } } -pub unsafe fn dispatch_node_sqlite_database_property( +pub unsafe fn dispatch_bun_sqlite_database_property( handle: Handle, property_name: &str, ) -> Option { - if js_node_sqlite_is_database_sync_handle(handle) == 0 { + if js_bun_sqlite_is_database_handle(handle) == 0 { return None; } match property_name { "filename" => Some(f64_from_jsvalue(JSValue::string_ptr( js_bun_sqlite_database_filename(handle), ))), - "inTransaction" => Some(js_node_sqlite_database_sync_is_transaction(handle)), - "isOpen" => Some(js_node_sqlite_database_sync_is_open(handle)), - "isTransaction" => Some(js_node_sqlite_database_sync_is_transaction(handle)), - "limits" => Some(js_nanbox_pointer(js_node_sqlite_database_sync_limits( - handle, - ))), + "inTransaction" => Some(bun_sqlite_database_in_transaction(handle)), + "isOpen" => Some(bun_sqlite_database_is_open(handle)), _ => Some(bind_static_handle_method( handle, database_method_name_static(property_name)?, @@ -200,140 +129,22 @@ pub unsafe fn dispatch_node_sqlite_database_property( } } -pub(crate) extern "C" fn sql_tag_store_constructor_thunk( - _closure: *const ClosureHeader, - _this: perry_runtime::closure::JsThis, -) -> f64 { - throw_illegal_constructor() -} - -pub(crate) unsafe fn sql_tag_store_constructor_value() -> f64 { - let closure = perry_runtime::closure::js_closure_alloc_singleton(perry_runtime::fn_info!( - sql_tag_store_constructor_thunk, - 0; - with_declared(0) - )); - if closure.is_null() { - return undefined_f64(); - } - let ptr = js_string_from_bytes(b"SQLTagStore".as_ptr(), "SQLTagStore".len() as u32); - perry_runtime::closure::closure_define_dynamic_prop( - closure as usize, - "name", - f64_from_jsvalue(JSValue::string_ptr(ptr)), - ); - js_nanbox_pointer(closure as i64) -} - -pub unsafe fn dispatch_node_sqlite_tag_store_method( +pub unsafe fn dispatch_bun_sqlite_statement_method( handle: Handle, method: &str, args: &[f64], ) -> Option { - if js_node_sqlite_is_tag_store_handle(handle) == 0 { + if js_bun_sqlite_is_statement_handle(handle) == 0 { return None; } let args_arr = packed_args_array(args); match method { "run" => Some(js_nanbox_pointer( - js_node_sqlite_sql_tag_store_run(handle, args_arr) as i64, + js_bun_sqlite_statement_run(handle, args_arr) as i64, )), - "get" => Some(js_node_sqlite_sql_tag_store_get(handle, args_arr)), + "get" => Some(js_bun_sqlite_statement_get(handle, args_arr)), "all" => Some(js_nanbox_pointer( - js_node_sqlite_sql_tag_store_all(handle, args_arr) as i64, - )), - "iterate" => Some(js_node_sqlite_sql_tag_store_iterate(handle, args_arr)), - "clear" => { - js_node_sqlite_sql_tag_store_clear(handle); - Some(undefined_f64()) - } - _ => None, - } -} - -pub unsafe fn dispatch_node_sqlite_tag_store_property( - handle: Handle, - property_name: &str, -) -> Option { - if js_node_sqlite_is_tag_store_handle(handle) == 0 { - return None; - } - match property_name { - "size" => Some(js_node_sqlite_sql_tag_store_size(handle)), - "capacity" => Some(js_node_sqlite_sql_tag_store_capacity(handle)), - "db" => Some(js_nanbox_pointer(js_node_sqlite_sql_tag_store_db(handle))), - "constructor" => Some(sql_tag_store_constructor_value()), - _ => Some(bind_static_handle_method( - handle, - tag_store_method_name_static(property_name)?, - )), - } -} - -pub unsafe fn dispatch_node_sqlite_session_method( - handle: Handle, - method: &str, - _args: &[f64], -) -> Option { - if js_node_sqlite_is_session_handle(handle) == 0 { - return None; - } - match method { - "changeset" => Some(js_nanbox_pointer( - js_node_sqlite_session_changeset(handle) as i64 - )), - "patchset" => Some(js_nanbox_pointer( - js_node_sqlite_session_patchset(handle) as i64 - )), - "close" => { - js_node_sqlite_session_close(handle); - Some(undefined_f64()) - } - "__perry_dispose__" | "@@__perry_wk_dispose" => { - js_node_sqlite_session_dispose(handle); - Some(undefined_f64()) - } - _ => None, - } -} - -pub unsafe fn dispatch_node_sqlite_session_property( - handle: Handle, - property_name: &str, -) -> Option { - if js_node_sqlite_is_session_handle(handle) == 0 { - return None; - } - Some(bind_static_handle_method( - handle, - session_method_name_static(property_name)?, - )) -} - -pub(crate) unsafe fn packed_args_array(args: &[f64]) -> *mut ArrayHeader { - let mut arr = js_array_alloc(args.len() as u32); - for value in args { - arr = js_array_push_f64(arr, *value); - } - arr -} - -pub unsafe fn dispatch_node_sqlite_statement_method( - handle: Handle, - method: &str, - args: &[f64], -) -> Option { - if js_node_sqlite_is_statement_sync_handle(handle) == 0 { - return None; - } - let args_arr = packed_args_array(args); - match method { - "run" => Some(js_nanbox_pointer( - js_node_sqlite_statement_sync_run(handle, args_arr) as i64, - )), - "get" => Some(js_node_sqlite_statement_sync_get(handle, args_arr)), - "all" => Some(js_nanbox_pointer( - js_node_sqlite_statement_sync_all(handle, args_arr) as i64, + js_bun_sqlite_statement_all(handle, args_arr) as i64, )), "values" => Some(js_nanbox_pointer( js_bun_sqlite_statement_values(handle, args_arr) as i64, @@ -346,55 +157,24 @@ pub unsafe fn dispatch_node_sqlite_statement_method( js_bun_sqlite_statement_finalize(handle); Some(undefined_f64()) } - "iterate" => Some(js_node_sqlite_statement_sync_iterate(handle, args_arr)), + "iterate" => Some(bun_sqlite_statement_iterate(handle, args_arr)), "columns" => Some(js_nanbox_pointer( - js_node_sqlite_statement_sync_columns(handle) as i64, + bun_sqlite_statement_columns(handle) as i64 )), - "setReadBigInts" => { - js_node_sqlite_statement_sync_set_read_bigints( - handle, - args.first().copied().unwrap_or_else(undefined_f64), - ); - Some(undefined_f64()) - } - "setReturnArrays" => { - js_node_sqlite_statement_sync_set_return_arrays( - handle, - args.first().copied().unwrap_or_else(undefined_f64), - ); - Some(undefined_f64()) - } - "setAllowBareNamedParameters" => { - js_node_sqlite_statement_sync_set_allow_bare_named_parameters( - handle, - args.first().copied().unwrap_or_else(undefined_f64), - ); - Some(undefined_f64()) - } - "setAllowUnknownNamedParameters" => { - js_node_sqlite_statement_sync_set_allow_unknown_named_parameters( - handle, - args.first().copied().unwrap_or_else(undefined_f64), - ); - Some(undefined_f64()) - } _ => None, } } -pub unsafe fn dispatch_node_sqlite_statement_property( +pub unsafe fn dispatch_bun_sqlite_statement_property( handle: Handle, property_name: &str, ) -> Option { - if js_node_sqlite_is_statement_sync_handle(handle) == 0 { + if js_bun_sqlite_is_statement_handle(handle) == 0 { return None; } match property_name { - "sourceSQL" => Some(f64_from_jsvalue(JSValue::string_ptr( - js_node_sqlite_statement_sync_source_sql(handle), - ))), - "expandedSQL" => Some(f64_from_jsvalue(JSValue::string_ptr( - js_node_sqlite_statement_sync_expanded_sql(handle), + "source" => Some(f64_from_jsvalue(JSValue::string_ptr( + bun_sqlite_statement_source(handle), ))), _ => Some(bind_static_handle_method( handle, @@ -403,107 +183,14 @@ pub unsafe fn dispatch_node_sqlite_statement_property( } } -pub unsafe fn dispatch_node_sqlite_limits_property( - handle: Handle, - property_name: &str, -) -> Option { - let limits = get_handle::(handle)?; - let (_, limit) = node_sqlite_limit(property_name)?; - Some(with_open_node_connection(limits.db_handle, |conn| { - // rusqlite 0.37: Connection::limit is now fallible. The category was - // already validated by node_sqlite_limit above, so it won't error here. - JSValue::number(conn.limit(limit).unwrap_or(0) as f64) - })) - .map(|value| f64::from_bits(value.bits())) -} - -pub unsafe fn dispatch_node_sqlite_limits_set( - handle: Handle, - property_name: &str, - value: f64, -) -> bool { - let Some(limits) = get_handle::(handle) else { - return false; - }; - let Some((_, limit)) = node_sqlite_limit(property_name) else { - return false; - }; - let new_value = non_negative_i32_value(value_from_f64(value), property_name, true); - with_open_node_connection(limits.db_handle, |conn| { - // limit id is pre-validated by node_sqlite_limit above; deliberately - // discard set_limit's prior-value Result. - let _ = conn.set_limit(limit, new_value); - }); - true -} - -pub unsafe fn dispatch_node_sqlite_own_property_names(handle: Handle) -> Option { - if js_node_sqlite_is_limits_handle(handle) == 0 { - return None; - } - let mut names = js_array_alloc(0); - for name in [ - "length", - "sqlLength", - "column", - "exprDepth", - "compoundSelect", - "vdbeOp", - "functionArg", - "attach", - "likePatternLength", - "variableNumber", - "triggerDepth", - ] { - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - names = js_array_push_f64(names, f64_from_jsvalue(JSValue::string_ptr(key))); - } - Some(js_nanbox_pointer(names as i64)) -} - #[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_database_sync_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } +pub unsafe extern "C" fn js_bun_sqlite_is_database_handle(handle: Handle) -> i32 { + i32::from(get_handle::(handle).is_some()) } #[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_limits_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_statement_sync_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_tag_store_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_is_session_handle(handle: Handle) -> i32 { - if get_handle::(handle).is_some() { - 1 - } else { - 0 - } +pub unsafe extern "C" fn js_bun_sqlite_is_statement_handle(handle: Handle) -> i32 { + i32::from(get_handle::(handle).is_some()) } #[cfg(test)] @@ -534,36 +221,15 @@ mod static_method_name_tests { assert_static_lookup( database_method_name_static, &[ - "open", "close", "exec", "prepare", + "query", + "run", + "transaction", "serialize", "deserialize", - "function", - "aggregate", - "enableDefensive", - "setAuthorizer", - "createTagStore", - "createSession", - "applyChangeset", - "enableLoadExtension", "loadExtension", - "location", - "__perry_dispose__", - "@@__perry_wk_dispose", - ], - ); - assert_static_lookup( - tag_store_method_name_static, - &["run", "get", "all", "iterate", "clear"], - ); - assert_static_lookup( - session_method_name_static, - &[ - "changeset", - "patchset", - "close", "__perry_dispose__", "@@__perry_wk_dispose", ], @@ -574,12 +240,11 @@ mod static_method_name_tests { "run", "get", "all", + "values", "iterate", "columns", - "setReadBigInts", - "setReturnArrays", - "setAllowBareNamedParameters", - "setAllowUnknownNamedParameters", + "safeIntegers", + "finalize", ], ); } diff --git a/crates/perry-stdlib/src/sqlite/node_db.rs b/crates/perry-stdlib/src/sqlite/node_db.rs index c0ba224bd0..7e4be78a2a 100644 --- a/crates/perry-stdlib/src/sqlite/node_db.rs +++ b/crates/perry-stdlib/src/sqlite/node_db.rs @@ -1,177 +1,29 @@ +//! Registry-backed `bun:sqlite` `Database` / `Statement` handles, also used +//! by `Bun.SQL`'s SQLite adapter. `node:sqlite` no longer lives here: its +//! objects own native payloads (`database_sync.rs` and siblings). These +//! handles register no JS callbacks, so nothing here calls into JS from C. + use super::*; use crate::common::{get_handle, register_handle, Handle}; use perry_runtime::{ - buffer::{ - is_any_array_buffer, is_data_view, is_registered_buffer, is_uint8array_buffer, BufferHeader, - }, - js_get_string_pointer_unified, js_nanbox_pointer, js_promise_rejected, js_promise_resolved, - JSValue, Promise, StringHeader, + buffer::{is_any_array_buffer, is_data_view, is_registered_buffer, BufferHeader}, + js_array_alloc, js_array_push, js_get_string_pointer_unified, js_nanbox_pointer, + js_object_alloc_with_shape, js_object_set_field, js_string_from_bytes, ArrayHeader, JSValue, + ObjectHeader, StringHeader, }; use rusqlite::{ffi, Connection}; use std::collections::HashSet; use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_int, c_void}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_call( - _path_value: f64, - _options_value: f64, -) -> Handle { - throw_construct_required() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_native_dispatch( - method_name_ptr: *const u8, - method_name_len: usize, - args_ptr: *const f64, - args_len: usize, - construct: i32, -) -> f64 { - let method_name = if method_name_ptr.is_null() || method_name_len == 0 { - "" - } else { - std::str::from_utf8_unchecked(std::slice::from_raw_parts(method_name_ptr, method_name_len)) - }; - let arg = |index: usize| -> f64 { - if index < args_len && !args_ptr.is_null() { - *args_ptr.add(index) - } else { - undefined_f64() - } - }; - let arg0 = arg(0); - let arg1 = arg(1); - let arg2 = arg(2); - - match (method_name, construct != 0) { - ("DatabaseSync", true) => js_nanbox_pointer(js_node_sqlite_database_sync_new(arg0, arg1)), - ("DatabaseSync", false) => js_nanbox_pointer(js_node_sqlite_database_sync_call(arg0, arg1)), - ("Session", true) => js_nanbox_pointer(js_node_sqlite_session_new(arg0, arg1)), - ("Session", false) => js_nanbox_pointer(js_node_sqlite_session_call(arg0, arg1)), - ("StatementSync", true) => js_nanbox_pointer(js_node_sqlite_statement_sync_new(arg0, arg1)), - ("StatementSync", false) => { - js_nanbox_pointer(js_node_sqlite_statement_sync_call(arg0, arg1)) - } - ("backup", _) => js_nanbox_pointer(js_node_sqlite_backup(arg0, arg1, arg2) as i64), - _ => undefined_f64(), - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_backup( - source_db_value: f64, - path_value: f64, - options_value: f64, -) -> *mut Promise { - let db_handle = database_handle_from_backup_source(source_db_value); - let db = get_handle::(db_handle).unwrap_or_else(|| { - throw_type("The \"sourceDb\" argument must be an instance of DatabaseSync.") - }); - { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if conn.is_none() { - drop(conn); - throw_invalid_state("database is not open"); - } - } - - let path = path_like_from_value(path_value, "path"); - let options = parse_node_sqlite_backup_options(options_value); - let result = { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn.as_ref() else { - drop(conn); - throw_invalid_state("database is not open"); - }; - perform_node_sqlite_backup(conn, &path, &options) - }; - - match result { - Ok(total_pages) => { - js_promise_resolved(f64::from_bits(JSValue::number(total_pages as f64).bits())) - } - Err(error) => js_promise_rejected(sqlite_error_value(error)), - } -} - -/// Validate the `DatabaseSync` `path` argument per Node: a string or `file:` URL, -/// neither of which may contain null -/// bytes — with Node's exact `ERR_INVALID_ARG_TYPE` message (#6561). -pub(crate) unsafe fn node_sqlite_database_path(value: f64) -> String { - const PATH_TYPE_MSG: &str = - "The \"path\" argument must be a string, Uint8Array, or URL without null bytes."; - let js = value_from_f64(value); - let path = if js.is_any_string() { - let ptr = js_get_string_pointer_unified(value) as *const StringHeader; - string_from_header(ptr).unwrap_or_else(|| throw_type(PATH_TYPE_MSG)) - } else if let Some(bytes) = node_sqlite_path_bytes(value) { - if bytes.contains(&0) { - throw_type(PATH_TYPE_MSG); - } - String::from_utf8_lossy(&bytes).into_owned() - } else if js.is_pointer() { - // URL object — accept `file:` URLs, decoding the percent-encoded - // pathname like Node's `fileURLToPath`. - let protocol = string_from_jsvalue(object_field(value, "protocol")).unwrap_or_default(); - let pathname = string_from_jsvalue(object_field(value, "pathname")).unwrap_or_default(); - if protocol != "file:" || pathname.is_empty() { - throw_type(PATH_TYPE_MSG); - } - percent_decode_pathname(&pathname) - } else { - throw_type(PATH_TYPE_MSG); - }; - if path.as_bytes().contains(&0) { - throw_type(PATH_TYPE_MSG); - } - path -} - -unsafe fn node_sqlite_path_bytes(value: f64) -> Option> { - let raw = raw_addr_from_value(value); - if raw < 0x1000 { - return None; - } - if (is_registered_buffer(raw) && is_uint8array_buffer(raw)) - || perry_runtime::typedarray::lookup_typed_array_kind(raw) - == Some(perry_runtime::typedarray::KIND_UINT8) - { - return perry_runtime::buffer::bytes::no_gc(|scope| { - let value = f64::from_bits(JSValue::pointer(raw as *const u8).bits()); - perry_runtime::buffer::bytes::bytes(value, scope) - .ok() - .map(<[u8]>::to_vec) - }); - } - None -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_new( - path_value: f64, - options_value: f64, -) -> Handle { - let path = node_sqlite_database_path(path_value); - let options = parse_node_sqlite_options(options_value); - register_node_sqlite_database(path, options, "node:sqlite") -} - -pub(crate) unsafe fn register_node_sqlite_database( +pub(crate) unsafe fn register_bun_sqlite_database( path: String, options: NodeSqliteOptions, type_name: &str, ) -> Handle { let open = options.open; - let handle = register_handle(NodeSqliteDbHandle { + let handle = register_handle(BunSqliteDbHandle { conn: Mutex::new(None), path, read_only: options.read_only, @@ -187,10 +39,7 @@ pub(crate) unsafe fn register_node_sqlite_database( allow_load_extension: options.allow_extension, enable_load_extension: AtomicBool::new(options.allow_extension), defensive: AtomicBool::new(options.defensive), - authorizer_callback: Mutex::new(None), initial_limits: options.initial_limits, - limits_handle: Mutex::new(None), - sessions: Mutex::new(HashSet::new()), statements: Mutex::new(HashSet::new()), }); let type_symbol = @@ -201,14 +50,13 @@ pub(crate) unsafe fn register_node_sqlite_database( f64_from_jsvalue(string_value(type_name)), ); if open { - js_node_sqlite_database_sync_open(handle); + bun_sqlite_database_open(handle); } handle } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_open(db_handle: Handle) -> i32 { - let db = get_handle::(db_handle) +pub(crate) unsafe fn bun_sqlite_database_open(db_handle: Handle) { + let db = get_handle::(db_handle) .unwrap_or_else(|| throw_invalid_state("database is not open")); { let conn = db @@ -222,21 +70,20 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_open(db_handle: Handle) -> } let opened = match open_node_sqlite_connection(db) { Ok(opened) => opened, - // Carry the SQLite result code through (`errcode`/`errstr`), e.g. - // errcode 14 "unable to open database file" — matching Node (#6561). Err(err) => { perry_runtime::exception::js_throw(sqlite_error_value(sqlite_error_from_rusqlite(err))) } }; - if let Err(err) = configure_node_sqlite_defensive(&opened, db.defensive.load(Ordering::Relaxed)) + if let Err(err) = + configure_node_sqlite_defensive(opened.handle(), db.defensive.load(Ordering::Relaxed)) { throw_sqlite_error(&err); } - if let Err(err) = configure_node_sqlite_dqs(&opened, db.enable_dqs) { + if let Err(err) = configure_node_sqlite_dqs(opened.handle(), db.enable_dqs) { throw_sqlite_error(&err); } if let Err(err) = configure_node_sqlite_load_extension( - &opened, + opened.handle(), db.enable_load_extension.load(Ordering::Relaxed), ) { throw_sqlite_error(&err); @@ -246,12 +93,11 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_open(db_handle: Handle) -> .lock() .unwrap_or_else(|_| throw_invalid_state("database is not open")); *conn = Some(opened); - 1 } #[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_close(db_handle: Handle) -> i32 { - let db = get_handle::(db_handle) +pub unsafe extern "C" fn js_bun_sqlite_database_close(db_handle: Handle) -> i32 { + let db = get_handle::(db_handle) .unwrap_or_else(|| throw_invalid_state("database is not open")); { let conn = db @@ -263,102 +109,46 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_close(db_handle: Handle) - throw_invalid_state("database is not open"); } } - finalize_node_sqlite_statements(db); - delete_node_sqlite_sessions(db); - if let Ok(mut callback) = db.authorizer_callback.lock() { - *callback = None; - } - let mut conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if conn.is_some() { + finalize_bun_sqlite_statements(db); + if let Ok(mut conn) = db.conn.lock() { *conn = None; } 1 } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_dispose(db_handle: Handle) -> i32 { - if let Some(db) = get_handle::(db_handle) { - finalize_node_sqlite_statements(db); - delete_node_sqlite_sessions(db); - if let Ok(mut callback) = db.authorizer_callback.lock() { - *callback = None; - } +pub(crate) unsafe fn bun_sqlite_database_dispose(db_handle: Handle) { + if let Some(db) = get_handle::(db_handle) { + finalize_bun_sqlite_statements(db); if let Ok(mut conn) = db.conn.lock() { *conn = None; } } - 1 } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_is_open(db_handle: Handle) -> f64 { - let is_open = get_handle::(db_handle) +pub(crate) unsafe fn bun_sqlite_database_is_open(db_handle: Handle) -> f64 { + let is_open = get_handle::(db_handle) .and_then(|db| db.conn.lock().ok().map(|conn| conn.is_some())) .unwrap_or(false); bool_f64(is_open) } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_is_transaction(db_handle: Handle) -> f64 { - with_open_node_connection(db_handle, |conn| bool_f64(!conn.is_autocommit())) +pub(crate) unsafe fn bun_sqlite_database_in_transaction(db_handle: Handle) -> f64 { + with_open_bun_connection(db_handle, |conn| bool_f64(!conn.is_autocommit())) } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_exec( - db_handle: Handle, - sql_value: f64, -) -> i32 { - ensure_open_node_database(db_handle); +pub(crate) unsafe fn bun_sqlite_database_exec(db_handle: Handle, sql_value: f64) { + ensure_open_bun_database(db_handle); let sql = string_from_value(sql_value, "sql"); - let result = with_open_node_connection(db_handle, |conn| node_sqlite_exec_batch(conn, &sql)); - match result { - Ok(_) => 1, - Err((message, errcode)) => throw_sqlite_error_ext(&message, errcode), - } -} - -pub(crate) unsafe fn parse_statement_options( - db: &NodeSqliteDbHandle, - options_value: f64, -) -> NodeSqliteStmtOptions { - let js = value_from_f64(options_value); - if js.is_undefined() { - return NodeSqliteStmtOptions { - read_bigints: db.read_bigints, - return_arrays: db.return_arrays, - allow_bare_named_parameters: db.allow_bare_named_parameters, - allow_unknown_named_parameters: db.allow_unknown_named_parameters, - }; - } - if js.is_null() || !is_object_like(options_value) { - throw_type("The \"options\" argument must be an object."); - } - NodeSqliteStmtOptions { - read_bigints: bool_option(options_value, "readBigInts", db.read_bigints), - return_arrays: bool_option(options_value, "returnArrays", db.return_arrays), - allow_bare_named_parameters: bool_option( - options_value, - "allowBareNamedParameters", - db.allow_bare_named_parameters, - ), - allow_unknown_named_parameters: bool_option( - options_value, - "allowUnknownNamedParameters", - db.allow_unknown_named_parameters, - ), + let result = with_open_bun_connection(db_handle, |conn| node_sqlite_exec_batch(conn, &sql)); + if let Err((message, errcode)) = result { + throw_sqlite_error_ext(&message, errcode); } } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_prepare( - db_handle: Handle, - sql_value: f64, - options_value: f64, -) -> Handle { - ensure_open_node_database(db_handle); +/// `db.query(sql)` / `db.prepare(sql)`: a statement handle. The SQL is +/// compiled once here to surface syntax errors; every run re-prepares it. +pub(crate) unsafe fn bun_sqlite_database_prepare(db_handle: Handle, sql_value: f64) -> Handle { + ensure_open_bun_database(db_handle); let sql = { let js = value_from_f64(sql_value); if !js.is_any_string() { @@ -368,32 +158,19 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_prepare( string_from_header(ptr) .unwrap_or_else(|| throw_type("The \"sql\" argument must be of type string")) }; - let db = get_handle::(db_handle) + let db = get_handle::(db_handle) .unwrap_or_else(|| throw_invalid_state("database is not open")); - let options = parse_statement_options(db, options_value); - let expanded_sql = with_open_node_connection(db_handle, |conn| { - let raw = prepare_node_raw_statement(conn, &sql); - let expanded = ffi::sqlite3_expanded_sql(raw.ptr); - let expanded_sql = if expanded.is_null() { - String::new() - } else { - let text = CStr::from_ptr(expanded).to_string_lossy().into_owned(); - ffi::sqlite3_free(expanded.cast::()); - text - }; - drop(raw); - expanded_sql + with_open_bun_connection(db_handle, |conn| { + drop(prepare_node_raw_statement(conn, &sql)); }); - let handle = register_handle(NodeSqliteStmtHandle { + let handle = register_handle(BunSqliteStmtHandle { db_handle, sql, finalized: AtomicBool::new(false), - iteration_epoch: std::sync::atomic::AtomicU64::new(0), - read_bigints: AtomicBool::new(options.read_bigints), - return_arrays: AtomicBool::new(options.return_arrays), - allow_bare_named_parameters: AtomicBool::new(options.allow_bare_named_parameters), - allow_unknown_named_parameters: AtomicBool::new(options.allow_unknown_named_parameters), - expanded_sql: Mutex::new(expanded_sql), + read_bigints: AtomicBool::new(db.read_bigints), + return_arrays: AtomicBool::new(db.return_arrays), + allow_bare_named_parameters: AtomicBool::new(db.allow_bare_named_parameters), + allow_unknown_named_parameters: AtomicBool::new(db.allow_unknown_named_parameters), }); if let Ok(mut statements) = db.statements.lock() { statements.insert(handle); @@ -402,11 +179,11 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_prepare( } #[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_serialize( +pub unsafe extern "C" fn js_bun_sqlite_database_serialize( db_handle: Handle, schema_value: f64, ) -> *mut BufferHeader { - ensure_open_node_database(db_handle); + ensure_open_bun_database(db_handle); let schema = if value_from_f64(schema_value).is_undefined() { "main".to_string() } else { @@ -414,33 +191,37 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_serialize( }; let schema = CString::new(schema) .unwrap_or_else(|_| throw_type("The \"attachedDb\" argument must be a string")); - with_open_node_connection(db_handle, |conn| { - let mut size = 0; - let image = ffi::sqlite3_serialize(conn.handle(), schema.as_ptr(), &mut size, 0); - if image.is_null() || size < 0 { - throw_sqlite_error_from_conn(conn); - } - let len = size as usize; - let buffer = JSValue::from_bits( - perry_runtime::buffer::bytes::from_slice( - perry_runtime::buffer::bytes::Brand::Uint8Array, - std::slice::from_raw_parts(image, len), - ) - .to_bits(), - ) - .as_pointer::() - .cast_mut(); - ffi::sqlite3_free(image.cast()); - buffer + with_open_bun_connection(db_handle, |conn| { + sqlite_serialize_to_buffer(conn.handle(), &schema) }) } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_deserialize( - db_handle: Handle, - image_value: f64, -) -> i32 { - ensure_open_node_database(db_handle); +/// `sqlite3_serialize` of `schema` into a fresh `Uint8Array`. +pub(crate) unsafe fn sqlite_serialize_to_buffer( + db: *mut ffi::sqlite3, + schema: &CStr, +) -> *mut BufferHeader { + let mut size = 0; + let image = ffi::sqlite3_serialize(db, schema.as_ptr(), &mut size, 0); + if image.is_null() || size < 0 { + throw_sqlite_error_from_db(db); + } + let len = size as usize; + let buffer = JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Uint8Array, + std::slice::from_raw_parts(image, len), + ) + .to_bits(), + ) + .as_pointer::() + .cast_mut(); + ffi::sqlite3_free(image.cast()); + buffer +} + +/// The bytes of a `deserialize()` image argument (a Uint8Array or Buffer). +pub(crate) unsafe fn sqlite_image_bytes(image_value: f64) -> Vec { let raw = raw_addr_from_value(image_value); let bytes = if perry_runtime::typedarray::lookup_typed_array_kind(raw) == Some(perry_runtime::typedarray::KIND_UINT8) @@ -462,206 +243,83 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_deserialize( if bytes.is_empty() { throw_arg_value("The \"data\" argument must not be empty."); } - with_open_node_connection(db_handle, |conn| { - let allocation = ffi::sqlite3_malloc64(bytes.len() as u64).cast::(); - if allocation.is_null() { - throw_sqlite_error("out of memory"); - } - std::ptr::copy_nonoverlapping(bytes.as_ptr(), allocation, bytes.len()); - let schema = b"main\0"; - let rc = ffi::sqlite3_deserialize( - conn.handle(), - schema.as_ptr().cast(), - allocation, - bytes.len() as i64, - bytes.len() as i64, - ffi::SQLITE_DESERIALIZE_FREEONCLOSE | ffi::SQLITE_DESERIALIZE_RESIZEABLE, - ); - if rc != ffi::SQLITE_OK { - ffi::sqlite3_free(allocation.cast()); - throw_sqlite_error_from_conn(conn); - } - }); - 1 -} - -pub(crate) fn sqlite_function_name(name: String) -> CString { - let bytes = name.as_bytes(); - let end = bytes - .iter() - .position(|byte| *byte == 0) - .unwrap_or(bytes.len()); - CString::new(&bytes[..end]).unwrap_or_else(|_| CString::new("").unwrap()) + bytes } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_function( - db_handle: Handle, - name_value: f64, - options_or_function_value: f64, - function_value: f64, -) -> i32 { - ensure_open_node_database(db_handle); - let name = sqlite_function_name(string_from_value(name_value, "name")); - - let (options_value, callback) = if closure_ptr_from_value(options_or_function_value).is_some() { - (undefined_f64(), options_or_function_value) - } else { - let options_js = value_from_f64(options_or_function_value); - if options_js.is_undefined() && value_from_f64(function_value).is_undefined() { - node_sqlite_function_arg(options_or_function_value, "function"); - } - if options_js.is_null() - || options_js.is_undefined() - || !is_object_like(options_or_function_value) - { - throw_type("The \"options\" argument must be an object."); - } - ( - options_or_function_value, - node_sqlite_function_arg(function_value, "function"), - ) - }; - - let use_bigint_arguments = - node_sqlite_bool_option_exact(options_value, "useBigIntArguments", false); - let varargs = node_sqlite_bool_option_exact(options_value, "varargs", false); - let deterministic = node_sqlite_bool_option_exact(options_value, "deterministic", false); - let direct_only = node_sqlite_bool_option_exact(options_value, "directOnly", false); - let argc = if varargs { - -1 - } else { - node_sqlite_closure_arity(callback) - }; - - let mut text_rep = ffi::SQLITE_UTF8; - if deterministic { - text_rep |= ffi::SQLITE_DETERMINISTIC; - } - if direct_only { - text_rep |= ffi::SQLITE_DIRECTONLY; +/// Replace `main` with an image (no JS can run inside). +pub(crate) unsafe fn sqlite_deserialize_main(db: *mut ffi::sqlite3, bytes: &[u8]) { + let allocation = ffi::sqlite3_malloc64(bytes.len() as u64).cast::(); + if allocation.is_null() { + throw_sqlite_error("out of memory"); + } + std::ptr::copy_nonoverlapping(bytes.as_ptr(), allocation, bytes.len()); + let schema = b"main\0"; + let rc = ffi::sqlite3_deserialize( + db, + schema.as_ptr().cast(), + allocation, + bytes.len() as i64, + bytes.len() as i64, + ffi::SQLITE_DESERIALIZE_FREEONCLOSE | ffi::SQLITE_DESERIALIZE_RESIZEABLE, + ); + if rc != ffi::SQLITE_OK { + ffi::sqlite3_free(allocation.cast()); + throw_sqlite_error_from_db(db); } +} - perry_runtime::gc::js_write_barrier_root_nanbox(callback.to_bits()); - let info = Box::into_raw(Box::new(NodeSqliteCustomFunction { - callback, - use_bigint_arguments, - })); - register_node_sqlite_custom_function(info); - let rc = with_open_node_connection(db_handle, |conn| { - ffi::sqlite3_create_function_v2( - conn.handle(), - name.as_ptr(), - argc, - text_rep, - info as *mut c_void, - Some(node_sqlite_scalar_callback), - None, - None, - Some(node_sqlite_scalar_destroy), - ) +pub(crate) unsafe fn bun_sqlite_database_deserialize(db_handle: Handle, image_value: f64) { + ensure_open_bun_database(db_handle); + let bytes = sqlite_image_bytes(image_value); + with_open_bun_connection(db_handle, |conn| { + sqlite_deserialize_main(conn.handle(), &bytes) }); - if rc != ffi::SQLITE_OK { - if unregister_node_sqlite_custom_function(info) { - drop(Box::from_raw(info)); - } - with_open_node_connection(db_handle, |conn| throw_sqlite_error_from_conn(conn)) - } - 1 } #[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_aggregate( +pub unsafe extern "C" fn js_bun_sqlite_database_load_extension( db_handle: Handle, - name_value: f64, - options_value: f64, + path_value: f64, ) -> i32 { - ensure_open_node_database(db_handle); - let name = sqlite_function_name(string_from_value(name_value, "name")); - - if value_from_f64(options_value).is_null() || !is_object_like(options_value) { - throw_plain_type("The \"options\" argument must be an object."); - } + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("database is not open")); + ensure_open_bun_database(db_handle); + if !db.allow_load_extension || !db.enable_load_extension.load(Ordering::Relaxed) { + throw_invalid_state("extension loading is not allowed"); + } + let path = string_from_value(path_value, "path"); + let c_path = CString::new(path) + .unwrap_or_else(|_| throw_type("The \"path\" argument must not contain null bytes")); + with_open_bun_connection(db_handle, |conn| { + sqlite_load_extension_or_throw(conn.handle(), &c_path) + }); + 1 +} - let start = object_field(options_value, "start"); - if start.is_undefined() { - throw_type("The \"options.start\" argument must be a function or a primitive value."); +/// `sqlite3_load_extension`, throwing node's `ERR_LOAD_SQLITE_EXTENSION`. +pub(crate) unsafe fn sqlite_load_extension_or_throw(db: *mut ffi::sqlite3, path: &CStr) { + let mut error_message = std::ptr::null_mut(); + let rc = ffi::sqlite3_load_extension(db, path.as_ptr(), std::ptr::null(), &mut error_message); + if rc == ffi::SQLITE_OK { + return; } - let step = node_sqlite_optional_callback_option(options_value, "step", true) - .unwrap_or_else(|| throw_type("The \"options.step\" argument must be a function.")); - let result = node_sqlite_optional_callback_option(options_value, "result", false); - let inverse = node_sqlite_optional_callback_option(options_value, "inverse", true); - let use_bigint_arguments = - node_sqlite_bool_option_exact(options_value, "useBigIntArguments", false); - let varargs = node_sqlite_bool_option_exact(options_value, "varargs", false); - let direct_only = node_sqlite_bool_option_exact(options_value, "directOnly", false); - let argc = if varargs { - -1 + let message = if error_message.is_null() { + sqlite_errmsg_raw(db) } else { - node_sqlite_closure_arity(step).saturating_sub(1) + let message = CStr::from_ptr(error_message).to_string_lossy().into_owned(); + ffi::sqlite3_free(error_message.cast()); + message }; - - let mut text_rep = ffi::SQLITE_UTF8; - if direct_only { - text_rep |= ffi::SQLITE_DIRECTONLY; - } - - let start = f64::from_bits(start.bits()); - perry_runtime::gc::js_write_barrier_root_nanbox(start.to_bits()); - perry_runtime::gc::js_write_barrier_root_nanbox(step.to_bits()); - if let Some(result) = result { - perry_runtime::gc::js_write_barrier_root_nanbox(result.to_bits()); - } - if let Some(inverse) = inverse { - perry_runtime::gc::js_write_barrier_root_nanbox(inverse.to_bits()); - } - let aggregate = Box::into_raw(Box::new(NodeSqliteCustomAggregate { - start, - step, - result, - inverse, - use_bigint_arguments, - })); - register_node_sqlite_custom_aggregate(aggregate); - let has_inverse = inverse.is_some(); - let rc = with_open_node_connection(db_handle, |conn| { - ffi::sqlite3_create_window_function( - conn.handle(), - name.as_ptr(), - argc, - text_rep, - aggregate as *mut c_void, - Some(node_sqlite_aggregate_step), - Some(node_sqlite_aggregate_final), - if has_inverse { - Some(node_sqlite_aggregate_value) - } else { - None - }, - if has_inverse { - Some(node_sqlite_aggregate_inverse) - } else { - None - }, - Some(node_sqlite_aggregate_destroy), - ) - }); - if rc != ffi::SQLITE_OK { - if unregister_node_sqlite_custom_aggregate(aggregate) { - drop(Box::from_raw(aggregate)); - } - with_open_node_connection(db_handle, |conn| throw_sqlite_error_from_conn(conn)) - } - 1 + throw_load_sqlite_extension(&message) } pub(crate) unsafe fn configure_node_sqlite_defensive( - conn: &Connection, + db: *mut ffi::sqlite3, active: bool, ) -> Result<(), String> { let mut current = 0; let rc = ffi::sqlite3_db_config( - conn.handle(), + db, ffi::SQLITE_DBCONFIG_DEFENSIVE, if active { 1 } else { 0 }, &mut current, @@ -669,143 +327,239 @@ pub(crate) unsafe fn configure_node_sqlite_defensive( if rc == ffi::SQLITE_OK { return Ok(()); } - Err(CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned()) + Err(sqlite_errmsg_raw(db)) } pub(crate) unsafe fn configure_node_sqlite_dqs( - conn: &Connection, + db: *mut ffi::sqlite3, enabled: bool, ) -> Result<(), String> { for option in [ffi::SQLITE_DBCONFIG_DQS_DDL, ffi::SQLITE_DBCONFIG_DQS_DML] { let mut current = 0; - let rc = ffi::sqlite3_db_config( - conn.handle(), - option, - if enabled { 1 } else { 0 }, - &mut current, - ); + let rc = ffi::sqlite3_db_config(db, option, if enabled { 1 } else { 0 }, &mut current); if rc != ffi::SQLITE_OK { - return Err(CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned()); + return Err(sqlite_errmsg_raw(db)); } } Ok(()) } -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_enable_defensive( - db_handle: Handle, - active_value: f64, -) -> i32 { - let js = value_from_f64(active_value); - if !js.is_bool() { - throw_type("The \"active\" argument must be a boolean."); - } - let active = js.as_bool(); - ensure_open_node_database(db_handle); - let result = with_open_node_connection(db_handle, |conn| { - configure_node_sqlite_defensive(conn, active) - }); - if let Err(message) = result { - throw_sqlite_error(&message); +pub(crate) unsafe fn configure_node_sqlite_load_extension( + db: *mut ffi::sqlite3, + enable: bool, +) -> Result<(), String> { + let mut current = 0; + let rc = ffi::sqlite3_db_config( + db, + ffi::SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, + if enable { 1 } else { 0 }, + &mut current, + ); + if rc == ffi::SQLITE_OK { + return Ok(()); } - if let Some(db) = get_handle::(db_handle) { - db.defensive.store(active, Ordering::Relaxed); + Err(sqlite_errmsg_raw(db)) +} + +pub(crate) fn sqlite_function_name(name: String) -> CString { + let bytes = name.as_bytes(); + let end = bytes + .iter() + .position(|byte| *byte == 0) + .unwrap_or(bytes.len()); + CString::new(&bytes[..end]).unwrap_or_else(|_| CString::new("").unwrap()) +} + +// ---- bun:sqlite Statement ------------------------------------------------- + +pub(crate) unsafe fn with_bun_sqlite_statement( + stmt_handle: Handle, + params_arr: *const ArrayHeader, + action: F, +) -> R +where + F: FnOnce(&Connection, StmtFlags, *mut ffi::sqlite3_stmt) -> R, +{ + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + if stmt.finalized.load(Ordering::Relaxed) { + throw_invalid_state("statement has been finalized"); + } + let conn_ptr = bun_connection_ptr(stmt.db_handle); + let conn = &*conn_ptr; + let flags = stmt.flags(); + let raw = prepare_node_raw_statement(conn, &stmt.sql); + let raw_ptr = raw.ptr; + if let Err(error) = bind_node_sqlite_params(flags, conn.handle(), raw_ptr, params_arr) { + drop(raw); + error.throw(); } - 1 + let result = action(conn, flags, raw_ptr); + drop(raw); + result } -pub(crate) unsafe extern "C" fn node_sqlite_authorizer_callback( - user_data: *mut c_void, - action_code: c_int, - arg1: *const c_char, - arg2: *const c_char, - db_name: *const c_char, - trigger_or_view: *const c_char, -) -> c_int { - let db_handle = user_data as Handle; - let Some(db) = get_handle::(db_handle) else { - return ffi::SQLITE_OK; - }; - let callback = db - .authorizer_callback +unsafe fn bun_connection_ptr(db_handle: Handle) -> *const Connection { + let db = get_handle::(db_handle) + .unwrap_or_else(|| throw_invalid_state("database is not open")); + let conn_guard = db + .conn .lock() - .ok() - .and_then(|callback| *callback); - let Some(callback) = callback else { - return ffi::SQLITE_OK; - }; - let args = [ - f64_from_jsvalue(JSValue::number(action_code as f64)), - f64_from_jsvalue(sqlite_c_string_value(arg1)), - f64_from_jsvalue(sqlite_c_string_value(arg2)), - f64_from_jsvalue(sqlite_c_string_value(db_name)), - f64_from_jsvalue(sqlite_c_string_value(trigger_or_view)), - ]; - let result = value_from_f64(node_sqlite_call_closure(callback, &args)); - let code = if result.is_int32() { - result.as_int32() - } else if result.is_number() { - let number = result.as_number(); - if !number.is_finite() - || number.fract() != 0.0 - || number < c_int::MIN as f64 - || number > c_int::MAX as f64 - { - throw_plain_type("Authorizer callback must return an integer authorization code"); - } - number as c_int + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + if let Some(conn) = conn_guard.as_ref() { + conn as *const Connection } else { - throw_plain_type("Authorizer callback must return an integer authorization code"); - }; - match code { - ffi::SQLITE_OK | ffi::SQLITE_DENY | ffi::SQLITE_IGNORE => code, - _ => throw_plain_range("Authorizer callback returned a invalid authorization code"), + drop(conn_guard); + throw_invalid_state("database is not open"); } } #[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_set_authorizer( - db_handle: Handle, - callback_value: f64, -) -> i32 { - ensure_open_node_database(db_handle); - ensure_node_sqlite_gc_scanner_registered(); - let js = value_from_f64(callback_value); - let callback = if js.is_null() { - None - } else { - if closure_ptr_from_value(callback_value).is_none() { - throw_type("The \"callback\" argument must be a function or null."); +pub unsafe extern "C" fn js_bun_sqlite_statement_run( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> *mut ObjectHeader { + with_bun_sqlite_statement(stmt_handle, params_arr, |conn, flags, raw_stmt| { + loop { + match ffi::sqlite3_step(raw_stmt) { + ffi::SQLITE_ROW => continue, + ffi::SQLITE_DONE => break, + _ => throw_sqlite_error_from_conn(conn), + } + } + run_result_object(conn.handle(), flags.read_bigints) + }) +} + +/// `{ changes, lastInsertRowid }` after a statement ran to completion. +pub(crate) unsafe fn run_result_object( + db: *mut ffi::sqlite3, + read_bigints: bool, +) -> *mut ObjectHeader { + let changes = ffi::sqlite3_changes64(db); + let last_insert_rowid = ffi::sqlite3_last_insert_rowid(db); + let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; + let (packed_keys, shape_id) = build_packed_keys(&keys); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(js_object_alloc_with_shape( + shape_id, + 2, + packed_keys.as_ptr(), + packed_keys.len() as u32, + )); + let changes_value = scope.root_nanbox_u64( + if read_bigints { + JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(changes)) + } else { + node_sqlite_integer_value(changes, false) } - perry_runtime::gc::js_write_barrier_root_nanbox(callback_value.to_bits()); - Some(callback_value) + .bits(), + ); + let rowid_value = if read_bigints { + JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(last_insert_rowid)) + } else { + node_sqlite_integer_value(last_insert_rowid, false) }; - let rc = with_open_node_connection(db_handle, |conn| { - ffi::sqlite3_set_authorizer( - conn.handle(), - if callback.is_some() { - Some(node_sqlite_authorizer_callback) - } else { - None - }, - if callback.is_some() { - db_handle as *mut c_void - } else { - std::ptr::null_mut() - }, - ) - }); - if rc != ffi::SQLITE_OK { - with_open_node_connection(db_handle, |conn| throw_sqlite_error_from_conn(conn)) - } - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - if let Ok(mut stored) = db.authorizer_callback.lock() { - *stored = callback; + let obj = obj.get_raw_mut_ptr::(); + js_object_set_field(obj, 0, JSValue::from_bits(changes_value.get_nanbox_u64())); + js_object_set_field(obj, 1, rowid_value); + obj +} + +#[no_mangle] +pub unsafe extern "C" fn js_bun_sqlite_statement_get( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> f64 { + with_bun_sqlite_statement(stmt_handle, params_arr, |conn, flags, raw_stmt| { + match ffi::sqlite3_step(raw_stmt) { + ffi::SQLITE_ROW => f64_from_jsvalue(node_sqlite_row_value(flags, raw_stmt)), + ffi::SQLITE_DONE => undefined_f64(), + _ => throw_sqlite_error_from_conn(conn), + } + }) +} + +#[no_mangle] +pub unsafe extern "C" fn js_bun_sqlite_statement_all( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> *mut ArrayHeader { + with_bun_sqlite_statement(stmt_handle, params_arr, |conn, flags, raw_stmt| { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let rows = scope.root_raw_mut_ptr(js_array_alloc(0)); + let row = scope.root_nanbox_u64(JSValue::undefined().bits()); + loop { + match ffi::sqlite3_step(raw_stmt) { + ffi::SQLITE_ROW => { + row.set_nanbox_u64(node_sqlite_row_value(flags, raw_stmt).bits()); + let next = js_array_push( + rows.get_raw_mut_ptr(), + JSValue::from_bits(row.get_nanbox_u64()), + ); + rows.set_raw_mut_ptr(next); + } + ffi::SQLITE_DONE => break, + _ => throw_sqlite_error_from_conn(conn), + } + } + rows.get_raw_mut_ptr::() + }) +} + +pub(crate) unsafe fn bun_sqlite_statement_iterate( + stmt_handle: Handle, + params_arr: *const ArrayHeader, +) -> f64 { + let rows = js_bun_sqlite_statement_all(stmt_handle, params_arr); + perry_runtime::array::array_values_iter(f64_from_jsvalue(JSValue::array_ptr(rows))) +} + +pub(crate) unsafe fn bun_sqlite_statement_columns(stmt_handle: Handle) -> *mut ArrayHeader { + with_bun_sqlite_statement(stmt_handle, std::ptr::null(), |_conn, _flags, raw_stmt| { + sqlite_columns_array(raw_stmt) + }) +} + +/// node's `columns()` descriptor array for a prepared statement. +pub(crate) unsafe fn sqlite_columns_array(raw_stmt: *mut ffi::sqlite3_stmt) -> *mut ArrayHeader { + let column_count = ffi::sqlite3_column_count(raw_stmt); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let result = scope.root_raw_mut_ptr(js_array_alloc(column_count.max(0) as u32)); + let keys = [ + "column".to_string(), + "database".to_string(), + "name".to_string(), + "table".to_string(), + "type".to_string(), + ]; + for index in 0..column_count { + let mut values = Vec::with_capacity(5); + let mut roots = Vec::with_capacity(5); + for ptr in [ + ffi::sqlite3_column_origin_name(raw_stmt, index), + ffi::sqlite3_column_database_name(raw_stmt, index), + ffi::sqlite3_column_name(raw_stmt, index), + ffi::sqlite3_column_table_name(raw_stmt, index), + ffi::sqlite3_column_decltype(raw_stmt, index), + ] { + roots.push(scope.root_nanbox_u64(sqlite_c_string_value(ptr).bits())); + } + for root in &roots { + values.push(JSValue::from_bits(root.get_nanbox_u64())); + } + let obj = make_null_proto_object(&keys, &values); + let next = js_array_push( + result.get_raw_mut_ptr(), + JSValue::object_ptr(obj as *mut u8), + ); + result.set_raw_mut_ptr(next); } - 1 + result.get_raw_mut_ptr::() +} + +pub(crate) unsafe fn bun_sqlite_statement_source(stmt_handle: Handle) -> *mut StringHeader { + let stmt = get_handle::(stmt_handle) + .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); + js_string_from_bytes(stmt.sql.as_ptr(), stmt.sql.len() as u32) } diff --git a/crates/perry-stdlib/src/sqlite/node_stmt_session.rs b/crates/perry-stdlib/src/sqlite/node_stmt_session.rs deleted file mode 100644 index 14aa031349..0000000000 --- a/crates/perry-stdlib/src/sqlite/node_stmt_session.rs +++ /dev/null @@ -1,718 +0,0 @@ -use super::*; -use crate::common::{get_handle, register_handle, Handle}; -use perry_runtime::{ - buffer::{is_any_array_buffer, is_data_view, is_registered_buffer, BufferHeader}, - closure::{js_closure_call1, ClosureHeader}, - js_array_alloc, js_array_push, js_object_alloc_with_shape, js_object_set_field, - js_string_from_bytes, ArrayHeader, JSValue, ObjectHeader, StringHeader, -}; -use rusqlite::ffi; -use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_int, c_void}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Mutex; - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_call(_arg0: f64, _arg1: f64) -> Handle { - throw_illegal_constructor() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_new(_arg0: f64, _arg1: f64) -> Handle { - throw_illegal_constructor() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_call(_arg0: f64, _arg1: f64) -> Handle { - throw_illegal_constructor() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_new(_arg0: f64, _arg1: f64) -> Handle { - throw_illegal_constructor() -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_run( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> *mut ObjectHeader { - with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { - loop { - let rc = ffi::sqlite3_step(raw_stmt); - match rc { - ffi::SQLITE_ROW => continue, - ffi::SQLITE_DONE => break, - _ => throw_sqlite_error_from_conn(conn), - } - } - let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); - let changes = ffi::sqlite3_changes64(conn.handle()); - let last_insert_rowid = ffi::sqlite3_last_insert_rowid(conn.handle()); - let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; - let (packed_keys, shape_id) = build_packed_keys(&keys); - let obj = - js_object_alloc_with_shape(shape_id, 2, packed_keys.as_ptr(), packed_keys.len() as u32); - let changes_value = if read_bigints { - JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(changes)) - } else { - node_sqlite_integer_value(changes, false) - }; - let rowid_value = if read_bigints { - JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(last_insert_rowid)) - } else { - node_sqlite_integer_value(last_insert_rowid, false) - }; - js_object_set_field(obj, 0, changes_value); - js_object_set_field(obj, 1, rowid_value); - obj - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_get( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> f64 { - with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { - match ffi::sqlite3_step(raw_stmt) { - ffi::SQLITE_ROW => f64_from_jsvalue(node_sqlite_row_value(stmt, raw_stmt)), - ffi::SQLITE_DONE => undefined_f64(), - _ => throw_sqlite_error_from_conn(conn), - } - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_all( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> *mut ArrayHeader { - with_node_sqlite_statement(stmt_handle, params_arr, |conn, stmt, raw_stmt| { - let mut rows = js_array_alloc(0); - loop { - match ffi::sqlite3_step(raw_stmt) { - ffi::SQLITE_ROW => { - rows = js_array_push(rows, node_sqlite_row_value(stmt, raw_stmt)); - } - ffi::SQLITE_DONE => break, - _ => throw_sqlite_error_from_conn(conn), - } - } - rows - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_iterate( - stmt_handle: Handle, - params_arr: *const ArrayHeader, -) -> f64 { - // Rows are materialized eagerly (a known divergence from Node's lazy - // stepping — see the module TODO), but the iterator protocol matches - // Node (#6561): exhaustion and `return()` produce - // `{ done: true, value: null }`, and `return()` terminates iteration. - let rows = js_node_sqlite_statement_sync_all(stmt_handle, params_arr); - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - perry_runtime::array::array_values_iter_null_done( - f64_from_jsvalue(JSValue::array_ptr(rows)), - &stmt.iteration_epoch, - stmt.iteration_epoch.load(Ordering::Relaxed), - ) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_columns( - stmt_handle: Handle, -) -> *mut ArrayHeader { - with_node_sqlite_statement(stmt_handle, std::ptr::null(), |_conn, _stmt, raw_stmt| { - let column_count = ffi::sqlite3_column_count(raw_stmt); - let mut result = js_array_alloc(column_count as u32); - let keys = vec![ - "column".to_string(), - "database".to_string(), - "name".to_string(), - "table".to_string(), - "type".to_string(), - ]; - for index in 0..column_count { - let values = vec![ - sqlite_c_string_value(ffi::sqlite3_column_origin_name(raw_stmt, index)), - sqlite_c_string_value(ffi::sqlite3_column_database_name(raw_stmt, index)), - sqlite_c_string_value(ffi::sqlite3_column_name(raw_stmt, index)), - sqlite_c_string_value(ffi::sqlite3_column_table_name(raw_stmt, index)), - sqlite_c_string_value(ffi::sqlite3_column_decltype(raw_stmt, index)), - ]; - let obj = make_null_proto_object(&keys, &values); - result = js_array_push(result, JSValue::object_ptr(obj as *mut u8)); - } - result - }) -} - -pub(crate) unsafe fn set_node_statement_bool_option( - stmt_handle: Handle, - value: f64, - field: &AtomicBool, -) -> i32 { - if get_handle::(stmt_handle) - .map(|stmt| stmt.finalized.load(Ordering::Relaxed)) - .unwrap_or(true) - { - throw_invalid_state("statement has been finalized"); - } - let js = value_from_f64(value); - if !js.is_bool() { - throw_type("The \"enabled\" argument must be a boolean"); - } - field.store(js.as_bool(), Ordering::Relaxed); - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_read_bigints( - stmt_handle: Handle, - value: f64, -) -> i32 { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - set_node_statement_bool_option(stmt_handle, value, &stmt.read_bigints) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_return_arrays( - stmt_handle: Handle, - value: f64, -) -> i32 { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - set_node_statement_bool_option(stmt_handle, value, &stmt.return_arrays) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_allow_bare_named_parameters( - stmt_handle: Handle, - value: f64, -) -> i32 { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - set_node_statement_bool_option(stmt_handle, value, &stmt.allow_bare_named_parameters) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_set_allow_unknown_named_parameters( - stmt_handle: Handle, - value: f64, -) -> i32 { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - set_node_statement_bool_option(stmt_handle, value, &stmt.allow_unknown_named_parameters) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_source_sql( - stmt_handle: Handle, -) -> *mut StringHeader { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - if stmt.finalized.load(Ordering::Relaxed) { - throw_invalid_state("statement has been finalized"); - } - js_string_from_bytes(stmt.sql.as_ptr(), stmt.sql.len() as u32) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_statement_sync_expanded_sql( - stmt_handle: Handle, -) -> *mut StringHeader { - let stmt = get_handle::(stmt_handle) - .unwrap_or_else(|| throw_invalid_state("statement has been finalized")); - if stmt.finalized.load(Ordering::Relaxed) { - throw_invalid_state("statement has been finalized"); - } - let expanded = stmt - .expanded_sql - .lock() - .map(|sql| sql.clone()) - .unwrap_or_default(); - js_string_from_bytes(expanded.as_ptr(), expanded.len() as u32) -} - -pub(crate) unsafe fn changeset_bytes_from_value(value: f64) -> Vec { - let addr = raw_addr_from_value(value); - if addr != 0 - && ((is_registered_buffer(addr) && !is_any_array_buffer(addr) && !is_data_view(addr)) - || perry_runtime::typedarray::lookup_typed_array_kind(addr) - == Some(perry_runtime::typedarray::KIND_UINT8)) - { - if let Some(bytes) = perry_runtime::buffer::bytes::no_gc(|scope| { - let value = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); - perry_runtime::buffer::bytes::bytes(value, scope) - .ok() - .map(<[u8]>::to_vec) - }) { - return bytes; - } - } - throw_type("The \"changeset\" argument must be a Uint8Array."); -} - -pub(crate) unsafe fn sqlite_session_blob( - session_handle: Handle, - make_blob: unsafe extern "C" fn( - *mut ffi::sqlite3_session, - *mut c_int, - *mut *mut c_void, - ) -> c_int, -) -> *mut BufferHeader { - let session_handle = get_handle::(session_handle) - .unwrap_or_else(|| throw_invalid_state("session is not open")); - let db = get_handle::(session_handle.db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn_guard.as_ref() else { - drop(conn_guard); - throw_invalid_state("database is not open"); - }; - let session = session_handle - .session - .lock() - .unwrap_or_else(|_| throw_invalid_state("session is not open")); - let Some(raw_session) = *session else { - drop(session); - drop(conn_guard); - throw_invalid_state("session is not open"); - }; - - let mut len: c_int = 0; - let mut data: *mut c_void = std::ptr::null_mut(); - let rc = make_blob( - raw_session as *mut ffi::sqlite3_session, - &mut len, - &mut data, - ); - if rc != ffi::SQLITE_OK { - let message = sqlite_error_message(conn); - let errcode = ffi::sqlite3_extended_errcode(conn.handle()); - drop(session); - drop(conn_guard); - if !data.is_null() { - ffi::sqlite3_free(data); - } - throw_sqlite_error_ext(&message, errcode); - } - - let len = len.max(0) as usize; - let input = if len > 0 && !data.is_null() { - std::slice::from_raw_parts(data as *const u8, len) - } else { - &[] - }; - let buffer = JSValue::from_bits( - perry_runtime::buffer::bytes::from_slice( - perry_runtime::buffer::bytes::Brand::Uint8Array, - input, - ) - .to_bits(), - ) - .as_pointer::() - .cast_mut(); - if !data.is_null() { - ffi::sqlite3_free(data); - } - buffer -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_changeset( - session_handle: Handle, -) -> *mut BufferHeader { - sqlite_session_blob(session_handle, ffi::sqlite3session_changeset) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_patchset( - session_handle: Handle, -) -> *mut BufferHeader { - sqlite_session_blob(session_handle, ffi::sqlite3session_patchset) -} - -pub(crate) unsafe fn node_sqlite_session_close( - session_handle: Handle, - swallow_errors: bool, -) -> i32 { - let Some(session_handle_ref) = get_handle::(session_handle) else { - if swallow_errors { - return 1; - } - throw_invalid_state("session is not open"); - }; - let Some(db) = get_handle::(session_handle_ref.db_handle) else { - if swallow_errors { - return 1; - } - throw_invalid_state("database is not open"); - }; - { - let conn = match db.conn.lock() { - Ok(conn) => conn, - Err(_) => { - if swallow_errors { - return 1; - } - throw_invalid_state("database is not open"); - } - }; - if conn.is_none() { - if swallow_errors { - return 1; - } - drop(conn); - throw_invalid_state("database is not open"); - } - } - - if let Ok(mut sessions) = db.sessions.lock() { - sessions.remove(&session_handle); - } - let mut session = match session_handle_ref.session.lock() { - Ok(session) => session, - Err(_) => { - if swallow_errors { - return 1; - } - throw_invalid_state("session is not open"); - } - }; - let Some(raw_session) = session.take() else { - if swallow_errors { - return 1; - } - drop(session); - throw_invalid_state("session is not open"); - }; - ffi::sqlite3session_delete(raw_session as *mut ffi::sqlite3_session); - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_close(session_handle: Handle) -> i32 { - node_sqlite_session_close(session_handle, false) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_session_dispose(session_handle: Handle) -> i32 { - node_sqlite_session_close(session_handle, true) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_create_session( - db_handle: Handle, - options_value: f64, -) -> Handle { - validate_optional_object(options_value); - let db_name = string_option(options_value, "db", Some("main")).unwrap_or_else(|| "main".into()); - let table_name = string_option(options_value, "table", None); - ensure_open_node_database(db_handle); - - let db_name_c = CString::new(db_name) - .unwrap_or_else(|_| throw_type("The \"options.db\" argument must not contain null bytes")); - let table_name_c = table_name.as_ref().map(|name| { - CString::new(name.as_str()).unwrap_or_else(|_| { - throw_type("The \"options.table\" argument must not contain null bytes") - }) - }); - - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn_guard.as_ref() else { - drop(conn_guard); - throw_invalid_state("database is not open"); - }; - - let mut raw_session: *mut ffi::sqlite3_session = std::ptr::null_mut(); - let rc = ffi::sqlite3session_create(conn.handle(), db_name_c.as_ptr(), &mut raw_session); - if rc != ffi::SQLITE_OK { - let message = sqlite_error_message(conn); - let errcode = ffi::sqlite3_extended_errcode(conn.handle()); - drop(conn_guard); - throw_sqlite_error_ext(&message, errcode); - } - let table_ptr = table_name_c - .as_ref() - .map(|name| name.as_ptr()) - .unwrap_or(std::ptr::null()); - let rc = ffi::sqlite3session_attach(raw_session, table_ptr); - if rc != ffi::SQLITE_OK { - let message = sqlite_error_message(conn); - let errcode = ffi::sqlite3_extended_errcode(conn.handle()); - ffi::sqlite3session_delete(raw_session); - drop(conn_guard); - throw_sqlite_error_ext(&message, errcode); - } - drop(conn_guard); - - let handle = register_handle(NodeSqliteSessionHandle { - db_handle, - session: Mutex::new(Some(raw_session as usize)), - }); - if let Ok(mut sessions) = db.sessions.lock() { - sessions.insert(handle); - } - handle -} - -pub(crate) struct ChangesetApplyContext { - filter: Option<*const ClosureHeader>, - on_conflict: Option<*const ClosureHeader>, -} - -pub(crate) unsafe extern "C" fn node_sqlite_changeset_filter( - ctx: *mut c_void, - table: *const c_char, -) -> c_int { - let ctx = &mut *(ctx as *mut ChangesetApplyContext); - let Some(filter) = ctx.filter else { - return 1; - }; - let table = if table.is_null() { - "" - } else { - CStr::from_ptr(table).to_str().unwrap_or("") - }; - let table_value = JSValue::string_ptr(js_string_from_bytes(table.as_ptr(), table.len() as u32)); - let result = js_closure_call1( - filter, - perry_runtime::closure::plain_call_receiver(), - f64::from_bits(table_value.bits()), - ); - (perry_runtime::value::js_is_truthy(result) != 0) as c_int -} - -pub(crate) unsafe extern "C" fn node_sqlite_changeset_conflict( - ctx: *mut c_void, - conflict: c_int, - _iter: *mut ffi::sqlite3_changeset_iter, -) -> c_int { - let ctx = &mut *(ctx as *mut ChangesetApplyContext); - let Some(on_conflict) = ctx.on_conflict else { - return ffi::SQLITE_CHANGESET_ABORT; - }; - let result = js_closure_call1( - on_conflict, - perry_runtime::closure::plain_call_receiver(), - f64::from_bits(JSValue::number(conflict as f64).bits()), - ); - let result = value_from_f64(result); - if result.is_int32() { - return result.as_int32() as c_int; - } - if result.is_number() { - let number = result.as_number(); - if number.is_finite() - && number.fract() == 0.0 - && number >= c_int::MIN as f64 - && number <= c_int::MAX as f64 - { - return number as c_int; - } - } - ffi::SQLITE_CHANGESET_ABORT -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_apply_changeset( - db_handle: Handle, - changeset_value: f64, - options_value: f64, -) -> f64 { - ensure_open_node_database(db_handle); - let changeset = changeset_bytes_from_value(changeset_value); - validate_optional_object(options_value); - let filter = function_option(options_value, "filter").and_then(closure_ptr_from_value); - let on_conflict = function_option(options_value, "onConflict").and_then(closure_ptr_from_value); - let mut context = ChangesetApplyContext { - filter, - on_conflict, - }; - - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn_guard.as_ref() else { - drop(conn_guard); - throw_invalid_state("database is not open"); - }; - let rc = ffi::sqlite3changeset_apply( - conn.handle(), - changeset.len() as c_int, - changeset.as_ptr() as *mut c_void, - if context.filter.is_some() { - Some(node_sqlite_changeset_filter) - } else { - None - }, - Some(node_sqlite_changeset_conflict), - &mut context as *mut ChangesetApplyContext as *mut c_void, - ); - match rc { - ffi::SQLITE_OK => bool_f64(true), - ffi::SQLITE_ABORT => bool_f64(false), - _ => { - let message = sqlite_error_message(conn); - let errcode = ffi::sqlite3_extended_errcode(conn.handle()); - drop(conn_guard); - throw_sqlite_error_ext(&message, errcode); - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_enable_load_extension( - db_handle: Handle, - allow_value: f64, -) -> i32 { - let allow = { - let js = value_from_f64(allow_value); - if !js.is_bool() { - throw_type("The \"allow\" argument must be a boolean"); - } - js.as_bool() - }; - - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - if allow && !db.allow_load_extension { - throw_invalid_state( - "Cannot enable extension loading because it was disabled at database creation.", - ); - } - - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let config_error = conn - .as_ref() - .and_then(|conn| configure_node_sqlite_load_extension(conn, allow).err()); - drop(conn); - if let Some(err) = config_error { - throw_sqlite_error(&err); - } - db.enable_load_extension.store(allow, Ordering::Relaxed); - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_load_extension( - db_handle: Handle, - path_value: f64, -) -> i32 { - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - { - let conn = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if conn.is_none() { - drop(conn); - throw_invalid_state("database is not open"); - } - } - - if !db.allow_load_extension || !db.enable_load_extension.load(Ordering::Relaxed) { - throw_invalid_state("extension loading is not allowed"); - } - - let path = string_from_value(path_value, "path"); - let c_path = CString::new(path) - .unwrap_or_else(|_| throw_type("The \"path\" argument must not contain null bytes")); - let conn_guard = db - .conn - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - let Some(conn) = conn_guard.as_ref() else { - drop(conn_guard); - throw_invalid_state("database is not open"); - }; - let mut error_message = std::ptr::null_mut(); - let rc = ffi::sqlite3_load_extension( - conn.handle(), - c_path.as_ptr(), - std::ptr::null(), - &mut error_message, - ); - if rc == ffi::SQLITE_OK { - return 1; - } - - let message = if error_message.is_null() { - CStr::from_ptr(ffi::sqlite3_errmsg(conn.handle())) - .to_string_lossy() - .into_owned() - } else { - let message = CStr::from_ptr(error_message).to_string_lossy().into_owned(); - ffi::sqlite3_free(error_message.cast()); - message - }; - drop(conn_guard); - throw_load_sqlite_extension(&message) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_location( - db_handle: Handle, - db_name_value: f64, -) -> f64 { - ensure_open_node_database(db_handle); - let db_name = if value_from_f64(db_name_value).is_undefined() { - "main".to_string() - } else { - string_from_value(db_name_value, "dbName") - }; - let c_name = CString::new(db_name) - .unwrap_or_else(|_| throw_type("The \"dbName\" argument must not contain null bytes")); - with_open_node_connection(db_handle, |conn| { - let filename = - unsafe { rusqlite::ffi::sqlite3_db_filename(conn.handle(), c_name.as_ptr()) }; - if filename.is_null() { - return null_f64(); - } - let filename = unsafe { CStr::from_ptr(filename) }.to_str().unwrap_or(""); - if filename.is_empty() { - null_f64() - } else { - let ptr = js_string_from_bytes(filename.as_ptr(), filename.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_limits(db_handle: Handle) -> Handle { - ensure_open_node_database(db_handle); - let db = get_handle::(db_handle) - .unwrap_or_else(|| throw_invalid_state("database is not open")); - let mut limits_handle = db - .limits_handle - .lock() - .unwrap_or_else(|_| throw_invalid_state("database is not open")); - if let Some(handle) = *limits_handle { - return handle; - } - let handle = register_handle(NodeSqliteLimitsHandle { db_handle }); - *limits_handle = Some(handle); - handle -} diff --git a/crates/perry-stdlib/src/sqlite/node_tag_store.rs b/crates/perry-stdlib/src/sqlite/node_tag_store.rs deleted file mode 100644 index 28779e2f0a..0000000000 --- a/crates/perry-stdlib/src/sqlite/node_tag_store.rs +++ /dev/null @@ -1,273 +0,0 @@ -use super::*; -use crate::common::{get_handle, register_handle, Handle}; -use perry_runtime::{ - js_array_alloc, js_array_get, js_array_is_array, js_array_length, js_array_push, - js_object_alloc_with_shape, js_object_set_field, js_string_from_bytes, ArrayHeader, JSValue, - ObjectHeader, -}; -use rusqlite::{ffi, Connection}; -use std::sync::atomic::Ordering; -use std::sync::Mutex; - -pub(crate) fn node_sqlite_tag_store_capacity(value: f64) -> usize { - let js = value_from_f64(value); - let number = if js.is_int32() { - js.as_int32() as f64 - } else if js.is_number() { - js.as_number() - } else { - return 1000; - }; - - if !number.is_finite() { - return if number.is_sign_positive() { - i32::MAX as usize - } else { - 0 - }; - } - let truncated = number.trunc(); - if truncated <= 0.0 { - 0 - } else if truncated >= i32::MAX as f64 { - i32::MAX as usize - } else { - truncated as usize - } -} - -pub(crate) unsafe fn node_sqlite_tag_store_template_args( - args_arr: *const ArrayHeader, -) -> (String, Vec) { - let args = node_args_from_array(args_arr); - let strings_value = args.first().copied().unwrap_or_else(undefined_f64); - let is_array = value_from_f64(js_array_is_array(strings_value)); - if !is_array.is_bool() || !is_array.as_bool() { - throw_type("First argument must be an array of strings (template literal)."); - } - - let strings_ptr = raw_addr_from_value(strings_value) as *const ArrayHeader; - if strings_ptr.is_null() { - throw_type("First argument must be an array of strings (template literal)."); - } - - let strings_len = js_array_length(strings_ptr); - let mut sql = String::new(); - for index in 0..strings_len { - let Some(part) = string_key_from_js_value(js_array_get(strings_ptr, index)) else { - throw_type("Template literal parts must be strings."); - }; - sql.push_str(&part); - if index + 1 < strings_len { - sql.push('?'); - } - } - - (sql, args.into_iter().skip(1).collect()) -} - -pub(crate) unsafe fn prepare_node_sqlite_tag_store_statement( - db_handle: Handle, - sql: &str, -) -> Handle { - let sql_ptr = js_string_from_bytes(sql.as_ptr(), sql.len() as u32); - js_node_sqlite_database_sync_prepare( - db_handle, - f64_from_jsvalue(JSValue::string_ptr(sql_ptr)), - undefined_f64(), - ) -} - -pub(crate) unsafe fn node_sqlite_tag_store_statement( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> (Handle, Vec, bool) { - let store = get_handle::(tag_store_handle) - .unwrap_or_else(|| throw_invalid_state("SQLTagStore is not open")); - ensure_open_node_database(store.db_handle); - - let (sql, values) = node_sqlite_tag_store_template_args(args_arr); - if store.capacity == 0 { - let stmt = prepare_node_sqlite_tag_store_statement(store.db_handle, &sql); - return (stmt, values, true); - } - - { - let mut cache = store - .cache - .lock() - .unwrap_or_else(|_| throw_invalid_state("SQLTagStore is not open")); - if let Some(stmt_handle) = cache.get(&sql) { - let finalized = get_handle::(stmt_handle) - .map(|stmt| stmt.finalized.load(Ordering::Relaxed)) - .unwrap_or(true); - if !finalized { - return (stmt_handle, values, false); - } - cache.remove(&sql); - } - } - - let stmt_handle = prepare_node_sqlite_tag_store_statement(store.db_handle, &sql); - let evicted = { - let mut cache = store - .cache - .lock() - .unwrap_or_else(|_| throw_invalid_state("SQLTagStore is not open")); - cache.put(sql, stmt_handle, store.capacity) - }; - for handle in evicted { - if handle != stmt_handle { - finalize_node_sqlite_statement_handle(handle); - } - } - (stmt_handle, values, false) -} - -pub(crate) unsafe fn with_node_sqlite_tag_store_statement( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, - action: F, -) -> R -where - F: FnOnce(&Connection, &NodeSqliteStmtHandle, *mut ffi::sqlite3_stmt) -> R, -{ - let (stmt_handle, values, temporary) = - node_sqlite_tag_store_statement(tag_store_handle, args_arr); - let result = with_node_sqlite_statement_positional(stmt_handle, &values, action); - if temporary { - finalize_node_sqlite_statement_handle(stmt_handle); - } - result -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_database_sync_create_tag_store( - db_handle: Handle, - max_size_value: f64, -) -> Handle { - ensure_open_node_database(db_handle); - register_handle(NodeSqliteTagStoreHandle { - db_handle, - capacity: node_sqlite_tag_store_capacity(max_size_value), - cache: Mutex::new(NodeSqliteTagStoreCache::new()), - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_run( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> *mut ObjectHeader { - with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { - loop { - let rc = ffi::sqlite3_step(raw_stmt); - match rc { - ffi::SQLITE_ROW => continue, - ffi::SQLITE_DONE => break, - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - } - let read_bigints = stmt.read_bigints.load(Ordering::Relaxed); - let changes = ffi::sqlite3_changes64(conn.handle()); - let last_insert_rowid = ffi::sqlite3_last_insert_rowid(conn.handle()); - let keys = vec!["changes".to_string(), "lastInsertRowid".to_string()]; - let (packed_keys, shape_id) = build_packed_keys(&keys); - let obj = - js_object_alloc_with_shape(shape_id, 2, packed_keys.as_ptr(), packed_keys.len() as u32); - let changes_value = if read_bigints { - JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(changes)) - } else { - node_sqlite_integer_value(changes, false) - }; - let rowid_value = if read_bigints { - JSValue::bigint_ptr(perry_runtime::bigint::js_bigint_from_i64(last_insert_rowid)) - } else { - node_sqlite_integer_value(last_insert_rowid, false) - }; - js_object_set_field(obj, 0, changes_value); - js_object_set_field(obj, 1, rowid_value); - obj - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_get( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> f64 { - with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { - match ffi::sqlite3_step(raw_stmt) { - ffi::SQLITE_ROW => f64_from_jsvalue(node_sqlite_row_value(stmt, raw_stmt)), - ffi::SQLITE_DONE => undefined_f64(), - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_all( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> *mut ArrayHeader { - with_node_sqlite_tag_store_statement(tag_store_handle, args_arr, |conn, stmt, raw_stmt| { - let mut rows = js_array_alloc(0); - loop { - match ffi::sqlite3_step(raw_stmt) { - ffi::SQLITE_ROW => { - rows = js_array_push(rows, node_sqlite_row_value(stmt, raw_stmt)); - } - ffi::SQLITE_DONE => break, - _ => throw_sqlite_error(&sqlite_error_message(conn)), - } - } - rows - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_iterate( - tag_store_handle: Handle, - args_arr: *const ArrayHeader, -) -> f64 { - let rows = js_node_sqlite_sql_tag_store_all(tag_store_handle, args_arr); - perry_runtime::array::array_values_iter(f64_from_jsvalue(JSValue::array_ptr(rows))) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_clear(tag_store_handle: Handle) -> i32 { - let store = get_handle::(tag_store_handle) - .unwrap_or_else(|| throw_invalid_state("SQLTagStore is not open")); - let handles = store - .cache - .lock() - .map(|mut cache| cache.clear()) - .unwrap_or_default(); - for handle in handles { - finalize_node_sqlite_statement_handle(handle); - } - 1 -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_size(tag_store_handle: Handle) -> f64 { - let size = get_handle::(tag_store_handle) - .and_then(|store| store.cache.lock().ok().map(|cache| cache.len())) - .unwrap_or(0); - f64_from_jsvalue(JSValue::number(size as f64)) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_capacity(tag_store_handle: Handle) -> f64 { - let capacity = get_handle::(tag_store_handle) - .map(|store| store.capacity) - .unwrap_or(0); - f64_from_jsvalue(JSValue::number(capacity as f64)) -} - -#[no_mangle] -pub unsafe extern "C" fn js_node_sqlite_sql_tag_store_db(tag_store_handle: Handle) -> Handle { - get_handle::(tag_store_handle) - .map(|store| store.db_handle) - .unwrap_or(-1) -} diff --git a/crates/perry-stdlib/src/sqlite/options.rs b/crates/perry-stdlib/src/sqlite/options.rs index a285bb4489..a4b7b4bae4 100644 --- a/crates/perry-stdlib/src/sqlite/options.rs +++ b/crates/perry-stdlib/src/sqlite/options.rs @@ -35,12 +35,6 @@ pub(crate) fn throw_plain_type(message: &str) -> ! { perry_runtime::exception::js_throw(js_nanbox_pointer(err as i64)) } -pub(crate) fn throw_plain_range(message: &str) -> ! { - let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = perry_runtime::error::js_rangeerror_new(msg); - perry_runtime::exception::js_throw(js_nanbox_pointer(err as i64)) -} - pub(crate) fn throw_construct_required() -> ! { perry_runtime::fs::validate::throw_type_error_with_code( "Class constructor DatabaseSync cannot be invoked without 'new'", diff --git a/crates/perry-stdlib/src/sqlite/session.rs b/crates/perry-stdlib/src/sqlite/session.rs new file mode 100644 index 0000000000..31d677c0ba --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/session.rs @@ -0,0 +1,268 @@ +//! `node:sqlite` `Session` (`db.createSession()`) and `db.applyChangeset()`. +//! +//! A session is an ordinary object that owns a native payload (#11919). Its +//! `sqlite3_session` must be deleted before the connection closes, so the +//! database payload and the session payload share one `SessionCell`: whichever +//! goes first (an explicit `close()`, the database's release or a sweep) +//! deletes the C session, and the other finds it gone. `applyChangeset` is a +//! class-C family: its `filter` / `onConflict` live for one call, in stack +//! userdata of runtime handles. + +use super::*; +use perry_runtime::buffer::bytes::{from_slice, Brand}; +use perry_runtime::buffer::{ + is_any_array_buffer, is_data_view, is_registered_buffer, BufferHeader, +}; +use perry_runtime::closure::{ClosureHeader, JsThis}; +use perry_runtime::gc::RuntimeHandleScope; +use perry_runtime::native_class_ids::SQLITE_SESSION; +use perry_runtime::native_payload::{ + self, NativePayloadFamily, OpenSerial, PayloadMiss, PayloadPrototype, +}; +use rusqlite::ffi; +use std::ffi::CString; +use std::os::raw::{c_int, c_void}; + +perry_runtime::birth_memo!(static SESSION_BIRTH); +use std::rc::Rc; + +perry_runtime::state_key_memo!(static MEMO_DB); + +macro_rules! builtin { + ($body:path, $n:tt) => { + perry_runtime::fn_info!($body, $n; with_declared($n), with_flags(perry_runtime::closure::FN_BUILTIN)) + }; +} + +pub(crate) static SESSION_FAMILY: NativePayloadFamily = NativePayloadFamily { + class_id: SQLITE_SESSION, + links_owner: false, + name: "Session", + constructor_export: Some(("sqlite", "Session")), + constructor_length: 0, + install_prototype: install_session_prototype, +}; + +pub(crate) struct NodeSession { + cell: Rc, + serial: OpenSerial, +} + +impl Drop for NodeSession { + fn drop(&mut self) { + self.cell.delete(); + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_session_call(_arg0: f64, _arg1: f64) -> f64 { + throw_illegal_constructor() +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_session_new(_arg0: f64, _arg1: f64) -> f64 { + throw_illegal_constructor() +} + +/// `db.createSession(options)`. +pub(crate) unsafe fn new_session(db: f64, options_value: f64) -> f64 { + let scope = RuntimeHandleScope::new(); + let db = scope.root_nanbox_f64(db); + validate_optional_object(options_value); + let db_name = string_option(options_value, "db", Some("main")).unwrap_or_else(|| "main".into()); + let table_name = string_option(options_value, "table", None); + let raw = db_payload(db.get_nanbox_f64()).raw; + let db_name_c = CString::new(db_name) + .unwrap_or_else(|_| throw_type("The \"options.db\" argument must not contain null bytes")); + let table_name_c = table_name.as_ref().map(|name| { + CString::new(name.as_str()).unwrap_or_else(|_| { + throw_type("The \"options.table\" argument must not contain null bytes") + }) + }); + let mut raw_session: *mut ffi::sqlite3_session = std::ptr::null_mut(); + let rc = ffi::sqlite3session_create(raw, db_name_c.as_ptr(), &mut raw_session); + if rc != ffi::SQLITE_OK { + throw_sqlite_error_from_db(raw); + } + let table_ptr = table_name_c + .as_ref() + .map(|name| name.as_ptr()) + .unwrap_or(std::ptr::null()); + let rc = ffi::sqlite3session_attach(raw_session, table_ptr); + if rc != ffi::SQLITE_OK { + let error = capture_error(raw); + ffi::sqlite3session_delete(raw_session); + throw_captured(error); + } + let cell = SessionCell::new(raw_session); + let serial = { + let open = db_payload(db.get_nanbox_f64()); + open.sessions.retain(|session| !session.raw().is_null()); + open.sessions.push(cell.clone()); + open.serial + }; + native_payload::alloc_with_state( + &SESSION_FAMILY, + NodeSession { cell, serial }, + std::mem::size_of::(), + &[], + &[(b"db", db.get_nanbox_f64())], + &[], + &SESSION_BIRTH, + ) +} + +/// The open C session of `this`: node's "database is not open" first, then +/// "session is not open". +unsafe fn live_session(this: f64) -> *mut ffi::sqlite3_session { + if !native_payload::is_instance(this, &SESSION_FAMILY) { + throw_illegal_invocation(); + } + let db = native_payload::state_get_memo(this, &SESSION_FAMILY, b"db", &MEMO_DB); + let db_serial = match native_payload::payload_mut::(db, &DB_FAMILY) { + Ok(open) => open.serial, + Err(_) => throw_invalid_state("database is not open"), + }; + match native_payload::payload_mut::(this, &SESSION_FAMILY) { + Ok(session) if session.serial == db_serial && !session.cell.raw().is_null() => { + session.cell.raw() + } + Ok(_) | Err(PayloadMiss::Closed) => throw_invalid_state("session is not open"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + } +} + +unsafe fn session_blob( + this: f64, + make_blob: unsafe extern "C" fn( + *mut ffi::sqlite3_session, + *mut c_int, + *mut *mut c_void, + ) -> c_int, +) -> f64 { + let raw_session = live_session(this); + let db = native_payload::state_get_memo(this, &SESSION_FAMILY, b"db", &MEMO_DB); + let raw_db = db_payload(db).raw; + let mut len: c_int = 0; + let mut data: *mut c_void = std::ptr::null_mut(); + let rc = make_blob(raw_session, &mut len, &mut data); + if rc != ffi::SQLITE_OK { + let error = capture_error(raw_db); + if !data.is_null() { + ffi::sqlite3_free(data); + } + throw_captured(error); + } + let len = len.max(0) as usize; + let bytes: &[u8] = if len > 0 && !data.is_null() { + std::slice::from_raw_parts(data as *const u8, len) + } else { + &[] + }; + let buffer = from_slice(Brand::Uint8Array, bytes); + if !data.is_null() { + ffi::sqlite3_free(data); + } + buffer +} + +extern "C" fn session_changeset_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { session_blob(this.as_f64(), ffi::sqlite3session_changeset) } +} + +extern "C" fn session_patchset_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { session_blob(this.as_f64(), ffi::sqlite3session_patchset) } +} + +extern "C" fn session_close_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let this = this.as_f64(); + live_session(this); + native_payload::close(this, &SESSION_FAMILY); + } + undefined_f64() +} + +extern "C" fn session_dispose_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + let this = this.as_f64(); + native_payload::close(this, &SESSION_FAMILY); + undefined_f64() +} + +fn install_session_prototype(proto: &mut PayloadPrototype) { + proto.method("changeset", builtin!(session_changeset_thunk, 0), 0); + proto.method("patchset", builtin!(session_patchset_thunk, 0), 0); + proto.method("close", builtin!(session_close_thunk, 0), 0); + proto.symbol_method( + "dispose", + "[Symbol.dispose]", + builtin!(session_dispose_thunk, 0), + 0, + ); +} + +pub(crate) unsafe fn changeset_bytes_from_value(value: f64) -> Vec { + let addr = raw_addr_from_value(value); + if addr != 0 { + if (is_registered_buffer(addr) && !is_any_array_buffer(addr) && !is_data_view(addr)) + || perry_runtime::typedarray::lookup_typed_array_kind(addr) + == Some(perry_runtime::typedarray::KIND_UINT8) + { + let bytes = perry_runtime::buffer::bytes::no_gc(|scope| { + let value = + f64::from_bits(perry_runtime::JSValue::pointer(addr as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }); + if let Some(bytes) = bytes { + return bytes; + } + } + } + throw_type("The \"changeset\" argument must be a Uint8Array."); +} + +/// `db.applyChangeset(changeset, options)`: one guarded C call whose +/// callbacks are this call's arguments. +pub(crate) unsafe fn apply_changeset(db: f64, changeset_value: f64, options_value: f64) -> f64 { + let scope = RuntimeHandleScope::new(); + let db = scope.root_nanbox_f64(db); + db_payload(db.get_nanbox_f64()); + let changeset = changeset_bytes_from_value(changeset_value); + validate_optional_object(options_value); + let filter = function_option(options_value, "filter").map(|f| scope.root_nanbox_f64(f)); + let on_conflict = + function_option(options_value, "onConflict").map(|f| scope.root_nanbox_f64(f)); + let mut context = ChangesetApplyContext { + owner: &db, + filter: filter.as_ref(), + on_conflict: on_conflict.as_ref(), + }; + let raw = db_payload(db.get_nanbox_f64()).raw; + let has_filter = context.filter.is_some(); + let context_ptr = &mut context as *mut ChangesetApplyContext<'_, '_> as *mut c_void; + let ((rc, error), end) = guarded(db.get_nanbox_f64(), || { + let rc = ffi::sqlite3changeset_apply( + raw, + changeset.len() as c_int, + changeset.as_ptr() as *mut c_void, + if has_filter { + Some(node_sqlite_changeset_filter) + } else { + None + }, + Some(node_sqlite_changeset_conflict), + context_ptr, + ); + let error = (rc != ffi::SQLITE_OK && rc != ffi::SQLITE_ABORT).then(|| capture_error(raw)); + (rc, error) + }); + if let Err(end) = end { + throw_call_end(end); + } + if let Some(error) = error { + throw_captured(error); + } + bool_f64(rc == ffi::SQLITE_OK) +} diff --git a/crates/perry-stdlib/src/sqlite/sqlite_callbacks.rs b/crates/perry-stdlib/src/sqlite/sqlite_callbacks.rs new file mode 100644 index 0000000000..3e2f205cd1 --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/sqlite_callbacks.rs @@ -0,0 +1,624 @@ +//! The C trampolines SQLite calls for a `DatabaseSync`'s authorizer, scalar +//! functions and aggregates (class S), and for `applyChangeset`'s +//! `filter` / `onConflict` (class C). +//! +//! Rules (docs/native-payload-pattern.md, "Families whose native side calls +//! JS"): a trampoline never throws and never calls a throwing helper. It +//! finds its owner with `link_owner` (no JS once the database is closing, +//! closed or finalizing), reads the callback from the owner's `callbacks` +//! array after its last allocation, and calls JS only through +//! `call_from_link` / `call_from_native`, which park a throw on the owner. +//! Validation failures are parked with `set_pending_exception`. The entry +//! that made the C call rethrows after `finish`. + +use super::*; +use perry_runtime::gc::{RuntimeHandle, RuntimeHandleScope}; +use perry_runtime::native_payload::{self, CallbackSite, OwnerLink}; +use perry_runtime::{ + buffer::bytes::{from_slice, Brand}, + js_array_get, js_array_length, js_string_from_bytes, ArrayHeader, JSValue, +}; +use rusqlite::ffi; +use std::ffi::CStr; +use std::os::raw::{c_char, c_int, c_void}; + +perry_runtime::state_key_memo!(static MEMO_AGGSTATES); + +/// The authorizer's index in the `callbacks` array. Functions and aggregates +/// take the following indices; an aggregate takes [`AGG_SLOTS`] of them. +pub(crate) const AUTHORIZER_INDEX: u32 = 0; +pub(crate) const AGG_START: u32 = 0; +pub(crate) const AGG_STEP: u32 = 1; +pub(crate) const AGG_RESULT: u32 = 2; +pub(crate) const AGG_INVERSE: u32 = 3; +pub(crate) const AGG_SLOTS: u32 = 4; +/// Set in a site's index when the registration asked for +/// `useBigIntArguments` (the site's plain data; the array index is below it). +pub(crate) const SITE_BIGINT_ARGS: u32 = 1 << 31; + +/// A userdata site whose owner is open on this thread. +#[derive(Clone, Copy)] +struct Target { + link: OwnerLink, + index: u32, + use_bigints: bool, +} + +impl Target { + /// The site's link and decoded index, unchecked. + #[inline] + fn of(site: &CallbackSite) -> Self { + Target { + link: site.link, + index: site.index & !SITE_BIGINT_ARGS, + use_bigints: site.index & SITE_BIGINT_ARGS != 0, + } + } + + /// The owner now (the GC may have moved it since the last look); + /// `None` once a callback closed the database. + unsafe fn owner(self) -> Option { + native_payload::link_owner(self.link) + } + + /// The callback at `index + offset`, read now. + #[inline] + unsafe fn callback(self, offset: u32) -> f64 { + native_payload::callback_from_link(self.link, self.index + offset) + } + + /// Park `error` on the owner (the first pending exception wins). + unsafe fn park(self, error: f64) { + if let Some(owner) = self.owner() { + let _ = native_payload::set_pending_exception(owner, error); + } + } +} + +/// The site a userdata pointer names; `None` once the database is closing, +/// closed or finalizing, or on a foreign thread. +unsafe fn site_target(user_data: *mut c_void) -> Option { + if user_data.is_null() { + return None; + } + let site = &*(user_data as *const CallbackSite); + native_payload::link_owner(site.link)?; + Some(Target::of(site)) +} + +unsafe fn array_element(array: f64, index: u32) -> f64 { + if !value_from_f64(array).is_pointer() { + return undefined_f64(); + } + let arr = raw_addr_from_value(array) as *const ArrayHeader; + if index >= js_array_length(arr) { + return undefined_f64(); + } + f64_from_jsvalue(js_array_get(arr, index)) +} + +unsafe fn result_error_pending(ctx: *mut ffi::sqlite3_context) { + // The message never reaches JS: the parked exception is what the entry + // throws. + ffi::sqlite3_result_error(ctx, c"JavaScript callback threw".as_ptr(), -1); +} + +fn range_error_value(message: &str) -> f64 { + let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); + perry_runtime::node_submodules::register_error_code_pub(msg, "ERR_OUT_OF_RANGE"); + let err = perry_runtime::error::js_rangeerror_new(msg); + perry_runtime::js_nanbox_pointer(err as i64) +} + +fn plain_type_error_value(message: &str) -> f64 { + let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = perry_runtime::error::js_typeerror_new(msg); + perry_runtime::js_nanbox_pointer(err as i64) +} + +fn plain_range_error_value(message: &str) -> f64 { + let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = perry_runtime::error::js_rangeerror_new(msg); + perry_runtime::js_nanbox_pointer(err as i64) +} + +/// A SQLite integer as a JS value, or node's range error (not thrown). +pub(crate) unsafe fn integer_value_checked(value: i64, read_bigints: bool) -> Result { + if read_bigints { + return Ok(JSValue::bigint_ptr( + perry_runtime::bigint::js_bigint_from_i64(value), + )); + } + if !(JS_SAFE_INTEGER_MIN..=JS_SAFE_INTEGER_MAX).contains(&value) { + return Err(range_error_value(&format!( + "Value is too large to be represented as a JavaScript number: {}", + value + ))); + } + Ok(JSValue::number(value as f64)) +} + +unsafe fn arg_at(argv: *mut *mut ffi::sqlite3_value, index: usize) -> *mut ffi::sqlite3_value { + if argv.is_null() { + std::ptr::null_mut() + } else { + *argv.add(index) + } +} + +/// An argument whose conversion allocates nothing and cannot fail (null, a +/// float, a safe integer read as a number), or `None`. +unsafe fn immediate_arg(value: *mut ffi::sqlite3_value, use_bigints: bool) -> Option { + if value.is_null() { + return Some(f64_from_jsvalue(JSValue::null())); + } + match ffi::sqlite3_value_type(value) { + ffi::SQLITE_NULL => Some(f64_from_jsvalue(JSValue::null())), + ffi::SQLITE_FLOAT => Some(f64_from_jsvalue(JSValue::number( + ffi::sqlite3_value_double(value), + ))), + ffi::SQLITE_INTEGER if !use_bigints => { + let v = ffi::sqlite3_value_int64(value); + (JS_SAFE_INTEGER_MIN..=JS_SAFE_INTEGER_MAX) + .contains(&v) + .then(|| f64_from_jsvalue(JSValue::number(v as f64))) + } + _ => None, + } +} + +/// One `sqlite3_value` argument as a JS value, without throwing. +unsafe fn value_arg_checked( + value: *mut ffi::sqlite3_value, + use_bigints: bool, +) -> Result { + if value.is_null() { + return Ok(JSValue::null()); + } + match ffi::sqlite3_value_type(value) { + ffi::SQLITE_INTEGER => integer_value_checked(ffi::sqlite3_value_int64(value), use_bigints), + ffi::SQLITE_FLOAT => Ok(JSValue::number(ffi::sqlite3_value_double(value))), + ffi::SQLITE_TEXT => { + let ptr = ffi::sqlite3_value_text(value); + if ptr.is_null() { + return Ok(JSValue::null()); + } + let len = ffi::sqlite3_value_bytes(value) as usize; + Ok(JSValue::string_ptr(js_string_from_bytes(ptr, len as u32))) + } + ffi::SQLITE_BLOB => { + let len = ffi::sqlite3_value_bytes(value) as usize; + let ptr = ffi::sqlite3_value_blob(value) as *const u8; + let bytes: &[u8] = if len > 0 && !ptr.is_null() { + std::slice::from_raw_parts(ptr, len) + } else { + &[] + }; + Ok(JSValue::from_bits( + from_slice(Brand::Uint8Array, bytes).to_bits(), + )) + } + _ => Ok(JSValue::null()), + } +} + +/// Call `call` with the callback's arguments (`first` precedes them: an +/// aggregate's running state). When no argument allocates they are passed +/// from the stack with no handle scope; otherwise each is rooted as it is +/// built, and `call` runs after the last allocation (so it must read the +/// callee itself). `Err` carries a conversion error to park. +/// The arguments (after `first`) when none of them allocates: up to eight, +/// from the stack, with no handle scope. +#[inline] +unsafe fn immediate_args( + first: Option, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, + use_bigints: bool, +) -> Option<([f64; 8], usize)> { + let argc = argc.max(0) as usize; + let count = argc + usize::from(first.is_some()); + if count > 8 { + return None; + } + let mut buf = [0f64; 8]; + let k = usize::from(first.is_some()); + if let Some(first) = first { + buf[0] = first; + } + for i in 0..argc { + buf[k + i] = immediate_arg(arg_at(argv, i), use_bigints)?; + } + Some((buf, count)) +} + +unsafe fn with_callback_args( + first: Option, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, + use_bigints: bool, + call: impl FnOnce(&[f64]) -> R, +) -> Result { + if let Some((buf, count)) = immediate_args(first, argc, argv, use_bigints) { + return Ok(call(&buf[..count])); + } + let argc = argc.max(0) as usize; + let count = argc + usize::from(first.is_some()); + let scope = RuntimeHandleScope::new(); + let mut roots: Vec> = Vec::with_capacity(count); + if let Some(first) = first { + roots.push(scope.root_nanbox_f64(first)); + } + for i in 0..argc { + let js = value_arg_checked(arg_at(argv, i), use_bigints)?; + roots.push(scope.root_nanbox_u64(js.bits())); + } + let values: Vec = roots.iter().map(|root| root.get_nanbox_f64()).collect(); + Ok(call(&values)) +} + +pub(crate) unsafe extern "C" fn node_sqlite_scalar_trampoline( + ctx: *mut ffi::sqlite3_context, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, +) { + let user_data = ffi::sqlite3_user_data(ctx); + if user_data.is_null() { + ffi::sqlite3_result_error(ctx, c"database is not open".as_ptr(), -1); + return; + } + let target = Target::of(&*(user_data as *const CallbackSite)); + // Arguments that allocate nothing go straight to the call, which checks + // the owner. Any other conversion waits for an owner that may run JS. + let result = match immediate_args(None, argc, argv, target.use_bigints) { + Some((args, count)) => Ok(native_payload::call_callback( + target.link, + target.index, + &args[..count], + )), + None => { + if target.owner().is_none() { + ffi::sqlite3_result_error(ctx, c"database is not open".as_ptr(), -1); + return; + } + with_callback_args(None, argc, argv, target.use_bigints, |args| { + native_payload::call_from_link(target.link, target.callback(0), args) + }) + } + }; + match result { + Ok(Ok(value)) => node_sqlite_result_value(ctx, value), + Ok(Err(())) => result_error_pending(ctx), + Err(error) => { + target.park(error); + result_error_pending(ctx); + } + } +} + +unsafe fn c_string_arg(scope: &RuntimeHandleScope, ptr: *const c_char) -> f64 { + let value = sqlite_c_string_value(ptr); + scope.root_nanbox_u64(value.bits()).get_nanbox_f64() +} + +pub(crate) unsafe extern "C" fn node_sqlite_authorizer_trampoline( + user_data: *mut c_void, + action_code: c_int, + arg1: *const c_char, + arg2: *const c_char, + db_name: *const c_char, + trigger_or_view: *const c_char, +) -> c_int { + let Some(target) = site_target(user_data) else { + return ffi::SQLITE_DENY; + }; + if !value_from_f64(target.callback(0)).is_pointer() { + return ffi::SQLITE_OK; + } + let scope = RuntimeHandleScope::new(); + let args = [ + scope.root_nanbox_u64(JSValue::number(action_code as f64).bits()), + scope.root_nanbox_f64(c_string_arg(&scope, arg1)), + scope.root_nanbox_f64(c_string_arg(&scope, arg2)), + scope.root_nanbox_f64(c_string_arg(&scope, db_name)), + scope.root_nanbox_f64(c_string_arg(&scope, trigger_or_view)), + ]; + let values = args.map(|arg| arg.get_nanbox_f64()); + let Ok(result) = native_payload::call_from_link(target.link, target.callback(0), &values) + else { + return ffi::SQLITE_DENY; + }; + let result = value_from_f64(result); + let code = if result.is_int32() { + Some(result.as_int32()) + } else if result.is_number() { + let number = result.as_number(); + (number.is_finite() + && number.fract() == 0.0 + && number >= c_int::MIN as f64 + && number <= c_int::MAX as f64) + .then_some(number as c_int) + } else { + None + }; + let Some(code) = code else { + target.park(plain_type_error_value( + "Authorizer callback must return an integer authorization code", + )); + return ffi::SQLITE_DENY; + }; + match code { + ffi::SQLITE_OK | ffi::SQLITE_DENY | ffi::SQLITE_IGNORE => code, + _ => { + target.park(plain_range_error_value( + "Authorizer callback returned a invalid authorization code", + )); + ffi::SQLITE_DENY + } + } +} + +// ---- aggregates ------------------------------------------------------------ +// +// The running accumulator of each aggregate group is a JS value, so it lives +// in the owner's `aggStates` JS-state array; SQLite's aggregate context holds +// only `slot + 1` (0 = the group has not started). + +unsafe fn agg_states(owner: f64) -> f64 { + native_payload::state_get_memo(owner, &DB_FAMILY, b"aggStates", &MEMO_AGGSTATES) +} + +unsafe fn agg_get(target: Target, slot: u32) -> f64 { + match target.owner() { + Some(owner) => array_element(agg_states(owner), slot), + None => undefined_f64(), + } +} + +unsafe fn agg_set(target: Target, slot: u32, value: f64) { + let Some(owner) = target.owner() else { + return; + }; + let states = agg_states(owner); + if value_from_f64(states).is_pointer() { + let arr = raw_addr_from_value(states) as *mut ArrayHeader; + if slot < js_array_length(arr) { + // In range: a plain barriered store, nothing allocates. + perry_runtime::array::js_array_set_f64(arr, slot, value); + return; + } + } + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); + let owner = scope.root_nanbox_f64(owner); + let mut states = agg_states(owner.get_nanbox_f64()); + if !value_from_f64(states).is_pointer() { + let fresh = perry_runtime::js_nanbox_pointer(perry_runtime::js_array_alloc(0) as i64); + native_payload::state_set_memo( + owner.get_nanbox_f64(), + &DB_FAMILY, + b"aggStates", + fresh, + &MEMO_AGGSTATES, + ); + states = agg_states(owner.get_nanbox_f64()); + } + let arr = raw_addr_from_value(states) as *mut ArrayHeader; + let updated = perry_runtime::array::js_array_set_f64_extend(arr, slot, value.get_nanbox_f64()); + if updated != arr { + native_payload::state_set_memo( + owner.get_nanbox_f64(), + &DB_FAMILY, + b"aggStates", + perry_runtime::js_nanbox_pointer(updated as i64), + &MEMO_AGGSTATES, + ); + } +} + +/// The group's state slot, starting the group (calling `start`) on first +/// use. `Err` when an exception is pending, the database closed, or SQLite is +/// out of memory. +unsafe fn agg_slot(ctx: *mut ffi::sqlite3_context, target: Target) -> Result { + let context = + ffi::sqlite3_aggregate_context(ctx, std::mem::size_of::() as c_int) as *mut u32; + if context.is_null() { + ffi::sqlite3_result_error_nomem(ctx); + return Err(()); + } + if *context != 0 { + return Ok(*context - 1); + } + let start = target.callback(AGG_START); + let initial = if closure_ptr_from_value(start).is_some() { + native_payload::call_from_link(target.link, start, &[])? + } else { + start + }; + let scope = RuntimeHandleScope::new(); + let initial = scope.root_nanbox_f64(initial); + let owner = target.owner().ok_or(())?; + let slot = { + let Ok(db) = native_payload::payload_mut::(owner, &DB_FAMILY) else { + return Err(()); + }; + match db.agg_free.pop() { + Some(slot) => slot, + None => { + db.agg_len += 1; + db.agg_len - 1 + } + } + }; + agg_set(target, slot, initial.get_nanbox_f64()); + *context = slot + 1; + Ok(slot) +} + +unsafe fn agg_release(ctx: *mut ffi::sqlite3_context, target: Target, slot: u32) { + agg_set(target, slot, undefined_f64()); + if let Some(owner) = target.owner() { + if let Ok(db) = native_payload::payload_mut::(owner, &DB_FAMILY) { + db.agg_free.push(slot); + } + } + let context = ffi::sqlite3_aggregate_context(ctx, 0) as *mut u32; + if !context.is_null() { + *context = 0; + } +} + +unsafe fn aggregate_apply( + ctx: *mut ffi::sqlite3_context, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, + which: u32, +) { + let Some(target) = site_target(ffi::sqlite3_user_data(ctx)) else { + ffi::sqlite3_result_error(ctx, c"database is not open".as_ptr(), -1); + return; + }; + let Ok(slot) = agg_slot(ctx, target) else { + result_error_pending(ctx); + return; + }; + let state = agg_get(target, slot); + let result = with_callback_args(Some(state), argc, argv, target.use_bigints, |args| { + native_payload::call_from_link(target.link, target.callback(which), args) + }); + match result { + Ok(Ok(next)) => agg_set(target, slot, next), + Ok(Err(())) => result_error_pending(ctx), + Err(error) => { + target.park(error); + result_error_pending(ctx); + } + } +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_step( + ctx: *mut ffi::sqlite3_context, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, +) { + aggregate_apply(ctx, argc, argv, AGG_STEP); +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_inverse( + ctx: *mut ffi::sqlite3_context, + argc: c_int, + argv: *mut *mut ffi::sqlite3_value, +) { + aggregate_apply(ctx, argc, argv, AGG_INVERSE); +} + +unsafe fn aggregate_emit(ctx: *mut ffi::sqlite3_context, finalize: bool) { + // xFinal also fires while the connection is being released (close, a + // sweep or worker teardown): no JS then, and the payload drop discards + // every state. + let Some(target) = site_target(ffi::sqlite3_user_data(ctx)) else { + ffi::sqlite3_result_null(ctx); + return; + }; + let Ok(slot) = agg_slot(ctx, target) else { + result_error_pending(ctx); + return; + }; + let has_inverse = closure_ptr_from_value(target.callback(AGG_INVERSE)).is_some(); + let result_fn = target.callback(AGG_RESULT); + let state = agg_get(target, slot); + let value = if finalize && has_inverse { + Ok(state) + } else if closure_ptr_from_value(result_fn).is_some() { + native_payload::call_from_link(target.link, result_fn, &[state]) + } else { + Ok(state) + }; + match value { + Ok(value) => node_sqlite_result_value(ctx, value), + Err(()) => result_error_pending(ctx), + } + if finalize { + agg_release(ctx, target, slot); + } +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_final(ctx: *mut ffi::sqlite3_context) { + aggregate_emit(ctx, true); +} + +pub(crate) unsafe extern "C" fn node_sqlite_aggregate_value(ctx: *mut ffi::sqlite3_context) { + aggregate_emit(ctx, false); +} + +// ---- applyChangeset (class C) -------------------------------------------------- + +/// Per-call userdata of `applyChangeset`: the callbacks live for this call +/// only and are runtime handles of the entry's scope. +pub(crate) struct ChangesetApplyContext<'a, 's> { + pub(crate) owner: &'a RuntimeHandle<'s>, + pub(crate) filter: Option<&'a RuntimeHandle<'s>>, + pub(crate) on_conflict: Option<&'a RuntimeHandle<'s>>, +} + +pub(crate) unsafe extern "C" fn node_sqlite_changeset_filter( + ctx: *mut c_void, + table: *const c_char, +) -> c_int { + let ctx = &*(ctx as *const ChangesetApplyContext<'_, '_>); + let Some(filter) = ctx.filter else { + return 1; + }; + let scope = RuntimeHandleScope::new(); + let table = if table.is_null() { + "" + } else { + CStr::from_ptr(table).to_str().unwrap_or("") + }; + let table = scope.root_nanbox_u64( + JSValue::string_ptr(js_string_from_bytes(table.as_ptr(), table.len() as u32)).bits(), + ); + match native_payload::call_from_native( + ctx.owner.get_nanbox_f64(), + filter.get_nanbox_f64(), + undefined_f64(), + &[table.get_nanbox_f64()], + ) { + Ok(result) => (perry_runtime::value::js_is_truthy(result) != 0) as c_int, + Err(()) => 0, + } +} + +pub(crate) unsafe extern "C" fn node_sqlite_changeset_conflict( + ctx: *mut c_void, + conflict: c_int, + _iter: *mut ffi::sqlite3_changeset_iter, +) -> c_int { + let ctx = &*(ctx as *const ChangesetApplyContext<'_, '_>); + let Some(on_conflict) = ctx.on_conflict else { + return ffi::SQLITE_CHANGESET_ABORT; + }; + let Ok(result) = native_payload::call_from_native( + ctx.owner.get_nanbox_f64(), + on_conflict.get_nanbox_f64(), + undefined_f64(), + &[f64_from_jsvalue(JSValue::number(conflict as f64))], + ) else { + return ffi::SQLITE_CHANGESET_ABORT; + }; + let result = value_from_f64(result); + if result.is_int32() { + return result.as_int32() as c_int; + } + if result.is_number() { + let number = result.as_number(); + if number.is_finite() + && number.fract() == 0.0 + && number >= c_int::MIN as f64 + && number <= c_int::MAX as f64 + { + return number as c_int; + } + } + ffi::SQLITE_CHANGESET_ABORT +} diff --git a/crates/perry-stdlib/src/sqlite/statement_sync.rs b/crates/perry-stdlib/src/sqlite/statement_sync.rs new file mode 100644 index 0000000000..2828a420cb --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/statement_sync.rs @@ -0,0 +1,762 @@ +//! `node:sqlite` `StatementSync` and `StatementSyncIterator`: ordinary +//! objects that own a native payload (#11919). +//! +//! A statement owns the `sqlite3_stmt` its `prepare()` compiled, as node's +//! does: every run resets and rebinds it, so the authorizer runs once per +//! `prepare()` and `expandedSQL` reads the last bindings. The connection +//! finalizes it on close (its `live` token then reads false and the +//! statement's drop leaves it alone); otherwise the statement's drop +//! finalizes it. A run that re-enters a statement already stepping (a +//! callback calling the same statement) compiles its own copy for that run. +//! It enters C through its database's owner (held in its JS state) and +//! carries the database's `OpenSerial`, so a statement of an earlier open +//! reports "statement has been finalized" after `close()` / `open()`. + +use super::*; +use perry_runtime::closure::{ClosureHeader, JsThis}; +use perry_runtime::gc::{RuntimeHandle, RuntimeHandleScope}; +use perry_runtime::native_class_ids::{SQLITE_STATEMENT_ITERATOR, SQLITE_STATEMENT_SYNC}; +use perry_runtime::native_payload::{ + self, NativePayloadFamily, OpenSerial, PayloadMiss, PayloadPrototype, +}; +use perry_runtime::{ + buffer::bytes::{from_slice, Brand}, + js_array_alloc, js_array_get, js_array_length, js_array_push, js_nanbox_pointer, + js_object_alloc_null_proto, js_object_set_field, js_string_from_bytes, ArrayHeader, JSValue, + ObjectHeader, +}; +use rusqlite::ffi; +use std::cell::Cell; +use std::ffi::{CStr, CString}; +use std::os::raw::c_int; +use std::rc::Rc; + +perry_runtime::state_key_memo!(static MEMO_DB); +perry_runtime::state_key_memo!(static MEMO_STMT); +perry_runtime::state_key_memo!(static MEMO_ROWS); +perry_runtime::birth_memo!(static STMT_BIRTH); +perry_runtime::birth_memo!(static ITER_BIRTH); + +/// node's own enumerable, non-configurable getter `name` (no setter). +pub(crate) fn own_getter( + name: &'static str, + get: *const perry_runtime::closure::JsFunctionInfo, +) -> native_payload::OwnAccessor { + native_payload::OwnAccessor { + name, + get, + set: None, + enumerable: true, + configurable: false, + } +} + +macro_rules! builtin { + ($body:path, $n:tt) => { + perry_runtime::fn_info!($body, $n; with_declared($n), with_flags(perry_runtime::closure::FN_BUILTIN)) + }; +} + +macro_rules! builtin_rest { + ($body:path) => { + perry_runtime::fn_info!( + $body, 1; + with_rest(0), + with_declared(0), + with_flags(perry_runtime::closure::FN_BUILTIN) + ) + }; +} + +pub(crate) static STMT_FAMILY: NativePayloadFamily = NativePayloadFamily { + class_id: SQLITE_STATEMENT_SYNC, + links_owner: false, + name: "StatementSync", + constructor_export: Some(("sqlite", "StatementSync")), + constructor_length: 0, + install_prototype: install_stmt_prototype, +}; + +pub(crate) static ITER_FAMILY: NativePayloadFamily = NativePayloadFamily { + class_id: SQLITE_STATEMENT_ITERATOR, + links_owner: false, + name: "StatementSyncIterator", + constructor_export: None, + constructor_length: 0, + install_prototype: install_iter_prototype, +}; + +pub(crate) struct NodeStmt { + serial: OpenSerial, + sql: CString, + /// The compiled statement (null for SQL with no statement). Reset + /// whenever no run is stepping it, so finalizing it calls nothing back. + raw: *mut ffi::sqlite3_stmt, + /// The open's liveness token (see `NodeDb::live`). + conn: Rc>, + /// A run is using `raw`. + running: bool, + flags: StmtFlags, + /// Bumped by every run; an iterator is valid while it matches. + epoch: u64, +} + +impl Drop for NodeStmt { + fn drop(&mut self) { + // Always reset here (see `raw`): no callback can run. + if self.conn.get() && !self.raw.is_null() { + unsafe { ffi::sqlite3_finalize(self.raw) }; + } + } +} + +pub(crate) struct NodeStmtIter { + epoch: u64, + index: u32, + done: bool, +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_call(_arg0: f64, _arg1: f64) -> f64 { + throw_illegal_constructor() +} + +#[no_mangle] +pub unsafe extern "C" fn js_node_sqlite_statement_sync_new(_arg0: f64, _arg1: f64) -> f64 { + throw_illegal_constructor() +} + +/// A `StatementSync` of the database `db` (open #`serial`) owning the +/// compiled `raw`. +pub(crate) unsafe fn new_statement( + db: &RuntimeHandle<'_>, + serial: OpenSerial, + conn: Rc>, + sql: CString, + raw: *mut ffi::sqlite3_stmt, + flags: StmtFlags, +) -> f64 { + let compiled = if raw.is_null() { + 0 + } else { + ffi::sqlite3_stmt_status(raw, ffi::SQLITE_STMTSTATUS_MEMUSED, 0).max(0) as usize + }; + let bytes = std::mem::size_of::() + sql.as_bytes().len() + compiled; + let payload = NodeStmt { + serial, + sql, + raw, + conn, + running: false, + flags, + epoch: 0, + }; + native_payload::alloc_with_state( + &STMT_FAMILY, + payload, + bytes, + &[], + &[(b"db", db.get_nanbox_f64())], + &[ + own_getter("sourceSQL", builtin!(stmt_source_sql_getter, 0)), + own_getter("expandedSQL", builtin!(stmt_expanded_sql_getter, 0)), + ], + &STMT_BIRTH, + ) +} + +/// `this`'s payload, or node's "finalized" error when its database is +/// closed or was reopened since it was prepared. +unsafe fn live_stmt<'a>(this: f64) -> (&'a mut NodeStmt, f64) { + let stmt = match native_payload::payload_mut::(this, &STMT_FAMILY) { + Ok(stmt) => stmt, + Err(PayloadMiss::Closed) => throw_invalid_state("statement has been finalized"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + }; + let db = native_payload::state_get_memo(this, &STMT_FAMILY, b"db", &MEMO_DB); + match native_payload::payload_mut::(db, &DB_FAMILY) { + Ok(open) if open.serial == stmt.serial => (stmt, db), + _ => throw_invalid_state("statement has been finalized"), + } +} + +// ---- execution --------------------------------------------------------------- + +/// One compiled statement being stepped on its database. Every step runs in +/// its own guard; conversion happens outside it; any error finalizes the +/// statement before the throw. +pub(crate) struct Stepper<'a, 's> { + db: &'a RuntimeHandle<'s>, + raw_db: *mut ffi::sqlite3, + link: perry_runtime::native_payload::OwnerLink, + stmt: *mut ffi::sqlite3_stmt, + /// The statement whose compiled `stmt` this run uses: the run resets it + /// where a compiled-for-this-run statement is finalized. Null for the + /// latter. Stable: the caller roots the statement for the whole run. + owned: *mut NodeStmt, + pub(crate) flags: StmtFlags, +} + +impl<'a, 's> Stepper<'a, 's> { + /// Compile `sql` (guarded: the authorizer runs) and bind with `bind`. + /// A bind error finalizes the statement before it is thrown; when + /// `bind_runs_js` (a named-parameter object, whose getters are user + /// code) a JS throw from inside `bind` is caught for the same cleanup. + pub(crate) unsafe fn start( + db: &'a RuntimeHandle<'s>, + sql: &CStr, + flags: StmtFlags, + bind_runs_js: bool, + bind: impl FnOnce(*mut ffi::sqlite3, *mut ffi::sqlite3_stmt) -> Result<(), BindError>, + ) -> Self { + let raw_db = db_payload(db.get_nanbox_f64()).raw; + let (stmt, link) = prepare_guarded_with_link(db, raw_db, sql); + if !stmt.is_null() { + let bound = if bind_runs_js { + match perry_runtime::exception::catch_js_throw(|| bind(raw_db, stmt)) { + Ok(bound) => bound, + Err(error) => { + let scope = RuntimeHandleScope::new(); + let error = scope.root_nanbox_f64(error); + // Never stepped: finalizing calls nothing back. + ffi::sqlite3_finalize(stmt); + perry_runtime::exception::js_throw(error.get_nanbox_f64()); + } + } + } else { + bind(raw_db, stmt) + }; + if let Err(error) = bound { + ffi::sqlite3_finalize(stmt); + error.throw(); + } + } + Stepper { + db, + raw_db, + link, + stmt, + owned: std::ptr::null_mut(), + flags, + } + } + + /// Run `owner`'s compiled statement: clear and bind it (as + /// [`Stepper::start`] binds). It is reset, never finalized, when the + /// run ends. + unsafe fn start_owned( + db: &'a RuntimeHandle<'s>, + owner: *mut NodeStmt, + flags: StmtFlags, + bind_runs_js: bool, + bind: impl FnOnce(*mut ffi::sqlite3, *mut ffi::sqlite3_stmt) -> Result<(), BindError>, + ) -> Self { + let raw_db = db_payload(db.get_nanbox_f64()).raw; + let link = match native_payload::owner_link(db.get_nanbox_f64(), &DB_FAMILY) { + Ok(link) => link, + Err(PayloadMiss::Closed) => throw_invalid_state("database is not open"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + }; + let stmt = (*owner).raw; + (*owner).running = true; + // Every run binds from scratch. Clearing calls nothing back. + ffi::sqlite3_clear_bindings(stmt); + let bound = if bind_runs_js { + match perry_runtime::exception::catch_js_throw(|| bind(raw_db, stmt)) { + Ok(bound) => bound, + Err(error) => { + (*owner).running = false; + perry_runtime::exception::js_throw(error); + } + } + } else { + bind(raw_db, stmt) + }; + if let Err(error) = bound { + (*owner).running = false; + error.throw(); + } + Stepper { + db, + raw_db, + link, + stmt, + owned: owner, + flags, + } + } + + pub(crate) fn raw_db(&self) -> *mut ffi::sqlite3 { + self.raw_db + } + + /// Advance one row. `false` at the end (the statement is then reset). + pub(crate) unsafe fn step(&mut self) -> bool { + let (stmt, raw_db) = (self.stmt, self.raw_db); + let guard = native_payload::enter_link(self.link) + .unwrap_or_else(|_| throw_invalid_state("database is not open")); + let ((rc, error), end) = guard.call(|| { + let rc = ffi::sqlite3_step(stmt); + let error = + (rc != ffi::SQLITE_ROW && rc != ffi::SQLITE_DONE).then(|| capture_error(raw_db)); + if rc != ffi::SQLITE_ROW { + // Aggregates still open finish here, inside the guard, so a + // pending exception keeps them from running JS. + ffi::sqlite3_reset(stmt); + } + (rc, error) + }); + if let Err(end) = end { + self.forget_or_finalize(); + throw_call_end(end); + } + if let Some(error) = error { + self.forget_or_finalize(); + throw_captured(error); + } + rc == ffi::SQLITE_ROW + } + + /// The current row as a JS value; on a conversion error the statement + /// is finalized (guarded) before the throw. + pub(crate) unsafe fn row(&mut self) -> JSValue { + match row_value_checked(self.flags, self.stmt, self.flags.return_arrays) { + Ok(row) => row, + Err(error) => { + let scope = RuntimeHandleScope::new(); + let error = scope.root_nanbox_f64(error); + self.abandon(); + perry_runtime::exception::js_throw(error.get_nanbox_f64()) + } + } + } + + /// Stop early (after `get()`): finalize under a guard, since aggregates + /// still open run their `result` callbacks; rethrow what they threw. + pub(crate) unsafe fn finish_early(mut self) { + let end = self.finalize_guarded(); + if let Err(end) = end { + throw_call_end(end); + } + } + + /// Finish after `step()` returned `false`. + pub(crate) unsafe fn finish(mut self) { + self.forget_or_finalize(); + } + + unsafe fn finalize_guarded(&mut self) -> Result<(), perry_runtime::native_payload::CallEnd> { + let stmt = std::mem::replace(&mut self.stmt, std::ptr::null_mut()); + let owned = self.release_owned(); + if stmt.is_null() || !db_is_open(self.db.get_nanbox_f64()) { + return Ok(()); + } + // Aggregates still open run their `result` callbacks either way. + let (_, end) = guarded(self.db.get_nanbox_f64(), || { + if owned { + ffi::sqlite3_reset(stmt) + } else { + ffi::sqlite3_finalize(stmt) + } + }); + end + } + + /// End the use of an owned statement; true when this run had one. + unsafe fn release_owned(&mut self) -> bool { + let owned = std::mem::replace(&mut self.owned, std::ptr::null_mut()); + if owned.is_null() { + return false; + } + (*owned).running = false; + true + } + + /// Finalize, dropping whatever the callbacks threw (an earlier error + /// is already on its way out). + unsafe fn abandon(&mut self) { + let _ = self.finalize_guarded(); + } + + /// After a guarded call: a deferred close released inside it finalized + /// every statement of the connection, this one included. + unsafe fn forget_or_finalize(&mut self) { + let stmt = std::mem::replace(&mut self.stmt, std::ptr::null_mut()); + let owned = self.release_owned(); + if stmt.is_null() || !db_is_open(self.db.get_nanbox_f64()) { + return; + } + if !owned { + ffi::sqlite3_finalize(stmt); + } else if ffi::sqlite3_stmt_busy(stmt) != 0 { + // Stopped on a row: reset it (open aggregates finish) under a + // guard; what they throw loses to the error already leaving. + let _ = guarded(self.db.get_nanbox_f64(), || ffi::sqlite3_reset(stmt)); + } + } +} + +/// A row as a JS value without throwing (node's range error as `Err`). +pub(crate) unsafe fn row_value_checked( + flags: StmtFlags, + raw_stmt: *mut ffi::sqlite3_stmt, + return_arrays: bool, +) -> Result { + let column_count = ffi::sqlite3_column_count(raw_stmt).max(0); + let scope = RuntimeHandleScope::new(); + let mut values = Vec::with_capacity(column_count as usize); + for index in 0..column_count { + let value = column_value_checked(raw_stmt, index, flags.read_bigints)?; + values.push(scope.root_nanbox_u64(value.bits())); + } + if return_arrays { + let arr = scope.root_raw_mut_ptr(js_array_alloc(column_count as u32)); + for value in &values { + let next = js_array_push( + arr.get_raw_mut_ptr(), + JSValue::from_bits(value.get_nanbox_u64()), + ); + arr.set_raw_mut_ptr(next); + } + return Ok(JSValue::array_ptr(arr.get_raw_mut_ptr::())); + } + let mut names = Vec::with_capacity(column_count as usize); + for index in 0..column_count { + let name_ptr = ffi::sqlite3_column_name(raw_stmt, index); + names.push(if name_ptr.is_null() { + String::new() + } else { + CStr::from_ptr(name_ptr).to_string_lossy().into_owned() + }); + } + let obj = scope.root_raw_mut_ptr(js_object_alloc_null_proto(0, names.len() as u32)); + set_object_keys_from_names(obj.get_raw_mut_ptr::(), &names); + for (idx, value) in values.iter().enumerate() { + js_object_set_field( + obj.get_raw_mut_ptr::(), + idx as u32, + JSValue::from_bits(value.get_nanbox_u64()), + ); + } + Ok(JSValue::object_ptr( + obj.get_raw_mut_ptr::() as *mut u8 + )) +} + +unsafe fn column_value_checked( + raw_stmt: *mut ffi::sqlite3_stmt, + index: c_int, + read_bigints: bool, +) -> Result { + Ok(match ffi::sqlite3_column_type(raw_stmt, index) { + ffi::SQLITE_NULL => JSValue::null(), + ffi::SQLITE_INTEGER => { + integer_value_checked(ffi::sqlite3_column_int64(raw_stmt, index), read_bigints)? + } + ffi::SQLITE_FLOAT => JSValue::number(ffi::sqlite3_column_double(raw_stmt, index)), + ffi::SQLITE_TEXT => { + let ptr = ffi::sqlite3_column_text(raw_stmt, index); + if ptr.is_null() { + return Ok(JSValue::null()); + } + let len = ffi::sqlite3_column_bytes(raw_stmt, index) as usize; + JSValue::string_ptr(js_string_from_bytes(ptr, len as u32)) + } + ffi::SQLITE_BLOB => { + let len = ffi::sqlite3_column_bytes(raw_stmt, index) as usize; + let ptr = ffi::sqlite3_column_blob(raw_stmt, index) as *const u8; + let bytes: &[u8] = if len > 0 && !ptr.is_null() { + std::slice::from_raw_parts(ptr, len) + } else { + &[] + }; + JSValue::from_bits(from_slice(Brand::Uint8Array, bytes).to_bits()) + } + _ => JSValue::null(), + }) +} + +/// Run a statement to completion and return `{ changes, lastInsertRowid }`. +pub(crate) unsafe fn run_to_completion(mut stepper: Stepper<'_, '_>) -> f64 { + while stepper.step() {} + let (raw_db, read_bigints) = (stepper.raw_db(), stepper.flags.read_bigints); + stepper.finish(); + js_nanbox_pointer(run_result_object(raw_db, read_bigints) as i64) +} + +pub(crate) unsafe fn first_row(mut stepper: Stepper<'_, '_>) -> f64 { + if !stepper.step() { + stepper.finish(); + return undefined_f64(); + } + let scope = RuntimeHandleScope::new(); + let row = scope.root_nanbox_u64(stepper.row().bits()); + stepper.finish_early(); + row.get_nanbox_f64() +} + +pub(crate) unsafe fn all_rows(mut stepper: Stepper<'_, '_>) -> f64 { + let scope = RuntimeHandleScope::new(); + let rows = scope.root_raw_mut_ptr(js_array_alloc(0)); + let row = scope.root_nanbox_u64(JSValue::undefined().bits()); + while stepper.step() { + row.set_nanbox_u64(stepper.row().bits()); + let next = js_array_push( + rows.get_raw_mut_ptr(), + JSValue::from_bits(row.get_nanbox_u64()), + ); + rows.set_raw_mut_ptr(next); + } + stepper.finish(); + js_nanbox_pointer(rows.get_raw_mut_ptr::() as i64) +} + +/// Prepare this statement's SQL on its database and bind `params` (the +/// rest array of a run/get/all/iterate call). Bumps the iteration epoch. +unsafe fn start_statement<'a, 's>( + this: &RuntimeHandle<'s>, + db: &'a RuntimeHandle<'s>, + params: f64, +) -> Stepper<'a, 's> { + let (stmt, _) = live_stmt(this.get_nanbox_f64()); + stmt.epoch += 1; + let flags = stmt.flags; + // The payload is stable and only dropped once `this` is unreachable; + // `this` is rooted for the whole call. + let owner: *mut NodeStmt = stmt; + let sql: *const CStr = stmt.sql.as_c_str(); + let params_arr = raw_addr_from_value(params) as *const ArrayHeader; + let named = !params_arr.is_null() + && js_array_length(params_arr) > 0 + && is_named_parameter_object(f64_from_jsvalue(js_array_get(params_arr, 0))); + let bind = |raw_db, raw_stmt| bind_node_sqlite_params(flags, raw_db, raw_stmt, params_arr); + if (*owner).raw.is_null() || (*owner).running { + Stepper::start(db, &*sql, flags, named, bind) + } else { + Stepper::start_owned(db, owner, flags, named, bind) + } +} + +macro_rules! with_statement { + ($this:expr, $params:expr, |$stepper:ident| $body:expr) => {{ + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64($this); + let (_, db) = live_stmt(this.get_nanbox_f64()); + let db = scope.root_nanbox_f64(db); + let params = scope.root_nanbox_f64($params); + let $stepper = start_statement(&this, &db, params.get_nanbox_f64()); + $body + }}; +} + +extern "C" fn stmt_run_thunk(_c: *const ClosureHeader, this: JsThis, params: f64) -> f64 { + unsafe { with_statement!(this.as_f64(), params, |stepper| run_to_completion(stepper)) } +} + +extern "C" fn stmt_get_thunk(_c: *const ClosureHeader, this: JsThis, params: f64) -> f64 { + unsafe { with_statement!(this.as_f64(), params, |stepper| first_row(stepper)) } +} + +extern "C" fn stmt_all_thunk(_c: *const ClosureHeader, this: JsThis, params: f64) -> f64 { + unsafe { with_statement!(this.as_f64(), params, |stepper| all_rows(stepper)) } +} + +extern "C" fn stmt_iterate_thunk(_c: *const ClosureHeader, this: JsThis, params: f64) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this_root = scope.root_nanbox_f64(this.as_f64()); + let rows = with_statement!(this_root.get_nanbox_f64(), params, |stepper| all_rows( + stepper + )); + let rows = scope.root_nanbox_f64(rows); + // Rows are materialized eagerly; the iterator protocol matches node. + let (stmt, _) = live_stmt(this_root.get_nanbox_f64()); + let epoch = stmt.epoch; + native_payload::alloc_with_state( + &ITER_FAMILY, + NodeStmtIter { + epoch, + index: 0, + done: false, + }, + std::mem::size_of::(), + &[], + &[ + (b"stmt", this_root.get_nanbox_f64()), + (b"rows", rows.get_nanbox_f64()), + ], + &[], + &ITER_BIRTH, + ) + } +} + +extern "C" fn stmt_columns_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this.as_f64()); + let (stmt, _) = live_stmt(this.get_nanbox_f64()); + // Column metadata of the compiled statement: nothing runs or calls back. + let columns = if stmt.raw.is_null() { + js_array_alloc(0) + } else { + sqlite_columns_array(stmt.raw) + }; + js_nanbox_pointer(columns as i64) + } +} + +unsafe fn set_flag(this: f64, value: f64, set: impl FnOnce(&mut StmtFlags, bool)) -> f64 { + let (stmt, _) = live_stmt(this); + let js = value_from_f64(value); + if !js.is_bool() { + throw_type("The \"enabled\" argument must be a boolean"); + } + set(&mut stmt.flags, js.as_bool()); + undefined_f64() +} + +extern "C" fn stmt_set_read_bigints_thunk(_c: *const ClosureHeader, this: JsThis, v: f64) -> f64 { + unsafe { set_flag(this.as_f64(), v, |f, on| f.read_bigints = on) } +} + +extern "C" fn stmt_set_return_arrays_thunk(_c: *const ClosureHeader, this: JsThis, v: f64) -> f64 { + unsafe { set_flag(this.as_f64(), v, |f, on| f.return_arrays = on) } +} + +extern "C" fn stmt_set_allow_bare_thunk(_c: *const ClosureHeader, this: JsThis, v: f64) -> f64 { + unsafe { set_flag(this.as_f64(), v, |f, on| f.allow_bare_named_parameters = on) } +} + +extern "C" fn stmt_set_allow_unknown_thunk(_c: *const ClosureHeader, this: JsThis, v: f64) -> f64 { + unsafe { + set_flag(this.as_f64(), v, |f, on| { + f.allow_unknown_named_parameters = on + }) + } +} + +extern "C" fn stmt_source_sql_getter(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let (stmt, _) = live_stmt(this.as_f64()); + let bytes = stmt.sql.as_bytes(); + f64_from_jsvalue(JSValue::string_ptr(js_string_from_bytes( + bytes.as_ptr(), + bytes.len() as u32, + ))) + } +} + +extern "C" fn stmt_expanded_sql_getter(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let (stmt, _) = live_stmt(this.as_f64()); + // The compiled statement keeps the last run's bindings. + f64_from_jsvalue(string_value(&expanded_sql_of(stmt.raw))) + } +} + +fn install_stmt_prototype(proto: &mut PayloadPrototype) { + proto.method("run", builtin_rest!(stmt_run_thunk), 0); + proto.method("get", builtin_rest!(stmt_get_thunk), 0); + proto.method("all", builtin_rest!(stmt_all_thunk), 0); + proto.method("iterate", builtin_rest!(stmt_iterate_thunk), 0); + proto.method("columns", builtin!(stmt_columns_thunk, 0), 0); + proto.method( + "setReadBigInts", + builtin!(stmt_set_read_bigints_thunk, 1), + 1, + ); + proto.method( + "setReturnArrays", + builtin!(stmt_set_return_arrays_thunk, 1), + 1, + ); + proto.method( + "setAllowBareNamedParameters", + builtin!(stmt_set_allow_bare_thunk, 1), + 1, + ); + proto.method( + "setAllowUnknownNamedParameters", + builtin!(stmt_set_allow_unknown_thunk, 1), + 1, + ); +} + +// ---- StatementSyncIterator ------------------------------------------------------ + +fn install_iter_prototype(proto: &mut PayloadPrototype) { + proto.inherit(native_payload::iterator_prototype()); + proto.method("next", builtin!(iter_next_thunk, 0), 0); + proto.method("return", builtin!(iter_return_thunk, 0), 0); +} + +fn done_result() -> f64 { + native_payload::iter_result_done_value(true, null_f64()) +} + +extern "C" fn iter_next_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let this = this.as_f64(); + let iter = match native_payload::payload_mut::(this, &ITER_FAMILY) { + Ok(iter) => iter, + Err(PayloadMiss::Closed) => return done_result(), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + }; + if iter.done { + return done_result(); + } + let expected = iter.epoch; + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this); + let stmt_value = native_payload::state_get_memo( + this.get_nanbox_f64(), + &ITER_FAMILY, + b"stmt", + &MEMO_STMT, + ); + let (stmt, _) = live_stmt(stmt_value); + if stmt.epoch != expected { + throw_invalid_state("iterator was invalidated"); + } + let rows = native_payload::state_get_memo( + this.get_nanbox_f64(), + &ITER_FAMILY, + b"rows", + &MEMO_ROWS, + ); + let arr = raw_addr_from_value(rows) as *const ArrayHeader; + let Ok(iter) = + native_payload::payload_mut::(this.get_nanbox_f64(), &ITER_FAMILY) + else { + return done_result(); + }; + if arr.is_null() || iter.index >= js_array_length(arr) { + iter.done = true; + native_payload::state_set_memo( + this.get_nanbox_f64(), + &ITER_FAMILY, + b"rows", + undefined_f64(), + &MEMO_ROWS, + ); + return done_result(); + } + let row = f64_from_jsvalue(js_array_get(arr, iter.index)); + iter.index += 1; + native_payload::iter_result_done_value(false, row) + } +} + +extern "C" fn iter_return_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let this = this.as_f64(); + match native_payload::payload_mut::(this, &ITER_FAMILY) { + Ok(iter) => iter.done = true, + Err(PayloadMiss::Closed) => {} + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + } + native_payload::state_set_memo(this, &ITER_FAMILY, b"rows", undefined_f64(), &MEMO_ROWS); + done_result() + } +} diff --git a/crates/perry-stdlib/src/sqlite/tag_store.rs b/crates/perry-stdlib/src/sqlite/tag_store.rs new file mode 100644 index 0000000000..7fd9cd25c8 --- /dev/null +++ b/crates/perry-stdlib/src/sqlite/tag_store.rs @@ -0,0 +1,236 @@ +//! `node:sqlite` `SQLTagStore` (`db.createTagStore()`): an ordinary object +//! that owns a native payload (#11919). The payload is plain data: an LRU of +//! the SQL texts it has compiled, each stamped with the database open it +//! belongs to. Running a template compiles it on the database (statements own +//! no C resource), so nothing here outlives a `close()`. + +use super::*; +use perry_runtime::closure::{ClosureHeader, JsThis}; +use perry_runtime::gc::RuntimeHandleScope; +use perry_runtime::native_class_ids::SQLITE_TAG_STORE; +use perry_runtime::native_payload::{ + self, NativePayloadFamily, OpenSerial, PayloadMiss, PayloadPrototype, +}; +use perry_runtime::{js_array_get, js_array_is_array, js_array_length, ArrayHeader, JSValue}; +use std::collections::VecDeque; +use std::ffi::CString; + +perry_runtime::state_key_memo!(static MEMO_DB); +perry_runtime::birth_memo!(static TAG_STORE_BIRTH); + +macro_rules! builtin { + ($body:path, $n:tt) => { + perry_runtime::fn_info!($body, $n; with_declared($n), with_flags(perry_runtime::closure::FN_BUILTIN)) + }; +} + +macro_rules! builtin_rest { + ($body:path) => { + perry_runtime::fn_info!( + $body, 1; + with_rest(0), + with_declared(0), + with_flags(perry_runtime::closure::FN_BUILTIN) + ) + }; +} + +pub(crate) static TAG_STORE_FAMILY: NativePayloadFamily = NativePayloadFamily { + class_id: SQLITE_TAG_STORE, + links_owner: false, + name: "SQLTagStore", + constructor_export: None, + constructor_length: 0, + install_prototype: install_tag_store_prototype, +}; + +pub(crate) struct NodeTagStore { + capacity: usize, + /// Least recently used first. + cache: VecDeque<(String, OpenSerial)>, +} + +impl NodeTagStore { + /// Record a use of `sql` on open `serial`; a stale entry is replaced. + fn touch(&mut self, sql: &str, serial: OpenSerial) { + if self.capacity == 0 { + return; + } + self.cache.retain(|(cached, _)| cached != sql); + self.cache.push_back((sql.to_string(), serial)); + while self.cache.len() > self.capacity { + self.cache.pop_front(); + } + } +} + +pub(crate) fn node_sqlite_tag_store_capacity(value: f64) -> usize { + let js = value_from_f64(value); + let number = if js.is_int32() { + js.as_int32() as f64 + } else if js.is_number() { + js.as_number() + } else { + return 1000; + }; + if !number.is_finite() { + return if number.is_sign_positive() { + i32::MAX as usize + } else { + 0 + }; + } + let truncated = number.trunc(); + if truncated <= 0.0 { + 0 + } else if truncated >= i32::MAX as f64 { + i32::MAX as usize + } else { + truncated as usize + } +} + +/// `db.createTagStore(maxSize)`. +pub(crate) unsafe fn new_tag_store(db: f64, capacity: usize) -> f64 { + let scope = RuntimeHandleScope::new(); + let db = scope.root_nanbox_f64(db); + db_payload(db.get_nanbox_f64()); + let store = scope.root_nanbox_f64(native_payload::alloc_with_state( + &TAG_STORE_FAMILY, + NodeTagStore { + capacity, + cache: VecDeque::new(), + }, + std::mem::size_of::(), + &[], + &[(b"db", db.get_nanbox_f64())], + &[ + super::statement_sync::own_getter("capacity", builtin!(tag_store_capacity_getter, 0)), + super::statement_sync::own_getter("db", builtin!(tag_store_db_getter, 0)), + super::statement_sync::own_getter("size", builtin!(tag_store_size_getter, 0)), + ], + &TAG_STORE_BIRTH, + )); + store.get_nanbox_f64() +} + +unsafe fn store_payload<'a>(this: f64) -> &'a mut NodeTagStore { + match native_payload::payload_mut::(this, &TAG_STORE_FAMILY) { + Ok(store) => store, + Err(PayloadMiss::Closed) => throw_invalid_state("SQLTagStore is not open"), + Err(PayloadMiss::Foreign) => throw_illegal_invocation(), + } +} + +/// The SQL text of a tagged template (`?` between the parts) and its values. +unsafe fn template_args(args_arr: *const ArrayHeader) -> (String, Vec) { + let args = node_args_from_array(args_arr); + let strings_value = args.first().copied().unwrap_or_else(undefined_f64); + let is_array = value_from_f64(js_array_is_array(strings_value)); + if !is_array.is_bool() || !is_array.as_bool() { + throw_type("First argument must be an array of strings (template literal)."); + } + let strings_ptr = raw_addr_from_value(strings_value) as *const ArrayHeader; + if strings_ptr.is_null() { + throw_type("First argument must be an array of strings (template literal)."); + } + let strings_len = js_array_length(strings_ptr); + let mut sql = String::new(); + for index in 0..strings_len { + let Some(part) = string_key_from_js_value(js_array_get(strings_ptr, index)) else { + throw_type("Template literal parts must be strings."); + }; + sql.push_str(&part); + if index + 1 < strings_len { + sql.push('?'); + } + } + (sql, args.into_iter().skip(1).collect()) +} + +/// Compile the template on the store's database (recording it in the LRU) +/// and bind its values positionally. +macro_rules! with_template { + ($this:expr, $rest:expr, |$stepper:ident| $body:expr) => {{ + let scope = RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64($this); + store_payload(this.get_nanbox_f64()); + let db = scope.root_nanbox_f64(native_payload::state_get( + this.get_nanbox_f64(), + &TAG_STORE_FAMILY, + b"db", + )); + let rest = scope.root_nanbox_f64($rest); + let (sql, values) = + template_args(raw_addr_from_value(rest.get_nanbox_f64()) as *const ArrayHeader); + let value_roots: Vec<_> = values.iter().map(|v| scope.root_nanbox_f64(*v)).collect(); + let (serial, flags) = { + let db = db_payload(db.get_nanbox_f64()); + (db.serial, db.cfg.stmt_flags()) + }; + let Ok(c_sql) = CString::new(sql.clone()) else { + throw_sqlite_error("SQL string must not contain null bytes"); + }; + let $stepper = Stepper::start(&db, &c_sql, flags, false, |raw_db, raw_stmt| { + let values: Vec = value_roots.iter().map(|v| v.get_nanbox_f64()).collect(); + bind_node_sqlite_positional_params(raw_db, raw_stmt, &values) + }); + store_payload(this.get_nanbox_f64()).touch(&sql, serial); + $body + }}; +} + +extern "C" fn tag_store_run_thunk(_c: *const ClosureHeader, this: JsThis, rest: f64) -> f64 { + unsafe { with_template!(this.as_f64(), rest, |stepper| run_to_completion(stepper)) } +} + +extern "C" fn tag_store_get_thunk(_c: *const ClosureHeader, this: JsThis, rest: f64) -> f64 { + unsafe { with_template!(this.as_f64(), rest, |stepper| first_row(stepper)) } +} + +extern "C" fn tag_store_all_thunk(_c: *const ClosureHeader, this: JsThis, rest: f64) -> f64 { + unsafe { with_template!(this.as_f64(), rest, |stepper| all_rows(stepper)) } +} + +extern "C" fn tag_store_iterate_thunk(_c: *const ClosureHeader, this: JsThis, rest: f64) -> f64 { + unsafe { + let rows = with_template!(this.as_f64(), rest, |stepper| all_rows(stepper)); + perry_runtime::array::array_values_iter(rows) + } +} + +extern "C" fn tag_store_clear_thunk(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + store_payload(this.as_f64()).cache.clear(); + } + undefined_f64() +} + +extern "C" fn tag_store_size_getter(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let size = store_payload(this.as_f64()).cache.len(); + f64_from_jsvalue(JSValue::number(size as f64)) + } +} + +extern "C" fn tag_store_capacity_getter(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + let capacity = store_payload(this.as_f64()).capacity; + f64_from_jsvalue(JSValue::number(capacity as f64)) + } +} + +extern "C" fn tag_store_db_getter(_c: *const ClosureHeader, this: JsThis) -> f64 { + unsafe { + store_payload(this.as_f64()); + native_payload::state_get_memo(this.as_f64(), &TAG_STORE_FAMILY, b"db", &MEMO_DB) + } +} + +fn install_tag_store_prototype(proto: &mut PayloadPrototype) { + proto.method("run", builtin_rest!(tag_store_run_thunk), 0); + proto.method("get", builtin_rest!(tag_store_get_thunk), 0); + proto.method("all", builtin_rest!(tag_store_all_thunk), 0); + proto.method("iterate", builtin_rest!(tag_store_iterate_thunk), 0); + proto.method("clear", builtin!(tag_store_clear_thunk, 0), 0); +} diff --git a/docs/native-payload-pattern.md b/docs/native-payload-pattern.md index 808a148ab6..74e51dd503 100644 --- a/docs/native-payload-pattern.md +++ b/docs/native-payload-pattern.md @@ -117,8 +117,13 @@ source of truth: no owner registries, address maps or per-family latches. `NativePayloadFamily::links_owner` opts into one traced, NaN-boxed owner slot on the stable malloc cell. Both marking and relocation visit it. The owner store uses `runtime_write_barrier_external_slot`, the exact-slot barrier that recognizes -malloc parents (`runtime_write_barrier_slot` only remembers old arena parents). The cell -remains 136 bytes; families without callbacks keep the slot zero. +malloc parents (`runtime_write_barrier_slot` only remembers old arena parents). The cell also has a traced `callbacks` slot, mirroring the runtime-owned +JS-state callbacks array. Both edges use the exact malloc-parent barrier and +are visited for marking and relocation. Callback families allocate a traced +16-byte extension after the unchanged 136-byte header (on 64-bit targets): +one traced array word and one native catch-token pointer. The token contains +no JS values and is installed only for the duration of `NativeCallGuard::call`. +Legacy and N cells keep their layout and size; the owner stays zero for N. | Class | Native callback lifetime | Examples | Owner edge | |---|---|---|---| @@ -144,8 +149,18 @@ S checklist: before `CallbackSites`, so it closes first. C xDestroy does not free sites. 2. A trampoline calls `link_owner`; `None` returns the C error or no-op without JS. Root the owner immediately, and read it only through that - handle after allocation or JS. Read the callback from the owner's array. -3. Call JS only through `call_from_native`. It catches a throw, parks the + handle after allocation or JS. `callback_from_link` reads the traced + callbacks slot after the last argument allocation, without owner-key + lookup. A trampoline whose only JS is one registered callback with + arguments that allocate nothing calls `call_callback(link, index, args)`: + one owner check (open, this thread, nothing pending), the slot read and + the call. Use `set_callback`, `state_set`, or `state_set_memo` to replace or + clear the callbacks array; these keep the state field and cell slot in sync. +3. Call JS only through `call_from_native` / `call_from_link` / + `call_callback`. A native call (`NativeCallGuard::call`) captures one + handler at its first callback and reuses it for the rest; between + callbacks only runtime handle scopes may differ (a trampoline's argument + roots), never shadow frames or other managed stacks. It catches a throw, parks the exact value in `pendingException`, sets PENDING and returns `Err(())`. Further callbacks return the error without running JS: the first throw wins. Never throw through C or use throwing thread-validation helpers. @@ -155,7 +170,19 @@ S checklist: the borrow before calling C. Hold neither `&mut T` nor a mutex over a call that can re-enter JS. Bracket every callback-capable call (step, exec, prepare, close, backup, changeset_apply) with `enter` and `finish`. -5. Finish immediately when C returns, before result conversion or anything +5. A loop may obtain `owner_link` once while its owner stays rooted, then + use `enter_link` per C call. This checks OPEN and creator-thread affinity + on every entry without validating the moving JS receiver again. + `NativeCallGuard::owner_link` exposes the link already validated at entry. + Prefer unsafe `guard.call(|| native_call())`: it finishes immediately and + shares a lazily captured catch savepoint across the call's callbacks. + Each callback still arms its own C trampoline, so a throw never jumps + across the native library. Between callbacks the trap is inactive; the + cached new.target remains traced and rewritten. Root depths are refreshed + per callback, including argument-conversion scopes. Native helpers within + this span may not change other managed stacks. `enter`/`finish` remains + available with ordinary per-callback capture for existing callers. + Finish immediately when C returns, before result conversion or anything that can throw. `CallEnd::Threw(value)` throws that exact value outside C; `CallEnd::Closed` throws the family's closed error without converting or returning partial results. A callback throw takes priority over close. @@ -209,6 +236,39 @@ N checklist: 2. Use the ordinary payload conversion checklist. Add no callback sites, keep-alive pins or pending-exception machinery. +The first S family is node:sqlite (`perry-stdlib/src/sqlite/database_sync.rs`, +`statement_sync.rs`, `sqlite_callbacks.rs`, `tag_store.rs`, `session.rs`): + +* `DatabaseSync` owns the `sqlite3*`; its payload exists only while open. + Everything `open()` needs to reopen (path, flags, limits) is plain data + kept in the JS state, so `close()` / `open()` keep the same object. +* A statement owns no C resource: it keeps its SQL and options and compiles + them on its database per run, entering through the database. Children + carry the database's `OpenSerial`. +* Each `sqlite3_step` is one guard; rows are converted after `finish` + with non-throwing converters, and an error finalizes the statement before + the throw. Resets that can run `xFinal` stay inside the guard, so a + pending exception keeps them out of JS. +* The payload's `Drop` deletes sessions, finalizes any statement still on + the connection (the outer call of a deferred close), then + `sqlite3_close_v2`; the outer frame sees the database closed and forgets + its statement. A C child that must die before the connection (a + `sqlite3_session`) is shared through one `Rc` cell by the database + payload and the child payload: whichever goes first deletes it. +* A statement owns the `sqlite3_stmt` its `prepare()` compiled, as node's + does (the authorizer runs once per `prepare()`; `expandedSQL` reads the + last bindings). Every run clears and rebinds it and leaves it reset, so + finalizing it never calls back. The connection's close finalizes it; the + open's `live` token (an `Rc>` shared with the database + payload) tells the statement's drop whether it still must. +* node's own accessors (`stmt.sourceSQL`, `SQLTagStore#size`) are born with + the instance: `alloc_with_state(.., accessors, &BIRTH_MEMO)` installs them + at the site's first birth and records the final shapes; later births are + allocated in them, holding the family's shared accessor pairs. One-off + objects (`db.isOpen`, `db.limits.*`) still use + `native_payload::define_own_accessor`; a constructor export materializes + its prototype when the export is created (`native_payload::prototype`). + In each family conversion PR, delete its callback id registries and scanners, `js_write_barrier_root_nanbox` callback "rooting", and listener/pipe tables keyed by id. For sqlite this includes NODE_SQLITE_CUSTOM_FUNCTIONS, diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 23359f4663..760344e2e0 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -4275,10 +4275,6 @@ "file": "crates/perry-stdlib/src/readline/mod.rs", "name": "READLINE_GC_REGISTERED" }, - { - "file": "crates/perry-stdlib/src/sqlite.rs", - "name": "NODE_SQLITE_GC_SCANNER" - }, { "file": "crates/perry-stdlib/src/streams/gc.rs", "name": "GC_REGISTERED" diff --git a/scripts/native_handle_ledger.txt b/scripts/native_handle_ledger.txt index 480858934e..0d1e72181b 100644 --- a/scripts/native_handle_ledger.txt +++ b/scripts/native_handle_ledger.txt @@ -125,11 +125,8 @@ 1 0 crates/perry-stdlib/src/nodemailer/turnloop_bridge.rs 1 0 crates/perry-stdlib/src/readline/mod.rs 0 13 crates/perry-stdlib/src/sharp.rs -3 0 crates/perry-stdlib/src/sqlite.rs 0 2 crates/perry-stdlib/src/sqlite/better.rs 0 2 crates/perry-stdlib/src/sqlite/node_db.rs -0 2 crates/perry-stdlib/src/sqlite/node_stmt_session.rs -0 1 crates/perry-stdlib/src/sqlite/node_tag_store.rs 5 3 crates/perry-stdlib/src/streams.rs 1 1 crates/perry-stdlib/src/streams/byob.rs 1 0 crates/perry-stdlib/src/streams/expando.rs diff --git a/scripts/native_result_ledger.py b/scripts/native_result_ledger.py index 3d599d3dd9..a381351d8f 100644 --- a/scripts/native_result_ledger.py +++ b/scripts/native_result_ledger.py @@ -143,8 +143,13 @@ # raw Box representation. Removing the now-unused receiver rows also removes # the classified AsyncResource.bind pointer result. 264 -> 252 rows, # 237 -> 228 providers. -EXPECTED_ROWS = 252 -EXPECTED_PROVIDERS = 228 +# -16 rows / -13 providers (#11919 node:sqlite): DatabaseSync, StatementSync, +# SQLTagStore and Session are ordinary objects; their receiver rows are +# deleted and the three call-form rows return NR_F64. The bun:sqlite rows that +# reused node providers now name their own bun providers. 252 -> 236 rows, +# 228 -> 215 providers, as the script reports on the resolved tree. +EXPECTED_ROWS = 236 +EXPECTED_PROVIDERS = 215 KINDS = { "NR_GCPTR", "NR_NULLABLE_GCPTR", diff --git a/scripts/native_result_ledger.tsv b/scripts/native_result_ledger.tsv index 69110d6087..3eb3737a29 100644 --- a/scripts/native_result_ledger.tsv +++ b/scripts/native_result_ledger.tsv @@ -10,7 +10,10 @@ js_bun_serve NR_HANDLE_ID crates/perry-ext-http/src/server/bun_server.rs i64 js_bun_sqlite_database_call NR_HANDLE_ID crates/perry-stdlib/src/sqlite/bun.rs Handle js_bun_sqlite_database_query NR_HANDLE_ID crates/perry-stdlib/src/sqlite/bun.rs Handle js_bun_sqlite_database_run NR_GCPTR crates/perry-stdlib/src/sqlite/bun.rs *mut ObjectHeader +js_bun_sqlite_database_serialize NR_GCPTR crates/perry-stdlib/src/sqlite/node_db.rs *mut BufferHeader js_bun_sqlite_database_transaction NR_GCPTR crates/perry-stdlib/src/sqlite/bun.rs *mut ClosureHeader +js_bun_sqlite_statement_all NR_GCPTR crates/perry-stdlib/src/sqlite/node_db.rs *mut ArrayHeader +js_bun_sqlite_statement_run NR_GCPTR crates/perry-stdlib/src/sqlite/node_db.rs *mut ObjectHeader js_bun_sqlite_statement_values NR_GCPTR crates/perry-stdlib/src/sqlite/bun.rs *mut ArrayHeader js_bun_tcp_listen NR_HANDLE_ID crates/perry-ext-net/src/bun_tcp.rs i64 js_bun_transpiler_new NR_HANDLE_ID crates/perry-ext-typescript/src/bun.rs Handle @@ -109,22 +112,6 @@ js_node_https_server_listen NR_HANDLE_ID crates/perry-ext-http/src/server/https_ js_node_https_server_ref NR_HANDLE_ID crates/perry-ext-http/src/server/https_server.rs i64 js_node_https_server_set_timeout_method NR_HANDLE_ID crates/perry-ext-http/src/server/https_server.rs i64 js_node_https_server_unref NR_HANDLE_ID crates/perry-ext-http/src/server/https_server.rs i64 -js_node_sqlite_database_sync_call NR_HANDLE_ID crates/perry-stdlib/src/sqlite/node_db.rs Handle -js_node_sqlite_database_sync_create_session NR_HANDLE_ID crates/perry-stdlib/src/sqlite/node_stmt_session.rs Handle -js_node_sqlite_database_sync_create_tag_store NR_HANDLE_ID crates/perry-stdlib/src/sqlite/node_tag_store.rs Handle -js_node_sqlite_database_sync_limits NR_HANDLE_ID crates/perry-stdlib/src/sqlite/node_stmt_session.rs Handle -js_node_sqlite_database_sync_prepare NR_HANDLE_ID crates/perry-stdlib/src/sqlite/node_db.rs Handle -js_node_sqlite_database_sync_serialize NR_GCPTR crates/perry-stdlib/src/sqlite/node_db.rs *mut BufferHeader -js_node_sqlite_session_call NR_HANDLE_ID crates/perry-stdlib/src/sqlite/node_stmt_session.rs Handle -js_node_sqlite_session_changeset NR_GCPTR crates/perry-stdlib/src/sqlite/node_stmt_session.rs *mut BufferHeader -js_node_sqlite_session_patchset NR_GCPTR crates/perry-stdlib/src/sqlite/node_stmt_session.rs *mut BufferHeader -js_node_sqlite_sql_tag_store_all NR_GCPTR crates/perry-stdlib/src/sqlite/node_tag_store.rs *mut ArrayHeader -js_node_sqlite_sql_tag_store_db NR_HANDLE_ID crates/perry-stdlib/src/sqlite/node_tag_store.rs Handle -js_node_sqlite_sql_tag_store_run NR_GCPTR crates/perry-stdlib/src/sqlite/node_tag_store.rs *mut ObjectHeader -js_node_sqlite_statement_sync_all NR_GCPTR crates/perry-stdlib/src/sqlite/node_stmt_session.rs *mut ArrayHeader -js_node_sqlite_statement_sync_call NR_HANDLE_ID crates/perry-stdlib/src/sqlite/node_stmt_session.rs Handle -js_node_sqlite_statement_sync_columns NR_GCPTR crates/perry-stdlib/src/sqlite/node_stmt_session.rs *mut ArrayHeader -js_node_sqlite_statement_sync_run NR_GCPTR crates/perry-stdlib/src/sqlite/node_stmt_session.rs *mut ObjectHeader js_node_stream_method_event_names NR_GCPTR crates/perry-runtime/src/node_stream_event_emitter.rs i64 js_node_stream_method_listeners NR_GCPTR crates/perry-runtime/src/node_stream_event_emitter.rs i64 js_node_stream_method_raw_listeners NR_GCPTR crates/perry-runtime/src/node_stream_event_emitter.rs i64 diff --git a/scripts/thread_exit_address_globals.json b/scripts/thread_exit_address_globals.json index 65b8063426..987a409865 100644 --- a/scripts/thread_exit_address_globals.json +++ b/scripts/thread_exit_address_globals.json @@ -959,33 +959,6 @@ "verdict": "no_heap_address", "why": "OnceLock> of relocated text-symbol start addresses and demangled String names loaded from the executable image (error_stack_frames.rs:429-438); code addresses. | (second declaration) OnceLock>> of (generated function start address, Arc<[u8]> display name) snapshotted from the codegen function registry (error_stack_frames.rs:581-592); code addresses." }, - { - "file": "crates/perry-stdlib/src/sqlite.rs", - "names": [ - "NODE_SQLITE_CUSTOM_FUNCTIONS" - ], - "verdict": "thread_exit_invalidated", - "why": "Addresses of SQLite-owned boxes whose callback is a closure from the thread that called db.function(); a freed callback is set to undefined and the box unregistered at thread exit (xDestroy still frees it).", - "hook": "release_node_sqlite_callbacks_in_freed_ranges" - }, - { - "file": "crates/perry-stdlib/src/sqlite.rs", - "names": [ - "NODE_SQLITE_CUSTOM_AGGREGATES" - ], - "verdict": "thread_exit_invalidated", - "why": "Boxes holding db.aggregate() start/step/result/inverse closures; freed slots become undefined and the box is unregistered at thread exit.", - "hook": "release_node_sqlite_callbacks_in_freed_ranges" - }, - { - "file": "crates/perry-stdlib/src/sqlite.rs", - "names": [ - "NODE_SQLITE_ACTIVE_AGGREGATES" - ], - "verdict": "thread_exit_invalidated", - "why": "Running-aggregate state boxes; a freed state becomes undefined and the box is unregistered at thread exit, xFinal frees it as before.", - "hook": "release_node_sqlite_callbacks_in_freed_ranges" - }, { "file": "crates/perry-runtime/src/object/nm_namespace_hooks.rs", "names": [ diff --git a/scripts/unrooted_local_shape_baseline.json b/scripts/unrooted_local_shape_baseline.json index 0e18801b33..b67f6b95c1 100644 --- a/scripts/unrooted_local_shape_baseline.json +++ b/scripts/unrooted_local_shape_baseline.json @@ -13,13 +13,11 @@ "crates/perry-ext-net/src/lifecycle.rs": 1, "crates/perry-ext-streams/src/lib.rs": 4, "crates/perry-ext-ws/src/server.rs": 3, - "crates/perry-ext-zlib/src/stream.rs": 3, "crates/perry-stdlib/src/cheerio.rs": 6, "crates/perry-stdlib/src/crypto/kdf.rs": 9, "crates/perry-stdlib/src/crypto/keys.rs": 1, "crates/perry-stdlib/src/crypto/sign.rs": 22, "crates/perry-stdlib/src/crypto/util.rs": 2, - "crates/perry-stdlib/src/domain.rs": 3, "crates/perry-stdlib/src/ethers.rs": 5, "crates/perry-stdlib/src/events/events_on.rs": 12, "crates/perry-stdlib/src/events/module_helpers.rs": 1, @@ -27,12 +25,9 @@ "crates/perry-stdlib/src/lodash.rs": 21, "crates/perry-stdlib/src/querystring.rs": 2, "crates/perry-stdlib/src/readline/mod.rs": 4, - "crates/perry-stdlib/src/sqlite/backup.rs": 8, + "crates/perry-stdlib/src/sqlite/backup.rs": 3, "crates/perry-stdlib/src/sqlite/better.rs": 18, "crates/perry-stdlib/src/sqlite/bind.rs": 4, - "crates/perry-stdlib/src/sqlite/dispatch.rs": 6, - "crates/perry-stdlib/src/sqlite/node_stmt_session.rs": 4, - "crates/perry-stdlib/src/sqlite/node_tag_store.rs": 3, "crates/perry-stdlib/src/streams.rs": 24, "crates/perry-stdlib/src/streams/byob.rs": 12, "crates/perry-stdlib/src/streams/pipe.rs": 13, @@ -54,5 +49,5 @@ "crates/perry-stdlib/src/zlib.rs": 3 }, "schema_version": 3, - "total": 339 + "total": 315 } diff --git a/test-files/_helpers/gap_cb_sqlite_worker.ts b/test-files/_helpers/gap_cb_sqlite_worker.ts new file mode 100644 index 0000000000..7ed9b132f3 --- /dev/null +++ b/test-files/_helpers/gap_cb_sqlite_worker.ts @@ -0,0 +1,15 @@ +// Worker half of test_gap_native_payload_cb-sqlite_worker.ts: a database +// with a UDF that captures it and an aggregate, queried once. The database is +// never closed: worker teardown releases it. (Perry cannot yet preempt a +// worker busy in a loop, so the worker ends on its own rather than being +// terminated mid-query.) +import { parentPort } from "node:worker_threads"; +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +db.exec("create table t(a integer)"); +for (let i = 0; i < 100; i++) db.exec("insert into t values (" + i + ")"); +db.function("f", (x: number) => x + (db.isOpen ? 1 : 0)); +db.aggregate("s", { start: 0, step: (acc: number, x: number) => acc + x, result: (v: number) => v }); +const stmt = db.prepare("select s(f(a)) as v from t"); +parentPort!.postMessage("worker " + (stmt.get() as any).v); diff --git a/test-files/test_gap_11919_sqlite_statement_owned.ts b/test-files/test_gap_11919_sqlite_statement_owned.ts new file mode 100644 index 0000000000..fe52165928 --- /dev/null +++ b/test-files/test_gap_11919_sqlite_statement_owned.ts @@ -0,0 +1,69 @@ +// #11919: a StatementSync owns the statement its prepare() compiled, as +// node's does: the authorizer runs at prepare() only, expandedSQL reads the +// last bindings, columns() runs nothing; own getters are born on every +// statement; an own property shadows a prototype method; statements left +// over after close() are finalized once. +import { DatabaseSync } from 'node:sqlite'; + +const db = new DatabaseSync(':memory:'); +let auth = 0; +db.setAuthorizer(() => { + auth++; + return 0; +}); +db.exec('CREATE TABLE t(x INTEGER, y TEXT)'); +auth = 0; +const ins = db.prepare('INSERT INTO t VALUES (?, ?)'); +const afterPrepare = auth; +ins.run(1, 'a'); +ins.run(2, 'b'); +ins.run(3, 'c'); +console.log('authorizer: prepare', afterPrepare > 0, 'runs', auth - afterPrepare); +console.log('expanded', ins.expandedSQL); +console.log('source', ins.sourceSQL); +try { + ins.run(1, 2, 3); +} catch (e) { + console.log('bind error', (e as any).code); +} +console.log('after bind error', JSON.stringify(ins.run(4, 'd'))); + +const sel = db.prepare('SELECT x, y FROM t WHERE x >= ?'); +auth = 0; +console.log('columns', JSON.stringify(sel.columns().map((c: any) => c.name)), 'auth', auth); +console.log('all', JSON.stringify(sel.all(2))); +console.log('get', JSON.stringify(sel.get(3)), 'expanded', sel.expandedSQL); +console.log('iterate', JSON.stringify([...sel.iterate(4)]), 'runs', auth); + +const stmts: any[] = []; +for (let i = 0; i < 50; i++) stmts.push(db.prepare(`SELECT ${i} AS n`)); +console.log( + 'own getters', + stmts.every((s, i) => s.sourceSQL === `SELECT ${i} AS n` && s.expandedSQL === `SELECT ${i} AS n` && s.get().n === i), +); +const d = Object.getOwnPropertyDescriptor(stmts[7], 'expandedSQL')!; +console.log('descriptor', typeof d.get, d.set, d.enumerable, d.configurable); +console.log('keys', JSON.stringify(Object.keys(stmts[7])), JSON.stringify(Object.getOwnPropertyNames(stmts[8]))); + +Object.defineProperty(stmts[3], 'get', { value: () => 'own get', configurable: true }); +console.log('own method', stmts[3].get(), JSON.stringify(stmts[4].get())); + +const inner = db.prepare('SELECT count(*) AS c FROM t'); +db.function('cnt', () => (inner.get() as any).c); +const outer = db.prepare('SELECT cnt() AS a, cnt() AS b'); +console.log('nested', JSON.stringify(outer.get()), JSON.stringify(outer.get())); + +db.close(); +try { + sel.get(1); +} catch (e) { + console.log('after close', (e as any).code); +} +let garbage = 0; +for (let i = 0; i < 300; i++) { + const other = new DatabaseSync(':memory:'); + for (let j = 0; j < 5; j++) other.prepare(`SELECT ${j}`); + if (i % 2 === 0) other.close(); + garbage += new Array(2000).fill(i).length; +} +console.log('left over statements finalized once', garbage); diff --git a/test-files/test_gap_native_payload_cb-sqlite.ts b/test-files/test_gap_native_payload_cb-sqlite.ts new file mode 100644 index 0000000000..fd61488908 --- /dev/null +++ b/test-files/test_gap_native_payload_cb-sqlite.ts @@ -0,0 +1,235 @@ +// #11919: node:sqlite DatabaseSync / StatementSync / StatementSyncIterator / +// SQLTagStore / Session are ordinary objects that own a native payload, and +// SQLite calls back into JS (UDFs, aggregates, authorizer) through the +// database's traced owner edge. Shape, identity, instanceof, close/reopen +// errors, callbacks, throws from callbacks, reentrancy and churn, against node. +import { DatabaseSync, StatementSync, Session, constants } from "node:sqlite"; + +function shape(label: string, o: any): void { + console.log( + label, + typeof o, + JSON.stringify(Object.keys(o)), + Object.prototype.toString.call(o), + o.constructor.name, + ); +} +function protoNames(o: any): string { + return Object.getOwnPropertyNames(Object.getPrototypeOf(o)).sort().join(","); +} +function attempt(label: string, f: () => unknown): void { + try { + console.log(label, "ok", JSON.stringify(f())); + } catch (e: any) { + console.log(label, "threw", e.name, e.code, e.message); + } +} + +const db = new DatabaseSync(":memory:"); +const db2 = new DatabaseSync(":memory:"); +shape("db", db); +console.log("db json", JSON.stringify(db)); +console.log("db proto", protoNames(db)); +console.log("identity", db === db2, db !== db2, db === db); +console.log( + "instanceof", + db instanceof DatabaseSync, + db2 instanceof DatabaseSync, + ({}) instanceof DatabaseSync, + db.constructor === DatabaseSync, +); + +db.exec("create table t(a integer, b text)"); +const ins = db.prepare("insert into t values(?, ?)"); +shape("stmt", ins); +console.log("stmt json", JSON.stringify(ins)); +console.log("stmt proto", protoNames(ins)); +console.log("stmt instanceof", ins instanceof StatementSync, ins.constructor === StatementSync); +for (let i = 0; i < 5; i++) ins.run(i, "r" + i); +console.log("expanded", ins.expandedSQL); + +const sel = db.prepare("select a, b from t order by a"); +const it = sel.iterate(); +shape("iter", it); +console.log("iter proto", protoNames(it)); +console.log("iter", JSON.stringify(it.next()), [...it].length, JSON.stringify(it.next())); +const it2 = sel.iterate(); +sel.all(); +attempt("iter invalidated", () => it2.next()); +const it3 = sel.iterate(); +console.log("iter return", JSON.stringify(it3.return!()), JSON.stringify(it3.next())); + +const ts = db.createTagStore(2); +shape("tags", ts); +console.log("tags db", ts.db === db, ts.size, ts.capacity); +console.log("tags get", JSON.stringify(ts.get`select count(*) as n from t where a < ${3}`)); +console.log("tags all", JSON.stringify(ts.all`select a from t where a > ${2} order by a`)); +ts.run`insert into t values(${10}, ${"ten"})`; +console.log("tags size", ts.size); +ts.clear(); +console.log("tags cleared", ts.size); + +const s = db.createSession(); +shape("session", s); +console.log("session proto", protoNames(s), s instanceof Session); +db.exec("insert into t values(20, 'twenty')"); +const changes = s.changeset(); +console.log("changeset", changes instanceof Uint8Array, changes.length > 0); +s.close(); +attempt("session closed", () => s.changeset()); + +const m = new Map([[db, 1], [db2, 2]]); +const w = new WeakMap([[ins, "ins"], [ts, "ts"]]); +console.log("keys", m.get(db), m.get(db2), m.size, w.get(ins), w.get(ts)); + +// Callbacks: scalar, aggregate, window, authorizer. +db.function("twice", (x: number) => x * 2); +console.log("udf", db.prepare("select twice(21) as v").get()!.v); +db.function("nested", (x: number) => (db.prepare("select twice(?) as v").get(x) as any).v + 1); +console.log("reentrant udf", db.prepare("select nested(5) as v").get()!.v); +db.function("writer", (x: number) => { + db.exec("insert into t values(100, 'w')"); + return x; +}); +// A callback allocating ~10 MB per call moves the young heap mid-query. +db.function("big", (x: number) => { + let n = 0; + for (let i = 0; i < 20; i++) n += new Array(1 << 16).fill(x).length; + return n + x; +}); +console.log("allocating udf", JSON.stringify(db.prepare("select big(a) as v from t order by a").all())); +console.log( + "udf writes", + db.prepare("select writer(1) as v").get()!.v, + db.prepare("select count(*) as n from t where a = 100").get()!.n, +); +db.aggregate("sumsq", { + start: () => 0, + step: (acc: number, x: number) => acc + x * x, + result: (v: number) => "r" + v, +}); +console.log( + "aggregate", + db.prepare("select sumsq(a) as v from t where a < 5").get()!.v, + db.prepare("select sumsq(a) as v from t where a > 1000").get()!.v, +); +db.aggregate("win", { + start: 0, + step: (a: number, x: number) => a + x, + inverse: (a: number, x: number) => a - x, + result: (a: number) => a * 10, +}); +console.log( + "window", + JSON.stringify( + db + .prepare( + "select a, win(a) over (order by a rows between 1 preceding and current row) as w from t where a < 5 order by a", + ) + .all(), + ), +); +let authCalls = 0; +db.setAuthorizer((action: number) => { + authCalls++; + return action === constants.SQLITE_DELETE ? constants.SQLITE_DENY : constants.SQLITE_OK; +}); +attempt("authorizer deny", () => db.prepare("delete from t")); +console.log("authorizer ran", authCalls > 0); +let inAuth = false; +db.setAuthorizer(() => { + if (!inAuth) { + inAuth = true; + db.exec("select 1"); + inAuth = false; + } + return constants.SQLITE_OK; +}); +console.log("authorizer reentry", db.prepare("select 7 as v").get()!.v); +db.setAuthorizer(null); + +// Throws from callbacks surface as the exact value, outside SQLite. +class MyErr extends Error {} +const boom = new MyErr("boom"); +db.function("boom", () => { + throw boom; +}); +try { + db.prepare("select boom()").get(); +} catch (e) { + console.log("udf throw", e === boom, e instanceof MyErr); +} +console.log("reusable", db.prepare("select twice(2) as v").get()!.v); +let steps = 0; +db.aggregate("bad", { + start: 0, + step: (_acc: number, _x: number) => { + steps++; + throw new Error("step" + steps); + }, +}); +attempt("aggregate throw", () => db.prepare("select bad(a) from t").get()); +console.log("first throw wins", steps); +db.setAuthorizer(() => "x" as any); +attempt("authorizer non-int", () => db.prepare("select 1")); +db.setAuthorizer(() => 99); +attempt("authorizer bad code", () => db.prepare("select 1")); +const authThrow = new Error("auth boom"); +db.setAuthorizer(() => { + throw authThrow; +}); +try { + db.prepare("select 1"); +} catch (e) { + console.log("authorizer throw", e === authThrow); +} +db.setAuthorizer(null); +db.function("tryNested", () => { + try { + db.exec("select boom()"); + } catch (e) { + return e === boom ? "caught" : "other"; + } + return "none"; +}); +console.log("nested throw caught", db.prepare("select tryNested() as v").get()!.v); + +// Close, reopen, close-then-use. +const st = db.prepare("select twice(a) as v from t order by a"); +const live = st.iterate(); +db.close(); +console.log("closed", db.isOpen); +attempt("stmt after close", () => st.all()); +attempt("iter after close", () => live.next()); +attempt("exec after close", () => db.exec("select 1")); +attempt("prepare after close", () => db.prepare("select 1")); +attempt("tags after close", () => ts.get`select 1`); +attempt("double close", () => db.close()); +db.open(); +console.log("reopened", db.isOpen, db === m.keys().next().value); +attempt("stmt after reopen", () => st.all()); +attempt("udf after reopen", () => db.prepare("select twice(1)")); +attempt("double open", () => db.open()); +db.close(); +db2.close(); + +// Churn: open, register a UDF capturing the database, query, close. +function rssMB(): number { + return process.memoryUsage().rss / 1048576; +} +let sum = 0; +for (let i = 0; i < 2000; i++) { + const d = new DatabaseSync(":memory:"); + d.function("f", (x: number) => x + (d.isOpen ? 1 : 0)); + sum += d.prepare("select f(1) as v").get()!.v as number; + d.close(); +} +const before = rssMB(); +for (let i = 0; i < 50000; i++) { + const d = new DatabaseSync(":memory:"); + d.function("f", (x: number) => x + (d.isOpen ? 1 : 0)); + sum += d.prepare("select f(1) as v").get()!.v as number; + d.close(); +} +const grew = rssMB() - before; +console.log("churn", sum, grew < 64 ? "flat" : "grew " + Math.round(grew) + "MB"); diff --git a/test-files/test_gap_native_payload_cb-sqlite_perry.ts b/test-files/test_gap_native_payload_cb-sqlite_perry.ts new file mode 100644 index 0000000000..ff3d5779a9 --- /dev/null +++ b/test-files/test_gap_native_payload_cb-sqlite_perry.ts @@ -0,0 +1,160 @@ +// #11919 perry-only expectations for node:sqlite callbacks (CALLBACK-DESIGN +// T6/T7/T10, LIFECYCLE-DESIGN L7/L9). Node 26.5.1 crashes (SIGSEGV) on +// close() inside a running UDF and never collects a database whose UDF +// captures it, so this file is checked against +// test-parity/expected/test_gap_native_payload_cb-sqlite_perry.txt instead of +// node. +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +db.exec("create table t(a integer)"); +const ins = db.prepare("insert into t values(?)"); +for (let i = 0; i < 5; i++) ins.run(i); + +// T7: close() inside a running UDF is deferred to the end of the outer call; +// the outer query reports the closed database, no partial rows, and every +// later call sees it closed. +let ran = 0; +db.function("closer", (x: number) => { + ran++; + if (x === 2) { + db.close(); + console.log("t7 inside isOpen", db.isOpen); + } + return x; +}); +try { + console.log("t7 rows", JSON.stringify(db.prepare("select closer(a) as v from t order by a").all())); +} catch (e: any) { + console.log("t7 outer", e.code, e.message); +} +console.log("t7 ran", ran, "isOpen", db.isOpen); +try { + db.exec("select 1"); +} catch (e: any) { + console.log("t7 later", e.code, e.message); +} + +// open() inside the callback that closed the database is refused; after the +// outer call returns, open() works on the same object. +db.open(); +db.exec("create table u(a integer)"); +db.exec("insert into u values (1), (2)"); +db.function("reopener", (x: number) => { + db.close(); + try { + db.open(); + } catch (e: any) { + console.log("t7 open inside", e.code, e.message); + } + return x; +}); +try { + db.prepare("select reopener(a) from u").all(); +} catch (e: any) { + console.log("t7 outer2", e.code, e.message); +} +db.open(); +console.log("t7 reopened", db.isOpen, (db.prepare("select 41 + 1 as v").get() as any).v); + +// L9: the callback closes and then throws: its exception wins over the +// close, and the database is closed afterwards. +const err = new Error("after close"); +db.function("closeThrow", () => { + db.close(); + throw err; +}); +try { + db.prepare("select closeThrow()").get(); +} catch (e) { + console.log("l9", e === err, db.isOpen); +} + +// T5: the first throw wins: no step runs after it and the aggregate's +// result callback does not run for the failed group (node 26.5.1 calls +// result and drops the error). +let steps = 0; +let finals = 0; +db.open(); +db.exec("create table s(a integer)"); +db.exec("insert into s values (0), (1), (2), (3), (4)"); +db.aggregate("bad2", { + start: 0, + step: (acc: number, x: number) => { + steps++; + if (x >= 2) throw new Error("step " + x); + return acc + x; + }, + result: (acc: number) => { + finals++; + return acc; + }, +}); +try { + db.prepare("select bad2(a) from s").get(); +} catch (e: any) { + console.log("t5", e.message); +} +console.log("t5 steps", steps, "results", finals); +db.close(); + +// A nested call after close() inside the callback gets a catchable closed +// error; the outer call still ends closed. +db.open(); +db.function("nestedAfterClose", () => { + db.close(); + try { + db.exec("select 1"); + return "no"; + } catch (e: any) { + return e.code; + } +}); +try { + console.log("nested", JSON.stringify(db.prepare("select nestedAfterClose() as v").get())); +} catch (e: any) { + console.log("nested outer", e.code, e.message); +} + +// T6: an aggregate group still open when the connection is released (here +// by a close() from its own step) finishes without running JS. +db.open(); +db.exec("create table g(k integer, v integer)"); +db.exec("insert into g values (1, 1), (1, 2), (2, 3), (2, 4)"); +let results = 0; +db.aggregate("closingSum", { + start: 0, + step: (acc: number, v: number) => { + if (v === 3) db.close(); + return acc + v; + }, + result: (acc: number) => { + results++; + return acc; + }, +}); +try { + db.prepare("select k, closingSum(v) from g group by k order by k").all(); +} catch (e: any) { + console.log("t6 outer", e.code, e.message); +} +console.log("t6 result calls after release", results, db.isOpen); + +// T10: databases whose UDF captures the database, never closed: the +// owner <-> payload cycle is ordinary garbage, so RSS stays flat. +function rssMB(): number { + return process.memoryUsage().rss / 1048576; +} +let sum = 0; +function churn(n: number): void { + for (let i = 0; i < n; i++) { + const d = new DatabaseSync(":memory:"); + d.function("f", (x: number) => x + (d.isOpen ? 1 : 0)); + sum += (d.prepare("select f(1) as v").get() as any).v; + } +} +churn(20000); +const before = rssMB(); +churn(200000); +const grew = rssMB() - before; +console.log("t10", sum, grew < 64 ? "flat" : "grew " + Math.round(grew) + "MB"); diff --git a/test-files/test_gap_native_payload_cb-sqlite_worker.ts b/test-files/test_gap_native_payload_cb-sqlite_worker.ts new file mode 100644 index 0000000000..34a8efec6d --- /dev/null +++ b/test-files/test_gap_native_payload_cb-sqlite_worker.ts @@ -0,0 +1,27 @@ +// #11919 T11: a worker opens a node:sqlite database with a UDF that captures +// it and an aggregate, queries it and ends without closing it. Worker +// teardown releases the open database (no JS runs during the release) and the +// main thread carries on with its own database. +import { Worker } from "node:worker_threads"; +import { DatabaseSync } from "node:sqlite"; + +const worker = new Worker( + new URL("./_helpers/gap_cb_sqlite_worker.ts", import.meta.url), +); +const watchdog = setTimeout(() => { + console.log("WORKER STOPPED"); + process.exit(3); +}, 15000); +const first = await new Promise((resolve) => { + worker.on("message", (value: string) => resolve(String(value))); + worker.on("error", (e: Error) => resolve("worker-error " + e.message)); +}); +console.log(first); +await worker.terminate(); +clearTimeout(watchdog); +console.log("terminated"); + +const db = new DatabaseSync(":memory:"); +db.function("g", (x: number) => x * 3); +console.log("main", (db.prepare("select g(14) as v").get() as any).v); +db.close(); diff --git a/test-parity/expected/test_gap_native_payload_cb-sqlite_perry.txt b/test-parity/expected/test_gap_native_payload_cb-sqlite_perry.txt new file mode 100644 index 0000000000..eb66b84626 --- /dev/null +++ b/test-parity/expected/test_gap_native_payload_cb-sqlite_perry.txt @@ -0,0 +1,14 @@ +t7 inside isOpen false +t7 outer ERR_INVALID_STATE database is not open +t7 ran 3 isOpen false +t7 later ERR_INVALID_STATE database is not open +t7 open inside ERR_INVALID_STATE database is closing +t7 outer2 ERR_INVALID_STATE database is not open +t7 reopened true 42 +l9 true false +t5 step 2 +t5 steps 3 results 0 +nested outer ERR_INVALID_STATE database is not open +t6 outer ERR_INVALID_STATE database is not open +t6 result calls after release 1 false +t10 440000 flat