Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spanner/spansql): support ALTER DATABASE #4403

Merged
merged 6 commits into from Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()
hengfengli marked this conversation as resolved.
Show resolved Hide resolved
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:
hengfengli marked this conversation as resolved.
Show resolved Hide resolved
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 {
hengfengli marked this conversation as resolved.
Show resolved Hide resolved
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