diff --git a/spanner/spannertest/README.md b/spanner/spannertest/README.md index 7fc451a6e22..5f4317e4cd9 100644 --- a/spanner/spannertest/README.md +++ b/spanner/spannertest/README.md @@ -21,15 +21,16 @@ by ascending esotericism: - more aggregation functions - SELECT HAVING - case insensitivity -- multiple joins - alternate literal types (esp. strings) -- STRUCT types -- transaction simulation +- generated columns - expression type casting, coercion +- multiple joins - subselects +- transaction simulation - FOREIGN KEY and CHECK constraints - INSERT DML statements - set operations (UNION, INTERSECT, EXCEPT) +- STRUCT types - partition support - conditional expressions - table sampling (implementation) diff --git a/spanner/spansql/keywords.go b/spanner/spansql/keywords.go index 7964385d2da..39a0493f1ac 100644 --- a/spanner/spansql/keywords.go +++ b/spanner/spansql/keywords.go @@ -143,5 +143,8 @@ var funcs = map[string]bool{ // Hash functions. "SHA1": true, + // String functions. + "CHAR_LENGTH": true, + // TODO: many more } diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go index e7c27937b55..ce9badd8483 100644 --- a/spanner/spansql/parser.go +++ b/spanner/spansql/parser.go @@ -1373,7 +1373,7 @@ func (p *parser) parseColumnDef() (ColumnDef, *parseError) { /* column_def: - column_name {scalar_type | array_type} [NOT NULL] [options_def] + column_name {scalar_type | array_type} [NOT NULL] [AS ( expression ) STORED] [options_def] */ name, err := p.parseTableOrIndexOrColumnName() @@ -1392,6 +1392,19 @@ func (p *parser) parseColumnDef() (ColumnDef, *parseError) { cd.NotNull = true } + if p.eat("AS", "(") { + cd.Generated, err = p.parseExpr() + if err != nil { + return ColumnDef{}, err + } + if err := p.expect(")"); err != nil { + return ColumnDef{}, err + } + if err := p.expect("STORED"); err != nil { + return ColumnDef{}, err + } + } + if p.sniff("OPTIONS") { cd.Options, err = p.parseColumnOptions() if err != nil { diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go index f1b47a04b1d..e47018fd8d0 100644 --- a/spanner/spansql/parser_test.go +++ b/spanner/spansql/parser_test.go @@ -379,6 +379,12 @@ func TestParseDDL(t *testing.T) { Names ARRAY, ) PRIMARY KEY (Dummy); + -- Table with generated column. + CREATE TABLE GenCol ( + Name STRING(MAX) NOT NULL, + NameLen INT64 AS (CHAR_LENGTH(Name)) STORED, + ) PRIMARY KEY (Name); + -- Trailing comment at end of file. `, &DDL{Filename: "filename", List: []DDLStmt{ &CreateTable{ @@ -517,6 +523,19 @@ func TestParseDDL(t *testing.T) { PrimaryKey: []KeyPart{{Column: "Dummy"}}, Position: line(35), }, + &CreateTable{ + Name: "GenCol", + Columns: []ColumnDef{ + {Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(45)}, + { + Name: "NameLen", Type: Type{Base: Int64}, + Generated: Func{Name: "CHAR_LENGTH", Args: []Expr{ID("Name")}}, + Position: line(46), + }, + }, + PrimaryKey: []KeyPart{{Column: "Name"}}, + Position: line(44), + }, }, Comments: []*Comment{ {Marker: "#", Start: line(2), End: line(2), Text: []string{"This is a comment."}}, @@ -535,8 +554,10 @@ func TestParseDDL(t *testing.T) { {Marker: "--", Start: line(37), End: line(37), Text: []string{"comment on ids"}}, {Marker: "--", Isolated: true, Start: line(38), End: line(38), Text: []string{"leading multi comment immediately after inline comment"}}, + {Marker: "--", Isolated: true, Start: line(43), End: line(43), Text: []string{"Table with generated column."}}, + // Comment after everything else. - {Marker: "--", Isolated: true, Start: line(43), End: line(43), Text: []string{"Trailing comment at end of file."}}, + {Marker: "--", Isolated: true, Start: line(49), End: line(49), Text: []string{"Trailing comment at end of file."}}, }}}, // No trailing comma: {`ALTER TABLE T ADD COLUMN C2 INT64`, &DDL{Filename: "filename", List: []DDLStmt{ diff --git a/spanner/spansql/sql.go b/spanner/spansql/sql.go index 0ac2b90f26b..d5457de8676 100644 --- a/spanner/spansql/sql.go +++ b/spanner/spansql/sql.go @@ -180,6 +180,9 @@ func (cd ColumnDef) SQL() string { if cd.NotNull { str += " NOT NULL" } + if cd.Generated != nil { + str += " AS (" + cd.Generated.SQL() + ") STORED" + } if cd.Options != (ColumnOptions{}) { str += " " + cd.Options.SQL() } diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go index beb92ec47a0..d50089d63e1 100644 --- a/spanner/spansql/sql_test.go +++ b/spanner/spansql/sql_test.go @@ -75,6 +75,7 @@ func TestSQL(t *testing.T) { {Name: "Cj", Type: Type{Array: true, Base: Int64}, Position: line(11)}, {Name: "Ck", Type: Type{Array: true, Base: String, Len: MaxLen}, Position: line(12)}, {Name: "Cl", Type: Type{Base: Timestamp}, Options: ColumnOptions{AllowCommitTimestamp: boolAddr(false)}, Position: line(13)}, + {Name: "Cm", Type: Type{Base: Int64}, Generated: Func{Name: "CHAR_LENGTH", Args: []Expr{ID("Ce")}}, Position: line(14)}, }, PrimaryKey: []KeyPart{ {Column: "Ca"}, @@ -95,6 +96,7 @@ func TestSQL(t *testing.T) { Cj ARRAY, Ck ARRAY, Cl TIMESTAMP OPTIONS (allow_commit_timestamp = null), + Cm INT64 AS (CHAR_LENGTH(Ce)) STORED, ) PRIMARY KEY(Ca, Cb DESC)`, reparseDDL, }, diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go index ece38b084cc..5854e0e0d64 100644 --- a/spanner/spansql/types.go +++ b/spanner/spansql/types.go @@ -244,6 +244,8 @@ type ColumnDef struct { Type Type NotNull bool + Generated Expr // set of this is a generated column + Options ColumnOptions Position Position // position of the column name