Skip to content

Commit

Permalink
feat(spanner/spansql): support DATE and TIMESTAMP literals (#3557)
Browse files Browse the repository at this point in the history
This parses a reasonable subset, and renders in a reasonable format.
Future work will be required to support this in spannertest, as well as
correctly supporting the full range of valid layouts.

Fixes #3548.
  • Loading branch information
dsymonds committed Jan 14, 2021
1 parent 372b3c2 commit 1961930
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 5 deletions.
2 changes: 1 addition & 1 deletion spanner/spannertest/README.md
Expand Up @@ -22,7 +22,7 @@ by ascending esotericism:
- more aggregation functions
- SELECT HAVING
- case insensitivity
- alternate literal types (esp. strings)
- more literal types
- generated columns
- expression type casting, coercion
- multiple joins
Expand Down
88 changes: 84 additions & 4 deletions spanner/spansql/parser.go
Expand Up @@ -47,7 +47,10 @@ import (
"os"
"strconv"
"strings"
"time"
"unicode/utf8"

"cloud.google.com/go/civil"
)

const debug = false
Expand Down Expand Up @@ -780,7 +783,7 @@ func (p *parser) advance() {
p.cur.err = nil
p.cur.line, p.cur.offset = p.line, p.offset
p.cur.typ = unknownToken
// TODO: struct, date, timestamp literals
// TODO: struct literals
switch p.s[0] {
case ',', ';', '(', ')', '{', '}', '[', ']', '*', '+', '-':
// Single character symbol.
Expand Down Expand Up @@ -2601,13 +2604,20 @@ func (p *parser) parseLit() (Expr, *parseError) {
// case insensitivity for keywords.
}

// Handle array literals.
if tok.value == "ARRAY" || tok.value == "[" {
// Handle typed literals.
switch tok.value {
case "ARRAY", "[":
p.back()
return p.parseArrayLit()
case "DATE":
p.back()
return p.parseDateLit()
case "TIMESTAMP":
p.back()
return p.parseTimestampLit()
}

// TODO: more types of literals (struct, date, timestamp).
// TODO: struct literals

// Try a parameter.
// TODO: check character sets.
Expand Down Expand Up @@ -2645,6 +2655,76 @@ func (p *parser) parseArrayLit() (Array, *parseError) {
return arr, err
}

// TODO: There should be exported Parse{Date,Timestamp}Literal package-level funcs
// to support spannertest coercing plain string literals when used in a typed context.
// Those should wrap parseDateLit and parseTimestampLit below.

func (p *parser) parseDateLit() (DateLiteral, *parseError) {
if err := p.expect("DATE"); err != nil {
return DateLiteral{}, err
}
s, err := p.parseStringLit()
if err != nil {
return DateLiteral{}, err
}
d, perr := civil.ParseDate(string(s))
if perr != nil {
return DateLiteral{}, p.errorf("bad date literal %q: %v", s, perr)
}
// TODO: Enforce valid range.
return DateLiteral(d), nil
}

// TODO: A manual parser is probably better than this.
// There are a lot of variations that this does not handle.
var timestampFormats = []string{
// 'YYYY-[M]M-[D]D [[H]H:[M]M:[S]S[.DDDDDD] [timezone]]'
"2006-01-02",
"2006-01-02 15:04:05",
"2006-01-02 15:04:05.000000",
"2006-01-02 15:04:05 -07:00",
"2006-01-02 15:04:05.000000 -07:00",
}

var defaultLocation = func() *time.Location {
// The docs say "America/Los_Angeles" is the default.
// Use that if we can load it, but fall back on UTC if we don't have timezone data.
loc, err := time.LoadLocation("America/Los_Angeles")
if err == nil {
return loc
}
return time.UTC
}()

func (p *parser) parseTimestampLit() (TimestampLiteral, *parseError) {
if err := p.expect("TIMESTAMP"); err != nil {
return TimestampLiteral{}, err
}
s, err := p.parseStringLit()
if err != nil {
return TimestampLiteral{}, err
}
for _, format := range timestampFormats {
t, err := time.ParseInLocation(format, string(s), defaultLocation)
if err == nil {
// TODO: Enforce valid range.
return TimestampLiteral(t), nil
}
}
return TimestampLiteral{}, p.errorf("invalid timestamp literal %q", s)
}

func (p *parser) parseStringLit() (StringLiteral, *parseError) {
tok := p.next()
if tok.err != nil {
return "", tok.err
}
if tok.typ != stringToken {
return "", p.errorf("got %q, want string literal", tok.value)
}
return StringLiteral(tok.string), nil
}

func (p *parser) parsePathExp() (PathExp, *parseError) {
var pe PathExp
for {
Expand Down
6 changes: 6 additions & 0 deletions spanner/spansql/parser_test.go
Expand Up @@ -21,6 +21,9 @@ import (
"math"
"reflect"
"testing"
"time"

"cloud.google.com/go/civil"
)

func TestParseQuery(t *testing.T) {
Expand Down Expand Up @@ -302,6 +305,9 @@ func TestParseExpr(t *testing.T) {
{`RB"""\\//\\//"""`, BytesLiteral("\\\\//\\\\//")},
{"RB'''\\\\//\n\\\\//'''", BytesLiteral("\\\\//\n\\\\//")},

// Date and timestamp literals:
{`DATE '2014-09-27'`, DateLiteral(civil.Date{Year: 2014, Month: time.September, Day: 27})},

// Array literals:
// https://cloud.google.com/spanner/docs/lexical#array_literals
{`[1, 2, 3]`, Array{IntegerLiteral(1), IntegerLiteral(2), IntegerLiteral(3)}},
Expand Down
11 changes: 11 additions & 0 deletions spanner/spansql/sql.go
Expand Up @@ -27,6 +27,7 @@ import (
"fmt"
"strconv"
"strings"
"time"
)

func buildSQL(x interface{ addSQL(*strings.Builder) }) string {
Expand Down Expand Up @@ -585,3 +586,13 @@ func (sl StringLiteral) addSQL(sb *strings.Builder) { fmt.Fprintf(sb, "%q", sl)

func (bl BytesLiteral) SQL() string { return buildSQL(bl) }
func (bl BytesLiteral) addSQL(sb *strings.Builder) { fmt.Fprintf(sb, "B%q", bl) }

func (dl DateLiteral) SQL() string { return buildSQL(dl) }
func (dl DateLiteral) addSQL(sb *strings.Builder) {
fmt.Fprintf(sb, "DATE '%04d-%02d-%02d'", dl.Year, dl.Month, dl.Day)
}

func (tl TimestampLiteral) SQL() string { return buildSQL(tl) }
func (tl TimestampLiteral) addSQL(sb *strings.Builder) {
fmt.Fprintf(sb, "TIMESTAMP '%s'", time.Time(tl).Format("2006-01-02 15:04:05.000000 -07:00"))
}
18 changes: 18 additions & 0 deletions spanner/spansql/sql_test.go
Expand Up @@ -19,6 +19,9 @@ package spansql
import (
"reflect"
"testing"
"time"

"cloud.google.com/go/civil"
)

func boolAddr(b bool) *bool {
Expand Down Expand Up @@ -53,6 +56,11 @@ func TestSQL(t *testing.T) {
return e, nil
}

latz, err := time.LoadLocation("America/Los_Angeles")
if err != nil {
t.Fatalf("Loading Los Angeles time zone info: %v", err)
}

line := func(n int) Position { return Position{Line: n} }
tests := []struct {
data interface{ SQL() string }
Expand Down Expand Up @@ -300,6 +308,16 @@ func TestSQL(t *testing.T) {
"SELECT `Desc`",
reparseQuery,
},
{
DateLiteral(civil.Date{Year: 2014, Month: time.September, Day: 27}),
`DATE '2014-09-27'`,
reparseExpr,
},
{
TimestampLiteral(time.Date(2014, time.September, 27, 12, 34, 56, 123456e3, latz)),
`TIMESTAMP '2014-09-27 12:34:56.123456 -07:00'`,
reparseExpr,
},
}
for _, test := range tests {
sql := test.data.SQL()
Expand Down
15 changes: 15 additions & 0 deletions spanner/spansql/types.go
Expand Up @@ -23,6 +23,9 @@ import (
"math"
"sort"
"strings"
"time"

"cloud.google.com/go/civil"
)

// TODO: More Position fields throughout; maybe in Query/Select.
Expand Down Expand Up @@ -630,6 +633,18 @@ type BytesLiteral string

func (BytesLiteral) isExpr() {}

// DateLiteral represents a date literal.
// https://cloud.google.com/spanner/docs/lexical#date_literals
type DateLiteral civil.Date

func (DateLiteral) isExpr() {}

// TimestampLiteral represents a timestamp literal.
// https://cloud.google.com/spanner/docs/lexical#timestamp_literals
type TimestampLiteral time.Time

func (TimestampLiteral) isExpr() {}

type StarExpr int

// Star represents a "*" in an expression.
Expand Down

0 comments on commit 1961930

Please sign in to comment.