Skip to content

Commit 0bf576d

Browse files
authored
Allow specifying the Spanner DB default timezone as part of schema migration (GoogleCloudPlatform#1229)
* Add target profile flag to specify the default timezone of the Spanner DB * Allow specifying the default timezone via the web UI * Include setting of database options in DDL generation * Pass through DB options (currently only default timezone) when migrating schema * Ensure default time zone DDL command is included in output files and add default time zone to integration tests * Fix test issues from merge of master branch
1 parent 166a4a9 commit 0bf576d

30 files changed

+362
-42
lines changed

accessors/spanner/spanner_accessor.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,9 @@ func (sp *SpannerAccessorImpl) CreateDatabase(ctx context.Context, dbURI string,
306306
req.DatabaseDialect = adminpb.DatabaseDialect_POSTGRESQL
307307
} else {
308308
if migrationType == constants.DATAFLOW_MIGRATION {
309-
req.ExtraStatements = ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences)
309+
req.ExtraStatements = ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences, conv.DatabaseOptions)
310310
} else {
311-
req.ExtraStatements = ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: false, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences)
311+
req.ExtraStatements = ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: false, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences, conv.DatabaseOptions)
312312
}
313313

314314
}
@@ -350,7 +350,7 @@ func (sp *SpannerAccessorImpl) UpdateDatabase(ctx context.Context, dbURI string,
350350
// Spanner DDL doesn't accept them), and protects table and col names
351351
// using backticks (to avoid any issues with Spanner reserved words).
352352
// Foreign Keys are set to false since we create them post data migration.
353-
schema := ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: false, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences)
353+
schema := ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: false, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences, conv.DatabaseOptions)
354354
if len(schema) == 0 {
355355
return nil
356356
}
@@ -378,6 +378,10 @@ func (sp *SpannerAccessorImpl) CreateOrUpdateDatabase(ctx context.Context, dbURI
378378
if err != nil {
379379
return err
380380
}
381+
if conv.DatabaseOptions.DefaultTimezone != "" && len(tablesExistingOnSpanner) > 0 {
382+
logger.Log.Warn("Spanner database already contains tables, can not set default time zone of '" + conv.DatabaseOptions.DefaultTimezone + "'. See https://docs.cloud.google.com/spanner/docs/set-default-time-zone#limitations.")
383+
conv.DatabaseOptions.DefaultTimezone = ""
384+
}
381385
if dbExists {
382386
if conv.SpDialect != constants.DIALECT_POSTGRESQL && migrationType == constants.DATAFLOW_MIGRATION {
383387
return fmt.Errorf("spanner migration tool does not support minimal downtime schema/schema-and-data migrations to an existing database")
@@ -477,7 +481,8 @@ func (sp *SpannerAccessorImpl) UpdateDDLForeignKeys(ctx context.Context, dbURI s
477481
// Spanner DDL doesn't accept them), and protects table and col names
478482
// using backticks (to avoid any issues with Spanner reserved words).
479483
// Sequences will not be passed as they have already been created.
480-
fkStmts := ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: false, ForeignKeys: true, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, make(map[string]ddl.Sequence))
484+
// Database options will not be passed since they have also already been set.
485+
fkStmts := ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: false, ForeignKeys: true, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, make(map[string]ddl.Sequence), ddl.DatabaseOptions{})
481486
if len(fkStmts) == 0 {
482487
return
483488
}

accessors/spanner/spanner_accessor_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,10 @@ func TestSpannerAccessorImpl_CreateDatabase(t *testing.T) {
598598
},
599599
},
600600
}
601+
conv.DatabaseOptions = ddl.DatabaseOptions{
602+
DbName: "database-id",
603+
DefaultTimezone: "America/New_York",
604+
}
601605
spA := SpannerAccessorImpl{AdminClient: &tc.acm}
602606
err := spA.CreateDatabase(ctx, dbURI, conv, "", tc.migrationType)
603607
assert.Equal(t, tc.expectError, err != nil, tc.name)
@@ -1010,6 +1014,10 @@ func TestSpannerAccessorImpl_CreateOrUpdateDatabase(t *testing.T) {
10101014
PrimaryKeys: []ddl.IndexKey{{ColId: "c1"}},
10111015
Id: "t1",
10121016
}
1017+
conv.DatabaseOptions = ddl.DatabaseOptions{
1018+
DbName: "database-id",
1019+
DefaultTimezone: "America/New_York",
1020+
}
10131021
spA := SpannerAccessorImpl{AdminClient: &tc.acm}
10141022
err := spA.CreateOrUpdateDatabase(ctx, dbURI, "", conv, tc.migrationType, tc.tablesExistingOnSpanner)
10151023
assert.Equal(t, tc.expectError, err != nil, tc.name)

assessment/assessment_engine.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ func performQueryAssessment(ctx context.Context, collectors assessmentCollectors
139139
ddl.GetDDL(
140140
ddl.Config{Comments: true, ProtectIds: false, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: "mysql"},
141141
conv.SpSchema,
142-
conv.SpSequences),
142+
conv.SpSequences,
143+
conv.DatabaseOptions),
143144
"\n")
144145

145146
for _, query := range queries {
@@ -216,7 +217,8 @@ func initializeCollectors(conv *internal.Conv, sourceProfile profiles.SourceProf
216217
ddl.GetDDL(
217218
ddl.Config{Comments: true, ProtectIds: false, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: "mysql"},
218219
conv.SpSchema,
219-
conv.SpSequences),
220+
conv.SpSequences,
221+
conv.DatabaseOptions),
220222
"\n")
221223

222224
logger.Log.Debug("mysqlSchema", zap.String("schema", mysqlSchema))

cmd/import_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,8 @@ func fetchDDLString(conv *internal.Conv) string {
844844
ddl.GetDDL(
845845
ddl.Config{Comments: false, ProtectIds: false, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: "mysql"},
846846
conv.SpSchema,
847-
conv.SpSequences), ";"), "\n", " ", -1)
847+
conv.SpSequences,
848+
conv.DatabaseOptions), ";"), "\n", " ", -1)
848849
}
849850

850851
func TestGetDialectWithDefaults(t *testing.T) {

cmd/utils.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/GoogleCloudPlatform/spanner-migration-tool/conversion"
3737
"github.com/GoogleCloudPlatform/spanner-migration-tool/internal"
3838
"github.com/GoogleCloudPlatform/spanner-migration-tool/profiles"
39+
"github.com/GoogleCloudPlatform/spanner-migration-tool/spanner/ddl"
3940
"github.com/GoogleCloudPlatform/spanner-migration-tool/spanner/writer"
4041
"github.com/GoogleCloudPlatform/spanner-migration-tool/webv2/helpers"
4142
"google.golang.org/grpc/metadata"
@@ -162,6 +163,11 @@ func MigrateDatabase(ctx context.Context, migrationProjectId string, targetProfi
162163
}
163164
defer adminClient.Close()
164165
defer client.Close()
166+
// Before this point, the actual DB name to use isn't finalized...
167+
conv.DatabaseOptions = ddl.DatabaseOptions{
168+
DbName: targetProfile.Conn.Sp.Dbname,
169+
DefaultTimezone: targetProfile.Conn.Sp.DefaultTimezone,
170+
}
165171
switch v := cmd.(type) {
166172
case *SchemaCmd:
167173
err = migrateSchema(ctx, targetProfile, sourceProfile, ioHelper, conv, dbURI, adminClient, client)

conversion/conversion.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/GoogleCloudPlatform/spanner-migration-tool/profiles"
4444
"github.com/GoogleCloudPlatform/spanner-migration-tool/sources/common"
4545
"github.com/GoogleCloudPlatform/spanner-migration-tool/sources/csv"
46+
"github.com/GoogleCloudPlatform/spanner-migration-tool/spanner/ddl"
4647
"github.com/GoogleCloudPlatform/spanner-migration-tool/spanner/writer"
4748
)
4849

@@ -76,15 +77,21 @@ type ConvImpl struct{}
7677
// SchemaConv performs the schema conversion
7778
// The SourceProfile param provides the connection details to use the go SQL library.
7879
func (ci *ConvImpl) SchemaConv(migrationProjectId string, sourceProfile profiles.SourceProfile, targetProfile profiles.TargetProfile, ioHelper *utils.IOStreams, schemaFromSource SchemaFromSourceInterface) (*internal.Conv, error) {
80+
var conv *internal.Conv
81+
var err error
7982
switch sourceProfile.Driver {
8083
case constants.POSTGRES, constants.MYSQL, constants.DYNAMODB, constants.SQLSERVER, constants.ORACLE, constants.CASSANDRA:
81-
return schemaFromSource.schemaFromDatabase(migrationProjectId, sourceProfile, targetProfile, &GetInfoImpl{}, &common.ProcessSchemaImpl{})
84+
conv, err = schemaFromSource.schemaFromDatabase(migrationProjectId, sourceProfile, targetProfile, &GetInfoImpl{}, &common.ProcessSchemaImpl{})
8285
case constants.PGDUMP, constants.MYSQLDUMP:
8386
expressionVerificationAccessor, _ := expressions_api.NewExpressionVerificationAccessorImpl(context.Background(), targetProfile.Conn.Sp.Project, targetProfile.Conn.Sp.Instance)
84-
return schemaFromSource.SchemaFromDump(targetProfile.Conn.Sp.Project, targetProfile.Conn.Sp.Instance, sourceProfile.Driver, targetProfile.Conn.Sp.Dialect, ioHelper, &ProcessDumpByDialectImpl{ExpressionVerificationAccessor: expressionVerificationAccessor})
87+
conv, err = schemaFromSource.SchemaFromDump(targetProfile.Conn.Sp.Project, targetProfile.Conn.Sp.Instance, sourceProfile.Driver, targetProfile.Conn.Sp.Dialect, ioHelper, &ProcessDumpByDialectImpl{ExpressionVerificationAccessor: expressionVerificationAccessor})
8588
default:
8689
return nil, fmt.Errorf("schema conversion for driver %s not supported", sourceProfile.Driver)
8790
}
91+
conv.DatabaseOptions = ddl.DatabaseOptions{
92+
DefaultTimezone: targetProfile.Conn.Sp.DefaultTimezone,
93+
}
94+
return conv, err
8895
}
8996

9097
// DataConv performs the data conversion

conversion/store_files.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func WriteSchemaFile(conv *internal.Conv, now time.Time, name string, out *os.Fi
4242
// and doesn't add backticks around table and column names. This file is
4343
// intended for explanatory and documentation purposes, and is not strictly
4444
// legal Cloud Spanner DDL (Cloud Spanner doesn't currently support comments).
45-
spDDL := ddl.GetDDL(ddl.Config{Comments: true, ProtectIds: false, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences)
45+
spDDL := ddl.GetDDL(ddl.Config{Comments: true, ProtectIds: false, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences, conv.DatabaseOptions)
4646
if len(spDDL) == 0 {
4747
spDDL = []string{"\n-- Schema is empty -- no tables found\n"}
4848
}
@@ -69,7 +69,7 @@ func WriteSchemaFile(conv *internal.Conv, now time.Time, name string, out *os.Fi
6969

7070
// We change 'Comments' to false and 'ProtectIds' to true below to write out a
7171
// schema file that is a legal Cloud Spanner DDL.
72-
spDDL = ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences)
72+
spDDL = ddl.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: true, SpDialect: conv.SpDialect, Source: driver}, conv.SpSchema, conv.SpSequences, conv.DatabaseOptions)
7373
if len(spDDL) == 0 {
7474
spDDL = []string{"\n-- Schema is empty -- no tables found\n"}
7575
}

docs/cli/flags.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,10 @@ appropriate instance using gcloud.
7171
* **`dialect`**: Specifies the dialect of Spanner database. By default, Spanner
7272
databases are created with GoogleSQL dialect. You can override the same by
7373
setting `dialect=PostgreSQL` in the `-target-profile`. Learn more about support
74-
for PostgreSQL dialect in Cloud Spanner [here](https://cloud.google.com/spanner/docs/postgresql-interface).
74+
for PostgreSQL dialect in Cloud Spanner [here](https://cloud.google.com/spanner/docs/postgresql-interface).
75+
76+
* **`defaultTimezone`**: Specifies the default timezone of the Spanner database. Must be a valid entry from the [IANA
77+
Time Zone Database](https://www.iana.org/time-zones). If not specified, the default timezone is not set when creating
78+
the Spanner database, and the database will therefore default to `America/Los_Angeles` (the default timezone for
79+
Spanner databases). Note, the default timezone can only be set on an empty Spanner database without any tables; a
80+
warning will be logged and this setting will be ignored if the database already includes tables.

internal/convert.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type Conv struct {
5858
SpProjectId string // Spanner Project Id
5959
SpInstanceId string // Spanner Instance Id
6060
Source string // Source Database type being migrated
61+
DatabaseOptions ddl.DatabaseOptions
6162
}
6263

6364
type InvalidCheckExp struct {
@@ -375,6 +376,7 @@ func MakeConv() *Conv {
375376
Rules: []Rule{},
376377
SpSequences: make(map[string]ddl.Sequence),
377378
SrcSequences: make(map[string]ddl.Sequence),
379+
DatabaseOptions: ddl.DatabaseOptions{},
378380
}
379381
}
380382

profiles/target_profile.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type TargetProfileConnectionSpanner struct {
4646
Instance string
4747
Dbname string
4848
Dialect string
49+
DefaultTimezone string
4950
}
5051

5152
type TargetProfileConnection struct {
@@ -146,6 +147,9 @@ func NewTargetProfile(s string) (TargetProfile, error) {
146147
if dialect, ok := params["dialect"]; ok {
147148
sp.Dialect = strings.ToLower(dialect)
148149
}
150+
if defaultTimezone, ok := params["defaultTimezone"]; ok {
151+
sp.DefaultTimezone = defaultTimezone
152+
}
149153
if sp.Dialect == "" {
150154
sp.Dialect = constants.DIALECT_GOOGLESQL
151155
} else if sp.Dialect != constants.DIALECT_POSTGRESQL && sp.Dialect != constants.DIALECT_GOOGLESQL {
@@ -157,6 +161,13 @@ func NewTargetProfile(s string) (TargetProfile, error) {
157161
return TargetProfile{}, fmt.Errorf("found empty string for instance. please specify instance (spanner instance) in the target-profile")
158162
}
159163

164+
if sp.DefaultTimezone != "" {
165+
_, err := time.LoadLocation(sp.DefaultTimezone)
166+
if err != nil {
167+
return TargetProfile{}, err
168+
}
169+
}
170+
160171
conn := TargetProfileConnection{Ty: TargetProfileConnectionTypeSpanner, Sp: sp}
161172
return TargetProfile{Ty: TargetProfileTypeConnection, Conn: conn}, nil
162173
}

0 commit comments

Comments
 (0)