Skip to content

Commit 3594aee

Browse files
authored
migration: better error message on optout attempt (#3404)
better error message on optout attempt
1 parent 2939d7c commit 3594aee

7 files changed

Lines changed: 149 additions & 141 deletions

File tree

cmd/juno/juno.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const (
101101
dbCompactionConcurrencyF = "db-compaction-concurrency"
102102
dbMemtableSizeF = "db-memtable-size"
103103
dbCompressionF = "db-compression"
104-
transactionCombinedLayoutF = "transaction-combined-layout"
104+
transactionCombinedLayoutF = node.FlagTransactionCombinedLayout
105105

106106
defaultConfig = ""
107107
defaultHost = "localhost"

migration/registry.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@ const maxMigrations = 64
66

77
// Registry holds all migrations and builds the target version.
88
type Registry struct {
9-
entries [maxMigrations]Migration
10-
targetVersion SchemaVersion
11-
nextIndex uint8 // Next available index for registering a migration
9+
entries [maxMigrations]Migration
10+
targetVersion SchemaVersion
11+
nextIndex uint8 // Next available index for registering a migration
12+
optionalMigrationFlags [maxMigrations]string
1213
}
1314

1415
// NewRegistry creates a new migration registry.
1516
func NewRegistry() *Registry {
1617
return &Registry{
17-
entries: [maxMigrations]Migration{},
18-
targetVersion: 0,
19-
nextIndex: 0,
18+
entries: [maxMigrations]Migration{},
19+
targetVersion: 0,
20+
nextIndex: 0,
21+
optionalMigrationFlags: [maxMigrations]string{},
2022
}
2123
}
2224

@@ -35,12 +37,14 @@ func (r *Registry) With(m Migration) *Registry {
3537

3638
// WithOptional adds an optional migration to the registry.
3739
// If enabled is true, the migration is included in the target version.
40+
// name is the flag/name used to enable this migration.
3841
// Returns the registry for method chaining.
39-
func (r *Registry) WithOptional(m Migration, enabled bool) *Registry {
42+
func (r *Registry) WithOptional(m Migration, enabled bool, name string) *Registry {
4043
if r.nextIndex >= maxMigrations {
4144
panic("exceeded maximum number of 64 migrations")
4245
}
4346
r.entries[r.nextIndex] = m
47+
r.optionalMigrationFlags[r.nextIndex] = name
4448

4549
if enabled {
4650
r.targetVersion.Set(r.nextIndex)
@@ -64,3 +68,10 @@ func (r *Registry) Entries() []Migration {
6468
func (r *Registry) Count() int {
6569
return int(r.nextIndex)
6670
}
71+
72+
// OptionalMigrationFlags returns the array of optional migration flags.
73+
// The array is indexed by migration index, and the value is the flag/name
74+
// used to enable this migration.
75+
func (r *Registry) OptionalMigrationFlags() []string {
76+
return r.optionalMigrationFlags[:r.nextIndex]
77+
}

migration/registry_test.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,15 @@ func TestRegistry_WithOptional(t *testing.T) {
9090

9191
t.Run("Enabled false", func(t *testing.T) {
9292
r := migration.NewRegistry()
93-
result := r.WithOptional(m, false)
93+
result := r.WithOptional(m, false, "test-migration")
9494
require.Same(t, r, result)
9595
require.Equal(t, 1, r.Count())
9696
require.Equal(t, migration.SchemaVersion(0), r.TargetVersion())
9797
require.Equal(t, m, r.Entries()[0])
9898
})
9999
t.Run("Enabled true", func(t *testing.T) {
100100
r := migration.NewRegistry()
101-
result := r.WithOptional(m, true)
101+
result := r.WithOptional(m, true, "test-migration")
102102
require.Same(t, r, result)
103103
require.Equal(t, 1, r.Count())
104104
require.Equal(t, migration.SchemaVersion(1), r.TargetVersion())
@@ -118,7 +118,7 @@ func TestRegistry_WithOptional_PanicOnMaxMigrations(t *testing.T) {
118118
require.Panics(
119119
t,
120120
func() {
121-
r.WithOptional(m, true)
121+
r.WithOptional(m, true, "test-migration")
122122
},
123123
"expected panic when exceeding 64 migrations",
124124
)
@@ -131,8 +131,8 @@ func TestRegistry_Entries(t *testing.T) {
131131
m2 := &mockMigration{}
132132

133133
r.With(m0)
134-
r.WithOptional(m1, false)
135-
r.WithOptional(m2, false)
134+
r.WithOptional(m1, false, "migration-1")
135+
r.WithOptional(m2, false, "migration-2")
136136

137137
entries := r.Entries()
138138
require.Equal(t, 3, len(entries))
@@ -148,11 +148,28 @@ func TestRegistry_TargetVersion(t *testing.T) {
148148
m2 := &mockMigration{}
149149

150150
r.With(m0)
151-
r.WithOptional(m1, true)
152-
r.WithOptional(m2, false)
151+
r.WithOptional(m1, true, "migration-1")
152+
r.WithOptional(m2, false, "migration-2")
153153

154154
target := r.TargetVersion()
155155
require.True(t, target.Has(0))
156156
require.True(t, target.Has(1))
157157
require.False(t, target.Has(2))
158158
}
159+
160+
func TestRegistry_OptionalMigrationFlags(t *testing.T) {
161+
r := migration.NewRegistry()
162+
m0 := &mockMigration{}
163+
m1 := &mockMigration{}
164+
m2 := &mockMigration{}
165+
166+
r.With(m0)
167+
r.WithOptional(m1, true, "migration-1")
168+
r.WithOptional(m2, false, "migration-2")
169+
170+
flags := r.OptionalMigrationFlags()
171+
require.Equal(t, 3, len(flags))
172+
require.Equal(t, "", flags[0])
173+
require.Equal(t, "migration-1", flags[1])
174+
require.Equal(t, "migration-2", flags[2])
175+
}

migration/runner.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,16 @@ type MigrationRunner struct {
6161
// NewRunner creates a migration runner that executes pending migrations.
6262
//
6363
// Parameters:
64-
// - entries: All registered migrations
65-
// - targetVersion: End state to reach after running all migrations
64+
// - registry: Migration registry containing all migrations
6665
// - database: Database store
6766
// - network: Network configuration
6867
// - log: Logger for migration progress
6968
func NewRunner(
70-
entries []Migration,
71-
targetVersion SchemaVersion,
69+
registry *Registry,
7270
database db.KeyValueStore,
7371
network *utils.Network,
7472
log utils.StructuredLogger,
7573
) (*MigrationRunner, error) {
76-
// Validate that we have entries for all migrations up to the highest index in targetVersion.
77-
// Having more entries is acceptable (optional migrations may be registered but not enabled).
78-
// If targetVersion is empty (no migrations), HighestBit() returns -1
79-
expectedLen := targetVersion.HighestBit() + 1
80-
if len(entries) < expectedLen {
81-
return nil, fmt.Errorf("insufficient entries: have %d, need at least %d",
82-
len(entries),
83-
expectedLen,
84-
)
85-
}
8674
metadata, err := GetSchemaMetadata(database)
8775
if err != nil && !errors.Is(err, db.ErrKeyNotFound) {
8876
return nil, err
@@ -95,18 +83,25 @@ func NewRunner(
9583
}
9684
}
9785

86+
targetVersion := registry.TargetVersion()
9887
// Validate: check if database was migrated with a newer version of Juno (version downgrade)
99-
if err := validateNoVersionDowngrade(metadata.CurrentVersion, targetVersion); err != nil {
88+
err = validateNoVersionDowngrade(metadata.CurrentVersion, targetVersion)
89+
if err != nil {
10090
return nil, err
10191
}
10292

10393
// Validate: check if any previously opted-in migrations are now missing (opt-out attempt)
104-
if err := validateNoOptOut(targetVersion, metadata.LastTargetVersion); err != nil {
94+
err = validateNoOptOut(
95+
targetVersion,
96+
metadata.LastTargetVersion,
97+
registry.OptionalMigrationFlags(),
98+
)
99+
if err != nil {
105100
return nil, err
106101
}
107102

108103
return &MigrationRunner{
109-
entries: entries,
104+
entries: registry.Entries(),
110105
targetVersion: targetVersion,
111106
metadata: metadata,
112107
database: database,
@@ -212,10 +207,30 @@ func validateNoVersionDowngrade(current, target SchemaVersion) error {
212207
// validateNoOptOut checks if any previously opted-in migrations (in lastTargetVersion)
213208
// are missing from the target version (opt-out attempt).
214209
// Returns an error if opt-out attempts are detected, nil otherwise.
215-
func validateNoOptOut(target, lastTargetVersion SchemaVersion) error {
210+
func validateNoOptOut(
211+
target,
212+
lastTargetVersion SchemaVersion,
213+
optionalMigrationFlags []string,
214+
) error {
216215
optOutAttempts := lastTargetVersion.Difference(target)
217-
if optOutAttempts != 0 {
218-
return fmt.Errorf("cannot opt out of previously enabled migrations: %s", optOutAttempts.String())
216+
if optOutAttempts == 0 {
217+
return nil
218+
}
219+
// Build a list of migration flags that cannot be opted out of
220+
flagList := make([]string, optOutAttempts.Len())
221+
i := 0
222+
for idx := range optOutAttempts.Iter() {
223+
if flag := optionalMigrationFlags[idx]; flag != "" {
224+
flagList[i] = fmt.Sprintf("--%s", flag)
225+
} else {
226+
// Fallback to index if flag is not available
227+
flagList[i] = fmt.Sprintf("--migration-%d", idx)
228+
}
229+
i++
219230
}
220-
return nil
231+
232+
return fmt.Errorf(
233+
"cannot opt out of previously enabled migrations: %v (set these flags to enable)",
234+
flagList,
235+
)
221236
}

0 commit comments

Comments
 (0)