From c8b4d798b3dee3ab6c9df8268d991836c5e5d679 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 13:32:16 +1000 Subject: [PATCH 01/13] fix(ore_block_u64_8_256): self-commutator on = and <> operators The = and <> operators on eql_v2.ore_block_u64_8_256 were declared MERGES (mergejoinable) but carried no COMMUTATOR, so the planner raised "could not find commutator" the first time an ore_block equality reached a join qual. Equality is symmetric, so the commutator is the operator itself. Surfaced by the eql_v2_int4 ordered variants, whose equality wrappers inline to ord(a) = ord(b). --- src/ore_block_u64_8_256/operators.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ore_block_u64_8_256/operators.sql b/src/ore_block_u64_8_256/operators.sql index e9e34561..06a4fa65 100644 --- a/src/ore_block_u64_8_256/operators.sql +++ b/src/ore_block_u64_8_256/operators.sql @@ -123,10 +123,17 @@ $$; --! @brief = operator for ORE block types +--! +--! COMMUTATOR is the operator itself: equality is symmetric. The clause +--! is required for a MERGES (mergejoinable) operator — without it the +--! planner raises "could not find commutator" the first time an +--! ore_block equality is used as a join qual (e.g. via the inlined +--! eql_v2_int4_ord_ore equality wrappers). CREATE OPERATOR = ( FUNCTION=eql_v2.ore_block_u64_8_256_eq, LEFTARG=eql_v2.ore_block_u64_8_256, RIGHTARG=eql_v2.ore_block_u64_8_256, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel, @@ -137,10 +144,14 @@ CREATE OPERATOR = ( --! @brief <> operator for ORE block types +--! +--! COMMUTATOR is the operator itself: inequality is symmetric. Required +--! alongside the MERGES flag — see the = operator above. CREATE OPERATOR <> ( FUNCTION=eql_v2.ore_block_u64_8_256_neq, LEFTARG=eql_v2.ore_block_u64_8_256, RIGHTARG=eql_v2.ore_block_u64_8_256, + COMMUTATOR = <>, NEGATOR = =, RESTRICT = eqsel, JOIN = eqjoinsel, From adf549e08665c7fe952a5b3413f684d900f00889 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 13:32:16 +1000 Subject: [PATCH 02/13] feat(encrypted_int4): eql_v2_int4 capability-encoded domain type family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four domain types over int4 for encrypted columns, each encoding the operator surface its index terms support: - eql_v2_int4 — storage only, every operator blocked (carries c) - eql_v2_int4_eq — HMAC equality (=, <>; carries c, hm) - eql_v2_int4_ord_ore — equality + ORE-block ordering (carries c, ob) - eql_v2_int4_ord — recommended ordered name; same surface as _ord_ore Ordered variants expose a single uniform extractor, eql_v2.ord(col), returning the internal eql_v2.ore_block_u64_8_256 composite, so equality and range share one functional btree and ORDER BY eql_v2.ord(col) sorts in plaintext order. Per-variant SQL is split into _functions.sql and _operators.sql; operator wrappers are inlinable SQL while blocker operators are PL/pgSQL and raise on use. Updates tasks/build.sh, tasks/pin_search_path.sql, and the splinter allowlist to keep the inline-critical wrappers unpinned. All variants live in public and survive eql_v2 uninstall. --- src/encrypted_domain/functions.sql | 27 ++ .../int4/int4_eq_functions.sql | 336 ++++++++++++++++ .../int4/int4_eq_operators.sql | 127 ++++++ src/encrypted_domain/int4/int4_functions.sql | 343 ++++++++++++++++ src/encrypted_domain/int4/int4_operators.sql | 127 ++++++ .../int4/int4_ord_functions.sql | 374 ++++++++++++++++++ .../int4/int4_ord_operators.sql | 134 +++++++ .../int4/int4_ord_ore_functions.sql | 371 +++++++++++++++++ .../int4/int4_ord_ore_operators.sql | 131 ++++++ src/encrypted_domain/types.sql | 81 ++++ tasks/build.sh | 2 + tasks/pin_search_path.sql | 31 ++ tasks/test/splinter.sh | 15 + 13 files changed, 2099 insertions(+) create mode 100644 src/encrypted_domain/functions.sql create mode 100644 src/encrypted_domain/int4/int4_eq_functions.sql create mode 100644 src/encrypted_domain/int4/int4_eq_operators.sql create mode 100644 src/encrypted_domain/int4/int4_functions.sql create mode 100644 src/encrypted_domain/int4/int4_operators.sql create mode 100644 src/encrypted_domain/int4/int4_ord_functions.sql create mode 100644 src/encrypted_domain/int4/int4_ord_operators.sql create mode 100644 src/encrypted_domain/int4/int4_ord_ore_functions.sql create mode 100644 src/encrypted_domain/int4/int4_ord_ore_operators.sql create mode 100644 src/encrypted_domain/types.sql diff --git a/src/encrypted_domain/functions.sql b/src/encrypted_domain/functions.sql new file mode 100644 index 00000000..b00ab755 --- /dev/null +++ b/src/encrypted_domain/functions.sql @@ -0,0 +1,27 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql + +--! @file encrypted_domain/functions.sql +--! @brief Shared blocker helper for the eql_v2_int4 variant family. +--! +--! Per-variant wrapper functions live in src/encrypted_domain/int4/. +--! Blockers in those files delegate to encrypted_domain_unsupported_bool +--! so every variant raises a uniform variant-specific error rather than +--! letting an unsupported operator fall through to native jsonb +--! behaviour. + +--! @brief Shared blocker helper. Raises 'operator X is not supported +--! for TYPE' so unsupported domain operators surface a clear +--! error rather than fall through to native jsonb behaviour. +--! @param type_name Domain type name (eql_v2_int4*) +--! @param operator_name Operator symbol (=, <, ~~, @>, ->, etc.) +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.encrypted_domain_unsupported_bool(type_name text, operator_name text) +RETURNS boolean +IMMUTABLE PARALLEL SAFE +SET search_path = pg_catalog, extensions, public +AS $$ +BEGIN + RAISE EXCEPTION 'operator % is not supported for %', operator_name, type_name; +END; +$$ LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_eq_functions.sql b/src/encrypted_domain/int4/int4_eq_functions.sql new file mode 100644 index 00000000..add81e67 --- /dev/null +++ b/src/encrypted_domain/int4/int4_eq_functions.sql @@ -0,0 +1,336 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql +-- REQUIRE: src/encrypted_domain/functions.sql +-- REQUIRE: src/hmac_256/functions.sql + +--! @file encrypted_domain/int4/int4_eq_functions.sql +--! @brief Equality-only int4 variant — comparison/path functions. Supports = and <> via HMAC-256. +--! +--! eql_v2_int4_eq carries `c`, `hm` and supports HMAC equality. The +--! functional btree on ((eql_v2.hmac_256(col::jsonb))) engages for `=`. +--! `<>` is supported but is a seq-scan (btree supports only equality). +--! All other operators raise. Payload-term assumption: `c`, `hm`. + +-- = / <> (HMAC equality wrappers, 3 shapes each) + +--! @brief Equality wrapper for eql_v2_int4_eq. Inlines to hmac_256 comparison. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.hmac_256(a::jsonb) = eql_v2.hmac_256(b::jsonb) $$; + +--! @brief Equality wrapper for eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a eql_v2_int4_eq, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.hmac_256(a::jsonb) = eql_v2.hmac_256(b) $$; + +--! @brief Equality wrapper for eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a jsonb, b eql_v2_int4_eq) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.hmac_256(a) = eql_v2.hmac_256(b::jsonb) $$; + +--! @brief Inequality wrapper for eql_v2_int4_eq. Inlines to hmac_256 comparison. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.hmac_256(a::jsonb) <> eql_v2.hmac_256(b::jsonb) $$; + +--! @brief Inequality wrapper for eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a eql_v2_int4_eq, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.hmac_256(a::jsonb) <> eql_v2.hmac_256(b) $$; + +--! @brief Inequality wrapper for eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a jsonb, b eql_v2_int4_eq) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.hmac_256(a) <> eql_v2.hmac_256(b::jsonb) $$; + +-- <, <=, >, >=, ~~, ~~*, @>, <@ (blockers, 3 shapes each — 8 ops × 3 = 24 functions) + +--! @brief Blocker for < on eql_v2_int4_eq. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_lt(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for < on eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_lt(a eql_v2_int4_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for < on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_lt(a jsonb, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <= on eql_v2_int4_eq. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_lte(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <= on eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_lte(a eql_v2_int4_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <= on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_lte(a jsonb, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for > on eql_v2_int4_eq. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_gt(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for > on eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_gt(a eql_v2_int4_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for > on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_gt(a jsonb, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for >= on eql_v2_int4_eq. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_gte(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for >= on eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_gte(a eql_v2_int4_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for >= on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_gte(a jsonb, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4_eq. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_like(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_like(a eql_v2_int4_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_like(a jsonb, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_eq. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_ilike(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_ilike(a eql_v2_int4_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_ilike(a jsonb, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_eq. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_contains(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_contains(a eql_v2_int4_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_contains(a jsonb, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_eq. +--! @param a eql_v2_int4_eq +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_contained_by(a eql_v2_int4_eq, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<@'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_eq (domain, jsonb). +--! @param a eql_v2_int4_eq +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_contained_by(a eql_v2_int4_eq, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<@'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_eq +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_contained_by(a jsonb, b eql_v2_int4_eq) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<@'); END; $$ +LANGUAGE plpgsql; + +-- -> and ->> (blockers, 3 asymmetric shapes each) + +--! @brief Blocker for -> on eql_v2_int4_eq (domain, text). +--! @param a eql_v2_int4_eq +--! @param selector text +--! @return eql_v2_int4_eq (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow(a eql_v2_int4_eq, selector text) +RETURNS eql_v2_int4_eq IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for -> on eql_v2_int4_eq (domain, integer). +--! @param a eql_v2_int4_eq +--! @param selector integer +--! @return eql_v2_int4_eq (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow(a eql_v2_int4_eq, selector integer) +RETURNS eql_v2_int4_eq IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for -> on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param selector eql_v2_int4_eq +--! @return eql_v2_int4_eq (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow(a jsonb, selector eql_v2_int4_eq) +RETURNS eql_v2_int4_eq IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_eq (domain, text). +--! @param a eql_v2_int4_eq +--! @param selector text +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow_text(a eql_v2_int4_eq, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_eq (domain, integer). +--! @param a eql_v2_int4_eq +--! @param selector integer +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow_text(a eql_v2_int4_eq, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_eq'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_eq (jsonb, domain). +--! @param a jsonb +--! @param selector eql_v2_int4_eq +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow_text(a jsonb, selector eql_v2_int4_eq) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_eq'; END; $$ +LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_eq_operators.sql b/src/encrypted_domain/int4/int4_eq_operators.sql new file mode 100644 index 00000000..0234381c --- /dev/null +++ b/src/encrypted_domain/int4/int4_eq_operators.sql @@ -0,0 +1,127 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql +-- REQUIRE: src/encrypted_domain/int4/int4_eq_functions.sql + +--! @file encrypted_domain/int4/int4_eq_operators.sql +--! @brief Equality-only int4 variant — operator declarations. Supports = and <> via HMAC-256. +--! +--! eql_v2_int4_eq carries `c`, `hm` and supports HMAC equality. The +--! functional btree on ((eql_v2.hmac_256(col::jsonb))) engages for `=`. +--! `<>` is supported but is a seq-scan (btree supports only equality). +--! All other operators raise. Payload-term assumption: `c`, `hm`. + +-- Operator declarations + +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_eq_eq, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_eq_eq, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_eq_eq, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_eq_neq, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_eq_neq, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_eq_neq, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v2.eql_v2_int4_eq_lt, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_eq_lt, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); +CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_eq_lt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR <= ( + FUNCTION = eql_v2.eql_v2_int4_eq_lte, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_eq_lte, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); +CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_eq_lte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR > ( + FUNCTION = eql_v2.eql_v2_int4_eq_gt, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_eq_gt, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); +CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_eq_gt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR >= ( + FUNCTION = eql_v2.eql_v2_int4_eq_gte, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_eq_gte, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); +CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_eq_gte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_eq_like, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_eq_like, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_eq_like, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_eq_ilike, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_eq_ilike, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_eq_ilike, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_eq_contains, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_eq_contains, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_eq_contains, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_eq_contained_by, + LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_eq_contained_by, + LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_eq_contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow, + LEFTARG = eql_v2_int4_eq, RIGHTARG = text); +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow, + LEFTARG = eql_v2_int4_eq, RIGHTARG = integer); +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); + +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow_text, + LEFTARG = eql_v2_int4_eq, RIGHTARG = text); +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow_text, + LEFTARG = eql_v2_int4_eq, RIGHTARG = integer); +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow_text, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); diff --git a/src/encrypted_domain/int4/int4_functions.sql b/src/encrypted_domain/int4/int4_functions.sql new file mode 100644 index 00000000..e7d22f76 --- /dev/null +++ b/src/encrypted_domain/int4/int4_functions.sql @@ -0,0 +1,343 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql +-- REQUIRE: src/encrypted_domain/functions.sql + +--! @file encrypted_domain/int4/int4_functions.sql +--! @brief Storage-only int4 variant — comparison/path functions. All bool operators raise. +--! +--! eql_v2_int4 accepts the storage of an encrypted int4 column with +--! ciphertext (`c`) only. Every comparison, containment, LIKE, and path +--! operator is a blocker so callers cannot accidentally fall through to +--! native jsonb semantics. Payload-term assumption: `c` only. + +-- =, <> (blockers, 3 shapes each) + +--! @brief Blocker for = on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for = on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for = on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_eq(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <> on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_neq(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <> on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_neq(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <> on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_neq(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<>'); END; $$ +LANGUAGE plpgsql; + +-- <, <=, >, >= (blockers, 3 shapes each) + +--! @brief Blocker for < on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_lt(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for < on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_lt(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for < on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_lt(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <= on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_lte(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <= on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_lte(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <= on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_lte(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for > on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_gt(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for > on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_gt(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for > on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_gt(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for >= on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_gte(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for >= on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_gte(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>='); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for >= on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_gte(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>='); END; $$ +LANGUAGE plpgsql; + +-- ~~, ~~*, @>, <@ (blockers, 3 shapes each) + +--! @brief Blocker for ~~ on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_like(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_like(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_like(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ilike(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ilike(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ilike(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_contains(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_contains(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_contains(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4. +--! @param a eql_v2_int4 +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_contained_by(a eql_v2_int4, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<@'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4 (domain, jsonb). +--! @param a eql_v2_int4 +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_contained_by(a eql_v2_int4, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<@'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4 +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_contained_by(a jsonb, b eql_v2_int4) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<@'); END; $$ +LANGUAGE plpgsql; + +-- -> and ->> (blockers, 3 asymmetric shapes each) + +--! @brief Blocker for -> on eql_v2_int4 (domain, text). +--! @param a eql_v2_int4 +--! @param selector text +--! @return eql_v2_int4 (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_arrow(a eql_v2_int4, selector text) +RETURNS eql_v2_int4 IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for -> on eql_v2_int4 (domain, integer). +--! @param a eql_v2_int4 +--! @param selector integer +--! @return eql_v2_int4 (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_arrow(a eql_v2_int4, selector integer) +RETURNS eql_v2_int4 IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for -> on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param selector eql_v2_int4 +--! @return eql_v2_int4 (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_arrow(a jsonb, selector eql_v2_int4) +RETURNS eql_v2_int4 IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4 (domain, text). +--! @param a eql_v2_int4 +--! @param selector text +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_arrow_text(a eql_v2_int4, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4 (domain, integer). +--! @param a eql_v2_int4 +--! @param selector integer +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_arrow_text(a eql_v2_int4, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4 (jsonb, domain). +--! @param a jsonb +--! @param selector eql_v2_int4 +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_arrow_text(a jsonb, selector eql_v2_int4) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4'; END; $$ +LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_operators.sql b/src/encrypted_domain/int4/int4_operators.sql new file mode 100644 index 00000000..6e25e7e5 --- /dev/null +++ b/src/encrypted_domain/int4/int4_operators.sql @@ -0,0 +1,127 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql +-- REQUIRE: src/encrypted_domain/int4/int4_functions.sql + +--! @file encrypted_domain/int4/int4_operators.sql +--! @brief Storage-only int4 variant — operator declarations. All bool operators raise. +--! +--! eql_v2_int4 accepts the storage of an encrypted int4 column with +--! ciphertext (`c`) only. Every comparison, containment, LIKE, and path +--! operator is a blocker so callers cannot accidentally fall through to +--! native jsonb semantics. Payload-term assumption: `c` only. + +-- Operator declarations (10 symmetric ops × 3 shapes + 2 path ops × 3 asymmetric shapes) + +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_eq, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_eq, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_eq, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_neq, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_neq, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_neq, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v2.eql_v2_int4_lt, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_lt, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb); +CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_lt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR <= ( + FUNCTION = eql_v2.eql_v2_int4_lte, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_lte, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb); +CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_lte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR > ( + FUNCTION = eql_v2.eql_v2_int4_gt, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_gt, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb); +CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_gt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR >= ( + FUNCTION = eql_v2.eql_v2_int4_gte, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_gte, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb); +CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_gte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_like, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_like, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb); +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_like, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ilike, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ilike, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb); +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ilike, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_contains, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_contains, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb); +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_contains, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_contained_by, + LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_contained_by, + LEFTARG = eql_v2_int4, RIGHTARG = jsonb); +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_arrow, + LEFTARG = eql_v2_int4, RIGHTARG = text); +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_arrow, + LEFTARG = eql_v2_int4, RIGHTARG = integer); +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_arrow, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); + +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_arrow_text, + LEFTARG = eql_v2_int4, RIGHTARG = text); +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_arrow_text, + LEFTARG = eql_v2_int4, RIGHTARG = integer); +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_arrow_text, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4); diff --git a/src/encrypted_domain/int4/int4_ord_functions.sql b/src/encrypted_domain/int4/int4_ord_functions.sql new file mode 100644 index 00000000..c10d674f --- /dev/null +++ b/src/encrypted_domain/int4/int4_ord_functions.sql @@ -0,0 +1,374 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql +-- REQUIRE: src/encrypted_domain/functions.sql +-- REQUIRE: src/ore_block_u64_8_256/functions.sql +-- REQUIRE: src/ore_block_u64_8_256/operators.sql + +--! @file encrypted_domain/int4/int4_ord_functions.sql +--! @brief Concrete ordered int4 variant (D-E fallback) — comparison/path +--! functions. The recommended ordered name. +--! +--! eql_v2_int4_ord carries `c`, `ob`. It is a full concrete mirror of +--! int4_ord_ore.sql: the §8 verification spike showed the pure-alias +--! form (a domain over eql_v2_int4_ord_ore) does not transparently +--! inherit the operator surface — PostgreSQL resolves operators against +--! the ultimate base type (jsonb), so ordered operators fall through to +--! native jsonb comparison and the blockers do not engage. +--! eql_v2_int4_ord therefore carries its own eql_v2.ord() overload, +--! comparison wrappers, operator declarations, and blockers. +--! eql_v2_int4_ord_ore is the scheme-explicit ordered domain with the +--! identical operator surface. +--! +--! Equality and range both route through eql_v2.ord: ord(a) ord(b) +--! is the corresponding operator on eql_v2.ore_block_u64_8_256. ORE on a +--! full-domain int4 is lossless, so the order term is also an exact +--! equality term — there is no separate `hm` term (D#1). +--! +--! All six comparison wrappers are LANGUAGE sql IMMUTABLE STRICT +--! PARALLEL SAFE with no SET clause, so the planner inlines them: +--! `col < $1` becomes `eql_v2.ord(col) < eql_v2.ord($1)`. The inner `<` +--! is the operator on eql_v2.ore_block_u64_8_256, a member of main's +--! DEFAULT btree operator class. A functional index +--! `USING btree (eql_v2.ord(col))` therefore serves all six operators. +--! +--! @note The ORE-block operator class is excluded from the Supabase +--! build variant, so ordered int4 columns have no indexed range on +--! Supabase (seq-scan). See docs/upgrading/v2.4.md U-001. + +--! @brief Index/ORDER BY extractor for the ordered int4 variants. +--! +--! Returns the ORE-block composite carried in the `ob` field of the +--! jsonb payload. The returned eql_v2.ore_block_u64_8_256 type carries +--! main's DEFAULT btree operator class, so a functional index +--! USING btree (eql_v2.ord(col)) binds that opclass automatically. +--! This is the single uniform extractor for index creation and ORDER BY +--! across the ordered variants. +--! +--! @param a eql_v2_int4_ord Ordered encrypted int4 value +--! @return eql_v2.ore_block_u64_8_256 ORE-block index term +--! @throws Exception if the `ob` field is missing from the payload +--! @see eql_v2.ore_block_u64_8_256 +--! @example +--! -- functional index for range + equality +--! CREATE INDEX t_col_idx ON t USING btree (eql_v2.ord(col)); +--! -- ordering +--! SELECT ... FROM t ORDER BY eql_v2.ord(col); +CREATE FUNCTION eql_v2.ord(a eql_v2_int4_ord) +RETURNS eql_v2.ore_block_u64_8_256 +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ore_block_u64_8_256(a::jsonb) $$; + +-- = <> < <= > >= comparison wrappers, 3 arg-shapes each (18 functions). +-- All LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE, no SET clause, so they +-- inline: `col < $1` becomes `eql_v2.ord(col) < eql_v2.ord($1)`. + +--! @brief Less-than wrapper for eql_v2_int4_ord. Inlines to ORE-block compare. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b) $$; + +--! @brief Less-than wrapper for eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a eql_v2_int4_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b::eql_v2_int4_ord) $$; + +--! @brief Less-than wrapper for eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a jsonb, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) < eql_v2.ord(b) $$; + +--! @brief Less-than-or-equal wrapper for eql_v2_int4_ord. Inlines to ORE-block compare. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b) $$; + +--! @brief Less-than-or-equal wrapper for eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a eql_v2_int4_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b::eql_v2_int4_ord) $$; + +--! @brief Less-than-or-equal wrapper for eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a jsonb, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) <= eql_v2.ord(b) $$; + +--! @brief Greater-than wrapper for eql_v2_int4_ord. Inlines to ORE-block compare. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b) $$; + +--! @brief Greater-than wrapper for eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a eql_v2_int4_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b::eql_v2_int4_ord) $$; + +--! @brief Greater-than wrapper for eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a jsonb, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) > eql_v2.ord(b) $$; + +--! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord. Inlines to ORE-block compare. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b) $$; + +--! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a eql_v2_int4_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b::eql_v2_int4_ord) $$; + +--! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a jsonb, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) >= eql_v2.ord(b) $$; + +--! @brief Equality wrapper for eql_v2_int4_ord. Routes through ord — ORE on +--! full-domain int4 is lossless, so this is exact equality. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b) $$; + +--! @brief Equality wrapper for eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a eql_v2_int4_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b::eql_v2_int4_ord) $$; + +--! @brief Equality wrapper for eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a jsonb, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) = eql_v2.ord(b) $$; + +--! @brief Inequality wrapper for eql_v2_int4_ord. Routes through ord. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b) $$; + +--! @brief Inequality wrapper for eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a eql_v2_int4_ord, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b::eql_v2_int4_ord) $$; + +--! @brief Inequality wrapper for eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a jsonb, b eql_v2_int4_ord) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) <> eql_v2.ord(b) $$; + +-- ~~, ~~*, @>, <@ (blockers, 3 shapes each) + +--! @brief Blocker for ~~ on eql_v2_int4_ord. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_like(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_like(a eql_v2_int4_ord, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_like(a jsonb, b eql_v2_int4_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_ord. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ilike(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ilike(a eql_v2_int4_ord, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ilike(a jsonb, b eql_v2_int4_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_ord. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_contains(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_contains(a eql_v2_int4_ord, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_contains(a jsonb, b eql_v2_int4_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_ord. +--! @param a eql_v2_int4_ord +--! @param b eql_v2_int4_ord +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_contained_by(a eql_v2_int4_ord, b eql_v2_int4_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '<@'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_ord (domain, jsonb). +--! @param a eql_v2_int4_ord +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_contained_by(a eql_v2_int4_ord, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '<@'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_contained_by(a jsonb, b eql_v2_int4_ord) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '<@'); END; $$ +LANGUAGE plpgsql; + +-- -> and ->> (blockers, 3 asymmetric shapes each) + +--! @brief Blocker for -> on eql_v2_int4_ord (domain, text). +--! @param a eql_v2_int4_ord +--! @param selector text +--! @return eql_v2_int4_ord (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow(a eql_v2_int4_ord, selector text) +RETURNS eql_v2_int4_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for -> on eql_v2_int4_ord (domain, integer). +--! @param a eql_v2_int4_ord +--! @param selector integer +--! @return eql_v2_int4_ord (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow(a eql_v2_int4_ord, selector integer) +RETURNS eql_v2_int4_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for -> on eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param selector eql_v2_int4_ord +--! @return eql_v2_int4_ord (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow(a jsonb, selector eql_v2_int4_ord) +RETURNS eql_v2_int4_ord IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_ord (domain, text). +--! @param a eql_v2_int4_ord +--! @param selector text +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow_text(a eql_v2_int4_ord, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_ord (domain, integer). +--! @param a eql_v2_int4_ord +--! @param selector integer +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow_text(a eql_v2_int4_ord, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_ord (jsonb, domain). +--! @param a jsonb +--! @param selector eql_v2_int4_ord +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow_text(a jsonb, selector eql_v2_int4_ord) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord'; END; $$ +LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_ord_operators.sql b/src/encrypted_domain/int4/int4_ord_operators.sql new file mode 100644 index 00000000..29c1a98b --- /dev/null +++ b/src/encrypted_domain/int4/int4_ord_operators.sql @@ -0,0 +1,134 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql +-- REQUIRE: src/encrypted_domain/int4/int4_ord_functions.sql + +--! @file encrypted_domain/int4/int4_ord_operators.sql +--! @brief Concrete ordered int4 variant (D-E fallback) — operator +--! declarations. The recommended ordered name. +--! +--! eql_v2_int4_ord carries `c`, `ob`. It is a full concrete mirror of +--! int4_ord_ore.sql: the §8 verification spike showed the pure-alias +--! form (a domain over eql_v2_int4_ord_ore) does not transparently +--! inherit the operator surface — PostgreSQL resolves operators against +--! the ultimate base type (jsonb), so ordered operators fall through to +--! native jsonb comparison and the blockers do not engage. +--! eql_v2_int4_ord therefore carries its own eql_v2.ord() overload, +--! comparison wrappers, operator declarations, and blockers. +--! eql_v2_int4_ord_ore is the scheme-explicit ordered domain with the +--! identical operator surface. + +-- Operator declarations + +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_ord_eq, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_ord_eq, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_ord_eq, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_ord_neq, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_ord_neq, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_ord_neq, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v2.eql_v2_int4_ord_lt, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_ord_lt, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); +CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_ord_lt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR <= ( + FUNCTION = eql_v2.eql_v2_int4_ord_lte, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_ord_lte, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); +CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_ord_lte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR > ( + FUNCTION = eql_v2.eql_v2_int4_ord_gt, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_ord_gt, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); +CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_ord_gt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR >= ( + FUNCTION = eql_v2.eql_v2_int4_ord_gte, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_ord_gte, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); +CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_ord_gte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_like, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_like, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_like, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ilike, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ilike, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ilike, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_contains, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_contains, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_contains, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_contained_by, + LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_contained_by, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow, + LEFTARG = eql_v2_int4_ord, RIGHTARG = text); +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow, + LEFTARG = eql_v2_int4_ord, RIGHTARG = integer); +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); + +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow_text, + LEFTARG = eql_v2_int4_ord, RIGHTARG = text); +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow_text, + LEFTARG = eql_v2_int4_ord, RIGHTARG = integer); +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow_text, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); diff --git a/src/encrypted_domain/int4/int4_ord_ore_functions.sql b/src/encrypted_domain/int4/int4_ord_ore_functions.sql new file mode 100644 index 00000000..399456c6 --- /dev/null +++ b/src/encrypted_domain/int4/int4_ord_ore_functions.sql @@ -0,0 +1,371 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql +-- REQUIRE: src/encrypted_domain/functions.sql +-- REQUIRE: src/ore_block_u64_8_256/functions.sql +-- REQUIRE: src/ore_block_u64_8_256/operators.sql + +--! @file encrypted_domain/int4/int4_ord_ore_functions.sql +--! @brief Concrete ordered int4 variant — comparison/path functions +--! (equality + ORE-block ordering). +--! +--! eql_v2_int4_ord_ore carries `c`, `ob`. It is the scheme-explicit +--! ordered domain: it carries the eql_v2.ord() extractor, the six +--! comparison wrappers, the operator declarations, and the blockers. +--! eql_v2_int4_ord — the recommended ordered name — is a separate +--! concrete domain (int4_ord.sql) carrying its own copy of this +--! operator surface; the §8 spike showed a domain-over-domain alias +--! does not transparently inherit the operator surface (D-E fallback). +--! +--! Equality and range both route through eql_v2.ord: ord(a) ord(b) +--! is the corresponding operator on eql_v2.ore_block_u64_8_256. ORE on a +--! full-domain int4 is lossless, so the order term is also an exact +--! equality term — there is no separate `hm` term (D#1). +--! +--! All six comparison wrappers are LANGUAGE sql IMMUTABLE STRICT +--! PARALLEL SAFE with no SET clause, so the planner inlines them: +--! `col < $1` becomes `eql_v2.ord(col) < eql_v2.ord($1)`. The inner `<` +--! is the operator on eql_v2.ore_block_u64_8_256, a member of main's +--! DEFAULT btree operator class. A functional index +--! `USING btree (eql_v2.ord(col))` therefore serves all six operators. +--! +--! @note The ORE-block operator class is excluded from the Supabase +--! build variant, so ordered int4 columns have no indexed range on +--! Supabase (seq-scan). See docs/upgrading/v2.4.md U-001. + +--! @brief Index/ORDER BY extractor for the ordered int4 variants. +--! +--! Returns the ORE-block composite carried in the `ob` field of the +--! jsonb payload. The returned eql_v2.ore_block_u64_8_256 type carries +--! main's DEFAULT btree operator class, so a functional index +--! USING btree (eql_v2.ord(col)) binds that opclass automatically. +--! This is the single uniform extractor for index creation and ORDER BY +--! across the ordered variants. +--! +--! @param a eql_v2_int4_ord_ore Ordered encrypted int4 value +--! @return eql_v2.ore_block_u64_8_256 ORE-block index term +--! @throws Exception if the `ob` field is missing from the payload +--! @see eql_v2.ore_block_u64_8_256 +--! @example +--! -- functional index for range + equality +--! CREATE INDEX t_col_idx ON t USING btree (eql_v2.ord(col)); +--! -- ordering +--! SELECT ... FROM t ORDER BY eql_v2.ord(col); +CREATE FUNCTION eql_v2.ord(a eql_v2_int4_ord_ore) +RETURNS eql_v2.ore_block_u64_8_256 +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ore_block_u64_8_256(a::jsonb) $$; + +-- = <> < <= > >= comparison wrappers, 3 arg-shapes each (18 functions). +-- All LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE, no SET clause, so they +-- inline: `col < $1` becomes `eql_v2.ord(col) < eql_v2.ord($1)`. + +--! @brief Less-than wrapper for eql_v2_int4_ord_ore. Inlines to ORE-block compare. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b) $$; + +--! @brief Less-than wrapper for eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b::eql_v2_int4_ord_ore) $$; + +--! @brief Less-than wrapper for eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) < eql_v2.ord(b) $$; + +--! @brief Less-than-or-equal wrapper for eql_v2_int4_ord_ore. Inlines to ORE-block compare. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b) $$; + +--! @brief Less-than-or-equal wrapper for eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b::eql_v2_int4_ord_ore) $$; + +--! @brief Less-than-or-equal wrapper for eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) <= eql_v2.ord(b) $$; + +--! @brief Greater-than wrapper for eql_v2_int4_ord_ore. Inlines to ORE-block compare. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b) $$; + +--! @brief Greater-than wrapper for eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b::eql_v2_int4_ord_ore) $$; + +--! @brief Greater-than wrapper for eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) > eql_v2.ord(b) $$; + +--! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord_ore. Inlines to ORE-block compare. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b) $$; + +--! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b::eql_v2_int4_ord_ore) $$; + +--! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) >= eql_v2.ord(b) $$; + +--! @brief Equality wrapper for eql_v2_int4_ord_ore. Routes through ord — ORE on +--! full-domain int4 is lossless, so this is exact equality. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b) $$; + +--! @brief Equality wrapper for eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b::eql_v2_int4_ord_ore) $$; + +--! @brief Equality wrapper for eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) = eql_v2.ord(b) $$; + +--! @brief Inequality wrapper for eql_v2_int4_ord_ore. Routes through ord. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b) $$; + +--! @brief Inequality wrapper for eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b::eql_v2_int4_ord_ore) $$; + +--! @brief Inequality wrapper for eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) <> eql_v2.ord(b) $$; + +-- ~~, ~~*, @>, <@ (blockers, 3 shapes each) + +--! @brief Blocker for ~~ on eql_v2_int4_ord_ore. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_like(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_like(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~ on eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_like(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_ord_ore. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_ilike(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_ilike(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ~~* on eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_ilike(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~*'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_ord_ore. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contains(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contains(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for @> on eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contains(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '@>'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_ord_ore. +--! @param a eql_v2_int4_ord_ore +--! @param b eql_v2_int4_ord_ore +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contained_by(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '<@'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_ord_ore (domain, jsonb). +--! @param a eql_v2_int4_ord_ore +--! @param b jsonb +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contained_by(a eql_v2_int4_ord_ore, b jsonb) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '<@'); END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for <@ on eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param b eql_v2_int4_ord_ore +--! @return boolean (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contained_by(a jsonb, b eql_v2_int4_ord_ore) +RETURNS boolean IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '<@'); END; $$ +LANGUAGE plpgsql; + +-- -> and ->> (blockers, 3 asymmetric shapes each) + +--! @brief Blocker for -> on eql_v2_int4_ord_ore (domain, text). +--! @param a eql_v2_int4_ord_ore +--! @param selector text +--! @return eql_v2_int4_ord_ore (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow(a eql_v2_int4_ord_ore, selector text) +RETURNS eql_v2_int4_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for -> on eql_v2_int4_ord_ore (domain, integer). +--! @param a eql_v2_int4_ord_ore +--! @param selector integer +--! @return eql_v2_int4_ord_ore (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow(a eql_v2_int4_ord_ore, selector integer) +RETURNS eql_v2_int4_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for -> on eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param selector eql_v2_int4_ord_ore +--! @return eql_v2_int4_ord_ore (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow(a jsonb, selector eql_v2_int4_ord_ore) +RETURNS eql_v2_int4_ord_ore IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_ord_ore (domain, text). +--! @param a eql_v2_int4_ord_ore +--! @param selector text +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow_text(a eql_v2_int4_ord_ore, selector text) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_ord_ore (domain, integer). +--! @param a eql_v2_int4_ord_ore +--! @param selector integer +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow_text(a eql_v2_int4_ord_ore, selector integer) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord_ore'; END; $$ +LANGUAGE plpgsql; + +--! @brief Blocker for ->> on eql_v2_int4_ord_ore (jsonb, domain). +--! @param a jsonb +--! @param selector eql_v2_int4_ord_ore +--! @return text (never returns; always raises) +CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow_text(a jsonb, selector eql_v2_int4_ord_ore) +RETURNS text IMMUTABLE PARALLEL SAFE +AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord_ore'; END; $$ +LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_ord_ore_operators.sql b/src/encrypted_domain/int4/int4_ord_ore_operators.sql new file mode 100644 index 00000000..af193925 --- /dev/null +++ b/src/encrypted_domain/int4/int4_ord_ore_operators.sql @@ -0,0 +1,131 @@ +-- REQUIRE: src/schema.sql +-- REQUIRE: src/encrypted_domain/types.sql +-- REQUIRE: src/encrypted_domain/int4/int4_ord_ore_functions.sql + +--! @file encrypted_domain/int4/int4_ord_ore_operators.sql +--! @brief Concrete ordered int4 variant — operator declarations +--! (equality + ORE-block ordering). +--! +--! eql_v2_int4_ord_ore carries `c`, `ob`. It is the scheme-explicit +--! ordered domain: it carries the eql_v2.ord() extractor, the six +--! comparison wrappers, the operator declarations, and the blockers. +--! eql_v2_int4_ord — the recommended ordered name — is a separate +--! concrete domain (int4_ord.sql) carrying its own copy of this +--! operator surface; the §8 spike showed a domain-over-domain alias +--! does not transparently inherit the operator surface (D-E fallback). + +-- Operator declarations + +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +CREATE OPERATOR = ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, + NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); + +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); +CREATE OPERATOR <> ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, + NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); +CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR <= ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); +CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR > ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); +CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR >= ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); +CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_like, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_like, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); +CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_like, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ore_ilike, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ore_ilike, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); +CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ore_ilike, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contains, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contains, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); +CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contains, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contained_by, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contained_by, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); +CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contained_by, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = text); +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = integer); +CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); + +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow_text, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = text); +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow_text, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = integer); +CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow_text, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); diff --git a/src/encrypted_domain/types.sql b/src/encrypted_domain/types.sql new file mode 100644 index 00000000..0b5ef9b3 --- /dev/null +++ b/src/encrypted_domain/types.sql @@ -0,0 +1,81 @@ +-- REQUIRE: src/schema.sql + +--! @file encrypted_domain/types.sql +--! @brief High-level encrypted domain types: the eql_v2_int4 variant family. +--! +--! Four jsonb-backed domains in public, one per operator/index-term +--! capability: +--! eql_v2_int4 — storage only (all operators blocked); carries `c` +--! eql_v2_int4_eq — HMAC equality (=, <>); carries `c`, `hm` +--! eql_v2_int4_ord_ore — equality + ORE-block ordering (= <> < <= > >=); +--! carries `c`, `ob`; the scheme-explicit ordered +--! domain +--! eql_v2_int4_ord — equality + ORE-block ordering; the recommended +--! ordered name. A full concrete domain with its own +--! operators/wrappers/blockers (int4_ord.sql) — +--! identical operator surface to eql_v2_int4_ord_ore. +--! +--! These domains intentionally live in the public schema, matching the +--! existing lifecycle used by public.eql_v2_encrypted in +--! encrypted/types.sql: user table columns depend on stable public type +--! names, while implementation functions and operators live in eql_v2. +--! tasks/uninstall.sql drops eql_v2 but leaves public types in place. +--! +--! eql_v2_int4_ord is a concrete domain over jsonb (not a domain over +--! eql_v2_int4_ord_ore): the §8 verification spike showed that a +--! domain-over-domain does not transparently inherit the base domain's +--! operator surface — PostgreSQL resolves operators against the ultimate +--! base type (jsonb), so the ordered operators fall through to native +--! jsonb comparison and the blockers do not engage. eql_v2_int4_ord +--! therefore carries its own operator surface (int4_ord.sql). +--! +--! Ordered range and equality both engage a functional btree +--! USING btree (eql_v2.ord(col)) — eql_v2.ord returns +--! eql_v2.ore_block_u64_8_256, which carries main's DEFAULT btree +--! operator class. No operator class is defined on these domains. + +DO $$ +BEGIN + --! @brief Storage-only encrypted int4 domain (jsonb-backed). Every + --! operator is a blocker; carries ciphertext (`c`) only. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'eql_v2_int4' AND typnamespace = 'public'::regnamespace + ) THEN + CREATE DOMAIN public.eql_v2_int4 AS jsonb; + END IF; + + --! @brief Equality-only encrypted int4 domain (jsonb-backed). + --! Supports = and <> via HMAC-256; carries `c`, `hm`. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'eql_v2_int4_eq' AND typnamespace = 'public'::regnamespace + ) THEN + CREATE DOMAIN public.eql_v2_int4_eq AS jsonb; + END IF; + + --! @brief Scheme-explicit ordered encrypted int4 domain (jsonb-backed). + --! Supports = <> < <= > >= via the ORE-block term; carries + --! `c`, `ob`. Carries the eql_v2.ord extractor, the comparison + --! wrappers, the operator declarations, and the blockers. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'eql_v2_int4_ord_ore' AND typnamespace = 'public'::regnamespace + ) THEN + CREATE DOMAIN public.eql_v2_int4_ord_ore AS jsonb; + END IF; + + --! @brief Ordered encrypted int4 domain — the recommended ordered + --! name. A full concrete domain (its own operators/wrappers/ + --! blockers in int4_ord.sql) because the pure-alias form does + --! not transparently inherit the operator surface (spike §8). + --! Supports = <> < <= > >= via the ORE-block term; carries + --! `c`, `ob`. + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'eql_v2_int4_ord' AND typnamespace = 'public'::regnamespace + ) THEN + CREATE DOMAIN public.eql_v2_int4_ord AS jsonb; + END IF; +END +$$; diff --git a/tasks/build.sh b/tasks/build.sh index 0768dd34..39d657b6 100755 --- a/tasks/build.sh +++ b/tasks/build.sh @@ -38,6 +38,8 @@ rm -f release/cipherstash-encrypt-supabase.sql rm -f release/cipherstash-encrypt-protect.sql rm -f release/cipherstash-encrypt-protect-uninstall.sql +# dbdev/eql--0.0.0.sql is appended with >> below; truncate it here so +# repeated builds do not concatenate onto stale content. rm -f dbdev/eql--0.0.0.sql rm -f src/version.sql diff --git a/tasks/pin_search_path.sql b/tasks/pin_search_path.sql index 8369589e..5bbd3be9 100644 --- a/tasks/pin_search_path.sql +++ b/tasks/pin_search_path.sql @@ -241,6 +241,37 @@ BEGIN OR p.proargtypes[0] = (SELECT t.oid FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = 'eql_v2' AND t.typname = 'stevec_query'))) + -- eql_v2_int4 variant family inline-critical wrappers. Name-only + -- match (any arity) covers all three arg-shapes per operator. + -- Blockers are intentionally excluded — they are PL/pgSQL and must + -- NOT inline. + -- + -- The eql_v2_int4_ord_ore comparison wrappers (_eq/_neq and the + -- four range wrappers) are LANGUAGE sql and must inline so the + -- planner rewrites `col $1` to `eql_v2.ord(col) + -- eql_v2.ord($1)` and matches the functional btree on + -- eql_v2.ord(col). eql_v2.ord is the index extractor and must also + -- stay unpinned. The eql_v2_int4_eq wrappers must inline to match + -- the functional hmac btree. eql_v2_int4_ord is a concrete domain + -- (D-E fallback) carrying the same wrapper set as + -- eql_v2_int4_ord_ore. See docs/upgrading/v2.4.md U-001. + OR (p.pronargs = 1 AND p.proname = 'ord') + OR p.proname IN ( + 'eql_v2_int4_eq_eq', -- _eq variant equality + 'eql_v2_int4_eq_neq', + 'eql_v2_int4_ord_ore_eq', -- _ord_ore equality (routes through ord) + 'eql_v2_int4_ord_ore_neq', + 'eql_v2_int4_ord_ore_lt', -- _ord_ore range (routes through ord) + 'eql_v2_int4_ord_ore_lte', + 'eql_v2_int4_ord_ore_gt', + 'eql_v2_int4_ord_ore_gte', + 'eql_v2_int4_ord_eq', -- _ord equality (routes through ord) + 'eql_v2_int4_ord_neq', + 'eql_v2_int4_ord_lt', -- _ord range (routes through ord) + 'eql_v2_int4_ord_lte', + 'eql_v2_int4_ord_gt', + 'eql_v2_int4_ord_gte' + ) ); FOR fn_oid IN diff --git a/tasks/test/splinter.sh b/tasks/test/splinter.sh index dae147d6..c4dd461a 100755 --- a/tasks/test/splinter.sh +++ b/tasks/test/splinter.sh @@ -98,6 +98,21 @@ function_search_path_mutable eql_v2 eq_term function XOR-aware equality term ext function_search_path_mutable eql_v2 min function Aggregate (splinter labels these type=function): ALTER AGGREGATE has no SET configuration_parameter syntax, and ALTER ROUTINE/FUNCTION reject aggregates. The aggregate's SFUNC has a pinned search_path. function_search_path_mutable eql_v2 max function Aggregate: same as min. function_search_path_mutable eql_v2 grouped_value function Aggregate: same as min. +function_search_path_mutable eql_v2 ord function eql_v2_int4 ordered-variant index extractor: returns eql_v2.ore_block_u64_8_256 (carrying main DEFAULT btree opclass). Used inside the inlinable comparison wrappers and as the functional-index expression USING btree (eql_v2.ord(col)); must inline. SET search_path would disable SQL function inlining (see PostgreSQL inline_function). Covers both ord overloads (eql_v2_int4_ord_ore, eql_v2_int4_ord). +function_search_path_mutable eql_v2 eql_v2_int4_eq_eq function eql_v2_int4_eq variant equality: HMAC wrapper, inlines to hmac_256(a::jsonb) = hmac_256(b::jsonb) for functional-btree engagement. Three overloads: (domain,domain), (domain,jsonb), (jsonb,domain). +function_search_path_mutable eql_v2 eql_v2_int4_eq_neq function eql_v2_int4_eq variant inequality: same hmac_256 inlining rationale as eql_v2_int4_eq_eq. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_eq function eql_v2_int4_ord_ore equality: inlines to eql_v2.ord(a) = eql_v2.ord(b) for functional-btree engagement on eql_v2.ord(col). Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_neq function eql_v2_int4_ord_ore inequality: same eql_v2.ord inlining rationale as eql_v2_int4_ord_ore_eq. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_lt function eql_v2_int4_ord_ore range: inlines to eql_v2.ord(a) < eql_v2.ord(b) for functional-btree engagement on eql_v2.ord(col). Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_lte function eql_v2_int4_ord_ore range: same eql_v2.ord inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_gt function eql_v2_int4_ord_ore range: same eql_v2.ord inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_gte function eql_v2_int4_ord_ore range: same eql_v2.ord inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_eq function eql_v2_int4_ord equality (D-E fallback concrete domain): inlines to eql_v2.ord(a) = eql_v2.ord(b), same rationale as eql_v2_int4_ord_ore_eq. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_neq function eql_v2_int4_ord inequality (D-E fallback): same eql_v2.ord inlining rationale as eql_v2_int4_ord_eq. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_lt function eql_v2_int4_ord range (D-E fallback): inlines to eql_v2.ord(a) < eql_v2.ord(b), same rationale as eql_v2_int4_ord_ore_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_lte function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord inlining rationale as eql_v2_int4_ord_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_gt function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord inlining rationale as eql_v2_int4_ord_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_gte function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord inlining rationale as eql_v2_int4_ord_lt. Three overloads. ALLOW # Wrap splinter (a single bare SELECT expression) into a subquery we can From 4444d057985460070ac5f770a141fda90d2d87a6 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 13:32:23 +1000 Subject: [PATCH 03/13] test(encrypted_int4): per-variant Rust/SQLx test suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four SQLx suites — one per variant — covering operator surfaces, blocker raises on NULL and on use, HMAC/ORE index-cast contracts, and catalog assertions that the ordered comparison wrappers stay inlinable. Corrects OPE -> ORE-block terminology in the int4 fixture schema, generator, and install migration. --- tasks/fixtures/encrypted_int4_schema.sql | 2 +- tasks/fixtures/generate_encrypted_int4.sh | 9 +- .../009_install_encrypted_int4_fixture.sql | 7 +- tests/sqlx/tests/encrypted_int4_eq_tests.rs | 299 ++++++++++ .../tests/encrypted_int4_ord_ore_tests.rs | 540 ++++++++++++++++++ tests/sqlx/tests/encrypted_int4_ord_tests.rs | 463 +++++++++++++++ tests/sqlx/tests/encrypted_int4_tests.rs | 139 +++++ 7 files changed, 1453 insertions(+), 6 deletions(-) create mode 100644 tests/sqlx/tests/encrypted_int4_eq_tests.rs create mode 100644 tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs create mode 100644 tests/sqlx/tests/encrypted_int4_ord_tests.rs create mode 100644 tests/sqlx/tests/encrypted_int4_tests.rs diff --git a/tasks/fixtures/encrypted_int4_schema.sql b/tasks/fixtures/encrypted_int4_schema.sql index 5d870590..eb3cb3fb 100644 --- a/tasks/fixtures/encrypted_int4_schema.sql +++ b/tasks/fixtures/encrypted_int4_schema.sql @@ -25,6 +25,6 @@ SELECT eql_v2.remove_search_config('bench_int4', 'encrypted_int4', 'ore') WHERE c.data #> '{tables,bench_int4,encrypted_int4,indexes,ore}' IS NOT NULL ); --- unique → HMAC (drives =, <>); ore → OPE bytes (drives <, <=, >, >=). +-- unique → HMAC (drives =, <>); ore → ORE-block terms (drives <, <=, >, >=). SELECT eql_v2.add_search_config('bench_int4', 'encrypted_int4', 'unique', 'int'); SELECT eql_v2.add_search_config('bench_int4', 'encrypted_int4', 'ore', 'int'); diff --git a/tasks/fixtures/generate_encrypted_int4.sh b/tasks/fixtures/generate_encrypted_int4.sh index 5662845a..a7c11dab 100755 --- a/tasks/fixtures/generate_encrypted_int4.sh +++ b/tasks/fixtures/generate_encrypted_int4.sh @@ -54,13 +54,16 @@ done psql "$PROXY_URL" -v ON_ERROR_STOP=1 -f "$INSERT_SQL" >/dev/null echo "==> Dumping $ROW_COUNT rows to $OUTPUT" -cat > "$OUTPUT" <
"$OUTPUT" <<'HEADER' -- AUTO-GENERATED by tasks/fixtures/generate_encrypted_int4.sh -- DO NOT EDIT BY HAND. Re-run the generator to refresh. -- -- Source: 14-value integer set defined inline in the generator. --- Produced via CipherStash Proxy (HMAC + ORE block terms). --- Used by encrypted_int4 domain SQLx fixture tests. +-- Produced via CipherStash Proxy (HMAC + ORE-block terms). +-- Each row carries `c`, `hm`, `ob`. The ordered variants +-- (eql_v2_int4_ord_ore, eql_v2_int4_ord) read `ob` only; the `hm` +-- term is retained because the same payload feeds eql_v2_int4_eq. +-- Used by the encrypted_int4 variant-family SQLx test suites. DROP TABLE IF EXISTS encrypted_int4_plaintext; diff --git a/tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql b/tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql index 7556d9e6..d800e7ff 100644 --- a/tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql +++ b/tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql @@ -2,8 +2,11 @@ -- DO NOT EDIT BY HAND. Re-run the generator to refresh. -- -- Source: 14-value integer set defined inline in the generator. --- Produced via CipherStash Proxy (HMAC + OPE terms). --- Used by encrypted_int4 domain SQLx fixture tests. +-- Produced via CipherStash Proxy (HMAC + ORE-block terms). +-- Each row carries `c`, `hm`, `ob`. The ordered variants +-- (eql_v2_int4_ord_ore, eql_v2_int4_ord) read `ob` only; the `hm` +-- term is retained because the same payload feeds eql_v2_int4_eq. +-- Used by the encrypted_int4 variant-family SQLx test suites. DROP TABLE IF EXISTS encrypted_int4_plaintext; diff --git a/tests/sqlx/tests/encrypted_int4_eq_tests.rs b/tests/sqlx/tests/encrypted_int4_eq_tests.rs new file mode 100644 index 00000000..577c2b15 --- /dev/null +++ b/tests/sqlx/tests/encrypted_int4_eq_tests.rs @@ -0,0 +1,299 @@ +//! Synthetic test suite for `eql_v2_int4_eq` — HMAC equality only. +//! +//! `=` engages the functional btree on +//! `((eql_v2.hmac_256(col::jsonb)))` (EXPLAIN assertion). +//! `<>` is supported semantically but is seq-scan (btree only +//! supports equality, by design). All other operators raise. + +use anyhow::Result; +use sqlx::PgPool; + +fn payload(hm: &str) -> String { + format!(r#"{{"v":2,"i":{{"t":"typed","c":"int_col"}},"c":"ct-{hm}","hm":"{hm}"}}"#) +} + +async fn setup_eq_table( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + hmacs: &[&str], +) -> Result<()> { + sqlx::query( + r#" + CREATE TEMP TABLE typed_int4_eq ( + id integer GENERATED ALWAYS AS IDENTITY, + value eql_v2_int4_eq + ) ON COMMIT DROP; + "#, + ) + .execute(&mut **tx) + .await?; + + for hm in hmacs { + sqlx::query("INSERT INTO typed_int4_eq(value) VALUES ($1::jsonb::eql_v2_int4_eq)") + .bind(payload(hm)) + .execute(&mut **tx) + .await?; + } + Ok(()) +} + +#[sqlx::test] +async fn eq_engages_hmac_btree_for_equality(pool: PgPool) -> Result<()> { + let mut tx = pool.begin().await?; + setup_eq_table(&mut tx, &["aaa", "bbb", "ccc"]).await?; + + sqlx::query( + "CREATE INDEX typed_int4_eq_hmac_idx \ + ON typed_int4_eq ((eql_v2.hmac_256(value::jsonb)))", + ) + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE typed_int4_eq") + .execute(&mut *tx) + .await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let needle = payload("bbb"); + let plan: Vec = sqlx::query_scalar(&format!( + "EXPLAIN SELECT * FROM typed_int4_eq WHERE value = '{}'::jsonb::eql_v2_int4_eq", + needle + )) + .fetch_all(&mut *tx) + .await?; + let plan_text = plan.join("\n"); + assert!( + plan_text.contains("typed_int4_eq_hmac_idx"), + "= must engage the hmac btree; got plan:\n{plan_text}" + ); + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn eq_neq_returns_correct_rows(pool: PgPool) -> Result<()> { + let mut tx = pool.begin().await?; + setup_eq_table(&mut tx, &["aaa", "bbb", "ccc"]).await?; + + let count: i64 = sqlx::query_scalar(&format!( + "SELECT count(*) FROM typed_int4_eq WHERE value = '{}'::jsonb::eql_v2_int4_eq", + payload("bbb") + )) + .fetch_one(&mut *tx) + .await?; + assert_eq!(count, 1, "= must match exactly one row"); + + let count: i64 = sqlx::query_scalar(&format!( + "SELECT count(*) FROM typed_int4_eq WHERE value <> '{}'::jsonb::eql_v2_int4_eq", + payload("bbb") + )) + .fetch_one(&mut *tx) + .await?; + assert_eq!(count, 2, "<> must match the other two rows"); + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn eq_cross_type_shapes_for_equality(pool: PgPool) -> Result<()> { + let mut tx = pool.begin().await?; + setup_eq_table(&mut tx, &["aaa", "bbb"]).await?; + let needle = payload("bbb"); + + for sql in [ + format!( + "SELECT count(*) FROM typed_int4_eq WHERE value = '{}'::jsonb::eql_v2_int4_eq", + needle + ), + format!( + "SELECT count(*) FROM typed_int4_eq WHERE value = '{}'::jsonb", + needle + ), + format!( + "SELECT count(*) FROM typed_int4_eq WHERE '{}'::jsonb = value", + needle + ), + ] { + let count: i64 = sqlx::query_scalar(&sql).fetch_one(&mut *tx).await?; + assert_eq!(count, 1, "= shape must match one row; sql: {sql}"); + } + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn eq_unsupported_operators_raise(pool: PgPool) -> Result<()> { + let sample = payload("aaa"); + let shapes: &[(&str, &str)] = &[ + ("$1::jsonb::eql_v2_int4_eq", "$2::jsonb::eql_v2_int4_eq"), + ("$1::jsonb::eql_v2_int4_eq", "$2::jsonb"), + ("$1::jsonb", "$2::jsonb::eql_v2_int4_eq"), + ]; + for op in ["<", "<=", ">", ">=", "~~", "~~*", "@>", "<@"] { + for (lhs, rhs) in shapes { + let sql = format!("SELECT {lhs} {op} {rhs}"); + let err = sqlx::query(&sql) + .bind(&sample) + .bind(&sample) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_eq {op} must raise: {sql}")) + .to_string(); + let expected = format!("operator {op} is not supported for eql_v2_int4_eq"); + assert!(err.contains(&expected), "unexpected: {sql} → {err}"); + } + } + + for op in ["->", "->>"] { + for sql in [ + format!("SELECT $1::jsonb::eql_v2_int4_eq {op} 'field'::text"), + format!("SELECT $1::jsonb::eql_v2_int4_eq {op} 0::integer"), + format!("SELECT $1::jsonb {op} $1::jsonb::eql_v2_int4_eq"), + ] { + let err = sqlx::query(&sql) + .bind(&sample) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_eq {op} must raise: {sql}")) + .to_string(); + let expected = format!("operator {op} is not supported for eql_v2_int4_eq"); + assert!(err.contains(&expected), "unexpected: {sql} → {err}"); + } + } + Ok(()) +} + +#[sqlx::test] +async fn eq_blocked_operators_raise_on_null_input(pool: PgPool) -> Result<()> { + // A blocker declared STRICT lets PostgreSQL skip the body and return + // NULL on a NULL argument, silently bypassing the + // "operator … is not supported" exception. The blocker contract is + // "always raises" — guard against STRICT regressing back in. + let null: Option<&str> = None; + + let err = sqlx::query("SELECT $1::jsonb::eql_v2_int4_eq < $2::jsonb::eql_v2_int4_eq") + .bind(null) + .bind(null) + .fetch_one(&pool) + .await + .expect_err("eql_v2_int4_eq < must raise on NULL input") + .to_string(); + assert!( + err.contains("operator < is not supported for eql_v2_int4_eq"), + "unexpected error for < on NULL: {err}" + ); + + let err = sqlx::query("SELECT $1::jsonb -> $2::jsonb::eql_v2_int4_eq") + .bind(null) + .bind(null) + .fetch_one(&pool) + .await + .expect_err("eql_v2_int4_eq -> must raise on NULL input") + .to_string(); + assert!( + err.contains("operator -> is not supported for eql_v2_int4_eq"), + "unexpected error for -> on NULL: {err}" + ); + Ok(()) +} + +#[sqlx::test] +async fn eq_hmac_index_recipe_requires_jsonb_cast(pool: PgPool) -> Result<()> { + // The documented _eq index recipe is + // USING btree ((eql_v2.hmac_256(col::jsonb))) + // The ::jsonb cast is REQUIRED, not redundant. `eql_v2.hmac_256` is + // both a function and an index-term type, and an eql_v2_int4_eq + // column has no exact hmac_256 overload — so the bare form + // `eql_v2.hmac_256(col)` parses as a cast to the hmac_256 type + // (col::eql_v2.hmac_256), building an index the `=` predicate never + // matches. This test pins both halves of that contract so the + // docs/reference + v2.4.md U-001 recipe stays honest. + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE eq_idx (plaintext integer, value eql_v2_int4_eq) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO eq_idx(plaintext, value) \ + SELECT plaintext, payload::eql_v2_int4_eq FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE eq_idx").execute(&mut *tx).await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let pivot: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&mut *tx) + .await?; + let lit = pivot.replace('\'', "''"); + let eq_query = format!("SELECT * FROM eq_idx WHERE value = '{lit}'::jsonb::eql_v2_int4_eq"); + + // Footgun half: the bare eql_v2.hmac_256(value) form is a cast to the + // hmac_256 type, not a function call — the index it builds cannot + // serve the = predicate. + sqlx::query("CREATE INDEX eq_idx_bare ON eq_idx USING btree (eql_v2.hmac_256(value))") + .execute(&mut *tx) + .await?; + let bare_plan: Vec = sqlx::query_scalar(&format!("EXPLAIN {eq_query}")) + .fetch_all(&mut *tx) + .await?; + assert!( + !bare_plan.join("\n").contains("eq_idx_bare"), + "bare eql_v2.hmac_256(col) is a cast, not a call — must NOT serve = ; plan:\n{}", + bare_plan.join("\n") + ); + + // Recipe half: the explicit ::jsonb cast resolves the + // hmac_256(jsonb) function, and = engages the index. + sqlx::query("CREATE INDEX eq_idx_hmac ON eq_idx USING btree ((eql_v2.hmac_256(value::jsonb)))") + .execute(&mut *tx) + .await?; + let plan: Vec = sqlx::query_scalar(&format!("EXPLAIN {eq_query}")) + .fetch_all(&mut *tx) + .await?; + assert!( + plan.join("\n").contains("eq_idx_hmac"), + "the documented eql_v2.hmac_256(col::jsonb) recipe must engage for = ; plan:\n{}", + plan.join("\n") + ); + + let ids: Vec = sqlx::query_scalar(&format!( + "SELECT plaintext FROM eq_idx WHERE value = '{lit}'::jsonb::eql_v2_int4_eq" + )) + .fetch_all(&mut *tx) + .await?; + assert_eq!( + ids, + vec![42], + "= via the hmac index must return the matching row" + ); + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn eq_null_operand_yields_null(pool: PgPool) -> Result<()> { + // STRICT equality wrappers: a NULL operand propagates NULL. + let null: Option<&str> = None; + let sample = r#"{"v":2,"i":{"t":"t","c":"c"},"c":"x","hm":"aa"}"#; + for op in ["=", "<>"] { + let result: Option = sqlx::query_scalar(&format!( + "SELECT $1::jsonb::eql_v2_int4_eq {op} $2::jsonb::eql_v2_int4_eq" + )) + .bind(sample) + .bind(null) + .fetch_one(&pool) + .await?; + assert!(result.is_none(), "{op} with NULL operand must yield NULL"); + } + Ok(()) +} diff --git a/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs new file mode 100644 index 00000000..9c3e94b6 --- /dev/null +++ b/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs @@ -0,0 +1,540 @@ +//! Fixture-based test suite for `eql_v2_int4_ord_ore` — the concrete +//! ordered variant (equality + ORE-block ordering). +//! +//! Consumes `tests/sqlx/migrations/009_install_encrypted_int4_fixture.sql` +//! (table `encrypted_int4_plaintext`, column `payload JSONB NOT NULL`). +//! Each row pairs a plaintext integer with its encrypted JSONB payload +//! carrying `c`, `hm`, `ob` terms. +//! +//! Value set: { -100, -1, 1, 2, 5, 10, 17, 25, 42, 50, 100, 250, 1000, 9999 } +//! 14 rows. Range pivots produce distinct cardinalities so swapped +//! operators would fail the assertions, not silently pass. +//! +//! Equality and range both route through `eql_v2.ord`: `col $1` +//! inlines to `eql_v2.ord(col) eql_v2.ord($1)`, the operator on +//! `eql_v2.ore_block_u64_8_256`. A single functional btree +//! `USING btree (eql_v2.ord(col))` serves all six operators — there is +//! no operator class on the domain. `ORDER BY eql_v2.ord(col)` sorts in +//! plaintext numeric order. Equality routes through the `ob` term +//! (lossless ORE on full-domain int4 = exact equality); there is no +//! `hm` term on the ordered variants (D#1). +//! +//! Most tests cast `payload::eql_v2_int4_ord_ore` per-query so the +//! fixture table itself stays JSONB-shaped. + +use anyhow::Result; +use sqlx::PgPool; + +/// Pull plaintext column out of fixture rows whose payload satisfies a +/// predicate. The predicate is the SQL fragment that goes after `WHERE`. +async fn plaintexts_matching(pool: &PgPool, predicate: &str) -> Result> { + let sql = format!( + "SELECT plaintext FROM encrypted_int4_plaintext WHERE {predicate} ORDER BY plaintext" + ); + let mut rows: Vec = sqlx::query_scalar(&sql).fetch_all(pool).await?; + rows.sort(); + Ok(rows) +} + +#[sqlx::test] +async fn encrypted_int4_equality_matches_self(pool: PgPool) -> Result<()> { + // For each fixture plaintext, looking up by `=` against that row's own + // payload must return exactly that plaintext. + for target in [-100, -1, 1, 42, 9999] { + let needle: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = $1", + ) + .bind(target) + .fetch_one(&pool) + .await?; + + let matched: Vec = plaintexts_matching( + &pool, + &format!( + "payload::eql_v2_int4_ord_ore = '{}'::jsonb::eql_v2_int4_ord_ore", + needle.replace('\'', "''") + ), + ) + .await?; + assert_eq!(matched, vec![target], "= against payload of {target}"); + } + + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_equality_cross_type_shapes(pool: PgPool) -> Result<()> { + // = in all three signature shapes against the payload of 42. + let needle: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&pool) + .await?; + let lit = needle.replace('\'', "''"); + + // (domain, domain) + let ids: Vec = plaintexts_matching( + &pool, + &format!("payload::eql_v2_int4_ord_ore = '{lit}'::jsonb::eql_v2_int4_ord_ore"), + ) + .await?; + assert_eq!(ids, vec![42], "(domain, domain) ="); + + // (domain, jsonb) + let ids: Vec = plaintexts_matching( + &pool, + &format!("payload::eql_v2_int4_ord_ore = '{lit}'::jsonb"), + ) + .await?; + assert_eq!(ids, vec![42], "(domain, jsonb) ="); + + // (jsonb, domain) — ORM bind shape + let ids: Vec = plaintexts_matching( + &pool, + &format!("'{lit}'::jsonb = payload::eql_v2_int4_ord_ore"), + ) + .await?; + assert_eq!(ids, vec![42], "(jsonb, domain) ="); + + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_inequality_against_42(pool: PgPool) -> Result<()> { + let needle: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&pool) + .await?; + let lit = needle.replace('\'', "''"); + + let ids: Vec = plaintexts_matching( + &pool, + &format!("payload::eql_v2_int4_ord_ore <> '{lit}'::jsonb::eql_v2_int4_ord_ore"), + ) + .await?; + // 14 rows, exclude 42 → 13 remaining + let mut expected = vec![-100, -1, 1, 2, 5, 10, 17, 25, 50, 100, 250, 1000, 9999]; + expected.sort(); + assert_eq!(ids, expected, "<> against 42 should exclude only 42"); + + // Reverse shape sweep + let ids: Vec = plaintexts_matching( + &pool, + &format!("'{lit}'::jsonb <> payload::eql_v2_int4_ord_ore"), + ) + .await?; + assert_eq!(ids, expected, "reverse-shape <> against 42"); + + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_range_operators_match_numeric_semantics(pool: PgPool) -> Result<()> { + // Pivot value 10. Numeric ground truth for each range operator: + // < 10 → { -100, -1, 1, 2, 5 } (5 rows) + // <= 10 → { -100, -1, 1, 2, 5, 10 } (6 rows) + // > 10 → { 17, 25, 42, 50, 100, 250, 1000, 9999 } (8 rows) + // >= 10 → { 10, 17, 25, 42, 50, 100, 250, 1000, 9999 } (9 rows) + let pivot: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 10", + ) + .fetch_one(&pool) + .await?; + let lit = pivot.replace('\'', "''"); + + let cases: &[(&str, Vec)] = &[ + ("<", vec![-100, -1, 1, 2, 5]), + ("<=", vec![-100, -1, 1, 2, 5, 10]), + (">", vec![17, 25, 42, 50, 100, 250, 1000, 9999]), + (">=", vec![10, 17, 25, 42, 50, 100, 250, 1000, 9999]), + ]; + + for (op, expected) in cases { + let mut expected_sorted = expected.clone(); + expected_sorted.sort(); + + // Forward shapes — value on the LHS. + for rhs in ["'{LIT}'::jsonb::eql_v2_int4_ord_ore", "'{LIT}'::jsonb"] { + let rhs_sql = rhs.replace("{LIT}", &lit); + let predicate = format!("payload::eql_v2_int4_ord_ore {op} {rhs_sql}"); + let ids = plaintexts_matching(&pool, &predicate).await?; + assert_eq!( + ids, expected_sorted, + "forward {op} with rhs {rhs}: predicate={predicate}" + ); + } + + // Reverse shape — pivot on the LHS inverts the expected set. + // Forward `value < 10` → rows where value < 10 + // Reverse `10 < value` → rows where value > 10 → "opposite" op's set + let reverse_expected: Vec = match *op { + "<" => vec![17, 25, 42, 50, 100, 250, 1000, 9999], // > + "<=" => vec![10, 17, 25, 42, 50, 100, 250, 1000, 9999], // >= + ">" => vec![-100, -1, 1, 2, 5], // < + ">=" => vec![-100, -1, 1, 2, 5, 10], // <= + _ => unreachable!(), + }; + let mut reverse_sorted = reverse_expected.clone(); + reverse_sorted.sort(); + let predicate = format!("'{lit}'::jsonb {op} payload::eql_v2_int4_ord_ore"); + let ids = plaintexts_matching(&pool, &predicate).await?; + assert_eq!( + ids, reverse_sorted, + "reverse {op} (pivot {op} value): predicate={predicate}" + ); + } + + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_ore_ordering_matches_numeric_ordering(pool: PgPool) -> Result<()> { + // Critical invariant: ORE bytes from Proxy must preserve numeric order. + // Pulling all 14 rows ordered by eql_v2.ord — the uniform ordered-int4 + // index/ORDER BY extractor — must yield the plaintext sequence in + // ascending numeric order. A bug in Proxy's ORE-block encoding (sign + // handling, byte-order, padding) would fail this without throwing. + // + // ORDER BY eql_v2.ord(payload::eql_v2_int4_ord_ore) pins the sort to + // the ORE-block term; sorting the domain column directly would follow + // native jsonb comparison, not ORE order. + let ordered: Vec = sqlx::query_scalar( + r#" + SELECT plaintext + FROM encrypted_int4_plaintext + ORDER BY eql_v2.ord(payload::eql_v2_int4_ord_ore) + "#, + ) + .fetch_all(&pool) + .await?; + + let expected = vec![-100, -1, 1, 2, 5, 10, 17, 25, 42, 50, 100, 250, 1000, 9999]; + assert_eq!( + ordered, expected, + "eql_v2.ord ordering must match numeric ordering of plaintext" + ); + + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_ord_distinctness_sweep(pool: PgPool) -> Result<()> { + // Pairwise: no two distinct integer plaintexts share an ORE term. + // Equality routes through eql_v2.ord (the `ob` term), not HMAC — + // 14 distinct ints → 14 distinct ORE terms → no `=` collisions. + let collisions: i64 = sqlx::query_scalar( + r#" + SELECT count(*) + FROM encrypted_int4_plaintext a + JOIN encrypted_int4_plaintext b ON a.id < b.id + WHERE a.payload::eql_v2_int4_ord_ore = b.payload::eql_v2_int4_ord_ore + "#, + ) + .fetch_one(&pool) + .await?; + assert_eq!( + collisions, 0, + "no two distinct integer plaintexts may share an ORE term" + ); + + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_ord_ore_functional_index_serves_range_and_equality( + pool: PgPool, +) -> Result<()> { + // Range + equality on eql_v2_int4_ord_ore are served by one + // functional btree USING btree (eql_v2.ord(col)). eql_v2.ord + // returns eql_v2.ore_block_u64_8_256, which carries main's DEFAULT + // btree operator class — no opclass annotation needed. + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE ord_ore_fi (\ + plaintext integer, \ + value eql_v2_int4_ord_ore\ + ) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_ore_fi(plaintext, value) \ + SELECT plaintext, payload::eql_v2_int4_ord_ore FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + sqlx::query("CREATE INDEX ord_ore_fi_idx ON ord_ore_fi USING btree (eql_v2.ord(value))") + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE ord_ore_fi").execute(&mut *tx).await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let pivot: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 10", + ) + .fetch_one(&mut *tx) + .await?; + let lit = pivot.replace('\'', "''"); + + // Engagement: =, <, <=, >, >= each engage the functional btree. + for op in ["=", "<", "<=", ">", ">="] { + let plan: Vec = sqlx::query_scalar(&format!( + "EXPLAIN SELECT * FROM ord_ore_fi \ + WHERE value {op} '{lit}'::jsonb::eql_v2_int4_ord_ore" + )) + .fetch_all(&mut *tx) + .await?; + let plan_text = plan.join("\n"); + assert!( + plan_text.contains("ord_ore_fi_idx"), + "{op} must engage the eql_v2.ord functional btree; plan:\n{plan_text}" + ); + } + + // Correctness via the index: numeric ground truth against pivot 10. + let cases: &[(&str, Vec)] = &[ + ("=", vec![10]), + ("<", vec![-100, -1, 1, 2, 5]), + ("<=", vec![-100, -1, 1, 2, 5, 10]), + (">", vec![17, 25, 42, 50, 100, 250, 1000, 9999]), + (">=", vec![10, 17, 25, 42, 50, 100, 250, 1000, 9999]), + ]; + for (op, expected) in cases { + let mut ids: Vec = sqlx::query_scalar(&format!( + "SELECT plaintext FROM ord_ore_fi \ + WHERE value {op} '{lit}'::jsonb::eql_v2_int4_ord_ore" + )) + .fetch_all(&mut *tx) + .await?; + ids.sort(); + let mut want = expected.clone(); + want.sort(); + assert_eq!( + ids, want, + "{op} via functional index must match ground truth" + ); + } + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_ord_ore_unsupported_operators_raise(pool: PgPool) -> Result<()> { + // The _ord_ore variant supports equality + ORE range. Every other + // operator must raise the variant-specific blocker error rather than + // fall through to native jsonb semantics. + // + // We use the fixture payload of 42 (any row would work) cast to the + // domain to exercise the (domain, domain) shape, then sweep the other + // two declared shapes. + let payload: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&pool) + .await?; + let lit = payload.replace('\'', "''"); + + let shapes: &[(&str, &str)] = &[ + ( + "$1::jsonb::eql_v2_int4_ord_ore", + "$2::jsonb::eql_v2_int4_ord_ore", + ), + ("$1::jsonb::eql_v2_int4_ord_ore", "$2::jsonb"), + ("$1::jsonb", "$2::jsonb::eql_v2_int4_ord_ore"), + ]; + + for op in ["~~", "~~*", "@>", "<@"] { + for (lhs, rhs) in shapes { + let sql = format!("SELECT {lhs} {op} {rhs}"); + let err = sqlx::query(&sql) + .bind(&payload) + .bind(&payload) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_ord_ore {op} must raise: {sql}")) + .to_string(); + let expected = format!("operator {op} is not supported for eql_v2_int4_ord_ore"); + assert!( + err.contains(&expected), + "blocker error mismatch: {sql} -> {err}" + ); + } + } + + // Path operators across all three asymmetric shapes. + for op in ["->", "->>"] { + for sql in [ + format!( + "SELECT '{}'::jsonb::eql_v2_int4_ord_ore {op} 'field'::text", + lit + ), + format!( + "SELECT '{}'::jsonb::eql_v2_int4_ord_ore {op} 0::integer", + lit + ), + format!( + "SELECT '{}'::jsonb {op} '{}'::jsonb::eql_v2_int4_ord_ore", + lit, lit + ), + ] { + let err = sqlx::query(&sql) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_ord_ore {op} must raise: {sql}")) + .to_string(); + let expected = format!("operator {op} is not supported for eql_v2_int4_ord_ore"); + assert!( + err.contains(&expected), + "path-op blocker error mismatch: {sql} -> {err}" + ); + } + } + + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_ord_ore_blocked_operators_raise_on_null_input(pool: PgPool) -> Result<()> { + // A blocker declared STRICT lets PostgreSQL skip the body and return + // NULL on a NULL argument, silently bypassing the + // "operator … is not supported" exception. The blocker contract is + // "always raises" — guard against STRICT regressing back in. + let null: Option<&str> = None; + + let err = + sqlx::query("SELECT $1::jsonb::eql_v2_int4_ord_ore ~~ $2::jsonb::eql_v2_int4_ord_ore") + .bind(null) + .bind(null) + .fetch_one(&pool) + .await + .expect_err("eql_v2_int4_ord_ore ~~ must raise on NULL input") + .to_string(); + assert!( + err.contains("operator ~~ is not supported for eql_v2_int4_ord_ore"), + "unexpected error for ~~ on NULL: {err}" + ); + + let err = sqlx::query("SELECT $1::jsonb -> $2::jsonb::eql_v2_int4_ord_ore") + .bind(null) + .bind(null) + .fetch_one(&pool) + .await + .expect_err("eql_v2_int4_ord_ore -> must raise on NULL input") + .to_string(); + assert!( + err.contains("operator -> is not supported for eql_v2_int4_ord_ore"), + "unexpected error for -> on NULL: {err}" + ); + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_ord_ore_null_operand_yields_null(pool: PgPool) -> Result<()> { + // STRICT comparison wrappers: a NULL operand propagates NULL + // (standard SQL three-valued logic), not an error and not a match. + let payload: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&pool) + .await?; + let null: Option<&str> = None; + + for op in ["=", "<>", "<", "<=", ">", ">="] { + let result: Option = sqlx::query_scalar(&format!( + "SELECT $1::jsonb::eql_v2_int4_ord_ore {op} $2::jsonb::eql_v2_int4_ord_ore" + )) + .bind(&payload) + .bind(null) + .fetch_one(&pool) + .await?; + assert!( + result.is_none(), + "{op} with a NULL operand must yield NULL, got {result:?}" + ); + } + Ok(()) +} + +#[sqlx::test] +async fn encrypted_int4_ord_ore_equality_uses_ob_not_hm(pool: PgPool) -> Result<()> { + // D#1: ordered variants carry c + ob and drop hm. Equality routes + // through eql_v2.ord (the `ob` term), never HMAC. Strip `hm` from + // every payload: with no hm present, an accidental regression to + // HMAC equality fails instead of silently passing on the fixture. + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE ord_ore_no_hm (\ + plaintext integer, value eql_v2_int4_ord_ore\ + ) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_ore_no_hm(plaintext, value) \ + SELECT plaintext, (payload - 'hm')::eql_v2_int4_ord_ore \ + FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + // Sanity: no row carries `hm` (jsonb_exists is the function form of + // the `?` key-exists operator — avoids `?` in the SQLx query string). + let with_hm: i64 = sqlx::query_scalar( + "SELECT count(*) FROM ord_ore_no_hm WHERE jsonb_exists(value::jsonb, 'hm')", + ) + .fetch_one(&mut *tx) + .await?; + assert_eq!(with_hm, 0, "test rows must not carry hm"); + + sqlx::query("CREATE INDEX ord_ore_no_hm_idx ON ord_ore_no_hm USING btree (eql_v2.ord(value))") + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE ord_ore_no_hm") + .execute(&mut *tx) + .await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let pivot: String = sqlx::query_scalar( + "SELECT (payload - 'hm')::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&mut *tx) + .await?; + let lit = pivot.replace('\'', "''"); + + // Equality + inequality return correct rows with no hm present. + let eq: Vec = sqlx::query_scalar(&format!( + "SELECT plaintext FROM ord_ore_no_hm \ + WHERE value = '{lit}'::jsonb::eql_v2_int4_ord_ore" + )) + .fetch_all(&mut *tx) + .await?; + assert_eq!(eq, vec![42], "= must match via ob with no hm present"); + + let neq_count: i64 = sqlx::query_scalar(&format!( + "SELECT count(*) FROM ord_ore_no_hm \ + WHERE value <> '{lit}'::jsonb::eql_v2_int4_ord_ore" + )) + .fetch_one(&mut *tx) + .await?; + assert_eq!(neq_count, 13, "<> must match the other 13 rows"); + + // The functional btree still engages for equality with no hm. + let plan: Vec = sqlx::query_scalar(&format!( + "EXPLAIN SELECT * FROM ord_ore_no_hm \ + WHERE value = '{lit}'::jsonb::eql_v2_int4_ord_ore" + )) + .fetch_all(&mut *tx) + .await?; + assert!( + plan.join("\n").contains("ord_ore_no_hm_idx"), + "= must engage the eql_v2.ord functional btree with no hm present" + ); + + tx.commit().await?; + Ok(()) +} diff --git a/tests/sqlx/tests/encrypted_int4_ord_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_tests.rs new file mode 100644 index 00000000..c7f0010a --- /dev/null +++ b/tests/sqlx/tests/encrypted_int4_ord_tests.rs @@ -0,0 +1,463 @@ +//! End-to-end test suite for `eql_v2_int4_ord` — the recommended +//! ordered domain name. +//! +//! eql_v2_int4_ord is a concrete ordered domain with its own operators +//! (D-E fallback): the §8 verification spike showed a domain-over-domain +//! alias does not transparently inherit the operator surface. This suite +//! asserts eql_v2_int4_ord behaves correctly on a real column typed +//! eql_v2_int4_ord — operator routing to EQL ORE semantics, blocked +//! operators raising rather than falling through to native jsonb, and +//! functional-index engagement. eql_v2_int4_ord_ore (the scheme-explicit +//! domain) carries the identical operator surface. + +use std::path::PathBuf; + +use anyhow::Result; +use sqlx::PgPool; + +#[sqlx::test] +async fn ord_six_operators_resolve_to_ore_semantics(pool: PgPool) -> Result<()> { + // On a column typed eql_v2_int4_ord, every operator must resolve to + // EQL ORE semantics (numeric ground truth), not native jsonb + // comparison. Pivot is the payload of plaintext 10. + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE ord_t (plaintext integer, value eql_v2_int4_ord) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_t(plaintext, value) \ + SELECT plaintext, payload::eql_v2_int4_ord FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + + let pivot: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 10", + ) + .fetch_one(&mut *tx) + .await?; + let lit = pivot.replace('\'', "''"); + + let cases: &[(&str, Vec)] = &[ + ("=", vec![10]), + ( + "<>", + vec![-100, -1, 1, 2, 5, 17, 25, 42, 50, 100, 250, 1000, 9999], + ), + ("<", vec![-100, -1, 1, 2, 5]), + ("<=", vec![-100, -1, 1, 2, 5, 10]), + (">", vec![17, 25, 42, 50, 100, 250, 1000, 9999]), + (">=", vec![10, 17, 25, 42, 50, 100, 250, 1000, 9999]), + ]; + for (op, expected) in cases { + let mut ids: Vec = sqlx::query_scalar(&format!( + "SELECT plaintext FROM ord_t \ + WHERE value {op} '{lit}'::jsonb::eql_v2_int4_ord" + )) + .fetch_all(&mut *tx) + .await?; + ids.sort(); + let mut want = expected.clone(); + want.sort(); + assert_eq!( + ids, want, + "{op} on eql_v2_int4_ord must match ORE ground truth" + ); + } + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn ord_blocked_operators_raise(pool: PgPool) -> Result<()> { + // Blocked operators on eql_v2_int4_ord must raise, never fall through + // to native jsonb @>/<@/->/->>. The error names the concrete domain + // the blocker is defined on; assert only "is not supported" + the + // operator symbol so the test is robust to the exact type name. + let payload: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&pool) + .await?; + + let shapes: &[(&str, &str)] = &[ + ("$1::jsonb::eql_v2_int4_ord", "$2::jsonb::eql_v2_int4_ord"), + ("$1::jsonb::eql_v2_int4_ord", "$2::jsonb"), + ("$1::jsonb", "$2::jsonb::eql_v2_int4_ord"), + ]; + for op in ["~~", "~~*", "@>", "<@"] { + for (lhs, rhs) in shapes { + let sql = format!("SELECT {lhs} {op} {rhs}"); + let err = sqlx::query(&sql) + .bind(&payload) + .bind(&payload) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_ord {op} must raise: {sql}")) + .to_string(); + assert!( + err.contains("is not supported") && err.contains(op), + "blocked {op} must raise 'not supported': {sql} -> {err}" + ); + } + } + + let lit = payload.replace('\'', "''"); + for op in ["->", "->>"] { + for sql in [ + format!("SELECT '{lit}'::jsonb::eql_v2_int4_ord {op} 'field'::text"), + format!("SELECT '{lit}'::jsonb::eql_v2_int4_ord {op} 0::integer"), + format!("SELECT '{lit}'::jsonb {op} '{lit}'::jsonb::eql_v2_int4_ord"), + ] { + let err = sqlx::query(&sql) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_ord {op} must raise: {sql}")) + .to_string(); + assert!( + err.contains("is not supported") && err.contains(op), + "blocked {op} must raise 'not supported': {sql} -> {err}" + ); + } + } + Ok(()) +} + +#[sqlx::test] +async fn ord_functional_index_serves_range_and_equality(pool: PgPool) -> Result<()> { + // Range + equality on eql_v2_int4_ord are served by one functional + // btree USING btree (eql_v2.ord(col)). + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE ord_fi (plaintext integer, value eql_v2_int4_ord) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_fi(plaintext, value) \ + SELECT plaintext, payload::eql_v2_int4_ord FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + sqlx::query("CREATE INDEX ord_fi_idx ON ord_fi USING btree (eql_v2.ord(value))") + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE ord_fi").execute(&mut *tx).await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let pivot: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 10", + ) + .fetch_one(&mut *tx) + .await?; + let lit = pivot.replace('\'', "''"); + + for op in ["=", "<", "<=", ">", ">="] { + let plan: Vec = sqlx::query_scalar(&format!( + "EXPLAIN SELECT * FROM ord_fi WHERE value {op} '{lit}'::jsonb::eql_v2_int4_ord" + )) + .fetch_all(&mut *tx) + .await?; + let plan_text = plan.join("\n"); + assert!( + plan_text.contains("ord_fi_idx"), + "{op} must engage the eql_v2.ord functional btree; plan:\n{plan_text}" + ); + } + + let cases: &[(&str, Vec)] = &[ + ("=", vec![10]), + ("<", vec![-100, -1, 1, 2, 5]), + ("<=", vec![-100, -1, 1, 2, 5, 10]), + (">", vec![17, 25, 42, 50, 100, 250, 1000, 9999]), + (">=", vec![10, 17, 25, 42, 50, 100, 250, 1000, 9999]), + ]; + for (op, expected) in cases { + let mut ids: Vec = sqlx::query_scalar(&format!( + "SELECT plaintext FROM ord_fi WHERE value {op} '{lit}'::jsonb::eql_v2_int4_ord" + )) + .fetch_all(&mut *tx) + .await?; + ids.sort(); + let mut want = expected.clone(); + want.sort(); + assert_eq!( + ids, want, + "{op} via functional index must match ground truth" + ); + } + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn ord_order_by_preserves_numeric_order(pool: PgPool) -> Result<()> { + // ORDER BY eql_v2.ord(col) sorts an eql_v2_int4_ord column in + // plaintext numeric order. + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE ord_sort (plaintext integer, value eql_v2_int4_ord) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_sort(plaintext, value) \ + SELECT plaintext, payload::eql_v2_int4_ord FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + let ordered: Vec = + sqlx::query_scalar("SELECT plaintext FROM ord_sort ORDER BY eql_v2.ord(value)") + .fetch_all(&mut *tx) + .await?; + assert_eq!( + ordered, + vec![-100, -1, 1, 2, 5, 10, 17, 25, 42, 50, 100, 250, 1000, 9999], + "ORDER BY eql_v2.ord(value) must yield plaintext numeric order" + ); + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn ord_null_operand_yields_null(pool: PgPool) -> Result<()> { + // STRICT comparison wrappers: a NULL operand propagates NULL + // (standard SQL three-valued logic), not an error and not a match. + let payload: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&pool) + .await?; + let null: Option<&str> = None; + + for op in ["=", "<>", "<", "<=", ">", ">="] { + let result: Option = sqlx::query_scalar(&format!( + "SELECT $1::jsonb::eql_v2_int4_ord {op} $2::jsonb::eql_v2_int4_ord" + )) + .bind(&payload) + .bind(null) + .fetch_one(&pool) + .await?; + assert!( + result.is_none(), + "{op} with a NULL operand must yield NULL, got {result:?}" + ); + } + Ok(()) +} + +#[sqlx::test] +async fn ord_equality_independent_of_hm(pool: PgPool) -> Result<()> { + // D#1: ordered variants carry c + ob and drop hm. Equality on + // eql_v2_int4_ord routes through eql_v2.ord (the `ob` term), never + // HMAC. Strip `hm` so an accidental regression to HMAC equality + // fails instead of passing on the hm-carrying fixture. + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE ord_no_hm (plaintext integer, value eql_v2_int4_ord) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_no_hm(plaintext, value) \ + SELECT plaintext, (payload - 'hm')::eql_v2_int4_ord FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + // Sanity: no row carries `hm` (jsonb_exists is the function form of + // the `?` key-exists operator — avoids `?` in the SQLx query string). + let with_hm: i64 = + sqlx::query_scalar("SELECT count(*) FROM ord_no_hm WHERE jsonb_exists(value::jsonb, 'hm')") + .fetch_one(&mut *tx) + .await?; + assert_eq!(with_hm, 0, "test rows must not carry hm"); + + sqlx::query("CREATE INDEX ord_no_hm_idx ON ord_no_hm USING btree (eql_v2.ord(value))") + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE ord_no_hm").execute(&mut *tx).await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let pivot: String = sqlx::query_scalar( + "SELECT (payload - 'hm')::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&mut *tx) + .await?; + let lit = pivot.replace('\'', "''"); + + let eq: Vec = sqlx::query_scalar(&format!( + "SELECT plaintext FROM ord_no_hm WHERE value = '{lit}'::jsonb::eql_v2_int4_ord" + )) + .fetch_all(&mut *tx) + .await?; + assert_eq!(eq, vec![42], "= must match via ob with no hm present"); + + let neq_count: i64 = sqlx::query_scalar(&format!( + "SELECT count(*) FROM ord_no_hm WHERE value <> '{lit}'::jsonb::eql_v2_int4_ord" + )) + .fetch_one(&mut *tx) + .await?; + assert_eq!(neq_count, 13, "<> must match the other 13 rows"); + + let plan: Vec = sqlx::query_scalar(&format!( + "EXPLAIN SELECT * FROM ord_no_hm WHERE value = '{lit}'::jsonb::eql_v2_int4_ord" + )) + .fetch_all(&mut *tx) + .await?; + assert!( + plan.join("\n").contains("ord_no_hm_idx"), + "= must engage the eql_v2.ord functional btree with no hm present" + ); + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn ord_ore_wrappers_are_inlinable(pool: PgPool) -> Result<()> { + // The comparison wrappers on eql_v2_int4_ord_ore and eql_v2_int4_ord + // must be LANGUAGE sql, IMMUTABLE, and carry no pinned search_path, + // so the planner inlines `col < $1` to + // `eql_v2.ord(col) < eql_v2.ord($1)` and the functional btree on + // eql_v2.ord(col) engages. A pinned proconfig or a plpgsql body + // would break the inline chain. + let rows: Vec<(String, String, String, Option>)> = sqlx::query_as( + r#" + SELECT p.proname, + l.lanname, + p.provolatile::text, + p.proconfig + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + JOIN pg_catalog.pg_language l ON l.oid = p.prolang + WHERE n.nspname = 'eql_v2' + AND p.proname IN ( + 'eql_v2_int4_ord_ore_eq', 'eql_v2_int4_ord_ore_neq', + 'eql_v2_int4_ord_ore_lt', 'eql_v2_int4_ord_ore_lte', + 'eql_v2_int4_ord_ore_gt', 'eql_v2_int4_ord_ore_gte', + 'eql_v2_int4_ord_eq', 'eql_v2_int4_ord_neq', + 'eql_v2_int4_ord_lt', 'eql_v2_int4_ord_lte', + 'eql_v2_int4_ord_gt', 'eql_v2_int4_ord_gte' + ) + "#, + ) + .fetch_all(&pool) + .await?; + + // 12 wrapper names (6 on _ord_ore, 6 on the concrete _ord domain) + // × 3 arg-shapes = 36 rows. + assert_eq!( + rows.len(), + 36, + "expected 36 ordered comparison wrapper overloads" + ); + for (name, lang, volatile, config) in &rows { + assert_eq!(lang, "sql", "{name} must be LANGUAGE sql to inline"); + assert_eq!(volatile, "i", "{name} must be IMMUTABLE"); + assert!( + config.is_none(), + "{name} must have no pinned search_path (proconfig)" + ); + } + + // eql_v2.ord must be IMMUTABLE (functional-index requirement) in + // every spike outcome. The spike (Task 2) fixed its LANGUAGE as sql, + // so a LANGUAGE sql eql_v2.ord must additionally have no proconfig + // (it must inline); a LANGUAGE plpgsql ord is exempt from that check. + let ord: Vec<(String, String, Option>)> = sqlx::query_as( + r#" + SELECT l.lanname, p.provolatile::text, p.proconfig + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + JOIN pg_catalog.pg_language l ON l.oid = p.prolang + WHERE n.nspname = 'eql_v2' AND p.proname = 'ord' + "#, + ) + .fetch_all(&pool) + .await?; + assert!(!ord.is_empty(), "eql_v2.ord must exist"); + for (lang, volatile, config) in &ord { + assert_eq!(volatile, "i", "eql_v2.ord must be IMMUTABLE"); + if lang == "sql" { + assert!( + config.is_none(), + "a LANGUAGE sql eql_v2.ord must have no pinned search_path so it inlines" + ); + } + } + Ok(()) +} + +/// Structural-sync guard for the two ordered int4 domain file pairs. +/// +/// The `_ord_ore` variant (scheme-explicit) and the `_ord` variant (the +/// D-E fallback concrete domain) are deliberate twins: the same +/// `eql_v2.ord` extractor, the 18 comparison wrappers, the blockers, and +/// the operator declarations, differing only by the +/// `eql_v2_int4_ord_ore` <-> `eql_v2_int4_ord` type-name swap. A full +/// de-duplication refactor is out of scope for this branch, so this test +/// pins the invariant cheaply: after normalising both type names to a +/// common token, the executable body of each file (from the first +/// declaration onward — the file-header doc comments are intentionally +/// different and excluded) must be byte-identical between the twins. An +/// edit to one file that is not mirrored into the other fails here. +/// +/// The split keeps comparison/path functions and operator declarations +/// in separate `_functions.sql` / `_operators.sql` files, so both pairs +/// are checked: `int4_ord_functions.sql` <-> `int4_ord_ore_functions.sql` +/// and `int4_ord_operators.sql` <-> `int4_ord_ore_operators.sql`. +/// +/// This is a source-only test; it does not touch the database. +#[test] +fn ordered_int4_domain_files_stay_in_sync() { + fn body(rel: &str, marker: &str) -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../src/encrypted_domain/int4") + .join(rel); + let text = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e)); + // The header doc-comments differ by design; compare only the + // executable body, which starts at the given marker. + let start = text + .find(marker) + .unwrap_or_else(|| panic!("{} is missing the marker {:?}", path.display(), marker)); + // Normalise the two domain type names to one token. Replace the + // longer name first so `eql_v2_int4_ord` does not partially match + // inside `eql_v2_int4_ord_ore`. + text[start..] + .replace("eql_v2_int4_ord_ore", "ORDTYPE") + .replace("eql_v2_int4_ord", "ORDTYPE") + } + + // Functions: executable body starts at the eql_v2.ord extractor. + assert_eq!( + body( + "int4_ord_ore_functions.sql", + "--! @brief Index/ORDER BY extractor" + ), + body( + "int4_ord_functions.sql", + "--! @brief Index/ORDER BY extractor" + ), + "int4_ord_ore_functions.sql and int4_ord_functions.sql have \ + drifted apart. They must stay mechanical twins (type-name swap \ + only) below the file header; mirror every change into both files." + ); + + // Operators: executable body starts at the operator declarations. + assert_eq!( + body("int4_ord_ore_operators.sql", "-- Operator declarations"), + body("int4_ord_operators.sql", "-- Operator declarations"), + "int4_ord_ore_operators.sql and int4_ord_operators.sql have \ + drifted apart. They must stay mechanical twins (type-name swap \ + only) below the file header; mirror every change into both files." + ); +} diff --git a/tests/sqlx/tests/encrypted_int4_tests.rs b/tests/sqlx/tests/encrypted_int4_tests.rs new file mode 100644 index 00000000..c4c6cdeb --- /dev/null +++ b/tests/sqlx/tests/encrypted_int4_tests.rs @@ -0,0 +1,139 @@ +//! Synthetic test suite for `eql_v2_int4` — the storage-only variant. +//! +//! Every operator is a blocker that raises +//! `operator X is not supported for eql_v2_int4`. No fixture data is +//! needed; operator-on-literals is sufficient. + +use anyhow::Result; +use sqlx::PgPool; + +const SAMPLE_PAYLOAD: &str = r#"{"v":2,"i":{"t":"t","c":"c"},"c":"sample"}"#; + +#[sqlx::test] +async fn all_symmetric_operators_raise(pool: PgPool) -> Result<()> { + let shapes: &[(&str, &str)] = &[ + ("$1::jsonb::eql_v2_int4", "$2::jsonb::eql_v2_int4"), + ("$1::jsonb::eql_v2_int4", "$2::jsonb"), + ("$1::jsonb", "$2::jsonb::eql_v2_int4"), + ]; + + for op in ["=", "<>", "<", "<=", ">", ">=", "~~", "~~*", "@>", "<@"] { + for (lhs, rhs) in shapes { + let sql = format!("SELECT {lhs} {op} {rhs}"); + let err = sqlx::query(&sql) + .bind(SAMPLE_PAYLOAD) + .bind(SAMPLE_PAYLOAD) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4 {op} must raise: {sql}")) + .to_string(); + let expected = format!("operator {op} is not supported for eql_v2_int4"); + assert!( + err.contains(&expected), + "unexpected error for {sql}: got {err}, want {expected}" + ); + } + } + Ok(()) +} + +#[sqlx::test] +async fn path_operators_raise(pool: PgPool) -> Result<()> { + for op in ["->", "->>"] { + for sql in [ + format!("SELECT $1::jsonb::eql_v2_int4 {op} 'field'::text"), + format!("SELECT $1::jsonb::eql_v2_int4 {op} 0::integer"), + format!("SELECT $1::jsonb {op} $1::jsonb::eql_v2_int4"), + ] { + let err = sqlx::query(&sql) + .bind(SAMPLE_PAYLOAD) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4 {op} must raise: {sql}")) + .to_string(); + let expected = format!("operator {op} is not supported for eql_v2_int4"); + assert!(err.contains(&expected), "unexpected error for {sql}: {err}"); + } + } + Ok(()) +} + +#[sqlx::test] +async fn blockers_raise_on_typed_column(pool: PgPool) -> Result<()> { + // The other tests exercise blockers on cast literals + // ($1::jsonb::eql_v2_int4). This pins that the blockers also engage + // when the operand is a genuine eql_v2_int4-typed table column, the + // shape a real caller writes (`WHERE col = col`). + let mut tx = pool.begin().await?; + sqlx::query( + r#" + CREATE TEMP TABLE typed_int4 ( + id integer GENERATED ALWAYS AS IDENTITY, + value eql_v2_int4 + ) ON COMMIT DROP; + "#, + ) + .execute(&mut *tx) + .await?; + sqlx::query("INSERT INTO typed_int4(value) VALUES ($1::jsonb::eql_v2_int4)") + .bind(SAMPLE_PAYLOAD) + .execute(&mut *tx) + .await?; + + for op in ["=", "<>", "<", "<=", ">", ">=", "~~", "~~*", "@>", "<@"] { + // A raised blocker aborts the transaction; wrap each probe in a + // savepoint so the next operator can be checked after rollback. + sqlx::query("SAVEPOINT op_probe").execute(&mut *tx).await?; + let sql = format!("SELECT * FROM typed_int4 WHERE value {op} value"); + let err = sqlx::query(&sql) + .fetch_all(&mut *tx) + .await + .expect_err(&format!("eql_v2_int4 column {op} must raise: {sql}")) + .to_string(); + let expected = format!("operator {op} is not supported for eql_v2_int4"); + assert!( + err.contains(&expected), + "unexpected error for {sql}: got {err}, want {expected}" + ); + sqlx::query("ROLLBACK TO SAVEPOINT op_probe") + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn blocked_operators_raise_on_null_input(pool: PgPool) -> Result<()> { + // A blocker declared STRICT lets PostgreSQL skip the body and return + // NULL on a NULL argument, silently bypassing the + // "operator … is not supported" exception. The blocker contract is + // "always raises" — guard against STRICT regressing back in. + let null: Option<&str> = None; + + let err = sqlx::query("SELECT $1::jsonb::eql_v2_int4 = $2::jsonb::eql_v2_int4") + .bind(null) + .bind(null) + .fetch_one(&pool) + .await + .expect_err("eql_v2_int4 = must raise on NULL input") + .to_string(); + assert!( + err.contains("operator = is not supported for eql_v2_int4"), + "unexpected error for = on NULL: {err}" + ); + + let err = sqlx::query("SELECT $1::jsonb -> $2::jsonb::eql_v2_int4") + .bind(null) + .bind(null) + .fetch_one(&pool) + .await + .expect_err("eql_v2_int4 -> must raise on NULL input") + .to_string(); + assert!( + err.contains("operator -> is not supported for eql_v2_int4"), + "unexpected error for -> on NULL: {err}" + ); + Ok(()) +} From f1992d087859602a33bac63bc6ad63fcd8b58880 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 13:32:23 +1000 Subject: [PATCH 04/13] docs(encrypted_int4): reference, walkthrough, changelog, and upgrade notes Adds the eql_v2_int4 variant-family reference and walkthrough under docs/reference/, the v2.4 upgrade guide (U-001), and the CHANGELOG.md entry under [Unreleased] targeting 2.4.0. --- CHANGELOG.md | 10 + .../encrypted-domain-implementation-spec.md | 552 ++++++++++++++++++ 2 files changed, 562 insertions(+) create mode 100644 docs/reference/encrypted-domain-implementation-spec.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 24663d93..6004fe62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,16 @@ Each entry that ships in a published release links to the PR that introduced it. ## [Unreleased] +The additive `eql_v2_int4` variant family targets `2.4.0`; see [`docs/upgrading/v2.4.md`](docs/upgrading/v2.4.md) for its upgrade notes. + +### Added + +- **`eql_v2_int4` variant family — four capability-encoded domain types for encrypted `int4` columns.** Pick the variant whose operator surface matches the index terms your column carries: `eql_v2_int4` (storage only, every operator blocked — carries `c`), `eql_v2_int4_eq` (HMAC equality only — `=`, `<>` — carries `c`, `hm`), `eql_v2_int4_ord_ore` (equality + ORE-block ordering — `=`, `<>`, `<`, `<=`, `>`, `>=` — carries `c`, `ob`), or `eql_v2_int4_ord` (the recommended ordered name; the identical operator surface to `eql_v2_int4_ord_ore`). Ordered columns expose a single uniform index extractor, `eql_v2.ord(col)` — equality and range share one functional btree, `CREATE INDEX ... USING btree (eql_v2.ord(col))`, and `ORDER BY eql_v2.ord(col)` sorts in plaintext order. `eql_v2.ord` returns the internal `eql_v2.ore_block_u64_8_256` composite, which carries EQL's existing `DEFAULT` btree operator class, so no operator class is defined on the public domain types. The ordered variants do not carry an `hm` term: ORE on a full-domain `int4` is lossless, so the order term doubles as an exact equality term. All variants live in `public` and survive `eql_v2` uninstall. Per-variant payload requirements and index recipes: [U-001](docs/upgrading/v2.4.md#u-001-eql_v2_int4-variant-family). Note: the ORE operator class is excluded from the Supabase build, so ordered `int4` columns fall back to seq-scan for range on Supabase. ([#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) + +### Upgrade notes + +For the `2.4.0`-targeted `eql_v2_int4` variant family, see [`docs/upgrading/v2.4.md`](docs/upgrading/v2.4.md): [U-001 — `eql_v2_int4` variant family](docs/upgrading/v2.4.md#u-001-eql_v2_int4-variant-family). + ## [2.3.0] — 2026-05-20 `2.3.0` is a breaking release. Customers re-encrypt their data as part of the upgrade — the crypto-side counterpart (`@cipherstash/protect` / `protect-ffi` / proxy) emits a new ste_vec element shape. See [`docs/upgrading/v2.3.md`](docs/upgrading/v2.3.md) for the consolidated upgrade notes. diff --git a/docs/reference/encrypted-domain-implementation-spec.md b/docs/reference/encrypted-domain-implementation-spec.md new file mode 100644 index 00000000..1a0142b0 --- /dev/null +++ b/docs/reference/encrypted-domain-implementation-spec.md @@ -0,0 +1,552 @@ +# Encrypted domain type — implementation spec + +A consolidated, type-agnostic spec and checklist for implementing an +encrypted-domain type family in EQL. The pattern is the one established +by `eql_v2_int4` (PR #225); this document generalises it so the same +mechanics can be applied to `int8`, `bool`, `date`, `float`/`double`, +`numeric`, `timestamp`, and `jsonb`. + +**Audience:** contributors adding a new encrypted-domain type. +**Reference implementation:** `src/encrypted_domain/int4/`, +`tests/sqlx/tests/encrypted_int4*`. + +--- + +## 1. The model + +An encrypted-domain type is a **family of jsonb-backed PostgreSQL +domains**, one domain per operator/index-term **capability**. The +capability is encoded in the domain name: + +| Domain name | Capability | Capability terms | +|------------------------|-------------------------------------|------------------| +| `eql_v2_` | storage only — every operator raises | `c` | +| `eql_v2__eq` | equality (`=`, `<>`) | `c`, `hm` | +| `eql_v2__ord` | equality + ordering (`=` `<>` `<` `<=` `>` `>=`) | `c`, `ob` | +| `eql_v2__ord_` | as `_ord`, scheme-explicit name | `c`, `ob` | + +A caller picks the domain whose capability matches the searches they +need; an unmatched operator **raises** rather than silently falling +through to native `jsonb` behaviour. + +Every domain also carries the EQL envelope keys (`v`, `i`) in addition +to the capability terms above, and **each domain enforces a `CHECK` +constraint** requiring the envelope plus its capability terms — a +malformed payload is rejected at the point it is cast into the domain +(§3). + +### What is fixed vs. what each type decides + +**Fixed by this spec (the mechanics):** how domains are declared, the +inlinability rules, the operator surface and arg-shapes, the +no-opclass-on-domains rule, the test structure, the fixture format, and +the coverage bar. Sections 4–10 below. + +**Decided per type (its own design step):** which variants the family +has, the payload terms each variant carries, and the index/equality +scheme. These depend on the type's encryption scheme and are **not** +mechanical. Examples of decisions the int4 family made that do **not** +transfer automatically: + +- int4's ordered variants carry `c`, `ob` and **drop `hm`** because ORE + on a full-domain `int4` is lossless — the order term doubles as an + exact equality term. A type whose ORE is **lossy**, or whose domain is + not fully covered (candidates: `float`/`double`, `numeric`), must keep + `hm` on its ordered variants and route `=`/`<>` through `eq_term`, not + `ord_term`. +- int4 ships two ordered domains (`_ord` and `_ord_ore`) as mechanical + twins. A type with a single ordering scheme needs only `_ord`. +- `jsonb` does not fit the scalar storage/eq/ord shape; see §11. + +Resolve these before writing code, and record them in a short +type-specific design note. Everything else follows the checklist. + +--- + +## 2. Implementation checklist + +Work top to bottom. Each item links to its reference section. + +### Design (per type — resolve first) +- [ ] Choose the variant set and each variant's payload terms (§1). +- [ ] Confirm whether ORE is lossless for this type — decides whether + ordered variants carry `hm` and where `=`/`<>` route (§1, §4). +- [ ] Pick the index-term type(s) the extractors will return — they + must already carry a default operator class (§4). + +### Types +- [ ] Declare every domain in `src/encrypted_domain/types.sql` as an + idempotent `CREATE DOMAIN public. AS jsonb` with a `CHECK` + constraint enforcing the envelope (`v`, `i`) plus the variant's + capability terms (§3). + +### Per variant — functions, then operators +- [ ] `src/encrypted_domain//__functions.sql`: the + extractor (eq/ord variants), the inlinable comparison wrappers for + supported operators, and the blockers for every unsupported + operator (§5, §6). +- [ ] `src/encrypted_domain//__operators.sql`: a + `CREATE OPERATOR` for every operator × arg-shape (§6). +- [ ] Add `-- REQUIRE:` headers to every file (§9). + +### Wiring +- [ ] Allowlist every inlinable function in `tasks/pin_search_path.sql` + and `tasks/test/splinter.sh` (§5). +- [ ] `mise run clean && mise run build` — clean first, a bare build can + leave stale `release/*.sql` (§9). +- [ ] Confirm the Supabase and Protect build variants still build (§9). + +### Tests & fixtures +- [ ] Fixture generator `tasks/fixtures/generate_encrypted_.sh` and + its generated migration + `tests/sqlx/migrations/0NN_install_encrypted__fixture.sql` (§8). +- [ ] One SQLx suite per variant, + `tests/sqlx/tests/encrypted_[_variant]_tests.rs` (§7). +- [ ] A twin-sync `#[test]` if any variant is a mechanical twin (§7). +- [ ] Meet the coverage bar in §10. +- [ ] `mise run test` green on PostgreSQL 14–17. + +### Documentation +- [ ] Reference page, walkthrough, `CHANGELOG.md` `[Unreleased]` entry, + and a `docs/upgrading/v.md` upgrade note (§9). + +--- + +## 3. Reference — Type definitions + +- **One domain per capability**, all `AS jsonb`, all in the **`public`** + schema. Public placement matches `public.eql_v2_encrypted`: user table + columns depend on stable public type names, while implementation + functions and operators live in `eql_v2`. `tasks/uninstall.sql` drops + `eql_v2` but leaves the public domains in place. +- **Declare idempotently.** All domains for a type go in + `src/encrypted_domain/types.sql`, inside one `DO $$ … $$` block: + + ```sql + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_type + WHERE typname = 'eql_v2_' AND typnamespace = 'public'::regnamespace + ) THEN + CREATE DOMAIN public.eql_v2_ AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' + ); + END IF; + -- … one IF NOT EXISTS block per variant … + END + $$; + ``` + +- **Every domain carries a `CHECK` constraint.** The payload must be a + `jsonb` object carrying the EQL envelope (`v`, `i`), the ciphertext + (`c`), **and every capability term the variant relies on** — `hm` for + an `_eq` variant, `ob` for an `_ord` variant. The constraint is + enforced when a value is cast into the domain, so a malformed or + under-populated payload is rejected at write time rather than failing + obscurely inside an extractor later. The storage variant requires only + `v`, `i`, `c`; each capability variant adds its term: + + ```sql + CREATE DOMAIN public.eql_v2__eq AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' + ); + ``` + +- **Every domain is a concrete domain over `jsonb`.** Do **not** declare + one domain as a domain over another (`CREATE DOMAIN a AS b`). The + int4 verification spike showed PostgreSQL resolves operators against + the *ultimate base type* (`jsonb`), so a domain-over-domain does not + inherit the base domain's operator surface — ordered operators fall + through to native `jsonb` comparison and blockers do not engage. Two + domains with the same capability (e.g. `_ord` and `_ord_ore`) are each + a separate concrete domain over `jsonb` carrying their own operator + surface; keep them in sync with a twin-sync test (§7). +- **Payload terms** are a per-variant assumption, documented in each + file's `--! @file` header (e.g. *"Payload-term assumption: `c`, `hm`."*). + +--- + +## 4. Reference — Operator classes + +**Do not create an operator class on a domain type.** An opclass on a +public domain is a footgun and bloats the index — it stores the whole +`jsonb` payload rather than the compact index term. + +Instead, index through a **functional index on an extractor function**: + +- The extractor (`eq_term` / `ord_term`) returns an **internal + index-term type that already carries a default operator class**. The + exact return type is per-extractor — what matters is that it has a + default opclass for the access method you need: + - `eql_v2.ord_term(col)` returns `eql_v2.ore_block_u64_8_256`, which + carries `main`'s `DEFAULT FOR TYPE … USING btree` operator class. + `CREATE INDEX … USING btree (eql_v2.ord_term(col))` binds it + automatically — no opclass annotation. + - The `eq_term` overload on a scalar variant (e.g. + `eql_v2.eq_term(eql_v2__eq)`) returns `eql_v2.hmac_256` (a domain + over `text`); the `eq_term` overload on a `ste_vec` entry returns + `bytea`. Both `text` and `bytea` have default btree/hash opclasses, + so `USING hash` or `USING btree (eql_v2.eq_term(col))` engages + equality either way. Pick the return type to match an existing + opclass — do not invent one. +- A type implementer therefore creates **no operator class at all**. The + extractor is the bridge: pick a return type that already has the + opclass you need. + +**Build caveat:** the internal ORE composite operator class is excluded +from the **Supabase** build variant, so ordered columns have **no +indexed range on Supabase** (seq-scan). Note this in the upgrade doc. + +--- + +## 5. Reference — Inlinable function constraints + +The functional index only engages on a bare `WHERE col $1` if the +comparison wrapper **inlines** so the planner can fold +`col $1` into `extractor(col) extractor($1)` and match it +against the stored index expression. This splits every variant's +functions into two strictly-separated classes. + +### Inlinable: extractors and comparison wrappers + +Applies to `eq_term` / `ord_term` and every supported-operator wrapper. + +```sql +CREATE FUNCTION eql_v2.(…) +RETURNS +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT … $$; +``` + +Hard requirements — all four are needed for PostgreSQL to inline: + +- `LANGUAGE sql` — PL/pgSQL is never inlined. +- A **single-statement** `SELECT` body. +- `IMMUTABLE` — also required for use as a functional-index expression. +- **No `SET` clause** (no pinned `search_path` / `proconfig`). A pinned + `search_path` disables SQL-function inlining. + +Also `STRICT` and `PARALLEL SAFE`. `STRICT` gives standard three-valued +logic: `col NULL` yields `NULL`. + +Wrapper bodies are one-liners over the extractor: + +```sql +-- (domain, domain) +AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b) $$; +-- (domain, jsonb) — cast the jsonb operand to the domain +AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b::eql_v2__ord) $$; +-- (jsonb, domain) +AS $$ SELECT eql_v2.ord_term(a::eql_v2__ord) < eql_v2.ord_term(b) $$; +``` + +The extractor itself reads its term from the payload, e.g. +`SELECT eql_v2.ore_block_u64_8_256(a::jsonb)`. + +### Blockers: unsupported operators + +Every operator a variant does **not** support gets a blocker that always +raises. + +```sql +CREATE FUNCTION eql_v2.__(a …, b …) +RETURNS boolean +IMMUTABLE PARALLEL SAFE -- NOTE: not STRICT +AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2__', ''); END; $$ +LANGUAGE plpgsql; +``` + +- `LANGUAGE plpgsql`, `IMMUTABLE PARALLEL SAFE`. +- **Never `STRICT`.** A `STRICT` blocker lets PostgreSQL skip the body + and return `NULL` on a `NULL` argument, silently bypassing the + exception. The blocker contract is *always raises* — there is an + explicit regression test for this (§10). +- Boolean blockers delegate to the shared helper + `eql_v2.encrypted_domain_unsupported_bool(type_name, operator_name)` + (`src/encrypted_domain/functions.sql`) for a uniform message: + `operator is not supported for `. +- Path-operator blockers (`->`, `->>`) return non-boolean types, so they + cannot use the boolean helper — they `RAISE EXCEPTION` inline with the + same message text. + +The shared helper is itself `plpgsql`, `IMMUTABLE PARALLEL SAFE`, and +**does** carry `SET search_path = pg_catalog, extensions, public` — it +is never on an inline-critical path, so pinning is correct there. + +### Allowlist wiring (mandatory) + +Every **inlinable** function must be allowlisted, or the build/lint +tooling will break inlining or fail: + +- `tasks/pin_search_path.sql` — otherwise the function gets a pinned + `search_path`, which disables inlining. Add the extractor (matched by + `pronargs = 1 AND proname = ''`) and every wrapper name. +- `tasks/test/splinter.sh` — otherwise the linter flags + `function_search_path_mutable`. Add one row per inlinable function + with a short rationale. + +> **Caveat — overload coverage.** A name-only allowlist clause covers +> every overload of that name; an existing clause scoped by argument +> type (e.g. an `eq_term` clause matched by `proargtypes[0]`) does +> **not** cover a new overload on a different domain. If you reuse an +> extractor name (`eq_term`, `ord_term`) that another module already +> allowlists, confirm the existing clause actually matches your new +> overload — add a fresh `pronargs`/name clause if it does not. + +Blockers and the shared helper are **not** allowlisted — they carry a +pinned `search_path` like ordinary EQL functions. + +--- + +## 6. Reference — Operators + +### The operator surface + +Every variant declares the **full** 12-operator surface. Supported +operators route to an inlinable wrapper; all others route to a blocker. +Declaring the full surface is what prevents fall-through to native +`jsonb`. + +| Operators | Kind | Arg-shapes | +|-----------|------|-----------| +| `=` `<>` `<` `<=` `>` `>=` `~~` `~~*` `@>` `<@` | symmetric boolean (10) | `(domain,domain)`, `(domain,jsonb)`, `(jsonb,domain)` | +| `->` `->>` | path (2) | `(domain,text)`, `(domain,integer)`, `(jsonb,domain)` | + +The `(*,jsonb)` / `(jsonb,*)` shapes cover ORM bind patterns where one +operand arrives as raw `jsonb`. That is **12 operators × 3 shapes = 36 +`CREATE OPERATOR` statements per variant.** + +### Function counts per variant (reference: int4) + +| Variant | Extractor | Wrappers | Blockers | Functions | Operators | +|---------|-----------|----------|----------|-----------|-----------| +| storage `eql_v2_` | 0 | 0 | 36 | 36 | 36 | +| `eql_v2__eq` | 1 | 6 | 30 | 37 | 36 | +| `eql_v2__ord[_ore]` | 1 | 18 | 18 | 37 | 36 | + +(Wrappers/blockers = supported/unsupported operators × 3 shapes; the +storage variant supports nothing.) + +### `CREATE OPERATOR` metadata + +```sql +CREATE OPERATOR = ( + FUNCTION = eql_v2.__eq, + LEFTARG = eql_v2__, RIGHTARG = eql_v2__, + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel +); +``` + +- Supported operators carry full metadata: `COMMUTATOR`, `NEGATOR`, and + selectivity estimators — `eqsel`/`neqsel` for `=`/`<>`, + `scalarltsel`/`scalarlesel`/`scalargtsel`/`scalargesel` for the range + operators (with matching `*joinsel`). `COMMUTATOR` lets the planner + normalise `$1 < col` to `col > $1`; `NEGATOR` drives `NOT (…)`. +- Blockers carry minimal metadata. The wrappers inline to the index-term + comparison *before* index matching, so this metadata is for + plan-quality completeness, not index engagement. + +### File split + +Per `CLAUDE.md`: implementation in `_functions.sql`, operator +declarations in `_operators.sql`. One pair per variant: +`__functions.sql` and `__operators.sql`. + +--- + +## 7. Reference — Test structure + +- **One SQLx suite per variant**: + `tests/sqlx/tests/encrypted_[_variant]_tests.rs`. +- **Storage variant — synthetic.** No migration fixture is needed; cast + literals (`$1::jsonb::eql_v2_`), a `TEMP TABLE` for the typed-column + case, and payload-`CHECK` probes are sufficient. +- **eq / ord variants — fixture-based** (§8). Tests cast + `payload::eql_v2__` per query. +- Each case is an `async fn` under `#[sqlx::test]`. Source-only checks + (e.g. the twin-sync guard) use a plain `#[test]`. +- **Twin-sync guard.** When two variants are mechanical twins (same + surface, type-name swap only — e.g. `_ord` / `_ord_ore`), add a + `#[test]` that reads both `.sql` files, normalises the two type names + to a common token, and asserts the executable bodies are + byte-identical. This pins the duplication cheaply without a + de-duplication refactor. + +### Test categories (each variant covers the applicable ones) + +| Category | Applies to | +|----------|-----------| +| Supported operators return correct rows, all 3 shapes | eq, ord | +| Range operators match numeric/native semantics | ord | +| `ORDER BY extractor(col)` preserves plaintext order | ord | +| Functional index **engages** (`EXPLAIN` names the index, with `SET LOCAL enable_seqscan = off`) | eq, ord | +| Functional index **correctness** (rows via index = ground truth) | eq, ord | +| Constant-on-left / commuted shape engages the index | eq, ord | +| Index preferred at scale (no `enable_seqscan` override) | eq, ord | +| Unsupported operators raise the variant-specific error, all shapes | all | +| Blockers raise on `NULL` input (guards against `STRICT` regressing in) | all | +| Supported wrappers yield `NULL` on a `NULL` operand | eq, ord | +| Inlinability catalogue assertion (see §10) | eq, ord | +| Operator planner-metadata assertion (`COMMUTATOR`/`NEGATOR` present) | eq, ord | +| Blockers engage on a real typed column, not just cast literals | storage (+ others) | +| Domain `CHECK` rejects malformed / under-populated payloads at the cast | all | +| Twin-sync source check | twinned variants | + +--- + +## 8. Reference — Fixtures + +- **Generated, not hand-written.** Fixture generation uses three files + under `tasks/fixtures/`: + - `_generate_common.sh` — shared, **sourced** (not run) helper: + resolves the Postgres/Proxy connection, and exposes + `restart_proxy_and_wait` and `dump_fixture_table`. Reuse it as-is. + - `encrypted__schema.sql` — per-type schema: creates the + `bench_` source table with an `eql_v2_encrypted` column and + registers the index terms with `eql_v2.add_search_config(...)` so + Proxy emits the terms the variants need (e.g. `unique` → HMAC, + `ore` → ORE-block). Written to be idempotent. + - `generate_encrypted_.sh` — the per-type generator: applies the + schema, restarts Proxy, inserts plaintext rows, and dumps the + encrypted rows into the migration. +- The generator produces the migration + `tests/sqlx/migrations/0NN_install_encrypted__fixture.sql`, carrying + an `AUTO-GENERATED … DO NOT EDIT BY HAND` header. +- Encrypted payloads are produced via **CipherStash Proxy** (real HMAC + and ORE terms), not synthesised. +- **Table shape:** + + ```sql + CREATE TABLE encrypted__plaintext ( + id BIGINT PRIMARY KEY, + plaintext NOT NULL, + payload JSONB NOT NULL + ); + ``` + +- **One payload, all terms.** Each `payload` carries every term the + family uses (`c`, `hm`, `ob`) so a single fixture feeds every + variant's suite — the ordered suites read `ob`, the equality suite + reads `hm`, from the same rows. +- **Value-set design rules:** + - Choose pivots so each range operator yields a **distinct + cardinality** — a swapped operator then fails an assertion instead + of silently passing. + - Include negative values and boundary values where the type allows. + - All values distinct, so a distinctness sweep proves no two + plaintexts share an index term. + - int4 uses 14 values; size similarly. + +--- + +## 9. Reference — Build wiring & documentation + +### Build + +- Every `.sql` file declares its dependencies with `-- REQUIRE:` lines; + the build resolves order with `tsort`. Required edges for a variant: + `src/schema.sql`, `src/encrypted_domain/types.sql`, the shared + `src/encrypted_domain/functions.sql` (for blockers), the variant's own + `_functions.sql` (from its `_operators.sql`), and **the module that + defines the extractor's return type** (e.g. + `src/ore_block_u64_8_256/functions.sql` and `…/operators.sql`, or + `src/hmac_256/functions.sql`). +- Build with `mise run clean && mise run build` — clean first; a bare + `mise run build` can report sources up-to-date and leave stale + `release/*.sql`. +- Confirm the **Supabase** and **Protect** build variants still build. + +### Documentation + +A new type is user-facing, so per `CLAUDE.md` release discipline: + +- A reference page and a walkthrough under `docs/reference/`. +- A `## [Unreleased]` entry in `CHANGELOG.md` (`Added`). +- A numbered upgrade note (`U-NNN`) in the active + `docs/upgrading/v.md` — variant set, the extractor interface, + index recipes, and the Supabase seq-scan caveat for ordered columns. +- All SQL functions/types need Doxygen `--!` comments (`@brief`, + `@param`, `@return`, …) per `CLAUDE.md`. + +--- + +## 10. Reference — Coverage expectations + +The bar a new type's test suites must clear: + +- **Full operator surface.** Every declared operator × every arg-shape + is exercised — supported ops asserted for correctness, blocked ops + asserted to raise the exact `operator is not supported for + ` message. +- **Index engagement *and* correctness.** For every index-served + operator, assert both that `EXPLAIN` names the functional index (under + `SET LOCAL enable_seqscan = off`) **and** that the rows returned match + numeric/native ground truth. Cover the commuted (constant-on-left) + shape too. +- **NULL handled both ways.** Blockers must raise on `NULL` input + (catches a `STRICT` regression); supported wrappers must yield `NULL` + on a `NULL` operand (three-valued logic). +- **Inlinability asserted structurally.** Query `pg_catalog.pg_proc`: + every wrapper and a `LANGUAGE sql` extractor must have + `lanname = 'sql'`, `provolatile = 'i'`, and `proconfig IS NULL`. Do + not assume inlining — assert it. +- **Negative space.** Test the absence of capability: unsupported + operators raise; and where a term is dropped by design (e.g. ordered + variants without `hm`), strip that term from the payload and prove + the variant still routes correctly — so an accidental regression to + the wrong term fails instead of passing on a fully-populated fixture. +- **Payload validation.** Assert the domain `CHECK` rejects malformed + payloads — a non-object, and an object missing the envelope (`v`, + `i`), the ciphertext (`c`), or the variant's capability term — with a + `violates check constraint` error at the cast. +- **Real columns, not just literals.** At least one test per variant + runs operators against a genuine `eql_v2__`-typed table + column, the shape a real caller writes. +- **Twin drift.** Twinned variants are pinned byte-identical by a + source-only test. + +--- + +## 11. Appendix — `jsonb` + +`jsonb` uses the same family model but its capabilities differ from a +scalar type, so the variant set and which operators are *supported* +(vs. blocked) change: + +| Domain | Supported operators | Index term / extractor | +|--------------------------|--------------------------------|------------------------| +| `eql_v2_jsonb` | none — storage only | `c` | +| `eql_v2_jsonb_eq` | `=`, `<>` | `hm` via `eq_term` | +| `eql_v2_jsonb_ste_vec` | `@>`, `<@`, `->`, `->>`, and path-scoped `=`/ordering | ste_vec terms | + +Key divergences from the scalar template — the §3–§10 mechanics +otherwise hold unchanged: + +- **The operator surface inverts.** For scalar types `@>`, `<@`, `->`, + `->>` are always blockers. For the `jsonb` containment/ste_vec variant + they are *supported* — `@>`/`<@` are real containment queries and + `->`/`->>` are real path navigation. +- **Path operators return a sub-domain, not a scalar.** `col -> 'sel'` + yields an encrypted value that is itself searchable; the chained + recipe is `WHERE col -> 'sel' = $1` and an `ORDER BY` over an + ordering extractor on the selected entry. The extractors take a + selector, mirroring the existing ste_vec entry extractors. +- **The index term is ste_vec-shaped**, reusing the `eql_v2_encrypted` + ste_vec machinery rather than a single scalar ORE/HMAC term. + +> **Design intent, not current API.** This appendix describes the +> *target* shape, not shipped code. As of writing, `src/ste_vec/` +> exposes `eql_v2.eq_term(eql_v2.ste_vec_entry)` returning `bytea` and +> `eql_v2.ore_cllw(...)` for ordering — there is **no** +> `ord_term(ste_vec_entry)` overload, and no `eql_v2_jsonb` domain +> family exists yet. The existing `public.eql_v2_encrypted` type +> already covers general encrypted `jsonb`; an `eql_v2_jsonb` domain +> family would be the capability-scoped, fall-through-safe presentation +> of the same underlying scheme. Settle the exact extractor surface and +> the relationship to `eql_v2_encrypted` in the `jsonb` type's design +> note before implementing. From d7e02caf49ce4bb78f2252069323ddfc0c51cc59 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 14:31:00 +1000 Subject: [PATCH 05/13] feat(encrypted_int4): eq_term / ord_term extractor interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the eql_v2_int4_eq index recipe's eql_v2.hmac_256(col::jsonb) cast with an eql_v2.eq_term(col) extractor, and rename the ordered extractor eql_v2.ord -> eql_v2.ord_term for a uniform *_term interface. eql_v2.hmac_256 names both a function and a domain type, so the bare eql_v2.hmac_256(col) parses as a cast — forcing an explicit ::jsonb cast in every _eq index recipe. eql_v2.eq_term(eql_v2_int4_eq) hides that cast inside the function body, the same way the ordered extractor hides its own internal cast. eql_v2.eq_term returns eql_v2.hmac_256 (a domain over text), so USING hash and USING btree both engage = with no extra SQL. The six _eq equality wrappers route through eql_v2.eq_term so the inlined predicate matches the functional index. eql_v2.ord is renamed eql_v2.ord_term: the int4 family is unreleased, so renaming is free now, and eq_term / ord_term reads as a deliberate pair. eql_v2.eq_term reuses the existing extractor name from src/ste_vec/eq_term.sql rather than overloading the 2-arg eql_v2.eq comparator. Resolves coderdan PR #225 threads 13 (redundant ::jsonb cast) and 17 (eq extractor interface); reverses decision D-A of the rework spec. - src: add eql_v2.eq_term, rewrite the _eq wrappers, rename ord -> ord_term across both ordered functions files, types.sql, and the operators-file headers. - allowlists: pin_search_path.sql and splinter.sh updated for ord_term + eq_term. - docs: encrypted-int4 reference/walkthrough, v2.4 U-001, CHANGELOG — new recipes; the ::jsonb casts and the wart note are removed. - tests: the eq suite gets btree + hash eq_term engagement tests (the cast-footgun test is gone); ordered suites renamed to ord_term. Verified: mise run build, docs:validate (0 errors), full test suite (int4 suites 7+11+8+4, all green). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 +- .../int4/int4_eq_functions.sql | 47 ++++++++++---- .../int4/int4_ord_functions.sql | 55 ++++++++-------- .../int4/int4_ord_operators.sql | 2 +- .../int4/int4_ord_ore_functions.sql | 55 ++++++++-------- .../int4/int4_ord_ore_operators.sql | 2 +- src/encrypted_domain/types.sql | 4 +- tasks/pin_search_path.sql | 16 ++--- tasks/test/splinter.sh | 32 +++++----- tests/sqlx/tests/encrypted_int4_eq_tests.rs | 62 ++++++------------- .../tests/encrypted_int4_ord_ore_tests.rs | 36 ++++++----- tests/sqlx/tests/encrypted_int4_ord_tests.rs | 38 ++++++------ 12 files changed, 179 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6004fe62..13065a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ The additive `eql_v2_int4` variant family targets `2.4.0`; see [`docs/upgrading/ ### Added -- **`eql_v2_int4` variant family — four capability-encoded domain types for encrypted `int4` columns.** Pick the variant whose operator surface matches the index terms your column carries: `eql_v2_int4` (storage only, every operator blocked — carries `c`), `eql_v2_int4_eq` (HMAC equality only — `=`, `<>` — carries `c`, `hm`), `eql_v2_int4_ord_ore` (equality + ORE-block ordering — `=`, `<>`, `<`, `<=`, `>`, `>=` — carries `c`, `ob`), or `eql_v2_int4_ord` (the recommended ordered name; the identical operator surface to `eql_v2_int4_ord_ore`). Ordered columns expose a single uniform index extractor, `eql_v2.ord(col)` — equality and range share one functional btree, `CREATE INDEX ... USING btree (eql_v2.ord(col))`, and `ORDER BY eql_v2.ord(col)` sorts in plaintext order. `eql_v2.ord` returns the internal `eql_v2.ore_block_u64_8_256` composite, which carries EQL's existing `DEFAULT` btree operator class, so no operator class is defined on the public domain types. The ordered variants do not carry an `hm` term: ORE on a full-domain `int4` is lossless, so the order term doubles as an exact equality term. All variants live in `public` and survive `eql_v2` uninstall. Per-variant payload requirements and index recipes: [U-001](docs/upgrading/v2.4.md#u-001-eql_v2_int4-variant-family). Note: the ORE operator class is excluded from the Supabase build, so ordered `int4` columns fall back to seq-scan for range on Supabase. ([#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) +- **`eql_v2_int4` variant family — four capability-encoded domain types for encrypted `int4` columns.** Pick the variant whose operator surface matches the index terms your column carries: `eql_v2_int4` (storage only, every operator blocked — carries `c`), `eql_v2_int4_eq` (HMAC equality only — `=`, `<>` — carries `c`, `hm`), `eql_v2_int4_ord_ore` (equality + ORE-block ordering — `=`, `<>`, `<`, `<=`, `>`, `>=` — carries `c`, `ob`), or `eql_v2_int4_ord` (the recommended ordered name; the identical operator surface to `eql_v2_int4_ord_ore`). Each variant exposes a uniform index extractor — `eql_v2.eq_term(col)` for `eql_v2_int4_eq`, `eql_v2.ord_term(col)` for the ordered variants — and no index recipe needs a `::jsonb` cast. Ordered columns share one functional btree across equality and range, `CREATE INDEX ... USING btree (eql_v2.ord_term(col))`, with `ORDER BY eql_v2.ord_term(col)` sorting in plaintext order; `eql_v2_int4_eq` indexes `eql_v2.eq_term(col)` with `USING hash` or `USING btree`. `eql_v2.ord_term` returns the internal `eql_v2.ore_block_u64_8_256` composite, which carries EQL's existing `DEFAULT` btree operator class, so no operator class is defined on the public domain types. The ordered variants do not carry an `hm` term: ORE on a full-domain `int4` is lossless, so the order term doubles as an exact equality term. All variants live in `public` and survive `eql_v2` uninstall. Per-variant payload requirements and index recipes: [U-001](docs/upgrading/v2.4.md#u-001-eql_v2_int4-variant-family). Note: the ORE operator class is excluded from the Supabase build, so ordered `int4` columns fall back to seq-scan for range on Supabase. ([#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) ### Upgrade notes diff --git a/src/encrypted_domain/int4/int4_eq_functions.sql b/src/encrypted_domain/int4/int4_eq_functions.sql index add81e67..55eafd07 100644 --- a/src/encrypted_domain/int4/int4_eq_functions.sql +++ b/src/encrypted_domain/int4/int4_eq_functions.sql @@ -6,20 +6,43 @@ --! @file encrypted_domain/int4/int4_eq_functions.sql --! @brief Equality-only int4 variant — comparison/path functions. Supports = and <> via HMAC-256. --! ---! eql_v2_int4_eq carries `c`, `hm` and supports HMAC equality. The ---! functional btree on ((eql_v2.hmac_256(col::jsonb))) engages for `=`. ---! `<>` is supported but is a seq-scan (btree supports only equality). ---! All other operators raise. Payload-term assumption: `c`, `hm`. +--! eql_v2_int4_eq carries `c`, `hm` and supports HMAC equality. A +--! functional index on eql_v2.eq_term(col) — USING hash or USING btree — +--! engages for `=`. `<>` is supported but is a seq-scan (no index serves +--! inequality). All other operators raise. Payload-term assumption: +--! `c`, `hm`. + +-- index extractor + +--! @brief Index extractor for the eql_v2_int4_eq variant. +--! +--! Returns the HMAC-256 equality term carried in the `hm` field of the +--! jsonb payload. The returned eql_v2.hmac_256 is a domain over text, so +--! a functional index — USING hash (eql_v2.eq_term(col)) or +--! USING btree (eql_v2.eq_term(col)) — engages `=`. Inlinable +--! single-statement SQL: `col = $1` folds to +--! `eql_v2.eq_term(col) = eql_v2.eq_term($1)` and matches that index. +--! +--! @param a eql_v2_int4_eq Equality-variant encrypted int4 value +--! @return eql_v2.hmac_256 HMAC-256 equality index term +--! @see eql_v2.hmac_256 +--! @example +--! -- functional index for equality +--! CREATE INDEX t_col_idx ON t USING hash (eql_v2.eq_term(col)); +CREATE FUNCTION eql_v2.eq_term(a eql_v2_int4_eq) +RETURNS eql_v2.hmac_256 +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +AS $$ SELECT eql_v2.hmac_256(a::jsonb) $$; -- = / <> (HMAC equality wrappers, 3 shapes each) ---! @brief Equality wrapper for eql_v2_int4_eq. Inlines to hmac_256 comparison. +--! @brief Equality wrapper for eql_v2_int4_eq. Inlines to eq_term comparison. --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.hmac_256(a::jsonb) = eql_v2.hmac_256(b::jsonb) $$; +AS $$ SELECT eql_v2.eq_term(a) = eql_v2.eq_term(b) $$; --! @brief Equality wrapper for eql_v2_int4_eq (domain, jsonb). --! @param a eql_v2_int4_eq @@ -27,7 +50,7 @@ AS $$ SELECT eql_v2.hmac_256(a::jsonb) = eql_v2.hmac_256(b::jsonb) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a eql_v2_int4_eq, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.hmac_256(a::jsonb) = eql_v2.hmac_256(b) $$; +AS $$ SELECT eql_v2.eq_term(a) = eql_v2.eq_term(b::eql_v2_int4_eq) $$; --! @brief Equality wrapper for eql_v2_int4_eq (jsonb, domain). --! @param a jsonb @@ -35,15 +58,15 @@ AS $$ SELECT eql_v2.hmac_256(a::jsonb) = eql_v2.hmac_256(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a jsonb, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.hmac_256(a) = eql_v2.hmac_256(b::jsonb) $$; +AS $$ SELECT eql_v2.eq_term(a::eql_v2_int4_eq) = eql_v2.eq_term(b) $$; ---! @brief Inequality wrapper for eql_v2_int4_eq. Inlines to hmac_256 comparison. +--! @brief Inequality wrapper for eql_v2_int4_eq. Inlines to eq_term comparison. --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.hmac_256(a::jsonb) <> eql_v2.hmac_256(b::jsonb) $$; +AS $$ SELECT eql_v2.eq_term(a) <> eql_v2.eq_term(b) $$; --! @brief Inequality wrapper for eql_v2_int4_eq (domain, jsonb). --! @param a eql_v2_int4_eq @@ -51,7 +74,7 @@ AS $$ SELECT eql_v2.hmac_256(a::jsonb) <> eql_v2.hmac_256(b::jsonb) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a eql_v2_int4_eq, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.hmac_256(a::jsonb) <> eql_v2.hmac_256(b) $$; +AS $$ SELECT eql_v2.eq_term(a) <> eql_v2.eq_term(b::eql_v2_int4_eq) $$; --! @brief Inequality wrapper for eql_v2_int4_eq (jsonb, domain). --! @param a jsonb @@ -59,7 +82,7 @@ AS $$ SELECT eql_v2.hmac_256(a::jsonb) <> eql_v2.hmac_256(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a jsonb, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.hmac_256(a) <> eql_v2.hmac_256(b::jsonb) $$; +AS $$ SELECT eql_v2.eq_term(a::eql_v2_int4_eq) <> eql_v2.eq_term(b) $$; -- <, <=, >, >=, ~~, ~~*, @>, <@ (blockers, 3 shapes each — 8 ops × 3 = 24 functions) diff --git a/src/encrypted_domain/int4/int4_ord_functions.sql b/src/encrypted_domain/int4/int4_ord_functions.sql index c10d674f..992a9fe5 100644 --- a/src/encrypted_domain/int4/int4_ord_functions.sql +++ b/src/encrypted_domain/int4/int4_ord_functions.sql @@ -14,22 +14,23 @@ --! inherit the operator surface — PostgreSQL resolves operators against --! the ultimate base type (jsonb), so ordered operators fall through to --! native jsonb comparison and the blockers do not engage. ---! eql_v2_int4_ord therefore carries its own eql_v2.ord() overload, +--! eql_v2_int4_ord therefore carries its own eql_v2.ord_term() overload, --! comparison wrappers, operator declarations, and blockers. --! eql_v2_int4_ord_ore is the scheme-explicit ordered domain with the --! identical operator surface. --! ---! Equality and range both route through eql_v2.ord: ord(a) ord(b) +--! Equality and range both route through eql_v2.ord_term: +--! ord_term(a) ord_term(b) --! is the corresponding operator on eql_v2.ore_block_u64_8_256. ORE on a --! full-domain int4 is lossless, so the order term is also an exact --! equality term — there is no separate `hm` term (D#1). --! --! All six comparison wrappers are LANGUAGE sql IMMUTABLE STRICT --! PARALLEL SAFE with no SET clause, so the planner inlines them: ---! `col < $1` becomes `eql_v2.ord(col) < eql_v2.ord($1)`. The inner `<` +--! `col < $1` becomes `eql_v2.ord_term(col) < eql_v2.ord_term($1)`. The inner `<` --! is the operator on eql_v2.ore_block_u64_8_256, a member of main's --! DEFAULT btree operator class. A functional index ---! `USING btree (eql_v2.ord(col))` therefore serves all six operators. +--! `USING btree (eql_v2.ord_term(col))` therefore serves all six operators. --! --! @note The ORE-block operator class is excluded from the Supabase --! build variant, so ordered int4 columns have no indexed range on @@ -40,7 +41,7 @@ --! Returns the ORE-block composite carried in the `ob` field of the --! jsonb payload. The returned eql_v2.ore_block_u64_8_256 type carries --! main's DEFAULT btree operator class, so a functional index ---! USING btree (eql_v2.ord(col)) binds that opclass automatically. +--! USING btree (eql_v2.ord_term(col)) binds that opclass automatically. --! This is the single uniform extractor for index creation and ORDER BY --! across the ordered variants. --! @@ -50,17 +51,17 @@ --! @see eql_v2.ore_block_u64_8_256 --! @example --! -- functional index for range + equality ---! CREATE INDEX t_col_idx ON t USING btree (eql_v2.ord(col)); +--! CREATE INDEX t_col_idx ON t USING btree (eql_v2.ord_term(col)); --! -- ordering ---! SELECT ... FROM t ORDER BY eql_v2.ord(col); -CREATE FUNCTION eql_v2.ord(a eql_v2_int4_ord) +--! SELECT ... FROM t ORDER BY eql_v2.ord_term(col); +CREATE FUNCTION eql_v2.ord_term(a eql_v2_int4_ord) RETURNS eql_v2.ore_block_u64_8_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ore_block_u64_8_256(a::jsonb) $$; -- = <> < <= > >= comparison wrappers, 3 arg-shapes each (18 functions). -- All LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE, no SET clause, so they --- inline: `col < $1` becomes `eql_v2.ord(col) < eql_v2.ord($1)`. +-- inline: `col < $1` becomes `eql_v2.ord_term(col) < eql_v2.ord_term($1)`. --! @brief Less-than wrapper for eql_v2_int4_ord. Inlines to ORE-block compare. --! @param a eql_v2_int4_ord @@ -68,7 +69,7 @@ AS $$ SELECT eql_v2.ore_block_u64_8_256(a::jsonb) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b) $$; --! @brief Less-than wrapper for eql_v2_int4_ord (domain, jsonb). --! @param a eql_v2_int4_ord @@ -76,7 +77,7 @@ AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b::eql_v2_int4_ord) $$; +AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @brief Less-than wrapper for eql_v2_int4_ord (jsonb, domain). --! @param a jsonb @@ -84,7 +85,7 @@ AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b::eql_v2_int4_ord) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) < eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) < eql_v2.ord_term(b) $$; --! @brief Less-than-or-equal wrapper for eql_v2_int4_ord. Inlines to ORE-block compare. --! @param a eql_v2_int4_ord @@ -92,7 +93,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) < eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b) $$; --! @brief Less-than-or-equal wrapper for eql_v2_int4_ord (domain, jsonb). --! @param a eql_v2_int4_ord @@ -100,7 +101,7 @@ AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b::eql_v2_int4_ord) $$; +AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @brief Less-than-or-equal wrapper for eql_v2_int4_ord (jsonb, domain). --! @param a jsonb @@ -108,7 +109,7 @@ AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b::eql_v2_int4_ord) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) <= eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) <= eql_v2.ord_term(b) $$; --! @brief Greater-than wrapper for eql_v2_int4_ord. Inlines to ORE-block compare. --! @param a eql_v2_int4_ord @@ -116,7 +117,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) <= eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b) $$; --! @brief Greater-than wrapper for eql_v2_int4_ord (domain, jsonb). --! @param a eql_v2_int4_ord @@ -124,7 +125,7 @@ AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b::eql_v2_int4_ord) $$; +AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @brief Greater-than wrapper for eql_v2_int4_ord (jsonb, domain). --! @param a jsonb @@ -132,7 +133,7 @@ AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b::eql_v2_int4_ord) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) > eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) > eql_v2.ord_term(b) $$; --! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord. Inlines to ORE-block compare. --! @param a eql_v2_int4_ord @@ -140,7 +141,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) > eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b) $$; --! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord (domain, jsonb). --! @param a eql_v2_int4_ord @@ -148,7 +149,7 @@ AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b::eql_v2_int4_ord) $$; +AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord (jsonb, domain). --! @param a jsonb @@ -156,7 +157,7 @@ AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b::eql_v2_int4_ord) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) >= eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) >= eql_v2.ord_term(b) $$; --! @brief Equality wrapper for eql_v2_int4_ord. Routes through ord — ORE on --! full-domain int4 is lossless, so this is exact equality. @@ -165,7 +166,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) >= eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b) $$; --! @brief Equality wrapper for eql_v2_int4_ord (domain, jsonb). --! @param a eql_v2_int4_ord @@ -173,7 +174,7 @@ AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b::eql_v2_int4_ord) $$; +AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @brief Equality wrapper for eql_v2_int4_ord (jsonb, domain). --! @param a jsonb @@ -181,7 +182,7 @@ AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b::eql_v2_int4_ord) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) = eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) = eql_v2.ord_term(b) $$; --! @brief Inequality wrapper for eql_v2_int4_ord. Routes through ord. --! @param a eql_v2_int4_ord @@ -189,7 +190,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) = eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b) $$; --! @brief Inequality wrapper for eql_v2_int4_ord (domain, jsonb). --! @param a eql_v2_int4_ord @@ -197,7 +198,7 @@ AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b::eql_v2_int4_ord) $$; +AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @brief Inequality wrapper for eql_v2_int4_ord (jsonb, domain). --! @param a jsonb @@ -205,7 +206,7 @@ AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b::eql_v2_int4_ord) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord) <> eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) <> eql_v2.ord_term(b) $$; -- ~~, ~~*, @>, <@ (blockers, 3 shapes each) diff --git a/src/encrypted_domain/int4/int4_ord_operators.sql b/src/encrypted_domain/int4/int4_ord_operators.sql index 29c1a98b..8168104f 100644 --- a/src/encrypted_domain/int4/int4_ord_operators.sql +++ b/src/encrypted_domain/int4/int4_ord_operators.sql @@ -12,7 +12,7 @@ --! inherit the operator surface — PostgreSQL resolves operators against --! the ultimate base type (jsonb), so ordered operators fall through to --! native jsonb comparison and the blockers do not engage. ---! eql_v2_int4_ord therefore carries its own eql_v2.ord() overload, +--! eql_v2_int4_ord therefore carries its own eql_v2.ord_term() overload, --! comparison wrappers, operator declarations, and blockers. --! eql_v2_int4_ord_ore is the scheme-explicit ordered domain with the --! identical operator surface. diff --git a/src/encrypted_domain/int4/int4_ord_ore_functions.sql b/src/encrypted_domain/int4/int4_ord_ore_functions.sql index 399456c6..a32a431c 100644 --- a/src/encrypted_domain/int4/int4_ord_ore_functions.sql +++ b/src/encrypted_domain/int4/int4_ord_ore_functions.sql @@ -9,24 +9,25 @@ --! (equality + ORE-block ordering). --! --! eql_v2_int4_ord_ore carries `c`, `ob`. It is the scheme-explicit ---! ordered domain: it carries the eql_v2.ord() extractor, the six +--! ordered domain: it carries the eql_v2.ord_term() extractor, the six --! comparison wrappers, the operator declarations, and the blockers. --! eql_v2_int4_ord — the recommended ordered name — is a separate --! concrete domain (int4_ord.sql) carrying its own copy of this --! operator surface; the §8 spike showed a domain-over-domain alias --! does not transparently inherit the operator surface (D-E fallback). --! ---! Equality and range both route through eql_v2.ord: ord(a) ord(b) +--! Equality and range both route through eql_v2.ord_term: +--! ord_term(a) ord_term(b) --! is the corresponding operator on eql_v2.ore_block_u64_8_256. ORE on a --! full-domain int4 is lossless, so the order term is also an exact --! equality term — there is no separate `hm` term (D#1). --! --! All six comparison wrappers are LANGUAGE sql IMMUTABLE STRICT --! PARALLEL SAFE with no SET clause, so the planner inlines them: ---! `col < $1` becomes `eql_v2.ord(col) < eql_v2.ord($1)`. The inner `<` +--! `col < $1` becomes `eql_v2.ord_term(col) < eql_v2.ord_term($1)`. The inner `<` --! is the operator on eql_v2.ore_block_u64_8_256, a member of main's --! DEFAULT btree operator class. A functional index ---! `USING btree (eql_v2.ord(col))` therefore serves all six operators. +--! `USING btree (eql_v2.ord_term(col))` therefore serves all six operators. --! --! @note The ORE-block operator class is excluded from the Supabase --! build variant, so ordered int4 columns have no indexed range on @@ -37,7 +38,7 @@ --! Returns the ORE-block composite carried in the `ob` field of the --! jsonb payload. The returned eql_v2.ore_block_u64_8_256 type carries --! main's DEFAULT btree operator class, so a functional index ---! USING btree (eql_v2.ord(col)) binds that opclass automatically. +--! USING btree (eql_v2.ord_term(col)) binds that opclass automatically. --! This is the single uniform extractor for index creation and ORDER BY --! across the ordered variants. --! @@ -47,17 +48,17 @@ --! @see eql_v2.ore_block_u64_8_256 --! @example --! -- functional index for range + equality ---! CREATE INDEX t_col_idx ON t USING btree (eql_v2.ord(col)); +--! CREATE INDEX t_col_idx ON t USING btree (eql_v2.ord_term(col)); --! -- ordering ---! SELECT ... FROM t ORDER BY eql_v2.ord(col); -CREATE FUNCTION eql_v2.ord(a eql_v2_int4_ord_ore) +--! SELECT ... FROM t ORDER BY eql_v2.ord_term(col); +CREATE FUNCTION eql_v2.ord_term(a eql_v2_int4_ord_ore) RETURNS eql_v2.ore_block_u64_8_256 LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ore_block_u64_8_256(a::jsonb) $$; -- = <> < <= > >= comparison wrappers, 3 arg-shapes each (18 functions). -- All LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE, no SET clause, so they --- inline: `col < $1` becomes `eql_v2.ord(col) < eql_v2.ord($1)`. +-- inline: `col < $1` becomes `eql_v2.ord_term(col) < eql_v2.ord_term($1)`. --! @brief Less-than wrapper for eql_v2_int4_ord_ore. Inlines to ORE-block compare. --! @param a eql_v2_int4_ord_ore @@ -65,7 +66,7 @@ AS $$ SELECT eql_v2.ore_block_u64_8_256(a::jsonb) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b) $$; --! @brief Less-than wrapper for eql_v2_int4_ord_ore (domain, jsonb). --! @param a eql_v2_int4_ord_ore @@ -73,7 +74,7 @@ AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b::eql_v2_int4_ord_ore) $$; +AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @brief Less-than wrapper for eql_v2_int4_ord_ore (jsonb, domain). --! @param a jsonb @@ -81,7 +82,7 @@ AS $$ SELECT eql_v2.ord(a) < eql_v2.ord(b::eql_v2_int4_ord_ore) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) < eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) < eql_v2.ord_term(b) $$; --! @brief Less-than-or-equal wrapper for eql_v2_int4_ord_ore. Inlines to ORE-block compare. --! @param a eql_v2_int4_ord_ore @@ -89,7 +90,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) < eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b) $$; --! @brief Less-than-or-equal wrapper for eql_v2_int4_ord_ore (domain, jsonb). --! @param a eql_v2_int4_ord_ore @@ -97,7 +98,7 @@ AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b::eql_v2_int4_ord_ore) $$; +AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @brief Less-than-or-equal wrapper for eql_v2_int4_ord_ore (jsonb, domain). --! @param a jsonb @@ -105,7 +106,7 @@ AS $$ SELECT eql_v2.ord(a) <= eql_v2.ord(b::eql_v2_int4_ord_ore) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) <= eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) <= eql_v2.ord_term(b) $$; --! @brief Greater-than wrapper for eql_v2_int4_ord_ore. Inlines to ORE-block compare. --! @param a eql_v2_int4_ord_ore @@ -113,7 +114,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) <= eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b) $$; --! @brief Greater-than wrapper for eql_v2_int4_ord_ore (domain, jsonb). --! @param a eql_v2_int4_ord_ore @@ -121,7 +122,7 @@ AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b::eql_v2_int4_ord_ore) $$; +AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @brief Greater-than wrapper for eql_v2_int4_ord_ore (jsonb, domain). --! @param a jsonb @@ -129,7 +130,7 @@ AS $$ SELECT eql_v2.ord(a) > eql_v2.ord(b::eql_v2_int4_ord_ore) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) > eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) > eql_v2.ord_term(b) $$; --! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord_ore. Inlines to ORE-block compare. --! @param a eql_v2_int4_ord_ore @@ -137,7 +138,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) > eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b) $$; --! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord_ore (domain, jsonb). --! @param a eql_v2_int4_ord_ore @@ -145,7 +146,7 @@ AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b::eql_v2_int4_ord_ore) $$; +AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @brief Greater-than-or-equal wrapper for eql_v2_int4_ord_ore (jsonb, domain). --! @param a jsonb @@ -153,7 +154,7 @@ AS $$ SELECT eql_v2.ord(a) >= eql_v2.ord(b::eql_v2_int4_ord_ore) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) >= eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) >= eql_v2.ord_term(b) $$; --! @brief Equality wrapper for eql_v2_int4_ord_ore. Routes through ord — ORE on --! full-domain int4 is lossless, so this is exact equality. @@ -162,7 +163,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) >= eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b) $$; --! @brief Equality wrapper for eql_v2_int4_ord_ore (domain, jsonb). --! @param a eql_v2_int4_ord_ore @@ -170,7 +171,7 @@ AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b::eql_v2_int4_ord_ore) $$; +AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @brief Equality wrapper for eql_v2_int4_ord_ore (jsonb, domain). --! @param a jsonb @@ -178,7 +179,7 @@ AS $$ SELECT eql_v2.ord(a) = eql_v2.ord(b::eql_v2_int4_ord_ore) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) = eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) = eql_v2.ord_term(b) $$; --! @brief Inequality wrapper for eql_v2_int4_ord_ore. Routes through ord. --! @param a eql_v2_int4_ord_ore @@ -186,7 +187,7 @@ AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) = eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b) $$; --! @brief Inequality wrapper for eql_v2_int4_ord_ore (domain, jsonb). --! @param a eql_v2_int4_ord_ore @@ -194,7 +195,7 @@ AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b::eql_v2_int4_ord_ore) $$; +AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @brief Inequality wrapper for eql_v2_int4_ord_ore (jsonb, domain). --! @param a jsonb @@ -202,7 +203,7 @@ AS $$ SELECT eql_v2.ord(a) <> eql_v2.ord(b::eql_v2_int4_ord_ore) $$; --! @return boolean CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -AS $$ SELECT eql_v2.ord(a::eql_v2_int4_ord_ore) <> eql_v2.ord(b) $$; +AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) <> eql_v2.ord_term(b) $$; -- ~~, ~~*, @>, <@ (blockers, 3 shapes each) diff --git a/src/encrypted_domain/int4/int4_ord_ore_operators.sql b/src/encrypted_domain/int4/int4_ord_ore_operators.sql index af193925..ff02898a 100644 --- a/src/encrypted_domain/int4/int4_ord_ore_operators.sql +++ b/src/encrypted_domain/int4/int4_ord_ore_operators.sql @@ -7,7 +7,7 @@ --! (equality + ORE-block ordering). --! --! eql_v2_int4_ord_ore carries `c`, `ob`. It is the scheme-explicit ---! ordered domain: it carries the eql_v2.ord() extractor, the six +--! ordered domain: it carries the eql_v2.ord_term() extractor, the six --! comparison wrappers, the operator declarations, and the blockers. --! eql_v2_int4_ord — the recommended ordered name — is a separate --! concrete domain (int4_ord.sql) carrying its own copy of this diff --git a/src/encrypted_domain/types.sql b/src/encrypted_domain/types.sql index 0b5ef9b3..a49d581a 100644 --- a/src/encrypted_domain/types.sql +++ b/src/encrypted_domain/types.sql @@ -30,7 +30,7 @@ --! therefore carries its own operator surface (int4_ord.sql). --! --! Ordered range and equality both engage a functional btree ---! USING btree (eql_v2.ord(col)) — eql_v2.ord returns +--! USING btree (eql_v2.ord_term(col)) — eql_v2.ord_term returns --! eql_v2.ore_block_u64_8_256, which carries main's DEFAULT btree --! operator class. No operator class is defined on these domains. @@ -56,7 +56,7 @@ BEGIN --! @brief Scheme-explicit ordered encrypted int4 domain (jsonb-backed). --! Supports = <> < <= > >= via the ORE-block term; carries - --! `c`, `ob`. Carries the eql_v2.ord extractor, the comparison + --! `c`, `ob`. Carries the eql_v2.ord_term extractor, the comparison --! wrappers, the operator declarations, and the blockers. IF NOT EXISTS ( SELECT 1 FROM pg_type diff --git a/tasks/pin_search_path.sql b/tasks/pin_search_path.sql index 5bbd3be9..47822a73 100644 --- a/tasks/pin_search_path.sql +++ b/tasks/pin_search_path.sql @@ -248,14 +248,16 @@ BEGIN -- -- The eql_v2_int4_ord_ore comparison wrappers (_eq/_neq and the -- four range wrappers) are LANGUAGE sql and must inline so the - -- planner rewrites `col $1` to `eql_v2.ord(col) - -- eql_v2.ord($1)` and matches the functional btree on - -- eql_v2.ord(col). eql_v2.ord is the index extractor and must also - -- stay unpinned. The eql_v2_int4_eq wrappers must inline to match - -- the functional hmac btree. eql_v2_int4_ord is a concrete domain - -- (D-E fallback) carrying the same wrapper set as + -- planner rewrites `col $1` to `eql_v2.ord_term(col) + -- eql_v2.ord_term($1)` and matches the functional btree on + -- eql_v2.ord_term(col). eql_v2.ord_term is the index extractor and + -- must also stay unpinned (the 1-arg clause below). The + -- eql_v2_int4_eq wrappers must inline to match the functional + -- eql_v2.eq_term(col) index; eql_v2.eq_term itself stays unpinned + -- via the 1-arg `eq_term` clause above. eql_v2_int4_ord is a + -- concrete domain (D-E fallback) carrying the same wrapper set as -- eql_v2_int4_ord_ore. See docs/upgrading/v2.4.md U-001. - OR (p.pronargs = 1 AND p.proname = 'ord') + OR (p.pronargs = 1 AND p.proname = 'ord_term') OR p.proname IN ( 'eql_v2_int4_eq_eq', -- _eq variant equality 'eql_v2_int4_eq_neq', diff --git a/tasks/test/splinter.sh b/tasks/test/splinter.sh index c4dd461a..02149900 100755 --- a/tasks/test/splinter.sh +++ b/tasks/test/splinter.sh @@ -94,25 +94,25 @@ function_search_path_mutable eql_v2 ore_cllw_lte function Inner comparator for t function_search_path_mutable eql_v2 ore_cllw_gt function Inner comparator for the `eql_v2.ore_cllw` type's `>` operator (#221). Same rationale as `ore_cllw_eq`. function_search_path_mutable eql_v2 ore_cllw_gte function Inner comparator for the `eql_v2.ore_cllw` type's `>=` operator (#221). Same rationale as `ore_cllw_eq`. function_search_path_mutable eql_v2 -> function Typed sv-element selector lookup (U-007): inlinable SQL so the planner can fold `col -> ''` into the calling query, preserving functional-index match for the chained recipes `WHERE col -> 'sel' = $1::ste_vec_entry` (via eq_term) and `ORDER BY eql_v2.ore_cllw(col -> 'sel')`. Three overloads: (enc, text), (enc, enc), (enc, int). -function_search_path_mutable eql_v2 eq_term function XOR-aware equality term extractor on a ste_vec entry (U-007): coalesces hm and oc as bytea. Must inline so `eql_v2.eq_term(col -> 'sel')` folds into the calling query and matches a functional hash index built on the same expression — same precedent as ore_cllw / hmac_256 extractors on ste_vec_entry. +function_search_path_mutable eql_v2 eq_term function XOR-aware equality term extractor on a ste_vec entry (U-007): coalesces hm and oc as bytea. Must inline so `eql_v2.eq_term(col -> 'sel')` folds into the calling query and matches a functional hash index built on the same expression — same precedent as ore_cllw / hmac_256 extractors on ste_vec_entry. Also covers the eql_v2_int4_eq eq_term overload (PR #225). function_search_path_mutable eql_v2 min function Aggregate (splinter labels these type=function): ALTER AGGREGATE has no SET configuration_parameter syntax, and ALTER ROUTINE/FUNCTION reject aggregates. The aggregate's SFUNC has a pinned search_path. function_search_path_mutable eql_v2 max function Aggregate: same as min. function_search_path_mutable eql_v2 grouped_value function Aggregate: same as min. -function_search_path_mutable eql_v2 ord function eql_v2_int4 ordered-variant index extractor: returns eql_v2.ore_block_u64_8_256 (carrying main DEFAULT btree opclass). Used inside the inlinable comparison wrappers and as the functional-index expression USING btree (eql_v2.ord(col)); must inline. SET search_path would disable SQL function inlining (see PostgreSQL inline_function). Covers both ord overloads (eql_v2_int4_ord_ore, eql_v2_int4_ord). -function_search_path_mutable eql_v2 eql_v2_int4_eq_eq function eql_v2_int4_eq variant equality: HMAC wrapper, inlines to hmac_256(a::jsonb) = hmac_256(b::jsonb) for functional-btree engagement. Three overloads: (domain,domain), (domain,jsonb), (jsonb,domain). -function_search_path_mutable eql_v2 eql_v2_int4_eq_neq function eql_v2_int4_eq variant inequality: same hmac_256 inlining rationale as eql_v2_int4_eq_eq. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_eq function eql_v2_int4_ord_ore equality: inlines to eql_v2.ord(a) = eql_v2.ord(b) for functional-btree engagement on eql_v2.ord(col). Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_neq function eql_v2_int4_ord_ore inequality: same eql_v2.ord inlining rationale as eql_v2_int4_ord_ore_eq. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_lt function eql_v2_int4_ord_ore range: inlines to eql_v2.ord(a) < eql_v2.ord(b) for functional-btree engagement on eql_v2.ord(col). Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_lte function eql_v2_int4_ord_ore range: same eql_v2.ord inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_gt function eql_v2_int4_ord_ore range: same eql_v2.ord inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_gte function eql_v2_int4_ord_ore range: same eql_v2.ord inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_eq function eql_v2_int4_ord equality (D-E fallback concrete domain): inlines to eql_v2.ord(a) = eql_v2.ord(b), same rationale as eql_v2_int4_ord_ore_eq. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_neq function eql_v2_int4_ord inequality (D-E fallback): same eql_v2.ord inlining rationale as eql_v2_int4_ord_eq. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_lt function eql_v2_int4_ord range (D-E fallback): inlines to eql_v2.ord(a) < eql_v2.ord(b), same rationale as eql_v2_int4_ord_ore_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_lte function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord inlining rationale as eql_v2_int4_ord_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_gt function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord inlining rationale as eql_v2_int4_ord_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_gte function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord inlining rationale as eql_v2_int4_ord_lt. Three overloads. +function_search_path_mutable eql_v2 ord_term function eql_v2_int4 ordered-variant index extractor: returns eql_v2.ore_block_u64_8_256 (carrying main DEFAULT btree opclass). Used inside the inlinable comparison wrappers and as the functional-index expression USING btree (eql_v2.ord_term(col)); must inline. SET search_path would disable SQL function inlining (see PostgreSQL inline_function). Covers both ord_term overloads (eql_v2_int4_ord_ore, eql_v2_int4_ord). +function_search_path_mutable eql_v2 eql_v2_int4_eq_eq function eql_v2_int4_eq variant equality: inlines to eql_v2.eq_term(a) = eql_v2.eq_term(b) for functional-index engagement on eql_v2.eq_term(col) (USING hash or btree). Three overloads: (domain,domain), (domain,jsonb), (jsonb,domain). +function_search_path_mutable eql_v2 eql_v2_int4_eq_neq function eql_v2_int4_eq variant inequality: same eql_v2.eq_term inlining rationale as eql_v2_int4_eq_eq. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_eq function eql_v2_int4_ord_ore equality: inlines to eql_v2.ord_term(a) = eql_v2.ord_term(b) for functional-btree engagement on eql_v2.ord_term(col). Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_neq function eql_v2_int4_ord_ore inequality: same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_ore_eq. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_lt function eql_v2_int4_ord_ore range: inlines to eql_v2.ord_term(a) < eql_v2.ord_term(b) for functional-btree engagement on eql_v2.ord_term(col). Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_lte function eql_v2_int4_ord_ore range: same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_gt function eql_v2_int4_ord_ore range: same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_gte function eql_v2_int4_ord_ore range: same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_eq function eql_v2_int4_ord equality (D-E fallback concrete domain): inlines to eql_v2.ord_term(a) = eql_v2.ord_term(b), same rationale as eql_v2_int4_ord_ore_eq. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_neq function eql_v2_int4_ord inequality (D-E fallback): same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_eq. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_lt function eql_v2_int4_ord range (D-E fallback): inlines to eql_v2.ord_term(a) < eql_v2.ord_term(b), same rationale as eql_v2_int4_ord_ore_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_lte function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_gt function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_lt. Three overloads. +function_search_path_mutable eql_v2 eql_v2_int4_ord_gte function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_lt. Three overloads. ALLOW # Wrap splinter (a single bare SELECT expression) into a subquery we can diff --git a/tests/sqlx/tests/encrypted_int4_eq_tests.rs b/tests/sqlx/tests/encrypted_int4_eq_tests.rs index 577c2b15..97b4d70d 100644 --- a/tests/sqlx/tests/encrypted_int4_eq_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_eq_tests.rs @@ -1,9 +1,8 @@ //! Synthetic test suite for `eql_v2_int4_eq` — HMAC equality only. //! -//! `=` engages the functional btree on -//! `((eql_v2.hmac_256(col::jsonb)))` (EXPLAIN assertion). -//! `<>` is supported semantically but is seq-scan (btree only -//! supports equality, by design). All other operators raise. +//! `=` engages a functional index on `eql_v2.eq_term(col)` — hash or +//! btree (EXPLAIN assertion). `<>` is supported semantically but is +//! seq-scan (no index serves inequality). All other operators raise. use anyhow::Result; use sqlx::PgPool; @@ -37,13 +36,13 @@ async fn setup_eq_table( } #[sqlx::test] -async fn eq_engages_hmac_btree_for_equality(pool: PgPool) -> Result<()> { +async fn eq_engages_btree_for_equality(pool: PgPool) -> Result<()> { let mut tx = pool.begin().await?; setup_eq_table(&mut tx, &["aaa", "bbb", "ccc"]).await?; sqlx::query( - "CREATE INDEX typed_int4_eq_hmac_idx \ - ON typed_int4_eq ((eql_v2.hmac_256(value::jsonb)))", + "CREATE INDEX typed_int4_eq_btree_idx \ + ON typed_int4_eq USING btree (eql_v2.eq_term(value))", ) .execute(&mut *tx) .await?; @@ -63,8 +62,8 @@ async fn eq_engages_hmac_btree_for_equality(pool: PgPool) -> Result<()> { .await?; let plan_text = plan.join("\n"); assert!( - plan_text.contains("typed_int4_eq_hmac_idx"), - "= must engage the hmac btree; got plan:\n{plan_text}" + plan_text.contains("typed_int4_eq_btree_idx"), + "= must engage the eql_v2.eq_term btree index; got plan:\n{plan_text}" ); tx.commit().await?; @@ -201,16 +200,12 @@ async fn eq_blocked_operators_raise_on_null_input(pool: PgPool) -> Result<()> { } #[sqlx::test] -async fn eq_hmac_index_recipe_requires_jsonb_cast(pool: PgPool) -> Result<()> { - // The documented _eq index recipe is - // USING btree ((eql_v2.hmac_256(col::jsonb))) - // The ::jsonb cast is REQUIRED, not redundant. `eql_v2.hmac_256` is - // both a function and an index-term type, and an eql_v2_int4_eq - // column has no exact hmac_256 overload — so the bare form - // `eql_v2.hmac_256(col)` parses as a cast to the hmac_256 type - // (col::eql_v2.hmac_256), building an index the `=` predicate never - // matches. This test pins both halves of that contract so the - // docs/reference + v2.4.md U-001 recipe stays honest. +async fn eq_engages_hash_for_equality(pool: PgPool) -> Result<()> { + // `eql_v2.eq_term(col)` extracts the HMAC equality term — a domain + // over `text`, which carries a default hash operator class. A hash + // functional index on it engages `=` (btree does too — see + // eq_engages_btree_for_equality). No `::jsonb` cast: `eql_v2.eq_term` + // is a plain function name with no colliding type. let mut tx = pool.begin().await?; sqlx::query( "CREATE TEMP TABLE eq_idx (plaintext integer, value eql_v2_int4_eq) ON COMMIT DROP", @@ -223,6 +218,9 @@ async fn eq_hmac_index_recipe_requires_jsonb_cast(pool: PgPool) -> Result<()> { ) .execute(&mut *tx) .await?; + sqlx::query("CREATE INDEX eq_idx_hash ON eq_idx USING hash (eql_v2.eq_term(value))") + .execute(&mut *tx) + .await?; sqlx::query("ANALYZE eq_idx").execute(&mut *tx).await?; sqlx::query("SET LOCAL enable_seqscan = off") .execute(&mut *tx) @@ -236,32 +234,12 @@ async fn eq_hmac_index_recipe_requires_jsonb_cast(pool: PgPool) -> Result<()> { let lit = pivot.replace('\'', "''"); let eq_query = format!("SELECT * FROM eq_idx WHERE value = '{lit}'::jsonb::eql_v2_int4_eq"); - // Footgun half: the bare eql_v2.hmac_256(value) form is a cast to the - // hmac_256 type, not a function call — the index it builds cannot - // serve the = predicate. - sqlx::query("CREATE INDEX eq_idx_bare ON eq_idx USING btree (eql_v2.hmac_256(value))") - .execute(&mut *tx) - .await?; - let bare_plan: Vec = sqlx::query_scalar(&format!("EXPLAIN {eq_query}")) - .fetch_all(&mut *tx) - .await?; - assert!( - !bare_plan.join("\n").contains("eq_idx_bare"), - "bare eql_v2.hmac_256(col) is a cast, not a call — must NOT serve = ; plan:\n{}", - bare_plan.join("\n") - ); - - // Recipe half: the explicit ::jsonb cast resolves the - // hmac_256(jsonb) function, and = engages the index. - sqlx::query("CREATE INDEX eq_idx_hmac ON eq_idx USING btree ((eql_v2.hmac_256(value::jsonb)))") - .execute(&mut *tx) - .await?; let plan: Vec = sqlx::query_scalar(&format!("EXPLAIN {eq_query}")) .fetch_all(&mut *tx) .await?; assert!( - plan.join("\n").contains("eq_idx_hmac"), - "the documented eql_v2.hmac_256(col::jsonb) recipe must engage for = ; plan:\n{}", + plan.join("\n").contains("eq_idx_hash"), + "the eql_v2.eq_term hash recipe must engage for = ; plan:\n{}", plan.join("\n") ); @@ -273,7 +251,7 @@ async fn eq_hmac_index_recipe_requires_jsonb_cast(pool: PgPool) -> Result<()> { assert_eq!( ids, vec![42], - "= via the hmac index must return the matching row" + "= via the eq_term hash index must return the matching row" ); tx.commit().await?; diff --git a/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs index 9c3e94b6..9d936c1e 100644 --- a/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs @@ -10,11 +10,11 @@ //! 14 rows. Range pivots produce distinct cardinalities so swapped //! operators would fail the assertions, not silently pass. //! -//! Equality and range both route through `eql_v2.ord`: `col $1` -//! inlines to `eql_v2.ord(col) eql_v2.ord($1)`, the operator on +//! Equality and range both route through `eql_v2.ord_term`: `col $1` +//! inlines to `eql_v2.ord_term(col) eql_v2.ord_term($1)`, the operator on //! `eql_v2.ore_block_u64_8_256`. A single functional btree -//! `USING btree (eql_v2.ord(col))` serves all six operators — there is -//! no operator class on the domain. `ORDER BY eql_v2.ord(col)` sorts in +//! `USING btree (eql_v2.ord_term(col))` serves all six operators — there is +//! no operator class on the domain. `ORDER BY eql_v2.ord_term(col)` sorts in //! plaintext numeric order. Equality routes through the `ob` term //! (lossless ORE on full-domain int4 = exact equality); there is no //! `hm` term on the ordered variants (D#1). @@ -191,19 +191,19 @@ async fn encrypted_int4_range_operators_match_numeric_semantics(pool: PgPool) -> #[sqlx::test] async fn encrypted_int4_ore_ordering_matches_numeric_ordering(pool: PgPool) -> Result<()> { // Critical invariant: ORE bytes from Proxy must preserve numeric order. - // Pulling all 14 rows ordered by eql_v2.ord — the uniform ordered-int4 + // Pulling all 14 rows ordered by eql_v2.ord_term — the uniform ordered-int4 // index/ORDER BY extractor — must yield the plaintext sequence in // ascending numeric order. A bug in Proxy's ORE-block encoding (sign // handling, byte-order, padding) would fail this without throwing. // - // ORDER BY eql_v2.ord(payload::eql_v2_int4_ord_ore) pins the sort to + // ORDER BY eql_v2.ord_term(payload::eql_v2_int4_ord_ore) pins the sort to // the ORE-block term; sorting the domain column directly would follow // native jsonb comparison, not ORE order. let ordered: Vec = sqlx::query_scalar( r#" SELECT plaintext FROM encrypted_int4_plaintext - ORDER BY eql_v2.ord(payload::eql_v2_int4_ord_ore) + ORDER BY eql_v2.ord_term(payload::eql_v2_int4_ord_ore) "#, ) .fetch_all(&pool) @@ -212,7 +212,7 @@ async fn encrypted_int4_ore_ordering_matches_numeric_ordering(pool: PgPool) -> R let expected = vec![-100, -1, 1, 2, 5, 10, 17, 25, 42, 50, 100, 250, 1000, 9999]; assert_eq!( ordered, expected, - "eql_v2.ord ordering must match numeric ordering of plaintext" + "eql_v2.ord_term ordering must match numeric ordering of plaintext" ); Ok(()) @@ -221,7 +221,7 @@ async fn encrypted_int4_ore_ordering_matches_numeric_ordering(pool: PgPool) -> R #[sqlx::test] async fn encrypted_int4_ord_distinctness_sweep(pool: PgPool) -> Result<()> { // Pairwise: no two distinct integer plaintexts share an ORE term. - // Equality routes through eql_v2.ord (the `ob` term), not HMAC — + // Equality routes through eql_v2.ord_term (the `ob` term), not HMAC — // 14 distinct ints → 14 distinct ORE terms → no `=` collisions. let collisions: i64 = sqlx::query_scalar( r#" @@ -246,7 +246,7 @@ async fn encrypted_int4_ord_ore_functional_index_serves_range_and_equality( pool: PgPool, ) -> Result<()> { // Range + equality on eql_v2_int4_ord_ore are served by one - // functional btree USING btree (eql_v2.ord(col)). eql_v2.ord + // functional btree USING btree (eql_v2.ord_term(col)). eql_v2.ord_term // returns eql_v2.ore_block_u64_8_256, which carries main's DEFAULT // btree operator class — no opclass annotation needed. let mut tx = pool.begin().await?; @@ -264,7 +264,7 @@ async fn encrypted_int4_ord_ore_functional_index_serves_range_and_equality( ) .execute(&mut *tx) .await?; - sqlx::query("CREATE INDEX ord_ore_fi_idx ON ord_ore_fi USING btree (eql_v2.ord(value))") + sqlx::query("CREATE INDEX ord_ore_fi_idx ON ord_ore_fi USING btree (eql_v2.ord_term(value))") .execute(&mut *tx) .await?; sqlx::query("ANALYZE ord_ore_fi").execute(&mut *tx).await?; @@ -290,7 +290,7 @@ async fn encrypted_int4_ord_ore_functional_index_serves_range_and_equality( let plan_text = plan.join("\n"); assert!( plan_text.contains("ord_ore_fi_idx"), - "{op} must engage the eql_v2.ord functional btree; plan:\n{plan_text}" + "{op} must engage the eql_v2.ord_term functional btree; plan:\n{plan_text}" ); } @@ -462,7 +462,7 @@ async fn encrypted_int4_ord_ore_null_operand_yields_null(pool: PgPool) -> Result #[sqlx::test] async fn encrypted_int4_ord_ore_equality_uses_ob_not_hm(pool: PgPool) -> Result<()> { // D#1: ordered variants carry c + ob and drop hm. Equality routes - // through eql_v2.ord (the `ob` term), never HMAC. Strip `hm` from + // through eql_v2.ord_term (the `ob` term), never HMAC. Strip `hm` from // every payload: with no hm present, an accidental regression to // HMAC equality fails instead of silently passing on the fixture. let mut tx = pool.begin().await?; @@ -489,9 +489,11 @@ async fn encrypted_int4_ord_ore_equality_uses_ob_not_hm(pool: PgPool) -> Result< .await?; assert_eq!(with_hm, 0, "test rows must not carry hm"); - sqlx::query("CREATE INDEX ord_ore_no_hm_idx ON ord_ore_no_hm USING btree (eql_v2.ord(value))") - .execute(&mut *tx) - .await?; + sqlx::query( + "CREATE INDEX ord_ore_no_hm_idx ON ord_ore_no_hm USING btree (eql_v2.ord_term(value))", + ) + .execute(&mut *tx) + .await?; sqlx::query("ANALYZE ord_ore_no_hm") .execute(&mut *tx) .await?; @@ -532,7 +534,7 @@ async fn encrypted_int4_ord_ore_equality_uses_ob_not_hm(pool: PgPool) -> Result< .await?; assert!( plan.join("\n").contains("ord_ore_no_hm_idx"), - "= must engage the eql_v2.ord functional btree with no hm present" + "= must engage the eql_v2.ord_term functional btree with no hm present" ); tx.commit().await?; diff --git a/tests/sqlx/tests/encrypted_int4_ord_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_tests.rs index c7f0010a..30acd627 100644 --- a/tests/sqlx/tests/encrypted_int4_ord_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_ord_tests.rs @@ -129,7 +129,7 @@ async fn ord_blocked_operators_raise(pool: PgPool) -> Result<()> { #[sqlx::test] async fn ord_functional_index_serves_range_and_equality(pool: PgPool) -> Result<()> { // Range + equality on eql_v2_int4_ord are served by one functional - // btree USING btree (eql_v2.ord(col)). + // btree USING btree (eql_v2.ord_term(col)). let mut tx = pool.begin().await?; sqlx::query( "CREATE TEMP TABLE ord_fi (plaintext integer, value eql_v2_int4_ord) ON COMMIT DROP", @@ -142,7 +142,7 @@ async fn ord_functional_index_serves_range_and_equality(pool: PgPool) -> Result< ) .execute(&mut *tx) .await?; - sqlx::query("CREATE INDEX ord_fi_idx ON ord_fi USING btree (eql_v2.ord(value))") + sqlx::query("CREATE INDEX ord_fi_idx ON ord_fi USING btree (eql_v2.ord_term(value))") .execute(&mut *tx) .await?; sqlx::query("ANALYZE ord_fi").execute(&mut *tx).await?; @@ -166,7 +166,7 @@ async fn ord_functional_index_serves_range_and_equality(pool: PgPool) -> Result< let plan_text = plan.join("\n"); assert!( plan_text.contains("ord_fi_idx"), - "{op} must engage the eql_v2.ord functional btree; plan:\n{plan_text}" + "{op} must engage the eql_v2.ord_term functional btree; plan:\n{plan_text}" ); } @@ -198,7 +198,7 @@ async fn ord_functional_index_serves_range_and_equality(pool: PgPool) -> Result< #[sqlx::test] async fn ord_order_by_preserves_numeric_order(pool: PgPool) -> Result<()> { - // ORDER BY eql_v2.ord(col) sorts an eql_v2_int4_ord column in + // ORDER BY eql_v2.ord_term(col) sorts an eql_v2_int4_ord column in // plaintext numeric order. let mut tx = pool.begin().await?; sqlx::query( @@ -213,13 +213,13 @@ async fn ord_order_by_preserves_numeric_order(pool: PgPool) -> Result<()> { .execute(&mut *tx) .await?; let ordered: Vec = - sqlx::query_scalar("SELECT plaintext FROM ord_sort ORDER BY eql_v2.ord(value)") + sqlx::query_scalar("SELECT plaintext FROM ord_sort ORDER BY eql_v2.ord_term(value)") .fetch_all(&mut *tx) .await?; assert_eq!( ordered, vec![-100, -1, 1, 2, 5, 10, 17, 25, 42, 50, 100, 250, 1000, 9999], - "ORDER BY eql_v2.ord(value) must yield plaintext numeric order" + "ORDER BY eql_v2.ord_term(value) must yield plaintext numeric order" ); tx.commit().await?; Ok(()) @@ -255,7 +255,7 @@ async fn ord_null_operand_yields_null(pool: PgPool) -> Result<()> { #[sqlx::test] async fn ord_equality_independent_of_hm(pool: PgPool) -> Result<()> { // D#1: ordered variants carry c + ob and drop hm. Equality on - // eql_v2_int4_ord routes through eql_v2.ord (the `ob` term), never + // eql_v2_int4_ord routes through eql_v2.ord_term (the `ob` term), never // HMAC. Strip `hm` so an accidental regression to HMAC equality // fails instead of passing on the hm-carrying fixture. let mut tx = pool.begin().await?; @@ -278,7 +278,7 @@ async fn ord_equality_independent_of_hm(pool: PgPool) -> Result<()> { .await?; assert_eq!(with_hm, 0, "test rows must not carry hm"); - sqlx::query("CREATE INDEX ord_no_hm_idx ON ord_no_hm USING btree (eql_v2.ord(value))") + sqlx::query("CREATE INDEX ord_no_hm_idx ON ord_no_hm USING btree (eql_v2.ord_term(value))") .execute(&mut *tx) .await?; sqlx::query("ANALYZE ord_no_hm").execute(&mut *tx).await?; @@ -314,7 +314,7 @@ async fn ord_equality_independent_of_hm(pool: PgPool) -> Result<()> { .await?; assert!( plan.join("\n").contains("ord_no_hm_idx"), - "= must engage the eql_v2.ord functional btree with no hm present" + "= must engage the eql_v2.ord_term functional btree with no hm present" ); tx.commit().await?; @@ -326,8 +326,8 @@ async fn ord_ore_wrappers_are_inlinable(pool: PgPool) -> Result<()> { // The comparison wrappers on eql_v2_int4_ord_ore and eql_v2_int4_ord // must be LANGUAGE sql, IMMUTABLE, and carry no pinned search_path, // so the planner inlines `col < $1` to - // `eql_v2.ord(col) < eql_v2.ord($1)` and the functional btree on - // eql_v2.ord(col) engages. A pinned proconfig or a plpgsql body + // `eql_v2.ord_term(col) < eql_v2.ord_term($1)` and the functional btree on + // eql_v2.ord_term(col) engages. A pinned proconfig or a plpgsql body // would break the inline chain. let rows: Vec<(String, String, String, Option>)> = sqlx::query_as( r#" @@ -368,9 +368,9 @@ async fn ord_ore_wrappers_are_inlinable(pool: PgPool) -> Result<()> { ); } - // eql_v2.ord must be IMMUTABLE (functional-index requirement) in + // eql_v2.ord_term must be IMMUTABLE (functional-index requirement) in // every spike outcome. The spike (Task 2) fixed its LANGUAGE as sql, - // so a LANGUAGE sql eql_v2.ord must additionally have no proconfig + // so a LANGUAGE sql eql_v2.ord_term must additionally have no proconfig // (it must inline); a LANGUAGE plpgsql ord is exempt from that check. let ord: Vec<(String, String, Option>)> = sqlx::query_as( r#" @@ -378,18 +378,18 @@ async fn ord_ore_wrappers_are_inlinable(pool: PgPool) -> Result<()> { FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace JOIN pg_catalog.pg_language l ON l.oid = p.prolang - WHERE n.nspname = 'eql_v2' AND p.proname = 'ord' + WHERE n.nspname = 'eql_v2' AND p.proname = 'ord_term' "#, ) .fetch_all(&pool) .await?; - assert!(!ord.is_empty(), "eql_v2.ord must exist"); + assert!(!ord.is_empty(), "eql_v2.ord_term must exist"); for (lang, volatile, config) in &ord { - assert_eq!(volatile, "i", "eql_v2.ord must be IMMUTABLE"); + assert_eq!(volatile, "i", "eql_v2.ord_term must be IMMUTABLE"); if lang == "sql" { assert!( config.is_none(), - "a LANGUAGE sql eql_v2.ord must have no pinned search_path so it inlines" + "a LANGUAGE sql eql_v2.ord_term must have no pinned search_path so it inlines" ); } } @@ -400,7 +400,7 @@ async fn ord_ore_wrappers_are_inlinable(pool: PgPool) -> Result<()> { /// /// The `_ord_ore` variant (scheme-explicit) and the `_ord` variant (the /// D-E fallback concrete domain) are deliberate twins: the same -/// `eql_v2.ord` extractor, the 18 comparison wrappers, the blockers, and +/// `eql_v2.ord_term` extractor, the 18 comparison wrappers, the blockers, and /// the operator declarations, differing only by the /// `eql_v2_int4_ord_ore` <-> `eql_v2_int4_ord` type-name swap. A full /// de-duplication refactor is out of scope for this branch, so this test @@ -437,7 +437,7 @@ fn ordered_int4_domain_files_stay_in_sync() { .replace("eql_v2_int4_ord", "ORDTYPE") } - // Functions: executable body starts at the eql_v2.ord extractor. + // Functions: executable body starts at the eql_v2.ord_term extractor. assert_eq!( body( "int4_ord_ore_functions.sql", From 367771f020746e9ac3d69520999bc20a1bfc077d Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 15:02:57 +1000 Subject: [PATCH 06/13] feat(encrypted_int4): CHECK constraints on the int4 domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four eql_v2_int4* domains were bare CREATE DOMAIN ... AS jsonb with no validation — a column declared eql_v2_int4_ord could hold a payload missing `ob`, surfacing only at query time. Each domain now carries a CHECK requiring a jsonb object with the EQL envelope (`v`, `i`), the ciphertext (`c`), and the variant's index term(s) — `hm` for eql_v2_int4_eq, `ob` for the ordered variants. A malformed payload is rejected when it enters the domain: on INSERT/UPDATE and on every jsonb::eql_v2_int4* cast. Native jsonb operators only (no eql_v2 function calls), so the CHECK survives `eql_v2` uninstall — mirrors the eql_v2.ste_vec_entry domain-CHECK precedent. The family is unreleased, so the CHECK ships in the initial CREATE DOMAIN; no ALTER DOMAIN migration. Also fixes a pin_search_path bug: eql_v2.eq_term(eql_v2_int4_eq) was pinned because the `eq_term` allowlist clause was arg-type-restricted to the ste_vec overload. A pinned extractor does not inline. Broadened to a name-only match, mirroring the ord_term clause. Docs (v2.4 U-001, encrypted-int4 reference + walkthrough, CHANGELOG) reverse the former "domains do not enforce term presence" wording, and per-variant rejection tests are added. Bundles in-flight work on the same branch: COMMUTATOR / NEGATOR / RESTRICT / JOIN planner metadata on the int4 operator declarations and the accompanying inlinability / planner-metadata / scale test suites. Verified: mise run build, docs:validate (0 errors), full test suite (int4 suites 5+12+12+13, all green). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 +- .../int4/int4_eq_operators.sql | 19 +- .../int4/int4_ord_operators.sql | 91 +++++--- .../int4/int4_ord_ore_operators.sql | 91 +++++--- src/encrypted_domain/types.sql | 28 ++- tasks/pin_search_path.sql | 13 +- tests/sqlx/tests/encrypted_int4_eq_tests.rs | 207 ++++++++++++++++++ .../tests/encrypted_int4_ord_ore_tests.rs | 100 +++++++++ tests/sqlx/tests/encrypted_int4_ord_tests.rs | 196 +++++++++++++++++ tests/sqlx/tests/encrypted_int4_tests.rs | 24 ++ tests/sqlx/tests/lint_tests.rs | 45 ++++ 11 files changed, 747 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13065a41..962bc335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ The additive `eql_v2_int4` variant family targets `2.4.0`; see [`docs/upgrading/ ### Added -- **`eql_v2_int4` variant family — four capability-encoded domain types for encrypted `int4` columns.** Pick the variant whose operator surface matches the index terms your column carries: `eql_v2_int4` (storage only, every operator blocked — carries `c`), `eql_v2_int4_eq` (HMAC equality only — `=`, `<>` — carries `c`, `hm`), `eql_v2_int4_ord_ore` (equality + ORE-block ordering — `=`, `<>`, `<`, `<=`, `>`, `>=` — carries `c`, `ob`), or `eql_v2_int4_ord` (the recommended ordered name; the identical operator surface to `eql_v2_int4_ord_ore`). Each variant exposes a uniform index extractor — `eql_v2.eq_term(col)` for `eql_v2_int4_eq`, `eql_v2.ord_term(col)` for the ordered variants — and no index recipe needs a `::jsonb` cast. Ordered columns share one functional btree across equality and range, `CREATE INDEX ... USING btree (eql_v2.ord_term(col))`, with `ORDER BY eql_v2.ord_term(col)` sorting in plaintext order; `eql_v2_int4_eq` indexes `eql_v2.eq_term(col)` with `USING hash` or `USING btree`. `eql_v2.ord_term` returns the internal `eql_v2.ore_block_u64_8_256` composite, which carries EQL's existing `DEFAULT` btree operator class, so no operator class is defined on the public domain types. The ordered variants do not carry an `hm` term: ORE on a full-domain `int4` is lossless, so the order term doubles as an exact equality term. All variants live in `public` and survive `eql_v2` uninstall. Per-variant payload requirements and index recipes: [U-001](docs/upgrading/v2.4.md#u-001-eql_v2_int4-variant-family). Note: the ORE operator class is excluded from the Supabase build, so ordered `int4` columns fall back to seq-scan for range on Supabase. ([#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) +- **`eql_v2_int4` variant family — four capability-encoded domain types for encrypted `int4` columns.** Pick the variant whose operator surface matches the index terms your column carries: `eql_v2_int4` (storage only, every operator blocked — carries `c`), `eql_v2_int4_eq` (HMAC equality only — `=`, `<>` — carries `c`, `hm`), `eql_v2_int4_ord_ore` (equality + ORE-block ordering — `=`, `<>`, `<`, `<=`, `>`, `>=` — carries `c`, `ob`), or `eql_v2_int4_ord` (the recommended ordered name; the identical operator surface to `eql_v2_int4_ord_ore`). Each variant exposes a uniform index extractor — `eql_v2.eq_term(col)` for `eql_v2_int4_eq`, `eql_v2.ord_term(col)` for the ordered variants — and no index recipe needs a `::jsonb` cast. Ordered columns share one functional btree across equality and range, `CREATE INDEX ... USING btree (eql_v2.ord_term(col))`, with `ORDER BY eql_v2.ord_term(col)` sorting in plaintext order; `eql_v2_int4_eq` indexes `eql_v2.eq_term(col)` with `USING hash` or `USING btree`. `eql_v2.ord_term` returns the internal `eql_v2.ore_block_u64_8_256` composite, which carries EQL's existing `DEFAULT` btree operator class, so no operator class is defined on the public domain types. The ordered variants do not carry an `hm` term: ORE on a full-domain `int4` is lossless, so the order term doubles as an exact equality term. All variants live in `public` and survive `eql_v2` uninstall. Each domain carries a `CHECK` constraint requiring the EQL envelope (`v`, `i`), the ciphertext (`c`), and the variant's index term(s), so a payload missing a required key is rejected on insert or cast rather than surfacing later at query time. Per-variant payload requirements and index recipes: [U-001](docs/upgrading/v2.4.md#u-001-eql_v2_int4-variant-family). Note: the ORE operator class is excluded from the Supabase build, so ordered `int4` columns fall back to seq-scan for range on Supabase. ([#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) ### Upgrade notes diff --git a/src/encrypted_domain/int4/int4_eq_operators.sql b/src/encrypted_domain/int4/int4_eq_operators.sql index 0234381c..ec4ed7d0 100644 --- a/src/encrypted_domain/int4/int4_eq_operators.sql +++ b/src/encrypted_domain/int4/int4_eq_operators.sql @@ -10,38 +10,43 @@ --! `<>` is supported but is a seq-scan (btree supports only equality). --! All other operators raise. Payload-term assumption: `c`, `hm`. --- Operator declarations +-- Operator declarations. +-- +-- COMMUTATOR lets the planner normalise `$1 = col` to `col = $1`; +-- NEGATOR drives `NOT (...)` simplification. These wrappers inline to +-- the hmac-256 equality before index matching, so the metadata is for +-- plan-quality completeness, not index engagement. CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_eq_eq, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_eq_eq, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_eq_eq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_eq_neq, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_eq_neq, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_eq_neq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR < ( diff --git a/src/encrypted_domain/int4/int4_ord_operators.sql b/src/encrypted_domain/int4/int4_ord_operators.sql index 8168104f..1c2c8546 100644 --- a/src/encrypted_domain/int4/int4_ord_operators.sql +++ b/src/encrypted_domain/int4/int4_ord_operators.sql @@ -17,79 +17,120 @@ --! eql_v2_int4_ord_ore is the scheme-explicit ordered domain with the --! identical operator surface. --- Operator declarations +-- Operator declarations. +-- +-- COMMUTATOR lets the planner normalise `$1 < col` to `col > $1`; +-- NEGATOR drives `NOT (...)` simplification. These wrappers inline to +-- the ORE-block composite operators before index matching, so the +-- metadata is for plan-quality completeness, not index engagement. CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_ord_eq, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_ord_eq, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_ord_eq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_ord_neq, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_ord_neq, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_ord_neq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR < ( FUNCTION = eql_v2.eql_v2_int4_ord_lt, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, + COMMUTATOR = >, NEGATOR = >=, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR < ( + FUNCTION = eql_v2.eql_v2_int4_ord_lt, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, + COMMUTATOR = >, NEGATOR = >=, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR < ( + FUNCTION = eql_v2.eql_v2_int4_ord_lt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); -CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_ord_lt, - LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); -CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_ord_lt, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); CREATE OPERATOR <= ( FUNCTION = eql_v2.eql_v2_int4_ord_lte, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, - RESTRICT = scalarltsel, JOIN = scalarltjoinsel + COMMUTATOR = >=, NEGATOR = >, + RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); +CREATE OPERATOR <= ( + FUNCTION = eql_v2.eql_v2_int4_ord_lte, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, + COMMUTATOR = >=, NEGATOR = >, + RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); +CREATE OPERATOR <= ( + FUNCTION = eql_v2.eql_v2_int4_ord_lte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, + COMMUTATOR = >=, NEGATOR = >, + RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); -CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_ord_lte, - LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); -CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_ord_lte, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); CREATE OPERATOR > ( FUNCTION = eql_v2.eql_v2_int4_ord_gt, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, + COMMUTATOR = <, NEGATOR = <=, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR > ( + FUNCTION = eql_v2.eql_v2_int4_ord_gt, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, + COMMUTATOR = <, NEGATOR = <=, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR > ( + FUNCTION = eql_v2.eql_v2_int4_ord_gt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); -CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_ord_gt, - LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); -CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_ord_gt, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); CREATE OPERATOR >= ( FUNCTION = eql_v2.eql_v2_int4_ord_gte, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, - RESTRICT = scalargtsel, JOIN = scalargtjoinsel + COMMUTATOR = <=, NEGATOR = <, + RESTRICT = scalargesel, JOIN = scalargejoinsel +); +CREATE OPERATOR >= ( + FUNCTION = eql_v2.eql_v2_int4_ord_gte, + LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, + COMMUTATOR = <=, NEGATOR = <, + RESTRICT = scalargesel, JOIN = scalargejoinsel +); +CREATE OPERATOR >= ( + FUNCTION = eql_v2.eql_v2_int4_ord_gte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, + COMMUTATOR = <=, NEGATOR = <, + RESTRICT = scalargesel, JOIN = scalargejoinsel ); -CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_ord_gte, - LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); -CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_ord_gte, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_like, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); diff --git a/src/encrypted_domain/int4/int4_ord_ore_operators.sql b/src/encrypted_domain/int4/int4_ord_ore_operators.sql index ff02898a..36fe3ac0 100644 --- a/src/encrypted_domain/int4/int4_ord_ore_operators.sql +++ b/src/encrypted_domain/int4/int4_ord_ore_operators.sql @@ -14,79 +14,120 @@ --! operator surface; the §8 spike showed a domain-over-domain alias --! does not transparently inherit the operator surface (D-E fallback). --- Operator declarations +-- Operator declarations. +-- +-- COMMUTATOR lets the planner normalise `$1 < col` to `col > $1`; +-- NEGATOR drives `NOT (...)` simplification. These wrappers inline to +-- the ORE-block composite operators before index matching, so the +-- metadata is for plan-quality completeness, not index engagement. CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, - NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel + COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, - NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel + COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR < ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, + COMMUTATOR = >, NEGATOR = >=, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR < ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = >, NEGATOR = >=, + RESTRICT = scalarltsel, JOIN = scalarltjoinsel +); +CREATE OPERATOR < ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, + COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); -CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, - LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); -CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); CREATE OPERATOR <= ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, - RESTRICT = scalarltsel, JOIN = scalarltjoinsel + COMMUTATOR = >=, NEGATOR = >, + RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); +CREATE OPERATOR <= ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = >=, NEGATOR = >, + RESTRICT = scalarlesel, JOIN = scalarlejoinsel +); +CREATE OPERATOR <= ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, + COMMUTATOR = >=, NEGATOR = >, + RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); -CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, - LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); -CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); CREATE OPERATOR > ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, + COMMUTATOR = <, NEGATOR = <=, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR > ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = <, NEGATOR = <=, + RESTRICT = scalargtsel, JOIN = scalargtjoinsel +); +CREATE OPERATOR > ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, + COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); -CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, - LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); -CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); CREATE OPERATOR >= ( FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, - RESTRICT = scalargtsel, JOIN = scalargtjoinsel + COMMUTATOR = <=, NEGATOR = <, + RESTRICT = scalargesel, JOIN = scalargejoinsel +); +CREATE OPERATOR >= ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, + LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, + COMMUTATOR = <=, NEGATOR = <, + RESTRICT = scalargesel, JOIN = scalargejoinsel +); +CREATE OPERATOR >= ( + FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, + LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, + COMMUTATOR = <=, NEGATOR = <, + RESTRICT = scalargesel, JOIN = scalargejoinsel ); -CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, - LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); -CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_like, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); diff --git a/src/encrypted_domain/types.sql b/src/encrypted_domain/types.sql index a49d581a..ebfb9aba 100644 --- a/src/encrypted_domain/types.sql +++ b/src/encrypted_domain/types.sql @@ -38,31 +38,46 @@ DO $$ BEGIN --! @brief Storage-only encrypted int4 domain (jsonb-backed). Every --! operator is a blocker; carries ciphertext (`c`) only. + --! A CHECK constraint requires the `v`, `i`, `c` payload keys. IF NOT EXISTS ( SELECT 1 FROM pg_type WHERE typname = 'eql_v2_int4' AND typnamespace = 'public'::regnamespace ) THEN - CREATE DOMAIN public.eql_v2_int4 AS jsonb; + CREATE DOMAIN public.eql_v2_int4 AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' + ); END IF; --! @brief Equality-only encrypted int4 domain (jsonb-backed). --! Supports = and <> via HMAC-256; carries `c`, `hm`. + --! A CHECK constraint requires the `v`, `i`, `c`, `hm` payload keys. IF NOT EXISTS ( SELECT 1 FROM pg_type WHERE typname = 'eql_v2_int4_eq' AND typnamespace = 'public'::regnamespace ) THEN - CREATE DOMAIN public.eql_v2_int4_eq AS jsonb; + CREATE DOMAIN public.eql_v2_int4_eq AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'hm' + ); END IF; --! @brief Scheme-explicit ordered encrypted int4 domain (jsonb-backed). --! Supports = <> < <= > >= via the ORE-block term; carries --! `c`, `ob`. Carries the eql_v2.ord_term extractor, the comparison --! wrappers, the operator declarations, and the blockers. + --! A CHECK constraint requires the `v`, `i`, `c`, `ob` payload keys. IF NOT EXISTS ( SELECT 1 FROM pg_type WHERE typname = 'eql_v2_int4_ord_ore' AND typnamespace = 'public'::regnamespace ) THEN - CREATE DOMAIN public.eql_v2_int4_ord_ore AS jsonb; + CREATE DOMAIN public.eql_v2_int4_ord_ore AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'ob' + ); END IF; --! @brief Ordered encrypted int4 domain — the recommended ordered @@ -71,11 +86,16 @@ BEGIN --! not transparently inherit the operator surface (spike §8). --! Supports = <> < <= > >= via the ORE-block term; carries --! `c`, `ob`. + --! A CHECK constraint requires the `v`, `i`, `c`, `ob` payload keys. IF NOT EXISTS ( SELECT 1 FROM pg_type WHERE typname = 'eql_v2_int4_ord' AND typnamespace = 'public'::regnamespace ) THEN - CREATE DOMAIN public.eql_v2_int4_ord AS jsonb; + CREATE DOMAIN public.eql_v2_int4_ord AS jsonb + CHECK ( + jsonb_typeof(VALUE) = 'object' + AND VALUE ? 'v' AND VALUE ? 'i' AND VALUE ? 'c' AND VALUE ? 'ob' + ); END IF; END $$; diff --git a/tasks/pin_search_path.sql b/tasks/pin_search_path.sql index 47822a73..2d050bb4 100644 --- a/tasks/pin_search_path.sql +++ b/tasks/pin_search_path.sql @@ -215,13 +215,12 @@ BEGIN OR p.proargtypes[1] = (SELECT t.oid FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = 'pg_catalog' AND t.typname = 'int4'))) - -- XOR-aware equality term extractor on a ste_vec entry. Must - -- inline so `eql_v2.eq_term(col -> 'sel')` folds into the - -- calling query and matches a functional hash index built on - -- the same expression. - OR (p.pronargs = 1 - AND p.proname = 'eq_term' - AND p.proargtypes[0] = entry_oid) + -- Equality-term extractors — `eq_term` on a ste_vec entry + -- (XOR-aware) and on eql_v2_int4_eq. Must inline so + -- `eql_v2.eq_term(col)` folds into the calling query and matches + -- a functional index built on the same expression. Name-only + -- match (any arity-1 overload), mirroring the `ord_term` clause. + OR (p.pronargs = 1 AND p.proname = 'eq_term') -- Type-safe `@>` / `<@` overloads with typed needles -- (`stevec_query`, `ste_vec_entry`). Inline to the existing -- `ste_vec_contains` machinery — must stay unpinned to engage diff --git a/tests/sqlx/tests/encrypted_int4_eq_tests.rs b/tests/sqlx/tests/encrypted_int4_eq_tests.rs index 97b4d70d..d2175e45 100644 --- a/tests/sqlx/tests/encrypted_int4_eq_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_eq_tests.rs @@ -275,3 +275,210 @@ async fn eq_null_operand_yields_null(pool: PgPool) -> Result<()> { } Ok(()) } + +#[sqlx::test] +async fn eq_engages_btree_constant_on_left(pool: PgPool) -> Result<()> { + // The functional btree must engage when the literal is on the LEFT + // (`$1 = col`) as well as the right — the commuted shape ORMs and + // PostgREST emit. `=` is its own commutator. + let mut tx = pool.begin().await?; + setup_eq_table(&mut tx, &["aaa", "bbb", "ccc"]).await?; + + sqlx::query( + "CREATE INDEX typed_int4_eq_cl_idx \ + ON typed_int4_eq USING btree (eql_v2.eq_term(value))", + ) + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE typed_int4_eq") + .execute(&mut *tx) + .await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let needle = payload("bbb"); + for sql in [ + format!( + "EXPLAIN SELECT * FROM typed_int4_eq \ + WHERE '{needle}'::jsonb::eql_v2_int4_eq = value" + ), + format!("EXPLAIN SELECT * FROM typed_int4_eq WHERE '{needle}'::jsonb = value"), + ] { + let plan: Vec = sqlx::query_scalar(&sql).fetch_all(&mut *tx).await?; + let plan_text = plan.join("\n"); + assert!( + plan_text.contains("typed_int4_eq_cl_idx"), + "constant-on-left = must engage the eql_v2.eq_term btree; \ + sql: {sql}\nplan:\n{plan_text}" + ); + } + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn eq_operators_declare_planner_metadata(pool: PgPool) -> Result<()> { + // The real = / <> operators on eql_v2_int4_eq must declare + // COMMUTATOR, NEGATOR, and selectivity estimators (RESTRICT / JOIN) + // on all three arg-shapes, so the planner can normalise and cost + // commuted and negated predicates. + let rows: Vec<(String, String, String, bool, bool, bool, bool)> = sqlx::query_as( + r#" + SELECT o.oprname, + lt.typname AS lhs, + rt.typname AS rhs, + o.oprcom <> 0 AS has_commutator, + o.oprnegate <> 0 AS has_negator, + o.oprrest::oid <> 0 AS has_restrict, + o.oprjoin::oid <> 0 AS has_join + FROM pg_catalog.pg_operator o + JOIN pg_catalog.pg_type lt ON lt.oid = o.oprleft + JOIN pg_catalog.pg_type rt ON rt.oid = o.oprright + WHERE o.oprname IN ('=', '<>') + AND (lt.typname = 'eql_v2_int4_eq' OR rt.typname = 'eql_v2_int4_eq') + "#, + ) + .fetch_all(&pool) + .await?; + + assert_eq!( + rows.len(), + 6, + "expected = and <> x 3 arg-shapes on eql_v2_int4_eq" + ); + for (op, lhs, rhs, has_com, has_neg, has_rest, has_join) in &rows { + assert!( + has_com, + "operator {op}({lhs},{rhs}) must declare COMMUTATOR" + ); + assert!(has_neg, "operator {op}({lhs},{rhs}) must declare NEGATOR"); + assert!(has_rest, "operator {op}({lhs},{rhs}) must declare RESTRICT"); + assert!(has_join, "operator {op}({lhs},{rhs}) must declare JOIN"); + } + Ok(()) +} + +#[sqlx::test] +async fn eq_wrappers_are_inlinable(pool: PgPool) -> Result<()> { + // The = / <> wrappers on eql_v2_int4_eq must be LANGUAGE sql, + // IMMUTABLE, and carry no pinned search_path, so the planner inlines + // `col = $1` to `eql_v2.eq_term(col) = eql_v2.eq_term($1)` and the + // functional index on eql_v2.eq_term(col) engages. A pinned + // proconfig or a plpgsql body would break the inline chain. + let rows: Vec<(String, String, String, Option>)> = sqlx::query_as( + r#" + SELECT p.proname, l.lanname, p.provolatile::text, p.proconfig + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + JOIN pg_catalog.pg_language l ON l.oid = p.prolang + WHERE n.nspname = 'eql_v2' + AND p.proname IN ('eql_v2_int4_eq_eq', 'eql_v2_int4_eq_neq') + "#, + ) + .fetch_all(&pool) + .await?; + + // 2 wrapper names x 3 arg-shapes = 6 rows. + assert_eq!(rows.len(), 6, "expected 6 equality wrapper overloads"); + for (name, lang, volatile, config) in &rows { + assert_eq!(lang, "sql", "{name} must be LANGUAGE sql to inline"); + assert_eq!(volatile, "i", "{name} must be IMMUTABLE"); + assert!( + config.is_none(), + "{name} must have no pinned search_path (proconfig)" + ); + } + + // The eql_v2.eq_term index extractor must be IMMUTABLE — a + // functional index expression requires it. + let eq_term: Vec<(String, String, Option>)> = sqlx::query_as( + r#" + SELECT l.lanname, p.provolatile::text, p.proconfig + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace + JOIN pg_catalog.pg_language l ON l.oid = p.prolang + WHERE n.nspname = 'eql_v2' AND p.proname = 'eq_term' + "#, + ) + .fetch_all(&pool) + .await?; + assert!(!eq_term.is_empty(), "eql_v2.eq_term must exist"); + for (lang, volatile, config) in &eq_term { + assert_eq!(volatile, "i", "eql_v2.eq_term must be IMMUTABLE"); + if lang == "sql" { + assert!( + config.is_none(), + "a LANGUAGE sql eql_v2.eq_term must have no pinned search_path" + ); + } + } + Ok(()) +} + +#[sqlx::test] +async fn eq_btree_index_preferred_at_scale(pool: PgPool) -> Result<()> { + // The other EXPLAIN tests force `enable_seqscan = off`, proving the + // index is *usable*. This test proves the planner *prefers* it: at + // ~5000 rows with a highly selective `=` predicate, the functional + // btree must be chosen with seqscan left enabled. + let mut tx = pool.begin().await?; + sqlx::query("CREATE TEMP TABLE eq_scale (value eql_v2_int4_eq) ON COMMIT DROP") + .execute(&mut *tx) + .await?; + + let filler = payload("filler"); + let pivot = payload("pivot"); + sqlx::query( + "INSERT INTO eq_scale(value) \ + SELECT $1::jsonb::eql_v2_int4_eq FROM generate_series(1, 5000)", + ) + .bind(&filler) + .execute(&mut *tx) + .await?; + sqlx::query("INSERT INTO eq_scale(value) VALUES ($1::jsonb::eql_v2_int4_eq)") + .bind(&pivot) + .execute(&mut *tx) + .await?; + sqlx::query("CREATE INDEX eq_scale_idx ON eq_scale USING btree (eql_v2.eq_term(value))") + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE eq_scale").execute(&mut *tx).await?; + + let plan: Vec = sqlx::query_scalar(&format!( + "EXPLAIN SELECT * FROM eq_scale WHERE value = '{pivot}'::jsonb::eql_v2_int4_eq" + )) + .fetch_all(&mut *tx) + .await?; + let plan_text = plan.join("\n"); + assert!( + plan_text.contains("eq_scale_idx"), + "with seqscan enabled the planner must prefer the eql_v2.eq_term \ + btree for a selective = ; plan:\n{plan_text}" + ); + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn eq_rejects_payload_missing_required_keys(pool: PgPool) -> Result<()> { + // The eql_v2_int4_eq domain CHECK requires v, i, c, hm. A payload + // missing any required key is rejected at the cast. + for (label, json) in [ + ("missing hm", r#"{"v":2,"i":{"t":"t","c":"c"},"c":"x"}"#), + ("missing c", r#"{"v":2,"i":{"t":"t","c":"c"},"hm":"aa"}"#), + ] { + let err = sqlx::query(&format!("SELECT '{json}'::jsonb::eql_v2_int4_eq")) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_eq must reject payload: {label}")) + .to_string(); + assert!( + err.contains("violates check constraint"), + "{label}: expected a check-constraint violation, got: {err}" + ); + } + Ok(()) +} diff --git a/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs index 9d936c1e..a135c84a 100644 --- a/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs @@ -540,3 +540,103 @@ async fn encrypted_int4_ord_ore_equality_uses_ob_not_hm(pool: PgPool) -> Result< tx.commit().await?; Ok(()) } + +#[sqlx::test] +async fn ord_ore_functional_index_serves_constant_on_left(pool: PgPool) -> Result<()> { + // The functional btree on eql_v2.ord_term(col) must engage when the + // literal is on the LEFT (`$1 < col`) — the commuted shape — for an + // eql_v2_int4_ord_ore column, in both the (domain, domain) and + // (jsonb, domain) operator forms. + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE ord_ore_cl (\ + plaintext integer, value eql_v2_int4_ord_ore\ + ) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_ore_cl(plaintext, value) \ + SELECT plaintext, payload::eql_v2_int4_ord_ore FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + sqlx::query("CREATE INDEX ord_ore_cl_idx ON ord_ore_cl USING btree (eql_v2.ord_term(value))") + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE ord_ore_cl").execute(&mut *tx).await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let pivot: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 10", + ) + .fetch_one(&mut *tx) + .await?; + let lit = pivot.replace('\'', "''"); + + // Pivot 10 on the LEFT — the expected set is the commuted operator's + // ground truth (`10 < value` selects rows where value > 10). + let cases: &[(&str, Vec)] = &[ + ("=", vec![10]), + ("<", vec![17, 25, 42, 50, 100, 250, 1000, 9999]), + ("<=", vec![10, 17, 25, 42, 50, 100, 250, 1000, 9999]), + (">", vec![-100, -1, 1, 2, 5]), + (">=", vec![-100, -1, 1, 2, 5, 10]), + ]; + for (op, expected) in cases { + for rhs_cast in ["::eql_v2_int4_ord_ore", ""] { + let predicate = format!("'{lit}'::jsonb{rhs_cast} {op} value"); + let plan: Vec = sqlx::query_scalar(&format!( + "EXPLAIN SELECT * FROM ord_ore_cl WHERE {predicate}" + )) + .fetch_all(&mut *tx) + .await?; + let plan_text = plan.join("\n"); + assert!( + plan_text.contains("ord_ore_cl_idx"), + "constant-on-left {op} must engage the functional btree; \ + predicate={predicate}\nplan:\n{plan_text}" + ); + + let mut ids: Vec = sqlx::query_scalar(&format!( + "SELECT plaintext FROM ord_ore_cl WHERE {predicate}" + )) + .fetch_all(&mut *tx) + .await?; + ids.sort(); + let mut want = expected.clone(); + want.sort(); + assert_eq!( + ids, want, + "constant-on-left {op} must match commuted ground truth; \ + predicate={predicate}" + ); + } + } + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn ord_ore_rejects_payload_missing_required_keys(pool: PgPool) -> Result<()> { + // The eql_v2_int4_ord_ore domain CHECK requires v, i, c, ob. A + // payload missing any required key is rejected at the cast. + for (label, json) in [ + ("missing ob", r#"{"v":2,"i":{"t":"t","c":"c"},"c":"x"}"#), + ("missing c", r#"{"v":2,"i":{"t":"t","c":"c"},"ob":["aa"]}"#), + ] { + let err = sqlx::query(&format!("SELECT '{json}'::jsonb::eql_v2_int4_ord_ore")) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_ord_ore must reject payload: {label}")) + .to_string(); + assert!( + err.contains("violates check constraint"), + "{label}: expected a check-constraint violation, got: {err}" + ); + } + Ok(()) +} diff --git a/tests/sqlx/tests/encrypted_int4_ord_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_tests.rs index 30acd627..423e7dde 100644 --- a/tests/sqlx/tests/encrypted_int4_ord_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_ord_tests.rs @@ -461,3 +461,199 @@ fn ordered_int4_domain_files_stay_in_sync() { only) below the file header; mirror every change into both files." ); } + +#[sqlx::test] +async fn ord_functional_index_serves_constant_on_left(pool: PgPool) -> Result<()> { + // The functional btree on eql_v2.ord_term(col) must engage when the + // literal is on the LEFT (`$1 < col`) — the commuted shape — for + // both the (domain, domain) and (jsonb, domain) operator forms. + // `$1 < col` resolves through COMMUTATOR to `col > $1` for index + // matching. + let mut tx = pool.begin().await?; + sqlx::query( + "CREATE TEMP TABLE ord_cl (plaintext integer, value eql_v2_int4_ord) ON COMMIT DROP", + ) + .execute(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_cl(plaintext, value) \ + SELECT plaintext, payload::eql_v2_int4_ord FROM encrypted_int4_plaintext", + ) + .execute(&mut *tx) + .await?; + sqlx::query("CREATE INDEX ord_cl_idx ON ord_cl USING btree (eql_v2.ord_term(value))") + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE ord_cl").execute(&mut *tx).await?; + sqlx::query("SET LOCAL enable_seqscan = off") + .execute(&mut *tx) + .await?; + + let pivot: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 10", + ) + .fetch_one(&mut *tx) + .await?; + let lit = pivot.replace('\'', "''"); + + // Pivot 10 on the LEFT — the expected set is the commuted operator's + // ground truth (`10 < value` selects rows where value > 10). + let cases: &[(&str, Vec)] = &[ + ("=", vec![10]), + ("<", vec![17, 25, 42, 50, 100, 250, 1000, 9999]), + ("<=", vec![10, 17, 25, 42, 50, 100, 250, 1000, 9999]), + (">", vec![-100, -1, 1, 2, 5]), + (">=", vec![-100, -1, 1, 2, 5, 10]), + ]; + for (op, expected) in cases { + for rhs_cast in ["::eql_v2_int4_ord", ""] { + let predicate = format!("'{lit}'::jsonb{rhs_cast} {op} value"); + let plan: Vec = + sqlx::query_scalar(&format!("EXPLAIN SELECT * FROM ord_cl WHERE {predicate}")) + .fetch_all(&mut *tx) + .await?; + let plan_text = plan.join("\n"); + assert!( + plan_text.contains("ord_cl_idx"), + "constant-on-left {op} must engage the functional btree; \ + predicate={predicate}\nplan:\n{plan_text}" + ); + + let mut ids: Vec = + sqlx::query_scalar(&format!("SELECT plaintext FROM ord_cl WHERE {predicate}")) + .fetch_all(&mut *tx) + .await?; + ids.sort(); + let mut want = expected.clone(); + want.sort(); + assert_eq!( + ids, want, + "constant-on-left {op} must match commuted ground truth; \ + predicate={predicate}" + ); + } + } + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn ord_operators_declare_planner_metadata(pool: PgPool) -> Result<()> { + // The real comparison operators on the ordered int4 domains + // (eql_v2_int4_ord and eql_v2_int4_ord_ore) must declare COMMUTATOR, + // NEGATOR, and selectivity estimators (RESTRICT / JOIN) on all three + // arg-shapes, so the planner can normalise and cost commuted and + // negated predicates. + let rows: Vec<(String, String, String, bool, bool, bool, bool)> = sqlx::query_as( + r#" + SELECT o.oprname, + lt.typname AS lhs, + rt.typname AS rhs, + o.oprcom <> 0 AS has_commutator, + o.oprnegate <> 0 AS has_negator, + o.oprrest::oid <> 0 AS has_restrict, + o.oprjoin::oid <> 0 AS has_join + FROM pg_catalog.pg_operator o + JOIN pg_catalog.pg_type lt ON lt.oid = o.oprleft + JOIN pg_catalog.pg_type rt ON rt.oid = o.oprright + WHERE o.oprname IN ('=', '<>', '<', '<=', '>', '>=') + AND (lt.typname IN ('eql_v2_int4_ord', 'eql_v2_int4_ord_ore') + OR rt.typname IN ('eql_v2_int4_ord', 'eql_v2_int4_ord_ore')) + "#, + ) + .fetch_all(&pool) + .await?; + + // 6 operators x 3 arg-shapes x 2 ordered domains = 36 rows. + assert_eq!( + rows.len(), + 36, + "expected 6 operators x 3 arg-shapes x 2 ordered domains" + ); + for (op, lhs, rhs, has_com, has_neg, has_rest, has_join) in &rows { + assert!( + has_com, + "operator {op}({lhs},{rhs}) must declare COMMUTATOR" + ); + assert!(has_neg, "operator {op}({lhs},{rhs}) must declare NEGATOR"); + assert!(has_rest, "operator {op}({lhs},{rhs}) must declare RESTRICT"); + assert!(has_join, "operator {op}({lhs},{rhs}) must declare JOIN"); + } + Ok(()) +} + +#[sqlx::test] +async fn ord_functional_index_preferred_at_scale(pool: PgPool) -> Result<()> { + // The other EXPLAIN tests force `enable_seqscan = off`, proving the + // index is *usable*. This test proves the planner *prefers* it: at + // ~5000 rows with a highly selective `=` predicate, the functional + // btree must be chosen with seqscan left enabled. + let mut tx = pool.begin().await?; + sqlx::query("CREATE TEMP TABLE ord_scale (value eql_v2_int4_ord) ON COMMIT DROP") + .execute(&mut *tx) + .await?; + + let filler: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 5", + ) + .fetch_one(&mut *tx) + .await?; + let pivot: String = sqlx::query_scalar( + "SELECT payload::text FROM encrypted_int4_plaintext WHERE plaintext = 42", + ) + .fetch_one(&mut *tx) + .await?; + sqlx::query( + "INSERT INTO ord_scale(value) \ + SELECT $1::jsonb::eql_v2_int4_ord FROM generate_series(1, 5000)", + ) + .bind(&filler) + .execute(&mut *tx) + .await?; + sqlx::query("INSERT INTO ord_scale(value) VALUES ($1::jsonb::eql_v2_int4_ord)") + .bind(&pivot) + .execute(&mut *tx) + .await?; + sqlx::query("CREATE INDEX ord_scale_idx ON ord_scale USING btree (eql_v2.ord_term(value))") + .execute(&mut *tx) + .await?; + sqlx::query("ANALYZE ord_scale").execute(&mut *tx).await?; + + let lit = pivot.replace('\'', "''"); + let plan: Vec = sqlx::query_scalar(&format!( + "EXPLAIN SELECT * FROM ord_scale WHERE value = '{lit}'::jsonb::eql_v2_int4_ord" + )) + .fetch_all(&mut *tx) + .await?; + let plan_text = plan.join("\n"); + assert!( + plan_text.contains("ord_scale_idx"), + "with seqscan enabled the planner must prefer the eql_v2.ord_term \ + btree for a selective = ; plan:\n{plan_text}" + ); + + tx.commit().await?; + Ok(()) +} + +#[sqlx::test] +async fn ord_rejects_payload_missing_required_keys(pool: PgPool) -> Result<()> { + // The eql_v2_int4_ord domain CHECK requires v, i, c, ob. A payload + // missing any required key is rejected at the cast. + for (label, json) in [ + ("missing ob", r#"{"v":2,"i":{"t":"t","c":"c"},"c":"x"}"#), + ("missing c", r#"{"v":2,"i":{"t":"t","c":"c"},"ob":["aa"]}"#), + ] { + let err = sqlx::query(&format!("SELECT '{json}'::jsonb::eql_v2_int4_ord")) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4_ord must reject payload: {label}")) + .to_string(); + assert!( + err.contains("violates check constraint"), + "{label}: expected a check-constraint violation, got: {err}" + ); + } + Ok(()) +} diff --git a/tests/sqlx/tests/encrypted_int4_tests.rs b/tests/sqlx/tests/encrypted_int4_tests.rs index c4c6cdeb..8a2fed17 100644 --- a/tests/sqlx/tests/encrypted_int4_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_tests.rs @@ -137,3 +137,27 @@ async fn blocked_operators_raise_on_null_input(pool: PgPool) -> Result<()> { ); Ok(()) } + +#[sqlx::test] +async fn int4_rejects_invalid_payloads(pool: PgPool) -> Result<()> { + // The eql_v2_int4 domain CHECK requires a jsonb object carrying the + // EQL envelope (v, i) and the ciphertext (c). A payload missing a + // required key, or a non-object, is rejected at the cast. + for (label, json) in [ + ("missing c", r#"{"v":2,"i":{"t":"t","c":"c"}}"#), + ("missing v", r#"{"i":{"t":"t","c":"c"},"c":"x"}"#), + ("missing i", r#"{"v":2,"c":"x"}"#), + ("not an object", r#"["v","i","c"]"#), + ] { + let err = sqlx::query(&format!("SELECT '{json}'::jsonb::eql_v2_int4")) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4 must reject payload: {label}")) + .to_string(); + assert!( + err.contains("violates check constraint"), + "{label}: expected a check-constraint violation, got: {err}" + ); + } + Ok(()) +} diff --git a/tests/sqlx/tests/lint_tests.rs b/tests/sqlx/tests/lint_tests.rs index 1bd080c9..4972c34c 100644 --- a/tests/sqlx/tests/lint_tests.rs +++ b/tests/sqlx/tests/lint_tests.rs @@ -119,3 +119,48 @@ async fn lint_phase_1_operators_are_clean(pool: PgPool) -> Result<()> { ); Ok(()) } + +/// The real comparison operators on the `eql_v2_int4` variant family +/// (`=`, `<>` on `_eq`; `=`, `<>`, `<`, `<=`, `>`, `>=` on `_ord` and +/// `_ord_ore`) must report zero lint violations: they are inlinable +/// `LANGUAGE sql` wrappers, and a regression to plpgsql, VOLATILE, a +/// `SET` clause, or a non-inlinable callee would silently drop their +/// functional indexes to seq scan. The plpgsql blocker operators on the +/// same variants are intentionally non-inlinable and are excluded by +/// the variant-qualified prefixes. +#[sqlx::test] +async fn lint_int4_operators_are_clean(pool: PgPool) -> Result<()> { + let rows = fetch_lints(&pool).await?; + + // object_name is `operator (, ) -> ...`. A variant- + // qualified prefix excludes the storage-only eql_v2_int4 blockers: + // `operator =(eql_v2_int4,` does not match `..._eq` / `..._ord`. + let mut prefixes = vec![ + "operator =(eql_v2_int4_eq".to_string(), + "operator <>(eql_v2_int4_eq".to_string(), + "operator =(jsonb, eql_v2_int4_eq".to_string(), + "operator <>(jsonb, eql_v2_int4_eq".to_string(), + ]; + // `eql_v2_int4_ord` is a prefix of `eql_v2_int4_ord_ore`, so each + // entry covers both ordered variants. + for op in ["=", "<>", "<", "<=", ">", ">="] { + prefixes.push(format!("operator {op}(eql_v2_int4_ord")); + prefixes.push(format!("operator {op}(jsonb, eql_v2_int4_ord")); + } + + let violations: Vec<_> = rows + .iter() + .filter(|row| { + prefixes + .iter() + .any(|prefix| row.object_name.starts_with(prefix.as_str())) + }) + .collect(); + + assert!( + violations.is_empty(), + "eql_v2_int4 real operators should report zero lint violations, but got: {:#?}", + violations + ); + Ok(()) +} From a8edebb1cfb2fc18d84c240d25b17764fdcafc22 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 17:01:51 +1000 Subject: [PATCH 07/13] fix(encrypted_int4): correct <= / >= selectivity estimators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The <= operators on eql_v2_int4 and eql_v2_int4_eq declared scalarltsel / scalarltjoinsel, and >= declared scalargtsel / scalargtjoinsel — the strict less-/greater-than estimators, which give the planner skewed row-count estimates for range predicates. They now declare scalarlesel / scalarlejoinsel and scalargesel / scalargejoinsel. The eql_v2_int4_ord / eql_v2_int4_ord_ore variants were already correct. Mirrors the #217 fix on the eql_v2_encrypted operators. Resolves coderdan PR #225 review thread (int4 operator selectivity). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/encrypted_domain/int4/int4_eq_operators.sql | 4 ++-- src/encrypted_domain/int4/int4_operators.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/encrypted_domain/int4/int4_eq_operators.sql b/src/encrypted_domain/int4/int4_eq_operators.sql index ec4ed7d0..71c37c5a 100644 --- a/src/encrypted_domain/int4/int4_eq_operators.sql +++ b/src/encrypted_domain/int4/int4_eq_operators.sql @@ -62,7 +62,7 @@ CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_eq_lt, CREATE OPERATOR <= ( FUNCTION = eql_v2.eql_v2_int4_eq_lte, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, - RESTRICT = scalarltsel, JOIN = scalarltjoinsel + RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_eq_lte, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); @@ -82,7 +82,7 @@ CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_eq_gt, CREATE OPERATOR >= ( FUNCTION = eql_v2.eql_v2_int4_eq_gte, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, - RESTRICT = scalargtsel, JOIN = scalargtjoinsel + RESTRICT = scalargesel, JOIN = scalargejoinsel ); CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_eq_gte, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); diff --git a/src/encrypted_domain/int4/int4_operators.sql b/src/encrypted_domain/int4/int4_operators.sql index 6e25e7e5..9d7f3c3a 100644 --- a/src/encrypted_domain/int4/int4_operators.sql +++ b/src/encrypted_domain/int4/int4_operators.sql @@ -57,7 +57,7 @@ CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_lt, CREATE OPERATOR <= ( FUNCTION = eql_v2.eql_v2_int4_lte, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, - RESTRICT = scalarltsel, JOIN = scalarltjoinsel + RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_lte, LEFTARG = eql_v2_int4, RIGHTARG = jsonb); @@ -77,7 +77,7 @@ CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_gt, CREATE OPERATOR >= ( FUNCTION = eql_v2.eql_v2_int4_gte, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, - RESTRICT = scalargtsel, JOIN = scalargtjoinsel + RESTRICT = scalargesel, JOIN = scalargejoinsel ); CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_gte, LEFTARG = eql_v2_int4, RIGHTARG = jsonb); From b282bb739d028d8f2d77f0c7da3bd24d229c9435 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 17:27:06 +1000 Subject: [PATCH 08/13] refactor(encrypted_int4): converge operator functions to overloaded eql_v2. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The eql_v2_int4 variant family named its operator-backing functions per-type (eql_v2.eql_v2_int4_ord_lt, eql_v2.eql_v2_int4_eq_eq, ...). They are now overloaded eql_v2.eq / neq / lt / lte / gt / gte / contains / contained_by / "->" / "->>", discriminated by argument type — the scheme eql_v2.ste_vec_entry already uses. Argument types are unchanged; each operator keeps its three arg-shapes. pin_search_path.sql: the inline-critical wrapper allowlist was a name-only match, safe only while the per-type names were globally unique. The converged names now collide with the ste_vec_entry and eql_v2_encrypted overloads, so the int4 clauses are restricted by the int4 domain argument types (three domain OIDs resolved in the DO block). Only the real inlinable wrappers are allowlisted; the storage-variant and contains/contained_by/"->"/"->>" blockers stay pinned. splinter.sh: the 14 per-type int4 allowlist rows are removed; the existing eq/neq/lt/lte/gt/gte rows (splinter matches by name only) now also cover the converged int4 wrappers. Tests: the eq / ord catalog assertions filter by argument type so the converged names do not also count the ste_vec_entry / eql_v2_encrypted overloads. Resolves coderdan PR #225 review thread (per-type vs overloaded function naming). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../int4/int4_eq_functions.sql | 60 ++++++------ .../int4/int4_eq_operators.sql | 60 ++++++------ src/encrypted_domain/int4/int4_functions.sql | 60 ++++++------ src/encrypted_domain/int4/int4_operators.sql | 60 ++++++------ .../int4/int4_ord_functions.sql | 60 ++++++------ .../int4/int4_ord_operators.sql | 60 ++++++------ .../int4/int4_ord_ore_functions.sql | 60 ++++++------ .../int4/int4_ord_ore_operators.sql | 60 ++++++------ tasks/pin_search_path.sql | 93 ++++++++++++------- tasks/test/splinter.sh | 26 ++---- tests/sqlx/tests/encrypted_int4_eq_tests.rs | 5 +- tests/sqlx/tests/encrypted_int4_ord_tests.rs | 17 ++-- 12 files changed, 319 insertions(+), 302 deletions(-) diff --git a/src/encrypted_domain/int4/int4_eq_functions.sql b/src/encrypted_domain/int4/int4_eq_functions.sql index 55eafd07..3a7797a9 100644 --- a/src/encrypted_domain/int4/int4_eq_functions.sql +++ b/src/encrypted_domain/int4/int4_eq_functions.sql @@ -40,7 +40,7 @@ AS $$ SELECT eql_v2.hmac_256(a::jsonb) $$; --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a eql_v2_int4_eq, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.eq(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.eq_term(a) = eql_v2.eq_term(b) $$; @@ -48,7 +48,7 @@ AS $$ SELECT eql_v2.eq_term(a) = eql_v2.eq_term(b) $$; --! @param a eql_v2_int4_eq --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a eql_v2_int4_eq, b jsonb) +CREATE FUNCTION eql_v2.eq(a eql_v2_int4_eq, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.eq_term(a) = eql_v2.eq_term(b::eql_v2_int4_eq) $$; @@ -56,7 +56,7 @@ AS $$ SELECT eql_v2.eq_term(a) = eql_v2.eq_term(b::eql_v2_int4_eq) $$; --! @param a jsonb --! @param b eql_v2_int4_eq --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_eq_eq(a jsonb, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.eq(a jsonb, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.eq_term(a::eql_v2_int4_eq) = eql_v2.eq_term(b) $$; @@ -64,7 +64,7 @@ AS $$ SELECT eql_v2.eq_term(a::eql_v2_int4_eq) = eql_v2.eq_term(b) $$; --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a eql_v2_int4_eq, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.neq(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.eq_term(a) <> eql_v2.eq_term(b) $$; @@ -72,7 +72,7 @@ AS $$ SELECT eql_v2.eq_term(a) <> eql_v2.eq_term(b) $$; --! @param a eql_v2_int4_eq --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a eql_v2_int4_eq, b jsonb) +CREATE FUNCTION eql_v2.neq(a eql_v2_int4_eq, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.eq_term(a) <> eql_v2.eq_term(b::eql_v2_int4_eq) $$; @@ -80,7 +80,7 @@ AS $$ SELECT eql_v2.eq_term(a) <> eql_v2.eq_term(b::eql_v2_int4_eq) $$; --! @param a jsonb --! @param b eql_v2_int4_eq --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_eq_neq(a jsonb, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.neq(a jsonb, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.eq_term(a::eql_v2_int4_eq) <> eql_v2.eq_term(b) $$; @@ -90,7 +90,7 @@ AS $$ SELECT eql_v2.eq_term(a::eql_v2_int4_eq) <> eql_v2.eq_term(b) $$; --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_lt(a eql_v2_int4_eq, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.lt(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<'); END; $$ LANGUAGE plpgsql; @@ -99,7 +99,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_lt(a eql_v2_int4_eq, b jsonb) +CREATE FUNCTION eql_v2.lt(a eql_v2_int4_eq, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<'); END; $$ LANGUAGE plpgsql; @@ -108,7 +108,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_lt(a jsonb, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.lt(a jsonb, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<'); END; $$ LANGUAGE plpgsql; @@ -117,7 +117,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_lte(a eql_v2_int4_eq, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.lte(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<='); END; $$ LANGUAGE plpgsql; @@ -126,7 +126,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_lte(a eql_v2_int4_eq, b jsonb) +CREATE FUNCTION eql_v2.lte(a eql_v2_int4_eq, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<='); END; $$ LANGUAGE plpgsql; @@ -135,7 +135,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_lte(a jsonb, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.lte(a jsonb, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<='); END; $$ LANGUAGE plpgsql; @@ -144,7 +144,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_gt(a eql_v2_int4_eq, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.gt(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>'); END; $$ LANGUAGE plpgsql; @@ -153,7 +153,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_gt(a eql_v2_int4_eq, b jsonb) +CREATE FUNCTION eql_v2.gt(a eql_v2_int4_eq, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>'); END; $$ LANGUAGE plpgsql; @@ -162,7 +162,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_gt(a jsonb, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.gt(a jsonb, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>'); END; $$ LANGUAGE plpgsql; @@ -171,7 +171,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_gte(a eql_v2_int4_eq, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.gte(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>='); END; $$ LANGUAGE plpgsql; @@ -180,7 +180,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_gte(a eql_v2_int4_eq, b jsonb) +CREATE FUNCTION eql_v2.gte(a eql_v2_int4_eq, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>='); END; $$ LANGUAGE plpgsql; @@ -189,7 +189,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_gte(a jsonb, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.gte(a jsonb, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>='); END; $$ LANGUAGE plpgsql; @@ -252,7 +252,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_contains(a eql_v2_int4_eq, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.contains(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '@>'); END; $$ LANGUAGE plpgsql; @@ -261,7 +261,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_contains(a eql_v2_int4_eq, b jsonb) +CREATE FUNCTION eql_v2.contains(a eql_v2_int4_eq, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '@>'); END; $$ LANGUAGE plpgsql; @@ -270,7 +270,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_contains(a jsonb, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.contains(a jsonb, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '@>'); END; $$ LANGUAGE plpgsql; @@ -279,7 +279,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_contained_by(a eql_v2_int4_eq, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.contained_by(a eql_v2_int4_eq, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<@'); END; $$ LANGUAGE plpgsql; @@ -288,7 +288,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_contained_by(a eql_v2_int4_eq, b jsonb) +CREATE FUNCTION eql_v2.contained_by(a eql_v2_int4_eq, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<@'); END; $$ LANGUAGE plpgsql; @@ -297,7 +297,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_eq --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_contained_by(a jsonb, b eql_v2_int4_eq) +CREATE FUNCTION eql_v2.contained_by(a jsonb, b eql_v2_int4_eq) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '<@'); END; $$ LANGUAGE plpgsql; @@ -308,7 +308,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param selector text --! @return eql_v2_int4_eq (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow(a eql_v2_int4_eq, selector text) +CREATE FUNCTION eql_v2."->"(a eql_v2_int4_eq, selector text) RETURNS eql_v2_int4_eq IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_eq'; END; $$ LANGUAGE plpgsql; @@ -317,7 +317,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param selector integer --! @return eql_v2_int4_eq (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow(a eql_v2_int4_eq, selector integer) +CREATE FUNCTION eql_v2."->"(a eql_v2_int4_eq, selector integer) RETURNS eql_v2_int4_eq IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_eq'; END; $$ LANGUAGE plpgsql; @@ -326,7 +326,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param selector eql_v2_int4_eq --! @return eql_v2_int4_eq (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow(a jsonb, selector eql_v2_int4_eq) +CREATE FUNCTION eql_v2."->"(a jsonb, selector eql_v2_int4_eq) RETURNS eql_v2_int4_eq IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_eq'; END; $$ LANGUAGE plpgsql; @@ -335,7 +335,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param selector text --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow_text(a eql_v2_int4_eq, selector text) +CREATE FUNCTION eql_v2."->>"(a eql_v2_int4_eq, selector text) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_eq'; END; $$ LANGUAGE plpgsql; @@ -344,7 +344,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_eq --! @param selector integer --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow_text(a eql_v2_int4_eq, selector integer) +CREATE FUNCTION eql_v2."->>"(a eql_v2_int4_eq, selector integer) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_eq'; END; $$ LANGUAGE plpgsql; @@ -353,7 +353,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param selector eql_v2_int4_eq --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_arrow_text(a jsonb, selector eql_v2_int4_eq) +CREATE FUNCTION eql_v2."->>"(a jsonb, selector eql_v2_int4_eq) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_eq'; END; $$ LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_eq_operators.sql b/src/encrypted_domain/int4/int4_eq_operators.sql index 71c37c5a..36eb61cd 100644 --- a/src/encrypted_domain/int4/int4_eq_operators.sql +++ b/src/encrypted_domain/int4/int4_eq_operators.sql @@ -18,75 +18,75 @@ -- plan-quality completeness, not index engagement. CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_eq_eq, + FUNCTION = eql_v2.eq, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_eq_eq, + FUNCTION = eql_v2.eq, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_eq_eq, + FUNCTION = eql_v2.eq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_eq_neq, + FUNCTION = eql_v2.neq, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_eq_neq, + FUNCTION = eql_v2.neq, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_eq_neq, + FUNCTION = eql_v2.neq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR < ( - FUNCTION = eql_v2.eql_v2_int4_eq_lt, + FUNCTION = eql_v2.lt, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); -CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_eq_lt, +CREATE OPERATOR < (FUNCTION = eql_v2.lt, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); -CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_eq_lt, +CREATE OPERATOR < (FUNCTION = eql_v2.lt, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); CREATE OPERATOR <= ( - FUNCTION = eql_v2.eql_v2_int4_eq_lte, + FUNCTION = eql_v2.lte, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); -CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_eq_lte, +CREATE OPERATOR <= (FUNCTION = eql_v2.lte, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); -CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_eq_lte, +CREATE OPERATOR <= (FUNCTION = eql_v2.lte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); CREATE OPERATOR > ( - FUNCTION = eql_v2.eql_v2_int4_eq_gt, + FUNCTION = eql_v2.gt, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); -CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_eq_gt, +CREATE OPERATOR > (FUNCTION = eql_v2.gt, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); -CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_eq_gt, +CREATE OPERATOR > (FUNCTION = eql_v2.gt, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); CREATE OPERATOR >= ( - FUNCTION = eql_v2.eql_v2_int4_eq_gte, + FUNCTION = eql_v2.gte, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq, RESTRICT = scalargesel, JOIN = scalargejoinsel ); -CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_eq_gte, +CREATE OPERATOR >= (FUNCTION = eql_v2.gte, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); -CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_eq_gte, +CREATE OPERATOR >= (FUNCTION = eql_v2.gte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_eq_like, @@ -103,30 +103,30 @@ CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_eq_ilike, CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_eq_ilike, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_eq_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_eq_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_eq_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_eq_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_eq_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_eq_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = eql_v2_int4_eq, RIGHTARG = text); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = eql_v2_int4_eq, RIGHTARG = integer); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = eql_v2_int4_eq, RIGHTARG = text); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = eql_v2_int4_eq, RIGHTARG = integer); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_eq_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); diff --git a/src/encrypted_domain/int4/int4_functions.sql b/src/encrypted_domain/int4/int4_functions.sql index e7d22f76..4014e73e 100644 --- a/src/encrypted_domain/int4/int4_functions.sql +++ b/src/encrypted_domain/int4/int4_functions.sql @@ -16,7 +16,7 @@ --! @param a eql_v2_int4 --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq(a eql_v2_int4, b eql_v2_int4) +CREATE FUNCTION eql_v2.eq(a eql_v2_int4, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '='); END; $$ LANGUAGE plpgsql; @@ -25,7 +25,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq(a eql_v2_int4, b jsonb) +CREATE FUNCTION eql_v2.eq(a eql_v2_int4, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '='); END; $$ LANGUAGE plpgsql; @@ -34,7 +34,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq(a jsonb, b eql_v2_int4) +CREATE FUNCTION eql_v2.eq(a jsonb, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '='); END; $$ LANGUAGE plpgsql; @@ -43,7 +43,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_neq(a eql_v2_int4, b eql_v2_int4) +CREATE FUNCTION eql_v2.neq(a eql_v2_int4, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<>'); END; $$ LANGUAGE plpgsql; @@ -52,7 +52,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_neq(a eql_v2_int4, b jsonb) +CREATE FUNCTION eql_v2.neq(a eql_v2_int4, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<>'); END; $$ LANGUAGE plpgsql; @@ -61,7 +61,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_neq(a jsonb, b eql_v2_int4) +CREATE FUNCTION eql_v2.neq(a jsonb, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<>'); END; $$ LANGUAGE plpgsql; @@ -72,7 +72,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_lt(a eql_v2_int4, b eql_v2_int4) +CREATE FUNCTION eql_v2.lt(a eql_v2_int4, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<'); END; $$ LANGUAGE plpgsql; @@ -81,7 +81,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_lt(a eql_v2_int4, b jsonb) +CREATE FUNCTION eql_v2.lt(a eql_v2_int4, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<'); END; $$ LANGUAGE plpgsql; @@ -90,7 +90,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_lt(a jsonb, b eql_v2_int4) +CREATE FUNCTION eql_v2.lt(a jsonb, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<'); END; $$ LANGUAGE plpgsql; @@ -99,7 +99,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_lte(a eql_v2_int4, b eql_v2_int4) +CREATE FUNCTION eql_v2.lte(a eql_v2_int4, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<='); END; $$ LANGUAGE plpgsql; @@ -108,7 +108,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_lte(a eql_v2_int4, b jsonb) +CREATE FUNCTION eql_v2.lte(a eql_v2_int4, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<='); END; $$ LANGUAGE plpgsql; @@ -117,7 +117,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_lte(a jsonb, b eql_v2_int4) +CREATE FUNCTION eql_v2.lte(a jsonb, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<='); END; $$ LANGUAGE plpgsql; @@ -126,7 +126,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_gt(a eql_v2_int4, b eql_v2_int4) +CREATE FUNCTION eql_v2.gt(a eql_v2_int4, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>'); END; $$ LANGUAGE plpgsql; @@ -135,7 +135,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_gt(a eql_v2_int4, b jsonb) +CREATE FUNCTION eql_v2.gt(a eql_v2_int4, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>'); END; $$ LANGUAGE plpgsql; @@ -144,7 +144,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_gt(a jsonb, b eql_v2_int4) +CREATE FUNCTION eql_v2.gt(a jsonb, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>'); END; $$ LANGUAGE plpgsql; @@ -153,7 +153,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_gte(a eql_v2_int4, b eql_v2_int4) +CREATE FUNCTION eql_v2.gte(a eql_v2_int4, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>='); END; $$ LANGUAGE plpgsql; @@ -162,7 +162,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_gte(a eql_v2_int4, b jsonb) +CREATE FUNCTION eql_v2.gte(a eql_v2_int4, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>='); END; $$ LANGUAGE plpgsql; @@ -171,7 +171,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_gte(a jsonb, b eql_v2_int4) +CREATE FUNCTION eql_v2.gte(a jsonb, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>='); END; $$ LANGUAGE plpgsql; @@ -236,7 +236,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_contains(a eql_v2_int4, b eql_v2_int4) +CREATE FUNCTION eql_v2.contains(a eql_v2_int4, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '@>'); END; $$ LANGUAGE plpgsql; @@ -245,7 +245,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_contains(a eql_v2_int4, b jsonb) +CREATE FUNCTION eql_v2.contains(a eql_v2_int4, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '@>'); END; $$ LANGUAGE plpgsql; @@ -254,7 +254,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_contains(a jsonb, b eql_v2_int4) +CREATE FUNCTION eql_v2.contains(a jsonb, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '@>'); END; $$ LANGUAGE plpgsql; @@ -263,7 +263,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_contained_by(a eql_v2_int4, b eql_v2_int4) +CREATE FUNCTION eql_v2.contained_by(a eql_v2_int4, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<@'); END; $$ LANGUAGE plpgsql; @@ -272,7 +272,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_contained_by(a eql_v2_int4, b jsonb) +CREATE FUNCTION eql_v2.contained_by(a eql_v2_int4, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<@'); END; $$ LANGUAGE plpgsql; @@ -281,7 +281,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4 --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_contained_by(a jsonb, b eql_v2_int4) +CREATE FUNCTION eql_v2.contained_by(a jsonb, b eql_v2_int4) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '<@'); END; $$ LANGUAGE plpgsql; @@ -292,7 +292,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param selector text --! @return eql_v2_int4 (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_arrow(a eql_v2_int4, selector text) +CREATE FUNCTION eql_v2."->"(a eql_v2_int4, selector text) RETURNS eql_v2_int4 IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4'; END; $$ LANGUAGE plpgsql; @@ -301,7 +301,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param selector integer --! @return eql_v2_int4 (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_arrow(a eql_v2_int4, selector integer) +CREATE FUNCTION eql_v2."->"(a eql_v2_int4, selector integer) RETURNS eql_v2_int4 IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4'; END; $$ LANGUAGE plpgsql; @@ -310,7 +310,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param selector eql_v2_int4 --! @return eql_v2_int4 (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_arrow(a jsonb, selector eql_v2_int4) +CREATE FUNCTION eql_v2."->"(a jsonb, selector eql_v2_int4) RETURNS eql_v2_int4 IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4'; END; $$ LANGUAGE plpgsql; @@ -319,7 +319,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param selector text --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_arrow_text(a eql_v2_int4, selector text) +CREATE FUNCTION eql_v2."->>"(a eql_v2_int4, selector text) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4'; END; $$ LANGUAGE plpgsql; @@ -328,7 +328,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4 --! @param selector integer --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_arrow_text(a eql_v2_int4, selector integer) +CREATE FUNCTION eql_v2."->>"(a eql_v2_int4, selector integer) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4'; END; $$ LANGUAGE plpgsql; @@ -337,7 +337,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param selector eql_v2_int4 --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_arrow_text(a jsonb, selector eql_v2_int4) +CREATE FUNCTION eql_v2."->>"(a jsonb, selector eql_v2_int4) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4'; END; $$ LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_operators.sql b/src/encrypted_domain/int4/int4_operators.sql index 9d7f3c3a..6741224a 100644 --- a/src/encrypted_domain/int4/int4_operators.sql +++ b/src/encrypted_domain/int4/int4_operators.sql @@ -13,75 +13,75 @@ -- Operator declarations (10 symmetric ops × 3 shapes + 2 path ops × 3 asymmetric shapes) CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_eq, + FUNCTION = eql_v2.eq, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_eq, + FUNCTION = eql_v2.eq, LEFTARG = eql_v2_int4, RIGHTARG = jsonb, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_eq, + FUNCTION = eql_v2.eq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_neq, + FUNCTION = eql_v2.neq, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_neq, + FUNCTION = eql_v2.neq, LEFTARG = eql_v2_int4, RIGHTARG = jsonb, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_neq, + FUNCTION = eql_v2.neq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR < ( - FUNCTION = eql_v2.eql_v2_int4_lt, + FUNCTION = eql_v2.lt, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); -CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_lt, +CREATE OPERATOR < (FUNCTION = eql_v2.lt, LEFTARG = eql_v2_int4, RIGHTARG = jsonb); -CREATE OPERATOR < (FUNCTION = eql_v2.eql_v2_int4_lt, +CREATE OPERATOR < (FUNCTION = eql_v2.lt, LEFTARG = jsonb, RIGHTARG = eql_v2_int4); CREATE OPERATOR <= ( - FUNCTION = eql_v2.eql_v2_int4_lte, + FUNCTION = eql_v2.lte, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); -CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_lte, +CREATE OPERATOR <= (FUNCTION = eql_v2.lte, LEFTARG = eql_v2_int4, RIGHTARG = jsonb); -CREATE OPERATOR <= (FUNCTION = eql_v2.eql_v2_int4_lte, +CREATE OPERATOR <= (FUNCTION = eql_v2.lte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4); CREATE OPERATOR > ( - FUNCTION = eql_v2.eql_v2_int4_gt, + FUNCTION = eql_v2.gt, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); -CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_gt, +CREATE OPERATOR > (FUNCTION = eql_v2.gt, LEFTARG = eql_v2_int4, RIGHTARG = jsonb); -CREATE OPERATOR > (FUNCTION = eql_v2.eql_v2_int4_gt, +CREATE OPERATOR > (FUNCTION = eql_v2.gt, LEFTARG = jsonb, RIGHTARG = eql_v2_int4); CREATE OPERATOR >= ( - FUNCTION = eql_v2.eql_v2_int4_gte, + FUNCTION = eql_v2.gte, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4, RESTRICT = scalargesel, JOIN = scalargejoinsel ); -CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_gte, +CREATE OPERATOR >= (FUNCTION = eql_v2.gte, LEFTARG = eql_v2_int4, RIGHTARG = jsonb); -CREATE OPERATOR >= (FUNCTION = eql_v2.eql_v2_int4_gte, +CREATE OPERATOR >= (FUNCTION = eql_v2.gte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4); CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_like, @@ -98,30 +98,30 @@ CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ilike, CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ilike, LEFTARG = jsonb, RIGHTARG = eql_v2_int4); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4, RIGHTARG = jsonb); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = jsonb, RIGHTARG = eql_v2_int4); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = eql_v2_int4, RIGHTARG = jsonb); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = jsonb, RIGHTARG = eql_v2_int4); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = eql_v2_int4, RIGHTARG = text); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = eql_v2_int4, RIGHTARG = integer); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = jsonb, RIGHTARG = eql_v2_int4); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = eql_v2_int4, RIGHTARG = text); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = eql_v2_int4, RIGHTARG = integer); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = jsonb, RIGHTARG = eql_v2_int4); diff --git a/src/encrypted_domain/int4/int4_ord_functions.sql b/src/encrypted_domain/int4/int4_ord_functions.sql index 992a9fe5..fae4fa63 100644 --- a/src/encrypted_domain/int4/int4_ord_functions.sql +++ b/src/encrypted_domain/int4/int4_ord_functions.sql @@ -67,7 +67,7 @@ AS $$ SELECT eql_v2.ore_block_u64_8_256(a::jsonb) $$; --! @param a eql_v2_int4_ord --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a eql_v2_int4_ord, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.lt(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b) $$; @@ -75,7 +75,7 @@ AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a eql_v2_int4_ord, b jsonb) +CREATE FUNCTION eql_v2.lt(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b::eql_v2_int4_ord) $$; @@ -83,7 +83,7 @@ AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @param a jsonb --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_lt(a jsonb, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.lt(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) < eql_v2.ord_term(b) $$; @@ -91,7 +91,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) < eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a eql_v2_int4_ord, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.lte(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b) $$; @@ -99,7 +99,7 @@ AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a eql_v2_int4_ord, b jsonb) +CREATE FUNCTION eql_v2.lte(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b::eql_v2_int4_ord) $$; @@ -107,7 +107,7 @@ AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @param a jsonb --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_lte(a jsonb, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.lte(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) <= eql_v2.ord_term(b) $$; @@ -115,7 +115,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) <= eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a eql_v2_int4_ord, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.gt(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b) $$; @@ -123,7 +123,7 @@ AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a eql_v2_int4_ord, b jsonb) +CREATE FUNCTION eql_v2.gt(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b::eql_v2_int4_ord) $$; @@ -131,7 +131,7 @@ AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @param a jsonb --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_gt(a jsonb, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.gt(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) > eql_v2.ord_term(b) $$; @@ -139,7 +139,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) > eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a eql_v2_int4_ord, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.gte(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b) $$; @@ -147,7 +147,7 @@ AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a eql_v2_int4_ord, b jsonb) +CREATE FUNCTION eql_v2.gte(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b::eql_v2_int4_ord) $$; @@ -155,7 +155,7 @@ AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @param a jsonb --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_gte(a jsonb, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.gte(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) >= eql_v2.ord_term(b) $$; @@ -164,7 +164,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) >= eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a eql_v2_int4_ord, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.eq(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b) $$; @@ -172,7 +172,7 @@ AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a eql_v2_int4_ord, b jsonb) +CREATE FUNCTION eql_v2.eq(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b::eql_v2_int4_ord) $$; @@ -180,7 +180,7 @@ AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @param a jsonb --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_eq(a jsonb, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.eq(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) = eql_v2.ord_term(b) $$; @@ -188,7 +188,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) = eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a eql_v2_int4_ord, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.neq(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b) $$; @@ -196,7 +196,7 @@ AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a eql_v2_int4_ord, b jsonb) +CREATE FUNCTION eql_v2.neq(a eql_v2_int4_ord, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b::eql_v2_int4_ord) $$; @@ -204,7 +204,7 @@ AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b::eql_v2_int4_ord) $$; --! @param a jsonb --! @param b eql_v2_int4_ord --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_neq(a jsonb, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.neq(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) <> eql_v2.ord_term(b) $$; @@ -268,7 +268,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord --! @param b eql_v2_int4_ord --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_contains(a eql_v2_int4_ord, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.contains(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '@>'); END; $$ LANGUAGE plpgsql; @@ -277,7 +277,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_contains(a eql_v2_int4_ord, b jsonb) +CREATE FUNCTION eql_v2.contains(a eql_v2_int4_ord, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '@>'); END; $$ LANGUAGE plpgsql; @@ -286,7 +286,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_ord --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_contains(a jsonb, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.contains(a jsonb, b eql_v2_int4_ord) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '@>'); END; $$ LANGUAGE plpgsql; @@ -295,7 +295,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord --! @param b eql_v2_int4_ord --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_contained_by(a eql_v2_int4_ord, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.contained_by(a eql_v2_int4_ord, b eql_v2_int4_ord) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '<@'); END; $$ LANGUAGE plpgsql; @@ -304,7 +304,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_contained_by(a eql_v2_int4_ord, b jsonb) +CREATE FUNCTION eql_v2.contained_by(a eql_v2_int4_ord, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '<@'); END; $$ LANGUAGE plpgsql; @@ -313,7 +313,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_ord --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_contained_by(a jsonb, b eql_v2_int4_ord) +CREATE FUNCTION eql_v2.contained_by(a jsonb, b eql_v2_int4_ord) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '<@'); END; $$ LANGUAGE plpgsql; @@ -324,7 +324,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord --! @param selector text --! @return eql_v2_int4_ord (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow(a eql_v2_int4_ord, selector text) +CREATE FUNCTION eql_v2."->"(a eql_v2_int4_ord, selector text) RETURNS eql_v2_int4_ord IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord'; END; $$ LANGUAGE plpgsql; @@ -333,7 +333,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord --! @param selector integer --! @return eql_v2_int4_ord (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow(a eql_v2_int4_ord, selector integer) +CREATE FUNCTION eql_v2."->"(a eql_v2_int4_ord, selector integer) RETURNS eql_v2_int4_ord IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord'; END; $$ LANGUAGE plpgsql; @@ -342,7 +342,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param selector eql_v2_int4_ord --! @return eql_v2_int4_ord (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow(a jsonb, selector eql_v2_int4_ord) +CREATE FUNCTION eql_v2."->"(a jsonb, selector eql_v2_int4_ord) RETURNS eql_v2_int4_ord IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord'; END; $$ LANGUAGE plpgsql; @@ -351,7 +351,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord --! @param selector text --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow_text(a eql_v2_int4_ord, selector text) +CREATE FUNCTION eql_v2."->>"(a eql_v2_int4_ord, selector text) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord'; END; $$ LANGUAGE plpgsql; @@ -360,7 +360,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord --! @param selector integer --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow_text(a eql_v2_int4_ord, selector integer) +CREATE FUNCTION eql_v2."->>"(a eql_v2_int4_ord, selector integer) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord'; END; $$ LANGUAGE plpgsql; @@ -369,7 +369,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param selector eql_v2_int4_ord --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_arrow_text(a jsonb, selector eql_v2_int4_ord) +CREATE FUNCTION eql_v2."->>"(a jsonb, selector eql_v2_int4_ord) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord'; END; $$ LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_ord_operators.sql b/src/encrypted_domain/int4/int4_ord_operators.sql index 1c2c8546..896606f8 100644 --- a/src/encrypted_domain/int4/int4_ord_operators.sql +++ b/src/encrypted_domain/int4/int4_ord_operators.sql @@ -25,108 +25,108 @@ -- metadata is for plan-quality completeness, not index engagement. CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_ord_eq, + FUNCTION = eql_v2.eq, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_ord_eq, + FUNCTION = eql_v2.eq, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_ord_eq, + FUNCTION = eql_v2.eq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_ord_neq, + FUNCTION = eql_v2.neq, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_ord_neq, + FUNCTION = eql_v2.neq, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_ord_neq, + FUNCTION = eql_v2.neq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR < ( - FUNCTION = eql_v2.eql_v2_int4_ord_lt, + FUNCTION = eql_v2.lt, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); CREATE OPERATOR < ( - FUNCTION = eql_v2.eql_v2_int4_ord_lt, + FUNCTION = eql_v2.lt, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); CREATE OPERATOR < ( - FUNCTION = eql_v2.eql_v2_int4_ord_lt, + FUNCTION = eql_v2.lt, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); CREATE OPERATOR <= ( - FUNCTION = eql_v2.eql_v2_int4_ord_lte, + FUNCTION = eql_v2.lte, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); CREATE OPERATOR <= ( - FUNCTION = eql_v2.eql_v2_int4_ord_lte, + FUNCTION = eql_v2.lte, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); CREATE OPERATOR <= ( - FUNCTION = eql_v2.eql_v2_int4_ord_lte, + FUNCTION = eql_v2.lte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); CREATE OPERATOR > ( - FUNCTION = eql_v2.eql_v2_int4_ord_gt, + FUNCTION = eql_v2.gt, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); CREATE OPERATOR > ( - FUNCTION = eql_v2.eql_v2_int4_ord_gt, + FUNCTION = eql_v2.gt, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); CREATE OPERATOR > ( - FUNCTION = eql_v2.eql_v2_int4_ord_gt, + FUNCTION = eql_v2.gt, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); CREATE OPERATOR >= ( - FUNCTION = eql_v2.eql_v2_int4_ord_gte, + FUNCTION = eql_v2.gte, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel ); CREATE OPERATOR >= ( - FUNCTION = eql_v2.eql_v2_int4_ord_gte, + FUNCTION = eql_v2.gte, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb, COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel ); CREATE OPERATOR >= ( - FUNCTION = eql_v2.eql_v2_int4_ord_gte, + FUNCTION = eql_v2.gte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord, COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel @@ -146,30 +146,30 @@ CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ilike, CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ilike, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = eql_v2_int4_ord, RIGHTARG = text); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = eql_v2_int4_ord, RIGHTARG = integer); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = eql_v2_int4_ord, RIGHTARG = text); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = eql_v2_int4_ord, RIGHTARG = integer); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); diff --git a/src/encrypted_domain/int4/int4_ord_ore_functions.sql b/src/encrypted_domain/int4/int4_ord_ore_functions.sql index a32a431c..08cf89f8 100644 --- a/src/encrypted_domain/int4/int4_ord_ore_functions.sql +++ b/src/encrypted_domain/int4/int4_ord_ore_functions.sql @@ -64,7 +64,7 @@ AS $$ SELECT eql_v2.ore_block_u64_8_256(a::jsonb) $$; --! @param a eql_v2_int4_ord_ore --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.lt(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b) $$; @@ -72,7 +72,7 @@ AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a eql_v2_int4_ord_ore, b jsonb) +CREATE FUNCTION eql_v2.lt(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; @@ -80,7 +80,7 @@ AS $$ SELECT eql_v2.ord_term(a) < eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @param a jsonb --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lt(a jsonb, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.lt(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) < eql_v2.ord_term(b) $$; @@ -88,7 +88,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) < eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.lte(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b) $$; @@ -96,7 +96,7 @@ AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a eql_v2_int4_ord_ore, b jsonb) +CREATE FUNCTION eql_v2.lte(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; @@ -104,7 +104,7 @@ AS $$ SELECT eql_v2.ord_term(a) <= eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @param a jsonb --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_lte(a jsonb, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.lte(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) <= eql_v2.ord_term(b) $$; @@ -112,7 +112,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) <= eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.gt(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b) $$; @@ -120,7 +120,7 @@ AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a eql_v2_int4_ord_ore, b jsonb) +CREATE FUNCTION eql_v2.gt(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; @@ -128,7 +128,7 @@ AS $$ SELECT eql_v2.ord_term(a) > eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @param a jsonb --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gt(a jsonb, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.gt(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) > eql_v2.ord_term(b) $$; @@ -136,7 +136,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) > eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.gte(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b) $$; @@ -144,7 +144,7 @@ AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a eql_v2_int4_ord_ore, b jsonb) +CREATE FUNCTION eql_v2.gte(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; @@ -152,7 +152,7 @@ AS $$ SELECT eql_v2.ord_term(a) >= eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @param a jsonb --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_gte(a jsonb, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.gte(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) >= eql_v2.ord_term(b) $$; @@ -161,7 +161,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) >= eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.eq(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b) $$; @@ -169,7 +169,7 @@ AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a eql_v2_int4_ord_ore, b jsonb) +CREATE FUNCTION eql_v2.eq(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; @@ -177,7 +177,7 @@ AS $$ SELECT eql_v2.ord_term(a) = eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @param a jsonb --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_eq(a jsonb, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.eq(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) = eql_v2.ord_term(b) $$; @@ -185,7 +185,7 @@ AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) = eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.neq(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b) $$; @@ -193,7 +193,7 @@ AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b) $$; --! @param a eql_v2_int4_ord_ore --! @param b jsonb --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a eql_v2_int4_ord_ore, b jsonb) +CREATE FUNCTION eql_v2.neq(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; @@ -201,7 +201,7 @@ AS $$ SELECT eql_v2.ord_term(a) <> eql_v2.ord_term(b::eql_v2_int4_ord_ore) $$; --! @param a jsonb --! @param b eql_v2_int4_ord_ore --! @return boolean -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_neq(a jsonb, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.neq(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) <> eql_v2.ord_term(b) $$; @@ -265,7 +265,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord_ore --! @param b eql_v2_int4_ord_ore --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contains(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.contains(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '@>'); END; $$ LANGUAGE plpgsql; @@ -274,7 +274,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord_ore --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contains(a eql_v2_int4_ord_ore, b jsonb) +CREATE FUNCTION eql_v2.contains(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '@>'); END; $$ LANGUAGE plpgsql; @@ -283,7 +283,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_ord_ore --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contains(a jsonb, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.contains(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '@>'); END; $$ LANGUAGE plpgsql; @@ -292,7 +292,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord_ore --! @param b eql_v2_int4_ord_ore --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contained_by(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.contained_by(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '<@'); END; $$ LANGUAGE plpgsql; @@ -301,7 +301,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord_ore --! @param b jsonb --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contained_by(a eql_v2_int4_ord_ore, b jsonb) +CREATE FUNCTION eql_v2.contained_by(a eql_v2_int4_ord_ore, b jsonb) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '<@'); END; $$ LANGUAGE plpgsql; @@ -310,7 +310,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param b eql_v2_int4_ord_ore --! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_contained_by(a jsonb, b eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2.contained_by(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '<@'); END; $$ LANGUAGE plpgsql; @@ -321,7 +321,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord_ore --! @param selector text --! @return eql_v2_int4_ord_ore (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow(a eql_v2_int4_ord_ore, selector text) +CREATE FUNCTION eql_v2."->"(a eql_v2_int4_ord_ore, selector text) RETURNS eql_v2_int4_ord_ore IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord_ore'; END; $$ LANGUAGE plpgsql; @@ -330,7 +330,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord_ore --! @param selector integer --! @return eql_v2_int4_ord_ore (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow(a eql_v2_int4_ord_ore, selector integer) +CREATE FUNCTION eql_v2."->"(a eql_v2_int4_ord_ore, selector integer) RETURNS eql_v2_int4_ord_ore IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord_ore'; END; $$ LANGUAGE plpgsql; @@ -339,7 +339,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param selector eql_v2_int4_ord_ore --! @return eql_v2_int4_ord_ore (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow(a jsonb, selector eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2."->"(a jsonb, selector eql_v2_int4_ord_ore) RETURNS eql_v2_int4_ord_ore IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->', 'eql_v2_int4_ord_ore'; END; $$ LANGUAGE plpgsql; @@ -348,7 +348,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord_ore --! @param selector text --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow_text(a eql_v2_int4_ord_ore, selector text) +CREATE FUNCTION eql_v2."->>"(a eql_v2_int4_ord_ore, selector text) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord_ore'; END; $$ LANGUAGE plpgsql; @@ -357,7 +357,7 @@ LANGUAGE plpgsql; --! @param a eql_v2_int4_ord_ore --! @param selector integer --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow_text(a eql_v2_int4_ord_ore, selector integer) +CREATE FUNCTION eql_v2."->>"(a eql_v2_int4_ord_ore, selector integer) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord_ore'; END; $$ LANGUAGE plpgsql; @@ -366,7 +366,7 @@ LANGUAGE plpgsql; --! @param a jsonb --! @param selector eql_v2_int4_ord_ore --! @return text (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_arrow_text(a jsonb, selector eql_v2_int4_ord_ore) +CREATE FUNCTION eql_v2."->>"(a jsonb, selector eql_v2_int4_ord_ore) RETURNS text IMMUTABLE PARALLEL SAFE AS $$ BEGIN RAISE EXCEPTION 'operator % is not supported for %', '->>', 'eql_v2_int4_ord_ore'; END; $$ LANGUAGE plpgsql; diff --git a/src/encrypted_domain/int4/int4_ord_ore_operators.sql b/src/encrypted_domain/int4/int4_ord_ore_operators.sql index 36fe3ac0..b3518d1b 100644 --- a/src/encrypted_domain/int4/int4_ord_ore_operators.sql +++ b/src/encrypted_domain/int4/int4_ord_ore_operators.sql @@ -22,108 +22,108 @@ -- metadata is for plan-quality completeness, not index engagement. CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, + FUNCTION = eql_v2.eq, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, + FUNCTION = eql_v2.eq, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR = ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_eq, + FUNCTION = eql_v2.eq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = =, NEGATOR = <>, RESTRICT = eqsel, JOIN = eqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, + FUNCTION = eql_v2.neq, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, + FUNCTION = eql_v2.neq, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR <> ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_neq, + FUNCTION = eql_v2.neq, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = <>, NEGATOR = =, RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE OPERATOR < ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, + FUNCTION = eql_v2.lt, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); CREATE OPERATOR < ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, + FUNCTION = eql_v2.lt, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); CREATE OPERATOR < ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_lt, + FUNCTION = eql_v2.lt, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = >, NEGATOR = >=, RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); CREATE OPERATOR <= ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, + FUNCTION = eql_v2.lte, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); CREATE OPERATOR <= ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, + FUNCTION = eql_v2.lte, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); CREATE OPERATOR <= ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_lte, + FUNCTION = eql_v2.lte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = >=, NEGATOR = >, RESTRICT = scalarlesel, JOIN = scalarlejoinsel ); CREATE OPERATOR > ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, + FUNCTION = eql_v2.gt, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); CREATE OPERATOR > ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, + FUNCTION = eql_v2.gt, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); CREATE OPERATOR > ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_gt, + FUNCTION = eql_v2.gt, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = <, NEGATOR = <=, RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); CREATE OPERATOR >= ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, + FUNCTION = eql_v2.gte, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel ); CREATE OPERATOR >= ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, + FUNCTION = eql_v2.gte, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb, COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel ); CREATE OPERATOR >= ( - FUNCTION = eql_v2.eql_v2_int4_ord_ore_gte, + FUNCTION = eql_v2.gte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore, COMMUTATOR = <=, NEGATOR = <, RESTRICT = scalargesel, JOIN = scalargejoinsel @@ -143,30 +143,30 @@ CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ore_ilike, CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ore_ilike, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); -CREATE OPERATOR @> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contains, +CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); -CREATE OPERATOR <@ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_contained_by, +CREATE OPERATOR <@ (FUNCTION = eql_v2.contained_by, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = text); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = integer); -CREATE OPERATOR -> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow, +CREATE OPERATOR -> (FUNCTION = eql_v2."->", LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = text); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = integer); -CREATE OPERATOR ->> (FUNCTION = eql_v2.eql_v2_int4_ord_ore_arrow_text, +CREATE OPERATOR ->> (FUNCTION = eql_v2."->>", LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); diff --git a/tasks/pin_search_path.sql b/tasks/pin_search_path.sql index 2d050bb4..6002bd4a 100644 --- a/tasks/pin_search_path.sql +++ b/tasks/pin_search_path.sql @@ -32,6 +32,9 @@ DECLARE jsonb_oid oid; text_oid oid; entry_oid oid; + int4_eq_oid oid; + int4_ord_oid oid; + int4_ord_ore_oid oid; BEGIN -- Resolve type oids without depending on caller search_path. The encrypted -- composite type is created in `public`; jsonb / text are in `pg_catalog`; @@ -73,6 +76,38 @@ BEGIN RAISE EXCEPTION 'pin_search_path: type eql_v2.ste_vec_entry not found'; END IF; + -- The eql_v2_int4 variant-family domains are created in `public` + -- (alongside eql_v2_encrypted). Resolved so the inline-critical clauses + -- for the converged eq/neq/lt/lte/gt/gte wrappers can be restricted by + -- argument type — those bare names now collide with the ste_vec_entry + -- and eql_v2_encrypted overloads of the same name. + SELECT t.oid INTO int4_eq_oid + FROM pg_catalog.pg_type t + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE n.nspname = 'public' AND t.typname = 'eql_v2_int4_eq'; + + IF int4_eq_oid IS NULL THEN + RAISE EXCEPTION 'pin_search_path: type public.eql_v2_int4_eq not found'; + END IF; + + SELECT t.oid INTO int4_ord_oid + FROM pg_catalog.pg_type t + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE n.nspname = 'public' AND t.typname = 'eql_v2_int4_ord'; + + IF int4_ord_oid IS NULL THEN + RAISE EXCEPTION 'pin_search_path: type public.eql_v2_int4_ord not found'; + END IF; + + SELECT t.oid INTO int4_ord_ore_oid + FROM pg_catalog.pg_type t + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE n.nspname = 'public' AND t.typname = 'eql_v2_int4_ord_ore'; + + IF int4_ord_ore_oid IS NULL THEN + RAISE EXCEPTION 'pin_search_path: type public.eql_v2_int4_ord_ore not found'; + END IF; + -- Wrappers that must remain inlinable for functional-index matching. -- Verified empirically: with SET, EXPLAIN drops to Seq Scan; without, -- it uses Bitmap Index Scan / Index Scan. @@ -240,39 +275,35 @@ BEGIN OR p.proargtypes[0] = (SELECT t.oid FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = 'eql_v2' AND t.typname = 'stevec_query'))) - -- eql_v2_int4 variant family inline-critical wrappers. Name-only - -- match (any arity) covers all three arg-shapes per operator. - -- Blockers are intentionally excluded — they are PL/pgSQL and must - -- NOT inline. + -- eql_v2_int4 variant family inline-critical wrappers. -- - -- The eql_v2_int4_ord_ore comparison wrappers (_eq/_neq and the - -- four range wrappers) are LANGUAGE sql and must inline so the - -- planner rewrites `col $1` to `eql_v2.ord_term(col) - -- eql_v2.ord_term($1)` and matches the functional btree on - -- eql_v2.ord_term(col). eql_v2.ord_term is the index extractor and - -- must also stay unpinned (the 1-arg clause below). The - -- eql_v2_int4_eq wrappers must inline to match the functional - -- eql_v2.eq_term(col) index; eql_v2.eq_term itself stays unpinned - -- via the 1-arg `eq_term` clause above. eql_v2_int4_ord is a - -- concrete domain (D-E fallback) carrying the same wrapper set as - -- eql_v2_int4_ord_ore. See docs/upgrading/v2.4.md U-001. + -- After the PR #225 naming convergence the backing functions are + -- overloaded eql_v2.eq / neq / lt / lte / gt / gte discriminated by + -- argument type, sharing those bare names with the ste_vec_entry and + -- eql_v2_encrypted overloads — so these clauses MUST restrict by the + -- int4 domain arg types. The `proargtypes[0] OR proargtypes[1]` form + -- covers all three arg-shapes (domain,domain), (domain,jsonb), + -- (jsonb,domain). + -- + -- Only the real (LANGUAGE sql) wrappers appear here: eq/neq on + -- eql_v2_int4_eq, and all six comparisons on eql_v2_int4_ord / + -- eql_v2_int4_ord_ore. They must inline so the planner rewrites + -- `col $1` to `eql_v2.eq_term(col) ...` / + -- `eql_v2.ord_term(col) ...` and matches the functional index. + -- The extractors eql_v2.eq_term / eql_v2.ord_term stay unpinned via + -- the 1-arg clauses above. The storage-variant eql_v2_int4 blockers + -- and every contains / contained_by / "->" / "->>" blocker are + -- PL/pgSQL and must NOT inline — they are excluded by omission. OR (p.pronargs = 1 AND p.proname = 'ord_term') - OR p.proname IN ( - 'eql_v2_int4_eq_eq', -- _eq variant equality - 'eql_v2_int4_eq_neq', - 'eql_v2_int4_ord_ore_eq', -- _ord_ore equality (routes through ord) - 'eql_v2_int4_ord_ore_neq', - 'eql_v2_int4_ord_ore_lt', -- _ord_ore range (routes through ord) - 'eql_v2_int4_ord_ore_lte', - 'eql_v2_int4_ord_ore_gt', - 'eql_v2_int4_ord_ore_gte', - 'eql_v2_int4_ord_eq', -- _ord equality (routes through ord) - 'eql_v2_int4_ord_neq', - 'eql_v2_int4_ord_lt', -- _ord range (routes through ord) - 'eql_v2_int4_ord_lte', - 'eql_v2_int4_ord_gt', - 'eql_v2_int4_ord_gte' - ) + OR (p.pronargs = 2 + AND p.proname IN ('eq', 'neq') + AND (p.proargtypes[0] = int4_eq_oid OR p.proargtypes[1] = int4_eq_oid)) + OR (p.pronargs = 2 + AND p.proname IN ('eq', 'neq', 'lt', 'lte', 'gt', 'gte') + AND (p.proargtypes[0] = int4_ord_oid OR p.proargtypes[1] = int4_ord_oid)) + OR (p.pronargs = 2 + AND p.proname IN ('eq', 'neq', 'lt', 'lte', 'gt', 'gte') + AND (p.proargtypes[0] = int4_ord_ore_oid OR p.proargtypes[1] = int4_ord_ore_oid)) ); FOR fn_oid IN diff --git a/tasks/test/splinter.sh b/tasks/test/splinter.sh index 02149900..f739ddf6 100755 --- a/tasks/test/splinter.sh +++ b/tasks/test/splinter.sh @@ -81,12 +81,12 @@ function_search_path_mutable eql_v2 jsonb_contained_by function GIN-inlining: sa function_search_path_mutable eql_v2 ore_cllw function Consolidated ORE-CLLW extractor (U-006): inlinable SQL so the planner can fold `eql_v2.ore_cllw(col -> 'sel')` calls into the calling query. SET search_path would silently undo the inlining and prevent functional-index match through the extractor form. Two overloads: (jsonb), (eql_v2.ste_vec_entry). function_search_path_mutable eql_v2 has_ore_cllw function Consolidated ORE-CLLW presence check (U-006): inlinable SQL counterpart to `eql_v2.ore_cllw`. Same rationale as `ore_cllw` — must stay unpinned to inline into the calling query. Two overloads: (jsonb), (eql_v2.ste_vec_entry). function_search_path_mutable eql_v2 selector function STE-vec entry selector extractor (#219): typed (eql_v2.ste_vec_entry) overload, inlinable so the planner can fold `eql_v2.selector(col -> 'sel')` into the calling query. -function_search_path_mutable eql_v2 eq function Equality backing function for `eql_v2.ste_vec_entry × eql_v2.ste_vec_entry` (#219). Inlines to `hmac_256(a) = hmac_256(b)`; the `=` operator must reach the functional hash index on `eql_v2.hmac_256(col -> 'sel')` for bare-form field equality to engage Index Scan. -function_search_path_mutable eql_v2 neq function Inequality backing function for `eql_v2.ste_vec_entry`. Same rationale as `eq`. -function_search_path_mutable eql_v2 lt function Less-than backing function for `eql_v2.ste_vec_entry`. Inlines to `ore_cllw(a) < ore_cllw(b)`; must reach the functional btree opclass on `eql_v2.ore_cllw` for ordered field queries to engage Index Scan. -function_search_path_mutable eql_v2 lte function Less-than-or-equal backing function for `eql_v2.ste_vec_entry`. Same rationale as `lt`. -function_search_path_mutable eql_v2 gt function Greater-than backing function for `eql_v2.ste_vec_entry`. Same rationale as `lt`. -function_search_path_mutable eql_v2 gte function Greater-than-or-equal backing function for `eql_v2.ste_vec_entry`. Same rationale as `lt`. +function_search_path_mutable eql_v2 eq function Equality backing function for `eql_v2.ste_vec_entry × eql_v2.ste_vec_entry` (#219). Inlines to `hmac_256(a) = hmac_256(b)`; the `=` operator must reach the functional hash index on `eql_v2.hmac_256(col -> 'sel')` for bare-form field equality to engage Index Scan. Splinter matches by name only, so this row also covers the converged eql_v2.eq wrappers on eql_v2_int4_eq / _ord / _ord_ore (PR #225). +function_search_path_mutable eql_v2 neq function Inequality backing function for `eql_v2.ste_vec_entry`. Same rationale as `eq`. Also covers the converged eql_v2.neq wrappers on eql_v2_int4_eq / _ord / _ord_ore (PR #225). +function_search_path_mutable eql_v2 lt function Less-than backing function for `eql_v2.ste_vec_entry`. Inlines to `ore_cllw(a) < ore_cllw(b)`; must reach the functional btree opclass on `eql_v2.ore_cllw` for ordered field queries to engage Index Scan. Splinter matches by name only, so this row also covers the converged eql_v2.lt wrappers on eql_v2_int4_ord / _ord_ore (PR #225). +function_search_path_mutable eql_v2 lte function Less-than-or-equal backing function for `eql_v2.ste_vec_entry`. Same rationale as `lt`. Also covers the converged eql_v2.lte wrappers on eql_v2_int4_ord / _ord_ore (PR #225). +function_search_path_mutable eql_v2 gt function Greater-than backing function for `eql_v2.ste_vec_entry`. Same rationale as `lt`. Also covers the converged eql_v2.gt wrappers on eql_v2_int4_ord / _ord_ore (PR #225). +function_search_path_mutable eql_v2 gte function Greater-than-or-equal backing function for `eql_v2.ste_vec_entry`. Same rationale as `lt`. Also covers the converged eql_v2.gte wrappers on eql_v2_int4_ord / _ord_ore (PR #225). function_search_path_mutable eql_v2 ore_cllw_eq function Inner comparator for the `eql_v2.ore_cllw` type's `=` operator (#221). The outer same-type operators back the btree opclass on `eql_v2.ore_cllw`; the planner only carries the inlined form through to functional-index match if this inner function is also inlinable (no SET, IMMUTABLE). Mirrors ore_block_u64_8_256_eq. function_search_path_mutable eql_v2 ore_cllw_neq function Inner comparator for the `eql_v2.ore_cllw` type's `<>` operator (#221). Same rationale as `ore_cllw_eq`. function_search_path_mutable eql_v2 ore_cllw_lt function Inner comparator for the `eql_v2.ore_cllw` type's `<` operator (#221). Same rationale as `ore_cllw_eq`. @@ -99,20 +99,6 @@ function_search_path_mutable eql_v2 min function Aggregate (splinter labels thes function_search_path_mutable eql_v2 max function Aggregate: same as min. function_search_path_mutable eql_v2 grouped_value function Aggregate: same as min. function_search_path_mutable eql_v2 ord_term function eql_v2_int4 ordered-variant index extractor: returns eql_v2.ore_block_u64_8_256 (carrying main DEFAULT btree opclass). Used inside the inlinable comparison wrappers and as the functional-index expression USING btree (eql_v2.ord_term(col)); must inline. SET search_path would disable SQL function inlining (see PostgreSQL inline_function). Covers both ord_term overloads (eql_v2_int4_ord_ore, eql_v2_int4_ord). -function_search_path_mutable eql_v2 eql_v2_int4_eq_eq function eql_v2_int4_eq variant equality: inlines to eql_v2.eq_term(a) = eql_v2.eq_term(b) for functional-index engagement on eql_v2.eq_term(col) (USING hash or btree). Three overloads: (domain,domain), (domain,jsonb), (jsonb,domain). -function_search_path_mutable eql_v2 eql_v2_int4_eq_neq function eql_v2_int4_eq variant inequality: same eql_v2.eq_term inlining rationale as eql_v2_int4_eq_eq. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_eq function eql_v2_int4_ord_ore equality: inlines to eql_v2.ord_term(a) = eql_v2.ord_term(b) for functional-btree engagement on eql_v2.ord_term(col). Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_neq function eql_v2_int4_ord_ore inequality: same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_ore_eq. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_lt function eql_v2_int4_ord_ore range: inlines to eql_v2.ord_term(a) < eql_v2.ord_term(b) for functional-btree engagement on eql_v2.ord_term(col). Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_lte function eql_v2_int4_ord_ore range: same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_gt function eql_v2_int4_ord_ore range: same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_ore_gte function eql_v2_int4_ord_ore range: same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_ore_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_eq function eql_v2_int4_ord equality (D-E fallback concrete domain): inlines to eql_v2.ord_term(a) = eql_v2.ord_term(b), same rationale as eql_v2_int4_ord_ore_eq. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_neq function eql_v2_int4_ord inequality (D-E fallback): same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_eq. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_lt function eql_v2_int4_ord range (D-E fallback): inlines to eql_v2.ord_term(a) < eql_v2.ord_term(b), same rationale as eql_v2_int4_ord_ore_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_lte function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_gt function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_lt. Three overloads. -function_search_path_mutable eql_v2 eql_v2_int4_ord_gte function eql_v2_int4_ord range (D-E fallback): same eql_v2.ord_term inlining rationale as eql_v2_int4_ord_lt. Three overloads. ALLOW # Wrap splinter (a single bare SELECT expression) into a subquery we can diff --git a/tests/sqlx/tests/encrypted_int4_eq_tests.rs b/tests/sqlx/tests/encrypted_int4_eq_tests.rs index d2175e45..560b9ea8 100644 --- a/tests/sqlx/tests/encrypted_int4_eq_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_eq_tests.rs @@ -373,8 +373,11 @@ async fn eq_wrappers_are_inlinable(pool: PgPool) -> Result<()> { FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace JOIN pg_catalog.pg_language l ON l.oid = p.prolang + JOIN pg_catalog.pg_type lt ON lt.oid = p.proargtypes[0] + JOIN pg_catalog.pg_type rt ON rt.oid = p.proargtypes[1] WHERE n.nspname = 'eql_v2' - AND p.proname IN ('eql_v2_int4_eq_eq', 'eql_v2_int4_eq_neq') + AND p.proname IN ('eq', 'neq') + AND (lt.typname = 'eql_v2_int4_eq' OR rt.typname = 'eql_v2_int4_eq') "#, ) .fetch_all(&pool) diff --git a/tests/sqlx/tests/encrypted_int4_ord_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_tests.rs index 423e7dde..5d047107 100644 --- a/tests/sqlx/tests/encrypted_int4_ord_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_ord_tests.rs @@ -338,22 +338,19 @@ async fn ord_ore_wrappers_are_inlinable(pool: PgPool) -> Result<()> { FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace JOIN pg_catalog.pg_language l ON l.oid = p.prolang + JOIN pg_catalog.pg_type lt ON lt.oid = p.proargtypes[0] + JOIN pg_catalog.pg_type rt ON rt.oid = p.proargtypes[1] WHERE n.nspname = 'eql_v2' - AND p.proname IN ( - 'eql_v2_int4_ord_ore_eq', 'eql_v2_int4_ord_ore_neq', - 'eql_v2_int4_ord_ore_lt', 'eql_v2_int4_ord_ore_lte', - 'eql_v2_int4_ord_ore_gt', 'eql_v2_int4_ord_ore_gte', - 'eql_v2_int4_ord_eq', 'eql_v2_int4_ord_neq', - 'eql_v2_int4_ord_lt', 'eql_v2_int4_ord_lte', - 'eql_v2_int4_ord_gt', 'eql_v2_int4_ord_gte' - ) + AND p.proname IN ('eq', 'neq', 'lt', 'lte', 'gt', 'gte') + AND (lt.typname IN ('eql_v2_int4_ord', 'eql_v2_int4_ord_ore') + OR rt.typname IN ('eql_v2_int4_ord', 'eql_v2_int4_ord_ore')) "#, ) .fetch_all(&pool) .await?; - // 12 wrapper names (6 on _ord_ore, 6 on the concrete _ord domain) - // × 3 arg-shapes = 36 rows. + // 6 converged comparison wrappers (eq/neq/lt/lte/gt/gte) × 2 ordered + // domains (_ord_ore and the concrete _ord) × 3 arg-shapes = 36 rows. assert_eq!( rows.len(), 36, From 8413b776fd6137ef24e782ac211d8ed21a658ae4 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 21 May 2026 17:33:26 +1000 Subject: [PATCH 09/13] feat(encrypted_int4): drop ~~ / ~~* operators and the LIKE/ILIKE blockers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EQL is stepping back from the term "like": bloom filters implement a positional-unaware `match`, not SQL `LIKE`, and the int4 domains have no pattern-match capability at all. The ~~ / ~~* operator declarations and their _like / _ilike blocker functions are removed from all four variants. ~~ is not a native jsonb operator, so removing the declarations does not open a fall-through to native semantics (unlike @> / <@ / -> / ->>, which keep their blockers). A bare `col ~~ x` on an int4 domain now raises PostgreSQL's native "operator does not exist" rather than an EQL blocker message. Tests: ~~ / ~~* are removed from the blocker-loop assertions; the ord_ore NULL-input STRICT-regression probe switches from ~~ to @> (a still-declared blocker); a new like_operators_are_not_declared test pins that ~~ / ~~* stay absent. Resolves coderdan PR #225 review thread (stepping back from "like"). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../int4/int4_eq_functions.sql | 56 +----------------- .../int4/int4_eq_operators.sql | 14 ----- src/encrypted_domain/int4/int4_functions.sql | 58 +------------------ src/encrypted_domain/int4/int4_operators.sql | 18 +----- .../int4/int4_ord_functions.sql | 56 +----------------- .../int4/int4_ord_operators.sql | 14 ----- .../int4/int4_ord_ore_functions.sql | 56 +----------------- .../int4/int4_ord_ore_operators.sql | 14 ----- tests/sqlx/tests/encrypted_int4_eq_tests.rs | 2 +- .../tests/encrypted_int4_ord_ore_tests.rs | 10 ++-- tests/sqlx/tests/encrypted_int4_ord_tests.rs | 2 +- tests/sqlx/tests/encrypted_int4_tests.rs | 27 ++++++++- 12 files changed, 39 insertions(+), 288 deletions(-) diff --git a/src/encrypted_domain/int4/int4_eq_functions.sql b/src/encrypted_domain/int4/int4_eq_functions.sql index 3a7797a9..acea7f6c 100644 --- a/src/encrypted_domain/int4/int4_eq_functions.sql +++ b/src/encrypted_domain/int4/int4_eq_functions.sql @@ -84,7 +84,7 @@ CREATE FUNCTION eql_v2.neq(a jsonb, b eql_v2_int4_eq) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.eq_term(a::eql_v2_int4_eq) <> eql_v2.eq_term(b) $$; --- <, <=, >, >=, ~~, ~~*, @>, <@ (blockers, 3 shapes each — 8 ops × 3 = 24 functions) +-- <, <=, >, >=, @>, <@ (blockers, 3 shapes each — 6 ops × 3 = 18 functions) --! @brief Blocker for < on eql_v2_int4_eq. --! @param a eql_v2_int4_eq @@ -194,60 +194,6 @@ RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '>='); END; $$ LANGUAGE plpgsql; ---! @brief Blocker for ~~ on eql_v2_int4_eq. ---! @param a eql_v2_int4_eq ---! @param b eql_v2_int4_eq ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_like(a eql_v2_int4_eq, b eql_v2_int4_eq) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~ on eql_v2_int4_eq (domain, jsonb). ---! @param a eql_v2_int4_eq ---! @param b jsonb ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_like(a eql_v2_int4_eq, b jsonb) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~ on eql_v2_int4_eq (jsonb, domain). ---! @param a jsonb ---! @param b eql_v2_int4_eq ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_like(a jsonb, b eql_v2_int4_eq) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_eq. ---! @param a eql_v2_int4_eq ---! @param b eql_v2_int4_eq ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_ilike(a eql_v2_int4_eq, b eql_v2_int4_eq) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~*'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_eq (domain, jsonb). ---! @param a eql_v2_int4_eq ---! @param b jsonb ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_ilike(a eql_v2_int4_eq, b jsonb) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~*'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_eq (jsonb, domain). ---! @param a jsonb ---! @param b eql_v2_int4_eq ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_eq_ilike(a jsonb, b eql_v2_int4_eq) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_eq', '~~*'); END; $$ -LANGUAGE plpgsql; - --! @brief Blocker for @> on eql_v2_int4_eq. --! @param a eql_v2_int4_eq --! @param b eql_v2_int4_eq diff --git a/src/encrypted_domain/int4/int4_eq_operators.sql b/src/encrypted_domain/int4/int4_eq_operators.sql index 36eb61cd..90cd1f81 100644 --- a/src/encrypted_domain/int4/int4_eq_operators.sql +++ b/src/encrypted_domain/int4/int4_eq_operators.sql @@ -89,20 +89,6 @@ CREATE OPERATOR >= (FUNCTION = eql_v2.gte, CREATE OPERATOR >= (FUNCTION = eql_v2.gte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_eq_like, - LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_eq_like, - LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_eq_like, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); - -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_eq_ilike, - LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_eq_ilike, - LEFTARG = eql_v2_int4_eq, RIGHTARG = jsonb); -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_eq_ilike, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_eq); - CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_eq, RIGHTARG = eql_v2_int4_eq); CREATE OPERATOR @> (FUNCTION = eql_v2.contains, diff --git a/src/encrypted_domain/int4/int4_functions.sql b/src/encrypted_domain/int4/int4_functions.sql index 4014e73e..13074e11 100644 --- a/src/encrypted_domain/int4/int4_functions.sql +++ b/src/encrypted_domain/int4/int4_functions.sql @@ -6,7 +6,7 @@ --! @brief Storage-only int4 variant — comparison/path functions. All bool operators raise. --! --! eql_v2_int4 accepts the storage of an encrypted int4 column with ---! ciphertext (`c`) only. Every comparison, containment, LIKE, and path +--! ciphertext (`c`) only. Every comparison, containment, and path --! operator is a blocker so callers cannot accidentally fall through to --! native jsonb semantics. Payload-term assumption: `c` only. @@ -176,61 +176,7 @@ RETURNS boolean IMMUTABLE PARALLEL SAFE AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '>='); END; $$ LANGUAGE plpgsql; --- ~~, ~~*, @>, <@ (blockers, 3 shapes each) - ---! @brief Blocker for ~~ on eql_v2_int4. ---! @param a eql_v2_int4 ---! @param b eql_v2_int4 ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_like(a eql_v2_int4, b eql_v2_int4) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~ on eql_v2_int4 (domain, jsonb). ---! @param a eql_v2_int4 ---! @param b jsonb ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_like(a eql_v2_int4, b jsonb) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~ on eql_v2_int4 (jsonb, domain). ---! @param a jsonb ---! @param b eql_v2_int4 ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_like(a jsonb, b eql_v2_int4) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4. ---! @param a eql_v2_int4 ---! @param b eql_v2_int4 ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ilike(a eql_v2_int4, b eql_v2_int4) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~*'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4 (domain, jsonb). ---! @param a eql_v2_int4 ---! @param b jsonb ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ilike(a eql_v2_int4, b jsonb) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~*'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4 (jsonb, domain). ---! @param a jsonb ---! @param b eql_v2_int4 ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ilike(a jsonb, b eql_v2_int4) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4', '~~*'); END; $$ -LANGUAGE plpgsql; +-- @>, <@ (blockers, 3 shapes each) --! @brief Blocker for @> on eql_v2_int4. --! @param a eql_v2_int4 diff --git a/src/encrypted_domain/int4/int4_operators.sql b/src/encrypted_domain/int4/int4_operators.sql index 6741224a..9dd02571 100644 --- a/src/encrypted_domain/int4/int4_operators.sql +++ b/src/encrypted_domain/int4/int4_operators.sql @@ -6,11 +6,11 @@ --! @brief Storage-only int4 variant — operator declarations. All bool operators raise. --! --! eql_v2_int4 accepts the storage of an encrypted int4 column with ---! ciphertext (`c`) only. Every comparison, containment, LIKE, and path +--! ciphertext (`c`) only. Every comparison, containment, and path --! operator is a blocker so callers cannot accidentally fall through to --! native jsonb semantics. Payload-term assumption: `c` only. --- Operator declarations (10 symmetric ops × 3 shapes + 2 path ops × 3 asymmetric shapes) +-- Operator declarations (8 symmetric ops × 3 shapes + 2 path ops × 3 asymmetric shapes) CREATE OPERATOR = ( FUNCTION = eql_v2.eq, @@ -84,20 +84,6 @@ CREATE OPERATOR >= (FUNCTION = eql_v2.gte, CREATE OPERATOR >= (FUNCTION = eql_v2.gte, LEFTARG = jsonb, RIGHTARG = eql_v2_int4); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_like, - LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_like, - LEFTARG = eql_v2_int4, RIGHTARG = jsonb); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_like, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4); - -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ilike, - LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ilike, - LEFTARG = eql_v2_int4, RIGHTARG = jsonb); -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ilike, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4); - CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4, RIGHTARG = eql_v2_int4); CREATE OPERATOR @> (FUNCTION = eql_v2.contains, diff --git a/src/encrypted_domain/int4/int4_ord_functions.sql b/src/encrypted_domain/int4/int4_ord_functions.sql index fae4fa63..ba413227 100644 --- a/src/encrypted_domain/int4/int4_ord_functions.sql +++ b/src/encrypted_domain/int4/int4_ord_functions.sql @@ -208,61 +208,7 @@ CREATE FUNCTION eql_v2.neq(a jsonb, b eql_v2_int4_ord) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord) <> eql_v2.ord_term(b) $$; --- ~~, ~~*, @>, <@ (blockers, 3 shapes each) - ---! @brief Blocker for ~~ on eql_v2_int4_ord. ---! @param a eql_v2_int4_ord ---! @param b eql_v2_int4_ord ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_like(a eql_v2_int4_ord, b eql_v2_int4_ord) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~ on eql_v2_int4_ord (domain, jsonb). ---! @param a eql_v2_int4_ord ---! @param b jsonb ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_like(a eql_v2_int4_ord, b jsonb) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~ on eql_v2_int4_ord (jsonb, domain). ---! @param a jsonb ---! @param b eql_v2_int4_ord ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_like(a jsonb, b eql_v2_int4_ord) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_ord. ---! @param a eql_v2_int4_ord ---! @param b eql_v2_int4_ord ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ilike(a eql_v2_int4_ord, b eql_v2_int4_ord) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~*'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_ord (domain, jsonb). ---! @param a eql_v2_int4_ord ---! @param b jsonb ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ilike(a eql_v2_int4_ord, b jsonb) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~*'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_ord (jsonb, domain). ---! @param a jsonb ---! @param b eql_v2_int4_ord ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ilike(a jsonb, b eql_v2_int4_ord) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord', '~~*'); END; $$ -LANGUAGE plpgsql; +-- @>, <@ (blockers, 3 shapes each) --! @brief Blocker for @> on eql_v2_int4_ord. --! @param a eql_v2_int4_ord diff --git a/src/encrypted_domain/int4/int4_ord_operators.sql b/src/encrypted_domain/int4/int4_ord_operators.sql index 896606f8..dae43c53 100644 --- a/src/encrypted_domain/int4/int4_ord_operators.sql +++ b/src/encrypted_domain/int4/int4_ord_operators.sql @@ -132,20 +132,6 @@ CREATE OPERATOR >= ( RESTRICT = scalargesel, JOIN = scalargejoinsel ); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_like, - LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_like, - LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_like, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); - -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ilike, - LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ilike, - LEFTARG = eql_v2_int4_ord, RIGHTARG = jsonb); -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ilike, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord); - CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_ord, RIGHTARG = eql_v2_int4_ord); CREATE OPERATOR @> (FUNCTION = eql_v2.contains, diff --git a/src/encrypted_domain/int4/int4_ord_ore_functions.sql b/src/encrypted_domain/int4/int4_ord_ore_functions.sql index 08cf89f8..91e1f1d7 100644 --- a/src/encrypted_domain/int4/int4_ord_ore_functions.sql +++ b/src/encrypted_domain/int4/int4_ord_ore_functions.sql @@ -205,61 +205,7 @@ CREATE FUNCTION eql_v2.neq(a jsonb, b eql_v2_int4_ord_ore) RETURNS boolean LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.ord_term(a::eql_v2_int4_ord_ore) <> eql_v2.ord_term(b) $$; --- ~~, ~~*, @>, <@ (blockers, 3 shapes each) - ---! @brief Blocker for ~~ on eql_v2_int4_ord_ore. ---! @param a eql_v2_int4_ord_ore ---! @param b eql_v2_int4_ord_ore ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_like(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~ on eql_v2_int4_ord_ore (domain, jsonb). ---! @param a eql_v2_int4_ord_ore ---! @param b jsonb ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_like(a eql_v2_int4_ord_ore, b jsonb) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~ on eql_v2_int4_ord_ore (jsonb, domain). ---! @param a jsonb ---! @param b eql_v2_int4_ord_ore ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_like(a jsonb, b eql_v2_int4_ord_ore) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_ord_ore. ---! @param a eql_v2_int4_ord_ore ---! @param b eql_v2_int4_ord_ore ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_ilike(a eql_v2_int4_ord_ore, b eql_v2_int4_ord_ore) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~*'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_ord_ore (domain, jsonb). ---! @param a eql_v2_int4_ord_ore ---! @param b jsonb ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_ilike(a eql_v2_int4_ord_ore, b jsonb) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~*'); END; $$ -LANGUAGE plpgsql; - ---! @brief Blocker for ~~* on eql_v2_int4_ord_ore (jsonb, domain). ---! @param a jsonb ---! @param b eql_v2_int4_ord_ore ---! @return boolean (never returns; always raises) -CREATE FUNCTION eql_v2.eql_v2_int4_ord_ore_ilike(a jsonb, b eql_v2_int4_ord_ore) -RETURNS boolean IMMUTABLE PARALLEL SAFE -AS $$ BEGIN RETURN eql_v2.encrypted_domain_unsupported_bool('eql_v2_int4_ord_ore', '~~*'); END; $$ -LANGUAGE plpgsql; +-- @>, <@ (blockers, 3 shapes each) --! @brief Blocker for @> on eql_v2_int4_ord_ore. --! @param a eql_v2_int4_ord_ore diff --git a/src/encrypted_domain/int4/int4_ord_ore_operators.sql b/src/encrypted_domain/int4/int4_ord_ore_operators.sql index b3518d1b..e5cea3f3 100644 --- a/src/encrypted_domain/int4/int4_ord_ore_operators.sql +++ b/src/encrypted_domain/int4/int4_ord_ore_operators.sql @@ -129,20 +129,6 @@ CREATE OPERATOR >= ( RESTRICT = scalargesel, JOIN = scalargejoinsel ); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_like, - LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_like, - LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); -CREATE OPERATOR ~~ (FUNCTION = eql_v2.eql_v2_int4_ord_ore_like, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); - -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ore_ilike, - LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ore_ilike, - LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = jsonb); -CREATE OPERATOR ~~* (FUNCTION = eql_v2.eql_v2_int4_ord_ore_ilike, - LEFTARG = jsonb, RIGHTARG = eql_v2_int4_ord_ore); - CREATE OPERATOR @> (FUNCTION = eql_v2.contains, LEFTARG = eql_v2_int4_ord_ore, RIGHTARG = eql_v2_int4_ord_ore); CREATE OPERATOR @> (FUNCTION = eql_v2.contains, diff --git a/tests/sqlx/tests/encrypted_int4_eq_tests.rs b/tests/sqlx/tests/encrypted_int4_eq_tests.rs index 560b9ea8..e2ab3736 100644 --- a/tests/sqlx/tests/encrypted_int4_eq_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_eq_tests.rs @@ -131,7 +131,7 @@ async fn eq_unsupported_operators_raise(pool: PgPool) -> Result<()> { ("$1::jsonb::eql_v2_int4_eq", "$2::jsonb"), ("$1::jsonb", "$2::jsonb::eql_v2_int4_eq"), ]; - for op in ["<", "<=", ">", ">=", "~~", "~~*", "@>", "<@"] { + for op in ["<", "<=", ">", ">=", "@>", "<@"] { for (lhs, rhs) in shapes { let sql = format!("SELECT {lhs} {op} {rhs}"); let err = sqlx::query(&sql) diff --git a/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs index a135c84a..de202575 100644 --- a/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_ord_ore_tests.rs @@ -347,7 +347,7 @@ async fn encrypted_int4_ord_ore_unsupported_operators_raise(pool: PgPool) -> Res ("$1::jsonb", "$2::jsonb::eql_v2_int4_ord_ore"), ]; - for op in ["~~", "~~*", "@>", "<@"] { + for op in ["@>", "<@"] { for (lhs, rhs) in shapes { let sql = format!("SELECT {lhs} {op} {rhs}"); let err = sqlx::query(&sql) @@ -406,16 +406,16 @@ async fn encrypted_int4_ord_ore_blocked_operators_raise_on_null_input(pool: PgPo let null: Option<&str> = None; let err = - sqlx::query("SELECT $1::jsonb::eql_v2_int4_ord_ore ~~ $2::jsonb::eql_v2_int4_ord_ore") + sqlx::query("SELECT $1::jsonb::eql_v2_int4_ord_ore @> $2::jsonb::eql_v2_int4_ord_ore") .bind(null) .bind(null) .fetch_one(&pool) .await - .expect_err("eql_v2_int4_ord_ore ~~ must raise on NULL input") + .expect_err("eql_v2_int4_ord_ore @> must raise on NULL input") .to_string(); assert!( - err.contains("operator ~~ is not supported for eql_v2_int4_ord_ore"), - "unexpected error for ~~ on NULL: {err}" + err.contains("operator @> is not supported for eql_v2_int4_ord_ore"), + "unexpected error for @> on NULL: {err}" ); let err = sqlx::query("SELECT $1::jsonb -> $2::jsonb::eql_v2_int4_ord_ore") diff --git a/tests/sqlx/tests/encrypted_int4_ord_tests.rs b/tests/sqlx/tests/encrypted_int4_ord_tests.rs index 5d047107..746acdbc 100644 --- a/tests/sqlx/tests/encrypted_int4_ord_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_ord_tests.rs @@ -88,7 +88,7 @@ async fn ord_blocked_operators_raise(pool: PgPool) -> Result<()> { ("$1::jsonb::eql_v2_int4_ord", "$2::jsonb"), ("$1::jsonb", "$2::jsonb::eql_v2_int4_ord"), ]; - for op in ["~~", "~~*", "@>", "<@"] { + for op in ["@>", "<@"] { for (lhs, rhs) in shapes { let sql = format!("SELECT {lhs} {op} {rhs}"); let err = sqlx::query(&sql) diff --git a/tests/sqlx/tests/encrypted_int4_tests.rs b/tests/sqlx/tests/encrypted_int4_tests.rs index 8a2fed17..4eb3a212 100644 --- a/tests/sqlx/tests/encrypted_int4_tests.rs +++ b/tests/sqlx/tests/encrypted_int4_tests.rs @@ -17,7 +17,7 @@ async fn all_symmetric_operators_raise(pool: PgPool) -> Result<()> { ("$1::jsonb", "$2::jsonb::eql_v2_int4"), ]; - for op in ["=", "<>", "<", "<=", ">", ">=", "~~", "~~*", "@>", "<@"] { + for op in ["=", "<>", "<", "<=", ">", ">=", "@>", "<@"] { for (lhs, rhs) in shapes { let sql = format!("SELECT {lhs} {op} {rhs}"); let err = sqlx::query(&sql) @@ -58,6 +58,29 @@ async fn path_operators_raise(pool: PgPool) -> Result<()> { Ok(()) } +#[sqlx::test] +async fn like_operators_are_not_declared(pool: PgPool) -> Result<()> { + // EQL no longer declares ~~ / ~~* (LIKE / ILIKE) on the int4 domains — + // int4 has no pattern-match capability. With the operators removed, + // `col ~~ x` raises PostgreSQL's native "operator does not exist" + // rather than an EQL blocker message. Pin that they stay gone. + for op in ["~~", "~~*"] { + let sql = format!("SELECT $1::jsonb::eql_v2_int4 {op} $2::jsonb::eql_v2_int4"); + let err = sqlx::query(&sql) + .bind(SAMPLE_PAYLOAD) + .bind(SAMPLE_PAYLOAD) + .fetch_one(&pool) + .await + .expect_err(&format!("eql_v2_int4 {op} must not resolve: {sql}")) + .to_string(); + assert!( + err.contains("operator does not exist"), + "expected native 'operator does not exist' for {op}: {err}" + ); + } + Ok(()) +} + #[sqlx::test] async fn blockers_raise_on_typed_column(pool: PgPool) -> Result<()> { // The other tests exercise blockers on cast literals @@ -80,7 +103,7 @@ async fn blockers_raise_on_typed_column(pool: PgPool) -> Result<()> { .execute(&mut *tx) .await?; - for op in ["=", "<>", "<", "<=", ">", ">=", "~~", "~~*", "@>", "<@"] { + for op in ["=", "<>", "<", "<=", ">", ">=", "@>", "<@"] { // A raised blocker aborts the transaction; wrap each probe in a // savepoint so the next operator can be checked after rollback. sqlx::query("SAVEPOINT op_probe").execute(&mut *tx).await?; From e57ed4feeff68035f7003191a568b7a343e159d8 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Fri, 22 May 2026 09:08:47 +1000 Subject: [PATCH 10/13] docs(encrypted_int4): remove stale ~~ from unsupported-operator doc comment The ~~ / ~~* operators were dropped from the int4 variant family, so listing ~~ as an example operator symbol in the encrypted_domain_unsupported_bool @param comment is misleading. --- src/encrypted_domain/functions.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encrypted_domain/functions.sql b/src/encrypted_domain/functions.sql index b00ab755..3689f14c 100644 --- a/src/encrypted_domain/functions.sql +++ b/src/encrypted_domain/functions.sql @@ -14,7 +14,7 @@ --! for TYPE' so unsupported domain operators surface a clear --! error rather than fall through to native jsonb behaviour. --! @param type_name Domain type name (eql_v2_int4*) ---! @param operator_name Operator symbol (=, <, ~~, @>, ->, etc.) +--! @param operator_name Operator symbol (=, <, @>, ->, etc.) --! @return boolean (never returns; always raises) CREATE FUNCTION eql_v2.encrypted_domain_unsupported_bool(type_name text, operator_name text) RETURNS boolean From f7707d3e30bc49601a203cc3180a2a343effd069 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Fri, 22 May 2026 09:08:47 +1000 Subject: [PATCH 11/13] docs(encrypted_int4): drop v2.4 upgrade-note references from changelog The docs/upgrading/v2.4.md upgrade guide is not tracked on this branch and this work is not being cut as a new incremental release. Remove the version targeting line, the U-001 link in the variant-family entry, and the Upgrade notes subsection. The [Unreleased] entry and its #225 PR link stay. --- CHANGELOG.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 962bc335..c3f2061f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,15 +20,9 @@ Each entry that ships in a published release links to the PR that introduced it. ## [Unreleased] -The additive `eql_v2_int4` variant family targets `2.4.0`; see [`docs/upgrading/v2.4.md`](docs/upgrading/v2.4.md) for its upgrade notes. - ### Added -- **`eql_v2_int4` variant family — four capability-encoded domain types for encrypted `int4` columns.** Pick the variant whose operator surface matches the index terms your column carries: `eql_v2_int4` (storage only, every operator blocked — carries `c`), `eql_v2_int4_eq` (HMAC equality only — `=`, `<>` — carries `c`, `hm`), `eql_v2_int4_ord_ore` (equality + ORE-block ordering — `=`, `<>`, `<`, `<=`, `>`, `>=` — carries `c`, `ob`), or `eql_v2_int4_ord` (the recommended ordered name; the identical operator surface to `eql_v2_int4_ord_ore`). Each variant exposes a uniform index extractor — `eql_v2.eq_term(col)` for `eql_v2_int4_eq`, `eql_v2.ord_term(col)` for the ordered variants — and no index recipe needs a `::jsonb` cast. Ordered columns share one functional btree across equality and range, `CREATE INDEX ... USING btree (eql_v2.ord_term(col))`, with `ORDER BY eql_v2.ord_term(col)` sorting in plaintext order; `eql_v2_int4_eq` indexes `eql_v2.eq_term(col)` with `USING hash` or `USING btree`. `eql_v2.ord_term` returns the internal `eql_v2.ore_block_u64_8_256` composite, which carries EQL's existing `DEFAULT` btree operator class, so no operator class is defined on the public domain types. The ordered variants do not carry an `hm` term: ORE on a full-domain `int4` is lossless, so the order term doubles as an exact equality term. All variants live in `public` and survive `eql_v2` uninstall. Each domain carries a `CHECK` constraint requiring the EQL envelope (`v`, `i`), the ciphertext (`c`), and the variant's index term(s), so a payload missing a required key is rejected on insert or cast rather than surfacing later at query time. Per-variant payload requirements and index recipes: [U-001](docs/upgrading/v2.4.md#u-001-eql_v2_int4-variant-family). Note: the ORE operator class is excluded from the Supabase build, so ordered `int4` columns fall back to seq-scan for range on Supabase. ([#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) - -### Upgrade notes - -For the `2.4.0`-targeted `eql_v2_int4` variant family, see [`docs/upgrading/v2.4.md`](docs/upgrading/v2.4.md): [U-001 — `eql_v2_int4` variant family](docs/upgrading/v2.4.md#u-001-eql_v2_int4-variant-family). +- **`eql_v2_int4` variant family — four capability-encoded domain types for encrypted `int4` columns.** Pick the variant whose operator surface matches the index terms your column carries: `eql_v2_int4` (storage only, every operator blocked — carries `c`), `eql_v2_int4_eq` (HMAC equality only — `=`, `<>` — carries `c`, `hm`), `eql_v2_int4_ord_ore` (equality + ORE-block ordering — `=`, `<>`, `<`, `<=`, `>`, `>=` — carries `c`, `ob`), or `eql_v2_int4_ord` (the recommended ordered name; the identical operator surface to `eql_v2_int4_ord_ore`). Each variant exposes a uniform index extractor — `eql_v2.eq_term(col)` for `eql_v2_int4_eq`, `eql_v2.ord_term(col)` for the ordered variants — and no index recipe needs a `::jsonb` cast. Ordered columns share one functional btree across equality and range, `CREATE INDEX ... USING btree (eql_v2.ord_term(col))`, with `ORDER BY eql_v2.ord_term(col)` sorting in plaintext order; `eql_v2_int4_eq` indexes `eql_v2.eq_term(col)` with `USING hash` or `USING btree`. `eql_v2.ord_term` returns the internal `eql_v2.ore_block_u64_8_256` composite, which carries EQL's existing `DEFAULT` btree operator class, so no operator class is defined on the public domain types. The ordered variants do not carry an `hm` term: ORE on a full-domain `int4` is lossless, so the order term doubles as an exact equality term. All variants live in `public` and survive `eql_v2` uninstall. Each domain carries a `CHECK` constraint requiring the EQL envelope (`v`, `i`), the ciphertext (`c`), and the variant's index term(s), so a payload missing a required key is rejected on insert or cast rather than surfacing later at query time. Note: the ORE operator class is excluded from the Supabase build, so ordered `int4` columns fall back to seq-scan for range on Supabase. ([#225](https://github.com/cipherstash/encrypt-query-language/pull/225)) ## [2.3.0] — 2026-05-20 From 0bcb7bde86d86787533ce0b429ec92451f3db0fc Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Fri, 22 May 2026 09:39:54 +1000 Subject: [PATCH 12/13] Clarify encrypted domain implementation spec --- .../encrypted-domain-implementation-spec.md | 129 +++++++++++++----- 1 file changed, 94 insertions(+), 35 deletions(-) diff --git a/docs/reference/encrypted-domain-implementation-spec.md b/docs/reference/encrypted-domain-implementation-spec.md index 1a0142b0..56c1ec94 100644 --- a/docs/reference/encrypted-domain-implementation-spec.md +++ b/docs/reference/encrypted-domain-implementation-spec.md @@ -20,10 +20,10 @@ capability is encoded in the domain name: | Domain name | Capability | Capability terms | |------------------------|-------------------------------------|------------------| -| `eql_v2_` | storage only — every operator raises | `c` | -| `eql_v2__eq` | equality (`=`, `<>`) | `c`, `hm` | -| `eql_v2__ord` | equality + ordering (`=` `<>` `<` `<=` `>` `>=`) | `c`, `ob` | -| `eql_v2__ord_` | as `_ord`, scheme-explicit name | `c`, `ob` | +| `eql_v2_` | storage only — every operator raises | design-note ciphertext term; int4 uses `c` | +| `eql_v2__eq` | equality (`=`, `<>`) | ciphertext + equality term; int4 uses `c`, `hm` | +| `eql_v2__ord` | equality + ordering (`=` `<>` `<` `<=` `>` `>=`) | ciphertext + order term, and possibly equality term; int4 uses `c`, `ob` | +| `eql_v2__ord_` | as `_ord`, scheme-explicit name | same as that ordered variant's design note | A caller picks the domain whose capability matches the searches they need; an unmatched operator **raises** rather than silently falling @@ -31,9 +31,12 @@ through to native `jsonb` behaviour. Every domain also carries the EQL envelope keys (`v`, `i`) in addition to the capability terms above, and **each domain enforces a `CHECK` -constraint** requiring the envelope plus its capability terms — a -malformed payload is rejected at the point it is cast into the domain -(§3). +constraint** requiring the envelope plus its capability terms (§3). +The baseline `CHECK` pattern is presence/type-shape validation only: +it rejects non-object and under-populated payloads at the point they are +cast into the domain, but it does not prove that a present term has the +full internal structure an extractor expects unless the type chooses a +stronger structural `CHECK`. ### What is fixed vs. what each type decides @@ -57,6 +60,11 @@ transfer automatically: - int4 ships two ordered domains (`_ord` and `_ord_ore`) as mechanical twins. A type with a single ordering scheme needs only `_ord`. - `jsonb` does not fit the scalar storage/eq/ord shape; see §11. +- Native edge semantics are type-specific. The design note must + explicitly include or exclude edge values such as float/double `NaN`, + infinities and `-0.0`/`0.0`; `numeric` precision/scale boundaries; + date min/max and calendar boundaries; timestamp timezone and DST + behavior; and bool's two-value domain. Resolve these before writing code, and record them in a short type-specific design note. Everything else follows the checklist. @@ -73,6 +81,10 @@ Work top to bottom. Each item links to its reference section. ordered variants carry `hm` and where `=`/`<>` route (§1, §4). - [ ] Pick the index-term type(s) the extractors will return — they must already carry a default operator class (§4). +- [ ] Document native edge semantics for the type: include or exclude + float/double `NaN`, infinities, and signed zero; `numeric` + precision/scale boundaries; date boundaries; timestamp timezone + and DST behavior; and bool's tiny value domain (§8). ### Types - [ ] Declare every domain in `src/encrypted_domain/types.sql` as an @@ -142,12 +154,17 @@ Work top to bottom. Each item links to its reference section. - **Every domain carries a `CHECK` constraint.** The payload must be a `jsonb` object carrying the EQL envelope (`v`, `i`), the ciphertext - (`c`), **and every capability term the variant relies on** — `hm` for - an `_eq` variant, `ob` for an `_ord` variant. The constraint is - enforced when a value is cast into the domain, so a malformed or - under-populated payload is rejected at write time rather than failing - obscurely inside an extractor later. The storage variant requires only - `v`, `i`, `c`; each capability variant adds its term: + term, **and every capability term the variant relies on** — for + example `hm` for an int4 `_eq` variant and `ob` for an int4 lossless + `_ord` variant. The baseline constraint is enforced when a value is + cast into the domain and is intentionally limited to presence and + coarse type-shape unless the type chooses stronger structural checks. + It rejects non-object and under-populated payloads at write time; a + present but malformed term can still fail later inside an extractor or + query wrapper if the domain `CHECK` does not validate that term's + internal shape. The storage variant requires only the envelope and + ciphertext; each capability variant adds the terms from its design + note: ```sql CREATE DOMAIN public.eql_v2__eq AS jsonb @@ -168,6 +185,9 @@ Work top to bottom. Each item links to its reference section. surface; keep them in sync with a twin-sync test (§7). - **Payload terms** are a per-variant assumption, documented in each file's `--! @file` header (e.g. *"Payload-term assumption: `c`, `hm`."*). + If a variant chooses structural `CHECK` validation beyond + presence/type-shape, document that choice in the same design note and + add matching negative tests (§7, §10). --- @@ -199,8 +219,13 @@ Instead, index through a **functional index on an extractor function**: opclass you need. **Build caveat:** the internal ORE composite operator class is excluded -from the **Supabase** build variant, so ordered columns have **no -indexed range on Supabase** (seq-scan). Note this in the upgrade doc. +from the **Supabase** build variant, so ORE-backed range/order +predicates have **no indexed range on Supabase** (seq-scan). If an +ordered variant keeps a non-ORE equality term and routes `=`/`<>` +through `eq_term`, that equality path can still use its equality +functional index; only the ORE range/order path loses its opclass. If +equality is routed through a lossless ORE `ord_term`, it is subject to +the same Supabase limitation. Note this in the upgrade doc. --- @@ -317,20 +342,28 @@ Declaring the full surface is what prevents fall-through to native | `=` `<>` `<` `<=` `>` `>=` `~~` `~~*` `@>` `<@` | symmetric boolean (10) | `(domain,domain)`, `(domain,jsonb)`, `(jsonb,domain)` | | `->` `->>` | path (2) | `(domain,text)`, `(domain,integer)`, `(jsonb,domain)` | -The `(*,jsonb)` / `(jsonb,*)` shapes cover ORM bind patterns where one -operand arrives as raw `jsonb`. That is **12 operators × 3 shapes = 36 -`CREATE OPERATOR` statements per variant.** +The `(*,jsonb)` / `(jsonb,*)` boolean shapes cover ORM bind patterns +where one operand arrives as raw `jsonb`. The `(jsonb,domain)` path +shape for `->` / `->>` is defensive blocker coverage so native `jsonb` +path behavior cannot leak through; it is not a supported ORM bind +pattern. That is **12 operators × 3 shapes = 36 `CREATE OPERATOR` +statements per variant.** -### Function counts per variant (reference: int4) +### Function counts per variant -| Variant | Extractor | Wrappers | Blockers | Functions | Operators | -|---------|-----------|----------|----------|-----------|-----------| +| Variant | Extractors | Wrappers | Blockers | Functions | Operators | +|---------|------------|----------|----------|-----------|-----------| | storage `eql_v2_` | 0 | 0 | 36 | 36 | 36 | -| `eql_v2__eq` | 1 | 6 | 30 | 37 | 36 | -| `eql_v2__ord[_ore]` | 1 | 18 | 18 | 37 | 36 | +| equality-only `eql_v2__eq` | 1 (`eq_term`) | 6 | 30 | 37 | 36 | +| lossless ordered `eql_v2__ord[_scheme]` | 1 (`ord_term`) | 18 | 18 | 37 | 36 | +| lossy ordered `eql_v2__ord[_scheme]` | 2 (`eq_term`, `ord_term`) | 18 | 18 | 38 | 36 | (Wrappers/blockers = supported/unsupported operators × 3 shapes; the -storage variant supports nothing.) +storage variant supports nothing.) Lossless ordered variants may route +equality wrappers through `ord_term`; lossy ordered variants route +equality wrappers through `eq_term` and range/order wrappers through +`ord_term`. Lossy ordered variants therefore usually need separate +equality and order index recipes. ### `CREATE OPERATOR` metadata @@ -394,7 +427,8 @@ declarations in `_operators.sql`. One pair per variant: | Inlinability catalogue assertion (see §10) | eq, ord | | Operator planner-metadata assertion (`COMMUTATOR`/`NEGATOR` present) | eq, ord | | Blockers engage on a real typed column, not just cast literals | storage (+ others) | -| Domain `CHECK` rejects malformed / under-populated payloads at the cast | all | +| Domain `CHECK` rejects non-object / under-populated payloads at the cast | all | +| Malformed required term shapes fail at the domain `CHECK` if structurally enforced, otherwise at extractor/query time | eq, ord | | Twin-sync source check | twinned variants | --- @@ -430,16 +464,33 @@ declarations in `_operators.sql`. One pair per variant: ``` - **One payload, all terms.** Each `payload` carries every term the - family uses (`c`, `hm`, `ob`) so a single fixture feeds every - variant's suite — the ordered suites read `ob`, the equality suite - reads `hm`, from the same rows. + family design note says any variant uses, so a single fixture feeds + every variant's suite. For int4 this means `c`, `hm`, and `ob`: the + ordered suites read `ob`, the equality suite reads `hm`, from the same + rows. Do not hard-code those term names for a different type; derive + them from that type's variants and search configuration. - **Value-set design rules:** - Choose pivots so each range operator yields a **distinct cardinality** — a swapped operator then fails an assertion instead of silently passing. - Include negative values and boundary values where the type allows. - - All values distinct, so a distinctness sweep proves no two - plaintexts share an index term. + - All values distinct where the native type and semantics allow, so a + distinctness sweep proves no two plaintexts share an equality term + and, for lossless ordered variants, no two plaintexts share an order + term. + - For lossy ordered variants, do not assert distinct order terms. + Instead, prove `=`/`<>` use the equality term and range/order + predicates use order semantics, with separate equality and order + index recipes when both can be indexed. + - Add negative-space fixture/tests for omitted non-required terms + (for example, ordered variants that intentionally do not carry an + equality term) and malformed required terms per variant. + - Add a native edge-semantics checklist to the type design note: + float/double `NaN`, infinities, and `-0.0`/`0.0`; `numeric` + precision and scale limits; date boundaries; timestamp timezone and + DST behavior; and bool's tiny value domain. Each item must be + explicitly included or excluded, and the encrypted semantics must be + documented. - int4 uses 14 values; size similarly. --- @@ -469,7 +520,9 @@ A new type is user-facing, so per `CLAUDE.md` release discipline: - A `## [Unreleased]` entry in `CHANGELOG.md` (`Added`). - A numbered upgrade note (`U-NNN`) in the active `docs/upgrading/v.md` — variant set, the extractor interface, - index recipes, and the Supabase seq-scan caveat for ordered columns. + index recipes, and the Supabase seq-scan caveat for ORE range/order + predicates. If ordered equality is routed through a non-ORE equality + term, document that equality indexing remains available separately. - All SQL functions/types need Doxygen `--!` comments (`@brief`, `@param`, `@return`, …) per `CLAUDE.md`. @@ -500,10 +553,16 @@ The bar a new type's test suites must clear: variants without `hm`), strip that term from the payload and prove the variant still routes correctly — so an accidental regression to the wrong term fails instead of passing on a fully-populated fixture. -- **Payload validation.** Assert the domain `CHECK` rejects malformed - payloads — a non-object, and an object missing the envelope (`v`, - `i`), the ciphertext (`c`), or the variant's capability term — with a - `violates check constraint` error at the cast. + For lossy ordered variants, also prove equality predicates use the + equality term and range/order predicates use the order term. +- **Payload validation.** Assert the domain `CHECK` rejects payloads + outside its declared validation scope — at minimum a non-object and an + object missing the envelope (`v`, `i`), the ciphertext term, or the + variant's required capability term — with a `violates check + constraint` error at the cast. If a type chooses stronger structural + `CHECK` validation, malformed required term shapes must fail there + too. If it keeps the baseline presence/type-shape `CHECK`, malformed + required term shapes must fail in extractor/query-time tests instead. - **Real columns, not just literals.** At least one test per variant runs operators against a genuine `eql_v2__`-typed table column, the shape a real caller writes. From 333bfbcaba261eb55e4f73edf1a4a33d7ad2a0ce Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Fri, 22 May 2026 15:15:45 +1000 Subject: [PATCH 13/13] docs(claude): add encrypted-domain types section to CLAUDE.md Point contributors at docs/reference/encrypted-domain-implementation-spec.md when adding a new encrypted-domain type, and surface the footguns the spec exists to prevent: STRICT blockers, domain-over-domain, opclass-on-domain, the inlining requirements, and clean-before-build. Also add src/encrypted_domain/ to the Directory Structure list. --- CLAUDE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 65ed6d58..6c00d346 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,6 +61,7 @@ This is the **Encrypt Query Language (EQL)** - a PostgreSQL extension for search - `src/operators/` - SQL operators for encrypted data comparisons - `src/config/` - Configuration management functions - `src/blake3/`, `src/hmac_256/`, `src/bloom_filter/`, `src/ore_*` - Index implementations +- `src/encrypted_domain/` - Encrypted-domain type families (jsonb-backed PostgreSQL domains, one per operator/index capability) - `tasks/` - mise task scripts - `tests/sqlx/` - Rust/SQLx test framework (PostgreSQL 14-17 support) - `release/` - Generated SQL installation files @@ -72,6 +73,20 @@ This is the **Encrypt Query Language (EQL)** - a PostgreSQL extension for search - **Operators**: Support comparisons between encrypted and plain JSONB data - **CipherStash Proxy**: Required for encryption/decryption operations +### Encrypted-Domain Types + +`src/encrypted_domain/` holds **encrypted-domain type families** — jsonb-backed PostgreSQL domains, one domain per operator/index capability (`eql_v2_` storage-only, `eql_v2__eq`, `eql_v2__ord`). `eql_v2_int4` (PR #225) is the reference implementation; `int8`, `bool`, `date`, `float`, `numeric`, `timestamp`, and `jsonb` follow the same pattern. + +**Adding a new encrypted-domain type: follow `docs/reference/encrypted-domain-implementation-spec.md`.** It is the consolidated spec, checklist, and per-section reference. The mechanics are fixed; per-type judgment calls (variant set, payload terms, ORE lossless vs lossy, native edge semantics) go in a short type-specific design note resolved *first*. + +Footguns the spec exists to prevent: + +- **Blockers must never be `STRICT`.** A `STRICT` blocker lets PostgreSQL skip the body and return `NULL` on a `NULL` argument, silently bypassing the "operator not supported" exception. +- **No domain-over-domain** (`CREATE DOMAIN a AS b`). Operators resolve against the ultimate base type (`jsonb`), so a derived domain does not inherit the base domain's operator surface — blockers stop engaging. +- **No operator class on a domain.** Index through a functional index on the extractor (`eq_term` / `ord_term`), whose return type already carries a default opclass. +- **Inlinable functions** (extractors, comparison wrappers) need `LANGUAGE sql`, a single-statement `SELECT`, `IMMUTABLE`, and **no `SET` clause** — a pinned `search_path` disables inlining. Allowlist each one in `tasks/pin_search_path.sql` and `tasks/test/splinter.sh`. +- **Build with `mise run clean && mise run build`** — a bare build can leave stale `release/*.sql`. + ### Testing Infrastructure - Tests are written in Rust using SQLx, located in `tests/sqlx/` - Tests run against PostgreSQL 14, 15, 16, 17 using Docker containers