spanner/spansql, spanner/spannertest: support OFFSET clause
This parses the OFFSET clause and implements it in spannertest queries.
Fixes #1819.
Change-Id: If0455a385875982be1825d53d8572ba23fcdf8b1
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/52993
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Shanika Kuruppu <skuruppu@google.com>
diff --git a/spanner/spannertest/README.md b/spanner/spannertest/README.md
index ca5b602..df2363f 100644
--- a/spanner/spannertest/README.md
+++ b/spanner/spannertest/README.md
@@ -25,7 +25,6 @@
- joins
- transaction simulation
- expression type casting, coercion
-- query offset
- SELECT aliases in FROM clause, ORDER BY
- subselects
- set operations (UNION, INTERSECT, EXCEPT)
diff --git a/spanner/spannertest/db_eval.go b/spanner/spannertest/db_eval.go
index a03cb82..5537799 100644
--- a/spanner/spannertest/db_eval.go
+++ b/spanner/spannertest/db_eval.go
@@ -400,14 +400,14 @@
return nil, fmt.Errorf("couldn't resolve identifier %s", string(id))
}
-func evalLimit(lim spansql.Limit, params queryParams) (int64, error) {
- switch lim := lim.(type) {
+func evalLiteralOrParam(lop spansql.LiteralOrParam, params queryParams) (int64, error) {
+ switch v := lop.(type) {
case spansql.IntegerLiteral:
- return int64(lim), nil
+ return int64(v), nil
case spansql.Param:
- return paramAsInteger(lim, params)
+ return paramAsInteger(v, params)
default:
- return 0, fmt.Errorf("LIMIT with %T not supported", lim)
+ return 0, fmt.Errorf("LiteralOrParam with %T not supported", v)
}
}
diff --git a/spanner/spannertest/db_query.go b/spanner/spannertest/db_query.go
index 0f3037a..86ddf5b 100644
--- a/spanner/spannertest/db_query.go
+++ b/spanner/spannertest/db_query.go
@@ -39,7 +39,7 @@
SELECT
DISTINCT
ORDER BY
- OFFSET [TODO]
+ OFFSET
LIMIT
*/
@@ -223,6 +223,28 @@
}
}
+// offsetIter applies an OFFSET clause.
+type offsetIter struct {
+ ri rowIter
+ skip int64
+}
+
+func (oi *offsetIter) Cols() []colInfo { return oi.ri.Cols() }
+func (oi *offsetIter) Next() (row, error) {
+ for oi.skip > 0 {
+ _, err := oi.ri.Next()
+ if err != nil {
+ return nil, err
+ }
+ oi.skip--
+ }
+ row, err := oi.ri.Next()
+ if err != nil {
+ return nil, err
+ }
+ return row, nil
+}
+
// limitIter applies a LIMIT clause.
type limitIter struct {
ri rowIter
@@ -284,11 +306,17 @@
ri = raw
}
- // TODO: OFFSET
-
- // Apply LIMIT.
+ // Apply LIMIT, OFFSET.
if q.Limit != nil {
- lim, err := evalLimit(q.Limit, params)
+ if q.Offset != nil {
+ off, err := evalLiteralOrParam(q.Offset, params)
+ if err != nil {
+ return nil, err
+ }
+ ri = &offsetIter{ri: ri, skip: off}
+ }
+
+ lim, err := evalLiteralOrParam(q.Limit, params)
if err != nil {
return nil, err
}
diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go
index 07b15e1..e1ec959 100644
--- a/spanner/spansql/parser.go
+++ b/spanner/spansql/parser.go
@@ -1558,11 +1558,22 @@
}
if p.eat("LIMIT") {
- lim, err := p.parseLimitCount()
+ // "only literal or parameter values"
+ // https://cloud.google.com/spanner/docs/query-syntax#limit-clause-and-offset-clause
+
+ lim, err := p.parseLiteralOrParam()
if err != nil {
return Query{}, err
}
q.Limit = lim
+
+ if p.eat("OFFSET") {
+ off, err := p.parseLiteralOrParam()
+ if err != nil {
+ return Query{}, err
+ }
+ q.Offset = off
+ }
}
return q, nil
@@ -1761,10 +1772,7 @@
return o, nil
}
-func (p *parser) parseLimitCount() (Limit, *parseError) {
- // "only literal or parameter values"
- // https://cloud.google.com/spanner/docs/query-syntax#limit-clause-and-offset-clause
-
+func (p *parser) parseLiteralOrParam() (LiteralOrParam, *parseError) {
tok := p.next()
if tok.err != nil {
return nil, tok.err
diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go
index c4cf530..2c3d690 100644
--- a/spanner/spansql/parser_test.go
+++ b/spanner/spansql/parser_test.go
@@ -29,7 +29,7 @@
want Query
}{
{`SELECT 17`, Query{Select: Select{List: []Expr{IntegerLiteral(17)}}}},
- {`SELECT Alias AS aka FROM Characters WHERE Age < @ageLimit AND Alias IS NOT NULL ORDER BY Age DESC LIMIT @limit` + "\n\t",
+ {`SELECT Alias AS aka FROM Characters WHERE Age < @ageLimit AND Alias IS NOT NULL ORDER BY Age DESC LIMIT @limit OFFSET 3` + "\n\t",
Query{
Select: Select{
List: []Expr{ID("Alias")},
@@ -55,7 +55,8 @@
Expr: ID("Age"),
Desc: true,
}},
- Limit: Param("limit"),
+ Limit: Param("limit"),
+ Offset: IntegerLiteral(3),
},
},
{`SELECT COUNT(*) FROM Packages`,
diff --git a/spanner/spansql/sql.go b/spanner/spansql/sql.go
index d9011b2..361409c 100644
--- a/spanner/spansql/sql.go
+++ b/spanner/spansql/sql.go
@@ -214,6 +214,9 @@
}
if q.Limit != nil {
str += " LIMIT " + q.Limit.SQL()
+ if q.Offset != nil {
+ str += " OFFSET " + q.Offset.SQL()
+ }
}
return str
}
diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go
index bae3e51..1b5c840 100644
--- a/spanner/spansql/types.go
+++ b/spanner/spansql/types.go
@@ -253,7 +253,8 @@
type Query struct {
Select Select
Order []Order
- Limit Limit
+
+ Limit, Offset LiteralOrParam
}
// Select represents a SELECT statement.
@@ -313,8 +314,9 @@
SQL() string
}
-type Limit interface {
- isLimit()
+// LiteralOrParam is implemented by integer literal and parameter values.
+type LiteralOrParam interface {
+ isLiteralOrParam()
SQL() string
}
@@ -428,9 +430,9 @@
// Param represents a query parameter.
type Param string
-func (Param) isBoolExpr() {} // possibly bool
-func (Param) isExpr() {}
-func (Param) isLimit() {}
+func (Param) isBoolExpr() {} // possibly bool
+func (Param) isExpr() {}
+func (Param) isLiteralOrParam() {}
type BoolLiteral bool
@@ -454,8 +456,8 @@
// https://cloud.google.com/spanner/docs/lexical#integer-literals
type IntegerLiteral int64
-func (IntegerLiteral) isLimit() {}
-func (IntegerLiteral) isExpr() {}
+func (IntegerLiteral) isLiteralOrParam() {}
+func (IntegerLiteral) isExpr() {}
// FloatLiteral represents a floating point literal.
// https://cloud.google.com/spanner/docs/lexical#floating-point-literals