feat: prod app.db şemasını açılışta drizzle migration'larıyla eşitle
Some checks failed
Deploy / deploy (push) Failing after 13s
Some checks failed
Deploy / deploy (push) Failing after 13s
Volume'deki app.db yalnızca ilk kurulumda seed'den kopyalanıyordu; sonraki deploy'larda şemaya eklenen kolonlar (son örnek: kredi_bitti_at, kredi_hatirlatma_gonderildi_at) prod'a hiç ulaşmıyor ve 'no such column' hatalarıyla kırılıyordu. - drizzle-kit generate ile baseline migration üretildi (drizzle/0000_baslangic) - scripts/db-goc.mjs: entrypoint'te server'dan önce koşan migration runner; drizzle migrator protokolünü birebir taklit eder (__drizzle_migrations, sha256, created_at=journal.when) ama yalnızca @libsql/client'a dayanır — drizzle-orm standalone imajda bundle'landığından paket olarak çözülemiyor - Benimseme: migration kaydı olmayan mevcut volume'lerde baseline additive uzlaştırılır (IF NOT EXISTS + snapshot'tan eksik kolon ALTER'ları) ve damgalanır; sonraki migration'lar normal akışla uygulanır - sema-guvence.ts kaldırıldı: elle DDL aynalama yerini migration'lara bıraktı - Yeni akış: şema değişikliğinde pnpm db:generate ile migration commit'lenir Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
157
scripts/db-goc.mjs
Normal file
157
scripts/db-goc.mjs
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* app.db şemasını drizzle/ altındaki migration'larla eşitler — konteyner
|
||||
* açılışında (docker-entrypoint.sh) node server.js'den ÖNCE koşar, lokalde
|
||||
* `pnpm db:migrate` ile çalıştırılır. Idempotenttir.
|
||||
*
|
||||
* Neden drizzle-orm'un kendi migrate()'i değil: standalone çıktıda drizzle-orm
|
||||
* Turbopack'çe bundle'landığı için /app/node_modules'ta paket olarak yok;
|
||||
* @libsql/client ise serverExternalPackages'ta olduğundan her zaman çözülür.
|
||||
* Bu script drizzle migrator protokolünü birebir taklit eder (__drizzle_migrations
|
||||
* tablosu, sha256 hash, created_at = journal.when) — ileride resmi migrate()'e
|
||||
* geçiş kayıtları bozmaz.
|
||||
*
|
||||
* Benimseme (adoption): migration akışından önce doldurulmuş prod volume'lerinde
|
||||
* __drizzle_migrations tablosu yoktur ama şema (kısmen) vardır. Bu durumda
|
||||
* baseline (0000) bir kez additive uygulanır — CREATE'ler IF NOT EXISTS'e
|
||||
* çevrilir, eksik kolonlar snapshot'tan ALTER TABLE ile eklenir — ve baseline
|
||||
* uygulanmış olarak damgalanır. Sonraki migration'lar (0001+) normal akışla gelir.
|
||||
*/
|
||||
import { createClient } from "@libsql/client";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const GOC_KLASORU = process.env.DRIZZLE_DIR ?? join(process.cwd(), "drizzle");
|
||||
const DB_URL = process.env.APP_DB_URL ?? "file:./data/app.db";
|
||||
|
||||
const GOC_TABLOSU_DDL = `CREATE TABLE IF NOT EXISTS "__drizzle_migrations" (
|
||||
id SERIAL PRIMARY KEY,
|
||||
hash text NOT NULL,
|
||||
created_at numeric
|
||||
)`;
|
||||
|
||||
/** Journal'daki her girdiyi { when, tag, hash, ifadeler[] } olarak yükler. */
|
||||
function gocleriOku() {
|
||||
const journal = JSON.parse(
|
||||
readFileSync(join(GOC_KLASORU, "meta", "_journal.json"), "utf8"),
|
||||
);
|
||||
return journal.entries.map((girdi) => {
|
||||
const sql = readFileSync(join(GOC_KLASORU, `${girdi.tag}.sql`), "utf8");
|
||||
return {
|
||||
when: girdi.when,
|
||||
tag: girdi.tag,
|
||||
hash: createHash("sha256").update(sql).digest("hex"),
|
||||
ifadeler: sql
|
||||
.split("--> statement-breakpoint")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function tabloVar(db, ad) {
|
||||
const r = await db.execute({
|
||||
sql: "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
||||
args: [ad],
|
||||
});
|
||||
return r.rows.length > 0;
|
||||
}
|
||||
|
||||
/** Snapshot'taki default değerini SQL literaline çevirir (string zaten tırnaklı gelir). */
|
||||
function sqlVarsayilan(deger) {
|
||||
return typeof deger === "string" ? deger : String(deger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Benimseme adımı: baseline'ı additive uygular.
|
||||
* 1) CREATE TABLE/INDEX ifadeleri IF NOT EXISTS ile koşulur (eksik tablolar gelir).
|
||||
* 2) Mevcut tablolarda eksik kolonlar baseline snapshot'ından ALTER TABLE ile eklenir.
|
||||
*/
|
||||
async function baselineIleUzlastir(db, baseline) {
|
||||
for (const ifade of baseline.ifadeler) {
|
||||
const korunmus = ifade
|
||||
.replace(/^CREATE TABLE `/, "CREATE TABLE IF NOT EXISTS `")
|
||||
.replace(/^CREATE INDEX `/, "CREATE INDEX IF NOT EXISTS `")
|
||||
.replace(/^CREATE UNIQUE INDEX `/, "CREATE UNIQUE INDEX IF NOT EXISTS `");
|
||||
await db.execute(korunmus);
|
||||
}
|
||||
|
||||
const snapshot = JSON.parse(
|
||||
readFileSync(join(GOC_KLASORU, "meta", "0000_snapshot.json"), "utf8"),
|
||||
);
|
||||
for (const tablo of Object.values(snapshot.tables)) {
|
||||
const mevcut = await db.execute(`PRAGMA table_info(\`${tablo.name}\`)`);
|
||||
const mevcutKolonlar = new Set(mevcut.rows.map((r) => r.name));
|
||||
for (const kolon of Object.values(tablo.columns)) {
|
||||
if (mevcutKolonlar.has(kolon.name)) continue;
|
||||
let ddl = `ALTER TABLE \`${tablo.name}\` ADD COLUMN \`${kolon.name}\` ${kolon.type}`;
|
||||
if (kolon.default !== undefined) {
|
||||
ddl += ` DEFAULT ${sqlVarsayilan(kolon.default)}`;
|
||||
if (kolon.notNull) ddl += " NOT NULL";
|
||||
} else if (kolon.notNull) {
|
||||
// Dolu tabloya default'suz NOT NULL kolon eklenemez (SQLite kısıtı);
|
||||
// nullable ekleyip uyarıyoruz — uygulama zaten değeri hep yazıyor.
|
||||
console.warn(
|
||||
`[db-goc] uyarı: ${tablo.name}.${kolon.name} NOT NULL ama default'suz — nullable eklendi`,
|
||||
);
|
||||
}
|
||||
await db.execute(ddl);
|
||||
console.log(`[db-goc] kolon eklendi: ${tablo.name}.${kolon.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const gocler = gocleriOku();
|
||||
const db = createClient({
|
||||
url: DB_URL,
|
||||
authToken: process.env.APP_DB_AUTH_TOKEN,
|
||||
});
|
||||
|
||||
try {
|
||||
// Benimseme: şema var ama migration kaydı yok → baseline'ı uzlaştır + damgala.
|
||||
const kayitVar = await tabloVar(db, "__drizzle_migrations");
|
||||
if (!kayitVar && (await tabloVar(db, "user"))) {
|
||||
console.log("[db-goc] migration kaydı yok, mevcut şema benimseniyor...");
|
||||
await baselineIleUzlastir(db, gocler[0]);
|
||||
await db.execute(GOC_TABLOSU_DDL);
|
||||
await db.execute({
|
||||
sql: 'INSERT INTO "__drizzle_migrations" (hash, created_at) VALUES (?, ?)',
|
||||
args: [gocler[0].hash, gocler[0].when],
|
||||
});
|
||||
console.log(`[db-goc] baseline damgalandı: ${gocler[0].tag}`);
|
||||
}
|
||||
|
||||
// Normal akış: son uygulanan migration'dan sonrakileri sırayla uygula.
|
||||
await db.execute(GOC_TABLOSU_DDL);
|
||||
const son = await db.execute(
|
||||
'SELECT created_at FROM "__drizzle_migrations" ORDER BY created_at DESC LIMIT 1',
|
||||
);
|
||||
const sonZaman = son.rows.length ? Number(son.rows[0].created_at) : 0;
|
||||
|
||||
for (const goc of gocler) {
|
||||
if (goc.when <= sonZaman) continue;
|
||||
await db.batch(
|
||||
[
|
||||
...goc.ifadeler,
|
||||
{
|
||||
sql: 'INSERT INTO "__drizzle_migrations" (hash, created_at) VALUES (?, ?)',
|
||||
args: [goc.hash, goc.when],
|
||||
},
|
||||
],
|
||||
"write",
|
||||
);
|
||||
console.log(`[db-goc] uygulandı: ${goc.tag}`);
|
||||
}
|
||||
console.log("[db-goc] şema güncel");
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((hata) => {
|
||||
console.error("[db-goc] migration başarısız:", hata);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -9,9 +9,14 @@ for ek in wal shm; do
|
||||
fi
|
||||
done
|
||||
|
||||
# app.db kullanıcı verisi: sadece volume'de yoksa (ilk kurulum) şema tohumu kopyalanır
|
||||
# app.db kullanıcı verisi: sadece volume'de yoksa (ilk kurulum) tohum kopyalanır
|
||||
# (tohum tadimlik_havuzu içeriği de taşır, salt şema değil)
|
||||
if [ ! -s /app/data/app.db ] && [ -f /app/seed/app.db ]; then
|
||||
cp /app/seed/app.db /app/data/
|
||||
fi
|
||||
|
||||
# Volume'deki app.db şeması güncel şemayla eşitlenir: drizzle/ altındaki
|
||||
# migration'lar uygulanır (idempotent; ilk koşuda mevcut şema benimsenir).
|
||||
node /app/scripts/db-goc.mjs
|
||||
|
||||
exec node server.js
|
||||
|
||||
Reference in New Issue
Block a user