From 04dba04cd77ddfb4c7c45feee524f7cef0e0618b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 21:48:04 +0000 Subject: [PATCH 1/2] ast: tag every node with its type in JSON output 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 Claude-Session: https://claude.ai/code/session_01T9LCt1mwdY3mE14x3Zz5Vw --- docs/howto/analyze.md | 4 +- docs/howto/parse.md | 27 ++++ .../analyze_ast/postgresql/stdout.txt | 35 +++++- .../parse_basic/clickhouse/stdout.txt | 6 + .../testdata/parse_basic/duckdb/stdout.txt | 6 + .../testdata/parse_basic/googlesql/stdout.txt | 6 + .../testdata/parse_basic/mssql/stdout.txt | 6 + .../testdata/parse_basic/mysql/stdout.txt | 9 ++ .../parse_basic/postgresql/stdout.txt | 30 ++++- .../testdata/parse_basic/sqlite/stdout.txt | 10 ++ internal/sql/ast/json.go | 116 ++++++++++++++++++ internal/sql/ast/tag_test.go | 49 ++++++++ 12 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 internal/sql/ast/json.go create mode 100644 internal/sql/ast/tag_test.go diff --git a/docs/howto/analyze.md b/docs/howto/analyze.md index e7c2c66b41..87541d093e 100644 --- a/docs/howto/analyze.md +++ b/docs/howto/analyze.md @@ -106,4 +106,6 @@ reports the result columns and parameters: ] ``` -Pass `--ast` to also include each statement's parsed AST under an `ast` key. +Pass `--ast` to also include each statement's parsed AST under an `ast` key. It +has the same shape as the output of [`parse`](parse.md), with every node tagged +by type. diff --git a/docs/howto/parse.md b/docs/howto/parse.md index 2227c1f2b6..816693be2e 100644 --- a/docs/howto/parse.md +++ b/docs/howto/parse.md @@ -48,6 +48,7 @@ The output is a JSON array with one object per statement: "name": "GetAuthor", "cmd": ":one", "ast": { + "tag": "RawStmt", "Stmt": { "...": "..." }, @@ -60,3 +61,29 @@ The output is a JSON array with one object per statement: Statements without a `-- name:` annotation (for example schema DDL) omit the `name` and `cmd` fields. + +## Node types + +Every node in the AST carries a `tag` naming its type. Some nodes have no +fields of their own, so without it a star, a null literal and an untranslated +clause would all print as `{}`. + +```json +"Val": { + "tag": "ColumnRef", + "Name": "", + "Fields": { + "tag": "List", + "Items": [ + { + "tag": "A_Star" + } + ] + }, + "Location": 93 +} +``` + +A `tag` of `TODO` marks a clause the dialect's converter does not translate +yet. It means the clause was parsed but is not represented in the AST, not that +the clause was absent from the query. diff --git a/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt b/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt index b74264f687..eec934554f 100644 --- a/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt +++ b/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt @@ -24,23 +24,32 @@ } ], "ast": { + "tag": "RawStmt", "Stmt": { + "tag": "SelectStmt", "DistinctClause": { + "tag": "List", "Items": null }, "IntoClause": null, "TargetList": { + "tag": "List", "Items": [ { + "tag": "ResTarget", "Name": null, "Indirection": { + "tag": "List", "Items": null }, "Val": { + "tag": "ColumnRef", "Name": "", "Fields": { + "tag": "List", "Items": [ { + "tag": "String", "Str": "name" } ] @@ -52,8 +61,10 @@ ] }, "FromClause": { + "tag": "List", "Items": [ { + "tag": "RangeVar", "Catalogname": null, "Schemaname": null, "Relname": "authors", @@ -65,19 +76,25 @@ ] }, "WhereClause": { + "tag": "A_Expr", "Kind": 1, "Name": { + "tag": "List", "Items": [ { + "tag": "String", "Str": "=" } ] }, "Lexpr": { + "tag": "ColumnRef", "Name": "", "Fields": { + "tag": "List", "Items": [ { + "tag": "String", "Str": "id" } ] @@ -85,6 +102,7 @@ "Location": 59 }, "Rexpr": { + "tag": "ParamRef", "Number": 1, "Location": 64, "Dollar": true @@ -92,21 +110,32 @@ "Location": 62 }, "GroupClause": { + "tag": "List", "Items": null }, - "HavingClause": {}, + "HavingClause": { + "tag": "TODO" + }, "WindowClause": { + "tag": "List", "Items": null }, "ValuesLists": { + "tag": "List", "Items": null }, "SortClause": { + "tag": "List", "Items": null }, - "LimitOffset": {}, - "LimitCount": {}, + "LimitOffset": { + "tag": "TODO" + }, + "LimitCount": { + "tag": "TODO" + }, "LockingClause": { + "tag": "List", "Items": null }, "WithClause": null, diff --git a/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt b/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt index 28a5ce7e1f..02a280a4ff 100644 --- a/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt @@ -3,16 +3,22 @@ "name": "GetValue", "cmd": ":one", "ast": { + "tag": "RawStmt", "Stmt": { + "tag": "SelectStmt", "DistinctClause": null, "IntoClause": null, "TargetList": { + "tag": "List", "Items": [ { + "tag": "ResTarget", "Name": null, "Indirection": null, "Val": { + "tag": "A_Const", "Val": { + "tag": "Integer", "Ival": 1 }, "Location": 31 diff --git a/internal/endtoend/testdata/parse_basic/duckdb/stdout.txt b/internal/endtoend/testdata/parse_basic/duckdb/stdout.txt index ca847e616a..f3a5c9edb6 100644 --- a/internal/endtoend/testdata/parse_basic/duckdb/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/duckdb/stdout.txt @@ -3,16 +3,22 @@ "name": "GetValue", "cmd": ":one", "ast": { + "tag": "RawStmt", "Stmt": { + "tag": "SelectStmt", "DistinctClause": null, "IntoClause": null, "TargetList": { + "tag": "List", "Items": [ { + "tag": "ResTarget", "Name": null, "Indirection": null, "Val": { + "tag": "A_Const", "Val": { + "tag": "Integer", "Ival": 1 }, "Location": 30 diff --git a/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt b/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt index 086f885402..06f4a4deb9 100644 --- a/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt @@ -3,16 +3,22 @@ "name": "GetValue", "cmd": ":one", "ast": { + "tag": "RawStmt", "Stmt": { + "tag": "SelectStmt", "DistinctClause": null, "IntoClause": null, "TargetList": { + "tag": "List", "Items": [ { + "tag": "ResTarget", "Name": null, "Indirection": null, "Val": { + "tag": "A_Const", "Val": { + "tag": "Integer", "Ival": 1 }, "Location": 30 diff --git a/internal/endtoend/testdata/parse_basic/mssql/stdout.txt b/internal/endtoend/testdata/parse_basic/mssql/stdout.txt index b20fbdcee5..e1cdbd9086 100644 --- a/internal/endtoend/testdata/parse_basic/mssql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/mssql/stdout.txt @@ -3,16 +3,22 @@ "name": "GetValue", "cmd": ":one", "ast": { + "tag": "RawStmt", "Stmt": { + "tag": "SelectStmt", "DistinctClause": null, "IntoClause": null, "TargetList": { + "tag": "List", "Items": [ { + "tag": "ResTarget", "Name": null, "Indirection": null, "Val": { + "tag": "A_Const", "Val": { + "tag": "Integer", "Ival": 1 }, "Location": 30 diff --git a/internal/endtoend/testdata/parse_basic/mysql/stdout.txt b/internal/endtoend/testdata/parse_basic/mysql/stdout.txt index e9ed28784f..580120ab58 100644 --- a/internal/endtoend/testdata/parse_basic/mysql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/mysql/stdout.txt @@ -3,16 +3,22 @@ "name": "GetValue", "cmd": ":one", "ast": { + "tag": "RawStmt", "Stmt": { + "tag": "SelectStmt", "DistinctClause": null, "IntoClause": null, "TargetList": { + "tag": "List", "Items": [ { + "tag": "ResTarget", "Name": null, "Indirection": null, "Val": { + "tag": "A_Const", "Val": { + "tag": "Integer", "Ival": 1 }, "Location": 30 @@ -22,14 +28,17 @@ ] }, "FromClause": { + "tag": "List", "Items": null }, "WhereClause": null, "GroupClause": { + "tag": "List", "Items": null }, "HavingClause": null, "WindowClause": { + "tag": "List", "Items": [] }, "ValuesLists": null, diff --git a/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt b/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt index fe35a664c7..1b3abb1ea6 100644 --- a/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt @@ -3,20 +3,28 @@ "name": "GetValue", "cmd": ":one", "ast": { + "tag": "RawStmt", "Stmt": { + "tag": "SelectStmt", "DistinctClause": { + "tag": "List", "Items": null }, "IntoClause": null, "TargetList": { + "tag": "List", "Items": [ { + "tag": "ResTarget", "Name": null, "Indirection": { + "tag": "List", "Items": null }, "Val": { + "tag": "A_Const", "Val": { + "tag": "Integer", "Ival": 1 }, "Location": 30 @@ -26,25 +34,39 @@ ] }, "FromClause": { + "tag": "List", "Items": null }, - "WhereClause": {}, + "WhereClause": { + "tag": "TODO" + }, "GroupClause": { + "tag": "List", "Items": null }, - "HavingClause": {}, + "HavingClause": { + "tag": "TODO" + }, "WindowClause": { + "tag": "List", "Items": null }, "ValuesLists": { + "tag": "List", "Items": null }, "SortClause": { + "tag": "List", "Items": null }, - "LimitOffset": {}, - "LimitCount": {}, + "LimitOffset": { + "tag": "TODO" + }, + "LimitCount": { + "tag": "TODO" + }, "LockingClause": { + "tag": "List", "Items": null }, "WithClause": null, diff --git a/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt b/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt index c1303a9a1e..652d9bdaaf 100644 --- a/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt @@ -3,16 +3,22 @@ "name": "GetValue", "cmd": ":one", "ast": { + "tag": "RawStmt", "Stmt": { + "tag": "SelectStmt", "DistinctClause": null, "IntoClause": null, "TargetList": { + "tag": "List", "Items": [ { + "tag": "ResTarget", "Name": null, "Indirection": null, "Val": { + "tag": "A_Const", "Val": { + "tag": "Integer", "Ival": 1 }, "Location": 30 @@ -22,17 +28,21 @@ ] }, "FromClause": { + "tag": "List", "Items": null }, "WhereClause": null, "GroupClause": { + "tag": "List", "Items": null }, "HavingClause": null, "WindowClause": { + "tag": "List", "Items": null }, "ValuesLists": { + "tag": "List", "Items": null }, "SortClause": null, diff --git a/internal/sql/ast/json.go b/internal/sql/ast/json.go new file mode 100644 index 0000000000..23fbc052de --- /dev/null +++ b/internal/sql/ast/json.go @@ -0,0 +1,116 @@ +package ast + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// TagKey is the JSON key under which a node's concrete type is reported. +// +// Node is an interface, so the JSON encoding of an AST would otherwise carry no +// record of which node a given object is. Nodes with no fields (A_Star, Null, +// TODO) all encode as "{}", and an empty List is indistinguishable from them. +// Every node object is emitted with this key first, holding the name of its Go +// type. +const TagKey = "tag" + +var nodeType = reflect.TypeOf((*Node)(nil)).Elem() + +// MarshalJSON encodes the statement and every node beneath it, tagging each +// node object with its type. RawStmt is the root of the AST that the parse and +// analyze commands print, so implementing it here is enough to tag a whole +// tree. +func (n *RawStmt) MarshalJSON() ([]byte, error) { + if n == nil { + return []byte("null"), nil + } + return marshalValue(reflect.ValueOf(n)) +} + +func marshalValue(v reflect.Value) ([]byte, error) { + switch v.Kind() { + case reflect.Invalid: + return []byte("null"), nil + + case reflect.Interface, reflect.Pointer: + if v.IsNil() { + return []byte("null"), nil + } + return marshalValue(v.Elem()) + + case reflect.Struct: + return marshalStruct(v) + + case reflect.Slice: + if v.IsNil() { + return []byte("null"), nil + } + fallthrough + case reflect.Array: + return marshalArray(v) + + default: + // Scalars, strings and anything else encoding/json already handles. + return json.Marshal(v.Interface()) + } +} + +func marshalStruct(v reflect.Value) ([]byte, error) { + var buf bytes.Buffer + buf.WriteByte('{') + + // A node's Pos method is declared on the pointer type. + if reflect.PointerTo(v.Type()).Implements(nodeType) { + fmt.Fprintf(&buf, "%q:%q", TagKey, v.Type().Name()) + } + + t := v.Type() + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if !field.IsExported() { + continue + } + name := field.Name + if tag, ok := field.Tag.Lookup("json"); ok { + tagName, _, _ := strings.Cut(tag, ",") + if tagName == "-" { + continue + } + if tagName != "" { + name = tagName + } + } + value, err := marshalValue(v.Field(i)) + if err != nil { + return nil, err + } + if buf.Len() > 1 { + buf.WriteByte(',') + } + fmt.Fprintf(&buf, "%q:", name) + buf.Write(value) + } + + buf.WriteByte('}') + return buf.Bytes(), nil +} + +func marshalArray(v reflect.Value) ([]byte, error) { + var buf bytes.Buffer + buf.WriteByte('[') + for i := 0; i < v.Len(); i++ { + if i > 0 { + buf.WriteByte(',') + } + item, err := marshalValue(v.Index(i)) + if err != nil { + return nil, err + } + buf.Write(item) + } + buf.WriteByte(']') + return buf.Bytes(), nil +} diff --git a/internal/sql/ast/tag_test.go b/internal/sql/ast/tag_test.go new file mode 100644 index 0000000000..3165ca06f2 --- /dev/null +++ b/internal/sql/ast/tag_test.go @@ -0,0 +1,49 @@ +package ast + +import ( + "go/ast" + "go/parser" + "go/token" + "strings" + "testing" +) + +// TestNoFieldShadowsTagKey guards the JSON encoding in json.go: every node +// object is emitted with a TagKey key naming its type, so no node may declare a +// field of its own that collides with it. encoding/json matches field names +// case-insensitively, so a field named "Tag" would silently capture the tag +// when the output is decoded back into a node. +// +// This is a unit test because the invariant is about the declarations in this +// package, not about anything sqlc produces. No SQL input can exercise a field +// that does not exist yet, so the end-to-end tests cannot catch the day someone +// adds one. +func TestNoFieldShadowsTagKey(t *testing.T) { + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, ".", nil, 0) + if err != nil { + t.Fatalf("parsing package: %s", err) + } + + for _, pkg := range pkgs { + ast.Inspect(pkg, func(n ast.Node) bool { + spec, ok := n.(*ast.TypeSpec) + if !ok { + return true + } + structType, ok := spec.Type.(*ast.StructType) + if !ok { + return true + } + for _, field := range structType.Fields.List { + for _, name := range field.Names { + if strings.EqualFold(name.Name, TagKey) { + t.Errorf("%s declares a field %q, which collides with the %q key used to tag node types in JSON. Rename the field or pick a different TagKey.", + spec.Name.Name, name.Name, TagKey) + } + } + } + return true + }) + } +} From 246970c6cb35f38f85a5cafa01497694e8dc06b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 02:14:35 +0000 Subject: [PATCH 2/2] ast: omit absent fields from JSON output 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 Claude-Session: https://claude.ai/code/session_01T9LCt1mwdY3mE14x3Zz5Vw --- docs/howto/parse.md | 20 ++++++++++++ .../analyze_ast/postgresql/stdout.txt | 31 +++++-------------- .../parse_basic/clickhouse/stdout.txt | 19 +----------- .../testdata/parse_basic/duckdb/stdout.txt | 19 +----------- .../testdata/parse_basic/googlesql/stdout.txt | 19 +----------- .../testdata/parse_basic/mssql/stdout.txt | 19 +----------- .../testdata/parse_basic/mysql/stdout.txt | 22 ++----------- .../parse_basic/postgresql/stdout.txt | 31 ++++++------------- .../testdata/parse_basic/sqlite/stdout.txt | 27 +++------------- internal/sql/ast/json.go | 15 +++++++++ 10 files changed, 64 insertions(+), 158 deletions(-) diff --git a/docs/howto/parse.md b/docs/howto/parse.md index 816693be2e..623e3b8b90 100644 --- a/docs/howto/parse.md +++ b/docs/howto/parse.md @@ -87,3 +87,23 @@ clause would all print as `{}`. A `tag` of `TODO` marks a clause the dialect's converter does not translate yet. It means the clause was parsed but is not represented in the AST, not that the clause was absent from the query. + +## Absent fields + +A field the statement does not use is left out rather than printed as `null`. +An `A_Const` carrying an integer reports only what it has: + +```json +"Val": { + "tag": "A_Const", + "Val": { + "tag": "Integer", + "Ival": 1 + }, + "Location": 30 +} +``` + +Only absent fields are omitted. A zero keeps its place, because zero is a value +the parser can find: `StmtLocation` is 0 for the first statement in a file, and +`LIMIT 0` parses to an `Ival` of 0. diff --git a/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt b/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt index eec934554f..ab257bea23 100644 --- a/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt +++ b/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt @@ -28,19 +28,15 @@ "Stmt": { "tag": "SelectStmt", "DistinctClause": { - "tag": "List", - "Items": null + "tag": "List" }, - "IntoClause": null, "TargetList": { "tag": "List", "Items": [ { "tag": "ResTarget", - "Name": null, "Indirection": { - "tag": "List", - "Items": null + "tag": "List" }, "Val": { "tag": "ColumnRef", @@ -65,12 +61,9 @@ "Items": [ { "tag": "RangeVar", - "Catalogname": null, - "Schemaname": null, "Relname": "authors", "Inh": true, "Relpersistence": 112, - "Alias": null, "Location": 45 } ] @@ -110,23 +103,19 @@ "Location": 62 }, "GroupClause": { - "tag": "List", - "Items": null + "tag": "List" }, "HavingClause": { "tag": "TODO" }, "WindowClause": { - "tag": "List", - "Items": null + "tag": "List" }, "ValuesLists": { - "tag": "List", - "Items": null + "tag": "List" }, "SortClause": { - "tag": "List", - "Items": null + "tag": "List" }, "LimitOffset": { "tag": "TODO" @@ -135,14 +124,10 @@ "tag": "TODO" }, "LockingClause": { - "tag": "List", - "Items": null + "tag": "List" }, - "WithClause": null, "Op": 0, - "All": false, - "Larg": null, - "Rarg": null + "All": false }, "StmtLocation": 0, "StmtLen": 66 diff --git a/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt b/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt index 02a280a4ff..8b2e6a9f56 100644 --- a/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt @@ -6,15 +6,11 @@ "tag": "RawStmt", "Stmt": { "tag": "SelectStmt", - "DistinctClause": null, - "IntoClause": null, "TargetList": { "tag": "List", "Items": [ { "tag": "ResTarget", - "Name": null, - "Indirection": null, "Val": { "tag": "A_Const", "Val": { @@ -27,21 +23,8 @@ } ] }, - "FromClause": null, - "WhereClause": null, - "GroupClause": null, - "HavingClause": null, - "WindowClause": null, - "ValuesLists": null, - "SortClause": null, - "LimitOffset": null, - "LimitCount": null, - "LockingClause": null, - "WithClause": null, "Op": 0, - "All": false, - "Larg": null, - "Rarg": null + "All": false }, "StmtLocation": 0, "StmtLen": 32 diff --git a/internal/endtoend/testdata/parse_basic/duckdb/stdout.txt b/internal/endtoend/testdata/parse_basic/duckdb/stdout.txt index f3a5c9edb6..cae0296ac7 100644 --- a/internal/endtoend/testdata/parse_basic/duckdb/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/duckdb/stdout.txt @@ -6,15 +6,11 @@ "tag": "RawStmt", "Stmt": { "tag": "SelectStmt", - "DistinctClause": null, - "IntoClause": null, "TargetList": { "tag": "List", "Items": [ { "tag": "ResTarget", - "Name": null, - "Indirection": null, "Val": { "tag": "A_Const", "Val": { @@ -27,21 +23,8 @@ } ] }, - "FromClause": null, - "WhereClause": null, - "GroupClause": null, - "HavingClause": null, - "WindowClause": null, - "ValuesLists": null, - "SortClause": null, - "LimitOffset": null, - "LimitCount": null, - "LockingClause": null, - "WithClause": null, "Op": 0, - "All": false, - "Larg": null, - "Rarg": null + "All": false }, "StmtLocation": 0, "StmtLen": 33 diff --git a/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt b/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt index 06f4a4deb9..715536dec9 100644 --- a/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/googlesql/stdout.txt @@ -6,15 +6,11 @@ "tag": "RawStmt", "Stmt": { "tag": "SelectStmt", - "DistinctClause": null, - "IntoClause": null, "TargetList": { "tag": "List", "Items": [ { "tag": "ResTarget", - "Name": null, - "Indirection": null, "Val": { "tag": "A_Const", "Val": { @@ -27,21 +23,8 @@ } ] }, - "FromClause": null, - "WhereClause": null, - "GroupClause": null, - "HavingClause": null, - "WindowClause": null, - "ValuesLists": null, - "SortClause": null, - "LimitOffset": null, - "LimitCount": null, - "LockingClause": null, - "WithClause": null, "Op": 0, - "All": false, - "Larg": null, - "Rarg": null + "All": false }, "StmtLocation": 0, "StmtLen": 31 diff --git a/internal/endtoend/testdata/parse_basic/mssql/stdout.txt b/internal/endtoend/testdata/parse_basic/mssql/stdout.txt index e1cdbd9086..39e5aa0df6 100644 --- a/internal/endtoend/testdata/parse_basic/mssql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/mssql/stdout.txt @@ -6,15 +6,11 @@ "tag": "RawStmt", "Stmt": { "tag": "SelectStmt", - "DistinctClause": null, - "IntoClause": null, "TargetList": { "tag": "List", "Items": [ { "tag": "ResTarget", - "Name": null, - "Indirection": null, "Val": { "tag": "A_Const", "Val": { @@ -27,21 +23,8 @@ } ] }, - "FromClause": null, - "WhereClause": null, - "GroupClause": null, - "HavingClause": null, - "WindowClause": null, - "ValuesLists": null, - "SortClause": null, - "LimitOffset": null, - "LimitCount": null, - "LockingClause": null, - "WithClause": null, "Op": 0, - "All": false, - "Larg": null, - "Rarg": null + "All": false }, "StmtLocation": 0, "StmtLen": 32 diff --git a/internal/endtoend/testdata/parse_basic/mysql/stdout.txt b/internal/endtoend/testdata/parse_basic/mysql/stdout.txt index 580120ab58..5ee2116255 100644 --- a/internal/endtoend/testdata/parse_basic/mysql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/mysql/stdout.txt @@ -6,15 +6,11 @@ "tag": "RawStmt", "Stmt": { "tag": "SelectStmt", - "DistinctClause": null, - "IntoClause": null, "TargetList": { "tag": "List", "Items": [ { "tag": "ResTarget", - "Name": null, - "Indirection": null, "Val": { "tag": "A_Const", "Val": { @@ -28,29 +24,17 @@ ] }, "FromClause": { - "tag": "List", - "Items": null + "tag": "List" }, - "WhereClause": null, "GroupClause": { - "tag": "List", - "Items": null + "tag": "List" }, - "HavingClause": null, "WindowClause": { "tag": "List", "Items": [] }, - "ValuesLists": null, - "SortClause": null, - "LimitOffset": null, - "LimitCount": null, - "LockingClause": null, - "WithClause": null, "Op": 0, - "All": false, - "Larg": null, - "Rarg": null + "All": false }, "StmtLocation": 0, "StmtLen": 31 diff --git a/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt b/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt index 1b3abb1ea6..deaaea7d6c 100644 --- a/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt @@ -7,19 +7,15 @@ "Stmt": { "tag": "SelectStmt", "DistinctClause": { - "tag": "List", - "Items": null + "tag": "List" }, - "IntoClause": null, "TargetList": { "tag": "List", "Items": [ { "tag": "ResTarget", - "Name": null, "Indirection": { - "tag": "List", - "Items": null + "tag": "List" }, "Val": { "tag": "A_Const", @@ -34,30 +30,25 @@ ] }, "FromClause": { - "tag": "List", - "Items": null + "tag": "List" }, "WhereClause": { "tag": "TODO" }, "GroupClause": { - "tag": "List", - "Items": null + "tag": "List" }, "HavingClause": { "tag": "TODO" }, "WindowClause": { - "tag": "List", - "Items": null + "tag": "List" }, "ValuesLists": { - "tag": "List", - "Items": null + "tag": "List" }, "SortClause": { - "tag": "List", - "Items": null + "tag": "List" }, "LimitOffset": { "tag": "TODO" @@ -66,14 +57,10 @@ "tag": "TODO" }, "LockingClause": { - "tag": "List", - "Items": null + "tag": "List" }, - "WithClause": null, "Op": 0, - "All": false, - "Larg": null, - "Rarg": null + "All": false }, "StmtLocation": 0, "StmtLen": 31 diff --git a/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt b/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt index 652d9bdaaf..50d4c72330 100644 --- a/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt @@ -6,15 +6,11 @@ "tag": "RawStmt", "Stmt": { "tag": "SelectStmt", - "DistinctClause": null, - "IntoClause": null, "TargetList": { "tag": "List", "Items": [ { "tag": "ResTarget", - "Name": null, - "Indirection": null, "Val": { "tag": "A_Const", "Val": { @@ -28,32 +24,19 @@ ] }, "FromClause": { - "tag": "List", - "Items": null + "tag": "List" }, - "WhereClause": null, "GroupClause": { - "tag": "List", - "Items": null + "tag": "List" }, - "HavingClause": null, "WindowClause": { - "tag": "List", - "Items": null + "tag": "List" }, "ValuesLists": { - "tag": "List", - "Items": null + "tag": "List" }, - "SortClause": null, - "LimitOffset": null, - "LimitCount": null, - "LockingClause": null, - "WithClause": null, "Op": 0, - "All": false, - "Larg": null, - "Rarg": null + "All": false }, "StmtLocation": 0, "StmtLen": 31 diff --git a/internal/sql/ast/json.go b/internal/sql/ast/json.go index 23fbc052de..cf33cd69d7 100644 --- a/internal/sql/ast/json.go +++ b/internal/sql/ast/json.go @@ -83,6 +83,9 @@ func marshalStruct(v reflect.Value) ([]byte, error) { name = tagName } } + if isNil(v.Field(i)) { + continue + } value, err := marshalValue(v.Field(i)) if err != nil { return nil, err @@ -98,6 +101,18 @@ func marshalStruct(v reflect.Value) ([]byte, error) { return buf.Bytes(), nil } +// isNil reports whether a field is absent from the tree, which is the only +// thing left out of the encoding. Zero-valued scalars are kept: a Location of 0 +// is the start of the file and an Ival of 0 is the literal in "LIMIT 0", so +// dropping them would lose what the parser found. +func isNil(v reflect.Value) bool { + switch v.Kind() { + case reflect.Pointer, reflect.Interface, reflect.Slice, reflect.Map: + return v.IsNil() + } + return false +} + func marshalArray(v reflect.Value) ([]byte, error) { var buf bytes.Buffer buf.WriteByte('[')