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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,3 +43,6 @@ next-env.d.ts
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# YÖK Atlas ham API çekimleri (scripts/refresh.ts; ~yıllık, büyük, yeniden üretilebilir)
|
||||
data/ham/
|
||||
|
||||
230
docs/veri/yillik-guncelleme.md
Normal file
230
docs/veri/yillik-guncelleme.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Yıllık veri güncellemesi (YÖK Atlas → `data/yokatlas.db`)
|
||||
|
||||
Her yıl yerleştirme sonuçları açıklandıktan sonra **bir kez** yapılır. Bu sayfayı
|
||||
yukarıdan aşağıya izleyen biri, betiklerin içini bilmeden güncellemeyi
|
||||
tamamlayabilir. Toplam süre: yaklaşık **30 dakika** (çoğu bekleme ve gözle kontrol).
|
||||
|
||||
> Ürünün bütün değeri bu verinin doğruluğuna dayanır. Yanlış bir taban sıralaması,
|
||||
> bir öğrencinin yanlış tercih yapması demektir. Bu yüzden betikler şüphede
|
||||
> **durur**; "DURDU:" ile başlayan bir mesaj hata değil, emniyet kilididir.
|
||||
> Kilidi aşmaya çalışma — mesajı oku, aşağıdaki "Bir şey durursa" tablosuna bak.
|
||||
|
||||
Örneklerde yeni yıl `2027` diye yazıldı; o yıl hangisiyse onu yaz.
|
||||
|
||||
## 0. Ne zaman?
|
||||
|
||||
- ÖSYM yerleştirme sonuçlarını açıklar (genelde **Ağustos sonu**), YÖK Atlas verisini
|
||||
birkaç gün–birkaç hafta sonra günceller. Tarih her yıl değişir → **doğrulanmalı**.
|
||||
- Erken koşmanın zararı yok: YÖK Atlas henüz güncellenmediyse 3. adım
|
||||
"`… hâlâ 2026 verisi`" diyerek **hiçbir şey yazmadan** durur. Bir hafta sonra tekrar dene.
|
||||
- Dosya indirmen gerekmez. Veri, YÖK Atlas'ın herkese açık tercih kılavuzu
|
||||
servisinden (`https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search`) betikle çekilir:
|
||||
43 istek, aralarında yarım saniye — kaynağı yormaz. Betiği art arda defalarca koşma;
|
||||
ilk çekimden sonra hep kaydedilmiş ham dosyayı (`--ham`) kullan.
|
||||
|
||||
## 1. Hazırlık (5 dk)
|
||||
|
||||
```bash
|
||||
git switch -c veri/2027 # main'de çalışma
|
||||
```
|
||||
|
||||
`src/lib/veri-yillari.ts` dosyasında **tek satırı** değiştir:
|
||||
|
||||
```ts
|
||||
export const SON_YIL = 2027;
|
||||
```
|
||||
|
||||
Betikler ve site yılı yalnız buradan okur. Betiğe verdiğin `--yil` bununla aynı
|
||||
değilse betik durur (yanlış kolona yazmayı önler).
|
||||
|
||||
## 2. Kopya al — asıl dosyaya dokunma (1 dk)
|
||||
|
||||
Dev sunucusu açıksa kapat. Sonra:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/veri-2027
|
||||
sqlite3 data/yokatlas.db ".backup '/tmp/veri-2027/once.db'" # güncelleme ÖNCESİ hâl (geri dönüş + karşılaştırma)
|
||||
cp /tmp/veri-2027/once.db /tmp/veri-2027/yeni.db # üzerinde çalışılacak kopya
|
||||
```
|
||||
|
||||
Bundan sonraki her adım **`/tmp/veri-2027/yeni.db`** üzerinde koşar. `data/yokatlas.db`
|
||||
ancak 8. adımda, her şey yeşilken değişir.
|
||||
|
||||
## 3. Yeni yılı çek (3 dk)
|
||||
|
||||
```bash
|
||||
pnpm refresh --yil 2027 --db /tmp/veri-2027/yeni.db
|
||||
```
|
||||
|
||||
Ne yapar: şemaya `sira2027/puan2027/kontenjan2027/yerlesen2027` kolonlarını ekler,
|
||||
**yıl kilidini** sınar, tüm kılavuzu çeker, ham hâlini `data/ham/2027-<tarih>.jsonl.gz`
|
||||
olarak saklar (git'e girmez), sonra tek seferde DB'ye yazar.
|
||||
|
||||
Görmen gerekenler:
|
||||
|
||||
```
|
||||
Yıl kilidi: basariSirasi1 = sira2026 → 480/480 (%100.0); eksiz basariSirasi = sira2026 → 0/480 (%0.0)
|
||||
Çekilen: 21… kayıt, benzersiz kod: 21…, kodsuz: 0, API toplamı: 21… ← üç sayı aynı
|
||||
Güncellenen: 19…, eklenen: 1…, atlanan: 0
|
||||
```
|
||||
|
||||
- **Yıl kilidi** şunu kanıtlar: servisin "bir önceki yıl" diye verdiği sıralar bizim
|
||||
2026 kolonumuzla birebir aynı **ve** "bu yıl" diye verdikleri ondan farklı. Yani
|
||||
yazacağımız veri gerçekten 2027'dir. (Servisteki `yil` alanına güvenilmez; 2026'da
|
||||
bu alan yüzünden 2025 verisi bozulmak üzereydi.)
|
||||
- "atlanan" 0 değilse satırlar altında listelenir: kaynağın puan türü vermediği yeni
|
||||
programlardır; uydurulmaz. Sayıyı not et.
|
||||
- Bu betik mevcut programların **eski yıl kolonlarına dokunmaz**; dokunursa kendi
|
||||
sağlaması bunu yakalar ve her şeyi geri alır.
|
||||
|
||||
Aynı işlemi servise gitmeden tekrarlamak için (ör. 8. adımda):
|
||||
`pnpm refresh --yil 2027 --db <db> --ham data/ham/2027-<tarih>.jsonl.gz`
|
||||
|
||||
## 4. Geçmiş boşluklarını doldur (1 dk)
|
||||
|
||||
```bash
|
||||
pnpm gecmis-doldur --yil 2027 --db /tmp/veri-2027/yeni.db
|
||||
```
|
||||
|
||||
Kılavuz kodu değişen ya da yeni koda taşınan programların eski yıllarını tamamlar.
|
||||
Yalnız **boş** alanı doldurur; dolu değeri asla ezmez, tahmin yapmaz. Kaynak sırası:
|
||||
servisin kendi geçmiş alanları → servisin bildirdiği eski kod → aynı üniversitede
|
||||
bire bir aynı adlı ve en az bir yılı kanıtla örtüşen tek satır.
|
||||
|
||||
Çıktının sonundaki "**Dolu olup kaynağın FARKLI söylediği alanlar**" listesine bak.
|
||||
Birkaç onluk fark olağandır (kaynak eski sıraları ufak düzeltmelerle yeniden yayımlıyor;
|
||||
2026'da `sira2024` farklarının %95'i on binde birin altındaydı). **Binlerce satırda büyük
|
||||
fark** görürsen dur ve veri mühendisine sor.
|
||||
|
||||
## 5. Kılavuzdan çıkan programlar: önce sadece raporla (1 dk)
|
||||
|
||||
```bash
|
||||
pnpm eski-arsivle --yil 2027 --db /tmp/veri-2027/yeni.db --kuru
|
||||
```
|
||||
|
||||
Hiçbir şey yazmaz. Şunu söyler: bu yılın kılavuzunda **olmayan** kaç satır var ve
|
||||
bunlar siteden kalkarsa hangi `/bolum/...` ve `/universite/...` sayfaları tamamen
|
||||
kapanır (adlarıyla). Bu liste **[BİLAL] kararıdır** — sayfa kapanışı SEO'yu etkiler,
|
||||
gerekirse yönlendirme (redirect) eklenir.
|
||||
|
||||
Onaydan sonra gerçek taşıma (silmez; satırlar `programs_arsiv` tablosuna geçer):
|
||||
|
||||
```bash
|
||||
pnpm eski-arsivle --yil 2027 --db /tmp/veri-2027/yeni.db
|
||||
```
|
||||
|
||||
Neden gerekli: arşivlenmezse eski kodlu satırlar listelerde bir yıl önceki sırayla
|
||||
görünmeye devam eder; öğrenci o kodu tercih ekranında bulamaz, kodu değişen program
|
||||
iki kez listelenir.
|
||||
|
||||
## 6. İndeks + dosyayı toparla (1 dk)
|
||||
|
||||
```bash
|
||||
pnpm db:index --db /tmp/veri-2027/yeni.db --vacuum
|
||||
```
|
||||
|
||||
Son satırda `USING INDEX idx_programs_tur_onlisans_efektif` yazmalı. Yazmıyorsa betik
|
||||
zaten hata verir: sitedeki sıralama aralığı sorguları yavaşlar.
|
||||
|
||||
## 7. Kalite kapısı (1 dk)
|
||||
|
||||
```bash
|
||||
pnpm veri-kalite --yil 2027 --db /tmp/veri-2027/yeni.db --onceki /tmp/veri-2027/once.db --arsivli
|
||||
echo "çıkış kodu: $?" # 0 olmalı
|
||||
```
|
||||
|
||||
(`--arsivli`'yi yalnız 5. adımda gerçek arşivlemeyi yaptıysan ekle.)
|
||||
|
||||
- Her satır `GEÇTİ`, `KALDI`, `UYARI` ya da `bilgi` ile başlar. **Tek bir `KALDI` varsa
|
||||
yayına çıkılmaz**; çıkış kodu 1 olur.
|
||||
- En önemli iki satır:
|
||||
- `önceki DB: geçmiş yıllar korunmuş — … değişen/silinen satır: 0`
|
||||
- `sira2027/sira2026 ∈ [0,5; 2] — … (%9x)` — programların büyük çoğunluğunun sırası
|
||||
bir yılda yarıya inmez, iki katına çıkmaz. Oran düşükse yıllar kaymıştır.
|
||||
- `UYARI`'lar bilgilendirir (ör. ili boş 110 program, yetim net satırları); yeni bir
|
||||
uyarı türü ya da sayıda sıçrama varsa veri mühendisine göster.
|
||||
|
||||
Elle 3 program seç (biri çok bilinen: ör. Boğaziçi Bilgisayar Mühendisliği), YÖK Atlas
|
||||
sitesindeki 2027 taban sırasıyla karşılaştır:
|
||||
|
||||
```bash
|
||||
sqlite3 /tmp/veri-2027/yeni.db "SELECT id, universite, isim, sira2026, sira2027, puan2027 FROM programs WHERE isim LIKE 'Bilgisayar Mühendisliği%' AND universite LIKE 'BOĞAZİÇİ%';"
|
||||
```
|
||||
|
||||
## 8. Yerine koy ve siteyi kontrol et (5 dk)
|
||||
|
||||
```bash
|
||||
cp /tmp/veri-2027/yeni.db data/yokatlas.db
|
||||
rm -f data/yokatlas.db-wal data/yokatlas.db-shm # eski dosyanın artıkları; yenisi temiz (checkpoint yapılmış)
|
||||
pnpm build # katalog sayfaları yeni veriden üretilir; hata vermemeli
|
||||
```
|
||||
|
||||
Netler (yerleşen son kişinin netleri) ayrı bir servisten gelir ve genelde daha geç
|
||||
güncellenir; hazır olduğunda:
|
||||
|
||||
```bash
|
||||
pnpm detay --yil 2027 --db data/yokatlas.db # ~127 istek, ~2 dk; yıl sağlaması tutmazsa durur
|
||||
```
|
||||
|
||||
`--kirilim` bayrağını **kullanma**: kontenjan kırılımını (okul birincisi, şehit-gazi…)
|
||||
sitede okuyan kod yok ve servis bu alanları Eylül 2026'dan beri yayımlamıyor. Bayrak
|
||||
verilse bile alanlar yoksa adım kendini durdurur, mevcut değerlerin üstüne boş yazmaz.
|
||||
|
||||
Yıl değişince koddaki metinler (`"son 6 yıl"`, başlıklardaki yıl vb.) `veri-yillari.ts`
|
||||
sabitlerinden türediği için kendiliğinden güncellenir. Elle yıl yazılmış yer kaldıysa bul:
|
||||
|
||||
```bash
|
||||
grep -rnE "sira20[0-9]{2}|20(2[1-9])[–-]20" src scripts --include='*.ts' --include='*.tsx' | grep -v veri-yillari
|
||||
```
|
||||
|
||||
Tadımlık havuzu ve kayıtlı raporlar eski sıralarla üretilmiştir → yayından sonra prod'da
|
||||
`pnpm tadimlik --force` (Bilal koşar; ayrıntı `scripts/tadimlik-uret.ts` başlığında).
|
||||
|
||||
## 9. Commit ve yayın
|
||||
|
||||
```bash
|
||||
git add src/lib/veri-yillari.ts data/yokatlas.db
|
||||
git commit -m "veri: 2027 yerleştirme verisi"
|
||||
```
|
||||
|
||||
`data/ham/` git'e girmez (yerelde sakla; bir sonraki yıl karşılaştırma için işe yarar).
|
||||
Push öncesi `AGENTS.md`'deki "Push öncesi build zorunlu" kuralı geçerli. Yayın kararı Bilal'in.
|
||||
|
||||
## Geri alma
|
||||
|
||||
| Ne zaman | Nasıl | Süre |
|
||||
|---|---|---|
|
||||
| 8. adımdan önce | Hiçbir şey yapma; `data/yokatlas.db` hiç değişmedi. `/tmp/veri-2027/` silinebilir. | 0 |
|
||||
| 8. adımdan sonra, commit'ten önce | `git checkout -- data/yokatlas.db data/yokatlas.db-wal data/yokatlas.db-shm` | 1 dk |
|
||||
| Commit/yayından sonra | `git revert <commit>` + yeniden yayın. `SON_YIL` da aynı commit'te geri döner; site bir önceki yılın verisiyle tutarlı çalışır. | 10 dk |
|
||||
| Yalnız arşivlemeyi geri almak | `INSERT INTO programs SELECT * FROM programs_arsiv; DELETE FROM programs_arsiv;` (kopyada dene, sonra `pnpm db:index`). Netler yeni kodda kalır; zararsız. | 5 dk |
|
||||
|
||||
`/tmp/veri-2027/once.db` dosyasını yayından bir hafta sonrasına kadar silme.
|
||||
|
||||
## Bir şey durursa
|
||||
|
||||
| Mesaj | Anlamı | Ne yap |
|
||||
|---|---|---|
|
||||
| `--yil … ≠ SON_YIL` | 1. adım atlanmış | `veri-yillari.ts`'i güncelle |
|
||||
| `eksiz alanları hâlâ <geçen yıl> verisi` | YÖK Atlas henüz yeni yılı yayımlamamış | Bekle, bir hafta sonra tekrar dene |
|
||||
| `"…1" alanları DB'deki … verisiyle eşleşmiyor` | Servis bir yıl daha ilerlemiş (bir yılı atladık) **ya da** servis biçim değiştirmiş **ya da** yanlış DB | Yazma. Veri mühendisine göster |
|
||||
| `Benzersiz kod sayısı … ≠ API toplamı` | Çekim sırasında servis sayfaları kaydırdı | Birkaç dakika sonra 3. adımı tekrarla (DB'ye bir şey yazılmadı) |
|
||||
| `Aynı üniversitede birden fazla unitur` | Bir kurumun türü iki farklı yazılmış | Listelenen kurumları veri mühendisine göster; `scripts/unitur-onar.ts` |
|
||||
| `Güncel satır sayısı … refresh kaydıyla uyuşmuyor` (arşivle) | refresh yarım kalmış | 3. adımı tekrarla; arşivleme bilerek çalışmıyor |
|
||||
| `veri-kalite` içinde `KALDI` | O kontrol tutmadı | Yayına çıkma; satırdaki sayıyı ve örnek kodları veri mühendisine ilet |
|
||||
|
||||
## Betikler ve dokundukları
|
||||
|
||||
| Betik | Yazar mı? | Ne yazar |
|
||||
|---|---|---|
|
||||
| `scripts/yokatlas-goc.ts` (`pnpm yokatlas:goc`) | evet | Yalnız şema: yıl kolonları, `son_kilavuz_yili`, `eski_kod`, `programs_arsiv`, `veri_meta`. refresh bunu kendisi çağırır. |
|
||||
| `scripts/refresh.ts` | evet | `sira/puan/kontenjan/yerlesen<yıl>`, `son_kilavuz_yili`, `eski_kod`; yeni programlar; ham dosya |
|
||||
| `scripts/gecmis-doldur.ts` | evet | Yalnız **NULL** geçmiş yıl alanları + `eski_kod` |
|
||||
| `scripts/eski-arsivle.ts` | `--kuru` ile hayır | Satır taşıma `programs` → `programs_arsiv`; `netler.program_id` yeni koda |
|
||||
| `scripts/db-index.ts` | evet | İndeks, `--vacuum`, WAL checkpoint |
|
||||
| `scripts/veri-kalite.ts` | **hayır** | Salt okunur; çıkış kodu 0/1 |
|
||||
| `scripts/detay.ts` | evet | `netler`; `--kirilim` ile (alanlar varsa) bir önceki yılın kırılım kolonları |
|
||||
|
||||
Kolon sözlüğü: `son_kilavuz_yili` = programın görüldüğü son tercih kılavuzu (NULL = 2024
|
||||
ve öncesi, bilinmiyor). `eski_kod` = kılavuz kodu değiştiyse bir önceki kod — kayıtlı
|
||||
listelerdeki eski kodları yeni koda çevirmek için `SELECT id FROM programs WHERE eski_kod = ?`.
|
||||
@@ -10,6 +10,11 @@
|
||||
"ingest": "tsx scripts/ingest.ts",
|
||||
"refresh": "tsx scripts/refresh.ts",
|
||||
"detay": "tsx scripts/detay.ts",
|
||||
"yokatlas:goc": "tsx scripts/yokatlas-goc.ts",
|
||||
"gecmis-doldur": "tsx scripts/gecmis-doldur.ts",
|
||||
"eski-arsivle": "tsx scripts/eski-arsivle.ts",
|
||||
"veri-kalite": "tsx scripts/veri-kalite.ts",
|
||||
"db:index": "tsx scripts/db-index.ts",
|
||||
"logolar": "tsx scripts/logo-indir.ts",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
|
||||
@@ -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);
|
||||
`);
|
||||
|
||||
// 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 = db
|
||||
/** 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 COALESCE(sira2025, sira2024) IS NOT NULL
|
||||
AND COALESCE(sira2025, sira2024) BETWEEN ? AND ?
|
||||
ORDER BY COALESCE(sira2025, sira2024) ASC LIMIT 10`,
|
||||
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);
|
||||
.all("SAYISAL", 0, 1000, 50000) as { detail: string }[];
|
||||
return plan.map((p) => p.detail);
|
||||
}
|
||||
|
||||
console.log("Sorgu planı:", JSON.stringify(plan, null, 2));
|
||||
db.close();
|
||||
if (process.argv[1]?.endsWith("db-index.ts")) {
|
||||
calistir(() => {
|
||||
const db = dbAc(dbArgumani());
|
||||
console.log(`Efektif indeks (${EFEKTIF_SIRA_SQL}): ${efektifIndeksiKur(db)}`);
|
||||
|
||||
// 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);
|
||||
`);
|
||||
|
||||
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.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
350
scripts/detay.ts
350
scripts/detay.ts
@@ -1,45 +1,71 @@
|
||||
/**
|
||||
* Canlı YÖK Atlas API'sinden program detay verilerini çekip DB'ye işler:
|
||||
* Canlı YÖK Atlas API'sinden program detay verilerini çekip DB'ye işler.
|
||||
* İki BAĞIMSIZ adım:
|
||||
*
|
||||
* 1. Kontenjan türü kırılımı (son yerleştirme yılı, ör. 2025):
|
||||
* Genel / Okul Birincisi / Şehit-Gazi / Deprem / 34 Yaş Üstü Kadın
|
||||
* için kontenjan + yerleşen. Kaynak: POST /api/tercih-kilavuz/search
|
||||
* (gk1/gkY1, obk1/obkY1, sgy1/sgyY1, dprm1/dprmY1, y34_1/y34Y1 —
|
||||
* "1" son yerleştirme yılı demek; kayıttaki `yil` kılavuz yılıdır,
|
||||
* veri yılı = yil - 1).
|
||||
* A) Yerleşen son kişinin netleri (varsayılan; `--netler-atla` ile kapatılır)
|
||||
* Kaynak: POST /api/netler/search → `netler` tablosu. Bu API her satırda
|
||||
* verinin ait olduğu yılı `yil` alanında verir; betik bunu körlemesine
|
||||
* kabul etmez: ilk sayfada `tabanPuan`, DB'deki puan<yil> ile ≥%95
|
||||
* eşleşmiyorsa DURUR (yıl etiketi kaymış demektir).
|
||||
*
|
||||
* 2. Yerleşen son kişinin netleri (yıl bazında, tüm puan türleri):
|
||||
* Kaynak: POST /api/netler/search → `netler` tablosu.
|
||||
* B) Kontenjan türü kırılımı — YALNIZ `--kirilim` bayrağıyla (varsayılan KAPALI)
|
||||
* Genel / Okul Birincisi / Şehit-Gazi / Deprem / 34 Yaş Üstü Kadın için
|
||||
* kontenjan + yerleşen; API'nin `…1` alanları → <yil-1> kolonları.
|
||||
* `--yil`'in kendi kırılım kolonları BİLEREK yok (src/ içinde okuyan kod
|
||||
* yok; KARARLAR 2026-09-21).
|
||||
* Emniyet: (1) refresh ile aynı YIL KİLİDİ; (2) beklenen on alanın HER
|
||||
* BİRİ ilk sayfada en az bir kayıtta bulunmalı — API null alanları hiç
|
||||
* göndermediği için "ilk kayıt" tek başına yanıltır; bir alan ilk sayfanın
|
||||
* tamamında `undefined` ise API o alanı artık yayımlamıyordur ve adım
|
||||
* HİÇBİR ŞEY yazmadan DURUR; (3) yazım `COALESCE(yeni, mevcut)` —
|
||||
* mevcut değerin üstüne asla NULL yazılmaz.
|
||||
* (Eylül 2026: API `gkY1/obk1/…` alanlarını yayımlamıyor → bu adım durur;
|
||||
* 2025 kırılımı DB'de Ağustos 2026 çekiminden duruyor, korunur.)
|
||||
*
|
||||
* Yıl HİÇBİR adımda kılavuz kaydındaki `yil` alanından türetilmez.
|
||||
*
|
||||
* Not: YÖK Atlas 2026'da SPA'ya geçti; eski content/lisans-dynamic/*.php
|
||||
* panelleri (ve onlarla birlikte kadın-erkek dağılımı) yayından kalktı.
|
||||
* Cinsiyet verisi yeni API'lerin hiçbirinde yok.
|
||||
*
|
||||
* Kullanım: npx tsx scripts/detay.ts
|
||||
* Kullanım:
|
||||
* pnpm detay --yil 2026 --db /tmp/deneme.db
|
||||
* pnpm detay --yil 2026 --db /tmp/deneme.db --kirilim --netler-atla
|
||||
*/
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import type Database from "better-sqlite3";
|
||||
import { ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { yilKilidi, type KilavuzKaydi } from "./refresh";
|
||||
import {
|
||||
argOku,
|
||||
bayrakVar,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
kolonAdlari,
|
||||
yilArgumani,
|
||||
type Db,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
const KILAVUZ_API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||
const NETLER_API = "https://yokatlas.yok.gov.tr/api/netler/search";
|
||||
const PAGE_SIZE = 500;
|
||||
const DELAY_MS = 300;
|
||||
const DELAY_MS = 500;
|
||||
|
||||
type KilavuzRecord = {
|
||||
kilavuzKodu: number;
|
||||
eskiKilavuzKodu?: number | null;
|
||||
yil: number;
|
||||
gk1?: number | null;
|
||||
gkY1?: number | null;
|
||||
obk1?: number | null;
|
||||
obkY1?: number | null;
|
||||
sgy1?: number | null;
|
||||
sgyY1?: number | null;
|
||||
dprm1?: number | null;
|
||||
dprmY1?: number | null;
|
||||
y34_1?: number | null;
|
||||
y34Y1?: number | null;
|
||||
};
|
||||
/** API alanı → DB kolon öneki. `…1` = bir önceki yerleştirme yılı. */
|
||||
const KIRILIM_ALANLARI = [
|
||||
["gk1", "gk"],
|
||||
["gkY1", "gkY"],
|
||||
["obk1", "obk"],
|
||||
["obkY1", "obkY"],
|
||||
["sgy1", "sgy"],
|
||||
["sgyY1", "sgyY"],
|
||||
["dprm1", "dprm"],
|
||||
["dprmY1", "dprmY"],
|
||||
["y34_1", "y34k"],
|
||||
["y34Y1", "y34kY"],
|
||||
] as const;
|
||||
|
||||
type KirilimKaydi = KilavuzKaydi & Record<string, unknown>;
|
||||
|
||||
type NetlerRecord = {
|
||||
kilavuzKodu: number;
|
||||
@@ -113,96 +139,9 @@ const NET_COLS = [
|
||||
"ydtYdilNet",
|
||||
] as const;
|
||||
|
||||
async function main() {
|
||||
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
||||
db.pragma("journal_mode = WAL");
|
||||
// ── A) Netler ────────────────────────────────────────────────────────────
|
||||
|
||||
// ── 1. Kontenjan türü kırılımı ─────────────────────────────────────────
|
||||
const first = await fetchPage<KilavuzRecord>(KILAVUZ_API, 0);
|
||||
// API kaydındaki yil kılavuz yılı; "1" ekli alanlar bir önceki
|
||||
// yerleştirme dönemine ait (2026 kılavuzu → 2025 verisi)
|
||||
const veriYili = first.content[0].yil - 1;
|
||||
|
||||
const kontCols = [
|
||||
`gk${veriYili}`,
|
||||
`gkY${veriYili}`,
|
||||
`obk${veriYili}`,
|
||||
`obkY${veriYili}`,
|
||||
`sgy${veriYili}`,
|
||||
`sgyY${veriYili}`,
|
||||
`dprm${veriYili}`,
|
||||
`dprmY${veriYili}`,
|
||||
`y34k${veriYili}`,
|
||||
`y34kY${veriYili}`,
|
||||
];
|
||||
const mevcut = new Set(
|
||||
(db.prepare("PRAGMA table_info(programs)").all() as { name: string }[]).map(
|
||||
(c) => c.name
|
||||
)
|
||||
);
|
||||
for (const col of kontCols) {
|
||||
if (!mevcut.has(col)) {
|
||||
db.exec(`ALTER TABLE programs ADD COLUMN ${col} INTEGER`);
|
||||
}
|
||||
}
|
||||
|
||||
const updateKont = db.prepare(`
|
||||
UPDATE programs SET ${kontCols.map((c) => `${c} = ?`).join(", ")}
|
||||
WHERE id = ?
|
||||
`);
|
||||
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||
|
||||
// Kılavuz kodu yıllar arasında değişebiliyor (eskiKilavuzKodu); netler
|
||||
// API'si yeni kodla döndüğü için eşleşen DB id'sini haritada tutuyoruz.
|
||||
const dbId = new Map<number, string>();
|
||||
let kontUpdated = 0;
|
||||
let kontMissing = 0;
|
||||
|
||||
const processKilavuz = db.transaction((records: KilavuzRecord[]) => {
|
||||
for (const r of records) {
|
||||
if (!r.kilavuzKodu) continue;
|
||||
let id = String(r.kilavuzKodu);
|
||||
if (!exists.get(id)) {
|
||||
const eski = r.eskiKilavuzKodu ? String(r.eskiKilavuzKodu) : null;
|
||||
if (eski && exists.get(eski)) {
|
||||
id = eski;
|
||||
} else {
|
||||
kontMissing++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
dbId.set(r.kilavuzKodu, id);
|
||||
updateKont.run(
|
||||
num(r.gk1),
|
||||
num(r.gkY1),
|
||||
num(r.obk1),
|
||||
num(r.obkY1),
|
||||
num(r.sgy1),
|
||||
num(r.sgyY1),
|
||||
num(r.dprm1),
|
||||
num(r.dprmY1),
|
||||
num(r.y34_1),
|
||||
num(r.y34Y1),
|
||||
id
|
||||
);
|
||||
kontUpdated++;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Kontenjan kırılımı (${veriYili}): ${first.totalElements} kayıt, ${first.totalPages} sayfa…`
|
||||
);
|
||||
processKilavuz(first.content);
|
||||
for (let p = 1; p < first.totalPages; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
processKilavuz((await fetchPage<KilavuzRecord>(KILAVUZ_API, p)).content);
|
||||
process.stdout.write(`\rkontenjan sayfa ${p + 1}/${first.totalPages}`);
|
||||
}
|
||||
console.log(
|
||||
`\nKontenjan kırılımı: ${kontUpdated} güncellendi, ${kontMissing} DB'de yok`
|
||||
);
|
||||
|
||||
// ── 2. Yerleşen son kişinin netleri ────────────────────────────────────
|
||||
async function netlerAdimi(db: Db, yil: number, azamiSayfa: number): Promise<void> {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS netler (
|
||||
program_id TEXT NOT NULL,
|
||||
@@ -214,54 +153,165 @@ async function main() {
|
||||
);
|
||||
`);
|
||||
|
||||
const insertNet = db.prepare(`
|
||||
INSERT OR REPLACE INTO netler (program_id, yil, ${NET_COLS.join(", ")})
|
||||
VALUES (${["?", "?", ...NET_COLS.map(() => "?")].join(", ")})
|
||||
`);
|
||||
const first = await fetchPage<NetlerRecord>(NETLER_API, 0);
|
||||
|
||||
let netRows = 0;
|
||||
// Yıl etiketi sağlaması: netler.tabanPuan(yil=Y) ≈ programs.puanY
|
||||
const programKolonlari = new Set(kolonAdlari(db, "programs"));
|
||||
const puanGetir = new Map<number, Database.Statement<unknown[]>>();
|
||||
let kiyas = 0;
|
||||
let esit = 0;
|
||||
for (const r of first.content) {
|
||||
if (!r.kilavuzKodu || !r.yil || num(r.tabanPuan) == null) continue;
|
||||
if (r.yil < ILK_YIL || r.yil > yil || !programKolonlari.has(`puan${r.yil}`)) continue;
|
||||
let st = puanGetir.get(r.yil);
|
||||
if (!st) {
|
||||
st = db.prepare(`SELECT puan${r.yil} AS p FROM programs WHERE id = ?`);
|
||||
puanGetir.set(r.yil, st);
|
||||
}
|
||||
const satir = st.get(String(r.kilavuzKodu)) as { p: number | null } | undefined;
|
||||
if (!satir || satir.p == null) continue;
|
||||
kiyas++;
|
||||
if (Math.abs(satir.p - (r.tabanPuan as number)) <= 0.01) esit++;
|
||||
}
|
||||
const oran = kiyas ? esit / kiyas : 0;
|
||||
console.log(`Netler yıl sağlaması: tabanPuan = puan<yil> → ${esit}/${kiyas} (%${(oran * 100).toFixed(1)})`);
|
||||
if (kiyas < 50 || oran < 0.95) {
|
||||
dur(
|
||||
"Netler API'sindeki `yil` etiketi DB'deki yıl kolonlarıyla örtüşmüyor " +
|
||||
"(ya da karşılaştıracak veri yok). Netler yazılmadı."
|
||||
);
|
||||
}
|
||||
|
||||
// Mevcut değerin üstüne NULL yazılmaz: çakışmada COALESCE(yeni, eski).
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO netler (program_id, yil, ${NET_COLS.join(", ")})
|
||||
VALUES (${["?", "?", ...NET_COLS.map(() => "?")].join(", ")})
|
||||
ON CONFLICT(program_id, yil) DO UPDATE SET
|
||||
${NET_COLS.map((c) => `${c} = COALESCE(excluded.${c}, ${c})`).join(",\n ")}
|
||||
`);
|
||||
const programVar = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||
|
||||
let yazilan = 0;
|
||||
let programYok = 0;
|
||||
let yilDisi = 0;
|
||||
const processNetler = db.transaction((records: NetlerRecord[]) => {
|
||||
for (const r of records) {
|
||||
if (!r.kilavuzKodu || !r.yil) continue;
|
||||
const id = dbId.get(r.kilavuzKodu) ?? String(r.kilavuzKodu);
|
||||
insertNet.run(
|
||||
if (r.yil < ILK_YIL || r.yil > yil) {
|
||||
yilDisi++;
|
||||
continue;
|
||||
}
|
||||
const id = String(r.kilavuzKodu);
|
||||
// Yetim satır üretme: program DB'de yoksa (refresh koşulmamış / atlanmış) sayıp geç.
|
||||
if (!programVar.get(id)) {
|
||||
programYok++;
|
||||
continue;
|
||||
}
|
||||
upsert.run(
|
||||
id,
|
||||
r.yil,
|
||||
r.puanTuru?.trim() ?? null,
|
||||
...NET_COLS.slice(1).map((c) => num(r[c as keyof NetlerRecord]))
|
||||
);
|
||||
netRows++;
|
||||
yazilan++;
|
||||
}
|
||||
});
|
||||
|
||||
const netFirst = await fetchPage<NetlerRecord>(NETLER_API, 0);
|
||||
console.log(
|
||||
`Netler: ${netFirst.totalElements} kayıt, ${netFirst.totalPages} sayfa…`
|
||||
);
|
||||
processNetler(netFirst.content);
|
||||
for (let p = 1; p < netFirst.totalPages; p++) {
|
||||
const sayfa = Math.min(first.totalPages, azamiSayfa);
|
||||
console.log(`Netler: ${first.totalElements} kayıt, ${first.totalPages} sayfa${sayfa < first.totalPages ? ` (yalnız ilk ${sayfa})` : ""}…`);
|
||||
processNetler(first.content);
|
||||
for (let p = 1; p < sayfa; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
processNetler((await fetchPage<NetlerRecord>(NETLER_API, p)).content);
|
||||
process.stdout.write(`\rnetler sayfa ${p + 1}/${netFirst.totalPages}`);
|
||||
process.stdout.write(`\rnetler sayfa ${p + 1}/${sayfa}`);
|
||||
}
|
||||
console.log(`\nNetler: ${netRows} satır yazıldı`);
|
||||
|
||||
const ozet = db
|
||||
.prepare(
|
||||
`SELECT
|
||||
(SELECT COUNT(*) FROM programs WHERE gkY${veriYili} IS NOT NULL) AS kontlu,
|
||||
(SELECT COUNT(*) FROM netler) AS net,
|
||||
(SELECT COUNT(DISTINCT program_id) FROM netler) AS netli_program`
|
||||
)
|
||||
.get() as { kontlu: number; net: number; netli_program: number };
|
||||
const dagilim = db
|
||||
.prepare("SELECT yil, COUNT(*) AS n FROM netler GROUP BY yil ORDER BY yil")
|
||||
.all() as { yil: number; n: number }[];
|
||||
console.log(
|
||||
`Özet → kontenjan kırılımı olan program: ${ozet.kontlu}, ` +
|
||||
`net satırı: ${ozet.net} (${ozet.netli_program} program)`
|
||||
`\nNetler: ${yazilan} satır yazıldı/güncellendi; programı DB'de olmayan: ${programYok}; yıl aralığı dışı: ${yilDisi}`
|
||||
);
|
||||
db.close();
|
||||
console.log(`Netler yıl dağılımı: ${dagilim.map((d) => `${d.yil}=${d.n}`).join(" ")}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
// ── B) Kontenjan kırılımı ────────────────────────────────────────────────
|
||||
|
||||
async function kirilimAdimi(db: Db, yil: number, azamiSayfa: number): Promise<void> {
|
||||
const veriYili = yil - 1; // `…1` alanları; yıl kilidiyle doğrulanır, r.yil'e bakılmaz
|
||||
const first = await fetchPage<KirilimKaydi>(KILAVUZ_API, 0);
|
||||
yilKilidi(db, yil, first.content);
|
||||
|
||||
const eksik = KIRILIM_ALANLARI.filter(
|
||||
([alan]) => !first.content.some((r) => r[alan] !== undefined)
|
||||
).map(([alan]) => alan);
|
||||
if (eksik.length) {
|
||||
dur(
|
||||
`Kırılım alanları API'de yok (ilk sayfanın tamamında undefined): ${eksik.join(", ")}. ` +
|
||||
`${veriYili} kırılım kolonlarına DOKUNULMADI (mevcut değerler korunuyor).`
|
||||
);
|
||||
}
|
||||
|
||||
const kontCols = KIRILIM_ALANLARI.map(([, onek]) => `${onek}${veriYili}`);
|
||||
const mevcut = new Set(kolonAdlari(db, "programs"));
|
||||
for (const col of kontCols) {
|
||||
if (!mevcut.has(col)) db.exec(`ALTER TABLE programs ADD COLUMN ${col} INTEGER`);
|
||||
}
|
||||
const updateKont = db.prepare(`
|
||||
UPDATE programs SET ${kontCols.map((c) => `${c} = COALESCE(?, ${c})`).join(", ")}
|
||||
WHERE id = ?
|
||||
`);
|
||||
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||
|
||||
let guncellenen = 0;
|
||||
let dbdeYok = 0;
|
||||
const isle = db.transaction((records: KirilimKaydi[]) => {
|
||||
for (const r of records) {
|
||||
if (!r.kilavuzKodu) continue;
|
||||
const id = String(r.kilavuzKodu);
|
||||
if (!exists.get(id)) {
|
||||
dbdeYok++;
|
||||
continue;
|
||||
}
|
||||
updateKont.run(...KIRILIM_ALANLARI.map(([alan]) => num(r[alan])), id);
|
||||
guncellenen++;
|
||||
}
|
||||
});
|
||||
|
||||
const sayfa = Math.min(first.totalPages, azamiSayfa);
|
||||
console.log(`Kontenjan kırılımı (${veriYili}): ${first.totalElements} kayıt, ${sayfa} sayfa…`);
|
||||
isle(first.content);
|
||||
for (let p = 1; p < sayfa; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
isle((await fetchPage<KirilimKaydi>(KILAVUZ_API, p)).content);
|
||||
process.stdout.write(`\rkırılım sayfa ${p + 1}/${sayfa}`);
|
||||
}
|
||||
console.log(`\nKontenjan kırılımı: ${guncellenen} güncellendi, ${dbdeYok} DB'de yok`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const db = dbAc(dbArgumani());
|
||||
// Yalnız deneme için: kaynağı yormadan ilk N sayfayla dur.
|
||||
const azamiSayfa = Number.parseInt(argOku("azami-sayfa") ?? "", 10) || Number.POSITIVE_INFINITY;
|
||||
|
||||
let hata: unknown = null;
|
||||
if (!bayrakVar("netler-atla")) {
|
||||
try {
|
||||
await netlerAdimi(db, yil, azamiSayfa);
|
||||
} catch (err) {
|
||||
hata = err; // adımlar bağımsız: netler dursa da kırılım (istenmişse) denenir
|
||||
console.error(`Netler adımı durdu: ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
}
|
||||
if (bayrakVar("kirilim")) {
|
||||
await kirilimAdimi(db, yil, azamiSayfa);
|
||||
} else {
|
||||
console.log("Kontenjan kırılımı atlandı (yalnız --kirilim ile koşar).");
|
||||
}
|
||||
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
db.close();
|
||||
if (hata) throw hata;
|
||||
}
|
||||
|
||||
if (process.argv[1]?.endsWith("detay.ts")) calistir(main);
|
||||
|
||||
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);
|
||||
302
scripts/gecmis-doldur.ts
Normal file
302
scripts/gecmis-doldur.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Güncel kılavuzdaki programların GEÇMİŞ yıl kolonlarındaki boşlukları doldurur.
|
||||
*
|
||||
* Kurallar (pazarlıksız):
|
||||
* - Yalnız NULL alan doldurulur. Dolu değer ASLA ezilmez; kaynak farklı bir
|
||||
* değer söylüyorsa sayılır ve raporlanır (karar insanındır).
|
||||
* - Tahmin yok, komşu yıldan kopya yok. Kaynak yoksa alan NULL kalır.
|
||||
* - Belirsiz eşleşmede birleştirme yok (sayılır, raporlanır).
|
||||
*
|
||||
* Kaynak önceliği:
|
||||
* 1. API `…1/…2/…3` alanları (ham dosyadan; API'ye gidilmez) → yıl-1/-2/-3
|
||||
* için sıra, puan, genel kontenjan.
|
||||
* 2. `eski_kod` (API `eskiKilavuzKodu`): kodu değişen programın eski satırı
|
||||
* (programs ya da programs_arsiv) → tüm geçmiş yıllar, dört ölçü.
|
||||
* Eski kod hâlâ güncel kılavuzdaysa ya da birden çok yeni program aynı
|
||||
* eski koda işaret ediyorsa (bölünme) → belirsiz, atlanır.
|
||||
* 3. Aynı (üniversite baz adı, isim, tur, onlisans) için TAM 1 güncel + TAM 1
|
||||
* eski satır varsa; ortak yıllarda puan/kontenjan/yerleşen ÇELİŞMİYORSA ve
|
||||
* en az bir ortak değer AYNIYSA (kanıt) → eski satırdan doldurulur, güncel
|
||||
* satıra `eski_kod` yazılır. Kanıtsız ya da çelişkili eşleşme birleştirilmez.
|
||||
*
|
||||
* Kullanım:
|
||||
* pnpm gecmis-doldur --yil 2026 --db /tmp/deneme.db [--ham data/ham/2026-….jsonl.gz] [--kuru]
|
||||
*/
|
||||
import path from "node:path";
|
||||
import type Database from "better-sqlite3";
|
||||
import { ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { gecmisAlanlari, hamOku, sonHamDosya } from "./refresh";
|
||||
import {
|
||||
argOku,
|
||||
bayrakVar,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
kolonAdlari,
|
||||
tabloVar,
|
||||
yilArgumani,
|
||||
YIL_OLCULERI,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
type Satir = Record<string, string | number | null> & { id: string };
|
||||
|
||||
const PUAN_TOLERANS = 0.01;
|
||||
|
||||
class KuruGeriAl extends Error {}
|
||||
|
||||
function ayniMi(olcu: string, a: number, b: number): boolean {
|
||||
return olcu === "puan" ? Math.abs(a - b) <= PUAN_TOLERANS : a === b;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const dbYolu = dbArgumani();
|
||||
const kuru = bayrakVar("kuru");
|
||||
const hamArg = argOku("ham");
|
||||
const hamDosya = hamArg
|
||||
? path.resolve(process.cwd(), hamArg)
|
||||
: sonHamDosya(yil, path.resolve(process.cwd(), "data", "ham"));
|
||||
if (!hamDosya) {
|
||||
dur(`--ham verilmedi ve data/ham altında ${yil}-*.jsonl.gz yok. Önce refresh koş.`);
|
||||
}
|
||||
|
||||
const db = dbAc(dbYolu);
|
||||
const kolonlar = new Set(kolonAdlari(db, "programs"));
|
||||
if (!kolonlar.has("son_kilavuz_yili") || !kolonlar.has(`sira${yil}`)) {
|
||||
dur("Şema eski: önce `pnpm refresh` (ya da `pnpm yokatlas:goc`) koşulmalı.");
|
||||
}
|
||||
const guncelSayisi = (
|
||||
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
|
||||
).n;
|
||||
if (guncelSayisi === 0) dur(`son_kilavuz_yili=${yil} satır yok: önce refresh koşulmalı.`);
|
||||
|
||||
const gecmisYillar: number[] = [];
|
||||
for (let y = ILK_YIL; y < yil; y++) {
|
||||
if (kolonlar.has(`sira${y}`)) gecmisYillar.push(y);
|
||||
}
|
||||
const olculer = YIL_OLCULERI.map(([o]) => o);
|
||||
|
||||
console.log(`DB: ${dbYolu}\nHam: ${hamDosya}${kuru ? "\nKURU KİP: hiçbir şey yazılmayacak" : ""}`);
|
||||
const { kayitlar } = await hamOku(hamDosya, yil);
|
||||
|
||||
const doldurulan = new Map<string, number>(); // "kaynak|kolon" → adet
|
||||
const farkli = new Map<string, { adet: number; ornek: string[] }>();
|
||||
const say = (m: Map<string, number>, k: string) => m.set(k, (m.get(k) ?? 0) + 1);
|
||||
const farkYaz = (kaynak: string, kolon: string, id: string, db_: number, kaynakDeger: number) => {
|
||||
const k = `${kaynak}|${kolon}`;
|
||||
const f = farkli.get(k) ?? { adet: 0, ornek: [] };
|
||||
f.adet++;
|
||||
if (f.ornek.length < 3) f.ornek.push(`${id}: DB=${db_} kaynak=${kaynakDeger}`);
|
||||
farkli.set(k, f);
|
||||
};
|
||||
|
||||
const satirGetir = db.prepare("SELECT * FROM programs WHERE id = ?");
|
||||
const arsivGetir = tabloVar(db, "programs_arsiv")
|
||||
? db.prepare("SELECT * FROM programs_arsiv WHERE id = ?")
|
||||
: null;
|
||||
const setCache = new Map<string, Database.Statement<unknown[]>>();
|
||||
const alanYaz = (id: string, kolon: string, deger: number) => {
|
||||
let st = setCache.get(kolon);
|
||||
if (!st) {
|
||||
// Çifte emniyet: WHERE … IS NULL — dolu değerin üstüne yazılamaz.
|
||||
st = db.prepare(`UPDATE programs SET ${kolon} = ? WHERE id = ? AND ${kolon} IS NULL`);
|
||||
setCache.set(kolon, st);
|
||||
}
|
||||
st.run(deger, id);
|
||||
};
|
||||
|
||||
/** Hedefin NULL alanlarını kaynaktan doldurur; dolu+farklıyı sayar. */
|
||||
const satirdanDoldur = (kaynakAdi: string, hedef: Satir, kaynak: Satir) => {
|
||||
for (const y of gecmisYillar) {
|
||||
for (const olcu of olculer) {
|
||||
const kolon = `${olcu}${y}`;
|
||||
const k = kaynak[kolon] as number | null;
|
||||
if (k == null) continue;
|
||||
const h = hedef[kolon] as number | null;
|
||||
if (h == null) {
|
||||
alanYaz(hedef.id, kolon, k);
|
||||
hedef[kolon] = k;
|
||||
say(doldurulan, `${kaynakAdi}|${kolon}`);
|
||||
} else if (!ayniMi(olcu, h, k)) {
|
||||
farkYaz(kaynakAdi, kolon, hedef.id, h, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ozet = {
|
||||
apiEslesen: 0,
|
||||
eskiKodlu: 0,
|
||||
eskiKodBulunamadi: 0,
|
||||
eskiKodHalaGuncel: 0,
|
||||
eskiKodBolunme: 0,
|
||||
adEslesen: 0,
|
||||
adBelirsiz: 0,
|
||||
adPuanCelisik: 0,
|
||||
adKanitsiz: 0,
|
||||
};
|
||||
const ornekler: Record<string, string[]> = { bolunme: [], halaGuncel: [], adEslesen: [], adCelisik: [], adKanitsiz: [] };
|
||||
const ornekEkle = (k: string, s: string) => {
|
||||
if (ornekler[k].length < 3) ornekler[k].push(s);
|
||||
};
|
||||
|
||||
try {
|
||||
db.transaction(() => {
|
||||
// ── 1. API …1/…2/…3 ────────────────────────────────────────────────
|
||||
for (const r of kayitlar) {
|
||||
if (!r.kilavuzKodu) continue;
|
||||
const hedef = satirGetir.get(String(r.kilavuzKodu)) as Satir | undefined;
|
||||
if (!hedef || hedef.son_kilavuz_yili !== yil) continue;
|
||||
ozet.apiEslesen++;
|
||||
for (const g of gecmisAlanlari(r)) {
|
||||
const y = yil - g.fark;
|
||||
if (!gecmisYillar.includes(y)) continue;
|
||||
for (const olcu of ["sira", "puan", "kontenjan"] as const) {
|
||||
const k = g[olcu];
|
||||
if (k == null) continue;
|
||||
const kolon = `${olcu}${y}`;
|
||||
const h = hedef[kolon] as number | null;
|
||||
if (h == null) {
|
||||
alanYaz(hedef.id, kolon, k);
|
||||
hedef[kolon] = k;
|
||||
say(doldurulan, `1-api|${kolon}`);
|
||||
} else if (!ayniMi(olcu, h, k)) {
|
||||
farkYaz("1-api", kolon, hedef.id, h, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. eski_kod ────────────────────────────────────────────────────
|
||||
const eskiKodlular = db
|
||||
.prepare("SELECT * FROM programs WHERE son_kilavuz_yili = ? AND eski_kod IS NOT NULL")
|
||||
.all(yil) as Satir[];
|
||||
const eskiKullanim = new Map<string, number>();
|
||||
for (const s of eskiKodlular) say(eskiKullanim, s.eski_kod as string);
|
||||
for (const hedef of eskiKodlular) {
|
||||
const eskiId = hedef.eski_kod as string;
|
||||
if ((eskiKullanim.get(eskiId) ?? 0) > 1) {
|
||||
ozet.eskiKodBolunme++;
|
||||
ornekEkle("bolunme", `${hedef.id} ← ${eskiId} (${hedef.isim})`);
|
||||
continue;
|
||||
}
|
||||
const kaynak =
|
||||
(satirGetir.get(eskiId) as Satir | undefined) ??
|
||||
(arsivGetir?.get(eskiId) as Satir | undefined);
|
||||
if (!kaynak) {
|
||||
ozet.eskiKodBulunamadi++;
|
||||
continue;
|
||||
}
|
||||
if (kaynak.son_kilavuz_yili === yil) {
|
||||
ozet.eskiKodHalaGuncel++;
|
||||
ornekEkle("halaGuncel", `${hedef.id} ← ${eskiId} (${kaynak.isim})`);
|
||||
continue;
|
||||
}
|
||||
ozet.eskiKodlu++;
|
||||
satirdanDoldur("2-eski_kod", hedef, kaynak);
|
||||
}
|
||||
|
||||
// ── 3. Benzersiz (universite, isim, tur, onlisans) ─────────────────
|
||||
const sahiplenilmis = new Set(eskiKullanim.keys());
|
||||
const gruplar = new Map<string, { guncel: Satir[]; eski: Satir[] }>();
|
||||
for (const s of db.prepare("SELECT * FROM programs").all() as Satir[]) {
|
||||
// API 2026'da üniversite adına "(İL)" eki koyuyor; eski satırlar eksiz.
|
||||
const uniBaz = String(s.universite).replace(/\s*\([^)]*\)\s*$/, "").trim();
|
||||
const anahtar = JSON.stringify([uniBaz, s.isim, s.tur, s.onlisans]);
|
||||
const g = gruplar.get(anahtar) ?? { guncel: [], eski: [] };
|
||||
if (s.son_kilavuz_yili === yil) g.guncel.push(s);
|
||||
else if (!sahiplenilmis.has(s.id)) g.eski.push(s);
|
||||
gruplar.set(anahtar, g);
|
||||
}
|
||||
const eskiKodYaz = db.prepare(
|
||||
"UPDATE programs SET eski_kod = ? WHERE id = ? AND eski_kod IS NULL"
|
||||
);
|
||||
for (const g of gruplar.values()) {
|
||||
if (g.eski.length === 0 || g.guncel.length === 0) continue;
|
||||
if (g.eski.length !== 1 || g.guncel.length !== 1) {
|
||||
ozet.adBelirsiz++;
|
||||
continue;
|
||||
}
|
||||
const [hedef] = g.guncel;
|
||||
const [kaynak] = g.eski;
|
||||
if (hedef.eski_kod != null) {
|
||||
// API başka bir eski kod söylüyor; ad eşleşmesi onun önüne geçmez.
|
||||
ozet.adBelirsiz++;
|
||||
continue;
|
||||
}
|
||||
// Aynı yılda iki satırda da veri varsa değerler AYNI olmalı (çift satır);
|
||||
// farklıysa bunlar aynı anda var olmuş iki ayrı programdır (ör. 1 kişilik
|
||||
// özel kontenjanlı adaş program). Sıra bilerek dışarıda: 2024 sırası iki
|
||||
// kaynakta farklı tanımlı. Ortak yılı hiç olmayan eşleşme KANITSIZDIR →
|
||||
// birleştirilmez, yalnız sayılır.
|
||||
let kanit = 0;
|
||||
let celisik = false;
|
||||
for (const y of gecmisYillar) {
|
||||
for (const olcu of ["puan", "kontenjan", "yerlesen"]) {
|
||||
const a = hedef[`${olcu}${y}`] as number | null;
|
||||
const b = kaynak[`${olcu}${y}`] as number | null;
|
||||
if (a == null || b == null) continue;
|
||||
if (ayniMi(olcu, a, b)) kanit++;
|
||||
else celisik = true;
|
||||
}
|
||||
}
|
||||
if (celisik) {
|
||||
ozet.adPuanCelisik++;
|
||||
ornekEkle("adCelisik", `${hedef.id} ~ ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
|
||||
continue;
|
||||
}
|
||||
if (kanit === 0) {
|
||||
ozet.adKanitsiz++;
|
||||
ornekEkle("adKanitsiz", `${hedef.id} ? ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
|
||||
continue;
|
||||
}
|
||||
ozet.adEslesen++;
|
||||
ornekEkle("adEslesen", `${hedef.id} ← ${kaynak.id} (${hedef.universite} / ${hedef.isim})`);
|
||||
satirdanDoldur("3-ad", hedef, kaynak);
|
||||
eskiKodYaz.run(kaynak.id, hedef.id);
|
||||
}
|
||||
|
||||
if (kuru) throw new KuruGeriAl();
|
||||
})();
|
||||
} catch (err) {
|
||||
if (!(err instanceof KuruGeriAl)) throw err;
|
||||
}
|
||||
|
||||
// ── Rapor ──────────────────────────────────────────────────────────────
|
||||
console.log(`\nGüncel kılavuzdaki program: ${guncelSayisi}; API kaydıyla eşleşen: ${ozet.apiEslesen}`);
|
||||
console.log(
|
||||
`eski_kod: ${ozet.eskiKodlu} programın eski satırı bulundu ve boşlukları ondan dolduruldu; ` +
|
||||
`eski satır yok: ${ozet.eskiKodBulunamadi}; eski kod hâlâ güncel (atlandı): ${ozet.eskiKodHalaGuncel}; ` +
|
||||
`bölünme/çoklu (atlandı): ${ozet.eskiKodBolunme}`
|
||||
);
|
||||
console.log(
|
||||
`Ad eşleşmesi: ${ozet.adEslesen} birebir; belirsiz (atlandı): ${ozet.adBelirsiz}; ` +
|
||||
`aynı yılda farklı değer = ayrı program (atlandı): ${ozet.adPuanCelisik}; ` +
|
||||
`ortak yılı olmayan, kanıtsız (atlandı): ${ozet.adKanitsiz}`
|
||||
);
|
||||
for (const [k, v] of Object.entries(ornekler)) {
|
||||
if (v.length) console.log(` örnek ${k}: ${v.join(" ; ")}`);
|
||||
}
|
||||
|
||||
console.log(`\nDoldurulan NULL alanlar${kuru ? " (kuru — yazılmadı)" : ""}:`);
|
||||
const toplamlar = new Map<string, number>();
|
||||
for (const [k, n] of [...doldurulan].sort()) {
|
||||
const [kaynak] = k.split("|");
|
||||
toplamlar.set(kaynak, (toplamlar.get(kaynak) ?? 0) + n);
|
||||
console.log(` ${k.replace("|", " ")}: ${n}`);
|
||||
}
|
||||
for (const [kaynak, n] of toplamlar) console.log(` TOPLAM ${kaynak}: ${n}`);
|
||||
if (doldurulan.size === 0) console.log(" (yok)");
|
||||
|
||||
console.log("\nDolu olup kaynağın FARKLI söylediği alanlar (EZİLMEDİ):");
|
||||
for (const [k, f] of [...farkli].sort()) {
|
||||
console.log(` ${k.replace("|", " ")}: ${f.adet} ör. ${f.ornek.join(" ; ")}`);
|
||||
}
|
||||
if (farkli.size === 0) console.log(" (yok)");
|
||||
|
||||
if (!kuru) db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
db.close();
|
||||
}
|
||||
|
||||
if (process.argv[1]?.endsWith("gecmis-doldur.ts")) calistir(main);
|
||||
@@ -1,24 +1,66 @@
|
||||
/**
|
||||
* Canlı YÖK Atlas API'sinden 2025 yerleştirme verisini çekip DB'yi tazeler.
|
||||
* Canlı YÖK Atlas API'sinden SON yerleştirme yılının verisini çekip DB'ye işler.
|
||||
*
|
||||
* Endpoint: POST https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search
|
||||
* (yokatlas-py'nin kullandığı resmî tercih-kılavuz JSON API'si)
|
||||
*
|
||||
* - Mevcut programlarda sira/puan/kontenjan/yerlesen 2025 kolonlarını günceller
|
||||
* - DB'de olmayan (yeni açılan) programları ekler
|
||||
* - Az yerleşeni olan programlarda API basariSirasi döndürmez → NULL kalır
|
||||
* API'nin yıl düzeni (Eylül 2026'da doğrulandı): EKSİZ alanlar (basariSirasi,
|
||||
* minPuan, kontenjan, gkY…) en son yerleştirme yılıdır; `…1` bir önceki,
|
||||
* `…2` iki önceki, `…3` üç önceki yıldır. Kayıttaki `yil` alanı GÜVENİLMEZ
|
||||
* (kılavuz yılı / veri yılı anlamı yıl içinde değişiyor) — bu betik yılı
|
||||
* HİÇBİR ZAMAN oradan türetmez. Yıl `--yil` ile verilir ve YIL KİLİDİ ile
|
||||
* DB'ye karşı doğrulanır.
|
||||
*
|
||||
* Kullanım: npx tsx scripts/refresh.ts
|
||||
* Güvenlik sırası:
|
||||
* 1. `--yil` = SON_YIL, `--db` zorunlu (varsayılan yok).
|
||||
* 2. YIL KİLİDİ: ilk sayfadaki `basariSirasi1` değerleri DB'deki
|
||||
* sira<yil-1> ile ≥%95 eşleşmeli VE eksiz `basariSirasi` ondan farklı
|
||||
* olmalı. Değilse API henüz yeni yıla dönmemiştir (ya da bir yıl daha
|
||||
* ilerlemiştir) → HİÇBİR ŞEY yazılmadan durur.
|
||||
* 3. Tüm sayfalar önce belleğe + ham dosyaya alınır; benzersiz kod sayısı =
|
||||
* `totalElements` değilse (sayfalama kayması) durur.
|
||||
* 4. DB yazımı TEK transaction; önceki yılların kolon sağlaması transaction
|
||||
* içinde karşılaştırılır, fark varsa geri alınır.
|
||||
*
|
||||
* Yazdıkları: sira/puan/kontenjan/yerlesen<yil>, son_kilavuz_yili, eski_kod.
|
||||
* Mevcut satırların ÖNCEKİ yıl kolonlarına dokunmaz (onları yalnız NULL ise
|
||||
* gecmis-doldur.ts doldurur). Yeni program eklerken `…1/…2/…3` alanlarından
|
||||
* önceki üç yılı da yazar. API "veri yok" için 0 döndürür → NULL.
|
||||
*
|
||||
* Kaynağa nazik: sayfa başı 500 kayıt, istekler arası 0,5 sn, tek bağlantı.
|
||||
* `--ham <dosya>` verilirse API'ye HİÇ gidilmez; daha önce kaydedilmiş ham
|
||||
* dosyadan aynı işlem tekrarlanır (kopyada deneme → gerçek DB'ye uygulama).
|
||||
*
|
||||
* Kullanım:
|
||||
* pnpm refresh --yil 2026 --db /tmp/deneme.db
|
||||
* pnpm refresh --yil 2026 --db data/yokatlas.db --ham data/ham/2026-2026-09-21.jsonl.gz
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import readline from "node:readline";
|
||||
import zlib from "node:zlib";
|
||||
import { ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { efektifIndeksiKur } from "./db-index";
|
||||
import { uniAdiNormalize, uniturOnar } from "./unitur-onar";
|
||||
import {
|
||||
argOku,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
metaYaz,
|
||||
yilArgumani,
|
||||
yilKolonlariniGarantiEt,
|
||||
yokatlasGoc,
|
||||
type Db,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||
const PAGE_SIZE = 500;
|
||||
const DELAY_MS = 300;
|
||||
const DELAY_MS = 500;
|
||||
const KILIT_ESIK = 0.95;
|
||||
const KILIT_ASGARI_ORNEK = 50;
|
||||
|
||||
const TUR_MAP: Record<string, string> = {
|
||||
export const TUR_MAP: Record<string, string> = {
|
||||
SAY: "SAYISAL",
|
||||
EA: "EŞİT AĞIRLIK",
|
||||
SÖZ: "SÖZEL",
|
||||
@@ -26,27 +68,75 @@ const TUR_MAP: Record<string, string> = {
|
||||
TYT: "TYT",
|
||||
};
|
||||
|
||||
type ApiRecord = {
|
||||
/** API kaydından betiklerin kullandığı alanlar (ham dosyada kaydın tamamı durur). */
|
||||
export type KilavuzKaydi = {
|
||||
kilavuzKodu: number;
|
||||
eskiKilavuzKodu?: number | null;
|
||||
birimAdi: string;
|
||||
universiteAdi: string;
|
||||
universiteTuru: string | null;
|
||||
ilAdi: string | null;
|
||||
fymkAdi: string | null;
|
||||
universiteTuru?: string | null;
|
||||
ilAdi?: string | null;
|
||||
fymkAdi?: string | null;
|
||||
puanTuru: string;
|
||||
ogrenimSuresi: number | null;
|
||||
ogrenimSuresi?: number | null;
|
||||
birimTuruId: number;
|
||||
basariSirasi?: number | null;
|
||||
minPuan?: number | null;
|
||||
basariSirasi?: number | string | null;
|
||||
minPuan?: number | string | null;
|
||||
kontenjan?: number | null;
|
||||
gkY?: number | null;
|
||||
sgyY?: number | null;
|
||||
dprmY?: number | null;
|
||||
obkY?: number | null;
|
||||
basariSirasi1?: number | string | null;
|
||||
basariSirasi2?: number | string | null;
|
||||
basariSirasi3?: number | string | null;
|
||||
minPuan1?: number | string | null;
|
||||
minPuan2?: number | string | null;
|
||||
minPuan3?: number | string | null;
|
||||
gk1?: number | null;
|
||||
gk2?: number | null;
|
||||
gk3?: number | null;
|
||||
};
|
||||
|
||||
const TUTULAN_ALANLAR: (keyof KilavuzKaydi)[] = [
|
||||
"kilavuzKodu",
|
||||
"eskiKilavuzKodu",
|
||||
"birimAdi",
|
||||
"universiteAdi",
|
||||
"universiteTuru",
|
||||
"ilAdi",
|
||||
"fymkAdi",
|
||||
"puanTuru",
|
||||
"ogrenimSuresi",
|
||||
"birimTuruId",
|
||||
"basariSirasi",
|
||||
"minPuan",
|
||||
"kontenjan",
|
||||
"gkY",
|
||||
"sgyY",
|
||||
"dprmY",
|
||||
"obkY",
|
||||
"basariSirasi1",
|
||||
"basariSirasi2",
|
||||
"basariSirasi3",
|
||||
"minPuan1",
|
||||
"minPuan2",
|
||||
"minPuan3",
|
||||
"gk1",
|
||||
"gk2",
|
||||
"gk3",
|
||||
];
|
||||
|
||||
function inceKayit(ham: Record<string, unknown>): KilavuzKaydi {
|
||||
const ince: Record<string, unknown> = {};
|
||||
for (const alan of TUTULAN_ALANLAR) {
|
||||
if (ham[alan] !== undefined) ince[alan] = ham[alan];
|
||||
}
|
||||
return ince as KilavuzKaydi;
|
||||
}
|
||||
|
||||
type ApiPage = {
|
||||
content: ApiRecord[];
|
||||
content: Record<string, unknown>[];
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
};
|
||||
@@ -83,140 +173,435 @@ async function fetchPage(page: number): Promise<ApiPage> {
|
||||
return (await res.json()) as ApiPage;
|
||||
}
|
||||
|
||||
function yerlesen(r: ApiRecord): number | null {
|
||||
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
|
||||
(x): x is number => typeof x === "number"
|
||||
);
|
||||
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
|
||||
}
|
||||
|
||||
function intOrNull(v: unknown): number | null {
|
||||
/** Pozitif tam sayı; 0 / boş / sayı değil → NULL (API "veri yok"u 0 ile verir). */
|
||||
export function intOrNull(v: unknown): number | null {
|
||||
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||||
return typeof n === "number" && Number.isFinite(n) && n > 0
|
||||
? Math.round(n)
|
||||
: null;
|
||||
}
|
||||
|
||||
function realOrNull(v: unknown): number | null {
|
||||
export function realOrNull(v: unknown): number | null {
|
||||
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||||
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
||||
db.pragma("journal_mode = WAL");
|
||||
function yerlesen(r: KilavuzKaydi): number | null {
|
||||
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
|
||||
(x): x is number => typeof x === "number"
|
||||
);
|
||||
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
|
||||
}
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(programs)").all() as {
|
||||
name: string;
|
||||
}[];
|
||||
const has2025 = cols.some((c) => c.name === "sira2025");
|
||||
if (!has2025) {
|
||||
db.exec(`
|
||||
ALTER TABLE programs ADD COLUMN sira2025 INTEGER;
|
||||
ALTER TABLE programs ADD COLUMN puan2025 REAL;
|
||||
ALTER TABLE programs ADD COLUMN kontenjan2025 INTEGER;
|
||||
ALTER TABLE programs ADD COLUMN yerlesen2025 INTEGER;
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_tur_sira25 ON programs (tur, sira2025);
|
||||
`);
|
||||
/**
|
||||
* API `eskiKilavuzKodu` iki anlamda geliyor: gerçek eski program kodu (9 hane)
|
||||
* ya da yeni açılan programda üniversite öneki (4 hane, ör. 2031). Yalnız
|
||||
* gerçek program kodu biçimindeki ve yeni koddan farklı olanı döndürür.
|
||||
*/
|
||||
export function eskiKod(r: KilavuzKaydi): string | null {
|
||||
if (!r.eskiKilavuzKodu) return null;
|
||||
const eski = String(r.eskiKilavuzKodu);
|
||||
const yeni = String(r.kilavuzKodu);
|
||||
return eski.length === yeni.length && eski !== yeni ? eski : null;
|
||||
}
|
||||
|
||||
/** Yeni programda önceki üç yılın API karşılığı: [yıl farkı, sıra, puan, genel kontenjan]. */
|
||||
export function gecmisAlanlari(
|
||||
r: KilavuzKaydi
|
||||
): { fark: number; sira: number | null; puan: number | null; kontenjan: number | null }[] {
|
||||
return [
|
||||
{ fark: 1, sira: intOrNull(r.basariSirasi1), puan: realOrNull(r.minPuan1), kontenjan: intOrNull(r.gk1) },
|
||||
{ fark: 2, sira: intOrNull(r.basariSirasi2), puan: realOrNull(r.minPuan2), kontenjan: intOrNull(r.gk2) },
|
||||
{ fark: 3, sira: intOrNull(r.basariSirasi3), puan: realOrNull(r.minPuan3), kontenjan: intOrNull(r.gk3) },
|
||||
];
|
||||
}
|
||||
|
||||
// ── Ham dosya ────────────────────────────────────────────────────────────
|
||||
|
||||
/** `--ham` verilmemişse o yılın en yeni ham dosyası (ad sırası = tarih sırası). */
|
||||
export function sonHamDosya(yil: number, dizin: string): string | null {
|
||||
if (!fs.existsSync(dizin)) return null;
|
||||
const adaylar = fs
|
||||
.readdirSync(dizin)
|
||||
.filter((f) => f.startsWith(`${yil}-`) && f.endsWith(".jsonl.gz"))
|
||||
.sort();
|
||||
return adaylar.length ? path.join(dizin, adaylar[adaylar.length - 1]) : null;
|
||||
}
|
||||
|
||||
export type HamOkuma = { toplam: number; kayitlar: KilavuzKaydi[] };
|
||||
|
||||
/** Ham .jsonl.gz → ince kayıtlar. İlk satır `_meta` (yıl doğrulanır). */
|
||||
export async function hamOku(dosya: string, yil: number): Promise<HamOkuma> {
|
||||
if (!fs.existsSync(dosya)) dur(`Ham dosya yok: ${dosya}`);
|
||||
const rl = readline.createInterface({
|
||||
input: fs.createReadStream(dosya).pipe(zlib.createGunzip()),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
let toplam = -1;
|
||||
const kayitlar: KilavuzKaydi[] = [];
|
||||
for await (const satir of rl) {
|
||||
if (!satir) continue;
|
||||
const obj = JSON.parse(satir) as Record<string, unknown>;
|
||||
if (obj._meta) {
|
||||
const meta = obj._meta as { yil: number; totalElements: number };
|
||||
if (meta.yil !== yil) {
|
||||
dur(`Ham dosya ${meta.yil} yılı için çekilmiş, --yil ${yil} verildi.`);
|
||||
}
|
||||
toplam = meta.totalElements;
|
||||
continue;
|
||||
}
|
||||
kayitlar.push(inceKayit(obj));
|
||||
}
|
||||
if (toplam < 0) dur(`Ham dosyada _meta satırı yok: ${dosya}`);
|
||||
return { toplam, kayitlar };
|
||||
}
|
||||
|
||||
// ── Yıl kilidi ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* `basariSirasi1` = DB sira<yil-1> (≥%95) VE eksiz `basariSirasi` ≠ DB
|
||||
* sira<yil-1>. İkisi birlikte "eksiz alanlar gerçekten <yil> verisi" demektir.
|
||||
*/
|
||||
export function yilKilidi(db: Db, yil: number, ornek: KilavuzKaydi[]): void {
|
||||
const onceki = yil - 1;
|
||||
const bul = db.prepare(`SELECT sira${onceki} AS s FROM programs WHERE id = ?`);
|
||||
let kiyas1 = 0;
|
||||
let esit1 = 0;
|
||||
let kiyas0 = 0;
|
||||
let esit0 = 0;
|
||||
for (const r of ornek) {
|
||||
if (!r.kilavuzKodu) continue;
|
||||
let satir = bul.get(String(r.kilavuzKodu)) as { s: number | null } | undefined;
|
||||
const eski = eskiKod(r);
|
||||
if (!satir && eski) satir = bul.get(eski) as typeof satir;
|
||||
if (!satir || satir.s == null) continue;
|
||||
const s1 = intOrNull(r.basariSirasi1);
|
||||
if (s1 != null) {
|
||||
kiyas1++;
|
||||
if (s1 === satir.s) esit1++;
|
||||
}
|
||||
const s0 = intOrNull(r.basariSirasi);
|
||||
if (s0 != null) {
|
||||
kiyas0++;
|
||||
if (s0 === satir.s) esit0++;
|
||||
}
|
||||
}
|
||||
const oran1 = kiyas1 ? esit1 / kiyas1 : 0;
|
||||
const oran0 = kiyas0 ? esit0 / kiyas0 : 0;
|
||||
console.log(
|
||||
`Yıl kilidi: basariSirasi1 = sira${onceki} → ${esit1}/${kiyas1} (%${(oran1 * 100).toFixed(1)}); ` +
|
||||
`eksiz basariSirasi = sira${onceki} → ${esit0}/${kiyas0} (%${(oran0 * 100).toFixed(1)})`
|
||||
);
|
||||
if (kiyas1 < KILIT_ASGARI_ORNEK) {
|
||||
dur(
|
||||
`Yıl kilidi için yeterli karşılaştırma yok (${kiyas1} < ${KILIT_ASGARI_ORNEK}). ` +
|
||||
`DB'de sira${onceki} dolu mu, doğru DB mi?`
|
||||
);
|
||||
}
|
||||
if (oran0 >= 0.5) {
|
||||
dur(
|
||||
`API'nin eksiz alanları hâlâ ${onceki} verisi (DB ile %${(oran0 * 100).toFixed(1)} aynı). ` +
|
||||
`${yil} yerleştirme sonuçları henüz yayımlanmamış; bugün koşmak ${yil} kolonuna ${onceki} verisi yazar.`
|
||||
);
|
||||
}
|
||||
if (oran1 < KILIT_ESIK) {
|
||||
dur(
|
||||
`API'nin "…1" alanları DB'deki ${onceki} verisiyle eşleşmiyor (%${(oran1 * 100).toFixed(1)} < %95). ` +
|
||||
`API yıl düzeni değişmiş olabilir; --yil ${yil} ile yazmak yanlış kolonu doldurur.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ana akış ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function apidenCek(
|
||||
yil: number,
|
||||
hamDizin: string,
|
||||
ilkSayfaKontrol: (ornek: KilavuzKaydi[]) => void
|
||||
): Promise<HamOkuma & { hamDosya: string }> {
|
||||
fs.mkdirSync(hamDizin, { recursive: true });
|
||||
const tarih = new Date().toISOString().slice(0, 10);
|
||||
const hamDosya = path.join(hamDizin, `${yil}-${tarih}.jsonl.gz`);
|
||||
const gecici = `${hamDosya}.tmp`;
|
||||
|
||||
const first = await fetchPage(0);
|
||||
const kayitlar = first.content.map(inceKayit);
|
||||
// Kilit tutmazsa hiçbir şey yazılmadan ve kaynağa 1 istekle durulur.
|
||||
ilkSayfaKontrol(kayitlar);
|
||||
|
||||
console.log(
|
||||
`Toplam ${first.totalElements} kayıt, ${first.totalPages} sayfa çekilecek ` +
|
||||
`(~${Math.ceil((first.totalPages * (DELAY_MS + 1500)) / 1000)} sn)…`
|
||||
);
|
||||
const gzip = zlib.createGzip();
|
||||
const cikti = fs.createWriteStream(gecici);
|
||||
gzip.pipe(cikti);
|
||||
const bitti = new Promise<void>((resolve, reject) => {
|
||||
cikti.on("finish", resolve);
|
||||
cikti.on("error", reject);
|
||||
gzip.on("error", reject);
|
||||
});
|
||||
const yaz = (obj: unknown) => gzip.write(`${JSON.stringify(obj)}\n`);
|
||||
|
||||
yaz({
|
||||
_meta: {
|
||||
yil,
|
||||
cekimTarihi: new Date().toISOString(),
|
||||
kaynak: API,
|
||||
totalElements: first.totalElements,
|
||||
totalPages: first.totalPages,
|
||||
sayfaBoyu: PAGE_SIZE,
|
||||
},
|
||||
});
|
||||
for (const r of first.content) yaz(r);
|
||||
for (let p = 1; p < first.totalPages; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
const page = await fetchPage(p);
|
||||
for (const r of page.content) {
|
||||
yaz(r);
|
||||
kayitlar.push(inceKayit(r));
|
||||
}
|
||||
process.stdout.write(`\rsayfa ${p + 1}/${first.totalPages}`);
|
||||
}
|
||||
console.log();
|
||||
gzip.end();
|
||||
await bitti;
|
||||
fs.renameSync(gecici, hamDosya);
|
||||
console.log(`Ham veri: ${hamDosya}`);
|
||||
return { toplam: first.totalElements, kayitlar, hamDosya };
|
||||
}
|
||||
|
||||
function oncekiYilSaglamasi(db: Db, yil: number, azamiRowid: number): string {
|
||||
const kolonlar: string[] = [];
|
||||
for (let y = ILK_YIL; y < yil; y++) {
|
||||
for (const olcu of ["sira", "puan", "kontenjan", "yerlesen"]) {
|
||||
kolonlar.push(`COUNT(${olcu}${y})`, `TOTAL(${olcu}${y})`);
|
||||
}
|
||||
}
|
||||
const r = db
|
||||
.prepare(`SELECT COUNT(*), ${kolonlar.join(", ")} FROM programs WHERE rowid <= ?`)
|
||||
.raw()
|
||||
.get(azamiRowid) as number[];
|
||||
return r.join("|");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const dbYolu = dbArgumani();
|
||||
const hamArg = argOku("ham");
|
||||
const hamDizin = path.resolve(process.cwd(), argOku("ham-dizin") ?? path.join("data", "ham"));
|
||||
|
||||
const db = dbAc(dbYolu);
|
||||
console.log(`DB: ${dbYolu}\nYıl: ${yil}`);
|
||||
|
||||
// Şema (idempotent). Yeni programlarda …1/…2/…3 yazılacağı için önceki üç
|
||||
// yılın kolonları da garanti edilir (normalde zaten vardır).
|
||||
const eklenen = yokatlasGoc(db, yil);
|
||||
for (let fark = 1; fark <= 3; fark++) {
|
||||
if (yil - fark >= ILK_YIL) eklenen.push(...yilKolonlariniGarantiEt(db, yil - fark));
|
||||
}
|
||||
if (eklenen.length) console.log(`Göç: ${eklenen.join(", ")}`);
|
||||
|
||||
// ── Veri: API ya da ham dosya ──────────────────────────────────────────
|
||||
let toplam: number;
|
||||
let kayitlar: KilavuzKaydi[];
|
||||
let hamDosya: string;
|
||||
if (hamArg) {
|
||||
hamDosya = path.resolve(process.cwd(), hamArg);
|
||||
console.log(`Kaynak: ham dosya (API'ye gidilmiyor) → ${hamDosya}`);
|
||||
({ toplam, kayitlar } = await hamOku(hamDosya, yil));
|
||||
yilKilidi(db, yil, kayitlar.slice(0, PAGE_SIZE));
|
||||
} else {
|
||||
({ toplam, kayitlar, hamDosya } = await apidenCek(yil, hamDizin, (ornek) =>
|
||||
yilKilidi(db, yil, ornek)
|
||||
));
|
||||
}
|
||||
|
||||
// Sorgu yolunun kullandığı ifade indexi; ifade src/lib/db.ts EFEKTIF_SIRA
|
||||
// ile birebir aynı olmalı (bkz. scripts/db-index.ts). sira2025 kolonu
|
||||
// yukarıda garanti edildikten sonra kurulur.
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_programs_tur_onlisans_efektif
|
||||
ON programs (tur, onlisans, COALESCE(sira2025, sira2024));
|
||||
`);
|
||||
// ── Bütünlük: benzersiz kod = totalElements ────────────────────────────
|
||||
const benzersiz = new Map<string, KilavuzKaydi>();
|
||||
let kodsuz = 0;
|
||||
for (const r of kayitlar) {
|
||||
if (!r.kilavuzKodu) {
|
||||
kodsuz++;
|
||||
continue;
|
||||
}
|
||||
benzersiz.set(String(r.kilavuzKodu), r);
|
||||
}
|
||||
console.log(
|
||||
`Çekilen: ${kayitlar.length} kayıt, benzersiz kod: ${benzersiz.size}, kodsuz: ${kodsuz}, API toplamı: ${toplam}`
|
||||
);
|
||||
if (benzersiz.size + kodsuz !== toplam || kayitlar.length !== toplam) {
|
||||
dur(
|
||||
`Benzersiz kod sayısı (${benzersiz.size}) + kodsuz (${kodsuz}) ≠ API toplamı (${toplam}). ` +
|
||||
"Sayfalama sırasında kayıt kaymış/yinelenmiş; DB'ye HİÇBİR ŞEY yazılmadı. Tekrar dene."
|
||||
);
|
||||
}
|
||||
|
||||
// ── Yazım: tek transaction ─────────────────────────────────────────────
|
||||
const update = db.prepare(`
|
||||
UPDATE programs SET sira2025 = ?, puan2025 = ?, kontenjan2025 = ?, yerlesen2025 = ?
|
||||
UPDATE programs
|
||||
SET sira${yil} = ?, puan${yil} = ?, kontenjan${yil} = ?, yerlesen${yil} = ?,
|
||||
son_kilavuz_yili = ?, eski_kod = COALESCE(?, eski_kod)
|
||||
WHERE id = ?
|
||||
`);
|
||||
const gecmisYillar = [1, 2, 3].filter((f) => yil - f >= ILK_YIL);
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO programs (id, isim, universite, unitur, il, fakulte, tur, sure, onlisans,
|
||||
sira2025, puan2025, kontenjan2025, yerlesen2025)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
sira${yil}, puan${yil}, kontenjan${yil}, yerlesen${yil},
|
||||
son_kilavuz_yili, eski_kod
|
||||
${gecmisYillar.map((f) => `, sira${yil - f}, puan${yil - f}, kontenjan${yil - f}`).join("")})
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?${gecmisYillar.map(() => ", ?, ?, ?").join("")})
|
||||
`);
|
||||
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||
|
||||
// Yeni satırda yazım/sözlük tutarlılığı — TAHMİN YOK, yalnız DB'de zaten var
|
||||
// olan yazımı yeniden kullanma:
|
||||
// - unitur: API sözlüğü (DEVLET/VAKIF/KKTC/YURTDIŞI) DB sözlüğünden (VAKIF MYO,
|
||||
// YURTDISI KAMU…) farklı. Aynı üniversitenin mevcut satırlarında TEK bir
|
||||
// unitur varsa o kullanılır; yoksa API değeri olduğu gibi yazılır. Aksi
|
||||
// hâlde aynı kurumda iki unitur oluşur ve uniturOnar haklı olarak durur.
|
||||
// - fakulte: API 2026'da BÜYÜK HARF veriyor; aynı üniversitede büyük harfe
|
||||
// çevrilince aynı olan mevcut yazım varsa o kullanılır.
|
||||
const bazAd = (ad: string) => ad.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
||||
const uniturlar = new Map<string, Set<string>>();
|
||||
const fakulteler = new Map<string, Map<string, string>>();
|
||||
for (const s of db
|
||||
.prepare("SELECT DISTINCT universite, unitur, fakulte FROM programs")
|
||||
.all() as { universite: string; unitur: string | null; fakulte: string | null }[]) {
|
||||
const baz = bazAd(s.universite);
|
||||
if (s.unitur) {
|
||||
for (const k of [s.universite, baz]) {
|
||||
const set = uniturlar.get(k) ?? new Set<string>();
|
||||
set.add(s.unitur);
|
||||
uniturlar.set(k, set);
|
||||
}
|
||||
}
|
||||
if (s.fakulte) {
|
||||
const m = fakulteler.get(baz) ?? new Map<string, string>();
|
||||
const anahtar = s.fakulte.toLocaleUpperCase("tr-TR");
|
||||
// Aynı anahtara iki farklı yazım düşerse belirsiz → yeniden kullanma
|
||||
m.set(anahtar, m.has(anahtar) && m.get(anahtar) !== s.fakulte ? "" : s.fakulte);
|
||||
fakulteler.set(baz, m);
|
||||
}
|
||||
}
|
||||
const uniturSec = (universite: string, api: string | null): string | null => {
|
||||
for (const k of [universite, bazAd(universite)]) {
|
||||
const set = uniturlar.get(k);
|
||||
if (set?.size === 1) return [...set][0];
|
||||
}
|
||||
return api;
|
||||
};
|
||||
const fakulteSec = (universite: string, api: string | null): string | null => {
|
||||
if (!api) return null;
|
||||
const mevcut = fakulteler.get(bazAd(universite))?.get(api.toLocaleUpperCase("tr-TR"));
|
||||
return mevcut || api;
|
||||
};
|
||||
|
||||
let updated = 0;
|
||||
let inserted = 0;
|
||||
let skipped = 0;
|
||||
const atlananlar: string[] = [];
|
||||
|
||||
const first = await fetchPage(0);
|
||||
const totalPages = first.totalPages;
|
||||
console.log(
|
||||
`Toplam ${first.totalElements} kayıt, ${totalPages} sayfa çekilecek…`
|
||||
);
|
||||
db.transaction(() => {
|
||||
const azamiRowid = (
|
||||
db.prepare("SELECT COALESCE(MAX(rowid), 0) AS m FROM programs").get() as { m: number }
|
||||
).m;
|
||||
const once = oncekiYilSaglamasi(db, yil, azamiRowid);
|
||||
|
||||
const processPage = db.transaction((records: ApiRecord[]) => {
|
||||
for (const r of records) {
|
||||
for (const [id, r] of benzersiz) {
|
||||
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
|
||||
if (!r.kilavuzKodu || !tur) {
|
||||
skipped++;
|
||||
// Puan türü yalnız YENİ satır için gerekir; mevcut satırın türü DB'de var.
|
||||
// Kaynak türü vermiyorsa yeni satır UYDURULMAZ, atlanır ve raporlanır.
|
||||
if (!tur && !exists.get(id)) {
|
||||
atlananlar.push(`${id} ${r.universiteAdi} / ${r.birimAdi} (puanTuru="${r.puanTuru}")`);
|
||||
continue;
|
||||
}
|
||||
const id = String(r.kilavuzKodu);
|
||||
const vals = [
|
||||
intOrNull(r.basariSirasi),
|
||||
realOrNull(r.minPuan),
|
||||
intOrNull(r.kontenjan),
|
||||
yerlesen(r),
|
||||
];
|
||||
const eski = eskiKod(r);
|
||||
if (exists.get(id)) {
|
||||
update.run(...vals, id);
|
||||
update.run(...vals, yil, eski, id);
|
||||
updated++;
|
||||
} else {
|
||||
const gecmis = gecmisAlanlari(r).filter((g) => yil - g.fark >= ILK_YIL);
|
||||
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
|
||||
// duplikat kurum adı üretmesin
|
||||
const universite = uniAdiNormalize(r.universiteAdi);
|
||||
insert.run(
|
||||
id,
|
||||
r.birimAdi,
|
||||
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
|
||||
// duplikat kurum adı üretmesin
|
||||
uniAdiNormalize(r.universiteAdi),
|
||||
r.universiteTuru ?? null,
|
||||
universite,
|
||||
uniturSec(universite, r.universiteTuru ?? null),
|
||||
r.ilAdi ?? null,
|
||||
r.fymkAdi ?? null,
|
||||
fakulteSec(universite, r.fymkAdi ?? null),
|
||||
tur,
|
||||
r.ogrenimSuresi ?? null,
|
||||
r.birimTuruId === 47 ? 1 : 0,
|
||||
...vals
|
||||
...vals,
|
||||
yil,
|
||||
eski,
|
||||
...gecmis.flatMap((g) => [g.sira, g.puan, g.kontenjan])
|
||||
);
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
processPage(first.content);
|
||||
for (let p = 1; p < totalPages; p++) {
|
||||
await sleep(DELAY_MS);
|
||||
const page = await fetchPage(p);
|
||||
processPage(page.content);
|
||||
process.stdout.write(`\rsayfa ${p + 1}/${totalPages}`);
|
||||
const sonra = oncekiYilSaglamasi(db, yil, azamiRowid);
|
||||
if (once !== sonra) {
|
||||
dur(
|
||||
"Mevcut satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" +
|
||||
`önce : ${once}\nsonra: ${sonra}`
|
||||
);
|
||||
}
|
||||
const gorulen = (
|
||||
db.prepare("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?").get(yil) as { n: number }
|
||||
).n;
|
||||
const beklenen = toplam - kodsuz - atlananlar.length;
|
||||
if (gorulen !== beklenen) {
|
||||
dur(
|
||||
`son_kilavuz_yili=${yil} satır sayısı ${gorulen}, beklenen ${beklenen} ` +
|
||||
`(API ${toplam} − atlanan ${kodsuz + atlananlar.length}). Geri alındı.`
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
|
||||
const stats = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS toplam,
|
||||
SUM(CASE WHEN sira2025 IS NOT NULL THEN 1 ELSE 0 END) AS sirali
|
||||
FROM programs`
|
||||
)
|
||||
.get() as { toplam: number; sirali: number };
|
||||
metaYaz(db, `refresh_${yil}_api_toplam`, toplam);
|
||||
metaYaz(db, `refresh_${yil}_atlanan`, kodsuz + atlananlar.length);
|
||||
metaYaz(db, `refresh_${yil}_tarih`, new Date().toISOString());
|
||||
metaYaz(db, `refresh_${yil}_ham`, path.basename(hamDosya));
|
||||
})();
|
||||
|
||||
console.log(
|
||||
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${skipped}`
|
||||
);
|
||||
console.log(
|
||||
`DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}`
|
||||
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${kodsuz + atlananlar.length}`
|
||||
);
|
||||
for (const a of atlananlar) console.log(` atlandı: ${a}`);
|
||||
|
||||
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
|
||||
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).
|
||||
db.transaction(() => uniturOnar(db))();
|
||||
|
||||
// Sorgu yolunun ifade indeksi — ifade veri-yillari.ts'ten; eski yılınki DROP.
|
||||
console.log(`Efektif indeks: ${efektifIndeksiKur(db)}`);
|
||||
|
||||
const stats = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS toplam,
|
||||
SUM(son_kilavuz_yili = ${yil}) AS guncel,
|
||||
COUNT(sira${yil}) AS sirali
|
||||
FROM programs`
|
||||
)
|
||||
.get() as { toplam: number; guncel: number; sirali: number };
|
||||
console.log(
|
||||
`DB toplam: ${stats.toplam} program; ${yil} kılavuzunda: ${stats.guncel}; ` +
|
||||
`${yil} sıralaması olan: ${stats.sirali}`
|
||||
);
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
db.close();
|
||||
console.log(
|
||||
`\nSıradaki adım: pnpm gecmis-doldur --yil ${yil} --db <db> --ham ${path.relative(process.cwd(), hamDosya)}`
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
if (process.argv[1]?.endsWith("refresh.ts")) calistir(main);
|
||||
|
||||
228
scripts/veri-kalite.ts
Normal file
228
scripts/veri-kalite.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* yokatlas.db veri kalitesi denetimi — SALT OKUNUR. Yayın öncesi kapı:
|
||||
* bir ZORUNLU kontrol başarısızsa çıkış kodu 1 (CI/runbook durur).
|
||||
* UYARI'lar çıkış kodunu etkilemez; insan gözüyle bakılır.
|
||||
*
|
||||
* Kullanım:
|
||||
* pnpm veri-kalite --yil 2026 --db /tmp/deneme.db [--onceki data/yokatlas.db] [--arsivli]
|
||||
*
|
||||
* --onceki <db> Güncelleme ÖNCESİ DB. Verilirse: iki DB'de de bulunan her
|
||||
* programın önceki yıl kolonları (sira/puan/kontenjan/yerlesen)
|
||||
* birebir aynı mı (dolu değer değişmiş/silinmiş olamaz).
|
||||
* --arsivli programs içinde güncel kılavuzda olmayan satır kalmamış olmalı.
|
||||
*/
|
||||
import path from "node:path";
|
||||
import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari";
|
||||
import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index";
|
||||
import {
|
||||
argOku,
|
||||
bayrakVar,
|
||||
calistir,
|
||||
dbAc,
|
||||
dbArgumani,
|
||||
dur,
|
||||
kolonAdlari,
|
||||
metaOku,
|
||||
tabloVar,
|
||||
yilArgumani,
|
||||
} from "./yokatlas-goc";
|
||||
|
||||
const TURLER = ["SAYISAL", "EŞİT AĞIRLIK", "SÖZEL", "DİL", "TYT"];
|
||||
|
||||
async function main() {
|
||||
const yil = yilArgumani();
|
||||
const dbYolu = dbArgumani();
|
||||
const oncekiArg = argOku("onceki");
|
||||
const arsivli = bayrakVar("arsivli");
|
||||
const db = dbAc(dbYolu, { saltOkunur: true });
|
||||
const onceki = yil - 1;
|
||||
|
||||
let basarisiz = 0;
|
||||
let uyari = 0;
|
||||
const zorunlu = (ad: string, tamam: boolean, detay: string) => {
|
||||
console.log(`${tamam ? "GEÇTİ " : "KALDI "} ${ad} — ${detay}`);
|
||||
if (!tamam) basarisiz++;
|
||||
};
|
||||
const bilgi = (ad: string, sorunlu: boolean, detay: string) => {
|
||||
console.log(`${sorunlu ? "UYARI " : "bilgi "} ${ad} — ${detay}`);
|
||||
if (sorunlu) uyari++;
|
||||
};
|
||||
const sayi = (sql: string, ...args: unknown[]) =>
|
||||
(db.prepare(sql).get(...args) as { n: number }).n;
|
||||
const ornek = (sql: string, ...args: unknown[]) =>
|
||||
(db.prepare(sql).raw().all(...args) as unknown[][]).map((r) => r.join("/")).join(" ; ");
|
||||
|
||||
console.log(`DB: ${dbYolu}\nYıl: ${yil} Efektif sıra: ${EFEKTIF_SIRA_SQL}\n`);
|
||||
|
||||
// 1. Dosya bütünlüğü
|
||||
const butunluk = (db.pragma("integrity_check") as { integrity_check: string }[])
|
||||
.map((r) => r.integrity_check)
|
||||
.join(",");
|
||||
zorunlu("integrity_check", butunluk === "ok", butunluk);
|
||||
|
||||
// 2. Şema
|
||||
const kolonlar = new Set(kolonAdlari(db, "programs"));
|
||||
const gerekli = [`sira${yil}`, `puan${yil}`, `kontenjan${yil}`, `yerlesen${yil}`, "son_kilavuz_yili", "eski_kod"];
|
||||
const eksikKolon = gerekli.filter((k) => !kolonlar.has(k));
|
||||
zorunlu("şema", eksikKolon.length === 0, eksikKolon.length ? `eksik: ${eksikKolon.join(", ")}` : "yıl kolonları + son_kilavuz_yili + eski_kod var");
|
||||
if (eksikKolon.length) {
|
||||
dur("Şema eksik; kalan kontroller koşulamaz.");
|
||||
}
|
||||
|
||||
// 3. Kapsam: güncel satır = API toplamı − atlanan
|
||||
const apiToplam = Number(metaOku(db, `refresh_${yil}_api_toplam`) ?? NaN);
|
||||
const atlanan = Number(metaOku(db, `refresh_${yil}_atlanan`) ?? NaN);
|
||||
const guncel = sayi("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ?", yil);
|
||||
zorunlu(
|
||||
"kapsam",
|
||||
Number.isFinite(apiToplam) && guncel === apiToplam - atlanan,
|
||||
`son_kilavuz_yili=${yil}: ${guncel}; API toplamı ${apiToplam} − atlanan ${atlanan} = ${apiToplam - atlanan}`
|
||||
);
|
||||
|
||||
// 4. Doluluk
|
||||
const sirali = sayi(`SELECT COUNT(sira${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
|
||||
const oncekiSirali = sayi(`SELECT COUNT(sira${onceki}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
|
||||
const doluOran = guncel ? sirali / guncel : 0;
|
||||
zorunlu(
|
||||
`sira${yil} doluluk`,
|
||||
doluOran >= 0.7,
|
||||
`${sirali}/${guncel} (%${(doluOran * 100).toFixed(1)}; eşik %70). Aynı satırlarda sira${onceki} dolu: ${oncekiSirali}`
|
||||
);
|
||||
const puanli = sayi(`SELECT COUNT(puan${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
|
||||
const kontenjanli = sayi(`SELECT COUNT(kontenjan${yil}) AS n FROM programs WHERE son_kilavuz_yili = ?`, yil);
|
||||
bilgi(`${yil} diğer ölçüler`, kontenjanli < guncel * 0.95, `puan dolu ${puanli}, kontenjan dolu ${kontenjanli}, toplam ${guncel}`);
|
||||
|
||||
// 5. Yıllar arası tutarlılık: sira Y / sira Y-1 ∈ [0,5; 2]
|
||||
const ikisiDolu = sayi(`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${onceki} IS NOT NULL`);
|
||||
const aralikta = sayi(
|
||||
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${onceki} IS NOT NULL
|
||||
AND sira${yil} * 1.0 / sira${onceki} BETWEEN 0.5 AND 2`
|
||||
);
|
||||
const aralikOran = ikisiDolu ? aralikta / ikisiDolu : 0;
|
||||
zorunlu(
|
||||
`sira${yil}/sira${onceki} ∈ [0,5; 2]`,
|
||||
ikisiDolu > 0 && aralikOran >= 0.9,
|
||||
`${aralikta}/${ikisiDolu} (%${(aralikOran * 100).toFixed(1)}; eşik %90)`
|
||||
);
|
||||
const ayniSira = sayi(
|
||||
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND sira${yil} = sira${onceki} AND puan${yil} = puan${onceki}`
|
||||
);
|
||||
zorunlu(
|
||||
"yıl kayması (kopya yıl)",
|
||||
ikisiDolu > 0 && ayniSira / ikisiDolu < 0.02,
|
||||
`sıra VE puanı ${onceki} ile birebir aynı satır: ${ayniSira}/${ikisiDolu} (çoksa ${yil} kolonuna ${onceki} verisi yazılmıştır)`
|
||||
);
|
||||
|
||||
// 6. Değer aralıkları (tüm yıllar)
|
||||
const yillar: number[] = [];
|
||||
for (let y = ILK_YIL; y <= yil; y++) if (kolonlar.has(`sira${y}`)) yillar.push(y);
|
||||
const bozukKosul = yillar
|
||||
.map((y) => `sira${y} <= 0 OR sira${y} > 5000000 OR puan${y} <= 0 OR puan${y} > 600 OR kontenjan${y} < 0 OR yerlesen${y} < 0`)
|
||||
.join(" OR ");
|
||||
const bozuk = sayi(`SELECT COUNT(*) AS n FROM programs WHERE ${bozukKosul}`);
|
||||
zorunlu("değer aralığı", bozuk === 0, `0/negatif/aşırı sıra-puan-kontenjan: ${bozuk}${bozuk ? ` ör. ${ornek(`SELECT id FROM programs WHERE ${bozukKosul} LIMIT 3`)}` : ""}`);
|
||||
|
||||
const siraPuanTutarsiz = sayi(
|
||||
`SELECT COUNT(*) AS n FROM programs WHERE sira${yil} IS NOT NULL AND puan${yil} IS NULL`
|
||||
);
|
||||
zorunlu(`sira${yil} var, puan${yil} yok`, siraPuanTutarsiz === 0, `${siraPuanTutarsiz} satır`);
|
||||
|
||||
// 7. Kategorik alanlar
|
||||
const bozukTur = sayi(
|
||||
`SELECT COUNT(*) AS n FROM programs WHERE tur NOT IN (${TURLER.map(() => "?").join(",")}) OR onlisans NOT IN (0, 1)`,
|
||||
...TURLER
|
||||
);
|
||||
zorunlu("tur/onlisans", bozukTur === 0, `geçersiz değer: ${bozukTur}`);
|
||||
const tytTutarsiz = sayi("SELECT COUNT(*) AS n FROM programs WHERE (tur = 'TYT') <> (onlisans = 1) AND son_kilavuz_yili = ?", yil);
|
||||
bilgi("TYT ↔ önlisans", tytTutarsiz > 0, `tur=TYT ile onlisans=1 örtüşmeyen güncel satır: ${tytTutarsiz}${tytTutarsiz ? ` ör. ${ornek("SELECT id, tur, onlisans FROM programs WHERE (tur = 'TYT') <> (onlisans = 1) AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
|
||||
const uniturBos = sayi("SELECT COUNT(*) AS n FROM programs WHERE unitur IS NULL AND son_kilavuz_yili = ?", yil);
|
||||
bilgi("unitur boş", uniturBos > 0, `güncel satırlarda ${uniturBos} (devlet/vakıf filtresinden düşer)${uniturBos ? ` ör. ${ornek("SELECT DISTINCT universite FROM programs WHERE unitur IS NULL AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
|
||||
const uniturlar = ornek("SELECT COALESCE(unitur, '(NULL)'), COUNT(*) FROM programs GROUP BY unitur ORDER BY 2 DESC");
|
||||
bilgi("unitur dağılımı", false, uniturlar);
|
||||
const ilBos = sayi("SELECT COUNT(*) AS n FROM programs WHERE (il IS NULL OR TRIM(il) = '') AND son_kilavuz_yili = ?", yil);
|
||||
bilgi("il boş", ilBos > 0, `güncel satırlarda ${ilBos}${ilBos ? ` ör. ${ornek("SELECT id, universite FROM programs WHERE (il IS NULL OR TRIM(il) = '') AND son_kilavuz_yili = ? LIMIT 3", yil)}` : ""}`);
|
||||
|
||||
// 8. Kontenjan–yerleşen
|
||||
const tasan = sayi(`SELECT COUNT(*) AS n FROM programs WHERE yerlesen${yil} > kontenjan${yil} * 1.5 + 5`);
|
||||
bilgi(`yerlesen${yil} ≫ kontenjan${yil}`, tasan > guncel * 0.02, `yerleşen > 1,5×genel kontenjan + 5: ${tasan} (yerleşen okul birincisi/şehit-gazi/deprem kontenjanlarını da içerir)${tasan ? ` ör. ${ornek(`SELECT id, kontenjan${yil}, yerlesen${yil} FROM programs WHERE yerlesen${yil} > kontenjan${yil} * 1.5 + 5 LIMIT 3`)}` : ""}`);
|
||||
const doluAmaSirasiz = sayi(`SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili = ? AND sira${yil} IS NULL AND yerlesen${yil} > 0`, yil);
|
||||
bilgi(`yerleşen var, sira${yil} yok`, false, `${doluAmaSirasiz} (API az yerleşenli programda sıra vermez; efektif sıra ${onceki}'e düşer)`);
|
||||
|
||||
// 9. Eski/çift satırlar
|
||||
const eskiSatir = sayi("SELECT COUNT(*) AS n FROM programs WHERE son_kilavuz_yili IS NOT ?", yil);
|
||||
if (arsivli) zorunlu("arşiv", eskiSatir === 0, `programs içinde güncel kılavuzda olmayan satır: ${eskiSatir}`);
|
||||
else bilgi("arşivlenmemiş eski satır", eskiSatir > 0, `${eskiSatir} satır güncel kılavuzda yok ama sitede listelenir (efektif sıra ${onceki}'e düşer)`);
|
||||
const ciftAd = sayi(
|
||||
`SELECT COUNT(*) AS n FROM (SELECT 1 FROM programs WHERE son_kilavuz_yili = ?
|
||||
GROUP BY universite, fakulte, isim, tur HAVING COUNT(*) > 1)`,
|
||||
yil
|
||||
);
|
||||
bilgi("aynı adlı güncel program", false, `(universite, fakulte, isim, tur) yinelenen grup: ${ciftAd} (özel kontenjanlı adaş programlar olağandır)${ciftAd ? ` ör. ${ornek("SELECT universite, isim, COUNT(*) FROM programs WHERE son_kilavuz_yili = ? GROUP BY universite, fakulte, isim, tur HAVING COUNT(*) > 1 LIMIT 3", yil)}` : ""}`);
|
||||
const eskiKodCakisma = sayi(
|
||||
"SELECT COUNT(*) AS n FROM (SELECT eski_kod FROM programs WHERE eski_kod IS NOT NULL AND son_kilavuz_yili = ? GROUP BY eski_kod HAVING COUNT(*) > 1)",
|
||||
yil
|
||||
);
|
||||
bilgi("eski_kod paylaşımı (bölünme)", eskiKodCakisma > 0, `birden çok güncel programın işaret ettiği eski kod: ${eskiKodCakisma}`);
|
||||
|
||||
// 10. netler
|
||||
if (tabloVar(db, "netler")) {
|
||||
const arsivVar = tabloVar(db, "programs_arsiv");
|
||||
const yetim = sayi(
|
||||
`SELECT COUNT(*) AS n FROM netler WHERE program_id NOT IN (SELECT id FROM programs)
|
||||
${arsivVar ? "AND program_id NOT IN (SELECT id FROM programs_arsiv)" : ""}`
|
||||
);
|
||||
bilgi("yetim netler", yetim > 0, `ne programs ne arşivde karşılığı olan: ${yetim}`);
|
||||
const netliGuncel = sayi(
|
||||
"SELECT COUNT(DISTINCT n.program_id) AS n FROM netler n JOIN programs p ON p.id = n.program_id WHERE p.son_kilavuz_yili = ?",
|
||||
yil
|
||||
);
|
||||
bilgi("netler kapsamı", false, `net satırı olan güncel program: ${netliGuncel}/${guncel}; yıl dağılımı: ${ornek("SELECT yil, COUNT(*) FROM netler GROUP BY yil ORDER BY yil")}`);
|
||||
}
|
||||
|
||||
// 11. Sorgu planı
|
||||
const plan = efektifSorguPlani(db);
|
||||
zorunlu("sorgu planı", plan.some((s) => s.includes(EFEKTIF_INDEKS)), plan.join(" | "));
|
||||
|
||||
// 12. Önceki DB ile geçmiş yıl karşılaştırması
|
||||
if (oncekiArg) {
|
||||
const oncekiYol = path.resolve(process.cwd(), oncekiArg);
|
||||
db.prepare("ATTACH DATABASE ? AS eski").run(oncekiYol);
|
||||
const eskiKolonlar = new Set(
|
||||
(db.prepare("PRAGMA eski.table_info(programs)").all() as { name: string }[]).map((c) => c.name)
|
||||
);
|
||||
const ortak: string[] = [];
|
||||
for (let y = ILK_YIL; y < yil; y++) {
|
||||
for (const o of ["sira", "puan", "kontenjan", "yerlesen"]) {
|
||||
if (kolonlar.has(`${o}${y}`) && eskiKolonlar.has(`${o}${y}`)) ortak.push(`${o}${y}`);
|
||||
}
|
||||
}
|
||||
const simdi = tabloVar(db, "programs_arsiv")
|
||||
? `(SELECT id, ${ortak.join(", ")} FROM main.programs UNION ALL SELECT id, ${ortak.join(", ")} FROM main.programs_arsiv)`
|
||||
: "main.programs";
|
||||
const kaybolan = sayi(`SELECT COUNT(*) AS n FROM eski.programs e WHERE e.id NOT IN (SELECT id FROM ${simdi})`);
|
||||
zorunlu("önceki DB: satır kaybı", kaybolan === 0, `önceki DB'de olup şimdi ne programs ne arşivde olan: ${kaybolan}`);
|
||||
// Dolu değer değişmiş ya da NULL'a dönmüş olamaz; NULL → dolu (gecmis-doldur) serbest.
|
||||
const degisen = sayi(
|
||||
`SELECT COUNT(*) AS n FROM eski.programs e JOIN ${simdi} s ON s.id = e.id
|
||||
WHERE ${ortak.map((k) => `(e.${k} IS NOT NULL AND e.${k} IS NOT s.${k})`).join(" OR ")}`
|
||||
);
|
||||
zorunlu("önceki DB: geçmiş yıllar korunmuş", degisen === 0, `${ortak.length} kolonda dolu değeri değişen/silinen satır: ${degisen}`);
|
||||
const toplamlar = (tablo: string) =>
|
||||
(db
|
||||
.prepare(
|
||||
`SELECT COUNT(*), TOTAL(sira${onceki}), TOTAL(puan${onceki}), TOTAL(kontenjan${onceki}), TOTAL(yerlesen${onceki}) FROM ${tablo}`
|
||||
)
|
||||
.raw()
|
||||
.get() as number[]).join(" | ");
|
||||
console.log(` ${onceki} sağlaması (count | Σsira | Σpuan | Σkontenjan | Σyerlesen)`);
|
||||
console.log(` önceki DB : ${toplamlar("eski.programs")}`);
|
||||
console.log(` şimdi, önceki DB'deki aynı id'ler : ${toplamlar(`${simdi} WHERE id IN (SELECT id FROM eski.programs)`)}`);
|
||||
console.log(` şimdi, tüm satırlar (yeni dahil) : ${toplamlar(simdi)}`);
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log(`\nSonuç: ${basarisiz} ZORUNLU kontrol kaldı, ${uyari} uyarı.`);
|
||||
if (basarisiz > 0) process.exit(1);
|
||||
}
|
||||
|
||||
if (process.argv[1]?.endsWith("veri-kalite.ts")) calistir(main);
|
||||
267
scripts/yokatlas-goc.ts
Normal file
267
scripts/yokatlas-goc.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user