Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/stash-postgres-signed-range.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'stash': patch
---

Document how to express an absolute-value bucket over an EQL v3 ordered
column by decomposing it into positive and negative signed ranges. The
`stash-postgres` recipe encrypts all four bounds, shows the reversed negative
inequalities, and states the required bound validation.
46 changes: 44 additions & 2 deletions skills/stash-postgres/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ query domain always matches the *column's* domain, not the query type.
### Free-text match

```ts
// `bio` is a types.TextSearch column here; on a types.TextMatch column the
// cast is ::eql_v3.query_text_match.
// `bio` is a types.TextSearch column, so its query operand is
// eql_v3.query_text_search. A types.TextMatch column uses query_text_match.
const term = await client.encryptQuery('needle', {
table: users, column: users.bio, queryType: 'freeTextSearch',
})
Expand Down Expand Up @@ -327,6 +327,48 @@ raw encrypted payload — which is neither meaningful nor index-backed. Sorting
on `eql_v3.ord_term(col)` is both. Ordering is available on `_ord`,
`_ord_ore`, and `text_search` columns; use `ord_term_ore` for `_ord_ore`.

### Absolute-value ranges

Encrypted ordering cannot evaluate `abs(encrypted_column)`: the server can
compare encrypted order terms, but cannot transform their plaintext values.
Decompose an absolute-value bucket into its positive and negative signed
ranges. For non-negative bounds `min` and `max`, the half-open predicate
`min <= abs(amount) < max` is:

```sql
(amount >= min AND amount < max)
OR
(amount <= -min AND amount > -max)
```

Encrypt all four bounds separately against the same column. For example, when
`amount` is a `types.DoubleOrd()` column:

```ts
const query = async (value: number) => {
const term = await client.encryptQuery(value, {
table: payments, column: payments.amount, queryType: 'orderAndRange',
})
if (term.failure) throw new Error(term.failure.message)
return term.data
}

const [min, max, negativeMin, negativeMax] = await Promise.all([
query(10), query(20), query(-10), query(-20),
])

await sql`SELECT * FROM payments WHERE
(amount >= ${sql.json(min)}::jsonb::eql_v3.query_double_ord
AND amount < ${sql.json(max)}::jsonb::eql_v3.query_double_ord)
OR
(amount <= ${sql.json(negativeMin)}::jsonb::eql_v3.query_double_ord
AND amount > ${sql.json(negativeMax)}::jsonb::eql_v3.query_double_ord)`
```

The inequalities deliberately reverse on the negative branch. At `min = 0`
the two branches overlap at zero; that does not duplicate rows because this is
one `WHERE` predicate. Validate `0 <= min < max` before constructing it.

### Encrypted JSON — containment

```ts
Expand Down
Loading