Skip to content
44 changes: 1 addition & 43 deletions content/docs/reference/eql/dates-and-times.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,49 +95,7 @@ Blocked *operator* cells raise an `operator … is not supported` exception —

## Functions

Every operator has a function form, for managed platforms that disallow custom operators — same typed arguments, identical resolution. The `MIN` / `MAX` aggregates only exist as functions:

| Function | Equivalent | Available on |
| --- | --- | --- |
| `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

### Time window

```sql
SELECT * FROM audit_events
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.eql_v3_date_ord AND $2::public.eql_v3_date_ord;
```

### Retention cutoff

```sql
SELECT id FROM audit_events
WHERE occurred_at < $1::public.eql_v3_timestamp_ord;
```

### Newest-first listing

Write the sort key in extractor form to stream rows out of the index already ordered — at large row counts this is the difference between seconds and milliseconds (see [Sorting](/reference/eql/sorting)):

```sql
SELECT * FROM audit_events
WHERE occurred_at >= $1::public.eql_v3_timestamp_ord
ORDER BY eql_v3.ord_term(occurred_at) DESC
LIMIT 10;
```

### First and last event

```sql
SELECT eql_v3.min(occurred_at), eql_v3.max(occurred_at) FROM audit_events;
```
<include cwd>content/partials/eql/functions-dates-and-times.mdx</include>

## Where to next

Expand Down
118 changes: 81 additions & 37 deletions content/docs/reference/eql/json.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,85 +111,129 @@ Use containment (`@>` / `<@`), field access (`->` / `->>`), or the `eql_v3.jsonb
**Operands must be typed** (`doc -> 'email'::text`, not `doc -> 'email'`) — an untyped operand resolves the native `jsonb` operator, bypassing both the encrypted operator and the blockers. See [Core concepts](/reference/eql/core-concepts).
</Callout>

## Containment: `@>` and `<@`
## Functions

`@>` 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):
Every JSON query addresses paths by **selector hash** — the deterministic identifier the client emits for a JSON path during encryption, not a plaintext path like `$.customer.tier`. Operands must be typed, or PostgreSQL resolves the native `jsonb` operator instead of the encrypted one.

The examples below all query one encrypted `metadata` document. In plaintext:

```json
{
"customer": {
"tier": "premium",
"region": "apac"
},
"total": 149.95,
"items": ["sku-1042", "sku-2210"]
}
```

The `*_selector` placeholders stand for the selector hash of each path: `region_selector` for `$.customer.region`, `total_selector` for `$.total`, and `items_selector` for `$.items`.

### Containment [#fn-contains]

`@>` tests whether the encrypted document contains a structure; `<@` is the reverse. Build the needle in the client and cast it to `eql_v3.query_jsonb`; `eql_v3.ste_vec_contains(a, b)` is the function form.

<EqlFn ops="@>,<@" domains="eql_v3_json">

```sql
SELECT * FROM orders
WHERE metadata @> $1::eql_v3.query_jsonb;
```

This is the encrypted equivalent of the plaintext `metadata @> '{"customer": {"tier": "premium"}}'`: containment checks that every encrypted term in the needle exists in the document's `sv` vector. `eql_v3.to_ste_vec_query(doc)` converts a stored document into the needle shape, and `eql_v3.ste_vec_contains(a, b)` is the function form backing `@>`.
</EqlFn>

For large tables, back containment with a GIN index. The typed `@>` overload inlines to a native `jsonb @>` over `eql_v3.to_ste_vec_query(col)::jsonb`, so a GIN index on that same expression engages:
For large tables, back containment with a GIN index. The typed `@>` inlines to a native `jsonb @>` over `eql_v3.to_ste_vec_query(col)::jsonb`, so a GIN index on that same expression engages:

```sql
CREATE INDEX orders_metadata_gin
ON orders USING gin (eql_v3.to_ste_vec_query(metadata)::jsonb jsonb_path_ops);
ANALYZE orders;
```

See [Indexes](/reference/eql/indexes) for the full recipes.
### Field access [#fn-field-access]

## Field access: `->` and `->>`
`->` returns a `public.eql_v3_jsonb_entry`; `->>` serializes that entry as ciphertext text. Fields are addressed by selector hash, and array elements by 0-based index.

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`.
<EqlFn ops="->,->>" domains="eql_v3_json">

```sql
-- Field access by selector (returns public.eql_v3_jsonb_entry)
SELECT metadata -> 'selector_hash'::text FROM orders;
SELECT metadata -> 'selector_hash'::text FROM orders; -- entry
SELECT metadata ->> 'selector_hash'::text FROM orders; -- entry as ciphertext text
SELECT metadata -> 0 FROM orders; -- array element by index
```

-- The entry serialized as text (ciphertext JSON, not decrypted plaintext)
SELECT metadata ->> 'selector_hash'::text FROM orders;
</EqlFn>

-- Array element by 0-based index
SELECT metadata -> 0 FROM orders;
```
The extracted `public.eql_v3_jsonb_entry` is itself comparable.

The extracted `public.eql_v3_jsonb_entry` is itself comparable:
### eql_v3.eq_term [#fn-eq-term]

- `=` / `<>` resolve via `eql_v3.eq_term` — works on every node type
- `<` / `<=` / `>` / `>=` 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
Equality on an extracted leaf, via `eql_v3.eq_term`. Works on every node type, and drives `GROUP BY` / `DISTINCT`.

```sql
-- Equality on an extracted leaf
SELECT * FROM orders
WHERE metadata -> 'email_selector'::text = $1::public.eql_v3_jsonb_entry;
<EqlFn ops="=,<>" domains="eql_v3_jsonb_entry">

-- Group by an extracted leaf's equality term
```sql
SELECT eql_v3.eq_term(metadata -> 'region_selector'::text) AS region, COUNT(*)
FROM orders
GROUP BY eql_v3.eq_term(metadata -> 'region_selector'::text);
```

A hash index on `eql_v3.eq_term(col -> '<selector>'::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).
</EqlFn>

### eql_v3.ord_term [#fn-ord-term]

## Path queries and array helpers
Range comparisons and `ORDER BY` on an extracted **String or Number** leaf, via `eql_v3.ord_term`.

The function forms take the same selector hashes:
<EqlFn ops="<,<=,>,>=" domains="eql_v3_jsonb_entry">

```sql
-- All entries matching a selector
SELECT eql_v3.jsonb_path_query(metadata, 'selector_hash') FROM orders;
SELECT * FROM orders
WHERE (metadata -> 'total_selector'::text) > $1::public.eql_v3_jsonb_entry;
```

</EqlFn>

-- First match only
SELECT eql_v3.jsonb_path_query_first(metadata, 'selector_hash') FROM orders;
A hash index on `eql_v3.eq_term(col -> '<selector>'::text)` engages the equality lookup; a btree on `eql_v3.ord_term(...)` engages range and `ORDER BY`. See [Indexes](/reference/eql/indexes).

-- Does the selector exist in this document?
SELECT eql_v3.jsonb_path_exists(metadata, 'selector_hash') FROM orders;
### eql_v3.min / max [#fn-min-max]

`MIN` / `MAX` over an extracted ordered leaf.

<EqlFn agg ops="MIN,MAX" domains="eql_v3_jsonb_entry">

```sql
SELECT eql_v3.min(metadata -> 'total_selector'::text) FROM orders;
```

For encrypted array nodes:
</EqlFn>

### eql_v3.jsonb_path_query [#fn-path-query]

Path queries take the same selector hashes. `jsonb_path_query` returns every matching entry, `jsonb_path_query_first` the first, and `jsonb_path_exists` a boolean.

<EqlFn domains="eql_v3_json">

```sql
SELECT eql_v3.jsonb_path_query(metadata, 'selector_hash') FROM orders; -- all matches
SELECT eql_v3.jsonb_path_query_first(metadata, 'selector_hash') FROM orders; -- first match
SELECT eql_v3.jsonb_path_exists(metadata, 'selector_hash') FROM orders; -- boolean
```

</EqlFn>

### eql_v3.jsonb_array_* [#fn-array]

Helpers over an encrypted array node. `jsonb_array_elements` yields encrypted entries; `jsonb_array_elements_text` yields each element as ciphertext text.

<EqlFn domains="eql_v3_json">

```sql
SELECT eql_v3.jsonb_array_length(metadata -> 'items_selector'::text) FROM orders;
SELECT eql_v3.jsonb_array_elements(metadata -> 'items_selector'::text) FROM orders;
SELECT eql_v3.jsonb_array_length(metadata -> 'items_selector'::text) FROM orders;
SELECT eql_v3.jsonb_array_elements(metadata -> 'items_selector'::text) FROM orders;
SELECT eql_v3.jsonb_array_elements_text(metadata -> 'items_selector'::text) FROM orders;
```

`jsonb_array_elements` yields encrypted entries; `jsonb_array_elements_text` yields each element as ciphertext text.
</EqlFn>

## Worked example

Expand Down
47 changes: 1 addition & 46 deletions content/docs/reference/eql/numbers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,55 +101,10 @@ Blocked *operator* cells raise an `operator … is not supported` exception —

## Functions

Every operator has a function form, for managed platforms that disallow custom operators — same typed arguments, identical resolution. The `MIN` / `MAX` aggregates only exist as functions:

| Function | Equivalent | Available on |
| --- | --- | --- |
| `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 |
<include cwd>content/partials/eql/functions-numbers.mdx</include>

**`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.

## Example queries

### Range filter

```sql
SELECT * FROM employees
WHERE salary >= $1::public.eql_v3_bigint_ord;

SELECT * FROM employees
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 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;
SELECT eql_v3.max(salary) FROM employees;
```

### Sorted listing

Write the sort key in extractor form to stream rows out of the index already ordered (see [Sorting](/reference/eql/sorting) for why):

```sql
SELECT * FROM employees
ORDER BY eql_v3.ord_term(salary) DESC
LIMIT 10;
```

### Cast at the call site

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.eql_v3_bigint_ord) FROM employees;
```

## Where to next

<Cards>
Expand Down
61 changes: 1 addition & 60 deletions content/docs/reference/eql/text.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,7 @@ Blocked *operator* cells raise an `operator … is not supported` exception —

## Functions

Every operator has a function form, for managed platforms that disallow custom operators — same typed arguments, identical resolution. The `MIN` / `MAX` aggregates only exist as functions:

| Function | Equivalent | Available on |
| --- | --- | --- |
| `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 |
<include cwd>content/partials/eql/functions-text.mdx</include>

There are no `like` / `ilike` function forms — encrypted text matching is `eql_v3.contains` on a `text_match` value.

Expand All @@ -126,58 +119,6 @@ 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.

## Example queries

### Exact lookup

Equality on a `text_eq` column compares HMAC terms. `IN` desugars to `=`:

```sql
SELECT * FROM users WHERE tax_id = $1::public.eql_v3_text_eq;

SELECT * FROM users
WHERE tax_id IN ($1::public.eql_v3_text_eq, $2::public.eql_v3_text_eq);
```

### Free-text match

The client encrypts the search term into the bloom-filter needle:

```sql
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.eql_v3_text_match);
```

### The works: `text_search`

A `text_search` column answers exact lookup, free-text match, and ordering — here, all three in one query:

```sql
SELECT id, email FROM users
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

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
ORDER BY eql_v3.ord_term(email)
LIMIT 50;
```

`MIN` / `MAX` work on any ord-capable text column too:

```sql
SELECT eql_v3.min(email) FROM users;
```

## Where to next

<Cards>
Expand Down
Loading