Skip to content

Commit 488fb43

Browse files
author
xmtp-coder-agent
committed
fix: pin bun migration reconciliation version
1 parent 373ef40 commit 488fb43

3 files changed

Lines changed: 23 additions & 54 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,16 @@ On startup, the server does one of two things:
152152
1. Fresh database:
153153
It runs the embedded `.up.sql` files and creates both the application tables and `schema_migrations`.
154154
2. Existing Bun-initialized database:
155-
It detects that the full legacy application schema already exists, creates `schema_migrations` if needed, and records the latest embedded migration version without replaying those migrations.
155+
It detects that the full legacy application schema already exists, creates `schema_migrations` if needed, and records the fixed Bun handoff version (`2`) without replaying those baseline migrations.
156156

157157
In this codebase, "reconcile" means that second path: we leave the existing application tables alone and only establish `schema_migrations` so `golang-migrate` can take over from that point forward.
158158

159+
That handoff version is intentionally hardcoded. If we add new `golang-migrate` migrations later that never existed in Bun, older deployments must still run them after upgrade rather than being incorrectly marked as already up to date.
160+
159161
Expected state after a successful startup:
160162

161163
- Fresh DB: application tables exist and `schema_migrations` contains the latest version.
162-
- Bun-initialized DB: the same application tables still exist, `schema_migrations` now exists and contains the latest version, and `bun_migrations` may still exist but is no longer used by the server.
164+
- Bun-initialized DB: the same application tables still exist, `schema_migrations` now exists and contains version `2`, and `bun_migrations` may still exist but is no longer used by the server.
163165

164166
### Testing the API
165167

pkg/db/db_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ func TestMigrateExistingLegacySchema(t *testing.T) {
7272
require.NoError(t, database.Migrate(t.Context(), db))
7373

7474
var version int
75-
err := db.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM schema_migrations`).Scan(&version)
75+
err := db.QueryRowContext(context.Background(), `SELECT version FROM schema_migrations`).Scan(&version)
7676
require.NoError(t, err)
77-
require.Equal(t, 1, version)
77+
require.Equal(t, 2, version)
7878
}
7979

8080
func createRawDB(t *testing.T) *sql.DB {

pkg/db/migrations/migrations.go

Lines changed: 17 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ import (
55
"database/sql"
66
"embed"
77
"errors"
8-
"fmt"
9-
"path/filepath"
10-
"slices"
11-
"strconv"
12-
"strings"
138

149
"github.com/golang-migrate/migrate/v4"
1510
"github.com/golang-migrate/migrate/v4/database/postgres"
@@ -19,11 +14,21 @@ import (
1914
//go:embed *.sql
2015
var migrationFS embed.FS
2116

17+
// Legacy Bun deployments are considered equivalent to the first two golang-migrate
18+
// migrations:
19+
// 1. init schema
20+
// 2. add subscription_hmac_keys + is_silent + unique subscription index
21+
//
22+
// Reconciliation must stay pinned to that handoff point so future golang-migrate-only
23+
// migrations are still applied normally after older deployments upgrade.
24+
const legacyBunBaselineVersion = 2
25+
2226
// Migrate always uses golang-migrate's schema_migrations table as the source of truth.
2327
// For databases that were previously bootstrapped by Bun, we first "reconcile" by
24-
// recording the latest golang-migrate version in schema_migrations without replaying
25-
// the new migrations. That handoff lets already-initialized deployments keep their
26-
// existing application tables and begin using golang-migrate from the next startup on.
28+
// recording the fixed Bun-equivalent golang-migrate version in schema_migrations
29+
// without replaying those baseline migrations. That handoff lets already-initialized
30+
// deployments keep their existing application tables while still allowing future
31+
// golang-migrate-only migrations to run normally after upgrade.
2732
func Migrate(ctx context.Context, db *sql.DB) error {
2833
if err := reconcileExistingBunSchema(ctx, db); err != nil {
2934
return err
@@ -78,11 +83,12 @@ func Migrate(ctx context.Context, db *sql.DB) error {
7883
//
7984
// After reconciliation:
8085
// - the application tables are unchanged
81-
// - schema_migrations exists and contains the latest embedded migration version
86+
// - schema_migrations exists and contains the fixed Bun-equivalent baseline version
8287
//
8388
// We intentionally do not translate Bun's bun_migrations metadata into golang-migrate
8489
// rows. The application data tables are what matter for boot compatibility, so we detect
85-
// the fully-initialized legacy schema directly and mark the new migration runner as caught up.
90+
// the fully-initialized legacy schema directly and mark the new migration runner at the
91+
// Bun handoff version rather than at whatever the latest embedded migration happens to be.
8692
func reconcileExistingBunSchema(ctx context.Context, db *sql.DB) error {
8793
alreadyTracked, err := hasSchemaMigrationState(ctx, db)
8894
if err != nil {
@@ -100,11 +106,6 @@ func reconcileExistingBunSchema(ctx context.Context, db *sql.DB) error {
100106
return nil
101107
}
102108

103-
version, err := latestMigrationVersion()
104-
if err != nil {
105-
return err
106-
}
107-
108109
if _, err := db.ExecContext(ctx, `
109110
CREATE TABLE IF NOT EXISTS schema_migrations (
110111
version bigint NOT NULL PRIMARY KEY,
@@ -117,7 +118,7 @@ func reconcileExistingBunSchema(ctx context.Context, db *sql.DB) error {
117118
INSERT INTO schema_migrations (version, dirty)
118119
VALUES ($1, FALSE)
119120
ON CONFLICT (version) DO UPDATE SET dirty = EXCLUDED.dirty
120-
`, version)
121+
`, legacyBunBaselineVersion)
121122
return err
122123
}
123124

@@ -166,37 +167,3 @@ func hasLegacySchema(ctx context.Context, db *sql.DB) (bool, error) {
166167

167168
return true, nil
168169
}
169-
170-
func latestMigrationVersion() (uint, error) {
171-
entries, err := migrationFS.ReadDir(".")
172-
if err != nil {
173-
return 0, err
174-
}
175-
176-
versions := make([]uint, 0, len(entries))
177-
for _, entry := range entries {
178-
name := entry.Name()
179-
if entry.IsDir() || !strings.HasSuffix(name, ".up.sql") {
180-
continue
181-
}
182-
183-
base := filepath.Base(name)
184-
parts := strings.SplitN(base, "_", 2)
185-
if len(parts) != 2 {
186-
return 0, fmt.Errorf("invalid migration filename %q", name)
187-
}
188-
189-
version, err := strconv.ParseUint(parts[0], 10, 64)
190-
if err != nil {
191-
return 0, fmt.Errorf("parse migration version %q: %w", name, err)
192-
}
193-
versions = append(versions, uint(version))
194-
}
195-
196-
if len(versions) == 0 {
197-
return 0, errors.New("no embedded up migrations found")
198-
}
199-
200-
slices.Sort(versions)
201-
return versions[len(versions)-1], nil
202-
}

0 commit comments

Comments
 (0)