Skip to content

fix: derive comprehension JSON access from the range, not variable names - #194

Merged
richardwooding merged 1 commit into
mainfrom
fix/remove-remaining-name-lists
Sep 9, 2026
Merged

fix: derive comprehension JSON access from the range, not variable names#194
richardwooding merged 1 commit into
mainfrom
fix/remove-remaining-name-lists

Conversation

@richardwooding

Copy link
Copy Markdown
Contributor

Why

Two hardcoded name lists survived #193, both in the comprehension path. Removing them leaves none in the converter:

$ grep -rn '\[\]string{"' --include='*.go' . | grep -v _test | grep -v /examples/
$

isJSONObjectFieldAccess — variable names

Field access on a comprehension variable extracted from a JSON document whenever the variable was named attr, item, element, obj, feature or review. Both directions were wrong:

range variable before correct
jsonb[] row row.status (composite access on a document) row->>'status'
composite array item item->>'name' (extraction from a row) item.name

It leaked outside comprehensions too — with a schema declaring name as text, item.name.size() rendered as LENGTH(item->>'name') purely because the variable was called item.

isNumericJSONField — field names

Nineteen field names (level, score, price, rating, megapixels, vram, helpful, …) decided whether an extracted JSON value got wrapped in ::numeric. visitCall already drives that from the compared type — which is exactly where v3.7.0 left it when it removed the matching heuristic from visitIdent. The list was redundant.

How

A comprehension variable is now bound together with whether its range yields JSON, decided by the element type: a jsonb[] column unnests to documents, an array of a composite type unnests to rows.

The key move is that such a variable is then treated exactly like one declared through WithJSONVariables, scoped to the comprehension body. That reuses the existing JSON path machinery instead of duplicating it — which nested access needs:

r.metadata.active  over a jsonb[]
  name-based:  r->>'metadata'.active    ← extracts to text, then dots into it
  now:         r->'metadata'->>'active'

The binding is scoped like iterVars, because comprehensions nest and an inner one may reuse a name the outer bound to a different kind of collection. There's a test for that.

With the object-access branch gone, visitSelect loses its three-way switch and becomes a straight "JSON path or column".

Two tests corrected

Both pinned the old behaviour:

test was now
filter with nested field access on JSON — range is jsonb[], so r is a document r.metadata.active r->'metadata'->>'active'
TestIssue85_SizeOutsideComprehensionname is a text column LENGTH(item->>'name') LENGTH(item.name)

The second's own comment ("item.name will be treated as a struct field, generating item->>'name' for JSON") described a text column, so it was already confused.

Notably (item->>'price')::numeric > 10 from docs/json-support.md is unchanged — it just gets there because items is declared jsonb and 10 is numeric, rather than because the variable is called item and the field is called price.

Tests

New comprehension_json_vars_test.go: a jsonb range with an unlisted variable name; a composite range with a formerly-listed name; a scalar array; the numeric cast without the name list; nested access; the documented jsonb array-value example; all six formerly-listed names outside a comprehension getting no special treatment; and nested comprehensions where the inner rebinds the outer's name to a different kind of range.

⚠️ Please rely on CI

Docker isn't available in my environment, so the 10 pg/ testcontainer tests fail locally — identically on main, so it's environmental. pg/provider_testcontainer_test.go holds exact-SQL assertions over real comprehension queries, so that job is the gate. Everything else is clean: go build, go vet, golangci-lint (0 issues), go test -short.

Breaking

Field access on a comprehension variable follows the schema: iterating a composite array yields column access, iterating a jsonb range yields extraction, whichever the variable is called. Callers who relied on a variable name to force JSON treatment should declare the range as JSON in WithSchemas.

Pre-existing, not touched

docs/json-support.md:145 documents jsonb_array_elements(order.items) for a jsonb array value, but the converter emits UNNEST — identically before and after this change. Separate issue.

🤖 Generated with Claude Code

Two hardcoded name lists remained after #193, both in the comprehension
path. Removing them leaves none in the converter.

isJSONObjectFieldAccess treated field access on a comprehension variable as
JSON extraction whenever the variable was named attr, item, element, obj,
feature or review. Both directions were wrong. A jsonb[] column iterated as
"row" was treated as a composite and produced row.status; an array of a
composite type iterated as "item" produced item->>'name' for a real column.
The name leaked outside comprehensions too: with a schema declaring name as
text, item.name.size() rendered as LENGTH(item->>'name').

A comprehension variable is now bound along with whether its range yields
JSON, decided by the element type of the range, and a variable bound that way
is treated exactly like one declared through WithJSONVariables for as long as
the body is being written. That reuses the existing JSON path machinery
rather than duplicating it, which nested access needs: r.metadata.active over
a jsonb[] now routes through the path builder to r->'metadata'->>'active',
where the name-based branch extracted the first segment as text and then
dotted into it, producing r->>'metadata'.active.

The binding is scoped like iterVars, since comprehensions nest and an inner
one may reuse a name the outer bound to a different kind of collection.

isNumericJSONField carried nineteen field names — level, score, price,
rating, megapixels, vram, helpful and so on — deciding whether an extracted
JSON value was wrapped in ::numeric. visitCall already drives that from the
compared type, which is where v3.7.0 left it when it removed the matching
heuristic from visitIdent, so the list was redundant. (item->>'price')::numeric
> 10 is unchanged, now because 10 is numeric rather than because the field is
called price. With the branch gone, visitSelect loses its three-way switch.

Two tests pinned the old behaviour and are corrected: a filter over a jsonb[]
column expected r.metadata.active for a document, and the issue-85 test
expected item->>'name' for a text column.

BREAKING: field access on a comprehension variable follows the schema.
Iterating a composite array yields column access and iterating a jsonb range
yields extraction, whichever the variable is called.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@richardwooding
richardwooding merged commit d06c905 into main Sep 9, 2026
9 checks passed
@richardwooding
richardwooding deleted the fix/remove-remaining-name-lists branch September 9, 2026 17:18
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.

1 participant