Migrációk
A séma-kezelés a gpsystemben szándékosan egyszerű: időbélyeg-prefixes SQL-fájlok a projekt migrations/ mappájában, a binárisba ágyazva, egy generált cmd/migrate futtatóval. Nincs schema builder, nincs telepítendő külső eszköz: a migráció ugyanúgy go run-nal fut, mint bármi más a projektben.
Mi generálódik
Minden új projekt ezzel indul (lásd new project):
shop/
├── cmd/migrate/main.go # a futtató
└── migrations/
├── embed.go # //go:embed *.sql
├── 20200101000000_init.sql # üres placeholder, az első sémád helye
└── 20200101000100_outbox.sql # az outbox_events tábla
20200101000000_init.sqlüres váz, csak azért, hogy acmd/migrateaz első naptól forduljon (az embed*.sqlmintája legalább egy fájlt vár). Ide írod az első sémádat, vagy új fájlt kérsz a CLI-től. A fix, korai időbélyeg garantálja, hogy minden későbbi, generáláskori időbélyeggel ellátott migráció utána rendeződik.20200101000100_outbox.sqlaz outboxoutbox_eventstábláját hozza létre. A séma forrása a kitoutbox.MigrationSQLbeágyazott konstansa: az a séma, amire a relay query-jei épülnek. Régebbi, worker előtti projektekbe azadd workerparancs írja be a soron következő időbélyeggel.embed.goegyetlen//go:embed *.sqldirektíva: minden migráció a bináris része lesz, a deploy artifact önhordó.
A fájlformátum
Sima SQL, goose-markerekkel tagolva. Egy fájl egy migráció, fel- és leirányban:
-- +goose Up
-- +goose StatementBegin
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
price_cents bigint NOT NULL,
stock int NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE products;
-- +goose StatementEnd
A +goose Up / +goose Down a két irány; a StatementBegin/StatementEnd pár azt jelöli, hogy a köztes rész egyetlen statementként fusson (enélkül a goose pontosvesszőnként darabol, ami többsoros függvényeknél, triggereknél számít). A fájlnév formátuma <idobelyeg>_nev.sql: a 14 jegyű, UTC ÉÉÉÉHHNNOOPPMM időbélyeg adja a futtatási sorrendet és a verziót. Mivel az időbélyeg a generáláskori pillanatot rögzíti, nem sorszám, branch-ek között sosem ütközik.
Új migrációt a CLI-vel kérj: kiosztja az aktuális időbélyeget és legenerálja a vázat:
go tool gpsystem new migration create_products_and_orders
# -> migrations/20260720143012_create_products_and_orders.sql
Régebbi, még sorszámozott sémájú projekteket (00001_init.sql, 00002_outbox.sql) sem kell migrálni: az új migrációk mostantól is helyesen, a régiek után rendeződnek, mert a goose mindkét formátumot szám szerint sorba rakja.
Részletek: new migration.
A cmd/migrate bináris
A generált futtató rövid, és érdemes egyszer elolvasni a sajátodat, nagyjából ennyi:
cfg := envconf.MustLoad[config]() // csak a DB_* változókat tölti
pool := pg.MustNewPool(ctx, cfg.DB) // ugyanaz a pgx pool, amit a seedelés is használ
db := stdlib.OpenDBFromPool(pool) // database/sql a pool felett
goose.SetBaseFS(migrations.FS) // a beágyazott fájlokból dolgozik
goose.SetDialect("postgres")
args := os.Args[1:]
if len(args) == 0 {
args = []string{"up"} // argumentum nélkül: up
}
goose.RunContext(ctx, args[0], db, ".", args[1:]...)
Vagyis: a dbx.Config-ot tölti a DB_* env-változókból (ugyanazokból, amiket az app használ), és a goose-t könyvtárként futtatja a beágyazott fájlrendszer felett, egyetlen dbx/pg pool tetején (ugyanazt, amit a seedelés is megnyit, ha a --seed flaggel vagy a seed subcommanddal hívod). A parancs egy az egyben továbbadódik a goose-nak:
go run ./cmd/migrate # = up: minden függő migráció, sorrendben
go run ./cmd/migrate up
go run ./cmd/migrate down # a legutóbbi migráció visszagörgetése
go run ./cmd/migrate status # melyik fájl fut le, melyik függ még
go run ./cmd/migrate version # aktuális sémaverzió
go run ./cmd/migrate up-to 3 # adott verzióig (a sorszám számként)
go run ./cmd/migrate up --seed # migrál, majd lefuttatja a seedereket
go run ./cmd/migrate seed # csak a seedereket futtatja, migráció nélkül
A goose az alkalmazott migrációkat a goose_db_version táblában tartja nyilván (első futáskor hozza létre). Minden migrációs fájl tranzakcióban fut: ha egy statement hibázik, a fájl összes statementje visszagördül, és a verzió-tábla nem lép.
A --seed/seed a projekt központi seeds/ package-ét futtatja le; lásd a Seederek oldalt.
Miért nincs külső migrációs eszköz?
A stackben nincs golang-migrate, nincs atlas, és a goose CLI-jét sem kell telepítened: a goose kizárólag Go-függőségként, a saját cmd/migrate binárisodba fordítva fut. Ennek három következménye van:
- A deploy artifact önhordó. A migrációk a binárisba ágyazódnak; a CI-nek és a production-image-nek nem kell SQL-fájlokat másolnia vagy külön toolt tartalmaznia. Egy
go build ./cmd/migrateoutputja mindent visz. - Nincs verzió-drift. A migrációs futtató ugyanabból a
go.mod-ból jön, mint minden más: nem fordulhat elő, hogy a CI más goose-verzióval fut, mint a fejlesztői gép. - Sima SQL a source of truth. Nincs DSL és nincs diff-alapú varázslat: az fut le, ami a fájlban van, és a code review pontosan azt látja.
A nevesített kapcsolatok nem migrálódnak
A cmd/migrate mindig csak a projekt default DB_* kapcsolatát tölti be: nincs fogalma az add db által egy modulhoz adott nevesített kapcsolatokról. A goose verziótáblája adatbázisonkénti, és egy nevesített kapcsolat gyakran olyasmire mutat, amelynek sémáját a service nem is birtokolja (egy raktár, egy read replica, egy legacy adatbázis), ezért nincs hozzá generált futtató. Ha egy nevesített kapcsolatnak valóban saját, migrált sémára van szüksége, futtasd a goose-t kézzel ellene, vagy adj hozzá egy második cmd/migrate-<név> binárist, ugyanazt a mintát követve, mint a generált.
Deployment
- A migráció fusson az app előtt, tőle külön lépésként: init containerként, release-fázis parancsként vagy a deploy-pipeline lépéseként:
go run ./cmd/migrate up(vagy az előre fordított bináris). Az app-processz indulása nem futtat migrációt. - Egy futtató egyszerre. A migrációt a deploy egyetlen lépése futtassa, ne minden replika párhuzamosan, több replika mellett az init-container-per-pod minta helyett a pipeline-lépés a jó hely.
- Előre-kompatibilis séma. Rolling deploynál a régi app-verzió még fut, amikor az új séma már él, additív változásokkal (új tábla, új nullable oszlop) ez magától működik; oszlop-törlést, átnevezést két deployra bonts.
Nincs schema builder
A gpsystem tudatosan nem tartalmaz schema buildert: a migrációk sima SQL-ek. A builder hordozhatóságot ad adatbázisok között, amire itt nincs szükség (a kit PostgreSQL-re épül), cserébe elfedi, milyen DDL fut le valójában. A sima SQL-lel a PostgreSQL teljes felülete elérhető (partial indexek, GENERATED oszlopok, CTE-s backfillek), pont úgy, ahogy az adatbázis dokumentációjában áll. A 20200101000100_outbox.sql jó példa: partial index (WHERE published_at IS NULL) a publikálatlan sorokra, közvetlenül, absztrakciós réteg nélkül kifejezve.