Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
79606a0
feat(v3): add self-contained eql_v3.bloom_filter SEM domain
tobyhede Jun 5, 2026
fccabc8
feat(v3): add eql_v3.bloom_filter jsonb extractor + has_bloom_filter
tobyhede Jun 5, 2026
0eb43a3
feat(eql-scalars): add Term::Bloom (match capability, bf/match_term/b…
tobyhede Jun 5, 2026
b3af8b0
feat(codegen): containment metadata for @>/<@ (engages only on Bloom …
tobyhede Jun 5, 2026
4bbeda6
feat(eql-scalars): add text catalog row (eq/match/ord) + TEXT_VALUES
tobyhede Jun 5, 2026
1744209
test(v3): SQL smoke for eql_v3.text_match @> match + blocked =
tobyhede Jun 5, 2026
bf7d5dc
chore(v3): allowlist match_term/contains/contained_by in splinter lint
tobyhede Jun 5, 2026
2419518
docs(v3): document text scalar + Bloom term; changelog entry
tobyhede Jun 5, 2026
76f5ba8
feat(tests): EqlPlaintext for String (text cast + Plaintext::Text)
tobyhede Jun 5, 2026
d349d84
feat(tests-macros): add text marker (hand-written impl + pivot-presen…
tobyhede Jun 5, 2026
d6cd82d
test(v3): parametrize scalar_fixture indexes; add text arm with Match…
tobyhede Jun 5, 2026
0b4f69b
feat(tests): non-Copy text scalar harness (ScalarType/FixtureValue Co…
tobyhede Jun 5, 2026
2e9f5d2
test(v3): dedicated text_match containment suite (self/substring/disj…
tobyhede Jun 5, 2026
d2d75e3
test(v3): move text_smoke/text_match out of scalars:: (keep matrix-in…
tobyhede Jun 5, 2026
c1f7f9a
style(v3): rustfmt + clippy fixups for text catalog/containment tests
tobyhede Jun 5, 2026
5790f2f
docs: fill changelog PR link (#260)
tobyhede Jun 5, 2026
2695291
fix(review): gate Fixture::Zero to integer kinds; refresh EqlPlaintex…
tobyhede Jun 5, 2026
f868aed
refactor(v3): address PR #260 review — inline bloom_filter, table-dri…
tobyhede Jun 5, 2026
47101ec
style(test): rustfmt text_smoke empty-bloom test (wrap long const lit…
tobyhede Jun 5, 2026
b809fde
refactor(tests): split ScalarType into OrderedScalar/SignedScalar; fi…
tobyhede Jun 5, 2026
d27940e
refactor(v3): drop redundant IF val IS NULL from ore_block extractor …
tobyhede Jun 5, 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 @@ -27,6 +27,7 @@ Each entry that ships in a published release links to the PR that introduced it.
- **`eql_v3.int8` encrypted-domain type family.** Four jsonb-backed domains for encrypted `int8` columns — `eql_v3.int8` (storage-only), `eql_v3.int8_eq` (`=` / `<>` via HMAC), and `eql_v3.int8_ord` / `eql_v3.int8_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `int8` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: a type-safe, per-capability encrypted `bigint` column, extending the scalar generator across the full 64-bit integer width. ([#253](https://github.com/cipherstash/encrypt-query-language/pull/253))
- **`eql_v3.date` encrypted-domain type family.** Four jsonb-backed domains for encrypted `date` columns — `eql_v3.date` (storage-only), `eql_v3.date_eq` (`=` / `<>` via HMAC), and `eql_v3.date_ord` / `eql_v3.date_ord_ore` (also `<` `<=` `>` `>=` via ORE block terms, with `MIN` / `MAX` aggregates) — generated from the `date` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. Plaintexts encrypt under the `date` cast and compare via the same ORE block terms as the integer scalars (ORE is plaintext-agnostic — dates order like integers). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` extractors, not an operator class on the domain. Why: the first **non-integer ordered** scalar encrypted-domain type — a type-safe, per-capability encrypted `date` column — proving the generator and SQLx test matrix generalize beyond fixed-width integers. ([#256](https://github.com/cipherstash/encrypt-query-language/pull/256))
- **Per-domain `MIN` / `MAX` aggregates for the encrypted-domain family.** `eql_v3.min(eql_v3.<T>_ord)` / `eql_v3.max(eql_v3.<T>_ord)` (and the `_ord_ore` twin) are generated for every ord-capable scalar variant, giving type-safe extrema on domain-typed columns — comparison routes through the variant's `<` / `>` operator (ORE block term, no decryption). The aggregates are declared `PARALLEL = SAFE` with a combine function (the state function itself — min/max are associative), so PostgreSQL can use partial/parallel aggregation on large `GROUP BY` workloads. Why: the new domain types previously had no equivalent of the composite-type aggregates. The existing `eql_v2.min(eql_v2_encrypted)` / `eql_v2.max(eql_v2_encrypted)` aggregates are **retained** and continue to work on `eql_v2_encrypted` columns; the per-domain aggregates are additive and coexist with them. ([#239](https://github.com/cipherstash/encrypt-query-language/pull/239))
- **`eql_v3.text` encrypted-domain family (`text`, `text_eq`, `text_match`, `text_ord`, `text_ord_ore`).** Adds equality (`=` / `<>` via HMAC), match (`@>` / `<@` via a new self-contained `eql_v3.bloom_filter` SEM index term), and ORE ordering (`<` `<=` `>` `>=`, `min` / `max`) for encrypted text, at parity with EQL v2 text — generated from the `text` row in `eql-scalars::CATALOG` by the same materializer as the `eql_v3.int4` reference. `text` is the first scalar to add a new index `Term` (`Bloom`) and the first non-integer, unbounded ordered kind (lexicographic pivots, hand-written `impl ScalarType`). Index via a functional index on the `eql_v3.eq_term` / `eql_v3.ord_term` / `eql_v3.match_term` extractors, not an operator class on the domain. Why: brings searchable encrypted text to the namespaced, `eql_v2`-free `eql_v3` surface. Match is exposed as bloom-filter containment on the `text_match` domain — deliberately *not* SQL `LIKE` (no wildcard/anchoring; probabilistic ngram containment) — and never backs equality (which always routes through `Hm`). ([#260](https://github.com/cipherstash/encrypt-query-language/pull/260))
- **Self-contained `eql_v3` schema + standalone `release/cipherstash-encrypt-v3.sql` installer.** The `eql_v3` encrypted-domain surface no longer depends on `eql_v2` at runtime: it now owns its own copies of the searchable-encrypted-metadata (SEM) index-term types — `eql_v3.hmac_256` and `eql_v3.ore_block_u64_8_256` (with its btree operator class) — so the `eql_v3.eq_term` / `eql_v3.ord_term` extractors return `eql_v3` types and no `eql_v2.<symbol>` appears anywhere in the v3 SQL. The whole v3 surface relocated under a single `src/v3/` tree (`src/v3/sem/` for the hand-written SEM types, `src/v3/scalars/` for the generated domain families). A new build variant ships the `eql_v3` schema on its own as `release/cipherstash-encrypt-v3.sql`, installable into a database with no `eql_v2` present; a CI gate greps that artifact and its dependency closure to keep it `eql_v2`-free. Why: a clean foundation for the per-scalar encrypted-domain model to stand alone, ahead of it replacing the `eql_v2_encrypted` composite column type. This is additive — a new schema and a new artifact — and leaves `eql_v2` byte-for-byte unchanged. ([#255](https://github.com/cipherstash/encrypt-query-language/pull/255))

### Changed
Expand Down
52 changes: 38 additions & 14 deletions crates/eql-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,19 +310,43 @@ mod tests {
#[test]
fn operator_entry_emits_metadata_only_when_supported() {
use crate::operator_surface::operator;
// Supported comparison operator carries its planner metadata.
let eq = operator_entry(&operator("="), "eql_v3.int4_eq", "eql_v3.int4_eq", true);
assert_eq!(eq.symbol, "=");
assert_eq!(eq.function_name, "eq");
assert_eq!(
eq.metadata.as_deref(),
Some("COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel")
);
// The same operator, unsupported on this domain → no metadata line.
let eq_unsupported = operator_entry(&operator("="), "eql_v3.int4", "eql_v3.int4", false);
assert_eq!(eq_unsupported.metadata, None);
// Supported but metadata-less operator (`@>`) → still no metadata line.
let contains = operator_entry(&operator("@>"), "eql_v3.int4_eq", "eql_v3.int4_eq", true);
assert_eq!(contains.metadata, None);

// (symbol, domain, supported) -> expected `CREATE OPERATOR` metadata
// clause. Adding a term that carries operator metadata is one new row
// here, not another hand-rolled assertion block.
let cases: &[(&str, &str, bool, Option<&str>)] = &[
// Supported comparison operator carries its planner metadata.
(
"=",
"eql_v3.int4_eq",
true,
Some("COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel"),
),
// The same operator, unsupported on this domain → no metadata line.
("=", "eql_v3.int4", false, None),
// Supported but metadata-less operator (`->`) → still no metadata.
("->", "eql_v3.int4_eq", true, None),
// `@>` carries containment metadata when supported (the Bloom
// `text_match` path).
(
"@>",
"eql_v3.text_match",
true,
Some("COMMUTATOR = <@, RESTRICT = contsel, JOIN = contjoinsel"),
),
// ... but suppressed when `@>` is a blocker (non-Bloom domains),
// which is why the int4 golden is unchanged.
("@>", "eql_v3.int4_eq", false, None),
];

for (symbol, dom, supported, expected) in cases {
let entry = operator_entry(&operator(symbol), dom, dom, *supported);
assert_eq!(entry.symbol, *symbol);
assert_eq!(
entry.metadata.as_deref(),
*expected,
"operator {symbol} on {dom} (supported={supported})",
);
}
}
}
38 changes: 34 additions & 4 deletions crates/eql-codegen/src/operator_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl OperatorMetadata {
}

/// Render the `CREATE OPERATOR` metadata clause, or `None` when no hint is
/// present (the `@>`/`<@` symmetric-but-empty case collapses to `None`).
/// present (e.g. the path-selector operators, which carry no metadata).
pub fn render(self) -> Option<String> {
let mut extras = Vec::new();
if let Some(c) = self.commutator {
Expand Down Expand Up @@ -208,6 +208,18 @@ const fn cmp_metadata(
}
}

/// Containment-operator metadata (`@>` / `<@`): commutator is the mirror
/// operator, no negator (a non-containment is not another listed operator),
/// containment selectivity estimators.
const fn containment_metadata(commutator: &'static str) -> OperatorMetadata {
OperatorMetadata {
restrict: Some("contsel"),
join: Some("contjoinsel"),
commutator: Some(commutator),
negator: None,
}
}

/// The 20-operator catalog. Order is: comparison operators, then path-selector
/// operators, then the remaining native jsonb operators.
pub const OPERATORS: &[Operator] = &[
Expand Down Expand Up @@ -251,13 +263,13 @@ pub const OPERATORS: &[Operator] = &[
symbol: "@>",
function_name: "contains",
signatures: BOOL_SYMMETRIC_SIGNATURES,
metadata: OperatorMetadata::none(),
metadata: containment_metadata("<@"),
},
Operator {
symbol: "<@",
function_name: "contained_by",
signatures: BOOL_SYMMETRIC_SIGNATURES,
metadata: OperatorMetadata::none(),
metadata: containment_metadata("@>"),
},
Operator {
symbol: "->",
Expand Down Expand Up @@ -519,7 +531,25 @@ mod tests {
"COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel"
);
assert_eq!(operator("->").metadata.render(), None);
assert_eq!(operator("@>").metadata.render(), None);
// `@>`/`<@` now carry containment metadata (no negator).
assert_eq!(
operator("@>").metadata.render().unwrap(),
"COMMUTATOR = <@, RESTRICT = contsel, JOIN = contjoinsel"
);
}

#[test]
fn containment_operators_have_containment_metadata() {
let c = operator("@>");
assert_eq!(c.metadata.commutator, Some("<@"));
assert_eq!(c.metadata.restrict, Some("contsel"));
assert_eq!(c.metadata.join, Some("contjoinsel"));
assert_eq!(c.metadata.negator, None);
let cb = operator("<@");
assert_eq!(cb.metadata.commutator, Some("@>"));
assert_eq!(cb.metadata.restrict, Some("contsel"));
assert_eq!(cb.metadata.join, Some("contjoinsel"));
assert_eq!(cb.metadata.negator, None);
}

#[test]
Expand Down
Loading