Spaces:
Running
Running
| """ | |
| src/data/generate_text2sql_dataset.py | |
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| Text-to-SQL ํ๊ฐ์ฉ ํ ์คํธ ์ผ์ด์ค ์์ฑ๊ธฐ | |
| ํ๋ฆ: | |
| 1. data/sap_erp.db (SQLite)์์ ๋ค์ํ ์๋๋ฆฌ์ค ์ผ์ด์ค๋ฅผ ์ง์ ์ํ๋ง | |
| - WBSTA = A / B / C ์ผ์ด์ค | |
| - MARD JOIN ๊ฐ๋ฅํ ์ผ์ด์ค (available_stock ์์) | |
| - NULL ์ผ์ด์ค (no-join row) | |
| - ์กด์ฌํ์ง ์๋ order_id (negative case) | |
| 2. ์ํ๋งํ ์ค์ DB ๊ฐ์ผ๋ก expected_values ์๋ ๊ณ์ฐ | |
| 3. ๊ธฐ์กด text2sql_test_cases.json ํฌ๋งท์ผ๋ก ์ ์ฅ | |
| ์คํ: | |
| python -m src.data.generate_text2sql_dataset | |
| python -m src.data.generate_text2sql_dataset --n-samples 30 --output data/eval/text2sql_test_cases_gen.json | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| import random | |
| import sqlite3 | |
| import sys | |
| from dataclasses import asdict, dataclass | |
| from pathlib import Path | |
| from typing import Any | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| _ROOT_FOR_LOG = Path(__file__).resolve().parent.parent.parent | |
| _LOG_DIR = _ROOT_FOR_LOG / "logs" | |
| _LOG_DIR.mkdir(parents=True, exist_ok=True) | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(message)s", | |
| handlers=[ | |
| logging.FileHandler(_LOG_DIR / "data_generation.log", encoding="utf-8"), | |
| logging.StreamHandler(sys.stdout), | |
| ], | |
| force=True, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| _ROOT = Path(__file__).resolve().parent.parent.parent | |
| DEFAULT_DB = str(_ROOT / "data" / "sap_erp.db") | |
| DEFAULT_OUTPUT = str(_ROOT / "data" / "eval" / "text2sql_test_cases_gen.json") | |
| REQUIRED_COLUMNS = ["material_name", "quantity", "delivery_status", "delivery_date", "available_stock"] | |
| # negative case์ ์ฌ์ฉํ ๊ฐ์ง order_id | |
| NEGATIVE_ORDER_ID = "9999999999" | |
| # --------------------------------------------------------------------------- | |
| # ๋ฐ์ดํฐ ๊ตฌ์กฐ | |
| # --------------------------------------------------------------------------- | |
| class TestCase: | |
| id: str | |
| description: str | |
| order_id: str | |
| item_no: int | |
| expected_columns: list[str] | |
| expected_values: dict[str, Any] | None | |
| golden_sql: str # ์ ๋ต(๋ ํผ๋ฐ์ค) SQL โ ํ๋ก๋์ ๋๊ตฌ์ _hardcoded_query ๊ฒฐ๊ณผ | |
| notes: str | |
| # --------------------------------------------------------------------------- | |
| # DB ์ํ๋ง ์ฟผ๋ฆฌ ๋ชจ์ | |
| # --------------------------------------------------------------------------- | |
| # ๊ฐ ์๋๋ฆฌ์ค๋ฅผ (description_template, notes, sql) ํํ๋ก ์ ์ | |
| # SQL์ (VBELN, POSNR, quantity, delivery_status, delivery_date, available_stock) ๋ฐํ | |
| _SAMPLING_QUERIES: list[tuple[str, str, str]] = [ | |
| # โโ ์ผ์ด์ค 1: WBSTA = A (๋ฏธ์ฒ๋ฆฌ) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Order with WBSTA=A (goods movement not yet processed)", | |
| "WBSTA=A means goods movement not yet started", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| NULL AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| WHERE vbup.WBSTA = 'A' | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 2: WBSTA = B (๋ถ๋ถ ์ฒ๋ฆฌ) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Order with WBSTA=B (partially processed)", | |
| "WBSTA=B means goods movement partially processed", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| NULL AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| WHERE vbup.WBSTA = 'B' | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 3: WBSTA = C (์์ ์ฒ๋ฆฌ) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Order with WBSTA=C (fully processed)", | |
| "WBSTA=C means goods movement fully completed", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| NULL AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| WHERE vbup.WBSTA = 'C' | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 4: MARD JOIN ๊ฐ๋ฅ (available_stock ์์) โโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Order with available stock via MARD JOIN", | |
| "MARD.LABST (unrestricted stock) should be present", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| mard.LABST AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| LEFT JOIN MARD mard ON vbap.MATNR = mard.MATNR | |
| WHERE mard.LABST IS NOT NULL AND mard.LABST > 0 | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 5: delivery_date๋ง ์๊ณ stock ์์ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Order with delivery date only (no stock join)", | |
| "MARD join yields NULL โ delivery_date must be from VBEP", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| NULL AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| LEFT JOIN MARD mard ON vbap.MATNR = mard.MATNR | |
| WHERE vbep.EDATU IS NOT NULL AND mard.LABST IS NULL | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 6: ๋น๊ธฐ๋ณธ POSNR (POSNR != 10) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Order item with non-default POSNR (item number != 10)", | |
| "Tests that POSNR is handled correctly for non-standard line items", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| mard.LABST AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| LEFT JOIN MARD mard ON vbap.MATNR = mard.MATNR | |
| WHERE vbap.POSNR != 10 | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 7: ์ผ๋ฐ ๊ธฐ๋ณธ ์ผ์ด์ค (ํน์ ์กฐ๊ฑด ์์) โโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Standard order โ baseline with all joins present", | |
| "General case: material name, delivery status, and delivery date all populated", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| NULL AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| WHERE makt.MAKTX IS NOT NULL | |
| AND vbup.WBSTA IS NOT NULL | |
| AND vbep.EDATU IS NOT NULL | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 8: ๋ฉ๊ธฐ ์ด๊ณผ ๋ฏธ์๋ฃ ์ฃผ๋ฌธ (overdue) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Overdue order โ delivery date in the past, goods movement incomplete", | |
| "EDATU < today and WBSTA in (A, B) โ tests time-sensitive filtering", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| NULL AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| WHERE vbep.EDATU < strftime('%Y%m%d', 'now') | |
| AND vbup.WBSTA IN ('A', 'B') | |
| AND vbep.EDATU IS NOT NULL | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 9: ๋ฏธ๋ ๋ฉ๊ธฐ ์์ ์ฃผ๋ฌธ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Upcoming delivery โ delivery date in the future, not yet processed", | |
| "EDATU >= today and WBSTA = A โ tests future-date query handling", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| NULL AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| WHERE vbep.EDATU >= strftime('%Y%m%d', 'now') | |
| AND vbup.WBSTA = 'A' | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 10: ์ฌ๊ณ ๊ฐ ์ฃผ๋ฌธ๋ ์ด์์ธ ๊ฒฝ์ฐ (stock surplus) โโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Order where available stock exceeds ordered quantity", | |
| "MARD.LABST > VBAP.KWMENG โ tests stock-vs-quantity comparison logic", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| mard.LABST AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| LEFT JOIN MARD mard ON vbap.MATNR = mard.MATNR | |
| WHERE mard.LABST > vbap.KWMENG | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 11: ๋ค์ค ๋ผ์ธ ์์ดํ ์ฃผ๋ฌธ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Multi-line order โ sales order containing more than one item", | |
| "Tests item disambiguation when VBELN has multiple POSNRs", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| NULL AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| WHERE vbap.VBELN IN ( | |
| SELECT VBELN FROM VBAP GROUP BY VBELN HAVING COUNT(DISTINCT POSNR) > 1 | |
| ) | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| # โโ ์ผ์ด์ค 12: ์ฌ๊ณ ์์ (LABST = 0 ๋๋ JOIN ๋ถ๊ฐ) โโโโโโโโโโโโโโโโโโโโโโ | |
| ( | |
| "Order with zero or no available stock", | |
| "MARD.LABST = 0 or NULL โ tests zero-stock edge case handling", | |
| """ | |
| SELECT | |
| vbap.VBELN AS order_id, | |
| vbap.POSNR AS item_no, | |
| makt.MAKTX AS material_name, | |
| vbap.KWMENG AS quantity, | |
| vbup.WBSTA AS delivery_status, | |
| vbep.EDATU AS delivery_date, | |
| COALESCE(mard.LABST, 0.0) AS available_stock | |
| FROM VBAP vbap | |
| LEFT JOIN MAKT makt ON vbap.MATNR = makt.MATNR AND makt.SPRAS = 'E' | |
| LEFT JOIN VBUP vbup ON CAST(vbap.VBELN AS INTEGER) = CAST(vbup.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbup.POSNR AS INTEGER) | |
| LEFT JOIN VBEP vbep ON CAST(vbap.VBELN AS INTEGER) = CAST(vbep.VBELN AS INTEGER) AND CAST(vbap.POSNR AS INTEGER) = CAST(vbep.POSNR AS INTEGER) | |
| LEFT JOIN MARD mard ON vbap.MATNR = mard.MATNR | |
| WHERE (mard.LABST IS NULL OR mard.LABST = 0) | |
| AND vbap.KWMENG > 0 | |
| AND makt.MAKTX IS NOT NULL | |
| ORDER BY RANDOM() | |
| LIMIT {limit} | |
| """, | |
| ), | |
| ] | |
| # --------------------------------------------------------------------------- | |
| # DB ์ฐ๊ฒฐ ํฌํผ | |
| # --------------------------------------------------------------------------- | |
| def _query(db_path: str, sql: str, conn: sqlite3.Connection | None = None) -> list[dict]: | |
| """SQL ์คํ โ dict ๋ฆฌ์คํธ. conn์ด ์ฃผ์ด์ง๋ฉด ๊ทธ ์ฐ๊ฒฐ์ ์ฌ์ฌ์ฉ(๋ซ์ง ์์), | |
| ์์ผ๋ฉด ์ผํ์ฑ ์ฐ๊ฒฐ์ ์ด๊ณ ๋ซ๋๋ค(ํ์ํธํ). ๋ฐฐ์น ์ํ๋ง์์ conn์ ์ฌ์ฌ์ฉํด | |
| ์ฐ๊ฒฐ ์คํ ๋น์ฉ + SQLite ํ์ด์ง ์บ์ ์ฝ๋์คํํธ๋ฅผ ํผํ๋ค.""" | |
| own = conn is None | |
| if own: | |
| conn = sqlite3.connect(db_path) | |
| conn.row_factory = sqlite3.Row | |
| try: | |
| rows = conn.execute(sql).fetchall() | |
| return [dict(r) for r in rows] | |
| finally: | |
| if own: | |
| conn.close() | |
| def _edatu_to_iso(edatu: str | None) -> str | None: | |
| """YYYYMMDD โ YYYY-MM-DD ๋ณํ (์ด๋ฏธ ISO๋ฉด ๊ทธ๋๋ก)""" | |
| if edatu and len(edatu) == 8: | |
| return f"{edatu[:4]}-{edatu[4:6]}-{edatu[6:]}" | |
| return edatu | |
| # ์ํ๋งยท์บ๋ ธ๋์ปฌ ์ฟผ๋ฆฌ๊ฐ ์ฐ๋ CAST(...) ์กฐ์ธ ํค์ ๋ํ ํํ์ ์ธ๋ฑ์ค. | |
| # ์ด๊ฒ ์์ผ๋ฉด CAST ์๋ณ ์กฐ์ธ์ด ์ธ๋ฑ์ค๋ฅผ ๋ชป ํ VBAPรVBUP ์ค์ฒฉ๋ฃจํ(์์ญ์ต ๋น๊ต)๋ก ํญ๋ฐํ๋ค | |
| # (์๋๋ฆฌ์ค ์ฟผ๋ฆฌ 1ํ ~254์ด ๊ด์ธก). ์ธ๋ฑ์ค๊ฐ ์์ผ๋ฉด ~0.2์ด๋ก ๋จ์ด์ง๋ค. ๋ฉฑ๋ฑ ์์ฑ. | |
| _PERF_INDEXES = [ | |
| "CREATE INDEX IF NOT EXISTS ix_vbup_norm ON VBUP(CAST(VBELN AS INTEGER), CAST(POSNR AS INTEGER))", | |
| "CREATE INDEX IF NOT EXISTS ix_vbep_norm ON VBEP(CAST(VBELN AS INTEGER), CAST(POSNR AS INTEGER))", | |
| "CREATE INDEX IF NOT EXISTS ix_mard_mw ON MARD(MATNR, WERKS)", | |
| "CREATE INDEX IF NOT EXISTS ix_makt_m ON MAKT(MATNR)", | |
| ] | |
| def _ensure_perf_indexes(conn: sqlite3.Connection) -> None: | |
| """์ํ๋ง ์ฑ๋ฅ์ฉ ์ธ๋ฑ์ค๋ฅผ ๋ฉฑ๋ฑ ์์ฑ. ์คํจํด๋(์: ์ฝ๊ธฐ์ ์ฉ/๋ฝ) ํ๋ฆ์ ๋์ง ์๋๋ค.""" | |
| try: | |
| for ddl in _PERF_INDEXES: | |
| conn.execute(ddl) | |
| conn.commit() | |
| except sqlite3.Error as e: | |
| logger.warning("Perf index creation skipped: %s", e) | |
| def _canonical_query_and_row( | |
| db_path: str, order_id: str, item_no: str | int, | |
| conn: sqlite3.Connection | None = None, | |
| ) -> tuple[str, dict | None]: | |
| """expected_values + golden_sql์ ๋จ์ผ ์ถ์ฒ(source of truth). | |
| ํ๋ก๋์ ํ๋กฌํํธ๊ฐ LLM์๊ฒ ์ง์ํ๋ ์๋ฏธ์ 1:1๋ก ์ผ์นํ๋ ๋ ํผ๋ฐ์ค ์ฟผ๋ฆฌ | |
| (src.tools.text2sql._hardcoded_query)๋ฅผ ๊ทธ๋๋ก ์คํํด ์ ๋ต ํ์ ๋ง๋ ๋ค. | |
| ์๋๋ฆฌ์ค SQL์ (order_id, item_no) ๋ค์์ฑ ์ ํ์๋ง ์ฐ๊ณ , ๊ฐยท์ ๋ตSQL์ ์ฌ๊ธฐ์ ์ฐ์ถํ๋ค. | |
| ์ด๋ ๊ฒ ํด์ผ ๊ณจ๋ (๊ฐ+SQL)๊ณผ ํ๋กฌํํธ๊ฐ ๊ฐ์ ๊ฒฐ์ ์ ์ ์๋ฅผ ๊ณต์ ํ๋ค. | |
| conn์ด ์ฃผ์ด์ง๋ฉด ์ฌ์ฌ์ฉํ๋ค(๋ฐฐ์น ์ํ๋ง ์ฑ๋ฅ). | |
| Returns: (golden_sql, ์ ๋ต ํ or None) | |
| """ | |
| from src.tools.text2sql import _hardcoded_query | |
| sql = _hardcoded_query(str(order_id), str(item_no)) | |
| rows = _query(db_path, sql, conn=conn) | |
| return sql, (rows[0] if rows else None) | |
| # --------------------------------------------------------------------------- | |
| # ํ ์คํธ ์ผ์ด์ค ๋น๋ | |
| # --------------------------------------------------------------------------- | |
| def _build_expected_values(row: dict) -> dict | None: | |
| """DB ํ์์ expected_values dict๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.""" | |
| ev: dict = {} | |
| if row.get("material_name"): | |
| ev["material_name"] = row["material_name"] | |
| if row.get("quantity") is not None: | |
| ev["quantity"] = float(row["quantity"]) | |
| if row.get("delivery_status"): | |
| ev["delivery_status"] = row["delivery_status"] | |
| if row.get("delivery_date"): | |
| ev["delivery_date"] = _edatu_to_iso(row["delivery_date"]) | |
| if row.get("available_stock") is not None: | |
| ev["available_stock"] = float(row["available_stock"]) | |
| return ev if ev else None | |
| def _sample_cases( | |
| db_path: str, | |
| n_per_scenario: int, | |
| seed: int = 42, | |
| ) -> list[TestCase]: | |
| """๊ฐ ์ํ๋ง ์๋๋ฆฌ์ค์์ n_per_scenario๊ฐ์ฉ ์ผ์ด์ค๋ฅผ ์์งํฉ๋๋ค.""" | |
| random.seed(seed) | |
| cases: list[TestCase] = [] | |
| # ์ฐ๊ฒฐ 1๊ฐ ์ฌ์ฌ์ฉ(264+ ์คํ ๋ฐฉ์ง + ํ์ด์ง ์บ์ ์ ์ง) + CAST ์กฐ์ธ ์ธ๋ฑ์ค ๋ฉฑ๋ฑ ์์ฑ. | |
| # ์ธ๋ฑ์ค๊ฐ ์์ผ๋ฉด ์๋๋ฆฌ์ค ์ฟผ๋ฆฌ์ CAST ์๋ณ ์กฐ์ธ์ด ์ค์ฒฉ๋ฃจํ๋ก ํญ๋ฐํ๋ค(1ํ ~254์ด). | |
| conn = sqlite3.connect(db_path) | |
| conn.row_factory = sqlite3.Row | |
| _ensure_perf_indexes(conn) | |
| try: | |
| for desc_tmpl, notes, sql_tmpl in _SAMPLING_QUERIES: | |
| # ๋๋ํ๊ฒ ์ฟผ๋ฆฌ ํ ๋๋ค ์ ํ | |
| sql = sql_tmpl.format(limit=n_per_scenario * 5) | |
| try: | |
| rows = _query(db_path, sql, conn=conn) | |
| except Exception as e: | |
| logger.warning("Sampling failed (%s): %s", desc_tmpl[:40], e) | |
| continue | |
| sampled = random.sample(rows, min(n_per_scenario, len(rows))) | |
| for row in sampled: | |
| order_id = str(row.get("order_id", "")).strip() | |
| item_no = int(row.get("item_no", 10)) | |
| # expected_valuesยทgolden_sql์ ์๋๋ฆฌ์ค ํ์ด ์๋๋ผ ๋ ํผ๋ฐ์ค(์บ๋ ธ๋์ปฌ) ์ฟผ๋ฆฌ์์ ์ฐ์ถ | |
| golden_sql, canon = _canonical_query_and_row(db_path, order_id, item_no, conn=conn) | |
| cases.append(TestCase( | |
| id="", # ๋์ค์ ๋ถ์ฌ | |
| description=desc_tmpl, | |
| order_id=order_id, | |
| item_no=item_no, | |
| expected_columns=REQUIRED_COLUMNS, | |
| expected_values=_build_expected_values(canon) if canon else None, | |
| golden_sql=golden_sql, | |
| notes=notes, | |
| )) | |
| finally: | |
| conn.close() | |
| return cases | |
| def _add_negative_case(cases: list[TestCase]) -> None: | |
| """์กด์ฌํ์ง ์๋ ์ฃผ๋ฌธ๋ฒํธ ์ผ์ด์ค(expected_values=None)๋ฅผ ์ถ๊ฐํฉ๋๋ค. | |
| ์ ๋ต SQL์ ์ ํจํ์ง๋ง ๊ฒฐ๊ณผ ํ์ด ์๋ ์ผ์ด์ค๋ฅผ ๊ฒ์ฆํ๋ค.""" | |
| from src.tools.text2sql import _hardcoded_query | |
| cases.append(TestCase( | |
| id="", | |
| description="Non-existent order โ should return no row (None result)", | |
| order_id=NEGATIVE_ORDER_ID, | |
| item_no=10, | |
| expected_columns=REQUIRED_COLUMNS, | |
| expected_values=None, | |
| golden_sql=_hardcoded_query(NEGATIVE_ORDER_ID, "10"), | |
| notes="SQL must execute without error even if no rows returned", | |
| )) | |
| # --------------------------------------------------------------------------- | |
| # ๋ฉ์ธ ์์ฑ ํจ์ | |
| # --------------------------------------------------------------------------- | |
| def generate_text2sql_dataset( | |
| n_samples: int = 20, | |
| output_path: str = DEFAULT_OUTPUT, | |
| db_path: str = DEFAULT_DB, | |
| seed: int = 42, | |
| append: bool = False, | |
| ) -> list[dict]: | |
| """ | |
| SQLite DB๋ฅผ ์ํ๋งํ์ฌ text2sql ํ ์คํธ ์ผ์ด์ค๋ฅผ ์์ฑํฉ๋๋ค. | |
| Parameters | |
| ---------- | |
| n_samples : ์๋๋ฆฌ์ค๋น ์ํ ์ (์ค์ ์ด ์ผ์ด์ค ์ โ n_samples ร ์๋๋ฆฌ์ค ์) | |
| output_path : ์ถ๋ ฅ JSON ๊ฒฝ๋ก | |
| db_path : sap_erp.db ๊ฒฝ๋ก | |
| seed : ๋๋ค ์๋ (์ฌํ์ฑ) | |
| append : ๊ธฐ์กด ํ์ผ์ ์ถ๊ฐ ์ฌ๋ถ | |
| """ | |
| if not Path(db_path).exists(): | |
| raise FileNotFoundError( | |
| f"DB file not found: {db_path}\n" | |
| "Check the paths.erp_db configuration in configs.yaml." | |
| ) | |
| logger.info("Starting DB sampling: %s", db_path) | |
| cases = _sample_cases(db_path, n_samples, seed) | |
| _add_negative_case(cases) | |
| # ์ค๋ณต (order_id, item_no) ์ ๊ฑฐ | |
| seen: set[tuple] = set() | |
| deduped: list[TestCase] = [] | |
| for c in cases: | |
| key = (c.order_id, c.item_no) | |
| if key not in seen: | |
| seen.add(key) | |
| deduped.append(c) | |
| cases = deduped | |
| # id ๋ถ์ฌ | |
| start_idx = 1 | |
| if append: | |
| existing_path = Path(output_path) | |
| if existing_path.exists(): | |
| try: | |
| existing = json.loads(existing_path.read_text(encoding="utf-8")) | |
| start_idx = len(existing) + 1 | |
| except Exception: | |
| pass | |
| for idx, c in enumerate(cases, start=start_idx): | |
| c.id = f"tc_{idx:03d}" | |
| # dict ๋ณํ | |
| records = [asdict(c) for c in cases] | |
| # append ์ฒ๋ฆฌ | |
| if append: | |
| existing_path = Path(output_path) | |
| if existing_path.exists(): | |
| try: | |
| existing = json.loads(existing_path.read_text(encoding="utf-8")) | |
| records = existing + records | |
| except Exception as e: | |
| logger.warning("Failed to read existing file: %s", e) | |
| out = Path(output_path) | |
| out.parent.mkdir(parents=True, exist_ok=True) | |
| out.write_text(json.dumps(records, indent=2, ensure_ascii=False), encoding="utf-8") | |
| # Summary | |
| print("\n" + "=" * 60) | |
| print(" Text-to-SQL Dataset Generation Results") | |
| print("=" * 60) | |
| print(f" DB Path : {db_path}") | |
| print(f" Scenarios : {len(_SAMPLING_QUERIES) + 1}") | |
| print(f" Generated Cases : {len(cases)}") | |
| print(f" Output File : {output_path}") | |
| print("=" * 60) | |
| return records | |
| # --------------------------------------------------------------------------- | |
| # CLI | |
| # --------------------------------------------------------------------------- | |
| def _parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="Text-to-SQL ํ๊ฐ์ฉ ํ ์คํธ ์ผ์ด์ค ์์ฑ") | |
| parser.add_argument( | |
| "--n-samples", type=int, default=10, | |
| help="์๋๋ฆฌ์ค๋น ์ํ ์ (๊ธฐ๋ณธ: 10, ์ด ์ผ์ด์ค โ 10 ร ์๋๋ฆฌ์ค ์)", | |
| ) | |
| parser.add_argument( | |
| "--output", default=DEFAULT_OUTPUT, | |
| help=f"์ถ๋ ฅ JSON ๊ฒฝ๋ก (๊ธฐ๋ณธ: {DEFAULT_OUTPUT})", | |
| ) | |
| parser.add_argument( | |
| "--db", default=DEFAULT_DB, | |
| help=f"SQLite DB ๊ฒฝ๋ก (๊ธฐ๋ณธ: {DEFAULT_DB})", | |
| ) | |
| parser.add_argument( | |
| "--seed", type=int, default=42, | |
| help="๋๋ค ์๋ (๊ธฐ๋ณธ: 42)", | |
| ) | |
| parser.add_argument( | |
| "--append", action="store_true", | |
| help="๊ธฐ์กด ์ถ๋ ฅ ํ์ผ์ ๊ฒฐ๊ณผ๋ฅผ ์ถ๊ฐ", | |
| ) | |
| return parser.parse_args() | |
| if __name__ == "__main__": | |
| args = _parse_args() | |
| generate_text2sql_dataset( | |
| n_samples=args.n_samples, | |
| output_path=args.output, | |
| db_path=args.db, | |
| seed=args.seed, | |
| append=args.append, | |
| ) | |