8000 feat(postgres): type a json key read by the column's json schema by colinhacks · Pull Request #95 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

feat(postgres): type a json key read by the column's json schema - #95

Merged
colinhacks merged 2 commits into
mainfrom
pg-json-keys
Oct 23, 2026
Merged

colinhacks merged 2 commits into
mainfrom
pg-json-keys

Conversation

@colinhacks
@colinhacks colinhacks commented Oct 23, 2026 •
Copy link
Copy Markdown
Owner

On Postgres, a json or jsonb column a JSON Schema CHECK holds was typed by that schema only when read whole. A key read from it was typed by the operator's result alone: -> as jsonb, which is unknown, and ->> as text, both nullable, so a value the schema already describes needed a cast, and never came back non-null.

Now a read at literal keys, with the built-in ->, ->>, #> or #>>, takes what the schema says is there. Each step reads the schema's object at its key, as the server reads the value's:

  • -> and #> are typed by the property's schema; ->> and #>> are text, a union of strings where the schema keeps the value to a list of them.
  • The read is non-null only where the column is, and every step lands on an object that must hold its key. A text read is also nullable where the value may be a JSON null, which it answers with SQL NULL.
  • A key on a value that may not be an object (a JSON null, a string, an array) finds nothing there, which only takes the certainty away. A key Postgres reads as an integer under #> (digits, after a sign or leading whitespace, a negative one counting from the end) may index an array instead, which is not followed: the read stays unknown where a step may land on one.
  • A key that is no literal (a parameter, an expression, an integer), a user's own -> (hstore defines one), and any other read stay as they were.
  • A read inside a with query or a derived table keeps its type when the statement selects its column as it is, as built JSON does.

With doc checked against { name: string, kind: "a" | "b", size: integer | null, owner: { id: integer, email?: string }, meta?: { note: string } | null }, required name, kind, size and owner:

Read Before After
doc -> 'name' unknown | null string
doc ->> 'kind' string | null "a" | "b"
doc -> 'size' unknown | null number | null (a JSON null, in a non-null jsonb)
doc ->> 'size' string | null string | null
doc -> 'owner' -> 'id' / doc #> '{owner,id}' unknown | null number
doc -> 'meta' ->> 'note' string | null string | null
(doc -> 'owner' ->> 'id')::int number | null number
jsonb_build_object('id', doc #> '{owner,id}') { id: unknown | null } { id: number }
doc #> '{tags,0}', doc -> :key unknown | null unknown | null

The walk over a schema is core's (schema_type::at_path), over the canonical schema the engine already reads a CHECK into, so the MySQL and SQLite readers can use it too; this changes all three artifacts' fingerprints. A key read from a typed JSON parameter (:p::jsonb('…') -> 'x') stays unknown: its schema is applied after the analysis that reads the operator.

Tests:

  • oracle/cases/json_schema_keys: 15 statements over rows with a key left out, a JSON null, a nested object that may be null, a key no property names, an array, a value that is an array or an object read at signed and space-led keys, a tagged union, a json column and a nullable column, sound over the seeded edge values. json_schema_checks/expression.sql now types doc -> 'name' as string.
  • Unit tests in engine/core/src/schema_type.rs (the walk) and engine/postgres/src/infer.rs (which reads are claimed, the literal keys, and the read's nullability).
  • Docs: the Postgres page's JSON rules, with a card, and the skill.
  • Run locally: the whole oracle with --strict-coverage, the engine's tests, the workspace build with no warning, the three rebuilt artifacts' fingerprints, the loader's typecheck and tests, the docs site.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 22:36
@vercel
vercel Bot commented Oct 23, 2026 •
Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 23, 2026 11:19pm UTC

Request Review

Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ One narrow soundness gap inline. Everything else I traced holds up.

Reviewed changes

I reviewed the whole PR at 56457ca: the core schema walk, the Postgres reader and probe changes, the carry-through for derived tables and with queries, the new oracle case, and the docs and skill.

  • schema_type::at_path: walks a canonical schema one key at a time. It returns the schema found at the path and whether the value is there for certain. It gives None for not, and also for an integer key under #> that might land on an array.
  • Expr::JsonPath in the reader: the built-in ->, ->>, #> and #>> with literal keys over a column (bare or narrowed) now build a path, and chained reads extend it. Non-literal keys, shadowed operators and non-column bases stay Expr::Unknown, which matches the old behaviour, since -> was never in POSTGRES_OPERATORS.
  • json_read and the probe: a top-level read sets the column's document (for a JSON read) or its values (for a text read whose schema allows only strings). The checks.values loop no longer overwrites those values with None.
  • ItemColumn in item_columns: a derived table or with query now passes its column's document and values to an outer column that reads it as it is.
  • Tests and docs: a new oracle/cases/json_schema_keys case with 14 statements, unit tests for the walk and the reader, a captured docs card, and the skill line.

Points I checked and found sound:

  • Canonical schemas fold patternProperties into additionalProperties.
  • anyOf and enum ignore sibling keywords, which can only loosen the type.
  • A text key read with -> on an array returns SQL NULL.
  • #> '{}' returns the whole document.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread engine/core/src/schema_type.rs Resolved
@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. The new commit fixes the #> index gap from the prior review.

Reviewed changes

I reviewed the one commit pushed since the prior review, 6053c4b.

  • Index keys follow strtoint: under #> and #>>, at_key now counts a key as a possible array index if strtoint would read the whole key: optional leading C whitespace, one optional + or -, then digits. So -1, +0 and 1 now stop the walk where an array may be. A key with trailing whitespace, which strtoint rejects, is still read as an object key. A digit key too large to fit is also counted as an index, which is the safe side.
  • Coverage: the new oracle statement signed_index.sql reads a value that is either an array or an object, using signed keys, a space-led key and a plain key. Unit tests in schema_type.rs cover the same keys. The doc comments in schema_type.rs and infer.rs now describe the rule.
  • Artifacts: all three engine.wasm files were rebuilt for the core change.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit 569ba02 into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the pg-json-keys branch October 23, 2026 23:25
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.

2 participants

0