Skip to content

EQL v3#314

Open
tobyhede wants to merge 357 commits into
mainfrom
eql_v3
Open

EQL v3#314
tobyhede wants to merge 357 commits into
mainfrom
eql_v3

Conversation

@tobyhede

Copy link
Copy Markdown
Contributor

Long-running integration branch for the EQL v3 major release. Tracks eql_v3 against main.

v3 introduces a new, additional eql_v3 schema of type-safe, per-capability encrypted-domain types. The existing eql_v2 surface is unchanged and remains the documented public API.

Scalar encrypted-domain families

Per-type jsonb-backed domains, each with _eq (=/<> via HMAC) and _ord/_ord_ore (< <= > >=, MIN/MAX via ORE block terms) variants, plus a storage-only base domain. Supported operators resolve to inlinable wrappers; native jsonb operators reachable via domain fallback are blocked rather than silently mis-resolving.

Encrypted-JSONB (SteVec) document type

Self-contained encrypted-JSONB surface: eql_v3.json storage domain plus ste_vec_entry / ste_vec_query. Searchable without decryption via containment (@>, <@), field/array access, entry equality, and entry-level ordered range via CLLW ORE. Comparisons are leaf-level only; root-document and native-jsonb operators are blocked. (#267)

SEM index-term types

eql_v3 owns its searchable-encrypted-metadata index-term types directly — hmac_256, ore_block_256, ore_cllw, bloom_filter — with no eql_v2 dependency. The ORE block comparator orders ciphertexts of any block count (#241). =/<> on the v2 ORE term now declare a COMMUTATOR (#239).

Codegen

Scalar domains are generated from a Rust catalog (eql-scalars::CATALOG) by the eql-codegen materializer. The Python codegen toolchain is removed. Supporting crates: eql-types (canonical payload types + JSON Schemas), eql-scalars, eql-codegen. (#252, #236, #269)

Packaging

Self-contained, standalone release/cipherstash-encrypt-v3.sql installer + uninstaller, attached to GitHub Releases. (#255, #307)

tobyhede added 30 commits June 22, 2026 14:35
The crate's single lib.rs had grown to 1215 lines, ~55% of it tests, with each
enum's definition interleaved with its impl block. Reorganise for readability so
the catalog reads top-to-bottom:

- lib.rs (~240 lines) keeps the *definitions* — the six type defs, the two
  crate-internal macros (fixtures!, int_values!), and all catalog data
  (ORDERED_INT_DOMAINS, INT*_FIXTURES, INT4/INT2/INT8/DATE, CATALOG, INT*_VALUES).
- Inherent impls move to sibling modules: kind.rs (BoundedIntKind, ScalarKind),
  term.rs (Term), fixture.rs (Fixture), spec.rs (ScalarSpec). Methods travel with
  their types, so no re-exports are needed.
- The 7 unit-test modules move verbatim into tests.rs (#[cfg(test)] mod tests),
  kept as one group because rust_tests spans multiple types; use super::* becomes
  use crate::*.

Pure code-move, zero behaviour change. Public API surface of the crate is
unchanged. Verified: cargo test -p eql-scalars (40 pass), codegen:parity
(byte-for-byte identical int4 golden), test:crates clippy -D warnings clean,
eql-codegen/eql-tests-macros build unchanged.
The docstring claimed all returned strings are Rust type names as they
appear in generated source, but Numeric/Text/Jsonb return SQL tokens
(numeric/text/jsonb) and have no generated surface. Clarify that only
I16/I32/I64/Date return canonical Rust type names and the rest are
placeholders, and note the sole call site (tests.rs).
The three compare_ore_block_u64_8_256_term(s) overloads (term×term,
term[]×term[], composite×composite) are now IMMUTABLE, diverging from
the v2 originals which default to VOLATILE. The comparison is
deterministic — its only crypto call, pgcrypto encrypt(), is IMMUTABLE —
so the planner can fold/cache in ordering and index contexts. NOT STRICT:
the NULL-handling branches are load-bearing.

T8 (family::sem::ore_comparators_are_immutable) asserts exactly 3
overloads exist and all have provolatile = 'i', so a silent regression
to VOLATILE fails CI.
Widen the existing T8 catalog query to also assert all three
compare_ore_block_u64_8_256_term(s) overloads are NOT STRICT, closing
the lopsided pin where the equally load-bearing non-STRICT property was
only guarded behaviourally (T3, term overload only). Now pinned at the
same catalog layer T8 already queries, covering the array/composite
overloads T3 does not directly assert.
…-only)

Add the timestamptz scalar encrypted-domain type to the eql_v3 family as
EQUALITY-ONLY: storage + eql_v3.timestamptz_eq (= / <> via HMAC), no ord
domains, no MIN/MAX aggregates.

Ordering is deferred: cipherstash encrypts Plaintext::Timestamp at native
12-block ORE width, but EQL's only ORE comparator
(eql_v2.compare_ore_block_u64_8_256_term) is hardcoded to 8 blocks, so an
ordered timestamptz domain would silently mis-order. Ordering follows once a
wide-ORE (12-block) term lands.

- catalog: add EQ_ONLY_DOMAINS (storage + _eq); point TIMESTAMPTZ at it.
  Replace all_types_share_the_same_domain_shape with a shape-aware test
  (every type matches one of two known shapes) plus a pin of which token uses
  which shape.
- dispatch: extend the scalar_types! entry grammar with an optional [eq_only]
  marker; eq-only entries emit eq_only_scalar_matrix! instead of
  ordered_numeric_matrix!. ordered int4/int2/date emission is byte-identical.
- matrix: eq_only_scalar_matrix! now derives its three pivots from the
  ScalarType impl (like the ordered macro) instead of requiring explicit
  pivots — the first consumer wires cleanly with no per-call pivot authoring.
- inventory: make mise test:matrix:inventory shape-aware. Add the second
  canonical snapshot matrix_tests_eq_only.txt; each discovered type is
  compared against the snapshot matching its shape (ordered vs eq-only).
  Document the two-shape mechanism in snapshots/README.md. matrix_tests.txt
  is unchanged.
- CHANGELOG: timestamptz ships equality-only, ordering deferred.
…ctness

Address PR #257 review feedback. The temporal_values! auto-generated tests
re-run the exact parse closure over all-UTC (`…Z`) catalog fixtures, so they
cannot catch a regression that drops the offset→UTC conversion, and fixture
distinctness is keyed by string upstream in eql-scalars (which is zero-dep, no
chrono) so two RFC3339 strings denoting the same UTC instant pass as distinct.

Add two harness-side guards alongside the timestamptz temporal_values! call:

- rfc3339_offset_is_normalized_to_utc: feeds an offset-bearing RFC3339 string
  and asserts it resolves to the same instant as the Z form (fails on a switch
  to .naive_utc()).
- fixtures_are_distinct_by_instant: dedups timestamptz_values() by parsed
  DateTime<Utc>, guarding the property the fixture table keys on.
Adds the `Bloom` index `Term` (json key `bf`, extractor `match_term`, ctor
`bloom_filter`, role `match`, operators `@>`/`<@`) and the `text` row to the
scalar `CATALOG`: `ScalarKind::Text`, a `_match` (Bloom) domain on top of the
ordered shape, and the `TEXT_FIXTURES` / `TEXT_VALUES` plaintext list
(materialised by a `text_values!` macro alongside `int_values!`). `Fixture::Zero`
is gated to the integer kinds. Covered by `term_tests` and catalog `#[test]`s.
The searchable-encrypted-metadata `match` term for text. Adds the
`eql_v3.bloom_filter` domain (`smallint[]`) and the inlinable
`eql_v3.bloom_filter(jsonb)` extractor + `has_bloom_filter` predicate, mirroring
`eql_v3.hmac_256`: no RAISE, no pinned search_path, so the functional GIN index
on `match_term(col)` engages structurally. The extractor gates on
`jsonb_typeof(val -> 'bf') = 'array'`, returning NULL (not erroring) for absent
or malformed `bf` outside the domain CHECK. Adds the inline-critical clause to
`pin_search_path.sql` and allowlists `match_term`/containment in the splinter
lint so the unpinned extractor stays inlinable. Covered by family/sem.rs.
Teaches the codegen operator surface about the `@>`/`<@` containment operators
so they are generated only on domains carrying the `Bloom` term (the `text_match`
domain), and blocked elsewhere via the usual domain-fallback blockers. The
operator-metadata test is table-driven, so a new term's surface is one table row.
Registers `text => String` in the `scalar_types!` list and teaches the harness
about an owned, non-Copy scalar: `ScalarType`/fixtures go `Copy` -> `Clone`,
`to_sql_literal` takes `&Self`, and `String` gets a hand-written `impl ScalarType`
(lexicographic pivots, single-quote SQL literal). The `eql-tests-macros` dispatch
is catalog-derived (`is_int_token`/`is_temporal_token`/`is_text_token` read from
`eql_scalars::CATALOG`) rather than a dispatch-list marker, and the
`scalar_fixture!` macro gains a `text` arm stamping the `Match` index. Adds the
sealed `EqlPlaintext` impl for `String` (text cast + `Plaintext::Text`).
…x text "" pivot (#262)

The scalar matrix's third pivot was hardwired to `Default::default()` — `0` for
int, the epoch for date, but `""` for text, which encrypts to an empty ORE term
and broke ordering/aggregates. Introduce the taxonomy as traits:

- `ScalarType` (base) — identity, fixtures, literal rendering.
- `OrderedScalar: ScalarType` — `min_pivot`/`max_pivot` + an overridable interior
  `mid_pivot` (default `Self::default()`). int/date inherit (0/epoch); text
  overrides to a real median ("frank"), never the degenerate "".
- `SignedScalar: OrderedScalar` — `origin()` (numeric zero / sign boundary). int
  and date only; text is NOT `SignedScalar`.

The proc-macro and the `temporal_values!` macro emit the `OrderedScalar` (+
`SignedScalar`) impls; the unified `scalar_matrix!` sweeps `min`/`mid`/`max` from
`OrderedScalar` (the `_pivot_zero_` -> `_pivot_mid_` snapshot rename). The
signed-only sign-boundary test lives in `encrypted_domain/signed.rs`, generic
over `SignedScalar`, so a `text` instantiation is a compile error. Drops `""`
from `TEXT_FIXTURES`.
SQLx coverage for the text family beyond the generated matrix: `text_smoke`
exercises `eql_v3.text_match @> match` and the blocked `=` plus empty-bloom
set semantics; `text_match` is the dedicated containment suite (self / substring
/ disjoint / bare-operator GIN index engagement). Both live under
`encrypted_domain/text/` (outside the `scalars::` namespace) so the matrix
inventory snapshot stays the uniform per-type set, and are registered alongside
the signed-only suite in `encrypted_domain.rs`.
Documents the `eql_v3.text` family (eq / match / ord) and the `Bloom` index term
in the scalar-encrypted-domain reference guide — including the
`OrderedScalar`/`SignedScalar` pivot-trait section and the catalog-derived
(marker-free) text dispatch — and adds the `[Unreleased]` changelog entry
(#260).
…(STRICT)

The `eql_v3.ore_block_u64_8_256(jsonb)` extractor is `STRICT`, so PostgreSQL
already short-circuits to NULL on a NULL argument — the explicit
`IF val IS NULL` guard is dead code. Adjacent cleanup to the SEM extractors;
no behaviour change.
The bloom containment surface (eql_v3.text_match @>/<@) replaces deprecated
LIKE/ILIKE but is semantically different (probabilistic, ngram-based, no
wildcards/anchoring), which confuses users. Close the coverage gaps:

- <@ (contained-by) was implemented in SQL but completely untested: add
  positive, negative, and commutator (a @> b == b <@ a) assertions, plus a
  literal-payload <@ engage and empty-set test.
- match_null_propagates: @>/<@ are STRICT, so a NULL operand yields NULL.
- text_match_containment_requires_all_elements: pins set-containment
  semantics (every needle ngram must be present) — the property that makes
  @> not LIKE.
- text_match_like_ilike_absent: ~~/~~* resolve to 'operator does not exist'
  on text_match, the domain a LIKE user would reach for.
- text_match_payload_check_rejects_missing_bf: the domain CHECK requires bf.

Hand-written suites only (bloom is text-only, outside the cross-type matrix);
no SQL/fixture changes — reuses existing eql_v2_text fixtures.
Adds the four characterization tests @auxesis requested on #260, each
pinning a branch the existing suite never reached:

- has_bloom_filter(jsonb) presence predicate (present/absent/{"bf":null}
  -> false) — the IS NOT NULL half of its guard was untested, and it is
  not reached transitively by the extractor or domain CHECK.
- bloom_filter(jsonb) empty-array branch: {"bf":[]} -> empty smallint[],
  not NULL (the extractor basis for empty-set containment semantics).
- String::to_sql_literal single-quote escaping (O'Brien -> 'O''Brien');
  all TEXT fixtures are quote-free so no DB test hit the .replace.
- Fixture::Zero/Min/Max -> None on non-integer kinds (Date/Text), the arm
  changed from unconditional Some(0); previously only guarded indirectly
  by the pivot_sentinels_only_appear_with_integer_kinds catalog invariant.

All four pass; behaviour was already correct, these are regression nets.
Reconcile the adding-a-scalar guide with the post-timestamptz/text catalog:
correct the claim that timestamptz is ordered (it is equality-only via
EQ_ONLY_DOMAINS), add the missing Timestamptz/Date enum variants, and note
that @>/<@ back onto Bloom containment wrappers rather than blockers. Document
the previously-undocumented mechanics a follower needs: eq-only is selected by
the catalog domain slice (caps auto-derived), a new-capability domain like
_match needs hand-written #[path]-registered suites, non-integer types need the
third scalar_domains.rs registration, and the Bloom splinter allowlist names.
The extractor doc and its sibling test comment claimed the text_match domain
CHECK guarantees `bf` is an array, so a non-array `bf` could only occur outside
the domain. The CHECK only asserts key presence (`VALUE ? 'bf'`), so a typed
value like {"bf": null} reaches the extractor with a non-array `bf` — which is
exactly why the array-gate and its test exist. Also fix a stale macro name
(`ordered_numeric_matrix!` -> `scalar_matrix!`) and trim a thrice-repeated
rationale in the text_values wiring.
Adds int2/int8/date/text/timestamptz reference goldens alongside the
existing int4 set, generated once with `cargo run -p eql-codegen` and
byte-identical to the generator output. These cover the eq-only and Bloom
`text_match` render shapes that int4 never exercises.

Pure generated SQL. The parity machinery that discovers these dirs and
gates each against the generator (dynamic discovery +
`reference_dirs_match_catalog_tokens`) lands in the stacked follow-up
(eql-codegen/eql-scalars review hardening), which depends on these files
existing. Until then the existing int4 parity test is unaffected.
…lars

Acts on the three-agent code review of the v3 scalar codegen crates
(quality, idiomatic-Rust, test coverage). No user-facing behaviour
change: generated SQL is byte-identical (parity gate green, no churn in
release/ or src/).

Parity gate (the High findings):
- Commit goldens for every catalog type (int2/int8/date/timestamptz/
  text), generated once, not just int4 — catches regressions in the
  eq-only and Bloom text_match render shapes int4 never exercises.
- Parity tests discover reference dirs dynamically and cross-check the
  set against CATALOG (reference_dirs_match_catalog_tokens); shell gate
  loops every dir.
- Add a determinism test (two runs byte-identical) and an
  extractor_terms dedup unit test.

Type-system seams:
- Introduce Role enum; is_ord_capable compares == Role::Ord instead of a
  stringly-typed "ord" (a typo can no longer disable aggregate codegen).
- wrapper_entry/unsupported_entry take &Operator (no per-signature
  re-scan); add a should_panic guard for operator() on unknown symbols.

Dead code / consolidation / errors:
- Delete unused Fixture::render_literal; collapse three token+suffix
  impls into DomainSpec::name_with_token; move extractor_terms into
  eql-scalars; WriteError derives std::error::Error via thiserror;
  ScalarKind::rust_type panics on the no-surface numeric/jsonb arms.

Coverage + low cleanup:
- Tests for WriteError::Io and the commute_op/expected_forward panic
  guards; commit matrix_tests_eq_only.txt and pin the eq-only derivation.
- Shared eql_codegen::repo_root; Drop-cleaned parity tempdir; const
  AUTO_GENERATED_MARKER; ScalarKind::is_text; doc/comment fixes.
Sweep prose, comments, scripts, mise descriptions, and test identifiers
to use the codebase's existing "reference" vocabulary (the dir is already
tests/codegen/reference/). Renames the parity test functions
rust_generator_matches_golden_files -> rust_generator_matches_reference_files
and generator_matches_reference_goldens -> generator_matches_reference_files,
plus the shell golden_set -> reference_set, with all cross-references updated.
AUTO_GENERATED_HEADER was a #[cfg(test)]-only duplicate of
AUTO_GENERATED_MARKER + "\n", kept in sync by two dedicated tests. Drop
it: tests now build file bodies with format!("{AUTO_GENERATED_MARKER}\n...")
and the lone starts_with check appends the newline inline. One source of
truth, no behavior change.
tobyhede added 30 commits June 22, 2026 15:50
…gration READMEs

Review caught build_validation_tests.rs still reading cipherstash-encrypt-v3.sql
after the Task 9 rename to the canonical name — would fail CI. Repoint all five
path literals to cipherstash-encrypt{,-uninstall}.sql. Also refresh the migration
READMEs that still documented the deleted 002-007 migrations.
The v2-coupled tasks/pin_search_path.sql is removed; the v3 build appends
tasks/pin_search_path_v3.sql. Update the prose references in src/v3 doc comments
and inlinability.rs accordingly (comment-only; no functional change).
…lan in ADR

The structural guard `encrypted_domain_blockers_are_plpgsql_and_non_strict`
matched only `%encrypted_domain_unsupported_bool%`, so a `_jsonb` (`#>`, `||`,
`-`) or `_text` (`#>>`, `->>`) blocker regressing to LANGUAGE sql / STRICT would
escape it — even though that test exists to backstop `eql_v3.lints()` without
depending on it. Broaden to `%encrypted_domain_unsupported%`, matching the
`encrypted_domain_blockers` CTE in src/v3/lint/lints.sql verbatim so the two
cannot drift. The jsonb-domain-arg EXISTS still excludes the shared
`encrypted_domain_unsupported_*(text, text)` helpers.

Also cross-link the deferred Tier-1 reference-doc rewrite plan from ADR-0001's
Related section so the follow-up is discoverable.
Port version introspection to the self-contained eql_v3 surface after the
eql_v2 removal dropped eql_v2.version() (and left tasks/build.sh's --version
flag consumed nowhere).

- src/v3/version.template: eql_v3.version() returns bare-semver text, plus a
  COMMENT ON SCHEMA eql_v3 marker for obj_description() discoverability.
  REQUIRE src/v3/schema.sql only; self-contained, no eql_v2 symbols. The
  generated src/v3/version.sql is gitignored like the other v3 SQL.
- tasks/build.sh: sed-substitute the template before the v3 glob, reviving the
  --version flag (usage_version, DEV fallback).
- release workflows: pass prefix-stripped bare semver to --version; fix the
  stale 'eql_v2.version() byte-identical' comment.
- CHANGELOG/ADR-0001: document the re-home (not a silent drop).

version() is pinned by pin_search_path_v3.sql (not inline-critical), so the
splinter function_search_path_mutable lint does not flag it.

User-facing doc repointing is owned by the migrate-docs-to-eql-v3 branch.
- v3_jsonb_operator_surface_tests: drop dead eql_v2_encrypted exclusion
  subqueries (composite type removed -> subquery is always-true) and the
  now-stale explanatory comment
- xml-to-markdown.py: make the operator-schema match v3-aware
  (['eql_v2','public'] -> add 'eql_v3') so v3-only doc generation resolves
  eql_v3 operator names
- matrix.rs: fix now-false comment claiming the generic eql_v2_encrypted
  MIN/MAX overload is reachable via cast (overload removed); logic unchanged
- eql_plaintext.rs: reword Cast doc-comment off the removed
  eql_v2.add_search_config reference
Remove eql_v2 — ship only the self-contained eql_v3 surface
…b durable performance guidance; drop operator-class recipe
…ts, permissions, getting started, versioning)
The doc migration predated the version() port, so version() was documented
nowhere and docker/README still referenced the removed eql_v2 surface.

- README.md: add a 'SELECT eql_v3.version()' check to the Versioning section.
- docker/README.md: repoint the intro prose ('eql_v2 schema') and the version
  example onto eql_v3 (the file was missed by the migration).
- docs_v3_grep.sh: add docker/README.md to the Tier-1 gate so it stays
  eql_v2-free.

eql_v3.version() ships from the remove-eql-v2 branch.
…urface

Replace the removed eql_v2 architecture (eql_v2_encrypted composite, config
table, old src/ layout, db-side index config) with the v3 model: per-scalar
eql_v3.<T> domains, the catalog/codegen build, functional indexes on extractors,
and client-side configuration.
…md links

- json-support: fix ste_vec ORE leaf term key ocv/ocf -> oc (matches src/v3/jsonb)
- releasing-an-alpha: correct artifact model to the two real release artifacts
  (cipherstash-encrypt.sql is the self-contained eql_v3 surface) + docs bundle
- sql-documentation-templates: drop dead @see add_search_config; AS [base_type]
  -> AS jsonb (no domain-over-domain footgun)
- repoint inbound PAYLOAD.md links (docs index, eql-functions, proxy-configuration)
  to crates/eql-types (canonical wire types) + json-support.md
- WHY: markdown spacing fix
…ng docs

Correct the remaining "eql_v2 coexists / is the unchanged public API" framing
to match reality: eql_v2 was removed in 3.0.0 and eql_v3 is the sole shipped
surface.

- DEVELOPMENT.md: replace the coexistence "Schemas" section with an accurate
  "The eql_v3 surface" section (v2 removed, fork-provenance/historical mentions
  flagged as deliberate); fix TOC anchor.
- CLAUDE.md: fix the Schema bullet and Versioning paragraph that still asserted
  eql_v2 coexists/unchanged.
- SUPABASE.md: reword the two remaining v2 callouts to teach the v3 way with no
  v2 references (a separate v2->v3 migration guide can come later).
- docs/development/reference-sync-rules.md: update stale eql_v2 examples to
  verified v3 symbols (eql_v3.ciphertext, eql_v3.eq_term).

Remove obsolete/invented artifacts and dead v2 fixtures:
- delete docs/decisions/0001-remove-eql-v2.md and
  docs/plans/add-doxygen-sql-comments-plan.md; strip the 3 dangling ADR links
  from CHANGELOG (entries already explain the why inline).
- delete tests/ORE_FIXTURES.md (documented the removed v2 proxy fixture flow;
  v3 ORE coverage is subsumed by catalog-generated scalar fixtures) and the
  orphaned tests/ore.sql / tests/ore_text.sql data files (loaded by nothing).

Guard: refactor tasks/test/docs_v3_grep.sh from a hand-maintained allowlist to
scan-all-with-exclusions over git-tracked docs (new reference/tutorial/concept
pages covered automatically; untracked scratch ignored), and wire it into the
docs-static CI job (runs on every PR, already in ci-required).
Migrate user-facing docs to the eql_v3
Re-verify the eql_v3 implementation audit against the current tree and
correct drift introduced by the 3.0.0 overhaul (single self-contained
installer; eql_v2 removed):

- §1/§6: eql_v2 is removed, not 'the documented public API, unchanged';
  reframe the ORE-comparator note as historical fork provenance.
- §5: replace the 4-variant build table with the single-artifact build;
  re-cite build.sh and rename pin_search_path.sql -> pin_search_path_v3.sql.
- §3.3: drop stale 'Supabase variant' exclusion; explain the functional-
  index rationale that made the subset build redundant.
- §4.3: v3_ste_vec.sql is now generated/gitignored, not a committed fixture.
- §2.4: fix drifted scalar_domains.rs / spec.rs line citations.

De-enumerate snapshot baseline counts so they can't rot: CLAUDE.md and
audit §4.2 now point at tests/sqlx/snapshots/README.md as the source of
truth, and that README now documents matrix_jsonb_entry_tests.txt.
…ng comment

The `eql_v3.json` domain flattens to native `jsonb` when an operator's RHS is
an unknown-typed literal, so a bare `col -> 'sel'` binds the NATIVE `jsonb ->
text` (a root-key lookup on the envelope) instead of the v3 selector-lookup
operator — a silent wrong answer for direct-SQL callers (the Proxy is unaffected
because it always sends typed `$n`). This is intrinsic to the domain type-kind
and cannot be closed by an extra operator/blocker; it can only be pinned.

- Re-land v3_jsonb_bare_operand_flattens_to_native (blocker face: `?` / `||`
  succeed as native on a bare RHS, raise on a typed RHS), recovered from the
  dropped commit 817a9660.
- Add v3_jsonb_arrow_bare_operand_flattens_to_native (supported-operator face):
  assert via pg_typeof which operator binds AND the user-visible value
  divergence, so a resolution change in either direction goes red. Verified
  empirically against PG 17 (bare `-> 'sv'` -> jsonb `[]`; typed `-> 'sv'::text`
  -> eql_v3.ste_vec_entry NULL).
- Regenerate snapshots/v3_jsonb_tests.txt (74 -> 76) for test:v3-jsonb:inventory.
- Fix the inline comment in operators.sql (integer `->` overload): it claimed a
  bare `e -> 'sv'` would bind the v3 operator — empirically false and contrary to
  the file's own @warning. Bare binds native; the custom operator does not
  capture an untyped literal. Comment-only, no behaviour change.
…allthrough

test(eql-v3): pin ->/->> bare-literal domain-flattening; fix misleading comment
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.

2 participants