Guidance for working in this repository.
vgi-cbor is a VGI worker (a standalone binary DuckDB launches and talks to
over Apache Arrow IPC, ATTACH 'cbor' (TYPE vgi, LOCATION '…')) that brings
CBOR (RFC 8949) / MessagePack decode & encode, plus tag-aware COSE
(RFC 9052) / CWT (RFC 8392) / COSE_Key / WebAuthn / FIDO2 / CTAP2
structural decode, to SQL. Functions live under catalog cbor, schema main.
Built on the published VGI Rust SDK (vgi = "0.9.5" from crates.io), arrow 59.
Modeled on ../vgi-fixedformat. The repo builds standalone — no local SDK
checkout, no path dependency on the SDK.
The real value is the security-payload structural decode (COSE/CWT/WebAuthn)
and the bulk SQL join surface (cose_x5t / cose_x5chain / WebAuthn x5c join
to vgi-x509). The raw CBOR/msgpack codec is commodity.
crates/cbor-core/ # pure compute, NO arrow/vgi deps — independently testable
src/value.rs # bounded parse (MAX_NESTING), DecodeError taxonomy, dup-key scan
src/codec/ # json (to_json/from_json), diagnostic (EDN), encode (+ canonical), msgpack, tags
src/security/ # cose, cwt, cose_key, webauthn, registry (IANA alg/kty/crv)
src/validate.rs # is_valid / well_formed (kind taxonomy)
src/seq.rs # RFC 8742 CBOR Sequence
tests/vectors.rs # RFC 8949 App. A + COSE/CWT/WebAuthn golden fixtures
tests/fuzz.rs # proptest zero-panic gate (big-stack threads, see below)
crates/cbor-worker/ # arrow + vgi: maps core results onto DuckDB types, serves VGI
src/main.rs # bootstrap + catalog/schema metadata (source_url + tags)
src/arrow_io.rs # input blob reading + shared STRUCT schemas (COSE header, COSE_Key) + builders
src/value_in.rs # Arrow cell -> ciborium Value (encode paths)
src/value_out.rs # ciborium Value -> typed Arrow column (COPY FROM; inverse of value_in)
src/scalar/ # scalar fns (common.rs = blob_scalar! macro; codec, msgpack, cose, cwt, webauthn, version)
src/table/ # blended table-in-out fns (LATERAL): webauthn_attestation, seq_decode
src/copy/ # COPY FROM/TO row-file formats (common.rs = Wire/RowShape + shared metadata)
test/sql/basic.test # haybarn SQLLogic E2E over committed hex fixtures
test/sql/copy.test # COPY FROM/TO round-trip, options, ordering, error paths, scale
test/sql/copy_types.test # COPY type fidelity, driven by DuckDB's test_all_types()
See README for the full table. Catalog cbor, schema main; qualify as
cbor.main.<fn>(...) or SET search_path='cbor.main'.
- Codec scalars:
to_json,decode,diagnostic,from_json,encode,canonical,tags,untag,is_valid,well_formed. - MessagePack:
msgpack_to_json,msgpack_decode,msgpack_encode,msgpack_to_cbor. - Security:
cose_decode,cose_payload,cose_headers,cose_x5t,cose_x5chain,cose_key,cwt_claims,webauthn_authdata. - Table (LATERAL):
webauthn_attestation,seq_decode. - COPY formats:
FORMAT 'cbor.cbor'/FORMAT 'cbor.msgpack', each serving both directions under one name. Options:row_format('map' default | 'array'),ignore_errors(FROM),canonical(TO, cbor only).
The worker's build version is published as the catalog's
implementation_version (read via vgi_catalogs() / duckdb_databases()), not
as a scalar — a parameterless *_version() function is a vgi-lint VGI328 error.
- Optional
modeargs are arity overloads. DuckDB binds a const arg as required, sodecode/encode/canonicaleach register a 1-arg and a 2-arg form (with_mode: bool). Give each overload a distinctdescriptionand example (VGI120). blob_scalar!macro (scalar/common.rs) generates the many single-BLOB-input scalars: pass abuild: fn(&[Option<&[u8]>]) -> Result<ArrayRef>plus metadata. Functions with extra/non-blob args are written out by hand.- JSON =
Utf8. There is no DuckDB-JSON Arrow extension type here; JSON columns are VARCHAR carrying canonical JSON text.TIMESTAMPTZ=Timestamp(Microsecond, "UTC");UBIGINT=UInt64;UINTEGER=UInt32. decodereturns JSON, not a dynamic STRUCT. A scalar's output type is fixed at bind with no data sample, so per-scan STRUCT inference isn't possible; the typed value lives in the fixed-schema structural decoders. Documented in README and the function's own doc.- Untrusted-input discipline. All decode funnels through
value::parse/parse_strictwith bounded recursion (MAX_NESTING). Per-row decoders returnNone/ok=falseon error — never panic. Keep it that way; thetests/fuzz.rszero-panic proptest gates it. MAX_NESTINGis 64, not 256.ciborium's serdeValuedeserialization uses a large per-level stack frame (huge in debug builds), so the bounded recursion is kept well under the spec's nominal 256 to stay within a small worker-/test-thread stack. 64 is far deeper than any real document; deeper blobs are cleanly rejected asnesting-limit. The fuzz tests run on explicit 64 MB-stack threads so debug frames don't mask the assertions.- A COPY format serves both directions under one name. The DuckDB extension
registers one
CopyFunctionperalias.format_nameand keeps the first registration, so a reader and a writer advertising the same format name would silently drop one direction. The SDK pairs them into a singledirection="both"entry instead — which requires theCopyFromFunctionandCopyToFunctionfor a format to return the samehandler_name(the wire carries one handler per format). HenceWire::handler(), shared bycopy/from.rsandcopy/to.rs, and the single sharedformat_metadata(): reader and writer are one catalog object, so they must not disagree about it. Requires vgi ≥ 0.27. - COPY FROM sources may be globs.
cloud::resolve_locationsexpands a spec into concreteLocations — local via theglobcrate, remote vialist_glob's object-store listing — sorted, so a multi-file read is deterministic.RowProduceropens them one at a time, so a 10,000-file glob costs one handle rather than 10,000.resolve_localdeliberately does NOT existence-check a literal path: letting the open fail is what produces the worker-resolved-the-path diagnostic, which a bare "File not found" would shadow (there is a test pinning that message).COPY … TOrejects a pattern — a write needs exactly one destination, and otherwise*becomes a literal filename character. s3:///http(s)://COPY paths go throughcloud.rs(ported from../vgi-fixedformat), not the filesystem — which is what makes COPY usable from a container, a remote worker, or the browser.cloud::classifysplits local from remote;cloud::secret_lookupfeeds the COPY traits'secret_lookupshook so DuckDB'sTYPE s3secret is resolved and scoped to the URL. A remote destination is buffered and PUT as one object (object stores have no append); a local one still streams. Ported deliberately without fixedformat'sRangeReaderandlist_glob: one COPY statement names one path, and the reader parses a whole row file at once.http(s)://reads are SSRF-guarded (VGI_CBOR_ALLOW_INTERNAL_HOSTS=1overrides).- The wasm build needs
src/wasm/{http,crypto}.rs.object_store's native transport (reqwest/rustls) and crypto (aws-lc-rs) do not build for wasm, so the wasm target takes theaws-base/http-basefeatures and supplies a sync-XHRHttpServiceand a sha2/hmacCryptoProvider. The XHR side needswasm/vgi_http_lib.jspassed to emcc as a--js-library— if s3 silently fails in the browser, check that link flag first. - COPY does its local file I/O in the worker. A COPY path is resolved against the
worker's filesystem and cwd, not the SQL client's, so the COPY tests require
a co-located worker.
test/sql/copy*.testgate onrequire-env VGI_CBOR_COLOCATED, whichrun_tests.shandci/run-integration.shset only when they launch the worker themselves; the docker image_test runs the suite against a container, where those paths do not exist, and the two files self-skip. Any new test that moves real files needs the same gate. - COPY-TO writers are
ordered = true. A row file's order is part of its content, soCOPY (SELECT … ORDER BY …) TOmust preserve it; that costs the parallel sink (DuckDB installs a single-thread one).write()still shards throughctx.storagescoped byexecution_id— never buffer onself, since the sink and the terminalclose()can land on different worker processes. - COPY FROM streams; it does not buffer the source.
read_stream(vgi 0.28) hands back aRowProducerthat decodes one item at a time and emits a batch everyREAD_BATCH_ROWS, so peak memory is flat in the source size: measured on a 376 MB row file, buffered peaked at 415-429 MB against 67-73 MB streaming.SeqReader/StreamReadertakeBufRead, notRead— that is load-bearing, because ciborium reports both "end of sequence" and "final item truncated" asUnexpectedEof, andfill_bufis what tells them apart without consuming. Get that wrong and a truncated file loads silently.read()is kept only as the trait's buffered contract and just drains the producer, so there is one decode path. ignore_errorsretries a failing chunk row-by-row. COPY FROM converts a whole chunk at once; one unconvertible cell fails its column and would take every other row in the chunk with it. Underignore_errorsthe chunk is rebuilt one row at a time so only genuinely bad rows are dropped. Keep the bulk path as the default — the row-at-a-time path is the error path only.test_all_types()is the type-fidelity fixture.test/sql/copy_types.testround-trips DuckDB's own all-types table (every type at min / max / NULL) through both formats, so a type DuckDB adds later shows up as a test change rather than a silent gap. Each known gap is pinned to its exact failure mode — refused-on-write (INTERVAL/ENUM/UNION) and refused-at-bind unlessSET arrow_lossless_conversion = true(HUGEINT/UHUGEINT/UUID/BIT/TIMETZ, which otherwise reach the worker as an Arrow type that does not map back to the same DuckDB type). With that setting on, only the three write-unsupported families remain. When closing a gap, delete its pin — do not leave both.- Exactness beats brevity in the value encodings.
DECIMALis a CBOR tag-4 decimal fraction carrying the unscaled integer (aDECIMAL(38,s)mantissa overflows CBOR's native integer range, so it rides as a tag-2/3 bignum — seecbor_core::codec::bignum), and a sub-secondTIMESTAMPis a tag-4 fraction inside tag 1. Neither may be routed through anf64:1e-6has no finite binary expansion, so float seconds drift past ~104 days from the epoch for a nanosecond column, and a floatDECIMALloses everything past ~15 digits. MessagePack has no tags, socbor_to_mpmaps the bignum tags to anextof the same code — dropping to a bare byte string would erase the sign. value_inandvalue_outmust stay symmetric. Every branch added to one needs its inverse in the other, or COPY silently half-works: a type that encodes but has no read branch fails at bind, and vice versa. The temporal encodings are load-bearing —DATEis tag 100 (epoch days, not a midnight instant),TIMEis a bare microsecond count since midnight, andTIMESTAMPis tag 1 wrapping an integer of seconds, or a tag-4 decimal fraction when sub-second.- Never let a range check saturate.
timestamp_atbounds against2^63rather thani64::MAX as f64, because the latter rounds up to2^63and lets the overflow through, after whichas i64saturates and DuckDB renders the result asinfinity— silent corruption. Prefer a loud error. - vgi-lint VGI504 is waived for the two COPY handlers (
vgi-lint.toml,per_object). They are registered as worker functions, so the linter lints them as table functions, but SQL invokes a COPY format byFORMATidentifier and never by handler name — no correct example can callcbor_copyby name. It is the repo's only waiver; everything else stays clean at 100/100 withignore = []. - No crypto / no network / no state. Verification, decryption, and CDDL are
non-goals. Don't add a key-management or egress surface here — that belongs in
a separate
vgi-cose-verify.
cargo build --release # → target/release/cbor-worker
cargo test --workspace --all-features # RFC fixtures + proptest fuzz
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all --check
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace
./run_tests.sh # haybarn SQLLogic E2E
# vgi-lint metadata gate (must be clean at fail-on=info):
uvx --from vgi-lint-check vgi-lint lint \
"$PWD/target/release/cbor-worker" --catalog cbor --fail-on infoCI (.github/workflows/ci.yml) runs fmt/clippy/test/doc, the haybarn E2E, the
vgi-lint metadata gate, cargo audit, and an MSRV (1.90) check. Releases go
through the shared Query-farm/vgi-actions reusable workflow on a vX.Y.Z tag
(bump [workspace.package] version first; ci/check-version.sh enforces the
match).
Every function carries vgi.title / vgi.doc_llm / vgi.doc_md /
vgi.keywords / vgi.example_queries (each a described [{description,sql}]
JSON list — VGI515) plus per-arg docs on every argument; the catalog carries
source_url, implementation_version, classifying tags, and the
vgi.executable_examples (VGI509) verified examples. Keep the gate at 100/100,
no findings — titles must not merely restate the machine name (VGI125),
overloads must have distinct descriptions (VGI120), and DuckDB type names in
prose must be code-formatted (VGI182).