PureQL is a JSON-based declarative query language for relational data. Queries are plain JSON objects validated against a JSON Schema, making them easy to generate programmatically, serialize, transmit, and validate without a parser.
| Property | Required | Description |
|---|---|---|
from |
yes | Entity (table) to query from, with optional alias |
select |
yes | Array of expressions to return |
where |
no | Boolean filter applied before grouping |
joins |
no | Array of join clauses |
groupBy |
no | Fields to group rows by |
having |
no | Boolean filter applied after grouping |
orderBy |
no | Fields to order results by |
pagination |
no | skip and take for paging |
distinct |
no | When true, deduplicate result rows (default: false) |
| Type name | JSON representation |
|---|---|
string |
"name": "string" |
number |
"name": "number" |
boolean |
"name": "boolean" |
null |
"name": "null" |
date |
"name": "date" — ISO 8601 date string |
time |
"name": "time" — ISO 8601 time string |
datetime |
"name": "datetime" — ISO 8601 date-time string |
uuid |
"name": "uuid" — UUID string |
Each scalar type has a corresponding array variant: stringArray, numberArray, booleanArray, nullArray, dateArray, timeArray, datetimeArray, uuidArray.
A field reference selects a column from an entity. The entity value should match the entity name (or from alias). Fields are typed using the scalar type name.
{ "entity": "users", "field": "email", "type": { "name": "string" } }Fields carry their type as an array type in the schema — they represent a column of values. Use them in select, groupBy, orderBy, as operands of per-row predicates (eachEqual, eachGreaterThan, etc.), and as arguments to aggregate functions.
A scalar is a literal constant value embedded in the query.
{ "type": { "name": "number" }, "value": 42 }
{ "type": { "name": "string" }, "value": "active" }
{ "type": { "name": "date" }, "value": "2024-01-01" }Array scalars hold an array of literal values:
{ "type": { "name": "stringArray" }, "value": ["active", "pending"] }Parameters are named placeholders resolved at execution time, analogous to prepared statement bindings.
{ "param_name": "user_role", "type": { "name": "stringArray" } }
{ "param_name": "min_amount", "type": { "name": "number" } }{ "entity": "orders" }
{ "entity": "orders", "alias": "o" }Each item in select is a value-returning expression (field, scalar, aggregate, arithmetic, boolean expression) with an optional alias.
"select": [
{ "entity": "users", "field": "name", "type": { "name": "string" } },
{ "entity": "users", "field": "email", "type": { "name": "string" }, "alias": "contact_email" },
{ "operator": "count", "arg": { "entity": "orders", "field": "id", "type": { "name": "uuid" } }, "alias": "order_count" }
]They accept different shapes because they evaluate in different scopes:
whereis evaluated per row. It accepts either a boolean-returning expression (single boolean) or a boolean-array-returning expression (per-row boolean column). Use theeach*family for per-row predicates against fields.havingis evaluated per group, aftergroupBy. It accepts only a boolean-returning expression. Operands must reduce to a single value per group — typically aggregates compared withgreaterThan/equal/ etc. Per-roweach*operators do not fit inhaving.
where example using per-row predicates:
"where": {
"operator": "eachAnd",
"conditions": [
{ "operator": "eachEqual",
"left": { "entity": "orders", "field": "status", "type": { "name": "string" } },
"right": { "type": { "name": "string" }, "value": "completed" } },
{ "operator": "eachEqual",
"left": { "entity": "orders", "field": "is_paid", "type": { "name": "boolean" } },
"right": { "type": { "name": "boolean" }, "value": true } }
]
}Each join specifies its type (inner, left, right, full), the entity to join, and an on condition. Like where, on accepts either a boolean-returning or a boolean-array-returning expression. For column-to-column matching (the typical join), use eachEqual:
"joins": [
{
"type": "inner",
"entity": "users",
"on": {
"operator": "eachEqual",
"left": { "entity": "o", "field": "user_id", "type": { "name": "uuid" } },
"right": { "entity": "users", "field": "id", "type": { "name": "uuid" } }
}
}
]Both accept an array of field references.
"groupBy": [
{ "entity": "orders", "field": "user_id", "type": { "name": "uuid" } }
],
"orderBy": [
{ "entity": "users", "field": "name", "type": { "name": "string" } }
]"pagination": { "skip": 0, "take": 25 }PureQL has two parallel predicate families that you choose between based on whether you're working with single values or with columns/rows:
| Need | Family | Returns | Operators |
|---|---|---|---|
| Combine/compare single-value expressions (aggregates, scalars) | Single-value family | one boolean | and, or, not, equal, greaterThan, lessThan, … |
| Filter/compare values per row of a column | Per-row (each*) family |
one boolean per row | eachAnd, eachOr, eachNot, eachEqual, eachGreaterThan, eachLessThan, … |
where and join.on accept either family (they evaluate per row, but a literal true is also valid). having accepts only the single-value family. and / or / not themselves only accept single-value children — do not mix the two families inside the same boolean operator; use eachAnd / eachOr / eachNot to compose per-row predicates.
| Operator | Shape |
|---|---|
and |
{ "operator": "and", "conditions": [ ...booleanReturning ] } |
or |
{ "operator": "or", "conditions": [ ...booleanReturning ] } |
not |
{ "operator": "not", "condition": booleanReturning } |
Conditions must be single-boolean expressions: a boolean scalar, parameter, single-value equality, or single-value comparison. Typical use: combining aggregate comparisons in having.
"having": {
"operator": "and",
"conditions": [
{ "operator": "greaterThan",
"left": { "operator": "count", "arg": { "entity": "orders", "field": "id", "type": { "name": "uuid" } } },
"right": { "type": { "name": "number" }, "value": 5 } }
]
}| Operator | Shape |
|---|---|
eachAnd |
{ "operator": "eachAnd", "conditions": [ ...booleanArrayReturning ] } |
eachOr |
{ "operator": "eachOr", "conditions": [ ...booleanArrayReturning ] } |
eachNot |
{ "operator": "eachNot", "condition": booleanArrayReturning } |
These combine per-row boolean columns element-wise. The result is also a per-row boolean column. Use them to compose multiple each* predicates in where or join.on. There is no dedicated eachNotEqual — express it as eachNot(eachEqual(...)).
Compares two single-value expressions of the same type and returns one boolean. Useful in having against aggregates, or anywhere both operands reduce to scalars.
{
"operator": "equal",
"left": { "operator": "max_number", "arg": { "entity": "orders", "field": "total", "type": { "name": "number" } } },
"right": { "param_name": "target_max", "type": { "name": "number" } }
}The same equal operator, when both sides are array-returning expressions of the same type, asks "are these two sequences equal as wholes?" and returns one boolean. This is rarely needed; most "field equals value" filtering should use eachEqual instead.
{
"operator": "equal",
"left": { "param_name": "expected_ids", "type": { "name": "uuidArray" } },
"right": { "param_name": "received_ids", "type": { "name": "uuidArray" } }
}For each row, returns true when left equals right. The left operand is an array-returning expression (typically a field). The right operand is either a single-value-returning expression (the threshold/literal case) or another array-returning expression (element-wise field-to-field comparison).
Supported types: boolean, number, string, date, time, datetime, uuid.
Field-to-literal:
{
"operator": "eachEqual",
"left": { "entity": "users", "field": "role", "type": { "name": "string" } },
"right": { "type": { "name": "string" }, "value": "admin" }
}Field-to-field (per-row, used in joins or cross-column filters):
{
"operator": "eachEqual",
"left": { "entity": "orders", "field": "user_id", "type": { "name": "uuid" } },
"right": { "entity": "users", "field": "id", "type": { "name": "uuid" } }
}Range comparisons on single-value-returning expressions (scalars, parameters, aggregates). They return a boolean.
| Operator | Meaning |
|---|---|
greaterThan |
> |
lessThan |
< |
greaterThanOrEqual |
>= |
lessThanOrEqual |
<= |
Supported types: number, string, date, time, datetime.
{
"operator": "greaterThan",
"left": { "operator": "sum", "arg": { "entity": "orders", "field": "total", "type": { "name": "number" } } },
"right": { "type": { "name": "number" }, "value": 500 }
}Per-row analogues of the range comparisons. left is array-returning (typically a field), right is either single-value-returning (one threshold for all rows) or array-returning (element-wise field-to-field).
| Operator | Meaning |
|---|---|
eachGreaterThan |
> |
eachLessThan |
< |
eachGreaterThanOrEqual |
>= |
eachLessThanOrEqual |
<= |
Supported types: number, string, date, time, datetime.
Field vs literal:
{
"operator": "eachGreaterThan",
"left": { "entity": "orders", "field": "total", "type": { "name": "number" } },
"right": { "type": { "name": "number" }, "value": 100 }
}Field vs field (per-row):
{
"operator": "eachGreaterThan",
"left": { "entity": "order_items", "field": "unit_price", "type": { "name": "number" } },
"right": { "entity": "order_items", "field": "sale_price", "type": { "name": "number" } }
}Arithmetic works on numeric single-value-returning expressions. Use aggregates to bridge field data into arithmetic.
| Operator | Meaning |
|---|---|
add |
+ |
subtract |
- |
multiply |
* |
divide |
/ |
All operators take a values array with at least 2 operands.
{
"operator": "multiply",
"values": [
{ "operator": "sum", "arg": { "entity": "orders", "field": "amount", "type": { "name": "number" } } },
{ "type": { "name": "number" }, "value": 0.9 }
],
"alias": "discounted_total"
}Aggregates reduce an array of field values to a single value. The arg is any array-returning expression (typically a field reference).
| Operator | Input type | Returns |
|---|---|---|
count |
any array | number |
sum |
number | number |
average_number |
number | number |
min_number |
number | number |
max_number |
number | number |
min_string |
string | string |
max_string |
string | string |
min_date |
date | date |
max_date |
date | date |
average_date |
date | date |
min_time |
time | time |
max_time |
time | time |
average_time |
time | time |
min_datetime |
datetime | datetime |
max_datetime |
datetime | datetime |
average_datetime |
datetime | datetime |
{ "operator": "sum", "arg": { "entity": "items", "field": "price", "type": { "name": "number" } } }
{ "operator": "count", "arg": { "entity": "items", "field": "id", "type": { "name": "uuid" } } }
{ "operator": "max_date","arg": { "entity": "orders","field": "order_date","type": { "name": "date" } } }The samples/ directory contains query examples ordered by complexity.
| File | Description |
|---|---|
01_simple_select.json |
Select several fields from a single entity |
02_aliases_and_pagination.json |
from alias, field aliases, and pagination |
03_where_equality.json |
Filter rows with a single eachEqual condition |
04_boolean_logic.json |
Nested eachAnd / eachOr / eachNot over field equalities |
05_joins.json |
inner and left joins using eachEqual for per-row key matching |
06_count_aggregate.json |
count aggregate with a where filter |
07_group_by.json |
Multiple aggregates with groupBy |
08_having.json |
having clause with and of two aggregate comparisons |
09_arithmetic.json |
add, multiply, divide on aggregate results |
10_parameters.json |
Named scalar parameters in per-row predicates |
11_distinct.json |
distinct: true to deduplicate results |
12_complex_query.json |
Full query: joins, per-row where, groupBy, single-value having, arithmetic, parameters, orderBy, pagination |
13_range_filter.json |
eachGreaterThan and eachLessThan combined with eachAnd |
14_each_field_to_field.json |
Per-row range comparison between two fields (no scalar threshold) |
15_each_not_equal.json |
eachNot wrapping eachEqual — the idiom for "field ≠ literal" |
16_each_or_composition.json |
Mixing eachEqual and eachGreaterThan via eachAnd + eachOr |