Skip to content

feat(postgres): typed jsonb paths, @> and jsonb_set from the column schema - #115

Open
artile wants to merge 4 commits into
MapleTechLabs:mainfrom
artile:feat/json-paths
Open

artile wants to merge 4 commits into
MapleTechLabs:mainfrom
artile:feat/json-paths

Conversation

@artile

@artile artile commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

A jsonb column knows its document schema, but a query cannot read, filter or write one field of it with types. Today a path read is untyped text (jsonText), and a containment filter or a one-path update needs a raw CH.sql fragment.

Closes #84.
Part of #52.

Changes

  • PG.jsonPath(column, ...keys) reads a path with ->. The keys and the result type come from the column's TypeScript type, and the decoder comes from the column's Schema: jsonPath($.payload, "profile", "city") is an Expr<string | null>. A number key indexes an array. A missing key reads as null.
  • A value compared with a path is encoded by the field's schema and bound as JSON text. .eq("Kyiv") matches the JSON string "Kyiv".
  • PG.jsonPathText(column, ...keys) reads a path as text with ->>, for like and text comparisons.
  • PG.jsonContains(expr, part) writes (expr @> $n::jsonb). part is a deep partial of the document type. It also takes a jsonPath.
  • PG.jsonSet(expr, keys, value) writes jsonb_set(expr, ARRAY[...], $n::jsonb). Use it in an UPDATE set to write one path without a read-modify-write. The value has the path's type and is encoded by the field's schema. Nest calls to set more than one path. RETURNING decodes the full document.
  • The dialect-neutral part is in src/ch/json-path.ts: the path types (JsonPath, JsonPathValue, JsonContained, JsonKeyOf, JsonStep, JsonKey) and jsonPathSchema. jsonPathSchema walks the Schema AST through structs, records, arrays, tuples, unions, NullOr, optional keys and suspend. Only the SQL is in src/pg/functions.ts. These are exported from /postgres for helpers.
  • A path the schema does not have is a type error and a QueryBuilderDefect at compile. A path through a node whose stored form differs from its type (Schema.encodeKeys, a class) is refused, because the stored keys can differ. A value with no JSON form (a bigint) fails with QueryBuilderError InvalidLiteral, not a throw.
  • Keys are SQL literals, not bound params, so an expression index on (payload -> 'profile') matches.
  • Docs: a "JSON paths" section in docs/postgres.md with a PGlite example that check-doc-examples runs, docs/reference.md, a design/dialects.md note, and the P0 row in design/gap-review.md.
  • A minor changeset.

How SQLite and MySQL plug in: a SQLite or MySQL catalog adds its own jsonPath / jsonSet / jsonContains. They reuse the same path types and jsonPathSchema, and write json_extract(x, '$.a[0]') and json_set(x, '$.a', json(?)) on SQLite, or JSON_EXTRACT, JSON_UNQUOTE, JSON_CONTAINS and JSON_SET on MySQL. The only new piece is a function that writes the keys as a $.a[0] path string.

Left out

  • SQLite and MySQL rendering. Upstream main has no SQLite or MySQL dialect yet (SQLite is feat: add SQLite and Cloudflare D1 query dialects #49). The neutral module is ready for them, as described above.
  • The ?, ?|, ?& key-exists operators, and the array operators && and ANY. They are the other part of the gap-review P0 row. A follow-up can add them on the same base.
  • A property-access proxy ($.payload.profile.city, as in effect-qb). A proxy on a column ref collides with Expr method names (eq, get, in_) and with document keys that have those names. Variadic keys give the same types and autocompletion without that risk.
  • jsonContains does not encode its value through the schema, because a partial document does not satisfy a full schema. The value is written as given (the stored form). This is documented.
  • A param or expression as the value of jsonContains / jsonSet. CH.sql still covers that case.
  • jsonb_set's create_missing argument, #> / #>>, and jsonb_insert.

Validation

  • bun run build: pass.
  • bun run typecheck: pass (includes the new src/pg/json.test-d.ts).
  • bunx vitest run --testTimeout=60000 --hookTimeout=60000 with the live ClickHouse 26.8.2.7 env set: 61 files passed, 1046 tests passed, 0 skipped. The new src/pg/json.test.ts runs every query on PGlite. The postgres coverage manifest has a json-paths case.
  • node scripts/check-doc-citations.mjs: ok. node scripts/check-exports-documented.mjs: ok (217 /postgres exports). node scripts/check-doc-examples.mjs: ok (22 files type-checked; the new example runs on PGlite).
  • bun scripts/check-package.ts: ok.
  • Upstream CI for a fork PR waits until a maintainer approves the run.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 15 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 32396472-2e91-4477-926e-3cfbf82ae172
Commits

Reviewing files that changed from the base of the PR and between c5fa679 and 88fa4e6.

Files selected for processing: 12
  • .changeset/pg-json-paths.md
  • design/dialects.md
  • design/gap-review.md
  • docs/postgres.md
  • docs/reference.md
  • scripts/check-doc-examples.mjs
  • src/ch/json-path.ts
  • src/pg/functions.ts
  • src/pg/json.test-d.ts
  • src/pg/json.test.ts
  • src/postgres.ts
  • tests/dialect-cases.postgres.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Typed JSON paths and JSON updates on SQLite and MySQL

1 participant