Skip to content

Commit 589bc33

Browse files
Make pgroll latest schema ignore migrations for which no version schema exists (#916)
Fix the problem described in #915 where `pgroll latest schema` will return version schema names for which no version schema exists in the database, such as for `inferred` migrations. --- 1. Apply a migration ``` pgroll start examples/01_create_tables.yaml --complete ``` 2. Create an inferred migration ``` CREATE TABLE foo(id int PRIMARY KEY) ``` 3. Run `pgroll latest schema` Before this PR the output would be incorrect: ``` public_sql_fca0901e ``` The most recent migration (inferred from the `CREATE TABLE`) did not create a version schema - the most recent schema version is still `public_01_create_tables`. This PR corrects the behaviour of `pgroll latest schema` to consider only migrations for which a version schema exists. So the output is: ``` public_01_create_tables ``` as expected. --- Fixes #915 Related to #872
1 parent 716f47f commit 589bc33

File tree

6 files changed

+509
-106
lines changed

6 files changed

+509
-106
lines changed

pkg/roll/execute.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func (m *Roll) Complete(ctx context.Context) error {
175175

176176
// Drop the old schema
177177
if !m.disableVersionSchemas {
178-
prevVersion, err := m.state.PreviousVersion(ctx, m.schema, false)
178+
prevVersion, err := m.state.PreviousVersion(ctx, m.schema)
179179
if err != nil {
180180
return fmt.Errorf("unable to get name of previous version: %w", err)
181181
}

pkg/roll/latest_test.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,37 @@ func TestLatestVersionRemote(t *testing.T) {
7272
latestVersion, err := m.LatestVersionRemote(ctx)
7373
require.NoError(t, err)
7474

75-
// Assert latest migration name
75+
// Assert latest migration version schema name
76+
assert.Equal(t, "01_foo", latestVersion)
77+
})
78+
})
79+
80+
t.Run("inferred migrations without a version schema are ignored", func(t *testing.T) {
81+
testutils.WithMigratorAndConnectionToContainer(t, func(m *roll.Roll, db *sql.DB) {
82+
ctx := context.Background()
83+
84+
// Start and complete a migration
85+
err := m.Start(ctx, &migrations.Migration{
86+
Name: "01_first_migration",
87+
VersionSchema: "01_foo",
88+
Operations: migrations.Operations{
89+
&migrations.OpRawSQL{Up: "SELECT 1"},
90+
},
91+
}, backfill.NewConfig())
92+
require.NoError(t, err)
93+
err = m.Complete(ctx)
94+
require.NoError(t, err)
95+
96+
// Run some DDL to generate an inferred migration
97+
_, err = db.ExecContext(ctx, "CREATE TABLE apples(id int)")
98+
require.NoError(t, err)
99+
100+
// Get the latest version in the target schema
101+
latestVersion, err := m.LatestVersionRemote(ctx)
102+
require.NoError(t, err)
103+
104+
// Assert latest migration version schema name; the inferred migration
105+
// name is ignored
76106
assert.Equal(t, "01_foo", latestVersion)
77107
})
78108
})

pkg/state/init.sql

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -68,31 +68,7 @@ $$
6868
LANGUAGE SQL
6969
STABLE;
7070

71-
-- Get the latest version schema name
72-
CREATE OR REPLACE FUNCTION placeholder.latest_version (schemaname name)
73-
RETURNS text
74-
SECURITY DEFINER
75-
SET search_path = placeholder, pg_catalog, pg_temp
76-
AS $$
77-
SELECT
78-
COALESCE(p.migration ->> 'version_schema', p.name)
79-
FROM
80-
placeholder.migrations p
81-
WHERE
82-
NOT EXISTS (
83-
SELECT
84-
1
85-
FROM
86-
placeholder.migrations c
87-
WHERE
88-
SCHEMA = schemaname
89-
AND c.parent = p.name)
90-
AND SCHEMA = schemaname
91-
$$
92-
LANGUAGE SQL
93-
STABLE;
94-
95-
-- Get the name of the latest version schema, or NULL if there is none.
71+
-- Get the name of the latest migration, or NULL if there is none.
9672
-- This will be the same as the version-schema name of the migration in most
9773
-- cases, unless the migration sets its `versionSchema` field.
9874
CREATE OR REPLACE FUNCTION placeholder.latest_migration (schemaname name)
@@ -118,16 +94,13 @@ $$
11894
LANGUAGE SQL
11995
STABLE;
12096

121-
-- Get the previous version schema name, or NULL if there is none.
122-
-- This ignores previous versions for which no version schema exists, such as
123-
-- versions corresponding to inferred migrations.
124-
CREATE OR REPLACE FUNCTION placeholder.previous_version (schemaname name, includeInferred boolean)
97+
-- Get the name of the previous migration, or NULL if there is none.
98+
CREATE OR REPLACE FUNCTION placeholder.previous_migration (schemaname name)
12599
RETURNS text
126100
AS $$
127101
WITH RECURSIVE ancestors AS (
128102
SELECT
129103
name,
130-
migration ->> 'version_schema' AS versionSchema,
131104
schema,
132105
parent,
133106
migration_type,
@@ -140,7 +113,6 @@ CREATE OR REPLACE FUNCTION placeholder.previous_version (schemaname name, includ
140113
UNION ALL
141114
SELECT
142115
m.name,
143-
m.migration ->> 'version_schema' AS versionSchema,
144116
m.schema,
145117
m.parent,
146118
m.migration_type,
@@ -151,68 +123,92 @@ CREATE OR REPLACE FUNCTION placeholder.previous_version (schemaname name, includ
151123
AND m.schema = a.schema
152124
)
153125
SELECT
154-
COALESCE(a.versionSchema, a.name)
126+
a.name
155127
FROM
156128
ancestors a
157129
WHERE
158130
a.depth > 0
159-
AND (includeInferred
160-
OR (a.migration_type = 'pgroll'
161-
AND EXISTS (
162-
SELECT
163-
s.schema_name
164-
FROM
165-
information_schema.schemata s
166-
WHERE
167-
s.schema_name = schemaname || '_' || COALESCE(a.versionSchema, a.name))))
168131
ORDER BY
169132
a.depth ASC
170133
LIMIT 1;
171134
$$
172135
LANGUAGE SQL
173136
STABLE;
174137

175-
-- Get the name of the previous migration, or NULL if there is none.
176-
CREATE OR REPLACE FUNCTION placeholder.previous_migration (schemaname name)
138+
-- find_version_schema finds a recent version schema for a given schema name.
139+
-- How recent is determined by the minDepth parameter: for a minDepth of 0, it
140+
-- returns the latest version schema, for a minDepth of 1, it returns the
141+
-- previous version schema, and so on.
142+
-- Only version schemas that exist in the database are considered; migrations
143+
-- without version schema (such as inferred migrations) are ignored.
144+
CREATE OR REPLACE FUNCTION find_version_schema (p_schema_name name, p_depth integer DEFAULT 0)
177145
RETURNS text
178146
AS $$
179147
WITH RECURSIVE ancestors AS (
180148
SELECT
181149
name,
150+
COALESCE(migration ->> 'version_schema', name) AS version_schema,
182151
schema,
183152
parent,
184-
migration_type,
185153
0 AS depth
186154
FROM
187155
placeholder.migrations
188156
WHERE
189-
name = placeholder.latest_migration (schemaname)
190-
AND SCHEMA = schemaname
157+
name = placeholder.latest_migration (p_schema_name)
158+
AND SCHEMA = p_schema_name
191159
UNION ALL
192160
SELECT
193161
m.name,
162+
COALESCE(m.migration ->> 'version_schema', m.name) AS version_schema,
194163
m.schema,
195164
m.parent,
196-
m.migration_type,
197165
a.depth + 1
198166
FROM
199167
placeholder.migrations m
200168
JOIN ancestors a ON m.name = a.parent
201169
AND m.schema = a.schema
202170
)
203171
SELECT
204-
a.name
172+
a.version_schema
205173
FROM
206174
ancestors a
207175
WHERE
208-
a.depth > 0
176+
EXISTS (
177+
SELECT
178+
1
179+
FROM
180+
information_schema.schemata s
181+
WHERE
182+
s.schema_name = p_schema_name || '_' || a.version_schema)
209183
ORDER BY
210-
a.depth ASC
184+
a.depth ASC OFFSET p_depth
211185
LIMIT 1;
212186
$$
213187
LANGUAGE SQL
214188
STABLE;
215189

190+
-- previous_version returns the name of the previous version schema for a given
191+
-- schema name or NULL if there is no previous version schema.
192+
CREATE OR REPLACE FUNCTION previous_version (schemaname name)
193+
RETURNS text
194+
AS $$
195+
SELECT
196+
placeholder.find_version_schema (schemaname, 1);
197+
$$
198+
LANGUAGE SQL
199+
STABLE;
200+
201+
-- latest_version returns the name of the latest version schema for a given
202+
-- schema name or NULL if there are no version schema.
203+
CREATE OR REPLACE FUNCTION latest_version (schemaname name)
204+
RETURNS text
205+
AS $$
206+
SELECT
207+
placeholder.find_version_schema (schemaname, 0);
208+
$$
209+
LANGUAGE SQL
210+
STABLE;
211+
216212
-- Get the JSON representation of the current schema
217213
CREATE OR REPLACE FUNCTION placeholder.read_schema (schemaname text)
218214
RETURNS jsonb

pkg/state/latest.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package state
4+
5+
import (
6+
"context"
7+
"fmt"
8+
9+
"github.com/lib/pq"
10+
)
11+
12+
// LatestVersion returns the name of the latest version schema, or nil if there
13+
// is none.
14+
func (s *State) LatestVersion(ctx context.Context, schema string) (*string, error) {
15+
var version *string
16+
err := s.pgConn.QueryRowContext(ctx,
17+
fmt.Sprintf("SELECT %s.latest_version($1)", pq.QuoteIdentifier(s.schema)),
18+
schema).Scan(&version)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
return version, nil
24+
}
25+
26+
// PreviousVersion returns the name of the previous version schema
27+
func (s *State) PreviousVersion(ctx context.Context, schema string) (*string, error) {
28+
var parent *string
29+
err := s.pgConn.QueryRowContext(ctx,
30+
fmt.Sprintf("SELECT %s.previous_version($1)", pq.QuoteIdentifier(s.schema)),
31+
schema).Scan(&parent)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
return parent, nil
37+
}
38+
39+
// LatestMigration returns the name of the latest migration, or nil if there
40+
// is none.
41+
func (s *State) LatestMigration(ctx context.Context, schema string) (*string, error) {
42+
var migration *string
43+
err := s.pgConn.QueryRowContext(ctx,
44+
fmt.Sprintf("SELECT %s.latest_migration($1)", pq.QuoteIdentifier(s.schema)),
45+
schema).Scan(&migration)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
return migration, nil
51+
}
52+
53+
// PreviousMigration returns the name of the previous migration, or nil if
54+
// there is none.
55+
func (s *State) PreviousMigration(ctx context.Context, schema string) (*string, error) {
56+
var parent *string
57+
err := s.pgConn.QueryRowContext(ctx,
58+
fmt.Sprintf("SELECT %s.previous_migration($1)", pq.QuoteIdentifier(s.schema)),
59+
schema).Scan(&parent)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
return parent, nil
65+
}

0 commit comments

Comments
 (0)