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

Fix previous version detection in the presence of inferred DDL migrations #197

Merged
merged 4 commits into from
Nov 3, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 81 additions & 29 deletions pkg/roll/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,43 +62,95 @@ func TestSchemaIsCreatedfterMigrationStart(t *testing.T) {
func TestPreviousVersionIsDroppedAfterMigrationCompletion(t *testing.T) {
t.Parallel()

withMigratorAndConnectionToContainer(t, func(mig *roll.Roll, db *sql.DB) {
ctx := context.Background()
const (
firstVersion = "1_create_table"
secondVersion = "2_create_table"
)
t.Run("when the previous version is a pgroll migration", func(t *testing.T) {
withMigratorAndConnectionToContainer(t, func(mig *roll.Roll, db *sql.DB) {
ctx := context.Background()
const (
firstVersion = "1_create_table"
secondVersion = "2_create_table"
)

if err := mig.Start(ctx, &migrations.Migration{Name: firstVersion, Operations: migrations.Operations{createTableOp("table1")}}); err != nil {
t.Fatalf("Failed to start first migration: %v", err)
}
if err := mig.Complete(ctx); err != nil {
t.Fatalf("Failed to complete first migration: %v", err)
}
if err := mig.Start(ctx, &migrations.Migration{Name: secondVersion, Operations: migrations.Operations{createTableOp("table2")}}); err != nil {
t.Fatalf("Failed to start second migration: %v", err)
}
if err := mig.Complete(ctx); err != nil {
t.Fatalf("Failed to complete second migration: %v", err)
}

if err := mig.Start(ctx, &migrations.Migration{Name: firstVersion, Operations: migrations.Operations{createTableOp("table1")}}); err != nil {
t.Fatalf("Failed to start first migration: %v", err)
}
if err := mig.Complete(ctx); err != nil {
t.Fatalf("Failed to complete first migration: %v", err)
}
if err := mig.Start(ctx, &migrations.Migration{Name: secondVersion, Operations: migrations.Operations{createTableOp("table2")}}); err != nil {
t.Fatalf("Failed to start second migration: %v", err)
}
if err := mig.Complete(ctx); err != nil {
t.Fatalf("Failed to complete second migration: %v", err)
}
//
// Check that the schema for the first version has been dropped
//
var exists bool
err := db.QueryRow(`
SELECT EXISTS(
SELECT 1
FROM pg_catalog.pg_namespace
WHERE nspname = $1
)`, roll.VersionedSchemaName(schema, firstVersion)).Scan(&exists)
if err != nil {
t.Fatal(err)
}

//
// Check that the schema for the first version has been dropped
//
var exists bool
err := db.QueryRow(`
if exists {
t.Errorf("Expected schema %q to not exist", firstVersion)
}
})
})

t.Run("when the previous version is an inferred DDL migration", func(t *testing.T) {
withMigratorAndConnectionToContainer(t, func(mig *roll.Roll, db *sql.DB) {
ctx := context.Background()
const (
firstVersion = "1_create_table"
secondVersion = "2_create_table"
)

// Run the first pgroll migration
if err := mig.Start(ctx, &migrations.Migration{Name: firstVersion, Operations: migrations.Operations{createTableOp("table1")}}); err != nil {
t.Fatalf("Failed to start first migration: %v", err)
}
if err := mig.Complete(ctx); err != nil {
t.Fatalf("Failed to complete first migration: %v", err)
}

// Run a manual DDL migration
_, err := db.ExecContext(ctx, "CREATE TABLE foo (id integer)")
if err != nil {
t.Fatalf("Failed to create table: %v", err)
}

// Run the second pgroll migration
if err := mig.Start(ctx, &migrations.Migration{Name: secondVersion, Operations: migrations.Operations{createTableOp("table2")}}); err != nil {
t.Fatalf("Failed to start second migration: %v", err)
}
if err := mig.Complete(ctx); err != nil {
t.Fatalf("Failed to complete second migration: %v", err)
}

//
// Check that the schema for the first version has been dropped
//
var exists bool
err = db.QueryRow(`
SELECT EXISTS(
SELECT 1
FROM pg_catalog.pg_namespace
WHERE nspname = $1
)`, roll.VersionedSchemaName(schema, firstVersion)).Scan(&exists)
if err != nil {
t.Fatal(err)
}
if err != nil {
t.Fatal(err)
}

if exists {
t.Errorf("Expected schema %q to not exist", firstVersion)
}
if exists {
t.Errorf("Expected schema %q to not exist", firstVersion)
}
})
})
}

Expand Down
27 changes: 24 additions & 3 deletions pkg/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ CREATE UNIQUE INDEX IF NOT EXISTS only_first_migration_without_parent ON %[1]s.m
-- History is linear
CREATE UNIQUE INDEX IF NOT EXISTS history_is_linear ON %[1]s.migrations (schema, parent);

-- Add a column to tell whether the row represents an auto-detected DDL capture or a pgroll migration
ALTER TABLE %[1]s.migrations ADD COLUMN IF NOT EXISTS migration_type
andrew-farries marked this conversation as resolved.
Show resolved Hide resolved
VARCHAR(32)
andrew-farries marked this conversation as resolved.
Show resolved Hide resolved
DEFAULT 'pgroll'
CONSTRAINT migration_type_check CHECK (migration_type IN ('pgroll', 'inferred')
);

-- Helper functions

-- Are we in the middle of a migration?
Expand All @@ -65,7 +72,20 @@ STABLE;
-- Get the name of the previous version of the schema, or NULL if there is none.
CREATE OR REPLACE FUNCTION %[1]s.previous_version(schemaname NAME) RETURNS text
AS $$
SELECT parent FROM %[1]s.migrations WHERE name = (SELECT %[1]s.latest_version(schemaname)) AND schema=schemaname;
WITH RECURSIVE find_ancestor AS (
SELECT schema, name, parent, migration_type FROM pgroll.migrations
WHERE name = (SELECT %[1]s.latest_version(schemaname)) AND schema = schemaname

UNION ALL

SELECT m.schema, m.name, m.parent, m.migration_type FROM pgroll.migrations m
INNER JOIN find_ancestor fa ON fa.parent = m.name AND fa.schema = m.schema
WHERE m.migration_type = 'inferred'
)
SELECT a.parent
FROM find_ancestor AS a
JOIN pgroll.migrations AS b ON a.parent = b.name AND a.schema = b.schema
WHERE b.migration_type = 'pgroll';
$$
LANGUAGE SQL
STABLE;
Expand Down Expand Up @@ -192,14 +212,15 @@ BEGIN
END IF;

-- Someone did a schema change without pgroll, include it in the history
INSERT INTO %[1]s.migrations (schema, name, migration, resulting_schema, done, parent)
INSERT INTO %[1]s.migrations (schema, name, migration, resulting_schema, done, parent, migration_type)
VALUES (
schemaname,
pg_catalog.format('sql_%%s',pg_catalog.substr(pg_catalog.md5(pg_catalog.random()::text), 0, 15)),
pg_catalog.json_build_object('sql', pg_catalog.json_build_object('up', pg_catalog.current_query())),
%[1]s.read_schema(schemaname),
true,
%[1]s.latest_version(schemaname)
%[1]s.latest_version(schemaname),
'inferred'
);
END;
$$;
Expand Down
Loading