Skip to content
This repository was archived by the owner on Sep 2, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
61ef3f3
feat(eql-domains): add aliases field + group_names to DomainFamily
tobyhede Jul 8, 2026
1f7fe0b
feat(eql-domains): declare v1 native-spelling aliases (int2/int4/int8…
tobyhede Jul 8, 2026
7c95706
refactor(eql-codegen): name-parameterized render_type_named/render_ty…
tobyhede Jul 8, 2026
22b1d42
refactor(eql-codegen): per-entry domain on FnEntry wrapper/blocker (b…
tobyhede Jul 8, 2026
5939609
feat(eql-codegen): cross-name operator classifier + render_cross_file
tobyhede Jul 8, 2026
0b197ec
feat(eql-codegen): emit alias surfaces + cross files from generate_al…
tobyhede Jul 8, 2026
998f533
build: regenerate SQL surface with native-spelling alias domains + cr…
tobyhede Jul 8, 2026
1ebec0d
test(sqlx): alias interop — cross-name split, hmac routing, blocker r…
tobyhede Jul 8, 2026
43b1ed9
test(property): cross-name = routes hmac over all doubles pairs (not …
tobyhede Jul 8, 2026
4ad1a5d
docs: native-spelling encrypted-domain aliases + changelog
tobyhede Jul 8, 2026
aa0fb16
test(eql-codegen): parity gate counts alias group-name dirs
tobyhede Jul 8, 2026
f834d29
docs: clarify aliases must be schema-qualified (bare name binds built…
tobyhede Jul 8, 2026
c97be33
feat(eql-codegen): emit cross-name files for all group-name pairs (mu…
tobyhede Jul 8, 2026
90b62e6
test(property): comprehensive cross-name matrix over all 6 aliased fa…
tobyhede Jul 8, 2026
33f8e9d
test(sqlx): matrix-drive alias interop over all 6 families + all unsu…
tobyhede Jul 8, 2026
a2928d8
refactor(eql-codegen): share CREATE OPERATOR partial; hoist cross ret…
tobyhede Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Each entry that ships in a published release links to the PR that introduced it.

### Added

- **Postgres-native type-spelling aliases for encrypted domains.** `public.int4`, `public.int8`, `public.int2`, `public.float4`, `public.float8`, and `public.decimal` are now generated as full encrypted-domain types equivalent to `public.integer` / `bigint` / `smallint` / `real` / `double` / `numeric`, and interoperate with their canonical twin in both directions (a `public.int4` value and a `public.integer` value compare/join correctly). Why: schemas can use whichever native spelling they prefer. Each alias shares the canonical payload envelope exactly, so value conversion is a plain `::public.<name>` cast (no `CREATE CAST`, which is impossible on domains); cross-name comparisons route through the generated encrypted operator (HMAC/ORE), never native jsonb — so two independent encryptions of one value still compare equal across the alias boundary. **Always schema-qualify the domain name** (`public.int4`): every one of these names is also a built-in PostgreSQL type, so a bare unqualified `int4` in DDL binds the plaintext built-in, not the encrypted domain — exactly as the canonical `public.integer` already requires. See [Native-spelling type aliases](docs/reference/aliases.md). ([#370](https://github.com/cipherstash/encrypt-query-language/pull/370))
- **`eql_bindings::QueryPayload` + `from_v2_query_typed` — typed query payloads (eql-bindings 0.4.0).** The `eql-bindings` crate gains a `QueryPayload` enum spanning every v3 QUERY payload shape and `from_v2_query_typed(v2, target)`, the typed twin of `from_v2_query` that returns the enum variant instead of a shape-erased `serde_json::Value` — completing the typed-conversion surface started by `DomainPayload`/`from_v2_typed` (storage payloads, see below). Today the enum has exactly one variant, `SteVec(SteVecQuery)` (the `eql_v3.jsonb_query` containment needle — the only query conversion that exists): a scalar query value is a SINGLE index term (one Ore / Ope / Bloom / Hm term value, not a stored envelope), and no v3 scalar-query wire shape exists yet, so the scalar-term variants are deliberately absent and both entry points keep failing closed with `UnsupportedQueryTarget` rather than inventing a shape ahead of the eql-mapper redesign. Unlike `DomainPayload` (generated — its variant set IS the catalog), `QueryPayload` is hand-written next to the equally hand-written SteVec types: its variants are term-shaped, anchored to the stable hand-written `Term`-level surface (`terms.rs`), not to catalog rows a generator walks. Serialize-only (`#[serde(untagged)]` — `serde_json::to_value(&from_v2_query_typed(v2, t)?)` equals `from_v2_query(v2, t)?` byte-for-byte, pinned in tests), no `Deserialize` (a variant is only constructible from a known domain via `QueryPayload::parse`, never inferred from bytes), and no ts-rs/schemars derives (the enum adds no wire shape, so the exported TS/JSON-Schema artifacts are unchanged). Both query entry points share one conversion path and the final strict parse happens exactly once (`from_v2_query` validates and discards; `from_v2_query_typed` keeps it). Why: protect-ffi can now hold converted containment needles typed instead of as `serde_json::Value`, adopting after the eql-bindings 0.4.0 release. ([#350](https://github.com/cipherstash/encrypt-query-language/pull/350))
- **`eql_bindings::DomainPayload` + `from_v2_typed` — typed domain payloads.** The `eql-bindings` crate gains a catalog-generated `DomainPayload` enum spanning every stored-payload domain (one variant per catalog family/domain pair mapping to its binding struct — `IntegerEq`, `TextSearch`, … — plus `SteVecDocument` for `eql_v3.json`) and `from_v2_typed(v2, target)`, the typed twin of `from_v2` that returns the enum variant instead of a shape-erased `serde_json::Value`. The enum is generated by `eql-codegen bindings` alongside the family structs and inventory (drift-gated by `types:check`), serializes exactly as the inner struct (`#[serde(untagged)]`, Serialize-only — `serde_json::to_value(&from_v2_typed(v2, t)?)` equals `from_v2(v2, t)?` byte-for-byte, pinned in tests), and deliberately has no `Deserialize`: cross-token payloads are byte-identical on the wire, so a variant is only constructible from a known target domain (`DomainPayload::parse`), never inferred from bytes. Both conversion entry points share one path and the final strict parse through the target's binding struct still happens exactly once (`from_v2` validates and discards; `from_v2_typed` keeps it). No new TypeScript/JSON-Schema artifacts — the exported wire surface is unchanged. Why: protect-ffi had to store converted payloads type-erased (`V3(serde_json::Value)`) because no typed value was obtainable from eql-bindings; it can now hold `V3(DomainPayload)` without re-parsing or a hand-written enum that drifts as the catalog grows. ([#349](https://github.com/cipherstash/encrypt-query-language/pull/349))
- **`eql_v3.version()` — version introspection on the self-contained `eql_v3` surface.** `SELECT eql_v3.version()` returns the installed EQL version as bare-semver text (e.g. `'3.0.0'`, or `'DEV'` for local builds); the same value is published as the `eql_v3` schema comment, so it is also readable via `obj_description('eql_v3'::regnamespace)`. The version is baked in at build time from the release tag via `mise run build --version` (now passed as prefix-stripped semver by both release workflows). This replaces the removed `eql_v2.version()` (dropped with the rest of the `eql_v2` surface — see Removed): the "which EQL is installed?" probe moves to `eql_v3`, consistent with the schema namespace move. Why: the self-contained `eql_v3` surface had no version-introspection point after `eql_v2` was removed.
Expand Down
34 changes: 29 additions & 5 deletions crates/eql-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@ pub fn environment() -> minijinja::Environment<'static> {
include_str!("../templates/operators.sql.j2"),
)
.expect("operators.sql template");
env.add_template(
"operators/create.sql.j2",
include_str!("../templates/operators/create.sql.j2"),
)
.expect("operators/create.sql.j2 template");
env.add_template(
"aggregates.sql",
include_str!("../templates/aggregates.sql.j2"),
)
.expect("aggregates.sql template");
env.add_template("cross.sql", include_str!("../templates/cross.sql.j2"))
.expect("cross.sql template");
env.add_global("schema", SCHEMA);
env.add_global("internal_schema", INTERNAL_SCHEMA);
env
Expand Down Expand Up @@ -123,23 +130,37 @@ pub enum FnEntry {
args: [SqlParam; 2],
call_a: String, // e.g. eql_v3.eq_term(a) (embeds extract_arg cast logic)
call_b: String, // e.g. eql_v3.eq_term(b::public.integer_eq)
dom: String, // schema-qualified LEFT domain, for @brief (was file-level {{ dom }})
},
Unsupported {
operator_lit: String, // sql_str(op), escaped content for the RAISE literal
function_name: String, // e.g. lt / "->" / "#>"
args: [SqlParam; 2],
returns: String, // boolean / text / jsonb / domain (selection STAYS in Rust)
returns: String, // boolean / text / jsonb / domain (selection STAYS in Rust)
dom: String, // schema-qualified LEFT domain, for @brief
domain_lit: String, // sql_str(dom) — the RAISE literal (was file-level {{ domain_lit }})
},
}

#[derive(serde::Serialize)]
pub struct FunctionsContext {
pub requires: Vec<String>, // dependency paths only; template emits "-- REQUIRE:"
pub family_name: String,
pub name: String, // full domain name (family-name + "_" + domain-name)
pub dom: String, // schema-qualified domain, e.g. public.integer_eq
pub domain_lit: String, // sql_str(dom), defensively escaped for the RAISE literal
pub name: String, // full domain name (family-name + "_" + domain-name)
pub dom: String, // schema-qualified domain, e.g. public.integer_eq (file-level @brief)
pub entries: Vec<FnEntry>,
}

/// Template context for a cross-name operator file: both members' requires,
/// the two group names (for the file header), the function entries (wrappers +
/// blockers, both directions) and the CREATE OPERATOR entries.
#[derive(serde::Serialize)]
pub struct CrossContext {
pub requires: Vec<String>,
pub a: String,
pub b: String,
pub entries: Vec<FnEntry>,
pub operators: Vec<OpEntry>,
}

/// Build the inlinable index-extractor entry for a domain term.
Expand Down Expand Up @@ -181,19 +202,22 @@ pub fn wrapper_entry(
],
call_a: extract_arg(arg_a, extractor, dom, "a"),
call_b: extract_arg(arg_b, extractor, dom, "b"),
dom: dom.to_string(),
}
}

/// Build an unsupported-operator entry. Every such entry shares one uniform
/// `RAISE EXCEPTION` body; only signature facts vary. `op` is the
/// already-resolved operator (no symbol re-lookup needed).
pub fn unsupported_entry(op: &Operator, args: [SqlParam; 2], returns: &str) -> FnEntry {
pub fn unsupported_entry(dom: &str, op: &Operator, args: [SqlParam; 2], returns: &str) -> FnEntry {
FnEntry::Unsupported {
// operator_lit is sql_str-escaped defensively for the single-quoted RAISE literal.
operator_lit: sql_str(op.symbol),
function_name: op.function_name.to_string(),
args,
returns: returns.to_string(),
dom: dom.to_string(),
domain_lit: sql_str(dom),
}
}

Expand Down
Loading