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,576 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""P8 NL2SQL pipeline: orchestrator (moved from nl2sql_legacy).
|
|
3
|
+
|
|
4
|
+
``PipelineNL2SQLGenerator`` is the canonical base class the facade
|
|
5
|
+
(``backend.core.nl2sql``) extends. It owns the three-code-path
|
|
6
|
+
``generate()`` dispatch:
|
|
7
|
+
|
|
8
|
+
1. boolean/null fast path
|
|
9
|
+
2. template path (relational-keyword bypass)
|
|
10
|
+
3. enhanced fallback (G1 named-tables fail-closed gate)
|
|
11
|
+
|
|
12
|
+
All per-stage logic delegates to the pure stage modules
|
|
13
|
+
(``intent``, ``resolution``, ``predicates``, ``aggregations``,
|
|
14
|
+
``clause_extract``, ``templates``, ``ir``, ``renderer``, ``dialect``,
|
|
15
|
+
``validation``, ``suggestions``). Instance-method signatures are
|
|
16
|
+
unchanged so the facade's ``super()`` overrides resolve exactly as they
|
|
17
|
+
did against the legacy class.
|
|
18
|
+
"""
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from ..column_hint_validation import validate_column_hints
|
|
24
|
+
from ..nl2sql_components.boolean_conditions import extract_boolean_conditions
|
|
25
|
+
from ..nl2sql_components.evidence import GenerationEvidence
|
|
26
|
+
from ..nl2sql_components.mappings import (
|
|
27
|
+
COLUMN_PATTERNS as MAPPING_COLUMN_PATTERNS,
|
|
28
|
+
KEYWORDS as MAPPING_KEYWORDS,
|
|
29
|
+
TABLE_PATTERNS as MAPPING_TABLE_PATTERNS,
|
|
30
|
+
)
|
|
31
|
+
from ..nl2sql_components.templates import DEFAULT_QUERY_TEMPLATES, QueryTemplate
|
|
32
|
+
from ..nl2sql_components.tokenizer import Tokenizer
|
|
33
|
+
from . import ir as _nl2sql_ir
|
|
34
|
+
from .aggregations import extract_aggregations as _pipeline_extract_aggregations
|
|
35
|
+
from .clause_extract import (
|
|
36
|
+
check_distinct as _pipeline_check_distinct,
|
|
37
|
+
extract_group_by as _pipeline_extract_group_by,
|
|
38
|
+
extract_limit as _pipeline_extract_limit,
|
|
39
|
+
extract_ordering as _pipeline_extract_ordering,
|
|
40
|
+
)
|
|
41
|
+
from .dialect import apply_dialect_adjustments as _apply_dialect_adjustments_impl
|
|
42
|
+
from .intent import (
|
|
43
|
+
analyze_intent as _analyze_intent,
|
|
44
|
+
detect_operation as _detect_operation,
|
|
45
|
+
parse_text as _parse_text_impl,
|
|
46
|
+
)
|
|
47
|
+
from .models import NL2SQLResult
|
|
48
|
+
from .predicates import extract_conditions as _extract_conditions_enhanced_impl
|
|
49
|
+
from .renderer import add_limit as _add_limit_impl
|
|
50
|
+
from .renderer import join_sql_kind as _join_sql_kind_impl
|
|
51
|
+
from .renderer import render_sql as _render_sql_impl
|
|
52
|
+
from .resolution import (
|
|
53
|
+
extract_columns as _pipeline_extract_columns,
|
|
54
|
+
extract_joins as _pipeline_extract_joins,
|
|
55
|
+
extract_table as _pipeline_extract_table,
|
|
56
|
+
extract_table_subject as _pipeline_extract_table_subject,
|
|
57
|
+
guess_join_key as _pipeline_guess_join_key,
|
|
58
|
+
has_relational_keyword as _pipeline_has_relational_keyword,
|
|
59
|
+
named_tables as _pipeline_named_tables,
|
|
60
|
+
)
|
|
61
|
+
from .suggestions import generate_suggestions as _pipeline_generate_suggestions
|
|
62
|
+
from .templates import (
|
|
63
|
+
generate_from_template as _pipeline_generate_from_template,
|
|
64
|
+
template_suggestions as _pipeline_template_suggestions,
|
|
65
|
+
)
|
|
66
|
+
from .validation import validate_sql as _validate_sql_impl
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PipelineNL2SQLGenerator:
|
|
72
|
+
"""NL2SQL pipeline base class.
|
|
73
|
+
|
|
74
|
+
Identical in behaviour to the legacy ``NL2SQLGenerator`` — it merely
|
|
75
|
+
delegates every per-stage concern to a pure function in the
|
|
76
|
+
``nl2sql_pipeline`` package so the class body reads as an explicit
|
|
77
|
+
orchestration of the target stages:
|
|
78
|
+
|
|
79
|
+
Intent → Entity Resolution → Relation Resolution →
|
|
80
|
+
Predicate Resolution → Query IR → Dialect Renderer →
|
|
81
|
+
AST Validation → Evidence/Confidence
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# Canonical mappings live in nl2sql_components.mappings.
|
|
85
|
+
KEYWORDS = MAPPING_KEYWORDS
|
|
86
|
+
TABLE_PATTERNS = MAPPING_TABLE_PATTERNS
|
|
87
|
+
COLUMN_PATTERNS = MAPPING_COLUMN_PATTERNS
|
|
88
|
+
|
|
89
|
+
def __init__(self, default_dialect: str = "hive"):
|
|
90
|
+
"""Initialize generator with default dialect and template system."""
|
|
91
|
+
self.default_dialect = default_dialect
|
|
92
|
+
self.tokenizer = Tokenizer()
|
|
93
|
+
self._init_query_templates()
|
|
94
|
+
|
|
95
|
+
def _init_query_templates(self) -> None:
|
|
96
|
+
"""Initialize prioritized query templates from the canonical module."""
|
|
97
|
+
self.query_templates: List[QueryTemplate] = list(DEFAULT_QUERY_TEMPLATES)
|
|
98
|
+
self.query_templates.sort(key=lambda template: template.priority, reverse=True)
|
|
99
|
+
|
|
100
|
+
def _match_templates(
|
|
101
|
+
self, text: str
|
|
102
|
+
) -> Tuple[Optional[QueryTemplate], Optional[Dict[str, Any]]]:
|
|
103
|
+
"""Try to match input text against query templates.
|
|
104
|
+
|
|
105
|
+
Explicit INSERT/UPDATE/DELETE requests bypass template matching and
|
|
106
|
+
flow through the enhanced semantic builder instead.
|
|
107
|
+
"""
|
|
108
|
+
operation = self._detect_operation(text)
|
|
109
|
+
if operation in {"INSERT", "UPDATE", "DELETE"}:
|
|
110
|
+
return None, None
|
|
111
|
+
for template in self.query_templates:
|
|
112
|
+
match_result = template.match(text)
|
|
113
|
+
if match_result:
|
|
114
|
+
logger.debug(
|
|
115
|
+
"Matched template '%s' with priority %s",
|
|
116
|
+
template.name,
|
|
117
|
+
template.priority,
|
|
118
|
+
)
|
|
119
|
+
return template, match_result
|
|
120
|
+
return None, None
|
|
121
|
+
|
|
122
|
+
def _tokenize_and_analyze(self, text: str) -> Dict[str, Any]:
|
|
123
|
+
"""Tokenize text and perform semantic analysis.
|
|
124
|
+
|
|
125
|
+
Delegates to ``nl2sql_pipeline.intent.analyze_intent``.
|
|
126
|
+
"""
|
|
127
|
+
return _analyze_intent(text)
|
|
128
|
+
|
|
129
|
+
def _generate_from_template(
|
|
130
|
+
self,
|
|
131
|
+
template: QueryTemplate,
|
|
132
|
+
match_groups: Dict[str, Any],
|
|
133
|
+
analysis: Dict[str, Any],
|
|
134
|
+
dialect: str,
|
|
135
|
+
table_hint: Optional[str],
|
|
136
|
+
column_hints: Optional[List[str]],
|
|
137
|
+
text_lower: str = "",
|
|
138
|
+
text_original: str = "",
|
|
139
|
+
) -> NL2SQLResult:
|
|
140
|
+
"""Generate SQL from a matched template.
|
|
141
|
+
|
|
142
|
+
Delegates to ``nl2sql_pipeline.templates.generate_from_template``.
|
|
143
|
+
"""
|
|
144
|
+
return _pipeline_generate_from_template(
|
|
145
|
+
template, match_groups, analysis, dialect,
|
|
146
|
+
table_hint, column_hints,
|
|
147
|
+
text_lower, text_original,
|
|
148
|
+
table_patterns=self.TABLE_PATTERNS,
|
|
149
|
+
column_patterns=self.COLUMN_PATTERNS,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _generate_suggestions_for_template(
|
|
153
|
+
self,
|
|
154
|
+
template: QueryTemplate,
|
|
155
|
+
table: str,
|
|
156
|
+
columns: List[str],
|
|
157
|
+
dialect: str,
|
|
158
|
+
) -> List[str]:
|
|
159
|
+
"""Generate suggestions specific to template-based generation.
|
|
160
|
+
|
|
161
|
+
Delegates to ``nl2sql_pipeline.templates.template_suggestions``.
|
|
162
|
+
"""
|
|
163
|
+
return _pipeline_template_suggestions(template, table, columns, dialect)
|
|
164
|
+
|
|
165
|
+
def generate(
|
|
166
|
+
self,
|
|
167
|
+
text: str,
|
|
168
|
+
dialect: str = None,
|
|
169
|
+
table_hint: str = None,
|
|
170
|
+
column_hints: List[str] = None,
|
|
171
|
+
) -> NL2SQLResult:
|
|
172
|
+
"""Generate SQL from natural language text."""
|
|
173
|
+
# Validate inputs
|
|
174
|
+
if not isinstance(text, str):
|
|
175
|
+
raise TypeError("text must be a string")
|
|
176
|
+
normalized_dialect = (dialect or self.default_dialect).strip().lower()
|
|
177
|
+
from ..config import SUPPORTED_DIALECTS
|
|
178
|
+
if normalized_dialect not in SUPPORTED_DIALECTS:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"Unsupported dialect: {dialect}. "
|
|
181
|
+
f"Supported: {', '.join(SUPPORTED_DIALECTS)}"
|
|
182
|
+
)
|
|
183
|
+
_SAFE_TABLE_HINT = re.compile(
|
|
184
|
+
r"[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*"
|
|
185
|
+
)
|
|
186
|
+
normalized_table_hint = (
|
|
187
|
+
table_hint.strip() if isinstance(table_hint, str) else table_hint
|
|
188
|
+
)
|
|
189
|
+
if normalized_table_hint and not _SAFE_TABLE_HINT.fullmatch(normalized_table_hint):
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"table_hint must be a simple SQL identifier or dotted identifier"
|
|
192
|
+
)
|
|
193
|
+
column_hints_error = validate_column_hints(column_hints)
|
|
194
|
+
if column_hints_error:
|
|
195
|
+
return NL2SQLResult(
|
|
196
|
+
success=False,
|
|
197
|
+
input_text=text,
|
|
198
|
+
dialect=normalized_dialect,
|
|
199
|
+
explanation=column_hints_error,
|
|
200
|
+
confidence=0.0,
|
|
201
|
+
evidence=GenerationEvidence(),
|
|
202
|
+
)
|
|
203
|
+
# Length guard (from production_hardening)
|
|
204
|
+
from ..config import settings
|
|
205
|
+
max_input_length = getattr(settings, "nl2sql_max_input_length", 8192)
|
|
206
|
+
if len(text) > max_input_length:
|
|
207
|
+
return NL2SQLResult(
|
|
208
|
+
success=False,
|
|
209
|
+
input_text=text[:200] + "...",
|
|
210
|
+
dialect=normalized_dialect,
|
|
211
|
+
explanation=(
|
|
212
|
+
f"Input text exceeds maximum length of "
|
|
213
|
+
f"{max_input_length} characters"
|
|
214
|
+
),
|
|
215
|
+
suggestions=["Please shorten the natural-language query and try again."],
|
|
216
|
+
evidence=GenerationEvidence(),
|
|
217
|
+
)
|
|
218
|
+
dialect = normalized_dialect
|
|
219
|
+
table_hint = normalized_table_hint
|
|
220
|
+
if column_hints_error:
|
|
221
|
+
return NL2SQLResult(
|
|
222
|
+
success=False,
|
|
223
|
+
input_text=text if isinstance(text, str) else "",
|
|
224
|
+
dialect=dialect,
|
|
225
|
+
explanation=column_hints_error,
|
|
226
|
+
confidence=0.0,
|
|
227
|
+
evidence=GenerationEvidence(),
|
|
228
|
+
)
|
|
229
|
+
text_lower = text.lower()
|
|
230
|
+
|
|
231
|
+
log_text = text[:50].replace("\r", "\\r").replace("\n", "\\n")
|
|
232
|
+
logger.info("NL2SQL: Processing '%s...' for dialect %s", log_text, dialect)
|
|
233
|
+
analysis = self._tokenize_and_analyze(text)
|
|
234
|
+
logger.debug(
|
|
235
|
+
"Token analysis: %d tokens, %d tables, %d columns",
|
|
236
|
+
len(analysis["tokens"]),
|
|
237
|
+
len(analysis["tables"]),
|
|
238
|
+
len(analysis["columns"]),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
boolean_conditions = extract_boolean_conditions(text_lower)
|
|
242
|
+
has_explicit_null_predicate = any(
|
|
243
|
+
keyword in text_lower
|
|
244
|
+
for keyword in [
|
|
245
|
+
"为空",
|
|
246
|
+
"空值",
|
|
247
|
+
"is null",
|
|
248
|
+
"null",
|
|
249
|
+
"empty",
|
|
250
|
+
"非空",
|
|
251
|
+
"不为空",
|
|
252
|
+
"is not null",
|
|
253
|
+
"not null",
|
|
254
|
+
"not empty",
|
|
255
|
+
]
|
|
256
|
+
)
|
|
257
|
+
if boolean_conditions or has_explicit_null_predicate:
|
|
258
|
+
parsed = self._parse_text(text_lower, text)
|
|
259
|
+
parsed.update(analysis)
|
|
260
|
+
operation = self._detect_operation(text_lower)
|
|
261
|
+
if self._has_relational_keyword(text_lower):
|
|
262
|
+
# Relational subject: 'users who have orders where price = 5'
|
|
263
|
+
# must yield FROM users, not FROM orders.
|
|
264
|
+
table = (
|
|
265
|
+
table_hint
|
|
266
|
+
or self._extract_table_subject(text_lower)
|
|
267
|
+
or self._extract_table(text_lower)
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
table = table_hint or self._extract_table(text_lower)
|
|
271
|
+
columns = column_hints if column_hints else self._extract_columns(text_lower)
|
|
272
|
+
conditions = self._extract_conditions_enhanced(text_lower, text)
|
|
273
|
+
aggregations = self._extract_aggregations(text_lower)
|
|
274
|
+
group_by = self._extract_group_by(text_lower)
|
|
275
|
+
ordering = self._extract_ordering(text_lower)
|
|
276
|
+
limit = self._extract_limit(text_lower)
|
|
277
|
+
joins = self._extract_joins(text_lower)
|
|
278
|
+
distinct = self._check_distinct(text_lower)
|
|
279
|
+
sql, explanation, confidence, evidence = self._build_sql_enhanced(
|
|
280
|
+
operation, table, columns, conditions, aggregations,
|
|
281
|
+
group_by, ordering, limit, joins, distinct, dialect,
|
|
282
|
+
)
|
|
283
|
+
if sql is None:
|
|
284
|
+
suggestions = [
|
|
285
|
+
"Specify the relationship between tables, e.g., "
|
|
286
|
+
"'users.id = invoices.user_id'",
|
|
287
|
+
]
|
|
288
|
+
else:
|
|
289
|
+
suggestions = self._generate_suggestions(text, sql, dialect)
|
|
290
|
+
return NL2SQLResult(
|
|
291
|
+
success=sql is not None,
|
|
292
|
+
input_text=text,
|
|
293
|
+
sql=sql,
|
|
294
|
+
dialect=dialect,
|
|
295
|
+
explanation=explanation,
|
|
296
|
+
confidence=confidence,
|
|
297
|
+
evidence=evidence,
|
|
298
|
+
suggestions=suggestions,
|
|
299
|
+
parsed_elements=parsed,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
template, match_groups = self._match_templates(text_lower)
|
|
303
|
+
# If relational keywords detected (D2), bypass templates and use
|
|
304
|
+
# the enhanced path to ensure EXISTS/NOT EXISTS structure instead
|
|
305
|
+
# of a physical JOIN.
|
|
306
|
+
if template and self._has_relational_keyword(text_lower):
|
|
307
|
+
template = None
|
|
308
|
+
logger.debug("NL2SQL: Relational keyword detected, bypassing template matching")
|
|
309
|
+
if template:
|
|
310
|
+
result = self._generate_from_template(
|
|
311
|
+
template, match_groups, analysis, dialect,
|
|
312
|
+
table_hint, column_hints, text_lower, text,
|
|
313
|
+
)
|
|
314
|
+
if result.success:
|
|
315
|
+
logger.info(
|
|
316
|
+
"NL2SQL: Template '%s' matched with confidence %.2f",
|
|
317
|
+
template.name,
|
|
318
|
+
result.confidence,
|
|
319
|
+
)
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
logger.debug("NL2SQL: Falling back to keyword-based extraction")
|
|
323
|
+
parsed = self._parse_text(text_lower, text)
|
|
324
|
+
parsed.update(analysis)
|
|
325
|
+
operation = self._detect_operation(text_lower)
|
|
326
|
+
columns = column_hints if column_hints else self._extract_columns(text_lower)
|
|
327
|
+
conditions = self._extract_conditions_enhanced(text_lower, text)
|
|
328
|
+
aggregations = self._extract_aggregations(text_lower)
|
|
329
|
+
group_by = self._extract_group_by(text_lower)
|
|
330
|
+
ordering = self._extract_ordering(text_lower)
|
|
331
|
+
limit = self._extract_limit(text_lower)
|
|
332
|
+
joins = self._extract_joins(text_lower)
|
|
333
|
+
distinct = self._check_distinct(text_lower)
|
|
334
|
+
|
|
335
|
+
# Table-name ownership (G1): a request that mentions two or more
|
|
336
|
+
# tables is a relational/join request; it only succeeds when every
|
|
337
|
+
# pair between the mentioned tables has a canonical relationship.
|
|
338
|
+
named_tables = self._named_tables(text_lower)
|
|
339
|
+
if len(named_tables) >= 2:
|
|
340
|
+
table = table_hint or named_tables[0]
|
|
341
|
+
unresolved = [j for j in joins if j.get("condition") is None]
|
|
342
|
+
if unresolved:
|
|
343
|
+
pair_desc = " ⟷ ".join(
|
|
344
|
+
[j.get("subject") or table for j in unresolved]
|
|
345
|
+
+ [j["table"] for j in unresolved]
|
|
346
|
+
)
|
|
347
|
+
return NL2SQLResult(
|
|
348
|
+
success=False,
|
|
349
|
+
input_text=text,
|
|
350
|
+
sql=None,
|
|
351
|
+
dialect=dialect,
|
|
352
|
+
explanation=(
|
|
353
|
+
"Unable to safely infer the relationship between the "
|
|
354
|
+
f"table(s) involved ({pair_desc}). Please provide the "
|
|
355
|
+
"join condition or schema relationship."
|
|
356
|
+
),
|
|
357
|
+
confidence=0.0,
|
|
358
|
+
evidence=GenerationEvidence(),
|
|
359
|
+
suggestions=[
|
|
360
|
+
"Specify the relationship between tables, e.g., "
|
|
361
|
+
"'users.id = invoices.user_id'",
|
|
362
|
+
],
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
if self._has_relational_keyword(text_lower):
|
|
366
|
+
table = table_hint or self._extract_table_subject(text_lower) or "table_name"
|
|
367
|
+
else:
|
|
368
|
+
table = table_hint or self._extract_table(text_lower)
|
|
369
|
+
|
|
370
|
+
sql, explanation, confidence, evidence = self._build_sql_enhanced(
|
|
371
|
+
operation, table, columns, conditions, aggregations,
|
|
372
|
+
group_by, ordering, limit, joins, distinct, dialect,
|
|
373
|
+
)
|
|
374
|
+
if sql is None:
|
|
375
|
+
suggestions = [
|
|
376
|
+
"Specify the relationship between tables, e.g., "
|
|
377
|
+
"'users.id = invoices.user_id'",
|
|
378
|
+
]
|
|
379
|
+
else:
|
|
380
|
+
suggestions = self._generate_suggestions(text, sql, dialect)
|
|
381
|
+
|
|
382
|
+
return NL2SQLResult(
|
|
383
|
+
success=sql is not None,
|
|
384
|
+
input_text=text,
|
|
385
|
+
sql=sql,
|
|
386
|
+
dialect=dialect,
|
|
387
|
+
explanation=explanation,
|
|
388
|
+
confidence=confidence,
|
|
389
|
+
evidence=evidence,
|
|
390
|
+
suggestions=suggestions,
|
|
391
|
+
parsed_elements=parsed,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def _parse_text(self, text_lower: str, original: str) -> dict:
|
|
395
|
+
"""Parse and extract all elements from text.
|
|
396
|
+
|
|
397
|
+
Delegates to ``nl2sql_pipeline.intent.parse_text``.
|
|
398
|
+
"""
|
|
399
|
+
return _parse_text_impl(text_lower, original)
|
|
400
|
+
|
|
401
|
+
def _detect_operation(self, text: str) -> str:
|
|
402
|
+
"""Detect SQL operation type from text.
|
|
403
|
+
|
|
404
|
+
Delegates to ``nl2sql_pipeline.intent.detect_operation``.
|
|
405
|
+
"""
|
|
406
|
+
return _detect_operation(text)
|
|
407
|
+
|
|
408
|
+
def _extract_table(self, text: str) -> str:
|
|
409
|
+
"""Extract table name from text.
|
|
410
|
+
|
|
411
|
+
Delegates to ``nl2sql_pipeline.resolution.extract_table``.
|
|
412
|
+
"""
|
|
413
|
+
return _pipeline_extract_table(text, self.TABLE_PATTERNS)
|
|
414
|
+
|
|
415
|
+
def _extract_table_subject(self, text: str) -> Optional[str]:
|
|
416
|
+
"""Extract the subject table (first appearing in text) for D2 relational queries.
|
|
417
|
+
|
|
418
|
+
Delegates to ``nl2sql_pipeline.resolution.extract_table_subject``.
|
|
419
|
+
"""
|
|
420
|
+
return _pipeline_extract_table_subject(text, self.TABLE_PATTERNS)
|
|
421
|
+
|
|
422
|
+
def _named_tables(self, text: str) -> list:
|
|
423
|
+
"""Return the distinct table names the text mentions.
|
|
424
|
+
|
|
425
|
+
Delegates to ``nl2sql_pipeline.resolution.named_tables``.
|
|
426
|
+
"""
|
|
427
|
+
return _pipeline_named_tables(text, self.TABLE_PATTERNS)
|
|
428
|
+
|
|
429
|
+
def _extract_columns(self, text: str) -> list:
|
|
430
|
+
"""Extract column names from text.
|
|
431
|
+
|
|
432
|
+
Delegates to ``nl2sql_pipeline.resolution.extract_columns``.
|
|
433
|
+
"""
|
|
434
|
+
return _pipeline_extract_columns(text, self.COLUMN_PATTERNS)
|
|
435
|
+
|
|
436
|
+
def _extract_conditions_enhanced(self, text: str, original: str) -> list:
|
|
437
|
+
"""Extract WHERE conditions with enhanced parsing including boolean expressions.
|
|
438
|
+
|
|
439
|
+
Delegates to ``nl2sql_pipeline.predicates.extract_conditions``.
|
|
440
|
+
"""
|
|
441
|
+
return _extract_conditions_enhanced_impl(text, original, self.COLUMN_PATTERNS)
|
|
442
|
+
|
|
443
|
+
def _extract_aggregations(self, text: str) -> list:
|
|
444
|
+
"""Extract aggregation functions from text.
|
|
445
|
+
|
|
446
|
+
Delegates to ``nl2sql_pipeline.aggregations.extract_aggregations``.
|
|
447
|
+
"""
|
|
448
|
+
return _pipeline_extract_aggregations(text, self.COLUMN_PATTERNS)
|
|
449
|
+
|
|
450
|
+
def _extract_group_by(self, text: str) -> list:
|
|
451
|
+
"""Extract GROUP BY columns from text.
|
|
452
|
+
|
|
453
|
+
Delegates to ``nl2sql_pipeline.clause_extract.extract_group_by``.
|
|
454
|
+
"""
|
|
455
|
+
return _pipeline_extract_group_by(text, self.COLUMN_PATTERNS, self.TABLE_PATTERNS)
|
|
456
|
+
|
|
457
|
+
def _extract_ordering(self, text: str) -> Optional[Tuple[str, str]]:
|
|
458
|
+
"""Extract ORDER BY clause from text.
|
|
459
|
+
|
|
460
|
+
Delegates to ``nl2sql_pipeline.clause_extract.extract_ordering``.
|
|
461
|
+
"""
|
|
462
|
+
return _pipeline_extract_ordering(text, self.COLUMN_PATTERNS)
|
|
463
|
+
|
|
464
|
+
def _extract_limit(self, text: str) -> Optional[int]:
|
|
465
|
+
"""Extract LIMIT value from text.
|
|
466
|
+
|
|
467
|
+
Delegates to ``nl2sql_pipeline.clause_extract.extract_limit``.
|
|
468
|
+
"""
|
|
469
|
+
return _pipeline_extract_limit(text)
|
|
470
|
+
|
|
471
|
+
def _extract_joins(self, text: str) -> list:
|
|
472
|
+
"""Extract JOIN information from text.
|
|
473
|
+
|
|
474
|
+
Delegates to ``nl2sql_pipeline.resolution.extract_joins``.
|
|
475
|
+
Detects relational existence patterns (e.g., "users who have orders")
|
|
476
|
+
and returns structured join contexts for EXISTS-based SQL generation.
|
|
477
|
+
"""
|
|
478
|
+
return _pipeline_extract_joins(text, self.TABLE_PATTERNS)
|
|
479
|
+
|
|
480
|
+
def _guess_join_key(self, table1: str, table2: str) -> Optional[str]:
|
|
481
|
+
"""Guess the join key between two tables.
|
|
482
|
+
|
|
483
|
+
Delegates to ``nl2sql_pipeline.resolution.guess_join_key``.
|
|
484
|
+
Returns None for unknown relationships instead of guessing.
|
|
485
|
+
"""
|
|
486
|
+
return _pipeline_guess_join_key(table1, table2)
|
|
487
|
+
|
|
488
|
+
def _check_distinct(self, text: str) -> bool:
|
|
489
|
+
"""Check if DISTINCT is needed.
|
|
490
|
+
|
|
491
|
+
Delegates to ``nl2sql_pipeline.clause_extract.check_distinct``.
|
|
492
|
+
"""
|
|
493
|
+
return _pipeline_check_distinct(text)
|
|
494
|
+
|
|
495
|
+
def _has_relational_keyword(self, text: str) -> bool:
|
|
496
|
+
"""Check if text contains relational existence keywords (D2).
|
|
497
|
+
|
|
498
|
+
Delegates to ``nl2sql_pipeline.resolution.has_relational_keyword``.
|
|
499
|
+
"""
|
|
500
|
+
return _pipeline_has_relational_keyword(text)
|
|
501
|
+
|
|
502
|
+
def _build_sql_enhanced(
|
|
503
|
+
self,
|
|
504
|
+
operation,
|
|
505
|
+
table,
|
|
506
|
+
columns,
|
|
507
|
+
conditions,
|
|
508
|
+
aggregations,
|
|
509
|
+
group_by,
|
|
510
|
+
ordering,
|
|
511
|
+
limit,
|
|
512
|
+
joins,
|
|
513
|
+
distinct,
|
|
514
|
+
dialect,
|
|
515
|
+
) -> tuple:
|
|
516
|
+
"""Build SQL statement from extracted components.
|
|
517
|
+
|
|
518
|
+
Delegates to ``nl2sql_pipeline.renderer.render_sql``.
|
|
519
|
+
Fail-closed behaviour (unresolved join → success=False) is
|
|
520
|
+
enforced by ``render_sql``.
|
|
521
|
+
"""
|
|
522
|
+
ir = _nl2sql_ir.build_ir(
|
|
523
|
+
operation, table, columns, conditions, aggregations,
|
|
524
|
+
group_by, ordering, limit, joins, distinct, dialect,
|
|
525
|
+
)
|
|
526
|
+
return _render_sql_impl(ir)
|
|
527
|
+
|
|
528
|
+
def _validate_generated_sql(self, sql: str, dialect: str) -> float:
|
|
529
|
+
"""Validate generated SQL syntax and return confidence adjustment.
|
|
530
|
+
|
|
531
|
+
Delegates to ``nl2sql_pipeline.validation.validate_sql``.
|
|
532
|
+
"""
|
|
533
|
+
return _validate_sql_impl(sql, dialect)
|
|
534
|
+
|
|
535
|
+
def _add_limit(self, sql: str, limit: int, dialect: str) -> str:
|
|
536
|
+
"""Add LIMIT clause with dialect-specific syntax.
|
|
537
|
+
|
|
538
|
+
Delegates to ``nl2sql_pipeline.renderer.add_limit``.
|
|
539
|
+
"""
|
|
540
|
+
return _add_limit_impl(sql, limit, dialect)
|
|
541
|
+
|
|
542
|
+
def _apply_dialect_adjustments(self, sql: str, dialect: str) -> str:
|
|
543
|
+
"""Apply dialect-specific syntax adjustments.
|
|
544
|
+
|
|
545
|
+
Delegates to ``nl2sql_pipeline.dialect.apply_dialect_adjustments``
|
|
546
|
+
(moved unchanged — executable-region-aware regex rewriter).
|
|
547
|
+
"""
|
|
548
|
+
return _apply_dialect_adjustments_impl(sql, dialect)
|
|
549
|
+
|
|
550
|
+
def _generate_suggestions(self, text: str, sql: str, dialect: str) -> list:
|
|
551
|
+
"""Generate improvement suggestions.
|
|
552
|
+
|
|
553
|
+
Delegates to ``nl2sql_pipeline.suggestions.generate_suggestions``.
|
|
554
|
+
"""
|
|
555
|
+
return _pipeline_generate_suggestions(text, sql, dialect)
|
|
556
|
+
|
|
557
|
+
def _extract_conditions(self, text: str) -> list:
|
|
558
|
+
"""Backward-compatible alias for enhanced condition extraction."""
|
|
559
|
+
return self._extract_conditions_enhanced(text, text)
|
|
560
|
+
|
|
561
|
+
def _build_sql(
|
|
562
|
+
self,
|
|
563
|
+
operation,
|
|
564
|
+
table,
|
|
565
|
+
columns,
|
|
566
|
+
conditions,
|
|
567
|
+
aggregations,
|
|
568
|
+
ordering,
|
|
569
|
+
limit,
|
|
570
|
+
dialect,
|
|
571
|
+
) -> tuple:
|
|
572
|
+
"""Backward-compatible alias for enhanced SQL building."""
|
|
573
|
+
return self._build_sql_enhanced(
|
|
574
|
+
operation, table, columns, conditions, aggregations,
|
|
575
|
+
[], ordering, limit, [], False, dialect,
|
|
576
|
+
)
|