fix(lagoon): keep Status plugin-only; migrate:status adds the framework sets itself (WR-12)
57a849d made lagoon.Status read migrationSets, which changed its result
for consumers: the framework sets came first and a plugin with no
migrations lost its row. Status is back to its pre-57a849d behaviour.
migrate:status (table and --json) now reads the unexported setStatus,
which lists the framework sets and the plugins from migrationSets, so it
still agrees with migrate --pretend. Both share historyRows.
- TestStatusPluginRows pins Status: plugin rows only, an empty row for a
plugin with no migrations or no history table, none without HasMigrations
- TestMigrateStatusJSON also checks the table lists the framework sets
- TestTwoPluginMigrationSetsIsolated and TestDocsMigrate are back to the
plugin-only rows
This commit is contained in:
@@ -104,6 +104,8 @@ if err := lagoon.RollbackLast(db, plugins, "acme.blog"); err != nil {
|
|||||||
|
|
||||||
`lagoon.Migrate`, `lagoon.Status` and `lagoon.RollbackLast` take the activated plugins; a plugin that does not implement `pact.HasMigrations` is skipped.
|
`lagoon.Migrate`, `lagoon.Status` and `lagoon.RollbackLast` take the activated plugins; a plugin that does not implement `pact.HasMigrations` is skipped.
|
||||||
|
|
||||||
|
`lagoon.Status` returns one row per plugin, a plugin with no migrations included, and leaves out the framework's own sets that `migrate:status` prints.
|
||||||
|
|
||||||
## Porting version.yaml
|
## Porting version.yaml
|
||||||
|
|
||||||
WinterCMS runs a plugin's update scripts by version number and seeds data from the same files. When you port a plugin:
|
WinterCMS runs a plugin's update scripts by version number and seeds data from the same files. When you port a plugin:
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func (p *Plugin) Migrations() []*gormigrate.Migration {
|
|||||||
| `lagoon.DSN` | Reads `database.dsn` from config. |
|
| `lagoon.DSN` | Reads `database.dsn` from config. |
|
||||||
| `lagoon.Migrate` | Runs framework and plugin migrations in order. |
|
| `lagoon.Migrate` | Runs framework and plugin migrations in order. |
|
||||||
| `lagoon.RollbackLast` | Rolls back the last migration of one plugin. |
|
| `lagoon.RollbackLast` | Rolls back the last migration of one plugin. |
|
||||||
| `lagoon.Status` | Lists the applied migration IDs of every set `lagoon.Migrate` applies, framework sets first (under their history IDs, such as `summercms.attach`), then each plugin with migrations, as `lagoon.StatusRow` values. |
|
| `lagoon.Status` | Lists applied migration IDs per plugin as `lagoon.StatusRow` values: one row for each plugin that implements `pact.HasMigrations`, in plugin order; a plugin with no migrations, or without a history table yet, gets a row without IDs. The framework's own sets are not listed; `migrate:status` adds them. |
|
||||||
| `lagoon.DatabaseLabel` | Describes a DSN for a person as `host:port/name` (parsed with `pgconn.ParseConfig`), never the user or password; `not configured` when empty, `configured` when it does not parse. Used by the startup summaries of `serve` and `queue:work`. |
|
| `lagoon.DatabaseLabel` | Describes a DSN for a person as `host:port/name` (parsed with `pgconn.ParseConfig`), never the user or password; `not configured` when empty, `configured` when it does not parse. Used by the startup summaries of `serve` and `queue:work`. |
|
||||||
| `lagoon.PendingMigrations` | Lists the migrations `lagoon.Migrate` would apply, as `lagoon.MigrationRef` values (`Plugin`, `ID`): the framework sets first, then each plugin in order, each in declaration order. A missing history table means the whole set is pending. Read-only; `serve` reports the count in its startup summary. |
|
| `lagoon.PendingMigrations` | Lists the migrations `lagoon.Migrate` would apply, as `lagoon.MigrationRef` values (`Plugin`, `ID`): the framework sets first, then each plugin in order, each in declaration order. A missing history table means the whole set is pending. Read-only; `serve` reports the count in its startup summary. |
|
||||||
| `lagoon.HistoryTableName` | Returns the gormigrate history table for a plugin ID. |
|
| `lagoon.HistoryTableName` | Returns the gormigrate history table for a plugin ID. |
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ sets with its table and whether it is applied, for scripts.`,
|
|||||||
},
|
},
|
||||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||||
return withDB(ctx, app, func(gdb *gorm.DB) error {
|
return withDB(ctx, app, func(gdb *gorm.DB) error {
|
||||||
rows, err := Status(gdb, plugins)
|
rows, err := setStatus(gdb, plugins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,16 +359,9 @@ func TestDocsMigrate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// The framework's own sets come first (WR-12), then the plugin.
|
|
||||||
framework := []string{"summercms.attach", "summercms.cabana", lagoon.DeferredHistoryID, lagoon.QueueHistoryID}
|
|
||||||
want := "acme.blog summer_migrations_acme_blog [20260101000100_create_posts 20260101000200_create_comments_and_categories]"
|
want := "acme.blog summer_migrations_acme_blog [20260101000100_create_posts 20260101000200_create_comments_and_categories]"
|
||||||
if len(lines) != len(framework)+1 || lines[len(lines)-1] != want {
|
if len(lines) != 1 || lines[0] != want {
|
||||||
t.Fatalf("status = %q, want the framework sets and then %q", lines, want)
|
t.Fatalf("status = %q, want %q", lines, want)
|
||||||
}
|
|
||||||
for i, id := range framework {
|
|
||||||
if !strings.HasPrefix(lines[i], id+" ") {
|
|
||||||
t.Errorf("status line %d = %q, want the framework set %s", i, lines[i], id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if db.Migrator().HasTable("acme_blog_comments") {
|
if db.Migrator().HasTable("acme_blog_comments") {
|
||||||
t.Fatal("rollback left acme_blog_comments")
|
t.Fatal("rollback left acme_blog_comments")
|
||||||
|
|||||||
@@ -130,20 +130,34 @@ func lastMigrationPlugin(plugins []party.Plugin) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusRow is one migration set's recorded IDs: Plugin is the plugin ID or
|
// StatusRow is one plugin's recorded migration ids.
|
||||||
// the framework set's history ID (for example summercms.attach), Table its
|
|
||||||
// history table.
|
|
||||||
type StatusRow struct {
|
type StatusRow struct {
|
||||||
Plugin string
|
Plugin string
|
||||||
Table string
|
Table string
|
||||||
IDs []string
|
IDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status reads the history table of every set Migrate applies, in the
|
// Status reads each plugin history table: one row per plugin that
|
||||||
// same order: the framework's own sets first (uploads, backend admin,
|
// implements pact.HasMigrations, in plugin order, a plugin with no
|
||||||
// deferred bindings, job queue), then each plugin with at least one
|
// migrations included. The framework's own sets are not listed.
|
||||||
// migration. A missing history table gives a row without IDs.
|
|
||||||
func Status(gdb *gorm.DB, plugins []party.Plugin) ([]StatusRow, error) {
|
func Status(gdb *gorm.DB, plugins []party.Plugin) ([]StatusRow, error) {
|
||||||
|
if gdb == nil {
|
||||||
|
return nil, fmt.Errorf("lagoon: gorm db is nil")
|
||||||
|
}
|
||||||
|
var ids []string
|
||||||
|
for _, p := range plugins {
|
||||||
|
if _, ok := p.(pact.HasMigrations); ok {
|
||||||
|
ids = append(ids, p.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return historyRows(gdb, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setStatus reads the history table of every set Migrate applies, in its
|
||||||
|
// order (migrationSets): the framework's own sets first, then each plugin
|
||||||
|
// with at least one migration. migrate:status prints it, so that it agrees
|
||||||
|
// with migrate --pretend.
|
||||||
|
func setStatus(gdb *gorm.DB, plugins []party.Plugin) ([]StatusRow, error) {
|
||||||
if gdb == nil {
|
if gdb == nil {
|
||||||
return nil, fmt.Errorf("lagoon: gorm db is nil")
|
return nil, fmt.Errorf("lagoon: gorm db is nil")
|
||||||
}
|
}
|
||||||
@@ -151,19 +165,30 @@ func Status(gdb *gorm.DB, plugins []party.Plugin) ([]StatusRow, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var rows []StatusRow
|
ids := make([]string, 0, len(sets))
|
||||||
for _, s := range sets {
|
for _, s := range sets {
|
||||||
table, err := HistoryTableName(s.id)
|
ids = append(ids, s.id)
|
||||||
|
}
|
||||||
|
return historyRows(gdb, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// historyRows reads the applied IDs of each history ID's table, sorted. A
|
||||||
|
// missing history table gives a row without IDs (nil), an empty one a row
|
||||||
|
// with an empty slice.
|
||||||
|
func historyRows(gdb *gorm.DB, ids []string) ([]StatusRow, error) {
|
||||||
|
var rows []StatusRow
|
||||||
|
for _, id := range ids {
|
||||||
|
table, err := HistoryTableName(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
row := StatusRow{Plugin: s.id, Table: table}
|
row := StatusRow{Plugin: id, Table: table}
|
||||||
if !gdb.Migrator().HasTable(table) {
|
if !gdb.Migrator().HasTable(table) {
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := gdb.Table(table).Order("id").Pluck("id", &row.IDs).Error; err != nil {
|
if err := gdb.Table(table).Order("id").Pluck("id", &row.IDs).Error; err != nil {
|
||||||
return nil, fmt.Errorf("lagoon: status %s: %w", s.id, err)
|
return nil, fmt.Errorf("lagoon: status %s: %w", id, err)
|
||||||
}
|
}
|
||||||
if row.IDs == nil {
|
if row.IDs == nil {
|
||||||
row.IDs = []string{}
|
row.IDs = []string{}
|
||||||
|
|||||||
@@ -186,43 +186,18 @@ func TestTwoPluginMigrationSetsIsolated(t *testing.T) {
|
|||||||
t.Fatalf("idempotent migrate: %v", err)
|
t.Fatalf("idempotent migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status lists the sets Migrate applies (WR-12): the framework sets
|
status, err := Status(gdb, plugins)
|
||||||
// first, in migrationSets order, then the plugins.
|
if err != nil {
|
||||||
statusOf := func(t *testing.T) (alpha, beta StatusRow) {
|
t.Fatal(err)
|
||||||
t.Helper()
|
|
||||||
status, err := Status(gdb, plugins)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
framework := []string{"summercms.attach", "summercms.cabana", DeferredHistoryID, QueueHistoryID}
|
|
||||||
if len(status) != len(framework)+2 {
|
|
||||||
t.Fatalf("status rows = %+v", status)
|
|
||||||
}
|
|
||||||
for i, id := range framework {
|
|
||||||
if status[i].Plugin != id {
|
|
||||||
t.Fatalf("status row %d = %+v, want the framework set %s", i, status[i], id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
found := map[string]StatusRow{}
|
|
||||||
for _, row := range status[len(framework):] {
|
|
||||||
found[row.Plugin] = row
|
|
||||||
}
|
|
||||||
var ok bool
|
|
||||||
if alpha, ok = found["demo.alpha"]; !ok {
|
|
||||||
t.Fatalf("no demo.alpha row: %+v", status)
|
|
||||||
}
|
|
||||||
if beta, ok = found["demo.beta"]; !ok {
|
|
||||||
t.Fatalf("no demo.beta row: %+v", status)
|
|
||||||
}
|
|
||||||
return alpha, beta
|
|
||||||
}
|
}
|
||||||
|
if len(status) != 2 {
|
||||||
alphaRow, betaRow := statusOf(t)
|
t.Fatalf("status rows = %+v", status)
|
||||||
if alphaRow.Table != "summer_migrations_demo_alpha" || strings.Join(alphaRow.IDs, ",") != "202609170001_create_alpha" {
|
|
||||||
t.Fatalf("alpha status = %+v", alphaRow)
|
|
||||||
}
|
}
|
||||||
if betaRow.Table != "summer_migrations_demo_beta" || strings.Join(betaRow.IDs, ",") != "202609170001_create_beta,202609170002_seed_beta" {
|
if status[0].Plugin != "demo.alpha" || status[0].Table != "summer_migrations_demo_alpha" || strings.Join(status[0].IDs, ",") != "202609170001_create_alpha" {
|
||||||
t.Fatalf("beta status = %+v", betaRow)
|
t.Fatalf("alpha status = %+v", status[0])
|
||||||
|
}
|
||||||
|
if status[1].Plugin != "demo.beta" || status[1].Table != "summer_migrations_demo_beta" || strings.Join(status[1].IDs, ",") != "202609170001_create_beta,202609170002_seed_beta" {
|
||||||
|
t.Fatalf("beta status = %+v", status[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gdb.Migrator().HasTable("lagoon_alpha") || !gdb.Migrator().HasTable("lagoon_beta") {
|
if !gdb.Migrator().HasTable("lagoon_alpha") || !gdb.Migrator().HasTable("lagoon_beta") {
|
||||||
@@ -250,12 +225,15 @@ func TestTwoPluginMigrationSetsIsolated(t *testing.T) {
|
|||||||
t.Fatal("schema rollback must not run on the first beta RollbackLast")
|
t.Fatal("schema rollback must not run on the first beta RollbackLast")
|
||||||
}
|
}
|
||||||
|
|
||||||
alphaRow, betaRow = statusOf(t)
|
status, err = Status(gdb, plugins)
|
||||||
if strings.Join(alphaRow.IDs, ",") != "202609170001_create_alpha" {
|
if err != nil {
|
||||||
t.Fatalf("alpha history changed: %+v", alphaRow)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if strings.Join(betaRow.IDs, ",") != "202609170001_create_beta" {
|
if strings.Join(status[0].IDs, ",") != "202609170001_create_alpha" {
|
||||||
t.Fatalf("beta history = %+v", betaRow)
|
t.Fatalf("alpha history changed: %+v", status[0])
|
||||||
|
}
|
||||||
|
if strings.Join(status[1].IDs, ",") != "202609170001_create_beta" {
|
||||||
|
t.Fatalf("beta history = %+v", status[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RollbackLast(gdb, plugins, "demo.beta"); err != nil {
|
if err := RollbackLast(gdb, plugins, "demo.beta"); err != nil {
|
||||||
@@ -268,12 +246,15 @@ func TestTwoPluginMigrationSetsIsolated(t *testing.T) {
|
|||||||
t.Fatal("alpha table must survive beta rollback")
|
t.Fatal("alpha table must survive beta rollback")
|
||||||
}
|
}
|
||||||
|
|
||||||
alphaRow, betaRow = statusOf(t)
|
status, err = Status(gdb, plugins)
|
||||||
if len(alphaRow.IDs) != 1 {
|
if err != nil {
|
||||||
t.Fatalf("alpha history after beta down = %+v", alphaRow)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(betaRow.IDs) != 0 {
|
if len(status[0].IDs) != 1 {
|
||||||
t.Fatalf("beta history should be empty, got %+v", betaRow)
|
t.Fatalf("alpha history after beta down = %+v", status[0])
|
||||||
|
}
|
||||||
|
if len(status[1].IDs) != 0 {
|
||||||
|
t.Fatalf("beta history should be empty, got %+v", status[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -334,6 +335,51 @@ func TestMigrateStatusJSON(t *testing.T) {
|
|||||||
t.Errorf("%v: pending entries lack \"applied\": false:\n%s", args, out)
|
t.Errorf("%v: pending entries lack \"applied\": false:\n%s", args, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The table lists the same sets: the framework's own first, in the
|
||||||
|
// order migrate applies them, then the plugins.
|
||||||
|
out, errOut, code, _ := runLagoon(t, "development", dsn, plugins, "migrate:status")
|
||||||
|
if code != bonfire.ExitOK {
|
||||||
|
t.Fatalf("migrate:status: exit %d\n%s", code, errOut)
|
||||||
|
}
|
||||||
|
var sets []string
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(out), "\n")[1:] {
|
||||||
|
sets = append(sets, strings.Fields(line)[0])
|
||||||
|
}
|
||||||
|
wantSets := []string{"summercms.attach", "summercms.cabana", DeferredHistoryID, QueueHistoryID, "demo.alpha", "demo.beta"}
|
||||||
|
if !slices.Equal(sets, wantSets) {
|
||||||
|
t.Errorf("migrate:status sets = %q, want %q:\n%s", sets, wantSets, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStatusPluginRows: Status lists the plugins only, never the
|
||||||
|
// framework's own sets (migrate:status adds those itself). Every plugin
|
||||||
|
// that implements pact.HasMigrations gets a row in plugin order, one with
|
||||||
|
// no migrations and one whose history table does not exist yet included;
|
||||||
|
// a plugin without HasMigrations gets none.
|
||||||
|
func TestStatusPluginRows(t *testing.T) {
|
||||||
|
db, _ := dedicatedDB(t, "lagoon_status_plugin_rows")
|
||||||
|
gdb, err := Use(t.Context(), db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
alpha, beta := pendingPlugins()
|
||||||
|
empty := migPlugin{id: "demo.empty"}
|
||||||
|
if err := Migrate(gdb, []party.Plugin{alpha}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rows, err := Status(gdb, []party.Plugin{alpha, empty, stubPlugin{id: "demo.plain"}, beta})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []StatusRow{
|
||||||
|
{Plugin: "demo.alpha", Table: "summer_migrations_demo_alpha", IDs: []string{"202610210001_create_pending_alpha"}},
|
||||||
|
{Plugin: "demo.empty", Table: "summer_migrations_demo_empty"},
|
||||||
|
{Plugin: "demo.beta", Table: "summer_migrations_demo_beta"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(rows, want) {
|
||||||
|
t.Fatalf("Status = %+v, want %+v", rows, want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMigrateAppliedLines (WR-07): migrate's "N applied" lines count what
|
// TestMigrateAppliedLines (WR-07): migrate's "N applied" lines count what
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ DOCKER_LAGOON_TESTS=(
|
|||||||
TestMigrateAppliedLines
|
TestMigrateAppliedLines
|
||||||
# quick 261022-4z8 (re-review fixes): WR-13.
|
# quick 261022-4z8 (re-review fixes): WR-13.
|
||||||
TestMigrateRereadFailureWarns
|
TestMigrateRereadFailureWarns
|
||||||
|
# quick 261022-4z8: WR-12 rework, lagoon.Status stays plugin-only.
|
||||||
|
TestStatusPluginRows
|
||||||
)
|
)
|
||||||
DOCKER_CABANA_TESTS=(TestTokenCreateJSON TestTokenListJSON TestTokenCreateQuietKeepsToken)
|
DOCKER_CABANA_TESTS=(TestTokenCreateJSON TestTokenListJSON TestTokenCreateQuietKeepsToken)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user