Skip to content

Commit 1dd00ff

Browse files
Allow migration files to specify a versionSchema field (#884)
#852 removed support for specifying a migration name via the `name` field in a migration file. While having an unambiguous source of truth for the migration name (the filename) is good, it is still often desirable to decouple the filename from the name of the version schema that the migration will create when applied because filenames: * can be longer than the limit Postgres imposes on schema names * can contain characters that are not legal in Postgres schema names * must be named so that migration files are ordered lexicographically on disk This PR adds support for a new `version_schema` field in migration files that allows the migration author to specify the name of the version schema that the migration will create. ## Example ```yaml # migration files can now specify the version schema # name to be created by the migration version_schema: my_version_schema operations: - create_table: name: items columns: - name: id type: serial pk: true - name: name type: varchar(255) ``` Running this migration: ``` $ pgroll start migrations/01_create_table.yaml --complete ``` Creates this version schema: ``` +-----------------------+-------------------+ | Name | Owner | |-----------------------+-------------------| ... | public_my_version_schema | postgres | +-----------------------+-------------------+ ``` Had the `version_schema` field not been specified in the migration file the version schema would have been: ``` +-----------------------+-------------------+ | Name | Owner | |-----------------------+-------------------| ... | public_01_create_table | postgres | +-----------------------+-------------------+ ``` --- This PR is part of a stack: * #884 (this PR) * #886 * #887 Part of #882
1 parent 146d740 commit 1dd00ff

File tree

8 files changed

+265
-44
lines changed

8 files changed

+265
-44
lines changed

cmd/migrate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func migrateCmd() *cobra.Command {
3636
}
3737
defer m.Close()
3838

39-
latestVersion, err := m.State().LatestVersion(ctx, m.Schema())
39+
latestMigration, err := m.State().LatestMigration(ctx, m.Schema())
4040
if err != nil {
4141
return fmt.Errorf("unable to determine latest version: %w", err)
4242
}
@@ -46,7 +46,7 @@ func migrateCmd() *cobra.Command {
4646
return fmt.Errorf("unable to determine active migration period: %w", err)
4747
}
4848
if active {
49-
fmt.Printf("migration %q is active\n", *latestVersion)
49+
fmt.Printf("migration %q is active\n", *latestMigration)
5050
return nil
5151
}
5252

cmd/start.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ func runMigration(ctx context.Context, m *roll.Roll, migration *migrations.Migra
105105
}
106106
}
107107

108-
version := migration.Name
109-
viewName := roll.VersionedSchemaName(flags.Schema(), version)
108+
viewName := roll.VersionedSchemaName(flags.Schema(), migration.VersionSchemaName())
110109
msg := fmt.Sprintf("New version of the schema available under the postgres %q schema", viewName)
111110
sp.Success(msg)
112111

pkg/migrations/migrations.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,26 @@ type RequiresSchemaRefreshOperation interface {
6868
type (
6969
Operations []Operation
7070
Migration struct {
71-
Name string `json:"-"`
72-
Operations Operations `json:"operations"`
71+
Name string `json:"-"`
72+
VersionSchema string `json:"version_schema,omitempty"`
73+
Operations Operations `json:"operations"`
7374
}
7475
RawMigration struct {
75-
Name string `json:"-"`
76-
Operations json.RawMessage `json:"operations"`
76+
Name string `json:"-"`
77+
VersionSchema string `json:"version_schema,omitempty"`
78+
Operations json.RawMessage `json:"operations"`
7779
}
7880
)
7981

82+
// VersionSchemaName returns the version schema name for the migration.
83+
// It defaults to the migration name if no version schema is set.
84+
func (m *Migration) VersionSchemaName() string {
85+
if m.VersionSchema != "" {
86+
return m.VersionSchema
87+
}
88+
return m.Name
89+
}
90+
8091
// Validate will check that the migration can be applied to the given schema
8192
// returns a descriptive error if the migration is invalid
8293
func (m *Migration) Validate(ctx context.Context, s *schema.Schema) error {

pkg/migrations/op_common.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ func ParseMigration(raw *RawMigration) (*Migration, error) {
132132
}
133133

134134
return &Migration{
135-
Name: raw.Name,
136-
Operations: ops,
135+
Name: raw.Name,
136+
VersionSchema: raw.VersionSchema,
137+
Operations: ops,
137138
}, nil
138139
}
139140

pkg/roll/execute.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,15 @@ func (m *Roll) StartDDLOperations(ctx context.Context, migration *migrations.Mig
136136
}
137137

138138
// create views for the new version
139-
if err := m.ensureViews(ctx, newSchema, migration.Name); err != nil {
139+
if err := m.ensureViews(ctx, newSchema, migration); err != nil {
140140
return nil, err
141141
}
142142

143143
return tablesToBackfill, nil
144144
}
145145

146-
func (m *Roll) ensureViews(ctx context.Context, schema *schema.Schema, version string) error {
147-
// create schema for the new version
148-
versionSchema := VersionedSchemaName(m.schema, version)
146+
func (m *Roll) ensureViews(ctx context.Context, schema *schema.Schema, mig *migrations.Migration) error {
147+
versionSchema := VersionedSchemaName(m.schema, mig.VersionSchemaName())
149148
_, err := m.pgConn.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", pq.QuoteIdentifier(versionSchema)))
150149
if err != nil {
151150
return err
@@ -156,13 +155,13 @@ func (m *Roll) ensureViews(ctx context.Context, schema *schema.Schema, version s
156155
if table.Deleted {
157156
continue
158157
}
159-
err = m.ensureView(ctx, version, name, table)
158+
err = m.ensureView(ctx, mig.VersionSchemaName(), name, table)
160159
if err != nil {
161160
return fmt.Errorf("unable to create view: %w", err)
162161
}
163162
}
164163

165-
m.logger.LogSchemaCreation(version, versionSchema)
164+
m.logger.LogSchemaCreation(mig.VersionSchemaName(), versionSchema)
166165

167166
return nil
168167
}
@@ -237,7 +236,7 @@ func (m *Roll) Complete(ctx context.Context) error {
237236
return fmt.Errorf("unable to read schema: %w", err)
238237
}
239238

240-
err = m.ensureViews(ctx, currentSchema, migration.Name)
239+
err = m.ensureViews(ctx, currentSchema, migration)
241240
if err != nil {
242241
return err
243242
}
@@ -266,7 +265,7 @@ func (m *Roll) Rollback(ctx context.Context) error {
266265

267266
if !m.disableVersionSchemas {
268267
// delete the schema and view for the new version
269-
versionSchema := VersionedSchemaName(m.schema, migration.Name)
268+
versionSchema := VersionedSchemaName(m.schema, migration.VersionSchemaName())
270269
_, err = m.pgConn.ExecContext(ctx, fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", pq.QuoteIdentifier(versionSchema)))
271270
if err != nil {
272271
return err
@@ -276,15 +275,15 @@ func (m *Roll) Rollback(ctx context.Context) error {
276275
}
277276

278277
// get the name of the previous version of the schema
279-
previousVersion, err := m.state.PreviousVersion(ctx, m.schema, true)
278+
previousMigration, err := m.state.PreviousMigration(ctx, m.schema, true)
280279
if err != nil {
281280
return fmt.Errorf("unable to get name of previous version: %w", err)
282281
}
283282

284283
// get the schema after the previous migration was applied
285284
schema := schema.New()
286-
if previousVersion != nil {
287-
schema, err = m.state.SchemaAfterMigration(ctx, m.schema, *previousVersion)
285+
if previousMigration != nil {
286+
schema, err = m.state.SchemaAfterMigration(ctx, m.schema, *previousMigration)
288287
if err != nil {
289288
return fmt.Errorf("unable to read schema: %w", err)
290289
}

pkg/roll/execute_test.go

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,49 @@ func TestPreviousVersionIsDroppedAfterMigrationCompletion(t *testing.T) {
155155
}
156156
})
157157
})
158+
159+
t.Run("when the previous version sets a non-default version schema name", func(t *testing.T) {
160+
testutils.WithMigratorAndConnectionToContainer(t, func(m *roll.Roll, db *sql.DB) {
161+
ctx := context.Background()
162+
163+
migs := []migrations.Migration{
164+
{
165+
Name: "01_create_table",
166+
VersionSchema: "01_foo",
167+
Operations: migrations.Operations{
168+
&migrations.OpCreateTable{
169+
Name: "users",
170+
Columns: []migrations.Column{
171+
{Name: "id", Type: "serial", Pk: true},
172+
},
173+
},
174+
},
175+
},
176+
{
177+
Name: "02_create_another_table",
178+
Operations: migrations.Operations{
179+
&migrations.OpCreateTable{
180+
Name: "items",
181+
Columns: []migrations.Column{
182+
{Name: "id", Type: "serial", Pk: true},
183+
},
184+
},
185+
},
186+
},
187+
}
188+
189+
// Start and complete both migrations
190+
for _, mig := range migs {
191+
err := m.Start(ctx, &mig, backfill.NewConfig())
192+
require.NoError(t, err)
193+
err = m.Complete(ctx)
194+
require.NoError(t, err)
195+
}
196+
197+
// Ensure that the schema for the first migration has been dropped
198+
require.False(t, schemaExists(t, db, roll.VersionedSchemaName("public", "01_foo")))
199+
})
200+
})
158201
}
159202

160203
func TestNoVersionSchemaForRawSQLMigrationsOptionIsRespected(t *testing.T) {
@@ -215,23 +258,48 @@ func TestNoVersionSchemaForRawSQLMigrationsOptionIsRespected(t *testing.T) {
215258
func TestSchemaIsDroppedAfterMigrationRollback(t *testing.T) {
216259
t.Parallel()
217260

218-
testutils.WithMigratorAndConnectionToContainer(t, func(mig *roll.Roll, db *sql.DB) {
219-
ctx := context.Background()
220-
version := "1_create_table"
261+
t.Run("when the migration does not set an explicit version schema name ", func(t *testing.T) {
262+
testutils.WithMigratorAndConnectionToContainer(t, func(mig *roll.Roll, db *sql.DB) {
263+
ctx := context.Background()
264+
version := "1_create_table"
221265

222-
if err := mig.Start(ctx, &migrations.Migration{Name: version, Operations: migrations.Operations{createTableOp("table1")}}, backfill.NewConfig()); err != nil {
223-
t.Fatalf("Failed to start migration: %v", err)
224-
}
225-
if err := mig.Rollback(ctx); err != nil {
226-
t.Fatalf("Failed to rollback migration: %v", err)
227-
}
266+
if err := mig.Start(ctx, &migrations.Migration{
267+
Name: version,
268+
Operations: migrations.Operations{createTableOp("table1")},
269+
}, backfill.NewConfig()); err != nil {
270+
t.Fatalf("Failed to start migration: %v", err)
271+
}
272+
if err := mig.Rollback(ctx); err != nil {
273+
t.Fatalf("Failed to rollback migration: %v", err)
274+
}
228275

229-
//
230-
// Check that the schema has been dropped
231-
//
232-
if schemaExists(t, db, roll.VersionedSchemaName(cSchema, version)) {
233-
t.Errorf("Expected schema %q to not exist", version)
234-
}
276+
// Check that the schema has been dropped
277+
if schemaExists(t, db, roll.VersionedSchemaName(cSchema, version)) {
278+
t.Errorf("Expected schema %q to not exist", version)
279+
}
280+
})
281+
})
282+
283+
t.Run("when the migration does set an explicit version schema name ", func(t *testing.T) {
284+
testutils.WithMigratorAndConnectionToContainer(t, func(mig *roll.Roll, db *sql.DB) {
285+
ctx := context.Background()
286+
287+
if err := mig.Start(ctx, &migrations.Migration{
288+
Name: "1_create_table",
289+
VersionSchema: "1_foo",
290+
Operations: migrations.Operations{createTableOp("table1")},
291+
}, backfill.NewConfig()); err != nil {
292+
t.Fatalf("Failed to start migration: %v", err)
293+
}
294+
if err := mig.Rollback(ctx); err != nil {
295+
t.Fatalf("Failed to rollback migration: %v", err)
296+
}
297+
298+
// Check that the schema has been dropped
299+
if schemaExists(t, db, roll.VersionedSchemaName(cSchema, "1_foo")) {
300+
t.Errorf("Expected schema %q to not exist", "1_foo")
301+
}
302+
})
235303
})
236304
}
237305

@@ -710,6 +778,43 @@ func TestRollSchemaMethodReturnsCorrectSchema(t *testing.T) {
710778
})
711779
}
712780

781+
func TestLatestVersionAndLatestMigrationMethodsRespectVersionSchemaAndName(t *testing.T) {
782+
t.Parallel()
783+
784+
testutils.WithMigratorAndConnectionToContainer(t, func(r *roll.Roll, db *sql.DB) {
785+
ctx := context.Background()
786+
787+
// Create a migration with an explicit version schema
788+
mig := &migrations.Migration{
789+
Name: "01_create_table",
790+
VersionSchema: "01_foo",
791+
Operations: migrations.Operations{createTableOp("table1")},
792+
}
793+
794+
// Start and complete a migration
795+
err := r.Start(ctx, mig, backfill.NewConfig())
796+
require.NoError(t, err)
797+
err = r.Complete(ctx)
798+
require.NoError(t, err)
799+
800+
// Get the latest version
801+
latestVersion, err := r.State().LatestVersion(ctx, "public")
802+
require.NoError(t, err)
803+
804+
// Get the latest migration name
805+
latestMigration, err := r.State().LatestMigration(ctx, "public")
806+
require.NoError(t, err)
807+
808+
// Assert that the latest version is correct
809+
require.NotNil(t, latestVersion)
810+
require.Equal(t, "01_foo", *latestVersion)
811+
812+
// Assert that the latest migration name is correct
813+
require.NotNil(t, latestMigration)
814+
require.Equal(t, "01_create_table", *latestMigration)
815+
})
816+
}
817+
713818
func TestWithSearchPathOptionIsRespected(t *testing.T) {
714819
t.Parallel()
715820

0 commit comments

Comments
 (0)