From 96840ab1232bbdb788e37f81cf113ee0f1b4e8e7 Mon Sep 17 00:00:00 2001 From: TOGASHI Tomoki Date: Thu, 20 Jul 2023 12:44:17 +0900 Subject: [PATCH] feat(spanner/spansql): add support for IF NOT EXISTS and IF EXISTS clause (#8245) * feat(spanner/spansql): support CREATE TABLE IF NOT EXISTS * feat(spanner/spansql): support CREATE INDEX IF NOT EXISTS * feat(spanner/spansql): support ALTER TABLE ADD COLUMN IF NOT EXISTS * feat(spanner/spansql): support DROP TABLE IF EXISTS, DROP INDEX IF EXISTS --- spanner/spansql/parser.go | 40 ++++++++++++++----- spanner/spansql/parser_test.go | 72 ++++++++++++++++++++++++++++++++++ spanner/spansql/sql.go | 33 +++++++++++++--- spanner/spansql/sql_test.go | 62 +++++++++++++++++++++++++++++ spanner/spansql/types.go | 13 ++++-- 5 files changed, 202 insertions(+), 18 deletions(-) diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go index 1a6741e8aeb1..5a763ff92177 100644 --- a/spanner/spansql/parser.go +++ b/spanner/spansql/parser.go @@ -1005,8 +1005,8 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { } else if p.eat("DROP") { pos := p.Pos() // These statements are simple. - // DROP TABLE table_name - // DROP INDEX index_name + // DROP TABLE [ IF EXISTS ] table_name + // DROP INDEX [ IF EXISTS ] index_name // DROP VIEW view_name // DROP ROLE role_name // DROP CHANGE STREAM change_stream_name @@ -1018,17 +1018,25 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { default: return nil, p.errorf("got %q, want TABLE, VIEW, INDEX or CHANGE", tok.value) case tok.caseEqual("TABLE"): + var ifExists bool + if p.eat("IF", "EXISTS") { + ifExists = true + } name, err := p.parseTableOrIndexOrColumnName() if err != nil { return nil, err } - return &DropTable{Name: name, Position: pos}, nil + return &DropTable{Name: name, IfExists: ifExists, Position: pos}, nil case tok.caseEqual("INDEX"): + var ifExists bool + if p.eat("IF", "EXISTS") { + ifExists = true + } name, err := p.parseTableOrIndexOrColumnName() if err != nil { return nil, err } - return &DropIndex{Name: name, Position: pos}, nil + return &DropIndex{Name: name, IfExists: ifExists, Position: pos}, nil case tok.caseEqual("VIEW"): name, err := p.parseTableOrIndexOrColumnName() if err != nil { @@ -1081,7 +1089,7 @@ func (p *parser) parseCreateTable() (*CreateTable, *parseError) { debugf("parseCreateTable: %v", p) /* - CREATE TABLE table_name( + CREATE TABLE [ IF NOT EXISTS ] table_name( [column_def, ...] [ table_constraint, ...] ) primary_key [, cluster] @@ -1091,6 +1099,7 @@ func (p *parser) parseCreateTable() (*CreateTable, *parseError) { cluster: INTERLEAVE IN PARENT table_name [ ON DELETE { CASCADE | NO ACTION } ] */ + var ifNotExists bool if err := p.expect("CREATE"); err != nil { return nil, err @@ -1099,12 +1108,15 @@ func (p *parser) parseCreateTable() (*CreateTable, *parseError) { if err := p.expect("TABLE"); err != nil { return nil, err } + if p.eat("IF", "NOT", "EXISTS") { + ifNotExists = true + } tname, err := p.parseTableOrIndexOrColumnName() if err != nil { return nil, err } - ct := &CreateTable{Name: tname, Position: pos} + ct := &CreateTable{Name: tname, Position: pos, IfNotExists: ifNotExists} err = p.parseCommaList("(", ")", func(p *parser) *parseError { if p.sniffTableConstraint() { tc, err := p.parseTableConstraint() @@ -1204,7 +1216,7 @@ func (p *parser) parseCreateIndex() (*CreateIndex, *parseError) { debugf("parseCreateIndex: %v", p) /* - CREATE [UNIQUE] [NULL_FILTERED] INDEX index_name + CREATE [UNIQUE] [NULL_FILTERED] INDEX [IF NOT EXISTS] index_name ON table_name ( key_part [, ...] ) [ storing_clause ] [ , interleave_clause ] index_name: @@ -1217,7 +1229,7 @@ func (p *parser) parseCreateIndex() (*CreateIndex, *parseError) { INTERLEAVE IN table_name */ - var unique, nullFiltered bool + var unique, nullFiltered, ifNotExists bool if err := p.expect("CREATE"); err != nil { return nil, err @@ -1232,6 +1244,9 @@ func (p *parser) parseCreateIndex() (*CreateIndex, *parseError) { if err := p.expect("INDEX"); err != nil { return nil, err } + if p.eat("IF", "NOT", "EXISTS") { + ifNotExists = true + } iname, err := p.parseTableOrIndexOrColumnName() if err != nil { return nil, err @@ -1249,6 +1264,7 @@ func (p *parser) parseCreateIndex() (*CreateIndex, *parseError) { Unique: unique, NullFiltered: nullFiltered, + IfNotExists: ifNotExists, Position: pos, } @@ -1527,7 +1543,7 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { ALTER TABLE table_name { table_alteration | table_column_alteration } table_alteration: - { ADD [ COLUMN ] column_def + { ADD [ COLUMN ] [ IF NOT EXISTS ] column_def | DROP [ COLUMN ] column_name | ADD table_constraint | DROP CONSTRAINT constraint_name @@ -1580,11 +1596,15 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { if err := p.expect("COLUMN"); err != nil { return nil, err } + var ifNotExists bool + if p.eat("IF", "NOT", "EXISTS") { + ifNotExists = true + } cd, err := p.parseColumnDef() if err != nil { return nil, err } - a.Alteration = AddColumn{Def: cd} + a.Alteration = AddColumn{Def: cd, IfNotExists: ifNotExists} return a, nil case tok.caseEqual("DROP"): if p.eat("CONSTRAINT") { diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go index bf71f8dcf221..848960fa9576 100644 --- a/spanner/spansql/parser_test.go +++ b/spanner/spansql/parser_test.go @@ -1483,6 +1483,78 @@ func TestParseDDL(t *testing.T) { }, }, }, + { + `CREATE TABLE IF NOT EXISTS tname (id INT64, name STRING(64)) PRIMARY KEY (id)`, + &DDL{ + Filename: "filename", + List: []DDLStmt{ + &CreateTable{ + Name: "tname", + IfNotExists: true, + Columns: []ColumnDef{ + {Name: "id", Type: Type{Base: Int64}, Position: line(1)}, + {Name: "name", Type: Type{Base: String, Len: 64}, Position: line(1)}, + }, + PrimaryKey: []KeyPart{ + {Column: "id"}, + }, + Position: line(1), + }, + }, + }, + }, + { + `CREATE INDEX IF NOT EXISTS iname ON tname (cname)`, + &DDL{ + Filename: "filename", + List: []DDLStmt{ + &CreateIndex{ + Name: "iname", + IfNotExists: true, + Table: "tname", + Columns: []KeyPart{ + {Column: "cname"}, + }, + Position: line(1), + }, + }, + }, + }, + { + `ALTER TABLE tname ADD COLUMN IF NOT EXISTS cname STRING(64)`, + &DDL{ + Filename: "filename", + List: []DDLStmt{ + &AlterTable{ + Name: "tname", + Alteration: AddColumn{ + IfNotExists: true, + Def: ColumnDef{Name: "cname", Type: Type{Base: String, Len: 64}, Position: line(1)}, + }, + Position: line(1), + }, + }, + }, + }, + { + `DROP TABLE IF EXISTS tname; + DROP INDEX IF EXISTS iname;`, + &DDL{ + Filename: "filename", + List: []DDLStmt{ + &DropTable{ + Name: "tname", + IfExists: true, + Position: line(1), + }, + &DropIndex{ + Name: "iname", + IfExists: true, + Position: line(2), + }, + }, + }, + }, } for _, test := range tests { got, err := ParseDDL("filename", test.in) diff --git a/spanner/spansql/sql.go b/spanner/spansql/sql.go index 70c69638da8e..d59c7799ec0b 100644 --- a/spanner/spansql/sql.go +++ b/spanner/spansql/sql.go @@ -38,7 +38,11 @@ func buildSQL(x interface{ addSQL(*strings.Builder) }) string { } func (ct CreateTable) SQL() string { - str := "CREATE TABLE " + ct.Name.SQL() + " (\n" + str := "CREATE TABLE " + if ct.IfNotExists { + str += "IF NOT EXISTS " + } + str += ct.Name.SQL() + " (\n" for _, c := range ct.Columns { str += " " + c.SQL() + ",\n" } @@ -70,7 +74,11 @@ func (ci CreateIndex) SQL() string { if ci.NullFiltered { str += " NULL_FILTERED" } - str += " INDEX " + ci.Name.SQL() + " ON " + ci.Table.SQL() + "(" + str += " INDEX " + if ci.IfNotExists { + str += "IF NOT EXISTS " + } + str += ci.Name.SQL() + " ON " + ci.Table.SQL() + "(" for i, c := range ci.Columns { if i > 0 { str += ", " @@ -138,11 +146,21 @@ func (w WatchDef) SQL() string { } func (dt DropTable) SQL() string { - return "DROP TABLE " + dt.Name.SQL() + str := "DROP TABLE " + if dt.IfExists { + str += "IF EXISTS " + } + str += dt.Name.SQL() + return str } func (di DropIndex) SQL() string { - return "DROP INDEX " + di.Name.SQL() + str := "DROP INDEX " + if di.IfExists { + str += "IF EXISTS " + } + str += di.Name.SQL() + return str } func (dv DropView) SQL() string { @@ -258,7 +276,12 @@ func (at AlterTable) SQL() string { } func (ac AddColumn) SQL() string { - return "ADD COLUMN " + ac.Def.SQL() + str := "ADD COLUMN " + if ac.IfNotExists { + str += "IF NOT EXISTS " + } + str += ac.Def.SQL() + return str } func (dc DropColumn) SQL() string { diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go index ca4045c7c84d..82b1f1885893 100644 --- a/spanner/spansql/sql_test.go +++ b/spanner/spansql/sql_test.go @@ -655,6 +655,68 @@ func TestSQL(t *testing.T) { "ALTER INDEX iname DROP STORED COLUMN cname", reparseDDL, }, + { + &CreateTable{ + Name: "tname", + IfNotExists: true, + Columns: []ColumnDef{ + {Name: "id", Type: Type{Base: Int64}, Position: line(2)}, + {Name: "name", Type: Type{Base: String, Len: 64}, Position: line(3)}, + }, + PrimaryKey: []KeyPart{ + {Column: "id"}, + }, + Position: line(1), + }, + `CREATE TABLE IF NOT EXISTS tname ( + id INT64, + name STRING(64), +) PRIMARY KEY(id)`, + reparseDDL, + }, + { + &CreateIndex{ + Name: "Ia", + Table: "Ta", + Columns: []KeyPart{ + {Column: "Ca"}, + }, + IfNotExists: true, + Position: line(1), + }, + "CREATE INDEX IF NOT EXISTS Ia ON Ta(Ca)", + reparseDDL, + }, + { + &AlterTable{ + Name: "tname", + Alteration: AddColumn{ + IfNotExists: true, + Def: ColumnDef{Name: "cname", Type: Type{Base: String, Len: 64}, Position: line(1)}, + }, + Position: line(1), + }, + "ALTER TABLE tname ADD COLUMN IF NOT EXISTS cname STRING(64)", + reparseDDL, + }, + { + &DropTable{ + Name: "tname", + IfExists: true, + Position: line(1), + }, + "DROP TABLE IF EXISTS tname", + reparseDDL, + }, + { + &DropIndex{ + Name: "iname", + IfExists: true, + Position: line(1), + }, + "DROP INDEX IF EXISTS iname", + reparseDDL, + }, { &Insert{ Table: "Singers", diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go index 7a0dbfaeae50..571a76bc9d5d 100644 --- a/spanner/spansql/types.go +++ b/spanner/spansql/types.go @@ -34,6 +34,7 @@ import ( // https://cloud.google.com/spanner/docs/data-definition-language#create_table type CreateTable struct { Name ID + IfNotExists bool Columns []ColumnDef Constraints []TableConstraint PrimaryKey []KeyPart @@ -106,6 +107,7 @@ type CreateIndex struct { Unique bool NullFiltered bool + IfNotExists bool Storing []ID Interleave ID @@ -149,7 +151,8 @@ func (cr *CreateRole) clearOffset() { cr.Position.Offset = 0 } // DropTable represents a DROP TABLE statement. // https://cloud.google.com/spanner/docs/data-definition-language#drop_table type DropTable struct { - Name ID + Name ID + IfExists bool Position Position // position of the "DROP" token } @@ -162,7 +165,8 @@ func (dt *DropTable) clearOffset() { dt.Position.Offset = 0 } // DropIndex represents a DROP INDEX statement. // https://cloud.google.com/spanner/docs/data-definition-language#drop-index type DropIndex struct { - Name ID + Name ID + IfExists bool Position Position // position of the "DROP" token } @@ -284,7 +288,10 @@ func (ReplaceRowDeletionPolicy) isTableAlteration() {} func (DropRowDeletionPolicy) isTableAlteration() {} type ( - AddColumn struct{ Def ColumnDef } + AddColumn struct { + IfNotExists bool + Def ColumnDef + } DropColumn struct{ Name ID } AddConstraint struct{ Constraint TableConstraint } DropConstraint struct{ Name ID }