JSON / JSONB
UQL provides first-class support for JSON/JSONB fields across PostgreSQL, MySQL, MariaDB, and SQLite. Query, update, and sort by nested JSON properties using a consistent, type-safe API. UQL generates dialect-specific SQL automatically.
Entity Setup
Section titled “Entity Setup”Wrap JSONB field types with Json<T> to enable full type safety: IDE autocompletion for dot-notation paths, $set keys, $unset keys, and $push/$pull targets.
import { Entity, Id, Field, Json } from 'uql-orm';
@Entity()export class Company { @Id() id?: number;
@Field() name?: string;
@Field({ type: 'jsonb' }) kind?: Json<{ public?: number; private?: number; tags?: string[] }>;
@Field({ type: 'jsonb' }) settings?: Json<{ isArchived?: boolean; theme?: string; locale?: string }>;}Filtering (Dot-Notation)
Section titled “Filtering (Dot-Notation)”Query nested JSON properties using dot-notation paths in $where. Every comparison operator applicable to the path’s value type is supported.
Dot-notation keys are fully typed: paths are restricted to real JSON fields, and each path resolves its value type from Json<T>, so a typo’d path ('settings.thme'), a dot-path on a non-JSON field, an operator that does not apply to the path’s type ($size on a string-typed path), or a mismatched value (a number where the path is a string) is a compile error. An untyped Json<unknown> field keeps its flexibility: any field.suffix path is accepted with permissive values.
const companies = await querier.findMany(Company, { $where: { 'settings.isArchived': { $ne: true }, 'settings.theme': 'dark', },});SELECT * FROM "Company"WHERE ("settings"->>'isArchived') IS DISTINCT FROM $1 AND "settings"->>'theme' = $2SELECT * FROM `Company`WHERE (`settings`->>'isArchived') <> ? AND (`settings`->>'theme') = ?SELECT * FROM `Company`WHERE JSON_VALUE(`settings`, '$.isArchived') <> ? AND JSON_VALUE(`settings`, '$.theme') = ?SELECT * FROM `Company`WHERE json_extract(`settings`, '$.isArchived') IS NOT ? AND json_extract(`settings`, '$.theme') = ?Updating ($set / $unset / $push / $pull)
Section titled “Updating ($set / $unset / $push / $pull)”Atomically merge or remove keys in JSON fields directly from update payloads. No need to overwrite the entire JSON value.
$set: Assign Keys
Section titled “$set: Assign Keys”Assign top-level keys of an existing JSON field. Keys not named are preserved.
await querier.updateOneById(Company, id, { kind: { $set: { public: 1 } },});UPDATE "Company" SET "kind" = COALESCE("kind", '{}'::jsonb) || $1::jsonb WHERE "id" = $2-- values: ['{"public":1}', id]UPDATE `Company` SET `kind` = JSON_SET(COALESCE(`kind`, '{}'), '$.public', CAST(? AS JSON)) WHERE `id` = ?-- values: ['1', id]UPDATE `Company` SET `kind` = JSON_SET(COALESCE(`kind`, '{}'), '$.public', CAST(? AS JSON)) WHERE `id` = ?-- values: ['1', id]UPDATE `Company` SET `kind` = json_set(COALESCE(`kind`, '{}'), '$.public', json(?)) WHERE `id` = ?-- values: ['1', id]$unset: Remove Keys
Section titled “$unset: Remove Keys”Remove specific keys from a JSON field.
await querier.updateOneById(Company, id, { kind: { $unset: ['private'] },});UPDATE "Company" SET "kind" = ("kind") - $1::text[] WHERE "id" = $2-- values: [['private'], id]UPDATE `Company` SET `kind` = JSON_REMOVE(`kind`, '$.private') WHERE `id` = ?UPDATE `Company` SET `kind` = JSON_REMOVE(`kind`, '$.private') WHERE `id` = ?UPDATE `Company` SET `kind` = json_remove(`kind`, '$.private') WHERE `id` = ?$push: Append to Array
Section titled “$push: Append to Array”Append a value to the end of a JSON array. Only keys whose type is an array are valid $push targets (type-checked at compile time). If the key does not exist yet, it is created with a single-element array - identically on every dialect.
await querier.updateOneById(Company, id, { kind: { $push: { tags: 'new-tag' } },});UPDATE "Company" SET "kind" = jsonb_set("kind", '{tags}', COALESCE("kind"->'tags', '[]'::jsonb) || jsonb_build_array($1::jsonb)) WHERE "id" = $2-- values: ['"new-tag"', id]UPDATE `Company` SET `kind` = JSON_MERGE_PRESERVE(`kind`, JSON_OBJECT('tags', JSON_ARRAY(CAST(? AS JSON)))) WHERE `id` = ?-- values: ['"new-tag"', id]UPDATE `Company` SET `kind` = JSON_MERGE_PRESERVE(`kind`, JSON_OBJECT('tags', JSON_ARRAY(JSON_EXTRACT(?, '$')))) WHERE `id` = ?-- values: ['"new-tag"', id]UPDATE `Company` SET `kind` = json_insert(`kind`, '$.tags[#]', json(?)) WHERE `id` = ?-- values: ['"new-tag"', id]$pull: Remove From Array
Section titled “$pull: Remove From Array”Remove every element equal to the given value. Like $push, only array-typed keys are valid targets and the value is typed as the array’s element.
await querier.updateOneById(Company, id, { kind: { $pull: { tags: 'stale-tag' } },});UPDATE "Company" SET "kind" = jsonb_set("kind", '{tags}', COALESCE(( SELECT jsonb_agg(uql_pull.val ORDER BY uql_pull.ord) FROM jsonb_array_elements("kind"->'tags') WITH ORDINALITY AS uql_pull(val, ord) WHERE uql_pull.val <> $1::jsonb), '[]'::jsonb), false) WHERE "id" = $2-- values: ['"stale-tag"', id]UPDATE `Company` SET `kind` = JSON_REPLACE(`kind`, '$.tags', ( SELECT COALESCE(JSON_ARRAYAGG(uql_pull.v), JSON_ARRAY()) FROM JSON_TABLE(`kind`, '$.tags[*]' COLUMNS (v JSON PATH '$')) uql_pull WHERE uql_pull.v <> CAST(? AS JSON))) WHERE `id` = ?-- values: ['"stale-tag"', id]UPDATE `Company` SET `kind` = JSON_REPLACE(`kind`, '$.tags', ( SELECT COALESCE(JSON_ARRAYAGG(JSON_COMPACT(uql_pull.v)), JSON_ARRAY()) FROM JSON_TABLE(`kind`, '$.tags[*]' COLUMNS (v JSON PATH '$')) uql_pull WHERE NOT JSON_EQUALS(uql_pull.v, JSON_EXTRACT(?, '$')))) WHERE `id` = ?-- values: ['"stale-tag"', id]UPDATE `Company` SET `kind` = json_replace(`kind`, '$.tags', ( SELECT json_group_array(json(`kind` -> uql_pull.fullkey)) FROM json_each(`kind`, '$.tags') uql_pull WHERE `kind` -> uql_pull.fullkey <> json(?))) WHERE `id` = ?-- values: ['"stale-tag"', id]A $pull on a key that does not exist (or on a NULL column) is a no-op: it never creates the key and never nulls the document. Removing the last element leaves an empty array, not a missing key.
Combining Operators
Section titled “Combining Operators”All four operators can be freely combined in a single, atomic update. They are applied in a fixed order - $pull -> $set -> $push -> $unset - so every combination produces the same result on every dialect.
await querier.updateOneById(Company, id, { kind: { $set: { public: 1 }, $push: { tags: 'new-tag' }, $unset: ['private'] },});That order is what makes “replace an element” a single atomic statement: the $pull filters the stored array and the $push appends to that result.
await querier.updateOneById(Company, id, { kind: { $pull: { tags: 'old' }, $push: { tags: 'new' } },});Combining two operators on the same key works as well, and follows the same order: a $set replaces the array outright, so a $push beside it appends to the value you just set.
await querier.updateOneById(Company, id, { kind: { $set: { tags: ['kept'] }, $push: { tags: 'appended' } }, // -> ['kept', 'appended']});Sorting (Dot-Notation)
Section titled “Sorting (Dot-Notation)”Sort by nested JSON field values using the same dot-notation syntax.
const companies = await querier.findMany(Company, { $sort: { 'kind.public': 'desc' },});SELECT * FROM "Company" ORDER BY "kind"->>'public' DESCSELECT * FROM `Company` ORDER BY (`kind`->>'public') DESCSELECT * FROM `Company` ORDER BY JSON_VALUE(`kind`, '$.public') DESCSELECT * FROM `Company` ORDER BY json_extract(`kind`, '$.public') DESCSupported Dialects
Section titled “Supported Dialects”All JSON features work across four SQL dialects:
| Feature | PostgreSQL | MySQL | MariaDB | SQLite |
|---|---|---|---|---|
| Dot-notation filtering | ->>'key' |
->>'key' |
JSON_VALUE() |
json_extract() |
$set |
|| ::jsonb |
JSON_SET() |
JSON_SET() |
json_set() |
$unset |
- ::text[] |
JSON_REMOVE() |
JSON_REMOVE() |
json_remove() |
$push |
jsonb_set() + || |
JSON_MERGE_PRESERVE() |
JSON_MERGE_PRESERVE() |
json_insert() |
$pull |
jsonb_agg() filter |
JSON_TABLE() filter |
JSON_TABLE() + JSON_EQUALS() |
json_each() filter |
| Dot-notation sorting | ->>'key' |
->>'key' |
JSON_VALUE() |
json_extract() |
$size |
jsonb_array_length() |
JSON_LENGTH() |
JSON_LENGTH() |
json_array_length() |
$all |
@> ::jsonb |
JSON_CONTAINS() |
JSON_CONTAINS() |
json_each() |
$elemMatch |
jsonb_array_elements |
JSON_TABLE() |
JSON_TABLE() |
json_each() |
Dialect Compatibility
Section titled “Dialect Compatibility”This page targets modern, actively maintained database lines. Baselines below reflect the current compatibility target for generated SQL:
| Dialect | Practical baseline | Notes |
|---|---|---|
| PostgreSQL | 16+ | Uses jsonb operators/functions (->>, ` |
| MySQL | 8.4+ | Uses ->>, JSON_SET, JSON_REMOVE, JSON_MERGE_PRESERVE, JSON_TABLE ($pull needs 8.0.4+) |
| MariaDB | 12.2+ | Uses JSON_VALUE for dot-notation path extraction (not ->>), plus JSON_SET, JSON_REMOVE, JSON_MERGE_PRESERVE, JSON_TABLE ($pull needs 10.7+ for JSON_EQUALS) |
| SQLite | 3.45+ | Uses json_extract, json_set, json_remove, json_insert(..., '$[#]', ...) for append, and json_each/-> for $pull (needs 3.38+) |