Skip to content

Commit

Permalink
feat(spanner/spansql): parse path expressions (#2924)
Browse files Browse the repository at this point in the history
These are not properly documented, so this only attempts to support a
limited but widely used subset, which will be sufficient for more fully
supporting aliases and joins.

Updates #2462.
Updates #2463.
Updates #2850.
  • Loading branch information
dsymonds committed Sep 28, 2020
1 parent b084dce commit 7ca409b
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 23 deletions.
63 changes: 57 additions & 6 deletions spanner/spansql/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ const (
float64Token
stringToken
bytesToken
unquotedID
quotedID
)

Expand Down Expand Up @@ -752,10 +753,24 @@ func (p *parser) skipSpace() bool {

// advance moves the parser to the next token, which will be available in p.cur.
func (p *parser) advance() {
prevID := p.cur.typ == quotedID || p.cur.typ == unquotedID

p.skipSpace()
if p.done {
return
}

// If the previous token was an identifier (quoted or unquoted),
// the next token being a dot means this is a path expression (not a number).
if prevID && p.s[0] == '.' {
p.cur.err = nil
p.cur.line, p.cur.offset = p.line, p.offset
p.cur.typ = unknownToken
p.cur.value, p.s = p.s[:1], p.s[1:]
p.offset++
return
}

p.cur.err = nil
p.cur.line, p.cur.offset = p.line, p.offset
p.cur.typ = unknownToken
Expand Down Expand Up @@ -806,6 +821,7 @@ func (p *parser) advance() {
i++
}
p.cur.value, p.s = p.s[:i], p.s[i:]
p.cur.typ = unquotedID
p.offset += i
return
}
Expand Down Expand Up @@ -2314,8 +2330,6 @@ func (p *parser) parseLit() (Expr, *parseError) {
return StringLiteral(tok.string), nil
case bytesToken:
return BytesLiteral(tok.string), nil
case quotedID: // Unquoted identifers are handled below.
return ID(tok.string), nil
}

// Handle parenthesized expressions.
Expand Down Expand Up @@ -2367,7 +2381,40 @@ func (p *parser) parseLit() (Expr, *parseError) {
if strings.HasPrefix(tok.value, "@") {
return Param(tok.value[1:]), nil
}
return ID(tok.value), nil

// Only thing left is a path expression or standalone identifier.
p.back()
pe, err := p.parsePathExp()
if err != nil {
return nil, err
}
if len(pe) == 1 {
return pe[0], nil // identifier
}
return pe, nil
}

func (p *parser) parsePathExp() (PathExp, *parseError) {
var pe PathExp
for {
tok := p.next()
if tok.err != nil {
return nil, tok.err
}
switch tok.typ {
case quotedID:
pe = append(pe, ID(tok.string))
case unquotedID:
pe = append(pe, ID(tok.value))
default:
// TODO: Is this correct?
return nil, p.errorf("expected identifer")
}
if !p.eat(".") {
break
}
}
return pe, nil
}

func (p *parser) parseBoolExpr() (BoolExpr, *parseError) {
Expand Down Expand Up @@ -2398,11 +2445,15 @@ func (p *parser) parseTableOrIndexOrColumnName() (ID, *parseError) {
if tok.err != nil {
return "", tok.err
}
if tok.typ == quotedID {
switch tok.typ {
case quotedID:
return ID(tok.string), nil
case unquotedID:
// TODO: enforce restrictions
return ID(tok.value), nil
default:
return "", p.errorf("expected identifier")
}
// TODO: enforce restrictions
return ID(tok.value), nil
}

func (p *parser) parseOnDelete() (OnDelete, *parseError) {
Expand Down
35 changes: 24 additions & 11 deletions spanner/spansql/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,7 @@ func TestParseQuery(t *testing.T) {
},
},
// https://github.com/googleapis/google-cloud-go/issues/1973
// except that "l.user_id" is replaced with "l_user_id" since we don't support
// the dot operator yet.
{`SELECT COUNT(*) AS count FROM Lists AS l WHERE l_user_id=@userID`,
{`SELECT COUNT(*) AS count FROM Lists AS l WHERE l.user_id=@userID`,
Query{
Select: Select{
List: []Expr{
Expand All @@ -106,14 +104,30 @@ func TestParseQuery(t *testing.T) {
From: []SelectFrom{SelectFromTable{Table: "Lists", Alias: "l"}},
Where: ComparisonOp{
Op: Eq,
LHS: ID("l_user_id"),
LHS: PathExp{"l", "user_id"},
RHS: Param("userID"),
},
ListAliases: []ID{"count"},
},
},
},
// TODO: `SELECT * FROM A INNER JOIN B ON A.w = B.y`
{`SELECT * FROM A INNER JOIN B ON A.w = B.y`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromJoin{
Type: InnerJoin,
LHS: SelectFromTable{Table: "A"},
RHS: SelectFromTable{Table: "B"},
On: ComparisonOp{
Op: Eq,
LHS: PathExp{"A", "w"},
RHS: PathExp{"B", "y"},
},
}},
},
},
},
{`SELECT * FROM A INNER JOIN B USING (x)`,
Query{
Select: Select{
Expand All @@ -127,22 +141,21 @@ func TestParseQuery(t *testing.T) {
},
},
},
// TODO: This should be `SELECT Roster.LastName, TeamMascot.Mascot FROM Roster JOIN TeamMascot ON Roster.SchoolID = TeamMascot.SchoolID`
{`SELECT RosterLastName, TeamMascotMascot FROM Roster JOIN TeamMascot ON RosterSchoolID = TeamMascotSchoolID`,
{`SELECT Roster . LastName, TeamMascot.Mascot FROM Roster JOIN TeamMascot ON Roster.SchoolID = TeamMascot.SchoolID`,
Query{
Select: Select{
List: []Expr{
ID("RosterLastName"),
ID("TeamMascotMascot"),
PathExp{"Roster", "LastName"},
PathExp{"TeamMascot", "Mascot"},
},
From: []SelectFrom{SelectFromJoin{
Type: InnerJoin,
LHS: SelectFromTable{Table: "Roster"},
RHS: SelectFromTable{Table: "TeamMascot"},
On: ComparisonOp{
Op: Eq,
LHS: ID("RosterSchoolID"),
RHS: ID("TeamMascotSchoolID"),
LHS: PathExp{"Roster", "SchoolID"},
RHS: PathExp{"TeamMascot", "SchoolID"},
},
}},
},
Expand Down
14 changes: 8 additions & 6 deletions spanner/spansql/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (ci CreateIndex) SQL() string {
}
str += ")"
if len(ci.Storing) > 0 {
str += " STORING (" + idList(ci.Storing) + ")"
str += " STORING (" + idList(ci.Storing, ", ") + ")"
}
if ci.Interleave != "" {
str += ", INTERLEAVE IN " + ci.Interleave.SQL()
Expand Down Expand Up @@ -168,9 +168,9 @@ func (tc TableConstraint) SQL() string {
}

func (fk ForeignKey) SQL() string {
str := "FOREIGN KEY (" + idList(fk.Columns)
str := "FOREIGN KEY (" + idList(fk.Columns, ", ")
str += ") REFERENCES " + fk.RefTable.SQL() + " ("
str += idList(fk.RefColumns) + ")"
str += idList(fk.RefColumns, ", ") + ")"
return str
}

Expand Down Expand Up @@ -296,7 +296,7 @@ func (sfj SelectFromJoin) SQL() string {
if sfj.On != nil {
str += " " + sfj.On.SQL()
} else if len(sfj.Using) > 0 {
str += " USING (" + idList(sfj.Using) + ")"
str += " USING (" + idList(sfj.Using, ", ") + ")"
}
return str
}
Expand Down Expand Up @@ -426,14 +426,16 @@ func (f Func) SQL() string {
return str
}

func idList(l []ID) string {
func idList(l []ID, join string) string {
var ss []string
for _, s := range l {
ss = append(ss, s.SQL())
}
return strings.Join(ss, ", ")
return strings.Join(ss, join)
}

func (pe PathExp) SQL() string { return idList([]ID(pe), ".") }

func (p Paren) SQL() string { return "(" + p.Expr.SQL() + ")" }

func (id ID) SQL() string {
Expand Down
8 changes: 8 additions & 0 deletions spanner/spansql/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,14 @@ type IsExpr interface {
SQL() string
}

// PathExp represents a path expression.
//
// The grammar for path expressions is not defined (see b/169017423 internally),
// so this captures the most common form only, namely a dotted sequence of identifiers.
type PathExp []ID

func (PathExp) isExpr() {}

// Func represents a function call.
type Func struct {
Name string // not ID
Expand Down

0 comments on commit 7ca409b

Please sign in to comment.