Files
kolaytercih/scripts/eski-arsivle.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

229 lines
9.5 KiB
TypeScript
Raw 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.
/**
* Güncel tercih kılavuzunda OLMAYAN programları `programs` → `programs_arsiv`
* tablosuna taşır (silmez; aynı şema, aynı değerler). Kodu değişen programların
* çift satırı (eski kod + yeni kod) böylece sorgu koşuluyla değil DB inşasında
* çözülür: site yalnız bu yıl tercih edilebilen programları görür.
*
* `netler.program_id`: arşive giden eski kodun TEK bir güncel karşılığı varsa
* (programs.eski_kod) yeni koda çevrilir; yeni kodda o yıl zaten net satırı
* varsa eski satır olduğu gibi bırakılır (veri silinmez).
*
* ÖNCE `--kuru` ile koş: hiçbir şey yazmaz; kaç satır gideceğini ve hangi bölüm
* / üniversite sayfalarının (slug) tamamen kapanacağını adlarıyla listeler.
* Gerçek koşu [BİLAL] onayı ister — sayfalar siteden kalkar.
*
* Kullanım:
* pnpm eski-arsivle --yil 2026 --db /tmp/deneme.db --kuru
* pnpm eski-arsivle --yil 2026 --db /tmp/deneme.db
*/
import { bolumBazAdi, turkishSlugify, uniSayfaSlug } from "../src/lib/slug";
import {
arsivTablosunuGarantiEt,
bayrakVar,
calistir,
dbAc,
dbArgumani,
dur,
kolonAdlari,
metaOku,
metaYaz,
tabloVar,
yilArgumani,
} from "./yokatlas-goc";
type Ad = { ad: string; n: number };
async function main() {
const yil = yilArgumani();
const dbYolu = dbArgumani();
const kuru = bayrakVar("kuru");
const db = dbAc(dbYolu, { saltOkunur: kuru });
console.log(`DB: ${dbYolu}${kuru ? "\nKURU KİP: hiçbir şey yazılmayacak" : ""}`);
if (!kolonAdlari(db, "programs").includes("son_kilavuz_yili")) {
dur("son_kilavuz_yili kolonu yok: önce refresh koşulmalı.");
}
// ── Emniyet: refresh bu yıl için tam koşmuş olmalı ─────────────────────
const apiToplam = Number(metaOku(db, `refresh_${yil}_api_toplam`) ?? NaN);
const atlanan = Number(metaOku(db, `refresh_${yil}_atlanan`) ?? NaN);
const guncel = (
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
).n;
if (!Number.isFinite(apiToplam) || guncel !== apiToplam - atlanan) {
dur(
`Güncel satır sayısı (${guncel}) refresh kaydıyla (API ${apiToplam} − atlanan ${atlanan}) uyuşmuyor. ` +
"Yarım bir refresh'in üstüne arşivleme yapılmaz — güncel programlar da gidebilir."
);
}
// NOT: `<>` NULL'ları kaçırır; son_kilavuz_yili bilinmeyen (NULL) satırlar da eskidir.
const ESKI = "son_kilavuz_yili IS NOT ?";
const eskiSayi = (
db.prepare(`SELECT COUNT(*) AS n FROM programs WHERE ${ESKI}`).get(yil) as { n: number }
).n;
console.log(`\nGüncel (${yil} kılavuzunda): ${guncel} Arşivlenecek: ${eskiSayi}`);
if (eskiSayi === 0) {
console.log("Arşivlenecek satır yok.");
db.close();
return;
}
const kirilim = db
.prepare(
`SELECT COALESCE(CAST(son_kilavuz_yili AS TEXT), 'bilinmiyor (≤2024)') AS y, COUNT(*) AS n,
COUNT(sira${yil - 1}) AS onceki_sirali
FROM programs WHERE ${ESKI} GROUP BY son_kilavuz_yili ORDER BY son_kilavuz_yili`
)
.all(yil) as { y: string; n: number; onceki_sirali: number }[];
for (const k of kirilim) {
console.log(` son kılavuz yılı ${k.y}: ${k.n} satır (${k.onceki_sirali} tanesinde sira${yil - 1} dolu)`);
}
const halefli = (
db
.prepare(
`SELECT COUNT(*) AS n FROM programs e WHERE e.${ESKI}
AND EXISTS (SELECT 1 FROM programs g WHERE g.eski_kod = e.id AND g.son_kilavuz_yili = ?)`
)
.get(yil, yil) as { n: number }
).n;
console.log(
` yeni kodla devam eden (eski_kod eşleşmesi): ${halefli}; karşılığı olmayan (kapanmış/ad değişmiş): ${eskiSayi - halefli}`
);
// ── SEO: tamamen kaybolan bölüm / üniversite sayfaları ─────────────────
const eskiIsimler = db
.prepare(`SELECT isim AS ad, COUNT(*) AS n FROM programs WHERE ${ESKI} GROUP BY isim`)
.all(yil) as Ad[];
const guncelIsimler = db
.prepare("SELECT DISTINCT isim AS ad FROM programs WHERE son_kilavuz_yili = ?")
.all(yil) as { ad: string }[];
const guncelHamIsim = new Set(guncelIsimler.map((r) => r.ad));
const guncelBolumSlug = new Set(guncelIsimler.map((r) => turkishSlugify(bolumBazAdi(r.ad))));
const kaybolanHamIsim = eskiIsimler.filter((r) => !guncelHamIsim.has(r.ad));
const kaybolanBolum = new Map<string, { ad: string; n: number }>();
for (const r of eskiIsimler) {
const baz = bolumBazAdi(r.ad);
const slug = turkishSlugify(baz);
if (!slug || guncelBolumSlug.has(slug)) continue;
const k = kaybolanBolum.get(slug) ?? { ad: baz, n: 0 };
k.n += r.n;
kaybolanBolum.set(slug, k);
}
const eskiUniler = db
.prepare(`SELECT universite AS ad, COUNT(*) AS n FROM programs WHERE ${ESKI} GROUP BY universite`)
.all(yil) as Ad[];
const guncelUniler = db
.prepare("SELECT DISTINCT universite AS ad FROM programs WHERE son_kilavuz_yili = ?")
.all(yil) as { ad: string }[];
const guncelHamUni = new Set(guncelUniler.map((r) => r.ad));
const guncelUniSlug = new Set(guncelUniler.map((r) => uniSayfaSlug(r.ad)));
const kaybolanHamUni = eskiUniler.filter((r) => !guncelHamUni.has(r.ad));
const kaybolanUni = new Map<string, { ad: string; n: number }>();
for (const r of eskiUniler) {
const slug = uniSayfaSlug(r.ad);
if (!slug || guncelUniSlug.has(slug)) continue;
const k = kaybolanUni.get(slug) ?? { ad: r.ad, n: 0 };
k.n += r.n;
kaybolanUni.set(slug, k);
}
console.log(
`\nTamamen kaybolan ham 'isim' değeri: ${kaybolanHamIsim.length} ` +
`(çoğu burs/dil parantezi varyantı; sayfa kaybı aşağıdaki slug listesidir)`
);
console.log(`KAPANACAK /bolum/<slug> sayfası: ${kaybolanBolum.size}`);
for (const [slug, k] of [...kaybolanBolum].sort((a, b) => a[0].localeCompare(b[0]))) {
console.log(` /bolum/${slug} — ${k.ad} (${k.n} satır)`);
}
console.log(`Tamamen kaybolan ham 'universite' değeri: ${kaybolanHamUni.length}`);
for (const r of kaybolanHamUni) console.log(` ${r.ad} (${r.n} satır)`);
console.log(`KAPANACAK /universite/<slug> sayfası: ${kaybolanUni.size}`);
for (const [slug, k] of [...kaybolanUni].sort((a, b) => a[0].localeCompare(b[0]))) {
console.log(` /universite/${slug} — ${k.ad} (${k.n} satır)`);
}
// ── netler ─────────────────────────────────────────────────────────────
const netlerVar = tabloVar(db, "netler");
if (netlerVar) {
const net = db
.prepare(
`SELECT COUNT(*) AS satir, COUNT(DISTINCT n.program_id) AS program
FROM netler n JOIN programs e ON e.id = n.program_id WHERE e.${ESKI}`
)
.get(yil) as { satir: number; program: number };
console.log(`\nnetler: arşive gidecek programlara bağlı ${net.satir} satır (${net.program} program)`);
}
if (kuru) {
console.log("\nKURU KİP bitti — DB değişmedi.");
db.close();
return;
}
// ── Gerçek taşıma: tek transaction ─────────────────────────────────────
db.transaction(() => {
arsivTablosunuGarantiEt(db);
const kolonlar = kolonAdlari(db, "programs");
const saglama = () =>
(
db
.prepare(
`SELECT COUNT(*) || '|' || TOTAL(sira${yil - 1}) || '|' || TOTAL(puan${yil - 1}) AS s
FROM (SELECT * FROM programs UNION ALL SELECT ${kolonlar.join(", ")} FROM programs_arsiv)`
)
.get() as { s: string }
).s;
const once = saglama();
if (netlerVar) {
// Tek halefli eski kodlar → yeni koda. OR IGNORE: yeni kodda o yıl varsa dokunma.
const r = db
.prepare(
`UPDATE OR IGNORE netler SET program_id = (
SELECT g.id FROM programs g WHERE g.eski_kod = netler.program_id AND g.son_kilavuz_yili = @yil
)
WHERE program_id IN (SELECT id FROM programs WHERE son_kilavuz_yili IS NOT @yil)
AND (SELECT COUNT(*) FROM programs g
WHERE g.eski_kod = netler.program_id AND g.son_kilavuz_yili = @yil) = 1`
)
.run({ yil });
console.log(`\nnetler: ${r.changes} satır yeni koda çevrildi`);
}
const tasinan = db
.prepare(
`INSERT INTO programs_arsiv (${kolonlar.join(", ")})
SELECT ${kolonlar.join(", ")} FROM programs WHERE ${ESKI}`
)
.run(yil).changes;
const cikan = db.prepare(`DELETE FROM programs WHERE ${ESKI}`).run(yil).changes;
if (tasinan !== eskiSayi || cikan !== eskiSayi) {
dur(`Taşınan (${tasinan}) / çıkarılan (${cikan}) ≠ beklenen (${eskiSayi}). Geri alındı.`);
}
const sonra = saglama();
if (once !== sonra) {
dur(`programs ∪ programs_arsiv sağlaması değişti. Geri alındı.\nönce : ${once}\nsonra: ${sonra}`);
}
metaYaz(db, `arsiv_${yil}_tarih`, new Date().toISOString());
metaYaz(db, `arsiv_${yil}_satir`, tasinan);
console.log(`Arşivlendi: ${tasinan} satır → programs_arsiv. Sağlama aynı: ${sonra}`);
})();
if (netlerVar) {
const yetim = (
db
.prepare("SELECT COUNT(*) AS n FROM netler WHERE program_id NOT IN (SELECT id FROM programs)")
.get() as { n: number }
).n;
console.log(`netler: programs'ta karşılığı olmayan satır (arşivdeki programlara ait): ${yetim}`);
}
db.pragma("wal_checkpoint(TRUNCATE)");
db.close();
console.log("\nSıradaki adım: pnpm db:index --db <db> --vacuum sonra pnpm veri-kalite …");
}
if (process.argv[1]?.endsWith("eski-arsivle.ts")) calistir(main);