feat(spanner/spansql): support table_hint_expr at from_clause on query_statement (#4457)
* feat(spanner/spansql): support table_hint_expr at from_clause on
query_statement
This fixes parse error when query statement includes table hint expr.
This add function parseHints and use in parsing table hints and join
hints.
* feat(spanner/spansql): modify to enable multiple table hint keys.
* feat(spanner/spansql): use ++ increment for lint
Co-authored-by: Hengfeng Li <hengfeng@google.com>
diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go
index afe7d21..ab32202 100644
--- a/spanner/spansql/parser.go
+++ b/spanner/spansql/parser.go
@@ -1885,7 +1885,7 @@
[ LIMIT count [ OFFSET skip_rows ] ]
*/
- // TODO: hints, sub-selects, etc.
+ // TODO: sub-selects, etc.
if err := p.expect("SELECT"); err != nil {
return Query{}, err
@@ -2111,6 +2111,13 @@
return nil, err
}
sf := SelectFromTable{Table: tname}
+ if p.eat("@") {
+ hints, err := p.parseHints(map[string]string{})
+ if err != nil {
+ return nil, err
+ }
+ sf.Hints = hints
+ }
// TODO: The "AS" keyword is optional.
if p.eat("AS") {
@@ -2159,46 +2166,20 @@
Type: jt,
LHS: sf,
}
- setHint := func(k, v string) {
- if sfj.Hints == nil {
- sfj.Hints = make(map[string]string)
- }
- sfj.Hints[k] = v
- }
+ var hints map[string]string
if hashJoin {
- setHint("JOIN_METHOD", "HASH_JOIN")
+ hints = map[string]string{}
+ hints["JOIN_METHOD"] = "HASH_JOIN"
}
if p.eat("@") {
- if err := p.expect("{"); err != nil {
+ h, err := p.parseHints(hints)
+ if err != nil {
return nil, err
}
- for {
- if p.sniff("}") {
- break
- }
- tok := p.next()
- if tok.err != nil {
- return nil, tok.err
- }
- k := tok.value
- if err := p.expect("="); err != nil {
- return nil, err
- }
- tok = p.next()
- if tok.err != nil {
- return nil, tok.err
- }
- v := tok.value
- setHint(k, v)
- if !p.eat(",") {
- break
- }
- }
- if err := p.expect("}"); err != nil {
- return nil, err
- }
+ hints = h
}
+ sfj.Hints = hints
sfj.RHS, err = p.parseSelectFrom()
if err != nil {
@@ -2889,6 +2870,41 @@
return p.parseTableOrIndexOrColumnName()
}
+func (p *parser) parseHints(hints map[string]string) (map[string]string, *parseError) {
+ if hints == nil {
+ hints = map[string]string{}
+ }
+ if err := p.expect("{"); err != nil {
+ return nil, err
+ }
+ for {
+ if p.sniff("}") {
+ break
+ }
+ tok := p.next()
+ if tok.err != nil {
+ return nil, tok.err
+ }
+ k := tok.value
+ if err := p.expect("="); err != nil {
+ return nil, err
+ }
+ tok = p.next()
+ if tok.err != nil {
+ return nil, tok.err
+ }
+ v := tok.value
+ hints[k] = v
+ if !p.eat(",") {
+ break
+ }
+ }
+ if err := p.expect("}"); err != nil {
+ return nil, err
+ }
+ return hints, nil
+}
+
func (p *parser) parseTableOrIndexOrColumnName() (ID, *parseError) {
/*
table_name and column_name and index_name:
diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go
index fbc68c5..fd1360c 100644
--- a/spanner/spansql/parser_test.go
+++ b/spanner/spansql/parser_test.go
@@ -114,6 +114,34 @@
},
},
},
+ // with single table hint
+ {`SELECT * FROM Packages@{FORCE_INDEX=PackagesIdx} WHERE package_idx=@packageIdx`,
+ Query{
+ Select: Select{
+ List: []Expr{Star},
+ From: []SelectFrom{SelectFromTable{Table: "Packages", Hints: map[string]string{"FORCE_INDEX": "PackagesIdx"}}},
+ Where: ComparisonOp{
+ Op: Eq,
+ LHS: ID("package_idx"),
+ RHS: Param("packageIdx"),
+ },
+ },
+ },
+ },
+ // with multiple table hints
+ {`SELECT * FROM Packages@{ FORCE_INDEX=PackagesIdx, GROUPBY_SCAN_OPTIMIZATION=TRUE } WHERE package_idx=@packageIdx`,
+ Query{
+ Select: Select{
+ List: []Expr{Star},
+ From: []SelectFrom{SelectFromTable{Table: "Packages", Hints: map[string]string{"FORCE_INDEX": "PackagesIdx", "GROUPBY_SCAN_OPTIMIZATION": "TRUE"}}},
+ Where: ComparisonOp{
+ Op: Eq,
+ LHS: ID("package_idx"),
+ RHS: Param("packageIdx"),
+ },
+ },
+ },
+ },
{`SELECT * FROM A INNER JOIN B ON A.w = B.y`,
Query{
Select: Select{
diff --git a/spanner/spansql/sql.go b/spanner/spansql/sql.go
index 19aac6d..80289d8 100644
--- a/spanner/spansql/sql.go
+++ b/spanner/spansql/sql.go
@@ -365,6 +365,18 @@
func (sft SelectFromTable) SQL() string {
str := sft.Table.SQL()
+ if len(sft.Hints) > 0 {
+ str += "@{"
+ kvs := make([]string, len(sft.Hints))
+ i := 0
+ for k, v := range sft.Hints {
+ kvs[i] = fmt.Sprintf("%s=%s", k, v)
+ i++
+ }
+ str += strings.Join(kvs, ",")
+ str += "}"
+ }
+
if sft.Alias != "" {
str += " AS " + sft.Alias.SQL()
}
diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go
index 3632aba..7452714 100644
--- a/spanner/spansql/sql_test.go
+++ b/spanner/spansql/sql_test.go
@@ -312,6 +312,42 @@
{
Query{
Select: Select{
+ List: []Expr{ID("A")},
+ From: []SelectFrom{SelectFromTable{
+ Table: "Table",
+ Hints: map[string]string{"FORCE_INDEX": "Idx"},
+ }},
+ Where: ComparisonOp{
+ LHS: ID("B"),
+ Op: Eq,
+ RHS: Param("b"),
+ },
+ },
+ },
+ `SELECT A FROM Table@{FORCE_INDEX=Idx} WHERE B = @b`,
+ reparseQuery,
+ },
+ {
+ Query{
+ Select: Select{
+ List: []Expr{ID("A")},
+ From: []SelectFrom{SelectFromTable{
+ Table: "Table",
+ Hints: map[string]string{"FORCE_INDEX": "Idx", "GROUPBY_SCAN_OPTIMIZATION": "TRUE"},
+ }},
+ Where: ComparisonOp{
+ LHS: ID("B"),
+ Op: Eq,
+ RHS: Param("b"),
+ },
+ },
+ },
+ `SELECT A FROM Table@{FORCE_INDEX=Idx,GROUPBY_SCAN_OPTIMIZATION=TRUE} WHERE B = @b`,
+ reparseQuery,
+ },
+ {
+ Query{
+ Select: Select{
List: []Expr{IntegerLiteral(7)},
},
},
diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go
index 34d0e0f..e7d622f 100644
--- a/spanner/spansql/types.go
+++ b/spanner/spansql/types.go
@@ -394,6 +394,7 @@
type SelectFromTable struct {
Table ID
Alias ID // empty if not aliased
+ Hints map[string]string
}
func (SelectFromTable) isSelectFrom() {}