Skip to content
19 changes: 13 additions & 6 deletions ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -2062,10 +2062,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.
Expand All @@ -2074,17 +2076,22 @@ 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
}

// 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 {
Expand Down
21 changes: 21 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2824,6 +2824,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")
}
Expand Down
50 changes: 50 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4044,6 +4044,35 @@ func TestParser_ParseStatement(t *testing.T) {
}},
})

// 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{
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),
},
}},
})

// Test table alias without AS keyword
AssertParseStatement(t, `UPDATE vals v SET a=1`, &sql.UpdateStatement{
Update: pos(0),
Expand Down Expand Up @@ -4867,6 +4896,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) {
Expand Down
5 changes: 5 additions & 0 deletions walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,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 {
Expand Down