Skip to content

Commit cd43b39

Browse files
committed
docs(test-kit): correct the ORE deferral reason — unusable, not silently wrong
Measured on supabase/postgres as the non-superuser `postgres` role: the ORE domains are created, but their CHECK calls `ore_domain_unavailable()` and the first INSERT raises, with a hint naming the alternatives. The same insert succeeds on plain Postgres as a superuser. So ORE is loudly unusable on managed Postgres, not silently wrong. The bundle guards correctly. The earlier reason — 'ORDER BY silently mis-sorts' — described a type-level property (`ore_block_256` is a composite type, so with no opclass Postgres falls back to record comparison) that no value can reach, because the write is refused first. The deferral still stands, for a better reason: a matrix that must pass on both a superuser and a managed database cannot seed an ORE column at all. Corrects the claim in the plan doc, and in cipherstash/encrypt-query-language#395.
1 parent 9b65ae8 commit cd43b39

2 files changed

Lines changed: 20 additions & 29 deletions

File tree

docs/superpowers/plans/2026-07-10-eql-v3-integration-tests.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,8 @@
2626
- **v3 JSON is unimplemented, not merely untested.** The bundle ships `public.eql_v3_json` and `public.eql_v3_jsonb_entry` (51 `CREATE DOMAIN`s, 56 `eql_v3.{ste_vec,jsonb_path,selector,contained_by}` functions), but the SDK models 41 domains and `eql/v3/columns.ts` admits only `bigint | boolean | date | number | string | timestamp` as `cast_as`. There is no `'json'` kind, so a v3 JSON column cannot be declared. **The gap is in the core v3 schema; both adapters inherit it.** `eql/v3/drizzle/codec.ts:38` already carries defensive SteVec decode logic (`sv[0].c`) for documents nothing can currently create.
2727
- **`@cipherstash/stack/drizzle` is not `@cipherstash/drizzle`.** They are a fork: `@cipherstash/drizzle@3.0.3` peer-depends on the predecessor SDK `@cipherstash/protect@12` and exports `createProtectOperators` (2,038 lines); stack's in-tree copy exports `createEncryptionOperators` (1,945 lines); ~645 lines have diverged. There is no dependency between them — hence no cycle today, only duplication. Docs reference the stack subpath 13× and the package 2×.
2828
- **Ordering is safe on OPE.** EQL 3.0.0 pins `_ord` domains to `op` (CLLW-OPE), which orders via a native `bytea` btree, so `ORDER BY eql_v3.ord_term(col)` works on every provider without superuser.
29-
- **`_ord_ore` is not "absent" on managed Postgres — it is silently wrong, and only for `ORDER BY`.** Measured against `supabase/postgres:17.4.1.048` after `stash eql install --eql-version 3 --supabase --direct` as the non-superuser `postgres` role: all 51 `public.eql_v3_*` domains install, and so do the 33 ORE comparison functions; only the btree opclass `eql_v3_internal.ore_block_256_operator_class` is skipped (`pg_opclass` count 0 vs 1 on plain Postgres).
30-
- **Range filters still work everywhere.** `<`/`>`/`<=`/`>=` on `ore_block_256` are backed by `ore_block_256_lt` → the ORE comparator, and operators need no opclass.
31-
- **`ORDER BY eql_v3.ord_term_ore(col)` does not error — it sorts, wrongly.** `ore_block_256` is a *composite* type, so with no opclass to resolve, Postgres falls back to built-in record comparison, which walks down to the raw `bytes` field and compares bytewise. That order is deterministic and stable but uncorrelated with plaintext: over 200 random well-formed terms it disagreed with the ORE comparator on **87**. Proof that the two variants take different paths: `ORDER BY` on a malformed 1-byte term returns rows on Supabase and raises `Malformed ORE term` on plain Postgres.
32-
- A btree **index** on such a column cannot be created at all.
33-
- That is why ORE gets its own suite rather than a skip. See `docs/eql-v3-ord-term-ordering-defect.md`.
29+
- **`_ord_ore` columns cannot hold data on managed Postgres — and that is correct.** Measured against `supabase/postgres:17.4.1.048`, EQL 3.0.0, as the non-superuser `postgres` role: the nine ORE domains are created, but their CHECK calls `eql_v3_internal.ore_domain_unavailable()`, so the first `INSERT` raises with a hint naming `_eq` / `_ord` / `_ord_ope` as alternatives. The same insert succeeds on plain Postgres as a superuser. The bundle guards correctly; there is no silent wrongness. (An earlier draft of this plan claimed ORDER BY silently mis-sorts. It does at the *type* level — `ore_block_256` is a composite type, so with no opclass Postgres falls back to record comparison — but no value can reach a table to be ordered. `cipherstash/encrypt-query-language#395` records the correction.) A matrix that must pass on both databases cannot cover ORE, because the seed insert fails on one; ORE gets a superuser-only suite.
30+
- **Supabase CAN order encrypted `_ord` columns.** A bare `ORDER BY col` sorts the ciphertext envelope through jsonb's default opclass (measured: `r00,r04,r08,r01,…` for plaintext `r00..r09`). But `eql_v3.ord_term` returns the `op` term, OPE is order-preserving, and PostgREST can emit the jsonb path `order=col->>op`, which reproduces plaintext order in both directions for `integer_ord` and `text_search`. The adapter now does exactly that, and rejects only ORE-backed and ordering-less columns.
3431
- **The CLI installs fine as a non-superuser.** `stash eql install --eql-version 3 --supabase --direct --database-url …` runs non-interactively as `postgres` on `supabase/postgres` and grants `anon` USAGE on **both** `eql_v3` and `eql_v3_internal`. No `SUPABASE_ADMIN_URL` is needed. (`--supabase` without `--direct`/`--migration` prompts, so pass `--direct`.)
3532
- **`postgres-eql:17-2.3.1` ships EQL v2, not v3.** Every DB variant must install v3 at setup.
3633
- **`./supabase` and `./drizzle` are published stack subpaths** (`npm view @cipherstash/stack exports`); `./eql/v3/drizzle` is not.

packages/test-kit/src/catalog.ts

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -282,38 +282,32 @@ const BIGINT_ERR = [9223372036854775808n, -9223372036854775809n] as const
282282
// biome-ignore format: one row per domain reads as a table; keep it dense.
283283
/**
284284
* The `_ord_ore` domains are block-ORE, whose operator class is superuser-only:
285-
* the eql-3.0.0 bundle self-skips those statements on `insufficient_privilege`.
285+
* the eql-3.0.0 bundle self-skips that statement on `insufficient_privilege`.
286286
*
287287
* Measured against `supabase/postgres:17.4.1.048` after `stash eql install
288-
* --eql-version 3 --supabase --direct`, connected as the non-superuser
289-
* `postgres` role: all 51 `public.eql_v3_*` domains install, and so do the 33
290-
* ORE comparison functions. Only the btree OPCLASS
291-
* (`eql_v3_internal.ore_block_256_operator_class`) is skipped.
288+
* --eql-version 3 --supabase --direct`, as the non-superuser `postgres` role:
289+
* the domains ARE created, but they cannot hold data. Their CHECK calls
290+
* `eql_v3_internal.ore_domain_unavailable()`, so the first INSERT raises
292291
*
293-
* What that does and does not break:
292+
* ERROR: EQL: public.eql_v3_integer_ord_ore cannot be used on this platform:
293+
* the EQL installer could not create the ORE operator class
294+
* HINT: Use public.eql_v3_integer_eq (equality) or public.eql_v3_integer_ord
295+
* (ordering) or public.eql_v3_integer_ord_ope (ordering) instead.
294296
*
295-
* - RANGE FILTERS STILL WORK. The `<`/`>`/`<=`/`>=` operators on
296-
* `ore_block_256` are backed by `ore_block_256_lt` → the ORE comparator, and
297-
* operators need no opclass. `eql_v3.gt`/`lt`/`gte`/`lte` compare in true ORE
298-
* order on every provider.
299-
* - `ORDER BY eql_v3.ord_term_ore(col)` DOES NOT ERROR — it sorts, wrongly.
300-
* `ore_block_256` is a COMPOSITE type, so with no opclass to resolve, Postgres
301-
* falls back to its built-in record comparison, which walks down to the raw
302-
* `bytes` field and compares bytewise. Bytewise order over ORE ciphertext is
303-
* deterministic and stable but uncorrelated with plaintext order: measured
304-
* over 200 random well-formed terms, it disagreed with the ORE comparator on
305-
* 87 of them. A stable, plausible-looking, silently wrong ordering.
306-
* - A btree INDEX on such a column cannot be created at all.
297+
* The same INSERT succeeds on plain Postgres as a superuser. So ORE is not
298+
* silently wrong on managed Postgres — it is loudly unusable, which is the right
299+
* behaviour and means no query can ever return wrong rows from an ORE column.
307300
*
308-
* A matrix whose ordering assertions must hold on every provider cannot straddle
309-
* that, so ORE gets its own suite.
301+
* A matrix that must pass on both a superuser and a managed database therefore
302+
* cannot cover them: the seed INSERT fails on one of the two. ORE gets a
303+
* superuser-only suite instead.
310304
*
311-
* The OPE-backed `_ord` domains have no such split: they order via a native
312-
* `bytea` btree and behave identically everywhere. See
313-
* `docs/eql-v3-ord-term-ordering-defect.md`.
305+
* (The OPE-backed `_ord` domains have no such split. `eql_v3_internal.ope_cllw`
306+
* is a domain over `bytea`, so it orders via the native btree everywhere, and
307+
* the Supabase adapter sorts by it through the jsonb path `col->>op`.)
314308
*/
315309
const ORE_DEFERRED =
316-
'block-ORE opclass is superuser-only, so ORDER BY silently mis-sorts on managed Postgres; covered by a follow-up suite'
310+
'block-ORE columns cannot hold data on managed Postgres (domain CHECK raises ore_domain_unavailable); covered by a superuser-only follow-up suite'
317311

318312
export const V3_MATRIX = {
319313
// integer

0 commit comments

Comments
 (0)