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,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
|