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>
This commit is contained in:
228
scripts/eski-arsivle.ts
Normal file
228
scripts/eski-arsivle.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user