Repository navigation
Restore operator class for CLLW ORE (eql_v2.ore_cllw) #220
Description
Summary
CLLW ORE has no operator class on eql_v2_encrypted columns, which means:
ORDER BY eql_v2.ore_cllw(value -> '<selector>'::text)falls back to PostgreSQL's default composite comparison (lex bytea onbytes), which is incorrect for CLLW ORE — the protocol isy + 1 == xmodulo 256, not lex.- A functional btree index on the extractor doesn't help because there's no opclass to drive ordered comparison.
- The only correct path through
eql_v2.compare()is the plpgsql priority-list dispatch — uninlinable and seq-scan only.
This blocks any functional-index path for Standard-mode ste_vec field-level range queries. See the bench numbers below.
History
There was an opclass on eql_v2.ore_cllw_var_8 and eql_v2.ore_cllw_u64_8. It was disabled on 2025-06-24 in commit 69afdc8 — fix: disable ore_cllw operators. The commit message gives no rationale; the rename simply moved everything to .skip files.
Reviewing the disabled code (now also deleted as part of the oc/op consolidation), three likely reasons it was disabled:
-
HASHES, MERGESflags on operators with no hash function defined. Every comparison operator was declaredCREATE OPERATOR ... HASHES, MERGESbut nohash_ore_cllw_var_8was ever registered. HASH JOIN planning would fail with "could not identify a hash function for type eql_v2.ore_cllw_var_8". -
Inconsistent inlinability on the
_ltbacking function. All comparison helpers wereLANGUAGE SQLexcept_lt, which was plpgsql with the SQL form commented out:CREATE FUNCTION eql_v2.ore_cllw_var_8_lt(...) RETURNS boolean -- AS $$ SELECT eql_v2.compare_ore_cllw_var_8(a, b) = -1 $$ LANGUAGE SQL; AS $$ BEGIN RETURN eql_v2.compare_ore_cllw_var_8(a, b) = -1; END; $$ LANGUAGE plpgsql;
Suggests someone hit a planner / dispatch issue with the SQL form and gave up.
-
Equality used
bytea_eqdirectly. Correct for CLLW (deterministic ORE), but inconsistent with the per-byte CLLW protocol used everywhere else, and easy to break if the underlying type ever becomes probabilistic.
Bench data showing the gap
From the feat/bench-refresh-oc-op-consolidation branch (validating PR #219), running JSON ste_vec scenarios at 1M rows on Standard mode (sv elements carrying oc):
| Scenario | 100k median | 1M median | Notes |
|---|---|---|---|
field_order/bare (ORDER BY value -> 'sel'::text) |
7.85 s | 28.9 s | seq scan + sort |
field_order/functional (ORDER BY eql_v2.ore_cllw(value -> 'sel'::text)) |
5.61 s | 20.7 s | seq scan + sort with inlined extractor |
The functional form's ~1.4× win is just the per-row inlining of eql_v2.ore_cllw extractor. Index match is impossible without an opclass.
Compare against the equality path (post-#205 inlining), which IS fully functional:
| Scenario | 100k | 1M |
|---|---|---|
field_eq/extractor (hmac_256_terms(value) @> $1) |
637 µs | 634 µs |
field_eq/functional (hmac_256(value, 'sel') = hmac_256($1)) |
611 µs | 1.09 ms |
That's what field_order can look like with proper infrastructure.
What this issue wants
Restore an operator class on eql_v2.ore_cllw (the consolidated CLLW ORE type from #219) that:
-
Implements
<,<=,=,>=,>,<>oneql_v2.ore_cllw, backed by the existingeql_v2.compare_ore_cllw_term(per-byte CLLW protocol). The per-byte loop can't be inlined, but the operator backing function should be the minimal wrapper so the planner can structurally matchORDER BY eql_v2.ore_cllw(col)against a btree index expression. -
Drops the
HASHES, MERGESflags unless a hash function is also defined. CLLW ORE doesn't have a natural hash function (ciphertexts of the same plaintext are deterministic but ordering protocols don't imply hashability), so safer to leave hash-join off the operator declaration. -
Equality via
eql_v2.compare_ore_cllw_term(a, b) = 0(not rawbytea_eq) — consistent with the rest of the CLLW path, and resilient to any future change in the underlying ciphertext encoding. -
DEFAULT FOR TYPEso functional btree indexes oneql_v2.ore_cllw(col)engage without opclass annotation. -
Splinter / pin_search_path allowlist — the operator backing functions should stay unpinned where possible (the wrappers are single-statement SQL even if
compare_ore_cllw_termis plpgsql; pinning the wrappers prevents the planner from seeing through them to the index expression).
Out of scope
- Operator class for
eql_v2.ope_cllw— separate discussion (OPE may move to a new type per recent product direction). - Cross-type operators (
eql_v2.ore_cllw×jsonb,eql_v2.ore_cllw×eql_v2_encrypted). Could be follow-ups once the same-type opclass is solid. - Cross-domain (numeric vs string) safety — the leading domain-tag byte handles it via the CLLW protocol, but worth a test.
Acceptance
- A functional btree index on
eql_v2.ore_cllw(value -> '<sel>'::text)engages forORDER BY ... LIMIT nandWHERE ... < $1queries. - The 1M
field_order/*scenarios in the JSON bench drop from ~20 s to milliseconds with the index in place. - Pre-existing CLLW ORE comparisons through
eql_v2.compare()continue to work (no regression on sort_compare / sort dispatch). - New tests in
tests/sqlx/tests/cover: same-domain ordering (numeric × numeric, string × string), cross-domain (numeric < string via the tag byte), equality (=), and the absence of HASH JOIN engagement (negative test asserting the planner falls back to merge / nestloop on a hash-join attempt).
Activity
feat(eql_v2)!: collapse ste_vec ORE terms to single oc field
feat(eql_v2)!: collapse ste_vec ORE terms to single oc field
feat(eql_v2): btree operator class on eql_v2.ore_cllw
feat(eql_v2): btree operator class on eql_v2.ore_cllw
feat(eql_v2): btree operator class on eql_v2.ore_cllw
feat(eql_v2): btree operator class on eql_v2.ore_cllw
feat(eql_v2): btree operator class on eql_v2.ore_cllw
feat(eql_v2): btree operator class on eql_v2.ore_cllw
feat(eql_v2)!: collapse ste_vec ORE terms to single oc field
feat(eql_v2): btree operator class on eql_v2.ore_cllw