feat: migrate definitions into additional addresses

- migration 202610280002 copies the served definition's url items into
  extra_urls, frontend_url into site_url and frontend_default_locale into
  default_locale; reference items are dropped
- the definitions table and the Phase 17 columns keep their data, so a
  rollback loses nothing (TestMigrateDefinitions)
This commit is contained in:
Jakub Zych
2026-11-04 11:14:01 +01:00
parent 84cb27a810
commit 62fc7d71a3
2 changed files with 159 additions and 1 deletions

View File

@@ -9,7 +9,11 @@ import (
"time"
"git.golem15.com/golem15/sm-sitemap-plugin/classes"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"git.golem15.com/golem15/sm-sitemap-plugin/updates"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/pact"
"github.com/go-gormigrate/gormigrate/v2"
)
// bootIndex boots the fake content plugin and the sitemap on a fresh
@@ -304,3 +308,97 @@ func TestSettingsSingleton(t *testing.T) {
}
}
}
// TestMigrateDefinitions pins the D-32 data move of migration
// 202610280002: the url items of the served definition (theme default)
// become the additional addresses as given (no lastmod, unsafe and repeated
// ones skipped), frontend_url becomes site_url and frontend_default_locale
// default_locale; reference items are dropped, the definitions table and
// the Phase 17 columns keep their data, and a rollback removes only the
// copied values.
func TestMigrateDefinitions(t *testing.T) {
sqlDB := freshDB(t)
gdb, err := lagoon.Use(t.Context(), sqlDB)
if err != nil {
t.Fatal(err)
}
all := updates.All()
opts := &gormigrate.Options{TableName: "sitemap_migrations", IDColumnName: "id", IDColumnSize: 255, UseTransaction: true}
upTo := 0
for i, m := range all {
if m.ID == "202610110002_create_golem15_sitemap_settings" {
upTo = i + 1
}
}
if upTo == 0 {
t.Fatal("the Phase 17 settings migration is missing")
}
if err := gormigrate.New(gdb, opts, all[:upTo]).Migrate(); err != nil {
t.Fatal(err)
}
items := `[{"type":"url","url":"/","changefreq":"always","priority":"1.0"},` +
`{"type":"acme-post","reference":"7","nesting":true},` +
`{"type":"url","url":"/o-nas"},` +
`{"type":"url","url":"javascript:alert(1)"},` +
`{"type":"url","url":"/"},` +
`{"type":"url","url":"https://www.example.test/kontakt"}]`
for _, stmt := range []struct {
sql string
args []any
}{
{`INSERT INTO golem15_sitemap_definitions (theme, data) VALUES (?, ?)`, []any{"default", items}},
{`INSERT INTO golem15_sitemap_definitions (theme, data) VALUES (?, ?)`, []any{"other", `[{"type":"url","url":"/other"}]`}},
{`UPDATE golem15_sitemap_settings SET frontend_url = ?, frontend_default_locale = ?, priority_blog = ? WHERE id = 1`, []any{"https://www.example.test/", "pl", "0.7"}},
} {
if err := gdb.Exec(stmt.sql, stmt.args...).Error; err != nil {
t.Fatal(err)
}
}
if err := gormigrate.New(gdb, opts, all).Migrate(); err != nil {
t.Fatal(err)
}
s, err := classes.LoadSettings(gdb)
if err != nil {
t.Fatal(err)
}
if got := extraURLs(s.Extra()); got != "/ /o-nas https://www.example.test/kontakt" {
t.Fatalf("extra_urls = %q", got)
}
for _, e := range s.Extra() {
if e.LastMod != "" {
t.Fatalf("a copied address has a lastmod: %+v", e)
}
}
if models.Text(s.SiteURL) != "https://www.example.test/" || models.Text(s.DefaultLocale) != "pl" || s.EffectiveFileSize() != 1000 {
t.Fatalf("settings = %+v", s)
}
var defs int64
gdb.Raw(`SELECT COUNT(*) FROM golem15_sitemap_definitions`).Scan(&defs)
var data string
gdb.Raw(`SELECT data FROM golem15_sitemap_definitions WHERE theme = 'default'`).Scan(&data)
if defs != 2 || data != items {
t.Fatalf("the definitions table changed: %d rows, data %s", defs, data)
}
if err := gormigrate.New(gdb, opts, all).RollbackTo("202610110002_create_golem15_sitemap_settings"); err != nil {
t.Fatal(err)
}
var old struct {
FrontendURL string `gorm:"column:frontend_url"`
Locale string `gorm:"column:frontend_default_locale"`
Blog string `gorm:"column:priority_blog"`
}
gdb.Raw(`SELECT frontend_url, frontend_default_locale, priority_blog FROM golem15_sitemap_settings WHERE id = 1`).Scan(&old)
if old.FrontendURL != "https://www.example.test/" || old.Locale != "pl" || old.Blog != "0.7" {
t.Fatalf("the rollback changed the Phase 17 settings: %+v", old)
}
gdb.Raw(`SELECT COUNT(*) FROM golem15_sitemap_definitions`).Scan(&defs)
if defs != 2 {
t.Fatalf("the rollback changed the definitions: %d rows", defs)
}
var columns int64
gdb.Raw(`SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'golem15_sitemap_settings' AND column_name IN ('site_url', 'extra_urls')`).Scan(&columns)
if columns != 0 {
t.Fatalf("the rollback left %d copied columns", columns)
}
}

View File

@@ -1,6 +1,10 @@
package updates
import (
"encoding/json"
"strings"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
@@ -33,7 +37,10 @@ var updateSettings = []*gormigrate.Migration{
return err
}
}
return tx.Exec(`INSERT INTO golem15_sitemap_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING`).Error
if err := tx.Exec(`INSERT INTO golem15_sitemap_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING`).Error; err != nil {
return err
}
return copyDefinitions(tx)
},
Rollback: func(tx *gorm.DB) error {
for _, name := range []string{"site_url", "file_size", "default_locale", "extra_urls", "rebuild_pending_until", "numbered_size", "built_at", "rebuild_requested_at"} {
@@ -49,3 +56,56 @@ var updateSettings = []*gormigrate.Migration{
func init() {
Register(updateSettings...)
}
// copyDefinitions moves the Phase 17 data into the new columns (D-32): the
// url items of the definition /sitemap.xml served (theme default, the
// first one) become the additional addresses as given, without a lastmod
// (an address that models.AllowedAddress refuses, or one already copied,
// is skipped); frontend_url becomes site_url when models.AllowedFrontendURL
// accepts it, and frontend_default_locale default_locale. Reference items
// are dropped: the content types list their records. The definitions table
// and the Phase 17 columns are left as they are, so a rollback (which drops
// the new columns) keeps every value.
func copyDefinitions(tx *gorm.DB) error {
var data []string
if err := tx.Raw(`SELECT COALESCE(data, '') FROM golem15_sitemap_definitions WHERE theme = 'default' ORDER BY id ASC LIMIT 1`).Scan(&data).Error; err != nil {
return err
}
rows := []models.ExtraURL{}
if len(data) > 0 {
items, _, err := models.ParseLegacyItems([]byte(data[0]))
if err != nil {
// A definition the Phase 17 code could not read either: nothing
// is copied, and the row stays for a manual look.
items = nil
}
seen := map[string]bool{}
for _, item := range items {
u := strings.TrimSpace(item.URL)
if item.Type != models.LegacyItemURL || seen[u] || !models.AllowedAddress(u) {
continue
}
seen[u] = true
rows = append(rows, models.ExtraURL{URL: u})
}
}
raw, err := json.Marshal(rows)
if err != nil {
return err
}
var old struct {
FrontendURL *string `gorm:"column:frontend_url"`
Locale *string `gorm:"column:frontend_default_locale"`
}
if err := tx.Raw(`SELECT frontend_url, frontend_default_locale FROM golem15_sitemap_settings WHERE id = 1`).Scan(&old).Error; err != nil {
return err
}
var site, locale any
if u := strings.TrimSpace(models.Text(old.FrontendURL)); u != "" && models.AllowedFrontendURL(u) {
site = u
}
if l := strings.TrimSpace(models.Text(old.Locale)); l != "" {
locale = l
}
return tx.Exec(`UPDATE golem15_sitemap_settings SET extra_urls = ?::jsonb, site_url = ?, default_locale = ? WHERE id = 1`, string(raw), site, locale).Error
}