aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-14 00:51:19 +0300
committergrm <grm@eyesin.space>2026-09-14 00:51:19 +0300
commit282d2ab3e0fb3a74f32160a231225773001acd14 (patch)
treeb15c105c0c5d79467d3b815af4c67f5bf4a02d9d
parent0de17af2318134549fa6f4db822ba6921b35f12f (diff)
downloadblogspace-282d2ab3e0fb3a74f32160a231225773001acd14.tar.gz
blogspace-282d2ab3e0fb3a74f32160a231225773001acd14.tar.bz2
blogspace-282d2ab3e0fb3a74f32160a231225773001acd14.zip
Drop the single-database split and re-baseline the control migrations
Every deployment has been through the per-blog split, so the code that performed it (split.go, control migrations 00002–00007) is dead weight, and a fresh install replaying six migrations only to drop the tables again was silly. The control chain is now a single 00001_init.sql with the final users and blogs shape, matching the blog chain. Goose ignores versions recorded in the database that no longer exist in the source, but refuses a future migration numbered below the database's highest version. Databases that went through the split therefore need `DELETE FROM goose_db_version WHERE version_id > 1` once; README and AGENTS.md say so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
-rw-r--r--AGENTS.md11
-rw-r--r--README.md9
-rw-r--r--internal/db/db.go18
-rw-r--r--internal/db/migrations/control/00001_init.sql55
-rw-r--r--internal/db/migrations/control/00002_sections.sql19
-rw-r--r--internal/db/migrations/control/00003_layout.sql56
-rw-r--r--internal/db/migrations/control/00004_section_placement.sql13
-rw-r--r--internal/db/migrations/control/00005_per_blog_databases.sql12
-rw-r--r--internal/db/migrations/control/00007_drop_content.sql9
-rw-r--r--internal/db/split.go152
-rw-r--r--internal/store/modules.go2
11 files changed, 27 insertions, 329 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 8b997e4..4ff23f8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -47,8 +47,7 @@ cmd/blogspace/ main.go (serve|seed|migrate, superadmin + root blog bootst
migrates every blog database at start), seed.go
internal/config/ env → Config; RootSubdomain = "www"
internal/db/ Cluster (control pool + lazy per-blog pools, CREATE/DROP DATABASE),
- goose providers; migrations/control/*.sql, migrations/blog/*.sql,
- split.go (control Go migration 00006: the one-off move to per-blog DBs)
+ goose providers; migrations/control/*.sql, migrations/blog/*.sql
internal/store/ Store = control DB (users, blog registry, create/delete blog);
BlogStore = one blog's DB (settings, pages, posts, images, sections, modules, menu)
internal/auth/ bcrypt, JWT issue/parse, cookie, HMAC CSRF
@@ -218,9 +217,11 @@ superadmin password to `admin`. Production refuses both.
(runs in every blog database — it must not reference `users`/`blogs`) and
`internal/db/migrations/control/` for users and the registry; goose
`-- +goose Up/Down` sections; they run automatically at startup. Never
- edit an applied migration. Control 00005–00007 are the one-off split (add
- `db_name`, move content in `split.go`, drop the old tables) — a fresh
- install still replays them.
+ edit an applied migration. Both chains were re-baselined at 00001 after
+ the move to per-blog databases; deployments from before it have
+ `goose_db_version` rows 2–7 in the control DB that must be deleted once
+ (README "Upgrading from a single database") or the next control migration
+ is refused as "missing".
- Blog content goes through `BlogStore` methods, which take `ctx` first and
are bound to one blog's database, so a blogger can never touch another
blog's rows; users and the registry go through `Store`.
diff --git a/README.md b/README.md
index fbf7412..6a6f224 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,14 @@ above. A dump taken with an older version of Blogspace is upgraded at the next
start (or with `blogspace migrate`).
Deleting a user in `/admin/` drops their blog database — take a dump first if
-you may want it back. Blog pools are small (4 connections each, closed when
+you may want it back.
+
+Upgrading from a single database (installs older than the per-blog split): run
+the release that contains the split once (it moves each blog into its own
+database at start-up), then clear the old migration history so later
+migrations apply: `docker compose exec db psql -U blogspace -c "DELETE FROM
+goose_db_version WHERE version_id > 1"`. Current releases no longer carry the
+split code. Blog pools are small (4 connections each, closed when
idle); with many blogs busy at once, raise `max_connections` on the `db` service.
## Layout
diff --git a/internal/db/db.go b/internal/db/db.go
index 601680d..0f66fcc 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -139,12 +139,7 @@ func (c *Cluster) CreateBlogDB(ctx context.Context, dbName string) error {
return nil
}
-// execer is the pool or connection createDatabase runs on.
-type execer interface {
- Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
-}
-
-func createDatabase(ctx context.Context, db execer, dbName string) error {
+func createDatabase(ctx context.Context, db *pgxpool.Pool, dbName string) error {
_, err := db.Exec(ctx, `CREATE DATABASE `+pgx.Identifier{dbName}.Sanitize())
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "42P04" { // duplicate_database
@@ -169,10 +164,9 @@ func (c *Cluster) DropBlogDB(ctx context.Context, dbName string) error {
// ---- migrations ------------------------------------------------------------
-// MigrateControl applies the control database migrations, including the Go
-// migration that moves content out into the blog databases.
+// MigrateControl applies the control database migrations.
func MigrateControl(ctx context.Context, controlURL string) error {
- return migrate(ctx, controlURL, "migrations/control", goose.WithGoMigrations(splitMigration(controlURL)))
+ return migrate(ctx, controlURL, "migrations/control")
}
// MigrateBlog applies the blog schema migrations to one blog database.
@@ -181,8 +175,8 @@ func MigrateBlog(ctx context.Context, blogURL string) error {
}
// migrate runs goose over database/sql. A Provider (rather than the package
-// globals) keeps the two migration sets, and the Go migration, apart.
-func migrate(ctx context.Context, dsn, dir string, opts ...goose.ProviderOption) error {
+// globals) keeps the two migration sets apart.
+func migrate(ctx context.Context, dsn, dir string) error {
sqldb, err := sql.Open("pgx", dsn)
if err != nil {
return err
@@ -192,7 +186,7 @@ func migrate(ctx context.Context, dsn, dir string, opts ...goose.ProviderOption)
if err != nil {
return err
}
- p, err := goose.NewProvider(goose.DialectPostgres, sqldb, fsys, opts...)
+ p, err := goose.NewProvider(goose.DialectPostgres, sqldb, fsys)
if err != nil {
return err
}
diff --git a/internal/db/migrations/control/00001_init.sql b/internal/db/migrations/control/00001_init.sql
index e27dad4..0b26b37 100644
--- a/internal/db/migrations/control/00001_init.sql
+++ b/internal/db/migrations/control/00001_init.sql
@@ -1,4 +1,6 @@
-- +goose Up
+-- The control database: accounts and the list of blogs. Everything a blog
+-- contains lives in its own database (see migrations/blog).
CREATE TABLE users (
id bigserial PRIMARY KEY,
username text NOT NULL UNIQUE,
@@ -9,60 +11,15 @@ CREATE TABLE users (
created_at timestamptz NOT NULL DEFAULT now()
);
+-- "blog_" + subdomain must fit a 63-char Postgres identifier, hence 58.
CREATE TABLE blogs (
id bigserial PRIMARY KEY,
owner_id bigint NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
- subdomain text NOT NULL UNIQUE CHECK (subdomain ~ '^[a-z0-9](-?[a-z0-9]){0,62}$'),
- title text NOT NULL,
- tagline text NOT NULL DEFAULT '',
- theme jsonb NOT NULL DEFAULT '{}'::jsonb,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
+ subdomain text NOT NULL UNIQUE CHECK (subdomain ~ '^[a-z0-9](-?[a-z0-9]){0,57}$'),
+ db_name text NOT NULL UNIQUE,
+ created_at timestamptz NOT NULL DEFAULT now()
);
-CREATE TABLE pages (
- id bigserial PRIMARY KEY,
- blog_id bigint NOT NULL REFERENCES blogs(id) ON DELETE CASCADE,
- slug text NOT NULL,
- title text NOT NULL,
- intro_md text NOT NULL DEFAULT '',
- intro_html text NOT NULL DEFAULT '',
- nav_order integer NOT NULL DEFAULT 0,
- show_in_nav boolean NOT NULL DEFAULT true,
- is_home boolean NOT NULL DEFAULT false,
- created_at timestamptz NOT NULL DEFAULT now(),
- UNIQUE (blog_id, slug)
-);
-CREATE UNIQUE INDEX pages_one_home_per_blog ON pages (blog_id) WHERE is_home;
-
-CREATE TABLE posts (
- id bigserial PRIMARY KEY,
- page_id bigint NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
- slug text NOT NULL,
- title text NOT NULL,
- body_md text NOT NULL DEFAULT '',
- body_html text NOT NULL DEFAULT '',
- published boolean NOT NULL DEFAULT true,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now(),
- UNIQUE (page_id, slug)
-);
-CREATE INDEX posts_page_created ON posts (page_id, created_at DESC);
-
-CREATE TABLE images (
- id uuid PRIMARY KEY,
- blog_id bigint NOT NULL REFERENCES blogs(id) ON DELETE CASCADE,
- filename text NOT NULL,
- content_type text NOT NULL,
- size integer NOT NULL,
- data bytea NOT NULL,
- created_at timestamptz NOT NULL DEFAULT now()
-);
-CREATE INDEX images_blog ON images (blog_id, created_at DESC);
-
-- +goose Down
-DROP TABLE images;
-DROP TABLE posts;
-DROP TABLE pages;
DROP TABLE blogs;
DROP TABLE users;
diff --git a/internal/db/migrations/control/00002_sections.sql b/internal/db/migrations/control/00002_sections.sql
deleted file mode 100644
index 388cb17..0000000
--- a/internal/db/migrations/control/00002_sections.sql
+++ /dev/null
@@ -1,19 +0,0 @@
--- +goose Up
--- Announcements: blog-wide notices shown on every page and post.
-CREATE TABLE sections (
- id bigserial PRIMARY KEY,
- blog_id bigint NOT NULL REFERENCES blogs(id) ON DELETE CASCADE,
- title text NOT NULL DEFAULT '',
- body_md text NOT NULL DEFAULT '',
- body_html text NOT NULL DEFAULT '',
- placement text NOT NULL DEFAULT 'above' CHECK (placement IN ('above', 'below', 'sidebar')),
- style text NOT NULL DEFAULT 'note' CHECK (style IN ('plain', 'note', 'warning')),
- enabled boolean NOT NULL DEFAULT true,
- sort_order integer NOT NULL DEFAULT 0,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-CREATE INDEX sections_blog ON sections (blog_id, sort_order, id);
-
--- +goose Down
-DROP TABLE sections;
diff --git a/internal/db/migrations/control/00003_layout.sql b/internal/db/migrations/control/00003_layout.sql
deleted file mode 100644
index e8fff23..0000000
--- a/internal/db/migrations/control/00003_layout.sql
+++ /dev/null
@@ -1,56 +0,0 @@
--- +goose Up
--- Layout modules: what each area of a blog (header, columns, footer) shows.
-CREATE TABLE modules (
- id bigserial PRIMARY KEY,
- blog_id bigint NOT NULL REFERENCES blogs(id) ON DELETE CASCADE,
- area text NOT NULL CHECK (area IN ('header', 'left', 'right', 'above', 'below', 'footer')),
- kind text NOT NULL CHECK (kind IN ('title', 'logo', 'menu', 'archive', 'recent', 'html', 'rss', 'text', 'sitemap')),
- title text NOT NULL DEFAULT '',
- body text NOT NULL DEFAULT '',
- count integer NOT NULL DEFAULT 5,
- sort_order integer NOT NULL DEFAULT 0,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-CREATE INDEX modules_blog ON modules (blog_id, area, sort_order, id);
-
--- The menu: blog pages and custom links in one ordered list.
-CREATE TABLE menu_items (
- id bigserial PRIMARY KEY,
- blog_id bigint NOT NULL REFERENCES blogs(id) ON DELETE CASCADE,
- page_id bigint REFERENCES pages(id) ON DELETE CASCADE,
- label text NOT NULL DEFAULT '',
- url text NOT NULL DEFAULT '',
- sort_order integer NOT NULL DEFAULT 0,
- CHECK (page_id IS NOT NULL OR url <> '')
-);
-CREATE UNIQUE INDEX menu_items_page ON menu_items (page_id) WHERE page_id IS NOT NULL;
-CREATE INDEX menu_items_blog ON menu_items (blog_id, sort_order, id);
-
--- Existing blogs keep the menu they had: pages marked "show in menu", in menu
--- order, with the home page only if the theme included it.
-INSERT INTO menu_items (blog_id, page_id, sort_order)
-SELECT p.blog_id, p.id, row_number() OVER (PARTITION BY p.blog_id ORDER BY p.nav_order, p.id) - 1
-FROM pages p JOIN blogs b ON b.id = p.blog_id
-WHERE p.show_in_nav AND (NOT p.is_home OR coalesce((b.theme->>'nav_show_home')::boolean, true));
-ALTER TABLE pages DROP COLUMN show_in_nav;
-
--- ...and the layout their theme described.
-INSERT INTO modules (blog_id, area, kind, sort_order)
-SELECT id, 'header', 'title', CASE WHEN theme->>'nav_position' = 'top-bar' THEN 1 ELSE 0 END
-FROM blogs WHERE coalesce((theme->>'header_show_title')::boolean, true);
-INSERT INTO modules (blog_id, area, kind, sort_order)
-SELECT id, 'header', 'menu', CASE WHEN theme->>'nav_position' = 'top-bar' THEN 0 ELSE 1 END
-FROM blogs WHERE coalesce(theme->>'nav_position', 'below-header') <> 'left-sidebar';
-INSERT INTO modules (blog_id, area, kind, sort_order)
-SELECT id, 'left', 'menu', 0 FROM blogs WHERE theme->>'nav_position' = 'left-sidebar';
-INSERT INTO modules (blog_id, area, kind, body, sort_order)
-SELECT id, 'footer', 'text', theme->>'footer_text', 0 FROM blogs WHERE coalesce(theme->>'footer_text', '') <> '';
-INSERT INTO modules (blog_id, area, kind, sort_order)
-SELECT id, 'footer', 'rss', 1 FROM blogs WHERE coalesce((theme->>'footer_show_rss')::boolean, true);
-
--- +goose Down
-ALTER TABLE pages ADD COLUMN show_in_nav boolean NOT NULL DEFAULT true;
-UPDATE pages SET show_in_nav = EXISTS (SELECT 1 FROM menu_items m WHERE m.page_id = pages.id);
-DROP TABLE menu_items;
-DROP TABLE modules;
diff --git a/internal/db/migrations/control/00004_section_placement.sql b/internal/db/migrations/control/00004_section_placement.sql
deleted file mode 100644
index c36787e..0000000
--- a/internal/db/migrations/control/00004_section_placement.sql
+++ /dev/null
@@ -1,13 +0,0 @@
--- +goose Up
--- Announcements are placed in a column (left, main, right) at its top or bottom.
-ALTER TABLE sections DROP CONSTRAINT sections_placement_check;
-UPDATE sections SET placement = CASE placement WHEN 'above' THEN 'main-top' WHEN 'below' THEN 'main-bottom' ELSE 'left-top' END;
-ALTER TABLE sections ALTER COLUMN placement SET DEFAULT 'main-top';
-ALTER TABLE sections ADD CONSTRAINT sections_placement_check
- CHECK (placement IN ('left-top', 'left-bottom', 'main-top', 'main-bottom', 'right-top', 'right-bottom'));
-
--- +goose Down
-ALTER TABLE sections DROP CONSTRAINT sections_placement_check;
-UPDATE sections SET placement = CASE placement WHEN 'main-top' THEN 'above' WHEN 'main-bottom' THEN 'below' ELSE 'sidebar' END;
-ALTER TABLE sections ALTER COLUMN placement SET DEFAULT 'above';
-ALTER TABLE sections ADD CONSTRAINT sections_placement_check CHECK (placement IN ('above', 'below', 'sidebar'));
diff --git a/internal/db/migrations/control/00005_per_blog_databases.sql b/internal/db/migrations/control/00005_per_blog_databases.sql
deleted file mode 100644
index 92d3fa2..0000000
--- a/internal/db/migrations/control/00005_per_blog_databases.sql
+++ /dev/null
@@ -1,12 +0,0 @@
--- +goose Up
--- Each blog gets its own database; the registry remembers which one.
--- db_name stays nullable until the Go migration 00006 has moved the content.
-ALTER TABLE blogs ADD COLUMN db_name text UNIQUE;
--- "blog_" + subdomain must fit in a 63-char Postgres identifier.
-ALTER TABLE blogs DROP CONSTRAINT blogs_subdomain_check;
-ALTER TABLE blogs ADD CONSTRAINT blogs_subdomain_check CHECK (subdomain ~ '^[a-z0-9](-?[a-z0-9]){0,57}$');
-
--- +goose Down
-ALTER TABLE blogs DROP CONSTRAINT blogs_subdomain_check;
-ALTER TABLE blogs ADD CONSTRAINT blogs_subdomain_check CHECK (subdomain ~ '^[a-z0-9](-?[a-z0-9]){0,62}$');
-ALTER TABLE blogs DROP COLUMN db_name;
diff --git a/internal/db/migrations/control/00007_drop_content.sql b/internal/db/migrations/control/00007_drop_content.sql
deleted file mode 100644
index 9daa1ac..0000000
--- a/internal/db/migrations/control/00007_drop_content.sql
+++ /dev/null
@@ -1,9 +0,0 @@
--- +goose Up
--- The content now lives in the per-blog databases (see 00006 in split.go);
--- the control database keeps only users and the blog registry.
-DROP TABLE menu_items, modules, sections, images, posts, pages;
-ALTER TABLE blogs DROP COLUMN title, DROP COLUMN tagline, DROP COLUMN theme, DROP COLUMN updated_at;
-ALTER TABLE blogs ALTER COLUMN db_name SET NOT NULL;
-
--- +goose Down
--- Not reversible: the content is gone from this database. Restore from a backup instead.
diff --git a/internal/db/split.go b/internal/db/split.go
deleted file mode 100644
index 668d255..0000000
--- a/internal/db/split.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package db
-
-import (
- "context"
- "database/sql"
- "errors"
- "fmt"
- "log"
-
- "github.com/jackc/pgx/v5"
- "github.com/pressly/goose/v3"
-)
-
-// splitMigration is control migration 00006: it moves every blog's content out
-// of the control database into a database of its own. It runs in the control
-// transaction, so either every blog is moved and marked with its db_name or
-// nothing changes; the blog databases it created are then dropped by hand
-// (the error says which) and the next start retries. 00007 drops the old
-// tables afterwards.
-func splitMigration(controlURL string) *goose.Migration {
- up := func(ctx context.Context, tx *sql.Tx) error {
- return splitBlogs(ctx, tx, controlURL)
- }
- down := func(ctx context.Context, tx *sql.Tx) error {
- return errors.New("the per-blog split cannot be undone; restore the control database from a backup")
- }
- return goose.NewGoMigration(6, &goose.GoFunc{RunTx: up}, &goose.GoFunc{RunTx: down})
-}
-
-// splitTable is one content table to copy: the query selects the blog's rows
-// in the column order of the new table.
-type splitTable struct {
- name string
- cols []string
- query string // $1 = blog id
- seq bool // bigserial id to bump after the copy
-}
-
-var splitTables = []splitTable{
- {"pages", []string{"id", "slug", "title", "intro_md", "intro_html", "nav_order", "is_home", "created_at"},
- `SELECT id, slug, title, intro_md, intro_html, nav_order, is_home, created_at FROM pages WHERE blog_id=$1`, true},
- {"posts", []string{"id", "page_id", "slug", "title", "body_md", "body_html", "published", "created_at", "updated_at"},
- `SELECT p.id, p.page_id, p.slug, p.title, p.body_md, p.body_html, p.published, p.created_at, p.updated_at
- FROM posts p JOIN pages g ON g.id=p.page_id WHERE g.blog_id=$1`, true},
- {"images", []string{"id", "filename", "content_type", "size", "data", "created_at"},
- `SELECT id, filename, content_type, size, data, created_at FROM images WHERE blog_id=$1`, false},
- {"sections", []string{"id", "title", "body_md", "body_html", "placement", "style", "enabled", "sort_order", "created_at", "updated_at"},
- `SELECT id, title, body_md, body_html, placement, style, enabled, sort_order, created_at, updated_at FROM sections WHERE blog_id=$1`, true},
- {"modules", []string{"id", "area", "kind", "title", "body", "count", "sort_order", "created_at", "updated_at"},
- `SELECT id, area, kind, title, body, count, sort_order, created_at, updated_at FROM modules WHERE blog_id=$1`, true},
- {"menu_items", []string{"id", "page_id", "label", "url", "sort_order"},
- `SELECT id, page_id, label, url, sort_order FROM menu_items WHERE blog_id=$1`, true},
-}
-
-func splitBlogs(ctx context.Context, tx *sql.Tx, controlURL string) error {
- type blog struct {
- id int64
- sub, title, tagline string
- theme []byte
- createdAt, updatedAt any
- }
- rows, err := tx.QueryContext(ctx, `SELECT id, subdomain, title, tagline, theme, created_at, updated_at FROM blogs WHERE db_name IS NULL ORDER BY id`)
- if err != nil {
- return err
- }
- var blogs []blog
- for rows.Next() {
- var b blog
- if err := rows.Scan(&b.id, &b.sub, &b.title, &b.tagline, &b.theme, &b.createdAt, &b.updatedAt); err != nil {
- rows.Close()
- return err
- }
- blogs = append(blogs, b)
- }
- rows.Close()
- if err := rows.Err(); err != nil {
- return err
- }
- if len(blogs) == 0 {
- return nil
- }
-
- // DDL needs its own connection: CREATE DATABASE refuses to run in a transaction.
- admin, err := pgx.Connect(ctx, controlURL)
- if err != nil {
- return err
- }
- defer admin.Close(ctx)
- var created []string
- for _, b := range blogs {
- name := DBName(b.sub)
- log.Printf("moving blog %q into database %s", b.sub, name)
- if err := createDatabase(ctx, admin, name); err != nil {
- return fmt.Errorf("%w — a leftover of an earlier failed split must be dropped by hand (created so far: %v)", err, created)
- }
- created = append(created, name)
- if err := MigrateBlog(ctx, withDatabase(controlURL, name)); err != nil {
- return fmt.Errorf("migrate %s: %w (drop the databases %v before retrying)", name, err, created)
- }
- if err := copyBlog(ctx, tx, withDatabase(controlURL, name), b.id, b.title, b.tagline, b.theme, b.createdAt, b.updatedAt); err != nil {
- return fmt.Errorf("copy blog %q: %w (drop the databases %v before retrying)", b.sub, err, created)
- }
- if _, err := tx.ExecContext(ctx, `UPDATE blogs SET db_name=$1 WHERE id=$2`, name, b.id); err != nil {
- return err
- }
- }
- return nil
-}
-
-// copyBlog streams one blog's rows from the control transaction into its new database.
-func copyBlog(ctx context.Context, tx *sql.Tx, blogURL string, blogID int64, title, tagline string, theme []byte, createdAt, updatedAt any) error {
- conn, err := pgx.Connect(ctx, blogURL)
- if err != nil {
- return err
- }
- defer conn.Close(ctx)
- if _, err := conn.Exec(ctx, `INSERT INTO settings (title, tagline, theme, created_at, updated_at) VALUES ($1,$2,$3,$4,$5)`,
- title, tagline, theme, createdAt, updatedAt); err != nil {
- return fmt.Errorf("settings: %w", err)
- }
- for _, t := range splitTables {
- rows, err := tx.QueryContext(ctx, t.query, blogID)
- if err != nil {
- return fmt.Errorf("%s: %w", t.name, err)
- }
- n := len(t.cols)
- src := pgx.CopyFromFunc(func() ([]any, error) {
- if !rows.Next() {
- return nil, rows.Err()
- }
- vals := make([]any, n)
- ptrs := make([]any, n)
- for i := range vals {
- ptrs[i] = &vals[i]
- }
- // database/sql hands back int64/string/bool/[]byte/time.Time,
- // which pgx encodes for the matching column types.
- return vals, rows.Scan(ptrs...)
- })
- _, err = conn.CopyFrom(ctx, pgx.Identifier{t.name}, t.cols, src)
- rows.Close()
- if err != nil {
- return fmt.Errorf("%s: %w", t.name, err)
- }
- if t.seq {
- if _, err := conn.Exec(ctx, `SELECT setval(pg_get_serial_sequence($1,'id'), coalesce(max(id),0)+1, false) FROM `+pgx.Identifier{t.name}.Sanitize(), t.name); err != nil {
- return fmt.Errorf("%s sequence: %w", t.name, err)
- }
- }
- }
- return nil
-}
diff --git a/internal/store/modules.go b/internal/store/modules.go
index e0c3cd5..d077975 100644
--- a/internal/store/modules.go
+++ b/internal/store/modules.go
@@ -127,7 +127,7 @@ func (bs *BlogStore) ResetModules(ctx context.Context) error {
}
// insertDefaultModules is the starting layout: title and menu in the header,
-// an RSS link in the footer. Must match what the layout migration gave old blogs.
+// an RSS link in the footer.
func insertDefaultModules(ctx context.Context, tx querier) error {
_, err := tx.Exec(ctx, `INSERT INTO modules (area, kind, sort_order) VALUES
('header','title',0), ('header','menu',1), ('footer','rss',0)`)