Skip to main content

Operators and Conditions

.where() (on Select, Update and Delete) and .on() (on join elements) accept operator instances built with the functions below, plain object literals (a shorthand that's expanded into the same operator instances), Raw fragments, or nested And/Or groups. Multiple arguments — and multiple calls to .where() — are combined with an implicit top-level And.

import { Select, Eq, And, Or } from '@sqb/builder';

Select().from('customers').where(Eq('active', true), Eq('country', 'US'));
// where active = true and country = 'US'

Every operator listed here has its own API reference page under API Reference → Operators.

The object-literal shorthand

Instead of calling operator functions directly, you can pass plain objects to .where(). Each own-property is expanded into a comparison. The key is <field> [operator suffix]; the suffix defaults to eq (or in when the value is an array):

Select().from('customers').where({ id: 1, 'name !=': 'John', age: [18, 19, 20] });
// where id = 1 and name != 'John' and age in (18,19,20)

The suffix maps to an operator through this table (from Operators in op.ns.ts):

Suffix(es)Operator
(none, non-array value)Eq
(none, array value)In
=, eqEq
!=, neNe
>, gtGt
>=, gteGte
<, ltLt
<=, lteLte
between, btwBetween
!between, !btw, notBetween, nbtwNotBetween
inIn
!in, notIn, ninNotIn
likeLike
!like, notLike, nlikeNotLike
ilikeILike
!ilike, notILike, nilikeNotILike
isIs
!is, isNotIsNot

The two keys and/or (case-insensitive) and exists/!exists are special-cased instead of being suffixes:

Select().from('customers').where({ and: [{ id: 1 }, { id: 2 }] });
// where (id = 1 and id = 2)

Select().from('customers').where({ OR: [{ id: 1 }, { id: 2 }] });
// where (id = 1 or id = 2)

Select().from('customers').where({ exists: Select().from('orders') });
// where exists (select * from orders)

Select().from('customers').where({ '!exists': Select().from('orders') });
// where not exists (select * from orders)

An unrecognized suffix throws:

Select().from('customers').where({ 'id non': 3 });
// Error: Unknown operator "non"

and() / or()

Combine any number of operators, Raw fragments, or nested And/Or groups. Falsy items (null, undefined, 0) are silently skipped.

import { And, Or, Eq } from '@sqb/builder';

Select().from('customers').where(And(Eq('id', 1), Eq('id', 2)));
// where (id = 1 and id = 2)

Select().from('customers').where(Or(And(Eq('id', 1), Eq('id', 2)), Eq('id', 3)));
// where ((id = 1 and id = 2) or id = 3)

Both .where() on a query and And/Or themselves accept a mix of operator instances and plain object literals — objects are expanded with the same shorthand rules described above.

eq() / ne() / gt() / gte() / lt() / lte()

Comparison operators. The left-hand side can be a field-name string (optionally with a trailing [] to mark it an array field) or an SQL element (Field, Raw, sub-Select); the right-hand side can be a literal, Param, Raw, or sub-Select.

import { Eq, Ne, Gt, Gte, Lt, Lte } from '@sqb/builder';

Eq('id', 1); // id = 1
Ne('id', 1); // id != 1
Gt('age', 18); // age > 18
Gte('age', 18); // age >= 18
Lt('price', 100); // price < 100
Lte('price', 100); // price <= 100

Comparing two columns

A bare string on the right-hand side is treated as a literal value, not a column — it gets quoted like any other string. To compare against another column, wrap it in Field so it's serialized as an identifier instead:

import { Eq, Field } from '@sqb/builder';

Eq('customer_id', 'customers.id'); // customer_id = 'customers.id' (a string literal!)
Eq('customer_id', Field('customers.id')); // customer_id = customers.id (a column reference)

This is exactly why join conditions and correlated sub-queries always wrap the outer column in Field(...) — see Joins.

between() / notBetween()

import { Between, NotBetween } from '@sqb/builder';

Between('id', 10, 20); // id between 10 and 20
Between('id', [10, 20]); // same, as an array
Between('id', [10]); // id between 10 and 10 (missing upper bound repeats the lower)
NotBetween('id', 10, 20); // id not between 10 and 20

in() / notIn()

import { In, NotIn } from '@sqb/builder';

In('id', [1, 2, 3]); // id in (1,2,3)
NotIn('id', [1, 2, 3]); // id not in (1,2,3)

An empty array is special-cased to avoid silently dropping the filter: In('id', []) serializes to the literal 1=0 (always false) and NotIn('id', []) to 1=1 (always true).

like() / notLike() / ilike() / notILike()

import { Like, NotLike, ILike, NotILike } from '@sqb/builder';

Like('name', 'John%'); // name like 'John%'
NotLike('name', 'John%'); // name not like 'John%'
ILike('name', 'john%'); // name ilike 'john%' (case-insensitive)
NotILike('name', 'john%'); // name not ilike 'john%'

ILike/NotILike emit the Postgres-style ilike/not ilike keywords by default; a dialect that lacks native ILIKE (e.g. by registering a SerializerExtension) can rewrite this to upper(name) like upper('john%') or similar.

is() / isNot()

import { Is, IsNot } from '@sqb/builder';

Is('deleted_at', null); // deleted_at is null
IsNot('deleted_at', null); // deleted_at is not null

exists() / notExists()

Takes a Select or Raw sub-query — anything else throws a TypeError.

import { Exists, NotExists, Select, Field } from '@sqb/builder';

const hasOrders = Select().from('orders').where(Eq('customer_id', Field('customers.id')));

Select().from('customers').where(Exists(hasOrders));
// where exists (select * from orders where customer_id = customers.id)

Select().from('customers').where(NotExists(hasOrders));
// where not exists (...)

not()

Negates a single operator or Raw expression by prefixing it with not. This is different from Ne/IsNot: it wraps an arbitrary expression rather than comparing two values.

import { Not, Eq } from '@sqb/builder';

Select().from('customers').where(Not(Eq('active', true)));
// where not active = true

match()

A comparison operator intended as an extension point for dialect-specific full-text search (MATCH() AGAINST() in MySQL, @@ in Postgres, CONTAINS() in SQL Server, ...). By itself it serializes like Eq (field = 'value'); a SerializerExtension registered for SerializationType.COMPARISON_EXPRESSION can inspect _operatorType === OperatorType.match (or the customArgs you pass as the third argument) and render the dialect-specific syntax instead.

import { Match } from '@sqb/builder';

Match('description', 'wireless mouse');
// falls back to: description = 'wireless mouse'

See Generating SQL per dialect for how dialect packages hook into serialization.

Nesting and sub-queries

Any operator's left or right side may itself be a Select, Raw, or another operator/field — the builder parenthesizes sub-queries automatically:

Select().from('customers').where(Eq('id', Select('id').from('vip_customers')));
// where id = (select id from vip_customers)