Skip to content

Commit

Permalink
feat: support CREATE | DROP VIEW (#4937)
Browse files Browse the repository at this point in the history
Co-authored-by: Hengfeng Li <hengfeng@google.com>
  • Loading branch information
olavloite and hengfengli committed Oct 5, 2021
1 parent 2fa096d commit 7b242bd
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 7 deletions.
18 changes: 18 additions & 0 deletions spanner/spannertest/db.go
Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions spanner/spannertest/integration_test.go
Expand Up @@ -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)
Expand Down
59 changes: 53 additions & 6 deletions spanner/spansql/parser.go
Expand Up @@ -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 {
Expand All @@ -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()
Expand Down Expand Up @@ -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)

Expand Down
54 changes: 53 additions & 1 deletion spanner/spansql/parser_test.go
Expand Up @@ -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{
Expand Down Expand Up @@ -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."}},
Expand All @@ -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{
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions spanner/spansql/sql.go
Expand Up @@ -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()
}
Expand All @@ -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()
}
Expand Down
29 changes: 29 additions & 0 deletions spanner/spansql/sql_test.go
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions spanner/spansql/types.go
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down

0 comments on commit 7b242bd

Please sign in to comment.