Skip to content

feat(codegen): catalog-driven Rust payload bindings (PR 4) - #327

Merged
tobyhede merged 12 commits into
eql_v3from
pr4-catalog-driven-bindings-codegen
Jun 30, 2026
Merged

feat(codegen): catalog-driven Rust payload bindings (PR 4)#327
tobyhede merged 12 commits into
eql_v3from
pr4-catalog-driven-bindings-codegen

Conversation

@tobyhede

@tobyhede tobyhede commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 4 (the final, central PR) of the unified catalog-codegen design. eql-codegen now emits the Rust payload bindings (crates/eql-bindings/src/v3/<family>.rs structs + DomainType impls + a generated inventory.rs carrying all()) from the eql-domains catalog — the same generate-to-committed-source mechanism it already uses for SQL. The existing ts-rs/schemars derives then produce the committed TypeScript/JSON bindings downstream. Drift is caught by the established committed-reference pattern: mise run types:check regenerates and git diffs the committed output, now broadened to cover the generated .rs.

The hand-written DomainType trait, the shared newtypes (SchemaVersion/Identifier/Ciphertext/Hmac256/OreBlock256/BloomFilter), the PhantomData plumbing, and the architectural module doc (incl. the non-catalog-derivable float-NaN and bool storage-only caveats) stay hand-written in mod.rs/domain_type.rs/terms.rs.

What changed

  • schemars 0.8 → 1.x — committed schema/v3/*.json reshaped to JSON Schema 2020-12 ($defs, $ref carries description as a sibling). Wire contract (required, strictness, const: 2, i16 bounds) unchanged; ts-rs .ts unaffected.
  • eql-codegen Rust emitterquote! + prettyplease + stable rustfmt, with a // @generated ownership marker as line 1; GeneratedKind {Sql, Rust} generalizes the writer; Term::binding_newtype/Term::payload_terms keep the term→newtype map exhaustive at compile time; a dedicated eql-codegen bindings subcommand (default no-arg run stays SQL-only).
  • Cutover — hand-written per-family modules + inventory.rs replaced by generated output. Only diff in the committed .ts/.json is doc prose (one catalog-derived struct line, no field docs); fields/types/order/required/strictness unchanged — verified by catalog_parity + v3_conformance + the new ts_property_order guard.
  • Gatestypes:generate runs eql-codegen bindings first; types:check broadened to crates/eql-bindings/src/v3. Added a cargo-level byte-parity + determinism test for the bindings (so cargo test alone catches a stale/hand-edited .rs), a CLI smoke test, an ENVELOPE_KEYS tie-in test, and restored the ordered inventory guard.

Notes

  • capability_label now panic!s on an unmapped bare-domain name instead of silently emitting generic doc text (preserves the compile-checked-catalog intent).
  • No CHANGELOG.md entry: PR 4 is internal — protect-ffi/cipherstash-client do not consume eql-bindings, and the JSON payload shapes are unchanged; only the provenance of the Rust bindings (hand-written → generated) changed. (If a reviewer judges the JSON Schema 2020-12 reshape user-visible, a one-line Changed entry can be added.)

Verification (no DB / no creds)

All green: cargo test (eql-domains/codegen/bindings), cargo fmt --check, cargo clippy -D warnings, mise run test:crates, codegen:parity, types:check, test:matrix:inventory, test:self_contained_v3.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added generated Rust payload bindings for domain types, plus a command to regenerate them.
    • Added a clearer inventory of available domain types and tighter parity checks across generated outputs.
  • Bug Fixes

    • Updated schema generation to a newer JSON Schema standard and aligned required-field ordering.
    • Improved handling for generated files so handwritten files are not overwritten accidentally.
  • Documentation

    • Expanded setup guidance for generating and checking bindings, including the recommended regeneration workflow.

tobyhede added 8 commits June 29, 2026 16:52
Migrate the two manual JsonSchema impls (SchemaVersion const:2, BloomFilter
i16 bounds) to the 1.x json_schema! macro API, the DomainType::schema return
type RootSchema -> schemars::Schema, and the schema-inspecting catalog_parity
assertions to the 2020-12 shape ($defs, $ref carries description as a sibling).
Regenerate schema/v3/*.json as 2020-12. Wire contract (required, strictness,
const:2, i16 bounds) unchanged; ts-rs .ts bindings unchanged.
quote/proc-macro2/syn(v2)/prettyplease(=0.2.37) + RUST_GENERATED_MARKER +
format_rs (prettyplease then stable rustfmt, with the @generated marker as line
1). rustfmt is the final formatter so committed generated files are clean under
cargo fmt --check.
The four writer fns take a GeneratedKind selecting the ownership marker and the
cleanup extension (.sql vs .rs). SQL call sites pass Sql; generated SQL
unchanged (codegen:parity green).
Move the trait/consts/PhantomData impl into hand-written domain_type.rs and the
all() inventory into inventory.rs (re-exported at crate::v3, public paths
unchanged). Relocate the non-catalog-derivable float NaN and bool storage-only
caveats into the hand-written mod.rs doc so the generated per-family modules
don't drop them at cutover. mod.rs stays hand-written. types:check green.
The term->binding-newtype mapping (Hm->Hmac256, Ore->OreBlock256,
Bloom->BloomFilter) is wire-contract data, so it lives on Term beside
json_key/ctor/extractor (unit-tested). payload_terms returns the distinct
field-bearing terms in wire order. PR 4's bindings emitter matches on these
instead of a &str round-trip, keeping it exhaustive at compile time.
render_family_bindings renders each DomainFamily to its struct set + three-
method DomainType impls (schema -> schemars::Schema, 1.x), with pinned
envelope-then-term field order (Term::payload_terms, matching on the enum), the
canonical derive/ts/serde attributes, a precise term-newtype import set, and one
catalog-derived struct doc line (no field docs).
render_inventory_rs generates all() in CATALOG order (entries via super::),
replacing the hand-maintained inventory. mod.rs stays hand-written (module doc
+ pub mod decls + re-exports).
generate_bindings writes one <family>.rs per catalog family + inventory.rs under
crates/eql-bindings/src/v3 (GeneratedKind::Rust ownership). Exposed as
`eql-codegen bindings`; the default no-arg run stays SQL-only so build /
codegen:parity are unaffected.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7023e7d9-d272-43d3-8e0e-4f38ba60ee2d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr4-catalog-driven-bindings-codegen

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tobyhede
tobyhede changed the base branch from main to eql_v3 June 29, 2026 08:04
@tobyhede
tobyhede marked this pull request as draft June 29, 2026 08:05
@tobyhede
tobyhede force-pushed the pr4-catalog-driven-bindings-codegen branch from 01843c4 to c60c032 Compare June 29, 2026 09:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
crates/eql-codegen/src/bindings.rs (1)

28-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the render helpers non-public.

Only generate_bindings is used outside this module in the provided diff. Leaving format_rs, render_family_bindings, and render_inventory_rs as pub makes formatting/output-shape details part of eql-codegen's public contract for no clear caller. Prefer private visibility here and keep generate_bindings as the single public entrypoint.

♻️ Proposed change
-pub fn format_rs(tokens: TokenStream) -> String {
+fn format_rs(tokens: TokenStream) -> String {
@@
-pub fn render_family_bindings(family: &DomainFamily) -> String {
+fn render_family_bindings(family: &DomainFamily) -> String {
@@
-pub fn render_inventory_rs() -> String {
+fn render_inventory_rs() -> String {

Also applies to: 175-212, 220-255

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/eql-codegen/src/bindings.rs` around lines 28 - 32, The render helpers
are exposing implementation details unnecessarily; keep only generate_bindings
public and make format_rs, render_family_bindings, and render_inventory_rs
private within bindings.rs. Update their visibility declarations and verify any
internal callers in the same module still compile, preserving generate_bindings
as the sole external entrypoint.
crates/eql-bindings/src/v3/inventory.rs (1)

6-48: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching or lazily initializing the inventory vector.

all() reallocates 46 boxed PhantomData values on every call. For a function used in tests and drift checks, consider LazyLock or once_cell to return a static slice.

// Alternative: static slice avoids per-call allocation
static ALL: &[Box<dyn DomainType>] = &[
    Box::new(PhantomData::<super::int4::Int4>),
    // ...
];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/eql-bindings/src/v3/inventory.rs` around lines 6 - 48, The all()
function in inventory.rs rebuilds the full DomainType inventory on every call,
causing repeated allocations for the PhantomData boxes. Update all() to use a
cached, lazily initialized static collection (for example via LazyLock or
once_cell) and return a shared slice/reference instead of allocating a new Vec
each time; keep the existing inventory entries for symbols like Int4, Text, and
Float8 in the cached initializer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/eql-bindings/src/v3/mod.rs`:
- Around line 29-34: Update the module-level docs in mod.rs to broaden the
ordered-domain comparison beyond just integers, since the generated API also
includes ob-only ordered payloads for non-text types like date, timestamptz,
float8, and numeric. Adjust the wording around the text ordered domains and the
“integer ordered domains” sentence so it refers to non-text ordered domains
instead, while keeping the explanation that text routes equality through hm
because ORE is not equality-lossless.

In `@crates/eql-bindings/tests/catalog_parity.rs`:
- Around line 1-11: Update the module header doc in catalog_parity to match the
current test coverage: it should no longer say the inventory set/order is “no
longer policed here” since inventory_exactly_covers_catalog_in_order() now
enforces that check. Revise the top comment to clearly describe that the file
validates both the JSON Schema parity and the catalog inventory coverage/order,
using the existing symbols CATLOG, eql_bindings::SchemaVersion, and
inventory_exactly_covers_catalog_in_order() as the anchor points.

In `@crates/eql-codegen/src/writer.rs`:
- Around line 77-80: The directory scan in writer::write_generated_files is
swallowing read_dir entry errors via filter_map(|e| e.ok()), which can hide
cleanup failures. Update the PathBuf collection logic to propagate any
directory-entry I/O error from fs::read_dir(directory) instead of skipping it,
keeping the cleanup path in writer.rs deterministic and surfacing unexpected
generation issues.

In `@mise.toml`:
- Around line 214-222: The rollback trap in the v3 snapshot flow only restores
the src/v3 tree, so interrupted or failed replacement leaves bindings and schema
half-swapped. Update the existing trap and surrounding restore logic in this
script block to also back up and restore crates/eql-bindings/bindings and
crates/eql-bindings/schema when ok is not set. Make sure the mv/removal sequence
remains atomic-on-failure by preserving the old trees until both generated
outputs have been safely moved into place.

---

Nitpick comments:
In `@crates/eql-bindings/src/v3/inventory.rs`:
- Around line 6-48: The all() function in inventory.rs rebuilds the full
DomainType inventory on every call, causing repeated allocations for the
PhantomData boxes. Update all() to use a cached, lazily initialized static
collection (for example via LazyLock or once_cell) and return a shared
slice/reference instead of allocating a new Vec each time; keep the existing
inventory entries for symbols like Int4, Text, and Float8 in the cached
initializer.

In `@crates/eql-codegen/src/bindings.rs`:
- Around line 28-32: The render helpers are exposing implementation details
unnecessarily; keep only generate_bindings public and make format_rs,
render_family_bindings, and render_inventory_rs private within bindings.rs.
Update their visibility declarations and verify any internal callers in the same
module still compile, preserving generate_bindings as the sole external
entrypoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e987a943-c7b9-4e9a-890a-25220317934d

📥 Commits

Reviewing files that changed from the base of the PR and between 0878e06 and c60c032.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (111)
  • CLAUDE.md
  • crates/eql-bindings/Cargo.toml
  • crates/eql-bindings/bindings/v3/Bool.ts
  • crates/eql-bindings/bindings/v3/Date.ts
  • crates/eql-bindings/bindings/v3/DateEq.ts
  • crates/eql-bindings/bindings/v3/DateOrd.ts
  • crates/eql-bindings/bindings/v3/DateOrdOre.ts
  • crates/eql-bindings/bindings/v3/Float4.ts
  • crates/eql-bindings/bindings/v3/Float4Eq.ts
  • crates/eql-bindings/bindings/v3/Float4Ord.ts
  • crates/eql-bindings/bindings/v3/Float4OrdOre.ts
  • crates/eql-bindings/bindings/v3/Float8.ts
  • crates/eql-bindings/bindings/v3/Float8Eq.ts
  • crates/eql-bindings/bindings/v3/Float8Ord.ts
  • crates/eql-bindings/bindings/v3/Float8OrdOre.ts
  • crates/eql-bindings/bindings/v3/Int2.ts
  • crates/eql-bindings/bindings/v3/Int2Eq.ts
  • crates/eql-bindings/bindings/v3/Int2Ord.ts
  • crates/eql-bindings/bindings/v3/Int2OrdOre.ts
  • crates/eql-bindings/bindings/v3/Int4.ts
  • crates/eql-bindings/bindings/v3/Int4Eq.ts
  • crates/eql-bindings/bindings/v3/Int4Ord.ts
  • crates/eql-bindings/bindings/v3/Int4OrdOre.ts
  • crates/eql-bindings/bindings/v3/Int8.ts
  • crates/eql-bindings/bindings/v3/Int8Eq.ts
  • crates/eql-bindings/bindings/v3/Int8Ord.ts
  • crates/eql-bindings/bindings/v3/Int8OrdOre.ts
  • crates/eql-bindings/bindings/v3/Numeric.ts
  • crates/eql-bindings/bindings/v3/NumericEq.ts
  • crates/eql-bindings/bindings/v3/NumericOrd.ts
  • crates/eql-bindings/bindings/v3/NumericOrdOre.ts
  • crates/eql-bindings/bindings/v3/Text.ts
  • crates/eql-bindings/bindings/v3/TextEq.ts
  • crates/eql-bindings/bindings/v3/TextMatch.ts
  • crates/eql-bindings/bindings/v3/TextOrd.ts
  • crates/eql-bindings/bindings/v3/TextOrdOre.ts
  • crates/eql-bindings/bindings/v3/TextSearch.ts
  • crates/eql-bindings/bindings/v3/Timestamptz.ts
  • crates/eql-bindings/bindings/v3/TimestamptzEq.ts
  • crates/eql-bindings/bindings/v3/TimestamptzOrd.ts
  • crates/eql-bindings/bindings/v3/TimestamptzOrdOre.ts
  • crates/eql-bindings/schema/v3/bool.json
  • crates/eql-bindings/schema/v3/date.json
  • crates/eql-bindings/schema/v3/date_eq.json
  • crates/eql-bindings/schema/v3/date_ord.json
  • crates/eql-bindings/schema/v3/date_ord_ore.json
  • crates/eql-bindings/schema/v3/float4.json
  • crates/eql-bindings/schema/v3/float4_eq.json
  • crates/eql-bindings/schema/v3/float4_ord.json
  • crates/eql-bindings/schema/v3/float4_ord_ore.json
  • crates/eql-bindings/schema/v3/float8.json
  • crates/eql-bindings/schema/v3/float8_eq.json
  • crates/eql-bindings/schema/v3/float8_ord.json
  • crates/eql-bindings/schema/v3/float8_ord_ore.json
  • crates/eql-bindings/schema/v3/int2.json
  • crates/eql-bindings/schema/v3/int2_eq.json
  • crates/eql-bindings/schema/v3/int2_ord.json
  • crates/eql-bindings/schema/v3/int2_ord_ore.json
  • crates/eql-bindings/schema/v3/int4.json
  • crates/eql-bindings/schema/v3/int4_eq.json
  • crates/eql-bindings/schema/v3/int4_ord.json
  • crates/eql-bindings/schema/v3/int4_ord_ore.json
  • crates/eql-bindings/schema/v3/int8.json
  • crates/eql-bindings/schema/v3/int8_eq.json
  • crates/eql-bindings/schema/v3/int8_ord.json
  • crates/eql-bindings/schema/v3/int8_ord_ore.json
  • crates/eql-bindings/schema/v3/numeric.json
  • crates/eql-bindings/schema/v3/numeric_eq.json
  • crates/eql-bindings/schema/v3/numeric_ord.json
  • crates/eql-bindings/schema/v3/numeric_ord_ore.json
  • crates/eql-bindings/schema/v3/text.json
  • crates/eql-bindings/schema/v3/text_eq.json
  • crates/eql-bindings/schema/v3/text_match.json
  • crates/eql-bindings/schema/v3/text_ord.json
  • crates/eql-bindings/schema/v3/text_ord_ore.json
  • crates/eql-bindings/schema/v3/text_search.json
  • crates/eql-bindings/schema/v3/timestamptz.json
  • crates/eql-bindings/schema/v3/timestamptz_eq.json
  • crates/eql-bindings/schema/v3/timestamptz_ord.json
  • crates/eql-bindings/schema/v3/timestamptz_ord_ore.json
  • crates/eql-bindings/src/lib.rs
  • crates/eql-bindings/src/v3/bool.rs
  • crates/eql-bindings/src/v3/date.rs
  • crates/eql-bindings/src/v3/domain_type.rs
  • crates/eql-bindings/src/v3/float4.rs
  • crates/eql-bindings/src/v3/float8.rs
  • crates/eql-bindings/src/v3/int2.rs
  • crates/eql-bindings/src/v3/int4.rs
  • crates/eql-bindings/src/v3/int8.rs
  • crates/eql-bindings/src/v3/inventory.rs
  • crates/eql-bindings/src/v3/mod.rs
  • crates/eql-bindings/src/v3/numeric.rs
  • crates/eql-bindings/src/v3/terms.rs
  • crates/eql-bindings/src/v3/text.rs
  • crates/eql-bindings/src/v3/timestamptz.rs
  • crates/eql-bindings/tests/catalog_parity.rs
  • crates/eql-bindings/tests/mod_pins_catalog.rs
  • crates/eql-bindings/tests/ts_property_order.rs
  • crates/eql-codegen/Cargo.toml
  • crates/eql-codegen/src/bindings.rs
  • crates/eql-codegen/src/consts.rs
  • crates/eql-codegen/src/generate.rs
  • crates/eql-codegen/src/lib.rs
  • crates/eql-codegen/src/main.rs
  • crates/eql-codegen/src/writer.rs
  • crates/eql-codegen/tests/bindings_parity.rs
  • crates/eql-codegen/tests/cli.rs
  • crates/eql-domains/src/spec.rs
  • crates/eql-domains/src/term.rs
  • docs/reference/adding-a-scalar-encrypted-domain-type.md
  • mise.toml

Comment thread crates/eql-bindings/src/v3/mod.rs
Comment thread crates/eql-bindings/tests/catalog_parity.rs Outdated
Comment thread crates/eql-codegen/src/writer.rs
Comment thread mise.toml Outdated
tobyhede added 3 commits June 29, 2026 23:38
One Rust payload struct per eql_v3 SQL domain, generated by eql-codegen from
eql-domains::CATALOG, with ts-rs/schemars deriving the committed TypeScript and
JSON Schema. Each struct carries a catalog-derived doc — a summary line plus the
supported operators and required payload keys (capability label,
Term::operators_for_terms, ENVELOPE_KEYS ++ Term::term_json_keys); the keys list
surfaces structural distinctions such as text's dual-term ordered domains
(hm + ob) versus the integer ordered domains (ob). No per-field docs: per-term
semantics live on the shared term newtypes and non-derivable per-family caveats
in mod.rs. mise run types:generate regenerates Rust then TS/JSON; types:check is
the committed-reference drift gate.
…ndings

Byte-parity and determinism gates for the committed Rust bindings, TS property-
order pin, catalog/inventory parity, mod.rs pub-mod pin, and a bindings CLI
smoke test — so a stale or hand-edited generated file, a dropped domain, or a
reordered payload fails CI.
Document in CLAUDE.md and the adding-a-scalar reference that the Rust payload
bindings (and downstream TS/JSON via ts-rs/schemars) are catalog-generated, that
generated structs carry a catalog-derived struct doc (summary + operators +
required keys, no per-field docs), schemars emits JSON Schema 2020-12,
non-derivable caveats live in mod.rs, and types:check is the drift gate.
@tobyhede
tobyhede force-pushed the pr4-catalog-driven-bindings-codegen branch from c60c032 to af24b0b Compare June 29, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant