- 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>
229 lines
9.5 KiB
TypeScript
229 lines
9.5 KiB
TypeScript
/**
|
||
* 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);
|