Przejdź do głównej zawartości

Migracje bazy danych (SQLite / D1)

Dokument opisuje, jak schemat bazy danych jest budowany i aktualizowany, oraz jak uruchomić migracje lokalnie i na serwerze VPS (scripts/migrate.mjs). Migracje na Cloudflare D1 (yarn db:update) działają bez zmian do końca migracji na VPS — patrz plan docs/plans/migracja-vps-sqlite-coolify.md.

👤 Instrukcja dla developera

Lokalna baza bez wranglera

  1. Zastosuj wszystkie migracje na pustym pliku SQLite:

    yarn db:migrate

    Plik powstaje w ./.data/dev.db (katalog jest w .gitignore). Pełny zestaw 282 migracji wykonuje się w około 0,7 s.

  2. Uruchom aplikację na tym pliku:

    APP_RUNTIME=node yarn dev

    next.config.mjs pomija wtedy inicjalizację @opennextjs/cloudflare, a getRuntimeEnv() zwraca bazę i cache z ./.data/. Sprawdzenie: curl http://localhost:3000/api/health odpowiada {"status":"ok","runtime":"node",...}.

  3. Dane testowe i czyszczenie działają na tej samej bazie:

    yarn db:seed
    yarn db:clean

    Skrypty (scripts/seed-test-data.js, scripts/clean-local-db.js, scripts/seed-camp-registrations.js), fixture Playwright (tests/fixtures/database-setup.ts) oraz serwery deweloperskie hardware (server.ts, scripts/hardware-ws-dev.ts) wybierają plik bazy w jednej kolejności (scripts/lib/local-database.js):

    1. DATABASE_PATH, jeśli ustawione,
    2. ./.data/dev.db, jeśli istnieje lub APP_RUNTIME=node,
    3. lokalna baza wranglera w .wrangler/state/v3/d1/miniflare-D1DatabaseObject/ — pierwszy plik *.sqlite, który zawiera tabele aplikacji (d1_migrations, game, hardware_device); metadata.sqlite i puste pliki innych bindingów są pomijane.

    Bez żadnej bazy skrypty kończą się komunikatem z trzema sposobami jej utworzenia.

Po pobraniu nowych migracji z develop

yarn db:migrate:status # ile migracji czeka
yarn db:migrate # jedna migracja — bez dodatkowych flag
yarn db:migrate --allow-multiple # więcej niż jedna

Runner odmawia zastosowania kilku migracji naraz, gdy baza ma już historię — to ochrona przed przypadkowym przeskokiem wielu wersji na produkcji. Na pustej bazie i w CI (CI=true) flaga nie jest wymagana.

Dodawanie migracji

Zasady numerowania i kolejności są w CLAUDE.md (sekcja Database Migrations). Nowy plik migrations/NNNN_nazwa.sql jest wykrywany automatycznie; sprawdź go lokalnie:

yarn db:migrate --dry-run # pokaże nazwę jako oczekującą
yarn db:migrate # zastosuje; błąd SQL = rollback całego pliku

🛠 Dokumentacja techniczna

scripts/migrate.mjs

Samodzielny skrypt ESM na node:sqlite (bez zależności z node_modules, więc działa w obrazie produkcyjnym po yarn install --production).

OpcjaZnaczenie
apply (domyślnie)stosuje oczekujące migracje
statuswypisuje liczbę zastosowanych i listę oczekujących, nic nie zmienia
--database <ścieżka>plik SQLite; domyślnie DATABASE_PATH albo ./.data/dev.db; katalog nadrzędny jest tworzony
--migrations-dir <kat.>domyślnie migrations
--to <nazwa>zatrzymuje się po wskazanej migracji; nazwa z .sql lub bez, albo sam numer, jeśli jest jednoznaczny (0265 nie jest)
--dry-runjak status
--allow-multiplewymagane poza CI, gdy oczekuje więcej niż jedna migracja, a baza ma już zastosowane

Zachowanie:

  • Kolejność — leksykograficzna po pełnej nazwie pliku (tak jak wrangler). W repozytorium istnieją dwa pliki 0265_*, więc sam numer nie jest kluczem.
  • Tabela historiid1_migrations(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL), identyczna z tabelą tworzoną przez wrangler d1 migrations apply. Baza wyeksportowana z D1 od razu „wie”, które migracje ma, a runner stosuje tylko brakujące.
  • Transakcja na plikBEGIN IMMEDIATEexec(plik)INSERT INTO d1_migrationsCOMMIT; każdy błąd to ROLLBACK całego pliku, wpis w historii nie powstaje, kod wyjścia 1. BEGIN IMMEDIATE blokuje zapis, więc dwa równoległe runnery (np. dwa kontenery przy deployu) nie wejdą sobie w drogę — drugi czeka do busy_timeout (5 s).
  • PRAGMA w plikachPRAGMA foreign_keys = OFF wewnątrz transakcji jest w SQLite no-opem (tak samo było na D1); PRAGMA defer_foreign_keys = ON działa i jest używane przez migracje przebudowujące tabele (np. 0137, 0262). Po pełnym przebiegu PRAGMA foreign_key_check zwraca 0 wierszy, integrity_check = ok.
  • Triggery — ciała CREATE TRIGGER … BEGIN … END są wykonywane przez exec() całego pliku, więc wielowyrażeniowe triggery (26 plików) nie wymagają dzielenia SQL na instrukcje.
  • Połączeniejournal_mode = WAL, busy_timeout = 5000, foreign_keys = ON (te same ustawienia co lib/runtime/sqlite.ts).

Wybór lokalnej bazy — scripts/lib/local-database.js

resolveLocalDatabasePath({ cwd, env }) zwraca ścieżkę albo null; describeMissingDatabase() zwraca komunikat z instrukcją. Moduł jest CommonJS, żeby działał zarówno w skryptach node scripts/*.js, jak i w plikach TypeScript (server.ts, fixture Playwright) przez tsx/Playwright.

Skrypty package.json

SkryptRuntimePolecenie
yarn db:updateCloudflarewrangler d1 migrations apply DB (używane przez scripts/build-and-migrate.sh z --remote; znika w fazie 6)
yarn db:migrateNodenode scripts/migrate.mjs
yarn db:migrate:statusNodenode scripts/migrate.mjs status
yarn db:add-migrationobawrangler d1 migrations create DB

Na VPS entrypoint kontenera web uruchamia node dist/migrate.js (kopia pre-deploy VACUUM INTO przed nim) — opis w planie migracji, pkt 4.11–4.12.

Testy

__tests__/scripts/migrate.test.ts uruchamia runner jako proces potomny na tymczasowym katalogu migracji (kolejność, tabela zgodna z wranglerem, rollback, --to, --dry-run, --allow-multiple, CI) oraz stosuje pełny zestaw z migrations/ na pustym pliku i sprawdza foreign_key_check / integrity_check. __tests__/scripts/local-database.test.ts pokrywa kolejność wyboru bazy.