CipherStashDocs
ReferenceEQL

Numbers

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.

Last updated

EQL version

This reference is generated and validated against EQL 3.0.4. Running EQL 2.x? See the EQL v2 reference.

Six numeric types share one identical query surface: int2, int4, int8, float4, float8, and numeric. These are the columns you filter by range, sort, and take a MIN / MAX over — salaries, totals, rates, quantities.

Date and time columns have the same capabilities but their own semantics — see Dates & times. There is no free-text matching for numeric types — _match and _search are text-only variants.

Variants

Each numeric type generates the same jsonb-backed domain variants. The generic form:

Domain variantCapability
public.eql_v3_<T>Storage and decryption only.
public.eql_v3_<T>_eqEquality: =, <>, IN, GROUP BY, DISTINCT, equijoins.
public.eql_v3_<T>_ordComparisons, BETWEEN, ORDER BY, MIN / MAX — plus equality.
public.eql_v3_<T>_ord_opeThe byte-identical twin of <T>_ord, with OPE pinned. See SEM specifiers.
public.eql_v3_<T>_ord_oreAs <T>_ord, with the ORE mechanism pinned.

<T> is the encrypted type name from the table below, so public.eql_v3_<T>_ord is public.eql_v3_bigint_ord for int8.

And every concrete domain this page covers:

TypeVariants
int2public.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
int4public.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
int8public.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
float4public.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
float8public.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
numericpublic.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), and the variant model itself is covered in Core concepts.

Example

A payroll table mixing the variants by how each column is queried:

CREATE TABLE employees (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    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
);

SEM specifiers

All six types take the same mechanism specifiers on their orderable variant (the concept is defined in Core concepts):

SpecifierMechanismOrdering termExtractor
_ordThe default, currently CLLW OPEopeql_v3.ord_term
_ord_opeCLLW OPE, pinned explicitlyopeql_v3.ord_term
_ord_oreBlock-ORE, pinned explicitlyobeql_v3.ord_term_ore

<T>_ord and <T>_ord_ope are byte-identical today. Pin _ord_ope when you want a column's mechanism frozen against a future change of 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. <T>_ord_ore then raises feature_not_supported on the first value written to it. Use <T>_ord there.

Payload

A value for an _ord column carries the shared envelope keys (v, i, c — see Core concepts) plus the op ordering term. Here is a payload for the public.eql_v3_bigint_ord salary column:

{
  "v": 3,
  "i": { "t": "employees", "c": "salary" },
  "c": "mBbKmsMM%bK#QQOx1yLDBHyD...",
  "op": "5f2b1a9e4c07d38b6ea15c92..."
}
  • 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 operatorpublic.eql_v3_<T><T>_eq<T>_ord variants
= / <>
< <= > >=
BETWEEN (desugars to >= and <=)
IN (desugars to =)
GROUP BY / DISTINCT
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). 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.

Functions

Every operator has a function form, for managed platforms that disallow custom operators — same typed arguments, identical resolution. Each lists the encrypted domains it applies to; the MIN / MAX aggregates only exist as functions.

eql_v3.eq(a, b)

Operators=
Onbigint_eqbigint_ord
SELECT * FROM payments
WHERE eql_v3.eq(amount, $1::public.eql_v3_bigint_eq);

eql_v3.neq(a, b)

Operators<>
Onbigint_eqbigint_ord
SELECT * FROM payments
WHERE eql_v3.neq(amount, $1::public.eql_v3_bigint_eq);

eql_v3.lt / lte / gt / gte

Operators<<=>>=
Onbigint_ordbigint_ord_ope
-- a range uses two of the four
SELECT * FROM payments
WHERE eql_v3.gte(amount, $1::public.eql_v3_bigint_ord)
  AND eql_v3.lt(amount, $2::public.eql_v3_bigint_ord);

eql_v3.min(col)

AggregateMIN
Onbigint_ordbigint_ord_ope
-- compares ordering terms; result decrypts client-side
SELECT eql_v3.min(amount) FROM payments;

eql_v3.max(col)

AggregateMAX
Onbigint_ordbigint_ord_ope
SELECT eql_v3.max(amount) FROM payments;

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.

Where to next

On this page