Files
kolaytercih/scripts/yokatlas-goc.ts
bilalgursen a773ac7beb feat(veri): 2026 yükleme hattı — yıl kilitli refresh, geçmiş doldurma, arşiv, kalite kapısı (P-A)
- 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>
2026-09-21 17:03:01 +03:00

268 lines
8.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
});
}