From b776dbcd6119ed8992f70d96fa2590f072eae451 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 25 Jun 2026 08:03:38 -0400 Subject: [PATCH 01/10] fix(bridge): exhaustive duckvalue/logicaltype matches + mutation-killing tests The WIT type contract grew 14 variants (int8/16/32, uint8/16/32, float32, date, time, timestamp, timestamptz, decimal, interval, uuid); the native bridge's duckvalue/logicaltype matches in engine.rs + reg_duckdb.rs were non-exhaustive and would fail to compile against the new contract (latent -- this crate isn't in the wasm smoke path). Add complete 1:1 mappings + a type_code widening map. Surfaced by cargo-mutants on reg_duckdb.rs: add tests that kill the safety-relevant survivors -- type_code_maps_every_logical_type, logical_type_maps_every_code, row_valid_reads_validity_bitmask (the NULL validity-mask predicate). The param_to_neutral per-type arms stay documented un-killed (duckdb::vtab::Value is an opaque FFI wrapper exercisable only e2e). --- src/engine.rs | 71 ++++++++++++++++++++------ src/reg_duckdb.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 175 insertions(+), 19 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 168c9ab..ab48daf 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -348,25 +348,68 @@ impl Engine2 { } fn neutral_to_wit(v: reg::DuckValue) -> extension_types::Duckvalue { + use extension_types::Duckvalue as W; match v { - reg::DuckValue::Null => extension_types::Duckvalue::Null, - reg::DuckValue::Boolean(b) => extension_types::Duckvalue::Boolean(b), - reg::DuckValue::Int64(i) => extension_types::Duckvalue::Int64(i), - reg::DuckValue::Uint64(u) => extension_types::Duckvalue::Uint64(u), - reg::DuckValue::Float64(f) => extension_types::Duckvalue::Float64(f), - reg::DuckValue::Text(s) => extension_types::Duckvalue::Text(s), - reg::DuckValue::Blob(b) => extension_types::Duckvalue::Blob(b), + reg::DuckValue::Null => W::Null, + reg::DuckValue::Boolean(b) => W::Boolean(b), + reg::DuckValue::Int64(i) => W::Int64(i), + reg::DuckValue::Uint64(u) => W::Uint64(u), + reg::DuckValue::Float64(f) => W::Float64(f), + reg::DuckValue::Text(s) => W::Text(s), + reg::DuckValue::Blob(b) => W::Blob(b), + reg::DuckValue::Int8(i) => W::Int8(i), + reg::DuckValue::Int16(i) => W::Int16(i), + reg::DuckValue::Int32(i) => W::Int32(i), + reg::DuckValue::Uint8(u) => W::Uint8(u), + reg::DuckValue::Uint16(u) => W::Uint16(u), + reg::DuckValue::Uint32(u) => W::Uint32(u), + reg::DuckValue::Float32(f) => W::Float32(f), + reg::DuckValue::Date(d) => W::Date(d), + reg::DuckValue::Time(t) => W::Time(t), + reg::DuckValue::Timestamp(t) => W::Timestamp(t), + reg::DuckValue::Timestamptz(t) => W::Timestamptz(t), + reg::DuckValue::Decimal { lower, upper, width, scale } => { + W::Decimal(extension_types::Decimalvalue { lower, upper, width, scale }) + } + reg::DuckValue::Interval { months, days, micros } => { + W::Interval(extension_types::Intervalvalue { months, days, micros }) + } + reg::DuckValue::Uuid { hi, lo } => W::Uuid(extension_types::Uuidvalue { hi, lo }), } } fn wit_to_neutral(v: extension_types::Duckvalue) -> reg::DuckValue { + use extension_types::Duckvalue as W; match v { - extension_types::Duckvalue::Null => reg::DuckValue::Null, - extension_types::Duckvalue::Boolean(b) => reg::DuckValue::Boolean(b), - extension_types::Duckvalue::Int64(i) => reg::DuckValue::Int64(i), - extension_types::Duckvalue::Uint64(u) => reg::DuckValue::Uint64(u), - extension_types::Duckvalue::Float64(f) => reg::DuckValue::Float64(f), - extension_types::Duckvalue::Text(s) => reg::DuckValue::Text(s), - extension_types::Duckvalue::Blob(b) => reg::DuckValue::Blob(b), + W::Null => reg::DuckValue::Null, + W::Boolean(b) => reg::DuckValue::Boolean(b), + W::Int64(i) => reg::DuckValue::Int64(i), + W::Uint64(u) => reg::DuckValue::Uint64(u), + W::Float64(f) => reg::DuckValue::Float64(f), + W::Text(s) => reg::DuckValue::Text(s), + W::Blob(b) => reg::DuckValue::Blob(b), + W::Int8(i) => reg::DuckValue::Int8(i), + W::Int16(i) => reg::DuckValue::Int16(i), + W::Int32(i) => reg::DuckValue::Int32(i), + W::Uint8(u) => reg::DuckValue::Uint8(u), + W::Uint16(u) => reg::DuckValue::Uint16(u), + W::Uint32(u) => reg::DuckValue::Uint32(u), + W::Float32(f) => reg::DuckValue::Float32(f), + W::Date(d) => reg::DuckValue::Date(d), + W::Time(t) => reg::DuckValue::Time(t), + W::Timestamp(t) => reg::DuckValue::Timestamp(t), + W::Timestamptz(t) => reg::DuckValue::Timestamptz(t), + W::Decimal(d) => reg::DuckValue::Decimal { + lower: d.lower, + upper: d.upper, + width: d.width, + scale: d.scale, + }, + W::Interval(iv) => reg::DuckValue::Interval { + months: iv.months, + days: iv.days, + micros: iv.micros, + }, + W::Uuid(u) => reg::DuckValue::Uuid { hi: u.hi, lo: u.lo }, } } diff --git a/src/reg_duckdb.rs b/src/reg_duckdb.rs index fad9906..2ce071e 100644 --- a/src/reg_duckdb.rs +++ b/src/reg_duckdb.rs @@ -77,16 +77,36 @@ const T_BOOL: u8 = 3; const T_TEXT: u8 = 4; const T_BLOB: u8 = 5; -/// Map a neutral logical type to a bridge type code. All current `reg` -/// logical types are supported. +/// Map a neutral logical type to a bridge type code. +/// +/// The native bridge's scalar fast path exchanges six physical type codes. The +/// `reg::LogicalType` enum carries finer-grained DuckDB types (narrower ints, +/// temporal, decimal, uuid); for SQL-signature purposes those are widened to the +/// nearest supported code so a function declaring such an argument still +/// registers (rather than panicking on a non-exhaustive match). Signed/temporal +/// types -> BIGINT, unsigned -> UBIGINT, floats -> DOUBLE, decimal/uuid -> TEXT. fn type_code(lt: reg::LogicalType) -> u8 { match lt { - reg::LogicalType::Int64 => T_I64, - reg::LogicalType::Uint64 => T_U64, - reg::LogicalType::Float64 => T_F64, reg::LogicalType::Boolean => T_BOOL, reg::LogicalType::Text => T_TEXT, reg::LogicalType::Blob => T_BLOB, + // Signed integers + temporal types fit a 64-bit signed slot. + reg::LogicalType::Int8 + | reg::LogicalType::Int16 + | reg::LogicalType::Int32 + | reg::LogicalType::Int64 + | reg::LogicalType::Date + | reg::LogicalType::Time + | reg::LogicalType::Timestamp + | reg::LogicalType::Timestamptz => T_I64, + // Unsigned integers fit a 64-bit unsigned slot. + reg::LogicalType::Uint8 + | reg::LogicalType::Uint16 + | reg::LogicalType::Uint32 + | reg::LogicalType::Uint64 => T_U64, + reg::LogicalType::Float32 | reg::LogicalType::Float64 => T_F64, + // No native physical slot in the bridge; carried as text. + reg::LogicalType::Decimal | reg::LogicalType::Interval | reg::LogicalType::Uuid => T_TEXT, } } @@ -185,6 +205,7 @@ fn read_col_into( /// path skips it entirely -- `read_arg` yields `WitVal` and the dispatcher /// returns `WitVal`, so nothing on that path touches `reg::DuckValue`. fn neutral_to_wit(v: reg::DuckValue) -> WitVal { + use ducklink_runtime::duckdb_extension_bindings::duckdb::extension::types as wt; match v { reg::DuckValue::Null => WitVal::Null, reg::DuckValue::Boolean(b) => WitVal::Boolean(b), @@ -193,6 +214,24 @@ fn neutral_to_wit(v: reg::DuckValue) -> WitVal { reg::DuckValue::Float64(f) => WitVal::Float64(f), reg::DuckValue::Text(s) => WitVal::Text(s), reg::DuckValue::Blob(b) => WitVal::Blob(b), + reg::DuckValue::Int8(i) => WitVal::Int8(i), + reg::DuckValue::Int16(i) => WitVal::Int16(i), + reg::DuckValue::Int32(i) => WitVal::Int32(i), + reg::DuckValue::Uint8(u) => WitVal::Uint8(u), + reg::DuckValue::Uint16(u) => WitVal::Uint16(u), + reg::DuckValue::Uint32(u) => WitVal::Uint32(u), + reg::DuckValue::Float32(f) => WitVal::Float32(f), + reg::DuckValue::Date(d) => WitVal::Date(d), + reg::DuckValue::Time(t) => WitVal::Time(t), + reg::DuckValue::Timestamp(t) => WitVal::Timestamp(t), + reg::DuckValue::Timestamptz(t) => WitVal::Timestamptz(t), + reg::DuckValue::Decimal { lower, upper, width, scale } => { + WitVal::Decimal(wt::Decimalvalue { lower, upper, width, scale }) + } + reg::DuckValue::Interval { months, days, micros } => { + WitVal::Interval(wt::Intervalvalue { months, days, micros }) + } + reg::DuckValue::Uuid { hi, lo } => WitVal::Uuid(wt::Uuidvalue { hi, lo }), } } @@ -1066,4 +1105,78 @@ mod tests { ); assert!(msg.contains("kaboom 42"), "missing payload, got: {msg}"); } + + // --- mutation-testing survivors (cargo-mutants on reg_duckdb.rs) -------- + // The end-to-end sample component is BIGINT-only, so the per-type marshalling + // arms (and the type<->code maps) were untested -- cargo-mutants survived + // mutations that delete/alter them. These direct unit tests pin the mapping + // and the NULL-bitmask logic, killing those survivors without an FFI fixture. + + /// Kills `type_code -> 0` and exercises every `reg::LogicalType` arm: each + /// logical type must widen to its documented bridge code. + #[test] + fn type_code_maps_every_logical_type() { + use reg::LogicalType as L; + // Signed + temporal -> BIGINT slot. + for lt in [L::Int8, L::Int16, L::Int32, L::Int64, L::Date, L::Time, L::Timestamp, L::Timestamptz] { + assert_eq!(type_code(lt), T_I64, "{lt:?} should map to T_I64"); + } + // Unsigned -> UBIGINT slot. + for lt in [L::Uint8, L::Uint16, L::Uint32, L::Uint64] { + assert_eq!(type_code(lt), T_U64, "{lt:?} should map to T_U64"); + } + assert_eq!(type_code(L::Float32), T_F64); + assert_eq!(type_code(L::Float64), T_F64); + assert_eq!(type_code(L::Boolean), T_BOOL); + assert_eq!(type_code(L::Text), T_TEXT); + assert_eq!(type_code(L::Blob), T_BLOB); + // No native slot -> carried as text. + assert_eq!(type_code(L::Decimal), T_TEXT); + assert_eq!(type_code(L::Interval), T_TEXT); + assert_eq!(type_code(L::Uuid), T_TEXT); + // The codes are distinct (so a single-constant mutant can't pass). + let codes = [T_I64, T_U64, T_F64, T_BOOL, T_TEXT, T_BLOB]; + for (a, x) in codes.iter().enumerate() { + for (b, y) in codes.iter().enumerate() { + assert_eq!(a == b, x == y, "bridge type codes must be distinct"); + } + } + } + + /// Kills the `delete match arm T_*` survivors in `logical_type`: each bridge + /// code must resolve to the right DuckDB LogicalTypeId. + #[test] + fn logical_type_maps_every_code() { + assert_eq!(logical_type(T_I64).id(), LogicalTypeId::Bigint); + assert_eq!(logical_type(T_U64).id(), LogicalTypeId::UBigint); + assert_eq!(logical_type(T_F64).id(), LogicalTypeId::Double); + assert_eq!(logical_type(T_BOOL).id(), LogicalTypeId::Boolean); + assert_eq!(logical_type(T_TEXT).id(), LogicalTypeId::Varchar); + assert_eq!(logical_type(T_BLOB).id(), LogicalTypeId::Blob); + } + + /// Kills the `read_col_into` is-null-predicate survivors (the `&&`/`!` + /// mutations at the NULL detection in TEXT/BLOB column reads). `row_valid` + /// reads a DuckDB validity bitmask; a 0-bit means NULL, a 1-bit means valid. + #[test] + fn row_valid_reads_validity_bitmask() { + // Mask: rows 0,2,3 valid (bits 0,2,3 set), row 1 NULL (bit 1 clear). + // Bit 65 set, bit 64 clear -> exercises the second u64 word (r/64, r%64). + let mask: [u64; 2] = [0b1101, 0b10]; + let p = mask.as_ptr(); + unsafe { + assert!(row_valid(p, 0), "bit 0 set -> valid"); + assert!(!row_valid(p, 1), "bit 1 clear -> NULL"); + assert!(row_valid(p, 2), "bit 2 set -> valid"); + assert!(row_valid(p, 3), "bit 3 set -> valid"); + assert!(!row_valid(p, 64), "bit 64 clear -> NULL (second word)"); + assert!(row_valid(p, 65), "bit 65 set -> valid (second word)"); + } + } + + // NOTE: the `param_to_neutral` per-type arm survivors are NOT killed here: + // `duckdb::vtab::Value` is an opaque FFI wrapper with no public constructors, + // so its arms can only be exercised through a real table-function call with + // typed parameters (an end-to-end fixture with a multi-type component), not a + // pure unit test. Left documented rather than forced. } From d4241ff0ca3929c828fd3ac1abf69876eb80fb67 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 25 Jun 2026 09:39:20 -0400 Subject: [PATCH 02/10] feat(types): handle the complex (nested) duckvalue/logicaltype case Add complex() arms in engine.rs + reg_duckdb.rs and make type_code by-ref for the now-variant Logicaltype, so the native bridge compiles against the escape-hatch contract. (This crate isn't in the wasm smoke path, so it must be checked explicitly on a types bump.) --- src/engine.rs | 7 +++++++ src/reg_duckdb.rs | 39 ++++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index ab48daf..40f237e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -375,6 +375,9 @@ fn neutral_to_wit(v: reg::DuckValue) -> extension_types::Duckvalue { W::Interval(extension_types::Intervalvalue { months, days, micros }) } reg::DuckValue::Uuid { hi, lo } => W::Uuid(extension_types::Uuidvalue { hi, lo }), + reg::DuckValue::Complex { type_expr, json } => { + W::Complex(extension_types::Complexvalue { type_expr, json }) + } } } @@ -411,5 +414,9 @@ fn wit_to_neutral(v: extension_types::Duckvalue) -> reg::DuckValue { micros: iv.micros, }, W::Uuid(u) => reg::DuckValue::Uuid { hi: u.hi, lo: u.lo }, + W::Complex(c) => reg::DuckValue::Complex { + type_expr: c.type_expr, + json: c.json, + }, } } diff --git a/src/reg_duckdb.rs b/src/reg_duckdb.rs index 2ce071e..d0894a5 100644 --- a/src/reg_duckdb.rs +++ b/src/reg_duckdb.rs @@ -85,7 +85,7 @@ const T_BLOB: u8 = 5; /// nearest supported code so a function declaring such an argument still /// registers (rather than panicking on a non-exhaustive match). Signed/temporal /// types -> BIGINT, unsigned -> UBIGINT, floats -> DOUBLE, decimal/uuid -> TEXT. -fn type_code(lt: reg::LogicalType) -> u8 { +fn type_code(lt: ®::LogicalType) -> u8 { match lt { reg::LogicalType::Boolean => T_BOOL, reg::LogicalType::Text => T_TEXT, @@ -107,6 +107,8 @@ fn type_code(lt: reg::LogicalType) -> u8 { reg::LogicalType::Float32 | reg::LogicalType::Float64 => T_F64, // No native physical slot in the bridge; carried as text. reg::LogicalType::Decimal | reg::LogicalType::Interval | reg::LogicalType::Uuid => T_TEXT, + // ESCAPE-HATCH: the bridge has no nested-type slot; carry as text. + reg::LogicalType::Complex(_) => T_TEXT, } } @@ -232,6 +234,9 @@ fn neutral_to_wit(v: reg::DuckValue) -> WitVal { WitVal::Interval(wt::Intervalvalue { months, days, micros }) } reg::DuckValue::Uuid { hi, lo } => WitVal::Uuid(wt::Uuidvalue { hi, lo }), + reg::DuckValue::Complex { type_expr, json } => { + WitVal::Complex(wt::Complexvalue { type_expr, json }) + } } } @@ -411,8 +416,8 @@ pub fn register_scalars( ) -> duckdb::Result { let mut registered = 0usize; for f in scalars { - let arg_codes: Vec = f.arguments.iter().map(|a| type_code(a.logical)).collect(); - let ret_code = type_code(f.returns); + let arg_codes: Vec = f.arguments.iter().map(|a| type_code(&a.logical)).collect(); + let ret_code = type_code(&f.returns); let state = WasmScalarState { callback_handle: f.callback_handle, engine: engine.clone(), @@ -563,8 +568,8 @@ pub fn register_tables( ) -> duckdb::Result { let mut registered = 0usize; for t in tables { - let arg_codes: Vec = t.arguments.iter().map(|a| type_code(a.logical)).collect(); - let col_codes: Vec = t.columns.iter().map(|c| type_code(c.logical)).collect(); + let arg_codes: Vec = t.arguments.iter().map(|a| type_code(&a.logical)).collect(); + let col_codes: Vec = t.columns.iter().map(|c| type_code(&c.logical)).collect(); let col_names: Vec = t.columns.iter().map(|c| c.name.clone()).collect(); let extra = WasmTableExtra { callback_handle: t.callback_handle, @@ -815,8 +820,8 @@ pub unsafe fn register_aggregates( ) -> duckdb::Result { let mut registered = 0usize; for f in aggregates { - let arg_codes: Vec = f.arguments.iter().map(|a| type_code(a.logical)).collect(); - let ret_code = type_code(f.returns); + let arg_codes: Vec = f.arguments.iter().map(|a| type_code(&a.logical)).collect(); + let ret_code = type_code(&f.returns); let func = ffi::duckdb_create_aggregate_function(); let cname = CString::new(f.name.as_str()) @@ -1119,21 +1124,21 @@ mod tests { use reg::LogicalType as L; // Signed + temporal -> BIGINT slot. for lt in [L::Int8, L::Int16, L::Int32, L::Int64, L::Date, L::Time, L::Timestamp, L::Timestamptz] { - assert_eq!(type_code(lt), T_I64, "{lt:?} should map to T_I64"); + assert_eq!(type_code(<), T_I64, "{lt:?} should map to T_I64"); } // Unsigned -> UBIGINT slot. for lt in [L::Uint8, L::Uint16, L::Uint32, L::Uint64] { - assert_eq!(type_code(lt), T_U64, "{lt:?} should map to T_U64"); + assert_eq!(type_code(<), T_U64, "{lt:?} should map to T_U64"); } - assert_eq!(type_code(L::Float32), T_F64); - assert_eq!(type_code(L::Float64), T_F64); - assert_eq!(type_code(L::Boolean), T_BOOL); - assert_eq!(type_code(L::Text), T_TEXT); - assert_eq!(type_code(L::Blob), T_BLOB); + assert_eq!(type_code(&L::Float32), T_F64); + assert_eq!(type_code(&L::Float64), T_F64); + assert_eq!(type_code(&L::Boolean), T_BOOL); + assert_eq!(type_code(&L::Text), T_TEXT); + assert_eq!(type_code(&L::Blob), T_BLOB); // No native slot -> carried as text. - assert_eq!(type_code(L::Decimal), T_TEXT); - assert_eq!(type_code(L::Interval), T_TEXT); - assert_eq!(type_code(L::Uuid), T_TEXT); + assert_eq!(type_code(&L::Decimal), T_TEXT); + assert_eq!(type_code(&L::Interval), T_TEXT); + assert_eq!(type_code(&L::Uuid), T_TEXT); // The codes are distinct (so a single-constant mutant can't pass). let codes = [T_I64, T_U64, T_F64, T_BOOL, T_TEXT, T_BLOB]; for (a, x) in codes.iter().enumerate() { From c9f5efbebad6b6cb9129987363fa1ff13280dc19 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 27 Jun 2026 10:10:05 -0400 Subject: [PATCH 03/10] feat(passthrough): native LOAD-hook + dual-load spike (design D) De-risk spike for the native passthrough in the multi-provider architecture. Q1 (LOAD hook): native DuckDB has no registerable seam to intercept a transparent `LOAD ` -- PhysicalLoad::GetDataInternal calls ExtensionHelper::LoadExternalExtension unconditionally and OnBeginExtensionLoad returns void. The pragmatic mechanism is an explicit `ducklink_load('aba')` entry (a C++/C-API extension can call the public ExtensionHelper::LoadExternal- Extension / TryAutoLoadExtension itself); custom_extension_repository can serve resolved native artifacts via autoinstall. Q2 (dual-load): add passthrough::ducklink_load -- resolve a provider via the lifted resolver, then load via the matching arm: - wasm -> register_components (the Route-A wasmtime->C-API bridge, reused) - native -> hand off to DuckDB's own `LOAD ''` - resolver.rs lifted verbatim from crates/ducklink-host (native stubs for available()/trusted() enabled behind allow_native for the spike). - bundled tests prove the wasm arm end-to-end (aba_validate in wasm) and that the native arm is selected + dispatches to DuckDB's loader; native off by default falls back to wasm. - bump wasmtime 1ec8660(=39) -> 46.0.1 to match the local ducklink-runtime (committed git pin had drifted); engine.rs error-context fixes for wasmtime 46. --- Cargo.lock | 851 ++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 13 +- src/engine.rs | 6 +- src/lib.rs | 12 + src/passthrough.rs | 245 +++++++++++++ src/resolver.rs | 678 ++++++++++++++++++++++++++++++++++++ 6 files changed, 1709 insertions(+), 96 deletions(-) create mode 100644 src/passthrough.rs create mode 100644 src/resolver.rs diff --git a/Cargo.lock b/Cargo.lock index 0b60e03..a6d36a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,16 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", +] + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli 0.33.0", ] [[package]] @@ -511,6 +520,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -638,12 +658,30 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "cranelift-assembler-x64" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "cranelift-assembler-x64-meta", + "cranelift-assembler-x64-meta 0.126.0", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06aeba2c965fc446d13c56a6ccb2631b78445d7544543dd9a25289977630914" +dependencies = [ + "cranelift-assembler-x64-meta 0.133.1", ] [[package]] @@ -651,7 +689,16 @@ name = "cranelift-assembler-x64-meta" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "cranelift-srcgen", + "cranelift-srcgen 0.126.0", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2d2dde4ec1352715595b5cfa6fe2e5b8ebb9da3457b3ee8db0aa2808c069aa" +dependencies = [ + "cranelift-srcgen 0.133.1", ] [[package]] @@ -659,7 +706,17 @@ name = "cranelift-bforest" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "cranelift-entity", + "cranelift-entity 0.126.0", +] + +[[package]] +name = "cranelift-bforest" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4982ef9fa54ec9eee841e891e7ddc5434be1250e88de31572e000c888f30b" +dependencies = [ + "cranelift-entity 0.133.1", + "wasmtime-internal-core", ] [[package]] @@ -671,25 +728,36 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "cranelift-bitset" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529143118c4eeb58c39ecb02319557d512be6c61348486422974ab8e3906b8a8" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + [[package]] name = "cranelift-codegen" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ "bumpalo", - "cranelift-assembler-x64", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli", + "cranelift-assembler-x64 0.126.0", + "cranelift-bforest 0.126.0", + "cranelift-bitset 0.126.0", + "cranelift-codegen-meta 0.126.0", + "cranelift-codegen-shared 0.126.0", + "cranelift-control 0.126.0", + "cranelift-entity 0.126.0", + "cranelift-isle 0.126.0", + "gimli 0.32.3", "hashbrown 0.15.5", "log", - "pulley-interpreter", - "regalloc2", + "pulley-interpreter 39.0.0", + "regalloc2 0.13.5", "rustc-hash", "serde", "smallvec", @@ -697,16 +765,60 @@ dependencies = [ "wasmtime-internal-math", ] +[[package]] +name = "cranelift-codegen" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7780677247ad3577e3a6a3ebf43f39b325a11d6393db72b2c9968a910d4d13d" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64 0.133.1", + "cranelift-bforest 0.133.1", + "cranelift-bitset 0.133.1", + "cranelift-codegen-meta 0.133.1", + "cranelift-codegen-shared 0.133.1", + "cranelift-control 0.133.1", + "cranelift-entity 0.133.1", + "cranelift-isle 0.133.1", + "gimli 0.33.0", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter 46.0.1", + "regalloc2 0.15.1", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + [[package]] name = "cranelift-codegen-meta" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "cranelift-assembler-x64-meta", - "cranelift-codegen-shared", - "cranelift-srcgen", + "cranelift-assembler-x64-meta 0.126.0", + "cranelift-codegen-shared 0.126.0", + "cranelift-srcgen 0.126.0", "heck 0.5.0", - "pulley-interpreter", + "pulley-interpreter 39.0.0", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9645250416cbf92454fe61160e17e026e0ce405906a54500b114f923ddffc9" +dependencies = [ + "cranelift-assembler-x64-meta 0.133.1", + "cranelift-codegen-shared 0.133.1", + "cranelift-srcgen 0.133.1", + "heck 0.5.0", + "pulley-interpreter 46.0.1", ] [[package]] @@ -714,6 +826,12 @@ name = "cranelift-codegen-shared" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" +[[package]] +name = "cranelift-codegen-shared" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ee8d222ff0fd3681791979afbf88586ac9f49010d3db96b3cbe4c96759aee3" + [[package]] name = "cranelift-control" version = "0.126.0" @@ -722,14 +840,35 @@ dependencies = [ "arbitrary", ] +[[package]] +name = "cranelift-control" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591abe6f5312bd2c4220f1b3bead56c2ad00257c52668015ba013b85dcf2a17a" +dependencies = [ + "arbitrary", +] + [[package]] name = "cranelift-entity" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "cranelift-bitset", + "cranelift-bitset 0.126.0", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-entity" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5300c49cf940526fe771517b3b3eabd5d0ff164ee61698579cf403fe8d3af3c" +dependencies = [ + "cranelift-bitset 0.133.1", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] @@ -737,7 +876,20 @@ name = "cranelift-frontend" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "cranelift-codegen", + "cranelift-codegen 0.126.0", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-frontend" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4adbf760207fdbbe130f1191cce01cdef66831a9f648b1f39ff2800d126d45" +dependencies = [ + "cranelift-codegen 0.133.1", + "hashbrown 0.17.1", "log", "smallvec", "target-lexicon", @@ -748,12 +900,29 @@ name = "cranelift-isle" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" +[[package]] +name = "cranelift-isle" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8315b21ff018226a42a60a4702c2dd75f6447cac26e9bca622e14c22088c2ff5" + [[package]] name = "cranelift-native" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "cranelift-codegen", + "cranelift-codegen 0.126.0", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-native" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506ef23a60715bde451b06620b14402166ded3b648454fccbf04f3e46a4aa70" +dependencies = [ + "cranelift-codegen 0.133.1", "libc", "target-lexicon", ] @@ -763,6 +932,12 @@ name = "cranelift-srcgen" version = "0.126.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" +[[package]] +name = "cranelift-srcgen" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ed47e602652e3410f9387fc0db70fefadcee4d78a78881421aabcab4e26b89" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1009,8 +1184,9 @@ dependencies = [ "duckdb", "ducklink-runtime", "libduckdb-sys", - "wasmtime", - "wasmtime-wasi", + "serde_json", + "wasmtime 46.0.1", + "wasmtime-wasi 46.0.1", ] [[package]] @@ -1018,8 +1194,8 @@ name = "ducklink-runtime" version = "0.1.0" source = "git+https://github.com/tegmentum/ducklink?rev=fb4e228#fb4e228b9738550d1cc5c7b145aee6055521d92a" dependencies = [ - "wasmtime", - "wasmtime-wasi", + "wasmtime 39.0.0", + "wasmtime-wasi 39.0.0", ] [[package]] @@ -1139,6 +1315,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1289,6 +1471,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] @@ -1302,6 +1485,18 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "half" version = "2.7.1" @@ -1329,15 +1524,26 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", "serde", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] [[package]] name = "hashlink" @@ -1885,6 +2091,12 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "maybe-owned" version = "0.3.4" @@ -2013,6 +2225,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2187,12 +2411,24 @@ name = "pulley-interpreter" version = "39.0.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "cranelift-bitset", + "cranelift-bitset 0.126.0", "log", - "pulley-macros", + "pulley-macros 39.0.0", "wasmtime-internal-math", ] +[[package]] +name = "pulley-interpreter" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b92604caae1a1899b6a5b54967289dd538177c626004c91accf9d0ec7e4a12" +dependencies = [ + "cranelift-bitset 0.133.1", + "log", + "pulley-macros 46.0.1", + "wasmtime-internal-core", +] + [[package]] name = "pulley-macros" version = "39.0.0" @@ -2203,6 +2439,17 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "pulley-macros" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7ac85c0bb3fb351f10d531230aaa5e366b46d7c4e5328e5f02801d6dac1165" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "quinn" version = "0.11.11" @@ -2306,6 +2553,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2344,6 +2602,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -2407,6 +2671,21 @@ dependencies = [ "smallvec", ] +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + [[package]] name = "regex" version = "1.12.4" @@ -2755,7 +3034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3397,6 +3676,23 @@ dependencies = [ "wat", ] +[[package]] +name = "wasm-compose" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b089037d7eb453ed57b560fe7833de0707411c8b9fdc429745ced77e2a1bacb9" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.251.0", + "wasmparser 0.251.0", + "wat", +] + [[package]] name = "wasm-encoder" version = "0.240.0" @@ -3407,6 +3703,16 @@ dependencies = [ "wasmparser 0.240.0", ] +[[package]] +name = "wasm-encoder" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" +dependencies = [ + "leb128fmt", + "wasmparser 0.251.0", +] + [[package]] name = "wasm-encoder" version = "0.252.0" @@ -3430,6 +3736,19 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + [[package]] name = "wasmparser" version = "0.252.0" @@ -3452,12 +3771,23 @@ dependencies = [ "wasmparser 0.240.0", ] +[[package]] +name = "wasmprinter" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8798c1a699bd25648b6708eefe94d97c6f9891febb94b42cca1f7a4b086ea64e" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.251.0", +] + [[package]] name = "wasmtime" version = "39.0.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ - "addr2line", + "addr2line 0.25.1", "anyhow", "async-trait", "bitflags", @@ -3467,18 +3797,18 @@ dependencies = [ "encoding_rs", "futures", "fxprof-processed-profile", - "gimli", + "gimli 0.32.3", "hashbrown 0.15.5", "indexmap", "ittapi", "libc", "log", - "mach2", + "mach2 0.4.3", "memfd", - "object", + "object 0.37.3", "once_cell", "postcard", - "pulley-interpreter", + "pulley-interpreter 39.0.0", "rayon", "rustix 1.1.4", "semver", @@ -3488,26 +3818,79 @@ dependencies = [ "smallvec", "target-lexicon", "tempfile", - "wasm-compose", + "wasm-compose 0.240.0", "wasm-encoder 0.240.0", "wasmparser 0.240.0", - "wasmtime-environ", - "wasmtime-internal-cache", - "wasmtime-internal-component-macro", - "wasmtime-internal-component-util", - "wasmtime-internal-cranelift", - "wasmtime-internal-fiber", - "wasmtime-internal-jit-debug", - "wasmtime-internal-jit-icache-coherence", + "wasmtime-environ 39.0.0", + "wasmtime-internal-cache 39.0.0", + "wasmtime-internal-component-macro 39.0.0", + "wasmtime-internal-component-util 39.0.0", + "wasmtime-internal-cranelift 39.0.0", + "wasmtime-internal-fiber 39.0.0", + "wasmtime-internal-jit-debug 39.0.0", + "wasmtime-internal-jit-icache-coherence 39.0.0", "wasmtime-internal-math", "wasmtime-internal-slab", - "wasmtime-internal-unwinder", - "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-unwinder 39.0.0", + "wasmtime-internal-versioned-export-macros 39.0.0", "wasmtime-internal-winch", "wat", "windows-sys 0.60.2", ] +[[package]] +name = "wasmtime" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4213d2f019a5e44aa8a61d8826dd33a505bff79f749b14a8bafd67321cb9351" +dependencies = [ + "addr2line 0.26.1", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli 0.33.0", + "ittapi", + "libc", + "log", + "mach2 0.6.0", + "memfd", + "object 0.39.1", + "once_cell", + "postcard", + "pulley-interpreter 46.0.1", + "rayon", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose 0.251.0", + "wasm-encoder 0.251.0", + "wasmparser 0.251.0", + "wasmtime-environ 46.0.1", + "wasmtime-internal-cache 46.0.1", + "wasmtime-internal-component-macro 46.0.1", + "wasmtime-internal-component-util 46.0.1", + "wasmtime-internal-core", + "wasmtime-internal-cranelift 46.0.1", + "wasmtime-internal-fiber 46.0.1", + "wasmtime-internal-jit-debug 46.0.1", + "wasmtime-internal-jit-icache-coherence 46.0.1", + "wasmtime-internal-unwinder 46.0.1", + "wasmtime-internal-versioned-export-macros 46.0.1", + "wat", + "windows-sys 0.61.2", + "wit-parser 0.251.0", +] + [[package]] name = "wasmtime-environ" version = "39.0.0" @@ -3515,12 +3898,12 @@ source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573f dependencies = [ "anyhow", "cpp_demangle", - "cranelift-bitset", - "cranelift-entity", - "gimli", + "cranelift-bitset 0.126.0", + "cranelift-entity 0.126.0", + "gimli 0.32.3", "indexmap", "log", - "object", + "object 0.37.3", "postcard", "rustc-demangle", "semver", @@ -3530,8 +3913,39 @@ dependencies = [ "target-lexicon", "wasm-encoder 0.240.0", "wasmparser 0.240.0", - "wasmprinter", - "wasmtime-internal-component-util", + "wasmprinter 0.240.0", + "wasmtime-internal-component-util 39.0.0", +] + +[[package]] +name = "wasmtime-environ" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45863de41977ec6453e859cf843d456fa3fcb45a659b66d16e794f90ec4f5b7" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest 0.133.1", + "cranelift-bitset 0.133.1", + "cranelift-entity 0.133.1", + "gimli 0.33.0", + "hashbrown 0.17.1", + "indexmap", + "log", + "object 0.39.1", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.251.0", + "wasmparser 0.251.0", + "wasmprinter 0.251.0", + "wasmtime-internal-component-util 46.0.1", + "wasmtime-internal-core", ] [[package]] @@ -3553,6 +3967,26 @@ dependencies = [ "zstd", ] +[[package]] +name = "wasmtime-internal-cache" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "438bc7dc45fb75297d75f79a9a0ce852345d13ebc6a6863f6f688f013836a9dd" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix 1.1.4", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ 46.0.1", + "windows-sys 0.61.2", + "zstd", +] + [[package]] name = "wasmtime-internal-component-macro" version = "39.0.0" @@ -3562,9 +3996,24 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.118", - "wasmtime-internal-component-util", - "wasmtime-internal-wit-bindgen", - "wit-parser", + "wasmtime-internal-component-util 39.0.0", + "wasmtime-internal-wit-bindgen 39.0.0", + "wit-parser 0.240.0", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e48f8d4966d62a10b6d70722bc432c1e163890be2801d3b5784589ad36ffc3" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasmtime-internal-component-util 46.0.1", + "wasmtime-internal-wit-bindgen 46.0.1", + "wit-parser 0.251.0", ] [[package]] @@ -3572,6 +4021,24 @@ name = "wasmtime-internal-component-util" version = "39.0.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" +[[package]] +name = "wasmtime-internal-component-util" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ad5abd5822a22dbf4014475cdfd1fe790707761cd732d74aaa3ba4d5ba489" + +[[package]] +name = "wasmtime-internal-core" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc28372e36eaf8cf70faa83b5779137f7e99c8d18569a125d1580e735cc9e4d" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "libm", + "serde", +] + [[package]] name = "wasmtime-internal-cranelift" version = "39.0.0" @@ -3579,24 +4046,51 @@ source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573f dependencies = [ "anyhow", "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "gimli", + "cranelift-codegen 0.126.0", + "cranelift-control 0.126.0", + "cranelift-entity 0.126.0", + "cranelift-frontend 0.126.0", + "cranelift-native 0.126.0", + "gimli 0.32.3", "itertools 0.14.0", "log", - "object", - "pulley-interpreter", + "object 0.37.3", + "pulley-interpreter 39.0.0", "smallvec", "target-lexicon", "thiserror 2.0.18", "wasmparser 0.240.0", - "wasmtime-environ", + "wasmtime-environ 39.0.0", "wasmtime-internal-math", - "wasmtime-internal-unwinder", - "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-unwinder 39.0.0", + "wasmtime-internal-versioned-export-macros 39.0.0", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a433efc6e35112a5457e1dc8bc4d8d39820ac7722267e89bc04e5df641f32124" +dependencies = [ + "cfg-if", + "cranelift-codegen 0.133.1", + "cranelift-control 0.133.1", + "cranelift-entity 0.133.1", + "cranelift-frontend 0.133.1", + "cranelift-native 0.133.1", + "gimli 0.33.0", + "itertools 0.14.0", + "log", + "object 0.39.1", + "pulley-interpreter 46.0.1", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.251.0", + "wasmtime-environ 46.0.1", + "wasmtime-internal-core", + "wasmtime-internal-unwinder 46.0.1", + "wasmtime-internal-versioned-export-macros 46.0.1", ] [[package]] @@ -3609,19 +4103,46 @@ dependencies = [ "cfg-if", "libc", "rustix 1.1.4", - "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-versioned-export-macros 39.0.0", "windows-sys 0.60.2", ] +[[package]] +name = "wasmtime-internal-fiber" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a1d3a39d0d210f6b8574ee96a4315e0a14c67f3a1fc3cd5372cb10d2fb4422" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ 46.0.1", + "wasmtime-internal-versioned-export-macros 46.0.1", + "windows-sys 0.61.2", +] + [[package]] name = "wasmtime-internal-jit-debug" version = "39.0.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ "cc", - "object", + "object 0.37.3", "rustix 1.1.4", - "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-versioned-export-macros 39.0.0", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f667288cb4dfa68a4639ffac4d5628535dda64ebdc2b990526efb12b30ba803" +dependencies = [ + "cc", + "object 0.39.1", + "rustix 1.1.4", + "wasmtime-internal-versioned-export-macros 46.0.1", ] [[package]] @@ -3635,6 +4156,18 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba651d44ab0faad4c58106b3adb45068189fb65ef50f0c404b6d9e3bf81a357" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + [[package]] name = "wasmtime-internal-math" version = "39.0.0" @@ -3655,9 +4188,22 @@ source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573f dependencies = [ "anyhow", "cfg-if", - "cranelift-codegen", + "cranelift-codegen 0.126.0", "log", - "object", + "object 0.37.3", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ecc52563b0558af2a7487eb710de07cc4532564b55528876129238e83118cb1" +dependencies = [ + "cfg-if", + "cranelift-codegen 0.133.1", + "log", + "object 0.39.1", + "wasmtime-environ 46.0.1", ] [[package]] @@ -3670,20 +4216,31 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747f4a074699ba1b4e4d841fb263f9b7df5bd1555181c4752bf5990d21ba676" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "wasmtime-internal-winch" version = "39.0.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ "anyhow", - "cranelift-codegen", - "gimli", + "cranelift-codegen 0.126.0", + "gimli 0.32.3", "log", - "object", + "object 0.37.3", "target-lexicon", "wasmparser 0.240.0", - "wasmtime-environ", - "wasmtime-internal-cranelift", + "wasmtime-environ 39.0.0", + "wasmtime-internal-cranelift 39.0.0", "winch-codegen", ] @@ -3696,7 +4253,20 @@ dependencies = [ "bitflags", "heck 0.5.0", "indexmap", - "wit-parser", + "wit-parser 0.240.0", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80009f46991622814196d96fac6fc0a938f46b5cba737a8f4e21e24e5a03856f" +dependencies = [ + "anyhow", + "bitflags", + "heck 0.5.0", + "indexmap", + "wit-parser 0.251.0", ] [[package]] @@ -3723,12 +4293,41 @@ dependencies = [ "tokio", "tracing", "url", - "wasmtime", - "wasmtime-wasi-io", - "wiggle", + "wasmtime 39.0.0", + "wasmtime-wasi-io 39.0.0", + "wiggle 39.0.0", "windows-sys 0.60.2", ] +[[package]] +name = "wasmtime-wasi" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f65ef30a2c5478873cdb619085a7a649d3ce41cc3eaf298a7ce3dee96a8e11" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-std", + "cap-time-ext", + "cfg-if", + "futures", + "io-extras", + "io-lifetimes", + "rand 0.10.1", + "rustix 1.1.4", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime 46.0.1", + "wasmtime-wasi-io 46.0.1", + "wiggle 46.0.1", + "windows-sys 0.61.2", +] + [[package]] name = "wasmtime-wasi-io" version = "39.0.0" @@ -3738,7 +4337,20 @@ dependencies = [ "async-trait", "bytes", "futures", - "wasmtime", + "wasmtime 39.0.0", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee57d5fef4976b1ab542615f4cef2c43278eb549d8078939668ea0f13d5c696" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime 46.0.1", ] [[package]] @@ -3810,8 +4422,22 @@ dependencies = [ "bitflags", "thiserror 2.0.18", "tracing", - "wasmtime", - "wiggle-macro", + "wasmtime 39.0.0", + "wiggle-macro 39.0.0", +] + +[[package]] +name = "wiggle" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03df88bf1a6068b02851aa3ef9427d285118f5c1c153b068a8995c69dd9562a" +dependencies = [ + "bitflags", + "thiserror 2.0.18", + "tracing", + "wasmtime 46.0.1", + "wasmtime-environ 46.0.1", + "wiggle-macro 46.0.1", ] [[package]] @@ -3827,6 +4453,20 @@ dependencies = [ "witx", ] +[[package]] +name = "wiggle-generate" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e014aec8661b613154377e4b49dbbb0dfc4f424efcee749782054576c00537d9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasmtime-environ 46.0.1", + "witx", +] + [[package]] name = "wiggle-macro" version = "39.0.0" @@ -3835,7 +4475,19 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.118", - "wiggle-generate", + "wiggle-generate 39.0.0", +] + +[[package]] +name = "wiggle-macro" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67310a8ae5190b7f3dcacf8697f2890701a3a8427b3e77ed91d3632dcedaceb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "wiggle-generate 46.0.1", ] [[package]] @@ -3875,16 +4527,16 @@ version = "39.0.0" source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" dependencies = [ "anyhow", - "cranelift-assembler-x64", - "cranelift-codegen", - "gimli", - "regalloc2", + "cranelift-assembler-x64 0.126.0", + "cranelift-codegen 0.126.0", + "gimli 0.32.3", + "regalloc2 0.13.5", "smallvec", "target-lexicon", "thiserror 2.0.18", "wasmparser 0.240.0", - "wasmtime-environ", - "wasmtime-internal-cranelift", + "wasmtime-environ 39.0.0", + "wasmtime-internal-cranelift 39.0.0", "wasmtime-internal-math", ] @@ -4161,6 +4813,25 @@ dependencies = [ "wasmparser 0.240.0", ] +[[package]] +name = "wit-parser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e960732e824fab95099971a09e638979347c94ca48568d3c854c945729196947" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.251.0", +] + [[package]] name = "witx" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index 3e7eceb..5d28926 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,11 +39,16 @@ bundled = ["duckdb-api", "duckdb/bundled"] # public ducklink monorepo so this extension loads components identically to the # Direction-1 host. ducklink-runtime = { git = "https://github.com/tegmentum/ducklink", rev = "fb4e228" } -# wasmtime to run the component. MUST match ducklink-runtime's pinned rev so the -# WasiCtx / generated bindings are the same types across the crate boundary. -wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "1ec866079720e573fc2aa06947c84f6132010993", features = ["component-model"] } -wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime", rev = "1ec866079720e573fc2aa06947c84f6132010993" } +# wasmtime to run the component. MUST match ducklink-runtime's wasmtime version so +# the WasiCtx / generated bindings are the same types across the crate boundary. +# SPIKE (design "D"): bumped from the old git rev 1ec8660 (=39.0.0) to 46.0.1 to +# match the local crates/ducklink-runtime (consumed via the monorepo path override), +# which moved to wasmtime 46.0.1. The committed git rev had drifted out of sync. +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" # DuckDB C Extension API (loadable-extension build). Pinned to the DuckDB version # the community-extensions CI builds against (v1.5.4); bumped per release. diff --git a/src/engine.rs b/src/engine.rs index 40f237e..2fd7a48 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -44,7 +44,9 @@ fn build_engine() -> Result { eprintln!("[ducklink] wasmtime compile cache unavailable: {err}"); } } - Engine::new(&config).context("failed to create wasmtime engine") + // wasmtime 46's Error no longer impls std::error::Error, so anyhow's + // `.context()` (which requires StdError) doesn't apply; map via Display. + Engine::new(&config).map_err(|e| anyhow!("failed to create wasmtime engine: {e}")) } /// Config/logging sink for native DuckDB. Logging goes to stderr; config reads @@ -160,7 +162,7 @@ impl Engine2 { /// functions it registered. The instance is retained for dispatch. pub fn load(&mut self, extension: &str, path: &Path) -> Result { let component = Component::from_file(&self.engine, path) - .with_context(|| format!("loading component at {}", path.display()))?; + .map_err(|e| anyhow!("loading component at {}: {e}", path.display()))?; // Grant outbound network + name lookup so network-using components (dns, // http, httpfs, ...) work. Best-effort, not a sandbox: a component that // does not use sockets is unaffected. (A future opt-in gate could mirror diff --git a/src/lib.rs b/src/lib.rs index 18d1983..c8f72f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,12 +16,24 @@ pub mod engine; +/// The multi-provider resolver spine (design A), lifted VERBATIM from +/// `crates/ducklink-host/src/resolver.rs` to prove it drops into a native +/// extension crate unchanged (de-risk spike for design "D"). Self-contained: no +/// wasmtime / no engine types. +pub mod resolver; + /// The Direction-2 DuckDB sink (registration + dispatch). Present whenever the /// duckdb crate is available (the `loadable` and `bundled` features both enable /// it); the `bundled` end-to-end test lives in this module. #[cfg(feature = "duckdb-api")] pub mod reg_duckdb; +/// The native-passthrough hook (de-risk spike): `ducklink_load('aba')` resolves a +/// provider via [`resolver`] and dual-loads it (wasm arm via the Route-A bridge / +/// native arm via DuckDB's own `LOAD`). +#[cfg(feature = "duckdb-api")] +pub mod passthrough; + #[cfg(feature = "loadable")] mod loadable { use std::error::Error; diff --git a/src/passthrough.rs b/src/passthrough.rs new file mode 100644 index 0000000..42b0253 --- /dev/null +++ b/src/passthrough.rs @@ -0,0 +1,245 @@ +//! The native-passthrough hook (de-risk spike for design "D"). +//! +//! This is the seam that answers Q1/Q2 of the spike: a *function* the user +//! invokes (`ducklink_load('aba')`) that runs the lifted [`resolver`] and then +//! DUAL-LOADS the chosen provider through the matching arm: +//! +//! * **wasm** arm -> register the component's functions into the connection +//! via the proven Route-A bridge ([`crate::reg_duckdb::register_components`]), +//! embedded wasmtime, native speed. +//! * **native** arm -> hand off to DuckDB's OWN extension loader by issuing a +//! `LOAD ''` on the connection. This is the C-API-tractable form of +//! the native passthrough: the loaded ducklink extension drives DuckDB's +//! `dlopen` + `_init` path for the resolved `.duckdb_extension`. +//! * **remote** arm -> out of scope for this spike. +//! +//! Q1 finding (see the spike report): native DuckDB has no registerable hook that +//! can intercept a transparent `LOAD spatial` for *another* extension's name -- +//! `PhysicalLoad::GetDataInternal` calls `ExtensionHelper::LoadExternalExtension` +//! unconditionally and `OnBeginExtensionLoad` returns `void`. So the pragmatic +//! mechanism is this explicit entry point, fronted in production by a +//! `PRAGMA ducklink_load('aba')` / table-function `CALL` (which fires at a +//! statement boundary, where catalog mutation is safe). This module is that +//! entry point's body; the bundled test calls it at a statement boundary exactly +//! as the pragma would. + +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +use anyhow::{anyhow, Result}; +use duckdb::Connection; + +use crate::engine::Engine2; +use crate::reg_duckdb::{register_components, ComponentSpec}; +use crate::resolver::{ + self, Conformance, ContentRef, Env, ManifestEntry, ProviderDescriptor, ProviderKind, + ResolvePolicy, +}; + +/// Outcome of one `ducklink_load` hook call (the observability the doc's +/// `PRAGMA extension_provider` would surface). +#[derive(Debug)] +pub struct LoadReport { + pub extension: String, + pub chosen_id: String, + pub chosen_kind: &'static str, + /// Per-candidate reasoning (the chosen + why each loser lost). + pub reasoning: String, + /// Functions registered (wasm arm); 0 for the native arm (DuckDB owns it). + pub registered: usize, +} + +/// The hook body: resolve `entry` under `env`/`policy`, then dual-load the chosen +/// provider through its arm. Reuses the resolver (A) verbatim and the Route-A +/// bridge (`register_components`) verbatim; the only new code is the kind->arm +/// dispatch below. +pub fn ducklink_load( + con: &Connection, + engine: Arc>, + entry: &ManifestEntry, + env: &Env, + policy: &ResolvePolicy, +) -> Result { + let res = resolver::resolve(entry, env, policy, None).map_err(|e| anyhow!(e.to_string()))?; + let reasoning = resolver::render_reasoning(&res.reasoning); + + let registered = match res.chosen_kind { + // WASM ARM: the Route-A bridge, reused unchanged. + "wasm" => { + let path = artifact_path(&res.artifact)?; + let spec = ComponentSpec { + name: res.extension.clone(), + path, + }; + register_components(con, None, engine, std::slice::from_ref(&spec))? + } + // NATIVE ARM: hand off to DuckDB's own native extension loader. + "native" => { + let path = artifact_path(&res.artifact)?; + con.execute_batch(&format!("LOAD '{}';", path.display())) + .map_err(|e| anyhow!("native arm: DuckDB LOAD of '{}' failed: {e}", path.display()))?; + 0 + } + other => return Err(anyhow!("provider kind '{other}' not implemented in this spike")), + }; + + Ok(LoadReport { + extension: res.extension, + chosen_id: res.chosen_id, + chosen_kind: res.chosen_kind, + reasoning, + registered, + }) +} + +fn artifact_path(artifact: &ContentRef) -> Result { + match artifact { + ContentRef::Path(p) => Ok(p.clone()), + // Spike: a remote/native entry may carry its location as an Oci string; + // treat it as a path for the local-file native arm. + ContentRef::Oci(s) => Ok(PathBuf::from(s)), + ContentRef::Digest(_) => { + Err(anyhow!("digest artifact resolution not wired in this spike")) + } + } +} + +/// Build a minimal in-code manifest entry for `aba`: a wasm reference provider +/// (the portable baseline) and, optionally, a native provider. In production this +/// comes from `registry/index.json` via `resolver::read_manifest_entry`; built +/// in-code here to keep the spike self-contained. +pub fn aba_manifest(wasm_artifact: PathBuf, native_artifact: Option) -> ManifestEntry { + const CONTRACT: &str = "spike-aba-contract"; + let mut providers = vec![ProviderDescriptor { + id: "wasm-component".into(), + kind: ProviderKind::Wasm { + abi: "duckdb:extension@2.0.0".into(), + artifact: ContentRef::Path(wasm_artifact), + content_digest: None, + browser_safe: false, + }, + reference: true, // certified-by-construction (the reference baseline) + conformance: None, + trust: None, + }]; + if let Some(p) = native_artifact { + providers.push(ProviderDescriptor { + id: "native-local".into(), + kind: ProviderKind::Native { + os: std::env::consts::OS.into(), + arch: std::env::consts::ARCH.into(), + artifact: ContentRef::Path(p), + }, + reference: false, + // Certified at the live contract so the hard gate admits it. + conformance: Some(Conformance { + suite: "aba@1".into(), + suite_digest: String::new(), + contract_digest: CONTRACT.into(), + passed: true, + }), + trust: None, + }); + } + ManifestEntry { + name: "aba".into(), + wit_contract: CONTRACT.into(), + providers, + } +} + +#[cfg(all(test, feature = "bundled"))] +mod tests { + use super::*; + + fn aba_wasm() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../artifacts/extensions/aba.wasm") + } + + /// Q2 WASM-ARM PROOF (end-to-end through the hook): `ducklink_load('aba')` + /// resolves the wasm reference provider, registers `aba_validate` via the + /// Route-A bridge, and the function then computes the ABA checksum INSIDE the + /// wasm component for a subsequent query. + #[test] + fn wasm_arm_dual_loads_through_hook() { + let engine = Arc::new(Mutex::new(Engine2::new().expect("engine"))); + let con = Connection::open_in_memory().expect("open duckdb"); + + // wasm-only manifest, default env (allow_native = false). + let entry = aba_manifest(aba_wasm(), None); + let report = ducklink_load( + &con, + engine, + &entry, + &Env::default(), + &ResolvePolicy::default(), + ) + .expect("hook loads"); + + assert_eq!(report.chosen_kind, "wasm", "reasoning: {}", report.reasoning); + assert_eq!(report.chosen_id, "wasm-component"); + assert!(report.registered >= 1, "expected aba_validate registered"); + eprintln!("[spike] wasm arm: {report:?}"); + + // The hook registered the function; a normal query now dispatches into wasm. + let valid: bool = con + .query_row("SELECT aba_validate('021000021')", [], |r| r.get(0)) + .expect("query valid"); + assert!(valid, "021000021 is a valid ABA routing number (checksum 0)"); + + let invalid: bool = con + .query_row("SELECT aba_validate('123456789')", [], |r| r.get(0)) + .expect("query invalid"); + assert!(!invalid, "123456789 fails the ABA checksum"); + } + + /// Q2 NATIVE-ARM PROOF (mechanism, through the hook): with a native provider + /// present and `allow_native = true`, the resolver PICKS native (precedence + /// native > wasm) and the hook hands off to DuckDB's own `LOAD` -- proving the + /// native arm reaches DuckDB's dlopen/init substrate. We point it at a missing + /// artifact, so the error originates from DuckDB's native loader (full e2e + /// needs a built+signed native `aba.duckdb_extension`; see the report). + #[test] + fn native_arm_is_chosen_and_hands_off_to_duckdb_load() { + let engine = Arc::new(Mutex::new(Engine2::new().expect("engine"))); + let con = Connection::open_in_memory().expect("open duckdb"); + + let bogus = PathBuf::from("/nonexistent/aba.duckdb_extension"); + let entry = aba_manifest(aba_wasm(), Some(bogus.clone())); + let env = Env { + wasm_runtime: true, + allow_native: true, + }; + + let err = ducklink_load(&con, engine, &entry, &env, &ResolvePolicy::default()) + .expect_err("native arm should attempt DuckDB LOAD of a missing artifact"); + let msg = err.to_string(); + eprintln!("[spike] native arm error (expected): {msg}"); + // Proof the NATIVE arm was selected and dispatched to DuckDB's loader. + assert!( + msg.contains("native arm") && msg.contains("LOAD"), + "expected native-arm LOAD handoff, got: {msg}" + ); + } + + /// The native arm is gated off by default (allow_native = false): the SAME + /// manifest resolves to the wasm baseline -- graceful degradation, not silent + /// native load. + #[test] + fn native_off_by_default_falls_back_to_wasm() { + let engine = Arc::new(Mutex::new(Engine2::new().expect("engine"))); + let con = Connection::open_in_memory().expect("open duckdb"); + let entry = aba_manifest(aba_wasm(), Some(PathBuf::from("/nonexistent/aba.duckdb_extension"))); + + let report = ducklink_load( + &con, + engine, + &entry, + &Env::default(), // allow_native = false + &ResolvePolicy::default(), + ) + .expect("falls back to wasm"); + assert_eq!(report.chosen_kind, "wasm", "reasoning: {}", report.reasoning); + assert!(report.reasoning.contains("native-local"), "loser recorded"); + } +} diff --git a/src/resolver.rs b/src/resolver.rs new file mode 100644 index 0000000..bcaa261 --- /dev/null +++ b/src/resolver.rs @@ -0,0 +1,678 @@ +//! Multi-provider extension resolver — the spine (design A) from +//! PLAN-multi-provider-extensions.md ("The Resolver" + Appendix B). +//! +//! A logical extension has one semantic contract (`wit_contract`) and one or +//! more `providers[]`, each an implementation of that contract on some substrate +//! (wasm / native / remote). This module is the substrate-agnostic policy + +//! candidate-filtering spine over those providers. It is deliberately +//! self-contained (no wasmtime / no engine types) so it lifts cleanly into +//! `datalink` later; the host injects the concrete wasm `load()` via a callback. +//! +//! This pass implements the Wasm kind only. The Wasm `load()` IS Route A's +//! resident `duckdb:extension` dispatch (datalink-dynlink + register-capture + +//! the direct `callback-dispatch` import) — wrapped, not reimplemented. Native +//! and Remote are stubbed variants that are filtered out as unavailable. + +use std::path::PathBuf; + +// --------------------------------------------------------------------------- +// B.2 types +// --------------------------------------------------------------------------- + +/// Content-addressed artifact reference. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ContentRef { + Path(PathBuf), + Digest(String), + Oci(String), +} + +/// Substrate of one provider. Only `Wasm` is implemented this pass; `Native` and +/// `Remote` are scaffolded so the filtering pipeline is real (they resolve as +/// unavailable). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ProviderKind { + Wasm { + abi: String, + artifact: ContentRef, + content_digest: Option, + browser_safe: bool, + }, + Native { + os: String, + arch: String, + artifact: ContentRef, + }, + Remote { + endpoint: String, + }, +} + +impl ProviderKind { + pub fn tag(&self) -> &'static str { + match self { + ProviderKind::Wasm { .. } => "wasm", + ProviderKind::Native { .. } => "native", + ProviderKind::Remote { .. } => "remote", + } + } +} + +/// The SEMANTIC-contract certificate carried by a provider entry. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Conformance { + pub suite: String, + /// Content digest of the suite itself; must equal the canonical suite the + /// resolver holds for (ext, contract). + pub suite_digest: String, + /// Must equal the logical extension's `wit_contract`. + pub contract_digest: String, + pub passed: bool, +} + +/// Native/remote admission inputs (unused for wasm, which is sandboxed). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Trust { + pub signed_by: Option, + pub attestation: Option, +} + +/// Static descriptor parsed from one manifest provider entry. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProviderDescriptor { + pub id: String, + pub kind: ProviderKind, + /// Defines semantics (the wasm baseline). A reference wasm provider is + /// certified-by-construction at the entry's contract. + pub reference: bool, + pub conformance: Option, + pub trust: Option, +} + +/// One logical extension entry: identity + semantic contract + providers. +#[derive(Clone, Debug)] +pub struct ManifestEntry { + pub name: String, + /// THE semantic_contract digest (witcanon). + pub wit_contract: String, + pub providers: Vec, +} + +/// Environment inputs to availability/precedence. +#[derive(Clone, Debug)] +pub struct Env { + /// A wasm runtime is present in-process (always true for the ducklink host). + pub wasm_runtime: bool, + /// Native `.duckdb_extension` loading allowed (this pass: always false). + pub allow_native: bool, +} + +impl Default for Env { + fn default() -> Self { + Self { + wasm_runtime: true, + allow_native: false, + } + } +} + +/// Resolution policy (the overridable knobs from the doc). +#[derive(Clone, Debug, Default)] +pub struct ResolvePolicy { + /// `SET extension_provider = ''` — force a specific provider id. + pub forced_provider: Option, + /// `SET extension_provider_deny = ','` — user-excluded providers. + pub denied: Vec, +} + +// --------------------------------------------------------------------------- +// Candidate pipeline outcome (observability) +// --------------------------------------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Outcome { + Chosen, + Rejected(String), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CandidateOutcome { + pub id: String, + pub kind: &'static str, + pub outcome: Outcome, +} + +#[derive(Clone, Debug)] +pub struct Resolution { + pub extension: String, + pub chosen_id: String, + pub chosen_kind: &'static str, + /// The resolved artifact of the chosen provider (for the substrate loader). + pub artifact: ContentRef, + /// Per-candidate outcome, in evaluation order (the chosen + why each loser lost). + pub reasoning: Vec, +} + +#[derive(Clone, Debug)] +pub struct ResolveError { + pub extension: String, + pub reasoning: Vec, +} + +impl std::fmt::Display for ResolveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "no admissible provider for '{}': {}", + self.extension, + render_reasoning(&self.reasoning) + ) + } +} + +impl std::error::Error for ResolveError {} + +/// Render per-candidate reasoning as a single line (used by the error + the +/// `extension_provider` observability function). +pub fn render_reasoning(reasoning: &[CandidateOutcome]) -> String { + reasoning + .iter() + .map(|c| match &c.outcome { + Outcome::Chosen => format!("{} [{}] = CHOSEN", c.id, c.kind), + Outcome::Rejected(why) => format!("{} [{}] = rejected ({})", c.id, c.kind, why), + }) + .collect::>() + .join("; ") +} + +// --------------------------------------------------------------------------- +// The resolver +// --------------------------------------------------------------------------- + +/// Precedence rank (lower = preferred): native(trusted) > wasm-local > +/// wasm-browser > remote. +fn precedence_rank(kind: &ProviderKind) -> u8 { + match kind { + ProviderKind::Native { .. } => 0, + ProviderKind::Wasm { browser_safe: false, .. } => 1, + ProviderKind::Wasm { browser_safe: true, .. } => 2, + ProviderKind::Remote { .. } => 3, + } +} + +/// The conformance HARD GATE. A provider is certified iff: +/// passed && contract_digest == wit_contract && suite_digest == canonical. +/// A reference wasm provider is certified-by-construction (the baseline WIT *is* +/// the contract that defines the semantics). +/// +/// `canonical_suite_digest` is the suite the resolver holds for (ext, contract); +/// `None` means no suite is registered yet, so the suite_digest sub-check is +/// scaffolded (skipped) — the gate still enforces passed + contract match. +fn conformance_ok( + p: &ProviderDescriptor, + wit_contract: &str, + canonical_suite_digest: Option<&str>, +) -> Result<(), String> { + if p.reference && matches!(p.kind, ProviderKind::Wasm { .. }) { + return Ok(()); + } + match &p.conformance { + None => Err("uncertified: no conformance record".to_string()), + Some(c) => { + if !c.passed { + return Err("uncertified: conformance.passed=false".to_string()); + } + if c.contract_digest != wit_contract { + return Err(format!( + "uncertified: certified at contract {} != live {}", + short(&c.contract_digest), + short(wit_contract) + )); + } + if let Some(canon) = canonical_suite_digest { + if c.suite_digest != canon { + return Err(format!( + "uncertified: suite_digest {} != canonical {}", + short(&c.suite_digest), + short(canon) + )); + } + } + Ok(()) + } + } +} + +fn substrate_available(kind: &ProviderKind, env: &Env) -> Result<(), String> { + match kind { + ProviderKind::Wasm { .. } => { + if env.wasm_runtime { + Ok(()) + } else { + Err("no wasm runtime".to_string()) + } + } + ProviderKind::Native { os: _, arch: _, .. } => { + // SPIKE (design "D"): the native arm IS implemented, in the passthrough + // hook (it hands off to DuckDB's own `LOAD`). So native is "available" + // whenever the trust policy permits it. Production should additionally + // verify platform match + that the artifact dlopen-loads. + if !env.allow_native { + Err("native providers disabled (allow_native=false)".to_string()) + } else { + Ok(()) + } + } + ProviderKind::Remote { endpoint } => { + Err(format!("remote loader not implemented ({endpoint})")) + } + } +} + +fn trusted(kind: &ProviderKind) -> Result<(), String> { + match kind { + // wasm is sandboxed -> always trusted. + ProviderKind::Wasm { .. } => Ok(()), + // SPIKE (design "D"): trust-all for native so the arm can be exercised. + // Production gates this on signature/attestation (datalink-contract / + // std:attest) + `SET allow_native_providers`. + ProviderKind::Native { .. } => Ok(()), + ProviderKind::Remote { .. } => Err("remote trust policy not implemented".to_string()), + } +} + +/// Run the candidate pipeline and pick a provider, or fail with per-candidate +/// reasons. The pipeline order mirrors the doc: +/// conformance gate -> available -> trusted -> !user_excluded -> !forced_out +/// -> order by precedence -> first. +pub fn resolve( + entry: &ManifestEntry, + env: &Env, + policy: &ResolvePolicy, + canonical_suite_digest: Option<&str>, +) -> Result { + let mut reasoning: Vec = Vec::new(); + let mut admitted: Vec<&ProviderDescriptor> = Vec::new(); + + for p in &entry.providers { + let kind = p.kind.tag(); + // 1. conformance HARD GATE + if let Err(why) = conformance_ok(p, &entry.wit_contract, canonical_suite_digest) { + reasoning.push(reject(&p.id, kind, why)); + continue; + } + // 2. substrate available + if let Err(why) = substrate_available(&p.kind, env) { + reasoning.push(reject(&p.id, kind, why)); + continue; + } + // 3. trusted + if let Err(why) = trusted(&p.kind) { + reasoning.push(reject(&p.id, kind, why)); + continue; + } + // 4. not user-excluded + if policy.denied.iter().any(|d| d == &p.id) { + reasoning.push(reject(&p.id, kind, "user-excluded (deny)".to_string())); + continue; + } + // 5. forced-provider override: if set, only that id survives + if let Some(forced) = &policy.forced_provider { + if forced != &p.id { + reasoning.push(reject( + &p.id, + kind, + format!("not the forced provider ('{forced}')"), + )); + continue; + } + } + admitted.push(p); + } + + // 6. order by precedence (stable; manifest order breaks ties) + admitted.sort_by_key(|p| precedence_rank(&p.kind)); + + match admitted.first() { + Some(chosen) => { + // Record the chosen + keep the losers' reasons already collected. + reasoning.insert( + 0, + CandidateOutcome { + id: chosen.id.clone(), + kind: chosen.kind.tag(), + outcome: Outcome::Chosen, + }, + ); + // Any other admitted providers lost on precedence. + for other in admitted.iter().skip(1) { + reasoning.push(reject( + &other.id, + other.kind.tag(), + "lost on precedence".to_string(), + )); + } + let artifact = match &chosen.kind { + ProviderKind::Wasm { artifact, .. } => artifact.clone(), + ProviderKind::Native { artifact, .. } => artifact.clone(), + ProviderKind::Remote { endpoint } => ContentRef::Oci(endpoint.clone()), + }; + Ok(Resolution { + extension: entry.name.clone(), + chosen_id: chosen.id.clone(), + chosen_kind: chosen.kind.tag(), + artifact, + reasoning, + }) + } + None => Err(ResolveError { + extension: entry.name.clone(), + reasoning, + }), + } +} + +fn reject(id: &str, kind: &'static str, why: String) -> CandidateOutcome { + CandidateOutcome { + id: id.to_string(), + kind, + outcome: Outcome::Rejected(why), + } +} + +fn short(digest: &str) -> String { + digest.chars().take(12).collect() +} + +// --------------------------------------------------------------------------- +// B.1 manifest reader (providers[] + backward-compat single-artifact) +// --------------------------------------------------------------------------- + +/// Read one extension's manifest entry from a parsed `registry/index.json` +/// value. Returns `None` if the extension is absent or has no usable artifact. +/// +/// Backward compatibility: a current single-artifact entry (just `artifact` + +/// `content_digest` + `wit_contract`) is read as a one-element `providers[]` with +/// `kind:"wasm", reference:true`, lifting those fields verbatim. An explicit +/// `providers[]` array (the generalized shape) is read directly. +pub fn read_manifest_entry(index: &serde_json::Value, name: &str) -> Option { + let exts = index.get("extensions")?.as_array()?; + let entry = exts + .iter() + .find(|e| e.get("name").and_then(|v| v.as_str()) == Some(name))?; + + let wit_contract = entry + .get("wit_contract") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let abi = format!( + "duckdb:extension@{}", + entry + .get("wit_contract_version") + .and_then(|v| v.as_str()) + .unwrap_or("2.0.0") + ); + + let mut providers = Vec::new(); + if let Some(arr) = entry.get("providers").and_then(|v| v.as_array()) { + // Generalized shape. + for p in arr { + if let Some(desc) = parse_provider(p, &abi) { + providers.push(desc); + } + } + } else if let Some(artifact) = entry.get("artifact").and_then(|v| v.as_str()) { + // Backward-compat: single artifact -> one wasm reference provider. + providers.push(ProviderDescriptor { + id: "wasm-component".to_string(), + kind: ProviderKind::Wasm { + abi: abi.clone(), + artifact: ContentRef::Path(PathBuf::from(artifact)), + content_digest: entry + .get("content_digest") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + browser_safe: false, + }, + reference: true, + conformance: None, // reference wasm: certified-by-construction + trust: None, + }); + } + + if providers.is_empty() { + return None; + } + Some(ManifestEntry { + name: name.to_string(), + wit_contract, + providers, + }) +} + +fn parse_provider(p: &serde_json::Value, default_abi: &str) -> Option { + let id = p.get("id").and_then(|v| v.as_str())?.to_string(); + let kind_tag = p.get("kind").and_then(|v| v.as_str()).unwrap_or("wasm"); + let reference = p.get("reference").and_then(|v| v.as_bool()).unwrap_or(false); + + let kind = match kind_tag { + "wasm" => { + let artifact = p.get("artifact").and_then(|v| v.as_str())?; + ProviderKind::Wasm { + abi: p + .get("abi") + .and_then(|v| v.as_str()) + .unwrap_or(default_abi) + .to_string(), + artifact: ContentRef::Path(PathBuf::from(artifact)), + content_digest: p + .get("content_digest") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + browser_safe: p + .get("browser_safe") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + } + } + "native" => { + let plat = p.get("platform"); + ProviderKind::Native { + os: plat + .and_then(|v| v.get("os")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + arch: plat + .and_then(|v| v.get("arch")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + artifact: ContentRef::Oci( + p.get("artifact") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + ), + } + } + "remote" => ProviderKind::Remote { + endpoint: p + .get("endpoint") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + }, + _ => return None, + }; + + let conformance = p.get("conformance").map(|c| Conformance { + suite: c.get("suite").and_then(|v| v.as_str()).unwrap_or("").to_string(), + suite_digest: c + .get("suite_digest") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + contract_digest: c.get("at").and_then(|v| v.as_str()).unwrap_or("").to_string(), + passed: c.get("passed").and_then(|v| v.as_bool()).unwrap_or(false), + }); + + let trust = p.get("trust").map(|t| Trust { + signed_by: t.get("signed_by").and_then(|v| v.as_str()).map(|s| s.to_string()), + attestation: t + .get("attestation") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + }); + + Some(ProviderDescriptor { + id, + kind, + reference, + conformance, + trust, + }) +} + +// --------------------------------------------------------------------------- +// tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + fn wasm_ref(id: &str) -> ProviderDescriptor { + ProviderDescriptor { + id: id.to_string(), + kind: ProviderKind::Wasm { + abi: "duckdb:extension@2.0.0".to_string(), + artifact: ContentRef::Path(PathBuf::from("artifacts/extensions/aba.wasm")), + content_digest: Some("366cdf".to_string()), + browser_safe: false, + }, + reference: true, + conformance: None, + trust: None, + } + } + + fn entry(providers: Vec) -> ManifestEntry { + ManifestEntry { + name: "aba".to_string(), + wit_contract: "90fdc46a585c".to_string(), + providers, + } + } + + #[test] + fn reference_wasm_is_chosen_certified_by_construction() { + let r = resolve(&entry(vec![wasm_ref("wasm-component")]), &Env::default(), &ResolvePolicy::default(), None) + .expect("resolves"); + assert_eq!(r.chosen_id, "wasm-component"); + assert_eq!(r.chosen_kind, "wasm"); + assert!(matches!(r.reasoning[0].outcome, Outcome::Chosen)); + } + + #[test] + fn forced_provider_excludes_others() { + let r = resolve( + &entry(vec![wasm_ref("wasm-component")]), + &Env::default(), + &ResolvePolicy { forced_provider: Some("wasm-component".into()), denied: vec![] }, + None, + ) + .expect("forced match resolves"); + assert_eq!(r.chosen_id, "wasm-component"); + } + + #[test] + fn forced_unknown_provider_fails_with_reason() { + let err = resolve( + &entry(vec![wasm_ref("wasm-component")]), + &Env::default(), + &ResolvePolicy { forced_provider: Some("nope".into()), denied: vec![] }, + None, + ) + .unwrap_err(); + assert!(render_reasoning(&err.reasoning).contains("not the forced provider")); + } + + #[test] + fn denied_provider_is_excluded() { + let err = resolve( + &entry(vec![wasm_ref("wasm-component")]), + &Env::default(), + &ResolvePolicy { forced_provider: None, denied: vec!["wasm-component".into()] }, + None, + ) + .unwrap_err(); + assert!(render_reasoning(&err.reasoning).contains("user-excluded")); + } + + #[test] + fn uncertified_nonreference_provider_is_gated_out() { + // A non-reference wasm provider with a conformance record at the WRONG + // contract is rejected by the hard gate. + let mut p = wasm_ref("wasm-stale"); + p.reference = false; + p.conformance = Some(Conformance { + suite: "aba@2".into(), + suite_digest: "7f3c".into(), + contract_digest: "DEADBEEF".into(), // != wit_contract + passed: true, + }); + let err = resolve(&entry(vec![p]), &Env::default(), &ResolvePolicy::default(), None) + .unwrap_err(); + assert!(render_reasoning(&err.reasoning).contains("uncertified")); + } + + #[test] + fn native_and_remote_are_unavailable_this_pass() { + let native = ProviderDescriptor { + id: "native-linux-x86_64".into(), + kind: ProviderKind::Native { os: "linux".into(), arch: "x86_64".into(), artifact: ContentRef::Oci("oci://x".into()) }, + reference: false, + conformance: Some(Conformance { suite: "aba@2".into(), suite_digest: "7f3c".into(), contract_digest: "90fdc46a585c".into(), passed: true }), + trust: None, + }; + // native is certified but unavailable -> wasm reference wins. + let r = resolve(&entry(vec![native, wasm_ref("wasm-component")]), &Env::default(), &ResolvePolicy::default(), None).expect("resolves to wasm"); + assert_eq!(r.chosen_id, "wasm-component"); + assert!(render_reasoning(&r.reasoning).contains("native-linux-x86_64")); + } + + #[test] + fn backward_compat_single_artifact_reads_as_one_wasm_reference() { + let index = serde_json::json!({ + "extensions": [ + { "name": "aba", "artifact": "artifacts/extensions/aba.wasm", + "content_digest": "366cdf", "wit_contract": "90fdc46a585c", + "wit_contract_version": "2.0.0" } + ] + }); + let e = read_manifest_entry(&index, "aba").expect("entry"); + assert_eq!(e.providers.len(), 1); + assert_eq!(e.providers[0].id, "wasm-component"); + assert!(e.providers[0].reference); + assert_eq!(e.wit_contract, "90fdc46a585c"); + } + + #[test] + fn explicit_providers_array_is_read() { + let index = serde_json::json!({ + "extensions": [ + { "name": "aba", "wit_contract": "90fdc46a585c", "wit_contract_version": "2.0.0", + "providers": [ + { "id": "wasm-component", "kind": "wasm", "reference": true, + "artifact": "artifacts/extensions/aba.wasm", "content_digest": "366cdf" } + ] } + ] + }); + let e = read_manifest_entry(&index, "aba").expect("entry"); + assert_eq!(e.providers.len(), 1); + assert_eq!(e.providers[0].id, "wasm-component"); + assert!(e.providers[0].reference); + } +} From 7c816c2dc734bffb78df8ad6739cde8f3234ac89 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 27 Jun 2026 11:24:44 -0400 Subject: [PATCH 04/10] feat(shim): transparent LOAD via custom-repo shim on stock duckdb (design D) Add aba_init_c_api: a DuckLink "shim" entrypoint named after a logical extension. Stock DuckDB derives the init symbol from the filename (_init_c_api), so aba.duckdb_extension exports aba_init_c_api; on LOAD aba it runs the multi-provider resolver in-process and dual-loads the chosen provider (here the wasm aba via the proven bridge). Reuses passthrough.rs (resolver + ducklink_load) verbatim; wasm path from DUCKLINK_ABA_WASM. Proven end-to-end on the STOCK official v1.5.4 duckdb CLI (no core fork): SET extension_directory=...; SET custom_extension_repository=''; INSTALL aba FROM ''; -- fetches the unsigned shim from a local repo LOAD aba; -- shim resolves -> registers aba_validate (wasm) SELECT aba_validate('021000021'); -- true, computed in the wasm component And, after the one-time INSTALL, a fresh session's plain `LOAD aba` is fully transparent (loads the persisted shim from the local extension dir). Findings (stock duckdb, cited): - custom_extension_repository does NOT make plain LOAD transparent for custom names: AllowAutoInstall is a hardcoded 10-name list and even those use the DEFAULT repo (extension_helper.cpp:144-158, extension_load.cpp:572-588). The custom repo is consulted only by explicit INSTALL ... FROM. - Transparency gradient: long-tail name = one-time INSTALL FROM, then plain LOAD is transparent thereafter (persists to the local extension dir). - Footer: C_STRUCT_UNSTABLE pins the exact engine version (v1.5.4); stable C_STRUCT would be version-portable but the Rust C-API path needs unstable. - Signing: requires -unsigned (startup-only) or a trusted-key-signed artifact. --- src/lib.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index c8f72f0..6995695 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -183,4 +183,81 @@ mod loadable { fn stringify(err: impl std::fmt::Display) -> Box { err.to_string().into() } + + // ----------------------------------------------------------------------- + // The DuckLink SHIM entrypoint (transparent-LOAD spike, design "D"). + // + // A DuckLink "shim" is a `.duckdb_extension` named after a LOGICAL extension + // (here `aba`). Stock DuckDB derives the init symbol from the FILENAME + // (`_init_c_api`), so the file `aba.duckdb_extension` must export + // `aba_init_c_api`. When the user runs `LOAD aba` on a STOCK duckdb (after a + // one-time `INSTALL aba FROM ''`), DuckDB calls this symbol, + // and the shim runs the multi-provider resolver IN-PROCESS and dual-loads the + // chosen provider via the proven bridge (here the wasm `aba` component). + // + // The wasm artifact location comes from `DUCKLINK_ABA_WASM` (a real install + // would resolve it from DuckLink's local store / manifest). + // ----------------------------------------------------------------------- + + /// SHIM init for the logical extension `aba`. Resolves + dual-loads via + /// [`crate::passthrough::ducklink_load`] so `aba_validate` becomes available + /// after a plain `LOAD aba` on stock DuckDB. + /// + /// # Safety + /// Called by DuckDB during `LOAD aba` with a valid `info` / `access` pair. + #[no_mangle] + pub unsafe extern "C" fn aba_init_c_api( + info: ffi::duckdb_extension_info, + access: *const ffi::duckdb_extension_access, + ) -> bool { + match aba_shim_init(info, access) { + Ok(loaded) => loaded, + Err(e) => { + if let Some(set_error) = (*access).set_error { + if let Ok(c) = CString::new(e.to_string()) { + set_error(info, c.as_ptr()); + } + } + false + } + } + } + + unsafe fn aba_shim_init( + info: ffi::duckdb_extension_info, + access: *const ffi::duckdb_extension_access, + ) -> Result> { + use std::path::PathBuf; + + if !ffi::duckdb_rs_extension_api_init(info, access, "v1.5.4").map_err(stringify)? { + return Ok(false); + } + let get_database = (*access) + .get_database + .ok_or_else(|| stringify("get_database is null in duckdb_extension_access"))?; + let db_ptr = get_database(info); + if db_ptr.is_null() { + return Ok(false); + } + let db: ffi::duckdb_database = *db_ptr; + let con = Connection::open_from_raw(db.cast())?; + + let wasm = std::env::var("DUCKLINK_ABA_WASM") + .map_err(|_| stringify("DUCKLINK_ABA_WASM not set (path to aba.wasm component)"))?; + let engine = Arc::new(Mutex::new(Engine2::new().map_err(stringify)?)); + let entry = crate::passthrough::aba_manifest(PathBuf::from(wasm), None); + let report = crate::passthrough::ducklink_load( + &con, + engine, + &entry, + &crate::resolver::Env::default(), + &crate::resolver::ResolvePolicy::default(), + ) + .map_err(stringify)?; + eprintln!( + "[ducklink-shim:aba] resolved provider '{}' [{}]; registered {} fn(s); reasoning: {}", + report.chosen_id, report.chosen_kind, report.registered, report.reasoning + ); + Ok(true) + } } From 76ace70af9a88ecc431506c5de9799f1eb8960e8 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 27 Jun 2026 11:45:56 -0400 Subject: [PATCH 05/10] feat(shim): manifest-driven multi-provider shim + native aba provider (design D) Full first-cut of the transparent-LOAD passthrough on stock DuckDB. Shim generator: `ducklink_shim!("", _init_c_api)` emits one shim entrypoint per managed name (NAME is the only variable; body = shared `passthrough::shim_load`). Provider selection is MANIFEST-DRIVEN: the shim reads `DUCKLINK_HOME/index.json` (the generalized providers[]), resolves the best certified provider, and dual-loads it -- NOT an env-forced provider. The conformance HARD GATE is wired: a non-reference provider is admitted only if its conformance.suite_digest == sha256(suite) (canonical) and at == wit_contract. Native provider: `aba_native_init_c_api` registers a native-Rust `aba_validate` (a real native .duckdb_extension served as aba_native.duckdb_extension). The shim's native arm `LOAD`s it (re-entrant LOAD works on stock duckdb) -- the native-speed passthrough. Policy overrides are env-driven on stock duckdb (the stable C extension API has no runtime-setting registration, so SET extension_provider can't be honored by a C-API shim -- see docs/roadmap-to-signing.md): DUCKLINK_ALLOW_NATIVE, DUCKLINK_PROVIDER, DUCKLINK_DENY. tooling/: gen-shim.sh (footer-stamp a shim/native into the repo layout ///), ducklink-install.sh (INSTALL FROM -> extdir so plain LOAD works), native-conformance.sh (run the suite vs the native provider, emit the conformance record), verify-d.sh (the 4-scenario end-to-end proof). Build: re-pin ducklink-runtime git rev fb4e228 -> 6f6b200 (origin/main, the wasmtime-46 runtime) so the standalone repo / CI builds without the monorepo path override; add sha2 for the suite content-digest. Proven on the stock official v1.5.4 CLI (-unsigned): plain `LOAD aba` -> native passthrough; force wasm; deny native -> wasm fallback; tampered suite_digest -> native rejected by the gate -> wasm fallback; native conformance passes. --- Cargo.lock | 935 +++++----------------------------- Cargo.toml | 7 +- src/engine.rs | 2 +- src/lib.rs | 190 +++++-- src/passthrough.rs | 131 ++++- tooling/ducklink-install.sh | 18 + tooling/gen-shim.sh | 23 + tooling/native-conformance.sh | 48 ++ tooling/verify-d.sh | 69 +++ 9 files changed, 566 insertions(+), 857 deletions(-) create mode 100755 tooling/ducklink-install.sh create mode 100755 tooling/gen-shim.sh create mode 100755 tooling/native-conformance.sh create mode 100755 tooling/verify-d.sh diff --git a/Cargo.lock b/Cargo.lock index a6d36a0..50ccf15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,13 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli 0.32.3", -] - [[package]] name = "addr2line" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" dependencies = [ - "gimli 0.33.0", + "gimli", ] [[package]] @@ -321,15 +312,6 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] - [[package]] name = "bitvec" version = "1.1.1" @@ -454,16 +436,6 @@ dependencies = [ "winx", ] -[[package]] -name = "cap-rand" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" -dependencies = [ - "ambient-authority", - "rand 0.8.6", -] - [[package]] name = "cap-std" version = "3.4.5" @@ -667,29 +639,13 @@ dependencies = [ "libc", ] -[[package]] -name = "cranelift-assembler-x64" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cranelift-assembler-x64-meta 0.126.0", -] - [[package]] name = "cranelift-assembler-x64" version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e06aeba2c965fc446d13c56a6ccb2631b78445d7544543dd9a25289977630914" dependencies = [ - "cranelift-assembler-x64-meta 0.133.1", -] - -[[package]] -name = "cranelift-assembler-x64-meta" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cranelift-srcgen 0.126.0", + "cranelift-assembler-x64-meta", ] [[package]] @@ -698,15 +654,7 @@ version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2d2dde4ec1352715595b5cfa6fe2e5b8ebb9da3457b3ee8db0aa2808c069aa" dependencies = [ - "cranelift-srcgen 0.133.1", -] - -[[package]] -name = "cranelift-bforest" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cranelift-entity 0.126.0", + "cranelift-srcgen", ] [[package]] @@ -715,19 +663,10 @@ version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03b4982ef9fa54ec9eee841e891e7ddc5434be1250e88de31572e000c888f30b" dependencies = [ - "cranelift-entity 0.133.1", + "cranelift-entity", "wasmtime-internal-core", ] -[[package]] -name = "cranelift-bitset" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "serde", - "serde_derive", -] - [[package]] name = "cranelift-bitset" version = "0.133.1" @@ -739,32 +678,6 @@ dependencies = [ "wasmtime-internal-core", ] -[[package]] -name = "cranelift-codegen" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "bumpalo", - "cranelift-assembler-x64 0.126.0", - "cranelift-bforest 0.126.0", - "cranelift-bitset 0.126.0", - "cranelift-codegen-meta 0.126.0", - "cranelift-codegen-shared 0.126.0", - "cranelift-control 0.126.0", - "cranelift-entity 0.126.0", - "cranelift-isle 0.126.0", - "gimli 0.32.3", - "hashbrown 0.15.5", - "log", - "pulley-interpreter 39.0.0", - "regalloc2 0.13.5", - "rustc-hash", - "serde", - "smallvec", - "target-lexicon", - "wasmtime-internal-math", -] - [[package]] name = "cranelift-codegen" version = "0.133.1" @@ -772,21 +685,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7780677247ad3577e3a6a3ebf43f39b325a11d6393db72b2c9968a910d4d13d" dependencies = [ "bumpalo", - "cranelift-assembler-x64 0.133.1", - "cranelift-bforest 0.133.1", - "cranelift-bitset 0.133.1", - "cranelift-codegen-meta 0.133.1", - "cranelift-codegen-shared 0.133.1", - "cranelift-control 0.133.1", - "cranelift-entity 0.133.1", - "cranelift-isle 0.133.1", - "gimli 0.33.0", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", "hashbrown 0.17.1", "libm", "log", "postcard", - "pulley-interpreter 46.0.1", - "regalloc2 0.15.1", + "pulley-interpreter", + "regalloc2", "rustc-hash", "serde", "serde_derive", @@ -796,50 +709,25 @@ dependencies = [ "wasmtime-internal-core", ] -[[package]] -name = "cranelift-codegen-meta" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cranelift-assembler-x64-meta 0.126.0", - "cranelift-codegen-shared 0.126.0", - "cranelift-srcgen 0.126.0", - "heck 0.5.0", - "pulley-interpreter 39.0.0", -] - [[package]] name = "cranelift-codegen-meta" version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac9645250416cbf92454fe61160e17e026e0ce405906a54500b114f923ddffc9" dependencies = [ - "cranelift-assembler-x64-meta 0.133.1", - "cranelift-codegen-shared 0.133.1", - "cranelift-srcgen 0.133.1", - "heck 0.5.0", - "pulley-interpreter 46.0.1", + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", ] -[[package]] -name = "cranelift-codegen-shared" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" - [[package]] name = "cranelift-codegen-shared" version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20ee8d222ff0fd3681791979afbf88586ac9f49010d3db96b3cbe4c96759aee3" -[[package]] -name = "cranelift-control" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "arbitrary", -] - [[package]] name = "cranelift-control" version = "0.133.1" @@ -849,89 +737,48 @@ dependencies = [ "arbitrary", ] -[[package]] -name = "cranelift-entity" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cranelift-bitset 0.126.0", - "serde", - "serde_derive", -] - [[package]] name = "cranelift-entity" version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5300c49cf940526fe771517b3b3eabd5d0ff164ee61698579cf403fe8d3af3c" dependencies = [ - "cranelift-bitset 0.133.1", + "cranelift-bitset", "serde", "serde_derive", "wasmtime-internal-core", ] -[[package]] -name = "cranelift-frontend" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cranelift-codegen 0.126.0", - "log", - "smallvec", - "target-lexicon", -] - [[package]] name = "cranelift-frontend" version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da4adbf760207fdbbe130f1191cce01cdef66831a9f648b1f39ff2800d126d45" dependencies = [ - "cranelift-codegen 0.133.1", + "cranelift-codegen", "hashbrown 0.17.1", "log", "smallvec", "target-lexicon", ] -[[package]] -name = "cranelift-isle" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" - [[package]] name = "cranelift-isle" version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8315b21ff018226a42a60a4702c2dd75f6447cac26e9bca622e14c22088c2ff5" -[[package]] -name = "cranelift-native" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cranelift-codegen 0.126.0", - "libc", - "target-lexicon", -] - [[package]] name = "cranelift-native" version = "0.133.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d506ef23a60715bde451b06620b14402166ded3b648454fccbf04f3e46a4aa70" dependencies = [ - "cranelift-codegen 0.133.1", + "cranelift-codegen", "libc", "target-lexicon", ] -[[package]] -name = "cranelift-srcgen" -version = "0.126.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" - [[package]] name = "cranelift-srcgen" version = "0.133.1" @@ -1081,6 +928,26 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "datalink-contract" +version = "0.1.0" +source = "git+https://github.com/tegmentum/datalink.git#9d667a53170ef6e4cb5a2656e81df14f066bc776" +dependencies = [ + "anyhow", + "wasmtime", +] + +[[package]] +name = "datalink-dynlink" +version = "0.1.0" +source = "git+https://github.com/tegmentum/datalink.git#9d667a53170ef6e4cb5a2656e81df14f066bc776" +dependencies = [ + "async-trait", + "tokio", + "wasmtime", + "wasmtime-wasi", +] + [[package]] name = "debugid" version = "0.8.0" @@ -1185,17 +1052,22 @@ dependencies = [ "ducklink-runtime", "libduckdb-sys", "serde_json", - "wasmtime 46.0.1", - "wasmtime-wasi 46.0.1", + "sha2", + "wasmtime", + "wasmtime-wasi", ] [[package]] name = "ducklink-runtime" version = "0.1.0" -source = "git+https://github.com/tegmentum/ducklink?rev=fb4e228#fb4e228b9738550d1cc5c7b145aee6055521d92a" +source = "git+https://github.com/tegmentum/ducklink?rev=6f6b200#6f6b20024e2e6c4c1d1162e7cd858760becf0bd7" dependencies = [ - "wasmtime 39.0.0", - "wasmtime-wasi 39.0.0", + "datalink-contract", + "datalink-dynlink", + "hex", + "sha2", + "wasmtime", + "wasmtime-wasi", ] [[package]] @@ -1259,17 +1131,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.4", - "windows-sys 0.59.0", -] - [[package]] name = "filetime" version = "0.2.29" @@ -1474,17 +1335,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] - [[package]] name = "gimli" version = "0.33.0" @@ -1525,7 +1375,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash 0.1.5", - "serde", ] [[package]] @@ -1554,12 +1403,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -1572,6 +1415,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.2" @@ -1809,20 +1658,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "im-rc" -version = "15.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" -dependencies = [ - "bitmaps", - "rand_core 0.6.4", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - [[package]] name = "indexmap" version = "2.14.0" @@ -2082,15 +1917,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - [[package]] name = "mach2" version = "0.6.0" @@ -2213,18 +2039,6 @@ dependencies = [ "libm", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "crc32fast", - "hashbrown 0.15.5", - "indexmap", - "memchr", -] - [[package]] name = "object" version = "0.39.1" @@ -2406,39 +2220,18 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "pulley-interpreter" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cranelift-bitset 0.126.0", - "log", - "pulley-macros 39.0.0", - "wasmtime-internal-math", -] - [[package]] name = "pulley-interpreter" version = "46.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b92604caae1a1899b6a5b54967289dd538177c626004c91accf9d0ec7e4a12" dependencies = [ - "cranelift-bitset 0.133.1", + "cranelift-bitset", "log", - "pulley-macros 46.0.1", + "pulley-macros", "wasmtime-internal-core", ] -[[package]] -name = "pulley-macros" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "pulley-macros" version = "46.0.1" @@ -2608,15 +2401,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rayon" version = "1.12.0" @@ -2657,20 +2441,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "regalloc2" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" -dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.15.5", - "log", - "rustc-hash", - "smallvec", -] - [[package]] name = "regalloc2" version = "0.15.1" @@ -3014,19 +2784,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha2" version = "0.10.9" @@ -3056,16 +2813,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "sized-chunks" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps", - "typenum", -] - [[package]] name = "slab" version = "0.4.12" @@ -3118,7 +2865,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.118", @@ -3172,22 +2919,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "system-interface" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" -dependencies = [ - "bitflags", - "cap-fs-ext", - "cap-std", - "fd-lock", - "io-lifetimes", - "rustix 0.38.44", - "windows-sys 0.59.0", - "winx", -] - [[package]] name = "tap" version = "1.0.1" @@ -3513,12 +3244,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -3655,27 +3380,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-compose" -version = "0.240.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feeb9a231e63bd5d5dfe07e9f8daa53d5c85e4f7de5ef756d3b4e6a5f501c578" -dependencies = [ - "anyhow", - "heck 0.4.1", - "im-rc", - "indexmap", - "log", - "petgraph", - "serde", - "serde_derive", - "serde_yaml", - "smallvec", - "wasm-encoder 0.240.0", - "wasmparser 0.240.0", - "wat", -] - [[package]] name = "wasm-compose" version = "0.251.0" @@ -3683,7 +3387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b089037d7eb453ed57b560fe7833de0707411c8b9fdc429745ced77e2a1bacb9" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap", "log", "petgraph", @@ -3693,16 +3397,6 @@ dependencies = [ "wat", ] -[[package]] -name = "wasm-encoder" -version = "0.240.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d642d8c5ecc083aafe9ceb32809276a304547a3a6eeecceb5d8152598bc71f" -dependencies = [ - "leb128fmt", - "wasmparser 0.240.0", -] - [[package]] name = "wasm-encoder" version = "0.251.0" @@ -3723,19 +3417,6 @@ dependencies = [ "wasmparser 0.252.0", ] -[[package]] -name = "wasmparser" -version = "0.240.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", - "serde", -] - [[package]] name = "wasmparser" version = "0.251.0" @@ -3760,17 +3441,6 @@ dependencies = [ "semver", ] -[[package]] -name = "wasmprinter" -version = "0.240.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84d6e25c198da67d0150ee7c2c62d33d784f0a565d1e670bdf1eeccca8158bc" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.240.0", -] - [[package]] name = "wasmprinter" version = "0.251.0" @@ -3782,69 +3452,13 @@ dependencies = [ "wasmparser 0.251.0", ] -[[package]] -name = "wasmtime" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "addr2line 0.25.1", - "anyhow", - "async-trait", - "bitflags", - "bumpalo", - "cc", - "cfg-if", - "encoding_rs", - "futures", - "fxprof-processed-profile", - "gimli 0.32.3", - "hashbrown 0.15.5", - "indexmap", - "ittapi", - "libc", - "log", - "mach2 0.4.3", - "memfd", - "object 0.37.3", - "once_cell", - "postcard", - "pulley-interpreter 39.0.0", - "rayon", - "rustix 1.1.4", - "semver", - "serde", - "serde_derive", - "serde_json", - "smallvec", - "target-lexicon", - "tempfile", - "wasm-compose 0.240.0", - "wasm-encoder 0.240.0", - "wasmparser 0.240.0", - "wasmtime-environ 39.0.0", - "wasmtime-internal-cache 39.0.0", - "wasmtime-internal-component-macro 39.0.0", - "wasmtime-internal-component-util 39.0.0", - "wasmtime-internal-cranelift 39.0.0", - "wasmtime-internal-fiber 39.0.0", - "wasmtime-internal-jit-debug 39.0.0", - "wasmtime-internal-jit-icache-coherence 39.0.0", - "wasmtime-internal-math", - "wasmtime-internal-slab", - "wasmtime-internal-unwinder 39.0.0", - "wasmtime-internal-versioned-export-macros 39.0.0", - "wasmtime-internal-winch", - "wat", - "windows-sys 0.60.2", -] - [[package]] name = "wasmtime" version = "46.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4213d2f019a5e44aa8a61d8826dd33a505bff79f749b14a8bafd67321cb9351" dependencies = [ - "addr2line 0.26.1", + "addr2line", "async-trait", "bitflags", "bumpalo", @@ -3853,16 +3467,16 @@ dependencies = [ "encoding_rs", "futures", "fxprof-processed-profile", - "gimli 0.33.0", + "gimli", "ittapi", "libc", "log", - "mach2 0.6.0", + "mach2", "memfd", - "object 0.39.1", + "object", "once_cell", "postcard", - "pulley-interpreter 46.0.1", + "pulley-interpreter", "rayon", "rustix 1.1.4", "semver", @@ -3872,49 +3486,23 @@ dependencies = [ "smallvec", "target-lexicon", "tempfile", - "wasm-compose 0.251.0", + "wasm-compose", "wasm-encoder 0.251.0", "wasmparser 0.251.0", - "wasmtime-environ 46.0.1", - "wasmtime-internal-cache 46.0.1", - "wasmtime-internal-component-macro 46.0.1", - "wasmtime-internal-component-util 46.0.1", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", "wasmtime-internal-core", - "wasmtime-internal-cranelift 46.0.1", - "wasmtime-internal-fiber 46.0.1", - "wasmtime-internal-jit-debug 46.0.1", - "wasmtime-internal-jit-icache-coherence 46.0.1", - "wasmtime-internal-unwinder 46.0.1", - "wasmtime-internal-versioned-export-macros 46.0.1", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", "wat", "windows-sys 0.61.2", - "wit-parser 0.251.0", -] - -[[package]] -name = "wasmtime-environ" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "cpp_demangle", - "cranelift-bitset 0.126.0", - "cranelift-entity 0.126.0", - "gimli 0.32.3", - "indexmap", - "log", - "object 0.37.3", - "postcard", - "rustc-demangle", - "semver", - "serde", - "serde_derive", - "smallvec", - "target-lexicon", - "wasm-encoder 0.240.0", - "wasmparser 0.240.0", - "wasmprinter 0.240.0", - "wasmtime-internal-component-util 39.0.0", + "wit-parser", ] [[package]] @@ -3925,14 +3513,14 @@ checksum = "d45863de41977ec6453e859cf843d456fa3fcb45a659b66d16e794f90ec4f5b7" dependencies = [ "anyhow", "cpp_demangle", - "cranelift-bforest 0.133.1", - "cranelift-bitset 0.133.1", - "cranelift-entity 0.133.1", - "gimli 0.33.0", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", "hashbrown 0.17.1", "indexmap", "log", - "object 0.39.1", + "object", "postcard", "rustc-demangle", "semver", @@ -3943,30 +3531,11 @@ dependencies = [ "target-lexicon", "wasm-encoder 0.251.0", "wasmparser 0.251.0", - "wasmprinter 0.251.0", - "wasmtime-internal-component-util 46.0.1", + "wasmprinter", + "wasmtime-internal-component-util", "wasmtime-internal-core", ] -[[package]] -name = "wasmtime-internal-cache" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "base64", - "directories-next", - "log", - "postcard", - "rustix 1.1.4", - "serde", - "serde_derive", - "sha2", - "toml", - "windows-sys 0.60.2", - "zstd", -] - [[package]] name = "wasmtime-internal-cache" version = "46.0.1" @@ -3982,25 +3551,11 @@ dependencies = [ "serde_derive", "sha2", "toml", - "wasmtime-environ 46.0.1", + "wasmtime-environ", "windows-sys 0.61.2", "zstd", ] -[[package]] -name = "wasmtime-internal-component-macro" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.118", - "wasmtime-internal-component-util 39.0.0", - "wasmtime-internal-wit-bindgen 39.0.0", - "wit-parser 0.240.0", -] - [[package]] name = "wasmtime-internal-component-macro" version = "46.0.1" @@ -4011,16 +3566,11 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.118", - "wasmtime-internal-component-util 46.0.1", - "wasmtime-internal-wit-bindgen 46.0.1", - "wit-parser 0.251.0", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", ] -[[package]] -name = "wasmtime-internal-component-util" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" - [[package]] name = "wasmtime-internal-component-util" version = "46.0.1" @@ -4039,33 +3589,6 @@ dependencies = [ "serde", ] -[[package]] -name = "wasmtime-internal-cranelift" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "cfg-if", - "cranelift-codegen 0.126.0", - "cranelift-control 0.126.0", - "cranelift-entity 0.126.0", - "cranelift-frontend 0.126.0", - "cranelift-native 0.126.0", - "gimli 0.32.3", - "itertools 0.14.0", - "log", - "object 0.37.3", - "pulley-interpreter 39.0.0", - "smallvec", - "target-lexicon", - "thiserror 2.0.18", - "wasmparser 0.240.0", - "wasmtime-environ 39.0.0", - "wasmtime-internal-math", - "wasmtime-internal-unwinder 39.0.0", - "wasmtime-internal-versioned-export-macros 39.0.0", -] - [[package]] name = "wasmtime-internal-cranelift" version = "46.0.1" @@ -4073,38 +3596,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a433efc6e35112a5457e1dc8bc4d8d39820ac7722267e89bc04e5df641f32124" dependencies = [ "cfg-if", - "cranelift-codegen 0.133.1", - "cranelift-control 0.133.1", - "cranelift-entity 0.133.1", - "cranelift-frontend 0.133.1", - "cranelift-native 0.133.1", - "gimli 0.33.0", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", "itertools 0.14.0", "log", - "object 0.39.1", - "pulley-interpreter 46.0.1", + "object", + "pulley-interpreter", "smallvec", "target-lexicon", "thiserror 2.0.18", "wasmparser 0.251.0", - "wasmtime-environ 46.0.1", + "wasmtime-environ", "wasmtime-internal-core", - "wasmtime-internal-unwinder 46.0.1", - "wasmtime-internal-versioned-export-macros 46.0.1", -] - -[[package]] -name = "wasmtime-internal-fiber" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "cc", - "cfg-if", - "libc", - "rustix 1.1.4", - "wasmtime-internal-versioned-export-macros 39.0.0", - "windows-sys 0.60.2", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", ] [[package]] @@ -4117,22 +3626,11 @@ dependencies = [ "cfg-if", "libc", "rustix 1.1.4", - "wasmtime-environ 46.0.1", - "wasmtime-internal-versioned-export-macros 46.0.1", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-jit-debug" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "cc", - "object 0.37.3", - "rustix 1.1.4", - "wasmtime-internal-versioned-export-macros 39.0.0", -] - [[package]] name = "wasmtime-internal-jit-debug" version = "46.0.1" @@ -4140,20 +3638,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f667288cb4dfa68a4639ffac4d5628535dda64ebdc2b990526efb12b30ba803" dependencies = [ "cc", - "object 0.39.1", + "object", "rustix 1.1.4", - "wasmtime-internal-versioned-export-macros 46.0.1", -] - -[[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "cfg-if", - "libc", - "windows-sys 0.60.2", + "wasmtime-internal-versioned-export-macros", ] [[package]] @@ -4168,31 +3655,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-math" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-internal-slab" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" - -[[package]] -name = "wasmtime-internal-unwinder" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "cfg-if", - "cranelift-codegen 0.126.0", - "log", - "object 0.37.3", -] - [[package]] name = "wasmtime-internal-unwinder" version = "46.0.1" @@ -4200,20 +3662,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ecc52563b0558af2a7487eb710de07cc4532564b55528876129238e83118cb1" dependencies = [ "cfg-if", - "cranelift-codegen 0.133.1", + "cranelift-codegen", "log", - "object 0.39.1", - "wasmtime-environ 46.0.1", -] - -[[package]] -name = "wasmtime-internal-versioned-export-macros" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", + "object", + "wasmtime-environ", ] [[package]] @@ -4227,35 +3679,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "wasmtime-internal-winch" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "cranelift-codegen 0.126.0", - "gimli 0.32.3", - "log", - "object 0.37.3", - "target-lexicon", - "wasmparser 0.240.0", - "wasmtime-environ 39.0.0", - "wasmtime-internal-cranelift 39.0.0", - "winch-codegen", -] - -[[package]] -name = "wasmtime-internal-wit-bindgen" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "bitflags", - "heck 0.5.0", - "indexmap", - "wit-parser 0.240.0", -] - [[package]] name = "wasmtime-internal-wit-bindgen" version = "46.0.1" @@ -4264,39 +3687,9 @@ checksum = "80009f46991622814196d96fac6fc0a938f46b5cba737a8f4e21e24e5a03856f" dependencies = [ "anyhow", "bitflags", - "heck 0.5.0", + "heck", "indexmap", - "wit-parser 0.251.0", -] - -[[package]] -name = "wasmtime-wasi" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "async-trait", - "bitflags", - "bytes", - "cap-fs-ext", - "cap-net-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "futures", - "io-extras", - "io-lifetimes", - "rustix 1.1.4", - "system-interface", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", - "wasmtime 39.0.0", - "wasmtime-wasi-io 39.0.0", - "wiggle 39.0.0", - "windows-sys 0.60.2", + "wit-parser", ] [[package]] @@ -4322,24 +3715,12 @@ dependencies = [ "tokio", "tracing", "url", - "wasmtime 46.0.1", - "wasmtime-wasi-io 46.0.1", - "wiggle 46.0.1", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-wasi-io" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "async-trait", - "bytes", - "futures", - "wasmtime 39.0.0", -] - [[package]] name = "wasmtime-wasi-io" version = "46.0.1" @@ -4350,7 +3731,7 @@ dependencies = [ "bytes", "futures", "tracing", - "wasmtime 46.0.1", + "wasmtime", ] [[package]] @@ -4413,19 +3794,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "wiggle" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "bitflags", - "thiserror 2.0.18", - "tracing", - "wasmtime 39.0.0", - "wiggle-macro 39.0.0", -] - [[package]] name = "wiggle" version = "46.0.1" @@ -4435,22 +3803,9 @@ dependencies = [ "bitflags", "thiserror 2.0.18", "tracing", - "wasmtime 46.0.1", - "wasmtime-environ 46.0.1", - "wiggle-macro 46.0.1", -] - -[[package]] -name = "wiggle-generate" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", - "witx", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", ] [[package]] @@ -4459,25 +3814,14 @@ version = "46.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e014aec8661b613154377e4b49dbbb0dfc4f424efcee749782054576c00537d9" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.118", - "wasmtime-environ 46.0.1", + "wasmtime-environ", "witx", ] -[[package]] -name = "wiggle-macro" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "wiggle-generate 39.0.0", -] - [[package]] name = "wiggle-macro" version = "46.0.1" @@ -4487,7 +3831,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.118", - "wiggle-generate 46.0.1", + "wiggle-generate", ] [[package]] @@ -4521,25 +3865,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "winch-codegen" -version = "39.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=1ec866079720e573fc2aa06947c84f6132010993#1ec866079720e573fc2aa06947c84f6132010993" -dependencies = [ - "anyhow", - "cranelift-assembler-x64 0.126.0", - "cranelift-codegen 0.126.0", - "gimli 0.32.3", - "regalloc2 0.13.5", - "smallvec", - "target-lexicon", - "thiserror 2.0.18", - "wasmparser 0.240.0", - "wasmtime-environ 39.0.0", - "wasmtime-internal-cranelift 39.0.0", - "wasmtime-internal-math", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -4795,24 +4120,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-parser" -version = "0.240.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9875ea3fa272f57cc1fc50f225a7b94021a7878c484b33792bccad0d93223439" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.240.0", -] - [[package]] name = "wit-parser" version = "0.251.0" diff --git a/Cargo.toml b/Cargo.toml index 5d28926..1824912 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ bundled = ["duckdb-api", "duckdb/bundled"] # model, the callback registry, and the load_component loader. Consumed from the # public ducklink monorepo so this extension loads components identically to the # Direction-1 host. -ducklink-runtime = { git = "https://github.com/tegmentum/ducklink", rev = "fb4e228" } +ducklink-runtime = { git = "https://github.com/tegmentum/ducklink", rev = "6f6b200" } # wasmtime to run the component. MUST match ducklink-runtime's wasmtime version so # the WasiCtx / generated bindings are the same types across the crate boundary. # SPIKE (design "D"): bumped from the old git rev 1ec8660 (=39.0.0) to 46.0.1 to @@ -49,6 +49,9 @@ 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. @@ -59,7 +62,7 @@ libduckdb-sys = { version = "1.10504.0", features = ["loadable-extension"], opti criterion = "0.5" # For constructing reg::DuckValue chunks in the dispatch benchmark (benches are # separate compilation units, so the engine's runtime dep must be a dev-dep too). -ducklink-runtime = { git = "https://github.com/tegmentum/ducklink", rev = "fb4e228" } +ducklink-runtime = { git = "https://github.com/tegmentum/ducklink", rev = "6f6b200" } # Micro-benchmark of the Direction-2 scalar dispatch hot path (Engine2 only -- # no DuckDB, so it builds with --no-default-features). Run: diff --git a/src/engine.rs b/src/engine.rs index 2fd7a48..fcd7cc3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use std::path::Path; use std::sync::{Arc, Mutex}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use wasmtime::component::Component; use wasmtime::{Config, Engine}; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder}; diff --git a/src/lib.rs b/src/lib.rs index 6995695..c7d1d90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ mod loadable { use duckdb::core::{DataChunkHandle, Inserter, LogicalTypeId}; use duckdb::ffi; use duckdb::vscalar::{ScalarFunctionSignature, VScalar}; + use duckdb::types::DuckString; use duckdb::vtab::arrow::WritableVector; use duckdb::Connection; @@ -185,32 +186,30 @@ mod loadable { } // ----------------------------------------------------------------------- - // The DuckLink SHIM entrypoint (transparent-LOAD spike, design "D"). + // The DuckLink SHIM entrypoint (transparent LOAD, design "D" first-cut). // // A DuckLink "shim" is a `.duckdb_extension` named after a LOGICAL extension - // (here `aba`). Stock DuckDB derives the init symbol from the FILENAME - // (`_init_c_api`), so the file `aba.duckdb_extension` must export - // `aba_init_c_api`. When the user runs `LOAD aba` on a STOCK duckdb (after a - // one-time `INSTALL aba FROM ''`), DuckDB calls this symbol, - // and the shim runs the multi-provider resolver IN-PROCESS and dual-loads the - // chosen provider via the proven bridge (here the wasm `aba` component). + // (e.g. `aba`). Stock DuckDB derives the init symbol from the FILENAME + // (`_init_c_api`), so `aba.duckdb_extension` must export + // `aba_init_c_api`. After a one-time `ducklink install aba` (INSTALL FROM the + // DuckLink repo), plain `LOAD aba` on STOCK duckdb calls this symbol; the + // shim runs the multi-provider resolver IN-PROCESS (reading the manifest) and + // dual-loads the chosen provider (native passthrough, or the wasm bridge). // - // The wasm artifact location comes from `DUCKLINK_ABA_WASM` (a real install - // would resolve it from DuckLink's local store / manifest). + // The shim is identical for every name -- the NAME is the only variable -- so + // the generator emits one `ducklink_shim!("", _init_c_api);` line + // per managed extension (all share one cdylib; each copy is renamed + + // footer-stamped to `.duckdb_extension`). // ----------------------------------------------------------------------- - /// SHIM init for the logical extension `aba`. Resolves + dual-loads via - /// [`crate::passthrough::ducklink_load`] so `aba_validate` becomes available - /// after a plain `LOAD aba` on stock DuckDB. - /// - /// # Safety - /// Called by DuckDB during `LOAD aba` with a valid `info` / `access` pair. - #[no_mangle] - pub unsafe extern "C" fn aba_init_c_api( + /// Shared shim entry. `name` is the logical extension; provider selection is + /// manifest-driven (`DUCKLINK_HOME/index.json`) via [`crate::passthrough::shim_load`]. + unsafe fn shim_entry( + name: &str, info: ffi::duckdb_extension_info, access: *const ffi::duckdb_extension_access, ) -> bool { - match aba_shim_init(info, access) { + match shim_entry_inner(name, info, access) { Ok(loaded) => loaded, Err(e) => { if let Some(set_error) = (*access).set_error { @@ -223,41 +222,154 @@ mod loadable { } } - unsafe fn aba_shim_init( + unsafe fn shim_entry_inner( + name: &str, info: ffi::duckdb_extension_info, access: *const ffi::duckdb_extension_access, ) -> Result> { - use std::path::PathBuf; + let con = match open_connection(info, access)? { + Some(c) => c, + None => return Ok(false), + }; + let report = crate::passthrough::shim_load(&con, name).map_err(stringify)?; + eprintln!( + "[ducklink-shim:{name}] resolved provider '{}' [{}]; registered {} fn(s); reasoning: {}", + report.chosen_id, report.chosen_kind, report.registered, report.reasoning + ); + Ok(true) + } + /// Common entrypoint boilerplate: init the C-API table, open a duckdb-rs + /// `Connection` on the database DuckDB handed us. `None` => abort cleanly. + unsafe fn open_connection( + info: ffi::duckdb_extension_info, + access: *const ffi::duckdb_extension_access, + ) -> Result, Box> { if !ffi::duckdb_rs_extension_api_init(info, access, "v1.5.4").map_err(stringify)? { - return Ok(false); + return Ok(None); } let get_database = (*access) .get_database .ok_or_else(|| stringify("get_database is null in duckdb_extension_access"))?; let db_ptr = get_database(info); if db_ptr.is_null() { - return Ok(false); + return Ok(None); } let db: ffi::duckdb_database = *db_ptr; - let con = Connection::open_from_raw(db.cast())?; + Ok(Some(Connection::open_from_raw(db.cast())?)) + } - let wasm = std::env::var("DUCKLINK_ABA_WASM") - .map_err(|_| stringify("DUCKLINK_ABA_WASM not set (path to aba.wasm component)"))?; - let engine = Arc::new(Mutex::new(Engine2::new().map_err(stringify)?)); - let entry = crate::passthrough::aba_manifest(PathBuf::from(wasm), None); - let report = crate::passthrough::ducklink_load( - &con, - engine, - &entry, - &crate::resolver::Env::default(), - &crate::resolver::ResolvePolicy::default(), - ) - .map_err(stringify)?; - eprintln!( - "[ducklink-shim:aba] resolved provider '{}' [{}]; registered {} fn(s); reasoning: {}", - report.chosen_id, report.chosen_kind, report.registered, report.reasoning - ); + /// Emit a `_init_c_api` shim entrypoint. The generator adds one line + /// per managed logical extension; the body is the shared, manifest-driven + /// [`shim_entry`]. + macro_rules! ducklink_shim { + ($name:literal, $init:ident) => { + // DuckLink shim entrypoint (auto-generated by `ducklink_shim!`). + // Safety: called by DuckDB during `LOAD` with a valid info/access pair. + #[no_mangle] + pub unsafe extern "C" fn $init( + info: ffi::duckdb_extension_info, + access: *const ffi::duckdb_extension_access, + ) -> bool { + shim_entry($name, info, access) + } + }; + } + + // ---- The managed-extension shim table (the generator owns this list) ---- + ducklink_shim!("aba", aba_init_c_api); + + // ----------------------------------------------------------------------- + // The NATIVE provider implementation for `aba` (a real native + // `.duckdb_extension`). Served as `aba_native.duckdb_extension` (exports + // `aba_native_init_c_api`); the shim's native arm `LOAD`s it. This is the + // native-speed passthrough: `aba_validate` computed in compiled Rust, no wasm. + // ----------------------------------------------------------------------- + + /// ABA routing-number checksum, native Rust (mirrors the wasm component's + /// semantics: 9 digits, weights 3,7,1,..., sum % 10 == 0; spaces/hyphens + /// ignored). The provider-neutral conformance suite certifies the two agree. + fn aba_checksum_valid(s: &str) -> bool { + let mut digits: Vec = Vec::with_capacity(s.len()); + for c in s.chars() { + if c.is_whitespace() || c == '-' { + continue; + } + match c.to_digit(10) { + Some(d) => digits.push(d), + None => return false, + } + } + if digits.len() != 9 { + return false; + } + let w = [3u32, 7, 1, 3, 7, 1, 3, 7, 1]; + let sum: u32 = digits.iter().zip(w).map(|(&d, k)| d * k).sum(); + sum % 10 == 0 + } + + struct AbaValidateNative; + + impl VScalar for AbaValidateNative { + type State = (); + fn invoke( + _: &Self::State, + input: &mut DataChunkHandle, + output: &mut dyn WritableVector, + ) -> Result<(), Box> { + let len = input.len(); + let in_vec = input.flat_vector(0); + let strs = unsafe { in_vec.as_slice_with_len::(len) }; + let mut out = output.flat_vector(); + let out_slice = unsafe { out.as_mut_slice_with_len::(len) }; + for i in 0..len { + let mut t = strs[i]; + let s = DuckString::new(&mut t).as_str(); + out_slice[i] = aba_checksum_valid(&s); + } + Ok(()) + } + fn signatures() -> Vec { + vec![ScalarFunctionSignature::exact( + vec![LogicalTypeId::Varchar.into()], + LogicalTypeId::Boolean.into(), + )] + } + } + + /// NATIVE provider entrypoint for `aba` (file `aba_native.duckdb_extension`). + /// + /// # Safety + /// Called by DuckDB during `LOAD 'aba_native...'` with a valid pair. + #[no_mangle] + pub unsafe extern "C" fn aba_native_init_c_api( + info: ffi::duckdb_extension_info, + access: *const ffi::duckdb_extension_access, + ) -> bool { + match aba_native_init(info, access) { + Ok(b) => b, + Err(e) => { + if let Some(set_error) = (*access).set_error { + if let Ok(c) = CString::new(e.to_string()) { + set_error(info, c.as_ptr()); + } + } + false + } + } + } + + unsafe fn aba_native_init( + info: ffi::duckdb_extension_info, + access: *const ffi::duckdb_extension_access, + ) -> Result> { + let con = match open_connection(info, access)? { + Some(c) => c, + None => return Ok(false), + }; + con.register_scalar_function::("aba_validate") + .map_err(stringify)?; + eprintln!("[ducklink-native:aba] registered native aba_validate"); Ok(true) } } diff --git a/src/passthrough.rs b/src/passthrough.rs index 42b0253..a2764c0 100644 --- a/src/passthrough.rs +++ b/src/passthrough.rs @@ -60,7 +60,23 @@ pub fn ducklink_load( env: &Env, policy: &ResolvePolicy, ) -> Result { - let res = resolver::resolve(entry, env, policy, None).map_err(|e| anyhow!(e.to_string()))?; + ducklink_load_gated(con, engine, entry, env, policy, None) +} + +/// As [`ducklink_load`], but with the conformance HARD GATE's canonical +/// suite-digest wired in: a non-reference provider is admitted only if its +/// `conformance.suite_digest` equals `canonical_suite_digest` (anti-forgery), on +/// top of passed + contract match. `None` skips the suite-digest sub-check. +pub fn ducklink_load_gated( + con: &Connection, + engine: Arc>, + entry: &ManifestEntry, + env: &Env, + policy: &ResolvePolicy, + canonical_suite_digest: Option<&str>, +) -> Result { + let res = resolver::resolve(entry, env, policy, canonical_suite_digest) + .map_err(|e| anyhow!(e.to_string()))?; let reasoning = resolver::render_reasoning(&res.reasoning); let registered = match res.chosen_kind { @@ -148,6 +164,119 @@ pub fn aba_manifest(wasm_artifact: PathBuf, native_artifact: Option) -> } } +// --------------------------------------------------------------------------- +// The productionized SHIM body (manifest-driven; design "D" first-cut). +// +// A shim named `.duckdb_extension` (exporting `_init_c_api`) calls +// `shim_load(con, "")`. It is the same for every logical extension -- the +// NAME is the only variable -- so one body + one macro generate every shim. +// +// Provider selection comes from the MULTI-PROVIDER MANIFEST (the generalized +// `registry/index.json` `providers[]`), NOT from an env var. The manifest + +// suite live under `DUCKLINK_HOME` (the DuckLink install dir): +// $DUCKLINK_HOME/index.json +// $DUCKLINK_HOME/ +// $DUCKLINK_HOME/suites/.sql (the conformance suite; its content +// digest is the canonical suite_digest) +// +// Policy OVERRIDES are env-driven on STOCK duckdb, because the stable C +// extension API has no runtime-setting registration (no `duckdb_add_extension_ +// option`; only open-time `duckdb_set_config`), so `SET extension_provider=...` +// cannot be honored by a C-API shim. The equivalents: +// DUCKLINK_ALLOW_NATIVE=0 -> deny the native arm (== SET allow_native_providers=false) +// DUCKLINK_PROVIDER= -> force a provider id (== SET extension_provider=) +// DUCKLINK_DENY=, -> exclude provider ids +// (See the report: `SET`-based overrides need a C++ base extension / +// DuckDB exposing add_extension_option in the C API -- roadmap.) +// --------------------------------------------------------------------------- + +/// The shared, manifest-driven shim body. Reads `DUCKLINK_HOME/index.json`, +/// resolves the best certified provider for `name`, and dual-loads it. +pub fn shim_load(con: &Connection, name: &str) -> Result { + let home = std::env::var("DUCKLINK_HOME") + .map(PathBuf::from) + .map_err(|_| anyhow!("DUCKLINK_HOME not set (the DuckLink install dir with index.json)"))?; + + let index_path = home.join("index.json"); + let index_bytes = std::fs::read(&index_path) + .map_err(|e| anyhow!("reading {}: {e}", index_path.display()))?; + let index: serde_json::Value = serde_json::from_slice(&index_bytes) + .map_err(|e| anyhow!("parsing {}: {e}", index_path.display()))?; + + let mut entry = resolver::read_manifest_entry(&index, name) + .ok_or_else(|| anyhow!("no manifest entry for '{name}' in {}", index_path.display()))?; + absolutize_artifacts(&mut entry, &home); + + // Canonical suite digest = content digest of the suite the resolver holds. + let canonical = canonical_suite_digest(&home, name); + + let env = env_from_environment(); + let policy = policy_from_environment(); + let engine = Arc::new(Mutex::new(Engine2::new()?)); + ducklink_load_gated(con, engine, &entry, &env, &policy, canonical.as_deref()) +} + +/// Rewrite each provider's artifact (relative in the manifest) to an absolute +/// path under `home`. Leaves remote URLs (`oci://`, `http`) untouched. +fn absolutize_artifacts(entry: &mut ManifestEntry, home: &std::path::Path) { + fn fix(r: &ContentRef, home: &std::path::Path) -> ContentRef { + match r { + ContentRef::Path(p) if p.is_relative() => ContentRef::Path(home.join(p)), + ContentRef::Oci(s) if !s.contains("://") => { + ContentRef::Oci(home.join(s).to_string_lossy().into_owned()) + } + other => other.clone(), + } + } + for p in &mut entry.providers { + match &mut p.kind { + ProviderKind::Wasm { artifact, .. } => *artifact = fix(artifact, home), + ProviderKind::Native { artifact, .. } => *artifact = fix(artifact, home), + ProviderKind::Remote { .. } => {} + } + } +} + +/// Content digest of the conformance suite for `name` (sha256 hex of +/// `$home/suites/.sql`). `None` if absent -> the gate's suite-digest +/// sub-check is skipped (passed + contract still enforced). +fn canonical_suite_digest(home: &std::path::Path, name: &str) -> Option { + let suite = home.join("suites").join(format!("{name}.sql")); + let bytes = std::fs::read(suite).ok()?; + Some(sha256_hex(&bytes)) +} + +fn sha256_hex(bytes: &[u8]) -> String { + use sha2::{Digest, Sha256}; + let mut h = Sha256::new(); + h.update(bytes); + h.finalize().iter().map(|b| format!("{b:02x}")).collect() +} + +/// Native arm allowed unless `DUCKLINK_ALLOW_NATIVE=0`. Default ON -- the native +/// passthrough is the headline: plain `LOAD aba` prefers native (precedence). +fn env_from_environment() -> Env { + let allow_native = std::env::var("DUCKLINK_ALLOW_NATIVE") + .map(|v| v != "0" && !v.eq_ignore_ascii_case("false")) + .unwrap_or(true); + Env { + wasm_runtime: true, + allow_native, + } +} + +fn policy_from_environment() -> ResolvePolicy { + let forced_provider = std::env::var("DUCKLINK_PROVIDER").ok().filter(|s| !s.is_empty()); + let denied = std::env::var("DUCKLINK_DENY") + .ok() + .map(|s| s.split(',').filter(|x| !x.is_empty()).map(|x| x.to_string()).collect()) + .unwrap_or_default(); + ResolvePolicy { + forced_provider, + denied, + } +} + #[cfg(all(test, feature = "bundled"))] mod tests { use super::*; diff --git a/tooling/ducklink-install.sh b/tooling/ducklink-install.sh new file mode 100755 index 0000000..e72abae --- /dev/null +++ b/tooling/ducklink-install.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# ducklink-install.sh - pre-populate the local extension dir so plain +# `LOAD ` works on stock DuckDB (the `ducklink install ` step). +# +# Drives stock DuckDB's `INSTALL FROM ''`, which fetches the shim +# from the DuckLink repo and copies it into the extension directory. Thereafter a +# plain `LOAD ` (any session) loads it - no manual INSTALL. +# +# Usage: ducklink-install.sh [duckdb_cli] +set -euo pipefail +NAME=${1:?name}; REPO=${2:?repo_dir}; EXTDIR=${3:?extension_dir}; CLI=${4:-duckdb} +mkdir -p "$EXTDIR" +"$CLI" -unsigned :memory: <_init_c_api` per managed name (added via +# the `ducklink_shim!` table in src/lib.rs) plus the native-provider entrypoints. +# This stamps a copy as `.duckdb_extension` with the DuckDB metadata footer +# and lays it out at /// (the INSTALL FROM layout). +# +# Usage: gen-shim.sh +# e.g. gen-shim.sh aba osx_arm64 v1.5.4 ./ducklink-repo target/debug/libducklink.dylib +set -euo pipefail +NAME=${1:?name}; PLATFORM=${2:?platform}; DV=${3:?duckdb_version}; REPO=${4:?repo_dir}; LIB=${5:?lib_path} +HERE=$(cd "$(dirname "$0")/.." && pwd) +APPEND="$HERE/extension-ci-tools/scripts/append_extension_metadata.py" +OUT="$REPO/$DV/$PLATFORM" +mkdir -p "$OUT" +RAW=$(mktemp -t "$NAME.raw.XXXX") +cp "$LIB" "$RAW" +python3 "$APPEND" -l "$RAW" -o "$OUT/$NAME.duckdb_extension" -n "$NAME" \ + --abi-type C_STRUCT_UNSTABLE -dv "$DV" -p "$PLATFORM" -ev v0.0.1 +gzip -kf "$OUT/$NAME.duckdb_extension" +rm -f "$RAW" +echo "wrote $OUT/$NAME.duckdb_extension (+ .gz)" diff --git a/tooling/native-conformance.sh b/tooling/native-conformance.sh new file mode 100755 index 0000000..1ea1e1f --- /dev/null +++ b/tooling/native-conformance.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# native-conformance.sh - run the provider-neutral conformance suite against the +# NATIVE provider of on stock DuckDB and, on pass, emit its conformance +# record (suite + content-digest + contract + passed) for registry/index.json. +# +# The suite is provider-blind SQL (the seed is each extension's smoke.sql). A +# pass certifies the native provider AT the current contract + suite digest; the +# resolver's hard gate admits only records whose suite_digest equals the +# canonical (sha256 of the suite) and whose `at` equals the live wit_contract. +# +# Usage: native-conformance.sh [cli] +set -euo pipefail +NAME=${1:?name}; ART=${2:?native_artifact}; SUITE=${3:?suite.sql}; CONTRACT=${4:?contract_digest}; CLI=${5:-duckdb} + +# Canonical suite digest = content digest of the suite itself. +SUITE_DIGEST=$(shasum -a 256 "$SUITE" | awk '{print $1}') + +# Run the suite (provider-blind SQL) against the native provider in `.mode csv`, +# the SAME way the canonical smoke/conformance harness emits, so it compares +# against the EXISTING reviewed fixture (suite's companion `.expected`, minus its +# `#` comment lines). The reference (wasm) provider is certified-by-construction +# against this same fixture; matching it certifies the native provider. +GOT=$("$CLI" -unsigned :memory: 2>/dev/null </dev/null; then + PASSED=false + fi +fi + +if [[ "$PASSED" == true ]]; then + echo "[$NAME native] CONFORMANCE PASSED at contract ${CONTRACT:0:12} suite ${SUITE_DIGEST:0:12}" + cat < NATIVE passthrough (precedence native > wasm) +# 2. DUCKLINK_PROVIDER=wasm... -> force the WASM provider +# 3. DUCKLINK_ALLOW_NATIVE=0 -> native denied, WASM fallback +# 4. tampered suite_digest -> conformance gate rejects native, WASM fallback +# plus the native conformance run (suite vs the native provider). +# +# Requires: a stock DuckDB v1.5.4 CLI (path in $DUCKDB_CLI), built against the +# unstable C API. Run with `-unsigned` (the Stage-0 signing posture). +# +# Usage: DUCKDB_CLI=/path/to/duckdb verify-d.sh [workdir] +set -euo pipefail +HERE=$(cd "$(dirname "$0")/.." && pwd) # native-extension/ducklink +ROOT=$(cd "$HERE/../.." && pwd) # ducklink monorepo +CLI=${DUCKDB_CLI:?set DUCKDB_CLI to a stock duckdb v1.5.4 binary} +WORK=${1:-$(mktemp -d)} +PLATFORM=$("$CLI" -noheader -list -c "PRAGMA platform" :memory:) +REV=$("$CLI" -noheader -list -c "SELECT version()" :memory:) +CONTRACT=$(python3 -c "import json;d=json.load(open('$ROOT/registry/index.json'));print([e for e in d['extensions'] if e['name']=='aba'][0]['wit_contract'])") +SUITE=$ROOT/extensions/aba-component/smoke.sql +LIB=$HERE/target/debug/libducklink.dylib + +REPO=$WORK/repo; DLHOME=$WORK/home; EXTDIR=$WORK/extdir +rm -rf "$REPO" "$DLHOME" "$EXTDIR"; mkdir -p "$DLHOME/artifacts" "$DLHOME/suites" "$EXTDIR" + +echo "## build"; ( cd "$HERE" && cargo build >/dev/null 2>&1 ) +echo "## generate shim + native provider into the repo ($REV/$PLATFORM)" +bash "$HERE/tooling/gen-shim.sh" aba "$PLATFORM" "$REV" "$REPO" "$LIB" >/dev/null +bash "$HERE/tooling/gen-shim.sh" aba_native "$PLATFORM" "$REV" "$REPO" "$LIB" >/dev/null + +echo "## DuckLink home (manifest + artifacts + suite)" +cp "$ROOT/artifacts/extensions/aba.wasm" "$DLHOME/artifacts/aba.wasm" +cp "$REPO/$REV/$PLATFORM/aba_native.duckdb_extension" "$DLHOME/artifacts/aba_native.duckdb_extension" +cp "$SUITE" "$DLHOME/suites/aba.sql" +SUITE_DIGEST=$(shasum -a 256 "$SUITE" | awk '{print $1}') +python3 - "$DLHOME/index.json" "$CONTRACT" "$SUITE_DIGEST" <<'PY' +import json,sys +out,contract,suite=sys.argv[1:4] +json.dump({"extensions":[{"name":"aba","wit_contract":contract,"wit_contract_version":"2.0.0","providers":[ + {"id":"wasm-component","kind":"wasm","reference":True,"abi":"duckdb:extension@2.0.0","artifact":"artifacts/aba.wasm", + "conformance":{"suite":"aba@2","suite_digest":suite,"at":contract,"passed":True}}, + {"id":"native-arm64-macos","kind":"native","platform":{"os":"macos","arch":"arm64"},"artifact":"artifacts/aba_native.duckdb_extension", + "trust":{"signed_by":"ed25519:ducklink-dev","attestation":"none"}, + "conformance":{"suite":"aba@2","suite_digest":suite,"at":contract,"passed":True}}]}]},open(out,"w"),indent=1) +PY + +echo "## ducklink install aba" +bash "$HERE/tooling/ducklink-install.sh" aba "$REPO" "$EXTDIR" "$CLI" >/dev/null + +run(){ DUCKLINK_HOME="$DLHOME" "$@" "$CLI" -unsigned -box :memory:; } +echo "== 1: plain LOAD aba -> NATIVE ==" +DUCKLINK_HOME="$DLHOME" "$CLI" -unsigned :memory: < wasm ==" +DUCKLINK_HOME="$DLHOME" DUCKLINK_ALLOW_NATIVE=0 "$CLI" -unsigned :memory: < Date: Sat, 27 Jun 2026 12:09:26 -0400 Subject: [PATCH 06/10] fix(tooling): conformance digest = C's structured scheme; verify-d scenario 4 native-conformance.sh now computes the canonical suite_digest with build C's structured scheme over conformance.{sql,expected} (byte-identical to resolver::compute_suite_digest), not a plain sha256, so the emitted record matches the resolver's canonical. verify-d.sh adds scenario 4 (tampered suite_digest -> gate rejects native -> wasm fallback) and runs over the unified conformance suite. --- tooling/native-conformance.sh | 35 ++++++++++++++++++++++++++--------- tooling/verify-d.sh | 12 ++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/tooling/native-conformance.sh b/tooling/native-conformance.sh index 1ea1e1f..9a915df 100755 --- a/tooling/native-conformance.sh +++ b/tooling/native-conformance.sh @@ -3,17 +3,35 @@ # NATIVE provider of on stock DuckDB and, on pass, emit its conformance # record (suite + content-digest + contract + passed) for registry/index.json. # -# The suite is provider-blind SQL (the seed is each extension's smoke.sql). A -# pass certifies the native provider AT the current contract + suite digest; the -# resolver's hard gate admits only records whose suite_digest equals the -# canonical (sha256 of the suite) and whose `at` equals the live wit_contract. +# The suite is provider-blind SQL (extensions/-component/conformance.sql). +# A pass certifies the native provider AT the current contract + suite digest; +# the resolver's hard gate admits only records whose suite_digest equals the +# canonical and whose `at` equals the live wit_contract. # -# Usage: native-conformance.sh [cli] +# Usage: native-conformance.sh [cli] set -euo pipefail -NAME=${1:?name}; ART=${2:?native_artifact}; SUITE=${3:?suite.sql}; CONTRACT=${4:?contract_digest}; CLI=${5:-duckdb} +NAME=${1:?name}; ART=${2:?native_artifact}; SUITE=${3:?conformance.sql}; CONTRACT=${4:?contract_digest}; CLI=${5:-duckdb} +EXP="${SUITE%.sql}.expected" -# Canonical suite digest = content digest of the suite itself. -SUITE_DIGEST=$(shasum -a 256 "$SUITE" | awk '{print $1}') +# Canonical suite digest -- build C's STRUCTURED scheme over conformance.{sql,expected} +# (byte-identical to resolver::compute_suite_digest / tooling/conformance.py), +# NOT a plain sha256, so the emitted record's suite_digest matches the canonical +# the resolver recomputes. +SUITE_DIGEST=$(python3 - "$SUITE" "$EXP" <<'PY' +import hashlib,sys +sql=open(sys.argv[1]).read(); exp=open(sys.argv[2]).read() +nsql="\n".join(l.rstrip() for l in sql.splitlines() if l.strip() and not l.lstrip().startswith("--")) +ne=[] +for l in exp.splitlines(): + s=l.rstrip() + if not s.strip(): continue + ls=s.lstrip() + if ls=="#" or ls.startswith("# "): continue + ne.append(s) +canon=b"duckdb:conformance-suite:1\n"+nsql.encode()+b"\n\x1e\n"+"\n".join(ne).encode() +print(hashlib.sha256(canon).hexdigest()) +PY +) # Run the suite (provider-blind SQL) against the native provider in `.mode csv`, # the SAME way the canonical smoke/conformance harness emits, so it compares @@ -29,7 +47,6 @@ SQL GOT="${GOT//$'\r'/}" # .mode csv emits RFC-4180 CRLF; normalize to LF echo "[$NAME native] suite output:"; echo "$GOT" | sed 's/^/ /' -EXP="${SUITE%.sql}.expected" PASSED=true if [[ -f "$EXP" ]]; then if ! diff <(echo "$GOT") <(grep -vE '^[[:space:]]*(#|$)' "$EXP") >/dev/null; then diff --git a/tooling/verify-d.sh b/tooling/verify-d.sh index 7a5f4b1..807069c 100755 --- a/tooling/verify-d.sh +++ b/tooling/verify-d.sh @@ -83,6 +83,18 @@ echo "== 3: deny native -> wasm ==" DUCKLINK_HOME="$DLHOME" DUCKLINK_ALLOW_NATIVE=0 "$CLI" -unsigned :memory: < gate rejects native -> wasm ==" +DLBAD=$WORK/home-tampered; rm -rf "$DLBAD"; cp -R "$DLHOME" "$DLBAD" +python3 - "$DLBAD/index.json" <<'PY' +import json,sys +p=sys.argv[1]; d=json.load(open(p)) +for pr in d["extensions"][0]["providers"]: + if pr["kind"]=="native": pr["conformance"]["suite_digest"]="DEADBEEF-forged" +json.dump(d,open(p,"w"),indent=1) +PY +DUCKLINK_HOME="$DLBAD" "$CLI" -unsigned :memory: < Date: Thu, 9 Jul 2026 15:33:11 -0400 Subject: [PATCH 07/10] feat(engine): thread logical_types + casts through LoadedComponent (#79 scaffold) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guest register_logical_type + register_cast calls succeed at load() and get captured into pending_logical_types / pending_casts, but engine.rs::load() dropped them on the floor when building LoadedComponent — only scalars/ tables/aggregates survived. This left GEOMETRY unregistered at DuckDB and every scalar taking GEOMETRY failed to bind: 'st_astext(BLOB) no match'. New LogicalTypeAlias + CastReg fields on LoadedComponent, populated from drain_pending output. reg_duckdb.rs gains register_logical_types_stub + register_casts_stub that log each intended registration. The stubs are no-ops today; the real DuckDB C-API wiring is the follow-up: 1. duckdb_create_logical_type(physical) -> handle 2. duckdb_logical_type_set_alias(handle, name) 3. duckdb_register_cast(source_handle, target_handle, callback) Once (1)-(3) land, ~213 currently-failing GEOMETRY-typed cases (#64/#67/#73 downstream of #79) should verify. This commit is the plumbing so the follow-up delta is minimal. --- src/engine.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++--- src/reg_duckdb.rs | 26 +++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 02bb2e4..ae926db 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -134,6 +134,30 @@ pub struct AggregateFunc { pub callback_handle: u32, } +/// A logical-type registration a component requested during load(). +/// #79 scaffold: drain path carries these through; the DuckDB sink currently +/// logs but does not yet invoke `duckdb_create_logical_type` + alias — that's +/// a follow-up C-API impl. Once wired, GEOMETRY/GEOGRAPHY etc. become +/// bind-time aliases over BLOB, closing the "st_astext(BLOB) no-match" gap. +#[derive(Clone, Debug)] +pub struct LogicalTypeAlias { + pub extension: String, + pub name: String, + pub physical: String, +} + +/// A cast registration a component requested during load(). #79 scaffold — +/// paired with `LogicalTypeAlias` for the eventual DuckDB C-API implicit-cast +/// wiring. `callback_handle` refers to the guest-side identity/marshal fn +/// (currently ignored by the stub sink). +#[derive(Clone, Debug)] +pub struct CastReg { + pub extension: String, + pub source: String, + pub target: String, + pub callback_handle: u32, +} + /// What a component registered: the functions a direction-specific sink bridges /// into the database. #[derive(Clone, Debug, Default)] @@ -141,6 +165,10 @@ pub struct LoadedComponent { pub scalars: Vec, pub tables: Vec, pub aggregates: Vec, + /// #79 scaffold — see `LogicalTypeAlias`. + pub logical_types: Vec, + /// #79 scaffold — see `CastReg`. + pub casts: Vec, } /// Process-wide Direction-2 engine: loads components and dispatches DuckDB @@ -218,11 +246,39 @@ impl Engine2 { callback_handle: a.callback_handle, }) .collect(); + // #79 scaffold — drain logical_types + casts so a follow-up sink can + // wire them into DuckDB's C-API. Currently the direction-2 sink (see + // reg_duckdb::register_logical_types_stub) logs but does not create + // aliases; every GEOMETRY-typed scalar therefore still surfaces to + // the binder as BLOB. When the C-API impl lands (duckdb_create_logical_type + // + duckdb_logical_type_set_alias + duckdb_register_cast), the binder + // resolves GEOMETRY → BLOB at bind time. + let logical_types = pending + .logical_types + .into_iter() + .map(|lt| LogicalTypeAlias { + extension: lt.extension, + name: lt.name, + physical: lt.physical, + }) + .collect(); + let casts = pending + .casts + .into_iter() + .map(|c| CastReg { + extension: c.extension, + source: c.source, + target: c.target, + callback_handle: c.callback_handle, + }) + .collect(); self.instances.insert(extension.to_string(), instance); Ok(LoadedComponent { scalars, tables, aggregates, + logical_types, + casts, }) } @@ -243,7 +299,7 @@ impl Engine2 { }; let instance = self .instances - .get_mut(&entry.extension) + .get_mut(&*entry.extension) .ok_or_else(|| anyhow!("extension '{}' is not loaded", entry.extension))?; let wit_args: Vec = args.into_iter().map(neutral_to_wit).collect(); @@ -282,7 +338,7 @@ impl Engine2 { }; let instance = self .instances - .get_mut(&entry.extension) + .get_mut(&*entry.extension) .ok_or_else(|| anyhow!("extension '{}' is not loaded", entry.extension))?; let ctx = extension_runtime::Invokeinfo { rowindex: Some(base_row_index), @@ -309,7 +365,7 @@ impl Engine2 { }; let instance = self .instances - .get_mut(&entry.extension) + .get_mut(&*entry.extension) .ok_or_else(|| anyhow!("extension '{}' is not loaded", entry.extension))?; let wit_args: Vec = args.into_iter().map(neutral_to_wit).collect(); @@ -339,7 +395,7 @@ impl Engine2 { }; let instance = self .instances - .get_mut(&entry.extension) + .get_mut(&*entry.extension) .ok_or_else(|| anyhow!("extension '{}' is not loaded", entry.extension))?; let wit_rows: Vec> = rows .into_iter() diff --git a/src/reg_duckdb.rs b/src/reg_duckdb.rs index 70bef3f..b1afd2b 100644 --- a/src/reg_duckdb.rs +++ b/src/reg_duckdb.rs @@ -754,6 +754,32 @@ impl VTab for WasmTable { } } +/// #79 STUB — log the logical-type aliases a component wants to register. +/// Follow-up C-API impl will replace the eprintln with: +/// let handle = ffi::duckdb_create_logical_type(duckdb_type_of_physical(<.physical)); +/// ffi::duckdb_logical_type_set_alias(handle, CString::new(<.name)?); +/// // stash the handle so the CastReg pass can reference it for +/// // duckdb_register_cast(source_handle, target_handle, callback). +/// Currently a no-op so builds pass and the plumbing stays live. +pub fn register_logical_types_stub(logical_types: &[super::engine::LogicalTypeAlias]) { + for lt in logical_types { + eprintln!( + "[reg_duckdb] logical_type: ext='{}' name='{}' physical='{}' -- STUB (C-API follow-up #79)", + lt.extension, lt.name, lt.physical + ); + } +} + +/// #79 STUB — companion to register_logical_types_stub. Currently a no-op. +pub fn register_casts_stub(casts: &[super::engine::CastReg]) { + for c in casts { + eprintln!( + "[reg_duckdb] cast: ext='{}' source='{}' target='{}' callback={} -- STUB (C-API follow-up #79)", + c.extension, c.source, c.target, c.callback_handle + ); + } +} + /// Register every component table function on `con`. Returns the count /// registered. Parameter and column types use the same `reg` logical-type set as /// scalars. From 053cbe351ed254ab6de32b407db8816de957b142 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 9 Jul 2026 16:20:29 -0400 Subject: [PATCH 08/10] feat(reg_duckdb): implement #79 C-API for register_logical_types + register_casts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the stubs with real DuckDB C-API calls: register_logical_types: 1. duckdb_create_logical_type(physical) -> handle 2. duckdb_logical_type_set_alias(handle, name) 3. duckdb_register_logical_type(con, handle, null) 4. duckdb_destroy_logical_type(&mut handle) register_casts: 1. duckdb_create_cast_function() + set_source_type + set_target_type 2. duckdb_cast_function_set_implicit_cast_cost(cast, 0) # cheapest 3. duckdb_cast_function_set_function(cast, identity_cast_trampoline) 4. duckdb_register_cast_function(con, cast) 5. duckdb_destroy_cast_function + destroy_logical_type on locals Wired into direction-2 load pipeline BEFORE register_scalars so the binder sees GEOMETRY/GEOGRAPHY as valid types when scalars register their arg-type expectations. Casts follow with cost=0 for implicit resolution. identity_cast_trampoline: same-physical-type casts are byte-identical; the trampoline returns true without writing, and DuckDB's default row-copy propagates values verbatim. duckdb_type_of_physical: string→duckdb_type map covering BLOB/TEXT/INT64/ INT32/FLOAT64/FLOAT32/BOOLEAN with BLOB as safe fallback. Closes #79. Downstream #64 (GEOMETRY binder no-match) and #73 (st_makepoint demotion tracking) should resolve on the next batch: ~213 currently-failing GEOMETRY-typed corpus cases now have a bind path. --- src/reg_duckdb.rs | 168 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 155 insertions(+), 13 deletions(-) diff --git a/src/reg_duckdb.rs b/src/reg_duckdb.rs index b1afd2b..f5e6206 100644 --- a/src/reg_duckdb.rs +++ b/src/reg_duckdb.rs @@ -754,30 +754,153 @@ impl VTab for WasmTable { } } -/// #79 STUB — log the logical-type aliases a component wants to register. -/// Follow-up C-API impl will replace the eprintln with: -/// let handle = ffi::duckdb_create_logical_type(duckdb_type_of_physical(<.physical)); -/// ffi::duckdb_logical_type_set_alias(handle, CString::new(<.name)?); -/// // stash the handle so the CastReg pass can reference it for -/// // duckdb_register_cast(source_handle, target_handle, callback). -/// Currently a no-op so builds pass and the plumbing stays live. -pub fn register_logical_types_stub(logical_types: &[super::engine::LogicalTypeAlias]) { +/// Map a physical-type string (from the guest's LogicalTypeReg.physical) to a +/// DuckDB C-API type code. The physical is the storage-level type; the alias +/// is a bind-time name that resolves through it. +fn duckdb_type_of_physical(physical: &str) -> ffi::duckdb_type { + match physical.to_ascii_uppercase().as_str() { + "BLOB" => duckdb_type_of(T_BLOB), + "TEXT" | "VARCHAR" => duckdb_type_of(T_TEXT), + "INT64" | "BIGINT" => duckdb_type_of(T_I64), + "INT32" | "INTEGER" => duckdb_type_of(T_I32), + "FLOAT64" | "DOUBLE" => duckdb_type_of(T_F64), + "FLOAT32" | "REAL" => duckdb_type_of(T_F32), + "BOOLEAN" | "BOOL" => duckdb_type_of(T_BOOL), + _ => { + // Unknown physical types fall back to BLOB — safe: opaque bytes + // survive round-trip. Follow-up: expand this table when new WIT + // Logicaltype variants land. + duckdb_type_of(T_BLOB) + } + } +} + +/// #79 — Register logical-type aliases at DuckDB via the C-API. For each +/// LogicalTypeAlias: +/// 1. `duckdb_create_logical_type(physical)` -> handle +/// 2. `duckdb_logical_type_set_alias(handle, name)` +/// 3. `duckdb_register_logical_type(con, handle, null)` — registers the +/// alias on the connection so subsequent `SELECT ... ::GEOMETRY` binds +/// to the handle instead of failing at type-resolution. +/// 4. `duckdb_destroy_logical_type` — the C-API copies internally. +/// +/// After this pass, a subsequent scalar registered as `st_area(BLOB)` accepts +/// arguments cast as GEOMETRY because the alias resolves to BLOB physically. +/// +/// # Safety +/// `raw_con` must be a valid `duckdb_connection`. +pub unsafe fn register_logical_types( + raw_con: ffi::duckdb_connection, + logical_types: &[super::engine::LogicalTypeAlias], +) -> duckdb::Result { + let mut registered = 0usize; for lt in logical_types { + let physical = duckdb_type_of_physical(<.physical); + let mut handle = ffi::duckdb_create_logical_type(physical); + if handle.is_null() { + eprintln!( + "[reg_duckdb] logical_type: create_logical_type returned null for ext='{}' name='{}' physical='{}' — skipping", + lt.extension, lt.name, lt.physical + ); + continue; + } + let cname = CString::new(lt.name.as_str()) + .map_err(|_| duckdb::Error::DuckDBFailure(ffi::Error::new(ffi::DuckDBError), None))?; + ffi::duckdb_logical_type_set_alias(handle, cname.as_ptr()); + // `info` param is opaque/optional per the C-API docs — null is accepted. + let rc = ffi::duckdb_register_logical_type(raw_con, handle, std::ptr::null_mut()); + ffi::duckdb_destroy_logical_type(&mut handle); + if rc != ffi::duckdb_state_DuckDBSuccess { + eprintln!( + "[reg_duckdb] logical_type: register_logical_type failed (rc={}) for ext='{}' name='{}' — likely already registered", + rc, lt.extension, lt.name + ); + continue; + } eprintln!( - "[reg_duckdb] logical_type: ext='{}' name='{}' physical='{}' -- STUB (C-API follow-up #79)", + "[reg_duckdb] logical_type: registered ext='{}' name='{}' physical='{}'", lt.extension, lt.name, lt.physical ); + registered += 1; } + Ok(registered) } -/// #79 STUB — companion to register_logical_types_stub. Currently a no-op. -pub fn register_casts_stub(casts: &[super::engine::CastReg]) { +/// #79 — Identity cast callback. GEOMETRY↔BLOB (and similar same-physical +/// aliases) are byte-identical, so the cast is a pointer/data copy. DuckDB's +/// vectorised cast API expects us to write output values chunk-at-a-time; we +/// invoke `duckdb_data_chunk_copy_from` moral equivalent via memcpy on the +/// underlying vectors. For the initial impl, defer to DuckDB's default row- +/// copy behavior by returning true from the trampoline: since source and +/// target physical types are identical, the C runtime propagates values +/// verbatim. +unsafe extern "C" fn identity_cast_trampoline( + _info: ffi::duckdb_function_info, + _row_count: u64, + _input: ffi::duckdb_vector, + _output: ffi::duckdb_vector, +) -> bool { + // Same physical type on both sides — signal success without transforming. + // DuckDB's default cast for identical physical types is a memcpy, which + // this callback opts into by returning true and not writing anything. + true +} + +/// #79 — Register implicit casts between aliased and underlying types. For a +/// GEOMETRY↔BLOB alias, this makes `st_area(GEOMETRY)` and `st_area(BLOB)` +/// interchangeable at the binder level. +/// +/// # Safety +/// `raw_con` must be a valid `duckdb_connection`. Every `source` and `target` +/// name must resolve to a registered logical type (via `register_logical_types` +/// or as a DuckDB built-in). +pub unsafe fn register_casts( + raw_con: ffi::duckdb_connection, + casts: &[super::engine::CastReg], +) -> duckdb::Result { + let mut registered = 0usize; for c in casts { + // Both source and target resolve through the same physical-type map. + // For GEOMETRY↔BLOB this is BLOB on both sides, and the identity + // trampoline handles the "cast". + let mut source_ty = ffi::duckdb_create_logical_type(duckdb_type_of_physical(&c.source)); + let mut target_ty = ffi::duckdb_create_logical_type(duckdb_type_of_physical(&c.target)); + // Restore the alias for lookup — DuckDB matches casts by (source, + // target) logical-type identity, which includes the alias if set. + if let Ok(cname) = CString::new(c.source.as_str()) { + ffi::duckdb_logical_type_set_alias(source_ty, cname.as_ptr()); + } + if let Ok(cname) = CString::new(c.target.as_str()) { + ffi::duckdb_logical_type_set_alias(target_ty, cname.as_ptr()); + } + let cast = ffi::duckdb_create_cast_function(); + ffi::duckdb_cast_function_set_source_type(cast, source_ty); + ffi::duckdb_cast_function_set_target_type(cast, target_ty); + // Cost 0 => cheapest implicit cast; the binder prefers it over the + // built-in explicit-cast lookup so alias→physical resolution wins. + ffi::duckdb_cast_function_set_implicit_cast_cost(cast, 0); + ffi::duckdb_cast_function_set_function(cast, Some(identity_cast_trampoline)); + let rc = ffi::duckdb_register_cast_function(raw_con, cast); + // The FFI docs say we own the cast function object; destroy it after + // register_cast_function copies whatever it needs. + let mut cast_owned = cast; + ffi::duckdb_destroy_cast_function(&mut cast_owned); + ffi::duckdb_destroy_logical_type(&mut source_ty); + ffi::duckdb_destroy_logical_type(&mut target_ty); + if rc != ffi::duckdb_state_DuckDBSuccess { + eprintln!( + "[reg_duckdb] cast: register_cast_function failed (rc={}) for ext='{}' source='{}' target='{}' — likely already registered", + rc, c.extension, c.source, c.target + ); + continue; + } eprintln!( - "[reg_duckdb] cast: ext='{}' source='{}' target='{}' callback={} -- STUB (C-API follow-up #79)", - c.extension, c.source, c.target, c.callback_handle + "[reg_duckdb] cast: registered ext='{}' source='{}' target='{}' (implicit-cost=0)", + c.extension, c.source, c.target ); + registered += 1; } + Ok(registered) } /// Register every component table function on `con`. Returns the count @@ -1224,6 +1347,25 @@ pub fn register_components( let mut e = engine.lock().expect("engine mutex poisoned"); e.load(&spec.name, &spec.path)? }; + // #79 — register logical-type aliases BEFORE scalars so the binder + // sees GEOMETRY/GEOGRAPHY/... as valid types when the scalars register + // their arg-type expectations. Casts follow, giving the binder an + // implicit path between the alias and its physical type. + if let Some(rc) = raw_con { + if !loaded.logical_types.is_empty() { + total += unsafe { register_logical_types(rc, &loaded.logical_types)? }; + } + if !loaded.casts.is_empty() { + total += unsafe { register_casts(rc, &loaded.casts)? }; + } + } else if !loaded.logical_types.is_empty() || !loaded.casts.is_empty() { + eprintln!( + "[ducklink] skipping {} logical_type + {} cast registration(s) from '{}': no raw connection available", + loaded.logical_types.len(), + loaded.casts.len(), + spec.name + ); + } total += register_scalars(con, engine.clone(), &loaded.scalars)?; total += register_tables(con, engine.clone(), &loaded.tables)?; match raw_con { From 7dbf911010d0ba59f22d5b282c21b0e830538a58 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 9 Jul 2026 18:34:46 -0400 Subject: [PATCH 09/10] fix(reg_duckdb): populate output vector in identity cast trampoline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the trampoline returned `true` without writing to the output vector, on the assumption that DuckDB would memcpy same-physical-type values by default. It does not — the callback must populate `output` itself. The empty output surfaced as a bare "Conversion Error" for BLOB->GEOMETRY (and the reverse) casts registered via #79. Iterate rows explicitly and copy validity + BLOB payloads via `duckdb_vector_assign_string_element_len`, mirroring the write path already used in write_ret_raw for BLOB returns. --- src/reg_duckdb.rs | 48 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/reg_duckdb.rs b/src/reg_duckdb.rs index f5e6206..78e00d6 100644 --- a/src/reg_duckdb.rs +++ b/src/reg_duckdb.rs @@ -827,22 +827,44 @@ pub unsafe fn register_logical_types( } /// #79 — Identity cast callback. GEOMETRY↔BLOB (and similar same-physical -/// aliases) are byte-identical, so the cast is a pointer/data copy. DuckDB's -/// vectorised cast API expects us to write output values chunk-at-a-time; we -/// invoke `duckdb_data_chunk_copy_from` moral equivalent via memcpy on the -/// underlying vectors. For the initial impl, defer to DuckDB's default row- -/// copy behavior by returning true from the trampoline: since source and -/// target physical types are identical, the C runtime propagates values -/// verbatim. +/// aliases) are byte-identical: the cast is a straight per-row copy of the +/// BLOB payload. DuckDB's cast API does NOT auto-populate the output vector +/// when the callback returns true — a "no-op" trampoline leaves `output` +/// uninitialised, which surfaces as an empty "Conversion Error" at query +/// time (that was the initial buggy impl documented in G3). Iterate rows +/// explicitly and copy validity + BLOB bytes via the same primitives the +/// scalar-return path uses (`duckdb_vector_assign_string_element_len`). +/// +/// Scope: this trampoline is only registered for BLOB↔BLOB aliases (the +/// GEOMETRY case). Extending to other same-physical-type aliases would +/// need a per-physical-type branch here — for now the register_casts +/// caller only produces BLOB pairs. unsafe extern "C" fn identity_cast_trampoline( _info: ffi::duckdb_function_info, - _row_count: u64, - _input: ffi::duckdb_vector, - _output: ffi::duckdb_vector, + row_count: u64, + input: ffi::duckdb_vector, + output: ffi::duckdb_vector, ) -> bool { - // Same physical type on both sides — signal success without transforming. - // DuckDB's default cast for identical physical types is a memcpy, which - // this callback opts into by returning true and not writing anything. + let in_validity = ffi::duckdb_vector_get_validity(input); + let in_data = ffi::duckdb_vector_get_data(input) as *const duckdb_string_t; + for i in 0..(row_count as usize) { + let is_valid = in_validity.is_null() + || ffi::duckdb_validity_row_is_valid(in_validity, i as u64); + if !is_valid { + ffi::duckdb_vector_ensure_validity_writable(output); + let out_validity = ffi::duckdb_vector_get_validity(output); + ffi::duckdb_validity_set_row_validity(out_validity, i as u64, false); + continue; + } + let mut s = *in_data.add(i); + let bytes = DuckString::new(&mut s).as_bytes(); + ffi::duckdb_vector_assign_string_element_len( + output, + i as u64, + bytes.as_ptr() as *const c_char, + bytes.len() as u64, + ); + } true } From f088df05977d9cd129c887de68658ee7e6a3127c Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 13 Jul 2026 18:24:04 -0400 Subject: [PATCH 10/10] feat(runtime): add storage-write-dispatch trampolines (contract 2.1.0) Ports the writable-storage half from the ducklink-runtime workspace crate (@4.0.0) into this SDK's runtime (@2.0.0). Additive within contract-major 2: existing @2.0.0 read-only storage components keep loading unmodified; only components that additionally export storage-write-dispatch instantiate the new `duckdb-extension-storage-write` world. WIT delta: * new `storage-write-dispatch.wit` interface (7 methods: begin/commit/ rollback-transaction, create-table, insert-rows, delete-rows, update-rows) mirroring the @4.0.0 shape verbatim, package header rewritten to duckdb:extension@2.0.0. * new `duckdb-extension-storage-write` world in the host WIT, exporting storage-dispatch + storage-write-dispatch on top of the base imports. * mirrored into wit-canonical/ so the witcanon digest reflects the additive interface as part of the contract identity. Runtime delta: * new `duckdb_extension_storage_write_bindings` bindgen module; types remapped via `with:` to the base bindings so the write trampolines exchange `extension_types::*` directly (no per-world conversion). * 7 new `storage_{begin,commit,rollback}_transaction` / `storage_{create_table,insert,delete,update}_rows` methods on `ExtensionInstance`, lazily building the writable-storage bindings from the already-loaded raw instance. --- runtime/src/extension.rs | 144 ++++++++++++++++++ runtime/src/lib.rs | 23 +++ .../storage-write-dispatch.wit | 48 ++++++ .../storage-write-dispatch.wit | 48 ++++++ runtime/wit/duckdb-extension-host.wit | 21 +++ 5 files changed, 284 insertions(+) create mode 100644 runtime/wit-canonical/duckdb-extension/storage-write-dispatch.wit create mode 100644 runtime/wit/deps/duckdb-extension/storage-write-dispatch.wit diff --git a/runtime/src/extension.rs b/runtime/src/extension.rs index 6f6cf90..68da05e 100644 --- a/runtime/src/extension.rs +++ b/runtime/src/extension.rs @@ -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, + // 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, // Item 3 / M2a: lazily-built index bindings (None until first index-dispatch // call or for non-index extensions). index_bindings: Option, @@ -1561,6 +1567,7 @@ impl ExtensionInstance { bindings, instance, storage_bindings: None, + storage_write_bindings: None, index_bindings: None, files_bindings: None, } @@ -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 { + 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], + ) -> Result { + 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 { + 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], + ) -> Result { + 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 diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 0ac66a5..561657e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -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 diff --git a/runtime/wit-canonical/duckdb-extension/storage-write-dispatch.wit b/runtime/wit-canonical/duckdb-extension/storage-write-dispatch.wit new file mode 100644 index 0000000..5fa6a1f --- /dev/null +++ b/runtime/wit-canonical/duckdb-extension/storage-write-dispatch.wit @@ -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; + + 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) -> result<_, duckerror>; + + // Append rows; returns the number inserted. + insert-rows: func(handle: u32, + txn: u32, + table: string, + rows: list>) -> result; + + // Delete rows by row-id; returns the number deleted. + delete-rows: func(handle: u32, + txn: u32, + table: string, + rowids: list) -> result; + + // Update rows by row-id (parallel `rowids` / `rows`); returns the number + // updated. + update-rows: func(handle: u32, + txn: u32, + table: string, + rowids: list, + rows: list>) -> result; +} diff --git a/runtime/wit/deps/duckdb-extension/storage-write-dispatch.wit b/runtime/wit/deps/duckdb-extension/storage-write-dispatch.wit new file mode 100644 index 0000000..5fa6a1f --- /dev/null +++ b/runtime/wit/deps/duckdb-extension/storage-write-dispatch.wit @@ -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; + + 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) -> result<_, duckerror>; + + // Append rows; returns the number inserted. + insert-rows: func(handle: u32, + txn: u32, + table: string, + rows: list>) -> result; + + // Delete rows by row-id; returns the number deleted. + delete-rows: func(handle: u32, + txn: u32, + table: string, + rowids: list) -> result; + + // Update rows by row-id (parallel `rowids` / `rows`); returns the number + // updated. + update-rows: func(handle: u32, + txn: u32, + table: string, + rowids: list, + rows: list>) -> result; +} diff --git a/runtime/wit/duckdb-extension-host.wit b/runtime/wit/duckdb-extension-host.wit index 0d0f417..4bc68df 100644 --- a/runtime/wit/duckdb-extension-host.wit +++ b/runtime/wit/duckdb-extension-host.wit @@ -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; @@ -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.