|
| 1 | +-- REQUIRE: src/v3/schema.sql |
| 2 | +-- REQUIRE: src/v3/sem/ore_cllw/types.sql |
| 3 | + |
| 4 | +--! @file v3/sem/ore_cllw/functions.sql |
| 5 | +--! @brief CLLW ORE index-term extraction and comparison (eql_v3 SEM). |
| 6 | + |
| 7 | +--! @brief Extract CLLW ORE index term from raw jsonb |
| 8 | +--! |
| 9 | +--! Returns the CLLW ORE ciphertext from the `oc` field of a single sv element |
| 10 | +--! supplied as raw jsonb. Inlinable single-statement SQL — the planner folds |
| 11 | +--! the body into the calling query. |
| 12 | +--! |
| 13 | +--! **Missing-`oc` semantics**: returns SQL-level NULL (not a composite with |
| 14 | +--! NULL bytes) when `oc` is absent, so btree's NULL handling filters those |
| 15 | +--! rows from range queries. |
| 16 | +--! |
| 17 | +--! @param val jsonb An object carrying an `oc` field |
| 18 | +--! @return eql_v3.ore_cllw Composite carrying the CLLW ciphertext, or NULL |
| 19 | +--! when the `oc` field is absent. |
| 20 | +--! @see eql_v3.has_ore_cllw |
| 21 | +--! @see eql_v3.compare_ore_cllw_term |
| 22 | +CREATE FUNCTION eql_v3.ore_cllw(val jsonb) |
| 23 | + RETURNS eql_v3.ore_cllw |
| 24 | + LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE |
| 25 | +AS $$ |
| 26 | + SELECT CASE WHEN val ->> 'oc' IS NULL THEN NULL |
| 27 | + ELSE ROW(decode(val ->> 'oc', 'hex'))::eql_v3.ore_cllw |
| 28 | + END |
| 29 | +$$; |
| 30 | + |
| 31 | +COMMENT ON FUNCTION eql_v3.ore_cllw(jsonb) IS |
| 32 | + 'eql-inline-critical: raw-jsonb CLLW extractor; must stay inlinable (unpinned search_path)'; |
| 33 | + |
| 34 | +--! @brief Check if a raw jsonb value contains a CLLW ORE index term |
| 35 | +--! @param val jsonb An object that may carry an `oc` field |
| 36 | +--! @return boolean True if `oc` field is present and non-null |
| 37 | +CREATE FUNCTION eql_v3.has_ore_cllw(val jsonb) |
| 38 | + RETURNS boolean |
| 39 | + LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE |
| 40 | +AS $$ |
| 41 | + SELECT val ->> 'oc' IS NOT NULL |
| 42 | +$$; |
| 43 | + |
| 44 | +COMMENT ON FUNCTION eql_v3.has_ore_cllw(jsonb) IS |
| 45 | + 'eql-inline-critical: raw-jsonb CLLW presence helper; must stay inlinable (unpinned search_path)'; |
| 46 | + |
| 47 | +--! @brief CLLW per-byte comparison helper |
| 48 | +--! @internal |
| 49 | +--! |
| 50 | +--! Byte-by-byte comparison implementing the CLLW order-revealing protocol. |
| 51 | +--! Identify the index of the first differing byte; if `(y_byte + 1) == x_byte` |
| 52 | +--! (mod 256) there, then x > y; otherwise x < y. Equal inputs return 0. Inputs |
| 53 | +--! MUST be the same length (the caller guarantees this). Stays `LANGUAGE |
| 54 | +--! plpgsql` — the per-byte loop can't be a single inlinable SQL expression. |
| 55 | +--! |
| 56 | +--! @param a bytea First CLLW ciphertext slice |
| 57 | +--! @param b bytea Second CLLW ciphertext slice |
| 58 | +--! @return integer -1, 0, or 1 |
| 59 | +--! @throws Exception if inputs are different lengths |
| 60 | +--! @see eql_v3.compare_ore_cllw_term |
| 61 | +CREATE FUNCTION eql_v3.compare_ore_cllw_term_bytes(a bytea, b bytea) |
| 62 | +RETURNS int |
| 63 | + SET search_path = pg_catalog, extensions, public |
| 64 | +AS $$ |
| 65 | +DECLARE |
| 66 | + len_a INT; |
| 67 | + len_b INT; |
| 68 | + i INT; |
| 69 | + first_diff INT := 0; |
| 70 | +BEGIN |
| 71 | + |
| 72 | + len_a := LENGTH(a); |
| 73 | + len_b := LENGTH(b); |
| 74 | + |
| 75 | + IF len_a != len_b THEN |
| 76 | + RAISE EXCEPTION 'ore_cllw index terms are not the same length'; |
| 77 | + END IF; |
| 78 | + |
| 79 | + FOR i IN 1..len_a LOOP |
| 80 | + IF first_diff = 0 AND get_byte(a, i - 1) != get_byte(b, i - 1) THEN |
| 81 | + first_diff := i; |
| 82 | + END IF; |
| 83 | + END LOOP; |
| 84 | + |
| 85 | + IF first_diff = 0 THEN |
| 86 | + RETURN 0; |
| 87 | + END IF; |
| 88 | + |
| 89 | + IF ((get_byte(b, first_diff - 1) + 1) & 255) = get_byte(a, first_diff - 1) THEN |
| 90 | + RETURN 1; |
| 91 | + ELSE |
| 92 | + RETURN -1; |
| 93 | + END IF; |
| 94 | +END; |
| 95 | +$$ LANGUAGE plpgsql; |
| 96 | + |
| 97 | +--! @brief Variable-length CLLW ORE term comparison |
| 98 | +--! @internal |
| 99 | +--! |
| 100 | +--! Three-way comparison of two CLLW ORE ciphertext terms of potentially |
| 101 | +--! different lengths. Compares the shared prefix via the CLLW per-byte |
| 102 | +--! protocol; on equal prefixes, the shorter input sorts first. The leading |
| 103 | +--! domain-tag byte makes numeric (`0x00`) sort before string (`0x01`). Stays |
| 104 | +--! `LANGUAGE plpgsql` because it dispatches to `compare_ore_cllw_term_bytes`. |
| 105 | +--! |
| 106 | +--! btree filters NULL composites at the row level, so this should never see a |
| 107 | +--! NULL composite under normal operation; the IS-NULL guard returns NULL |
| 108 | +--! defensively. A non-NULL composite with NULL `bytes` is a contract violation |
| 109 | +--! — the extractor returns SQL NULL (not ROW(NULL)) on missing `oc`, so raise |
| 110 | +--! loudly rather than silently misorder. |
| 111 | +--! |
| 112 | +--! @param a eql_v3.ore_cllw First term |
| 113 | +--! @param b eql_v3.ore_cllw Second term |
| 114 | +--! @return integer -1, 0, or 1; NULL if either composite is NULL |
| 115 | +--! @throws Exception if either composite has a NULL `bytes` field |
| 116 | +--! @see eql_v3.compare_ore_cllw_term_bytes |
| 117 | +CREATE FUNCTION eql_v3.compare_ore_cllw_term(a eql_v3.ore_cllw, b eql_v3.ore_cllw) |
| 118 | +RETURNS int |
| 119 | + SET search_path = pg_catalog, extensions, public |
| 120 | +AS $$ |
| 121 | +DECLARE |
| 122 | + len_a INT; |
| 123 | + len_b INT; |
| 124 | + common_len INT; |
| 125 | + cmp_result INT; |
| 126 | +BEGIN |
| 127 | + IF a::text IS NULL OR b::text IS NULL THEN |
| 128 | + RETURN NULL; |
| 129 | + END IF; |
| 130 | + |
| 131 | + IF a.bytes IS NULL OR b.bytes IS NULL THEN |
| 132 | + RAISE EXCEPTION 'eql_v3.compare_ore_cllw_term: composite has NULL bytes field — extractor invariant violated. Check that the index expression uses eql_v3.ore_cllw(...) and not a hand-crafted ROW(NULL).'; |
| 133 | + END IF; |
| 134 | + |
| 135 | + len_a := LENGTH(a.bytes); |
| 136 | + len_b := LENGTH(b.bytes); |
| 137 | + |
| 138 | + IF len_a = 0 AND len_b = 0 THEN |
| 139 | + RETURN 0; |
| 140 | + ELSIF len_a = 0 THEN |
| 141 | + RETURN -1; |
| 142 | + ELSIF len_b = 0 THEN |
| 143 | + RETURN 1; |
| 144 | + END IF; |
| 145 | + |
| 146 | + IF len_a < len_b THEN |
| 147 | + common_len := len_a; |
| 148 | + ELSE |
| 149 | + common_len := len_b; |
| 150 | + END IF; |
| 151 | + |
| 152 | + cmp_result := eql_v3.compare_ore_cllw_term_bytes( |
| 153 | + SUBSTRING(a.bytes FROM 1 FOR common_len), |
| 154 | + SUBSTRING(b.bytes FROM 1 FOR common_len) |
| 155 | + ); |
| 156 | + |
| 157 | + IF cmp_result = -1 THEN |
| 158 | + RETURN -1; |
| 159 | + ELSIF cmp_result = 1 THEN |
| 160 | + RETURN 1; |
| 161 | + END IF; |
| 162 | + |
| 163 | + IF len_a < len_b THEN |
| 164 | + RETURN -1; |
| 165 | + ELSIF len_a > len_b THEN |
| 166 | + RETURN 1; |
| 167 | + ELSE |
| 168 | + RETURN 0; |
| 169 | + END IF; |
| 170 | +END; |
| 171 | +$$ LANGUAGE plpgsql; |
0 commit comments