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): add support for interval arg of some date/timestamp functions #6950

Merged
merged 4 commits into from Nov 3, 2022
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
7 changes: 7 additions & 0 deletions spanner/spansql/keywords.go
Expand Up @@ -138,6 +138,13 @@ func init() {
funcArgParsers["CAST"] = typedArgParser
funcArgParsers["SAFE_CAST"] = typedArgParser
funcArgParsers["EXTRACT"] = extractArgParser
// Spacial case of INTERVAL arg for DATE_ADD, DATE_SUB, GENERATE_DATE_ARRAY
funcArgParsers["DATE_ADD"] = dateIntervalArgParser
funcArgParsers["DATE_SUB"] = dateIntervalArgParser
funcArgParsers["GENERATE_DATE_ARRAY"] = dateIntervalArgParser
// Spacial case of INTERVAL arg for TIMESTAMP_ADD, TIMESTAMP_SUB
funcArgParsers["TIMESTAMP_ADD"] = timestampIntervalArgParser
funcArgParsers["TIMESTAMP_SUB"] = timestampIntervalArgParser
}

var allFuncs = []string{
Expand Down
63 changes: 63 additions & 0 deletions spanner/spansql/parser.go
Expand Up @@ -2818,6 +2818,69 @@ var extractArgParser = func(p *parser) (Expr, *parseError) {
}, nil
}

var intervalArgParser = func(parseDatePart func(*parser) (string, *parseError)) func(*parser) (Expr, *parseError) {
return func(p *parser) (Expr, *parseError) {
if p.eat("INTERVAL") {
expr, err := p.parseExpr()
if err != nil {
return nil, err
}
datePart, err := parseDatePart(p)
if err != nil {
return nil, err
}
return IntervalExpr{Expr: expr, DatePart: datePart}, nil
}
return p.parseExpr()
}
}

var dateIntervalDateParts map[string]bool = map[string]bool{
"DAY": true,
"WEEK": true,
"MONTH": true,
"QUARTER": true,
"YEAR": true,
}

func (p *parser) parseDateIntervalDatePart() (string, *parseError) {
tok := p.next()
if tok.err != nil {
return "", tok.err
}
if dateIntervalDateParts[strings.ToUpper(tok.value)] {
return strings.ToUpper(tok.value), nil
}
return "", p.errorf("got %q, want valid date part names", tok.value)
}

var timestampIntervalDateParts map[string]bool = map[string]bool{
"NANOSECOND": true,
"MICROSECOND": true,
"MILLISECOND": true,
"SECOND": true,
"MINUTE": true,
"HOUR": true,
"DAY": true,
}

func (p *parser) parseTimestampIntervalDatePart() (string, *parseError) {
tok := p.next()
if tok.err != nil {
return "", tok.err
}
if timestampIntervalDateParts[strings.ToUpper(tok.value)] {
return strings.ToUpper(tok.value), nil
}
return "", p.errorf("got %q, want valid date part names", tok.value)
}

// Special argument parser for DATE_ADD, DATE_SUB
var dateIntervalArgParser = intervalArgParser((*parser).parseDateIntervalDatePart)

// Special argument parser for TIMESTAMP_ADD, TIMESTAMP_SUB
var timestampIntervalArgParser = intervalArgParser((*parser).parseTimestampIntervalDatePart)

/*
Expressions

Expand Down
5 changes: 5 additions & 0 deletions spanner/spansql/parser_test.go
Expand Up @@ -411,6 +411,11 @@ func TestParseExpr(t *testing.T) {
{`SAFE_CAST(Bar AS INT64)`, Func{Name: "SAFE_CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: Int64}}}}},
{`EXTRACT(DATE FROM TIMESTAMP AT TIME ZONE "America/Los_Angeles")`, Func{Name: "EXTRACT", Args: []Expr{ExtractExpr{Part: "DATE", Type: Type{Base: Date}, Expr: AtTimeZoneExpr{Expr: ID("TIMESTAMP"), Zone: "America/Los_Angeles", Type: Type{Base: Timestamp}}}}}},
{`EXTRACT(DAY FROM DATE)`, Func{Name: "EXTRACT", Args: []Expr{ExtractExpr{Part: "DAY", Expr: ID("DATE"), Type: Type{Base: Int64}}}}},
{`DATE_ADD(CURRENT_DATE(), INTERVAL 1 DAY)`, Func{Name: "DATE_ADD", Args: []Expr{Func{Name: "CURRENT_DATE"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "DAY"}}}},
{`DATE_SUB(CURRENT_DATE(), INTERVAL 1 WEEK)`, Func{Name: "DATE_SUB", Args: []Expr{Func{Name: "CURRENT_DATE"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "WEEK"}}}},
{`GENERATE_DATE_ARRAY('2022-01-01', CURRENT_DATE(), INTERVAL 1 MONTH)`, Func{Name: "GENERATE_DATE_ARRAY", Args: []Expr{StringLiteral("2022-01-01"), Func{Name: "CURRENT_DATE"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "MONTH"}}}},
{`TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)`, Func{Name: "TIMESTAMP_ADD", Args: []Expr{Func{Name: "CURRENT_TIMESTAMP"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "HOUR"}}}},
{`TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 MINUTE)`, Func{Name: "TIMESTAMP_SUB", Args: []Expr{Func{Name: "CURRENT_TIMESTAMP"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "MINUTE"}}}},

// Conditional expressions
{
Expand Down
9 changes: 9 additions & 0 deletions spanner/spansql/sql.go
Expand Up @@ -694,6 +694,15 @@ func (aze AtTimeZoneExpr) addSQL(sb *strings.Builder) {
sb.WriteString(aze.Zone)
}

func (ie IntervalExpr) SQL() string { return buildSQL(ie) }
func (ie IntervalExpr) addSQL(sb *strings.Builder) {
sb.WriteString("INTERVAL")
sb.WriteString(" ")
ie.Expr.addSQL(sb)
sb.WriteString(" ")
sb.WriteString(ie.DatePart)
}

func idList(l []ID, join string) string {
var ss []string
for _, s := range l {
Expand Down
8 changes: 8 additions & 0 deletions spanner/spansql/types.go
Expand Up @@ -702,6 +702,14 @@ type AtTimeZoneExpr struct {
func (AtTimeZoneExpr) isBoolExpr() {} // possibly bool
func (AtTimeZoneExpr) isExpr() {}

type IntervalExpr struct {
Expr Expr
DatePart string
}

func (IntervalExpr) isBoolExpr() {} // possibly bool
func (IntervalExpr) isExpr() {}

// Paren represents a parenthesised expression.
type Paren struct {
Expr Expr
Expand Down