PostgreSQL jsonb type support for the PgWire compatibility layer.
Stores jsonb values as Clojure data structures (maps, vectors, strings, numbers, booleans, nil) serialized to/from JSON strings via jsonista.
Implements PostgreSQL jsonb operators and functions as pure Clojure functions that can be used as Datalog predicates or post-processing.
PostgreSQL jsonb type support for the PgWire compatibility layer. Stores jsonb values as Clojure data structures (maps, vectors, strings, numbers, booleans, nil) serialized to/from JSON strings via jsonista. Implements PostgreSQL jsonb operators and functions as pure Clojure functions that can be used as Datalog predicates or post-processing.
Object punctuation for the writer.
PostgreSQL renders the two families differently, and the difference
is only in objects: jsonb emits ": " after a key and ", "
between pairs, while a json value that PostgreSQL BUILT (rather
than echoed verbatim) is compact — {"a":1}. Arrays are [1, 2]
in both.
Object punctuation for the writer.
PostgreSQL renders the two families differently, and the difference
is only in objects: `jsonb` emits `": "` after a key and `", "`
between pairs, while a `json` value that PostgreSQL BUILT (rather
than echoed verbatim) is compact — `{"a":1}`. Arrays are `[1, 2]`
in both.Deprecated alias for serialize-jsonb; they were always the same fn.
Deprecated alias for `serialize-jsonb`; they were always the same fn.
(json-build-array & args)PostgreSQL json_build_array(v1, …). Arrays render identically in
both families, so only the element order matters and it is preserved
by construction.
PostgreSQL `json_build_array(v1, …)`. Arrays render identically in both families, so only the element order matters and it is preserved by construction.
(json-build-object & args)PostgreSQL json_build_object(k1, v1, …).
NOT jsonb_build_object with a different name. json is the
text-faithful type, so this preserves ARGUMENT ORDER and KEEPS
DUPLICATE KEYS, where the jsonb form sorts and takes the last:
json_build_object('b',1,'a',2,'a',3) -> {"b" : 1, "a" : 2, "a" : 3} jsonb_build_object(same) -> {"a": 3, "b": 1}
Which is why it builds TEXT straight from the arguments instead of
going through a Clojure map — a map cannot hold either property.
PostgreSQL's separator here is " : ", spaces on both sides
(json.c's composite_to_json), not the jsonb writer's ": ".
PostgreSQL `json_build_object(k1, v1, …)`.
NOT `jsonb_build_object` with a different name. `json` is the
text-faithful type, so this preserves ARGUMENT ORDER and KEEPS
DUPLICATE KEYS, where the jsonb form sorts and takes the last:
json_build_object('b',1,'a',2,'a',3) -> {"b" : 1, "a" : 2, "a" : 3}
jsonb_build_object(same) -> {"a": 3, "b": 1}
Which is why it builds TEXT straight from the arguments instead of
going through a Clojure map — a map cannot hold either property.
PostgreSQL's separator here is `" : "`, spaces on both sides
(json.c's composite_to_json), not the jsonb writer's `": "`.PostgreSQL's JSON null is a VALUE, distinct from SQL NULL: IS NULL
on it is false and jsonb_typeof answers "null". Representing it as
Clojure nil conflated the two, and because a datalog function
binding that yields nil FILTERS THE ROW, SELECT p->'k' on a JSON
null returned no rows at all where PostgreSQL returns one row.
Distinct from :__null__, which is this codebase's SQL-NULL
sentinel — ->> collapses JSON null TO SQL NULL, so both exist and
they are not the same thing.
PostgreSQL's JSON `null` is a VALUE, distinct from SQL NULL: `IS NULL` on it is false and `jsonb_typeof` answers "null". Representing it as Clojure `nil` conflated the two, and because a datalog function binding that yields nil FILTERS THE ROW, `SELECT p->'k'` on a JSON null returned no rows at all where PostgreSQL returns one row. Distinct from `:__null__`, which is this codebase's SQL-NULL sentinel — `->>` collapses JSON null TO SQL NULL, so both exist and they are not the same thing.
(jsonb-array-elements v)PostgreSQL jsonb_array_elements(jsonb): expand array to element rows.
PostgreSQL jsonb_array_elements(jsonb): expand array to element rows.
(jsonb-array-elements-text v)PostgreSQL jsonb_array_elements_text(jsonb): expand array to text rows.
PostgreSQL jsonb_array_elements_text(jsonb): expand array to text rows.
(jsonb-array-length v)PostgreSQL jsonb_array_length(jsonb): return array length.
PostgreSQL jsonb_array_length(jsonb): return array length.
(jsonb-build-array & args)PostgreSQL jsonb_build_array(v1, v2, ...): build jsonb array.
PostgreSQL jsonb_build_array(v1, v2, ...): build jsonb array.
(jsonb-build-object & args)PostgreSQL jsonb_build_object(k1, v1, k2, v2, ...): build jsonb from pairs.
PostgreSQL jsonb_build_object(k1, v1, k2, v2, ...): build jsonb from pairs.
(jsonb-concat left right)PostgreSQL || operator: concatenate/merge two jsonb values.
PostgreSQL || operator: concatenate/merge two jsonb values.
(jsonb-contained? left right)PostgreSQL <@ operator: is left contained in right?
PostgreSQL <@ operator: is left contained in right?
(jsonb-contains? left right)PostgreSQL @> operator: does left contain right?
PostgreSQL @> operator: does left contain right?
(jsonb-delete-idx v idx)PostgreSQL - operator (int4): remove element by index from array.
PostgreSQL - operator (int4): remove element by index from array.
(jsonb-delete-key v key)PostgreSQL - operator (text): remove key from jsonb object.
PostgreSQL - operator (text): remove key from jsonb object.
(jsonb-delete-keys v keys)PostgreSQL - operator (text[]): remove multiple keys.
PostgreSQL - operator (text[]): remove multiple keys.
(jsonb-delete-path v path)PostgreSQL #- operator: remove element at path.
PostgreSQL #- operator: remove element at path.
(jsonb-each v)PostgreSQL jsonb_each(jsonb): expand object to (key, value) rows. Returns sequence of [key value] pairs where value is jsonb.
PostgreSQL jsonb_each(jsonb): expand object to (key, value) rows. Returns sequence of [key value] pairs where value is jsonb.
(jsonb-each-text v)PostgreSQL jsonb_each_text(jsonb): expand object to (key, value) rows. Returns sequence of [key text-value] pairs.
PostgreSQL jsonb_each_text(jsonb): expand object to (key, value) rows. Returns sequence of [key text-value] pairs.
(jsonb-eq? a b)PostgreSQL's jsonb =, which compares VALUES and is numeric-scale
INSENSITIVE: '1.00'::jsonb = '1'::jsonb is true even though the two
render differently. Comparing our canonical text alone is therefore
too STRICT — it is a canonical form for structure, not for numbers,
because PostgreSQL keeps display scale on purpose.
Text equality is the fast path and is sound in one direction: equal canonical text implies equal values, so only differing text has to be parsed. That confines the cost to exactly the case that was wrong.
Structural comparison is then just = on the parsed trees: numbers
are uniformly BigDecimal in the value model, and Clojure's = on
BigDecimal is scale-insensitive, which is numeric_eq.
PostgreSQL's jsonb `=`, which compares VALUES and is numeric-scale INSENSITIVE: `'1.00'::jsonb = '1'::jsonb` is true even though the two render differently. Comparing our canonical text alone is therefore too STRICT — it is a canonical form for structure, not for numbers, because PostgreSQL keeps display scale on purpose. Text equality is the fast path and is sound in one direction: equal canonical text implies equal values, so only differing text has to be parsed. That confines the cost to exactly the case that was wrong. Structural comparison is then just `=` on the parsed trees: numbers are uniformly BigDecimal in the value model, and Clojure's `=` on BigDecimal is scale-insensitive, which is `numeric_eq`.
(jsonb-exists-all? v keys)PostgreSQL ?& operator: do all keys exist?
PostgreSQL ?& operator: do all keys exist?
(jsonb-exists-any? v keys)PostgreSQL ?| operator: does any of the keys exist?
PostgreSQL ?| operator: does any of the keys exist?
(jsonb-exists? v key)PostgreSQL ? operator: does key exist in jsonb object?
PostgreSQL ? operator: does key exist in jsonb object?
(jsonb-get v key-or-idx)PostgreSQL -> operator: get jsonb field by key (text) or element by index (int). Returns jsonb (Clojure data structure). Returns :null sentinel unchanged (NULL propagation for get-else).
PostgreSQL -> operator: get jsonb field by key (text) or element by index (int). Returns jsonb (Clojure data structure). Returns :__null__ sentinel unchanged (NULL propagation for get-else).
(jsonb-get-path v path)PostgreSQL #> operator: extract jsonb at path.
PostgreSQL #> operator: extract jsonb at path.
(jsonb-get-path-text v path)PostgreSQL #>> operator: extract text at path.
Returns :__null__ sentinel when the path doesn't exist — returning nil
would make Datahike's function-binding clause filter the row.
PostgreSQL #>> operator: extract text at path. Returns `:__null__` sentinel when the path doesn't exist — returning nil would make Datahike's function-binding clause filter the row.
(jsonb-get-text v key-or-idx)PostgreSQL ->> operator: get field/element as text string.
Returns a string or :__null__ sentinel. Never returns nil — Datahike's
function-binding clauses filter the row when the binding returns nil, but
foo->>missing_key should produce SQL NULL while keeping the row.
PostgreSQL ->> operator: get field/element as text string. Returns a string or `:__null__` sentinel. Never returns nil — Datahike's function-binding clauses filter the row when the binding returns nil, but `foo->>missing_key` should produce SQL NULL while keeping the row.
(jsonb-insert target path new-value)(jsonb-insert target path new-value insert-after?)PostgreSQL jsonb_insert(target, path, new_value, insert_after?): Insert value at path position in jsonb array.
PostgreSQL jsonb_insert(target, path, new_value, insert_after?): Insert value at path position in jsonb array.
(jsonb-object-keys v)PostgreSQL jsonb_object_keys(jsonb): return keys of object. Returns a sequence (set-returning in SQL).
PostgreSQL jsonb_object_keys(jsonb): return keys of object. Returns a sequence (set-returning in SQL).
(jsonb-pretty v)PostgreSQL jsonb_pretty(jsonb): pretty-print jsonb.
PostgreSQL jsonb_pretty(jsonb): pretty-print jsonb.
(jsonb-set target path new-value)(jsonb-set target path new-value create-missing?)PostgreSQL jsonb_set(target, path, new_value, create_missing?): Set value at path in jsonb.
PostgreSQL jsonb_set(target, path, new_value, create_missing?): Set value at path in jsonb.
(jsonb-strip-nulls v)PostgreSQL jsonb_strip_nulls(jsonb): recursively remove null-valued keys.
PostgreSQL jsonb_strip_nulls(jsonb): recursively remove null-valued keys.
(jsonb-typeof v)PostgreSQL jsonb_typeof(jsonb): return type name as string.
PostgreSQL jsonb_typeof(jsonb): return type name as string.
SQL operator string → the runtime fn implementing it.
THE registry. Every consumer — the SELECT emitter that lowers an
operator into a datalog function-call clause, and the UPDATE SET
interpreter that applies it eagerly to a materialised entity map —
looks the fn up here rather than carrying its own if. Those two
had already drifted: one wrapped the -> result in
serialize-jsonb and the other did not, which is precisely the
divergence a shared table prevents. (That difference is preserved
at the UPDATE call site for now and resolved deliberately when the
operator semantics are fixed, not silently by this refactor.)
Adding an operator is one entry here plus, for the ones the parser
currently rejects, a narrowing of sql/unsupported-op-chars.
SQL operator string → the runtime fn implementing it. THE registry. Every consumer — the SELECT emitter that lowers an operator into a datalog function-call clause, and the UPDATE SET interpreter that applies it eagerly to a materialised entity map — looks the fn up here rather than carrying its own `if`. Those two had already drifted: one wrapped the `->` result in `serialize-jsonb` and the other did not, which is precisely the divergence a shared table prevents. (That difference is preserved at the UPDATE call site for now and resolved deliberately when the operator semantics are fixed, not silently by this refactor.) Adding an operator is one entry here plus, for the ones the parser currently rejects, a narrowing of `sql/unsupported-op-chars`.
(parse-jsonb v)Parse a JSON string to the jsonb value model. Returns nil for nil input, passes through non-strings.
Parse a JSON string to the jsonb value model. Returns nil for nil input, passes through non-strings.
(serialize-json v)Canonical text in the json family's punctuation — compact objects,
as PostgreSQL renders a json value it constructed
(json_strip_nulls('{"a":1,"z":null}') -> {"a":1}).
Canonical text in the `json` family's punctuation — compact objects,
as PostgreSQL renders a json value it constructed
(`json_strip_nulls('{"a":1,"z":null}')` -> `{"a":1}`).(serialize-jsonb v)The canonical jsonb TEXT for a value, byte-for-byte as PostgreSQL
renders it: keys length-first then bytewise, ", " between pairs and
": " after each key, numbers via numeric semantics, duplicate keys
already collapsed last-wins by the parser.
This is both the stored form and the form a client reads back, because
PostgreSQL normalizes jsonb on input and has no memory of the original
text. json is the text-faithful type and must never come through
here.
A string that is valid JSON is re-emitted canonically; a string that is not JSON becomes a JSON string scalar; a Clojure map/vector is written directly. nil in, nil out.
The canonical jsonb TEXT for a value, byte-for-byte as PostgreSQL renders it: keys length-first then bytewise, `", "` between pairs and `": "` after each key, numbers via numeric semantics, duplicate keys already collapsed last-wins by the parser. This is both the stored form and the form a client reads back, because PostgreSQL normalizes jsonb on input and has no memory of the original text. `json` is the text-faithful type and must never come through here. A string that is valid JSON is re-emitted canonically; a string that is not JSON becomes a JSON string scalar; a Clojure map/vector is written directly. nil in, nil out.
(to-jsonb v)(to-jsonb v already-json?)PostgreSQL to_jsonb(anyelement) / to_json — convert a SQL value
INTO a json value.
It does NOT parse its argument. to_jsonb('{"a":1}'::text) is the
json STRING "{\"a\":1}", not an object — the text is a text
value being wrapped, not a document being read. We parsed it, so a
text column holding JSON silently became a structure.
Only an argument that is ALREADY json/jsonb passes through, and the caller decides that from the column type, since at runtime both are Clojure strings.
Returns canonical TEXT, so a json string renders quoted ("x") the
way PostgreSQL prints it.
PostgreSQL `to_jsonb(anyelement)` / `to_json` — convert a SQL value
INTO a json value.
It does NOT parse its argument. `to_jsonb('{"a":1}'::text)` is the
json STRING `"{\"a\":1}"`, not an object — the text is a text
value being wrapped, not a document being read. We parsed it, so a
text column holding JSON silently became a structure.
Only an argument that is ALREADY json/jsonb passes through, and the
caller decides that from the column type, since at runtime both are
Clojure strings.
Returns canonical TEXT, so a json string renders quoted (`"x"`) the
way PostgreSQL prints it.(validate-json! s)Parse s the way PostgreSQL's json_in does: a full RFC-8259 parse
that RAISES on anything malformed.
Both json and jsonb validate on input — json then stores the
original bytes, jsonb stores the parsed tree — so this gates both.
We were not validating at all: parse-jsonb catches its own parse
error and falls back to treating the text as a JSON string scalar,
which is right for to_jsonb('some text') and wrong for
'...'::jsonb. So '"abc'::jsonb (unclosed quote) silently became
the string "abc, and 27 statements in PostgreSQL's own
jsonb.sql returned a value where PostgreSQL raises.
Jackson is already strict about the rest — invalid escapes, raw
control bytes, leading zeros, NaN, unquoted keys, trailing commas,
uppercase literals — so this is about not SWALLOWING its verdict.
Parse `s` the way PostgreSQL's `json_in` does: a full RFC-8259 parse
that RAISES on anything malformed.
Both `json` and `jsonb` validate on input — `json` then stores the
original bytes, `jsonb` stores the parsed tree — so this gates both.
We were not validating at all: `parse-jsonb` catches its own parse
error and falls back to treating the text as a JSON string scalar,
which is right for `to_jsonb('some text')` and wrong for
`'...'::jsonb`. So `'"abc'::jsonb` (unclosed quote) silently became
the string `"abc`, and 27 statements in PostgreSQL's own
`jsonb.sql` returned a value where PostgreSQL raises.
Jackson is already strict about the rest — invalid escapes, raw
control bytes, leading zeros, `NaN`, unquoted keys, trailing commas,
uppercase literals — so this is about not SWALLOWING its verdict.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |