Skip to content

Commit 84c8ad3

Browse files
author
Ralph Küpper
committed
feat(sqlite): add bun:sqlite compatibility
1 parent 8d837df commit 84c8ad3

22 files changed

Lines changed: 658 additions & 12 deletions

File tree

changelog.d/8525-bun-sqlite.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Added
2+
3+
- Add a native `bun:sqlite` compatibility facade backed by the same rusqlite
4+
engine as Perry's `node:sqlite` implementation. `Database` construction,
5+
prepared statements, positional and named parameters, object and array row
6+
modes, blobs, safe integers, transactions, change metadata, serialization,
7+
extension loading, and handle lifetime operations now support OpenCode's Bun
8+
SQLite adapter without leaving an unresolved `bun:` import in the graph.

crates/perry-api-manifest/src/entries.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub const NATIVE_MODULES: &[&str] = &[
5959
// #6562: Bun FFI (C-ABI). The `bun:` prefix is part of the specifier
6060
// (unlike `node:`, which is stripped) — `import { dlopen } from "bun:ffi"`.
6161
"bun:ffi",
62+
"bun:sqlite", // Bun facade over Perry's native SQLite engine
6263
"node-cron", // cron-style scheduler (npm node-cron; aliases `cron`)
6364
"nodemailer", // SMTP email sending
6465
// ── Node.js builtin modules ──

crates/perry-api-manifest/src/entries/part_1.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,26 @@ pub(crate) const API_MANIFEST_PART_1: &[ApiEntry] = &[
230230
.stub_note("stage ≥2 — not yet implemented, throws at runtime (#6562)"),
231231
method("bun:ffi", "read", false, None)
232232
.stub_note("stage ≥2 — not yet implemented, throws at runtime (#6562)"),
233+
// bun:sqlite (#8510) shares node:sqlite's rusqlite handles while keeping
234+
// Bun's public constructor and statement vocabulary.
235+
class("bun:sqlite", "Database"),
236+
class("bun:sqlite", "Statement"),
237+
method("bun:sqlite", "Database", false, None),
238+
method("bun:sqlite", "query", true, Some("Database")),
239+
method("bun:sqlite", "prepare", true, Some("Database")),
240+
method("bun:sqlite", "run", true, Some("Database")),
241+
method("bun:sqlite", "close", true, Some("Database")),
242+
method("bun:sqlite", "serialize", true, Some("Database")),
243+
method("bun:sqlite", "loadExtension", true, Some("Database")),
244+
method("bun:sqlite", "transaction", true, Some("Database")),
245+
property("bun:sqlite", "filename"),
246+
property("bun:sqlite", "inTransaction"),
247+
method("bun:sqlite", "run", true, Some("Statement")),
248+
method("bun:sqlite", "get", true, Some("Statement")),
249+
method("bun:sqlite", "all", true, Some("Statement")),
250+
method("bun:sqlite", "values", true, Some("Statement")),
251+
method("bun:sqlite", "safeIntegers", true, Some("Statement")),
252+
method("bun:sqlite", "finalize", true, Some("Statement")),
233253
class("sqlite", "DatabaseSync"),
234254
class("sqlite", "Session"),
235255
class("sqlite", "SQLTagStore"),

crates/perry-codegen/src/lower_call/builtin.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,29 @@ pub(super) fn lower_builtin_new<'a>(
637637
let handle = blk.call(I64, "js_pg_pool_new", &[(DOUBLE, &config_val)]);
638638
Ok(Some(nanbox_pointer_inline(blk, &handle)))
639639
}
640+
// bun:sqlite Database — distinct internal name avoids colliding with
641+
// better-sqlite3's exported `Database` while preserving full JS values
642+
// for Bun's optional filename and flags object.
643+
"BunSqliteDatabase" => {
644+
let path_idx = adopt_optional_arg(ctx, args, 0, group)?;
645+
let options_idx = adopt_optional_arg(ctx, args, 1, group)?;
646+
let undef = || double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
647+
let path_value = match path_idx {
648+
Some(i) => group.reread(ctx, i)?,
649+
None => undef(),
650+
};
651+
let options_value = match options_idx {
652+
Some(i) => group.reread(ctx, i)?,
653+
None => undef(),
654+
};
655+
let blk = ctx.block();
656+
let handle = blk.call(
657+
I64,
658+
"js_bun_sqlite_database_new",
659+
&[(DOUBLE, &path_value), (DOUBLE, &options_value)],
660+
);
661+
Ok(Some(nanbox_pointer_inline(blk, &handle)))
662+
}
640663
// better-sqlite3 Database — `new Database(filename)` opens a SQLite
641664
// connection. Without this, `new Database(...)` falls into lower_new's
642665
// empty-object placeholder, so `db` is a generic ObjectHeader pointer

crates/perry-codegen/src/lower_call/native_table/databases.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,133 @@ pub(super) const DATABASES_ROWS: &[NativeModSig] = &[
717717
args: &[],
718718
ret: NR_VOID,
719719
},
720+
// ========== bun:sqlite ==========
721+
NativeModSig {
722+
module: "bun:sqlite",
723+
has_receiver: false,
724+
method: "Database",
725+
class_filter: None,
726+
runtime: "js_bun_sqlite_database_call",
727+
args: &[NA_F64, NA_F64],
728+
ret: NR_PTR,
729+
},
730+
NativeModSig {
731+
module: "bun:sqlite",
732+
has_receiver: true,
733+
method: "query",
734+
class_filter: Some("Database"),
735+
runtime: "js_bun_sqlite_database_query",
736+
args: &[NA_F64],
737+
ret: NR_PTR,
738+
},
739+
NativeModSig {
740+
module: "bun:sqlite",
741+
has_receiver: true,
742+
method: "prepare",
743+
class_filter: Some("Database"),
744+
runtime: "js_bun_sqlite_database_query",
745+
args: &[NA_F64],
746+
ret: NR_PTR,
747+
},
748+
NativeModSig {
749+
module: "bun:sqlite",
750+
has_receiver: true,
751+
method: "run",
752+
class_filter: Some("Database"),
753+
runtime: "js_bun_sqlite_database_run",
754+
args: &[NA_F64, NA_VARARGS],
755+
ret: NR_PTR,
756+
},
757+
NativeModSig {
758+
module: "bun:sqlite",
759+
has_receiver: true,
760+
method: "close",
761+
class_filter: Some("Database"),
762+
runtime: "js_node_sqlite_database_sync_close",
763+
args: &[],
764+
ret: NR_I32,
765+
},
766+
NativeModSig {
767+
module: "bun:sqlite",
768+
has_receiver: true,
769+
method: "serialize",
770+
class_filter: Some("Database"),
771+
runtime: "js_node_sqlite_database_sync_serialize",
772+
args: &[NA_F64],
773+
ret: NR_PTR,
774+
},
775+
NativeModSig {
776+
module: "bun:sqlite",
777+
has_receiver: true,
778+
method: "loadExtension",
779+
class_filter: Some("Database"),
780+
runtime: "js_node_sqlite_database_sync_load_extension",
781+
args: &[NA_F64],
782+
ret: NR_I32,
783+
},
784+
NativeModSig {
785+
module: "bun:sqlite",
786+
has_receiver: true,
787+
method: "transaction",
788+
class_filter: Some("Database"),
789+
runtime: "js_bun_sqlite_database_transaction",
790+
args: &[NA_F64],
791+
ret: NR_PTR,
792+
},
793+
NativeModSig {
794+
module: "bun:sqlite",
795+
has_receiver: true,
796+
method: "run",
797+
class_filter: Some("Statement"),
798+
runtime: "js_node_sqlite_statement_sync_run",
799+
args: &[NA_VARARGS],
800+
ret: NR_PTR,
801+
},
802+
NativeModSig {
803+
module: "bun:sqlite",
804+
has_receiver: true,
805+
method: "get",
806+
class_filter: Some("Statement"),
807+
runtime: "js_node_sqlite_statement_sync_get",
808+
args: &[NA_VARARGS],
809+
ret: NR_F64,
810+
},
811+
NativeModSig {
812+
module: "bun:sqlite",
813+
has_receiver: true,
814+
method: "all",
815+
class_filter: Some("Statement"),
816+
runtime: "js_node_sqlite_statement_sync_all",
817+
args: &[NA_VARARGS],
818+
ret: NR_PTR,
819+
},
820+
NativeModSig {
821+
module: "bun:sqlite",
822+
has_receiver: true,
823+
method: "values",
824+
class_filter: Some("Statement"),
825+
runtime: "js_bun_sqlite_statement_values",
826+
args: &[NA_VARARGS],
827+
ret: NR_PTR,
828+
},
829+
NativeModSig {
830+
module: "bun:sqlite",
831+
has_receiver: true,
832+
method: "safeIntegers",
833+
class_filter: Some("Statement"),
834+
runtime: "js_bun_sqlite_statement_safe_integers",
835+
args: &[NA_F64],
836+
ret: NR_F64,
837+
},
838+
NativeModSig {
839+
module: "bun:sqlite",
840+
has_receiver: true,
841+
method: "finalize",
842+
class_filter: Some("Statement"),
843+
runtime: "js_bun_sqlite_statement_finalize",
844+
args: &[],
845+
ret: NR_VOID,
846+
},
720847
// ========== node:sqlite ==========
721848
NativeModSig {
722849
module: "sqlite",

crates/perry-codegen/src/runtime_decls/stdlib_ffi/data_stores.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,19 @@ pub(crate) fn declare_data_stores(module: &mut LlModule) {
103103
module.declare_function("js_sqlite_transaction", I64, &[I64, I64]);
104104
module.declare_function("js_sqlite_transaction_commit", VOID, &[I64]);
105105
module.declare_function("js_sqlite_transaction_rollback", VOID, &[I64]);
106+
module.declare_function("js_bun_sqlite_database_call", I64, &[DOUBLE, DOUBLE]);
107+
module.declare_function("js_bun_sqlite_database_new", I64, &[DOUBLE, DOUBLE]);
108+
module.declare_function("js_bun_sqlite_database_query", I64, &[I64, DOUBLE]);
109+
module.declare_function("js_bun_sqlite_database_run", I64, &[I64, DOUBLE, I64]);
110+
module.declare_function("js_bun_sqlite_database_filename", I64, &[I64]);
111+
module.declare_function("js_bun_sqlite_database_transaction", I64, &[I64, DOUBLE]);
112+
module.declare_function("js_bun_sqlite_statement_values", I64, &[I64, I64]);
113+
module.declare_function(
114+
"js_bun_sqlite_statement_safe_integers",
115+
DOUBLE,
116+
&[I64, DOUBLE],
117+
);
118+
module.declare_function("js_bun_sqlite_statement_finalize", VOID, &[I64]);
106119
module.declare_function("js_node_sqlite_backup", I64, &[DOUBLE, DOUBLE, DOUBLE]);
107120
module.declare_function("js_node_sqlite_database_sync_call", I64, &[DOUBLE, DOUBLE]);
108121
module.declare_function("js_node_sqlite_database_sync_new", I64, &[DOUBLE, DOUBLE]);

crates/perry-hir/src/js_transform/local_natives.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,31 @@ pub fn fix_native_instance_expr_with_locals(
11511151
}
11521152
fix_native_instance_expr_with_locals(inner, native_instances, local_id_instances);
11531153
}
1154+
// #8510: the AST lowerer's any-receiver fallback folds a zero-argument
1155+
// `.values()` into ArrayValues before this pass knows that the local is
1156+
// a bun:sqlite Statement. Recover the native call once the statement
1157+
// result from Database.query()/prepare() has been tracked. Without
1158+
// this, `statement.values()` runs the Array iterator helper against a
1159+
// native statement handle and produces undefined rows.
1160+
Expr::ArrayValues(array) => {
1161+
if let Expr::LocalGet(local_id) = array.as_ref() {
1162+
if matches!(
1163+
local_id_instances.get(local_id),
1164+
Some((module, class)) if module == "bun:sqlite" && class == "Statement"
1165+
) {
1166+
let object = std::mem::replace(array.as_mut(), Expr::Undefined);
1167+
*expr = Expr::NativeMethodCall {
1168+
module: "bun:sqlite".to_string(),
1169+
class_name: Some("Statement".to_string()),
1170+
object: Some(Box::new(object)),
1171+
method: "values".to_string(),
1172+
args: Vec::new(),
1173+
};
1174+
return;
1175+
}
1176+
}
1177+
fix_native_instance_expr_with_locals(array, native_instances, local_id_instances);
1178+
}
11541179
// Recurse into other expressions
11551180
Expr::Binary { left, right, .. } => {
11561181
fix_native_instance_expr_with_locals(left, native_instances, local_id_instances);
@@ -1398,6 +1423,9 @@ pub fn detect_native_instance_creation_with_context(
13981423
("sqlite", "DatabaseSync", "createSession") => {
13991424
Some((module.clone(), "Session".to_string()))
14001425
}
1426+
("bun:sqlite", "Database", "query" | "prepare") => {
1427+
Some((module.clone(), "Statement".to_string()))
1428+
}
14011429
_ => None,
14021430
}
14031431
}
@@ -1434,6 +1462,9 @@ pub fn detect_native_instance_creation_with_context(
14341462
("sqlite", "DatabaseSync", "createSession") => {
14351463
Some((module.clone(), "Session".to_string()))
14361464
}
1465+
("bun:sqlite", "Database", "query" | "prepare") => {
1466+
Some((module.clone(), "Statement".to_string()))
1467+
}
14371468
_ => None,
14381469
};
14391470
}
@@ -1457,6 +1488,7 @@ pub fn detect_native_instance_creation_with_context(
14571488
"Database" => Some(("better-sqlite3".to_string(), "Database".to_string())),
14581489
"DatabaseSync" => Some(("sqlite".to_string(), "DatabaseSync".to_string())),
14591490
"StatementSync" => Some(("sqlite".to_string(), "StatementSync".to_string())),
1491+
"BunSqliteDatabase" => Some(("bun:sqlite".to_string(), "Database".to_string())),
14601492
_ => None,
14611493
}
14621494
}

crates/perry-hir/src/lower/expr_new.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
7373
}
7474

7575
if let ast::Expr::Ident(callee_ident) = callee_expr {
76+
// Keep Bun's `Database` distinct from better-sqlite3's same-named
77+
// constructor while still allocating the shared native SQLite handle.
78+
if matches!(
79+
ctx.lookup_native_module(callee_ident.sym.as_ref()),
80+
Some(("bun:sqlite", Some("Database")))
81+
) {
82+
return Ok(Expr::New {
83+
class_name: "BunSqliteDatabase".to_string(),
84+
args: lower_optional_args(ctx, new_expr.args.as_deref())?,
85+
type_args: Vec::new(),
86+
byte_offset: new_byte_offset,
87+
cap_args_appended: 0,
88+
});
89+
}
7690
let module_constructor = ctx
7791
.lookup_native_module(callee_ident.sym.as_ref())
7892
.map(|(module_name, method)| {

crates/perry-hir/src/lower/expr_new/member.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,15 @@ pub(crate) fn lower_new_member_native(
404404
}
405405
if let Some((module_name, _)) = ctx.lookup_native_module(module_alias) {
406406
let class_name = prop_ident.sym.as_ref();
407+
if module_name == "bun:sqlite" && class_name == "Database" {
408+
return Ok(Some(Expr::New {
409+
class_name: "BunSqliteDatabase".to_string(),
410+
args: lower_optional_args(ctx, new_expr.args.as_deref())?,
411+
type_args: Vec::new(),
412+
byte_offset: new_byte_offset,
413+
cap_args_appended: 0,
414+
}));
415+
}
407416
if matches!(
408417
(module_name, class_name),
409418
("events", "EventEmitter")

crates/perry-hir/src/lower/tests.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,41 @@ fn test_perry_ui_state_value_uses_native_getter() {
646646
);
647647
}
648648

649+
/// #8510: `.values()` on a bun:sqlite Statement is not Array.prototype.values.
650+
/// The statement is discovered by the post-lowering native-instance pass, so
651+
/// that pass must repair the eager any-receiver ArrayValues fold.
652+
#[test]
653+
fn test_bun_sqlite_statement_values_uses_native_dispatch() {
654+
use crate::ir::clear_current_module_source;
655+
use crate::js_transform::fix_local_native_instances;
656+
657+
let source = r#"
658+
import { Database } from "bun:sqlite";
659+
const db = new Database(":memory:");
660+
const statement = db.query("SELECT 1");
661+
const rows = statement.values();
662+
console.log(rows[0][0]);
663+
"#;
664+
let module = perry_parser::parse_typescript(source, "bun_sqlite_values.ts")
665+
.expect("source should parse");
666+
let mut hir =
667+
super::lower_module(&module, "test", "bun_sqlite_values.ts").expect("source should lower");
668+
clear_current_module_source();
669+
fix_local_native_instances(&mut hir);
670+
671+
let dump = format!("{hir:#?}");
672+
assert!(
673+
dump.contains("module: \"bun:sqlite\"")
674+
&& dump.contains("class_name: Some(\n \"Statement\"")
675+
&& dump.contains("method: \"values\""),
676+
"Statement.values() must lower through bun:sqlite native dispatch: {dump}"
677+
);
678+
assert!(
679+
!dump.contains("ArrayValues"),
680+
"Statement.values() must not retain the Array iterator fold: {dump}"
681+
);
682+
}
683+
649684
/// #6642: the Widget `.addChild()` compatibility method must use the same
650685
/// native FFI dispatch as the canonical `widgetAddChild(parent, child)` free
651686
/// function, including for basic widget factories such as VStack and Text.

0 commit comments

Comments
 (0)