From 1458dc9c21d98ffffb871943f178678cc3c21306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 12 Jul 2021 16:24:43 +0200 Subject: [PATCH] feat(spanner/spansql): support ALTER DATABASE (#4403) * feat(spansql): support ALTER DATABASE Adds support for ALTER DATABASE dbname SET OPTIONS (..) statements. Resolves #4390 * fix: variable names * fix: address review comments --- spanner/spansql/parser.go | 138 +++++++++++++++++++++++++++++++++ spanner/spansql/parser_test.go | 30 +++++++ spanner/spansql/sql.go | 46 +++++++++++ spanner/spansql/sql_test.go | 26 +++++++ spanner/spansql/types.go | 31 ++++++++ 5 files changed, 271 insertions(+) diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go index b4513ebcd58..afe7d210d6e 100644 --- a/spanner/spansql/parser.go +++ b/spanner/spansql/parser.go @@ -979,6 +979,9 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { } return &DropIndex{Name: name, Position: pos}, nil } + } else if p.sniff("ALTER", "DATABASE") { + a, err := p.parseAlterDatabase() + return a, err } return nil, p.errorf("unknown DDL statement") @@ -1286,6 +1289,55 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { } } +func (p *parser) parseAlterDatabase() (*AlterDatabase, *parseError) { + debugf("parseAlterDatabase: %v", p) + + /* + ALTER DATABASE database_id + action + + where database_id is: + {a—z}[{a—z|0—9|_|-}+]{a—z|0—9} + + and action is: + SET OPTIONS ( optimizer_version = { 1 ... 2 | null }, + version_retention_period = { 'duration' | null } ) + */ + + if err := p.expect("ALTER"); err != nil { + return nil, err + } + pos := p.Pos() + if err := p.expect("DATABASE"); err != nil { + return nil, err + } + // This is not 100% correct as database identifiers have slightly more + // restrictions than table names, but the restrictions are currently not + // applied in the spansql parser. + // TODO: Apply restrictions for all identifiers. + dbname, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + a := &AlterDatabase{Name: dbname, Position: pos} + + tok := p.next() + if tok.err != nil { + return nil, tok.err + } + switch { + default: + return nil, p.errorf("got %q, expected SET", tok.value) + case tok.caseEqual("SET"): + options, err := p.parseDatabaseOptions() + if err != nil { + return nil, err + } + a.Alteration = SetDatabaseOptions{Options: options} + return a, nil + } +} + func (p *parser) parseDMLStmt() (DMLStmt, *parseError) { debugf("parseDMLStmt: %v", p) @@ -1495,6 +1547,92 @@ func (p *parser) parseColumnOptions() (ColumnOptions, *parseError) { return co, nil } +func (p *parser) parseDatabaseOptions() (DatabaseOptions, *parseError) { + debugf("parseDatabaseOptions: %v", p) + /* + options_def: + OPTIONS (enable_key_visualizer = { true | null }, + optimizer_version = { 1 ... 2 | null }, + version_retention_period = { 'duration' | null }) + */ + + if err := p.expect("OPTIONS"); err != nil { + return DatabaseOptions{}, err + } + if err := p.expect("("); err != nil { + return DatabaseOptions{}, err + } + + // We ignore case for the key (because it is easier) but not the value. + var opts DatabaseOptions + for { + if p.eat("enable_key_visualizer", "=") { + tok := p.next() + if tok.err != nil { + return DatabaseOptions{}, tok.err + } + enableKeyVisualizer := new(bool) + switch tok.value { + case "true": + *enableKeyVisualizer = true + case "null": + *enableKeyVisualizer = false + default: + return DatabaseOptions{}, p.errorf("invalid enable_key_visualizer_value: %v", tok.value) + } + opts.EnableKeyVisualizer = enableKeyVisualizer + } else if p.eat("optimizer_version", "=") { + tok := p.next() + if tok.err != nil { + return DatabaseOptions{}, tok.err + } + optimizerVersion := new(int) + if tok.value == "null" { + *optimizerVersion = 0 + } else { + if tok.typ != int64Token { + return DatabaseOptions{}, p.errorf("invalid optimizer_version value: %v", tok.value) + } + version, err := strconv.Atoi(tok.value) + if err != nil { + return DatabaseOptions{}, p.errorf("invalid optimizer_version value: %v", tok.value) + } + optimizerVersion = &version + } + opts.OptimizerVersion = optimizerVersion + } else if p.eat("version_retention_period", "=") { + tok := p.next() + if tok.err != nil { + return DatabaseOptions{}, tok.err + } + retentionPeriod := new(string) + if tok.value == "null" { + *retentionPeriod = "" + } else { + if tok.typ != stringToken { + return DatabaseOptions{}, p.errorf("invalid version_retention_period: %v", tok.value) + } + retentionPeriod = &tok.string + } + opts.VersionRetentionPeriod = retentionPeriod + } else { + tok := p.next() + return DatabaseOptions{}, p.errorf("unknown database option: %v", tok.value) + } + if p.sniff(")") { + break + } + if !p.eat(",") { + return DatabaseOptions{}, p.errorf("missing ',' in options list") + } + } + if err := p.expect(")"); err != nil { + return DatabaseOptions{}, err + } + + return opts, nil +} + func (p *parser) parseKeyPartList() ([]KeyPart, *parseError) { var list []KeyPart err := p.parseCommaList("(", ")", func(p *parser) *parseError { diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go index a79e54828f6..fbc68c55970 100644 --- a/spanner/spansql/parser_test.go +++ b/spanner/spansql/parser_test.go @@ -611,6 +611,36 @@ func TestParseDDL(t *testing.T) { Position: line(1), }, }}}, + {`ALTER DATABASE dbname SET OPTIONS (optimizer_version=2, version_retention_period='7d', enable_key_visualizer=true)`, + &DDL{Filename: "filename", List: []DDLStmt{ + &AlterDatabase{ + Name: "dbname", + Alteration: SetDatabaseOptions{ + Options: DatabaseOptions{ + OptimizerVersion: func(i int) *int { return &i }(2), + VersionRetentionPeriod: func(s string) *string { return &s }("7d"), + EnableKeyVisualizer: func(b bool) *bool { return &b }(true), + }, + }, + Position: line(1), + }, + }, + }}, + {`ALTER DATABASE dbname SET OPTIONS (optimizer_version=null, version_retention_period=null, enable_key_visualizer=null)`, + &DDL{Filename: "filename", List: []DDLStmt{ + &AlterDatabase{ + Name: "dbname", + Alteration: SetDatabaseOptions{ + Options: DatabaseOptions{ + OptimizerVersion: func(i int) *int { return &i }(0), + VersionRetentionPeriod: func(s string) *string { return &s }(""), + EnableKeyVisualizer: func(b bool) *bool { return &b }(false), + }, + }, + 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 fee79916f50..19aac6dc480 100644 --- a/spanner/spansql/sql.go +++ b/spanner/spansql/sql.go @@ -155,6 +155,52 @@ func (co ColumnOptions) SQL() string { return str } +func (ad AlterDatabase) SQL() string { + return "ALTER DATABASE " + ad.Name.SQL() + " " + ad.Alteration.SQL() +} + +func (sdo SetDatabaseOptions) SQL() string { + return "SET " + sdo.Options.SQL() +} + +func (do DatabaseOptions) SQL() string { + str := "OPTIONS (" + hasOpt := false + if do.OptimizerVersion != nil { + hasOpt = true + if *do.OptimizerVersion == 0 { + str += "optimizer_version=null" + + } else { + str += fmt.Sprintf("optimizer_version=%v", *do.OptimizerVersion) + } + } + if do.VersionRetentionPeriod != nil { + hasOpt = true + if hasOpt { + str += ", " + } + if *do.VersionRetentionPeriod == "" { + str += "version_retention_period=null" + } else { + str += fmt.Sprintf("version_retention_period='%s'", *do.VersionRetentionPeriod) + } + } + if do.EnableKeyVisualizer != nil { + hasOpt = true + if hasOpt { + str += ", " + } + if *do.EnableKeyVisualizer { + str += "enable_key_visualizer=true" + } else { + str += "enable_key_visualizer=null" + } + } + str += ")" + return str +} + func (d *Delete) SQL() string { return "DELETE FROM " + d.Table.SQL() + " WHERE " + d.Where.SQL() } diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go index b4805489ed4..3632aba06b3 100644 --- a/spanner/spansql/sql_test.go +++ b/spanner/spansql/sql_test.go @@ -230,6 +230,32 @@ func TestSQL(t *testing.T) { "ALTER TABLE Ta ALTER COLUMN Ci SET OPTIONS (allow_commit_timestamp = null)", reparseDDL, }, + { + &AlterDatabase{ + Name: "dbname", + Alteration: SetDatabaseOptions{Options: DatabaseOptions{ + VersionRetentionPeriod: func(s string) *string { return &s }("7d"), + OptimizerVersion: func(i int) *int { return &i }(2), + EnableKeyVisualizer: func(b bool) *bool { return &b }(true), + }}, + Position: line(1), + }, + "ALTER DATABASE dbname SET OPTIONS (optimizer_version=2, version_retention_period='7d', enable_key_visualizer=true)", + reparseDDL, + }, + { + &AlterDatabase{ + Name: "dbname", + Alteration: SetDatabaseOptions{Options: DatabaseOptions{ + VersionRetentionPeriod: func(s string) *string { return &s }(""), + OptimizerVersion: func(i int) *int { return &i }(0), + EnableKeyVisualizer: func(b bool) *bool { return &b }(false), + }}, + Position: line(1), + }, + "ALTER DATABASE dbname SET OPTIONS (optimizer_version=null, version_retention_period=null, enable_key_visualizer=null)", + reparseDDL, + }, { &Delete{ Table: "Ta", diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go index 70ea1889ef4..34d0e0fee5f 100644 --- a/spanner/spansql/types.go +++ b/spanner/spansql/types.go @@ -208,6 +208,37 @@ const ( CascadeOnDelete ) +// AlterDatabase represents an ALTER DATABASE statement. +// https://cloud.google.com/spanner/docs/data-definition-language#alter-database +type AlterDatabase struct { + Name ID + Alteration DatabaseAlteration + + Position Position // position of the "ALTER" token +} + +func (ad *AlterDatabase) String() string { return fmt.Sprintf("%#v", ad) } +func (*AlterDatabase) isDDLStmt() {} +func (ad *AlterDatabase) Pos() Position { return ad.Position } +func (ad *AlterDatabase) clearOffset() { ad.Position.Offset = 0 } + +type DatabaseAlteration interface { + isDatabaseAlteration() + SQL() string +} + +type SetDatabaseOptions struct{ Options DatabaseOptions } + +func (SetDatabaseOptions) isDatabaseAlteration() {} + +// DatabaseOptions represents options on a database as part of a +// ALTER DATABASE statement. +type DatabaseOptions struct { + OptimizerVersion *int + VersionRetentionPeriod *string + EnableKeyVisualizer *bool +} + // Delete represents a DELETE statement. // https://cloud.google.com/spanner/docs/dml-syntax#delete-statement type Delete struct {