- yokatlas-goc: idempotent şema göçü (yıl kolonları, son_kilavuz_yili, eski_kod, programs_arsiv, veri_meta) + betiklerin ortak yardımcıları - refresh: --yil/--db zorunlu, YIL KİLİDİ (…1 = DB sira<yil-1> ≥%95 ve eksiz ≠), yıl asla r.yil'den türetilmez; ham veri data/ham/*.jsonl.gz, --ham ile API'siz tekrar; benzersiz kod = totalElements; tek transaction + önceki yıl sağlaması; yeni programda …1/2/3 → üç önceki yıl; efektif indeks EFEKTIF_SIRA_SQL ile - gecmis-doldur: yalnız NULL alan; API …1/2/3 → eski_kod → kanıtlı 1:1 ad eşleşmesi - eski-arsivle: güncel kılavuzda olmayanlar → programs_arsiv, netler yeni koda; --kuru - detay: netler bağımsız + yıl sağlaması; kırılım yalnız --kirilim, alan yoksa durur, mevcut değerin üstüne NULL yazılmaz; `yil - 1` varsayımı kalktı - veri-kalite: salt okunur kalite kapısı, zorunlu kontrol kalırsa çıkış kodu 1 - docs/veri/yillik-guncelleme.md: adım adım runbook + geri alma DB commit'lenmedi (arşivleme kararı [Bilal]). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
268 lines
8.7 KiB
TypeScript
268 lines
8.7 KiB
TypeScript
/**
|
||
* yokatlas.db şema göçü — idempotent. Yeni yerleştirme yılı için kolonları,
|
||
* `son_kilavuz_yili` / `eski_kod` kolonlarını, `programs_arsiv` ve `veri_meta`
|
||
* tablolarını garanti eder. Hiçbir mevcut değeri silmez/ezmez.
|
||
*
|
||
* Betiklerin ortak yardımcıları (argüman okuma, DB açma) da buradadır;
|
||
* refresh / gecmis-doldur / eski-arsivle / detay / veri-kalite buradan alır.
|
||
*
|
||
* Kullanım: pnpm yokatlas:goc --yil 2026 --db data/yokatlas.db
|
||
*/
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import Database from "better-sqlite3";
|
||
import { ILK_YIL, SON_YIL } from "../src/lib/veri-yillari";
|
||
|
||
export type Db = Database.Database;
|
||
|
||
/** Her yıl için tutulan dört ölçü ve SQLite tipi. */
|
||
export const YIL_OLCULERI = [
|
||
["sira", "INTEGER"],
|
||
["puan", "REAL"],
|
||
["kontenjan", "INTEGER"],
|
||
["yerlesen", "INTEGER"],
|
||
] as const;
|
||
|
||
// ── Komut satırı ─────────────────────────────────────────────────────────
|
||
|
||
/** `--ad deger` ya da `--ad=deger`. Yoksa undefined. */
|
||
export function argOku(ad: string): string | undefined {
|
||
const argv = process.argv.slice(2);
|
||
for (let i = 0; i < argv.length; i++) {
|
||
if (argv[i] === `--${ad}`) {
|
||
const sonraki = argv[i + 1];
|
||
return sonraki && !sonraki.startsWith("--") ? sonraki : undefined;
|
||
}
|
||
if (argv[i].startsWith(`--${ad}=`)) return argv[i].slice(ad.length + 3);
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
export function bayrakVar(ad: string): boolean {
|
||
return process.argv.slice(2).includes(`--${ad}`);
|
||
}
|
||
|
||
export class DurHatasi extends Error {}
|
||
|
||
/** Güvenlik kilidi: mesajı yazıp betiği durdurmak için fırlatılır. */
|
||
export function dur(mesaj: string): never {
|
||
throw new DurHatasi(mesaj);
|
||
}
|
||
|
||
/** main() sarmalayıcı: DurHatasi → temiz mesaj + çıkış kodu 1. */
|
||
export function calistir(main: () => Promise<void> | void): void {
|
||
Promise.resolve()
|
||
.then(main)
|
||
.catch((err) => {
|
||
if (err instanceof DurHatasi) console.error(`\nDURDU: ${err.message}`);
|
||
else console.error(err);
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* `--yil` ZORUNLU ve `SON_YIL` ile aynı olmalı. Varsayılan yok: betik hangi
|
||
* yılı yazdığını API'den ya da takvimden TAHMİN ETMEZ.
|
||
*/
|
||
export function yilArgumani(): number {
|
||
const ham = argOku("yil");
|
||
if (!ham) {
|
||
dur(
|
||
`--yil zorunlu (ör. --yil ${SON_YIL}). Varsayılan yok; yıl API kaydındaki ` +
|
||
"`yil` alanından da türetilmez."
|
||
);
|
||
}
|
||
const yil = Number.parseInt(ham, 10);
|
||
if (!Number.isInteger(yil) || String(yil) !== ham.trim()) {
|
||
dur(`--yil sayı olmalı: "${ham}"`);
|
||
}
|
||
if (yil !== SON_YIL) {
|
||
dur(
|
||
`--yil ${yil} ≠ SON_YIL ${SON_YIL} (src/lib/veri-yillari.ts). Yeni yıl ` +
|
||
"yükleniyorsa önce SON_YIL'i güncelle; betikler ve site aynı sabiti okur."
|
||
);
|
||
}
|
||
return yil;
|
||
}
|
||
|
||
/** `--db` ZORUNLU; dosya var olmalı. Yanlışlıkla boş DB yaratılmaz. */
|
||
export function dbArgumani(): string {
|
||
const ham = argOku("db");
|
||
if (!ham) {
|
||
dur(
|
||
"--db zorunlu (ör. --db data/yokatlas.db). Önce kopya üzerinde dene: " +
|
||
"cp data/yokatlas.db /tmp/deneme.db"
|
||
);
|
||
}
|
||
const yol = path.resolve(process.cwd(), ham);
|
||
if (!fs.existsSync(yol)) dur(`DB bulunamadı: ${yol}`);
|
||
return yol;
|
||
}
|
||
|
||
export function dbAc(yol: string, secenek: { saltOkunur?: boolean } = {}): Db {
|
||
const db = new Database(yol, {
|
||
readonly: secenek.saltOkunur ?? false,
|
||
fileMustExist: true,
|
||
});
|
||
if (!secenek.saltOkunur) db.pragma("journal_mode = WAL");
|
||
return db;
|
||
}
|
||
|
||
// ── Şema ─────────────────────────────────────────────────────────────────
|
||
|
||
export function tabloVar(db: Db, tablo: string): boolean {
|
||
return (
|
||
db
|
||
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
|
||
.get(tablo) !== undefined
|
||
);
|
||
}
|
||
|
||
export function kolonAdlari(db: Db, tablo: string): string[] {
|
||
return (
|
||
db.prepare(`PRAGMA table_info(${tablo})`).all() as { name: string }[]
|
||
).map((c) => c.name);
|
||
}
|
||
|
||
function yilDogrula(yil: number): void {
|
||
if (!Number.isInteger(yil) || yil < ILK_YIL || yil > 2100) {
|
||
dur(`Geçersiz yıl: ${yil}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* sira/puan/kontenjan/yerlesen<yil> kolonlarını (yoksa) ekler. İdempotent.
|
||
* Kontenjan kırılımı (gk/obk/…) ve kontenjanMeb kolonları BİLEREK eklenmez:
|
||
* `src/` içinde okuyan kod yok (KARARLAR 2026-09-21).
|
||
*/
|
||
export function yilKolonlariniGarantiEt(
|
||
db: Db,
|
||
yil: number,
|
||
tablo = "programs"
|
||
): string[] {
|
||
yilDogrula(yil);
|
||
const mevcut = new Set(kolonAdlari(db, tablo));
|
||
const eklenen: string[] = [];
|
||
for (const [olcu, tip] of YIL_OLCULERI) {
|
||
const kolon = `${olcu}${yil}`;
|
||
if (mevcut.has(kolon)) continue;
|
||
db.exec(`ALTER TABLE ${tablo} ADD COLUMN ${kolon} ${tip}`);
|
||
eklenen.push(kolon);
|
||
}
|
||
return eklenen;
|
||
}
|
||
|
||
/**
|
||
* `son_kilavuz_yili`: programın görüldüğü son tercih kılavuzu yılı.
|
||
* `eski_kod`: kılavuz kodu değiştiyse bir önceki kod (API `eskiKilavuzKodu`
|
||
* ya da gecmis-doldur'un benzersiz ad eşleşmesi).
|
||
*
|
||
* Geri doldurma TEK SEFERLİK — yalnız kolon ilk kez eklenirken koşar:
|
||
* kontenjan2025 dolu → 2025 kılavuzunda vardı. Gerisi NULL (bilinmiyor) kalır.
|
||
*/
|
||
export function kilavuzKolonlariniGarantiEt(db: Db): string[] {
|
||
const mevcut = new Set(kolonAdlari(db, "programs"));
|
||
const eklenen: string[] = [];
|
||
if (!mevcut.has("son_kilavuz_yili")) {
|
||
db.exec("ALTER TABLE programs ADD COLUMN son_kilavuz_yili INTEGER");
|
||
eklenen.push("son_kilavuz_yili");
|
||
if (mevcut.has("kontenjan2025")) {
|
||
const r = db
|
||
.prepare(
|
||
"UPDATE programs SET son_kilavuz_yili = 2025 WHERE kontenjan2025 IS NOT NULL"
|
||
)
|
||
.run();
|
||
console.log(` son_kilavuz_yili geri doldurma (2025): ${r.changes} satır`);
|
||
}
|
||
}
|
||
if (!mevcut.has("eski_kod")) {
|
||
db.exec("ALTER TABLE programs ADD COLUMN eski_kod TEXT");
|
||
eklenen.push("eski_kod");
|
||
}
|
||
return eklenen;
|
||
}
|
||
|
||
/**
|
||
* `programs_arsiv`: programs ile AYNI kolonlar (ad + tip + sıra). Yoksa kurar;
|
||
* varsa programs'a sonradan eklenmiş kolonları ona da ekler. Böylece
|
||
* `INSERT INTO programs_arsiv SELECT * FROM programs` her zaman geçerlidir…
|
||
* yine de taşıma kolon adlarıyla yapılır (bkz. eski-arsivle.ts).
|
||
*/
|
||
export function arsivTablosunuGarantiEt(db: Db): string[] {
|
||
const kolonlar = db.prepare("PRAGMA table_info(programs)").all() as {
|
||
name: string;
|
||
type: string;
|
||
}[];
|
||
if (!tabloVar(db, "programs_arsiv")) {
|
||
const tanim = kolonlar
|
||
.map((c) =>
|
||
c.name === "id"
|
||
? "id TEXT PRIMARY KEY"
|
||
: `${c.name} ${c.type || "TEXT"}`
|
||
)
|
||
.join(",\n ");
|
||
db.exec(`CREATE TABLE programs_arsiv (\n ${tanim}\n)`);
|
||
return ["programs_arsiv"];
|
||
}
|
||
const arsivde = new Set(kolonAdlari(db, "programs_arsiv"));
|
||
const eklenen: string[] = [];
|
||
for (const c of kolonlar) {
|
||
if (arsivde.has(c.name)) continue;
|
||
db.exec(
|
||
`ALTER TABLE programs_arsiv ADD COLUMN ${c.name} ${c.type || "TEXT"}`
|
||
);
|
||
eklenen.push(`programs_arsiv.${c.name}`);
|
||
}
|
||
return eklenen;
|
||
}
|
||
|
||
/** Koşu kayıtları (API toplamı, atlanan, ham dosya…): veri-kalite okur. */
|
||
export function metaTablosunuGarantiEt(db: Db): void {
|
||
db.exec(
|
||
"CREATE TABLE IF NOT EXISTS veri_meta (anahtar TEXT PRIMARY KEY, deger TEXT NOT NULL)"
|
||
);
|
||
}
|
||
|
||
export function metaYaz(db: Db, anahtar: string, deger: string | number): void {
|
||
metaTablosunuGarantiEt(db);
|
||
db.prepare(
|
||
"INSERT INTO veri_meta (anahtar, deger) VALUES (?, ?) " +
|
||
"ON CONFLICT(anahtar) DO UPDATE SET deger = excluded.deger"
|
||
).run(anahtar, String(deger));
|
||
}
|
||
|
||
export function metaOku(db: Db, anahtar: string): string | undefined {
|
||
if (!tabloVar(db, "veri_meta")) return undefined;
|
||
const r = db
|
||
.prepare("SELECT deger FROM veri_meta WHERE anahtar = ?")
|
||
.get(anahtar) as { deger: string } | undefined;
|
||
return r?.deger;
|
||
}
|
||
|
||
/** Tüm göç: yıl kolonları + kılavuz kolonları + arşiv + meta. İdempotent. */
|
||
export function yokatlasGoc(db: Db, yil: number): string[] {
|
||
const eklenen: string[] = [];
|
||
db.transaction(() => {
|
||
eklenen.push(...yilKolonlariniGarantiEt(db, yil));
|
||
eklenen.push(...kilavuzKolonlariniGarantiEt(db));
|
||
eklenen.push(...arsivTablosunuGarantiEt(db));
|
||
metaTablosunuGarantiEt(db);
|
||
})();
|
||
return eklenen;
|
||
}
|
||
|
||
// Doğrudan koşulduğunda
|
||
if (process.argv[1]?.endsWith("yokatlas-goc.ts")) {
|
||
calistir(() => {
|
||
const yil = yilArgumani();
|
||
const db = dbAc(dbArgumani());
|
||
const eklenen = yokatlasGoc(db, yil);
|
||
console.log(
|
||
eklenen.length
|
||
? `Göç tamam. Eklenen: ${eklenen.join(", ")}`
|
||
: "Göç tamam. Şema zaten güncel (değişiklik yok)."
|
||
);
|
||
db.close();
|
||
});
|
||
}
|