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.
- backend/__init__.py +1 -0
- backend/api/__init__.py +1 -0
- backend/api/main.py +732 -0
- backend/api/middleware.py +274 -0
- backend/api/rate_limit_store.py +105 -0
- backend/api/readiness.py +178 -0
- backend/core/__init__.py +57 -0
- backend/core/cache.py +261 -0
- backend/core/column_hint_validation.py +22 -0
- backend/core/config.py +546 -0
- backend/core/constants.py +84 -0
- backend/core/exceptions.py +124 -0
- backend/core/function_call_scanner.py +64 -0
- backend/core/function_semantics.py +148 -0
- backend/core/functions_db.json +601 -0
- backend/core/functions_lookup.py +332 -0
- backend/core/metadata.py +71 -0
- backend/core/nl2sql.py +85 -0
- backend/core/nl2sql_components/__init__.py +72 -0
- backend/core/nl2sql_components/aggregations.py +35 -0
- backend/core/nl2sql_components/boolean_conditions.py +279 -0
- backend/core/nl2sql_components/core_extract.py +106 -0
- backend/core/nl2sql_components/evidence.py +122 -0
- backend/core/nl2sql_components/mappings.py +185 -0
- backend/core/nl2sql_components/relations.py +180 -0
- backend/core/nl2sql_components/templates.py +130 -0
- backend/core/nl2sql_components/tokenizer.py +187 -0
- backend/core/nl2sql_legacy.py +22 -0
- backend/core/nl2sql_pipeline/__init__.py +13 -0
- backend/core/nl2sql_pipeline/aggregations.py +26 -0
- backend/core/nl2sql_pipeline/clause_extract.py +97 -0
- backend/core/nl2sql_pipeline/dialect.py +139 -0
- backend/core/nl2sql_pipeline/intent.py +85 -0
- backend/core/nl2sql_pipeline/ir.py +70 -0
- backend/core/nl2sql_pipeline/models.py +30 -0
- backend/core/nl2sql_pipeline/orchestrator.py +576 -0
- backend/core/nl2sql_pipeline/predicates.py +182 -0
- backend/core/nl2sql_pipeline/renderer.py +248 -0
- backend/core/nl2sql_pipeline/resolution.py +69 -0
- backend/core/nl2sql_pipeline/suggestions.py +30 -0
- backend/core/nl2sql_pipeline/templates.py +371 -0
- backend/core/nl2sql_pipeline/validation.py +28 -0
- backend/core/p1_sql_scanner.py +196 -0
- backend/core/p2_data_validation.py +72 -0
- backend/core/parser.py +280 -0
- backend/core/post_processor.py +129 -0
- backend/core/rules.py +808 -0
- backend/core/semantic_diff.py +385 -0
- backend/core/transpiler.py +791 -0
- backend/core/type_mapping.json +593 -0
- backend/core/type_mapping.py +765 -0
- backend/utils/__init__.py +2 -0
- backend/utils/validators.py +407 -0
- frontend/__init__.py +10 -0
- frontend/app_context_v2.py +201 -0
- frontend/core/__init__.py +33 -0
- frontend/core/design_tokens.py +127 -0
- frontend/core/escaping.py +13 -0
- frontend/core/navigation.py +135 -0
- frontend/core/state.py +612 -0
- frontend/core/styles.py +523 -0
- frontend/core/themes.py +69 -0
- frontend/core/viewmodels.py +499 -0
- frontend/pages/__init__.py +0 -0
- frontend/pages/convert.py +283 -0
- frontend/pages/diff.py +157 -0
- frontend/pages/functions.py +100 -0
- frontend/pages/history.py +144 -0
- frontend/pages/lineage.py +168 -0
- frontend/pages/nl2sql.py +305 -0
- frontend/pages/query_analysis.py +120 -0
- frontend/pages/runtime.py +66 -0
- frontend/pages/settings.py +60 -0
- frontend/pages/templates.py +67 -0
- frontend/pages/types.py +110 -0
- frontend/templates_v2.py +53 -0
- frontend/ui/__init__.py +0 -0
- frontend/ui/command_palette.py +153 -0
- frontend/ui/diff.py +275 -0
- frontend/ui/editor.py +102 -0
- frontend/ui/status.py +255 -0
- sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
- sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
- sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
- sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
- sql_dialect_master-1.1.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Boolean condition extraction for NL2SQL.
|
|
2
|
+
|
|
3
|
+
Extract explicit comparison, range, text, null, status, and set-membership
|
|
4
|
+
predicates while preserving normal SQL AND-over-OR precedence and
|
|
5
|
+
user-supplied parentheses.
|
|
6
|
+
"""
|
|
7
|
+
import re
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_COMPARISON_PATTERNS = (
|
|
12
|
+
re.compile(
|
|
13
|
+
r"(?<![A-Za-z0-9_])(price|quantity|amount|age|score|rating|views|clicks)\s+"
|
|
14
|
+
r"(greater(?:\s+than)?|more\s+than|above|over|less(?:\s+than)?|"
|
|
15
|
+
r"below|under|equal(?:\s+to)?|equals)\s+"
|
|
16
|
+
r"([0-9]+(?:\.[0-9]+)?)",
|
|
17
|
+
re.IGNORECASE,
|
|
18
|
+
),
|
|
19
|
+
re.compile(
|
|
20
|
+
r"(?<![A-Za-z0-9_])(price|quantity|amount|age|score|rating|views|clicks)\s*"
|
|
21
|
+
r"(>=|<=|!=|=|>|<)\s*([0-9]+(?:\.[0-9]+)?)",
|
|
22
|
+
re.IGNORECASE,
|
|
23
|
+
),
|
|
24
|
+
re.compile(
|
|
25
|
+
r"(价格|数量|金额|年龄|得分|评分|浏览量|点击量)\s*"
|
|
26
|
+
r"(大于等于|小于等于|不等于|大于|小于|超过|低于|等于)\s*"
|
|
27
|
+
r"([0-9]+(?:\.[0-9]+)?)"
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_RANGE_PATTERN = re.compile(
|
|
32
|
+
r"(?<![A-Za-z0-9_])(price|quantity|amount|age|score|rating|views|clicks)\s+"
|
|
33
|
+
r"(?:between\s+([0-9]+(?:\.[0-9]+)?)\s+and\s+([0-9]+(?:\.[0-9]+)?)|"
|
|
34
|
+
r"in\s+(?:the\s+)?range\s+([0-9]+(?:\.[0-9]+)?)\s*(?:to|-)\s*"
|
|
35
|
+
r"([0-9]+(?:\.[0-9]+)?))",
|
|
36
|
+
re.IGNORECASE,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_CN_RANGE_PATTERN = re.compile(
|
|
40
|
+
r"(价格|数量|金额|年龄|得分|评分|浏览量|点击量)\s*"
|
|
41
|
+
r"(?:在|介于)\s*([0-9]+(?:\.[0-9]+)?)\s*(?:和|到|至|-)\s*"
|
|
42
|
+
r"([0-9]+(?:\.[0-9]+)?)\s*(?:之间|范围内|以内)?"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_TEXT_PATTERN = re.compile(
|
|
46
|
+
r"(?<![A-Za-z0-9_])(name|title|description|category|brand)\s+"
|
|
47
|
+
r"(?:contains|containing|includes|including)\s+"
|
|
48
|
+
r"(?:the\s+)?[\"']?([^\"']+?)[\"']?(?=\s+(?:and|or)\s+|$)",
|
|
49
|
+
re.IGNORECASE,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_CN_TEXT_PATTERN = re.compile(
|
|
53
|
+
r"(名称|名字|标题|描述|分类|品牌)\s*(?:包含|含有|包括)\s*[“\"]?([^”\"]+?)[”\"]?(?=(?:且|并且|或者|或|和)|$)"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_NULL_PATTERN = re.compile(
|
|
57
|
+
r"(?<![A-Za-z0-9_])(name|title|description|category|brand|price|quantity|amount|"
|
|
58
|
+
r"age|score|rating|views|clicks)\s+(is\s+(?:not\s+)?null)\b",
|
|
59
|
+
re.IGNORECASE,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_CN_NULL_PATTERN = re.compile(
|
|
63
|
+
r"(名称|名字|标题|描述|分类|品牌|价格|数量|金额|年龄|得分|评分|浏览量|点击量)\s*"
|
|
64
|
+
r"(为空|为\s*空|是空|不为空|不为\s*空|非空)"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
_SET_PREFIX_PATTERN = re.compile(
|
|
68
|
+
r"(?<![A-Za-z0-9_])(category|type|department|region|status)\s+"
|
|
69
|
+
r"(?:(not)\s+)?in\s*\(",
|
|
70
|
+
re.IGNORECASE,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
_CN_SET_PREFIX_PATTERN = re.compile(
|
|
74
|
+
r"(类别|类型|部门|地区|状态)\s*(不在|在)\s*"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
_STATUS_PATTERN = re.compile(
|
|
78
|
+
r"(?:\bstatus\s+|状态\s*(?:为|是|=)\s*)"
|
|
79
|
+
r"(active|enabled|valid|inactive|disabled|invalid|completed|done|pending|paid|unpaid|"
|
|
80
|
+
r"有效|无效|已完成|未完成|已支付|未支付)",
|
|
81
|
+
re.IGNORECASE,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
_STATUS_CONDITIONS = {
|
|
85
|
+
"active": "status = 'active'",
|
|
86
|
+
"enabled": "status = 'active'",
|
|
87
|
+
"valid": "status = 'active'",
|
|
88
|
+
"有效": "status = 'active'",
|
|
89
|
+
"inactive": "status = 'inactive'",
|
|
90
|
+
"disabled": "status = 'inactive'",
|
|
91
|
+
"invalid": "status = 'inactive'",
|
|
92
|
+
"无效": "status = 'inactive'",
|
|
93
|
+
"completed": "status = 'completed'",
|
|
94
|
+
"done": "status = 'completed'",
|
|
95
|
+
"已完成": "status = 'completed'",
|
|
96
|
+
"pending": "status = 'pending'",
|
|
97
|
+
"未完成": "status = 'pending'",
|
|
98
|
+
"paid": "status = 'paid'",
|
|
99
|
+
"已支付": "status = 'paid'",
|
|
100
|
+
"unpaid": "status = 'unpaid'",
|
|
101
|
+
"未支付": "status = 'unpaid'",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_CN_COLUMNS = {
|
|
105
|
+
"价格": "price",
|
|
106
|
+
"数量": "quantity",
|
|
107
|
+
"金额": "amount",
|
|
108
|
+
"年龄": "age",
|
|
109
|
+
"得分": "score",
|
|
110
|
+
"评分": "rating",
|
|
111
|
+
"浏览量": "views",
|
|
112
|
+
"点击量": "clicks",
|
|
113
|
+
"名称": "name",
|
|
114
|
+
"名字": "name",
|
|
115
|
+
"标题": "title",
|
|
116
|
+
"描述": "description",
|
|
117
|
+
"分类": "category",
|
|
118
|
+
"品牌": "brand",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_CN_SET_COLUMNS = {
|
|
122
|
+
"类别": "category",
|
|
123
|
+
"类型": "type",
|
|
124
|
+
"部门": "department",
|
|
125
|
+
"地区": "region",
|
|
126
|
+
"状态": "status",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_CN_OPERATORS = {
|
|
130
|
+
"大于等于": ">=",
|
|
131
|
+
"小于等于": "<=",
|
|
132
|
+
"不等于": "!=",
|
|
133
|
+
"大于": ">",
|
|
134
|
+
"小于": "<",
|
|
135
|
+
"超过": ">",
|
|
136
|
+
"低于": "<",
|
|
137
|
+
"等于": "=",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_CONNECTOR_PATTERN = re.compile(r"\b(?:and|or)\b|以及|并且|且|或者|或|和", re.IGNORECASE)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _normalize_operator(raw: str) -> str:
|
|
144
|
+
normalized = raw.lower()
|
|
145
|
+
if normalized.startswith(("greater", "more", "above", "over")):
|
|
146
|
+
return ">"
|
|
147
|
+
if normalized.startswith(("less", "below", "under")):
|
|
148
|
+
return "<"
|
|
149
|
+
if normalized.startswith(("equal", "equals")):
|
|
150
|
+
return "="
|
|
151
|
+
return _CN_OPERATORS.get(raw, normalized.upper())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _text_sql(column: str, value: str) -> str:
|
|
155
|
+
escaped = value.strip().replace("'", "''")
|
|
156
|
+
return f"{column} LIKE '%{escaped}%'"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _format_set_values(raw_values: str) -> str:
|
|
160
|
+
values = []
|
|
161
|
+
for raw in re.split(r"[,,]", raw_values):
|
|
162
|
+
value = raw.strip().strip("\"'")
|
|
163
|
+
if not value:
|
|
164
|
+
continue
|
|
165
|
+
values.append("'" + value.replace("'", "''") + "'")
|
|
166
|
+
return ", ".join(values)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _comparison_matches(text: str) -> List[Tuple[int, int, str]]:
|
|
170
|
+
matches: List[Tuple[int, int, str]] = []
|
|
171
|
+
|
|
172
|
+
for pattern in _COMPARISON_PATTERNS:
|
|
173
|
+
for match in pattern.finditer(text):
|
|
174
|
+
first, raw_operator, value = match.groups()
|
|
175
|
+
column = _CN_COLUMNS.get(first, first)
|
|
176
|
+
matches.append((match.start(), match.end(), f"{column} {_normalize_operator(raw_operator)} {value}"))
|
|
177
|
+
|
|
178
|
+
for pattern in (_RANGE_PATTERN, _CN_RANGE_PATTERN):
|
|
179
|
+
for match in pattern.finditer(text):
|
|
180
|
+
groups = match.groups()
|
|
181
|
+
if len(groups) == 5:
|
|
182
|
+
first, low1, high1, low2, high2 = groups
|
|
183
|
+
low, high = low1 or low2, high1 or high2
|
|
184
|
+
else:
|
|
185
|
+
first, low, high = groups
|
|
186
|
+
column = _CN_COLUMNS.get(first, first)
|
|
187
|
+
matches.append((match.start(), match.end(), f"{column} BETWEEN {low} AND {high}"))
|
|
188
|
+
|
|
189
|
+
for pattern in (_TEXT_PATTERN, _CN_TEXT_PATTERN):
|
|
190
|
+
for match in pattern.finditer(text):
|
|
191
|
+
first, value = match.groups()
|
|
192
|
+
column = _CN_COLUMNS.get(first, first)
|
|
193
|
+
matches.append((match.start(), match.end(), _text_sql(column, value)))
|
|
194
|
+
|
|
195
|
+
for pattern in (_NULL_PATTERN, _CN_NULL_PATTERN):
|
|
196
|
+
for match in pattern.finditer(text):
|
|
197
|
+
first, raw_predicate = match.groups()
|
|
198
|
+
column = _CN_COLUMNS.get(first, first)
|
|
199
|
+
normalized = raw_predicate.replace(" ", "").lower()
|
|
200
|
+
is_not_null = normalized.startswith(("isnotnull", "不为空", "非空", "不为"))
|
|
201
|
+
matches.append((match.start(), match.end(), f"{column} IS {'NOT ' if is_not_null else ''}NULL"))
|
|
202
|
+
|
|
203
|
+
for match in _SET_PREFIX_PATTERN.finditer(text):
|
|
204
|
+
close_paren = text.find(")", match.end())
|
|
205
|
+
if close_paren == -1:
|
|
206
|
+
continue
|
|
207
|
+
column, negation = match.groups()
|
|
208
|
+
raw_values = text[match.end() : close_paren]
|
|
209
|
+
values = _format_set_values(raw_values)
|
|
210
|
+
if values:
|
|
211
|
+
operator = "NOT IN" if negation else "IN"
|
|
212
|
+
matches.append((match.start(), close_paren + 1, f"{column.lower()} {operator} ({values})"))
|
|
213
|
+
|
|
214
|
+
for match in _CN_SET_PREFIX_PATTERN.finditer(text):
|
|
215
|
+
column, operator_text = match.groups()
|
|
216
|
+
value_end = len(text)
|
|
217
|
+
connector = _CONNECTOR_PATTERN.search(text, match.end())
|
|
218
|
+
if connector is not None:
|
|
219
|
+
value_end = connector.start()
|
|
220
|
+
raw_values = text[match.end() : value_end].strip()
|
|
221
|
+
for suffix in ("之中", "其中", "中"):
|
|
222
|
+
if raw_values.endswith(suffix):
|
|
223
|
+
raw_values = raw_values[: -len(suffix)].rstrip()
|
|
224
|
+
break
|
|
225
|
+
if not raw_values or any(char in raw_values for char in "()"):
|
|
226
|
+
continue
|
|
227
|
+
values = _format_set_values(raw_values)
|
|
228
|
+
if values:
|
|
229
|
+
operator = "NOT IN" if operator_text == "不在" else "IN"
|
|
230
|
+
matches.append((match.start(), value_end, f"{_CN_SET_COLUMNS[column]} {operator} ({values})"))
|
|
231
|
+
|
|
232
|
+
for match in _STATUS_PATTERN.finditer(text):
|
|
233
|
+
status = match.group(1).lower()
|
|
234
|
+
matches.append((match.start(), match.end(), _STATUS_CONDITIONS[status]))
|
|
235
|
+
|
|
236
|
+
matches.sort(key=lambda item: (item[0], item[1]))
|
|
237
|
+
deduped: List[Tuple[int, int, str]] = []
|
|
238
|
+
for item in matches:
|
|
239
|
+
if deduped and item[0] == deduped[-1][0] and item[1] <= deduped[-1][1]:
|
|
240
|
+
continue
|
|
241
|
+
deduped.append(item)
|
|
242
|
+
return deduped
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def extract_boolean_conditions(text: str) -> Optional[List[str]]:
|
|
246
|
+
"""Return one boolean SQL expression preserving connector precedence/grouping."""
|
|
247
|
+
matches = _comparison_matches(text)
|
|
248
|
+
if len(matches) < 2:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
tokens: List[str] = []
|
|
252
|
+
prefix = text[: matches[0][0]]
|
|
253
|
+
tokens.extend(char for char in prefix if char in "()")
|
|
254
|
+
|
|
255
|
+
for index, current in enumerate(matches):
|
|
256
|
+
tokens.append(f"({current[2]})")
|
|
257
|
+
if index == len(matches) - 1:
|
|
258
|
+
suffix = text[current[1] :]
|
|
259
|
+
tokens.extend(char for char in suffix if char in "()")
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
separator = text[current[1] : matches[index + 1][0]]
|
|
263
|
+
events = []
|
|
264
|
+
for paren in re.finditer(r"[()]", separator):
|
|
265
|
+
events.append((paren.start(), paren.group(0)))
|
|
266
|
+
connector_matches = list(_CONNECTOR_PATTERN.finditer(separator))
|
|
267
|
+
if len(connector_matches) != 1:
|
|
268
|
+
return None
|
|
269
|
+
connector = connector_matches[0]
|
|
270
|
+
events.append((connector.start(), connector.group(0)))
|
|
271
|
+
events.sort(key=lambda item: item[0])
|
|
272
|
+
for _, event in events:
|
|
273
|
+
if event == "(" or event == ")":
|
|
274
|
+
tokens.append(event)
|
|
275
|
+
elif event:
|
|
276
|
+
normalized = event.lower()
|
|
277
|
+
tokens.append("OR" if normalized in {"or", "或者", "或"} else "AND")
|
|
278
|
+
|
|
279
|
+
return [" ".join(tokens)]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Core NL2SQL extraction primitives: operation, table, column, limit, distinct.
|
|
3
|
+
|
|
4
|
+
These are stateless, dependency-free functions that operate on raw text
|
|
5
|
+
and the shared mapping dicts (KEYWORDS, TABLE_PATTERNS, COLUMN_PATTERNS
|
|
6
|
+
from ``backend.core.nl2sql_components.mappings``).
|
|
7
|
+
"""
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def detect_operation(text: str, keywords: Dict[str, str]) -> str:
|
|
13
|
+
"""Detect SQL operation type from text.
|
|
14
|
+
|
|
15
|
+
Scans the keyword mapping for operation keywords present in the text
|
|
16
|
+
and returns the first matching operation (SELECT/INSERT/UPDATE/DELETE).
|
|
17
|
+
Defaults to SELECT.
|
|
18
|
+
"""
|
|
19
|
+
for keyword, op in keywords.items():
|
|
20
|
+
if keyword in text and op in ["SELECT", "INSERT", "UPDATE", "DELETE"]:
|
|
21
|
+
return op
|
|
22
|
+
return "SELECT"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_table(text: str, table_patterns: Dict[str, str]) -> str:
|
|
26
|
+
"""Extract a single table name from text.
|
|
27
|
+
|
|
28
|
+
Prefers the longest matching pattern; ties broken by first position.
|
|
29
|
+
Returns the fallback name "table_name" when no pattern matches.
|
|
30
|
+
"""
|
|
31
|
+
tables_found = []
|
|
32
|
+
for pattern, table in table_patterns.items():
|
|
33
|
+
if pattern in text:
|
|
34
|
+
tables_found.append((text.find(pattern), table, len(pattern)))
|
|
35
|
+
if tables_found:
|
|
36
|
+
tables_found.sort(key=lambda item: (-item[2], item[0]))
|
|
37
|
+
return tables_found[0][1]
|
|
38
|
+
return "table_name"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_table_subject(text: str, table_patterns: Dict[str, str]) -> Optional[str]:
|
|
42
|
+
"""Extract the subject table (first appearing in text) for D2 relational queries.
|
|
43
|
+
|
|
44
|
+
For queries like 'users who have orders', returns 'users' (the subject)
|
|
45
|
+
rather than 'orders' (the relation). Uses first occurrence position.
|
|
46
|
+
"""
|
|
47
|
+
candidates = []
|
|
48
|
+
for pattern, table in table_patterns.items():
|
|
49
|
+
if table not in candidates:
|
|
50
|
+
pos = text.find(pattern)
|
|
51
|
+
if pos != -1:
|
|
52
|
+
candidates.append((pos, table))
|
|
53
|
+
if candidates:
|
|
54
|
+
candidates.sort(key=lambda x: x[0])
|
|
55
|
+
return candidates[0][1]
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def named_tables(text: str, table_patterns: Dict[str, str]) -> List[str]:
|
|
60
|
+
"""Return the distinct table names the text mentions, in order of first appearance."""
|
|
61
|
+
occurrences = []
|
|
62
|
+
seen = set()
|
|
63
|
+
for pattern, table in table_patterns.items():
|
|
64
|
+
if table in seen:
|
|
65
|
+
continue
|
|
66
|
+
pos = text.find(pattern)
|
|
67
|
+
if pos != -1:
|
|
68
|
+
occurrences.append((pos, table))
|
|
69
|
+
seen.add(table)
|
|
70
|
+
return [t for _, t in sorted(occurrences)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_columns(text: str, column_patterns: Dict[str, str]) -> List[str]:
|
|
74
|
+
"""Extract column names from text.
|
|
75
|
+
|
|
76
|
+
Returns a list of distinct column names, or ["*"] when no column
|
|
77
|
+
keyword is present.
|
|
78
|
+
"""
|
|
79
|
+
columns = []
|
|
80
|
+
for pattern, column in column_patterns.items():
|
|
81
|
+
if pattern in text and column not in columns:
|
|
82
|
+
columns.append(column)
|
|
83
|
+
return columns if columns else ["*"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def extract_limit(text: str) -> Optional[int]:
|
|
87
|
+
"""Extract LIMIT value from text.
|
|
88
|
+
|
|
89
|
+
Supports Chinese and English cardinality phrasings such as
|
|
90
|
+
'前10', 'top 10', 'limit 10', '10条', '10行'.
|
|
91
|
+
"""
|
|
92
|
+
patterns = [
|
|
93
|
+
r"前\s*(\d+)", r"top\s*(\d+)", r"limit\s*(\d+)",
|
|
94
|
+
r"first\s*(\d+)", r"(\d+)\s*条", r"(\d+)\s*个",
|
|
95
|
+
r"(\d+)\s*行", r"(\d+)\s*rows?", r"only\s*(\d+)",
|
|
96
|
+
]
|
|
97
|
+
for pattern in patterns:
|
|
98
|
+
match = re.search(pattern, text, re.IGNORECASE)
|
|
99
|
+
if match:
|
|
100
|
+
return int(match.group(1))
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def check_distinct(text: str) -> bool:
|
|
105
|
+
"""Check if DISTINCT is requested in the text."""
|
|
106
|
+
return any(k in text for k in ["去重", "唯一", "不重复", "distinct", "unique"])
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Evidence-based confidence model for NL2SQL generation.
|
|
3
|
+
|
|
4
|
+
Replaces opaque ``confidence += 0.1`` heuristic increments with a
|
|
5
|
+
structured list of named evidence items. Each item records:
|
|
6
|
+
|
|
7
|
+
evidence_id - stable identifier for the evidence source
|
|
8
|
+
label - human-readable description
|
|
9
|
+
weight - contribution to the confidence score (signed)
|
|
10
|
+
|
|
11
|
+
The confidence score is the sum of all item weights, clamped to [0, 1],
|
|
12
|
+
exactly matching the previous heuristic's numeric output for the
|
|
13
|
+
same inputs. Consumers can inspect the items list to understand
|
|
14
|
+
*why* a result was given its score.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import List, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class EvidenceItem:
|
|
24
|
+
"""A single evidence item contributing to the confidence score.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
evidence_id: Stable identifier, e.g. "table_known" or "parse_ok".
|
|
28
|
+
label: Human-readable description shown to the user.
|
|
29
|
+
weight: Signed contribution to the confidence score.
|
|
30
|
+
detail: Optional machine-readable context (table name, parse error, etc.).
|
|
31
|
+
"""
|
|
32
|
+
evidence_id: str
|
|
33
|
+
label: str
|
|
34
|
+
weight: float
|
|
35
|
+
detail: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class GenerationEvidence:
|
|
40
|
+
"""Structured evidence backing a generated SQL confidence score.
|
|
41
|
+
|
|
42
|
+
Holds the full list of EvidenceItems and a computed score.
|
|
43
|
+
The score is the sum of all weights, clamped to [0.0, 1.0],
|
|
44
|
+
matching the previous heuristic confidence exactly.
|
|
45
|
+
|
|
46
|
+
Use ``GenerationEvidence.score`` as the ``confidence`` value and
|
|
47
|
+
pass ``GenerationEvidence.items`` as the evidence trail.
|
|
48
|
+
"""
|
|
49
|
+
items: List[EvidenceItem] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def score(self) -> float:
|
|
53
|
+
"""Clamped sum of all item weights (matches legacy heuristic range)."""
|
|
54
|
+
return max(0.0, min(1.0, sum(i.weight for i in self.items)))
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict:
|
|
57
|
+
return {
|
|
58
|
+
"score": self.score,
|
|
59
|
+
"items": [
|
|
60
|
+
{
|
|
61
|
+
"evidence_id": i.evidence_id,
|
|
62
|
+
"label": i.label,
|
|
63
|
+
"weight": i.weight,
|
|
64
|
+
"detail": i.detail,
|
|
65
|
+
}
|
|
66
|
+
for i in self.items
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Evidence ID constants ──────────────────────────────────────────────
|
|
72
|
+
# Use these instead of raw string literals so the IDs are stable
|
|
73
|
+
# across refactors and can be grepped.
|
|
74
|
+
|
|
75
|
+
# Table / schema evidence
|
|
76
|
+
EVIDENCE_TABLE_KNOWN = "table_known"
|
|
77
|
+
EVIDENCE_TABLE_UNKNOWN = "table_unknown"
|
|
78
|
+
|
|
79
|
+
# Column / aggregate evidence
|
|
80
|
+
EVIDENCE_COLUMNS_EXPLICIT = "columns_explicit"
|
|
81
|
+
EVIDENCE_COLUMNS_WILDCARD = "columns_wildcard"
|
|
82
|
+
|
|
83
|
+
# Predicate / condition evidence
|
|
84
|
+
EVIDENCE_CONDITIONS_PRESENT = "conditions_present"
|
|
85
|
+
|
|
86
|
+
# Relationship / join evidence
|
|
87
|
+
EVIDENCE_JOIN_KNOWN = "join_known"
|
|
88
|
+
EVIDENCE_JOIN_UNKNOWN = "join_unknown"
|
|
89
|
+
|
|
90
|
+
# Aggregation evidence
|
|
91
|
+
EVIDENCE_AGGREGATION_PRESENT = "aggregation_present"
|
|
92
|
+
|
|
93
|
+
# Ordering / limit / group-by evidence
|
|
94
|
+
EVIDENCE_GROUP_BY = "group_by"
|
|
95
|
+
EVIDENCE_ORDERING = "ordering"
|
|
96
|
+
EVIDENCE_LIMIT = "limit"
|
|
97
|
+
|
|
98
|
+
# Generated SQL structural evidence
|
|
99
|
+
EVIDENCE_PARSE_OK = "target_parse_ok"
|
|
100
|
+
EVIDENCE_PARSE_FAIL = "target_parse_fail"
|
|
101
|
+
|
|
102
|
+
# Template evidence
|
|
103
|
+
EVIDENCE_TEMPLATE_MATCHED = "template_matched"
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"EvidenceItem",
|
|
107
|
+
"GenerationEvidence",
|
|
108
|
+
"EVIDENCE_TABLE_KNOWN",
|
|
109
|
+
"EVIDENCE_TABLE_UNKNOWN",
|
|
110
|
+
"EVIDENCE_COLUMNS_EXPLICIT",
|
|
111
|
+
"EVIDENCE_COLUMNS_WILDCARD",
|
|
112
|
+
"EVIDENCE_CONDITIONS_PRESENT",
|
|
113
|
+
"EVIDENCE_JOIN_KNOWN",
|
|
114
|
+
"EVIDENCE_JOIN_UNKNOWN",
|
|
115
|
+
"EVIDENCE_AGGREGATION_PRESENT",
|
|
116
|
+
"EVIDENCE_GROUP_BY",
|
|
117
|
+
"EVIDENCE_ORDERING",
|
|
118
|
+
"EVIDENCE_LIMIT",
|
|
119
|
+
"EVIDENCE_PARSE_OK",
|
|
120
|
+
"EVIDENCE_PARSE_FAIL",
|
|
121
|
+
"EVIDENCE_TEMPLATE_MATCHED",
|
|
122
|
+
]
|