Skip to content
Merged
53 changes: 53 additions & 0 deletions src/encrypted/hash.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- REQUIRE: src/schema.sql
-- REQUIRE: src/encrypted/types.sql
-- REQUIRE: src/hmac_256/types.sql
-- REQUIRE: src/hmac_256/functions.sql
-- REQUIRE: src/blake3/types.sql
-- REQUIRE: src/blake3/functions.sql
-- REQUIRE: src/ste_vec/functions.sql

--! @brief Compute hash integer for encrypted value
--!
--! Produces a 32-bit integer hash suitable for PostgreSQL hash joins, GROUP BY,
--! DISTINCT, and hash aggregate operations. Uses deterministic index terms
--! (Blake3 or HMAC-256) to ensure consistency with the equality operator:
--! if a = b then hash(a) = hash(b).
--!
--! Blake3 is checked before HMAC-256 to maintain the hash/equality contract.
--! eql_v2.compare uses the first index term present in BOTH operands (priority:
--! ORE > HMAC > Blake3). If value A has hm+b3 and value B has only b3, compare
--! uses Blake3. The hash function must also use Blake3 for A so that
--! hash(A) == hash(B). Preferring Blake3 (the lowest-priority deterministic
--! term) ensures any two values that compare equal will hash identically.
--!
--! @param val eql_v2_encrypted Encrypted value to hash
--! @return integer 32-bit hash value derived from Blake3 or HMAC-256 index term
--!
--! @throws Exception if no HMAC-256 or Blake3 index term is present
--!
--! @note Requires a match (blake3) or unique (hmac_256) index configured on the column
--! @note ORE-only values cannot be hashed (ORE ciphertext is not deterministic)
--!
--! @see eql_v2.blake3
--! @see eql_v2.hmac_256
--! @see eql_v2.compare
CREATE FUNCTION eql_v2.hash_encrypted(val eql_v2_encrypted)
RETURNS integer
IMMUTABLE STRICT PARALLEL SAFE
AS $$
DECLARE
ste_val eql_v2_encrypted;
BEGIN
ste_val := eql_v2.to_ste_vec_value(val);

IF eql_v2.has_blake3(ste_val) THEN
RETURN hashtext(eql_v2.blake3(ste_val)::text);
END IF;

IF eql_v2.has_hmac_256(ste_val) THEN
RETURN hashtext(eql_v2.hmac_256(ste_val)::text);
END IF;

RAISE EXCEPTION 'Cannot hash eql_v2_encrypted value: no hmac_256 or blake3 index term found. Configure a unique or match index for hash operations (GROUP BY, DISTINCT, hash joins).';
END;
$$ LANGUAGE plpgsql;
3 changes: 0 additions & 3 deletions src/operators/<>.sql
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ CREATE OPERATOR <> (
NEGATOR = =,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand All @@ -78,7 +77,6 @@ CREATE OPERATOR <> (
NEGATOR = =,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand All @@ -105,7 +103,6 @@ CREATE OPERATOR <> (
NEGATOR = =,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand Down
2 changes: 0 additions & 2 deletions src/operators/=.sql
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ CREATE OPERATOR = (
NEGATOR = <>,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand Down Expand Up @@ -131,7 +130,6 @@ CREATE OPERATOR = (
NEGATOR = <>,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

29 changes: 29 additions & 0 deletions src/operators/hash_operator_class.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- REQUIRE: src/schema.sql
-- REQUIRE: src/encrypted/types.sql
-- REQUIRE: src/encrypted/hash.sql
-- REQUIRE: src/operators/=.sql

--! @brief PostgreSQL hash operator class for encrypted value hashing
--!
--! Defines the hash operator family and operator class required for hash-based
--! operations on encrypted values. This enables PostgreSQL to use hash strategies for:
--! - Hash joins (cross-row equality via hash)
--! - GROUP BY (hash aggregation)
--! - DISTINCT (hash-based deduplication)
--! - UNION (hash-based set operations)
--!
--! Only the same-type equality operator (eql_v2_encrypted = eql_v2_encrypted) is
--! registered. Cross-type operators (encrypted/jsonb) are excluded because hash
--! joins require independent hashing of each side before comparison.
--!
--! @note Requires hmac_256 or blake3 index terms for correct hashing
--! @see eql_v2.hash_encrypted
--! @see eql_v2.encrypted_operator_class (btree)

CREATE OPERATOR FAMILY eql_v2.encrypted_hash_operator_family USING hash;

CREATE OPERATOR CLASS eql_v2.encrypted_hash_operator_class
DEFAULT FOR TYPE eql_v2_encrypted USING hash
FAMILY eql_v2.encrypted_hash_operator_family AS
OPERATOR 1 = (eql_v2_encrypted, eql_v2_encrypted),
FUNCTION 1 eql_v2.hash_encrypted(eql_v2_encrypted);
6 changes: 0 additions & 6 deletions src/operators/~~.sql
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ CREATE OPERATOR ~~(
RIGHTARG=eql_v2_encrypted,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand All @@ -108,7 +107,6 @@ CREATE OPERATOR ~~*(
RIGHTARG=eql_v2_encrypted,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand Down Expand Up @@ -140,7 +138,6 @@ CREATE OPERATOR ~~(
RIGHTARG=jsonb,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand All @@ -150,7 +147,6 @@ CREATE OPERATOR ~~*(
RIGHTARG=jsonb,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand Down Expand Up @@ -182,7 +178,6 @@ CREATE OPERATOR ~~(
RIGHTARG=eql_v2_encrypted,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand All @@ -192,7 +187,6 @@ CREATE OPERATOR ~~*(
RIGHTARG=eql_v2_encrypted,
RESTRICT = eqsel,
JOIN = eqjoinsel,
HASHES,
MERGES
);

Expand Down
2 changes: 1 addition & 1 deletion tasks/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ cat tasks/uninstall.sql >> release/cipherstash-encrypt-uninstall.sql


# Supabase specific build which excludes operator classes as they are not supported
find src -type f -path "*.sql" ! -path "*_test.sql" ! -path "**/operator_class.sql" | while IFS= read -r sql_file; do
find src -type f -path "*.sql" ! -path "*_test.sql" ! -path "**/*operator_class.sql" | while IFS= read -r sql_file; do
echo $sql_file

echo "$sql_file $sql_file" >> src/deps-supabase.txt
Expand Down
Loading