Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spanner/spansql): support DATE and TIMESTAMP literals #3557

Merged
merged 1 commit into from Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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