Download t2s/errors.py from SevdanurGenc/Text-to-SQL: direct link, hf CLI and curl.
- Browser
- Download file 17.9 kB
-
https://huggingface.co/SevdanurGenc/Text-to-SQL/resolve/main/t2s/errors.py
- Command line
-
hf download hf://SevdanurGenc/Text-to-SQL/t2s/errors.py
-
curl -L -o errors.py https://huggingface.co/SevdanurGenc/Text-to-SQL/resolve/main/t2s/errors.py
17.9 kB
| """Hata kataloğu: tip tanımları, çalışma zamanı sınıflandırıcısı ve değerlendirme hatası analizi.""" | |
| from __future__ import annotations | |
| import json | |
| import re | |
| import sqlite3 | |
| from dataclasses import asdict, dataclass, field | |
| from datetime import datetime | |
| from pathlib import Path | |
| from .trnum import AYLAR, fold | |
| # --------------------------------------------------------------------------- katalog | |
| CATALOG = { | |
| "SEMA_ESLEME": { | |
| "ad": "Şema eşleme hatası", | |
| "tanim": "Soru, yanlış veya var olmayan tablo/sütuna bağlandı (ör. 'gerçekleşme' yerine 'bütçe' sütunu).", | |
| "ornek": "no such column: gerceklesme2024", | |
| "cozum": "Benzer sütun adlarını öner; orijinal başlıkları ve birimleri şemada göster.", | |
| }, | |
| "DEGER_ESLEME": { | |
| "ad": "Değer eşleme hatası", | |
| "tanim": "Metin değeri tablodakiyle birebir eşleşmedi: İ/ı-I/i büyük-küçük harf, ek almış adlar, kısaltmalar.", | |
| "ornek": "WHERE il = 'Istanbul' (tabloda 'İstanbul')", | |
| "cozum": "TR_NORM() ile katlanmış karşılaştırma; tablodaki en yakın değerleri geri besle.", | |
| }, | |
| "BIRIM": { | |
| "ad": "Birim hatası", | |
| "tanim": "Ölçek/birim dönüşümü unutuldu veya yanlış yapıldı (bin TL ↔ TL, bin MWh ↔ MWh, m³ ↔ litre, % ↔ oran).", | |
| "ornek": "Soru TL isterken 'bin TL' sütunu doğrudan döndürüldü (1000 kat fark).", | |
| "cozum": "Sütun birimlerini meta veride sakla, şemada göster; sonuçta birim etiketi kullan.", | |
| }, | |
| "TARIH_BICIMI": { | |
| "ad": "Tarih biçimi hatası", | |
| "tanim": "Türkçe tarih yazımı ('15.03.2025', 'Ocak 2024') ile veritabanındaki ISO biçim ('2025-03-15', '2024-01') uyuşmadı.", | |
| "ornek": "WHERE yururluk_tarihi = '15.03.2025'", | |
| "cozum": "Tarihleri aktarımda ISO'ya çevir; SQL'deki Türkçe tarih literallerini yakala ve dönüştürmeyi öner.", | |
| }, | |
| "SAYI_BICIMI": { | |
| "ad": "Türkçe sayı biçimi hatası", | |
| "tanim": "'1.234,56' biçimi yanlış yorumlandı: sayı metin kaldı, nokta ondalık sanıldı (1000 kat hata) " | |
| "veya virgül binlik sanıldı (10/100 kat hata); SQL'e Türkçe sayı literali yazıldı.", | |
| "ornek": "SUM('1.234,56') = 1.234 | '4,5' -> 45", | |
| "cozum": "Aktarımda TR yerel ayarıyla ayrıştır; SQL literallerini denetle.", | |
| }, | |
| "SAYISAL_AKIL": { | |
| "ad": "Sayısal akıl yürütme hatası", | |
| "tanim": "Toplama/ortalama/yüzde formülü yanlış; belgedeki 'Toplam' satırı tekrar sayıldı; " | |
| "işaret (negatif gider) veya tamsayı bölmesi hatası.", | |
| "ornek": "SUM(...) toplam satırını da içerdi (2 kat sonuç)", | |
| "cozum": "toplam_satiri bayrağı; formül kanıtları; sonuç makullük kontrolleri.", | |
| }, | |
| "SOZDIZIMI": { | |
| "ad": "SQL sözdizimi hatası", | |
| "tanim": "Üretilen sorgu ayrıştırılamadı veya SQLite lehçesine uymuyor.", | |
| "ornek": "near \"TOP\": syntax error", | |
| "cozum": "Hata mesajını geri besle; yalnızca SQLite sözdizimi iste.", | |
| }, | |
| "BOS_SONUC": { | |
| "ad": "Boş sonuç", | |
| "tanim": "Sorgu çalıştı ama satır dönmedi; çoğunlukla değer/tarih eşleme sorunudur.", | |
| "ornek": "WHERE donem = 'Mart 2025' -> 0 satır", | |
| "cozum": "WHERE literallerini tablodaki gerçek değerlerle karşılaştır, en yakınlarını öner.", | |
| }, | |
| "CEVAPSIZLIK": { | |
| "ad": "Cevaplanabilirlik hatası", | |
| "tanim": "Cevabı tablolarda olmayan soruya uydurma cevap verildi ya da cevaplanabilir soru reddedildi.", | |
| "ornek": "'Mersin'in tüketimi' sorusuna başka bir ilin değeri döndü.", | |
| "cozum": "Kapsam dışı yıl/varlık denetimi; emin değilse açıkça 'cevap bulunamadı' de.", | |
| }, | |
| "YURUTME": { | |
| "ad": "Diğer yürütme hatası", | |
| "tanim": "Bilinmeyen fonksiyon, tip uyuşmazlığı, zaman aşımı vb.", | |
| "ornek": "no such function: YEAR", | |
| "cozum": "SQLite fonksiyonlarını kullan (strftime, substr).", | |
| }, | |
| "YANLIS_SONUC": { | |
| "ad": "Yanlış sonuç (sınıflandırılamadı)", | |
| "tanim": "Sorgu çalıştı fakat altın sonuçla uyuşmadı ve belirgin bir alt tipe atanamadı.", | |
| "ornek": "-", | |
| "cozum": "Elle inceleme.", | |
| }, | |
| } | |
| class ErrorEvent: | |
| tip: str | |
| mesaj: str | |
| sql: str = "" | |
| oneriler: list[str] = field(default_factory=list) | |
| asama: str = "yurutme" # statik | yurutme | sonuc | degerlendirme | |
| soru: str = "" | |
| zaman: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds")) | |
| def feedback(self) -> str: | |
| info = CATALOG.get(self.tip, {}) | |
| s = f"[{self.tip} – {info.get('ad', '')}] {self.mesaj}" | |
| if self.oneriler: | |
| s += "\nÖneriler: " + " | ".join(self.oneriler) | |
| return s | |
| # --------------------------------------------------------------------------- yardımcılar | |
| _STR_LIT = re.compile(r"'((?:[^']|'')*)'") | |
| _TR_NUM_LIT = re.compile(r"^\s*-?\d{1,3}(\.\d{3})+(,\d+)?\s*$|^\s*-?\d+,\d+\s*$") | |
| _BARE_TR_NUM = re.compile(r"(?<![\w.'])\d{1,3}(?:\.\d{3}){2,}(?:,\d+)?(?![\w.])") | |
| _DMY = re.compile(r"^\d{1,2}[./]\d{1,2}[./]\d{4}$") | |
| def string_literals(sql: str) -> list[str]: | |
| return [m.group(1).replace("''", "'") for m in _STR_LIT.finditer(sql or "")] | |
| def _all_columns(con) -> list[tuple[str, str]]: | |
| try: | |
| return con.execute("SELECT tablo, sutun FROM _sutunlar").fetchall() | |
| except sqlite3.Error: | |
| return [] | |
| def _close(name: str, options: list[str], n: int = 3) -> list[str]: | |
| try: | |
| from rapidfuzz import process | |
| return [m[0] for m in process.extract(name, options, limit=n, score_cutoff=45)] | |
| except Exception: | |
| import difflib | |
| return difflib.get_close_matches(name, options, n=n, cutoff=0.4) | |
| def static_checks(sql: str, con) -> list[ErrorEvent]: | |
| """Yürütme öncesi denetimler: sözdizimi, salt-okuma, TR sayı/tarih literalleri, toplam satırı.""" | |
| events = [] | |
| s = (sql or "").strip().rstrip(";") | |
| if not s: | |
| return [ErrorEvent("SOZDIZIMI", "Boş sorgu üretildi.", sql, asama="statik")] | |
| # yazma komutları: anahtar sözcük denetimi; REPLACE(...) metin işlevi serbesttir (REPLACE INTO değil) | |
| if not re.match(r"^\s*(select|with)\b", s, re.I) or re.search( | |
| r"\b(insert|update|delete|drop|alter|create|attach|detach|pragma|vacuum|reindex)\b|\breplace\b(?!\s*\()", | |
| s, re.I): | |
| return [ErrorEvent("SOZDIZIMI", "Yalnızca SELECT/WITH sorgularına izin verilir.", sql, asama="statik")] | |
| if s.count(";") > 0: | |
| return [ErrorEvent("SOZDIZIMI", "Tek bir SQL ifadesi üretin (';' ile birden çok ifade yok).", sql, | |
| asama="statik")] | |
| tree = None | |
| try: | |
| import sqlglot | |
| from sqlglot import exp | |
| tree = sqlglot.parse_one(s, read="sqlite") | |
| # ifade tipi denetimi: kök sorgu SELECT/birleşim olmalı, içinde hiçbir yazma düğümü bulunmamalı | |
| write_nodes = tuple(getattr(exp, n) for n in ("Insert", "Update", "Delete", "Drop", "Create", "Alter", | |
| "Merge", "Command") if hasattr(exp, n)) | |
| if not isinstance(tree, (exp.Select, exp.Union, exp.Intersect, exp.Except, exp.Subquery)) or \ | |
| any(isinstance(n, write_nodes) for n in tree.walk()): | |
| return [ErrorEvent("SOZDIZIMI", "Yalnızca SELECT/WITH sorgularına izin verilir.", sql, asama="statik")] | |
| except Exception as e: # sqlglot yoksa veya hata | |
| if e.__class__.__name__ in ("ParseError", "TokenError"): | |
| events.append(ErrorEvent("SOZDIZIMI", f"SQL ayrıştırılamadı: {str(e).splitlines()[0][:200]}", | |
| sql, asama="statik")) | |
| return events | |
| for lit in string_literals(s): | |
| if _TR_NUM_LIT.match(lit): | |
| v = lit.replace(".", "").replace(",", ".") | |
| events.append(ErrorEvent("SAYI_BICIMI", f"SQL'de Türkçe biçimli sayı metni var: '{lit}'.", | |
| sql, [f"Sayıları tırnaksız ve nokta ondalıkla yazın: {v}"], "statik")) | |
| elif _DMY.match(lit.strip()): | |
| d, m, y = re.split(r"[./]", lit.strip()) | |
| events.append(ErrorEvent("TARIH_BICIMI", f"SQL'de Türkçe tarih yazımı var: '{lit}'.", sql, | |
| [f"Tarihler ISO biçimde saklanır: '{y}-{int(m):02d}-{int(d):02d}'"], "statik")) | |
| else: | |
| parts = fold(lit).split() | |
| if len(parts) == 2 and parts[0] in AYLAR and parts[1].isdigit(): | |
| events.append(ErrorEvent("TARIH_BICIMI", f"SQL'de ay adı ile tarih var: '{lit}'.", sql, | |
| [f"Dönemler 'YYYY-AA' biçimindedir: '{parts[1]}-{AYLAR[parts[0]]:02d}'"], | |
| "statik")) | |
| for m in _BARE_TR_NUM.finditer(s): | |
| events.append(ErrorEvent("SAYI_BICIMI", f"SQL'de binlik ayraçlı sayı var: {m.group(0)}", sql, | |
| [f"{m.group(0).replace('.', '').replace(',', '.')} yazın"], "statik")) | |
| # toplam satırı tuzağı | |
| try: | |
| flagged = {t for (t,) in con.execute("SELECT tablo FROM _sutunlar WHERE sutun='toplam_satiri'")} | |
| except sqlite3.Error: | |
| flagged = set() | |
| low = s.lower() | |
| uses_agg = re.search(r"\b(sum|avg|count|max|min)\s*\(|\border\s+by\b|\bgroup\s+by\b", low) | |
| for t in flagged: | |
| if re.search(rf'["`]?\b{re.escape(t)}\b["`]?', low) and uses_agg and "toplam_satiri" not in low \ | |
| and _total_row_reachable(tree, t, con): | |
| events.append(ErrorEvent( | |
| "SAYISAL_AKIL", | |
| f"'{t}' tablosunda belgenin kendi Toplam satırı var (toplam_satiri=1); toplama/sıralama " | |
| "yaparken bu satır sonucu bozabilir.", sql, | |
| ["WHERE koşuluna 'toplam_satiri = 0' ekleyin (yalnızca toplam değeri isteniyorsa " | |
| "'toplam_satiri = 1' satırını seçin)."], "statik")) | |
| return events | |
| def _total_row_reachable(tree, table: str, con) -> bool: | |
| """Sorgunun WHERE koşulu belgenin Toplam satırını zaten dışlıyor mu? Dışlıyorsa uyarı gereksizdir. | |
| Tabloyu okuyan her SELECT için 'SELECT COUNT(*) FROM tablo WHERE (koşul) AND toplam_satiri = 1' çalıştırılır. | |
| Koşul çıkarılamazsa ya da sorgu çalışmazsa (takma ad, birleştirme vb.) temkinli davranılır: uyarı korunur.""" | |
| if tree is None: | |
| return True | |
| try: | |
| from sqlglot import exp | |
| selects = [sel for sel in tree.find_all(exp.Select) | |
| if any(tb.name.lower() == table.lower() for tb in sel.find_all(exp.Table))] | |
| if not selects: | |
| return True | |
| for sel in selects: | |
| if len({tb.name.lower() for tb in sel.find_all(exp.Table)}) != 1 or any( | |
| tb.alias for tb in sel.find_all(exp.Table)): | |
| return True | |
| where = sel.args.get("where") | |
| cond = f"({where.this.sql(dialect='sqlite')}) AND " if where is not None else "" | |
| n = con.execute(f'SELECT COUNT(*) FROM "{table}" WHERE {cond}toplam_satiri = 1').fetchone()[0] | |
| if n: | |
| return True | |
| return False | |
| except Exception: # noqa: BLE001 | |
| return True | |
| def classify_exception(exc: Exception, sql: str, con) -> ErrorEvent: | |
| msg = str(exc) | |
| low = msg.lower() | |
| cols = _all_columns(con) | |
| if "no such column" in low: | |
| bad = msg.split(":", 1)[-1].strip().split(".")[-1] | |
| sugg = _close(bad, sorted({c for _, c in cols})) | |
| return ErrorEvent("SEMA_ESLEME", msg, sql, [f"Benzer sütunlar: {', '.join(sugg)}"] if sugg else []) | |
| if "no such table" in low: | |
| bad = msg.split(":", 1)[-1].strip() | |
| sugg = _close(bad, sorted({t for t, _ in cols})) | |
| return ErrorEvent("SEMA_ESLEME", msg, sql, [f"Benzer tablolar: {', '.join(sugg)}"] if sugg else []) | |
| if "ambiguous column" in low: | |
| return ErrorEvent("SEMA_ESLEME", msg, sql, ["Sütunu tablo adıyla niteleyin (tablo.sutun)."]) | |
| if "syntax error" in low or "incomplete input" in low or "unrecognized token" in low: | |
| return ErrorEvent("SOZDIZIMI", msg, sql, ["Yalnızca SQLite sözdizimi kullanın."]) | |
| if "no such function" in low: | |
| return ErrorEvent("YURUTME", msg, sql, | |
| ["SQLite fonksiyonları: strftime, substr, ROUND, ABS, TR_NORM, TR_LOWER"]) | |
| return ErrorEvent("YURUTME", msg, sql) | |
| def check_result(sql: str, rows: list, con) -> list[ErrorEvent]: | |
| """Sonuç denetimi: boş sonuçta WHERE literallerini tablodaki değerlerle karşılaştırır.""" | |
| if rows and not all(all(v is None for v in r) for r in rows): | |
| return [] | |
| events = [] | |
| lits = [l for l in string_literals(sql) if not l.endswith("%")] | |
| suggestions = [] | |
| for lit in lits: | |
| hits = [] | |
| for t, c in _all_columns(con): | |
| try: | |
| vals = [v for (v,) in con.execute(f'SELECT DISTINCT "{c}" FROM "{t}" WHERE typeof("{c}")="text"')] | |
| except sqlite3.Error: | |
| continue | |
| exact_fold = [v for v in vals if fold(v) == fold(lit) and v != lit] | |
| if exact_fold: | |
| hits.append(f"{t}.{c} = '{exact_fold[0]}' (büyük/küçük harf veya Türkçe karakter farkı)") | |
| continue | |
| close = [v for v in _close(lit, vals, 2)] | |
| if close and len(lit) > 2: | |
| hits.append(f"{t}.{c} içinde yakın değerler: " + ", ".join(repr(v) for v in close)) | |
| if hits: | |
| suggestions.extend(hits[:2]) | |
| if rows and all(all(v is None for v in r) for r in rows): | |
| return [ErrorEvent("BOS_SONUC", "Sorgu yalnızca NULL döndürdü (eşleşen satır yok veya sütun metin " | |
| "olarak saklanmış olabilir).", sql, suggestions or ["WHERE koşullarını gözden geçirin."], | |
| "sonuc")] | |
| tip = "DEGER_ESLEME" if any("harf" in s for s in suggestions) else "BOS_SONUC" | |
| events.append(ErrorEvent(tip, "Sorgu 0 satır döndürdü.", sql, | |
| suggestions or ["WHERE koşullarını gevşetin veya değerleri kontrol edin; " | |
| "metin karşılaştırmalarında TR_NORM(sutun)=TR_NORM('değer') kullanın."], | |
| "sonuc")) | |
| return events | |
| # --------------------------------------------------------------------------- günlük | |
| class ErrorLog: | |
| """Hataları JSONL dosyasına yazar (arayüzdeki Hata Kataloğu sayfası okur).""" | |
| def __init__(self, path: str | Path): | |
| self.path = Path(path) | |
| self.path.parent.mkdir(parents=True, exist_ok=True) | |
| def add(self, ev: ErrorEvent, kaynak: str = "arayuz", **extra): | |
| rec = asdict(ev) | {"kaynak": kaynak} | extra | |
| with self.path.open("a", encoding="utf-8") as f: | |
| f.write(json.dumps(rec, ensure_ascii=False) + "\n") | |
| def read(self) -> list[dict]: | |
| if not self.path.exists(): | |
| return [] | |
| return [json.loads(l) for l in self.path.read_text(encoding="utf-8").splitlines() if l.strip()] | |
| # --------------------------------------------------------------------------- değerlendirme analizi | |
| def _num(x): | |
| return x if isinstance(x, (int, float)) and not isinstance(x, bool) else None | |
| def diagnose_failure(item: dict, pred_sql: str | None, pred_rows, gold_rows, abstained: bool, | |
| exec_error: str | None, db_mode: str) -> str: | |
| """Değerlendirmede başarısız bir örneğin hata tipini kestirir.""" | |
| cats = set(item.get("kategoriler", [])) | |
| if not item["cevaplanabilir"]: | |
| return "CEVAPSIZLIK" | |
| if abstained and not pred_sql: | |
| return "CEVAPSIZLIK" | |
| if exec_error: | |
| e = exec_error.lower() | |
| if "no such column" in e or "no such table" in e or "ambiguous" in e: | |
| return "SEMA_ESLEME" | |
| if "syntax" in e or "incomplete" in e: | |
| return "SOZDIZIMI" | |
| return "YURUTME" | |
| lits = string_literals(pred_sql or "") | |
| if any(_DMY.match(l) for l in lits) or any( | |
| len(fold(l).split()) == 2 and fold(l).split()[0] in AYLAR for l in lits): | |
| return "TARIH_BICIMI" | |
| if any(_TR_NUM_LIT.match(l) for l in lits): | |
| return "SAYI_BICIMI" | |
| if not pred_rows: | |
| return "DEGER_ESLEME" if "deger_esleme" in cats else ("TARIH_BICIMI" if "tarih" in cats else "BOS_SONUC") | |
| # sayısal oranlar | |
| p = [_num(v) for r in pred_rows for v in r] | |
| g = [_num(v) for r in gold_rows for v in r] | |
| p = [v for v in p if v is not None] | |
| g = [v for v in g if v is not None] | |
| if db_mode != "tr" and pred_rows and any(isinstance(v, str) and re.search(r"\d[.,]\d", v) | |
| for r in pred_rows for v in r): | |
| return "SAYI_BICIMI" | |
| if len(p) == 1 and len(g) == 1 and g[0] and p[0]: | |
| ratio = abs(p[0] / g[0]) | |
| for k in (1e3, 1e-3, 1e6, 1e-6): | |
| if abs(ratio - k) / k < 0.01: | |
| return "SAYI_BICIMI" if db_mode != "tr" else "BIRIM" | |
| if any(abs(ratio - k) / k < 0.02 for k in (10, 100, 0.1, 0.01)): | |
| return "SAYI_BICIMI" if db_mode != "tr" else "SAYISAL_AKIL" | |
| if abs(ratio - 2) < 0.02: | |
| return "SAYISAL_AKIL" # toplam satırı çift sayım | |
| return "SAYISAL_AKIL" | |
| if db_mode != "tr" and "tr_sayi" in cats: | |
| return "SAYI_BICIMI" | |
| if "toplam_satiri" in cats and pred_sql and "toplam_satiri" not in pred_sql.lower(): | |
| return "SAYISAL_AKIL" | |
| if "birim" in cats: | |
| return "BIRIM" | |
| if "deger_esleme" in cats: | |
| return "DEGER_ESLEME" | |
| if "tarih" in cats: | |
| return "TARIH_BICIMI" | |
| if p and g: | |
| return "SAYISAL_AKIL" | |
| return "SEMA_ESLEME" | |