From 36dd4dcbe55fc2d134f5d98e9f7e9750bd10ed70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:23:26 +0000 Subject: [PATCH 1/9] Initial plan From 51ff38de2fdd2778655822c9f61dcd1c8fc8e324 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:29:08 +0000 Subject: [PATCH 2/9] Add support for schema.table.column expressions - Modified QualifiedRef AST to include Schema field - Updated parseQualifiedRef to handle three-part identifiers - Added tests for schema.table.column expressions - Updated walk.go to traverse Schema field Co-authored-by: otoolep <536312+otoolep@users.noreply.github.com> --- ast.go | 19 +++++++++++++------ parser.go | 21 +++++++++++++++++++++ parser_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ walk.go | 5 +++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/ast.go b/ast.go index 56dd5d55..dfe49512 100644 --- a/ast.go +++ b/ast.go @@ -2055,10 +2055,12 @@ func (r *Range) String() string { } type QualifiedRef struct { - Table *Ident // table name - Dot Pos // position of dot - Star Pos // position of * (result column only) - Column *Ident // column name + Schema *Ident // optional schema name + SchemaPos Pos // position of schema dot + Table *Ident // table name + Dot Pos // position of dot + Star Pos // position of * (result column only) + Column *Ident // column name } // Clone returns a deep copy of r. @@ -2067,6 +2069,7 @@ func (r *QualifiedRef) Clone() *QualifiedRef { return nil } other := *r + other.Schema = r.Schema.Clone() other.Table = r.Table.Clone() other.Column = r.Column.Clone() return &other @@ -2074,10 +2077,14 @@ func (r *QualifiedRef) Clone() *QualifiedRef { // String returns the string representation of the expression. func (r *QualifiedRef) String() string { + var s string + if r.Schema != nil { + s = r.Schema.String() + "." + } if r.Star.IsValid() { - return fmt.Sprintf("%s.*", r.Table.String()) + return s + fmt.Sprintf("%s.*", r.Table.String()) } - return fmt.Sprintf("%s.%s", r.Table.String(), r.Column.String()) + return s + fmt.Sprintf("%s.%s", r.Table.String(), r.Column.String()) } type Call struct { diff --git a/parser.go b/parser.go index 987cb77a..c19bf4b9 100644 --- a/parser.go +++ b/parser.go @@ -2697,6 +2697,27 @@ func (p *Parser) parseQualifiedRef(table *Ident) (_ *QualifiedRef, err error) { } else if isIdentToken(p.peek()) { pos, tok, lit := p.scan() expr.Column = &Ident{Name: lit, NamePos: pos, Quoted: tok == QIDENT || tok == BIDENT} + + // Check if there's another DOT for schema.table.column format + if p.peek() == DOT { + // What we thought was table.column is actually schema.table + // Shift the values: table -> schema, column -> table + expr.Schema = expr.Table + expr.SchemaPos = expr.Dot + expr.Table = expr.Column + expr.Dot, _, _ = p.scan() + + // Now parse the actual column + if p.peek() == STAR { + expr.Column = nil + expr.Star, _, _ = p.scan() + } else if isIdentToken(p.peek()) { + pos, tok, lit := p.scan() + expr.Column = &Ident{Name: lit, NamePos: pos, Quoted: tok == QIDENT || tok == BIDENT} + } else { + return &expr, p.errorExpected(p.pos, p.tok, "column name") + } + } } else { return &expr, p.errorExpected(p.pos, p.tok, "column name") } diff --git a/parser_test.go b/parser_test.go index bb42d3b1..0ba9823b 100644 --- a/parser_test.go +++ b/parser_test.go @@ -3900,6 +3900,35 @@ func TestParser_ParseStatement(t *testing.T) { }}, }) + // Test schema.table.column in expressions (issue fix) + AssertParseStatement(t, `UPDATE main.vals SET a=lower(main.vals.a)`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Schema: &sql.Ident{NamePos: pos(7), Name: "main"}, + Dot: pos(11), + Name: &sql.Ident{NamePos: pos(12), Name: "vals"}, + }, + Set: pos(17), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(21), Name: "a"}}, + Eq: pos(22), + Expr: &sql.Call{ + Name: &sql.Ident{NamePos: pos(23), Name: "lower"}, + Lparen: pos(28), + Args: []sql.Expr{ + &sql.QualifiedRef{ + Schema: &sql.Ident{NamePos: pos(29), Name: "main"}, + SchemaPos: pos(33), + Table: &sql.Ident{NamePos: pos(34), Name: "vals"}, + Dot: pos(38), + Column: &sql.Ident{NamePos: pos(39), Name: "a"}, + }, + }, + Rparen: pos(40), + }, + }}, + }) + AssertParseStatementError(t, `UPDATE`, `1:6: expected table name, found 'EOF'`) AssertParseStatementError(t, `UPDATE OR`, `1:9: expected ROLLBACK, REPLACE, ABORT, FAIL, or IGNORE, found 'EOF'`) AssertParseStatementError(t, `UPDATE tbl`, `1:10: expected SET, found 'EOF'`) @@ -4425,6 +4454,27 @@ func TestParser_ParseExpr(t *testing.T) { Dot: pos(5), Column: &sql.Ident{NamePos: pos(6), Name: "col", Quoted: true}, }) + AssertParseExpr(t, `schema.tbl.col`, &sql.QualifiedRef{ + Schema: &sql.Ident{NamePos: pos(0), Name: "schema"}, + SchemaPos: pos(6), + Table: &sql.Ident{NamePos: pos(7), Name: "tbl"}, + Dot: pos(10), + Column: &sql.Ident{NamePos: pos(11), Name: "col"}, + }) + AssertParseExpr(t, `"schema"."tbl"."col"`, &sql.QualifiedRef{ + Schema: &sql.Ident{NamePos: pos(0), Name: "schema", Quoted: true}, + SchemaPos: pos(8), + Table: &sql.Ident{NamePos: pos(9), Name: "tbl", Quoted: true}, + Dot: pos(14), + Column: &sql.Ident{NamePos: pos(15), Name: "col", Quoted: true}, + }) + AssertParseExpr(t, `main.vals.a`, &sql.QualifiedRef{ + Schema: &sql.Ident{NamePos: pos(0), Name: "main"}, + SchemaPos: pos(4), + Table: &sql.Ident{NamePos: pos(5), Name: "vals"}, + Dot: pos(9), + Column: &sql.Ident{NamePos: pos(10), Name: "a"}, + }) AssertParseExprError(t, `tbl.`, `1:4: expected column name, found 'EOF'`) }) t.Run("Exists", func(t *testing.T) { diff --git a/walk.go b/walk.go index b032e74d..9754e5a3 100644 --- a/walk.go +++ b/walk.go @@ -646,6 +646,11 @@ func walk(v Visitor, n Node) (retNode Node, err error) { } case *QualifiedRef: + if ri, err := walkIdent(v, nn.Schema); err != nil { + return nil, err + } else { + nn.Schema = ri + } if ri, err := walkIdent(v, nn.Table); err != nil { return nil, err } else { From 1023153cafeca5c9d17afed5a447fb4037454891 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:59:16 +0000 Subject: [PATCH 3/9] Run go fmt on parser.go Co-authored-by: otoolep <536312+otoolep@users.noreply.github.com> --- parser.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parser.go b/parser.go index c19bf4b9..44c26f44 100644 --- a/parser.go +++ b/parser.go @@ -2697,7 +2697,7 @@ func (p *Parser) parseQualifiedRef(table *Ident) (_ *QualifiedRef, err error) { } else if isIdentToken(p.peek()) { pos, tok, lit := p.scan() expr.Column = &Ident{Name: lit, NamePos: pos, Quoted: tok == QIDENT || tok == BIDENT} - + // Check if there's another DOT for schema.table.column format if p.peek() == DOT { // What we thought was table.column is actually schema.table @@ -2706,7 +2706,7 @@ func (p *Parser) parseQualifiedRef(table *Ident) (_ *QualifiedRef, err error) { expr.SchemaPos = expr.Dot expr.Table = expr.Column expr.Dot, _, _ = p.scan() - + // Now parse the actual column if p.peek() == STAR { expr.Column = nil From 9144503ad2c9491f1a285894d1f62307fb1a364b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:01:24 +0000 Subject: [PATCH 4/9] Initial plan From a47c64f06e5ff83f0491ab654e8e671ac28fb8be Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Thu, 5 Feb 2026 09:27:51 -0500 Subject: [PATCH 5/9] Revert --- parser_test.go | 142 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/parser_test.go b/parser_test.go index affbba88..afc90602 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4046,6 +4046,148 @@ func TestParser_ParseStatement(t *testing.T) { }}, }) + // Test INDEXED BY clause + AssertParseStatement(t, `UPDATE tbl INDEXED BY idx SET x=1`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "tbl"}, + Indexed: pos(11), + IndexedBy: pos(19), + Index: &sql.Ident{NamePos: pos(22), Name: "idx"}, + }, + Set: pos(26), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(30), Name: "x"}}, + Eq: pos(31), + Expr: &sql.NumberLit{ValuePos: pos(32), Value: "1"}, + }}, + }) + + // Test NOT INDEXED clause + AssertParseStatement(t, `UPDATE tbl NOT INDEXED SET x=1`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "tbl"}, + Not: pos(11), + NotIndexed: pos(15), + }, + Set: pos(23), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(27), Name: "x"}}, + Eq: pos(28), + Expr: &sql.NumberLit{ValuePos: pos(29), Value: "1"}, + }}, + }) + + // Test UPDATE FROM with simple table + AssertParseStatement(t, `UPDATE t SET a=v.b FROM v`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "t"}, + }, + Set: pos(9), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(13), Name: "a"}}, + Eq: pos(14), + Expr: &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(15), Name: "v"}, + Dot: pos(16), + Column: &sql.Ident{NamePos: pos(17), Name: "b"}, + }, + }}, + From: pos(19), + Source: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(24), Name: "v"}, + }, + }) + + // Test UPDATE FROM with subquery + AssertParseStatement(t, `UPDATE t SET a=v.b FROM (SELECT b FROM v) AS v`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "t"}, + }, + Set: pos(9), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(13), Name: "a"}}, + Eq: pos(14), + Expr: &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(15), Name: "v"}, + Dot: pos(16), + Column: &sql.Ident{NamePos: pos(17), Name: "b"}, + }, + }}, + From: pos(19), + Source: &sql.ParenSource{ + Lparen: pos(24), + X: &sql.SelectStatement{ + Select: pos(25), + Columns: []*sql.ResultColumn{ + {Expr: &sql.Ident{NamePos: pos(32), Name: "b"}}, + }, + From: pos(34), + Source: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(39), Name: "v"}, + }, + }, + Rparen: pos(40), + As: pos(42), + Alias: &sql.Ident{NamePos: pos(45), Name: "v"}, + }, + }) + + // Test UPDATE FROM with WHERE clause + AssertParseStatement(t, `UPDATE t SET a=v.b FROM v WHERE t.id = v.id`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "t"}, + }, + Set: pos(9), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(13), Name: "a"}}, + Eq: pos(14), + Expr: &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(15), Name: "v"}, + Dot: pos(16), + Column: &sql.Ident{NamePos: pos(17), Name: "b"}, + }, + }}, + From: pos(19), + Source: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(24), Name: "v"}, + }, + Where: pos(26), + WhereExpr: &sql.BinaryExpr{ + X: &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(32), Name: "t"}, + Dot: pos(33), + Column: &sql.Ident{NamePos: pos(34), Name: "id"}, + }, + OpPos: pos(37), + Op: sql.EQ, + Y: &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(39), Name: "v"}, + Dot: pos(40), + Column: &sql.Ident{NamePos: pos(41), Name: "id"}, + }, + }, + }) + + // Test table alias without AS keyword + AssertParseStatement(t, `UPDATE vals v SET a=1`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "vals"}, + Alias: &sql.Ident{NamePos: pos(12), Name: "v"}, + }, + Set: pos(14), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(18), Name: "a"}}, + Eq: pos(19), + Expr: &sql.NumberLit{ValuePos: pos(20), Value: "1"}, + }}, + }) + AssertParseStatementError(t, `UPDATE`, `1:6: expected table name, found 'EOF'`) AssertParseStatementError(t, `UPDATE OR`, `1:9: expected ROLLBACK, REPLACE, ABORT, FAIL, or IGNORE, found 'EOF'`) AssertParseStatementError(t, `UPDATE tbl`, `1:10: expected SET, found 'EOF'`) From 28c0a4d05c5112f9f8c3f95dce611581d285ef97 Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Thu, 5 Feb 2026 09:28:35 -0500 Subject: [PATCH 6/9] More revert --- parser_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/parser_test.go b/parser_test.go index afc90602..a4f63449 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4046,6 +4046,21 @@ func TestParser_ParseStatement(t *testing.T) { }}, }) + // Test table alias without AS keyword + AssertParseStatement(t, `UPDATE vals v SET a=1`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "vals"}, + Alias: &sql.Ident{NamePos: pos(12), Name: "v"}, + }, + Set: pos(14), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(18), Name: "a"}}, + Eq: pos(19), + Expr: &sql.NumberLit{ValuePos: pos(20), Value: "1"}, + }}, + }) + // Test INDEXED BY clause AssertParseStatement(t, `UPDATE tbl INDEXED BY idx SET x=1`, &sql.UpdateStatement{ Update: pos(0), From 2b6cd9e6b8c843f4e1cfe786bcb722326858fe03 Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Thu, 5 Feb 2026 09:31:00 -0500 Subject: [PATCH 7/9] Revert --- parser_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/parser_test.go b/parser_test.go index a4f63449..8e13c7aa 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4017,6 +4017,33 @@ func TestParser_ParseStatement(t *testing.T) { }}, }) + // Test table alias with AS keyword + AssertParseStatement(t, `UPDATE vals AS v SET a=upper(v.a)`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "vals"}, + As: pos(12), + Alias: &sql.Ident{NamePos: pos(15), Name: "v"}, + }, + Set: pos(17), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(21), Name: "a"}}, + Eq: pos(22), + Expr: &sql.Call{ + Name: &sql.Ident{NamePos: pos(23), Name: "upper"}, + Lparen: pos(28), + Args: []sql.Expr{ + &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(29), Name: "v"}, + Dot: pos(30), + Column: &sql.Ident{NamePos: pos(31), Name: "a"}, + }, + }, + Rparen: pos(32), + }, + }}, + }) + // Test schema.table.column in expressions (issue fix) AssertParseStatement(t, `UPDATE main.vals SET a=lower(main.vals.a)`, &sql.UpdateStatement{ Update: pos(0), From 6342deb81070e5d0e6ffa544df236010ada26ee5 Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Thu, 5 Feb 2026 09:32:01 -0500 Subject: [PATCH 8/9] More revert --- parser_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/parser_test.go b/parser_test.go index 8e13c7aa..391ae627 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4230,6 +4230,53 @@ func TestParser_ParseStatement(t *testing.T) { }}, }) + // Test UPDATE FROM with JOIN + AssertParseStatement(t, `UPDATE t SET a=v.b FROM v JOIN w ON v.id = w.id`, &sql.UpdateStatement{ + Update: pos(0), + Table: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(7), Name: "t"}, + }, + Set: pos(9), + Assignments: []*sql.Assignment{{ + Columns: []*sql.Ident{{NamePos: pos(13), Name: "a"}}, + Eq: pos(14), + Expr: &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(15), Name: "v"}, + Dot: pos(16), + Column: &sql.Ident{NamePos: pos(17), Name: "b"}, + }, + }}, + From: pos(19), + Source: &sql.JoinClause{ + X: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(24), Name: "v"}, + }, + Operator: &sql.JoinOperator{ + Join: pos(26), + }, + Y: &sql.QualifiedTableName{ + Name: &sql.Ident{NamePos: pos(31), Name: "w"}, + }, + Constraint: &sql.OnConstraint{ + On: pos(33), + X: &sql.BinaryExpr{ + X: &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(36), Name: "v"}, + Dot: pos(37), + Column: &sql.Ident{NamePos: pos(38), Name: "id"}, + }, + OpPos: pos(41), + Op: sql.EQ, + Y: &sql.QualifiedRef{ + Table: &sql.Ident{NamePos: pos(43), Name: "w"}, + Dot: pos(44), + Column: &sql.Ident{NamePos: pos(45), Name: "id"}, + }, + }, + }, + }, + }) + AssertParseStatementError(t, `UPDATE`, `1:6: expected table name, found 'EOF'`) AssertParseStatementError(t, `UPDATE OR`, `1:9: expected ROLLBACK, REPLACE, ABORT, FAIL, or IGNORE, found 'EOF'`) AssertParseStatementError(t, `UPDATE tbl`, `1:10: expected SET, found 'EOF'`) From 097c1f0ba7ef89ffaff1fe6977454955ccff136d Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Thu, 5 Feb 2026 09:33:31 -0500 Subject: [PATCH 9/9] Revert --- parser_test.go | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/parser_test.go b/parser_test.go index 391ae627..cf322c17 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4044,7 +4044,7 @@ func TestParser_ParseStatement(t *testing.T) { }}, }) - // Test schema.table.column in expressions (issue fix) + // Test schema.table.column in expressions AssertParseStatement(t, `UPDATE main.vals SET a=lower(main.vals.a)`, &sql.UpdateStatement{ Update: pos(0), Table: &sql.QualifiedTableName{ @@ -4215,21 +4215,6 @@ func TestParser_ParseStatement(t *testing.T) { }, }) - // Test table alias without AS keyword - AssertParseStatement(t, `UPDATE vals v SET a=1`, &sql.UpdateStatement{ - Update: pos(0), - Table: &sql.QualifiedTableName{ - Name: &sql.Ident{NamePos: pos(7), Name: "vals"}, - Alias: &sql.Ident{NamePos: pos(12), Name: "v"}, - }, - Set: pos(14), - Assignments: []*sql.Assignment{{ - Columns: []*sql.Ident{{NamePos: pos(18), Name: "a"}}, - Eq: pos(19), - Expr: &sql.NumberLit{ValuePos: pos(20), Value: "1"}, - }}, - }) - // Test UPDATE FROM with JOIN AssertParseStatement(t, `UPDATE t SET a=v.b FROM v JOIN w ON v.id = w.id`, &sql.UpdateStatement{ Update: pos(0),