Files
projax/db/migrate.go
mAi b8d3418876 feat(db): projax schema, path trigger, seed areas
- 0001_init.sql: projax.items + projax.item_links tables with indices,
  partial-unique root slug, updated_at trigger, schema grants to the
  application role.
- 0002_path_trigger.sql: BEFORE-write trigger maintains items.path via
  recursive parent walk; rejects cycles and structural-rule violations
  (areas at root, projects not at root). AFTER trigger rewrites
  descendant paths on slug rename or re-parent.
- 0003_seed_areas.sql: dev, sports, home, work, health, finances, social.
- db/migrate.go: embed.FS-backed sequential runner.
- db/migrate_test.go: integration suite covering idempotency, nest,
  rename propagation, re-parent propagation, cycle rejection, and
  structural rules. Skips when no DB env var is set.

Also ignores .m/events.log and .m/locks (per-worker scratch).
2026-05-15 13:16:24 +02:00

47 lines
1.1 KiB
Go

package db
import (
"context"
"embed"
"fmt"
"sort"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
)
//go:embed migrations/*.sql
var migrations embed.FS
// EmbeddedMigrations exposes the raw embedded FS for tests.
var EmbeddedMigrations = migrations
// ApplyMigrations runs every embedded migration file in lexicographic order.
// Idempotent because each migration is written as CREATE ... IF NOT EXISTS /
// CREATE OR REPLACE / ON CONFLICT DO NOTHING.
func ApplyMigrations(ctx context.Context, pool *pgxpool.Pool) error {
entries, err := migrations.ReadDir("migrations")
if err != nil {
return fmt.Errorf("read migrations dir: %w", err)
}
names := make([]string, 0, len(entries))
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
continue
}
names = append(names, e.Name())
}
sort.Strings(names)
for _, name := range names {
body, err := migrations.ReadFile("migrations/" + name)
if err != nil {
return fmt.Errorf("read %s: %w", name, err)
}
if _, err := pool.Exec(ctx, string(body)); err != nil {
return fmt.Errorf("apply %s: %w", name, err)
}
}
return nil
}