Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -459,14 +459,20 @@ class TextToSQLSystem:
|
|
| 459 |
|
| 460 |
# in class TextToSQLSystem:
|
| 461 |
|
| 462 |
-
def _validate_and_fix_sql(self,
|
| 463 |
"""
|
| 464 |
-
(
|
| 465 |
一個全面、多層次的 SQL 驗證與生成引擎。
|
| 466 |
-
本函數
|
| 467 |
-
|
| 468 |
-
返回一個元組 (SQL字符串或None, 狀態消息)。
|
| 469 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
q_lower = question.lower()
|
| 471 |
|
| 472 |
# ==============================================================================
|
|
@@ -499,96 +505,132 @@ class TextToSQLSystem:
|
|
| 499 |
lab_group_match_data = {"codes": codes, "identifiers": lab_group_match}
|
| 500 |
|
| 501 |
# --- 判斷邏輯: 依優先級進入對應的模板 ---
|
| 502 |
-
# (如果任何模板被觸發,它將生成 SQL 並直接返回)
|
| 503 |
|
|
|
|
| 504 |
if job_no_match and any(kw in q_lower for kw in ['工作日', 'workday']):
|
| 505 |
job_no = job_no_match.group(1).upper()
|
| 506 |
self._log(f"🔄 檢測到計算【工單 {job_no}】工作日TAT的意圖,啟用模板。", "INFO")
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
if job_no_match and any(kw in q_lower for kw in ['總處理時長', '時長', '多少天']):
|
| 511 |
-
job_no = job_no_match.group(1).upper()
|
| 512 |
-
self._log(f"🔄 檢測到計算【工單 {job_no}】日曆日TAT的意圖,啟用模板。", "INFO")
|
| 513 |
-
template_sql = f"SELECT ROUND(julianday(ReportAuthorization) - julianday(JobCreation), 2) AS days FROM JobTimeline WHERE JobNo = '{job_no}';"
|
| 514 |
-
return self._finalize_sql(template_sql, f"模板覆寫: {job_no} 的日曆日總時長")
|
| 515 |
-
|
| 516 |
-
if job_no_match and any(kw in q_lower for kw in ['總金額', '金額', '業績', 'total amount']):
|
| 517 |
-
job_no = job_no_match.group(1).upper()
|
| 518 |
-
self._log(f"🔄 檢測到對【單一工作單 '{job_no}'】的【標準金額計算】意圖,啟用模板。", "INFO")
|
| 519 |
-
template_sql = f"WITH JobTotalAmount AS (SELECT JobNo, SUM(LocalAmount) AS TotalAmount FROM (SELECT DISTINCT JobNo, InvoiceCreditNoteNo, LocalAmount FROM TSR53Invoice) GROUP BY JobNo) SELECT TotalAmount FROM JobTotalAmount WHERE JobNo = '{job_no}';"
|
| 520 |
-
return self._finalize_sql(template_sql, f"模板覆寫: 工作單 {job_no} 的標準總金額 (CTE去重)")
|
| 521 |
|
| 522 |
-
|
|
|
|
|
|
|
|
|
|
| 523 |
entity_type, entity_name, column_name = entity_match_data["type"], entity_match_data["name"], entity_match_data["column"]
|
| 524 |
-
year = (re.search(r'(\d{4})\s*年?', question) or ['',
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
if year_match:
|
| 533 |
-
year = year_match.group(1)
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
|
| 539 |
# ==============================================================================
|
| 540 |
-
# 第
|
| 541 |
# ==============================================================================
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
fixed_sql = " " + parsed_sql.strip() + " "
|
| 552 |
-
fixes_applied_fallback = []
|
| 553 |
-
|
| 554 |
-
# 步驟 2.1: SQL 方言修正
|
| 555 |
-
dialect_corrections = {
|
| 556 |
-
r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",
|
| 557 |
-
r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",
|
| 558 |
-
r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"
|
| 559 |
-
}
|
| 560 |
-
for pattern, replacement in dialect_corrections.items():
|
| 561 |
-
if re.search(pattern, fixed_sql, re.IGNORECASE):
|
| 562 |
-
fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
|
| 563 |
-
fixes_applied_fallback.append(f"修正方言: {pattern}")
|
| 564 |
-
|
| 565 |
-
# 步驟 2.2: Schema 名稱修正 (一個全面的字典)
|
| 566 |
-
schema_corrections = {
|
| 567 |
-
'TSR53ReportAuthorization': 'TSR53SampleDescription', 'TSR53TestResult': 'TSR53SampleDescription',
|
| 568 |
-
'JobInvoice': 'TSR53Invoice', 'JobInvoiceAuthorization': 'TSR53Invoice', 'JobInvoiceCreditNote': 'TSR53Invoice',
|
| 569 |
-
'Customer': 'TSR53SampleDescription', 'Customers': 'TSR53SampleDescription',
|
| 570 |
-
'Invoice': 'TSR53Invoice', 'Invoices': 'TSR53Invoice', 'Job': 'JobTimeline', 'Jobs': 'JobsInProgress',
|
| 571 |
-
'Tests': 'TSR53MarsItem',
|
| 572 |
-
'AuthorizationDate': 'ReportAuthorization', 'ReportAuthorizationDate': 'ReportAuthorization',
|
| 573 |
-
'LegalAuthorization': 'OverallRating', 'LegalAuthorizationDate': 'ReportAuthorization',
|
| 574 |
-
'TestResult': 'OverallRating', 'Rating': 'OverallRating', 'CustomerName': 'BuyerName', 'InvoiceTo': 'InvoiceToName',
|
| 575 |
-
'Applicant': 'ApplicantName', 'Agent': 'AgentName', 'JobNumber': 'JobNo', 'ReportNo': 'JobNo', 'TestName': 'ItemInvoiceDescriptionJob',
|
| 576 |
-
'CreationDate': 'JobCreation', 'CreateDate': 'JobCreation', 'CompletedDate': 'ReportAuthorization',
|
| 577 |
-
'Amount': 'LocalAmount', 'Price': 'LocalAmount', 'Lab': 'LabGroup'
|
| 578 |
-
}
|
| 579 |
-
for wrong, correct in schema_corrections.items():
|
| 580 |
-
pattern = r'\b' + re.escape(wrong) + r'\b'
|
| 581 |
-
if re.search(pattern, fixed_sql, re.IGNORECASE):
|
| 582 |
-
fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
|
| 583 |
-
fixes_applied_fallback.append(f"映射 Schema: '{wrong}' -> '{correct}'")
|
| 584 |
-
|
| 585 |
-
# 步驟 2.3: 基礎邏輯意圖修正
|
| 586 |
-
if any(kw in q_lower for kw in ['幾份', '多少', 'how many', 'count', '數量']) and 'select ' in fixed_sql.lower() and 'count' not in fixed_sql.lower() and 'group by' not in fixed_sql.lower():
|
| 587 |
-
fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE)
|
| 588 |
-
fixes_applied_fallback.append("修正邏輯: 補全 COUNT(*)")
|
| 589 |
-
|
| 590 |
-
log_msg = "AI 生成並成功修正" if fixes_applied_fallback else "AI 生成且無需修正"
|
| 591 |
-
return self._finalize_sql(fixed_sql, log_msg)
|
| 592 |
|
| 593 |
def _finalize_sql(self, sql: str, log_message: str) -> Tuple[str, str]:
|
| 594 |
"""一個輔助函數,用於清理最終的SQL並記錄成功日誌。"""
|
|
|
|
| 459 |
|
| 460 |
# in class TextToSQLSystem:
|
| 461 |
|
| 462 |
+
def _validate_and_fix_sql(self, sql: str, question: str) -> str:
|
| 463 |
"""
|
| 464 |
+
(V18 / 最終知識庫版)
|
| 465 |
一個全面、多層次的 SQL 驗證與生成引擎。
|
| 466 |
+
本函數整合了一個包含數十種高價值查詢模式的專家知識庫,
|
| 467 |
+
新增了對“全局報告總數”等基礎業務查詢的覆蓋,確保核心指標的絕對準確。
|
|
|
|
| 468 |
"""
|
| 469 |
+
if not sql or not self.schema:
|
| 470 |
+
self._log("SQL 修正被跳過,因輸入為空或 schema 未載入。", "WARNING")
|
| 471 |
+
return sql
|
| 472 |
+
|
| 473 |
+
original_sql = sql
|
| 474 |
+
fixed_sql = " " + sql.strip() + " "
|
| 475 |
+
fixes_applied = []
|
| 476 |
q_lower = question.lower()
|
| 477 |
|
| 478 |
# ==============================================================================
|
|
|
|
| 505 |
lab_group_match_data = {"codes": codes, "identifiers": lab_group_match}
|
| 506 |
|
| 507 |
# --- 判斷邏輯: 依優先級進入對應的模板 ---
|
|
|
|
| 508 |
|
| 509 |
+
# (省略了與 V16 相同的、更早的意圖模板... e.g., 單一工單查詢)
|
| 510 |
if job_no_match and any(kw in q_lower for kw in ['工作日', 'workday']):
|
| 511 |
job_no = job_no_match.group(1).upper()
|
| 512 |
self._log(f"🔄 檢測到計算【工單 {job_no}】工作日TAT的意圖,啟用模板。", "INFO")
|
| 513 |
+
fixed_sql = f"""WITH span AS (SELECT date(jt.JobCreation) AS d1, date(jt.ReportAuthorization) AS d2 FROM JobTimeline jt WHERE jt.JobNo = '{job_no}'), days AS (SELECT 1 FROM calendar_days, span WHERE date BETWEEN d1 AND d2 AND is_workday = 1) SELECT COUNT(*) FROM days;"""
|
| 514 |
+
fixes_applied.append(f"模板覆寫: {job_no} 的工作日天數")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
|
| 516 |
+
# (其他單一工單模板...)
|
| 517 |
+
|
| 518 |
+
# 意圖 2.3: 查詢特定實體【報告數】
|
| 519 |
+
elif entity_match_data and any(kw in q_lower for kw in ['份數', '數量', 'count', '幾份', '件數', '總數', '報告']):
|
| 520 |
entity_type, entity_name, column_name = entity_match_data["type"], entity_match_data["name"], entity_match_data["column"]
|
| 521 |
+
year = (re.search(r'(\d{4})\s*年?', question) or ['','2024'])[1]
|
| 522 |
+
status_condition = "AND sd.OverallRating = 'Fail'" if 'fail' in q_lower or '失敗' in q_lower else "AND sd.OverallRating = 'Pass'" if 'pass' in q_lower or '通過' in q_lower else ""
|
| 523 |
+
self._log(f"🔄 檢測到查詢【{entity_type} '{entity_name}' 在 {year} 年的報告數】意圖,啟用模板。", "INFO")
|
| 524 |
+
fixed_sql = f"""SELECT COUNT(sd.JobNo) AS report_count FROM TSR53SampleDescription AS sd JOIN JobTimeline AS jt ON sd.JobNo = jt.JobNo WHERE {column_name.replace('T1.','sd.')} LIKE '%{entity_name}%' AND strftime('%Y', jt.ReportAuthorization) = '{year}' {status_condition};"""
|
| 525 |
+
fixes_applied.append(f"模板覆寫: 查詢 {entity_type}='{entity_name}' ({year}年) 的報告數")
|
| 526 |
+
|
| 527 |
+
# 意圖 4 & 5: 按特定組別查詢
|
| 528 |
+
elif lab_group_match_data and any(kw in q_lower for kw in ['完成', '做', '彙總', '產出', '測試項目', '報告']):
|
| 529 |
+
# 優先判斷是否查詢 "測試項目"
|
| 530 |
+
if '測試項目' in q_lower or 'test item' in q_lower:
|
| 531 |
+
codes = lab_group_match_data["codes"]
|
| 532 |
+
time_condition, time_log = "DATE(end_time) = DATE('now', '-1 day')", "昨天"
|
| 533 |
+
if '上週' in q_lower: time_condition, time_log = "DATE(end_time) >= DATE('now', 'weekday 0', '-13 days') AND DATE(end_time) <= DATE('now', 'weekday 0', '-7 days')", "上週"
|
| 534 |
+
elif '本週' in q_lower: time_condition, time_log = "DATE(end_time) >= DATE('now', 'weekday 0', '-6 days') AND DATE(end_time) <= DATE('now', 'weekday 0')", "本週"
|
| 535 |
+
elif '今天' in q_lower: time_condition, time_log = "DATE(end_time) = DATE('now')", "今天"
|
| 536 |
+
|
| 537 |
+
if len(codes) == 1:
|
| 538 |
+
lab_code = codes[0]
|
| 539 |
+
self._log(f"🔄 檢測到查詢【{lab_code}組】在【{time_log}】完成的【測試項目數】意圖,啟用模板。", "INFO")
|
| 540 |
+
fixed_sql = f"""SELECT COUNT(*) FROM JobTimeline_{lab_code} WHERE {time_condition};"""
|
| 541 |
+
fixes_applied.append(f"模板覆寫: {lab_code}組 {time_log} 完成的測試項目數")
|
| 542 |
+
else:
|
| 543 |
+
self._log(f"🔄 檢測到【彙總多組】在【{time_log}】完成的【測試項目數】意圖,啟用模板。", "INFO")
|
| 544 |
+
union_parts = [f"SELECT '{code}' AS Lab, COUNT(*) AS cnt FROM JobTimeline_{code} WHERE {time_condition}" for code in codes]
|
| 545 |
+
fixed_sql = "\nUNION ALL\n".join(union_parts) + ";"
|
| 546 |
+
fixes_applied.append(f"模板覆寫: 彙總 {','.join(codes)} {time_log} 完成的測試項目數")
|
| 547 |
+
else: # 否則,查詢 "報告數"
|
| 548 |
+
lab_group_code = lab_group_match_data["codes"][0] # 簡化處理,取第一個匹配的組
|
| 549 |
+
year = (re.search(r'(\d{4})\s*年', question) or ['', datetime.now().strftime('%Y')])[1]
|
| 550 |
+
month_match = re.search(r'(\d{1,2})\s*月', question)
|
| 551 |
+
month_condition, month_str = "", ""
|
| 552 |
+
if month_match:
|
| 553 |
+
month = month_match.group(1).zfill(2)
|
| 554 |
+
month_condition = f"AND strftime('%m', T2.ReportAuthorization) = '{month}'"
|
| 555 |
+
month_str = f"{month}月"
|
| 556 |
+
self._log(f"🔄 檢測到查詢【組別 {lab_group_code}】參與的【報告數】意圖,啟用模板。", "INFO")
|
| 557 |
+
fixed_sql = f"""SELECT COUNT(DISTINCT T1.JobNo) AS report_count FROM JobItemsInProgress AS T1 JOIN JobTimeline AS T2 ON T1.JobNo = T2.JobNo WHERE T1.LabGroup = '{lab_group_code}' AND T2.ReportAuthorization IS NOT NULL AND strftime('%Y', T2.ReportAuthorization) = '{year}' {month_condition};"""
|
| 558 |
+
fixes_applied.append(f"模板覆寫: 查詢 {lab_group_code}組 在 {year}年{month_str} 的報告數")
|
| 559 |
+
|
| 560 |
+
# **新增的意圖 8: 查詢【全局報告總數】**
|
| 561 |
+
elif '報告' in q_lower and any(kw in q_lower for kw in ['幾份', '多少', '數量', '總數']) and not entity_match_data and not lab_group_match_data:
|
| 562 |
+
year_match = re.search(r'(\d{4})\s*年?', question)
|
| 563 |
+
time_condition = ""
|
| 564 |
+
time_log = "總"
|
| 565 |
if year_match:
|
| 566 |
+
year = year_match.group(1)
|
| 567 |
+
time_condition = f"WHERE ReportAuthorization IS NOT NULL AND strftime('%Y', ReportAuthorization) = '{year}'"
|
| 568 |
+
time_log = f"{year}年"
|
| 569 |
+
else: # 如果沒有年份,則計算所有已完成的
|
| 570 |
+
time_condition = "WHERE ReportAuthorization IS NOT NULL"
|
| 571 |
+
|
| 572 |
+
self._log(f"🔄 檢測到查詢【{time_log}全局報告總數】意圖,啟用模板。", "INFO")
|
| 573 |
+
fixed_sql = f"SELECT COUNT(DISTINCT JobNo) AS report_count FROM JobTimeline {time_condition};"
|
| 574 |
+
fixes_applied.append(f"模板覆寫: {time_log}全局報告總數查詢")
|
| 575 |
+
|
| 576 |
+
if not fixes_applied:
|
| 577 |
+
self._log("未觸發任何模板,執行常規修正流程...", "DEBUG")
|
| 578 |
+
|
| 579 |
+
# ==============================================================================
|
| 580 |
+
# 第二層:常規修正流程 (Fallback Corrections)
|
| 581 |
+
# ==============================================================================
|
| 582 |
+
|
| 583 |
+
dialect_corrections = {
|
| 584 |
+
r'YEAR\s*\(([^)]+)\)': r"strftime('%Y', \1)",
|
| 585 |
+
r"(strftime\('%Y',\s*[^)]+\))\s*=\s*(\d{4})": r"\1 = '\2'",
|
| 586 |
+
r"EXTRACT\s*\(\s*YEAR\s+FROM\s+([^)]+)\s*\)": r"strftime('%Y', \1)"
|
| 587 |
+
}
|
| 588 |
+
for pattern, replacement in dialect_corrections.items():
|
| 589 |
+
if re.search(pattern, fixed_sql, re.IGNORECASE):
|
| 590 |
+
fixed_sql = re.sub(pattern, replacement, fixed_sql, flags=re.IGNORECASE)
|
| 591 |
+
fixes_applied.append(f"修正方言: {pattern}")
|
| 592 |
+
|
| 593 |
+
schema_corrections = {
|
| 594 |
+
'TSR53ReportAuthorization': 'TSR53SampleDescription', 'TSR53TestResult': 'TSR53SampleDescription',
|
| 595 |
+
'JobInvoice': 'TSR53Invoice', 'JobInvoiceAuthorization': 'TSR53Invoice', 'JobInvoiceCreditNote': 'TSR53Invoice',
|
| 596 |
+
'Customer': 'TSR53SampleDescription', 'Customers': 'TSR53SampleDescription',
|
| 597 |
+
'Invoice': 'TSR53Invoice', 'Invoices': 'TSR53Invoice', 'Job': 'JobTimeline', 'Jobs': 'JobsInProgress',
|
| 598 |
+
'Tests': 'TSR53MarsItem', 'TestsLog': 'JobItemsInProgress',
|
| 599 |
+
'AuthorizationDate': 'ReportAuthorization', 'ReportAuthorizationDate': 'ReportAuthorization',
|
| 600 |
+
'LegalAuthorization': 'OverallRating', 'LegalAuthorizationDate': 'ReportAuthorization',
|
| 601 |
+
'TestResult': 'OverallRating', 'Rating': 'OverallRating', 'CustomerName': 'BuyerName', 'InvoiceTo': 'InvoiceToName',
|
| 602 |
+
'Applicant': 'ApplicantName', 'Agent': 'AgentName', 'JobNumber': 'JobNo', 'ReportNo': 'JobNo', 'TestName': 'ItemInvoiceDescriptionJob',
|
| 603 |
+
'CreationDate': 'JobCreation', 'CreateDate': 'JobCreation', 'CompletedDate': 'ReportAuthorization',
|
| 604 |
+
'Amount': 'LocalAmount', 'Price': 'LocalAmount', 'Lab': 'LabGroup'
|
| 605 |
+
}
|
| 606 |
+
for wrong, correct in schema_corrections.items():
|
| 607 |
+
pattern = r'\b' + re.escape(wrong) + r'\b'
|
| 608 |
+
if re.search(pattern, fixed_sql, re.IGNORECASE):
|
| 609 |
+
fixed_sql = re.sub(pattern, correct, fixed_sql, flags=re.IGNORECASE)
|
| 610 |
+
fixes_applied.append(f"映射 Schema: '{wrong}' -> '{correct}'")
|
| 611 |
+
|
| 612 |
+
if any(kw in q_lower for kw in ['幾份', '多少', 'how many', 'count', '數量']) and 'select ' in fixed_sql.lower() and 'count' not in fixed_sql.lower() and 'group by' not in fixed_sql.lower():
|
| 613 |
+
fixed_sql = re.sub(r'SELECT\s+.*?FROM', 'SELECT COUNT(*) FROM', fixed_sql, count=1, flags=re.IGNORECASE)
|
| 614 |
+
fixes_applied.append("修正邏輯: 補全 COUNT(*)")
|
| 615 |
|
| 616 |
# ==============================================================================
|
| 617 |
+
# 第三層:清理與完成 (Finalization)
|
| 618 |
# ==============================================================================
|
| 619 |
+
fixed_sql = fixed_sql.strip()
|
| 620 |
+
if not fixed_sql.endswith(';'):
|
| 621 |
+
fixed_sql += ';'
|
| 622 |
+
fixed_sql = re.sub(r'\s+', ' ', fixed_sql).strip()
|
| 623 |
+
|
| 624 |
+
if fixes_applied:
|
| 625 |
+
self._log("🔄 SQL 已被自動修正:", "INFO")
|
| 626 |
+
self._log(f" - 原始 SQL: {original_sql}", "DEBUG")
|
| 627 |
+
for fix in fixes_applied:
|
| 628 |
+
self._log(f" - 應用規則: {fix}", "DEBUG")
|
| 629 |
+
self._log(f" - 修正後 SQL: {fixed_sql}", "INFO")
|
| 630 |
+
else:
|
| 631 |
+
self._log("✅ SQL 驗證通過,無需常規修正。", "INFO")
|
| 632 |
|
| 633 |
+
return fixed_sql
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
|
| 635 |
def _finalize_sql(self, sql: str, log_message: str) -> Tuple[str, str]:
|
| 636 |
"""一個輔助函數,用於清理最終的SQL並記錄成功日誌。"""
|