Operators
Vincent Depassier · August 29, 2026
PraxQL — Operators
Operators appear in where conditions, and in having for aggregation queries. Every condition has the form:
{ "field": "ColumnName", "op": "operator", "value": someValue }Operator names are matched case-insensitively: isNull, isnull and ISNULL are the same operator.
Comparison
Operator | SQL | Description |
|
| Equals |
|
| Not equals |
|
| Greater than |
|
| Greater than or equal |
|
| Less than |
|
| Less than or equal |
{ "field": "Total", "op": "gte", "value": 100 }
{ "field": "Status", "op": "neq", "value": "Deleted" }
{ "field": "CreatedDate", "op": "gt", "value": "2026-01-01" }Pattern matching
Operator | SQL | Description |
|
| Case-sensitive pattern. |
|
| Case-insensitive pattern. |
{ "field": "Name", "op": "like", "value": "%Corp%" }
{ "field": "Email", "op": "ilike", "value": "%@gmail.com" }A pattern longer than 200 characters is rejected.
Set membership
Operator | SQL | Description |
|
| The value is one of the array |
|
| The value is none of them. |
{ "field": "Status", "op": "in", "value": ["Active", "Pending", "Trial"] }
{ "field": "Status", "op": "notIn", "value": ["Deleted", "Archived"] }value must be an array.
Null checks
Operator | SQL | Description |
|
| The column has no value |
|
| The column has a value |
|
| The same test, decided by whether you send a value |
{ "field": "DeletedAt", "op": "isNull" }
{ "field": "DeletedAt", "op": "isNotNull" }Prefer isNull and isNotNull. They say outright what is expresses through the presence or absence of a value, which is the part nobody guesses: with is, sending "value": null tests IS NULL, and omitting the key entirely tests IS NOT NULL.
To test a boolean column, compare it instead:
{ "field": "IsVerified", "op": "eq", "value": true }Send a real JSON true, never the string "true". A string matches zero rows and reports no error, which makes it one of the slowest bugs to notice.
Range
Operator | SQL | Description |
|
| Inclusive range |
{ "field": "Date", "op": "between", "value": ["2026-01-01", "2026-06-30"] }
{ "field": "Score", "op": "between", "value": [70, 100] }value must be a two-element array, [min, max]. Both bounds are inclusive.
Array and text
Operator | Description |
| An array column contains all the given values |
| Full-text search on a text column |
{ "field": "Tags", "op": "contains", "value": ["urgent", "billing"] }
{ "field": "Description", "op": "textsearch", "value": "warehouse logistics" }textsearch suits long-text columns. It returns results unranked — there is no relevance score, so you cannot sort by "best match".
Logical grouping
AND, the default
Conditions in the same array are AND'd automatically:
"where": [
{ "field": "IsActive", "op": "eq", "value": true },
{ "field": "Region", "op": "eq", "value": "EU" }
]→ WHERE IsActive = true AND Region = 'EU'
OR
"where": [
{
"or": [
{ "field": "Region", "op": "eq", "value": "EU" },
{ "field": "Region", "op": "eq", "value": "US" }
]
}
]→ WHERE (Region = 'EU' OR Region = 'US')
Nesting
"where": [
{ "field": "IsActive", "op": "eq", "value": true },
{
"or": [
{ "field": "Region", "op": "eq", "value": "EU" },
{
"and": [
{ "field": "Region", "op": "eq", "value": "US" },
{ "field": "Tier", "op": "eq", "value": "Enterprise" }
]
}
]
}
]→ WHERE IsActive = true AND (Region = 'EU' OR (Region = 'US' AND Tier = 'Enterprise'))
Guardrails
Limit | Value |
Where conditions per query | 50 |
Condition nesting depth | 5 |
| 200 characters |
Exceeding any of them returns 400 INVALID_QUERY rather than a truncated result. If you hit the nesting cap, the query is usually asking two questions at once — split it.