A kit

Migrációk

Időbélyeg-prefixes SQL-fájlok a binárisba ágyazva, saját cmd/migrate futtatóval, külső tool nélkül.

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 a cmd/migrate az első naptól forduljon (az embed *.sql mintá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.sql az outbox outbox_events tábláját hozza létre. A séma forrása a kit outbox.MigrationSQL beágyazott konstansa: az a séma, amire a relay query-jei épülnek. Régebbi, worker előtti projektekbe az add worker parancs írja be a soron következő időbélyeggel.
  • embed.go egyetlen //go:embed *.sql direktí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:

migrations/20260720143012_create_products_and_orders.sql
-- +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:

cmd/migrate/main.go
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:

  1. 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/migrate outputja mindent visz.
  2. 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.
  3. 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.

Copyright © 2026