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,332 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Function Encyclopedia - Search and compare SQL functions across dialects.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive SQL function lookup with:
|
|
5
|
+
- 50+ SQL functions across 11 categories
|
|
6
|
+
- Cross-database syntax comparison for 12 dialects
|
|
7
|
+
- Fuzzy search with relevance scoring
|
|
8
|
+
- Category-based filtering
|
|
9
|
+
- Function examples and parameter documentation
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, List, Dict, Any
|
|
15
|
+
from difflib import SequenceMatcher
|
|
16
|
+
|
|
17
|
+
from .config import (
|
|
18
|
+
SUPPORTED_DIALECTS,
|
|
19
|
+
FUNCTION_CATEGORIES,
|
|
20
|
+
CATEGORY_DESCRIPTIONS,
|
|
21
|
+
FunctionInfo,
|
|
22
|
+
settings
|
|
23
|
+
)
|
|
24
|
+
from .exceptions import ConfigurationError
|
|
25
|
+
from .function_semantics import FunctionSemanticRegistry
|
|
26
|
+
from .p2_data_validation import _validate_function_entry
|
|
27
|
+
|
|
28
|
+
# Configure module logger
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FunctionEncyclopedia:
|
|
33
|
+
"""SQL Function Encyclopedia with multi-dialect comparison.
|
|
34
|
+
|
|
35
|
+
Features:
|
|
36
|
+
- 50+ SQL functions
|
|
37
|
+
- 11 categories: string, date, math, aggregate, window, conditional, conversion, json, array, system, geo
|
|
38
|
+
- 12 database dialects support
|
|
39
|
+
- Fuzzy search with relevance scoring
|
|
40
|
+
- Function examples and parameter documentation
|
|
41
|
+
- Strict semantic equivalence classification for reviewed function pairs
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
CATEGORIES = FUNCTION_CATEGORIES
|
|
45
|
+
CATEGORY_DESCRIPTIONS = CATEGORY_DESCRIPTIONS
|
|
46
|
+
|
|
47
|
+
def __init__(self, data_path: Optional[Path] = None, semantic_registry: Optional[FunctionSemanticRegistry] = None):
|
|
48
|
+
"""Initialize encyclopedia with function data.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
data_path: Path to functions_db.json. Uses default if None.
|
|
52
|
+
semantic_registry: Optional reviewed semantic registry. Defaults to the shared registry.
|
|
53
|
+
"""
|
|
54
|
+
if data_path is None:
|
|
55
|
+
data_path = Path(__file__).parent / "functions_db.json"
|
|
56
|
+
self.data_path = data_path
|
|
57
|
+
self.semantic_registry = semantic_registry or FunctionSemanticRegistry()
|
|
58
|
+
self.functions: List[FunctionInfo] = self._load_data()
|
|
59
|
+
self._build_index()
|
|
60
|
+
|
|
61
|
+
def _load_data(self) -> List[FunctionInfo]:
|
|
62
|
+
"""Load function data from JSON file.
|
|
63
|
+
|
|
64
|
+
Missing or malformed packaged data is a configuration error and must
|
|
65
|
+
not be silently converted into an empty encyclopedia.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
with open(self.data_path, 'r', encoding='utf-8') as f:
|
|
69
|
+
data = json.load(f)
|
|
70
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
71
|
+
raise ConfigurationError(
|
|
72
|
+
f"Unable to load function data from {self.data_path}: {exc}",
|
|
73
|
+
details={"data_path": str(self.data_path)},
|
|
74
|
+
) from exc
|
|
75
|
+
|
|
76
|
+
if not isinstance(data, dict):
|
|
77
|
+
raise ConfigurationError(
|
|
78
|
+
f"Invalid function data in {self.data_path}: top-level JSON value must be an object",
|
|
79
|
+
details={"data_path": str(self.data_path)},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
functions = data.get("functions", [])
|
|
83
|
+
if not isinstance(functions, list):
|
|
84
|
+
raise ConfigurationError(
|
|
85
|
+
f"Invalid function data in {self.data_path}: 'functions' must be a list",
|
|
86
|
+
details={"data_path": str(self.data_path)},
|
|
87
|
+
)
|
|
88
|
+
for index, entry in enumerate(functions):
|
|
89
|
+
_validate_function_entry(self.data_path, index, entry)
|
|
90
|
+
return functions
|
|
91
|
+
|
|
92
|
+
def _build_index(self) -> None:
|
|
93
|
+
"""Build search index for fast lookup."""
|
|
94
|
+
self.name_index: Dict[str, FunctionInfo] = {}
|
|
95
|
+
self.category_index: Dict[str, List[FunctionInfo]] = {cat: [] for cat in self.CATEGORIES}
|
|
96
|
+
|
|
97
|
+
for func in self.functions:
|
|
98
|
+
name = func.get("name", "").upper()
|
|
99
|
+
self.name_index[name] = func
|
|
100
|
+
|
|
101
|
+
category = func.get("category", "").lower()
|
|
102
|
+
if category in self.category_index:
|
|
103
|
+
self.category_index[category].append(func)
|
|
104
|
+
|
|
105
|
+
def search(self, query: str, limit: int = None) -> List[FunctionInfo]:
|
|
106
|
+
"""Search functions with fuzzy matching.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
query: Search query
|
|
110
|
+
limit: Maximum results to return
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of matching functions sorted by relevance
|
|
114
|
+
"""
|
|
115
|
+
limit = limit or settings.function_search_limit
|
|
116
|
+
query = query.upper().strip()
|
|
117
|
+
|
|
118
|
+
if not query:
|
|
119
|
+
return self.functions[:limit]
|
|
120
|
+
|
|
121
|
+
results: List[tuple] = []
|
|
122
|
+
|
|
123
|
+
for func in self.functions:
|
|
124
|
+
name = func.get("name", "").upper()
|
|
125
|
+
description = func.get("description", "").upper()
|
|
126
|
+
|
|
127
|
+
if query == name:
|
|
128
|
+
results.append((func, 1.0))
|
|
129
|
+
elif name.startswith(query):
|
|
130
|
+
results.append((func, 0.9))
|
|
131
|
+
elif query in name:
|
|
132
|
+
results.append((func, 0.7))
|
|
133
|
+
elif query in description:
|
|
134
|
+
results.append((func, 0.5))
|
|
135
|
+
else:
|
|
136
|
+
ratio = SequenceMatcher(None, query, name).ratio()
|
|
137
|
+
if ratio > settings.function_fuzzy_threshold:
|
|
138
|
+
results.append((func, ratio))
|
|
139
|
+
|
|
140
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
141
|
+
return [r[0] for r in results[:limit]]
|
|
142
|
+
|
|
143
|
+
def get_function(self, name: str) -> Optional[FunctionInfo]:
|
|
144
|
+
"""Get detailed function information by exact name."""
|
|
145
|
+
return self.name_index.get(name.upper())
|
|
146
|
+
|
|
147
|
+
def list_by_category(self, category: str) -> List[FunctionInfo]:
|
|
148
|
+
"""List all functions in a category."""
|
|
149
|
+
category = category.lower()
|
|
150
|
+
return self.category_index.get(category, [])
|
|
151
|
+
|
|
152
|
+
def get_dialect_syntax(self, func_name: str, dialect: str) -> Optional[str]:
|
|
153
|
+
"""Get function syntax for specific dialect."""
|
|
154
|
+
func = self.get_function(func_name)
|
|
155
|
+
if func:
|
|
156
|
+
return func.get("dialects", {}).get(dialect.lower())
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def resolve_function_semantics(self, func_name: str, source_dialect: str, target_dialect: str) -> Dict[str, Any]:
|
|
160
|
+
"""Resolve reviewed semantic equivalence without inferring from syntax presence."""
|
|
161
|
+
return self.semantic_registry.resolve(func_name, source_dialect, target_dialect)
|
|
162
|
+
|
|
163
|
+
def compare_dialects(self, func_name: str, dialects: List[str] = None) -> Dict[str, Any]:
|
|
164
|
+
"""Compare function syntax across multiple dialects."""
|
|
165
|
+
func = self.get_function(func_name)
|
|
166
|
+
if not func:
|
|
167
|
+
return {"error": f"Function {func_name} not found"}
|
|
168
|
+
|
|
169
|
+
if dialects is None:
|
|
170
|
+
dialects = SUPPORTED_DIALECTS
|
|
171
|
+
|
|
172
|
+
comparison = {
|
|
173
|
+
"name": func.get("name"),
|
|
174
|
+
"description": func.get("description"),
|
|
175
|
+
"category": func.get("category"),
|
|
176
|
+
"parameters": func.get("parameters", []),
|
|
177
|
+
"examples": func.get("examples", {}),
|
|
178
|
+
"notes": func.get("notes", ""),
|
|
179
|
+
"dialects": {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for dialect in dialects:
|
|
183
|
+
syntax = func.get("dialects", {}).get(dialect)
|
|
184
|
+
comparison["dialects"][dialect] = syntax if syntax else "N/A"
|
|
185
|
+
|
|
186
|
+
available = [d for d in dialects if comparison["dialects"].get(d, "N/A") != "N/A"]
|
|
187
|
+
comparison["availability"] = {
|
|
188
|
+
"supported": available,
|
|
189
|
+
"not_supported": [d for d in dialects if d not in available],
|
|
190
|
+
"coverage": f"{len(available)}/{len(dialects)}"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return comparison
|
|
194
|
+
|
|
195
|
+
def get_all_categories(self) -> List[Dict[str, Any]]:
|
|
196
|
+
"""Get list of all categories with function counts and descriptions."""
|
|
197
|
+
return [
|
|
198
|
+
{
|
|
199
|
+
"name": cat,
|
|
200
|
+
"count": len(funcs),
|
|
201
|
+
"description": self.CATEGORY_DESCRIPTIONS.get(cat, ""),
|
|
202
|
+
"icon": self._get_category_icon(cat)
|
|
203
|
+
}
|
|
204
|
+
for cat, funcs in self.category_index.items()
|
|
205
|
+
if funcs
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
def _get_category_icon(self, category: str) -> str:
|
|
209
|
+
"""Get icon for category."""
|
|
210
|
+
icons = {
|
|
211
|
+
"string": "📝", "date": "📅", "math": "🔢", "aggregate": "📊",
|
|
212
|
+
"window": "🪟", "conditional": "❓", "conversion": "🔄", "json": "📋",
|
|
213
|
+
"array": "📦", "system": "⚙️", "geo": "🌍"
|
|
214
|
+
}
|
|
215
|
+
return icons.get(category, "📄")
|
|
216
|
+
|
|
217
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
218
|
+
"""Get comprehensive encyclopedia statistics."""
|
|
219
|
+
dialect_coverage = {d: 0 for d in SUPPORTED_DIALECTS}
|
|
220
|
+
for func in self.functions:
|
|
221
|
+
for dialect, syntax in func.get("dialects", {}).items():
|
|
222
|
+
if syntax and syntax != "N/A":
|
|
223
|
+
dialect_coverage[dialect] = dialect_coverage.get(dialect, 0) + 1
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"total_functions": len(self.functions),
|
|
227
|
+
"categories": len([c for c in self.category_index.values() if c]),
|
|
228
|
+
"by_category": {cat: len(funcs) for cat, funcs in self.category_index.items()},
|
|
229
|
+
"dialects_covered": len(SUPPORTED_DIALECTS),
|
|
230
|
+
"dialect_coverage": dialect_coverage,
|
|
231
|
+
"top_categories": sorted(
|
|
232
|
+
[(cat, len(funcs)) for cat, funcs in self.category_index.items()],
|
|
233
|
+
key=lambda x: x[1], reverse=True
|
|
234
|
+
)[:5]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def find_equivalent(self, func_name: str, source_dialect: str, target_dialect: str) -> Optional[Dict[str, Any]]:
|
|
238
|
+
"""Find target syntax while exposing strict semantic classification."""
|
|
239
|
+
func = self.get_function(func_name)
|
|
240
|
+
if not func:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
dialects = func.get("dialects", {})
|
|
244
|
+
source_dialect = source_dialect.lower()
|
|
245
|
+
target_dialect = target_dialect.lower()
|
|
246
|
+
source_syntax = dialects.get(source_dialect)
|
|
247
|
+
target_syntax = dialects.get(target_dialect)
|
|
248
|
+
|
|
249
|
+
if not source_syntax or source_syntax == "N/A":
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
semantic = self.resolve_function_semantics(func_name, source_dialect, target_dialect)
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
"function": func_name,
|
|
256
|
+
"source_dialect": source_dialect,
|
|
257
|
+
"target_dialect": target_dialect,
|
|
258
|
+
"source_syntax": source_syntax,
|
|
259
|
+
"target_syntax": target_syntax if target_syntax and target_syntax != "N/A" else None,
|
|
260
|
+
"supported_in_target": target_syntax is not None and target_syntax != "N/A",
|
|
261
|
+
"semantic_status": semantic["status"],
|
|
262
|
+
"semantic_equivalent": semantic["equivalent"],
|
|
263
|
+
"semantic_argument_semantics": semantic.get("argument_semantics", ""),
|
|
264
|
+
"semantic_return_shape": semantic.get("return_shape", ""),
|
|
265
|
+
"semantic_null_behavior": semantic.get("null_behavior", ""),
|
|
266
|
+
"semantic_notes": semantic.get("notes", ""),
|
|
267
|
+
"notes": func.get("notes", "")
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
def get_functions_by_dialect(self, dialect: str) -> List[Dict[str, Any]]:
|
|
271
|
+
"""Get all functions available in a specific dialect."""
|
|
272
|
+
dialect = dialect.lower()
|
|
273
|
+
available = []
|
|
274
|
+
|
|
275
|
+
for func in self.functions:
|
|
276
|
+
syntax = func.get("dialects", {}).get(dialect)
|
|
277
|
+
if syntax and syntax != "N/A":
|
|
278
|
+
available.append({
|
|
279
|
+
"name": func.get("name"),
|
|
280
|
+
"category": func.get("category"),
|
|
281
|
+
"syntax": syntax,
|
|
282
|
+
"description": func.get("description", ""),
|
|
283
|
+
"parameters": func.get("parameters", [])
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return available
|
|
287
|
+
|
|
288
|
+
def search_by_description(self, query: str, limit: int = 10) -> List[FunctionInfo]:
|
|
289
|
+
"""Search functions by description text."""
|
|
290
|
+
query = query.lower()
|
|
291
|
+
results = []
|
|
292
|
+
|
|
293
|
+
for func in self.functions:
|
|
294
|
+
desc = func.get("description", "").lower()
|
|
295
|
+
name = func.get("name", "").lower()
|
|
296
|
+
|
|
297
|
+
if query in desc or query in name:
|
|
298
|
+
score = 1.0 if query in name else 0.7
|
|
299
|
+
results.append((func, score))
|
|
300
|
+
|
|
301
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
302
|
+
return [r[0] for r in results[:limit]]
|
|
303
|
+
|
|
304
|
+
def get_function_alternatives(self, func_name: str, dialect: str) -> List[Dict[str, Any]]:
|
|
305
|
+
"""Get alternative functions for a dialect that doesn't support the original."""
|
|
306
|
+
func = self.get_function(func_name)
|
|
307
|
+
if not func:
|
|
308
|
+
return []
|
|
309
|
+
|
|
310
|
+
category = func.get("category", "")
|
|
311
|
+
dialect = dialect.lower()
|
|
312
|
+
|
|
313
|
+
alternatives = []
|
|
314
|
+
for other_func in self.category_index.get(category, []):
|
|
315
|
+
if other_func.get("name") == func_name:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
syntax = other_func.get("dialects", {}).get(dialect)
|
|
319
|
+
if syntax and syntax != "N/A":
|
|
320
|
+
alternatives.append({
|
|
321
|
+
"name": other_func.get("name"),
|
|
322
|
+
"syntax": syntax,
|
|
323
|
+
"description": other_func.get("description", ""),
|
|
324
|
+
"similarity": SequenceMatcher(
|
|
325
|
+
None,
|
|
326
|
+
func.get("description", ""),
|
|
327
|
+
other_func.get("description", "")
|
|
328
|
+
).ratio()
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
alternatives.sort(key=lambda x: x["similarity"], reverse=True)
|
|
332
|
+
return alternatives[:5]
|
backend/core/metadata.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Canonical metadata for SQL Dialect Master.
|
|
3
|
+
|
|
4
|
+
Single source of truth for product counts, version facts, and
|
|
5
|
+
dialect information. Every public surface (API, README verifier,
|
|
6
|
+
frontend) must derive its displayed numbers from this module rather
|
|
7
|
+
than from hardcoded literals.
|
|
8
|
+
|
|
9
|
+
Design rules:
|
|
10
|
+
- Counts (functions, types, rules, dialects) are computed at import
|
|
11
|
+
time from the canonical data files / engine objects.
|
|
12
|
+
- Version constants live here and are the sole release source
|
|
13
|
+
(``PROJECT_VERSION`` / ``API_VERSION``).
|
|
14
|
+
- The frontend architecture version is deliberately separate from
|
|
15
|
+
the product version: "Frontend v2" is not "release v2.0".
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
# ── Release version facts (sole source of truth) ────────────────────────
|
|
20
|
+
PROJECT_VERSION = "1.1.1"
|
|
21
|
+
API_VERSION = "1.1.1"
|
|
22
|
+
# Frontend v2 architecture — independent of the product release version.
|
|
23
|
+
FRONTEND_ARCHITECTURE_VERSION = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _canonical_counts() -> dict:
|
|
27
|
+
"""Compute dialect / function / type / rule counts from data files."""
|
|
28
|
+
import json
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
base = Path(__file__).parent
|
|
32
|
+
dialects, functions, types = 0, 0, 0
|
|
33
|
+
type_file = base / "type_mapping.json"
|
|
34
|
+
if type_file.exists():
|
|
35
|
+
with type_file.open(encoding="utf-8") as f:
|
|
36
|
+
data = json.load(f)
|
|
37
|
+
dialects = len(data.get("dialects", []))
|
|
38
|
+
types = len(data.get("mappings", {}))
|
|
39
|
+
func_file = base / "functions_db.json"
|
|
40
|
+
if func_file.exists():
|
|
41
|
+
with func_file.open(encoding="utf-8") as f:
|
|
42
|
+
fdata = json.load(f)
|
|
43
|
+
functions = len(fdata.get("functions", []))
|
|
44
|
+
return {"dialects": dialects, "functions": functions, "types": types}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_counts = _canonical_counts()
|
|
48
|
+
|
|
49
|
+
SUPPORTED_DIALECTS_COUNT = _counts["dialects"]
|
|
50
|
+
FUNCTION_COUNT = _counts["functions"]
|
|
51
|
+
TYPE_COUNT = _counts["types"]
|
|
52
|
+
|
|
53
|
+
# Rule count depends on the RuleEngine; compute lazily to keep the
|
|
54
|
+
# import graph clean. Uses the same engine instance the transpiler
|
|
55
|
+
# uses, so RULE_COUNT() matches the API's reported rule count.
|
|
56
|
+
def RULE_COUNT() -> int:
|
|
57
|
+
"""Number of active conversion rules in the RuleEngine."""
|
|
58
|
+
from backend.core.post_processor import PostProcessor
|
|
59
|
+
|
|
60
|
+
return len(PostProcessor().engine.rules)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"PROJECT_VERSION",
|
|
65
|
+
"API_VERSION",
|
|
66
|
+
"FRONTEND_ARCHITECTURE_VERSION",
|
|
67
|
+
"SUPPORTED_DIALECTS_COUNT",
|
|
68
|
+
"FUNCTION_COUNT",
|
|
69
|
+
"TYPE_COUNT",
|
|
70
|
+
"RULE_COUNT",
|
|
71
|
+
]
|
backend/core/nl2sql.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Stable NL2SQL facade with focused semantic extraction fixes."""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from .nl2sql_pipeline.orchestrator import PipelineNL2SQLGenerator as _NL2SQLGenerator
|
|
5
|
+
from .nl2sql_pipeline.models import NL2SQLResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NL2SQLGenerator(_NL2SQLGenerator):
|
|
9
|
+
"""Keep the public generator API while fixing count/group semantics."""
|
|
10
|
+
|
|
11
|
+
def _match_templates(self, text: str):
|
|
12
|
+
"""Route count-by-group requests through enhanced semantic extraction."""
|
|
13
|
+
template, match_groups = super()._match_templates(text)
|
|
14
|
+
if template and template.name == "count_by_group":
|
|
15
|
+
return None, None
|
|
16
|
+
return template, match_groups
|
|
17
|
+
|
|
18
|
+
def _extract_aggregations(self, text: str) -> list:
|
|
19
|
+
"""Preserve the established ``count`` alias for COUNT aggregates."""
|
|
20
|
+
aggregations = super()._extract_aggregations(text)
|
|
21
|
+
return [
|
|
22
|
+
"COUNT(*) AS count" if aggregation == "COUNT(*)" else aggregation
|
|
23
|
+
for aggregation in aggregations
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
def _extract_group_by(self, text: str) -> list:
|
|
27
|
+
"""Extract explicit per-group intent such as '每个部门' when needed."""
|
|
28
|
+
group_cols = super()._extract_group_by(text)
|
|
29
|
+
if group_cols:
|
|
30
|
+
return group_cols
|
|
31
|
+
|
|
32
|
+
match = re.search(r"(?:每个|各个)\s*([^的\s]+?)\s*的", text, re.IGNORECASE)
|
|
33
|
+
if not match:
|
|
34
|
+
match = re.search(r"(?:per|each)\s+(\w+)\b", text, re.IGNORECASE)
|
|
35
|
+
if not match:
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
group_text = match.group(1).strip()
|
|
39
|
+
for keyword, column in self.COLUMN_PATTERNS.items():
|
|
40
|
+
if keyword in group_text:
|
|
41
|
+
return [column]
|
|
42
|
+
if "部门" in group_text or "department" in group_text:
|
|
43
|
+
return ["department"]
|
|
44
|
+
if "用户" in group_text or "user" in group_text:
|
|
45
|
+
return ["user_id"]
|
|
46
|
+
if "日期" in group_text or "date" in group_text:
|
|
47
|
+
return ["date"]
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
def _extract_ordering(self, text: str):
|
|
51
|
+
"""Order aggregate-count requests by the requested count deterministically."""
|
|
52
|
+
ordering = super()._extract_ordering(text)
|
|
53
|
+
count_requested = any(
|
|
54
|
+
keyword in text
|
|
55
|
+
for keyword in ["数量", "计数", "多少", "count", "how many"]
|
|
56
|
+
)
|
|
57
|
+
sorting_requested = any(
|
|
58
|
+
keyword in text
|
|
59
|
+
for keyword in ["排序", "排列", "sort", "order", "sorted"]
|
|
60
|
+
)
|
|
61
|
+
if count_requested and sorting_requested:
|
|
62
|
+
descending = any(
|
|
63
|
+
keyword in text
|
|
64
|
+
for keyword in [
|
|
65
|
+
"降序",
|
|
66
|
+
"从大到小",
|
|
67
|
+
"递减",
|
|
68
|
+
"desc",
|
|
69
|
+
"descending",
|
|
70
|
+
"decreasing",
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
ascending = any(
|
|
74
|
+
keyword in text
|
|
75
|
+
for keyword in [
|
|
76
|
+
"升序",
|
|
77
|
+
"从小到大",
|
|
78
|
+
"递增",
|
|
79
|
+
"asc",
|
|
80
|
+
"ascending",
|
|
81
|
+
"increasing",
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
return "count", "ASC" if ascending and not descending else "DESC"
|
|
85
|
+
return ordering
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""NL2SQL Components Package - Modular components for NL2SQL generation.
|
|
3
|
+
|
|
4
|
+
This package provides modular components for natural language to SQL
|
|
5
|
+
generation:
|
|
6
|
+
- Tokenizer: Smart tokenization with Chinese and English support
|
|
7
|
+
- QueryTemplate: Pattern-based SQL generation using prioritized templates
|
|
8
|
+
- Mappings: Language mappings for tables, columns, and SQL operations
|
|
9
|
+
- CoreExtract: Operation, table, column, limit, distinct extraction
|
|
10
|
+
- Relations: Join extraction, join-key inference, relational keywords
|
|
11
|
+
- Aggregations: SQL aggregate function extraction
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
from backend.core.nl2sql_components import Tokenizer, KEYWORDS
|
|
15
|
+
|
|
16
|
+
tokens = Tokenizer.tokenize("查询所有用户")
|
|
17
|
+
print(tokens) # ['查询', '所有', '用户']
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Import main components for convenient access
|
|
21
|
+
from .aggregations import extract_aggregations
|
|
22
|
+
from .boolean_conditions import extract_boolean_conditions
|
|
23
|
+
from .core_extract import (
|
|
24
|
+
check_distinct,
|
|
25
|
+
detect_operation,
|
|
26
|
+
extract_columns,
|
|
27
|
+
extract_limit,
|
|
28
|
+
extract_table,
|
|
29
|
+
extract_table_subject,
|
|
30
|
+
named_tables,
|
|
31
|
+
)
|
|
32
|
+
from .mappings import COLUMN_PATTERNS, KEYWORDS, TABLE_PATTERNS
|
|
33
|
+
from .relations import (
|
|
34
|
+
JoinSpec,
|
|
35
|
+
extract_joins,
|
|
36
|
+
guess_join_key,
|
|
37
|
+
has_relational_keyword,
|
|
38
|
+
parse_join_spec,
|
|
39
|
+
)
|
|
40
|
+
from .templates import DEFAULT_QUERY_TEMPLATES, QueryTemplate
|
|
41
|
+
from .tokenizer import JIEBA_AVAILABLE, Tokenizer
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# tokenizer
|
|
45
|
+
"Tokenizer",
|
|
46
|
+
"JIEBA_AVAILABLE",
|
|
47
|
+
# templates
|
|
48
|
+
"QueryTemplate",
|
|
49
|
+
"DEFAULT_QUERY_TEMPLATES",
|
|
50
|
+
# mappings
|
|
51
|
+
"KEYWORDS",
|
|
52
|
+
"TABLE_PATTERNS",
|
|
53
|
+
"COLUMN_PATTERNS",
|
|
54
|
+
# core_extract
|
|
55
|
+
"detect_operation",
|
|
56
|
+
"extract_table",
|
|
57
|
+
"extract_table_subject",
|
|
58
|
+
"named_tables",
|
|
59
|
+
"extract_columns",
|
|
60
|
+
"extract_limit",
|
|
61
|
+
"check_distinct",
|
|
62
|
+
# relations
|
|
63
|
+
"extract_joins",
|
|
64
|
+
"guess_join_key",
|
|
65
|
+
"has_relational_keyword",
|
|
66
|
+
"JoinSpec",
|
|
67
|
+
"parse_join_spec",
|
|
68
|
+
# aggregations
|
|
69
|
+
"extract_aggregations",
|
|
70
|
+
# boolean conditions
|
|
71
|
+
"extract_boolean_conditions",
|
|
72
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""NL2SQL aggregation extraction.
|
|
3
|
+
|
|
4
|
+
Extracts aggregate function calls (COUNT, SUM, AVG, MAX, MIN) from
|
|
5
|
+
natural-language text using keyword matching. Uses the shared
|
|
6
|
+
COLUMN_PATTERNS to resolve the target column for value aggregates.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract_aggregations(text: str, column_patterns: Dict[str, str]) -> List[str]:
|
|
12
|
+
"""Extract aggregation expressions from text.
|
|
13
|
+
|
|
14
|
+
Returns a list of SQL aggregate expressions, e.g.
|
|
15
|
+
['COUNT(*)', 'SUM(amount)']. An empty list means no aggregation
|
|
16
|
+
was requested.
|
|
17
|
+
"""
|
|
18
|
+
aggs: List[str] = []
|
|
19
|
+
agg_column = None
|
|
20
|
+
for pattern, column in column_patterns.items():
|
|
21
|
+
if pattern in text:
|
|
22
|
+
agg_column = column
|
|
23
|
+
break
|
|
24
|
+
col = agg_column or "amount"
|
|
25
|
+
if any(k in text for k in ["统计", "计数", "数量", "多少", "几个", "count", "total", "how many"]):
|
|
26
|
+
aggs.append("COUNT(*)")
|
|
27
|
+
if any(k in text for k in ["求和", "总和", "合计", "总计", "sum", "total of"]):
|
|
28
|
+
aggs.append(f"SUM({col})")
|
|
29
|
+
if any(k in text for k in ["平均", "均值", "平均值", "average", "avg", "mean"]):
|
|
30
|
+
aggs.append(f"AVG({col})")
|
|
31
|
+
if any(k in text for k in ["最大", "最高", "最多", "max", "maximum", "highest", "largest"]):
|
|
32
|
+
aggs.append(f"MAX({col})")
|
|
33
|
+
if any(k in text for k in ["最小", "最低", "最少", "min", "minimum", "lowest", "smallest"]):
|
|
34
|
+
aggs.append(f"MIN({col})")
|
|
35
|
+
return aggs
|