-
Notifications
You must be signed in to change notification settings - Fork 137
tapd+sqlc: separate SQL migrations from code migrations #1881
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import ( | |
| "os" | ||
| "path/filepath" | ||
| "regexp" | ||
| "sort" | ||
| "strings" | ||
| "testing" | ||
|
|
||
|
|
@@ -573,6 +574,10 @@ func TestMigration33(t *testing.T) { | |
|
|
||
| // And now that we have test data inserted, we can migrate to the latest | ||
| // version. | ||
| // NOTE: the post-migration check was originally run at migration | ||
| // version 33, but was later moved to version 48. Targeting the latest | ||
| // migration, will execute post-migration check, but at version 48 | ||
| // instead of 33. | ||
| err := db.ExecuteMigrations(TargetLatest, WithPostStepCallbacks( | ||
| makePostStepCallbacks(db, postMigrationChecks), | ||
| )) | ||
|
|
@@ -672,6 +677,116 @@ func TestMigration33(t *testing.T) { | |
| ) | ||
| } | ||
|
|
||
| // TestMigration48ScriptKeyTypeReplay makes sure that if the script key type | ||
| // backfill already ran for a user (originally at migration 33), the replay at | ||
| // migration 48 is a no-op and doesn't rewrite any data. | ||
| func TestMigration48ScriptKeyTypeReplay(t *testing.T) { | ||
| ctx := context.Background() | ||
|
|
||
| db := NewTestDBWithVersion(t, 32) | ||
|
|
||
| InsertTestdata(t, db.BaseDB, "migrations_test_00033_dummy_data.sql") | ||
|
|
||
| // We simulate that a user previously ran migration 33, which at the | ||
| // time also contained the code migration which has now been separated | ||
| // to migration 48. | ||
| err := db.ExecuteMigrations(TargetVersion(33), WithPostStepCallbacks( | ||
| makePostStepCallbacks(db, map[uint]postMigrationCheck{ | ||
| 33: determineAndAssignScriptKeyType, | ||
| }), | ||
| )) | ||
| require.NoError(t, err) | ||
|
|
||
| const ( | ||
| key1 = "039c571fffcac1a1a7cd3372bd202ad8562f28e48b90f8a4eb714" + | ||
| "eca062f576ee6" | ||
| key2 = "029c571fffcac1a1a7cd3372bd202ad8562f28e48b90f8a4eb714" + | ||
| "eca062f576ee6" | ||
| key3 = "03f9cdf1ff7c9fbb0ea3c8533cd7048994f41ea20a79764469c22" + | ||
| "aa18aa6696169" | ||
| key4 = "027c79b9b26e463895eef5679d8558942c86c4ad2233adef01bc3" + | ||
| "e6d540b3653fe" | ||
| key5 = "0350aaeb166f4234650d84a2d8a130987aeaf6950206e0905401e" + | ||
| "e74ff3f8d18e6" | ||
| key6 = "02248bca7dbb12dcf0b490263a1d521691691aa2541842b7472c8" + | ||
| "3acac0e88443b" | ||
| ) | ||
|
|
||
| expectedKeyTypes := map[string]asset.ScriptKeyType{ | ||
| key1: asset.ScriptKeyUnknown, | ||
| key2: asset.ScriptKeyBip86, | ||
| key3: asset.ScriptKeyScriptPathExternal, | ||
| key4: asset.ScriptKeyTombstone, | ||
| key5: asset.ScriptKeyScriptPathChannel, | ||
| key6: asset.ScriptKeyBurn, | ||
| } | ||
|
|
||
| // fetchTypes returns the script key types currently persisted in the | ||
| // database keyed by their tweaked hex representation. | ||
| fetchTypes := func() map[string]asset.ScriptKeyType { | ||
| currentTypes := make(map[string]asset.ScriptKeyType) | ||
|
|
||
| for keyHex := range expectedKeyTypes { | ||
| keyBytes, err := hex.DecodeString(keyHex) | ||
| require.NoError(t, err) | ||
|
|
||
| dbKey, err := db.BaseDB.FetchScriptKeyByTweakedKey( | ||
| ctx, keyBytes, | ||
| ) | ||
| require.NoError(t, err) | ||
|
|
||
| currentTypes[keyHex] = | ||
| extractSqlInt16[asset.ScriptKeyType]( | ||
| dbKey.ScriptKey.KeyType, | ||
| ) | ||
| } | ||
|
|
||
| return currentTypes | ||
| } | ||
|
|
||
| // Verify that the database contains the expected script keys after the | ||
| // first migration has been run. | ||
| require.Equal(t, expectedKeyTypes, fetchTypes()) | ||
|
|
||
| // Now let's change the ScriptKey type for one of the entries, to an | ||
| // incorrect value. When the code migration is rerun, this value should | ||
| // not be changed despite being incorrect, as the replay of the code | ||
| // migration won't act on values which have already been assigned. | ||
| keyBytes, err := hex.DecodeString(key5) | ||
| require.NoError(t, err) | ||
|
|
||
| dbKey, err := db.BaseDB.FetchScriptKeyByTweakedKey( | ||
| ctx, keyBytes, | ||
| ) | ||
| require.NoError(t, err) | ||
|
|
||
| _, err = db.BaseDB.UpsertScriptKey(ctx, NewScriptKey{ | ||
| InternalKeyID: dbKey.InternalKey.KeyID, | ||
| TweakedScriptKey: dbKey.ScriptKey.TweakedScriptKey, | ||
| Tweak: dbKey.ScriptKey.Tweak, | ||
| KeyType: sqlInt16(asset.ScriptKeyBip86), | ||
| }) | ||
| require.NoError(t, err) | ||
|
|
||
| // Executing the code migration again (now at migration 48) should not | ||
| // change or add any new values than the values assigned when the code | ||
| // migration was run for migration version 33. | ||
| err = db.ExecuteMigrations(TargetLatest, WithPostStepCallbacks( | ||
| makePostStepCallbacks(db, postMigrationChecks), | ||
| )) | ||
| require.NoError(t, err) | ||
|
Comment on lines
+771
to
+777
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Target 48 explicitly here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same below for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be nice to have const values for migration numbers (33, 49 and 48, etc) in these unit tests. Start/replacement. Maybe? |
||
|
|
||
| // As we changed the value for key5 to an incorrect value, we expect | ||
| // that the db still contains that, hence not being equal to the | ||
| // expectedKeyTypes. | ||
| require.NotEqual(t, expectedKeyTypes, fetchTypes()) | ||
|
|
||
| // If we change the expectedKeyTypes to contain the incorrect value, | ||
| // they should however be equal. | ||
| expectedKeyTypes[key5] = asset.ScriptKeyBip86 | ||
| require.Equal(t, expectedKeyTypes, fetchTypes()) | ||
| } | ||
|
|
||
| // TestMigration37 tests that the Golang based post-migration check for the | ||
| // asset burn insertion works as expected. | ||
| func TestMigration37(t *testing.T) { | ||
|
|
@@ -685,6 +800,10 @@ func TestMigration37(t *testing.T) { | |
|
|
||
| // And now that we have test data inserted, we can migrate to the latest | ||
| // version. | ||
| // NOTE: the post-migration check was originally run at migration | ||
| // version 37, but was later moved to version 49. Targeting the latest | ||
| // migration, will execute post-migration check, but at version 49 | ||
| // instead of 37. | ||
| err := db.ExecuteMigrations(TargetLatest, WithPostStepCallbacks( | ||
| makePostStepCallbacks(db, postMigrationChecks), | ||
| )) | ||
|
|
@@ -696,6 +815,71 @@ func TestMigration37(t *testing.T) { | |
| require.Len(t, burns, 5) | ||
| } | ||
|
|
||
| // TestMigration49BurnReplay makes sure that if the asset burn code migration | ||
| // already ran for a user (originally at migration 37), the replay at migration | ||
| // 49 is a no-op and doesn't insert duplicate burns. | ||
| func TestMigration49BurnReplay(t *testing.T) { | ||
| ctx := context.Background() | ||
|
|
||
| db := NewTestDBWithVersion(t, 36) | ||
|
|
||
| InsertTestdata(t, db.BaseDB, "migrations_test_00037_dummy_data.sql") | ||
|
|
||
| // The test data inserts 3 burns into the database before the migration | ||
| // is run. After the migration is run, 2 more entries will be added. | ||
| burnsBefore, err := db.QueryBurns(ctx, QueryBurnsFilters{}) | ||
| require.NoError(t, err) | ||
| require.Len(t, burnsBefore, 3) | ||
|
|
||
| // We simulate that a user previously ran migration 37, which at the | ||
| // time also contained the code migration which has now been separated | ||
| // to migration 49. | ||
| err = db.ExecuteMigrations(TargetVersion(37), WithPostStepCallbacks( | ||
| makePostStepCallbacks(db, map[uint]postMigrationCheck{ | ||
| 37: insertAssetBurns, | ||
| }), | ||
| )) | ||
| require.NoError(t, err) | ||
|
|
||
| // Since the migration was insertAssetBurns code migration was run for | ||
| // version 37, the database should now contain 5 entries. | ||
| burnsAfterFirstMigration, err := db.QueryBurns(ctx, QueryBurnsFilters{}) | ||
| require.NoError(t, err) | ||
| require.Len(t, burnsAfterFirstMigration, 5) | ||
|
|
||
| normalizeBurns := func(burns []sqlc.QueryBurnsRow) []string { | ||
| result := make([]string, 0, len(burns)) | ||
|
|
||
| for _, burn := range burns { | ||
| result = append(result, fmt.Sprintf("%x:%x:%x:%d", | ||
| burn.AnchorTxid, burn.AssetID, burn.GroupKey, | ||
| burn.Amount)) | ||
| } | ||
|
|
||
| sort.Strings(result) | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| // Execute the rest of the migrations, which will trigger the migration | ||
| // version 49 code migration. | ||
| err = db.ExecuteMigrations(TargetLatest, WithPostStepCallbacks( | ||
| makePostStepCallbacks(db, postMigrationChecks), | ||
| )) | ||
| require.NoError(t, err) | ||
|
|
||
| burnsAfter, err := db.QueryBurns(ctx, QueryBurnsFilters{}) | ||
| require.NoError(t, err) | ||
|
|
||
| // Despite that the code migration in migration version 49 was executed | ||
| // once more, the asset burns persisted in the database should not have | ||
| // changed. | ||
| require.Equal( | ||
| t, normalizeBurns(burnsAfterFirstMigration), | ||
| normalizeBurns(burnsAfter), | ||
| ) | ||
| } | ||
|
|
||
| // TestDirtySqliteVersion tests that if a migration fails and leaves an Sqlite | ||
| // database backend in a dirty state, any attempts of re-executing migrations on | ||
| // the db (i.e. restart tapd), will fail with an error indicating that the | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -- The file intentionally only contains this comment to ensure the file created and picked up in the migration stream. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -- The file intentionally only contains this comment to ensure the file created and picked up in the migration stream. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated:
Do we need to include this commit? Is it required for your end goal?
We don’t often have no-op down migrations, but IMO it’s fine to include them. It keeps the migration sequence contiguous, and IMO it’s cleaner to always provide a down file for every up, even if the down is empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s not required, but I want to explain why I think this change is beneficial.
This change specifically targets code migrations, allowing them to omit a
down.sqlfile when appropriate.To illustrate why SQL and code migrations should behave differently, consider the following example of migrations:
0001_example.up.sqlCREATE TABLE example (id INTEGER PRIMARY KEY);0001_example.down.sqlDROP TABLE example;Followed by a code migration:
0002_example_code_migration.up.sql(This migration inserts data into the
exampletable.)If we also add a
down.sqlfor the code migration:0002_example_code_migration.down.sql(This would simply re-run the code migration and insert the data again.)
This highlights the mismatch:
down.sqlundoes theup.sql.down.sqlwould unintentionally re-execute the migration instead of undoing the changes.In this example, the data inserted by the code migration would actually be removed only when the table itself is dropped by
0001_example.down.sql.Currently, the code migrations in
tapdare structured as no-ops when re-executed, but that assumption can't be true for all code migrations. For example, it'd be much more complex for migrations like the kvdb → SQL migration inlitd.For that reason, I think the code migration itself should indicate whether a
down.sqlshould be present or not, rather than having CI enforce for all types of migrations. Themigratelibrary explicitly supports migrations without adown.sqlfile, and I think this is a case where that functionality makes sense to use.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And just to clarify, this
ifclause here will only trigger when theup.sqlcase contains any executable content, i.e. when theup.sqlis an SQL migration. It's not checking if thedown.sqlfile is empty.