Skip to content

fix(compiler): preserve nullability when casting JSON arrow expressions (#3792)#4458

Open
luongs3 wants to merge 1 commit into
sqlc-dev:mainfrom
luongs3:fix/3792-json-arrow-cast-nullable
Open

fix(compiler): preserve nullability when casting JSON arrow expressions (#3792)#4458
luongs3 wants to merge 1 commit into
sqlc-dev:mainfrom
luongs3:fix/3792-json-arrow-cast-nullable

Conversation

@luongs3
Copy link
Copy Markdown

@luongs3 luongs3 commented May 28, 2026

Fixes #3792.

Problem

PostgreSQL's JSON accessor operators (->, ->>, #>, #>>) return SQL NULL when the requested key or path is missing from the value. Wrapping such an expression in a type cast does not make the result non-nullable.

Today sqlc emits a non-nullable Go string for the issue's exact query:

-- name: ListJobs :many
SELECT id,
       (data ->> 'PhoneNumber')::text AS phone_number,
       (data ->> 'ContactName')::text AS contact_name,
       (data ->> 'State')::text       AS state
FROM jobs;

Generated row before this fix:

type ListJobsRow struct {
    ID          int64
    PhoneNumber string
    ContactName string
    State       string
}

Generated row after:

type ListJobsRow struct {
    ID          int64
    PhoneNumber pgtype.Text
    ContactName pgtype.Text
    State       pgtype.Text
}

Any consumer scanning into pgtype.Text / *string / sql.NullString previously crashed on rows where the JSON key happened to be absent. Real Postgres returns NULL in that case; the generated row now matches.

Fix

Two small changes:

  • internal/sql/lang/operator.go — new IsJSONNullableOperator(s string) bool classifier covering ->, ->>, #>, #>>.
  • internal/compiler/output_columns.go — in the *ast.TypeCast case, when the wrapped argument is an *ast.A_Expr whose operator is JSON-nullable, set col.NotNull = false. Tight scope: only the immediate cast argument. All other cast paths (literal cast, column-from-NOT-NULL cast, etc.) are unchanged.

Broader nullability inference for casts is intentionally out of scope (the existing // TODO Add correct, real type inference comment in the cast handler stays — this PR addresses the specific JSON-arrow case from #3792 without expanding into general expression-tree analysis).

Tests

  • internal/sql/lang/operator_test.goTestIsJSONNullableOperator table-driven test, positive (->, ->>, #>, #>>) and negative (+, -, =, @>, <@, ->>> lookalike, empty).
  • internal/endtoend/testdata/json_arrow_cast_3792/postgresql/pgx/v5/ — end-to-end fixture reproducing the issue's exact query under pgx/v5; golden files show pgtype.Text for the three ->>-derived columns.

Verified locally

  • Issue repro generates pgtype.Text instead of string (the bug).
  • ('foo')::text and (name)::text where name is NOT NULL still emit string — no regression on the common cast cases.
  • (NULL)::text still emits pgtype.Text (preserves the existing NULL-literal-cast carve-out).
  • (data -> 'k')::jsonb correctly flows through the same path.
  • go test ./internal/sql/... ./internal/compiler/... ./internal/engine/... passes; gofmt -l and go vet clean on touched files.

…ns (sqlc-dev#3792)

PostgreSQL's JSON accessor operators (-->, ->>, #>, #>>) return SQL
NULL when the requested key or path is missing. Wrapping such an
expression in a type cast does not make the result non-nullable.

Today sqlc emits a non-nullable Go `string` for queries like:

    SELECT id,
           (data ->> 'PhoneNumber')::text AS phone_number,
           (data ->> 'ContactName')::text AS contact_name
    FROM jobs;

which crashes any consumer scanning into pgtype.Text / *string / sql.NullString
when the key happens to be absent. Real Postgres returns NULL in that
case; the generated row should too.

Fix:

* internal/sql/lang/operator.go — new IsJSONNullableOperator(s)
  classifier covering ->, ->>, #>, #>>.
* internal/compiler/output_columns.go — in the TypeCast case, when the
  wrapped argument is an A_Expr whose operator IsJSONNullableOperator,
  set col.NotNull = false. Tight scope: only the immediate cast arg.
  All other cast paths (literal, column from NOT NULL source, etc.)
  are unchanged — confirmed via regression repros.

Tests:

* internal/sql/lang/operator_test.go — TestIsJSONNullableOperator
  positive + negative table-driven test.
* internal/endtoend/testdata/json_arrow_cast_3792/postgresql/pgx/v5/ —
  end-to-end fixture reproducing the issue under pgx/v5; golden files
  show pgtype.Text for the three ->>-derived columns.

Verified locally:

* Repro from the issue now generates pgtype.Text instead of string.
* ('foo')::text, (column_not_null)::text still emit string. NULL
  literal cast still emits pgtype.Text (existing behavior preserved).
* go test ./internal/sql/... ./internal/compiler/... ./internal/engine/...
  passes; gofmt -l clean on touched files; go vet clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@luongs3 luongs3 force-pushed the fix/3792-json-arrow-cast-nullable branch from 7dc03c6 to 4aa7a49 Compare May 28, 2026 09:41
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.

Postgres JSON ->> operator generates string not *string

1 participant