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:
@@ -1,44 +1,83 @@
|
||||
// Sorgu yolundaki ifade indexi + WAL bakımı. Bir kez çalıştırılır
|
||||
// (pnpm tsx scripts/db-index.ts); ingest/refresh sonrası da güvenle
|
||||
// tekrar çalıştırılabilir (IF NOT EXISTS).
|
||||
// Sorgu yolundaki ifade indexi + WAL bakımı. refresh sonrası (ve istenince tek
|
||||
// başına) çalıştırılır; tekrar çalıştırmak güvenlidir.
|
||||
//
|
||||
// src/lib/db.ts tüm aralık sorgularını COALESCE(sira2025, sira2024)
|
||||
// üzerinden yapar; SQLite ifade indexleri METİNSEL eşleştiği için buradaki
|
||||
// ifade oradaki EFEKTIF_SIRA sabitiyle birebir aynı olmalıdır. Bu index
|
||||
// olmadan her sorgu tur-partisyonunu tarayıp temp B-tree ile sıralıyordu.
|
||||
// src/lib/db.ts tüm aralık sorgularını EFEKTIF_SIRA_SQL üzerinden yapar;
|
||||
// SQLite ifade indexleri METİNSEL eşleştiği için indeks ifadesi
|
||||
// src/lib/veri-yillari.ts'teki sabitle birebir aynı olmalıdır — bu yüzden
|
||||
// buraya elle yazılmaz, import edilir. Bu index olmadan her sorgu
|
||||
// tur-partisyonunu tarayıp temp B-tree ile sıralıyordu.
|
||||
//
|
||||
// Kullanım: pnpm db:index --db data/yokatlas.db [--vacuum]
|
||||
// --vacuum: arşivleme gibi büyük silmelerden sonra dosyayı küçültür (site kapalıyken).
|
||||
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import { EFEKTIF_SIRA_SQL } from "../src/lib/veri-yillari";
|
||||
import { bayrakVar, calistir, dbAc, dbArgumani, dur, type Db } from "./yokatlas-goc";
|
||||
|
||||
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
||||
export const EFEKTIF_INDEKS = "idx_programs_tur_onlisans_efektif";
|
||||
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_tur_onlisans_efektif
|
||||
ON programs (tur, onlisans, COALESCE(sira2025, sira2024));
|
||||
`);
|
||||
/**
|
||||
* Efektif sıra indeksini güncel ifadeyle kurar. Eski yılın ifadesiyle kurulmuş
|
||||
* aynı adlı indeks varsa DROP edip yeniden kurar (yıl değişince ifade değişir).
|
||||
*/
|
||||
export function efektifIndeksiKur(db: Db): "kuruldu" | "yenilendi" | "guncel" {
|
||||
const mevcut = db
|
||||
.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
|
||||
.get(EFEKTIF_INDEKS) as { sql: string } | undefined;
|
||||
let sonuc: "kuruldu" | "yenilendi" | "guncel" = "kuruldu";
|
||||
if (mevcut) {
|
||||
if (mevcut.sql.includes(EFEKTIF_SIRA_SQL)) return "guncel";
|
||||
db.exec(`DROP INDEX ${EFEKTIF_INDEKS}`);
|
||||
sonuc = "yenilendi";
|
||||
}
|
||||
db.exec(
|
||||
`CREATE INDEX ${EFEKTIF_INDEKS} ON programs (tur, onlisans, ${EFEKTIF_SIRA_SQL})`
|
||||
);
|
||||
return sonuc;
|
||||
}
|
||||
|
||||
// Programatik SEO sayfaları (src/lib/katalog.ts) bölüm/üniversite detaylarını
|
||||
// ham ada birebir IN (...) eşleşmesiyle çeker; bu iki index olmadan her
|
||||
// sayfa tam tablo taraması yapar.
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_isim ON programs (isim);
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_universite ON programs (universite);
|
||||
`);
|
||||
/** Sihirbaz/liste aralık sorgusunun planı (src/lib/db.ts dilimAra ile aynı biçim). */
|
||||
export function efektifSorguPlani(db: Db): string[] {
|
||||
const plan = db
|
||||
.prepare(
|
||||
`EXPLAIN QUERY PLAN
|
||||
SELECT id FROM programs
|
||||
WHERE tur = ? AND onlisans = ?
|
||||
AND ${EFEKTIF_SIRA_SQL} IS NOT NULL
|
||||
AND ${EFEKTIF_SIRA_SQL} BETWEEN ? AND ?
|
||||
ORDER BY ${EFEKTIF_SIRA_SQL} ASC LIMIT 10`
|
||||
)
|
||||
.all("SAYISAL", 0, 1000, 50000) as { detail: string }[];
|
||||
return plan.map((p) => p.detail);
|
||||
}
|
||||
|
||||
// Uygulama DB'yi readonly açtığı için checkpoint yapamaz; şişen WAL'i
|
||||
// (okuma başına ekstra maliyet) burada dosyaya geri yaz ve sıfırla.
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
if (process.argv[1]?.endsWith("db-index.ts")) {
|
||||
calistir(() => {
|
||||
const db = dbAc(dbArgumani());
|
||||
console.log(`Efektif indeks (${EFEKTIF_SIRA_SQL}): ${efektifIndeksiKur(db)}`);
|
||||
|
||||
const plan = db
|
||||
.prepare(
|
||||
`EXPLAIN QUERY PLAN
|
||||
SELECT id FROM programs
|
||||
WHERE tur = ? AND onlisans = ?
|
||||
AND COALESCE(sira2025, sira2024) IS NOT NULL
|
||||
AND COALESCE(sira2025, sira2024) BETWEEN ? AND ?
|
||||
ORDER BY COALESCE(sira2025, sira2024) ASC LIMIT 10`,
|
||||
)
|
||||
.all("SAYISAL", 0, 1000, 50000);
|
||||
// Programatik SEO sayfaları (src/lib/katalog.ts) bölüm/üniversite
|
||||
// detaylarını ham ada birebir IN (...) eşleşmesiyle çeker; bu iki index
|
||||
// olmadan her sayfa tam tablo taraması yapar.
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_isim ON programs (isim);
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_universite ON programs (universite);
|
||||
`);
|
||||
|
||||
console.log("Sorgu planı:", JSON.stringify(plan, null, 2));
|
||||
db.close();
|
||||
if (bayrakVar("vacuum")) {
|
||||
db.exec("VACUUM");
|
||||
console.log("VACUUM tamam.");
|
||||
}
|
||||
|
||||
// Uygulama DB'yi readonly açtığı için checkpoint yapamaz; şişen WAL'i
|
||||
// (okuma başına ekstra maliyet) burada dosyaya geri yaz ve sıfırla.
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
|
||||
const plan = efektifSorguPlani(db);
|
||||
console.log("Sorgu planı:");
|
||||
for (const satir of plan) console.log(` ${satir}`);
|
||||
db.close();
|
||||
if (!plan.some((s) => s.includes(EFEKTIF_INDEKS))) {
|
||||
dur(`Sorgu planı ${EFEKTIF_INDEKS} indeksini kullanmıyor.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user