Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ ducklink-runtime = { path = "runtime" }
wasmtime = { version = "46.0.1", features = ["component-model"] }
wasmtime-wasi = "46.0.1"
anyhow = "1"
# Manifest parsing for the lifted resolver (de-risk spike for design "D").
serde_json = "1"
# Conformance suite content-digest (the canonical suite_digest the resolver gate
# checks against the manifest's recorded digest).
sha2 = "0.10"

# DuckDB C Extension API (loadable-extension build). Pinned to the DuckDB version
# the community-extensions CI builds against (v1.5.4); bumped per release.
Expand Down
144 changes: 144 additions & 0 deletions runtime/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,12 @@ pub struct ExtensionInstance {
// Lazily-built storage bindings (None until first storage-dispatch call or
// for non-storage extensions).
storage_bindings: Option<crate::duckdb_extension_storage_bindings::DuckdbExtensionStorage>,
// 2.1.0 additive: lazily-built writable-storage bindings (None until first
// storage-write-dispatch call or for read-only storage backends). The types
// are remapped via `with:` to the storage bindings' generated types, so the
// existing `storage_*_to_ext` helpers apply on the return path.
storage_write_bindings:
Option<crate::duckdb_extension_storage_write_bindings::DuckdbExtensionStorageWrite>,
// Item 3 / M2a: lazily-built index bindings (None until first index-dispatch
// call or for non-index extensions).
index_bindings: Option<crate::duckdb_extension_index_bindings::DuckdbExtensionIndex>,
Expand Down Expand Up @@ -1561,6 +1567,7 @@ impl ExtensionInstance {
bindings,
instance,
storage_bindings: None,
storage_write_bindings: None,
index_bindings: None,
files_bindings: None,
}
Expand Down Expand Up @@ -1824,6 +1831,143 @@ impl ExtensionInstance {
.map_err(storage_duckerror_to_ext)
}

// --- 2.1.0 (additive): storage-write-dispatch re-entry ---
//
// Mirrors the storage-dispatch trampolines above but drives the WRITABLE
// half (transactions + DDL + DML) of a storage backend. Types are remapped
// via `with:` in the lib-level bindings module to the SAME
// `extension_types::*` the rest of the runtime uses, so no per-world
// conversion is needed on either the argument or the return path.

/// Builds (once) the writable-storage bindings from the raw instance.
/// Errors if this component does not export storage-write-dispatch (i.e.
/// is not a writable storage backend).
fn storage_write_bindings(
&mut self,
) -> Result<
&crate::duckdb_extension_storage_write_bindings::DuckdbExtensionStorageWrite,
extension_types::Duckerror,
> {
if self.storage_write_bindings.is_none() {
let built =
crate::duckdb_extension_storage_write_bindings::DuckdbExtensionStorageWrite::new(
self.store.as_context_mut(),
&self.instance,
)
.map_err(map_extension_trap)?;
self.storage_write_bindings = Some(built);
}
Ok(self.storage_write_bindings.as_ref().unwrap())
}

/// Begin a write transaction on `catalog`; returns a transaction handle.
pub fn storage_begin_transaction(
&mut self,
handle: u32,
catalog: u32,
) -> Result<u32, extension_types::Duckerror> {
self.storage_write_bindings()?;
let bindings = self.storage_write_bindings.as_ref().unwrap();
let guest = bindings.duckdb_extension_storage_write_dispatch();
let store = &mut self.store;
guest
.call_begin_transaction(store.as_context_mut(), handle, catalog)
.map_err(map_extension_trap)?
}

pub fn storage_commit_transaction(
&mut self,
handle: u32,
txn: u32,
) -> Result<(), extension_types::Duckerror> {
self.storage_write_bindings()?;
let bindings = self.storage_write_bindings.as_ref().unwrap();
let guest = bindings.duckdb_extension_storage_write_dispatch();
let store = &mut self.store;
guest
.call_commit_transaction(store.as_context_mut(), handle, txn)
.map_err(map_extension_trap)?
}

pub fn storage_rollback_transaction(
&mut self,
handle: u32,
txn: u32,
) -> Result<(), extension_types::Duckerror> {
self.storage_write_bindings()?;
let bindings = self.storage_write_bindings.as_ref().unwrap();
let guest = bindings.duckdb_extension_storage_write_dispatch();
let store = &mut self.store;
guest
.call_rollback_transaction(store.as_context_mut(), handle, txn)
.map_err(map_extension_trap)?
}

pub fn storage_create_table(
&mut self,
handle: u32,
txn: u32,
table: &str,
columns: &[extension_types::Columndef],
) -> Result<(), extension_types::Duckerror> {
self.storage_write_bindings()?;
let bindings = self.storage_write_bindings.as_ref().unwrap();
let guest = bindings.duckdb_extension_storage_write_dispatch();
let store = &mut self.store;
guest
.call_create_table(store.as_context_mut(), handle, txn, table, columns)
.map_err(map_extension_trap)?
}

pub fn storage_insert_rows(
&mut self,
handle: u32,
txn: u32,
table: &str,
rows: &[Vec<extension_types::Duckvalue>],
) -> Result<u64, extension_types::Duckerror> {
self.storage_write_bindings()?;
let bindings = self.storage_write_bindings.as_ref().unwrap();
let guest = bindings.duckdb_extension_storage_write_dispatch();
let store = &mut self.store;
guest
.call_insert_rows(store.as_context_mut(), handle, txn, table, rows)
.map_err(map_extension_trap)?
}

pub fn storage_delete_rows(
&mut self,
handle: u32,
txn: u32,
table: &str,
rowids: &[i64],
) -> Result<u64, extension_types::Duckerror> {
self.storage_write_bindings()?;
let bindings = self.storage_write_bindings.as_ref().unwrap();
let guest = bindings.duckdb_extension_storage_write_dispatch();
let store = &mut self.store;
guest
.call_delete_rows(store.as_context_mut(), handle, txn, table, rowids)
.map_err(map_extension_trap)?
}

pub fn storage_update_rows(
&mut self,
handle: u32,
txn: u32,
table: &str,
rowids: &[i64],
rows: &[Vec<extension_types::Duckvalue>],
) -> Result<u64, extension_types::Duckerror> {
self.storage_write_bindings()?;
let bindings = self.storage_write_bindings.as_ref().unwrap();
let guest = bindings.duckdb_extension_storage_write_dispatch();
let store = &mut self.store;
guest
.call_update_rows(store.as_context_mut(), handle, txn, table, rowids, rows)
.map_err(map_extension_trap)?
}

// --- Item 3 / M2a: index-dispatch (custom index build + search) re-entry ---
// Mirrors the storage-dispatch `storage_*` methods but drives the component's
// exported `index-dispatch` interface. The HNSW (or other ANN) build happens
Expand Down
23 changes: 23 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,29 @@ pub mod duckdb_extension_storage_bindings {
});
}

/// Bindings for the writable-storage world (`duckdb-extension-storage-write`,
/// additive within contract-major 2), which additionally exports
/// `storage-write-dispatch` on top of the read-only `storage-dispatch`. Only
/// writable storage backends satisfy this; the runtime builds these bindings
/// lazily from an already-loaded component instance so non-writable storage
/// backends (which don't export storage-write-dispatch) still load against the
/// read-only storage world above.
///
/// Types (`duckerror`, `duckvalue`, `columndef`) are remapped to the BASE
/// bindings' generated types via `with:`, so the write trampolines exchange
/// the same `extension_types::*` the rest of the runtime uses -- no per-world
/// conversion on either the argument or the return path.
pub mod duckdb_extension_storage_write_bindings {
wasmtime::component::bindgen!({
path: "./wit",
world: "duckdb:extension-host/duckdb-extension-storage-write",
require_store_data_send: true,
with: {
"duckdb:extension/types@2.0.0": crate::duckdb_extension_bindings::duckdb::extension::types,
},
});
}

/// Bindings for the index-capable world (`duckdb-extension-index`), which
/// additionally exports `index-dispatch` (Item 3 / M2a custom index). Only
/// custom-index backend components (e.g. hnswfns) satisfy this; the runtime
Expand Down
48 changes: 48 additions & 0 deletions runtime/wit-canonical/duckdb-extension/storage-write-dispatch.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package duckdb:extension@2.0.0;

use types;

// Host -> component callbacks for the WRITABLE half of a storage backend
// (additive, 2.1.0). The read-only `storage-dispatch` (attach/list/scan) is left
// INTACT; this adds the mutating path -- transactions, DDL, and DML -- for
// storage backends that can write through. Exported by writable storage
// components via the separate `duckdb-extension-storage-write` world.
//
// Every call carries `handle` = the callback-handle the component passed to
// storage.register-storage; `catalog` is the catalog handle from
// storage-dispatch.storage-attach.
interface storage-write-dispatch {
use types.{duckerror, duckvalue, columndef};

// Begin a transaction on `catalog`; returns a transaction handle.
begin-transaction: func(handle: u32, catalog: u32) -> result<u32, duckerror>;

commit-transaction: func(handle: u32, txn: u32) -> result<_, duckerror>;
rollback-transaction: func(handle: u32, txn: u32) -> result<_, duckerror>;

// CREATE TABLE within `txn`.
create-table: func(handle: u32,
txn: u32,
table: string,
columns: list<columndef>) -> result<_, duckerror>;

// Append rows; returns the number inserted.
insert-rows: func(handle: u32,
txn: u32,
table: string,
rows: list<list<duckvalue>>) -> result<u64, duckerror>;

// Delete rows by row-id; returns the number deleted.
delete-rows: func(handle: u32,
txn: u32,
table: string,
rowids: list<s64>) -> result<u64, duckerror>;

// Update rows by row-id (parallel `rowids` / `rows`); returns the number
// updated.
update-rows: func(handle: u32,
txn: u32,
table: string,
rowids: list<s64>,
rows: list<list<duckvalue>>) -> result<u64, duckerror>;
}
48 changes: 48 additions & 0 deletions runtime/wit/deps/duckdb-extension/storage-write-dispatch.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package duckdb:extension@2.0.0;

use types;

// Host -> component callbacks for the WRITABLE half of a storage backend
// (additive, 2.1.0). The read-only `storage-dispatch` (attach/list/scan) is left
// INTACT; this adds the mutating path -- transactions, DDL, and DML -- for
// storage backends that can write through. Exported by writable storage
// components via the separate `duckdb-extension-storage-write` world.
//
// Every call carries `handle` = the callback-handle the component passed to
// storage.register-storage; `catalog` is the catalog handle from
// storage-dispatch.storage-attach.
interface storage-write-dispatch {
use types.{duckerror, duckvalue, columndef};

// Begin a transaction on `catalog`; returns a transaction handle.
begin-transaction: func(handle: u32, catalog: u32) -> result<u32, duckerror>;

commit-transaction: func(handle: u32, txn: u32) -> result<_, duckerror>;
rollback-transaction: func(handle: u32, txn: u32) -> result<_, duckerror>;

// CREATE TABLE within `txn`.
create-table: func(handle: u32,
txn: u32,
table: string,
columns: list<columndef>) -> result<_, duckerror>;

// Append rows; returns the number inserted.
insert-rows: func(handle: u32,
txn: u32,
table: string,
rows: list<list<duckvalue>>) -> result<u64, duckerror>;

// Delete rows by row-id; returns the number deleted.
delete-rows: func(handle: u32,
txn: u32,
table: string,
rowids: list<s64>) -> result<u64, duckerror>;

// Update rows by row-id (parallel `rowids` / `rows`); returns the number
// updated.
update-rows: func(handle: u32,
txn: u32,
table: string,
rowids: list<s64>,
rows: list<list<duckvalue>>) -> result<u64, duckerror>;
}
21 changes: 21 additions & 0 deletions runtime/wit/duckdb-extension-host.wit
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use duckdb:extension/catalog@2.0.0;
use duckdb:extension/files@2.0.0;
use duckdb:extension/storage@2.0.0;
use duckdb:extension/storage-dispatch@2.0.0;
use duckdb:extension/storage-write-dispatch@2.0.0;
use duckdb:extension/index@2.0.0;
use duckdb:extension/index-dispatch@2.0.0;
use duckdb:extension/collation@2.0.0;
Expand Down Expand Up @@ -76,6 +77,26 @@ world duckdb-extension-index {
export index-dispatch;
}

// Writable-storage world (additive, 2.1.0 within contract-major 2): additionally
// exports storage-write-dispatch on top of the read-only storage-dispatch. Only
// storage backends that can write through satisfy this; the runtime instantiates
// its bindings lazily from an already-loaded ExtensionInstance. Existing @2.0.0
// components do NOT export storage-write-dispatch and keep loading un-rebuilt.
world duckdb-extension-storage-write {
import duckdb:extension/runtime@2.0.0;
import duckdb:extension/config@2.0.0;
import duckdb:extension/logging@2.0.0;
import duckdb:extension/catalog@2.0.0;
import duckdb:extension/files@2.0.0;
import duckdb:extension/storage@2.0.0;
import duckdb:extension/index@2.0.0;
import duckdb:extension/collation@2.0.0;
export duckdb:extension/guest@2.0.0;
export callback-dispatch;
export storage-dispatch;
export storage-write-dispatch;
}

// Files-capable world: additionally exports file-dispatch. Only files backend
// components (e.g. webfs) satisfy this; the runtime instantiates its bindings
// lazily from an already-loaded ExtensionInstance.
Expand Down
Loading