Skip to content

ast: tag node types and omit absent fields in JSON output - #4591

Closed
kyleconroy wants to merge 2 commits into
mainfrom
claude/release-notes-author-queries-yv4tsn
Closed

ast: tag node types and omit absent fields in JSON output#4591
kyleconroy wants to merge 2 commits into
mainfrom
claude/release-notes-author-queries-yv4tsn

Conversation

@kyleconroy

Copy link
Copy Markdown
Collaborator

Two changes to the JSON that sqlc parse and sqlc analyze --ast print. Both are in the AST encoding only; analyze's own columns and params output is untouched.

Tag every node with its type

ast.Node is an interface, so the JSON encoding carried no record of which node a given object was. Nodes with no fields of their own — A_Star, Null, TODO — all encoded as {}, and an empty List was indistinguishable from them. On INSERT ... RETURNING *, the star and four untranslated clauses all printed as the same empty object.

RawStmt now marshals itself and everything beneath it through a reflective encoder that emits each node's Go type name first:

"Val": {
  "tag": "ColumnRef",
  "Fields": { "tag": "List", "Items": [ { "tag": "A_Star" } ] },
  "Location": 93
}

tag is the one candidate that does not collide with an existing field name. encoding/json matches field names case-insensitively, so kind would capture A_Expr.Kind (10 node types have one), type would capture the eight nodes with a Type field, and node would silently capture SortBy.Node — which holds an interface, so it accepts the tag string without an error and replaces the sort expression. TestNoFieldShadowsTagKey parses the package and fails if a node ever declares a field that collides.

Omit absent fields

Most of what parse printed was fields the statement did not have: on a four-query file, 25% of keys were null and another 9% were empty containers.

A field holding a nil pointer, interface, slice or map is now left out. Zero-valued scalars stay, because zero is a value the parser can find: StmtLocation is 0 for the first statement in every file, and LIMIT 0 parses to an Ival of 0, so omitting zeros would drop what the parser found rather than what it did not.

This depends on the tags landing first. Omitting a nil Items turns an empty List into {}, which without a tag would be indistinguishable from A_Star and TODO — it would have roughly tripled the number of unidentifiable nodes.

Notes

The output is longer than before, not shorter: on the four author queries, 113 lines → 158 with tags → 137 after omission. Tags cost a line per node; what they buy is that every object says what it is.

docs/howto/parse.md already carries a beta notice about the JSON shape changing. It gains sections for both, including that a TODO tag means a clause was parsed but is not represented in the AST, rather than that it was absent from the query.

Testing

go test --tags=examples -timeout 20m ./... passes with PostgreSQL and MySQL running, as do go vet and gofmt.

Eight goldens are regenerated: the seven parse_basic cases and analyze_ast/postgresql. I regenerated all 34 parse and analyze cases; the other 26 came back byte-identical, confirming nothing outside AST output moved. I also checked every regenerated golden programmatically for objects lacking a tag — there are none.

Not covered: no end-to-end case exercises an enum-valued field, since parse_basic is SELECT 1;. That predates this change and is worth adding separately.


Generated by Claude Code

claude added 2 commits August 27, 2026 21:48
Node is an interface, so the JSON that parse and analyze --ast print carried
no record of which node a given object was. Nodes with no fields of their own
all encoded as "{}": a star in a RETURNING clause was indistinguishable from
an untranslated clause, and an empty List was indistinguishable from both.

Encode RawStmt and everything beneath it through a marshaller that emits each
node's Go type name under a "tag" key. RawStmt is the root of the AST both
commands print, so implementing MarshalJSON there covers the whole tree.

"tag" is the one candidate that does not collide with an existing field name.
encoding/json matches field names case-insensitively, so "kind" would capture
A_Expr.Kind, "type" would capture the eight nodes with a Type field, and
"node" would silently capture SortBy.Node, which holds an interface and so
accepts the tag string without error. A test guards the invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9LCt1mwdY3mE14x3Zz5Vw
Most of what parse printed was absent fields. On the four author queries,
25% of the keys were null and another 9% were empty containers, so reading an
AST meant scanning past clauses the statement never had.

Leave a field out when it holds a nil pointer, interface, slice or map. Zeros
stay: StmtLocation is 0 for the first statement in a file and LIMIT 0 parses
to an Ival of 0, so dropping zero-valued scalars would lose what the parser
found rather than what it did not.

This depends on the type tags. Omitting a nil Items turns an empty List into
"{}", which without a tag would be indistinguishable from A_Star and TODO --
it would have roughly tripled the number of unidentifiable nodes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9LCt1mwdY3mE14x3Zz5Vw

Copy link
Copy Markdown
Collaborator Author

Closing in favor of a redesign. The reflective encoder here worked, but the replacement gets the same output from stock encoding/json with no custom marshaller: each node declares a zero-sized Tag NodeTag[T] marker field whose generic type parameter carries the node's identity to MarshalJSON at compile time, nil omission becomes ,omitempty on reference-typed fields, and all field names move to explicit snake_case tags. Superseded by the PR from claude/ast-json-node-tag-yv4tsn.


Generated by Claude Code

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