@@ -8804,6 +8804,11 @@ pub extern "C-unwind" fn knot_value_call(
8804) -> *mut Value {
8804) -> *mut Value {
8805 match unsafe { as_ref(func) } {
8805 match unsafe { as_ref(func) } {
8806 Value::Function(f) => {
8806 Value::Function(f) => {
8807+ // Establish the callee's db as this thread's current-db for the
8808+ // duration of the call, so a `base.compile` inside it (or inside a
8809+ // deeper call) runs its snippet on this handle — sharing any
8810+ // enclosing `atomic` SAVEPOINT — then restore the caller's.
8811+ let _db_scope = RestoreDb::new(db);
8807 let fun: extern "C-unwind" fn(*mut c_void, *mut Value, *mut Value) -> *mut Value =
8812 let fun: extern "C-unwind" fn(*mut c_void, *mut Value, *mut Value) -> *mut Value =
8808 unsafe { std::mem::transmute(f.fn_ptr) };
8813 unsafe { std::mem::transmute(f.fn_ptr) };
8809 fun(db, f.env, arg)
8814 fun(db, f.env, arg)
@@ -9472,6 +9477,9 @@ pub extern "C-unwind" fn knot_io_pure(val: *mut Value) -> *mut Value {
9472/// Execute an IO thunk. If the value is not IO, return it as-is.
9477/// Execute an IO thunk. If the value is not IO, return it as-is.
9473#[unsafe(no_mangle)]
9478#[unsafe(no_mangle)]
9474pub extern "C-unwind" fn knot_io_run(db: *mut c_void, val: *mut Value) -> *mut Value {
9479pub extern "C-unwind" fn knot_io_run(db: *mut c_void, val: *mut Value) -> *mut Value {
9480+ // Current-db for the whole IO chain: any `base.compile` reached while
9481+ // running these thunks runs on this handle (sharing an enclosing atomic).
9482+ let _db_scope = RestoreDb::new(db);
9475 let mut current = val;
9483 let mut current = val;
9477 // Cooperative cancellation: a `race` loser exits its IO chain at the
9485 // Cooperative cancellation: a `race` loser exits its IO chain at the
@@ -11857,16 +11865,28 @@ fn compile_run(
11857 Some("no JIT linked (knot-compile-rt not registered)".into()),
11865 Some("no JIT linked (knot-compile-rt not registered)".into()),
11860− // Open a db handle for the snippet. knot_db_open records the path on first
11868+ // Run the snippet on the CALLER's live db handle when one is active
11861− // open (DB_PATH), so reuse it; a pure program (no store) gets :memory:.
11869+ // (the normal case: the host program called `base.compile` from running
11870+ // code, so CURRENT_DB holds its handle). This makes the compiled value
11871+ // share the host's connection — and any enclosing `atomic` SAVEPOINT — so
11872+ // its reads see uncommitted writes and its writes roll back with the
11873+ // transaction. When there is no caller (the compiler's const-eval thread),
11874+ // fall back to opening a connection: reuse the recorded host path so a
11862− // A snippet reading the host's persisted relations thus sees real data.
11875+ // snippet reading persisted relations sees real data, else `:memory:`.
11877+ let cur = current_db();
11878+ if !cur.is_null() {
11863− let db_path = DB_PATH.lock().unwrap_or_else(|e| e.into_inner()).clone();
11881+ let db_path = DB_PATH.lock().unwrap_or_else(|e| e.into_inner()).clone();
11864− let (pstr, plen) = if db_path.is_empty() {
11882+ let (pstr, plen) = if db_path.is_empty() {
11865− (":memory:".as_bytes().as_ptr(), ":memory:".len())
11883+ (":memory:".as_bytes().as_ptr(), ":memory:".len())
11866− } else {
11884+ } else {
11867− (db_path.as_ptr(), db_path.len())
11885+ (db_path.as_ptr(), db_path.len())
11887+ knot_db_open(pstr, plen)
11869− let db = knot_db_open(pstr, plen);
11871 // The host's expected type (source-annotation string) forwarded to the JIT,
11891 // The host's expected type (source-annotation string) forwarded to the JIT,
11872 // which runs the real-type subsumption. Empty → unpinned.
11892 // which runs the real-type subsumption. Empty → unpinned.
@@ -14272,6 +14292,9 @@ pub extern "C-unwind" fn knot_run_user_main(
14272) -> *mut c_void {
14292) -> *mut c_void {
14273 let f: extern "C-unwind" fn(*mut c_void) -> *mut c_void =
14293 let f: extern "C-unwind" fn(*mut c_void) -> *mut c_void =
14274 unsafe { std::mem::transmute(main_fn) };
14294 unsafe { std::mem::transmute(main_fn) };
14295+ // Establish the program's db as current-db so a top-level `base.compile`
14296+ // (before any knot_value_call/knot_io_run) runs its snippet on this handle.
14297+ let _db_scope = RestoreDb::new(db);
14275 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(db))) {
14298 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(db))) {
14276 Ok(v) => v,
14299 Ok(v) => v,
14277 Err(payload) => {
14300 Err(payload) => {
@@ -19694,6 +19717,45 @@ pub extern "C-unwind" fn knot_constraint_register(
19695// ── Atomic (transactions) ─────────────────────────────────────────
19718// ── Atomic (transactions) ─────────────────────────────────────────
19721+ /// The db handle of the knot code currently executing on this thread —
19722+ /// the handle the innermost active `knot_user_main`/`knot_value_call`/
19723+ /// `knot_io_run` was invoked with. `base.compile` runs its snippet on this
19724+ /// handle (rather than opening a fresh connection) so a compiled value used
19725+ /// inside the host's `atomic` block shares the host's SAVEPOINT: its reads
19726+ /// see uncommitted writes and its own writes roll back with the transaction.
19728+ /// Set on entry to the foreign-boundary crossings (`knot_run_user_main`,
19729+ /// `knot_value_call`, `knot_io_run`) and restored on return, so a nested
19730+ /// `compile`'s snippet doesn't clobber the host's current-db after the
19731+ /// snippet returns. Null outside any knot execution (e.g. the compiler's
19732+ /// const-eval thread), where `compile` falls back to its own connection.
19733+ static CURRENT_DB: std::cell::Cell<*mut c_void> = const { std::cell::Cell::new(std::ptr::null_mut()) };
19736+/// Set the current-db for this thread, returning the previous value so the
19737+/// caller can restore it on scope exit (see [`RestoreDb`]).
19738+fn set_current_db(db: *mut c_void) -> *mut c_void {
19739+ CURRENT_DB.with(|c| c.replace(db))
19742+/// RAII guard restoring the previous current-db on drop.
19743+struct RestoreDb(*mut c_void);
19745+ fn new(db: *mut c_void) -> Self {
19746+ RestoreDb(set_current_db(db))
19749+impl Drop for RestoreDb {
19750+ fn drop(&mut self) {
19751+ set_current_db(self.0);
19755+/// The current thread's db handle, or null when outside knot execution.
19756+fn current_db() -> *mut c_void {
19757+ CURRENT_DB.with(|c| c.get())
19698#[unsafe(no_mangle)]
19760#[unsafe(no_mangle)]
19699pub extern "C-unwind" fn knot_atomic_begin(db: *mut c_void) {
19761pub extern "C-unwind" fn knot_atomic_begin(db: *mut c_void) {