@prisma-next/extension-cipherstash 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/dist/call-classes-CSvD7w8U.mjs +206 -0
- package/dist/call-classes-CSvD7w8U.mjs.map +1 -0
- package/dist/column-types.d.mts +33 -0
- package/dist/column-types.d.mts.map +1 -0
- package/dist/column-types.mjs +42 -0
- package/dist/column-types.mjs.map +1 -0
- package/dist/constants-BDxL9Pe3.d.mts +22 -0
- package/dist/constants-BDxL9Pe3.d.mts.map +1 -0
- package/dist/constants-B_2TNvUi.mjs +46 -0
- package/dist/constants-B_2TNvUi.mjs.map +1 -0
- package/dist/control.d.mts +7 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +430 -0
- package/dist/control.mjs.map +1 -0
- package/dist/descriptor-meta-BgQfZTAF.mjs +129 -0
- package/dist/descriptor-meta-BgQfZTAF.mjs.map +1 -0
- package/dist/envelope-P9BxfJNr.mjs +271 -0
- package/dist/envelope-P9BxfJNr.mjs.map +1 -0
- package/dist/middleware.d.mts +13 -0
- package/dist/middleware.d.mts.map +1 -0
- package/dist/middleware.mjs +129 -0
- package/dist/middleware.mjs.map +1 -0
- package/dist/migration.d.mts +141 -0
- package/dist/migration.d.mts.map +1 -0
- package/dist/migration.mjs +2 -0
- package/dist/operation-types.d.mts +49 -0
- package/dist/operation-types.d.mts.map +1 -0
- package/dist/operation-types.mjs +1 -0
- package/dist/pack.d.mts +86 -0
- package/dist/pack.d.mts.map +1 -0
- package/dist/pack.mjs +2 -0
- package/dist/runtime.d.mts +207 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +429 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/sdk-D5FTGyzp.d.mts +67 -0
- package/dist/sdk-D5FTGyzp.d.mts.map +1 -0
- package/package.json +69 -0
- package/src/contract/authoring.ts +62 -0
- package/src/contract/contract.d.ts +149 -0
- package/src/contract/contract.json +104 -0
- package/src/contract/contract.prisma +46 -0
- package/src/execution/abort.ts +143 -0
- package/src/execution/codec-runtime.ts +209 -0
- package/src/execution/decrypt-all.ts +217 -0
- package/src/execution/envelope.ts +263 -0
- package/src/execution/operators.ts +211 -0
- package/src/execution/parameterized.ts +71 -0
- package/src/execution/routing.ts +93 -0
- package/src/execution/sdk.ts +68 -0
- package/src/exports/column-types.ts +62 -0
- package/src/exports/contract-space-typing.ts +86 -0
- package/src/exports/control.ts +120 -0
- package/src/exports/middleware.ts +24 -0
- package/src/exports/migration.ts +43 -0
- package/src/exports/operation-types.ts +16 -0
- package/src/exports/pack.ts +13 -0
- package/src/exports/runtime.ts +110 -0
- package/src/extension-metadata/codec-metadata.ts +81 -0
- package/src/extension-metadata/constants.ts +70 -0
- package/src/extension-metadata/descriptor-meta.ts +76 -0
- package/src/middleware/bulk-encrypt.ts +192 -0
- package/src/migration/call-classes.ts +350 -0
- package/src/migration/cipherstash-codec.ts +157 -0
- package/src/migration/eql-bundle.ts +29 -0
- package/src/migration/eql-install.generated.ts +5751 -0
- package/src/types/operation-types.ts +81 -0
package/dist/control.mjs
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { i as CIPHERSTASH_STRING_CODEC_ID, r as CIPHERSTASH_SPACE_ID, t as CIPHERSTASH_BASELINE_MIGRATION_NAME } from "./constants-B_2TNvUi.mjs";
|
|
2
|
+
import { t as cipherstashPackMeta } from "./descriptor-meta-BgQfZTAF.mjs";
|
|
3
|
+
import { n as cipherstashRemoveSearchConfig, t as cipherstashAddSearchConfig } from "./call-classes-CSvD7w8U.mjs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
//#region migrations/cipherstash/20260601T0000_install_eql_bundle/migration.json
|
|
6
|
+
var migration_default = {
|
|
7
|
+
from: null,
|
|
8
|
+
to: "sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4",
|
|
9
|
+
labels: [],
|
|
10
|
+
providedInvariants: ["cipherstash:install-eql-bundle-v1"],
|
|
11
|
+
createdAt: "2026-05-09T03:42:56.902Z",
|
|
12
|
+
fromContract: null,
|
|
13
|
+
toContract: {
|
|
14
|
+
"schemaVersion": "1",
|
|
15
|
+
"targetFamily": "sql",
|
|
16
|
+
"target": "postgres",
|
|
17
|
+
"profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e",
|
|
18
|
+
"roots": { "eql_v2_configuration": "EqlV2Configuration" },
|
|
19
|
+
"models": { "EqlV2Configuration": {
|
|
20
|
+
"fields": {
|
|
21
|
+
"data": {
|
|
22
|
+
"nullable": false,
|
|
23
|
+
"type": {
|
|
24
|
+
"codecId": "pg/jsonb@1",
|
|
25
|
+
"kind": "scalar"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"id": {
|
|
29
|
+
"nullable": false,
|
|
30
|
+
"type": {
|
|
31
|
+
"codecId": "pg/text@1",
|
|
32
|
+
"kind": "scalar"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"state": {
|
|
36
|
+
"nullable": false,
|
|
37
|
+
"type": {
|
|
38
|
+
"codecId": "pg/text@1",
|
|
39
|
+
"kind": "scalar"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"relations": {},
|
|
44
|
+
"storage": {
|
|
45
|
+
"fields": {
|
|
46
|
+
"data": { "column": "data" },
|
|
47
|
+
"id": { "column": "id" },
|
|
48
|
+
"state": { "column": "state" }
|
|
49
|
+
},
|
|
50
|
+
"table": "eql_v2_configuration"
|
|
51
|
+
}
|
|
52
|
+
} },
|
|
53
|
+
"storage": {
|
|
54
|
+
"storageHash": "sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4",
|
|
55
|
+
"tables": { "eql_v2_configuration": {
|
|
56
|
+
"columns": {
|
|
57
|
+
"data": {
|
|
58
|
+
"codecId": "pg/jsonb@1",
|
|
59
|
+
"nativeType": "jsonb",
|
|
60
|
+
"nullable": false
|
|
61
|
+
},
|
|
62
|
+
"id": {
|
|
63
|
+
"codecId": "pg/text@1",
|
|
64
|
+
"nativeType": "text",
|
|
65
|
+
"nullable": false
|
|
66
|
+
},
|
|
67
|
+
"state": {
|
|
68
|
+
"codecId": "pg/text@1",
|
|
69
|
+
"nativeType": "text",
|
|
70
|
+
"nullable": false
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"foreignKeys": [],
|
|
74
|
+
"indexes": [],
|
|
75
|
+
"primaryKey": { "columns": ["id"] },
|
|
76
|
+
"uniques": []
|
|
77
|
+
} }
|
|
78
|
+
},
|
|
79
|
+
"capabilities": {
|
|
80
|
+
"postgres": {
|
|
81
|
+
"jsonAgg": true,
|
|
82
|
+
"lateral": true,
|
|
83
|
+
"limit": true,
|
|
84
|
+
"orderBy": true,
|
|
85
|
+
"returning": true
|
|
86
|
+
},
|
|
87
|
+
"sql": {
|
|
88
|
+
"defaultInInsert": true,
|
|
89
|
+
"enums": true,
|
|
90
|
+
"returning": true
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"extensionPacks": {},
|
|
94
|
+
"meta": {},
|
|
95
|
+
"_generated": {
|
|
96
|
+
"warning": "⚠️ GENERATED FILE - DO NOT EDIT",
|
|
97
|
+
"message": "This file is automatically generated by \"prisma-next contract emit\".",
|
|
98
|
+
"regenerate": "To regenerate, run: prisma-next contract emit"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
hints: {
|
|
102
|
+
"used": [],
|
|
103
|
+
"applied": [],
|
|
104
|
+
"plannerVersion": "2.0.0"
|
|
105
|
+
},
|
|
106
|
+
migrationHash: "sha256:c9ac07fca95d6159ecb21841514b87fbc51c2cf7ba55f2f28ae4c07f25781ab2"
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region migrations/cipherstash/20260601T0000_install_eql_bundle/ops.json
|
|
110
|
+
var ops_default = [{
|
|
111
|
+
"id": "cipherstash.install-eql-bundle",
|
|
112
|
+
"label": "Install EQL bundle (functions, operators, casts, op classes, schema, types)",
|
|
113
|
+
"operationClass": "additive",
|
|
114
|
+
"invariantId": "cipherstash:install-eql-bundle-v1",
|
|
115
|
+
"target": { "id": "postgres" },
|
|
116
|
+
"precheck": [],
|
|
117
|
+
"execute": [{
|
|
118
|
+
"description": "Install EQL bundle (functions, operators, casts, op classes, schema, types)",
|
|
119
|
+
"sql": "--! @file schema.sql\n--! @brief EQL v2 schema creation\n--!\n--! Creates the eql_v2 schema which contains all Encrypt Query Language\n--! functions, types, and tables. Drops existing schema if present to\n--! support clean reinstallation.\n--!\n--! @warning DROP SCHEMA CASCADE will remove all objects in the schema\n--! @note All EQL objects (functions, types, tables) reside in eql_v2 schema\n\n--! @brief Drop existing EQL v2 schema\n--! @warning CASCADE will drop all dependent objects\nDROP SCHEMA IF EXISTS eql_v2 CASCADE;\n\n--! @brief Create EQL v2 schema\n--! @note All EQL functions and types will be created in this schema\nCREATE SCHEMA eql_v2;\n\n--! @brief Composite type for encrypted column data\n--!\n--! Core type used for all encrypted columns in EQL. Stores encrypted data as JSONB\n--! with the following structure:\n--! - `c`: ciphertext (base64-encoded encrypted value)\n--! - `i`: index terms (searchable metadata for encrypted searches)\n--! - `k`: key ID (identifier for encryption key)\n--! - `m`: metadata (additional encryption metadata)\n--!\n--! Created in public schema to persist independently of eql_v2 schema lifecycle.\n--! Customer data columns use this type, so it must not be dropped if data exists.\n--!\n--! @note DO NOT DROP this type unless absolutely certain no encrypted data uses it\n--! @see eql_v2.ciphertext\n--! @see eql_v2.meta_data\n--! @see eql_v2.add_column\nDO $$\n BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'eql_v2_encrypted') THEN\n CREATE TYPE public.eql_v2_encrypted AS (\n data jsonb\n );\n END IF;\n END\n$$;\n\n\n\n\n\n\n\n\n\n\n--! @brief Bloom filter index term type\n--!\n--! Domain type representing Bloom filter bit arrays stored as smallint arrays.\n--! Used for pattern-match encrypted searches via the 'match' index type.\n--! The filter is stored in the 'bf' field of encrypted data payloads.\n--!\n--! @see eql_v2.add_search_config\n--! @see eql_v2.\"~~\"\n--! @note This is a transient type used only during query execution\nCREATE DOMAIN eql_v2.bloom_filter AS smallint[];\n\n\n\n--! @brief ORE block term type for Order-Revealing Encryption\n--!\n--! Composite type representing a single ORE (Order-Revealing Encryption) block term.\n--! Stores encrypted data as bytea that enables range comparisons without decryption.\n--!\n--! @see eql_v2.ore_block_u64_8_256\n--! @see eql_v2.compare_ore_block_u64_8_256_term\nCREATE TYPE eql_v2.ore_block_u64_8_256_term AS (\n bytes bytea\n);\n\n\n--! @brief ORE block index term type for range queries\n--!\n--! Composite type containing an array of ORE block terms. Used for encrypted\n--! range queries via the 'ore' index type. The array is stored in the 'ob' field\n--! of encrypted data payloads.\n--!\n--! @see eql_v2.add_search_config\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\n--! @note This is a transient type used only during query execution\nCREATE TYPE eql_v2.ore_block_u64_8_256 AS (\n terms eql_v2.ore_block_u64_8_256_term[]\n);\n\n--! @brief HMAC-SHA256 index term type\n--!\n--! Domain type representing HMAC-SHA256 hash values.\n--! Used for exact-match encrypted searches via the 'unique' index type.\n--! The hash is stored in the 'hm' field of encrypted data payloads.\n--!\n--! @see eql_v2.add_search_config\n--! @note This is a transient type used only during query execution\nCREATE DOMAIN eql_v2.hmac_256 AS text;\n-- AUTOMATICALLY GENERATED FILE\n\n--! @file common.sql\n--! @brief Common utility functions\n--!\n--! Provides general-purpose utility functions used across EQL:\n--! - Constant-time bytea comparison for security\n--! - JSONB to bytea array conversion\n--! - Logging helpers for debugging and testing\n\n\n--! @brief Constant-time comparison of bytea values\n--! @internal\n--!\n--! Compares two bytea values in constant time to prevent timing attacks.\n--! Always checks all bytes even after finding differences, maintaining\n--! consistent execution time regardless of where differences occur.\n--!\n--! @param a bytea First value to compare\n--! @param b bytea Second value to compare\n--! @return boolean True if values are equal\n--!\n--! @note Returns false immediately if lengths differ (length is not secret)\n--! @note Used for secure comparison of cryptographic values\nCREATE FUNCTION eql_v2.bytea_eq(a bytea, b bytea) RETURNS boolean AS $$\nDECLARE\n result boolean;\n differing bytea;\nBEGIN\n\n -- Check if the bytea values are the same length\n IF LENGTH(a) != LENGTH(b) THEN\n RETURN false;\n END IF;\n\n -- Compare each byte in the bytea values\n result := true;\n FOR i IN 1..LENGTH(a) LOOP\n IF SUBSTRING(a FROM i FOR 1) != SUBSTRING(b FROM i FOR 1) THEN\n result := result AND false;\n END IF;\n END LOOP;\n\n RETURN result;\nEND;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Convert JSONB hex array to bytea array\n--! @internal\n--!\n--! Converts a JSONB array of hex-encoded strings into a PostgreSQL bytea array.\n--! Used for deserializing binary data (like ORE terms) from JSONB storage.\n--!\n--! @param jsonb JSONB array of hex-encoded strings\n--! @return bytea[] Array of decoded binary values\n--!\n--! @note Returns NULL if input is JSON null\n--! @note Each array element is hex-decoded to bytea\nCREATE FUNCTION eql_v2.jsonb_array_to_bytea_array(val jsonb)\nRETURNS bytea[] AS $$\nDECLARE\n terms_arr bytea[];\nBEGIN\n IF jsonb_typeof(val) = 'null' THEN\n RETURN NULL;\n END IF;\n\n SELECT array_agg(decode(value::text, 'hex')::bytea)\n INTO terms_arr\n FROM jsonb_array_elements_text(val) AS value;\n\n RETURN terms_arr;\nEND;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Log message for debugging\n--!\n--! Convenience function to emit log messages during testing and debugging.\n--! Uses RAISE NOTICE to output messages to PostgreSQL logs.\n--!\n--! @param text Message to log\n--!\n--! @note Primarily used in tests and development\n--! @see eql_v2.log(text, text) for contextual logging\nCREATE FUNCTION eql_v2.log(s text)\n RETURNS void\nAS $$\n BEGIN\n RAISE NOTICE '[LOG] %', s;\nEND;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Log message with context\n--!\n--! Overload of log function that includes context label for better\n--! log organization during testing.\n--!\n--! @param ctx text Context label (e.g., test name, module name)\n--! @param s text Message to log\n--!\n--! @note Format: \"[LOG] {ctx} {message}\"\n--! @see eql_v2.log(text)\nCREATE FUNCTION eql_v2.log(ctx text, s text)\n RETURNS void\nAS $$\n BEGIN\n RAISE NOTICE '[LOG] % %', ctx, s;\nEND;\n$$ LANGUAGE plpgsql;\n\n--! @brief CLLW ORE index term type for range queries\n--!\n--! Composite type for CLLW (Copyless Logarithmic Width) Order-Revealing Encryption.\n--! Each output block is 8-bits. Used for encrypted range queries via the 'ore' index type.\n--! The ciphertext is stored in the 'ocf' field of encrypted data payloads.\n--!\n--! @see eql_v2.add_search_config\n--! @see eql_v2.compare_ore_cllw_u64_8\n--! @note This is a transient type used only during query execution\nCREATE TYPE eql_v2.ore_cllw_u64_8 AS (\n bytes bytea\n);\n\n--! @file crypto.sql\n--! @brief PostgreSQL pgcrypto extension enablement\n--!\n--! Enables the pgcrypto extension which provides cryptographic functions\n--! used by EQL for hashing and other cryptographic operations.\n--!\n--! @note pgcrypto provides functions like digest(), hmac(), gen_random_bytes()\n--! @note IF NOT EXISTS prevents errors if extension already enabled\n\n--! @brief Enable pgcrypto extension\n--! @note Provides cryptographic functions for hashing and random number generation\nCREATE EXTENSION IF NOT EXISTS pgcrypto;\n\n\n--! @brief Extract ciphertext from encrypted JSONB value\n--!\n--! Extracts the ciphertext (c field) from a raw JSONB encrypted value.\n--! The ciphertext is the base64-encoded encrypted data.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Text Base64-encoded ciphertext string\n--! @throws Exception if 'c' field is not present in JSONB\n--!\n--! @example\n--! -- Extract ciphertext from JSONB literal\n--! SELECT eql_v2.ciphertext('{\"c\":\"AQIDBA==\",\"i\":{\"unique\":\"...\"}}'::jsonb);\n--!\n--! @see eql_v2.ciphertext(eql_v2_encrypted)\n--! @see eql_v2.meta_data\nCREATE FUNCTION eql_v2.ciphertext(val jsonb)\n RETURNS text\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val ? 'c' THEN\n RETURN val->>'c';\n END IF;\n RAISE 'Expected a ciphertext (c) value in json: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Extract ciphertext from encrypted column value\n--!\n--! Extracts the ciphertext from an encrypted column value. Convenience\n--! overload that unwraps eql_v2_encrypted type and delegates to JSONB version.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Text Base64-encoded ciphertext string\n--! @throws Exception if encrypted value is malformed\n--!\n--! @example\n--! -- Extract ciphertext from encrypted column\n--! SELECT eql_v2.ciphertext(encrypted_email) FROM users;\n--!\n--! @see eql_v2.ciphertext(jsonb)\n--! @see eql_v2.meta_data\nCREATE FUNCTION eql_v2.ciphertext(val eql_v2_encrypted)\n RETURNS text\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.ciphertext(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief State transition function for grouped_value aggregate\n--! @internal\n--!\n--! Returns the first non-null value encountered. Used as state function\n--! for the grouped_value aggregate to select first value in each group.\n--!\n--! @param $1 JSONB Accumulated state (first non-null value found)\n--! @param $2 JSONB New value from current row\n--! @return JSONB First non-null value (state or new value)\n--!\n--! @see eql_v2.grouped_value\nCREATE FUNCTION eql_v2._first_grouped_value(jsonb, jsonb)\nRETURNS jsonb AS $$\n SELECT COALESCE($1, $2);\n$$ LANGUAGE sql IMMUTABLE;\n\n--! @brief Return first non-null encrypted value in a group\n--!\n--! Aggregate function that returns the first non-null encrypted value\n--! encountered within a GROUP BY clause. Useful for deduplication or\n--! selecting representative values from grouped encrypted data.\n--!\n--! @param input JSONB Encrypted values to aggregate\n--! @return JSONB First non-null encrypted value in group\n--!\n--! @example\n--! -- Get first email per user group\n--! SELECT user_id, eql_v2.grouped_value(encrypted_email)\n--! FROM user_emails\n--! GROUP BY user_id;\n--!\n--! -- Deduplicate encrypted values\n--! SELECT DISTINCT ON (user_id)\n--! user_id,\n--! eql_v2.grouped_value(encrypted_ssn) as primary_ssn\n--! FROM user_records\n--! GROUP BY user_id;\n--!\n--! @see eql_v2._first_grouped_value\nCREATE AGGREGATE eql_v2.grouped_value(jsonb) (\n SFUNC = eql_v2._first_grouped_value,\n STYPE = jsonb\n);\n\n--! @brief Add validation constraint to encrypted column\n--!\n--! Adds a CHECK constraint to ensure column values conform to encrypted data\n--! structure. Constraint uses eql_v2.check_encrypted to validate format.\n--! Called automatically by eql_v2.add_column.\n--!\n--! @param table_name TEXT Name of table containing the column\n--! @param column_name TEXT Name of column to constrain\n--! @return Void\n--!\n--! @example\n--! -- Manually add constraint (normally done by add_column)\n--! SELECT eql_v2.add_encrypted_constraint('users', 'encrypted_email');\n--!\n--! -- Resulting constraint:\n--! -- ALTER TABLE users ADD CONSTRAINT eql_v2_encrypted_check_encrypted_email\n--! -- CHECK (eql_v2.check_encrypted(encrypted_email));\n--!\n--! @see eql_v2.add_column\n--! @see eql_v2.remove_encrypted_constraint\nCREATE FUNCTION eql_v2.add_encrypted_constraint(table_name TEXT, column_name TEXT)\n RETURNS void\nAS $$\n BEGIN\n EXECUTE format('ALTER TABLE %I ADD CONSTRAINT eql_v2_encrypted_constraint_%I_%I CHECK (eql_v2.check_encrypted(%I))', table_name, table_name, column_name, column_name);\n EXCEPTION\n WHEN duplicate_table THEN\n WHEN duplicate_object THEN\n RAISE NOTICE 'Constraint `eql_v2_encrypted_constraint_%_%` already exists, skipping', table_name, column_name;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Remove validation constraint from encrypted column\n--!\n--! Removes the CHECK constraint that validates encrypted data structure.\n--! Called automatically by eql_v2.remove_column. Uses IF EXISTS to avoid\n--! errors if constraint doesn't exist.\n--!\n--! @param table_name TEXT Name of table containing the column\n--! @param column_name TEXT Name of column to unconstrain\n--! @return Void\n--!\n--! @example\n--! -- Manually remove constraint (normally done by remove_column)\n--! SELECT eql_v2.remove_encrypted_constraint('users', 'encrypted_email');\n--!\n--! @see eql_v2.remove_column\n--! @see eql_v2.add_encrypted_constraint\nCREATE FUNCTION eql_v2.remove_encrypted_constraint(table_name TEXT, column_name TEXT)\n RETURNS void\nAS $$\n BEGIN\n EXECUTE format('ALTER TABLE %I DROP CONSTRAINT IF EXISTS eql_v2_encrypted_constraint_%I_%I', table_name, table_name, column_name);\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Extract metadata from encrypted JSONB value\n--!\n--! Extracts index terms (i) and version (v) from a raw JSONB encrypted value.\n--! Returns metadata object containing searchable index terms without ciphertext.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return JSONB Metadata object with 'i' (index terms) and 'v' (version) fields\n--!\n--! @example\n--! -- Extract metadata to inspect index terms\n--! SELECT eql_v2.meta_data('{\"c\":\"...\",\"i\":{\"unique\":\"abc123\"},\"v\":1}'::jsonb);\n--! -- Returns: {\"i\":{\"unique\":\"abc123\"},\"v\":1}\n--!\n--! @see eql_v2.meta_data(eql_v2_encrypted)\n--! @see eql_v2.ciphertext\nCREATE FUNCTION eql_v2.meta_data(val jsonb)\n RETURNS jsonb\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN jsonb_build_object(\n 'i', val->'i',\n 'v', val->'v'\n );\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Extract metadata from encrypted column value\n--!\n--! Extracts index terms and version from an encrypted column value.\n--! Convenience overload that unwraps eql_v2_encrypted type and\n--! delegates to JSONB version.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return JSONB Metadata object with 'i' (index terms) and 'v' (version) fields\n--!\n--! @example\n--! -- Inspect index terms for encrypted column\n--! SELECT user_id, eql_v2.meta_data(encrypted_email) as email_metadata\n--! FROM users;\n--!\n--! @see eql_v2.meta_data(jsonb)\n--! @see eql_v2.ciphertext\nCREATE FUNCTION eql_v2.meta_data(val eql_v2_encrypted)\n RETURNS jsonb\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.meta_data(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Variable-width CLLW ORE index term type for range queries\n--!\n--! Composite type for variable-width CLLW (Copyless Logarithmic Width) Order-Revealing Encryption.\n--! Each output block is 8-bits. Unlike ore_cllw_u64_8, supports variable-length ciphertexts.\n--! Used for encrypted range queries via the 'ore' index type.\n--! The ciphertext is stored in the 'ocv' field of encrypted data payloads.\n--!\n--! @see eql_v2.add_search_config\n--! @see eql_v2.compare_ore_cllw_var_8\n--! @note This is a transient type used only during query execution\nCREATE TYPE eql_v2.ore_cllw_var_8 AS (\n bytes bytea\n);\n\n\n--! @brief Extract CLLW ORE index term from JSONB payload\n--!\n--! Extracts the CLLW ORE ciphertext from the 'ocf' field of an encrypted\n--! data payload. Used internally for range query comparisons.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return eql_v2.ore_cllw_u64_8 CLLW ORE ciphertext\n--! @throws Exception if 'ocf' field is missing when ore index is expected\n--!\n--! @see eql_v2.has_ore_cllw_u64_8\n--! @see eql_v2.compare_ore_cllw_u64_8\nCREATE FUNCTION eql_v2.ore_cllw_u64_8(val jsonb)\n RETURNS eql_v2.ore_cllw_u64_8\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF NOT (eql_v2.has_ore_cllw_u64_8(val)) THEN\n RAISE 'Expected a ore_cllw_u64_8 index (ocf) value in json: %', val;\n END IF;\n\n RETURN ROW(decode(val->>'ocf', 'hex'));\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract CLLW ORE index term from encrypted column value\n--!\n--! Extracts the CLLW ORE ciphertext from an encrypted column value by accessing\n--! its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return eql_v2.ore_cllw_u64_8 CLLW ORE ciphertext\n--!\n--! @see eql_v2.ore_cllw_u64_8(jsonb)\nCREATE FUNCTION eql_v2.ore_cllw_u64_8(val eql_v2_encrypted)\n RETURNS eql_v2.ore_cllw_u64_8\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (SELECT eql_v2.ore_cllw_u64_8(val.data));\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if JSONB payload contains CLLW ORE index term\n--!\n--! Tests whether the encrypted data payload includes an 'ocf' field,\n--! indicating a CLLW ORE ciphertext is available for range queries.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Boolean True if 'ocf' field is present and non-null\n--!\n--! @see eql_v2.ore_cllw_u64_8\nCREATE FUNCTION eql_v2.has_ore_cllw_u64_8(val jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN val ->> 'ocf' IS NOT NULL;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if encrypted column value contains CLLW ORE index term\n--!\n--! Tests whether an encrypted column value includes a CLLW ORE ciphertext\n--! by checking its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Boolean True if CLLW ORE ciphertext is present\n--!\n--! @see eql_v2.has_ore_cllw_u64_8(jsonb)\nCREATE FUNCTION eql_v2.has_ore_cllw_u64_8(val eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.has_ore_cllw_u64_8(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Compare CLLW ORE ciphertext bytes\n--! @internal\n--!\n--! Byte-by-byte comparison of CLLW ORE ciphertexts implementing the CLLW\n--! comparison algorithm. Used by both fixed-width (ore_cllw_u64_8) and\n--! variable-width (ore_cllw_var_8) ORE variants.\n--!\n--! @param a Bytea First CLLW ORE ciphertext\n--! @param b Bytea Second CLLW ORE ciphertext\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--! @throws Exception if ciphertexts are different lengths\n--!\n--! @note Shared comparison logic for multiple ORE CLLW schemes\n--! @see eql_v2.compare_ore_cllw_u64_8\nCREATE FUNCTION eql_v2.compare_ore_cllw_term_bytes(a bytea, b bytea)\nRETURNS int AS $$\nDECLARE\n len_a INT;\n len_b INT;\n x BYTEA;\n y BYTEA;\n i INT;\n differing boolean;\nBEGIN\n\n -- Check if the lengths of the two bytea arguments are the same\n len_a := LENGTH(a);\n len_b := LENGTH(b);\n\n IF len_a != len_b THEN\n RAISE EXCEPTION 'ore_cllw index terms are not the same length';\n END IF;\n\n -- Iterate over each byte and compare them\n FOR i IN 1..len_a LOOP\n x := SUBSTRING(a FROM i FOR 1);\n y := SUBSTRING(b FROM i FOR 1);\n\n -- Check if there's a difference\n IF x != y THEN\n differing := true;\n EXIT;\n END IF;\n END LOOP;\n\n -- If a difference is found, compare the bytes as in Rust logic\n IF differing THEN\n IF (get_byte(y, 0) + 1) % 256 = get_byte(x, 0) THEN\n RETURN 1;\n ELSE\n RETURN -1;\n END IF;\n ELSE\n RETURN 0;\n END IF;\nEND;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Blake3 hash index term type\n--!\n--! Domain type representing Blake3 cryptographic hash values.\n--! Used for exact-match encrypted searches via the 'unique' index type.\n--! The hash is stored in the 'b3' field of encrypted data payloads.\n--!\n--! @see eql_v2.add_search_config\n--! @note This is a transient type used only during query execution\nCREATE DOMAIN eql_v2.blake3 AS text;\n\n--! @brief Extract Blake3 hash index term from JSONB payload\n--!\n--! Extracts the Blake3 hash value from the 'b3' field of an encrypted\n--! data payload. Used internally for exact-match comparisons.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return eql_v2.blake3 Blake3 hash value, or NULL if not present\n--! @throws Exception if 'b3' field is missing when blake3 index is expected\n--!\n--! @see eql_v2.has_blake3\n--! @see eql_v2.compare_blake3\nCREATE FUNCTION eql_v2.blake3(val jsonb)\n RETURNS eql_v2.blake3\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF NOT eql_v2.has_blake3(val) THEN\n RAISE 'Expected a blake3 index (b3) value in json: %', val;\n END IF;\n\n IF val->>'b3' IS NULL THEN\n RETURN NULL;\n END IF;\n\n RETURN val->>'b3';\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract Blake3 hash index term from encrypted column value\n--!\n--! Extracts the Blake3 hash from an encrypted column value by accessing\n--! its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return eql_v2.blake3 Blake3 hash value, or NULL if not present\n--!\n--! @see eql_v2.blake3(jsonb)\nCREATE FUNCTION eql_v2.blake3(val eql_v2_encrypted)\n RETURNS eql_v2.blake3\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (SELECT eql_v2.blake3(val.data));\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if JSONB payload contains Blake3 index term\n--!\n--! Tests whether the encrypted data payload includes a 'b3' field,\n--! indicating a Blake3 hash is available for exact-match queries.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Boolean True if 'b3' field is present and non-null\n--!\n--! @see eql_v2.blake3\nCREATE FUNCTION eql_v2.has_blake3(val jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN val ->> 'b3' IS NOT NULL;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if encrypted column value contains Blake3 index term\n--!\n--! Tests whether an encrypted column value includes a Blake3 hash\n--! by checking its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Boolean True if Blake3 hash is present\n--!\n--! @see eql_v2.has_blake3(jsonb)\nCREATE FUNCTION eql_v2.has_blake3(val eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.has_blake3(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract HMAC-SHA256 index term from JSONB payload\n--!\n--! Extracts the HMAC-SHA256 hash value from the 'hm' field of an encrypted\n--! data payload. Used internally for exact-match comparisons.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return eql_v2.hmac_256 HMAC-SHA256 hash value\n--! @throws Exception if 'hm' field is missing when hmac_256 index is expected\n--!\n--! @see eql_v2.has_hmac_256\n--! @see eql_v2.compare_hmac_256\nCREATE FUNCTION eql_v2.hmac_256(val jsonb)\n RETURNS eql_v2.hmac_256\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF eql_v2.has_hmac_256(val) THEN\n RETURN val->>'hm';\n END IF;\n RAISE 'Expected a hmac_256 index (hm) value in json: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if JSONB payload contains HMAC-SHA256 index term\n--!\n--! Tests whether the encrypted data payload includes an 'hm' field,\n--! indicating an HMAC-SHA256 hash is available for exact-match queries.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Boolean True if 'hm' field is present and non-null\n--!\n--! @see eql_v2.hmac_256\nCREATE FUNCTION eql_v2.has_hmac_256(val jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN val ->> 'hm' IS NOT NULL;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if encrypted column value contains HMAC-SHA256 index term\n--!\n--! Tests whether an encrypted column value includes an HMAC-SHA256 hash\n--! by checking its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Boolean True if HMAC-SHA256 hash is present\n--!\n--! @see eql_v2.has_hmac_256(jsonb)\nCREATE FUNCTION eql_v2.has_hmac_256(val eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.has_hmac_256(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Extract HMAC-SHA256 index term from encrypted column value\n--!\n--! Extracts the HMAC-SHA256 hash from an encrypted column value by accessing\n--! its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return eql_v2.hmac_256 HMAC-SHA256 hash value\n--!\n--! @see eql_v2.hmac_256(jsonb)\nCREATE FUNCTION eql_v2.hmac_256(val eql_v2_encrypted)\n RETURNS eql_v2.hmac_256\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (SELECT eql_v2.hmac_256(val.data));\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n\n--! @brief Convert JSONB array to ORE block composite type\n--! @internal\n--!\n--! Converts a JSONB array of hex-encoded ORE terms from the CipherStash Proxy\n--! payload into the PostgreSQL composite type used for ORE operations.\n--!\n--! @param val JSONB Array of hex-encoded ORE block terms\n--! @return eql_v2.ore_block_u64_8_256 ORE block composite type, or NULL if input is null\n--!\n--! @see eql_v2.ore_block_u64_8_256(jsonb)\nCREATE FUNCTION eql_v2.jsonb_array_to_ore_block_u64_8_256(val jsonb)\nRETURNS eql_v2.ore_block_u64_8_256 AS $$\nDECLARE\n terms eql_v2.ore_block_u64_8_256_term[];\nBEGIN\n IF jsonb_typeof(val) = 'null' THEN\n RETURN NULL;\n END IF;\n\n SELECT array_agg(ROW(b)::eql_v2.ore_block_u64_8_256_term)\n INTO terms\n FROM unnest(eql_v2.jsonb_array_to_bytea_array(val)) AS b;\n\n RETURN ROW(terms)::eql_v2.ore_block_u64_8_256;\nEND;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract ORE block index term from JSONB payload\n--!\n--! Extracts the ORE block array from the 'ob' field of an encrypted\n--! data payload. Used internally for range query comparisons.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return eql_v2.ore_block_u64_8_256 ORE block index term\n--! @throws Exception if 'ob' field is missing when ore index is expected\n--!\n--! @see eql_v2.has_ore_block_u64_8_256\n--! @see eql_v2.compare_ore_block_u64_8_256\nCREATE FUNCTION eql_v2.ore_block_u64_8_256(val jsonb)\n RETURNS eql_v2.ore_block_u64_8_256\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF eql_v2.has_ore_block_u64_8_256(val) THEN\n RETURN eql_v2.jsonb_array_to_ore_block_u64_8_256(val->'ob');\n END IF;\n RAISE 'Expected an ore index (ob) value in json: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract ORE block index term from encrypted column value\n--!\n--! Extracts the ORE block from an encrypted column value by accessing\n--! its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return eql_v2.ore_block_u64_8_256 ORE block index term\n--!\n--! @see eql_v2.ore_block_u64_8_256(jsonb)\nCREATE FUNCTION eql_v2.ore_block_u64_8_256(val eql_v2_encrypted)\n RETURNS eql_v2.ore_block_u64_8_256\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.ore_block_u64_8_256(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if JSONB payload contains ORE block index term\n--!\n--! Tests whether the encrypted data payload includes an 'ob' field,\n--! indicating an ORE block is available for range queries.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Boolean True if 'ob' field is present and non-null\n--!\n--! @see eql_v2.ore_block_u64_8_256\nCREATE FUNCTION eql_v2.has_ore_block_u64_8_256(val jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN val ->> 'ob' IS NOT NULL;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if encrypted column value contains ORE block index term\n--!\n--! Tests whether an encrypted column value includes an ORE block\n--! by checking its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Boolean True if ORE block is present\n--!\n--! @see eql_v2.has_ore_block_u64_8_256(jsonb)\nCREATE FUNCTION eql_v2.has_ore_block_u64_8_256(val eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.has_ore_block_u64_8_256(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Compare two ORE block terms using cryptographic comparison\n--! @internal\n--!\n--! Performs a three-way comparison (returns -1/0/1) of individual ORE block terms\n--! using the ORE cryptographic protocol. Compares PRP and PRF blocks to determine\n--! ordering without decryption.\n--!\n--! @param a eql_v2.ore_block_u64_8_256_term First ORE term to compare\n--! @param b eql_v2.ore_block_u64_8_256_term Second ORE term to compare\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--! @throws Exception if ciphertexts are different lengths\n--!\n--! @note Uses AES-ECB encryption for bit comparisons per ORE protocol\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\nCREATE FUNCTION eql_v2.compare_ore_block_u64_8_256_term(a eql_v2.ore_block_u64_8_256_term, b eql_v2.ore_block_u64_8_256_term)\n RETURNS integer\nAS $$\n DECLARE\n eq boolean := true;\n unequal_block smallint := 0;\n hash_key bytea;\n data_block bytea;\n encrypt_block bytea;\n target_block bytea;\n\n left_block_size CONSTANT smallint := 16;\n right_block_size CONSTANT smallint := 32;\n right_offset CONSTANT smallint := 136; -- 8 * 17\n\n indicator smallint := 0;\n BEGIN\n IF a IS NULL AND b IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b IS NULL THEN\n RETURN 1;\n END IF;\n\n IF bit_length(a.bytes) != bit_length(b.bytes) THEN\n RAISE EXCEPTION 'Ciphertexts are different lengths';\n END IF;\n\n FOR block IN 0..7 LOOP\n -- Compare each PRP (byte from the first 8 bytes) and PRF block (8 byte\n -- chunks of the rest of the value).\n -- NOTE:\n -- * Substr is ordinally indexed (hence 1 and not 0, and 9 and not 8).\n -- * We are not worrying about timing attacks here; don't fret about\n -- the OR or !=.\n IF\n substr(a.bytes, 1 + block, 1) != substr(b.bytes, 1 + block, 1)\n OR substr(a.bytes, 9 + left_block_size * block, left_block_size) != substr(b.bytes, 9 + left_block_size * BLOCK, left_block_size)\n THEN\n -- set the first unequal block we find\n IF eq THEN\n unequal_block := block;\n END IF;\n eq = false;\n END IF;\n END LOOP;\n\n IF eq THEN\n RETURN 0::integer;\n END IF;\n\n -- Hash key is the IV from the right CT of b\n hash_key := substr(b.bytes, right_offset + 1, 16);\n\n -- first right block is at right offset + nonce_size (ordinally indexed)\n target_block := substr(b.bytes, right_offset + 17 + (unequal_block * right_block_size), right_block_size);\n\n data_block := substr(a.bytes, 9 + (left_block_size * unequal_block), left_block_size);\n\n encrypt_block := public.encrypt(data_block::bytea, hash_key::bytea, 'aes-ecb');\n\n indicator := (\n get_bit(\n encrypt_block,\n 0\n ) + get_bit(target_block, get_byte(a.bytes, unequal_block))) % 2;\n\n IF indicator = 1 THEN\n RETURN 1::integer;\n ELSE\n RETURN -1::integer;\n END IF;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Compare arrays of ORE block terms recursively\n--! @internal\n--!\n--! Recursively compares arrays of ORE block terms element-by-element.\n--! Empty arrays are considered less than non-empty arrays. If the first elements\n--! are equal, recursively compares remaining elements.\n--!\n--! @param a eql_v2.ore_block_u64_8_256_term[] First array of ORE terms\n--! @param b eql_v2.ore_block_u64_8_256_term[] Second array of ORE terms\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b, NULL if either array is NULL\n--!\n--! @note Empty arrays sort before non-empty arrays\n--! @see eql_v2.compare_ore_block_u64_8_256_term\nCREATE FUNCTION eql_v2.compare_ore_block_u64_8_256_terms(a eql_v2.ore_block_u64_8_256_term[], b eql_v2.ore_block_u64_8_256_term[])\nRETURNS integer AS $$\n DECLARE\n cmp_result integer;\n BEGIN\n\n -- NULLs are NULL\n IF a IS NULL OR b IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- empty a and b\n IF cardinality(a) = 0 AND cardinality(b) = 0 THEN\n RETURN 0;\n END IF;\n\n -- empty a and some b\n IF (cardinality(a) = 0) AND cardinality(b) > 0 THEN\n RETURN -1;\n END IF;\n\n -- some a and empty b\n IF cardinality(a) > 0 AND (cardinality(b) = 0) THEN\n RETURN 1;\n END IF;\n\n cmp_result := eql_v2.compare_ore_block_u64_8_256_term(a[1], b[1]);\n\n IF cmp_result = 0 THEN\n -- Removes the first element in the array, and calls this fn again to compare the next element/s in the array.\n RETURN eql_v2.compare_ore_block_u64_8_256_terms(a[2:array_length(a,1)], b[2:array_length(b,1)]);\n END IF;\n\n RETURN cmp_result;\n END\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Compare ORE block composite types\n--! @internal\n--!\n--! Wrapper function that extracts term arrays from ORE block composite types\n--! and delegates to the array comparison function.\n--!\n--! @param a eql_v2.ore_block_u64_8_256 First ORE block\n--! @param b eql_v2.ore_block_u64_8_256 Second ORE block\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @see eql_v2.compare_ore_block_u64_8_256_terms(eql_v2.ore_block_u64_8_256_term[], eql_v2.ore_block_u64_8_256_term[])\nCREATE FUNCTION eql_v2.compare_ore_block_u64_8_256_terms(a eql_v2.ore_block_u64_8_256, b eql_v2.ore_block_u64_8_256)\nRETURNS integer AS $$\n BEGIN\n RETURN eql_v2.compare_ore_block_u64_8_256_terms(a.terms, b.terms);\n END\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract variable-width CLLW ORE index term from JSONB payload\n--!\n--! Extracts the variable-width CLLW ORE ciphertext from the 'ocv' field of an encrypted\n--! data payload. Used internally for range query comparisons.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return eql_v2.ore_cllw_var_8 Variable-width CLLW ORE ciphertext\n--! @throws Exception if 'ocv' field is missing when ore index is expected\n--!\n--! @see eql_v2.has_ore_cllw_var_8\n--! @see eql_v2.compare_ore_cllw_var_8\nCREATE FUNCTION eql_v2.ore_cllw_var_8(val jsonb)\n RETURNS eql_v2.ore_cllw_var_8\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF NOT (eql_v2.has_ore_cllw_var_8(val)) THEN\n RAISE 'Expected a ore_cllw_var_8 index (ocv) value in json: %', val;\n END IF;\n\n RETURN ROW(decode(val->>'ocv', 'hex'));\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract variable-width CLLW ORE index term from encrypted column value\n--!\n--! Extracts the variable-width CLLW ORE ciphertext from an encrypted column value by accessing\n--! its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return eql_v2.ore_cllw_var_8 Variable-width CLLW ORE ciphertext\n--!\n--! @see eql_v2.ore_cllw_var_8(jsonb)\nCREATE FUNCTION eql_v2.ore_cllw_var_8(val eql_v2_encrypted)\n RETURNS eql_v2.ore_cllw_var_8\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (SELECT eql_v2.ore_cllw_var_8(val.data));\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if JSONB payload contains variable-width CLLW ORE index term\n--!\n--! Tests whether the encrypted data payload includes an 'ocv' field,\n--! indicating a variable-width CLLW ORE ciphertext is available for range queries.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Boolean True if 'ocv' field is present and non-null\n--!\n--! @see eql_v2.ore_cllw_var_8\nCREATE FUNCTION eql_v2.has_ore_cllw_var_8(val jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN val ->> 'ocv' IS NOT NULL;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if encrypted column value contains variable-width CLLW ORE index term\n--!\n--! Tests whether an encrypted column value includes a variable-width CLLW ORE ciphertext\n--! by checking its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Boolean True if variable-width CLLW ORE ciphertext is present\n--!\n--! @see eql_v2.has_ore_cllw_var_8(jsonb)\nCREATE FUNCTION eql_v2.has_ore_cllw_var_8(val eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.has_ore_cllw_var_8(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Compare variable-width CLLW ORE ciphertext terms\n--! @internal\n--!\n--! Three-way comparison of variable-width CLLW ORE ciphertexts. Compares the common\n--! prefix using byte-by-byte CLLW comparison, then falls back to length comparison\n--! if the common prefix is equal. Used by compare_ore_cllw_var_8 for range queries.\n--!\n--! @param a eql_v2.ore_cllw_var_8 First variable-width CLLW ORE ciphertext\n--! @param b eql_v2.ore_cllw_var_8 Second variable-width CLLW ORE ciphertext\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @note Handles variable-length ciphertexts by comparing common prefix first\n--! @note Returns NULL if either input is NULL\n--!\n--! @see eql_v2.compare_ore_cllw_term_bytes\n--! @see eql_v2.compare_ore_cllw_var_8\nCREATE FUNCTION eql_v2.compare_ore_cllw_var_8_term(a eql_v2.ore_cllw_var_8, b eql_v2.ore_cllw_var_8)\nRETURNS int AS $$\nDECLARE\n len_a INT;\n len_b INT;\n -- length of the common part of the two bytea values\n common_len INT;\n cmp_result INT;\nBEGIN\n IF a IS NULL OR b IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- Get the lengths of both bytea inputs\n len_a := LENGTH(a.bytes);\n len_b := LENGTH(b.bytes);\n\n -- Handle empty cases\n IF len_a = 0 AND len_b = 0 THEN\n RETURN 0;\n ELSIF len_a = 0 THEN\n RETURN -1;\n ELSIF len_b = 0 THEN\n RETURN 1;\n END IF;\n\n -- Find the length of the shorter bytea\n IF len_a < len_b THEN\n common_len := len_a;\n ELSE\n common_len := len_b;\n END IF;\n\n -- Use the compare_ore_cllw_term function to compare byte by byte\n cmp_result := eql_v2.compare_ore_cllw_term_bytes(\n SUBSTRING(a.bytes FROM 1 FOR common_len),\n SUBSTRING(b.bytes FROM 1 FOR common_len)\n );\n\n -- If the comparison returns 'less' or 'greater', return that result\n IF cmp_result = -1 THEN\n RETURN -1;\n ELSIF cmp_result = 1 THEN\n RETURN 1;\n END IF;\n\n -- If the bytea comparison is 'equal', compare lengths\n IF len_a < len_b THEN\n RETURN -1;\n ELSIF len_a > len_b THEN\n RETURN 1;\n ELSE\n RETURN 0;\n END IF;\nEND;\n$$ LANGUAGE plpgsql;\n\n\n\n\n\n\n--! @brief Core comparison function for encrypted values\n--!\n--! Compares two encrypted values using their index terms without decryption.\n--! This function implements all comparison operators required for btree indexing\n--! (<, <=, =, >=, >).\n--!\n--! Index terms are checked in the following priority order:\n--! 1. ore_block_u64_8_256 (Order-Revealing Encryption)\n--! 2. ore_cllw_u64_8 (Order-Revealing Encryption)\n--! 3. ore_cllw_var_8 (Order-Revealing Encryption)\n--! 4. hmac_256 (Hash-based equality)\n--! 5. blake3 (Hash-based equality)\n--!\n--! The first index term type present in both values is used for comparison.\n--! If no matching index terms are found, falls back to JSONB literal comparison\n--! to ensure consistent ordering (required for btree correctness).\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @note Literal fallback prevents \"lock BufferContent is not held\" errors\n--! @see eql_v2.compare_ore_block_u64_8_256\n--! @see eql_v2.compare_blake3\n--! @see eql_v2.compare_hmac_256\nCREATE FUNCTION eql_v2.compare(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n\n IF a IS NULL AND b IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b IS NULL THEN\n RETURN 1;\n END IF;\n\n a := eql_v2.to_ste_vec_value(a);\n b := eql_v2.to_ste_vec_value(b);\n\n IF eql_v2.has_ore_block_u64_8_256(a) AND eql_v2.has_ore_block_u64_8_256(b) THEN\n RETURN eql_v2.compare_ore_block_u64_8_256(a, b);\n END IF;\n\n IF eql_v2.has_ore_cllw_u64_8(a) AND eql_v2.has_ore_cllw_u64_8(b) THEN\n RETURN eql_v2.compare_ore_cllw_u64_8(a, b);\n END IF;\n\n IF eql_v2.has_ore_cllw_var_8(a) AND eql_v2.has_ore_cllw_var_8(b) THEN\n RETURN eql_v2.compare_ore_cllw_var_8(a, b);\n END IF;\n\n IF eql_v2.has_hmac_256(a) AND eql_v2.has_hmac_256(b) THEN\n RETURN eql_v2.compare_hmac_256(a, b);\n END IF;\n\n IF eql_v2.has_blake3(a) AND eql_v2.has_blake3(b) THEN\n RETURN eql_v2.compare_blake3(a, b);\n END IF;\n\n -- Fallback to literal comparison of the encrypted data\n -- Compare must have consistent ordering for a given state\n -- Without this text fallback, database errors with \"lock BufferContent is not held\"\n RETURN eql_v2.compare_literal(a, b);\n\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Convert JSONB to encrypted type\n--!\n--! Wraps a JSONB encrypted payload into the eql_v2_encrypted composite type.\n--! Used internally for type conversions and operator implementations.\n--!\n--! @param jsonb JSONB encrypted payload with structure: {\"c\": \"...\", \"i\": {...}, \"k\": \"...\", \"v\": \"2\"}\n--! @return eql_v2_encrypted Encrypted value wrapped in composite type\n--!\n--! @note This is primarily used for implicit casts in operator expressions\n--! @see eql_v2.to_jsonb\nCREATE FUNCTION eql_v2.to_encrypted(data jsonb)\n RETURNS public.eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\nBEGIN\n IF data IS NULL THEN\n RETURN NULL;\n END IF;\n\n RETURN ROW(data)::public.eql_v2_encrypted;\nEND;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Implicit cast from JSONB to encrypted type\n--!\n--! Enables PostgreSQL to automatically convert JSONB values to eql_v2_encrypted\n--! in assignment contexts and comparison operations.\n--!\n--! @see eql_v2.to_encrypted(jsonb)\nCREATE CAST (jsonb AS public.eql_v2_encrypted)\n WITH FUNCTION eql_v2.to_encrypted(jsonb) AS ASSIGNMENT;\n\n\n--! @brief Convert text to encrypted type\n--!\n--! Parses a text representation of encrypted JSONB payload and wraps it\n--! in the eql_v2_encrypted composite type.\n--!\n--! @param text Text representation of JSONB encrypted payload\n--! @return eql_v2_encrypted Encrypted value wrapped in composite type\n--!\n--! @note Delegates to eql_v2.to_encrypted(jsonb) after parsing text as JSON\n--! @see eql_v2.to_encrypted(jsonb)\nCREATE FUNCTION eql_v2.to_encrypted(data text)\n RETURNS public.eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\nBEGIN\n IF data IS NULL THEN\n RETURN NULL;\n END IF;\n\n RETURN eql_v2.to_encrypted(data::jsonb);\nEND;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Implicit cast from text to encrypted type\n--!\n--! Enables PostgreSQL to automatically convert text JSON strings to eql_v2_encrypted\n--! in assignment contexts.\n--!\n--! @see eql_v2.to_encrypted(text)\nCREATE CAST (text AS public.eql_v2_encrypted)\n WITH FUNCTION eql_v2.to_encrypted(text) AS ASSIGNMENT;\n\n\n\n--! @brief Convert encrypted type to JSONB\n--!\n--! Extracts the underlying JSONB payload from an eql_v2_encrypted composite type.\n--! Useful for debugging or when raw encrypted payload access is needed.\n--!\n--! @param e eql_v2_encrypted Encrypted value to unwrap\n--! @return jsonb Raw JSONB encrypted payload\n--!\n--! @note Returns the raw encrypted structure including ciphertext and index terms\n--! @see eql_v2.to_encrypted(jsonb)\nCREATE FUNCTION eql_v2.to_jsonb(e public.eql_v2_encrypted)\n RETURNS jsonb\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\nBEGIN\n IF e IS NULL THEN\n RETURN NULL;\n END IF;\n\n RETURN e.data;\nEND;\n$$ LANGUAGE plpgsql;\n\n--! @brief Implicit cast from encrypted type to JSONB\n--!\n--! Enables PostgreSQL to automatically extract the JSONB payload from\n--! eql_v2_encrypted values in assignment contexts.\n--!\n--! @see eql_v2.to_jsonb(eql_v2_encrypted)\nCREATE CAST (public.eql_v2_encrypted AS jsonb)\n WITH FUNCTION eql_v2.to_jsonb(public.eql_v2_encrypted) AS ASSIGNMENT;\n\n\n\n--! @file config/types.sql\n--! @brief Configuration state type definition\n--!\n--! Defines the ENUM type for tracking encryption configuration lifecycle states.\n--! The configuration table uses this type to manage transitions between states\n--! during setup, activation, and encryption operations.\n--!\n--! @note CREATE TYPE does not support IF NOT EXISTS, so wrapped in DO block\n--! @note Configuration data stored as JSONB directly, not as DOMAIN\n--! @see config/tables.sql\n\n\n--! @brief Configuration lifecycle state\n--!\n--! Defines valid states for encryption configurations in the eql_v2_configuration table.\n--! Configurations transition through these states during setup and activation.\n--!\n--! @note Only one configuration can be in 'active', 'pending', or 'encrypting' state at once\n--! @see config/indexes.sql for uniqueness enforcement\n--! @see config/tables.sql for usage in eql_v2_configuration table\nDO $$\n BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'eql_v2_configuration_state') THEN\n CREATE TYPE public.eql_v2_configuration_state AS ENUM ('active', 'inactive', 'encrypting', 'pending');\n END IF;\n END\n$$;\n\n\n\n--! @brief Extract Bloom filter index term from JSONB payload\n--!\n--! Extracts the Bloom filter array from the 'bf' field of an encrypted\n--! data payload. Used internally for pattern-match queries (LIKE operator).\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return eql_v2.bloom_filter Bloom filter as smallint array\n--! @throws Exception if 'bf' field is missing when bloom_filter index is expected\n--!\n--! @see eql_v2.has_bloom_filter\n--! @see eql_v2.\"~~\"\nCREATE FUNCTION eql_v2.bloom_filter(val jsonb)\n RETURNS eql_v2.bloom_filter\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF eql_v2.has_bloom_filter(val) THEN\n RETURN ARRAY(SELECT jsonb_array_elements(val->'bf'))::eql_v2.bloom_filter;\n END IF;\n\n RAISE 'Expected a match index (bf) value in json: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract Bloom filter index term from encrypted column value\n--!\n--! Extracts the Bloom filter from an encrypted column value by accessing\n--! its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return eql_v2.bloom_filter Bloom filter as smallint array\n--!\n--! @see eql_v2.bloom_filter(jsonb)\nCREATE FUNCTION eql_v2.bloom_filter(val eql_v2_encrypted)\n RETURNS eql_v2.bloom_filter\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (SELECT eql_v2.bloom_filter(val.data));\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if JSONB payload contains Bloom filter index term\n--!\n--! Tests whether the encrypted data payload includes a 'bf' field,\n--! indicating a Bloom filter is available for pattern-match queries.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Boolean True if 'bf' field is present and non-null\n--!\n--! @see eql_v2.bloom_filter\nCREATE FUNCTION eql_v2.has_bloom_filter(val jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN val ->> 'bf' IS NOT NULL;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if encrypted column value contains Bloom filter index term\n--!\n--! Tests whether an encrypted column value includes a Bloom filter\n--! by checking its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Boolean True if Bloom filter is present\n--!\n--! @see eql_v2.has_bloom_filter(jsonb)\nCREATE FUNCTION eql_v2.has_bloom_filter(val eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.has_bloom_filter(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Fallback literal comparison for encrypted values\n--! @internal\n--!\n--! Compares two encrypted values by their raw JSONB representation when no\n--! suitable index terms are available. This ensures consistent ordering required\n--! for btree correctness and prevents \"lock BufferContent is not held\" errors.\n--!\n--! Used as a last resort fallback in eql_v2.compare() when encrypted values\n--! lack matching index terms (blake3, hmac_256, ore).\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @note This compares the encrypted payloads directly, not the plaintext values\n--! @note Ordering is consistent but not meaningful for range queries\n--! @see eql_v2.compare\nCREATE FUNCTION eql_v2.compare_literal(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n a_data jsonb;\n b_data jsonb;\n BEGIN\n\n IF a IS NULL AND b IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b IS NULL THEN\n RETURN 1;\n END IF;\n\n a_data := a.data;\n b_data := b.data;\n\n IF a_data < b_data THEN\n RETURN -1;\n END IF;\n\n IF a_data > b_data THEN\n RETURN 1;\n END IF;\n\n RETURN 0;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Less-than comparison helper for encrypted values\n--! @internal\n--!\n--! Internal helper that delegates to eql_v2.compare for less-than testing.\n--! Returns true if first value is less than second using ORE comparison.\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return Boolean True if a < b (compare result = -1)\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.\"<\"\nCREATE FUNCTION eql_v2.lt(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.compare(a, b) = -1;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Less-than operator for encrypted values\n--!\n--! Implements the < operator for comparing two encrypted values using Order-Revealing\n--! Encryption (ORE) index terms. Enables range queries and sorting without decryption.\n--! Requires 'ore' index configuration on the column.\n--!\n--! @param a eql_v2_encrypted Left operand\n--! @param b eql_v2_encrypted Right operand\n--! @return Boolean True if a is less than b\n--!\n--! @example\n--! -- Range query on encrypted timestamps\n--! SELECT * FROM events\n--! WHERE encrypted_timestamp < '2024-01-01'::timestamp::text::eql_v2_encrypted;\n--!\n--! -- Compare encrypted numeric columns\n--! SELECT * FROM products WHERE encrypted_price < encrypted_discount_price;\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.\"<\"(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.lt(a, b);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR <(\n FUNCTION=eql_v2.\"<\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted,\n COMMUTATOR = >,\n NEGATOR = >=,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n--! @brief Less-than operator for encrypted value and JSONB\n--!\n--! Overload of < operator accepting JSONB on the right side. Automatically\n--! casts JSONB to eql_v2_encrypted for ORE comparison.\n--!\n--! @param eql_v2_encrypted Left operand (encrypted value)\n--! @param b JSONB Right operand (will be cast to eql_v2_encrypted)\n--! @return Boolean True if a < b\n--!\n--! @example\n--! SELECT * FROM events WHERE encrypted_age < '18'::int::text::jsonb;\n--!\n--! @see eql_v2.\"<\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"<\"(a eql_v2_encrypted, b jsonb)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.lt(a, b::eql_v2_encrypted);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR <(\n FUNCTION=eql_v2.\"<\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=jsonb,\n COMMUTATOR = >,\n NEGATOR = >=,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n--! @brief Less-than operator for JSONB and encrypted value\n--!\n--! Overload of < operator accepting JSONB on the left side. Automatically\n--! casts JSONB to eql_v2_encrypted for ORE comparison.\n--!\n--! @param a JSONB Left operand (will be cast to eql_v2_encrypted)\n--! @param eql_v2_encrypted Right operand (encrypted value)\n--! @return Boolean True if a < b\n--!\n--! @example\n--! SELECT * FROM events WHERE '2023-01-01'::date::text::jsonb < encrypted_date;\n--!\n--! @see eql_v2.\"<\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"<\"(a jsonb, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.lt(a::eql_v2_encrypted, b);\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR <(\n FUNCTION=eql_v2.\"<\",\n LEFTARG=jsonb,\n RIGHTARG=eql_v2_encrypted,\n COMMUTATOR = >,\n NEGATOR = >=,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n\n\n--! @brief Less-than-or-equal comparison helper for encrypted values\n--! @internal\n--!\n--! Internal helper that delegates to eql_v2.compare for <= testing.\n--! Returns true if first value is less than or equal to second using ORE comparison.\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return Boolean True if a <= b (compare result <= 0)\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.\"<=\"\nCREATE FUNCTION eql_v2.lte(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.compare(a, b) <= 0;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Less-than-or-equal operator for encrypted values\n--!\n--! Implements the <= operator for comparing encrypted values using ORE index terms.\n--! Enables range queries with inclusive lower bounds without decryption.\n--!\n--! @param a eql_v2_encrypted Left operand\n--! @param b eql_v2_encrypted Right operand\n--! @return Boolean True if a <= b\n--!\n--! @example\n--! -- Find records with encrypted age 18 or under\n--! SELECT * FROM users WHERE encrypted_age <= '18'::int::text::eql_v2_encrypted;\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.\"<=\"(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.lte(a, b);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR <=(\n FUNCTION = eql_v2.\"<=\",\n LEFTARG = eql_v2_encrypted,\n RIGHTARG = eql_v2_encrypted,\n COMMUTATOR = >=,\n NEGATOR = >,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n--! @brief <= operator for encrypted value and JSONB\n--! @see eql_v2.\"<=\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"<=\"(a eql_v2_encrypted, b jsonb)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.lte(a, b::eql_v2_encrypted);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR <=(\n FUNCTION = eql_v2.\"<=\",\n LEFTARG = eql_v2_encrypted,\n RIGHTARG = jsonb,\n COMMUTATOR = >=,\n NEGATOR = >,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n--! @brief <= operator for JSONB and encrypted value\n--! @see eql_v2.\"<=\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"<=\"(a jsonb, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.lte(a::eql_v2_encrypted, b);\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR <=(\n FUNCTION = eql_v2.\"<=\",\n LEFTARG = jsonb,\n RIGHTARG = eql_v2_encrypted,\n COMMUTATOR = >=,\n NEGATOR = >,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n\n\n--! @brief Equality comparison helper for encrypted values\n--! @internal\n--!\n--! Internal helper that delegates to eql_v2.compare for equality testing.\n--! Returns true if encrypted values are equal via encrypted index comparison.\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return Boolean True if values are equal (compare result = 0)\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.\"=\"\nCREATE FUNCTION eql_v2.eq(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.compare(a, b) = 0;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Equality operator for encrypted values\n--!\n--! Implements the = operator for comparing two encrypted values using their\n--! encrypted index terms (unique/blake3). Enables WHERE clause comparisons\n--! without decryption.\n--!\n--! @param a eql_v2_encrypted Left operand\n--! @param b eql_v2_encrypted Right operand\n--! @return Boolean True if encrypted values are equal\n--!\n--! @example\n--! -- Compare encrypted columns\n--! SELECT * FROM users WHERE encrypted_email = other_encrypted_email;\n--!\n--! -- Search using encrypted literal\n--! SELECT * FROM users\n--! WHERE encrypted_email = '{\"c\":\"...\",\"i\":{\"unique\":\"...\"}}'::eql_v2_encrypted;\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.\"=\"(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.eq(a, b);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR = (\n FUNCTION=eql_v2.\"=\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted,\n NEGATOR = <>,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n--! @brief Equality operator for encrypted value and JSONB\n--!\n--! Overload of = operator accepting JSONB on the right side. Automatically\n--! casts JSONB to eql_v2_encrypted for comparison. Useful for comparing\n--! against JSONB literals or columns.\n--!\n--! @param eql_v2_encrypted Left operand (encrypted value)\n--! @param b JSONB Right operand (will be cast to eql_v2_encrypted)\n--! @return Boolean True if values are equal\n--!\n--! @example\n--! -- Compare encrypted column to JSONB literal\n--! SELECT * FROM users\n--! WHERE encrypted_email = '{\"c\":\"...\",\"i\":{\"unique\":\"...\"}}'::jsonb;\n--!\n--! @see eql_v2.\"=\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"=\"(a eql_v2_encrypted, b jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.eq(a, b::eql_v2_encrypted);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR = (\n FUNCTION=eql_v2.\"=\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=jsonb,\n NEGATOR = <>,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n--! @brief Equality operator for JSONB and encrypted value\n--!\n--! Overload of = operator accepting JSONB on the left side. Automatically\n--! casts JSONB to eql_v2_encrypted for comparison. Enables commutative\n--! equality comparisons.\n--!\n--! @param a JSONB Left operand (will be cast to eql_v2_encrypted)\n--! @param eql_v2_encrypted Right operand (encrypted value)\n--! @return Boolean True if values are equal\n--!\n--! @example\n--! -- Compare JSONB literal to encrypted column\n--! SELECT * FROM users\n--! WHERE '{\"c\":\"...\",\"i\":{\"unique\":\"...\"}}'::jsonb = encrypted_email;\n--!\n--! @see eql_v2.\"=\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"=\"(a jsonb, b eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.eq(a::eql_v2_encrypted, b);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR = (\n FUNCTION=eql_v2.\"=\",\n LEFTARG=jsonb,\n RIGHTARG=eql_v2_encrypted,\n NEGATOR = <>,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n\n--! @brief Greater-than-or-equal comparison helper for encrypted values\n--! @internal\n--!\n--! Internal helper that delegates to eql_v2.compare for >= testing.\n--! Returns true if first value is greater than or equal to second using ORE comparison.\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return Boolean True if a >= b (compare result >= 0)\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.\">=\"\nCREATE FUNCTION eql_v2.gte(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.compare(a, b) >= 0;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Greater-than-or-equal operator for encrypted values\n--!\n--! Implements the >= operator for comparing encrypted values using ORE index terms.\n--! Enables range queries with inclusive upper bounds without decryption.\n--!\n--! @param a eql_v2_encrypted Left operand\n--! @param b eql_v2_encrypted Right operand\n--! @return Boolean True if a >= b\n--!\n--! @example\n--! -- Find records with age 18 or over\n--! SELECT * FROM users WHERE encrypted_age >= '18'::int::text::eql_v2_encrypted;\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.\">=\"(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.gte(a, b);\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR >=(\n FUNCTION = eql_v2.\">=\",\n LEFTARG = eql_v2_encrypted,\n RIGHTARG = eql_v2_encrypted,\n COMMUTATOR = <=,\n NEGATOR = <,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n--! @brief >= operator for encrypted value and JSONB\n--! @see eql_v2.\">=\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\">=\"(a eql_v2_encrypted, b jsonb)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.gte(a, b::eql_v2_encrypted);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR >=(\n FUNCTION = eql_v2.\">=\",\n LEFTARG = eql_v2_encrypted,\n RIGHTARG=jsonb,\n COMMUTATOR = <=,\n NEGATOR = <,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n--! @brief >= operator for JSONB and encrypted value\n--! @see eql_v2.\">=\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\">=\"(a jsonb, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.gte(a::eql_v2_encrypted, b);\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR >=(\n FUNCTION = eql_v2.\">=\",\n LEFTARG = jsonb,\n RIGHTARG =eql_v2_encrypted,\n COMMUTATOR = <=,\n NEGATOR = <,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n\n\n--! @brief Greater-than comparison helper for encrypted values\n--! @internal\n--!\n--! Internal helper that delegates to eql_v2.compare for greater-than testing.\n--! Returns true if first value is greater than second using ORE comparison.\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return Boolean True if a > b (compare result = 1)\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.\">\"\nCREATE FUNCTION eql_v2.gt(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.compare(a, b) = 1;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Greater-than operator for encrypted values\n--!\n--! Implements the > operator for comparing encrypted values using ORE index terms.\n--! Enables range queries and sorting without decryption. Requires 'ore' index\n--! configuration on the column.\n--!\n--! @param a eql_v2_encrypted Left operand\n--! @param b eql_v2_encrypted Right operand\n--! @return Boolean True if a is greater than b\n--!\n--! @example\n--! -- Find records above threshold\n--! SELECT * FROM events\n--! WHERE encrypted_value > '100'::int::text::eql_v2_encrypted;\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.\">\"(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.gt(a, b);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR >(\n FUNCTION=eql_v2.\">\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted,\n COMMUTATOR = <,\n NEGATOR = <=,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n--! @brief > operator for encrypted value and JSONB\n--! @see eql_v2.\">\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\">\"(a eql_v2_encrypted, b jsonb)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.gt(a, b::eql_v2_encrypted);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR >(\n FUNCTION = eql_v2.\">\",\n LEFTARG = eql_v2_encrypted,\n RIGHTARG = jsonb,\n COMMUTATOR = <,\n NEGATOR = <=,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n--! @brief > operator for JSONB and encrypted value\n--! @see eql_v2.\">\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\">\"(a jsonb, b eql_v2_encrypted)\nRETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.gt(a::eql_v2_encrypted, b);\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR >(\n FUNCTION = eql_v2.\">\",\n LEFTARG = jsonb,\n RIGHTARG = eql_v2_encrypted,\n COMMUTATOR = <,\n NEGATOR = <=,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n\n\n\n--! @brief Extract STE vector index from JSONB payload\n--!\n--! Extracts the STE (Searchable Symmetric Encryption) vector from the 'sv' field\n--! of an encrypted data payload. Returns an array of encrypted values used for\n--! containment queries (@>, <@). If no 'sv' field exists, wraps the entire payload\n--! as a single-element array.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return eql_v2_encrypted[] Array of encrypted STE vector elements\n--!\n--! @see eql_v2.ste_vec(eql_v2_encrypted)\n--! @see eql_v2.ste_vec_contains\nCREATE FUNCTION eql_v2.ste_vec(val jsonb)\n RETURNS public.eql_v2_encrypted[]\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n sv jsonb;\n ary public.eql_v2_encrypted[];\n BEGIN\n\n IF val ? 'sv' THEN\n sv := val->'sv';\n ELSE\n sv := jsonb_build_array(val);\n END IF;\n\n SELECT array_agg(eql_v2.to_encrypted(elem))\n INTO ary\n FROM jsonb_array_elements(sv) AS elem;\n\n RETURN ary;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract STE vector index from encrypted column value\n--!\n--! Extracts the STE vector from an encrypted column value by accessing its\n--! underlying JSONB data field. Used for containment query operations.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return eql_v2_encrypted[] Array of encrypted STE vector elements\n--!\n--! @see eql_v2.ste_vec(jsonb)\nCREATE FUNCTION eql_v2.ste_vec(val eql_v2_encrypted)\n RETURNS public.eql_v2_encrypted[]\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (SELECT eql_v2.ste_vec(val.data));\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Check if JSONB payload is a single-element STE vector\n--!\n--! Tests whether the encrypted data payload contains an 'sv' field with exactly\n--! one element. Single-element STE vectors can be treated as regular encrypted values.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Boolean True if 'sv' field exists with exactly one element\n--!\n--! @see eql_v2.to_ste_vec_value\nCREATE FUNCTION eql_v2.is_ste_vec_value(val jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val ? 'sv' THEN\n RETURN jsonb_array_length(val->'sv') = 1;\n END IF;\n\n RETURN false;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Check if encrypted column value is a single-element STE vector\n--!\n--! Tests whether an encrypted column value is a single-element STE vector\n--! by checking its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Boolean True if value is a single-element STE vector\n--!\n--! @see eql_v2.is_ste_vec_value(jsonb)\nCREATE FUNCTION eql_v2.is_ste_vec_value(val eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.is_ste_vec_value(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Convert single-element STE vector to regular encrypted value\n--!\n--! Extracts the single element from a single-element STE vector and returns it\n--! as a regular encrypted value, preserving metadata. If the input is not a\n--! single-element STE vector, returns it unchanged.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return eql_v2_encrypted Regular encrypted value (unwrapped if single-element STE vector)\n--!\n--! @see eql_v2.is_ste_vec_value\nCREATE FUNCTION eql_v2.to_ste_vec_value(val jsonb)\n RETURNS eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n meta jsonb;\n sv jsonb;\n BEGIN\n\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF eql_v2.is_ste_vec_value(val) THEN\n meta := eql_v2.meta_data(val);\n sv := val->'sv';\n sv := sv[0];\n\n RETURN eql_v2.to_encrypted(meta || sv);\n END IF;\n\n RETURN eql_v2.to_encrypted(val);\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Convert single-element STE vector to regular encrypted value (encrypted type)\n--!\n--! Converts an encrypted column value to a regular encrypted value by unwrapping\n--! if it's a single-element STE vector.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return eql_v2_encrypted Regular encrypted value (unwrapped if single-element STE vector)\n--!\n--! @see eql_v2.to_ste_vec_value(jsonb)\nCREATE FUNCTION eql_v2.to_ste_vec_value(val eql_v2_encrypted)\n RETURNS eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.to_ste_vec_value(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Extract selector value from JSONB payload\n--!\n--! Extracts the selector ('s') field from an encrypted data payload.\n--! Selectors are used to match STE vector elements during containment queries.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Text The selector value\n--! @throws Exception if 's' field is missing\n--!\n--! @see eql_v2.ste_vec_contains\nCREATE FUNCTION eql_v2.selector(val jsonb)\n RETURNS text\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF val ? 's' THEN\n RETURN val->>'s';\n END IF;\n RAISE 'Expected a selector index (s) value in json: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract selector value from encrypted column value\n--!\n--! Extracts the selector from an encrypted column value by accessing its\n--! underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Text The selector value\n--!\n--! @see eql_v2.selector(jsonb)\nCREATE FUNCTION eql_v2.selector(val eql_v2_encrypted)\n RETURNS text\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (SELECT eql_v2.selector(val.data));\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Check if JSONB payload is marked as an STE vector array\n--!\n--! Tests whether the encrypted data payload has the 'a' (array) flag set to true,\n--! indicating it represents an array for STE vector operations.\n--!\n--! @param jsonb containing encrypted EQL payload\n--! @return Boolean True if 'a' field is present and true\n--!\n--! @see eql_v2.ste_vec\nCREATE FUNCTION eql_v2.is_ste_vec_array(val jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n IF val ? 'a' THEN\n RETURN (val->>'a')::boolean;\n END IF;\n\n RETURN false;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if encrypted column value is marked as an STE vector array\n--!\n--! Tests whether an encrypted column value has the array flag set by checking\n--! its underlying JSONB data field.\n--!\n--! @param eql_v2_encrypted Encrypted column value\n--! @return Boolean True if value is marked as an STE vector array\n--!\n--! @see eql_v2.is_ste_vec_array(jsonb)\nCREATE FUNCTION eql_v2.is_ste_vec_array(val eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (SELECT eql_v2.is_ste_vec_array(val.data));\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Extract full encrypted JSONB elements as array\n--!\n--! Extracts all JSONB elements from the STE vector including non-deterministic fields.\n--! Use jsonb_array() instead for GIN indexing and containment queries.\n--!\n--! @param val jsonb containing encrypted EQL payload\n--! @return jsonb[] Array of full JSONB elements\n--!\n--! @see eql_v2.jsonb_array\nCREATE FUNCTION eql_v2.jsonb_array_from_array_elements(val jsonb)\nRETURNS jsonb[]\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT CASE\n WHEN val ? 'sv' THEN\n ARRAY(SELECT elem FROM jsonb_array_elements(val->'sv') AS elem)\n ELSE\n ARRAY[val]\n END;\n$$;\n\n\n--! @brief Extract full encrypted JSONB elements as array from encrypted column\n--!\n--! @param val eql_v2_encrypted Encrypted column value\n--! @return jsonb[] Array of full JSONB elements\n--!\n--! @see eql_v2.jsonb_array_from_array_elements(jsonb)\nCREATE FUNCTION eql_v2.jsonb_array_from_array_elements(val eql_v2_encrypted)\nRETURNS jsonb[]\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT eql_v2.jsonb_array_from_array_elements(val.data);\n$$;\n\n\n--! @brief Extract deterministic fields as array for GIN indexing\n--!\n--! Extracts only deterministic search term fields (s, b3, hm, ocv, ocf) from each\n--! STE vector element. Excludes non-deterministic ciphertext for correct containment\n--! comparison using PostgreSQL's native @> operator.\n--!\n--! @param val jsonb containing encrypted EQL payload\n--! @return jsonb[] Array of JSONB elements with only deterministic fields\n--!\n--! @note Use this for GIN indexes and containment queries\n--! @see eql_v2.jsonb_contains\nCREATE FUNCTION eql_v2.jsonb_array(val jsonb)\nRETURNS jsonb[]\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT ARRAY(\n SELECT jsonb_object_agg(kv.key, kv.value)\n FROM jsonb_array_elements(\n CASE WHEN val ? 'sv' THEN val->'sv' ELSE jsonb_build_array(val) END\n ) AS elem,\n LATERAL jsonb_each(elem) AS kv(key, value)\n WHERE kv.key IN ('s', 'b3', 'hm', 'ocv', 'ocf')\n GROUP BY elem\n );\n$$;\n\n\n--! @brief Extract deterministic fields as array from encrypted column\n--!\n--! @param val eql_v2_encrypted Encrypted column value\n--! @return jsonb[] Array of JSONB elements with only deterministic fields\n--!\n--! @see eql_v2.jsonb_array(jsonb)\nCREATE FUNCTION eql_v2.jsonb_array(val eql_v2_encrypted)\nRETURNS jsonb[]\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT eql_v2.jsonb_array(val.data);\n$$;\n\n\n--! @brief GIN-indexable JSONB containment check\n--!\n--! Checks if encrypted value 'a' contains all JSONB elements from 'b'.\n--! Uses jsonb[] arrays internally for native PostgreSQL GIN index support.\n--!\n--! This function is designed for use with a GIN index on jsonb_array(column).\n--! When combined with such an index, PostgreSQL can efficiently search large tables.\n--!\n--! @param a eql_v2_encrypted Container value (typically a table column)\n--! @param b eql_v2_encrypted Value to search for\n--! @return Boolean True if a contains all elements of b\n--!\n--! @example\n--! -- Create GIN index for efficient containment queries\n--! CREATE INDEX idx ON mytable USING GIN (eql_v2.jsonb_array(encrypted_col));\n--!\n--! -- Query using the helper function\n--! SELECT * FROM mytable WHERE eql_v2.jsonb_contains(encrypted_col, search_value);\n--!\n--! @see eql_v2.jsonb_array\nCREATE FUNCTION eql_v2.jsonb_contains(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT eql_v2.jsonb_array(a) @> eql_v2.jsonb_array(b);\n$$;\n\n\n--! @brief GIN-indexable JSONB containment check (encrypted, jsonb)\n--!\n--! Checks if encrypted value 'a' contains all JSONB elements from jsonb value 'b'.\n--! Uses jsonb[] arrays internally for native PostgreSQL GIN index support.\n--!\n--! @param a eql_v2_encrypted Container value (typically a table column)\n--! @param b jsonb JSONB value to search for\n--! @return Boolean True if a contains all elements of b\n--!\n--! @see eql_v2.jsonb_array\n--! @see eql_v2.jsonb_contains(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.jsonb_contains(a eql_v2_encrypted, b jsonb)\nRETURNS boolean\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT eql_v2.jsonb_array(a) @> eql_v2.jsonb_array(b);\n$$;\n\n\n--! @brief GIN-indexable JSONB containment check (jsonb, encrypted)\n--!\n--! Checks if jsonb value 'a' contains all JSONB elements from encrypted value 'b'.\n--! Uses jsonb[] arrays internally for native PostgreSQL GIN index support.\n--!\n--! @param a jsonb Container JSONB value\n--! @param b eql_v2_encrypted Encrypted value to search for\n--! @return Boolean True if a contains all elements of b\n--!\n--! @see eql_v2.jsonb_array\n--! @see eql_v2.jsonb_contains(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.jsonb_contains(a jsonb, b eql_v2_encrypted)\nRETURNS boolean\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT eql_v2.jsonb_array(a) @> eql_v2.jsonb_array(b);\n$$;\n\n\n--! @brief GIN-indexable JSONB \"is contained by\" check\n--!\n--! Checks if all JSONB elements from 'a' are contained in 'b'.\n--! Uses jsonb[] arrays internally for native PostgreSQL GIN index support.\n--!\n--! @param a eql_v2_encrypted Value to check (typically a table column)\n--! @param b eql_v2_encrypted Container value\n--! @return Boolean True if all elements of a are contained in b\n--!\n--! @see eql_v2.jsonb_array\n--! @see eql_v2.jsonb_contains\nCREATE FUNCTION eql_v2.jsonb_contained_by(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT eql_v2.jsonb_array(a) <@ eql_v2.jsonb_array(b);\n$$;\n\n\n--! @brief GIN-indexable JSONB \"is contained by\" check (encrypted, jsonb)\n--!\n--! Checks if all JSONB elements from encrypted value 'a' are contained in jsonb value 'b'.\n--! Uses jsonb[] arrays internally for native PostgreSQL GIN index support.\n--!\n--! @param a eql_v2_encrypted Value to check (typically a table column)\n--! @param b jsonb Container JSONB value\n--! @return Boolean True if all elements of a are contained in b\n--!\n--! @see eql_v2.jsonb_array\n--! @see eql_v2.jsonb_contained_by(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.jsonb_contained_by(a eql_v2_encrypted, b jsonb)\nRETURNS boolean\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT eql_v2.jsonb_array(a) <@ eql_v2.jsonb_array(b);\n$$;\n\n\n--! @brief GIN-indexable JSONB \"is contained by\" check (jsonb, encrypted)\n--!\n--! Checks if all JSONB elements from jsonb value 'a' are contained in encrypted value 'b'.\n--! Uses jsonb[] arrays internally for native PostgreSQL GIN index support.\n--!\n--! @param a jsonb Value to check\n--! @param b eql_v2_encrypted Container encrypted value\n--! @return Boolean True if all elements of a are contained in b\n--!\n--! @see eql_v2.jsonb_array\n--! @see eql_v2.jsonb_contained_by(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.jsonb_contained_by(a jsonb, b eql_v2_encrypted)\nRETURNS boolean\nIMMUTABLE STRICT PARALLEL SAFE\nLANGUAGE SQL\nAS $$\n SELECT eql_v2.jsonb_array(a) <@ eql_v2.jsonb_array(b);\n$$;\n\n\n--! @brief Check if STE vector array contains a specific encrypted element\n--!\n--! Tests whether any element in the STE vector array 'a' contains the encrypted value 'b'.\n--! Matching requires both the selector and encrypted value to be equal.\n--! Used internally by ste_vec_contains(encrypted, encrypted) for array containment checks.\n--!\n--! @param eql_v2_encrypted[] STE vector array to search within\n--! @param eql_v2_encrypted Encrypted element to search for\n--! @return Boolean True if b is found in any element of a\n--!\n--! @note Compares both selector and encrypted value for match\n--!\n--! @see eql_v2.selector\n--! @see eql_v2.ste_vec_contains(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.ste_vec_contains(a public.eql_v2_encrypted[], b eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n result boolean;\n _a public.eql_v2_encrypted;\n BEGIN\n\n result := false;\n\n FOR idx IN 1..array_length(a, 1) LOOP\n _a := a[idx];\n result := result OR (eql_v2.selector(_a) = eql_v2.selector(b) AND _a = b);\n END LOOP;\n\n RETURN result;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check if encrypted value 'a' contains all elements of encrypted value 'b'\n--!\n--! Performs STE vector containment comparison between two encrypted values.\n--! Returns true if all elements in b's STE vector are found in a's STE vector.\n--! Used internally by the @> containment operator for searchable encryption.\n--!\n--! @param a eql_v2_encrypted First encrypted value (container)\n--! @param b eql_v2_encrypted Second encrypted value (elements to find)\n--! @return Boolean True if all elements of b are contained in a\n--!\n--! @note Empty b is always contained in any a\n--! @note Each element of b must match both selector and value in a\n--!\n--! @see eql_v2.ste_vec\n--! @see eql_v2.ste_vec_contains(eql_v2_encrypted[], eql_v2_encrypted)\n--! @see eql_v2.\"@>\"\nCREATE FUNCTION eql_v2.ste_vec_contains(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n result boolean;\n sv_a public.eql_v2_encrypted[];\n sv_b public.eql_v2_encrypted[];\n _b public.eql_v2_encrypted;\n BEGIN\n\n -- jsonb arrays of ste_vec encrypted values\n sv_a := eql_v2.ste_vec(a);\n sv_b := eql_v2.ste_vec(b);\n\n -- an empty b is always contained in a\n IF array_length(sv_b, 1) IS NULL THEN\n RETURN true;\n END IF;\n\n IF array_length(sv_a, 1) IS NULL THEN\n RETURN false;\n END IF;\n\n result := true;\n\n -- for each element of b check if it is in a\n FOR idx IN 1..array_length(sv_b, 1) LOOP\n _b := sv_b[idx];\n result := result AND eql_v2.ste_vec_contains(sv_a, _b);\n END LOOP;\n\n RETURN result;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @file config/tables.sql\n--! @brief Encryption configuration storage table\n--!\n--! Defines the main table for storing EQL v2 encryption configurations.\n--! Each row represents a configuration specifying which tables/columns to encrypt\n--! and what index types to use. Configurations progress through lifecycle states.\n--!\n--! @see config/types.sql for state ENUM definition\n--! @see config/indexes.sql for state uniqueness constraints\n--! @see config/constraints.sql for data validation\n\n\n--! @brief Encryption configuration table\n--!\n--! Stores encryption configurations with their state and metadata.\n--! The 'data' JSONB column contains the full configuration structure including\n--! table/column mappings, index types, and casting rules.\n--!\n--! @note Only one configuration can be 'active', 'pending', or 'encrypting' at once\n--! @note 'id' is auto-generated identity column\n--! @note 'state' defaults to 'pending' for new configurations\n--! @note 'data' validated by CHECK constraint (see config/constraints.sql)\nCREATE TABLE IF NOT EXISTS public.eql_v2_configuration\n(\n id bigint GENERATED ALWAYS AS IDENTITY,\n state eql_v2_configuration_state NOT NULL DEFAULT 'pending',\n data jsonb,\n created_at timestamptz not null default current_timestamp,\n PRIMARY KEY(id)\n);\n\n\n--! @brief Initialize default configuration structure\n--! @internal\n--!\n--! Creates a default configuration object if input is NULL. Used internally\n--! by public configuration functions to ensure consistent structure.\n--!\n--! @param config JSONB Existing configuration or NULL\n--! @return JSONB Configuration with default structure (version 1, empty tables)\nCREATE FUNCTION eql_v2.config_default(config jsonb)\n RETURNS jsonb\n IMMUTABLE PARALLEL SAFE\nAS $$\n BEGIN\n IF config IS NULL THEN\n SELECT jsonb_build_object('v', 1, 'tables', jsonb_build_object()) INTO config;\n END IF;\n RETURN config;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Add table to configuration if not present\n--! @internal\n--!\n--! Ensures the specified table exists in the configuration structure.\n--! Creates empty table entry if needed. Idempotent operation.\n--!\n--! @param table_name Text Name of table to add\n--! @param config JSONB Configuration object\n--! @return JSONB Updated configuration with table entry\nCREATE FUNCTION eql_v2.config_add_table(table_name text, config jsonb)\n RETURNS jsonb\n IMMUTABLE PARALLEL SAFE\nAS $$\n DECLARE\n tbl jsonb;\n BEGIN\n IF NOT config #> array['tables'] ? table_name THEN\n SELECT jsonb_insert(config, array['tables', table_name], jsonb_build_object()) INTO config;\n END IF;\n RETURN config;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Add column to table configuration if not present\n--! @internal\n--!\n--! Ensures the specified column exists in the table's configuration structure.\n--! Creates empty column entry with indexes object if needed. Idempotent operation.\n--!\n--! @param table_name Text Name of parent table\n--! @param column_name Text Name of column to add\n--! @param config JSONB Configuration object\n--! @return JSONB Updated configuration with column entry\nCREATE FUNCTION eql_v2.config_add_column(table_name text, column_name text, config jsonb)\n RETURNS jsonb\n IMMUTABLE PARALLEL SAFE\nAS $$\n DECLARE\n col jsonb;\n BEGIN\n IF NOT config #> array['tables', table_name] ? column_name THEN\n SELECT jsonb_build_object('indexes', jsonb_build_object()) into col;\n SELECT jsonb_set(config, array['tables', table_name, column_name], col) INTO config;\n END IF;\n RETURN config;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Set cast type for column in configuration\n--! @internal\n--!\n--! Updates the cast_as field for a column, specifying the PostgreSQL type\n--! that decrypted values should be cast to.\n--!\n--! @param table_name Text Name of parent table\n--! @param column_name Text Name of column\n--! @param cast_as Text PostgreSQL type for casting (e.g., 'text', 'int', 'jsonb')\n--! @param config JSONB Configuration object\n--! @return JSONB Updated configuration with cast_as set\nCREATE FUNCTION eql_v2.config_add_cast(table_name text, column_name text, cast_as text, config jsonb)\n RETURNS jsonb\n IMMUTABLE PARALLEL SAFE\nAS $$\n BEGIN\n SELECT jsonb_set(config, array['tables', table_name, column_name, 'cast_as'], to_jsonb(cast_as)) INTO config;\n RETURN config;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Add search index to column configuration\n--! @internal\n--!\n--! Inserts a search index entry (unique, match, ore, ste_vec) with its options\n--! into the column's indexes object.\n--!\n--! @param table_name Text Name of parent table\n--! @param column_name Text Name of column\n--! @param index_name Text Type of index to add\n--! @param opts JSONB Index-specific options\n--! @param config JSONB Configuration object\n--! @return JSONB Updated configuration with index added\nCREATE FUNCTION eql_v2.config_add_index(table_name text, column_name text, index_name text, opts jsonb, config jsonb)\n RETURNS jsonb\n IMMUTABLE PARALLEL SAFE\nAS $$\n BEGIN\n SELECT jsonb_insert(config, array['tables', table_name, column_name, 'indexes', index_name], opts) INTO config;\n RETURN config;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Generate default options for match index\n--! @internal\n--!\n--! Returns default configuration for match (LIKE) indexes: k=6, bf=2048,\n--! ngram tokenizer with token_length=3, downcase filter, include_original=true.\n--!\n--! @return JSONB Default match index options\nCREATE FUNCTION eql_v2.config_match_default()\n RETURNS jsonb\nLANGUAGE sql STRICT PARALLEL SAFE\nBEGIN ATOMIC\n SELECT jsonb_build_object(\n 'k', 6,\n 'bf', 2048,\n 'include_original', true,\n 'tokenizer', json_build_object('kind', 'ngram', 'token_length', 3),\n 'token_filters', json_build_array(json_build_object('kind', 'downcase')));\nEND;\n-- AUTOMATICALLY GENERATED FILE\n-- Source is version-template.sql\n\nDROP FUNCTION IF EXISTS eql_v2.version();\n\n--! @file version.sql\n--! @brief EQL version reporting\n--!\n--! This file is auto-generated from version.template during build.\n--! The version string placeholder is replaced with the actual release version.\n\n--! @brief Get EQL library version string\n--!\n--! Returns the version string for the installed EQL library.\n--! This value is set at build time from the project version.\n--!\n--! @return text Version string (e.g., \"2.1.0\" or \"DEV\" for development builds)\n--!\n--! @note Auto-generated during build from version.template\n--!\n--! @example\n--! -- Check installed EQL version\n--! SELECT eql_v2.version();\n--! -- Returns: '2.1.0'\nCREATE FUNCTION eql_v2.version()\n RETURNS text\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n SELECT 'eql-2.2.1';\n$$ LANGUAGE SQL;\n\n\n\n--! @brief Compare two encrypted values using variable-width CLLW ORE index terms\n--!\n--! Performs a three-way comparison (returns -1/0/1) of encrypted values using\n--! their variable-width CLLW ORE ciphertext index terms. Used internally by range operators\n--! (<, <=, >, >=) for order-revealing comparisons without decryption.\n--!\n--! @param a eql_v2_encrypted First encrypted value to compare\n--! @param b eql_v2_encrypted Second encrypted value to compare\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @note NULL values are sorted before non-NULL values\n--! @note Uses variable-width CLLW ORE cryptographic protocol for secure comparisons\n--!\n--! @see eql_v2.ore_cllw_var_8\n--! @see eql_v2.has_ore_cllw_var_8\n--! @see eql_v2.compare_ore_cllw_var_8_term\n--! @see eql_v2.\"<\"\n--! @see eql_v2.\">\"\nCREATE FUNCTION eql_v2.compare_ore_cllw_var_8(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n a_term eql_v2.ore_cllw_var_8;\n b_term eql_v2.ore_cllw_var_8;\n BEGIN\n\n -- PERFORM eql_v2.log('eql_v2.compare_ore_cllw_var_8');\n -- PERFORM eql_v2.log('a', a::text);\n -- PERFORM eql_v2.log('b', b::text);\n\n IF a IS NULL AND b IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b IS NULL THEN\n RETURN 1;\n END IF;\n\n IF eql_v2.has_ore_cllw_var_8(a) THEN\n a_term := eql_v2.ore_cllw_var_8(a);\n END IF;\n\n IF eql_v2.has_ore_cllw_var_8(a) THEN\n b_term := eql_v2.ore_cllw_var_8(b);\n END IF;\n\n IF a_term IS NULL AND b_term IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a_term IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b_term IS NULL THEN\n RETURN 1;\n END IF;\n\n RETURN eql_v2.compare_ore_cllw_var_8_term(a_term, b_term);\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Compare two encrypted values using CLLW ORE index terms\n--!\n--! Performs a three-way comparison (returns -1/0/1) of encrypted values using\n--! their CLLW ORE ciphertext index terms. Used internally by range operators\n--! (<, <=, >, >=) for order-revealing comparisons without decryption.\n--!\n--! @param a eql_v2_encrypted First encrypted value to compare\n--! @param b eql_v2_encrypted Second encrypted value to compare\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @note NULL values are sorted before non-NULL values\n--! @note Uses CLLW ORE cryptographic protocol for secure comparisons\n--!\n--! @see eql_v2.ore_cllw_u64_8\n--! @see eql_v2.has_ore_cllw_u64_8\n--! @see eql_v2.compare_ore_cllw_term_bytes\n--! @see eql_v2.\"<\"\n--! @see eql_v2.\">\"\nCREATE FUNCTION eql_v2.compare_ore_cllw_u64_8(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n a_term eql_v2.ore_cllw_u64_8;\n b_term eql_v2.ore_cllw_u64_8;\n BEGIN\n\n -- PERFORM eql_v2.log('eql_v2.compare_ore_cllw_u64_8');\n -- PERFORM eql_v2.log('a', a::text);\n -- PERFORM eql_v2.log('b', b::text);\n\n IF a IS NULL AND b IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b IS NULL THEN\n RETURN 1;\n END IF;\n\n IF eql_v2.has_ore_cllw_u64_8(a) THEN\n a_term := eql_v2.ore_cllw_u64_8(a);\n END IF;\n\n IF eql_v2.has_ore_cllw_u64_8(a) THEN\n b_term := eql_v2.ore_cllw_u64_8(b);\n END IF;\n\n IF a_term IS NULL AND b_term IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a_term IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b_term IS NULL THEN\n RETURN 1;\n END IF;\n\n RETURN eql_v2.compare_ore_cllw_term_bytes(a_term.bytes, b_term.bytes);\n END;\n$$ LANGUAGE plpgsql;\n\n-- NOTE FILE IS DISABLED\n\n\n--! @brief Equality operator for ORE block types\n--! @internal\n--!\n--! Implements the = operator for direct ORE block comparisons.\n--!\n--! @param a eql_v2.ore_block_u64_8_256 Left operand\n--! @param b eql_v2.ore_block_u64_8_256 Right operand\n--! @return Boolean True if ORE blocks are equal\n--!\n--! @note FILE IS DISABLED - Not included in build\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\nCREATE FUNCTION eql_v2.ore_block_u64_8_256_eq(a eql_v2.ore_block_u64_8_256, b eql_v2.ore_block_u64_8_256)\nRETURNS boolean AS $$\n SELECT eql_v2.compare_ore_block_u64_8_256_terms(a, b) = 0\n$$ LANGUAGE SQL;\n\n\n\n--! @brief Not equal operator for ORE block types\n--! @internal\n--!\n--! Implements the <> operator for direct ORE block comparisons.\n--!\n--! @param a eql_v2.ore_block_u64_8_256 Left operand\n--! @param b eql_v2.ore_block_u64_8_256 Right operand\n--! @return Boolean True if ORE blocks are not equal\n--!\n--! @note FILE IS DISABLED - Not included in build\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\nCREATE FUNCTION eql_v2.ore_block_u64_8_256_neq(a eql_v2.ore_block_u64_8_256, b eql_v2.ore_block_u64_8_256)\nRETURNS boolean AS $$\n SELECT eql_v2.compare_ore_block_u64_8_256_terms(a, b) <> 0\n$$ LANGUAGE SQL;\n\n\n\n--! @brief Less than operator for ORE block types\n--! @internal\n--!\n--! Implements the < operator for direct ORE block comparisons.\n--!\n--! @param a eql_v2.ore_block_u64_8_256 Left operand\n--! @param b eql_v2.ore_block_u64_8_256 Right operand\n--! @return Boolean True if left operand is less than right operand\n--!\n--! @note FILE IS DISABLED - Not included in build\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\nCREATE FUNCTION eql_v2.ore_block_u64_8_256_lt(a eql_v2.ore_block_u64_8_256, b eql_v2.ore_block_u64_8_256)\nRETURNS boolean AS $$\n SELECT eql_v2.compare_ore_block_u64_8_256_terms(a, b) = -1\n$$ LANGUAGE SQL;\n\n\n\n--! @brief Less than or equal operator for ORE block types\n--! @internal\n--!\n--! Implements the <= operator for direct ORE block comparisons.\n--!\n--! @param a eql_v2.ore_block_u64_8_256 Left operand\n--! @param b eql_v2.ore_block_u64_8_256 Right operand\n--! @return Boolean True if left operand is less than or equal to right operand\n--!\n--! @note FILE IS DISABLED - Not included in build\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\nCREATE FUNCTION eql_v2.ore_block_u64_8_256_lte(a eql_v2.ore_block_u64_8_256, b eql_v2.ore_block_u64_8_256)\nRETURNS boolean AS $$\n SELECT eql_v2.compare_ore_block_u64_8_256_terms(a, b) != 1\n$$ LANGUAGE SQL;\n\n\n\n--! @brief Greater than operator for ORE block types\n--! @internal\n--!\n--! Implements the > operator for direct ORE block comparisons.\n--!\n--! @param a eql_v2.ore_block_u64_8_256 Left operand\n--! @param b eql_v2.ore_block_u64_8_256 Right operand\n--! @return Boolean True if left operand is greater than right operand\n--!\n--! @note FILE IS DISABLED - Not included in build\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\nCREATE FUNCTION eql_v2.ore_block_u64_8_256_gt(a eql_v2.ore_block_u64_8_256, b eql_v2.ore_block_u64_8_256)\nRETURNS boolean AS $$\n SELECT eql_v2.compare_ore_block_u64_8_256_terms(a, b) = 1\n$$ LANGUAGE SQL;\n\n\n\n--! @brief Greater than or equal operator for ORE block types\n--! @internal\n--!\n--! Implements the >= operator for direct ORE block comparisons.\n--!\n--! @param a eql_v2.ore_block_u64_8_256 Left operand\n--! @param b eql_v2.ore_block_u64_8_256 Right operand\n--! @return Boolean True if left operand is greater than or equal to right operand\n--!\n--! @note FILE IS DISABLED - Not included in build\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\nCREATE FUNCTION eql_v2.ore_block_u64_8_256_gte(a eql_v2.ore_block_u64_8_256, b eql_v2.ore_block_u64_8_256)\nRETURNS boolean AS $$\n SELECT eql_v2.compare_ore_block_u64_8_256_terms(a, b) != -1\n$$ LANGUAGE SQL;\n\n\n\n--! @brief = operator for ORE block types\n--! @note FILE IS DISABLED - Not included in build\nCREATE OPERATOR = (\n FUNCTION=eql_v2.ore_block_u64_8_256_eq,\n LEFTARG=eql_v2.ore_block_u64_8_256,\n RIGHTARG=eql_v2.ore_block_u64_8_256,\n NEGATOR = <>,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n\n\n--! @brief <> operator for ORE block types\n--! @note FILE IS DISABLED - Not included in build\nCREATE OPERATOR <> (\n FUNCTION=eql_v2.ore_block_u64_8_256_neq,\n LEFTARG=eql_v2.ore_block_u64_8_256,\n RIGHTARG=eql_v2.ore_block_u64_8_256,\n NEGATOR = =,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n\n--! @brief > operator for ORE block types\n--! @note FILE IS DISABLED - Not included in build\nCREATE OPERATOR > (\n FUNCTION=eql_v2.ore_block_u64_8_256_gt,\n LEFTARG=eql_v2.ore_block_u64_8_256,\n RIGHTARG=eql_v2.ore_block_u64_8_256,\n COMMUTATOR = <,\n NEGATOR = <=,\n RESTRICT = scalargtsel,\n JOIN = scalargtjoinsel\n);\n\n\n\n--! @brief < operator for ORE block types\n--! @note FILE IS DISABLED - Not included in build\nCREATE OPERATOR < (\n FUNCTION=eql_v2.ore_block_u64_8_256_lt,\n LEFTARG=eql_v2.ore_block_u64_8_256,\n RIGHTARG=eql_v2.ore_block_u64_8_256,\n COMMUTATOR = >,\n NEGATOR = >=,\n RESTRICT = scalarltsel,\n JOIN = scalarltjoinsel\n);\n\n\n\n--! @brief <= operator for ORE block types\n--! @note FILE IS DISABLED - Not included in build\nCREATE OPERATOR <= (\n FUNCTION=eql_v2.ore_block_u64_8_256_lte,\n LEFTARG=eql_v2.ore_block_u64_8_256,\n RIGHTARG=eql_v2.ore_block_u64_8_256,\n COMMUTATOR = >=,\n NEGATOR = >,\n RESTRICT = scalarlesel,\n JOIN = scalarlejoinsel\n);\n\n\n\n--! @brief >= operator for ORE block types\n--! @note FILE IS DISABLED - Not included in build\nCREATE OPERATOR >= (\n FUNCTION=eql_v2.ore_block_u64_8_256_gte,\n LEFTARG=eql_v2.ore_block_u64_8_256,\n RIGHTARG=eql_v2.ore_block_u64_8_256,\n COMMUTATOR = <=,\n NEGATOR = <,\n RESTRICT = scalarlesel,\n JOIN = scalarlejoinsel\n);\n-- NOTE FILE IS DISABLED\n\n\n\n--! @brief B-tree operator family for ORE block types\n--!\n--! Defines the operator family for creating B-tree indexes on ORE block types.\n--!\n--! @note FILE IS DISABLED - Not included in build\n--! @see eql_v2.ore_block_u64_8_256_operator_class\nCREATE OPERATOR FAMILY eql_v2.ore_block_u64_8_256_operator_family USING btree;\n\n--! @brief B-tree operator class for ORE block encrypted values\n--!\n--! Defines the operator class required for creating B-tree indexes on columns\n--! using the ore_block_u64_8_256 type. Enables range queries and ORDER BY on\n--! ORE-encrypted data without decryption.\n--!\n--! Supports operators: <, <=, =, >=, >\n--! Uses comparison function: compare_ore_block_u64_8_256_terms\n--!\n--! @note FILE IS DISABLED - Not included in build\n--!\n--! @example\n--! -- Would be used like (if enabled):\n--! CREATE INDEX ON events USING btree (\n--! (encrypted_timestamp::jsonb->'ob')::eql_v2.ore_block_u64_8_256\n--! );\n--!\n--! @see CREATE OPERATOR CLASS in PostgreSQL documentation\n--! @see eql_v2.compare_ore_block_u64_8_256_terms\nCREATE OPERATOR CLASS eql_v2.ore_block_u64_8_256_operator_class DEFAULT FOR TYPE eql_v2.ore_block_u64_8_256 USING btree FAMILY eql_v2.ore_block_u64_8_256_operator_family AS\n OPERATOR 1 <,\n OPERATOR 2 <=,\n OPERATOR 3 =,\n OPERATOR 4 >=,\n OPERATOR 5 >,\n FUNCTION 1 eql_v2.compare_ore_block_u64_8_256_terms(a eql_v2.ore_block_u64_8_256, b eql_v2.ore_block_u64_8_256);\n\n\n--! @brief Compare two encrypted values using ORE block index terms\n--!\n--! Performs a three-way comparison (returns -1/0/1) of encrypted values using\n--! their ORE block index terms. Used internally by range operators (<, <=, >, >=)\n--! for order-revealing comparisons without decryption.\n--!\n--! @param a eql_v2_encrypted First encrypted value to compare\n--! @param b eql_v2_encrypted Second encrypted value to compare\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @note NULL values are sorted before non-NULL values\n--! @note Uses ORE cryptographic protocol for secure comparisons\n--!\n--! @see eql_v2.ore_block_u64_8_256\n--! @see eql_v2.has_ore_block_u64_8_256\n--! @see eql_v2.\"<\"\n--! @see eql_v2.\">\"\nCREATE FUNCTION eql_v2.compare_ore_block_u64_8_256(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n a_term eql_v2.ore_block_u64_8_256;\n b_term eql_v2.ore_block_u64_8_256;\n BEGIN\n\n IF a IS NULL AND b IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b IS NULL THEN\n RETURN 1;\n END IF;\n\n IF eql_v2.has_ore_block_u64_8_256(a) THEN\n a_term := eql_v2.ore_block_u64_8_256(a);\n END IF;\n\n IF eql_v2.has_ore_block_u64_8_256(a) THEN\n b_term := eql_v2.ore_block_u64_8_256(b);\n END IF;\n\n IF a_term IS NULL AND b_term IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a_term IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b_term IS NULL THEN\n RETURN 1;\n END IF;\n\n RETURN eql_v2.compare_ore_block_u64_8_256_terms(a_term.terms, b_term.terms);\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Cast text to ORE block term\n--! @internal\n--!\n--! Converts text to bytea and wraps in ore_block_u64_8_256_term type.\n--! Used internally for ORE block extraction and manipulation.\n--!\n--! @param t Text Text value to convert\n--! @return eql_v2.ore_block_u64_8_256_term ORE term containing bytea representation\n--!\n--! @see eql_v2.ore_block_u64_8_256_term\nCREATE FUNCTION eql_v2.text_to_ore_block_u64_8_256_term(t text)\n RETURNS eql_v2.ore_block_u64_8_256_term\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE\nBEGIN ATOMIC\n RETURN t::bytea;\nEND;\n\n--! @brief Implicit cast from text to ORE block term\n--!\n--! Defines an implicit cast allowing automatic conversion of text values\n--! to ore_block_u64_8_256_term type for ORE operations.\n--!\n--! @see eql_v2.text_to_ore_block_u64_8_256_term\nCREATE CAST (text AS eql_v2.ore_block_u64_8_256_term)\n WITH FUNCTION eql_v2.text_to_ore_block_u64_8_256_term(text) AS IMPLICIT;\n\n--! @brief Pattern matching helper using bloom filters\n--! @internal\n--!\n--! Internal helper for LIKE-style pattern matching on encrypted values.\n--! Uses bloom filter index terms to test substring containment without decryption.\n--! Requires 'match' index configuration on the column.\n--!\n--! @param a eql_v2_encrypted Haystack (value to search in)\n--! @param b eql_v2_encrypted Needle (pattern to search for)\n--! @return Boolean True if bloom filter of a contains bloom filter of b\n--!\n--! @see eql_v2.\"~~\"\n--! @see eql_v2.bloom_filter\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.like(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean AS $$\n SELECT eql_v2.bloom_filter(a) @> eql_v2.bloom_filter(b);\n$$ LANGUAGE SQL;\n\n--! @brief Case-insensitive pattern matching helper\n--! @internal\n--!\n--! Internal helper for ILIKE-style case-insensitive pattern matching.\n--! Case sensitivity is controlled by index configuration (token_filters with downcase).\n--! This function has same implementation as like() - actual case handling is in index terms.\n--!\n--! @param a eql_v2_encrypted Haystack (value to search in)\n--! @param b eql_v2_encrypted Needle (pattern to search for)\n--! @return Boolean True if bloom filter of a contains bloom filter of b\n--!\n--! @note Case sensitivity depends on match index token_filters configuration\n--! @see eql_v2.\"~~\"\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.ilike(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean AS $$\n SELECT eql_v2.bloom_filter(a) @> eql_v2.bloom_filter(b);\n$$ LANGUAGE SQL;\n\n--! @brief LIKE operator for encrypted values (pattern matching)\n--!\n--! Implements the ~~ (LIKE) operator for substring/pattern matching on encrypted\n--! text using bloom filter index terms. Enables WHERE col LIKE '%pattern%' queries\n--! without decryption. Requires 'match' index configuration on the column.\n--!\n--! Pattern matching uses n-gram tokenization configured in match index. Token length\n--! and filters affect matching behavior.\n--!\n--! @param a eql_v2_encrypted Haystack (encrypted text to search in)\n--! @param b eql_v2_encrypted Needle (encrypted pattern to search for)\n--! @return Boolean True if a contains b as substring\n--!\n--! @example\n--! -- Search for substring in encrypted email\n--! SELECT * FROM users\n--! WHERE encrypted_email ~~ '%@example.com%'::text::eql_v2_encrypted;\n--!\n--! -- Pattern matching on encrypted names\n--! SELECT * FROM customers\n--! WHERE encrypted_name ~~ 'John%'::text::eql_v2_encrypted;\n--!\n--! @brief SQL LIKE operator (~~ operator) for encrypted text pattern matching\n--!\n--! @param a eql_v2_encrypted Left operand (encrypted value)\n--! @param b eql_v2_encrypted Right operand (encrypted pattern)\n--! @return boolean True if pattern matches\n--!\n--! @note Requires match index: eql_v2.add_search_config(table, column, 'match')\n--! @see eql_v2.like\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.\"~~\"(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.like(a, b);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR ~~(\n FUNCTION=eql_v2.\"~~\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n--! @brief Case-insensitive LIKE operator (~~*)\n--!\n--! Implements ~~* (ILIKE) operator for case-insensitive pattern matching.\n--! Case handling depends on match index token_filters configuration (use downcase filter).\n--! Same implementation as ~~, with case sensitivity controlled by index configuration.\n--!\n--! @param a eql_v2_encrypted Haystack\n--! @param b eql_v2_encrypted Needle\n--! @return Boolean True if a contains b (case-insensitive)\n--!\n--! @note Configure match index with downcase token filter for case-insensitivity\n--! @see eql_v2.\"~~\"\nCREATE OPERATOR ~~*(\n FUNCTION=eql_v2.\"~~\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n--! @brief LIKE operator for encrypted value and JSONB\n--!\n--! Overload of ~~ operator accepting JSONB on the right side. Automatically\n--! casts JSONB to eql_v2_encrypted for bloom filter pattern matching.\n--!\n--! @param eql_v2_encrypted Haystack (encrypted value)\n--! @param b JSONB Needle (will be cast to eql_v2_encrypted)\n--! @return Boolean True if a contains b as substring\n--!\n--! @example\n--! SELECT * FROM users WHERE encrypted_email ~~ '%gmail%'::jsonb;\n--!\n--! @see eql_v2.\"~~\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"~~\"(a eql_v2_encrypted, b jsonb)\n RETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.like(a, b::eql_v2_encrypted);\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR ~~(\n FUNCTION=eql_v2.\"~~\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=jsonb,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\nCREATE OPERATOR ~~*(\n FUNCTION=eql_v2.\"~~\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=jsonb,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n--! @brief LIKE operator for JSONB and encrypted value\n--!\n--! Overload of ~~ operator accepting JSONB on the left side. Automatically\n--! casts JSONB to eql_v2_encrypted for bloom filter pattern matching.\n--!\n--! @param a JSONB Haystack (will be cast to eql_v2_encrypted)\n--! @param eql_v2_encrypted Needle (encrypted pattern)\n--! @return Boolean True if a contains b as substring\n--!\n--! @example\n--! SELECT * FROM users WHERE 'test@example.com'::jsonb ~~ encrypted_pattern;\n--!\n--! @see eql_v2.\"~~\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"~~\"(a jsonb, b eql_v2_encrypted)\n RETURNS boolean\nAS $$\n BEGIN\n RETURN eql_v2.like(a::eql_v2_encrypted, b);\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR ~~(\n FUNCTION=eql_v2.\"~~\",\n LEFTARG=jsonb,\n RIGHTARG=eql_v2_encrypted,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\nCREATE OPERATOR ~~*(\n FUNCTION=eql_v2.\"~~\",\n LEFTARG=jsonb,\n RIGHTARG=eql_v2_encrypted,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n\n-- -----------------------------------------------------------------------------\n\n--! @brief Extract ORE index term for ordering encrypted values\n--!\n--! Helper function that extracts the ore_block_u64_8_256 index term from an encrypted value\n--! for use in ORDER BY clauses when comparison operators are not appropriate or available.\n--!\n--! @param eql_v2_encrypted Encrypted value to extract order term from\n--! @return eql_v2.ore_block_u64_8_256 ORE index term for ordering\n--!\n--! @example\n--! -- Order encrypted values without using comparison operators\n--! SELECT * FROM users ORDER BY eql_v2.order_by(encrypted_age);\n--!\n--! @note Requires 'ore' index configuration on the column\n--! @see eql_v2.ore_block_u64_8_256\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.order_by(a eql_v2_encrypted)\n RETURNS eql_v2.ore_block_u64_8_256\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.ore_block_u64_8_256(a);\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n\n--! @brief PostgreSQL operator class definitions for encrypted value indexing\n--!\n--! Defines the operator family and operator class required for btree indexing\n--! of encrypted values. This enables PostgreSQL to use encrypted columns in:\n--! - CREATE INDEX statements\n--! - ORDER BY clauses\n--! - Range queries\n--! - Primary key constraints\n--!\n--! The operator class maps the five comparison operators (<, <=, =, >=, >)\n--! to the eql_v2.compare() support function for btree index operations.\n--!\n--! @note This is the default operator class for eql_v2_encrypted type\n--! @see eql_v2.compare\n--! @see PostgreSQL documentation on operator classes\n\n--------------------\n\nCREATE OPERATOR FAMILY eql_v2.encrypted_operator_family USING btree;\n\nCREATE OPERATOR CLASS eql_v2.encrypted_operator_class DEFAULT FOR TYPE eql_v2_encrypted USING btree FAMILY eql_v2.encrypted_operator_family AS\n OPERATOR 1 <,\n OPERATOR 2 <=,\n OPERATOR 3 =,\n OPERATOR 4 >=,\n OPERATOR 5 >,\n FUNCTION 1 eql_v2.compare(a eql_v2_encrypted, b eql_v2_encrypted);\n\n\n--------------------\n\n-- CREATE OPERATOR FAMILY eql_v2.encrypted_operator_ordered USING btree;\n\n-- CREATE OPERATOR CLASS eql_v2.encrypted_operator_ordered FOR TYPE eql_v2_encrypted USING btree FAMILY eql_v2.encrypted_operator_ordered AS\n-- OPERATOR 1 <,\n-- OPERATOR 2 <=,\n-- OPERATOR 3 =,\n-- OPERATOR 4 >=,\n-- OPERATOR 5 >,\n-- FUNCTION 1 eql_v2.compare_ore_block_u64_8_256(a eql_v2_encrypted, b eql_v2_encrypted);\n\n--------------------\n\n-- CREATE OPERATOR FAMILY eql_v2.encrypted_hmac_256_operator USING btree;\n\n-- CREATE OPERATOR CLASS eql_v2.encrypted_hmac_256_operator FOR TYPE eql_v2_encrypted USING btree FAMILY eql_v2.encrypted_hmac_256_operator AS\n-- OPERATOR 1 <,\n-- OPERATOR 2 <=,\n-- OPERATOR 3 =,\n-- OPERATOR 4 >=,\n-- OPERATOR 5 >,\n-- FUNCTION 1 eql_v2.compare_hmac(a eql_v2_encrypted, b eql_v2_encrypted);\n\n\n--! @brief Contains operator for encrypted values (@>)\n--!\n--! Implements the @> (contains) operator for testing if left encrypted value\n--! contains the right encrypted value. Uses ste_vec (secure tree encoding vector)\n--! index terms for containment testing without decryption.\n--!\n--! Primarily used for encrypted array or set containment queries.\n--!\n--! @param a eql_v2_encrypted Left operand (container)\n--! @param b eql_v2_encrypted Right operand (contained value)\n--! @return Boolean True if a contains b\n--!\n--! @example\n--! -- Check if encrypted array contains value\n--! SELECT * FROM documents\n--! WHERE encrypted_tags @> '[\"security\"]'::jsonb::eql_v2_encrypted;\n--!\n--! @note Requires ste_vec index configuration\n--! @see eql_v2.ste_vec_contains\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.\"@>\"(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean AS $$\n SELECT eql_v2.ste_vec_contains(a, b)\n$$ LANGUAGE SQL;\n\nCREATE OPERATOR @>(\n FUNCTION=eql_v2.\"@>\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted\n);\n\n--! @brief Contained-by operator for encrypted values (<@)\n--!\n--! Implements the <@ (contained-by) operator for testing if left encrypted value\n--! is contained by the right encrypted value. Uses ste_vec (secure tree encoding vector)\n--! index terms for containment testing without decryption. Reverse of @> operator.\n--!\n--! Primarily used for encrypted array or set containment queries.\n--!\n--! @param a eql_v2_encrypted Left operand (contained value)\n--! @param b eql_v2_encrypted Right operand (container)\n--! @return Boolean True if a is contained by b\n--!\n--! @example\n--! -- Check if value is contained in encrypted array\n--! SELECT * FROM documents\n--! WHERE '[\"security\"]'::jsonb::eql_v2_encrypted <@ encrypted_tags;\n--!\n--! @note Requires ste_vec index configuration\n--! @see eql_v2.ste_vec_contains\n--! @see eql_v2.\\\"@>\\\"\n--! @see eql_v2.add_search_config\n\nCREATE FUNCTION eql_v2.\"<@\"(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS boolean AS $$\n -- Contains with reversed arguments\n SELECT eql_v2.ste_vec_contains(b, a)\n$$ LANGUAGE SQL;\n\nCREATE OPERATOR <@(\n FUNCTION=eql_v2.\"<@\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted\n);\n\n--! @brief Not-equal comparison helper for encrypted values\n--! @internal\n--!\n--! Internal helper that delegates to eql_v2.compare for inequality testing.\n--! Returns true if encrypted values are not equal via encrypted index comparison.\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return Boolean True if values are not equal (compare result <> 0)\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.\"<>\"\nCREATE FUNCTION eql_v2.neq(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.compare(a, b) <> 0;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Not-equal operator for encrypted values\n--!\n--! Implements the <> (not equal) operator for comparing encrypted values using their\n--! encrypted index terms. Enables WHERE clause inequality comparisons without decryption.\n--!\n--! @param a eql_v2_encrypted Left operand\n--! @param b eql_v2_encrypted Right operand\n--! @return Boolean True if encrypted values are not equal\n--!\n--! @example\n--! -- Find records with non-matching values\n--! SELECT * FROM users\n--! WHERE encrypted_email <> 'admin@example.com'::text::eql_v2_encrypted;\n--!\n--! @see eql_v2.compare\n--! @see eql_v2.\"=\"\nCREATE FUNCTION eql_v2.\"<>\"(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.neq(a, b );\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR <> (\n FUNCTION=eql_v2.\"<>\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted,\n NEGATOR = =,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n--! @brief <> operator for encrypted value and JSONB\n--! @see eql_v2.\"<>\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"<>\"(a eql_v2_encrypted, b jsonb)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.neq(a, b::eql_v2_encrypted);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR <> (\n FUNCTION=eql_v2.\"<>\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=jsonb,\n NEGATOR = =,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n--! @brief <> operator for JSONB and encrypted value\n--!\n--! @param jsonb Plain JSONB value\n--! @param eql_v2_encrypted Encrypted value\n--! @return boolean True if values are not equal\n--!\n--! @see eql_v2.\"<>\"(eql_v2_encrypted, eql_v2_encrypted)\nCREATE FUNCTION eql_v2.\"<>\"(a jsonb, b eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.neq(a::eql_v2_encrypted, b);\n END;\n$$ LANGUAGE plpgsql;\n\nCREATE OPERATOR <> (\n FUNCTION=eql_v2.\"<>\",\n LEFTARG=jsonb,\n RIGHTARG=eql_v2_encrypted,\n NEGATOR = =,\n RESTRICT = eqsel,\n JOIN = eqjoinsel,\n HASHES,\n MERGES\n);\n\n\n\n\n\n--! @brief JSONB field accessor operator alias (->>)\n--!\n--! Implements the ->> operator as an alias of -> for encrypted JSONB data. This mirrors\n--! PostgreSQL semantics where ->> returns text via implicit casts. The underlying\n--! implementation delegates to eql_v2.\"->\" and allows PostgreSQL to coerce the result.\n--!\n--! Provides two overloads:\n--! - (eql_v2_encrypted, text) - Field name selector\n--! - (eql_v2_encrypted, eql_v2_encrypted) - Encrypted selector\n--!\n--! @see eql_v2.\"->\"\n--! @see eql_v2.selector\n\n--! @brief ->> operator with text selector\n--! @param eql_v2_encrypted Encrypted JSONB data\n--! @param text Field name to extract\n--! @return text Encrypted value at selector, implicitly cast from eql_v2_encrypted\n--! @example\n--! SELECT encrypted_json ->> 'field_name' FROM table;\nCREATE FUNCTION eql_v2.\"->>\"(e eql_v2_encrypted, selector text)\n RETURNS text\nIMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n found eql_v2_encrypted;\n BEGIN\n -- found = eql_v2.\"->\"(e, selector);\n -- RETURN eql_v2.ciphertext(found);\n RETURN eql_v2.\"->\"(e, selector);\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR ->> (\n FUNCTION=eql_v2.\"->>\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=text\n);\n\n\n\n---------------------------------------------------\n\n--! @brief ->> operator with encrypted selector\n--! @param e eql_v2_encrypted Encrypted JSONB data\n--! @param selector eql_v2_encrypted Encrypted field selector\n--! @return text Encrypted value at selector, implicitly cast from eql_v2_encrypted\n--! @see eql_v2.\"->>\"(eql_v2_encrypted, text)\nCREATE FUNCTION eql_v2.\"->>\"(e eql_v2_encrypted, selector eql_v2_encrypted)\n RETURNS text\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.\"->>\"(e, eql_v2.selector(selector));\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR ->> (\n FUNCTION=eql_v2.\"->>\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted\n);\n\n--! @brief JSONB field accessor operator for encrypted values (->)\n--!\n--! Implements the -> operator to access fields/elements from encrypted JSONB data.\n--! Returns encrypted value matching the provided selector without decryption.\n--!\n--! Encrypted JSON is represented as an array of eql_v2_encrypted values in the ste_vec format.\n--! Each element has a selector, ciphertext, and index terms:\n--! {\"sv\": [{\"c\": \"\", \"s\": \"\", \"b3\": \"\"}]}\n--!\n--! Provides three overloads:\n--! - (eql_v2_encrypted, text) - Field name selector\n--! - (eql_v2_encrypted, eql_v2_encrypted) - Encrypted selector\n--! - (eql_v2_encrypted, integer) - Array index selector (0-based)\n--!\n--! @note Operator resolution: Assignment casts are considered (PostgreSQL standard behavior).\n--! To use text selector, parameter may need explicit cast to text.\n--!\n--! @see eql_v2.ste_vec\n--! @see eql_v2.selector\n--! @see eql_v2.\"->>\"\n\n--! @brief -> operator with text selector\n--! @param eql_v2_encrypted Encrypted JSONB data\n--! @param text Field name to extract\n--! @return eql_v2_encrypted Encrypted value at selector\n--! @example\n--! SELECT encrypted_json -> 'field_name' FROM table;\nCREATE FUNCTION eql_v2.\"->\"(e eql_v2_encrypted, selector text)\n RETURNS eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n meta jsonb;\n sv eql_v2_encrypted[];\n found jsonb;\n BEGIN\n\n IF e IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- Column identifier and version\n meta := eql_v2.meta_data(e);\n\n sv := eql_v2.ste_vec(e);\n\n FOR idx IN 1..array_length(sv, 1) LOOP\n if eql_v2.selector(sv[idx]) = selector THEN\n found := sv[idx];\n END IF;\n END LOOP;\n\n RETURN (meta || found)::eql_v2_encrypted;\n END;\n$$ LANGUAGE plpgsql;\n\n\nCREATE OPERATOR ->(\n FUNCTION=eql_v2.\"->\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=text\n);\n\n---------------------------------------------------\n\n--! @brief -> operator with encrypted selector\n--! @param e eql_v2_encrypted Encrypted JSONB data\n--! @param selector eql_v2_encrypted Encrypted field selector\n--! @return eql_v2_encrypted Encrypted value at selector\n--! @see eql_v2.\"->\"(eql_v2_encrypted, text)\nCREATE FUNCTION eql_v2.\"->\"(e eql_v2_encrypted, selector eql_v2_encrypted)\n RETURNS eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN eql_v2.\"->\"(e, eql_v2.selector(selector));\n END;\n$$ LANGUAGE plpgsql;\n\n\n\nCREATE OPERATOR ->(\n FUNCTION=eql_v2.\"->\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=eql_v2_encrypted\n);\n\n\n---------------------------------------------------\n\n--! @brief -> operator with integer array index\n--! @param eql_v2_encrypted Encrypted array data\n--! @param integer Array index (0-based, JSONB convention)\n--! @return eql_v2_encrypted Encrypted value at array index\n--! @note Array index is 0-based (JSONB standard) despite PostgreSQL arrays being 1-based\n--! @example\n--! SELECT encrypted_array -> 0 FROM table;\n--! @see eql_v2.is_ste_vec_array\nCREATE FUNCTION eql_v2.\"->\"(e eql_v2_encrypted, selector integer)\n RETURNS eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n sv eql_v2_encrypted[];\n found eql_v2_encrypted;\n BEGIN\n IF NOT eql_v2.is_ste_vec_array(e) THEN\n RETURN NULL;\n END IF;\n\n sv := eql_v2.ste_vec(e);\n\n -- PostgreSQL arrays are 1-based\n -- JSONB arrays are 0-based and so the selector is 0-based\n FOR idx IN 1..array_length(sv, 1) LOOP\n if (idx-1) = selector THEN\n found := sv[idx];\n END IF;\n END LOOP;\n\n RETURN found;\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n\n\nCREATE OPERATOR ->(\n FUNCTION=eql_v2.\"->\",\n LEFTARG=eql_v2_encrypted,\n RIGHTARG=integer\n);\n\n\n--! @file jsonb/functions.sql\n--! @brief JSONB path query and array manipulation functions for encrypted data\n--!\n--! These functions provide PostgreSQL-compatible operations on encrypted JSONB values\n--! using Structured Transparent Encryption (STE). They support:\n--! - Path-based queries to extract nested encrypted values\n--! - Existence checks for encrypted fields\n--! - Array operations (length, elements extraction)\n--!\n--! @note STE stores encrypted JSONB as a vector of encrypted elements ('sv') with selectors\n--! @note Functions suppress errors for missing fields, type mismatches (similar to PostgreSQL jsonpath)\n\n\n--! @brief Query encrypted JSONB for elements matching selector\n--!\n--! Searches the Structured Transparent Encryption (STE) vector for elements matching\n--! the given selector path. Returns all matching encrypted elements. If multiple\n--! matches form an array, they are wrapped with array metadata.\n--!\n--! @param jsonb Encrypted JSONB payload containing STE vector ('sv')\n--! @param text Path selector to match against encrypted elements\n--! @return SETOF eql_v2_encrypted Matching encrypted elements (may return multiple rows)\n--!\n--! @note Returns empty set if selector is not found (does not throw exception)\n--! @note Array elements use same selector; multiple matches wrapped with 'a' flag\n--! @note Returns a set containing NULL if val is NULL; returns empty set if no matches found\n--! @see eql_v2.jsonb_path_query_first\n--! @see eql_v2.jsonb_path_exists\nCREATE FUNCTION eql_v2.jsonb_path_query(val jsonb, selector text)\n RETURNS SETOF eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n sv eql_v2_encrypted[];\n found jsonb[];\n e jsonb;\n meta jsonb;\n ary boolean;\n BEGIN\n\n IF val IS NULL THEN\n RETURN NEXT NULL;\n END IF;\n\n -- Column identifier and version\n meta := eql_v2.meta_data(val);\n\n sv := eql_v2.ste_vec(val);\n\n FOR idx IN 1..array_length(sv, 1) LOOP\n e := sv[idx];\n\n IF eql_v2.selector(e) = selector THEN\n found := array_append(found, e);\n IF eql_v2.is_ste_vec_array(e) THEN\n ary := true;\n END IF;\n\n END IF;\n END LOOP;\n\n IF found IS NOT NULL THEN\n\n IF ary THEN\n -- Wrap found array elements as eql_v2_encrypted\n\n RETURN NEXT (meta || jsonb_build_object(\n 'sv', found,\n 'a', 1\n ))::eql_v2_encrypted;\n\n ELSE\n RETURN NEXT (meta || found[1])::eql_v2_encrypted;\n END IF;\n\n END IF;\n\n RETURN;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Query encrypted JSONB with encrypted selector\n--!\n--! Overload that accepts encrypted selector and extracts its plaintext value\n--! before delegating to main jsonb_path_query implementation.\n--!\n--! @param val eql_v2_encrypted Encrypted JSONB value to query\n--! @param selector eql_v2_encrypted Encrypted selector to match against\n--! @return SETOF eql_v2_encrypted Matching encrypted elements\n--!\n--! @see eql_v2.jsonb_path_query(jsonb, text)\nCREATE FUNCTION eql_v2.jsonb_path_query(val eql_v2_encrypted, selector eql_v2_encrypted)\n RETURNS SETOF eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN QUERY\n SELECT * FROM eql_v2.jsonb_path_query(val.data, eql_v2.selector(selector));\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Query encrypted JSONB with text selector\n--!\n--! Overload that accepts encrypted JSONB value and text selector,\n--! extracting the JSONB payload before querying.\n--!\n--! @param eql_v2_encrypted Encrypted JSONB value to query\n--! @param text Path selector to match against\n--! @return SETOF eql_v2_encrypted Matching encrypted elements\n--!\n--! @example\n--! -- Query encrypted JSONB for specific field\n--! SELECT * FROM eql_v2.jsonb_path_query(encrypted_document, '$.address.city');\n--!\n--! @see eql_v2.jsonb_path_query(jsonb, text)\nCREATE FUNCTION eql_v2.jsonb_path_query(val eql_v2_encrypted, selector text)\n RETURNS SETOF eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN QUERY\n SELECT * FROM eql_v2.jsonb_path_query(val.data, selector);\n END;\n$$ LANGUAGE plpgsql;\n\n\n------------------------------------------------------------------------------------\n\n\n--! @brief Check if selector path exists in encrypted JSONB\n--!\n--! Tests whether any encrypted elements match the given selector path.\n--! More efficient than jsonb_path_query when only existence check is needed.\n--!\n--! @param jsonb Encrypted JSONB payload to check\n--! @param text Path selector to test\n--! @return boolean True if matching element exists, false otherwise\n--!\n--! @see eql_v2.jsonb_path_query(jsonb, text)\nCREATE FUNCTION eql_v2.jsonb_path_exists(val jsonb, selector text)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN EXISTS (\n SELECT eql_v2.jsonb_path_query(val, selector)\n );\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check existence with encrypted selector\n--!\n--! Overload that accepts encrypted selector and extracts its value\n--! before checking existence.\n--!\n--! @param val eql_v2_encrypted Encrypted JSONB value to check\n--! @param selector eql_v2_encrypted Encrypted selector to test\n--! @return boolean True if path exists\n--!\n--! @see eql_v2.jsonb_path_exists(jsonb, text)\nCREATE FUNCTION eql_v2.jsonb_path_exists(val eql_v2_encrypted, selector eql_v2_encrypted)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN EXISTS (\n SELECT eql_v2.jsonb_path_query(val, eql_v2.selector(selector))\n );\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Check existence with text selector\n--!\n--! Overload that accepts encrypted JSONB value and text selector.\n--!\n--! @param eql_v2_encrypted Encrypted JSONB value to check\n--! @param text Path selector to test\n--! @return boolean True if path exists\n--!\n--! @example\n--! -- Check if encrypted document has address field\n--! SELECT eql_v2.jsonb_path_exists(encrypted_document, '$.address');\n--!\n--! @see eql_v2.jsonb_path_exists(jsonb, text)\nCREATE FUNCTION eql_v2.jsonb_path_exists(val eql_v2_encrypted, selector text)\n RETURNS boolean\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN EXISTS (\n SELECT eql_v2.jsonb_path_query(val, selector)\n );\n END;\n$$ LANGUAGE plpgsql;\n\n\n------------------------------------------------------------------------------------\n\n\n--! @brief Get first element matching selector\n--!\n--! Returns only the first encrypted element matching the selector path,\n--! or NULL if no match found. More efficient than jsonb_path_query when\n--! only one result is needed.\n--!\n--! @param jsonb Encrypted JSONB payload to query\n--! @param text Path selector to match\n--! @return eql_v2_encrypted First matching element or NULL\n--!\n--! @note Uses LIMIT 1 internally for efficiency\n--! @see eql_v2.jsonb_path_query(jsonb, text)\nCREATE FUNCTION eql_v2.jsonb_path_query_first(val jsonb, selector text)\n RETURNS eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (\n SELECT e\n FROM eql_v2.jsonb_path_query(val, selector) AS e\n LIMIT 1\n );\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Get first element with encrypted selector\n--!\n--! Overload that accepts encrypted selector and extracts its value\n--! before querying for first match.\n--!\n--! @param val eql_v2_encrypted Encrypted JSONB value to query\n--! @param selector eql_v2_encrypted Encrypted selector to match\n--! @return eql_v2_encrypted First matching element or NULL\n--!\n--! @see eql_v2.jsonb_path_query_first(jsonb, text)\nCREATE FUNCTION eql_v2.jsonb_path_query_first(val eql_v2_encrypted, selector eql_v2_encrypted)\n RETURNS eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (\n SELECT e\n FROM eql_v2.jsonb_path_query(val.data, eql_v2.selector(selector)) AS e\n LIMIT 1\n );\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Get first element with text selector\n--!\n--! Overload that accepts encrypted JSONB value and text selector.\n--!\n--! @param eql_v2_encrypted Encrypted JSONB value to query\n--! @param text Path selector to match\n--! @return eql_v2_encrypted First matching element or NULL\n--!\n--! @example\n--! -- Get first matching address from encrypted document\n--! SELECT eql_v2.jsonb_path_query_first(encrypted_document, '$.addresses[*]');\n--!\n--! @see eql_v2.jsonb_path_query_first(jsonb, text)\nCREATE FUNCTION eql_v2.jsonb_path_query_first(val eql_v2_encrypted, selector text)\n RETURNS eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (\n SELECT e\n FROM eql_v2.jsonb_path_query(val.data, selector) AS e\n LIMIT 1\n );\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n------------------------------------------------------------------------------------\n\n\n--! @brief Get length of encrypted JSONB array\n--!\n--! Returns the number of elements in an encrypted JSONB array by counting\n--! elements in the STE vector ('sv'). The encrypted value must have the\n--! array flag ('a') set to true.\n--!\n--! @param jsonb Encrypted JSONB payload representing an array\n--! @return integer Number of elements in the array\n--! @throws Exception 'cannot get array length of a non-array' if 'a' flag is missing or not true\n--!\n--! @note Array flag 'a' must be present and set to true value\n--! @see eql_v2.jsonb_array_elements\nCREATE FUNCTION eql_v2.jsonb_array_length(val jsonb)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n sv eql_v2_encrypted[];\n found eql_v2_encrypted[];\n BEGIN\n\n IF val IS NULL THEN\n RETURN NULL;\n END IF;\n\n IF eql_v2.is_ste_vec_array(val) THEN\n sv := eql_v2.ste_vec(val);\n RETURN array_length(sv, 1);\n END IF;\n\n RAISE 'cannot get array length of a non-array';\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Get array length from encrypted type\n--!\n--! Overload that accepts encrypted composite type and extracts the\n--! JSONB payload before computing array length.\n--!\n--! @param eql_v2_encrypted Encrypted array value\n--! @return integer Number of elements in the array\n--! @throws Exception if value is not an array\n--!\n--! @example\n--! -- Get length of encrypted array\n--! SELECT eql_v2.jsonb_array_length(encrypted_tags);\n--!\n--! @see eql_v2.jsonb_array_length(jsonb)\nCREATE FUNCTION eql_v2.jsonb_array_length(val eql_v2_encrypted)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN (\n SELECT eql_v2.jsonb_array_length(val.data)\n );\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n\n--! @brief Extract elements from encrypted JSONB array\n--!\n--! Returns each element of an encrypted JSONB array as a separate row.\n--! Each element is returned as an eql_v2_encrypted value with metadata\n--! preserved from the parent array.\n--!\n--! @param jsonb Encrypted JSONB payload representing an array\n--! @return SETOF eql_v2_encrypted One row per array element\n--! @throws Exception if value is not an array (missing 'a' flag)\n--!\n--! @note Each element inherits metadata (version, ident) from parent\n--! @see eql_v2.jsonb_array_length\n--! @see eql_v2.jsonb_array_elements_text\nCREATE FUNCTION eql_v2.jsonb_array_elements(val jsonb)\n RETURNS SETOF eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n sv eql_v2_encrypted[];\n meta jsonb;\n item jsonb;\n BEGIN\n\n IF NOT eql_v2.is_ste_vec_array(val) THEN\n RAISE 'cannot extract elements from non-array';\n END IF;\n\n -- Column identifier and version\n meta := eql_v2.meta_data(val);\n\n sv := eql_v2.ste_vec(val);\n\n FOR idx IN 1..array_length(sv, 1) LOOP\n item = sv[idx];\n RETURN NEXT (meta || item)::eql_v2_encrypted;\n END LOOP;\n\n RETURN;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract elements from encrypted array type\n--!\n--! Overload that accepts encrypted composite type and extracts each\n--! array element as a separate row.\n--!\n--! @param eql_v2_encrypted Encrypted array value\n--! @return SETOF eql_v2_encrypted One row per array element\n--! @throws Exception if value is not an array\n--!\n--! @example\n--! -- Expand encrypted array into rows\n--! SELECT * FROM eql_v2.jsonb_array_elements(encrypted_tags);\n--!\n--! @see eql_v2.jsonb_array_elements(jsonb)\nCREATE FUNCTION eql_v2.jsonb_array_elements(val eql_v2_encrypted)\n RETURNS SETOF eql_v2_encrypted\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN QUERY\n SELECT * FROM eql_v2.jsonb_array_elements(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Extract encrypted array elements as ciphertext\n--!\n--! Returns each element of an encrypted JSONB array as its raw ciphertext\n--! value (text representation). Unlike jsonb_array_elements, this returns\n--! only the ciphertext 'c' field without metadata.\n--!\n--! @param jsonb Encrypted JSONB payload representing an array\n--! @return SETOF text One ciphertext string per array element\n--! @throws Exception if value is not an array (missing 'a' flag)\n--!\n--! @note Returns ciphertext only, not full encrypted structure\n--! @see eql_v2.jsonb_array_elements\nCREATE FUNCTION eql_v2.jsonb_array_elements_text(val jsonb)\n RETURNS SETOF text\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n sv eql_v2_encrypted[];\n found eql_v2_encrypted[];\n BEGIN\n IF NOT eql_v2.is_ste_vec_array(val) THEN\n RAISE 'cannot extract elements from non-array';\n END IF;\n\n sv := eql_v2.ste_vec(val);\n\n FOR idx IN 1..array_length(sv, 1) LOOP\n RETURN NEXT eql_v2.ciphertext(sv[idx]);\n END LOOP;\n\n RETURN;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Extract array elements as ciphertext from encrypted type\n--!\n--! Overload that accepts encrypted composite type and extracts each\n--! array element's ciphertext as text.\n--!\n--! @param eql_v2_encrypted Encrypted array value\n--! @return SETOF text One ciphertext string per array element\n--! @throws Exception if value is not an array\n--!\n--! @example\n--! -- Get ciphertext of each array element\n--! SELECT * FROM eql_v2.jsonb_array_elements_text(encrypted_tags);\n--!\n--! @see eql_v2.jsonb_array_elements_text(jsonb)\nCREATE FUNCTION eql_v2.jsonb_array_elements_text(val eql_v2_encrypted)\n RETURNS SETOF text\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN QUERY\n SELECT * FROM eql_v2.jsonb_array_elements_text(val.data);\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Compare two encrypted values using HMAC-SHA256 index terms\n--!\n--! Performs a three-way comparison (returns -1/0/1) of encrypted values using\n--! their HMAC-SHA256 hash index terms. Used internally by the equality operator (=)\n--! for exact-match queries without decryption.\n--!\n--! @param a eql_v2_encrypted First encrypted value to compare\n--! @param b eql_v2_encrypted Second encrypted value to compare\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @note NULL values are sorted before non-NULL values\n--! @note Comparison uses underlying text type ordering of HMAC-SHA256 hashes\n--!\n--! @see eql_v2.hmac_256\n--! @see eql_v2.has_hmac_256\n--! @see eql_v2.\"=\"\nCREATE FUNCTION eql_v2.compare_hmac_256(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n a_term eql_v2.hmac_256;\n b_term eql_v2.hmac_256;\n BEGIN\n\n IF a IS NULL AND b IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b IS NULL THEN\n RETURN 1;\n END IF;\n\n IF eql_v2.has_hmac_256(a) THEN\n a_term = eql_v2.hmac_256(a);\n END IF;\n\n IF eql_v2.has_hmac_256(b) THEN\n b_term = eql_v2.hmac_256(b);\n END IF;\n\n IF a_term IS NULL AND b_term IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a_term IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b_term IS NULL THEN\n RETURN 1;\n END IF;\n\n -- Using the underlying text type comparison\n IF a_term = b_term THEN\n RETURN 0;\n END IF;\n\n IF a_term < b_term THEN\n RETURN -1;\n END IF;\n\n IF a_term > b_term THEN\n RETURN 1;\n END IF;\n\n END;\n$$ LANGUAGE plpgsql;\n--! @file encryptindex/functions.sql\n--! @brief Configuration lifecycle and column encryption management\n--!\n--! Provides functions for managing encryption configuration transitions:\n--! - Comparing configurations to identify changes\n--! - Identifying columns needing encryption\n--! - Creating and renaming encrypted columns during initial setup\n--! - Tracking encryption progress\n--!\n--! These functions support the workflow of activating a pending configuration\n--! and performing the initial encryption of plaintext columns.\n\n\n--! @brief Compare two configurations and find differences\n--! @internal\n--!\n--! Returns table/column pairs where configuration differs between two configs.\n--! Used to identify which columns need encryption when activating a pending config.\n--!\n--! @param a jsonb First configuration to compare\n--! @param b jsonb Second configuration to compare\n--! @return TABLE(table_name text, column_name text) Columns with differing configuration\n--!\n--! @note Compares configuration structure, not just presence/absence\n--! @see eql_v2.select_pending_columns\nCREATE FUNCTION eql_v2.diff_config(a JSONB, b JSONB)\n RETURNS TABLE(table_name TEXT, column_name TEXT)\nIMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n RETURN QUERY\n WITH table_keys AS (\n SELECT jsonb_object_keys(a->'tables') AS key\n UNION\n SELECT jsonb_object_keys(b->'tables') AS key\n ),\n column_keys AS (\n SELECT tk.key AS table_key, jsonb_object_keys(a->'tables'->tk.key) AS column_key\n FROM table_keys tk\n UNION\n SELECT tk.key AS table_key, jsonb_object_keys(b->'tables'->tk.key) AS column_key\n FROM table_keys tk\n )\n SELECT\n ck.table_key AS table_name,\n ck.column_key AS column_name\n FROM\n column_keys ck\n WHERE\n (a->'tables'->ck.table_key->ck.column_key IS DISTINCT FROM b->'tables'->ck.table_key->ck.column_key);\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Get columns with pending configuration changes\n--!\n--! Compares 'pending' and 'active' configurations to identify columns that need\n--! encryption or re-encryption. Returns columns where configuration differs.\n--!\n--! @return TABLE(table_name text, column_name text) Columns needing encryption\n--! @throws Exception if no pending configuration exists\n--!\n--! @note Treats missing active config as empty config\n--! @see eql_v2.diff_config\n--! @see eql_v2.select_target_columns\nCREATE FUNCTION eql_v2.select_pending_columns()\n RETURNS TABLE(table_name TEXT, column_name TEXT)\nAS $$\n DECLARE\n active JSONB;\n pending JSONB;\n config_id BIGINT;\n BEGIN\n SELECT data INTO active FROM eql_v2_configuration WHERE state = 'active';\n\n -- set default config\n IF active IS NULL THEN\n active := '{}';\n END IF;\n\n SELECT id, data INTO config_id, pending FROM eql_v2_configuration WHERE state = 'pending';\n\n -- set default config\n IF config_id IS NULL THEN\n RAISE EXCEPTION 'No pending configuration exists to encrypt';\n END IF;\n\n RETURN QUERY\n SELECT d.table_name, d.column_name FROM eql_v2.diff_config(active, pending) as d;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Map pending columns to their encrypted target columns\n--!\n--! For each column with pending configuration, identifies the corresponding\n--! encrypted column. During initial encryption, target is '{column_name}_encrypted'.\n--! Returns NULL for target_column if encrypted column doesn't exist yet.\n--!\n--! @return TABLE(table_name text, column_name text, target_column text) Column mappings\n--!\n--! @note Target column is NULL if no column exists matching either 'column_name' or 'column_name_encrypted' with type eql_v2_encrypted\n--! @note The LEFT JOIN checks both original and '_encrypted' suffix variations with type verification\n--! @see eql_v2.select_pending_columns\n--! @see eql_v2.create_encrypted_columns\nCREATE FUNCTION eql_v2.select_target_columns()\n RETURNS TABLE(table_name TEXT, column_name TEXT, target_column TEXT)\n STABLE STRICT PARALLEL SAFE\nAS $$\n SELECT\n c.table_name,\n c.column_name,\n s.column_name as target_column\n FROM\n eql_v2.select_pending_columns() c\n LEFT JOIN information_schema.columns s ON\n s.table_name = c.table_name AND\n (s.column_name = c.column_name OR s.column_name = c.column_name || '_encrypted') AND\n s.udt_name = 'eql_v2_encrypted';\n$$ LANGUAGE sql;\n\n\n--! @brief Check if database is ready for encryption\n--!\n--! Verifies that all columns with pending configuration have corresponding\n--! encrypted target columns created. Returns true if encryption can proceed.\n--!\n--! @return boolean True if all pending columns have target encrypted columns\n--!\n--! @note Returns false if any pending column lacks encrypted column\n--! @see eql_v2.select_target_columns\n--! @see eql_v2.create_encrypted_columns\nCREATE FUNCTION eql_v2.ready_for_encryption()\n RETURNS BOOLEAN\n STABLE STRICT PARALLEL SAFE\nAS $$\n SELECT EXISTS (\n SELECT *\n FROM eql_v2.select_target_columns() AS c\n WHERE c.target_column IS NOT NULL);\n$$ LANGUAGE sql;\n\n\n--! @brief Create encrypted columns for initial encryption\n--!\n--! For each plaintext column with pending configuration that lacks an encrypted\n--! target column, creates a new column '{column_name}_encrypted' of type\n--! eql_v2_encrypted. This prepares the database schema for initial encryption.\n--!\n--! @return TABLE(table_name text, column_name text) Created encrypted columns\n--!\n--! @warning Executes dynamic DDL (ALTER TABLE ADD COLUMN) - modifies database schema\n--! @note Only creates columns that don't already exist\n--! @see eql_v2.select_target_columns\n--! @see eql_v2.rename_encrypted_columns\nCREATE FUNCTION eql_v2.create_encrypted_columns()\n RETURNS TABLE(table_name TEXT, column_name TEXT)\nAS $$\n BEGIN\n FOR table_name, column_name IN\n SELECT c.table_name, (c.column_name || '_encrypted') FROM eql_v2.select_target_columns() AS c WHERE c.target_column IS NULL\n LOOP\n EXECUTE format('ALTER TABLE %I ADD column %I eql_v2_encrypted;', table_name, column_name);\n RETURN NEXT;\n END LOOP;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Finalize initial encryption by renaming columns\n--!\n--! After initial encryption completes, renames columns to complete the transition:\n--! - Plaintext column '{column_name}' → '{column_name}_plaintext'\n--! - Encrypted column '{column_name}_encrypted' → '{column_name}'\n--!\n--! This makes the encrypted column the primary column with the original name.\n--!\n--! @return TABLE(table_name text, column_name text, target_column text) Renamed columns\n--!\n--! @warning Executes dynamic DDL (ALTER TABLE RENAME COLUMN) - modifies database schema\n--! @note Only renames columns where target is '{column_name}_encrypted'\n--! @see eql_v2.create_encrypted_columns\nCREATE FUNCTION eql_v2.rename_encrypted_columns()\n RETURNS TABLE(table_name TEXT, column_name TEXT, target_column TEXT)\nAS $$\n BEGIN\n FOR table_name, column_name, target_column IN\n SELECT * FROM eql_v2.select_target_columns() as c WHERE c.target_column = c.column_name || '_encrypted'\n LOOP\n EXECUTE format('ALTER TABLE %I RENAME %I TO %I;', table_name, column_name, column_name || '_plaintext');\n EXECUTE format('ALTER TABLE %I RENAME %I TO %I;', table_name, target_column, column_name);\n RETURN NEXT;\n END LOOP;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Count rows encrypted with active configuration\n--! @internal\n--!\n--! Counts rows in a table where the encrypted column was encrypted using\n--! the currently active configuration. Used to track encryption progress.\n--!\n--! @param table_name text Name of table to check\n--! @param column_name text Name of encrypted column to check\n--! @return bigint Count of rows encrypted with active configuration\n--!\n--! @note The 'v' field in encrypted payloads stores the payload version (\"2\"), not the configuration ID\n--! @note Configuration tracking mechanism is implementation-specific\nCREATE FUNCTION eql_v2.count_encrypted_with_active_config(table_name TEXT, column_name TEXT)\n RETURNS BIGINT\nAS $$\nDECLARE\n result BIGINT;\nBEGIN\n EXECUTE format(\n 'SELECT COUNT(%I) FROM %s t WHERE %I->>%L = (SELECT id::TEXT FROM eql_v2_configuration WHERE state = %L)',\n column_name, table_name, column_name, 'v', 'active'\n )\n INTO result;\n RETURN result;\nEND;\n$$ LANGUAGE plpgsql;\n\n\n\n--! @brief Validate presence of ident field in encrypted payload\n--! @internal\n--!\n--! Checks that the encrypted JSONB payload contains the required 'i' (ident) field.\n--! The ident field tracks which table and column the encrypted value belongs to.\n--!\n--! @param jsonb Encrypted payload to validate\n--! @return Boolean True if 'i' field is present\n--! @throws Exception if 'i' field is missing\n--!\n--! @note Used in CHECK constraints to ensure payload structure\n--! @see eql_v2.check_encrypted\nCREATE FUNCTION eql_v2._encrypted_check_i(val jsonb)\n RETURNS boolean\nAS $$\n BEGIN\n IF val ? 'i' THEN\n RETURN true;\n END IF;\n RAISE 'Encrypted column missing ident (i) field: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Validate table and column fields in ident\n--! @internal\n--!\n--! Checks that the 'i' (ident) field contains both 't' (table) and 'c' (column)\n--! subfields, which identify the origin of the encrypted value.\n--!\n--! @param jsonb Encrypted payload to validate\n--! @return Boolean True if both 't' and 'c' subfields are present\n--! @throws Exception if 't' or 'c' subfields are missing\n--!\n--! @note Used in CHECK constraints to ensure payload structure\n--! @see eql_v2.check_encrypted\nCREATE FUNCTION eql_v2._encrypted_check_i_ct(val jsonb)\n RETURNS boolean\nAS $$\n BEGIN\n IF (val->'i' ?& array['t', 'c']) THEN\n RETURN true;\n END IF;\n RAISE 'Encrypted column ident (i) missing table (t) or column (c) fields: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Validate version field in encrypted payload\n--! @internal\n--!\n--! Checks that the encrypted payload has version field 'v' set to '2',\n--! the current EQL v2 payload version.\n--!\n--! @param jsonb Encrypted payload to validate\n--! @return Boolean True if 'v' field is present and equals '2'\n--! @throws Exception if 'v' field is missing or not '2'\n--!\n--! @note Used in CHECK constraints to ensure payload structure\n--! @see eql_v2.check_encrypted\nCREATE FUNCTION eql_v2._encrypted_check_v(val jsonb)\n RETURNS boolean\nAS $$\n BEGIN\n IF (val ? 'v') THEN\n\n IF val->>'v' <> '2' THEN\n RAISE 'Expected encrypted column version (v) 2';\n RETURN false;\n END IF;\n\n RETURN true;\n END IF;\n RAISE 'Encrypted column missing version (v) field: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Validate ciphertext field in encrypted payload\n--! @internal\n--!\n--! Checks that the encrypted payload contains the required 'c' (ciphertext) field\n--! which stores the encrypted data.\n--!\n--! @param jsonb Encrypted payload to validate\n--! @return Boolean True if 'c' field is present\n--! @throws Exception if 'c' field is missing\n--!\n--! @note Used in CHECK constraints to ensure payload structure\n--! @see eql_v2.check_encrypted\nCREATE FUNCTION eql_v2._encrypted_check_c(val jsonb)\n RETURNS boolean\nAS $$\n BEGIN\n IF (val ? 'c') THEN\n RETURN true;\n END IF;\n RAISE 'Encrypted column missing ciphertext (c) field: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Validate complete encrypted payload structure\n--!\n--! Comprehensive validation function that checks all required fields in an\n--! encrypted JSONB payload: version ('v'), ciphertext ('c'), ident ('i'),\n--! and ident subfields ('t', 'c').\n--!\n--! This function is used in CHECK constraints to ensure encrypted column\n--! data integrity at the database level.\n--!\n--! @param jsonb Encrypted payload to validate\n--! @return Boolean True if all structure checks pass\n--! @throws Exception if any required field is missing or invalid\n--!\n--! @example\n--! -- Add validation constraint to encrypted column\n--! ALTER TABLE users ADD CONSTRAINT check_email_encrypted\n--! CHECK (eql_v2.check_encrypted(encrypted_email::jsonb));\n--!\n--! @see eql_v2._encrypted_check_v\n--! @see eql_v2._encrypted_check_c\n--! @see eql_v2._encrypted_check_i\n--! @see eql_v2._encrypted_check_i_ct\nCREATE FUNCTION eql_v2.check_encrypted(val jsonb)\n RETURNS BOOLEAN\nLANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE\nBEGIN ATOMIC\n RETURN (\n eql_v2._encrypted_check_v(val) AND\n eql_v2._encrypted_check_c(val) AND\n eql_v2._encrypted_check_i(val) AND\n eql_v2._encrypted_check_i_ct(val)\n );\nEND;\n\n\n--! @brief Validate encrypted composite type structure\n--!\n--! Validates an eql_v2_encrypted composite type by checking its underlying\n--! JSONB payload. Delegates to eql_v2.check_encrypted(jsonb).\n--!\n--! @param eql_v2_encrypted Encrypted value to validate\n--! @return Boolean True if structure is valid\n--! @throws Exception if any required field is missing or invalid\n--!\n--! @see eql_v2.check_encrypted(jsonb)\nCREATE FUNCTION eql_v2.check_encrypted(val eql_v2_encrypted)\n RETURNS BOOLEAN\nLANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE\nBEGIN ATOMIC\n RETURN eql_v2.check_encrypted(val.data);\nEND;\n\n\n-- Aggregate functions for ORE\n\n--! @brief State transition function for min aggregate\n--! @internal\n--!\n--! Returns the smaller of two encrypted values for use in MIN aggregate.\n--! Comparison uses ORE index terms without decryption.\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return eql_v2_encrypted The smaller of the two values\n--!\n--! @see eql_v2.min(eql_v2_encrypted)\nCREATE FUNCTION eql_v2.min(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS eql_v2_encrypted\nSTRICT\nAS $$\n BEGIN\n IF a < b THEN\n RETURN a;\n ELSE\n RETURN b;\n END IF;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Find minimum encrypted value in a group\n--!\n--! Aggregate function that returns the minimum encrypted value in a group\n--! using ORE index term comparisons without decryption.\n--!\n--! @param input eql_v2_encrypted Encrypted values to aggregate\n--! @return eql_v2_encrypted Minimum value in the group\n--!\n--! @example\n--! -- Find minimum age per department\n--! SELECT department, eql_v2.min(encrypted_age)\n--! FROM employees\n--! GROUP BY department;\n--!\n--! @note Requires 'ore' index configuration on the column\n--! @see eql_v2.min(eql_v2_encrypted, eql_v2_encrypted)\nCREATE AGGREGATE eql_v2.min(eql_v2_encrypted)\n(\n sfunc = eql_v2.min,\n stype = eql_v2_encrypted\n);\n\n\n--! @brief State transition function for max aggregate\n--! @internal\n--!\n--! Returns the larger of two encrypted values for use in MAX aggregate.\n--! Comparison uses ORE index terms without decryption.\n--!\n--! @param a eql_v2_encrypted First encrypted value\n--! @param b eql_v2_encrypted Second encrypted value\n--! @return eql_v2_encrypted The larger of the two values\n--!\n--! @see eql_v2.max(eql_v2_encrypted)\nCREATE FUNCTION eql_v2.max(a eql_v2_encrypted, b eql_v2_encrypted)\nRETURNS eql_v2_encrypted\nSTRICT\nAS $$\n BEGIN\n IF a > b THEN\n RETURN a;\n ELSE\n RETURN b;\n END IF;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Find maximum encrypted value in a group\n--!\n--! Aggregate function that returns the maximum encrypted value in a group\n--! using ORE index term comparisons without decryption.\n--!\n--! @param input eql_v2_encrypted Encrypted values to aggregate\n--! @return eql_v2_encrypted Maximum value in the group\n--!\n--! @example\n--! -- Find maximum salary per department\n--! SELECT department, eql_v2.max(encrypted_salary)\n--! FROM employees\n--! GROUP BY department;\n--!\n--! @note Requires 'ore' index configuration on the column\n--! @see eql_v2.max(eql_v2_encrypted, eql_v2_encrypted)\nCREATE AGGREGATE eql_v2.max(eql_v2_encrypted)\n(\n sfunc = eql_v2.max,\n stype = eql_v2_encrypted\n);\n\n\n--! @file config/indexes.sql\n--! @brief Configuration state uniqueness indexes\n--!\n--! Creates partial unique indexes to enforce that only one configuration\n--! can be in 'active', 'pending', or 'encrypting' state at any time.\n--! Multiple 'inactive' configurations are allowed.\n--!\n--! @note Uses partial indexes (WHERE clauses) for efficiency\n--! @note Prevents conflicting configurations from being active simultaneously\n--! @see config/types.sql for state definitions\n\n\n--! @brief Unique active configuration constraint\n--! @note Only one configuration can be 'active' at once\nCREATE UNIQUE INDEX ON public.eql_v2_configuration (state) WHERE state = 'active';\n\n--! @brief Unique pending configuration constraint\n--! @note Only one configuration can be 'pending' at once\nCREATE UNIQUE INDEX ON public.eql_v2_configuration (state) WHERE state = 'pending';\n\n--! @brief Unique encrypting configuration constraint\n--! @note Only one configuration can be 'encrypting' at once\nCREATE UNIQUE INDEX ON public.eql_v2_configuration (state) WHERE state = 'encrypting';\n\n\n--! @brief Add a search index configuration for an encrypted column\n--!\n--! Configures a searchable encryption index (unique, match, ore, or ste_vec) on an\n--! encrypted column. Creates or updates the pending configuration, then migrates\n--! and activates it unless migrating flag is set.\n--!\n--! @param table_name Text Name of the table containing the column\n--! @param column_name Text Name of the column to configure\n--! @param index_name Text Type of index ('unique', 'match', 'ore', 'ste_vec')\n--! @param cast_as Text PostgreSQL type for decrypted values (default: 'text')\n--! @param opts JSONB Index-specific options (default: '{}')\n--! @param migrating Boolean Skip auto-migration if true (default: false)\n--! @return JSONB Updated configuration object\n--! @throws Exception if index already exists for this column\n--! @throws Exception if cast_as is not a valid type\n--!\n--! @example\n--! -- Add unique index for exact-match searches\n--! SELECT eql_v2.add_search_config('users', 'email', 'unique');\n--!\n--! -- Add match index for LIKE searches with custom token length\n--! SELECT eql_v2.add_search_config('posts', 'content', 'match', 'text',\n--! '{\"token_filters\": [{\"kind\": \"downcase\"}], \"tokenizer\": {\"kind\": \"ngram\", \"token_length\": 3}}'\n--! );\n--!\n--! @see eql_v2.add_column\n--! @see eql_v2.remove_search_config\nCREATE FUNCTION eql_v2.add_search_config(table_name text, column_name text, index_name text, cast_as text DEFAULT 'text', opts jsonb DEFAULT '{}', migrating boolean DEFAULT false)\n RETURNS jsonb\n\nAS $$\n DECLARE\n o jsonb;\n _config jsonb;\n BEGIN\n\n -- set the active config\n SELECT data INTO _config FROM public.eql_v2_configuration WHERE state = 'active' OR state = 'pending' ORDER BY state DESC;\n\n -- if index exists\n IF _config #> array['tables', table_name, column_name, 'indexes'] ? index_name THEN\n RAISE EXCEPTION '% index exists for column: % %', index_name, table_name, column_name;\n END IF;\n\n IF NOT cast_as = ANY('{text, int, small_int, big_int, real, double, boolean, date, jsonb}') THEN\n RAISE EXCEPTION '% is not a valid cast type', cast_as;\n END IF;\n\n -- set default config\n SELECT eql_v2.config_default(_config) INTO _config;\n\n SELECT eql_v2.config_add_table(table_name, _config) INTO _config;\n\n SELECT eql_v2.config_add_column(table_name, column_name, _config) INTO _config;\n\n SELECT eql_v2.config_add_cast(table_name, column_name, cast_as, _config) INTO _config;\n\n -- set default options for index if opts empty\n IF index_name = 'match' AND opts = '{}' THEN\n SELECT eql_v2.config_match_default() INTO opts;\n END IF;\n\n SELECT eql_v2.config_add_index(table_name, column_name, index_name, opts, _config) INTO _config;\n\n -- create a new pending record if we don't have one\n INSERT INTO public.eql_v2_configuration (state, data) VALUES ('pending', _config)\n ON CONFLICT (state)\n WHERE state = 'pending'\n DO UPDATE\n SET data = _config;\n\n IF NOT migrating THEN\n PERFORM eql_v2.migrate_config();\n PERFORM eql_v2.activate_config();\n END IF;\n\n PERFORM eql_v2.add_encrypted_constraint(table_name, column_name);\n\n -- exeunt\n RETURN _config;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Remove a search index configuration from an encrypted column\n--!\n--! Removes a previously configured search index from an encrypted column.\n--! Updates the pending configuration, then migrates and activates it\n--! unless migrating flag is set.\n--!\n--! @param table_name Text Name of the table containing the column\n--! @param column_name Text Name of the column\n--! @param index_name Text Type of index to remove\n--! @param migrating Boolean Skip auto-migration if true (default: false)\n--! @return JSONB Updated configuration object\n--! @throws Exception if no active or pending configuration exists\n--! @throws Exception if table is not configured\n--! @throws Exception if column is not configured\n--!\n--! @example\n--! -- Remove match index from column\n--! SELECT eql_v2.remove_search_config('posts', 'content', 'match');\n--!\n--! @see eql_v2.add_search_config\n--! @see eql_v2.modify_search_config\nCREATE FUNCTION eql_v2.remove_search_config(table_name text, column_name text, index_name text, migrating boolean DEFAULT false)\n RETURNS jsonb\nAS $$\n DECLARE\n _config jsonb;\n BEGIN\n\n -- set the active config\n SELECT data INTO _config FROM public.eql_v2_configuration WHERE state = 'active' OR state = 'pending' ORDER BY state DESC;\n\n -- if no config\n IF _config IS NULL THEN\n RAISE EXCEPTION 'No active or pending configuration exists';\n END IF;\n\n -- if the table doesn't exist\n IF NOT _config #> array['tables'] ? table_name THEN\n RAISE EXCEPTION 'No configuration exists for table: %', table_name;\n END IF;\n\n -- if the index does not exist\n -- IF NOT _config->key ? index_name THEN\n IF NOT _config #> array['tables', table_name] ? column_name THEN\n RAISE EXCEPTION 'No % index exists for column: % %', index_name, table_name, column_name;\n END IF;\n\n -- create a new pending record if we don't have one\n INSERT INTO public.eql_v2_configuration (state, data) VALUES ('pending', _config)\n ON CONFLICT (state)\n WHERE state = 'pending'\n DO NOTHING;\n\n -- remove the index\n SELECT _config #- array['tables', table_name, column_name, 'indexes', index_name] INTO _config;\n\n -- update the config and migrate (even if empty)\n UPDATE public.eql_v2_configuration SET data = _config WHERE state = 'pending';\n\n IF NOT migrating THEN\n PERFORM eql_v2.migrate_config();\n PERFORM eql_v2.activate_config();\n END IF;\n\n -- exeunt\n RETURN _config;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Modify a search index configuration for an encrypted column\n--!\n--! Updates an existing search index configuration by removing and re-adding it\n--! with new options. Convenience function that combines remove and add operations.\n--! If index does not exist, it is added.\n--!\n--! @param table_name Text Name of the table containing the column\n--! @param column_name Text Name of the column\n--! @param index_name Text Type of index to modify\n--! @param cast_as Text PostgreSQL type for decrypted values (default: 'text')\n--! @param opts JSONB New index-specific options (default: '{}')\n--! @param migrating Boolean Skip auto-migration if true (default: false)\n--! @return JSONB Updated configuration object\n--!\n--! @example\n--! -- Change match index tokenizer settings\n--! SELECT eql_v2.modify_search_config('posts', 'content', 'match', 'text',\n--! '{\"tokenizer\": {\"kind\": \"ngram\", \"token_length\": 4}}'\n--! );\n--!\n--! @see eql_v2.add_search_config\n--! @see eql_v2.remove_search_config\nCREATE FUNCTION eql_v2.modify_search_config(table_name text, column_name text, index_name text, cast_as text DEFAULT 'text', opts jsonb DEFAULT '{}', migrating boolean DEFAULT false)\n RETURNS jsonb\nAS $$\n BEGIN\n PERFORM eql_v2.remove_search_config(table_name, column_name, index_name, migrating);\n RETURN eql_v2.add_search_config(table_name, column_name, index_name, cast_as, opts, migrating);\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Migrate pending configuration to encrypting state\n--!\n--! Transitions the pending configuration to encrypting state, validating that\n--! all configured columns have encrypted target columns ready. This is part of\n--! the configuration lifecycle: pending → encrypting → active.\n--!\n--! @return Boolean True if migration succeeds\n--! @throws Exception if encryption already in progress\n--! @throws Exception if no pending configuration exists\n--! @throws Exception if configured columns lack encrypted targets\n--!\n--! @example\n--! -- Manually migrate configuration (normally done automatically)\n--! SELECT eql_v2.migrate_config();\n--!\n--! @see eql_v2.activate_config\n--! @see eql_v2.add_column\nCREATE FUNCTION eql_v2.migrate_config()\n RETURNS boolean\nAS $$\n BEGIN\n\n IF EXISTS (SELECT FROM public.eql_v2_configuration c WHERE c.state = 'encrypting') THEN\n RAISE EXCEPTION 'An encryption is already in progress';\n END IF;\n\n IF NOT EXISTS (SELECT FROM public.eql_v2_configuration c WHERE c.state = 'pending') THEN\n RAISE EXCEPTION 'No pending configuration exists to encrypt';\n END IF;\n\n IF NOT eql_v2.ready_for_encryption() THEN\n RAISE EXCEPTION 'Some pending columns do not have an encrypted target';\n END IF;\n\n UPDATE public.eql_v2_configuration SET state = 'encrypting' WHERE state = 'pending';\n RETURN true;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Activate encrypting configuration\n--!\n--! Transitions the encrypting configuration to active state, making it the\n--! current operational configuration. Marks previous active configuration as\n--! inactive. Final step in configuration lifecycle: pending → encrypting → active.\n--!\n--! @return Boolean True if activation succeeds\n--! @throws Exception if no encrypting configuration exists to activate\n--!\n--! @example\n--! -- Manually activate configuration (normally done automatically)\n--! SELECT eql_v2.activate_config();\n--!\n--! @see eql_v2.migrate_config\n--! @see eql_v2.add_column\nCREATE FUNCTION eql_v2.activate_config()\n RETURNS boolean\nAS $$\n BEGIN\n\n IF EXISTS (SELECT FROM public.eql_v2_configuration c WHERE c.state = 'encrypting') THEN\n UPDATE public.eql_v2_configuration SET state = 'inactive' WHERE state = 'active';\n UPDATE public.eql_v2_configuration SET state = 'active' WHERE state = 'encrypting';\n RETURN true;\n ELSE\n RAISE EXCEPTION 'No encrypting configuration exists to activate';\n END IF;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Discard pending configuration\n--!\n--! Deletes the pending configuration without applying changes. Use this to\n--! abandon configuration changes before they are migrated and activated.\n--!\n--! @return Boolean True if discard succeeds\n--! @throws Exception if no pending configuration exists to discard\n--!\n--! @example\n--! -- Discard uncommitted configuration changes\n--! SELECT eql_v2.discard();\n--!\n--! @see eql_v2.add_column\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.discard()\n RETURNS boolean\nAS $$\n BEGIN\n IF EXISTS (SELECT FROM public.eql_v2_configuration c WHERE c.state = 'pending') THEN\n DELETE FROM public.eql_v2_configuration WHERE state = 'pending';\n RETURN true;\n ELSE\n RAISE EXCEPTION 'No pending configuration exists to discard';\n END IF;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Configure a column for encryption\n--!\n--! Adds a column to the encryption configuration, making it eligible for\n--! encrypted storage and search indexes. Creates or updates pending configuration,\n--! adds encrypted constraint, then migrates and activates unless migrating flag is set.\n--!\n--! @param table_name Text Name of the table containing the column\n--! @param column_name Text Name of the column to encrypt\n--! @param cast_as Text PostgreSQL type to cast decrypted values (default: 'text')\n--! @param migrating Boolean Skip auto-migration if true (default: false)\n--! @return JSONB Updated configuration object\n--! @throws Exception if column already configured for encryption\n--!\n--! @example\n--! -- Configure email column for encryption\n--! SELECT eql_v2.add_column('users', 'email', 'text');\n--!\n--! -- Configure age column with integer casting\n--! SELECT eql_v2.add_column('users', 'age', 'int');\n--!\n--! @see eql_v2.add_search_config\n--! @see eql_v2.remove_column\nCREATE FUNCTION eql_v2.add_column(table_name text, column_name text, cast_as text DEFAULT 'text', migrating boolean DEFAULT false)\n RETURNS jsonb\nAS $$\n DECLARE\n key text;\n _config jsonb;\n BEGIN\n -- set the active config\n SELECT data INTO _config FROM public.eql_v2_configuration WHERE state = 'active' OR state = 'pending' ORDER BY state DESC;\n\n -- set default config\n SELECT eql_v2.config_default(_config) INTO _config;\n\n -- if index exists\n IF _config #> array['tables', table_name] ? column_name THEN\n RAISE EXCEPTION 'Config exists for column: % %', table_name, column_name;\n END IF;\n\n SELECT eql_v2.config_add_table(table_name, _config) INTO _config;\n\n SELECT eql_v2.config_add_column(table_name, column_name, _config) INTO _config;\n\n SELECT eql_v2.config_add_cast(table_name, column_name, cast_as, _config) INTO _config;\n\n -- create a new pending record if we don't have one\n INSERT INTO public.eql_v2_configuration (state, data) VALUES ('pending', _config)\n ON CONFLICT (state)\n WHERE state = 'pending'\n DO UPDATE\n SET data = _config;\n\n IF NOT migrating THEN\n PERFORM eql_v2.migrate_config();\n PERFORM eql_v2.activate_config();\n END IF;\n\n PERFORM eql_v2.add_encrypted_constraint(table_name, column_name);\n\n -- exeunt\n RETURN _config;\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Remove a column from encryption configuration\n--!\n--! Removes a column from the encryption configuration, including all associated\n--! search indexes. Removes encrypted constraint, updates pending configuration,\n--! then migrates and activates unless migrating flag is set.\n--!\n--! @param table_name Text Name of the table containing the column\n--! @param column_name Text Name of the column to remove\n--! @param migrating Boolean Skip auto-migration if true (default: false)\n--! @return JSONB Updated configuration object\n--! @throws Exception if no active or pending configuration exists\n--! @throws Exception if table is not configured\n--! @throws Exception if column is not configured\n--!\n--! @example\n--! -- Remove email column from encryption\n--! SELECT eql_v2.remove_column('users', 'email');\n--!\n--! @see eql_v2.add_column\n--! @see eql_v2.remove_search_config\nCREATE FUNCTION eql_v2.remove_column(table_name text, column_name text, migrating boolean DEFAULT false)\n RETURNS jsonb\nAS $$\n DECLARE\n key text;\n _config jsonb;\n BEGIN\n -- set the active config\n SELECT data INTO _config FROM public.eql_v2_configuration WHERE state = 'active' OR state = 'pending' ORDER BY state DESC;\n\n -- if no config\n IF _config IS NULL THEN\n RAISE EXCEPTION 'No active or pending configuration exists';\n END IF;\n\n -- if the table doesn't exist\n IF NOT _config #> array['tables'] ? table_name THEN\n RAISE EXCEPTION 'No configuration exists for table: %', table_name;\n END IF;\n\n -- if the column does not exist\n IF NOT _config #> array['tables', table_name] ? column_name THEN\n RAISE EXCEPTION 'No configuration exists for column: % %', table_name, column_name;\n END IF;\n\n -- create a new pending record if we don't have one\n INSERT INTO public.eql_v2_configuration (state, data) VALUES ('pending', _config)\n ON CONFLICT (state)\n WHERE state = 'pending'\n DO NOTHING;\n\n -- remove the column\n SELECT _config #- array['tables', table_name, column_name] INTO _config;\n\n -- if table is now empty, remove the table\n IF _config #> array['tables', table_name] = '{}' THEN\n SELECT _config #- array['tables', table_name] INTO _config;\n END IF;\n\n PERFORM eql_v2.remove_encrypted_constraint(table_name, column_name);\n\n -- update the config (even if empty) and activate\n UPDATE public.eql_v2_configuration SET data = _config WHERE state = 'pending';\n\n IF NOT migrating THEN\n -- For empty configs, skip migration validation and directly activate\n IF _config #> array['tables'] = '{}' THEN\n UPDATE public.eql_v2_configuration SET state = 'inactive' WHERE state = 'active';\n UPDATE public.eql_v2_configuration SET state = 'active' WHERE state = 'pending';\n ELSE\n PERFORM eql_v2.migrate_config();\n PERFORM eql_v2.activate_config();\n END IF;\n END IF;\n\n -- exeunt\n RETURN _config;\n\n END;\n$$ LANGUAGE plpgsql;\n\n--! @brief Reload configuration from CipherStash Proxy\n--!\n--! Placeholder function for reloading configuration from the CipherStash Proxy.\n--! Currently returns NULL without side effects.\n--!\n--! @return Void\n--!\n--! @note This function may be used for configuration synchronization in future versions\nCREATE FUNCTION eql_v2.reload_config()\n RETURNS void\nLANGUAGE sql STRICT PARALLEL SAFE\nBEGIN ATOMIC\n RETURN NULL;\nEND;\n\n--! @brief Query encryption configuration in tabular format\n--!\n--! Returns the active encryption configuration as a table for easier querying\n--! and filtering. Shows all configured tables, columns, cast types, and indexes.\n--!\n--! @return TABLE Contains configuration state, relation name, column name, cast type, and indexes\n--!\n--! @example\n--! -- View all encrypted columns\n--! SELECT * FROM eql_v2.config();\n--!\n--! -- Find all columns with match indexes\n--! SELECT relation, col_name FROM eql_v2.config()\n--! WHERE indexes ? 'match';\n--!\n--! @see eql_v2.add_column\n--! @see eql_v2.add_search_config\nCREATE FUNCTION eql_v2.config() RETURNS TABLE (\n state eql_v2_configuration_state,\n relation text,\n col_name text,\n decrypts_as text,\n indexes jsonb\n)\nAS $$\nBEGIN\n RETURN QUERY\n WITH tables AS (\n SELECT config.state, tables.key AS table, tables.value AS config\n FROM public.eql_v2_configuration config, jsonb_each(data->'tables') tables\n WHERE config.data->>'v' = '1'\n )\n SELECT\n tables.state,\n tables.table,\n column_config.key,\n column_config.value->>'cast_as',\n column_config.value->'indexes'\n FROM tables, jsonb_each(tables.config) column_config;\nEND;\n$$ LANGUAGE plpgsql;\n\n--! @file config/constraints.sql\n--! @brief Configuration validation functions and constraints\n--!\n--! Provides CHECK constraint functions to validate encryption configuration structure.\n--! Ensures configurations have required fields (version, tables) and valid values\n--! for index types and cast types before being stored.\n--!\n--! @see config/tables.sql where constraints are applied\n\n\n--! @brief Extract index type names from configuration\n--! @internal\n--!\n--! Helper function that extracts all index type names from the configuration's\n--! 'indexes' sections across all tables and columns.\n--!\n--! @param jsonb Configuration data to extract from\n--! @return SETOF text Index type names (e.g., 'match', 'ore', 'unique', 'ste_vec')\n--!\n--! @note Used by config_check_indexes for validation\n--! @see eql_v2.config_check_indexes\nCREATE FUNCTION eql_v2.config_get_indexes(val jsonb)\n RETURNS SETOF text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE\nBEGIN ATOMIC\n SELECT jsonb_object_keys(jsonb_path_query(val,'$.tables.*.*.indexes'));\nEND;\n\n\n--! @brief Validate index types in configuration\n--! @internal\n--!\n--! Checks that all index types specified in the configuration are valid.\n--! Valid index types are: match, ore, unique, ste_vec.\n--!\n--! @param jsonb Configuration data to validate\n--! @return boolean True if all index types are valid\n--! @throws Exception if any invalid index type found\n--!\n--! @note Used in CHECK constraint on eql_v2_configuration table\n--! @see eql_v2.config_get_indexes\nCREATE FUNCTION eql_v2.config_check_indexes(val jsonb)\n RETURNS BOOLEAN\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n BEGIN\n\n IF (SELECT EXISTS (SELECT eql_v2.config_get_indexes(val))) THEN\n IF (SELECT bool_and(index = ANY('{match, ore, unique, ste_vec}')) FROM eql_v2.config_get_indexes(val) AS index) THEN\n RETURN true;\n END IF;\n RAISE 'Configuration has an invalid index (%). Index should be one of {match, ore, unique, ste_vec}', val;\n END IF;\n RETURN true;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Validate cast types in configuration\n--! @internal\n--!\n--! Checks that all 'cast_as' types specified in the configuration are valid.\n--! Valid cast types are: text, int, small_int, big_int, real, double, boolean, date, jsonb.\n--!\n--! @param jsonb Configuration data to validate\n--! @return boolean True if all cast types are valid or no cast types specified\n--! @throws Exception if any invalid cast type found\n--!\n--! @note Used in CHECK constraint on eql_v2_configuration table\n--! @note Empty configurations (no cast_as fields) are valid\n--! @note Cast type names are EQL's internal representations, not PostgreSQL native types\nCREATE FUNCTION eql_v2.config_check_cast(val jsonb)\n RETURNS BOOLEAN\nAS $$\n BEGIN\n -- If there are cast_as fields, validate them\n IF EXISTS (SELECT jsonb_array_elements_text(jsonb_path_query_array(val, '$.tables.*.*.cast_as'))) THEN\n IF (SELECT bool_and(cast_as = ANY('{text, int, small_int, big_int, real, double, boolean, date, jsonb}')) \n FROM (SELECT jsonb_array_elements_text(jsonb_path_query_array(val, '$.tables.*.*.cast_as')) AS cast_as) casts) THEN\n RETURN true;\n END IF;\n RAISE 'Configuration has an invalid cast_as (%). Cast should be one of {text, int, small_int, big_int, real, double, boolean, date, jsonb}', val;\n END IF;\n -- If no cast_as fields exist (empty config), that's valid\n RETURN true;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Validate tables field presence\n--! @internal\n--!\n--! Ensures the configuration has a 'tables' field, which is required\n--! to specify which database tables contain encrypted columns.\n--!\n--! @param jsonb Configuration data to validate\n--! @return boolean True if 'tables' field exists\n--! @throws Exception if 'tables' field is missing\n--!\n--! @note Used in CHECK constraint on eql_v2_configuration table\nCREATE FUNCTION eql_v2.config_check_tables(val jsonb)\n RETURNS boolean\nAS $$\n BEGIN\n IF (val ? 'tables') THEN\n RETURN true;\n END IF;\n RAISE 'Configuration missing tables (tables) field: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Validate version field presence\n--! @internal\n--!\n--! Ensures the configuration has a 'v' (version) field, which tracks\n--! the configuration format version.\n--!\n--! @param jsonb Configuration data to validate\n--! @return boolean True if 'v' field exists\n--! @throws Exception if 'v' field is missing\n--!\n--! @note Used in CHECK constraint on eql_v2_configuration table\nCREATE FUNCTION eql_v2.config_check_version(val jsonb)\n RETURNS boolean\nAS $$\n BEGIN\n IF (val ? 'v') THEN\n RETURN true;\n END IF;\n RAISE 'Configuration missing version (v) field: %', val;\n END;\n$$ LANGUAGE plpgsql;\n\n\n--! @brief Drop existing data validation constraint if present\n--! @note Allows constraint to be recreated during upgrades\nALTER TABLE public.eql_v2_configuration DROP CONSTRAINT IF EXISTS eql_v2_configuration_data_check;\n\n\n--! @brief Comprehensive configuration data validation\n--!\n--! CHECK constraint that validates all aspects of configuration data:\n--! - Version field presence\n--! - Tables field presence\n--! - Valid cast_as types\n--! - Valid index types\n--!\n--! @note Combines all config_check_* validation functions\n--! @see eql_v2.config_check_version\n--! @see eql_v2.config_check_tables\n--! @see eql_v2.config_check_cast\n--! @see eql_v2.config_check_indexes\nALTER TABLE public.eql_v2_configuration\n ADD CONSTRAINT eql_v2_configuration_data_check CHECK (\n eql_v2.config_check_version(data) AND\n eql_v2.config_check_tables(data) AND\n eql_v2.config_check_cast(data) AND\n eql_v2.config_check_indexes(data)\n);\n\n\n\n\n--! @brief Compare two encrypted values using Blake3 hash index terms\n--!\n--! Performs a three-way comparison (returns -1/0/1) of encrypted values using\n--! their Blake3 hash index terms. Used internally by the equality operator (=)\n--! for exact-match queries without decryption.\n--!\n--! @param a eql_v2_encrypted First encrypted value to compare\n--! @param b eql_v2_encrypted Second encrypted value to compare\n--! @return Integer -1 if a < b, 0 if a = b, 1 if a > b\n--!\n--! @note NULL values are sorted before non-NULL values\n--! @note Comparison uses underlying text type ordering of Blake3 hashes\n--!\n--! @see eql_v2.blake3\n--! @see eql_v2.has_blake3\n--! @see eql_v2.\"=\"\nCREATE FUNCTION eql_v2.compare_blake3(a eql_v2_encrypted, b eql_v2_encrypted)\n RETURNS integer\n IMMUTABLE STRICT PARALLEL SAFE\nAS $$\n DECLARE\n a_term eql_v2.blake3;\n b_term eql_v2.blake3;\n BEGIN\n\n IF a IS NULL AND b IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b IS NULL THEN\n RETURN 1;\n END IF;\n\n IF eql_v2.has_blake3(a) THEN\n a_term = eql_v2.blake3(a);\n END IF;\n\n IF eql_v2.has_blake3(b) THEN\n b_term = eql_v2.blake3(b);\n END IF;\n\n IF a_term IS NULL AND b_term IS NULL THEN\n RETURN 0;\n END IF;\n\n IF a_term IS NULL THEN\n RETURN -1;\n END IF;\n\n IF b_term IS NULL THEN\n RETURN 1;\n END IF;\n\n -- Using the underlying text type comparison\n IF a_term = b_term THEN\n RETURN 0;\n END IF;\n\n IF a_term < b_term THEN\n RETURN -1;\n END IF;\n\n IF a_term > b_term THEN\n RETURN 1;\n END IF;\n\n END;\n$$ LANGUAGE plpgsql;\n"
|
|
120
|
+
}],
|
|
121
|
+
"postcheck": [{
|
|
122
|
+
"description": "verify \"eql_v2\" schema exists",
|
|
123
|
+
"sql": "SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'eql_v2')"
|
|
124
|
+
}, {
|
|
125
|
+
"description": "verify \"eql_v2.eql_v2_encrypted\" composite type exists",
|
|
126
|
+
"sql": "SELECT EXISTS (SELECT 1 FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = 'eql_v2' AND t.typname = 'eql_v2_encrypted')"
|
|
127
|
+
}]
|
|
128
|
+
}];
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region migrations/cipherstash/refs/head.json
|
|
131
|
+
var head_default = {
|
|
132
|
+
hash: "sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4",
|
|
133
|
+
invariants: ["cipherstash:install-eql-bundle-v1"]
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/contract/contract.json
|
|
137
|
+
var contract_default = {
|
|
138
|
+
schemaVersion: "1",
|
|
139
|
+
targetFamily: "sql",
|
|
140
|
+
target: "postgres",
|
|
141
|
+
profileHash: "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e",
|
|
142
|
+
roots: { "eql_v2_configuration": "EqlV2Configuration" },
|
|
143
|
+
models: { "EqlV2Configuration": {
|
|
144
|
+
"fields": {
|
|
145
|
+
"data": {
|
|
146
|
+
"nullable": false,
|
|
147
|
+
"type": {
|
|
148
|
+
"codecId": "pg/jsonb@1",
|
|
149
|
+
"kind": "scalar"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"id": {
|
|
153
|
+
"nullable": false,
|
|
154
|
+
"type": {
|
|
155
|
+
"codecId": "pg/text@1",
|
|
156
|
+
"kind": "scalar"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"state": {
|
|
160
|
+
"nullable": false,
|
|
161
|
+
"type": {
|
|
162
|
+
"codecId": "pg/text@1",
|
|
163
|
+
"kind": "scalar"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"relations": {},
|
|
168
|
+
"storage": {
|
|
169
|
+
"fields": {
|
|
170
|
+
"data": { "column": "data" },
|
|
171
|
+
"id": { "column": "id" },
|
|
172
|
+
"state": { "column": "state" }
|
|
173
|
+
},
|
|
174
|
+
"table": "eql_v2_configuration"
|
|
175
|
+
}
|
|
176
|
+
} },
|
|
177
|
+
storage: {
|
|
178
|
+
"storageHash": "sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4",
|
|
179
|
+
"tables": { "eql_v2_configuration": {
|
|
180
|
+
"columns": {
|
|
181
|
+
"data": {
|
|
182
|
+
"codecId": "pg/jsonb@1",
|
|
183
|
+
"nativeType": "jsonb",
|
|
184
|
+
"nullable": false
|
|
185
|
+
},
|
|
186
|
+
"id": {
|
|
187
|
+
"codecId": "pg/text@1",
|
|
188
|
+
"nativeType": "text",
|
|
189
|
+
"nullable": false
|
|
190
|
+
},
|
|
191
|
+
"state": {
|
|
192
|
+
"codecId": "pg/text@1",
|
|
193
|
+
"nativeType": "text",
|
|
194
|
+
"nullable": false
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"foreignKeys": [],
|
|
198
|
+
"indexes": [],
|
|
199
|
+
"primaryKey": { "columns": ["id"] },
|
|
200
|
+
"uniques": []
|
|
201
|
+
} }
|
|
202
|
+
},
|
|
203
|
+
capabilities: {
|
|
204
|
+
"postgres": {
|
|
205
|
+
"jsonAgg": true,
|
|
206
|
+
"lateral": true,
|
|
207
|
+
"limit": true,
|
|
208
|
+
"orderBy": true,
|
|
209
|
+
"returning": true
|
|
210
|
+
},
|
|
211
|
+
"sql": {
|
|
212
|
+
"defaultInInsert": true,
|
|
213
|
+
"enums": true,
|
|
214
|
+
"returning": true
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
extensionPacks: {},
|
|
218
|
+
meta: {},
|
|
219
|
+
_generated: {
|
|
220
|
+
"warning": "⚠️ GENERATED FILE - DO NOT EDIT",
|
|
221
|
+
"message": "This file is automatically generated by \"prisma-next contract emit\".",
|
|
222
|
+
"regenerate": "To regenerate, run: prisma-next contract emit"
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/migration/cipherstash-codec.ts
|
|
227
|
+
const FLAG_TO_INDEX = {
|
|
228
|
+
equality: "unique",
|
|
229
|
+
freeTextSearch: "match"
|
|
230
|
+
};
|
|
231
|
+
const ALL_FLAGS = ["equality", "freeTextSearch"];
|
|
232
|
+
function isEnabled(typeParams, flag) {
|
|
233
|
+
return typeParams !== void 0 && typeParams[flag] === true;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Hook entry point. Called by `planFieldEventOperations` for every per-
|
|
237
|
+
* field delta dispatched to `cipherstash:string@1`. Pure and
|
|
238
|
+
* synchronous; callers replay it deterministically when re-emitting.
|
|
239
|
+
*/
|
|
240
|
+
function onFieldEvent(event, ctx) {
|
|
241
|
+
const { tableName, fieldName, priorField, newField } = ctx;
|
|
242
|
+
if (event === "added") {
|
|
243
|
+
if (newField === void 0) return [];
|
|
244
|
+
const calls = [];
|
|
245
|
+
for (const flag of ALL_FLAGS) if (isEnabled(newField.typeParams, flag)) calls.push(cipherstashAddSearchConfig({
|
|
246
|
+
table: tableName,
|
|
247
|
+
column: fieldName,
|
|
248
|
+
index: FLAG_TO_INDEX[flag]
|
|
249
|
+
}));
|
|
250
|
+
return calls;
|
|
251
|
+
}
|
|
252
|
+
if (event === "dropped") {
|
|
253
|
+
if (priorField === void 0) return [];
|
|
254
|
+
const calls = [];
|
|
255
|
+
for (const flag of ALL_FLAGS) if (isEnabled(priorField.typeParams, flag)) calls.push(cipherstashRemoveSearchConfig({
|
|
256
|
+
table: tableName,
|
|
257
|
+
column: fieldName,
|
|
258
|
+
index: FLAG_TO_INDEX[flag]
|
|
259
|
+
}));
|
|
260
|
+
return calls;
|
|
261
|
+
}
|
|
262
|
+
if (priorField === void 0 || newField === void 0) return [];
|
|
263
|
+
const calls = [];
|
|
264
|
+
for (const flag of ALL_FLAGS) {
|
|
265
|
+
const before = isEnabled(priorField.typeParams, flag);
|
|
266
|
+
const after = isEnabled(newField.typeParams, flag);
|
|
267
|
+
if (after && !before) calls.push(cipherstashAddSearchConfig({
|
|
268
|
+
table: tableName,
|
|
269
|
+
column: fieldName,
|
|
270
|
+
index: FLAG_TO_INDEX[flag]
|
|
271
|
+
}));
|
|
272
|
+
else if (before && !after) calls.push(cipherstashRemoveSearchConfig({
|
|
273
|
+
table: tableName,
|
|
274
|
+
column: fieldName,
|
|
275
|
+
index: FLAG_TO_INDEX[flag]
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
return calls;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The DDL type for an `Encrypted<string>` column is always
|
|
282
|
+
* `eql_v2_encrypted` regardless of any `typeParams` flags: the
|
|
283
|
+
* search-config wiring is delivered by the codec hook's
|
|
284
|
+
* `cipherstashAddSearchConfig` Calls (separate rows in
|
|
285
|
+
* `eql_v2_configuration`), not by the column type itself. Returning
|
|
286
|
+
* `nativeType` unchanged tells the planner "no expansion required" —
|
|
287
|
+
* see `expandParameterizedTypeSql` in
|
|
288
|
+
* `packages/3-targets/3-targets/postgres/src/core/migrations/planner-ddl-builders.ts`,
|
|
289
|
+
* which only requires this hook to *exist* for any column carrying
|
|
290
|
+
* `typeParams`. Without it, the planner refuses to render the column
|
|
291
|
+
* (the existing arktype-json extension wires the same identity hook).
|
|
292
|
+
*/
|
|
293
|
+
const expandNativeType = ({ nativeType }) => nativeType;
|
|
294
|
+
const cipherstashStringCodecHooks = {
|
|
295
|
+
onFieldEvent,
|
|
296
|
+
expandNativeType
|
|
297
|
+
};
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/exports/contract-space-typing.ts
|
|
300
|
+
function fail(field, value) {
|
|
301
|
+
throw new Error(`cipherstash contract-space JSON is missing or malformed at "${field}" (saw ${typeof value}). The on-disk JSON drifted from the framework's expected shape — re-run \`prisma-next contract emit\` and \`prisma-next migration plan\` for the cipherstash space.`);
|
|
302
|
+
}
|
|
303
|
+
function isRecord(value) {
|
|
304
|
+
return typeof value === "object" && value !== null;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Narrow a JSON-imported `contract.json` value to `Contract<SqlStorage>`.
|
|
308
|
+
* Checks the discriminators the framework relies on at descriptor
|
|
309
|
+
* registration time; everything else is consumed downstream by the
|
|
310
|
+
* runner / verifier, which performs its own validation.
|
|
311
|
+
*/
|
|
312
|
+
function asCipherstashContract(value) {
|
|
313
|
+
if (!isRecord(value)) fail("<root>", value);
|
|
314
|
+
if (typeof value["target"] !== "string") fail("target", value["target"]);
|
|
315
|
+
if (typeof value["targetFamily"] !== "string") fail("targetFamily", value["targetFamily"]);
|
|
316
|
+
const storage = value["storage"];
|
|
317
|
+
if (!isRecord(storage)) fail("storage", storage);
|
|
318
|
+
if (typeof storage["storageHash"] !== "string") fail("storage.storageHash", storage["storageHash"]);
|
|
319
|
+
return value;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Narrow a JSON-imported `migration.json` value to `MigrationMetadata`.
|
|
323
|
+
* The framework's runner consumes the metadata for ordering /
|
|
324
|
+
* provenance; missing `to` or a non-string `migrationHash` here means
|
|
325
|
+
* a non-emitted artefact slipped into the import path.
|
|
326
|
+
*/
|
|
327
|
+
function asCipherstashMigrationMetadata(value) {
|
|
328
|
+
if (!isRecord(value)) fail("<root>", value);
|
|
329
|
+
if (typeof value["to"] !== "string") fail("to", value["to"]);
|
|
330
|
+
if (typeof value["migrationHash"] !== "string") fail("migrationHash", value["migrationHash"]);
|
|
331
|
+
return value;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Narrow a JSON-imported `ops.json` value to
|
|
335
|
+
* `readonly MigrationPlanOperation[]`. Checks each entry carries the
|
|
336
|
+
* canonical `id` / `operationClass` discriminator so a malformed entry
|
|
337
|
+
* doesn't reach the planner.
|
|
338
|
+
*/
|
|
339
|
+
function asCipherstashMigrationOps(value) {
|
|
340
|
+
if (!Array.isArray(value)) fail("<root>", value);
|
|
341
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
342
|
+
const entry = value[index];
|
|
343
|
+
if (!isRecord(entry)) fail(`[${index}]`, entry);
|
|
344
|
+
if (typeof entry["id"] !== "string") fail(`[${index}].id`, entry["id"]);
|
|
345
|
+
if (typeof entry["operationClass"] !== "string") fail(`[${index}].operationClass`, entry["operationClass"]);
|
|
346
|
+
}
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/exports/control.ts
|
|
351
|
+
/**
|
|
352
|
+
* Control-plane descriptor for the CipherStash extension.
|
|
353
|
+
*
|
|
354
|
+
* **On-disk-in-package authoring.** The extension's contract +
|
|
355
|
+
* migrations are emitted by the same pipeline application authors use:
|
|
356
|
+
*
|
|
357
|
+
* `prisma-next contract emit` → `<package>/src/contract/contract.{json,d.ts}`
|
|
358
|
+
* `prisma-next migration plan` → `<package>/migrations/cipherstash/<dir>/...`
|
|
359
|
+
*
|
|
360
|
+
* The descriptor wires those JSON artefacts via JSON-import declarations
|
|
361
|
+
* so they flow through the consuming application's module resolver
|
|
362
|
+
* without filesystem assumptions, and synthesises the canonical
|
|
363
|
+
* {@link import('@prisma-next/migration-tools/package').MigrationPackage}
|
|
364
|
+
* shape (gaining `dirPath` from `import.meta.url`) for the framework's
|
|
365
|
+
* runner / verifier to consume.
|
|
366
|
+
*
|
|
367
|
+
* Wired surfaces:
|
|
368
|
+
*
|
|
369
|
+
* - `contractSpace.{contractJson,migrations,headRef}` — sourced from
|
|
370
|
+
* the on-disk artefacts emitted by `build:contract-space`.
|
|
371
|
+
* - `types.codecTypes.controlPlaneHooks[CIPHERSTASH_STRING_CODEC_ID]`
|
|
372
|
+
* — the lifecycle hook the SQL planner extracts via
|
|
373
|
+
* `extractCodecControlHooks` and inlines into the application's
|
|
374
|
+
* migration via `planFieldEventOperations`. Implements
|
|
375
|
+
* `add_search_config` / `remove_search_config` / rotate behaviour
|
|
376
|
+
* for `searchable: true` `Encrypted<string>` columns.
|
|
377
|
+
*
|
|
378
|
+
* @see docs/architecture docs/adrs/ADR 211 - Contract spaces.md
|
|
379
|
+
* (on-disk-in-package authoring convention).
|
|
380
|
+
* @see packages/3-extensions/test-contract-space/src/exports/control.ts
|
|
381
|
+
* (reference model).
|
|
382
|
+
*/
|
|
383
|
+
/**
|
|
384
|
+
* Resolve a migration package's on-disk path from this descriptor module's
|
|
385
|
+
* URL. The framework's runner uses `dirPath` for diagnostic messages and
|
|
386
|
+
* to locate sibling files (e.g. `start-contract.json` for non-baseline
|
|
387
|
+
* migrations); pinning it from `import.meta.url` keeps the value correct
|
|
388
|
+
* regardless of where the consuming application installs the package
|
|
389
|
+
* (workspace, node_modules, bundled, etc.).
|
|
390
|
+
*/
|
|
391
|
+
function resolveMigrationDirPath(dirName) {
|
|
392
|
+
return fileURLToPath(new URL(`../../migrations/${CIPHERSTASH_SPACE_ID}/${dirName}/`, import.meta.url));
|
|
393
|
+
}
|
|
394
|
+
const baselinePackage = {
|
|
395
|
+
dirName: CIPHERSTASH_BASELINE_MIGRATION_NAME,
|
|
396
|
+
dirPath: resolveMigrationDirPath(CIPHERSTASH_BASELINE_MIGRATION_NAME),
|
|
397
|
+
metadata: asCipherstashMigrationMetadata(migration_default),
|
|
398
|
+
ops: asCipherstashMigrationOps(ops_default)
|
|
399
|
+
};
|
|
400
|
+
const cipherstashContractSpace = {
|
|
401
|
+
contractJson: asCipherstashContract(contract_default),
|
|
402
|
+
migrations: [baselinePackage],
|
|
403
|
+
headRef: head_default
|
|
404
|
+
};
|
|
405
|
+
const cipherstashExtensionDescriptor = {
|
|
406
|
+
...cipherstashPackMeta,
|
|
407
|
+
contractSpace: cipherstashContractSpace,
|
|
408
|
+
/**
|
|
409
|
+
* Free-form `types.codecTypes.controlPlaneHooks` block — the SQL
|
|
410
|
+
* family's `extractCodecControlHooks` (in `@prisma-next/family-sql/
|
|
411
|
+
* control`) finds hooks via duck-typing on this exact path. Mirrors
|
|
412
|
+
* pgvector's wiring at `packages/3-extensions/pgvector/src/exports/
|
|
413
|
+
* control.ts`.
|
|
414
|
+
*/
|
|
415
|
+
types: {
|
|
416
|
+
...cipherstashPackMeta.types,
|
|
417
|
+
codecTypes: {
|
|
418
|
+
...cipherstashPackMeta.types.codecTypes,
|
|
419
|
+
controlPlaneHooks: { [CIPHERSTASH_STRING_CODEC_ID]: cipherstashStringCodecHooks }
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
create: () => ({
|
|
423
|
+
familyId: "sql",
|
|
424
|
+
targetId: "postgres"
|
|
425
|
+
})
|
|
426
|
+
};
|
|
427
|
+
//#endregion
|
|
428
|
+
export { cipherstashExtensionDescriptor, cipherstashExtensionDescriptor as default };
|
|
429
|
+
|
|
430
|
+
//# sourceMappingURL=control.mjs.map
|