merge: veri/2026 — P-0 yıl sözleşmesi + P-A veri betikleri
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,3 +43,6 @@ next-env.d.ts
|
|||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.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",
|
"ingest": "tsx scripts/ingest.ts",
|
||||||
"refresh": "tsx scripts/refresh.ts",
|
"refresh": "tsx scripts/refresh.ts",
|
||||||
"detay": "tsx scripts/detay.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",
|
"logolar": "tsx scripts/logo-indir.ts",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
|
|||||||
@@ -1,44 +1,83 @@
|
|||||||
// Sorgu yolundaki ifade indexi + WAL bakımı. Bir kez çalıştırılır
|
// Sorgu yolundaki ifade indexi + WAL bakımı. refresh sonrası (ve istenince tek
|
||||||
// (pnpm tsx scripts/db-index.ts); ingest/refresh sonrası da güvenle
|
// başına) çalıştırılır; tekrar çalıştırmak güvenlidir.
|
||||||
// tekrar çalıştırılabilir (IF NOT EXISTS).
|
|
||||||
//
|
//
|
||||||
// src/lib/db.ts tüm aralık sorgularını COALESCE(sira2025, sira2024)
|
// src/lib/db.ts tüm aralık sorgularını EFEKTIF_SIRA_SQL üzerinden yapar;
|
||||||
// üzerinden yapar; SQLite ifade indexleri METİNSEL eşleştiği için buradaki
|
// SQLite ifade indexleri METİNSEL eşleştiği için indeks ifadesi
|
||||||
// ifade oradaki EFEKTIF_SIRA sabitiyle birebir aynı olmalıdır. Bu index
|
// src/lib/veri-yillari.ts'teki sabitle birebir aynı olmalıdır — bu yüzden
|
||||||
// olmadan her sorgu tur-partisyonunu tarayıp temp B-tree ile sıralıyordu.
|
// 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 { EFEKTIF_SIRA_SQL } from "../src/lib/veri-yillari";
|
||||||
import Database from "better-sqlite3";
|
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
|
* Efektif sıra indeksini güncel ifadeyle kurar. Eski yılın ifadesiyle kurulmuş
|
||||||
ON programs (tur, onlisans, COALESCE(sira2025, sira2024));
|
* 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ı
|
/** Sihirbaz/liste aralık sorgusunun planı (src/lib/db.ts dilimAra ile aynı biçim). */
|
||||||
// ham ada birebir IN (...) eşleşmesiyle çeker; bu iki index olmadan her
|
export function efektifSorguPlani(db: Db): string[] {
|
||||||
// sayfa tam tablo taraması yapar.
|
const plan = db
|
||||||
db.exec(`
|
.prepare(
|
||||||
CREATE INDEX IF NOT EXISTS idx_programs_isim ON programs (isim);
|
`EXPLAIN QUERY PLAN
|
||||||
CREATE INDEX IF NOT EXISTS idx_programs_universite ON programs (universite);
|
SELECT id FROM programs
|
||||||
`);
|
WHERE tur = ? AND onlisans = ?
|
||||||
|
AND ${EFEKTIF_SIRA_SQL} IS NOT NULL
|
||||||
|
AND ${EFEKTIF_SIRA_SQL} BETWEEN ? AND ?
|
||||||
|
ORDER BY ${EFEKTIF_SIRA_SQL} ASC LIMIT 10`
|
||||||
|
)
|
||||||
|
.all("SAYISAL", 0, 1000, 50000) as { detail: string }[];
|
||||||
|
return plan.map((p) => p.detail);
|
||||||
|
}
|
||||||
|
|
||||||
// Uygulama DB'yi readonly açtığı için checkpoint yapamaz; şişen WAL'i
|
if (process.argv[1]?.endsWith("db-index.ts")) {
|
||||||
// (okuma başına ekstra maliyet) burada dosyaya geri yaz ve sıfırla.
|
calistir(() => {
|
||||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
const db = dbAc(dbArgumani());
|
||||||
|
console.log(`Efektif indeks (${EFEKTIF_SIRA_SQL}): ${efektifIndeksiKur(db)}`);
|
||||||
|
|
||||||
const plan = db
|
// Programatik SEO sayfaları (src/lib/katalog.ts) bölüm/üniversite
|
||||||
.prepare(
|
// detaylarını ham ada birebir IN (...) eşleşmesiyle çeker; bu iki index
|
||||||
`EXPLAIN QUERY PLAN
|
// olmadan her sayfa tam tablo taraması yapar.
|
||||||
SELECT id FROM programs
|
db.exec(`
|
||||||
WHERE tur = ? AND onlisans = ?
|
CREATE INDEX IF NOT EXISTS idx_programs_isim ON programs (isim);
|
||||||
AND COALESCE(sira2025, sira2024) IS NOT NULL
|
CREATE INDEX IF NOT EXISTS idx_programs_universite ON programs (universite);
|
||||||
AND COALESCE(sira2025, sira2024) BETWEEN ? AND ?
|
`);
|
||||||
ORDER BY COALESCE(sira2025, sira2024) ASC LIMIT 10`,
|
|
||||||
)
|
|
||||||
.all("SAYISAL", 0, 1000, 50000);
|
|
||||||
|
|
||||||
console.log("Sorgu planı:", JSON.stringify(plan, null, 2));
|
if (bayrakVar("vacuum")) {
|
||||||
db.close();
|
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):
|
* A) Yerleşen son kişinin netleri (varsayılan; `--netler-atla` ile kapatılır)
|
||||||
* Genel / Okul Birincisi / Şehit-Gazi / Deprem / 34 Yaş Üstü Kadın
|
* Kaynak: POST /api/netler/search → `netler` tablosu. Bu API her satırda
|
||||||
* için kontenjan + yerleşen. Kaynak: POST /api/tercih-kilavuz/search
|
* verinin ait olduğu yılı `yil` alanında verir; betik bunu körlemesine
|
||||||
* (gk1/gkY1, obk1/obkY1, sgy1/sgyY1, dprm1/dprmY1, y34_1/y34Y1 —
|
* kabul etmez: ilk sayfada `tabanPuan`, DB'deki puan<yil> ile ≥%95
|
||||||
* "1" son yerleştirme yılı demek; kayıttaki `yil` kılavuz yılıdır,
|
* eşleşmiyorsa DURUR (yıl etiketi kaymış demektir).
|
||||||
* veri yılı = yil - 1).
|
|
||||||
*
|
*
|
||||||
* 2. Yerleşen son kişinin netleri (yıl bazında, tüm puan türleri):
|
* B) Kontenjan türü kırılımı — YALNIZ `--kirilim` bayrağıyla (varsayılan KAPALI)
|
||||||
* Kaynak: POST /api/netler/search → `netler` tablosu.
|
* 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
|
* 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ı.
|
* 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 type Database from "better-sqlite3";
|
||||||
import 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 KILAVUZ_API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||||
const NETLER_API = "https://yokatlas.yok.gov.tr/api/netler/search";
|
const NETLER_API = "https://yokatlas.yok.gov.tr/api/netler/search";
|
||||||
const PAGE_SIZE = 500;
|
const PAGE_SIZE = 500;
|
||||||
const DELAY_MS = 300;
|
const DELAY_MS = 500;
|
||||||
|
|
||||||
type KilavuzRecord = {
|
/** API alanı → DB kolon öneki. `…1` = bir önceki yerleştirme yılı. */
|
||||||
kilavuzKodu: number;
|
const KIRILIM_ALANLARI = [
|
||||||
eskiKilavuzKodu?: number | null;
|
["gk1", "gk"],
|
||||||
yil: number;
|
["gkY1", "gkY"],
|
||||||
gk1?: number | null;
|
["obk1", "obk"],
|
||||||
gkY1?: number | null;
|
["obkY1", "obkY"],
|
||||||
obk1?: number | null;
|
["sgy1", "sgy"],
|
||||||
obkY1?: number | null;
|
["sgyY1", "sgyY"],
|
||||||
sgy1?: number | null;
|
["dprm1", "dprm"],
|
||||||
sgyY1?: number | null;
|
["dprmY1", "dprmY"],
|
||||||
dprm1?: number | null;
|
["y34_1", "y34k"],
|
||||||
dprmY1?: number | null;
|
["y34Y1", "y34kY"],
|
||||||
y34_1?: number | null;
|
] as const;
|
||||||
y34Y1?: number | null;
|
|
||||||
};
|
type KirilimKaydi = KilavuzKaydi & Record<string, unknown>;
|
||||||
|
|
||||||
type NetlerRecord = {
|
type NetlerRecord = {
|
||||||
kilavuzKodu: number;
|
kilavuzKodu: number;
|
||||||
@@ -113,96 +139,9 @@ const NET_COLS = [
|
|||||||
"ydtYdilNet",
|
"ydtYdilNet",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
async function main() {
|
// ── A) Netler ────────────────────────────────────────────────────────────
|
||||||
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
|
||||||
db.pragma("journal_mode = WAL");
|
|
||||||
|
|
||||||
// ── 1. Kontenjan türü kırılımı ─────────────────────────────────────────
|
async function netlerAdimi(db: Db, yil: number, azamiSayfa: number): Promise<void> {
|
||||||
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 ────────────────────────────────────
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS netler (
|
CREATE TABLE IF NOT EXISTS netler (
|
||||||
program_id TEXT NOT NULL,
|
program_id TEXT NOT NULL,
|
||||||
@@ -214,54 +153,165 @@ async function main() {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const insertNet = db.prepare(`
|
const first = await fetchPage<NetlerRecord>(NETLER_API, 0);
|
||||||
INSERT OR REPLACE INTO netler (program_id, yil, ${NET_COLS.join(", ")})
|
|
||||||
VALUES (${["?", "?", ...NET_COLS.map(() => "?")].join(", ")})
|
|
||||||
`);
|
|
||||||
|
|
||||||
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[]) => {
|
const processNetler = db.transaction((records: NetlerRecord[]) => {
|
||||||
for (const r of records) {
|
for (const r of records) {
|
||||||
if (!r.kilavuzKodu || !r.yil) continue;
|
if (!r.kilavuzKodu || !r.yil) continue;
|
||||||
const id = dbId.get(r.kilavuzKodu) ?? String(r.kilavuzKodu);
|
if (r.yil < ILK_YIL || r.yil > yil) {
|
||||||
insertNet.run(
|
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,
|
id,
|
||||||
r.yil,
|
r.yil,
|
||||||
r.puanTuru?.trim() ?? null,
|
r.puanTuru?.trim() ?? null,
|
||||||
...NET_COLS.slice(1).map((c) => num(r[c as keyof NetlerRecord]))
|
...NET_COLS.slice(1).map((c) => num(r[c as keyof NetlerRecord]))
|
||||||
);
|
);
|
||||||
netRows++;
|
yazilan++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const netFirst = await fetchPage<NetlerRecord>(NETLER_API, 0);
|
const sayfa = Math.min(first.totalPages, azamiSayfa);
|
||||||
console.log(
|
console.log(`Netler: ${first.totalElements} kayıt, ${first.totalPages} sayfa${sayfa < first.totalPages ? ` (yalnız ilk ${sayfa})` : ""}…`);
|
||||||
`Netler: ${netFirst.totalElements} kayıt, ${netFirst.totalPages} sayfa…`
|
processNetler(first.content);
|
||||||
);
|
for (let p = 1; p < sayfa; p++) {
|
||||||
processNetler(netFirst.content);
|
|
||||||
for (let p = 1; p < netFirst.totalPages; p++) {
|
|
||||||
await sleep(DELAY_MS);
|
await sleep(DELAY_MS);
|
||||||
processNetler((await fetchPage<NetlerRecord>(NETLER_API, p)).content);
|
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 dagilim = db
|
||||||
|
.prepare("SELECT yil, COUNT(*) AS n FROM netler GROUP BY yil ORDER BY yil")
|
||||||
const ozet = db
|
.all() as { yil: number; n: number }[];
|
||||||
.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 };
|
|
||||||
console.log(
|
console.log(
|
||||||
`Özet → kontenjan kırılımı olan program: ${ozet.kontlu}, ` +
|
`\nNetler: ${yazilan} satır yazıldı/güncellendi; programı DB'de olmayan: ${programYok}; yıl aralığı dışı: ${yilDisi}`
|
||||||
`net satırı: ${ozet.net} (${ozet.netli_program} program)`
|
|
||||||
);
|
);
|
||||||
db.close();
|
console.log(`Netler yıl dağılımı: ${dagilim.map((d) => `${d.yil}=${d.n}`).join(" ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
// ── B) Kontenjan kırılımı ────────────────────────────────────────────────
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
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
|
* 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
|
* API'nin yıl düzeni (Eylül 2026'da doğrulandı): EKSİZ alanlar (basariSirasi,
|
||||||
* - DB'de olmayan (yeni açılan) programları ekler
|
* minPuan, kontenjan, gkY…) en son yerleştirme yılıdır; `…1` bir önceki,
|
||||||
* - Az yerleşeni olan programlarda API basariSirasi döndürmez → NULL kalır
|
* `…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 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 { 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 API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||||
const PAGE_SIZE = 500;
|
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",
|
SAY: "SAYISAL",
|
||||||
EA: "EŞİT AĞIRLIK",
|
EA: "EŞİT AĞIRLIK",
|
||||||
SÖZ: "SÖZEL",
|
SÖZ: "SÖZEL",
|
||||||
@@ -26,27 +68,75 @@ const TUR_MAP: Record<string, string> = {
|
|||||||
TYT: "TYT",
|
TYT: "TYT",
|
||||||
};
|
};
|
||||||
|
|
||||||
type ApiRecord = {
|
/** API kaydından betiklerin kullandığı alanlar (ham dosyada kaydın tamamı durur). */
|
||||||
|
export type KilavuzKaydi = {
|
||||||
kilavuzKodu: number;
|
kilavuzKodu: number;
|
||||||
|
eskiKilavuzKodu?: number | null;
|
||||||
birimAdi: string;
|
birimAdi: string;
|
||||||
universiteAdi: string;
|
universiteAdi: string;
|
||||||
universiteTuru: string | null;
|
universiteTuru?: string | null;
|
||||||
ilAdi: string | null;
|
ilAdi?: string | null;
|
||||||
fymkAdi: string | null;
|
fymkAdi?: string | null;
|
||||||
puanTuru: string;
|
puanTuru: string;
|
||||||
ogrenimSuresi: number | null;
|
ogrenimSuresi?: number | null;
|
||||||
birimTuruId: number;
|
birimTuruId: number;
|
||||||
basariSirasi?: number | null;
|
basariSirasi?: number | string | null;
|
||||||
minPuan?: number | null;
|
minPuan?: number | string | null;
|
||||||
kontenjan?: number | null;
|
kontenjan?: number | null;
|
||||||
gkY?: number | null;
|
gkY?: number | null;
|
||||||
sgyY?: number | null;
|
sgyY?: number | null;
|
||||||
dprmY?: number | null;
|
dprmY?: number | null;
|
||||||
obkY?: 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 = {
|
type ApiPage = {
|
||||||
content: ApiRecord[];
|
content: Record<string, unknown>[];
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
totalElements: number;
|
totalElements: number;
|
||||||
};
|
};
|
||||||
@@ -83,140 +173,435 @@ async function fetchPage(page: number): Promise<ApiPage> {
|
|||||||
return (await res.json()) as ApiPage;
|
return (await res.json()) as ApiPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
function yerlesen(r: ApiRecord): number | null {
|
/** Pozitif tam sayı; 0 / boş / sayı değil → NULL (API "veri yok"u 0 ile verir). */
|
||||||
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
|
export function intOrNull(v: unknown): number | null {
|
||||||
(x): x is number => typeof x === "number"
|
|
||||||
);
|
|
||||||
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function intOrNull(v: unknown): number | null {
|
|
||||||
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||||||
return typeof n === "number" && Number.isFinite(n) && n > 0
|
return typeof n === "number" && Number.isFinite(n) && n > 0
|
||||||
? Math.round(n)
|
? Math.round(n)
|
||||||
: null;
|
: 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);
|
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||||||
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : null;
|
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
function yerlesen(r: KilavuzKaydi): number | null {
|
||||||
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
|
||||||
db.pragma("journal_mode = WAL");
|
(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;
|
* 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
|
||||||
const has2025 = cols.some((c) => c.name === "sira2025");
|
* gerçek program kodu biçimindeki ve yeni koddan farklı olanı döndürür.
|
||||||
if (!has2025) {
|
*/
|
||||||
db.exec(`
|
export function eskiKod(r: KilavuzKaydi): string | null {
|
||||||
ALTER TABLE programs ADD COLUMN sira2025 INTEGER;
|
if (!r.eskiKilavuzKodu) return null;
|
||||||
ALTER TABLE programs ADD COLUMN puan2025 REAL;
|
const eski = String(r.eskiKilavuzKodu);
|
||||||
ALTER TABLE programs ADD COLUMN kontenjan2025 INTEGER;
|
const yeni = String(r.kilavuzKodu);
|
||||||
ALTER TABLE programs ADD COLUMN yerlesen2025 INTEGER;
|
return eski.length === yeni.length && eski !== yeni ? eski : null;
|
||||||
CREATE INDEX IF NOT EXISTS idx_programs_tur_sira25 ON programs (tur, sira2025);
|
}
|
||||||
`);
|
|
||||||
|
/** 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
|
// ── Bütünlük: benzersiz kod = totalElements ────────────────────────────
|
||||||
// ile birebir aynı olmalı (bkz. scripts/db-index.ts). sira2025 kolonu
|
const benzersiz = new Map<string, KilavuzKaydi>();
|
||||||
// yukarıda garanti edildikten sonra kurulur.
|
let kodsuz = 0;
|
||||||
db.exec(`
|
for (const r of kayitlar) {
|
||||||
CREATE INDEX IF NOT EXISTS idx_programs_tur_onlisans_efektif
|
if (!r.kilavuzKodu) {
|
||||||
ON programs (tur, onlisans, COALESCE(sira2025, sira2024));
|
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(`
|
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 = ?
|
WHERE id = ?
|
||||||
`);
|
`);
|
||||||
|
const gecmisYillar = [1, 2, 3].filter((f) => yil - f >= ILK_YIL);
|
||||||
const insert = db.prepare(`
|
const insert = db.prepare(`
|
||||||
INSERT INTO programs (id, isim, universite, unitur, il, fakulte, tur, sure, onlisans,
|
INSERT INTO programs (id, isim, universite, unitur, il, fakulte, tur, sure, onlisans,
|
||||||
sira2025, puan2025, kontenjan2025, yerlesen2025)
|
sira${yil}, puan${yil}, kontenjan${yil}, yerlesen${yil},
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
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 = ?");
|
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 updated = 0;
|
||||||
let inserted = 0;
|
let inserted = 0;
|
||||||
let skipped = 0;
|
const atlananlar: string[] = [];
|
||||||
|
|
||||||
const first = await fetchPage(0);
|
db.transaction(() => {
|
||||||
const totalPages = first.totalPages;
|
const azamiRowid = (
|
||||||
console.log(
|
db.prepare("SELECT COALESCE(MAX(rowid), 0) AS m FROM programs").get() as { m: number }
|
||||||
`Toplam ${first.totalElements} kayıt, ${totalPages} sayfa çekilecek…`
|
).m;
|
||||||
);
|
const once = oncekiYilSaglamasi(db, yil, azamiRowid);
|
||||||
|
|
||||||
const processPage = db.transaction((records: ApiRecord[]) => {
|
for (const [id, r] of benzersiz) {
|
||||||
for (const r of records) {
|
|
||||||
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
|
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
|
||||||
if (!r.kilavuzKodu || !tur) {
|
// Puan türü yalnız YENİ satır için gerekir; mevcut satırın türü DB'de var.
|
||||||
skipped++;
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
const id = String(r.kilavuzKodu);
|
|
||||||
const vals = [
|
const vals = [
|
||||||
intOrNull(r.basariSirasi),
|
intOrNull(r.basariSirasi),
|
||||||
realOrNull(r.minPuan),
|
realOrNull(r.minPuan),
|
||||||
intOrNull(r.kontenjan),
|
intOrNull(r.kontenjan),
|
||||||
yerlesen(r),
|
yerlesen(r),
|
||||||
];
|
];
|
||||||
|
const eski = eskiKod(r);
|
||||||
if (exists.get(id)) {
|
if (exists.get(id)) {
|
||||||
update.run(...vals, id);
|
update.run(...vals, yil, eski, id);
|
||||||
updated++;
|
updated++;
|
||||||
} else {
|
} 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(
|
insert.run(
|
||||||
id,
|
id,
|
||||||
r.birimAdi,
|
r.birimAdi,
|
||||||
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
|
universite,
|
||||||
// duplikat kurum adı üretmesin
|
uniturSec(universite, r.universiteTuru ?? null),
|
||||||
uniAdiNormalize(r.universiteAdi),
|
|
||||||
r.universiteTuru ?? null,
|
|
||||||
r.ilAdi ?? null,
|
r.ilAdi ?? null,
|
||||||
r.fymkAdi ?? null,
|
fakulteSec(universite, r.fymkAdi ?? null),
|
||||||
tur,
|
tur,
|
||||||
r.ogrenimSuresi ?? null,
|
r.ogrenimSuresi ?? null,
|
||||||
r.birimTuruId === 47 ? 1 : 0,
|
r.birimTuruId === 47 ? 1 : 0,
|
||||||
...vals
|
...vals,
|
||||||
|
yil,
|
||||||
|
eski,
|
||||||
|
...gecmis.flatMap((g) => [g.sira, g.puan, g.kontenjan])
|
||||||
);
|
);
|
||||||
inserted++;
|
inserted++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
processPage(first.content);
|
const sonra = oncekiYilSaglamasi(db, yil, azamiRowid);
|
||||||
for (let p = 1; p < totalPages; p++) {
|
if (once !== sonra) {
|
||||||
await sleep(DELAY_MS);
|
dur(
|
||||||
const page = await fetchPage(p);
|
"Mevcut satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" +
|
||||||
processPage(page.content);
|
`önce : ${once}\nsonra: ${sonra}`
|
||||||
process.stdout.write(`\rsayfa ${p + 1}/${totalPages}`);
|
);
|
||||||
}
|
}
|
||||||
console.log();
|
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ı.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
})();
|
||||||
|
|
||||||
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 };
|
|
||||||
console.log(
|
console.log(
|
||||||
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${skipped}`
|
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${kodsuz + atlananlar.length}`
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}`
|
|
||||||
);
|
);
|
||||||
|
for (const a of atlananlar) console.log(` atlandı: ${a}`);
|
||||||
|
|
||||||
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
|
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
|
||||||
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).
|
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).
|
||||||
db.transaction(() => uniturOnar(db))();
|
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();
|
db.close();
|
||||||
|
console.log(
|
||||||
|
`\nSıradaki adım: pnpm gecmis-doldur --yil ${yil} --db <db> --ham ${path.relative(process.cwd(), hamDosya)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
if (process.argv[1]?.endsWith("refresh.ts")) calistir(main);
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|||||||
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
38
src/lib/veri-yillari.ts
Normal file
38
src/lib/veri-yillari.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Yerleştirme verisinin yıl kapsamı — TEK KAYNAK. Saf ve import'suz: client,
|
||||||
|
* server ve `scripts/` aynı dosyayı okur. Yeni yıl verisi yüklenince yalnız
|
||||||
|
* `SON_YIL` değişir (+ `pnpm refresh -- --yil <yıl>` ile DB inşası).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Veri penceresinin ilk yılı. Kayıtlı rapor/tadımlık `siraGecmisi` dizileri
|
||||||
|
* bu yıla demirlidir; pencere kaydırılmaz, uzar. */
|
||||||
|
export const ILK_YIL = 2021;
|
||||||
|
|
||||||
|
/** En son yerleştirme yılı (DB'de `sira${SON_YIL}` kolonları dolu). */
|
||||||
|
export const SON_YIL = 2026;
|
||||||
|
|
||||||
|
export const ONCEKI_YIL = SON_YIL - 1;
|
||||||
|
|
||||||
|
/** [2021, …, SON_YIL] — `siraGecmisi` dizileriyle indeks hizalı. */
|
||||||
|
export const YILLAR: readonly number[] = Array.from(
|
||||||
|
{ length: SON_YIL - ILK_YIL + 1 },
|
||||||
|
(_, i) => ILK_YIL + i,
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Bu veriyle tercih yapacak öğrencilerin yılı. */
|
||||||
|
export const TERCIH_YILI = SON_YIL + 1;
|
||||||
|
|
||||||
|
/** Sayfa başlıklarında görünen yıl ("… Taban Puanları 2026"). */
|
||||||
|
export const BASLIK_YILI = SON_YIL;
|
||||||
|
|
||||||
|
export const VERI_YIL_SAYISI = YILLAR.length;
|
||||||
|
|
||||||
|
/** "2021–2026" */
|
||||||
|
export const VERI_ARALIGI = `${ILK_YIL}–${SON_YIL}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Efektif sıra: son yılın sırası, yoksa bir önceki yılınki. İNDEKS SÖZLEŞMESİ —
|
||||||
|
* SQLite ifade indeksi metinsel eşleşmeyle çalışır; sorgular ve indeks
|
||||||
|
* betikleri bu dizgiyi birebir (virgülden sonra tek boşluk) kullanmalıdır.
|
||||||
|
*/
|
||||||
|
export const EFEKTIF_SIRA_SQL = `COALESCE(sira${SON_YIL}, sira${ONCEKI_YIL})`;
|
||||||
Reference in New Issue
Block a user