diff --git a/spanner/spannertest/db.go b/spanner/spannertest/db.go index f5c04b03704..82e7bcaca06 100644 --- a/spanner/spannertest/db.go +++ b/spanner/spannertest/db.go @@ -45,6 +45,7 @@ type database struct { lastTS time.Time // last commit timestamp tables map[spansql.ID]*table indexes map[spansql.ID]struct{} // only record their existence + views map[spansql.ID]struct{} // only record their existence rwMu sync.Mutex // held by read-write transactions } @@ -249,6 +250,9 @@ func (d *database) ApplyDDL(stmt spansql.DDLStmt) *status.Status { if d.indexes == nil { d.indexes = make(map[spansql.ID]struct{}) } + if d.views == nil { + d.views = make(map[spansql.ID]struct{}) + } switch stmt := stmt.(type) { default: @@ -305,6 +309,14 @@ func (d *database) ApplyDDL(stmt spansql.DDLStmt) *status.Status { } d.indexes[stmt.Name] = struct{}{} return nil + case *spansql.CreateView: + if !stmt.OrReplace { + if _, ok := d.views[stmt.Name]; ok { + return status.Newf(codes.AlreadyExists, "view %s already exists", stmt.Name) + } + } + d.views[stmt.Name] = struct{}{} + return nil case *spansql.DropTable: if _, ok := d.tables[stmt.Name]; !ok { return status.Newf(codes.NotFound, "no table named %s", stmt.Name) @@ -318,6 +330,12 @@ func (d *database) ApplyDDL(stmt spansql.DDLStmt) *status.Status { } delete(d.indexes, stmt.Name) return nil + case *spansql.DropView: + if _, ok := d.views[stmt.Name]; !ok { + return status.Newf(codes.NotFound, "no view named %s", stmt.Name) + } + delete(d.views, stmt.Name) + return nil case *spansql.AlterTable: t, ok := d.tables[stmt.Name] if !ok { diff --git a/spanner/spannertest/integration_test.go b/spanner/spannertest/integration_test.go index c67238e3c41..d31a7c6e971 100644 --- a/spanner/spannertest/integration_test.go +++ b/spanner/spannertest/integration_test.go @@ -1387,6 +1387,32 @@ func TestIntegration_GeneratedColumns(t *testing.T) { } } +func TestIntegration_Views(t *testing.T) { + _, adminClient, _, cleanup := makeClient(t) + defer cleanup() + + err := updateDDL(t, adminClient, `CREATE VIEW SingersView SQL SECURITY INVOKER AS SELECT * FROM Singers`) + if err != nil { + t.Fatalf("Creating view: %v", err) + } + err = updateDDL(t, adminClient, `CREATE VIEW SingersView SQL SECURITY INVOKER AS SELECT * FROM Singers ORDER BY LastName`) + if g, w := spanner.ErrCode(err), codes.AlreadyExists; g != w { + t.Fatalf("Creating duplicate view error code mismatch\n Got: %v\nWant: %v", g, w) + } + err = updateDDL(t, adminClient, `CREATE OR REPLACE VIEW SingersView SQL SECURITY INVOKER AS SELECT * FROM Singers ORDER BY LastName`) + if err != nil { + t.Fatalf("Replacing view: %v", err) + } + err = updateDDL(t, adminClient, `DROP VIEW SingersView`) + if err != nil { + t.Fatalf("Dropping view: %v", err) + } + err = updateDDL(t, adminClient, `DROP VIEW SingersView`) + if g, w := spanner.ErrCode(err), codes.NotFound; g != w { + t.Fatalf("Creating duplicate view error code mismatch\n Got: %v\nWant: %v", g, w) + } +} + func dropTable(t *testing.T, adminClient *dbadmin.DatabaseAdminClient, table string) error { t.Helper() err := updateDDL(t, adminClient, "DROP TABLE "+table) diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go index 1d11394c0db..6b3293069dd 100644 --- a/spanner/spansql/parser.go +++ b/spanner/spansql/parser.go @@ -959,26 +959,28 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { if p.sniff("CREATE", "TABLE") { ct, err := p.parseCreateTable() return ct, err - } else if p.sniff("CREATE") { - // The only other statement starting with CREATE is CREATE INDEX, - // which can have UNIQUE or NULL_FILTERED as the token after CREATE. + } else if p.sniff("CREATE", "INDEX") || p.sniff("CREATE", "UNIQUE", "INDEX") || p.sniff("CREATE", "NULL_FILTERED", "INDEX") || p.sniff("CREATE", "UNIQUE", "NULL_FILTERED", "INDEX") { ci, err := p.parseCreateIndex() return ci, err + } else if p.sniff("CREATE", "VIEW") || p.sniff("CREATE", "OR", "REPLACE", "VIEW") { + cv, err := p.parseCreateView() + return cv, err } else if p.sniff("ALTER", "TABLE") { a, err := p.parseAlterTable() return a, err } else if p.eat("DROP") { pos := p.Pos() // These statements are simple. - // DROP TABLE table_name - // DROP INDEX index_name + // DROP TABLE table_name + // DROP INDEX index_name + // DROP VIEW view_name tok := p.next() if tok.err != nil { return nil, tok.err } switch { default: - return nil, p.errorf("got %q, want TABLE or INDEX", tok.value) + return nil, p.errorf("got %q, want TABLE, VIEW or INDEX", tok.value) case tok.caseEqual("TABLE"): name, err := p.parseTableOrIndexOrColumnName() if err != nil { @@ -991,6 +993,12 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { return nil, err } return &DropIndex{Name: name, Position: pos}, nil + case tok.caseEqual("VIEW"): + name, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + return &DropView{Name: name, Position: pos}, nil } } else if p.sniff("ALTER", "DATABASE") { a, err := p.parseAlterDatabase() @@ -1197,6 +1205,45 @@ func (p *parser) parseCreateIndex() (*CreateIndex, *parseError) { return ci, nil } +func (p *parser) parseCreateView() (*CreateView, *parseError) { + debugf("parseCreateView: %v", p) + + /* + { CREATE VIEW | CREATE OR REPLACE VIEW } view_name + SQL SECURITY INVOKER + AS query + */ + + var orReplace bool + + if err := p.expect("CREATE"); err != nil { + return nil, err + } + pos := p.Pos() + if p.eat("OR", "REPLACE") { + orReplace = true + } + if err := p.expect("VIEW"); err != nil { + return nil, err + } + vname, err := p.parseTableOrIndexOrColumnName() + if err := p.expect("SQL", "SECURITY", "INVOKER", "AS"); err != nil { + return nil, err + } + query, err := p.parseQuery() + if err != nil { + return nil, err + } + + return &CreateView{ + Name: vname, + OrReplace: orReplace, + Query: query, + + Position: pos, + }, nil +} + func (p *parser) parseAlterTable() (*AlterTable, *parseError) { debugf("parseAlterTable: %v", p) diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go index ef2deaf2c37..6aca49eff8e 100644 --- a/spanner/spansql/parser_test.go +++ b/spanner/spansql/parser_test.go @@ -500,6 +500,12 @@ func TestParseDDL(t *testing.T) { ALTER TABLE WithRowDeletionPolicy ADD ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY )); ALTER TABLE WithRowDeletionPolicy REPLACE ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY )); + CREATE VIEW SingersView + SQL SECURITY INVOKER + AS SELECT SingerId, FullName + FROM Singers + ORDER BY LastName, FirstName; + -- Trailing comment at end of file. `, &DDL{Filename: "filename", List: []DDLStmt{ &CreateTable{ @@ -689,6 +695,23 @@ func TestParseDDL(t *testing.T) { }, Position: line(58), }, + &CreateView{ + Name: "SingersView", + OrReplace: false, + Query: Query{ + Select: Select{ + List: []Expr{ID("SingerId"), ID("FullName")}, + From: []SelectFrom{SelectFromTable{ + Table: "Singers", + }}, + }, + Order: []Order{ + {Expr: ID("LastName")}, + {Expr: ID("FirstName")}, + }, + }, + Position: line(60), + }, }, Comments: []*Comment{ {Marker: "#", Start: line(2), End: line(2), Text: []string{"This is a comment."}}, @@ -711,7 +734,7 @@ func TestParseDDL(t *testing.T) { {Marker: "--", Isolated: true, Start: line(49), End: line(49), Text: []string{"Table with row deletion policy."}}, // Comment after everything else. - {Marker: "--", Isolated: true, Start: line(60), End: line(60), Text: []string{"Trailing comment at end of file."}}, + {Marker: "--", Isolated: true, Start: line(66), End: line(66), Text: []string{"Trailing comment at end of file."}}, }}}, // No trailing comma: {`ALTER TABLE T ADD COLUMN C2 INT64`, &DDL{Filename: "filename", List: []DDLStmt{ @@ -767,6 +790,35 @@ func TestParseDDL(t *testing.T) { }, }, }}, + {"CREATE OR REPLACE VIEW `SingersView` SQL SECURITY INVOKER AS SELECT SingerId, FullName, Picture FROM Singers ORDER BY LastName, FirstName", + &DDL{Filename: "filename", List: []DDLStmt{ + &CreateView{ + Name: "SingersView", + OrReplace: true, + Query: Query{ + Select: Select{ + List: []Expr{ID("SingerId"), ID("FullName"), ID("Picture")}, + From: []SelectFrom{SelectFromTable{ + Table: "Singers", + }}, + }, + Order: []Order{ + {Expr: ID("LastName")}, + {Expr: ID("FirstName")}, + }, + }, + Position: line(1), + }, + }, + }}, + {"DROP VIEW `SingersView`", + &DDL{Filename: "filename", List: []DDLStmt{ + &DropView{ + Name: "SingersView", + Position: line(1), + }, + }, + }}, } for _, test := range tests { got, err := ParseDDL("filename", test.in) diff --git a/spanner/spansql/sql.go b/spanner/spansql/sql.go index 1ace18bdbbf..859c08afae3 100644 --- a/spanner/spansql/sql.go +++ b/spanner/spansql/sql.go @@ -87,6 +87,15 @@ func (ci CreateIndex) SQL() string { return str } +func (cv CreateView) SQL() string { + str := "CREATE" + if cv.OrReplace { + str += " OR REPLACE" + } + str += " VIEW " + cv.Name.SQL() + " SQL SECURITY INVOKER AS " + cv.Query.SQL() + return str +} + func (dt DropTable) SQL() string { return "DROP TABLE " + dt.Name.SQL() } @@ -95,6 +104,10 @@ func (di DropIndex) SQL() string { return "DROP INDEX " + di.Name.SQL() } +func (dv DropView) SQL() string { + return "DROP VIEW " + dv.Name.SQL() +} + func (at AlterTable) SQL() string { return "ALTER TABLE " + at.Name.SQL() + " " + at.Alteration.SQL() } diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go index 893de913e52..28897490bd7 100644 --- a/spanner/spansql/sql_test.go +++ b/spanner/spansql/sql_test.go @@ -185,6 +185,35 @@ func TestSQL(t *testing.T) { "DROP INDEX Ia", reparseDDL, }, + { + &CreateView{ + Name: "SingersView", + OrReplace: true, + Query: Query{ + Select: Select{ + List: []Expr{ID("SingerId"), ID("FullName"), ID("Picture")}, + From: []SelectFrom{SelectFromTable{ + Table: "Singers", + }}, + }, + Order: []Order{ + {Expr: ID("LastName")}, + {Expr: ID("FirstName")}, + }, + }, + Position: line(1), + }, + "CREATE OR REPLACE VIEW SingersView SQL SECURITY INVOKER AS SELECT SingerId, FullName, Picture FROM Singers ORDER BY LastName, FirstName", + reparseDDL, + }, + { + &DropView{ + Name: "SingersView", + Position: line(1), + }, + "DROP VIEW SingersView", + reparseDDL, + }, { &AlterTable{ Name: "Ta", diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go index 87b9c14482d..da592532b3b 100644 --- a/spanner/spansql/types.go +++ b/spanner/spansql/types.go @@ -118,6 +118,21 @@ func (*CreateIndex) isDDLStmt() {} func (ci *CreateIndex) Pos() Position { return ci.Position } func (ci *CreateIndex) clearOffset() { ci.Position.Offset = 0 } +// CreateView represents a CREATE [OR REPLACE] VIEW statement. +// https://cloud.google.com/spanner/docs/data-definition-language#view_statements +type CreateView struct { + Name ID + OrReplace bool + Query Query + + Position Position // position of the "CREATE" token +} + +func (cv *CreateView) String() string { return fmt.Sprintf("%#v", cv) } +func (*CreateView) isDDLStmt() {} +func (cv *CreateView) Pos() Position { return cv.Position } +func (cv *CreateView) clearOffset() { cv.Position.Offset = 0 } + // DropTable represents a DROP TABLE statement. // https://cloud.google.com/spanner/docs/data-definition-language#drop_table type DropTable struct { @@ -144,6 +159,19 @@ func (*DropIndex) isDDLStmt() {} func (di *DropIndex) Pos() Position { return di.Position } func (di *DropIndex) clearOffset() { di.Position.Offset = 0 } +// DropView represents a DROP VIEW statement. +// https://cloud.google.com/spanner/docs/data-definition-language#drop-view +type DropView struct { + Name ID + + Position Position // position of the "DROP" token +} + +func (dv *DropView) String() string { return fmt.Sprintf("%#v", dv) } +func (*DropView) isDDLStmt() {} +func (dv *DropView) Pos() Position { return dv.Position } +func (dv *DropView) clearOffset() { dv.Position.Offset = 0 } + // AlterTable represents an ALTER TABLE statement. // https://cloud.google.com/spanner/docs/data-definition-language#alter_table type AlterTable struct {