sql-dialect-master 1.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. backend/__init__.py +1 -0
  2. backend/api/__init__.py +1 -0
  3. backend/api/main.py +732 -0
  4. backend/api/middleware.py +274 -0
  5. backend/api/rate_limit_store.py +105 -0
  6. backend/api/readiness.py +178 -0
  7. backend/core/__init__.py +57 -0
  8. backend/core/cache.py +261 -0
  9. backend/core/column_hint_validation.py +22 -0
  10. backend/core/config.py +546 -0
  11. backend/core/constants.py +84 -0
  12. backend/core/exceptions.py +124 -0
  13. backend/core/function_call_scanner.py +64 -0
  14. backend/core/function_semantics.py +148 -0
  15. backend/core/functions_db.json +601 -0
  16. backend/core/functions_lookup.py +332 -0
  17. backend/core/metadata.py +71 -0
  18. backend/core/nl2sql.py +85 -0
  19. backend/core/nl2sql_components/__init__.py +72 -0
  20. backend/core/nl2sql_components/aggregations.py +35 -0
  21. backend/core/nl2sql_components/boolean_conditions.py +279 -0
  22. backend/core/nl2sql_components/core_extract.py +106 -0
  23. backend/core/nl2sql_components/evidence.py +122 -0
  24. backend/core/nl2sql_components/mappings.py +185 -0
  25. backend/core/nl2sql_components/relations.py +180 -0
  26. backend/core/nl2sql_components/templates.py +130 -0
  27. backend/core/nl2sql_components/tokenizer.py +187 -0
  28. backend/core/nl2sql_legacy.py +22 -0
  29. backend/core/nl2sql_pipeline/__init__.py +13 -0
  30. backend/core/nl2sql_pipeline/aggregations.py +26 -0
  31. backend/core/nl2sql_pipeline/clause_extract.py +97 -0
  32. backend/core/nl2sql_pipeline/dialect.py +139 -0
  33. backend/core/nl2sql_pipeline/intent.py +85 -0
  34. backend/core/nl2sql_pipeline/ir.py +70 -0
  35. backend/core/nl2sql_pipeline/models.py +30 -0
  36. backend/core/nl2sql_pipeline/orchestrator.py +576 -0
  37. backend/core/nl2sql_pipeline/predicates.py +182 -0
  38. backend/core/nl2sql_pipeline/renderer.py +248 -0
  39. backend/core/nl2sql_pipeline/resolution.py +69 -0
  40. backend/core/nl2sql_pipeline/suggestions.py +30 -0
  41. backend/core/nl2sql_pipeline/templates.py +371 -0
  42. backend/core/nl2sql_pipeline/validation.py +28 -0
  43. backend/core/p1_sql_scanner.py +196 -0
  44. backend/core/p2_data_validation.py +72 -0
  45. backend/core/parser.py +280 -0
  46. backend/core/post_processor.py +129 -0
  47. backend/core/rules.py +808 -0
  48. backend/core/semantic_diff.py +385 -0
  49. backend/core/transpiler.py +791 -0
  50. backend/core/type_mapping.json +593 -0
  51. backend/core/type_mapping.py +765 -0
  52. backend/utils/__init__.py +2 -0
  53. backend/utils/validators.py +407 -0
  54. frontend/__init__.py +10 -0
  55. frontend/app_context_v2.py +201 -0
  56. frontend/core/__init__.py +33 -0
  57. frontend/core/design_tokens.py +127 -0
  58. frontend/core/escaping.py +13 -0
  59. frontend/core/navigation.py +135 -0
  60. frontend/core/state.py +612 -0
  61. frontend/core/styles.py +523 -0
  62. frontend/core/themes.py +69 -0
  63. frontend/core/viewmodels.py +499 -0
  64. frontend/pages/__init__.py +0 -0
  65. frontend/pages/convert.py +283 -0
  66. frontend/pages/diff.py +157 -0
  67. frontend/pages/functions.py +100 -0
  68. frontend/pages/history.py +144 -0
  69. frontend/pages/lineage.py +168 -0
  70. frontend/pages/nl2sql.py +305 -0
  71. frontend/pages/query_analysis.py +120 -0
  72. frontend/pages/runtime.py +66 -0
  73. frontend/pages/settings.py +60 -0
  74. frontend/pages/templates.py +67 -0
  75. frontend/pages/types.py +110 -0
  76. frontend/templates_v2.py +53 -0
  77. frontend/ui/__init__.py +0 -0
  78. frontend/ui/command_palette.py +153 -0
  79. frontend/ui/diff.py +275 -0
  80. frontend/ui/editor.py +102 -0
  81. frontend/ui/status.py +255 -0
  82. sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
  83. sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
  84. sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
  85. sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
  86. sql_dialect_master-1.1.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """P8 NL2SQL pipeline: predicate extraction (moved from nl2sql_legacy).
3
+
4
+ ``extract_conditions`` is a verbatim move of
5
+ ``nl2sql_legacy._extract_conditions_enhanced``. All keyword tables,
6
+ IN-predicate parsing, comparison operators, BETWEEN / LIKE / NULL /
7
+ date / time / status rules are unchanged; the only difference is that
8
+ ``COLUMN_PATTERNS`` is now passed as a parameter instead of read from
9
+ the instance attribute.
10
+ """
11
+ import re
12
+ from typing import Dict, List, Optional
13
+
14
+ from ..nl2sql_components.boolean_conditions import extract_boolean_conditions
15
+
16
+
17
+ def extract_conditions(
18
+ text: str,
19
+ original: str,
20
+ column_patterns: Dict[str, str],
21
+ ) -> list:
22
+ """Extract WHERE conditions with enhanced parsing including boolean expressions.
23
+
24
+ Moved verbatim from ``nl2sql_legacy._extract_conditions_enhanced``.
25
+ ``column_patterns`` replaces ``self.COLUMN_PATTERNS``.
26
+ """
27
+ # Use the structured boolean extractor first — it correctly handles
28
+ # multiple predicates connected by AND/OR while preserving precedence.
29
+ boolean_conditions = extract_boolean_conditions(text)
30
+ if boolean_conditions:
31
+ return boolean_conditions
32
+
33
+ conditions = []
34
+ numbers = re.findall(r"\d+\.?\d*", original)
35
+ condition_column = None
36
+ for pattern, column in column_patterns.items():
37
+ if pattern in text:
38
+ condition_column = column
39
+ break
40
+
41
+ # Handle IN/NOT IN predicate (must come before generic comparison handling)
42
+ in_matched_columns: set = set()
43
+ in_match = re.search(
44
+ r"\b(\w+)\s+(not\s+)?in\s*\(([^)]+)\)",
45
+ text,
46
+ re.IGNORECASE,
47
+ )
48
+ if in_match:
49
+ col = in_match.group(1)
50
+ negation = in_match.group(2) is not None
51
+ raw_values = in_match.group(3).strip()
52
+ # Parse comma-separated values, preserving quoted strings
53
+ values = []
54
+ current = ""
55
+ quote_char = None
56
+ for char in raw_values:
57
+ if char in ("'", '"'):
58
+ if quote_char and quote_char == char:
59
+ quote_char = None
60
+ elif not quote_char:
61
+ quote_char = char
62
+ current += char
63
+ elif char == "," and not quote_char:
64
+ if current.strip():
65
+ values.append(current.strip())
66
+ current = ""
67
+ else:
68
+ current += char
69
+ if current.strip():
70
+ values.append(current.strip())
71
+
72
+ if values:
73
+ formatted_values = ", ".join(
74
+ f"'{v}'" if v.startswith(("'", '"')) or not re.match(r"^-?\d+(\.\d+)?$", v) else v
75
+ for v in values
76
+ )
77
+ operator = "NOT IN" if negation else "IN"
78
+ conditions.append(f"{col} {operator} ({formatted_values})")
79
+ in_matched_columns.add(col.lower())
80
+
81
+ comparisons = [
82
+ (["大于等于", "不小于", "至少", "greater than or equal to", "larger than or equal to"], ">="),
83
+ (["小于等于", "不大于", "最多", "less than or equal to", "smaller than or equal to"], "<="),
84
+ (["不等于", "不是", "不为", "not equal", "isn't", "doesn't equal"], "!="),
85
+ (["大于", "超过", "高于", "多于", "greater", "more than", "above", "over", ">"], ">"),
86
+ (["小于", "低于", "少于", "不足", "less", "less than", "below", "under", "<"], "<"),
87
+ (["等于", "是", "为", "equals", "equal", "="], "="),
88
+ ]
89
+ for keywords, op in comparisons:
90
+ if any(keyword in text for keyword in keywords) and numbers:
91
+ col = condition_column or "column"
92
+ conditions.append(f"{col} {op} {numbers[0]}")
93
+ break
94
+
95
+ if any(keyword in text for keyword in ["之间", "范围", "between", "from...to"]):
96
+ if len(numbers) >= 2:
97
+ col = condition_column or "column"
98
+ conditions.append(f"{col} BETWEEN {numbers[0]} AND {numbers[1]}")
99
+
100
+ if any(keyword in text for keyword in ["包含", "含有", "contains", "like", "includes"]):
101
+ match = re.search(r"[\"']([^\"']+)[\"']", original)
102
+ if match:
103
+ col = condition_column or "column"
104
+ conditions.append(f"{col} LIKE '%{match.group(1)}%'")
105
+
106
+ has_negative_null = any(
107
+ keyword in text
108
+ for keyword in ["非空", "不为空", "is not null", "not null", "not empty"]
109
+ )
110
+ if has_negative_null:
111
+ conditions.append(f"{condition_column or 'column'} IS NOT NULL")
112
+ elif any(keyword in text for keyword in ["为空", "空值", "is null", "null", "empty"]):
113
+ conditions.append(f"{condition_column or 'column'} IS NULL")
114
+
115
+ date_col = "created_at" if "created_at" in text else "date"
116
+ if "今天" in text or "today" in text:
117
+ conditions.append(f"{date_col} = CURRENT_DATE")
118
+ elif "昨天" in text or "yesterday" in text:
119
+ conditions.append(f"{date_col} = DATE_SUB(CURRENT_DATE, 1)")
120
+ elif "前天" in text or "day before" in text:
121
+ conditions.append(f"{date_col} = DATE_SUB(CURRENT_DATE, 2)")
122
+ elif "明天" in text or "tomorrow" in text:
123
+ conditions.append(f"{date_col} = DATE_ADD(CURRENT_DATE, 1)")
124
+
125
+ time_patterns = [
126
+ (r"(最近|过去|last|past)\s*(\d+)\s*(天|day)", "DAY"),
127
+ (r"(最近|过去|last|past)\s*(\d+)\s*(周|week)", "WEEK"),
128
+ (r"(最近|过去|last|past)\s*(\d+)\s*(月|month)", "MONTH"),
129
+ (r"(最近|过去|last|past)\s*(\d+)\s*(年|year)", "YEAR"),
130
+ ]
131
+ for pattern, unit in time_patterns:
132
+ match = re.search(pattern, text, re.IGNORECASE)
133
+ if match:
134
+ n = match.group(2)
135
+ if unit == "DAY":
136
+ conditions.append(f"{date_col} >= DATE_SUB(CURRENT_DATE, {n})")
137
+ elif unit == "WEEK":
138
+ conditions.append(f"{date_col} >= DATE_SUB(CURRENT_DATE, {int(n) * 7})")
139
+ elif unit == "MONTH":
140
+ conditions.append(f"{date_col} >= ADD_MONTHS(CURRENT_DATE, -{n})")
141
+ elif unit == "YEAR":
142
+ conditions.append(f"{date_col} >= ADD_MONTHS(CURRENT_DATE, -{int(n) * 12})")
143
+ break
144
+
145
+ # Bare period expressions without explicit count
146
+ if not any(c for c in conditions if "DATE_SUB" in c or "DATE_ADD" in c or "ADD_MONTHS" in c or "CURRENT_DATE" in c):
147
+ if ("last week" in text or "本周" in text) and "this week" not in text and "本周" not in text:
148
+ conditions.append(f"{date_col} >= DATE_SUB(CURRENT_DATE, 7)")
149
+ if "last month" in text:
150
+ conditions.append(f"{date_col} >= ADD_MONTHS(CURRENT_DATE, -1)")
151
+ if "last year" in text:
152
+ conditions.append(f"{date_col} >= ADD_MONTHS(CURRENT_DATE, -12)")
153
+
154
+ if "本周" in text or "this week" in text:
155
+ conditions.append(f"WEEKOFYEAR({date_col}) = WEEKOFYEAR(CURRENT_DATE)")
156
+ if "本月" in text or "this month" in text:
157
+ conditions.append(
158
+ f"MONTH({date_col}) = MONTH(CURRENT_DATE) AND YEAR({date_col}) = YEAR(CURRENT_DATE)"
159
+ )
160
+ if "本年" in text or "this year" in text:
161
+ conditions.append(f"YEAR({date_col}) = YEAR(CURRENT_DATE)")
162
+
163
+ status_patterns = [
164
+ (["有效", "active", "enabled", "valid"], "status = 'active'"),
165
+ (["无效", "inactive", "disabled", "invalid"], "status = 'inactive'"),
166
+ (["已删除", "deleted", "removed"], "is_deleted = 1"),
167
+ (["未删除", "not deleted"], "is_deleted = 0"),
168
+ (["已完成", "completed", "done", "finished"], "status = 'completed'"),
169
+ (["未完成", "pending", "incomplete"], "status = 'pending'"),
170
+ (["已支付", "paid"], "status = 'paid'"),
171
+ (["未支付", "unpaid"], "status = 'unpaid'"),
172
+ ]
173
+ for keywords, condition in status_patterns:
174
+ # Skip status pattern if the column was already matched via IN predicate
175
+ col_match = re.match(r"^(\w+)\s*=", condition)
176
+ col_name = col_match.group(1).lower() if col_match else None
177
+ if col_name and col_name in in_matched_columns:
178
+ continue
179
+ if any(keyword in text for keyword in keywords):
180
+ conditions.append(condition)
181
+ break
182
+ return conditions
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env python3
2
+ """P8 NL2SQL pipeline: SQL renderer (extracted from nl2sql_legacy).
3
+
4
+ ``render_sql`` is the renamed, pure-function form of the legacy
5
+ ``_build_sql_enhanced`` method. Its body is behavior-identical to the
6
+ legacy version; it now operates on a ``QueryIR`` instead of flat
7
+ positional args and returns the same 4-tuple
8
+ ``(sql, explanation, confidence, evidence)``.
9
+ """
10
+ import re
11
+ from typing import Dict, List, Optional, Tuple, Union
12
+
13
+ from ..nl2sql_components.evidence import (
14
+ EVIDENCE_AGGREGATION_PRESENT,
15
+ EVIDENCE_COLUMNS_EXPLICIT,
16
+ EVIDENCE_CONDITIONS_PRESENT,
17
+ EVIDENCE_GROUP_BY,
18
+ EVIDENCE_JOIN_KNOWN,
19
+ EVIDENCE_LIMIT,
20
+ EVIDENCE_PARSE_FAIL,
21
+ EVIDENCE_PARSE_OK,
22
+ EVIDENCE_TABLE_KNOWN,
23
+ EvidenceItem,
24
+ GenerationEvidence,
25
+ )
26
+ from .dialect import apply_dialect_adjustments
27
+ from .ir import QueryIR
28
+ from .validation import validate_sql
29
+
30
+
31
+ # ── Pure helper functions (moved verbatim from nl2sql_legacy) ───────────────
32
+
33
+ def join_sql_kind(join: Dict) -> str:
34
+ """Return the SQL JOIN keyword for a physical join record.
35
+
36
+ A structured ``join_spec`` from the relations module wins; an
37
+ unqualified join or a missing spec falls back to INNER. Physical
38
+ join kinds never include EXISTS/NOT EXISTS — those are relational.
39
+ """
40
+ spec = join.get("join_spec")
41
+ if spec is not None:
42
+ kind = spec.kind
43
+ return "INNER" if kind == "JOIN" else kind
44
+ join_type = join.get("type", "JOIN")
45
+ if join_type in ("EXISTS", "NOT EXISTS"):
46
+ return "INNER"
47
+ return "INNER" if join_type in ("JOIN", "INNER") else join_type
48
+
49
+
50
+ def add_limit(sql: str, limit: int, dialect: str) -> str:
51
+ """Add LIMIT clause with dialect-specific syntax."""
52
+ if dialect == "oracle":
53
+ return f"{sql}\nFETCH FIRST {limit} ROWS ONLY"
54
+ if dialect == "tsql":
55
+ return sql.replace("SELECT", f"SELECT TOP {limit}", 1)
56
+ return f"{sql}\nLIMIT {limit}"
57
+
58
+
59
+ # ── Renderer ────────────────────────────────────────────────────────────────
60
+
61
+ def render_sql(ir: QueryIR) -> Tuple[Optional[str], str, float, GenerationEvidence]:
62
+ """Build SQL statement from the Query IR.
63
+
64
+ Fail-closed: any join entry with ``condition is None`` (an
65
+ unresolved/unknown relationship) immediately returns
66
+ ``(None, explanation, 0.0, GenerationEvidence())`` so the caller
67
+ observes ``success=False``. The relationship is never silently
68
+ dropped.
69
+
70
+ Returns the same 4-tuple the legacy ``_build_sql_enhanced`` returns:
71
+ ``(sql, explanation, confidence, evidence)``.
72
+ """
73
+ for join in ir.joins:
74
+ if join.get("condition") is None:
75
+ subject = join.get("subject") or ir.table
76
+ pair_desc = f"{subject} ⟷ {join['table']}"
77
+ explanation = (
78
+ "Unable to safely infer the relationship between the "
79
+ f"table(s) involved ({pair_desc}). Please provide the "
80
+ "join condition or schema relationship."
81
+ )
82
+ return None, explanation, 0.0, GenerationEvidence()
83
+
84
+ operation = ir.operation
85
+ table = ir.table
86
+ columns = ir.columns
87
+ conditions = ir.conditions
88
+ aggregations = ir.aggregations
89
+ group_by = ir.group_by
90
+ ordering = ir.ordering
91
+ limit = ir.limit
92
+ joins = ir.joins
93
+ distinct = ir.distinct
94
+ dialect = ir.dialect
95
+
96
+ explanation_parts: list[str] = []
97
+ evidence = ir.evidence
98
+ evidence.items.append(EvidenceItem("base", "Base confidence for generated SQL", 0.5))
99
+
100
+ if operation == "SELECT":
101
+ distinct_kw = "DISTINCT " if distinct else ""
102
+ if aggregations:
103
+ if group_by:
104
+ select_parts = group_by + aggregations
105
+ select_clause = ", ".join(select_parts)
106
+ else:
107
+ select_clause = ", ".join(aggregations)
108
+ explanation_parts.append(f"聚合: {', '.join(aggregations)}")
109
+ evidence.items.append(EvidenceItem(
110
+ EVIDENCE_AGGREGATION_PRESENT,
111
+ f"Explicit aggregation requested: {', '.join(aggregations)}",
112
+ 0.15, detail=aggregations[0],
113
+ ))
114
+ else:
115
+ select_clause = ", ".join(columns)
116
+ if columns != ["*"]:
117
+ explanation_parts.append(f"列: {', '.join(columns)}")
118
+ evidence.items.append(EvidenceItem(
119
+ EVIDENCE_COLUMNS_EXPLICIT,
120
+ "Explicit columns requested", 0.1,
121
+ detail=columns[0],
122
+ ))
123
+ sql = f"SELECT {distinct_kw}{select_clause}\nFROM {table}"
124
+ explanation_parts.append(f"表: {table}")
125
+ if table != "table_name":
126
+ evidence.items.append(EvidenceItem(
127
+ EVIDENCE_TABLE_KNOWN,
128
+ f"Table resolved to known name: {table}", 0.2,
129
+ detail=table,
130
+ ))
131
+ for join in joins:
132
+ # Join conditions are only ever emitted from a resolved
133
+ # canonical relationship (G1). Unresolved pairs are blocked
134
+ # upstream by generate(); keep a structural guard so a join
135
+ # can never be composed with a missing condition.
136
+ if join.get("condition") is None:
137
+ continue
138
+ if join.get("relational"):
139
+ # EXISTS/NOT EXISTS: preserves row count, no duplicates
140
+ if join["type"] == "NOT EXISTS":
141
+ subquery = f"SELECT 1\n FROM {join['table']}\n WHERE {join['condition']}"
142
+ # Merge date conditions into subquery if present
143
+ if conditions:
144
+ cond_sql = conditions if isinstance(conditions, str) else " AND ".join(conditions)
145
+ subquery += f"\n AND {cond_sql}"
146
+ conditions = [] # Remove from top-level conditions
147
+ sql += f"\nWHERE NOT EXISTS (\n {subquery}\n)"
148
+ explanation_parts.append(f"关联: {join['table']} (NOT EXISTS)")
149
+ else:
150
+ subquery = f"SELECT 1\n FROM {join['table']}\n WHERE {join['condition']}"
151
+ # Merge date conditions into subquery if present
152
+ if conditions:
153
+ cond_sql = conditions if isinstance(conditions, str) else " AND ".join(conditions)
154
+ subquery += f"\n AND {cond_sql}"
155
+ conditions = [] # Remove from top-level conditions
156
+ sql += f"\nWHERE EXISTS (\n {subquery}\n)"
157
+ explanation_parts.append(f"关联: {join['table']} (EXISTS)")
158
+ evidence.items.append(EvidenceItem(
159
+ EVIDENCE_JOIN_KNOWN,
160
+ f"Known relationship: {join['subject']} → {join['table']}",
161
+ 0.1, detail=join["condition"],
162
+ ))
163
+ else:
164
+ # Physical JOIN — kind comes from the structured JoinSpec
165
+ # (LEFT/RIGHT/INNER/OUTER), never a first-token guess.
166
+ sql += f"\n{join_sql_kind(join)} JOIN {join['table']} ON {join['condition']}"
167
+ explanation_parts.append(f"关联: {join['table']}")
168
+ evidence.items.append(EvidenceItem(
169
+ EVIDENCE_JOIN_KNOWN,
170
+ f"Physical join: {join['table']} ON {join['condition']}",
171
+ 0.1, detail=join["condition"],
172
+ ))
173
+ if conditions:
174
+ condition_sql = conditions if isinstance(conditions, str) else " AND ".join(conditions)
175
+ sql += f"\nWHERE {condition_sql}"
176
+ count = len(conditions) if not isinstance(conditions, str) else 1
177
+ explanation_parts.append(f"条件: {count}个")
178
+ evidence.items.append(EvidenceItem(
179
+ EVIDENCE_CONDITIONS_PRESENT,
180
+ f"{count} explicit predicate(s) in WHERE clause",
181
+ 0.1,
182
+ ))
183
+ if group_by:
184
+ sql += f"\nGROUP BY {', '.join(group_by)}"
185
+ explanation_parts.append(f"分组: {', '.join(group_by)}")
186
+ evidence.items.append(EvidenceItem(
187
+ EVIDENCE_GROUP_BY,
188
+ f"GROUP BY clause: {', '.join(group_by)}", 0.1,
189
+ ))
190
+ elif aggregations and columns != ["*"]:
191
+ group_cols = [c for c in columns if c != "*"]
192
+ if group_cols:
193
+ sql += f"\nGROUP BY {', '.join(group_cols)}"
194
+ if ordering:
195
+ sql += f"\nORDER BY {ordering[0]} {ordering[1]}"
196
+ explanation_parts.append(f"排序: {ordering[0]} {ordering[1]}")
197
+ if limit:
198
+ sql = add_limit(sql, limit, dialect)
199
+ explanation_parts.append(f"限制: {limit}条")
200
+ evidence.items.append(EvidenceItem(
201
+ EVIDENCE_LIMIT,
202
+ f"Explicit limit: {limit}", 0.1, detail=str(limit),
203
+ ))
204
+ elif operation == "INSERT":
205
+ cols = columns if columns != ["*"] else ["col1", "col2"]
206
+ placeholders = ", ".join(["?"] * len(cols))
207
+ sql = f"INSERT INTO {table} ({', '.join(cols)})\nVALUES ({placeholders})"
208
+ explanation_parts.append(f"插入到: {table}")
209
+ elif operation == "UPDATE":
210
+ update_cols = columns if columns != ["*"] else ["column"]
211
+ set_clause = ", ".join([f"{c} = ?" for c in update_cols])
212
+ sql = f"UPDATE {table}\nSET {set_clause}"
213
+ if conditions:
214
+ condition_sql = conditions if isinstance(conditions, str) else " AND ".join(conditions)
215
+ sql += f"\nWHERE {condition_sql}"
216
+ else:
217
+ sql += "\nWHERE id = ?"
218
+ explanation_parts.append(f"更新: {table}")
219
+ elif operation == "DELETE":
220
+ sql = f"DELETE FROM {table}"
221
+ if conditions:
222
+ condition_sql = conditions if isinstance(conditions, str) else " AND ".join(conditions)
223
+ sql += f"\nWHERE {condition_sql}"
224
+ else:
225
+ sql += "\nWHERE id = ?"
226
+ explanation_parts.append(f"删除: {table}")
227
+ else:
228
+ sql = f"-- 无法解析: {operation}"
229
+ # Unknown operation — clear all evidence; score will be 0.0.
230
+ evidence = GenerationEvidence()
231
+
232
+ sql = f"-- Generated for {dialect.upper()}\n{sql}"
233
+ sql = apply_dialect_adjustments(sql, dialect)
234
+ parse_adjustment = validate_sql(sql, dialect)
235
+ if parse_adjustment > 0:
236
+ evidence.items.append(EvidenceItem(
237
+ EVIDENCE_PARSE_OK,
238
+ "Target dialect parser accepted the generated SQL",
239
+ parse_adjustment,
240
+ ))
241
+ elif parse_adjustment < 0:
242
+ evidence.items.append(EvidenceItem(
243
+ EVIDENCE_PARSE_FAIL,
244
+ "Target dialect parser rejected the generated SQL",
245
+ parse_adjustment,
246
+ ))
247
+ confidence = evidence.score
248
+ return sql, " | ".join(explanation_parts), confidence, evidence
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """P8 NL2SQL pipeline: entity and relation resolution (moved from nl2sql_legacy).
3
+
4
+ Thin function-level adapters over the existing ``nl2sql_components``
5
+ modules. Signatures mirror the legacy instance-method forms so the
6
+ facade's ``super()`` overrides continue to resolve unchanged.
7
+ """
8
+ from typing import Dict, List, Optional
9
+
10
+ from ..nl2sql_components.mappings import COLUMN_PATTERNS, TABLE_PATTERNS
11
+ from ..nl2sql_components.relations import (
12
+ extract_joins as _extract_joins_impl,
13
+ guess_join_key as _guess_join_key_impl,
14
+ has_relational_keyword as _has_relational_keyword_impl,
15
+ )
16
+ from ..nl2sql_components.core_extract import (
17
+ extract_columns as _extract_columns_impl,
18
+ extract_table as _extract_table_impl,
19
+ extract_table_subject as _extract_table_subject_impl,
20
+ named_tables as _named_tables_impl,
21
+ )
22
+
23
+
24
+ # ── Entity resolution ────────────────────────────────────────────────────────
25
+
26
+ def extract_table(text: str, table_patterns: Optional[Dict[str, str]] = None) -> str:
27
+ """Extract table name from text (delegates to core_extract)."""
28
+ return _extract_table_impl(text, table_patterns or TABLE_PATTERNS)
29
+
30
+
31
+ def extract_table_subject(
32
+ text: str, table_patterns: Optional[Dict[str, str]] = None
33
+ ) -> Optional[str]:
34
+ """Extract the subject table (first appearing in text) for D2 relational queries."""
35
+ return _extract_table_subject_impl(text, table_patterns or TABLE_PATTERNS)
36
+
37
+
38
+ def named_tables(text: str, table_patterns: Optional[Dict[str, str]] = None) -> list:
39
+ """Return the distinct table names the text mentions."""
40
+ return _named_tables_impl(text, table_patterns or TABLE_PATTERNS)
41
+
42
+
43
+ def extract_columns(
44
+ text: str, column_patterns: Optional[Dict[str, str]] = None
45
+ ) -> list:
46
+ """Extract column names from text (delegates to core_extract)."""
47
+ return _extract_columns_impl(text, column_patterns or COLUMN_PATTERNS)
48
+
49
+
50
+ # ── Relation resolution ───────────────────────────────────────────────────────
51
+
52
+ def extract_joins(text: str, table_patterns: Optional[Dict[str, str]] = None) -> list:
53
+ """Extract JOIN information from text.
54
+
55
+ Detects relational existence patterns (e.g. "users who have orders")
56
+ and returns structured join contexts for EXISTS-based SQL generation.
57
+ """
58
+ return _extract_joins_impl(text, table_patterns or TABLE_PATTERNS)
59
+
60
+
61
+ def guess_join_key(table1: str, table2: str) -> Optional[str]:
62
+ """Guess the join key between two tables. Returns None for unknown
63
+ relationships instead of guessing (fail-closed)."""
64
+ return _guess_join_key_impl(table1, table2)
65
+
66
+
67
+ def has_relational_keyword(text: str) -> bool:
68
+ """Check if text contains relational existence keywords (D2)."""
69
+ return _has_relational_keyword_impl(text)
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """P8 NL2SQL pipeline: suggestion generation (moved from nl2sql_legacy).
3
+
4
+ ``generate_suggestions`` is a verbatim move of
5
+ ``nl2sql_legacy._generate_suggestions``.
6
+ """
7
+
8
+
9
+ def generate_suggestions(text: str, sql: str, dialect: str) -> list:
10
+ """Generate improvement suggestions."""
11
+ suggestions = []
12
+ if "table_name" in sql:
13
+ suggestions.append("💡 请指定具体的表名,如:用户表、订单表")
14
+ if "column >" in sql or "column <" in sql or "column =" in sql:
15
+ suggestions.append("💡 请指定具体的列名用于条件判断,如:年龄大于30")
16
+ if dialect == "hive" and "SELECT *" in sql:
17
+ suggestions.append("💡 Hive 建议指定具体列名以提高性能")
18
+ if "JOIN" not in sql and any(k in text for k in ["关联", "连接", "join", "和", "与"]):
19
+ suggestions.append("💡 检测到关联需求,请明确指定两个表名")
20
+ if "?" in sql:
21
+ suggestions.append("💡 请替换 ? 占位符为实际值")
22
+ if "GROUP BY" in sql and "HAVING" not in sql:
23
+ suggestions.append("💡 可以添加 HAVING 子句过滤分组结果")
24
+ if dialect == "hive" and "ORDER BY" in sql and "LIMIT" not in sql:
25
+ suggestions.append("💡 Hive 中 ORDER BY 建议配合 LIMIT 使用")
26
+ if "DELETE" in sql and "WHERE" not in sql:
27
+ suggestions.append("⚠️ DELETE 没有 WHERE 条件将删除所有数据!")
28
+ if "UPDATE" in sql and "WHERE" not in sql:
29
+ suggestions.append("⚠️ UPDATE 没有 WHERE 条件将更新所有数据!")
30
+ return suggestions