diff --git a/content/docs/get-started/quickstart.mdx b/content/docs/get-started/quickstart.mdx
index 999ff0d..37a43d9 100644
--- a/content/docs/get-started/quickstart.mdx
+++ b/content/docs/get-started/quickstart.mdx
@@ -96,12 +96,12 @@ Prefer to have an agent do this? `npx stash plan` inspects your project and draf
## Create the encrypted column
-An encrypted column is typed with an EQL domain. The domain you pick has to match the capability you declared: `.equality()` on a text column means `public.text_eq`.
+An encrypted column is typed with an EQL domain. The domain you pick has to match the capability you declared: `.equality()` on a text column means `public.eql_v3_text_eq`.
```sql filename="schema.sql"
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- email public.text_eq
+ email public.eql_v3_text_eq
);
```
@@ -151,12 +151,12 @@ if (term.failure) {
}
const rows = await db.query(
- "SELECT id, email FROM users WHERE email = $1::public.text_eq",
+ "SELECT id, email FROM users WHERE email = $1::public.eql_v3_text_eq",
[term.data],
)
```
-The cast matters. An encrypted operator only resolves against a **typed operand**, so `$1::public.text_eq` is what tells Postgres to compare encrypted terms rather than fall back to raw `jsonb` semantics. See [typed operands](/reference/eql/core-concepts).
+The cast matters. An encrypted operator only resolves against a **typed operand**, so `$1::public.eql_v3_text_eq` is what tells Postgres to compare encrypted terms rather than fall back to raw `jsonb` semantics. See [typed operands](/reference/eql/core-concepts).
Postgres compares ciphertext against ciphertext. It never sees either plaintext.
diff --git a/content/docs/reference/eql/booleans.mdx b/content/docs/reference/eql/booleans.mdx
index 294a846..22f0794 100644
--- a/content/docs/reference/eql/booleans.mdx
+++ b/content/docs/reference/eql/booleans.mdx
@@ -1,6 +1,6 @@
---
title: Booleans
-description: "Encrypted booleans are storage-only by design: public.boolean stores and decrypts, carries no index terms, and blocks every comparison."
+description: "Encrypted booleans are storage-only by design: public.eql_v3_boolean stores and decrypts, carries no index terms, and blocks every comparison."
type: reference
components: [eql]
verifiedAgainst:
@@ -9,7 +9,7 @@ verifiedAgainst:
-Every scalar type has a storage-only variant — for `bool` it's the only one. EQL ships `public.boolean` and nothing else: there is no `bool_eq` and no `bool_ord`. An encrypted boolean column can be stored, decrypted, and null-checked; it cannot be filtered, sorted, grouped, or joined on.
+Every scalar type has a storage-only variant — for `bool` it's the only one. EQL ships `public.eql_v3_boolean` and nothing else: there is no `bool_eq` and no `bool_ord`. An encrypted boolean column can be stored, decrypted, and null-checked; it cannot be filtered, sorted, grouped, or joined on.
## Why there are no query variants
@@ -17,11 +17,11 @@ A two-value column has too little cardinality for any searchable index to be saf
## What works, what raises
-`public.boolean` follows the bare-variant contract described in [Core concepts](/reference/eql/core-concepts#variants-declare-capability): it carries no index terms, so `IS NULL` / `IS NOT NULL` are the only predicates that work. Every comparison operator routes to a blocker and raises — the [fail-loud behavior](/reference/eql/core-concepts#unsupported-operations-fail-loudly) shared by all encrypted variants:
+`public.eql_v3_boolean` follows the bare-variant contract described in [Core concepts](/reference/eql/core-concepts#variants-declare-capability): it carries no index terms, so `IS NULL` / `IS NOT NULL` are the only predicates that work. Every comparison operator routes to a blocker and raises — the [fail-loud behavior](/reference/eql/core-concepts#unsupported-operations-fail-loudly) shared by all encrypted variants:
```sql
--- ❌ Raises: operator = is not supported for public.boolean
-SELECT * FROM users WHERE is_active = $1::public.boolean;
+-- ❌ Raises: operator = is not supported for public.eql_v3_boolean
+SELECT * FROM users WHERE is_active = $1::public.eql_v3_boolean;
-- ✅ Works: NULL columns are not encrypted
SELECT * FROM users WHERE is_active IS NOT NULL;
@@ -34,16 +34,16 @@ Query on other columns, decrypt the boolean in your application, and filter ther
```sql
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- email public.text_eq, -- exact lookup
- created_at public.timestamp_ord, -- range queries, ORDER BY
- is_active public.boolean -- storage only (by design)
+ email public.eql_v3_text_eq, -- exact lookup
+ created_at public.eql_v3_timestamp_ord, -- range queries, ORDER BY
+ is_active public.eql_v3_boolean -- storage only (by design)
);
```
```sql
-- Narrow the result set with the columns that do carry index terms…
SELECT id, email, is_active FROM users
-WHERE created_at >= $1::public.timestamp_ord;
+WHERE created_at >= $1::public.eql_v3_timestamp_ord;
-- …then decrypt is_active in the client and filter on the plaintext.
```
@@ -53,12 +53,12 @@ If a boolean genuinely needs to be a server-side predicate, that is a data-model
## Storing without querying
-`bool` is the forced case of a pattern available to every scalar type: the bare variant `public.` (for example `public.integer`, `public.text`, `public.timestamp`) is storage-and-decryption only. It carries no index terms, and every comparison operator raises — use it for columns you only ever store and decrypt, so the database holds no searchable material for them at all.
+`bool` is the forced case of a pattern available to every scalar type: the bare variant `public.eql_v3_` (for example `public.eql_v3_integer`, `public.eql_v3_text`, `public.eql_v3_timestamp`) is storage-and-decryption only. It carries no index terms, and every comparison operator raises — use it for columns you only ever store and decrypt, so the database holds no searchable material for them at all.
For every type other than `bool`, storage-only is a choice you can walk back. If you later need to query, retype the column as a query variant — or, if the payloads already carry the needed term (the client decides which terms travel in the payload), cast at the call site:
```sql
-SELECT * FROM readings WHERE value::public.integer_ord > $1::public.integer_ord;
+SELECT * FROM readings WHERE value::public.eql_v3_integer_ord > $1::public.eql_v3_integer_ord;
```
The variant families and what each one enables are covered in [Core concepts](/reference/eql/core-concepts); the per-type specifics live in [Numbers](/reference/eql/numbers), [Dates & times](/reference/eql/dates-and-times), and [Text](/reference/eql/text).
diff --git a/content/docs/reference/eql/core-concepts.mdx b/content/docs/reference/eql/core-concepts.mdx
index 1445ac8..2b6ada8 100644
--- a/content/docs/reference/eql/core-concepts.mdx
+++ b/content/docs/reference/eql/core-concepts.mdx
@@ -21,46 +21,56 @@ For any scalar type ``, the family looks like this:
| Domain variant | Capability |
| --- | --- |
-| `public.` | Storage and decryption only. |
-| `public._eq` | Equality: `=`, `<>`, `IN`, `GROUP BY`, `DISTINCT`, equijoins. |
-| `public._ord` | Comparisons (`<` … `>=`), `BETWEEN`, `ORDER BY`, `MIN` / `MAX` — plus equality. |
-| `public._ord_ore` | As `_ord`, with the ORE mechanism pinned — see [SEM specifiers](#sem-specifiers). |
-| `public.text_match` (text only) | Free-text token containment: `@>` / `<@`. |
-| `public.text_search` (text only) | Equality + ordering + token containment. |
+| `public.eql_v3_` | Storage and decryption only. |
+| `public.eql_v3__eq` | Equality: `=`, `<>`, `IN`, `GROUP BY`, `DISTINCT`, equijoins. |
+| `public.eql_v3__ord` | Comparisons (`<` … `>=`), `BETWEEN`, `ORDER BY`, `MIN` / `MAX` — plus equality. |
+| `public.eql_v3__ord_ope` | The byte-identical twin of `_ord`, with OPE pinned. See [SEM specifiers](#sem-specifiers). |
+| `public.eql_v3__ord_ore` | As `_ord`, with block-ORE pinned. |
+| `public.eql_v3_text_match` (text only) | Free-text token containment: `@>` / `<@`. |
+| `public.eql_v3_text_search` (text only) | Equality + ordering + token containment. |
+| `public.eql_v3_text_search_ore` (text only) | As `text_search`, with block-ORE pinned. |
+
+Every public domain name carries the `eql_v3_` prefix. It keeps EQL's types from shadowing built-in Postgres type names such as `text` and `json`, and gives each EQL version its own column-type namespace so two versions can coexist. Query-operand domains live in the versioned `eql_v3` schema already, so they are unprefixed: `eql_v3.query_text_eq`, `eql_v3.query_jsonb`.
Two things worth calling out:
-- **The bare variant blocks everything.** `public.` carries no index term. Querying it with any comparison operator raises an "operator not supported" exception. Use it for columns you only ever store and decrypt — [Booleans](/reference/eql/booleans) covers this pattern in full.
+- **The bare variant blocks everything.** `public.eql_v3_` carries no index term. Querying it with any comparison operator raises an "operator not supported" exception. Use it for columns you only ever store and decrypt — [Booleans](/reference/eql/booleans) covers this pattern in full.
- **Which index term backs each capability** is an implementation detail of the payload — covered in [Anatomy of an encrypted value](#anatomy-of-an-encrypted-value) below.
### SEM specifiers
-A trailing mechanism suffix — the `_ore` in `_ord_ore` — is a **SEM specifier**: it pins *which* searchable-encryption mechanism implements the capability, rather than just declaring the capability itself.
+A trailing mechanism suffix — the `_ope` in `_ord_ope` — is a **SEM specifier**: it pins *which* searchable-encryption mechanism implements the capability, rather than just declaring the capability itself.
+
+| Variant | Mechanism | Term | Extractor |
+| --- | --- | --- | --- |
+| `_ord` | CLLW OPE (the default) | `op` | `eql_v3.ord_term(col)` |
+| `_ord_ope` | CLLW OPE, pinned. Byte-identical to `_ord` | `op` | `eql_v3.ord_term(col)` |
+| `_ord_ore` | Block-ORE, pinned | `ob` | `eql_v3.ord_term_ore(col)` |
+| `text_search` | CLLW OPE for its ordering term | `hm`, `op`, `bf` | `eq_term` / `ord_term` / `match_term` |
+| `text_search_ore` | Block-ORE for its ordering term | `hm`, `ob`, `bf` | `eq_term` / `ord_term_ore` / `match_term` |
-- `public._ord` declares *orderable* and leaves the mechanism to EQL's default.
-- `public._ord_ore` declares *orderable via ORE* (order-revealing encryption), explicitly. Its term is `ob`, extracted with `eql_v3.ord_term`.
-- `public._ord_ope` declares *orderable via OPE* (order-preserving encryption), explicitly. Its term is `op`, extracted with `eql_v3.ord_ope_term`.
+The two mechanisms differ in what they demand of the database, not in the capability they declare. `eql_v3_internal.ope_cllw` is a domain over `bytea`, so an ordered functional index on `eql_v3.ord_term(col)` binds `bytea_ops`, the base type's **default** operator class. It works anywhere you can `CREATE INDEX`, with no superuser.
-The two mechanisms differ in what they demand of the database, not in the capability they declare. An OPE term is a `bytea` domain that orders under Postgres's **default** btree operator class, so a functional index on it works anywhere you can `CREATE INDEX`. An ORE term needs a custom operator class and family, which managed platforms frequently block.
+Block-ORE's operator class is hand-written for a composite type and needs superuser to create.
-The mechanism behind the unpinned `public._ord` is **changing to OPE** before EQL 3.0.0 stabilizes, so that ordering works on every platform out of the box. ORE remains available, pinned explicitly as `_ord_ore`, on databases that permit operator class and family creation. Pin a specifier if you need to freeze a column's mechanism; leave it off to track the default.
+On a database where EQL cannot create that operator class (cloud Supabase and most managed Postgres), the installer **disables every ORE-backed domain**. The types still exist, but a `CHECK` constraint rejects the first value written to one, raising `feature_not_supported` and naming the alternative to use. That is deliberate: installing them anyway leaves `<` and `>` running as unindexable sequential scans while `CREATE INDEX ... (eql_v3.ord_term_ore(col))` fails with an opaque Postgres error. Failing loudly on the first write beats degrading silently.
-Each type page lists its available specifiers under an "SEM specifiers" heading.
+Use `_ord` unless you have a specific reason to pin block-ORE, and pin `_ord_ope` when you want a column's mechanism frozen against a future default change. Each type page lists its available specifiers under an "SEM specifiers" heading.
Declaring a table is just typing each column as the variant it needs:
```sql
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- email public.text_eq, -- equality only
- salary public.integer_ord, -- equality + range + ORDER BY
- created_at public.timestamp_ord
+ email public.eql_v3_text_eq, -- equality only
+ salary public.eql_v3_integer_ord, -- equality + range + ORDER BY
+ created_at public.eql_v3_timestamp_ord
);
```
-Every scalar type — `int2`, `int4`, `int8`, `numeric`, `float4`, `float8`, `date`, `timestamp`, `text`, and `bool` in EQL 3.0.0 — ships some subset of this family. The per-category pages list exactly which variants each type has and how to choose between them: [Numbers](/reference/eql/numbers), [Dates & times](/reference/eql/dates-and-times), [Text](/reference/eql/text), and [Booleans](/reference/eql/booleans). Encrypted JSON documents use a separate domain, `public.json`, with its own operator surface — see [JSON](/reference/eql/json).
+Every scalar type — `int2`, `int4`, `int8`, `numeric`, `float4`, `float8`, `date`, `timestamp`, `text`, and `bool` in EQL 3.0.0 — ships some subset of this family. The per-category pages list exactly which variants each type has and how to choose between them: [Numbers](/reference/eql/numbers), [Dates & times](/reference/eql/dates-and-times), [Text](/reference/eql/text), and [Booleans](/reference/eql/booleans). Encrypted JSON documents use a separate domain, `public.eql_v3_json`, with its own operator surface — see [JSON](/reference/eql/json).
## The three schemas
@@ -68,11 +78,11 @@ EQL spreads its surface across three PostgreSQL schemas, and the split is what m
| Schema | Holds | Do you call it? |
| --- | --- | --- |
-| `public` | The encrypted **domain types** — every `public.` variant you type a column as. | Referenced in your table DDL. |
+| `public` | The encrypted **domain types** — every `public.eql_v3_` variant you type a column as. | Referenced in your table DDL. |
| `eql_v3` | All **user-callable functions and operators** — the searchable-encryption API (`eql_v3.eq_term`, `eql_v3.jsonb_path_query`, the encrypted `=` / `<` / `@>` operators, `eql_v3.version()`). | Yes — this is the API. |
| `eql_v3_internal` | The **implementation functions** the domains and operators are built from. | No — never call these directly. |
-**Why the types live in `public`.** Your columns are typed as `public.integer_ord`, `public.text_eq`, and so on — never `eql_v3.*`. Keeping the types in the unversioned `public` schema means an EQL upgrade never rewrites your table definitions: the logic ships in a *versioned* schema (`eql_v3` today, a future `eql_v4` alongside it tomorrow) while the type names your schema depends on stay put. `eql_v2` was removed wholesale in 3.0.0 without any `public.*` type changing.
+**Why the types live in `public`, and carry a version prefix.** Your columns are typed as `public.eql_v3_integer_ord`, `public.eql_v3_text_eq`, and so on — never `eql_v3.*`. Types stay in `public` so a column's type resolves without `search_path` games. The `eql_v3_` prefix does two jobs: it stops EQL's types shadowing Postgres built-ins, since `text`, `json`, and `integer` already resolve in `public`, and it gives each EQL generation its own column-type namespace, so `eql_v3_text_eq` and a future `eql_v4_text_eq` can sit side by side in one database while you migrate table by table.
**Why `eql_v3` is versioned.** The schema name encodes the major API version and is itself part of the public contract — a breaking change introduces a new `eql_vN` schema *beside* the old one rather than mutating it, so you migrate on your own timeline. Everything in `eql_v3` is fair game to call.
@@ -107,13 +117,13 @@ Alongside the envelope, a payload carries the index terms for its column's capab
| Key | SEM type | Wire shape | Enables | Reveals |
| --- | --- | --- | --- | --- |
| `hm` | `eql_v3_internal.hmac_256` (domain over `text`) | Hex string (HMAC-SHA-256) | `=`, `<>` on `_eq` and `text_search` domains | Whether two values are equal — nothing else |
-| `ob` | `eql_v3_internal.ore_block_256` (composite: array of `bytea` block terms) | Array of hex-encoded ORE blocks (block count varies by scalar width) | `<`, `<=`, `>`, `>=`, `ORDER BY` on `_ord` / `_ord_ore` domains — and `=` / `<>`, since ORE comparison collapses to equality | The relative order of two values |
-| `op` | `eql_v3_internal.ope_cllw` (domain over `bytea`) | Hex-encoded CLLW OPE ciphertext | `<`, `<=`, `>`, `>=`, `ORDER BY` on `_ord_ope` domains, and on String / Number leaves of `public.json` | The relative order of two values |
+| `op` | `eql_v3_internal.ope_cllw` (domain over `bytea`) | Hex-encoded CLLW OPE ciphertext | `<`, `<=`, `>`, `>=`, `ORDER BY` on `_ord` / `_ord_ope` domains and `text_search`, and on String / Number leaves of `public.eql_v3_json` | The relative order of two values |
+| `ob` | `eql_v3_internal.ore_block_256` (composite: array of `bytea` block terms) | Array of hex-encoded ORE blocks (block count varies by scalar width) | The same comparisons on the pinned `_ord_ore` and `text_search_ore` domains — and `=` / `<>`, since ORE comparison collapses to equality | The relative order of two values |
| `bf` | `eql_v3_internal.bloom_filter` (domain over `smallint[]`) | Array of set bit positions (**signed** 16-bit — large filters emit negative positions) | `@>` / `<@` token containment on `_match` domains | Probabilistic token overlap between values |
-The capability is encoded as **required keys**: the payload for a `public.text_eq` column must carry `hm`; a `public.integer_ord` payload must carry `ob` (and only `ob`); a `text_match` payload must carry `bf`; a `text_search` payload carries all three. A payload missing its term key fails the domain `CHECK` — and fails to deserialize in the client bindings.
+The capability is encoded as **required keys**: the payload for a `public.eql_v3_text_eq` column must carry `hm`; a `public.eql_v3_integer_ord` payload must carry `op` (and only `op`); a `text_match` payload must carry `bf`; a `text_search` payload carries `hm`, `op`, and `bf`. A payload missing its term key fails the domain `CHECK` — and fails to deserialize in the client bindings.
-A scalar payload for a `public.text_search` column (lookup + ordering + free-text match, so all three terms are required):
+A scalar payload for a `public.eql_v3_text_search` column (lookup + ordering + free-text match, so all three terms are required):
```json
{
@@ -150,10 +160,10 @@ The `eql_v3` domains are backed by `jsonb`. When an operand has no known type
SELECT * FROM users WHERE email = $1;
-- ✅ Right: typed operand — the encrypted `=` resolves.
-SELECT * FROM users WHERE email = $1::public.text_eq;
+SELECT * FROM users WHERE email = $1::public.eql_v3_text_eq;
```
-Always type the operand: a typed parameter (`$1::public.text_eq`) or an explicit cast (`'…'::public.integer_ord`). The [Stack SDK](/reference/stack) and [CipherStash Proxy](/reference/proxy) type bound parameters automatically — raw SQL must do it by hand.
+Always type the operand: a typed parameter (`$1::public.eql_v3_text_eq`) or an explicit cast (`'…'::public.eql_v3_integer_ord`). The [Stack SDK](/reference/stack) and [CipherStash Proxy](/reference/proxy) type bound parameters automatically — raw SQL must do it by hand.
This is the one place where a mistake is *silent*. Everything else fails loudly:
@@ -162,17 +172,19 @@ This is the one place where a mistake is *silent*. Everything else fails loudly:
Unsupported operators are not silent no-ops. Every operator that a variant doesn't support is still *defined* — it routes to a blocker function that raises an `operator … is not supported` exception. A mis-typed query fails loudly instead of silently returning wrong results:
```sql
--- salary is public.bigint_eq (equality only)
-SELECT * FROM users WHERE salary > $1::public.bigint_eq;
--- ERROR: operator > is not supported for public.bigint_eq
+-- salary is public.eql_v3_bigint_eq (equality only)
+SELECT * FROM users WHERE salary > $1::public.eql_v3_bigint_eq;
+-- ERROR: operator > is not supported for public.eql_v3_bigint_eq
```
A `NULL` operand still raises — the blockers are deliberately not `STRICT`, so PostgreSQL can't skip the check. (A SQL `NULL` column value is not encrypted, so `IS NULL` / `IS NOT NULL` themselves always work, on every variant.)
`LIKE` and `ILIKE` are blocked on **every** encrypted variant — pattern matching is meaningless on ciphertext. Encrypted text matching is bloom-filter token containment instead; [Text](/reference/eql/text) covers it.
-One equality subtlety follows from the term table above: on `_ord` / `_ord_ore` columns, `=` and `<>` compare the **ORE (`ob`) term** — ORE comparison collapses to equality — so `_ord` payloads carry no `hm` term at all. On `_eq` and `text_search` columns, equality compares the HMAC (`hm`) term.
+One equality subtlety follows from the term table above, and it splits on whether the column is text.
+
+On the non-text scalars, ordering is equality-lossless: `=` and `<>` on an `_ord` column compare the **ordering term** (`op`, or `ob` on `_ord_ore`), so those payloads carry no `hm` term at all and get equality for free. On text, ordering is *not* equality-lossless, so every orderable text variant carries `hm` alongside its ordering term and resolves `=` and `<>` against it. `_eq` columns always compare `hm`.
## What the terms reveal
-Every index term a value carries is extra material stored in the database, and each term class reveals defined structure to an observer who can read the stored payloads: equality terms reveal *value repetition* (which rows share a value), ORE terms reveal *ordering* (which of two values is larger), and bloom terms reveal *probabilistic token overlap*. None of them reveal the plaintext — but you should only carry the terms you actually query on. The full analysis of what each term does and doesn't leak is in [Searchable encryption](/concepts/searchable-encryption).
+Every index term a value carries is extra material stored in the database, and each term class reveals defined structure to an observer who can read the stored payloads: equality terms reveal *value repetition* (which rows share a value), ordering terms reveal *ordering* (which of two values is larger), and bloom terms reveal *probabilistic token overlap*. None of them reveal the plaintext — but you should only carry the terms you actually query on. The full analysis of what each term does and doesn't leak is in [Searchable encryption](/concepts/searchable-encryption).
diff --git a/content/docs/reference/eql/dates-and-times.mdx b/content/docs/reference/eql/dates-and-times.mdx
index ea8f0a9..659dce1 100644
--- a/content/docs/reference/eql/dates-and-times.mdx
+++ b/content/docs/reference/eql/dates-and-times.mdx
@@ -1,6 +1,6 @@
---
title: Dates & times
-description: "The complete reference for encrypted date and timestamp columns: the domain variants, the ORE-backed payload, and time-window, newest-first, and MIN/MAX queries."
+description: "The complete reference for encrypted date and timestamp columns: the domain variants, the payload they carry, and time-window, newest-first, and MIN/MAX queries."
type: reference
components: [eql]
verifiedAgainst:
@@ -17,17 +17,20 @@ Both types generate the same `jsonb`-backed domain variants. The generic form:
| Domain variant | Capability |
| --- | --- |
-| `public.` | Storage and decryption only. |
-| `public._eq` | Equality: `=`, `<>`, `IN`, `GROUP BY`, `DISTINCT`, equijoins. |
-| `public._ord` | Comparisons, `BETWEEN`, `ORDER BY`, `MIN` / `MAX` — plus equality. |
-| `public._ord_ore` | As `_ord`, with the ORE mechanism pinned — see [SEM specifiers](#sem-specifiers). |
+| `public.eql_v3_` | Storage and decryption only. |
+| `public.eql_v3__eq` | Equality: `=`, `<>`, `IN`, `GROUP BY`, `DISTINCT`, equijoins. |
+| `public.eql_v3__ord` | Comparisons, `BETWEEN`, `ORDER BY`, `MIN` / `MAX` — plus equality. |
+| `public.eql_v3__ord_ope` | The byte-identical twin of `_ord`, with OPE pinned. See [SEM specifiers](#sem-specifiers). |
+| `public.eql_v3__ord_ore` | As `_ord`, with the ORE mechanism pinned. |
+
+`` is the encrypted type name from the table below, so `public.eql_v3__ord` is `public.eql_v3_timestamp_ord` for `timestamp`.
And every concrete domain this page covers:
| Type | Variants |
| --- | --- |
-| `date` | `public.date` · `public.date_eq` · `public.date_ord` · `public.date_ord_ore` |
-| `timestamp` | `public.timestamp` · `public.timestamp_eq` · `public.timestamp_ord` · `public.timestamp_ord_ore` |
+| `date` | `public.eql_v3_date` · `public.eql_v3_date_eq` · `public.eql_v3_date_ord` · `public.eql_v3_date_ord_ope` · `public.eql_v3_date_ord_ore` |
+| `timestamp` | `public.eql_v3_timestamp` · `public.eql_v3_timestamp_eq` · `public.eql_v3_timestamp_ord` · `public.eql_v3_timestamp_ord_ope` · `public.eql_v3_timestamp_ord_ore` |
Time columns are nearly always ranged and sorted, so `_ord` is the usual choice. Declare only the capability you query on — each capability stores extra searchable material with defined leakage (see [Searchable encryption](/concepts/searchable-encryption)), and the variant model itself is covered in [Core concepts](/reference/eql/core-concepts).
@@ -38,9 +41,9 @@ An audit-events table where the timestamps drive time-window queries and sorting
```sql
CREATE TABLE audit_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- occurred_at public.timestamp_ord, -- time windows, newest-first, MIN/MAX
- review_due public.date_ord, -- range filters
- sealed_on public.date -- store and decrypt only
+ occurred_at public.eql_v3_timestamp_ord, -- time windows, newest-first, MIN/MAX
+ review_due public.eql_v3_date_ord, -- range filters
+ sealed_on public.eql_v3_date -- store and decrypt only
);
```
@@ -48,36 +51,37 @@ CREATE TABLE audit_events (
Both types take the same mechanism specifiers on their orderable variant (the concept is defined in [Core concepts](/reference/eql/core-concepts#sem-specifiers)):
-| Specifier | Meaning |
-| --- | --- |
-| `_ord` | Orderable, using EQL's default mechanism (currently ORE). |
-| `_ord_ore` | Orderable via ORE, pinned explicitly. |
+| Specifier | Mechanism | Ordering term | Extractor |
+| --- | --- | --- | --- |
+| `_ord` | The default, currently CLLW OPE | `op` | `eql_v3.ord_term` |
+| `_ord_ope` | CLLW OPE, pinned explicitly | `op` | `eql_v3.ord_term` |
+| `_ord_ore` | Block-ORE, pinned explicitly | `ob` | `eql_v3.ord_term_ore` |
+
+`_ord` and `_ord_ope` are byte-identical today. Pin `_ord_ope` when you want a column's mechanism frozen against a future change of the default.
-The EQL v3 release adds an OPE specifier for every orderable type; unspecified `_ord` columns keep tracking the default.
+
+Block-ORE terms sort only under a custom btree operator class, and creating one requires superuser. Where the EQL installer runs as a non-superuser, which is the case on most managed Postgres including cloud Supabase, it cannot create the class, so it **disables every ORE-backed domain** rather than let them install half-working. `_ord_ore` then raises `feature_not_supported` on the first value written to it. Use `_ord` there.
+
## Payload
-A value for an `_ord` column carries the shared envelope keys (`v`, `i`, `c` — see [Core concepts](/reference/eql/core-concepts)) plus the `ob` ordering term. Here is a payload for the `public.timestamp_ord` `occurred_at` column:
+A value for an `_ord` column carries the shared envelope keys (`v`, `i`, `c` — see [Core concepts](/reference/eql/core-concepts)) plus the `op` ordering term. Here is a payload for the `public.eql_v3_timestamp_ord` `occurred_at` column:
```json
{
"v": 3,
"i": { "t": "audit_events", "c": "occurred_at" },
"c": "mBbKmsMM%bK#QQOx1yLDBHyD...",
- "ob": [
- "7a1fd0c2...", "d24c9be1...", "03fa66b8...", "91b7e04d...",
- "5c28aa19...", "e6f3071c...", "48d92ab5...", "0b64cf37...",
- "2ce8b1f4...", "a90d57e2...", "6f13c8ba...", "d4720e95..."
- ]
+ "op": "5f2b1a9e4c07d38b6ea15c92..."
}
```
-- **`ob` is the only index term.** An `_ord` payload carries no `hm`: equality on `_ord` variants compares ORE terms, which collapse to equality — see [Core concepts](/reference/eql/core-concepts).
-- **The `ob` block count varies with the plaintext width** — `timestamp` values carry 12 blocks.
+- **`op` is the only index term.** It is a hex-encoded CLLW OPE ciphertext, which Postgres sorts by native `bytea` comparison. An `_ord` payload carries no `hm`, because ordering over a date or timestamp is equality-lossless: `=` and `<>` resolve against the same term.
+- **An `_ord_ore` payload carries `ob` in place of `op`**: an array of block-ORE ciphertexts, 12 blocks for a `timestamp`.
## Operators
-| SQL operator | `public.` | `_eq` | `_ord` / `_ord_ore` |
+| SQL operator | `public.eql_v3_` | `_eq` | `_ord` variants |
| --- | :---: | :---: | :---: |
| `=` / `<>` | ❌ | ✅ | ✅ |
| `<` `<=` `>` `>=` | ❌ | ❌ | ✅ |
@@ -87,7 +91,7 @@ A value for an `_ord` column carries the shared envelope keys (`v`, `i`, `c` —
| `ORDER BY` | ❌ | ❌ | ✅ |
| `IS NULL` / `IS NOT NULL` | ✅ | ✅ | ✅ |
-Blocked *operator* cells raise an `operator … is not supported` exception — they never silently return wrong rows. `ORDER BY` is the one blocked cell that doesn't raise: it isn't an operator, so sorting a variant without an ordering term runs — but the order is meaningless (see [Sorting](/reference/eql/sorting)). Operands must be typed (`$1::public.timestamp_ord`), or PostgreSQL resolves the native `jsonb` operator instead of the encrypted one. Both rules are covered in [Core concepts](/reference/eql/core-concepts).
+Blocked *operator* cells raise an `operator … is not supported` exception — they never silently return wrong rows. `ORDER BY` is the one blocked cell that doesn't raise: it isn't an operator, so sorting a variant without an ordering term runs — but the order is meaningless (see [Sorting](/reference/eql/sorting)). Operands must be typed (`$1::public.eql_v3_timestamp_ord`), or PostgreSQL resolves the native `jsonb` operator instead of the encrypted one. Both rules are covered in [Core concepts](/reference/eql/core-concepts).
## Functions
@@ -95,9 +99,9 @@ Every operator has a function form, for managed platforms that disallow custom o
| Function | Equivalent | Available on |
| --- | --- | --- |
-| `eql_v3.eq(a, b)` / `eql_v3.neq(a, b)` | `=` / `<>` | `_eq`, `_ord` / `_ord_ore` |
-| `eql_v3.lt` / `lte` / `gt` / `gte` | `<` `<=` `>` `>=` | `_ord` / `_ord_ore` |
-| `eql_v3.min(col)` / `eql_v3.max(col)` | aggregate `MIN` / `MAX` | `_ord` / `_ord_ore` |
+| `eql_v3.eq(a, b)` / `eql_v3.neq(a, b)` | `=` / `<>` | `_eq`, all `_ord` variants |
+| `eql_v3.lt` / `lte` / `gt` / `gte` | `<` `<=` `>` `>=` | all `_ord` variants |
+| `eql_v3.min(col)` / `eql_v3.max(col)` | aggregate `MIN` / `MAX` | all `_ord` variants |
## Example queries
@@ -105,17 +109,17 @@ Every operator has a function form, for managed platforms that disallow custom o
```sql
SELECT * FROM audit_events
-WHERE occurred_at BETWEEN $1::public.timestamp_ord AND $2::public.timestamp_ord;
+WHERE occurred_at BETWEEN $1::public.eql_v3_timestamp_ord AND $2::public.eql_v3_timestamp_ord;
SELECT * FROM audit_events
-WHERE review_due BETWEEN $1::public.date_ord AND $2::public.date_ord;
+WHERE review_due BETWEEN $1::public.eql_v3_date_ord AND $2::public.eql_v3_date_ord;
```
### Retention cutoff
```sql
SELECT id FROM audit_events
-WHERE occurred_at < $1::public.timestamp_ord;
+WHERE occurred_at < $1::public.eql_v3_timestamp_ord;
```
### Newest-first listing
@@ -124,7 +128,7 @@ Write the sort key in extractor form to stream rows out of the index already ord
```sql
SELECT * FROM audit_events
-WHERE occurred_at >= $1::public.timestamp_ord
+WHERE occurred_at >= $1::public.eql_v3_timestamp_ord
ORDER BY eql_v3.ord_term(occurred_at) DESC
LIMIT 10;
```
diff --git a/content/docs/reference/eql/filtering.mdx b/content/docs/reference/eql/filtering.mdx
index 6210731..f81a6bf 100644
--- a/content/docs/reference/eql/filtering.mdx
+++ b/content/docs/reference/eql/filtering.mdx
@@ -9,25 +9,25 @@ verifiedAgainst:
-Every filter below is ordinary SQL — the encrypted operators resolve from the column's domain variant, and a functional index on the matching term extractor serves the predicate. One rule applies throughout: **operands must be typed** (`$1::public.text_eq`, not a bare literal), or PostgreSQL falls through to native `jsonb` semantics. See [Core concepts](/reference/eql/core-concepts) for the typed-operand rule and how unsupported operators fail loudly instead of returning wrong rows.
+Every filter below is ordinary SQL — the encrypted operators resolve from the column's domain variant, and a functional index on the matching term extractor serves the predicate. One rule applies throughout: **operands must be typed** (`$1::public.eql_v3_text_eq`, not a bare literal), or PostgreSQL falls through to native `jsonb` semantics. See [Core concepts](/reference/eql/core-concepts) for the typed-operand rule and how unsupported operators fail loudly instead of returning wrong rows.
## Equality: `=` and `<>`
-Works on `_eq` and `_ord` / `_ord_ore` variants of every scalar, and on `text_search`:
+Works on `_eq` and every `_ord` variant of every scalar, and on `text_search`:
```sql
-SELECT * FROM users WHERE email = $1::public.text_eq;
-SELECT * FROM users WHERE tax_id <> $1::public.text_eq;
+SELECT * FROM users WHERE email = $1::public.eql_v3_text_eq;
+SELECT * FROM users WHERE tax_id <> $1::public.eql_v3_text_eq;
```
-On `_eq` and `text_search` columns equality compares the HMAC (`hm`) term. On `_ord` variants there is no `hm` — equality compares the ORE (`ob`) term, which collapses to equality, so `_ord` columns get `=` and `<>` for free. See [Core concepts](/reference/eql/core-concepts) for the mechanism.
+On `_eq` columns, and on every text variant, equality compares the HMAC (`hm`) term. On the `_ord` variants of the non-text scalars there is no `hm`: equality compares the ordering term instead, which is lossless over those domains, so those columns get `=` and `<>` for free. See [Core concepts](/reference/eql/core-concepts) for the mechanism.
```sql
--- salary is public.bigint_ord: equality works without an hm term
-SELECT * FROM users WHERE salary = $1::public.bigint_ord;
+-- salary is public.eql_v3_bigint_ord: equality works without an hm term
+SELECT * FROM users WHERE salary = $1::public.eql_v3_bigint_ord;
```
-Bare storage-only variants (`public.text`, `public.integer`, …) block every comparison — see the type pages for what each variant supports: [Numbers](/reference/eql/numbers), [Dates & times](/reference/eql/dates-and-times), [Text](/reference/eql/text), [Booleans](/reference/eql/booleans).
+Bare storage-only variants (`public.eql_v3_text`, `public.eql_v3_integer`, …) block every comparison — see the type pages for what each variant supports: [Numbers](/reference/eql/numbers), [Dates & times](/reference/eql/dates-and-times), [Text](/reference/eql/text), [Booleans](/reference/eql/booleans).
## `IN` lists
@@ -35,29 +35,29 @@ Bare storage-only variants (`public.text`, `public.integer`, …) block every co
```sql
SELECT * FROM users
-WHERE email IN ($1::public.text_eq, $2::public.text_eq, $3::public.text_eq);
+WHERE email IN ($1::public.eql_v3_text_eq, $2::public.eql_v3_text_eq, $3::public.eql_v3_text_eq);
```
There is no way to encrypt a list as one value — the client encrypts each element and binds it as its own parameter. `IN (subquery)` also works, subject to the same-keyset rule covered in [Joins](/reference/eql/joins).
## Ranges and `BETWEEN`
-`<`, `<=`, `>`, `>=` work on `_ord` / `_ord_ore` variants and `text_search` — the variants carrying an ORE (`ob`) term:
+`<`, `<=`, `>`, `>=` work on `_ord` / `_ord_ope` variants and `text_search`, which carry an OPE (`op`) term, and on the pinned `_ord_ore` / `text_search_ore` variants, which carry a block-ORE (`ob`) term:
```sql
-SELECT * FROM users WHERE salary >= $1::public.bigint_ord;
+SELECT * FROM users WHERE salary >= $1::public.eql_v3_bigint_ord;
-- BETWEEN desugars to >= and <=
SELECT * FROM users
-WHERE created_at BETWEEN $1::public.timestamp_ord AND $2::public.timestamp_ord;
+WHERE created_at BETWEEN $1::public.eql_v3_timestamp_ord AND $2::public.eql_v3_timestamp_ord;
```
Half-open ranges compose the same way:
```sql
SELECT * FROM events
-WHERE occurred_at >= $1::public.timestamp_ord
- AND occurred_at < $2::public.timestamp_ord;
+WHERE occurred_at >= $1::public.eql_v3_timestamp_ord
+ AND occurred_at < $2::public.eql_v3_timestamp_ord;
```
## Text token matching: `@>`
@@ -65,14 +65,14 @@ WHERE occurred_at >= $1::public.timestamp_ord
There is no `LIKE` on encrypted columns — encrypted free-text matching is bloom-filter token containment via `@>` on a `text_match` or `text_search` column:
```sql
-SELECT * FROM users WHERE name @> $1::public.text_match;
+SELECT * FROM users WHERE name @> $1::public.eql_v3_text_match;
```
The client encrypts the search term into a bloom-filter query value; matching is probabilistic (false positives possible, false negatives not). For the full no-`LIKE` story and match-term tuning, see [Text](/reference/eql/text).
## JSON containment and path filters
-Encrypted JSON documents (`public.json`) filter by containment and path existence:
+Encrypted JSON documents (`public.eql_v3_json`) filter by containment and path existence:
```sql
-- Does the document contain this (encrypted) structure?
@@ -83,7 +83,7 @@ SELECT * FROM orders WHERE eql_v3.jsonb_path_exists(metadata, 'region_selector')
-- Equality on an extracted leaf
SELECT * FROM orders
-WHERE metadata -> 'email_selector'::text = $1::public.jsonb_entry;
+WHERE metadata -> 'email_selector'::text = $1::public.eql_v3_jsonb_entry;
```
Field access is by selector hash, not plaintext path. The full JSON surface — containment, field access, path queries, and range filters on extracted leaves — is in [JSON](/reference/eql/json).
@@ -95,9 +95,9 @@ Encrypted predicates compose with `AND`, `OR`, `NOT`, and parentheses like any o
```sql
SELECT * FROM users
WHERE status = 'active' -- plaintext column, native operator
- AND created_at >= $1::public.timestamp_ord -- encrypted range
- AND (email = $2::public.text_eq -- encrypted equality
- OR name @> $3::public.text_match); -- encrypted token match
+ AND created_at >= $1::public.eql_v3_timestamp_ord -- encrypted range
+ AND (email = $2::public.eql_v3_text_eq -- encrypted equality
+ OR name @> $3::public.eql_v3_text_match); -- encrypted token match
```
The planner treats each encrypted predicate independently, so it can combine an index on a plaintext column with a functional index on an encrypted one (bitmap-AND, or whichever plan is cheapest).
@@ -117,10 +117,10 @@ Don't confuse this with a JSON `null` *inside* an encrypted document, which is a
| Filter shape | Operators | Works on | Index |
| --- | --- | --- | --- |
-| Equality | `=` `<>` `IN` | `_eq`, `_ord` / `_ord_ore`, `text_search` | hash (or btree) on `eql_v3.eq_term` — btree on `eql_v3.ord_term` for `_ord` |
-| Range | `<` `<=` `>` `>=` `BETWEEN` | `_ord` / `_ord_ore`, `text_search` | btree on `eql_v3.ord_term` |
-| Text token match | `@>` `<@` | `text_match`, `text_search` | GIN on `eql_v3.match_term` |
-| JSON containment | `@>` `<@` | `public.json` | GIN on `eql_v3.to_ste_vec_query(col)::jsonb` |
+| Equality | `=` `<>` `IN` | `_eq`, all `_ord` variants, `text_search` variants | hash (or btree) on `eql_v3.eq_term` — btree on `eql_v3.ord_term` for the non-text `_ord` variants |
+| Range | `<` `<=` `>` `>=` `BETWEEN` | all `_ord` variants, `text_search` variants | btree on `eql_v3.ord_term` (`eql_v3.ord_term_ore` on `_ord_ore`) |
+| Text token match | `@>` `<@` | `text_match`, `text_search` variants | GIN on `eql_v3.match_term` |
+| JSON containment | `@>` `<@` | `public.eql_v3_json` | GIN on `eql_v3.to_ste_vec_query(col)::jsonb` |
| Null check | `IS NULL` / `IS NOT NULL` | every variant | — |
Every one of these has a full index recipe — which method, which extractor, and how to confirm the index engages with `EXPLAIN` — in [Indexes](/reference/eql/indexes).
diff --git a/content/docs/reference/eql/grouping-and-aggregates.mdx b/content/docs/reference/eql/grouping-and-aggregates.mdx
index 568be31..8798d33 100644
--- a/content/docs/reference/eql/grouping-and-aggregates.mdx
+++ b/content/docs/reference/eql/grouping-and-aggregates.mdx
@@ -9,7 +9,7 @@ verifiedAgainst:
-Grouping and deduplication need an equality term, so they work on the same variants as `=`: `_eq`, `_ord` / `_ord_ore`, and `text_search`. `MIN` / `MAX` need an ordering term (`_ord` / `_ord_ore`, `text_search`). Arithmetic aggregates don't work at all — that's the last section. As everywhere, operands and call-site casts must be typed; see [Core concepts](/reference/eql/core-concepts).
+Grouping and deduplication need an equality term, so they work on the same variants as `=`: `_eq`, every `_ord` variant, and the `text_search` variants. `MIN` / `MAX` need an ordering term (any `_ord` variant, or `text_search`). Arithmetic aggregates don't work at all — that's the last section. As everywhere, operands and call-site casts must be typed; see [Core concepts](/reference/eql/core-concepts).
## `GROUP BY` and `DISTINCT`
@@ -41,7 +41,7 @@ Note the trade-off: grouping on `eq_term` returns the *term*, not the encrypted
Plain `COUNT(col)` counts non-`NULL` rows — it never compares values, so it works on **any** variant, including storage-only ones:
```sql
-SELECT COUNT(tax_id) FROM users; -- works even on bare public.text
+SELECT COUNT(tax_id) FROM users; -- works even on bare public.eql_v3_text
```
`COUNT(DISTINCT col)` deduplicates, so it needs an equality-capable variant — and the same extractor advice applies:
@@ -55,13 +55,15 @@ SELECT COUNT(DISTINCT eql_v3.eq_term(email)) FROM logins;
EQL ships `min` / `max` aggregates per ord-capable variant of every scalar type. The input type selects the aggregate, and the return type matches the input:
```sql
-eql_v3.min(public._ord) RETURNS public._ord
-eql_v3.max(public._ord) RETURNS public._ord
-eql_v3.min(public._ord_ore) RETURNS public._ord_ore
-eql_v3.max(public._ord_ore) RETURNS public._ord_ore
+eql_v3.min(public.eql_v3__ord) RETURNS public.eql_v3__ord
+eql_v3.max(public.eql_v3__ord) RETURNS public.eql_v3__ord
+eql_v3.min(public.eql_v3__ord_ope) RETURNS public.eql_v3__ord_ope
+eql_v3.max(public.eql_v3__ord_ope) RETURNS public.eql_v3__ord_ope
+eql_v3.min(public.eql_v3__ord_ore) RETURNS public.eql_v3__ord_ore
+eql_v3.max(public.eql_v3__ord_ore) RETURNS public.eql_v3__ord_ore
```
-Comparison routes through the variant's `<` / `>` operator on the ORE term — no decryption happens in the database, and the result is an encrypted value the client decrypts. `NULL` inputs are skipped; an all-`NULL` input set returns `NULL`, matching native aggregate semantics.
+Comparison routes through the variant's `<` / `>` operator on the ordering term — no decryption happens in the database, and the result is an encrypted value the client decrypts. `NULL` inputs are skipped; an all-`NULL` input set returns `NULL`, matching native aggregate semantics.
```sql
SELECT eql_v3.min(salary) FROM users;
@@ -76,7 +78,7 @@ SELECT eql_v3.eq_term(department_code) AS dept, eql_v3.max(salary)
If the column is generic `jsonb` rather than a domain, cast to the right variant at the call site so overload resolution can pick the aggregate:
```sql
-SELECT eql_v3.min(salary_jsonb::public.bigint_ord) FROM users;
+SELECT eql_v3.min(salary_jsonb::public.eql_v3_bigint_ord) FROM users;
```
A btree on `eql_v3.ord_term(col)` serves `MIN` / `MAX` — the [Indexes](/reference/eql/indexes) page has the recipe.
@@ -84,7 +86,7 @@ A btree on `eql_v3.ord_term(col)` serves `MIN` / `MAX` — the [Indexes](/refere
## No `SUM`, no `AVG`
-**`SUM`, `AVG`, and every other arithmetic aggregate are unsupported** on encrypted columns — they would require homomorphic encryption, which EQL does not do. `MIN` / `MAX` work because they only need *comparison*, which the ORE term provides. For sums and averages, select the rows (or `MIN`/`MAX`/`COUNT` server-side to narrow them) and aggregate client-side after decryption.
+**`SUM`, `AVG`, and every other arithmetic aggregate are unsupported** on encrypted columns — they would require homomorphic encryption, which EQL does not do. `MIN` / `MAX` work because they only need *comparison*, which the ordering term provides. For sums and averages, select the rows (or `MIN`/`MAX`/`COUNT` server-side to narrow them) and aggregate client-side after decryption.
## Grouping on extracted JSON leaves
diff --git a/content/docs/reference/eql/indexes.mdx b/content/docs/reference/eql/indexes.mdx
index df87fb4..d518cbd 100644
--- a/content/docs/reference/eql/indexes.mdx
+++ b/content/docs/reference/eql/indexes.mdx
@@ -14,13 +14,14 @@ EQL indexes are ordinary PostgreSQL functional indexes over **term-extractor fun
| Extractor | Index method | Term | Capability |
| --- | --- | --- | --- |
| `eql_v3.eq_term(col)` | `hash` (or `btree`) | `hm` (HMAC-256) | equality |
-| `eql_v3.ord_term(col)` | `btree` | `ob` (ORE block) | range, `ORDER BY`, `MIN` / `MAX` |
+| `eql_v3.ord_term(col)` | `btree` | `op` (CLLW OPE) | range, `ORDER BY`, `MIN` / `MAX` |
+| `eql_v3.ord_term_ore(col)` | `btree` | `ob` (ORE block) | the same, on pinned `_ord_ore` / `text_search_ore` columns |
| `eql_v3.match_term(col)` | `gin` | `bf` (bloom filter) | text containment |
The extractors are inlinable SQL functions, so the planner rewrites a bare-form predicate into the same expression the index was built on. You don't rewrite queries to use the index:
```sql
-SELECT * FROM users WHERE email = $1::public.text_eq;
+SELECT * FROM users WHERE email = $1::public.eql_v3_text_eq;
-- planner inlines `=` to: eql_v3.eq_term(email) = eql_v3.eq_term($1)
-- Index Cond on USING hash (eql_v3.eq_term(email))
```
@@ -35,18 +36,23 @@ Type the column as the domain variant that carries the term (see [Core concepts]
```sql
-- Equality: hash index on eq_term
--- (columns typed public._eq or text_search; equality on _ord columns
--- compares ORE terms, so the btree on ord_term below serves it)
+-- (columns typed public.eql_v3__eq, any text variant, or text_search; equality on
+-- the non-text _ord columns compares ordering terms, so the btree below serves it)
CREATE INDEX users_email_eq
ON users USING hash (eql_v3.eq_term(email));
-- Range / ordering: btree index on ord_term
--- (columns typed public._ord or _ord_ore)
+-- (columns typed public.eql_v3__ord, _ord_ope, or text_search)
CREATE INDEX users_created_at_ord
ON users USING btree (eql_v3.ord_term(created_at));
+-- Range / ordering on a pinned block-ORE column: btree on ord_term_ore
+-- (columns typed public.eql_v3__ord_ore or text_search_ore)
+CREATE INDEX users_created_at_ord_ore
+ ON users USING btree (eql_v3.ord_term_ore(created_at_ore));
+
-- Text match: GIN index on match_term
--- (columns typed public.text_match or text_search)
+-- (columns typed public.eql_v3_text_match or text_search)
CREATE INDEX users_name_match
ON users USING gin (eql_v3.match_term(name));
@@ -61,13 +67,13 @@ Create indexes when the table has a significant number of rows (typically more t
All three must hold:
-1. **The value carries the required term.** Equality needs `hm`, range needs `ob`, containment needs `bf`. Which terms travel in a value's payload is decided by the encryption client — a value with only a bloom term will not drive an equality index.
+1. **The value carries the required term.** Range needs `op` (or `ob` on the pinned block-ORE variants), containment needs `bf`, and equality needs `hm`, except on the non-text `_ord` variants, where equality resolves against the ordering term instead. Which terms travel in a value's payload is decided by the encryption client — a value with only a bloom term will not drive an equality index.
2. **The index was built after the data carried the term.** If you change which terms a column's values carry, recreate the index.
3. **The query operand is typed.** A typed parameter (`$1`, which CipherStash Proxy supplies) or an explicit cast resolves the encrypted operator; a bare `jsonb` literal falls through to native `jsonb` semantics and skips the index entirely:
```sql
-- ✓ resolves the encrypted operator → uses the index
-WHERE email = $1::public.text_eq;
+WHERE email = $1::public.eql_v3_text_eq;
WHERE email = $1; -- only when the client (Stack SDK / Proxy) binds $1 typed
-- ✗ falls through to native jsonb semantics
@@ -79,7 +85,7 @@ WHERE email = '{"hm":"abc"}'::jsonb;
### Equality
```sql
-SELECT * FROM users WHERE email = $1::public.text_eq;
+SELECT * FROM users WHERE email = $1::public.eql_v3_text_eq;
-- Index Scan using users_email_eq
-- Index Cond: (eql_v3.eq_term(email) = eql_v3.eq_term($1))
```
@@ -89,14 +95,14 @@ SELECT * FROM users WHERE email = $1::public.text_eq;
The `<`, `<=`, `>`, `>=` operators inline to comparisons on `eql_v3.ord_term`, so natural-form range predicates match the btree:
```sql
-SELECT * FROM users WHERE created_at < $1::public.timestamp_ord;
+SELECT * FROM users WHERE created_at < $1::public.eql_v3_timestamp_ord;
```
`ORDER BY` needs care. The planner inlines operators in *predicates* but does not rewrite *sort keys*: `ORDER BY created_at` uses the index for the `WHERE` clause but still adds a `Sort` node, which scales linearly with the rows passing the filter. To stream rows out of the btree already ordered, write the sort key in extractor form:
```sql
SELECT * FROM users
- WHERE created_at < $1::public.timestamp_ord
+ WHERE created_at < $1::public.eql_v3_timestamp_ord
ORDER BY eql_v3.ord_term(created_at) DESC
LIMIT 10;
```
@@ -121,7 +127,7 @@ SELECT eql_v3.eq_term(email), count(*)
## Encrypted JSON
-Containment (`@>` / `<@`) on `public.json` document columns uses a GIN index over `eql_v3.to_ste_vec_query(col)::jsonb`, and field-level equality and ordering have their own extractor recipes. See [JSON](/reference/eql/json).
+Containment (`@>` / `<@`) on `public.eql_v3_json` document columns uses a GIN index over `eql_v3.to_ste_vec_query(col)::jsonb`, and field-level equality and ordering have their own extractor recipes. See [JSON](/reference/eql/json).
## Verify with EXPLAIN
@@ -177,7 +183,7 @@ Everything above is a functional index over an `IMMUTABLE` SQL function — no o
FROM users LIMIT 1;
```
-2. Verify the operand is typed (`$1::public.text_eq`, not `$1::jsonb`).
+2. Verify the operand is typed (`$1::public.eql_v3_text_eq`, not `$1::jsonb`).
3. Recreate the index if the column's terms changed after it was built.
4. Run `ANALYZE`. Very small tables may still choose a sequential scan — that's correct.
diff --git a/content/docs/reference/eql/joins.mdx b/content/docs/reference/eql/joins.mdx
index 2b31347..583c4b4 100644
--- a/content/docs/reference/eql/joins.mdx
+++ b/content/docs/reference/eql/joins.mdx
@@ -9,13 +9,13 @@ verifiedAgainst:
-Equijoins work on equality-capable variants (`_eq`, `_ord` / `_ord_ore`, `text_search`) — the join condition is just encrypted equality. But there is one constraint that has no plaintext equivalent, and it is the single thing to internalize on this page:
+Equijoins work on equality-capable variants (`_eq`, every `_ord` variant, `text_search` variants) — the join condition is just encrypted equality. But there is one constraint that has no plaintext equivalent, and it is the single thing to internalize on this page:
**Both sides of the join must be encrypted with the same keyset and typed as a matching variant.** Encrypted equality compares deterministic index terms, and those terms are derived from the encryption keys. Two columns encrypted under different keysets produce different terms for the *same plaintext* — their terms can **never** match, and the join returns no rows. This is not an error the database can detect: the query is valid, the plan is fine, the result is simply empty.
-"Matching variant" means both sides compare the same term kind: `_eq` with `_eq` (or `text_search`, which carries an `hm` term too) compares HMAC terms; `_ord` with `_ord` compares ORE terms. An `_eq` column can't join an `_ord` column — one side has no `hm`, the other no `ob`, and the equality operator between mismatched variants doesn't resolve. See [Core concepts](/reference/eql/core-concepts) for the term model.
+"Matching variant" means both sides are the same domain, so the equality operator resolves and both sides compare the same term kind: `_eq` columns compare HMAC terms, and so does every text variant; the `_ord` variants of the non-text scalars compare ordering terms. An `_eq` column can't join an `_ord` column, because EQL defines its equality operators per domain and none exists between the two. See [Core concepts](/reference/eql/core-concepts) for the term model.
## Equijoin
@@ -23,7 +23,7 @@ Equijoins work on equality-capable variants (`_eq`, `_ord` / `_ord_ore`, `text_s
SELECT u.*, o.total
FROM users u
JOIN orders o ON u.email = o.customer_email;
--- both columns public.text_eq, encrypted with the same keyset
+-- both columns public.eql_v3_text_eq, encrypted with the same keyset
```
No typed-operand cast is needed here — both operands are encrypted columns, so their domain types resolve the encrypted operator directly. All join types (`INNER`, `LEFT`, `RIGHT`, `FULL`) work; `LEFT JOIN` null-extension behaves normally because SQL `NULL`s are not encrypted.
@@ -49,17 +49,17 @@ If the two columns are under different keysets, `IN (subquery)` matches nothing,
## Worked example
-Two tables sharing an encrypted customer identifier, both columns typed `public.text_eq` and encrypted by the same client configuration (same keyset):
+Two tables sharing an encrypted customer identifier, both columns typed `public.eql_v3_text_eq` and encrypted by the same client configuration (same keyset):
```sql
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- email public.text_eq
+ email public.eql_v3_text_eq
);
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- customer_email public.text_eq,
+ customer_email public.eql_v3_text_eq,
total BIGINT NOT NULL
);
@@ -74,7 +74,7 @@ Orders per user, filtered by an encrypted lookup on one side:
SELECT u.id, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.email = o.customer_email
-WHERE u.email = $1::public.text_eq
+WHERE u.email = $1::public.eql_v3_text_eq
GROUP BY u.id;
```
diff --git a/content/docs/reference/eql/json.mdx b/content/docs/reference/eql/json.mdx
index 63add60..7253a6f 100644
--- a/content/docs/reference/eql/json.mdx
+++ b/content/docs/reference/eql/json.mdx
@@ -1,6 +1,6 @@
---
title: JSON
-description: "The complete reference for encrypted JSON documents with public.json — the ste_vec payload shape, containment, field access, and path queries over ciphertext, with the native jsonb operators that don't apply blocked outright."
+description: "The complete reference for encrypted JSON documents with public.eql_v3_json — the ste_vec payload shape, containment, field access, and path queries over ciphertext, with the native jsonb operators that don't apply blocked outright."
type: reference
components: [eql]
verifiedAgainst:
@@ -9,9 +9,9 @@ verifiedAgainst:
-`public.json` is EQL's encrypted JSON document type, built on structured encryption (**ste_vec**). The document is encrypted as a vector of encrypted entries — one entry per path inside the document — and every path is queryable without decryption: containment, field and array access, and equality or range comparisons on extracted leaves.
+`public.eql_v3_json` is EQL's encrypted JSON document type, built on structured encryption (**ste_vec**). The document is encrypted as a vector of encrypted entries — one entry per path inside the document — and every path is queryable without decryption: containment, field and array access, and equality or range comparisons on extracted leaves.
-Like every EQL type, `public.json` holds ciphertext the database can't read. Encryption, decryption, and selector generation happen in the client — the [Stack SDK](/reference/stack) or [CipherStash Proxy](/reference/proxy). See [Searchable encryption](/concepts/searchable-encryption) for how querying ciphertext works at all.
+Like every EQL type, `public.eql_v3_json` holds ciphertext the database can't read. Encryption, decryption, and selector generation happen in the client — the [Stack SDK](/reference/stack) or [CipherStash Proxy](/reference/proxy). See [Searchable encryption](/concepts/searchable-encryption) for how querying ciphertext works at all.
## The types
@@ -19,8 +19,8 @@ Three `jsonb`-backed domains make up the encrypted JSON surface:
| Type | What it is |
| --- | --- |
-| `public.json` | The column type. An encrypted document envelope carrying an `sv` array — one encrypted entry per path in the document. |
-| `public.jsonb_entry` | A single entry from the vector: a selector, a ciphertext, and exactly one index term. This is what `->` returns. |
+| `public.eql_v3_json` | The column type. An encrypted document envelope carrying an `sv` array — one encrypted entry per path in the document. |
+| `public.eql_v3_jsonb_entry` | A single entry from the vector: a selector, a ciphertext, and exactly one index term. This is what `->` returns. |
| `eql_v3.query_jsonb` | A containment needle: entries with selectors and index terms but **no ciphertext**. This is what you cast a `@>` operand to. |
## Payload shape
@@ -34,9 +34,9 @@ An encrypted JSON document uses a different payload shape from the scalar types:
| `hm` **or** `op` | Exactly one, never both — the domain `CHECK` enforces the exclusivity. `hm` (HMAC-256) on Boolean/`null` leaves and Object/Array roots; `op` (CLLW OPE, backed by `eql_v3_internal.ope_cllw`) on String/Number leaves. |
| `a` | Optional array marker — `true` when the selector points at an array context. |
-The decoded `op` value starts with a domain-tag byte (`0x00` numeric, `0x01` string) followed by the CLLW ciphertext, so numeric and string values in one column keep a consistent total order. Earlier alphas named this key `oc` and backed it with CLLW ORE; `3.0.0-alpha.4` renamed it to `op` and moved it to CLLW OPE, whose term orders under the default btree operator class. Older payload versions split it further, into `ocf` (fixed-width, numeric) and `ocv` (variable-width, string).
+The decoded `op` value starts with a domain-tag byte (`0x00` numeric, `0x01` string) followed by the CLLW ciphertext, so numeric and string values in one column keep a consistent total order. Pre-release payloads named this key `oc` and backed it with CLLW ORE, and older ones split it further, into `ocf` (fixed-width, numeric) and `ocv` (variable-width, string). EQL 3.0.0 ships `op`, backed by CLLW OPE, whose term orders under the default btree operator class.
-A document payload for a `public.json` column:
+A document payload for a `public.eql_v3_json` column:
```json
{
@@ -67,16 +67,16 @@ A containment **query** payload (`eql_v3.query_jsonb`) has the same `sv` shape b
## Storing encrypted JSON
-Type the column as `public.json`:
+Type the column as `public.eql_v3_json`:
```sql
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- metadata public.json
+ metadata public.eql_v3_json
);
```
-There is no database-side configuration step. Which index terms a document carries is decided by the encryption client; typing the column as `public.json` is what makes the encrypted operators and functions resolve. The domain's `CHECK` constraint validates the payload shape on insert, so malformed values are rejected at write time.
+There is no database-side configuration step. Which index terms a document carries is decided by the encryption client; typing the column as `public.eql_v3_json` is what makes the encrypted operators and functions resolve. The domain's `CHECK` constraint validates the payload shape on insert, so malformed values are rejected at write time.
Insert and read through the Stack SDK or Proxy, which encrypt the document into the ste_vec payload on write and decrypt it on read.
@@ -98,7 +98,7 @@ JSON `null` here means a `null` literal *inside* the document. A SQL `NULL` colu
## Blocked native jsonb operators
-These native PostgreSQL `jsonb` operators are **blocked** on `public.json`. They raise an error rather than silently running plaintext-jsonb semantics against the encrypted payload:
+These native PostgreSQL `jsonb` operators are **blocked** on `public.eql_v3_json`. They raise an error rather than silently running plaintext-jsonb semantics against the encrypted payload:
- Key/path existence: `?`, `?|`, `?&`, `@?`, `@@`
- Path extraction: `#>`, `#>>`
@@ -113,7 +113,7 @@ Use containment (`@>` / `<@`), field access (`->` / `->>`), or the `eql_v3.jsonb
## Containment: `@>` and `<@`
-`@>` tests whether the encrypted document contains a structure; `<@` is the reverse. Build the needle with the client and cast it to `eql_v3.query_jsonb` (a typed `public.json` or `public.jsonb_entry` operand also works):
+`@>` tests whether the encrypted document contains a structure; `<@` is the reverse. Build the needle with the client and cast it to `eql_v3.query_jsonb` (a typed `public.eql_v3_json` or `public.eql_v3_jsonb_entry` operand also works):
```sql
SELECT * FROM orders
@@ -137,7 +137,7 @@ See [Indexes](/reference/eql/indexes) for the full recipes.
Fields are addressed by **selector hash** — the deterministic identifier the client emits for a JSON path during encryption — not a plaintext path string like `$.customer.tier`.
```sql
--- Field access by selector (returns public.jsonb_entry)
+-- Field access by selector (returns public.eql_v3_jsonb_entry)
SELECT metadata -> 'selector_hash'::text FROM orders;
-- The entry serialized as text (ciphertext JSON, not decrypted plaintext)
@@ -147,16 +147,16 @@ SELECT metadata ->> 'selector_hash'::text FROM orders;
SELECT metadata -> 0 FROM orders;
```
-The extracted `public.jsonb_entry` is itself comparable:
+The extracted `public.eql_v3_jsonb_entry` is itself comparable:
- `=` / `<>` resolve via `eql_v3.eq_term` — works on every node type
-- `<` / `<=` / `>` / `>=` resolve via `eql_v3.ord_ope_term` — String and Number leaves only
+- `<` / `<=` / `>` / `>=` resolve via `eql_v3.ord_term` — String and Number leaves only
- `MIN` / `MAX` over an extracted ordered leaf use the `eql_v3.min` / `eql_v3.max` aggregates
```sql
-- Equality on an extracted leaf
SELECT * FROM orders
-WHERE metadata -> 'email_selector'::text = $1::public.jsonb_entry;
+WHERE metadata -> 'email_selector'::text = $1::public.eql_v3_jsonb_entry;
-- Group by an extracted leaf's equality term
SELECT eql_v3.eq_term(metadata -> 'region_selector'::text) AS region, COUNT(*)
@@ -164,7 +164,7 @@ FROM orders
GROUP BY eql_v3.eq_term(metadata -> 'region_selector'::text);
```
-A hash index on `eql_v3.eq_term(col -> ''::text)` engages the equality lookup; a btree on `eql_v3.ord_ope_term(...)` engages range and `ORDER BY`. The OPE term is a `bytea` domain that orders under the default btree operator class, so this index needs no custom operator class. See [Indexes](/reference/eql/indexes).
+A hash index on `eql_v3.eq_term(col -> ''::text)` engages the equality lookup; a btree on `eql_v3.ord_term(...)` engages range and `ORDER BY`. The OPE term is a `bytea` domain that orders under the default btree operator class, so this index needs no custom operator class. See [Indexes](/reference/eql/indexes).
## Path queries and array helpers
@@ -215,7 +215,7 @@ The client encrypts this into a ste_vec payload with selectors for `$`, `$.custo
```sql
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- metadata public.json
+ metadata public.eql_v3_json
);
INSERT INTO orders (metadata) VALUES ($1);
diff --git a/content/docs/reference/eql/numbers.mdx b/content/docs/reference/eql/numbers.mdx
index 7c74fd2..f0cdf81 100644
--- a/content/docs/reference/eql/numbers.mdx
+++ b/content/docs/reference/eql/numbers.mdx
@@ -1,6 +1,6 @@
---
title: Numbers
-description: "The complete reference for encrypted numeric columns: the int, float, and numeric domain variants, the ORE-backed payload they carry, and range, ORDER BY, and MIN/MAX queries."
+description: "The complete reference for encrypted numeric columns: the int, float, and numeric domain variants, the ordering term they carry, and range, ORDER BY, and MIN/MAX queries."
type: reference
components: [eql]
verifiedAgainst:
@@ -19,21 +19,24 @@ Each numeric type generates the same `jsonb`-backed domain variants. The generic
| Domain variant | Capability |
| --- | --- |
-| `public.` | Storage and decryption only. |
-| `public._eq` | Equality: `=`, `<>`, `IN`, `GROUP BY`, `DISTINCT`, equijoins. |
-| `public._ord` | Comparisons, `BETWEEN`, `ORDER BY`, `MIN` / `MAX` — plus equality. |
-| `public._ord_ore` | As `_ord`, with the ORE mechanism pinned — see [SEM specifiers](#sem-specifiers). |
+| `public.eql_v3_` | Storage and decryption only. |
+| `public.eql_v3__eq` | Equality: `=`, `<>`, `IN`, `GROUP BY`, `DISTINCT`, equijoins. |
+| `public.eql_v3__ord` | Comparisons, `BETWEEN`, `ORDER BY`, `MIN` / `MAX` — plus equality. |
+| `public.eql_v3__ord_ope` | The byte-identical twin of `_ord`, with OPE pinned. See [SEM specifiers](#sem-specifiers). |
+| `public.eql_v3__ord_ore` | As `_ord`, with the ORE mechanism pinned. |
+
+`` is the encrypted type name from the table below, so `public.eql_v3__ord` is `public.eql_v3_bigint_ord` for `int8`.
And every concrete domain this page covers:
| Type | Variants |
| --- | --- |
-| `int2` | `public.smallint` · `public.smallint_eq` · `public.smallint_ord` · `public.smallint_ord_ore` |
-| `int4` | `public.integer` · `public.integer_eq` · `public.integer_ord` · `public.integer_ord_ore` |
-| `int8` | `public.bigint` · `public.bigint_eq` · `public.bigint_ord` · `public.bigint_ord_ore` |
-| `float4` | `public.real` · `public.real_eq` · `public.real_ord` · `public.real_ord_ore` |
-| `float8` | `public.double` · `public.double_eq` · `public.double_ord` · `public.double_ord_ore` |
-| `numeric` | `public.numeric` · `public.numeric_eq` · `public.numeric_ord` · `public.numeric_ord_ore` |
+| `int2` | `public.eql_v3_smallint` · `public.eql_v3_smallint_eq` · `public.eql_v3_smallint_ord` · `public.eql_v3_smallint_ord_ope` · `public.eql_v3_smallint_ord_ore` |
+| `int4` | `public.eql_v3_integer` · `public.eql_v3_integer_eq` · `public.eql_v3_integer_ord` · `public.eql_v3_integer_ord_ope` · `public.eql_v3_integer_ord_ore` |
+| `int8` | `public.eql_v3_bigint` · `public.eql_v3_bigint_eq` · `public.eql_v3_bigint_ord` · `public.eql_v3_bigint_ord_ope` · `public.eql_v3_bigint_ord_ore` |
+| `float4` | `public.eql_v3_real` · `public.eql_v3_real_eq` · `public.eql_v3_real_ord` · `public.eql_v3_real_ord_ope` · `public.eql_v3_real_ord_ore` |
+| `float8` | `public.eql_v3_double` · `public.eql_v3_double_eq` · `public.eql_v3_double_ord` · `public.eql_v3_double_ord_ope` · `public.eql_v3_double_ord_ore` |
+| `numeric` | `public.eql_v3_numeric` · `public.eql_v3_numeric_eq` · `public.eql_v3_numeric_ord` · `public.eql_v3_numeric_ord_ope` · `public.eql_v3_numeric_ord_ore` |
Declare only the capability you query on — each capability stores extra searchable material with defined leakage (see [Searchable encryption](/concepts/searchable-encryption)), and the variant model itself is covered in [Core concepts](/reference/eql/core-concepts).
@@ -44,9 +47,9 @@ A payroll table mixing the variants by how each column is queried:
```sql
CREATE TABLE employees (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- salary public.bigint_ord, -- range queries, ORDER BY, MIN/MAX
- tax_rate public.numeric_eq, -- exact lookup only
- net_worth public.numeric -- store and decrypt only, never queried
+ salary public.eql_v3_bigint_ord, -- range queries, ORDER BY, MIN/MAX
+ tax_rate public.eql_v3_numeric_eq, -- exact lookup only
+ net_worth public.eql_v3_numeric -- store and decrypt only, never queried
);
```
@@ -54,35 +57,37 @@ CREATE TABLE employees (
All six types take the same mechanism specifiers on their orderable variant (the concept is defined in [Core concepts](/reference/eql/core-concepts#sem-specifiers)):
-| Specifier | Meaning |
-| --- | --- |
-| `_ord` | Orderable, using EQL's default mechanism (currently ORE). |
-| `_ord_ore` | Orderable via ORE, pinned explicitly. |
+| Specifier | Mechanism | Ordering term | Extractor |
+| --- | --- | --- | --- |
+| `_ord` | The default, currently CLLW OPE | `op` | `eql_v3.ord_term` |
+| `_ord_ope` | CLLW OPE, pinned explicitly | `op` | `eql_v3.ord_term` |
+| `_ord_ore` | Block-ORE, pinned explicitly | `ob` | `eql_v3.ord_term_ore` |
+
+`_ord` and `_ord_ope` are byte-identical today. Pin `_ord_ope` when you want a column's mechanism frozen against a future change of the default.
-The EQL v3 release adds an OPE specifier for every orderable type; unspecified `_ord` columns keep tracking the default.
+
+Block-ORE terms sort only under a custom btree operator class, and creating one requires superuser. Where the EQL installer runs as a non-superuser, which is the case on most managed Postgres including cloud Supabase, it cannot create the class, so it **disables every ORE-backed domain** rather than let them install half-working. `_ord_ore` then raises `feature_not_supported` on the first value written to it. Use `_ord` there.
+
## Payload
-A value for an `_ord` column carries the shared envelope keys (`v`, `i`, `c` — see [Core concepts](/reference/eql/core-concepts)) plus the `ob` ordering term. Here is a payload for the `public.bigint_ord` `salary` column:
+A value for an `_ord` column carries the shared envelope keys (`v`, `i`, `c` — see [Core concepts](/reference/eql/core-concepts)) plus the `op` ordering term. Here is a payload for the `public.eql_v3_bigint_ord` `salary` column:
```json
{
"v": 3,
"i": { "t": "employees", "c": "salary" },
"c": "mBbKmsMM%bK#QQOx1yLDBHyD...",
- "ob": [
- "7a1fd0c2...", "d24c9be1...", "03fa66b8...", "91b7e04d...",
- "5c28aa19...", "e6f3071c...", "48d92ab5...", "0b64cf37..."
- ]
+ "op": "5f2b1a9e4c07d38b6ea15c92..."
}
```
-- **`ob` is the only index term.** An `_ord` payload carries no `hm`: equality on `_ord` variants compares ORE terms, which collapse to equality — see [Core concepts](/reference/eql/core-concepts). Only `_eq` payloads carry `hm` (a single hex HMAC-SHA-256 string) instead of `ob`.
-- **The `ob` block count varies with the plaintext width**: 8 blocks for the int types, 14 for `numeric`.
+- **`op` is the only index term.** It is a hex-encoded CLLW OPE ciphertext, which Postgres sorts by native `bytea` comparison. An `_ord` payload carries no `hm`, because ordering over a numeric scalar is equality-lossless: `=` and `<>` resolve against the same term. Only `_eq` payloads carry `hm` (a single hex HMAC-SHA-256 string) instead.
+- **An `_ord_ore` payload carries `ob` in place of `op`**: an array of block-ORE ciphertexts, 8 blocks for the int types and 14 for `numeric`.
## Operators
-| SQL operator | `public.` | `_eq` | `_ord` / `_ord_ore` |
+| SQL operator | `public.eql_v3_` | `_eq` | `_ord` variants |
| --- | :---: | :---: | :---: |
| `=` / `<>` | ❌ | ✅ | ✅ |
| `<` `<=` `>` `>=` | ❌ | ❌ | ✅ |
@@ -92,7 +97,7 @@ A value for an `_ord` column carries the shared envelope keys (`v`, `i`, `c` —
| `ORDER BY` | ❌ | ❌ | ✅ |
| `IS NULL` / `IS NOT NULL` | ✅ | ✅ | ✅ |
-Blocked *operator* cells raise an `operator … is not supported` exception — they never silently return wrong rows. `ORDER BY` is the one blocked cell that doesn't raise: it isn't an operator, so sorting a variant without an ordering term runs — but the order is meaningless (see [Sorting](/reference/eql/sorting)). Operands must be typed (`$1::public.bigint_ord`), or PostgreSQL resolves the native `jsonb` operator instead of the encrypted one. Both rules are covered in [Core concepts](/reference/eql/core-concepts).
+Blocked *operator* cells raise an `operator … is not supported` exception — they never silently return wrong rows. `ORDER BY` is the one blocked cell that doesn't raise: it isn't an operator, so sorting a variant without an ordering term runs — but the order is meaningless (see [Sorting](/reference/eql/sorting)). Operands must be typed (`$1::public.eql_v3_bigint_ord`), or PostgreSQL resolves the native `jsonb` operator instead of the encrypted one. Both rules are covered in [Core concepts](/reference/eql/core-concepts).
## Functions
@@ -100,9 +105,9 @@ Every operator has a function form, for managed platforms that disallow custom o
| Function | Equivalent | Available on |
| --- | --- | --- |
-| `eql_v3.eq(a, b)` / `eql_v3.neq(a, b)` | `=` / `<>` | `_eq`, `_ord` / `_ord_ore` |
-| `eql_v3.lt` / `lte` / `gt` / `gte` | `<` `<=` `>` `>=` | `_ord` / `_ord_ore` |
-| `eql_v3.min(col)` / `eql_v3.max(col)` | aggregate `MIN` / `MAX` | `_ord` / `_ord_ore` |
+| `eql_v3.eq(a, b)` / `eql_v3.neq(a, b)` | `=` / `<>` | `_eq`, all `_ord` variants |
+| `eql_v3.lt` / `lte` / `gt` / `gte` | `<` `<=` `>` `>=` | all `_ord` variants |
+| `eql_v3.min(col)` / `eql_v3.max(col)` | aggregate `MIN` / `MAX` | all `_ord` variants |
**`SUM`, `AVG`, and other arithmetic aggregates are not supported** on encrypted columns — they would require homomorphic encryption. `MIN` / `MAX` work because they only need comparison; for sums and averages, decrypt at the application boundary and aggregate client-side.
@@ -112,15 +117,15 @@ Every operator has a function form, for managed platforms that disallow custom o
```sql
SELECT * FROM employees
-WHERE salary >= $1::public.bigint_ord;
+WHERE salary >= $1::public.eql_v3_bigint_ord;
SELECT * FROM employees
-WHERE salary BETWEEN $1::public.bigint_ord AND $2::public.bigint_ord;
+WHERE salary BETWEEN $1::public.eql_v3_bigint_ord AND $2::public.eql_v3_bigint_ord;
```
### MIN and MAX
-`eql_v3.min` / `eql_v3.max` compare ORE terms — no decryption happens in the database, and the encrypted result decrypts in the client. `NULL` inputs are skipped; an all-`NULL` input set returns `NULL`:
+`eql_v3.min` / `eql_v3.max` compare ordering terms — no decryption happens in the database, and the encrypted result decrypts in the client. `NULL` inputs are skipped; an all-`NULL` input set returns `NULL`:
```sql
SELECT eql_v3.min(salary) FROM employees;
@@ -139,10 +144,10 @@ LIMIT 10;
### Cast at the call site
-On a generic `jsonb` column whose payloads already carry the `ob` term, cast to the right domain in the query:
+On a generic `jsonb` column whose payloads already carry the `op` term, cast to the right domain in the query:
```sql
-SELECT eql_v3.min(salary_jsonb::public.bigint_ord) FROM employees;
+SELECT eql_v3.min(salary_jsonb::public.eql_v3_bigint_ord) FROM employees;
```
## Where to next
diff --git a/content/docs/reference/eql/sorting.mdx b/content/docs/reference/eql/sorting.mdx
index 2529fba..840faea 100644
--- a/content/docs/reference/eql/sorting.mdx
+++ b/content/docs/reference/eql/sorting.mdx
@@ -9,9 +9,11 @@ verifiedAgainst:
-`ORDER BY` on an encrypted column needs an ORE ordering term: it works on `_ord` / `_ord_ore` variants of every scalar and on `text_search`. ORE terms are order-preserving, so the database sorts ciphertext in exactly the order the plaintext would sort — without decrypting anything. Which variants carry the term is covered in [Numbers](/reference/eql/numbers), [Dates & times](/reference/eql/dates-and-times), and [Text](/reference/eql/text); the variant model itself is in [Core concepts](/reference/eql/core-concepts).
+`ORDER BY` on an encrypted column needs an ordering term: it works on the `_ord`, `_ord_ope`, and `_ord_ore` variants of every scalar, and on `text_search` / `text_search_ore`. Ordering terms are order-preserving, so the database sorts ciphertext in exactly the order the plaintext would sort — without decrypting anything. Which variants carry the term is covered in [Numbers](/reference/eql/numbers), [Dates & times](/reference/eql/dates-and-times), and [Text](/reference/eql/text); the variant model itself is in [Core concepts](/reference/eql/core-concepts).
-Sorting a variant *without* an ORE term (`_eq`, `text_match`, bare storage variants) won't raise — but the order is meaningless. Type the column as an `_ord` variant when ordering matters.
+Sorting a variant *without* an ordering term (`_eq`, `text_match`, bare storage variants) won't raise — but the order is meaningless. Type the column as an `_ord` variant when ordering matters.
+
+The mechanism behind the term, CLLW OPE (`op`) or block-ORE (`ob`), changes nothing about how you write the query. `eql_v3.ord_term` is the extractor for `_ord`, `_ord_ope`, and `text_search`; `eql_v3.ord_term_ore` is the extractor for `_ord_ore` and `text_search_ore`. Everything on this page applies to both, with one exception: only the OPE term indexes under Postgres's default btree operator class, so on managed Postgres it is the only mechanism available. See [SEM specifiers](/reference/eql/core-concepts#sem-specifiers).
## Bare form vs extractor form
@@ -35,7 +37,7 @@ CREATE INDEX users_created_at_ord
ANALYZE users;
SELECT * FROM users
- WHERE created_at < $1::public.timestamp_ord
+ WHERE created_at < $1::public.eql_v3_timestamp_ord
ORDER BY eql_v3.ord_term(created_at) DESC
LIMIT 10;
-- Index Scan Backward using users_created_at_ord — no Sort node
@@ -66,7 +68,7 @@ SELECT id, email, created_at FROM users
-- Next page: pass the last row's created_at back, re-encrypted as the cursor
SELECT id, email, created_at FROM users
- WHERE created_at < $1::public.timestamp_ord
+ WHERE created_at < $1::public.eql_v3_timestamp_ord
ORDER BY eql_v3.ord_term(created_at) DESC
LIMIT 20;
```
@@ -81,15 +83,15 @@ If you project the column with a cast and sort on it — `SELECT col::jsonb ...
## Sorting extracted JSON leaves
-String and Number leaves inside an encrypted JSON document carry a CLLW OPE term, so they sort too — the extractor is `eql_v3.ord_ope_term` on the extracted entry:
+String and Number leaves inside an encrypted JSON document carry a CLLW OPE term, so they sort too — the extractor is `eql_v3.ord_term` on the extracted entry:
```sql
SELECT * FROM orders
-ORDER BY eql_v3.ord_ope_term(metadata -> 'total_selector'::text) DESC
+ORDER BY eql_v3.ord_term(metadata -> 'total_selector'::text) DESC
LIMIT 10;
```
-A btree on the same `eql_v3.ord_ope_term(...)` expression streams this ordered, exactly like `ord_term` on a scalar column. Selectors, node types, and which leaves are orderable are covered in [JSON](/reference/eql/json).
+A btree on the same `eql_v3.ord_term(...)` expression streams this ordered, exactly like `ord_term` on a scalar column. Selectors, node types, and which leaves are orderable are covered in [JSON](/reference/eql/json).
## Where to go next
diff --git a/content/docs/reference/eql/text.mdx b/content/docs/reference/eql/text.mdx
index 7865e8c..810ff25 100644
--- a/content/docs/reference/eql/text.mdx
+++ b/content/docs/reference/eql/text.mdx
@@ -1,6 +1,6 @@
---
title: Text
-description: "The complete reference for encrypted text columns: all six text domain variants, the multi-term payload, why LIKE is gone everywhere, and bloom-filter token containment as the encrypted free-text match."
+description: "The complete reference for encrypted text columns: every text domain variant, the multi-term payload, why LIKE is gone everywhere, and bloom-filter token containment as the encrypted free-text match."
type: reference
components: [eql]
verifiedAgainst:
@@ -9,20 +9,22 @@ verifiedAgainst:
-Text is the richest encrypted scalar. Beyond the four variants every scalar type gets, `text` adds two of its own: `text_match` for encrypted free-text matching, and `text_search` for columns you need to look up, sort, *and* search. Emails, names, tax IDs, addresses — this page is the full surface for all of them.
+Text is the richest encrypted scalar. Beyond the variants every scalar type gets, `text` adds its own: `text_match` for encrypted free-text matching, and `text_search` for columns you need to look up, sort, *and* search. Emails, names, tax IDs, addresses — this page is the full surface for all of them.
## Variants
-All six are `jsonb`-backed domains. Which one you declare fixes the column's query capability — the variant model itself is covered in [Core concepts](/reference/eql/core-concepts):
+All of these are `jsonb`-backed domains. Which one you declare fixes the column's query capability — the variant model itself is covered in [Core concepts](/reference/eql/core-concepts):
| Domain variant | Capability |
| --- | --- |
-| `public.text` | Storage and decryption only. |
-| `public.text_eq` | Equality: `=`, `<>`, `IN`, `GROUP BY`, `DISTINCT`, equijoins. |
-| `public.text_ord` | Comparisons, `BETWEEN`, `ORDER BY`, `MIN` / `MAX` — plus equality. |
-| `public.text_ord_ore` | As `text_ord`, with the ORE mechanism pinned — see [SEM specifiers](#sem-specifiers). |
-| `public.text_match` | Free-text token containment: `@>` / `<@`. |
-| `public.text_search` | Equality + ordering + token containment. |
+| `public.eql_v3_text` | Storage and decryption only. |
+| `public.eql_v3_text_eq` | Equality: `=`, `<>`, `IN`, `GROUP BY`, `DISTINCT`, equijoins. |
+| `public.eql_v3_text_ord` | Comparisons, `BETWEEN`, `ORDER BY`, `MIN` / `MAX` — plus equality. |
+| `public.eql_v3_text_ord_ope` | The byte-identical twin of `text_ord`, with OPE pinned. See [SEM specifiers](#sem-specifiers). |
+| `public.eql_v3_text_ord_ore` | As `text_ord`, with the ORE mechanism pinned. |
+| `public.eql_v3_text_match` | Free-text token containment: `@>` / `<@`. |
+| `public.eql_v3_text_search` | Equality + ordering + token containment. |
+| `public.eql_v3_text_search_ore` | As `text_search`, with the ORE mechanism pinned. |
Declare only the capabilities you query on — each capability stores extra searchable material with defined leakage (see [Searchable encryption](/concepts/searchable-encryption)).
@@ -33,10 +35,10 @@ A users table mixing the variants by how each column is queried:
```sql
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
- email public.text_search, -- lookup, sort, and free-text match
- name public.text_match, -- free-text match only
- tax_id public.text_eq, -- exact lookup only
- notes public.text -- store and decrypt only
+ email public.eql_v3_text_search, -- lookup, sort, and free-text match
+ name public.eql_v3_text_match, -- free-text match only
+ tax_id public.eql_v3_text_eq, -- exact lookup only
+ notes public.eql_v3_text -- store and decrypt only
);
```
@@ -44,12 +46,19 @@ CREATE TABLE users (
Text takes the same mechanism specifiers as the other orderable types (the concept is defined in [Core concepts](/reference/eql/core-concepts#sem-specifiers)):
-| Specifier | Meaning |
-| --- | --- |
-| `_ord` | Orderable, using EQL's default mechanism (currently ORE). |
-| `_ord_ore` | Orderable via ORE, pinned explicitly. |
+| Specifier | Mechanism | Ordering term | Extractor |
+| --- | --- | --- | --- |
+| `_ord` | The default, currently CLLW OPE | `op` | `eql_v3.ord_term` |
+| `_ord_ope` | CLLW OPE, pinned explicitly | `op` | `eql_v3.ord_term` |
+| `_ord_ore` | Block-ORE, pinned explicitly | `ob` | `eql_v3.ord_term_ore` |
+| `text_search` | The default, currently CLLW OPE | `op` | `eql_v3.ord_term` |
+| `text_search_ore` | Block-ORE, pinned explicitly | `ob` | `eql_v3.ord_term_ore` |
+
+`text_ord` and `text_ord_ope` are byte-identical today. Pin `_ord_ope` when you want a column's mechanism frozen against a future change of the default.
-The EQL v3 release adds an OPE specifier for every orderable type — including `text` — so lexicographic ordering can be pinned to either mechanism; unspecified `_ord` columns keep tracking the default.
+
+Block-ORE terms sort only under a custom btree operator class, and creating one requires superuser. Where the EQL installer runs as a non-superuser, which is the case on most managed Postgres including cloud Supabase, it cannot create the class, so it **disables every ORE-backed domain** rather than let them install half-working. `text_ord_ore` and `text_search_ore` then raise `feature_not_supported` on the first value written to them. Use `text_ord` or `text_search` there.
+
## Payload
@@ -61,22 +70,24 @@ A value for a `text_search` column carries the shared envelope keys (`v`, `i`, `
"i": { "t": "users", "c": "email" },
"c": "mBbKmsMM%bK#QQOx1yLDBHyD...",
"hm": "9c8ec1d2f9932b979b1bf3f09f8a4e2f6a41f8de2f0c8b7a52e1f5c3d4b6a790",
- "ob": ["7a1fd0c2...", "d24c9be1...", "03fa66b8..."],
+ "op": "5f2b1a9e4c07d38b6ea15c92...",
"bf": [42, 1290, -8113, 30201]
}
```
- `hm` — equality term: `WHERE email = $1` compares this
-- `ob` — ordering term: `ORDER BY` and range comparisons walk these blocks
+- `op` — ordering term: a hex-encoded CLLW OPE ciphertext, which `ORDER BY` and range comparisons sort by native `bytea` comparison
- `bf` — bloom-filter term: `@>` token containment tests these bit positions
-The narrower variants carry only their own term: a `text_eq` payload carries `hm` only, `text_match` carries `bf` only, and `text_ord` / `text_ord_ore` carry `ob` only (no `hm` — equality on `_ord` variants compares ORE terms, see [Core concepts](/reference/eql/core-concepts)). A payload missing its variant's required term fails the domain `CHECK` at write time.
+A `text_search_ore` payload carries `ob` in place of `op`: an array of block-ORE ciphertexts rather than a single string.
+
+The narrower variants carry only their own terms. A `text_eq` payload carries `hm` alone, `text_match` carries `bf` alone, and the orderable variants carry `hm` plus their ordering term (`op` for `text_ord` / `text_ord_ope`, `ob` for `text_ord_ore`). Text keeps `hm` on every orderable variant, because ordering over text is not equality-lossless the way it is over the numeric scalars: `=` and `<>` resolve against the HMAC term rather than the ordering term. A payload missing its variant's required term fails the domain `CHECK` at write time.
**`bf` positions are signed**: EQL stores the filter as PostgreSQL `smallint[]`, and filters sized above 32768 emit upper-half bit positions as *negative* signed values. Consumers must use a signed 16-bit integer type.
## Operators
-| SQL operator | `public.text` | `text_eq` | `text_ord` / `text_ord_ore` | `text_match` | `text_search` |
+| SQL operator | `public.eql_v3_text` | `text_eq` | `text_ord` variants | `text_match` | `text_search` variants |
| --- | :---: | :---: | :---: | :---: | :---: |
| `=` / `<>` | ❌ | ✅ | ✅ | ❌ | ✅ |
| `<` `<=` `>` `>=` | ❌ | ❌ | ✅ | ❌ | ✅ |
@@ -86,7 +97,7 @@ The narrower variants carry only their own term: a `text_eq` payload carries `hm
| `ORDER BY` | ❌ | ❌ | ✅ | ❌ | ✅ |
| `IS NULL` / `IS NOT NULL` | ✅ | ✅ | ✅ | ✅ | ✅ |
-Blocked *operator* cells raise an `operator … is not supported` exception — they never silently return wrong rows. `ORDER BY` is the one blocked cell that doesn't raise: it isn't an operator, so sorting a variant without an ordering term runs — but the order is meaningless (see [Sorting](/reference/eql/sorting)). Operands must be typed (`$1::public.text_eq`), or PostgreSQL resolves the native `jsonb` operator instead of the encrypted one. Both rules are covered in [Core concepts](/reference/eql/core-concepts).
+Blocked *operator* cells raise an `operator … is not supported` exception — they never silently return wrong rows. `ORDER BY` is the one blocked cell that doesn't raise: it isn't an operator, so sorting a variant without an ordering term runs — but the order is meaningless (see [Sorting](/reference/eql/sorting)). Operands must be typed (`$1::public.eql_v3_text_eq`), or PostgreSQL resolves the native `jsonb` operator instead of the encrypted one. Both rules are covered in [Core concepts](/reference/eql/core-concepts).
## Functions
@@ -94,10 +105,10 @@ Every operator has a function form, for managed platforms that disallow custom o
| Function | Equivalent | Available on |
| --- | --- | --- |
-| `eql_v3.eq(a, b)` / `eql_v3.neq(a, b)` | `=` / `<>` | `text_eq`, `text_ord` / `text_ord_ore`, `text_search` |
-| `eql_v3.lt` / `lte` / `gt` / `gte` | `<` `<=` `>` `>=` | `text_ord` / `text_ord_ore`, `text_search` |
-| `eql_v3.contains(a, b)` / `eql_v3.contained_by(a, b)` | `@>` / `<@` | `text_match`, `text_search` |
-| `eql_v3.min(col)` / `eql_v3.max(col)` | aggregate `MIN` / `MAX` | `text_ord` / `text_ord_ore`, `text_search` |
+| `eql_v3.eq(a, b)` / `eql_v3.neq(a, b)` | `=` / `<>` | `text_eq`, all `text_ord` variants, all `text_search` variants |
+| `eql_v3.lt` / `lte` / `gt` / `gte` | `<` `<=` `>` `>=` | all `text_ord` variants, all `text_search` variants |
+| `eql_v3.contains(a, b)` / `eql_v3.contained_by(a, b)` | `@>` / `<@` | `text_match`, all `text_search` variants |
+| `eql_v3.min(col)` / `eql_v3.max(col)` | aggregate `MIN` / `MAX` | all `text_ord` variants, all `text_search` variants |
There are no `like` / `ilike` function forms — encrypted text matching is `eql_v3.contains` on a `text_match` value.
@@ -110,7 +121,7 @@ There are no `like` / `ilike` function forms — encrypted text matching is `eql
SELECT * FROM users WHERE email LIKE '%alice%';
-- ✅ Encrypted free-text match
-SELECT * FROM users WHERE email @> $1::public.text_match;
+SELECT * FROM users WHERE email @> $1::public.eql_v3_text_match;
```
`@>` / `<@` here is **probabilistic ngram-bloom containment** — it tests whether the encrypted text contains the (encrypted) search terms. It is not JSONB containment and not `LIKE`. The client encrypts the search term into a bloom-filter query value; false positives are possible, false negatives are not. There are no `like` / `ilike` function forms either — text matching is `eql_v3.contains` on a `text_match` value.
@@ -122,10 +133,10 @@ SELECT * FROM users WHERE email @> $1::public.text_match;
Equality on a `text_eq` column compares HMAC terms. `IN` desugars to `=`:
```sql
-SELECT * FROM users WHERE tax_id = $1::public.text_eq;
+SELECT * FROM users WHERE tax_id = $1::public.eql_v3_text_eq;
SELECT * FROM users
-WHERE tax_id IN ($1::public.text_eq, $2::public.text_eq);
+WHERE tax_id IN ($1::public.eql_v3_text_eq, $2::public.eql_v3_text_eq);
```
### Free-text match
@@ -133,10 +144,10 @@ WHERE tax_id IN ($1::public.text_eq, $2::public.text_eq);
The client encrypts the search term into the bloom-filter needle:
```sql
-SELECT * FROM users WHERE name @> $1::public.text_match;
+SELECT * FROM users WHERE name @> $1::public.eql_v3_text_match;
-- Function form, for platforms without custom operators
-SELECT * FROM users WHERE eql_v3.contains(name, $1::public.text_match);
+SELECT * FROM users WHERE eql_v3.contains(name, $1::public.eql_v3_text_match);
```
### The works: `text_search`
@@ -145,15 +156,15 @@ A `text_search` column answers exact lookup, free-text match, and ordering — h
```sql
SELECT id, email FROM users
-WHERE email @> $1::public.text_match -- token containment on bf
- AND email <> $2::public.text_eq -- exclude an exact value via hm
+WHERE email @> $1::public.eql_v3_text_match -- token containment on bf
+ AND email <> $2::public.eql_v3_text_eq -- exclude an exact value via hm
ORDER BY eql_v3.ord_term(email) -- sort on ob
LIMIT 20;
```
### Sorting text
-ORE terms are order-preserving, so `ORDER BY` sorts encrypted text correctly. Write the sort key in extractor form so a btree index can do the ordering instead of a `Sort` node — see [Sorting](/reference/eql/sorting):
+Ordering terms are order-preserving, so `ORDER BY` sorts encrypted text correctly. Write the sort key in extractor form so a btree index can do the ordering instead of a `Sort` node — see [Sorting](/reference/eql/sorting):
```sql
SELECT * FROM users
diff --git a/content/docs/reference/proxy/configuration.mdx b/content/docs/reference/proxy/configuration.mdx
index 4316a2f..e98b04a 100644
--- a/content/docs/reference/proxy/configuration.mdx
+++ b/content/docs/reference/proxy/configuration.mdx
@@ -53,7 +53,7 @@ For local development, create a client key and access key in the [Dashboard](htt
## Setting up the database schema
-Proxy queries encrypted data through [EQL](/reference/eql), which must be installed in the target database. Encrypted columns are typed with an EQL domain such as `public.text_eq`, and that type is what fixes the column's searchable capability. There is no database-side configuration table.
+Proxy queries encrypted data through [EQL](/reference/eql), which must be installed in the target database. Encrypted columns are typed with an EQL domain such as `public.eql_v3_text_eq`, and that type is what fixes the column's searchable capability. There is no database-side configuration table.
- [Install EQL](/reference/eql) covers the install itself, including the permissions split and Supabase.
- [Core concepts](/reference/eql/core-concepts) covers the variant model and which domain type to choose.
diff --git a/content/docs/reference/proxy/errors.mdx b/content/docs/reference/proxy/errors.mdx
index 76c1e0d..c6dd510 100644
--- a/content/docs/reference/proxy/errors.mdx
+++ b/content/docs/reference/proxy/errors.mdx
@@ -152,7 +152,7 @@ When Proxy decrypts data, it casts and encodes the result as the PostgreSQL repr
Proxy has no encrypted-column mapping for this column. In EQL v3 a column's searchable capability is fixed by the [domain variant](/reference/eql/core-concepts) it is typed as, so a column Proxy cannot map is usually one that was never given an EQL type:
```sql
-ALTER TABLE users ALTER COLUMN email TYPE public.text_eq;
+ALTER TABLE users ALTER COLUMN email TYPE public.eql_v3_text_eq;
```
Proxy reloads the database schema on the interval set by `CS_DATABASE__SCHEMA_RELOAD_INTERVAL` (60 seconds by default), so a newly typed column may take up to that long to become mappable.
diff --git a/scripts/generate-eql-api-docs.ts b/scripts/generate-eql-api-docs.ts
index 0967d92..727e050 100644
--- a/scripts/generate-eql-api-docs.ts
+++ b/scripts/generate-eql-api-docs.ts
@@ -243,11 +243,13 @@ function driftCheck(manifest: Manifest): string[] {
const text = fs.readFileSync(path.join(EQL_DIR, file), "utf8");
// Any schema-qualified reference — function call, domain cast, or type.
// A trailing `*` marks a prose family (e.g. `eql_v3.jsonb_path_*`), which
- // names a set rather than one symbol, so it's skipped.
+ // names a set rather than one symbol, so it's skipped. So is a trailing
+ // `` placeholder (e.g. `public.eql_v3__ord`).
for (const m of text.matchAll(
/\b(public|eql_v3_internal|eql_v3)\.([a-z0-9_]+)(\*?)/g,
)) {
if (m[3] === "*") continue;
+ if (text[(m.index ?? 0) + m[0].length] === "<") continue;
const fqn = `${m[1]}.${m[2]}`;
const pages = referenced.get(fqn) ?? new Set();
pages.add(file);
@@ -257,11 +259,25 @@ function driftCheck(manifest: Manifest): string[] {
const unknown: string[] = [];
for (const [fqn, pages] of referenced) {
- if (!known.has(fqn)) unknown.push(`${fqn} (in ${[...pages].join(", ")})`);
+ if (known.has(fqn) || MANIFEST_BLIND_SPOTS.has(fqn)) continue;
+ unknown.push(`${fqn} (in ${[...pages].join(", ")})`);
}
return unknown.sort();
}
+// Symbols that EQL really ships but the manifest's catalog does not list, so
+// the drift check would reject a correct reference.
+//
+// The scalar query-operand domains (`eql_v3.query_text_eq`, and 39 siblings)
+// are created inside a `DO ... EXECUTE` block in the install SQL, which the
+// catalog generator does not walk — `eql_v3.query_jsonb`, created at the top
+// level, IS in the manifest. Verified present in the 3.0.0 install SQL:
+// grep -c "CREATE DOMAIN eql_v3.query_" cipherstash-encrypt.sql # => 40
+//
+// Keep this list minimal, and delete entries as the manifest grows to cover
+// them.
+const MANIFEST_BLIND_SPOTS = new Set(["eql_v3.query_text_eq"]);
+
// ── Main ─────────────────────────────────────────────────────────────────────
function main() {
const manifest = loadManifest();
diff --git a/scripts/generate-eql-docs.ts b/scripts/generate-eql-docs.ts
index b7a5f7c..3b72377 100644
--- a/scripts/generate-eql-docs.ts
+++ b/scripts/generate-eql-docs.ts
@@ -28,7 +28,7 @@ import GithubSlugger from "github-slugger";
* prerelease and churning (alpha.3 and alpha.4 shipped a day apart), so this
* tracks an alpha deliberately rather than by accident.
*/
-const EQL_RELEASE_TAG = process.env.EQL_RELEASE_TAG ?? "eql-3.0.0-alpha.4";
+const EQL_RELEASE_TAG = process.env.EQL_RELEASE_TAG ?? "eql-3.0.0";
const GITHUB_RELEASE_DOWNLOAD =
"https://github.com/cipherstash/encrypt-query-language/releases/download";