Skip to content

Commit 81b7c85

Browse files
authored
feat(spanner/spansql): support EXTRACT (#5218)
* feat(spanner/spansql): support EXTRACT * added separate Expr for Extract func and added unit and integration tests * add test for year * repleace atTimeZone func with atTimeZone expression * fixing failing tests * added negative test, reduced the valid extract part values. * remove extra space Co-authored-by: Rahul Yadav <irahul@google.com>
1 parent 2c664a6 commit 81b7c85

File tree

8 files changed

+208
-16
lines changed

8 files changed

+208
-16
lines changed

spanner/spannertest/db_eval.go

+59
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,10 @@ func (ec evalContext) evalExpr(e spansql.Expr) (interface{}, error) {
469469
return ec.evalExpr(e.Expr)
470470
case spansql.TypedExpr:
471471
return ec.evalTypedExpr(e)
472+
case spansql.ExtractExpr:
473+
return ec.evalExtractExpr(e)
474+
case spansql.AtTimeZoneExpr:
475+
return ec.evalAtTimeZoneExpr(e)
472476
case spansql.Func:
473477
v, _, err := ec.evalFunc(e)
474478
if err != nil {
@@ -675,6 +679,61 @@ func (ec evalContext) evalTypedExpr(expr spansql.TypedExpr) (result interface{},
675679
return convert(val, expr.Type)
676680
}
677681

682+
func (ec evalContext) evalExtractExpr(expr spansql.ExtractExpr) (result interface{}, err error) {
683+
val, err := ec.evalExpr(expr.Expr)
684+
if err != nil {
685+
return nil, err
686+
}
687+
switch expr.Part {
688+
case "DATE":
689+
switch v := val.(type) {
690+
case time.Time:
691+
return civil.DateOf(v), nil
692+
case civil.Date:
693+
return v, nil
694+
}
695+
case "DAY":
696+
switch v := val.(type) {
697+
case time.Time:
698+
return int64(v.Day()), nil
699+
case civil.Date:
700+
return int64(v.Day), nil
701+
}
702+
case "MONTH":
703+
switch v := val.(type) {
704+
case time.Time:
705+
return int64(v.Month()), nil
706+
case civil.Date:
707+
return int64(v.Month), nil
708+
}
709+
case "YEAR":
710+
switch v := val.(type) {
711+
case time.Time:
712+
return int64(v.Year()), nil
713+
case civil.Date:
714+
return int64(v.Year), nil
715+
}
716+
}
717+
return nil, fmt.Errorf("Extract with part %v not supported", expr.Part)
718+
}
719+
720+
func (ec evalContext) evalAtTimeZoneExpr(expr spansql.AtTimeZoneExpr) (result interface{}, err error) {
721+
val, err := ec.evalExpr(expr.Expr)
722+
if err != nil {
723+
return nil, err
724+
}
725+
switch v := val.(type) {
726+
case time.Time:
727+
loc, err := time.LoadLocation(expr.Zone)
728+
if err != nil {
729+
return nil, fmt.Errorf("AtTimeZone with %T not supported", v)
730+
}
731+
return v.In(loc), nil
732+
default:
733+
return nil, fmt.Errorf("AtTimeZone with %T not supported", val)
734+
}
735+
}
736+
678737
func evalLiteralOrParam(lop spansql.LiteralOrParam, params queryParams) (int64, error) {
679738
switch v := lop.(type) {
680739
case spansql.IntegerLiteral:

spanner/spannertest/funcs.go

+26
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,32 @@ var functions = map[string]function{
107107
return "", spansql.Type{Base: spansql.String}, nil
108108
},
109109
},
110+
"EXTRACT": {
111+
Eval: func(values []interface{}, types []spansql.Type) (interface{}, spansql.Type, error) {
112+
date, okArg1 := values[0].(civil.Date)
113+
part, okArg2 := values[0].(int64)
114+
if !(okArg1 || okArg2) {
115+
return nil, spansql.Type{}, status.Error(codes.InvalidArgument, "No matching signature for function EXTRACT for the given argument types")
116+
}
117+
if okArg1 {
118+
return date, spansql.Type{Base: spansql.Date}, nil
119+
}
120+
return part, spansql.Type{Base: spansql.Int64}, nil
121+
},
122+
},
123+
"TIMESTAMP": {
124+
Eval: func(values []interface{}, types []spansql.Type) (interface{}, spansql.Type, error) {
125+
t, okArg1 := values[0].(string)
126+
if !(okArg1) {
127+
return nil, spansql.Type{}, status.Error(codes.InvalidArgument, "No matching signature for function TIMESTAMP for the given argument types")
128+
}
129+
timestamp, err := time.Parse(time.RFC3339, t)
130+
if err != nil {
131+
return nil, spansql.Type{}, status.Error(codes.InvalidArgument, "No matching signature for function TIMESTAMP for the given argument types")
132+
}
133+
return timestamp, spansql.Type{Base: spansql.Timestamp}, nil
134+
},
135+
},
110136
}
111137

112138
func cast(values []interface{}, types []spansql.Type, safe bool) (interface{}, spansql.Type, error) {

spanner/spannertest/integration_test.go

+26-13
Original file line numberDiff line numberDiff line change
@@ -748,16 +748,24 @@ func TestIntegration_ReadsAndQueries(t *testing.T) {
748748
}
749749
rows.Stop()
750750

751+
rows = client.Single().Query(ctx, spanner.NewStatement("SELECT EXTRACT(INVALID_PART FROM TIMESTAMP('2008-12-25T05:30:00Z')"))
752+
_, err = rows.Next()
753+
if g, w := spanner.ErrCode(err), codes.InvalidArgument; g != w {
754+
t.Errorf("error code mismatch for invalid part from EXTRACT\n Got: %v\nWant: %v", g, w)
755+
}
756+
rows.Stop()
757+
751758
// Do some complex queries.
752759
tests := []struct {
753760
q string
754761
params map[string]interface{}
755762
want [][]interface{}
756763
}{
757764
{
758-
`SELECT 17, "sweet", TRUE AND FALSE, NULL, B"hello", STARTS_WITH('Foo', 'B'), STARTS_WITH('Bar', 'B'), CAST(17 AS STRING), SAFE_CAST(TRUE AS STRING), SAFE_CAST('Foo' AS INT64)`,
765+
766+
`SELECT 17, "sweet", TRUE AND FALSE, NULL, B"hello", STARTS_WITH('Foo', 'B'), STARTS_WITH('Bar', 'B'), CAST(17 AS STRING), SAFE_CAST(TRUE AS STRING), SAFE_CAST('Foo' AS INT64), EXTRACT(DATE FROM TIMESTAMP('2008-12-25T05:30:00Z') AT TIME ZONE 'Europe/Amsterdam'), EXTRACT(YEAR FROM TIMESTAMP('2008-12-25T05:30:00Z'))`,
759767
nil,
760-
[][]interface{}{{int64(17), "sweet", false, nil, []byte("hello"), false, true, "17", "true", nil}},
768+
[][]interface{}{{int64(17), "sweet", false, nil, []byte("hello"), false, true, "17", "true", nil, civil.Date{Year: 2008, Month: 12, Day: 25}, int64(2008)}},
761769
},
762770
// Check handling of NULL values for the IS operator.
763771
// There was a bug that returned errors for some of these cases.
@@ -1277,13 +1285,16 @@ func TestIntegration_GeneratedColumns(t *testing.T) {
12771285
defer cancel()
12781286

12791287
tableName := "SongWriters"
1280-
12811288
err := updateDDL(t, adminClient,
12821289
`CREATE TABLE `+tableName+` (
12831290
Name STRING(50) NOT NULL,
12841291
NumSongs INT64,
1292+
CreatedAT TIMESTAMP,
1293+
CreatedDate DATE,
12851294
EstimatedSales INT64 NOT NULL,
12861295
CanonicalName STRING(50) AS (LOWER(Name)) STORED,
1296+
GeneratedCreatedDate DATE AS (EXTRACT(DATE FROM CreatedAT AT TIME ZONE "CET")) STORED,
1297+
GeneratedCreatedDay INT64 AS (EXTRACT(DAY FROM CreatedDate)) STORED,
12871298
) PRIMARY KEY (Name)`)
12881299
if err != nil {
12891300
t.Fatalf("Setting up fresh table: %v", err)
@@ -1295,16 +1306,18 @@ func TestIntegration_GeneratedColumns(t *testing.T) {
12951306
}
12961307

12971308
// Insert some data.
1309+
d1, _ := civil.ParseDate("2016-11-15")
1310+
t1, _ := time.Parse(time.RFC3339Nano, "2016-11-15T15:04:05.999999999Z")
12981311
_, err = client.Apply(ctx, []*spanner.Mutation{
12991312
spanner.Insert(tableName,
1300-
[]string{"Name", "EstimatedSales", "NumSongs"},
1301-
[]interface{}{"Average Writer", 10, 10}),
1313+
[]string{"Name", "EstimatedSales", "NumSongs", "CreatedAT", "CreatedDate"},
1314+
[]interface{}{"Average Writer", 10, 10, t1, d1}),
13021315
spanner.Insert(tableName,
1303-
[]string{"Name", "EstimatedSales"},
1304-
[]interface{}{"Great Writer", 100}),
1316+
[]string{"Name", "EstimatedSales", "CreatedAT", "CreatedDate"},
1317+
[]interface{}{"Great Writer", 100, t1, d1}),
13051318
spanner.Insert(tableName,
1306-
[]string{"Name", "EstimatedSales", "NumSongs"},
1307-
[]interface{}{"Poor Writer", 1, 50}),
1319+
[]string{"Name", "EstimatedSales", "NumSongs", "CreatedAT", "CreatedDate"},
1320+
[]interface{}{"Poor Writer", 1, 50, t1, d1}),
13081321
})
13091322
if err != nil {
13101323
t.Fatalf("Applying mutations: %v", err)
@@ -1317,7 +1330,7 @@ func TestIntegration_GeneratedColumns(t *testing.T) {
13171330
}
13181331

13191332
ri := client.Single().Query(ctx, spanner.NewStatement(
1320-
`SELECT CanonicalName, TotalSales FROM `+tableName+` ORDER BY Name`,
1333+
`SELECT CanonicalName, TotalSales, GeneratedCreatedDate, GeneratedCreatedDay FROM `+tableName+` ORDER BY Name`,
13211334
))
13221335
all, err := slurpRows(t, ri)
13231336
if err != nil {
@@ -1326,9 +1339,9 @@ func TestIntegration_GeneratedColumns(t *testing.T) {
13261339

13271340
// Great writer has nil because NumSongs is nil
13281341
want := [][]interface{}{
1329-
{"average writer", int64(100)},
1330-
{"great writer", nil},
1331-
{"poor writer", int64(50)},
1342+
{"average writer", int64(100), civil.Date{Year: 2016, Month: 11, Day: 15}, int64(15)},
1343+
{"great writer", nil, civil.Date{Year: 2016, Month: 11, Day: 15}, int64(15)},
1344+
{"poor writer", int64(50), civil.Date{Year: 2016, Month: 11, Day: 15}, int64(15)},
13321345
}
13331346
if !reflect.DeepEqual(all, want) {
13341347
t.Errorf("Expected values are wrong.\n got %v\nwant %v", all, want)

spanner/spansql/keywords.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ func init() {
134134
for _, f := range allFuncs {
135135
funcs[f] = true
136136
}
137-
// Special case for CAST and SAFE_CAST
137+
// Special case for CAST, SAFE_CAST and EXTRACT
138138
funcArgParsers["CAST"] = typedArgParser
139139
funcArgParsers["SAFE_CAST"] = typedArgParser
140+
funcArgParsers["EXTRACT"] = extractArgParser
140141
}
141142

142143
var allFuncs = []string{

spanner/spansql/parser.go

+49
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,27 @@ func (p *parser) parseType() (Type, *parseError) {
19011901
return p.parseBaseOrParameterizedType(true)
19021902
}
19031903

1904+
var extractPartTypes = map[string]TypeBase{
1905+
"DAY": Int64,
1906+
"MONTH": Int64,
1907+
"YEAR": Int64,
1908+
"DATE": Date,
1909+
}
1910+
1911+
func (p *parser) parseExtractType() (Type, string, *parseError) {
1912+
var t Type
1913+
tok := p.next()
1914+
if tok.err != nil {
1915+
return Type{}, "", tok.err
1916+
}
1917+
base, ok := extractPartTypes[strings.ToUpper(tok.value)] // valid part types for EXTRACT is keyed by upper case strings.
1918+
if !ok {
1919+
return Type{}, "", p.errorf("got %q, want valid EXTRACT types", tok.value)
1920+
}
1921+
t.Base = base
1922+
return t, strings.ToUpper(tok.value), nil
1923+
}
1924+
19041925
func (p *parser) parseBaseOrParameterizedType(withParam bool) (Type, *parseError) {
19051926
debugf("parseBaseOrParameterizedType: %v", p)
19061927

@@ -2482,6 +2503,34 @@ var typedArgParser = func(p *parser) (Expr, *parseError) {
24822503
}, nil
24832504
}
24842505

2506+
// Special argument parser for EXTRACT
2507+
var extractArgParser = func(p *parser) (Expr, *parseError) {
2508+
partType, part, err := p.parseExtractType()
2509+
if err != nil {
2510+
return nil, err
2511+
}
2512+
if err := p.expect("FROM"); err != nil {
2513+
return nil, err
2514+
}
2515+
e, err := p.parseExpr()
2516+
if err != nil {
2517+
return nil, err
2518+
}
2519+
// AT TIME ZONE is optional
2520+
if p.eat("AT", "TIME", "ZONE") {
2521+
tok := p.next()
2522+
if tok.err != nil {
2523+
return nil, err
2524+
}
2525+
return ExtractExpr{Part: part, Type: partType, Expr: AtTimeZoneExpr{Expr: e, Zone: tok.string, Type: Type{Base: Timestamp}}}, nil
2526+
}
2527+
return ExtractExpr{
2528+
Part: part,
2529+
Expr: e,
2530+
Type: partType,
2531+
}, nil
2532+
}
2533+
24852534
/*
24862535
Expressions
24872536

spanner/spansql/parser_test.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ func TestParseExpr(t *testing.T) {
340340
{`STARTS_WITH(Bar, 'B')`, Func{Name: "STARTS_WITH", Args: []Expr{ID("Bar"), StringLiteral("B")}}},
341341
{`CAST(Bar AS STRING)`, Func{Name: "CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: String}}}}},
342342
{`SAFE_CAST(Bar AS INT64)`, Func{Name: "SAFE_CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: Int64}}}}},
343+
{`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}}}}}},
344+
{`EXTRACT(DAY FROM DATE)`, Func{Name: "EXTRACT", Args: []Expr{ExtractExpr{Part: "DAY", Expr: ID("DATE"), Type: Type{Base: Int64}}}}},
343345

344346
// String literal:
345347
// Accept double quote and single quote.
@@ -524,7 +526,9 @@ func TestParseDDL(t *testing.T) {
524526
CREATE TABLE users (
525527
user_id STRING(36) NOT NULL,
526528
some_string STRING(16) NOT NULL,
529+
some_time TIMESTAMP NOT NULL,
527530
number_key INT64 AS (SAFE_CAST(SUBSTR(some_string, 2) AS INT64)) STORED,
531+
generated_date DATE AS (EXTRACT(DATE FROM some_time AT TIME ZONE "CET")) STORED,
528532
) PRIMARY KEY(user_id);
529533
530534
-- Trailing comment at end of file.
@@ -744,12 +748,20 @@ func TestParseDDL(t *testing.T) {
744748
Columns: []ColumnDef{
745749
{Name: "user_id", Type: Type{Base: String, Len: 36}, NotNull: true, Position: line(67)},
746750
{Name: "some_string", Type: Type{Base: String, Len: 16}, NotNull: true, Position: line(68)},
751+
{Name: "some_time", Type: Type{Base: Timestamp}, NotNull: true, Position: line(69)},
747752
{
748753
Name: "number_key", Type: Type{Base: Int64},
749754
Generated: Func{Name: "SAFE_CAST", Args: []Expr{
750755
TypedExpr{Expr: Func{Name: "SUBSTR", Args: []Expr{ID("some_string"), IntegerLiteral(2)}}, Type: Type{Base: Int64}},
751756
}},
752-
Position: line(69),
757+
Position: line(70),
758+
},
759+
{
760+
Name: "generated_date", Type: Type{Base: Date},
761+
Generated: Func{Name: "EXTRACT", Args: []Expr{
762+
ExtractExpr{Part: "DATE", Type: Type{Base: Date}, Expr: AtTimeZoneExpr{Expr: ID("some_time"), Zone: "CET", Type: Type{Base: Timestamp}}},
763+
}},
764+
Position: line(71),
753765
},
754766
},
755767
PrimaryKey: []KeyPart{{Column: "user_id"}},
@@ -777,7 +789,7 @@ func TestParseDDL(t *testing.T) {
777789
{Marker: "--", Isolated: true, Start: line(49), End: line(49), Text: []string{"Table with row deletion policy."}},
778790

779791
// Comment after everything else.
780-
{Marker: "--", Isolated: true, Start: line(72), End: line(72), Text: []string{"Trailing comment at end of file."}},
792+
{Marker: "--", Isolated: true, Start: line(74), End: line(74), Text: []string{"Trailing comment at end of file."}},
781793
}}},
782794
// No trailing comma:
783795
{`ALTER TABLE T ADD COLUMN C2 INT64`, &DDL{Filename: "filename", List: []DDLStmt{

spanner/spansql/sql.go

+14
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,20 @@ func (te TypedExpr) addSQL(sb *strings.Builder) {
589589
sb.WriteString(te.Type.SQL())
590590
}
591591

592+
func (ee ExtractExpr) SQL() string { return buildSQL(ee) }
593+
func (ee ExtractExpr) addSQL(sb *strings.Builder) {
594+
sb.WriteString(ee.Part)
595+
sb.WriteString(" FROM ")
596+
ee.Expr.addSQL(sb)
597+
}
598+
599+
func (aze AtTimeZoneExpr) SQL() string { return buildSQL(aze) }
600+
func (aze AtTimeZoneExpr) addSQL(sb *strings.Builder) {
601+
aze.Expr.addSQL(sb)
602+
sb.WriteString(" AT TIME ZONE ")
603+
sb.WriteString(aze.Zone)
604+
}
605+
592606
func idList(l []ID, join string) string {
593607
var ss []string
594608
for _, s := range l {

spanner/spansql/types.go

+18
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,24 @@ type TypedExpr struct {
647647
func (TypedExpr) isBoolExpr() {} // possibly bool
648648
func (TypedExpr) isExpr() {}
649649

650+
type ExtractExpr struct {
651+
Part string
652+
Type Type
653+
Expr Expr
654+
}
655+
656+
func (ExtractExpr) isBoolExpr() {} // possibly bool
657+
func (ExtractExpr) isExpr() {}
658+
659+
type AtTimeZoneExpr struct {
660+
Expr Expr
661+
Type Type
662+
Zone string
663+
}
664+
665+
func (AtTimeZoneExpr) isBoolExpr() {} // possibly bool
666+
func (AtTimeZoneExpr) isExpr() {}
667+
650668
// Paren represents a parenthesised expression.
651669
type Paren struct {
652670
Expr Expr

0 commit comments

Comments
 (0)