Skip to content

Commit c554d76

Browse files
committed
core/analyzer: bind FROM items against the scope being built
A set-returning function in FROM takes its arguments from the items before it, so bindRangeFunction types the call while the scope is still being assembled. The scope under construction was never installed on the analyzer, so a.scope was nil and resolving one of those arguments dereferenced it: FROM transactions, jsonb_each(jsonb_extract_path(transactions.data, '...')) AS x buildScope and relationScope now install the scope they are filling for as long as they fill it, and put back the one they replaced on the way out. Resolving against a nil scope also reports the column as unresolved rather than crashing, so no other half-built statement can panic on the way to the error it was going to report anyway. Both queries now analyze to an ordinary error instead of taking down the process, which is what the rest of the corpus needed: TestReplay's core context runs to completion in one process rather than aborting partway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161e7oMkzNW9DZMUPtyQibH
1 parent e56435b commit c554d76

2 files changed

Lines changed: 19 additions & 0 deletions

File tree

internal/core/analyzer/dml.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func (a *analyzer) analyzeDelete(s *ast.DeleteStmt) error {
8383
// report a multi-table DELETE that way — a single FROM node.
8484
func (a *analyzer) relationScope(relations, extra *ast.List, from ast.Node) (*scope, error) {
8585
sc := &scope{}
86+
defer a.binding(sc)()
8687
for _, item := range listItems(relations) {
8788
if err := a.appendFromItem(sc, item); err != nil {
8889
return nil, err

internal/core/analyzer/scope.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type scopeRel struct {
2222
func (a *analyzer) buildScope(from *ast.List) (*scope, error) {
2323
items := listItems(from)
2424
sc := &scope{rels: make([]scopeRel, 0, len(items))}
25+
defer a.binding(sc)()
2526
for _, item := range items {
2627
if err := a.appendFromItem(sc, item); err != nil {
2728
return nil, err
@@ -30,6 +31,17 @@ func (a *analyzer) buildScope(from *ast.List) (*scope, error) {
3031
return sc, nil
3132
}
3233

34+
// binding makes the scope under construction the one the analyzer resolves
35+
// against, and returns the func that puts back the scope it replaced. A FROM
36+
// item can refer to the ones before it — a set-returning function takes its
37+
// arguments from them — so binding an item has to see what is bound so far
38+
// rather than no scope at all.
39+
func (a *analyzer) binding(sc *scope) func() {
40+
prev := a.scope
41+
a.scope = sc
42+
return func() { a.scope = prev }
43+
}
44+
3345
func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error {
3446
switch v := item.(type) {
3547
case *ast.RangeVar:
@@ -202,6 +214,12 @@ func (a *analyzer) resolveColumn(relation, column string) (scopeRel, core.ClassC
202214
// relation. It reports an error when more than one relation in scope offers
203215
// that name.
204216
func (s *scope) resolveColumn(relation, column string) (rel scopeRel, col core.ClassColumn, ok bool, err error) {
217+
// A statement whose scope is not built yet offers no columns. Report the
218+
// column as unresolved and let the caller say so, rather than crashing on
219+
// the way to the same answer.
220+
if s == nil {
221+
return rel, col, false, nil
222+
}
205223
found := 0
206224
for _, r := range s.rels {
207225
if relation != "" && r.alias != relation {

0 commit comments

Comments
 (0)