Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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
  • Loading branch information
olavloite committed Jul 12, 2021
1 parent 99d5728 commit 1458dc9
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 0 deletions.
138 changes: 138 additions & 0 deletions spanner/spansql/parser.go
Expand Up @@ -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")
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
30 changes: 30 additions & 0 deletions spanner/spansql/parser_test.go
Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions spanner/spansql/sql.go
Expand Up @@ -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()
}
Expand Down
26 changes: 26 additions & 0 deletions spanner/spansql/sql_test.go
Expand Up @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions spanner/spansql/types.go
Expand Up @@ -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 {
Expand Down

0 comments on commit 1458dc9

Please sign in to comment.