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,385 @@
|
|
|
1
|
+
"""AST-based SQL semantic diff helpers.
|
|
2
|
+
|
|
3
|
+
The differ compares SQLGlot AST structure and canonical SQL. It distinguishes
|
|
4
|
+
syntax failures, structural/semantic differences, and cases where SQL semantics
|
|
5
|
+
cannot be proven from the AST alone.
|
|
6
|
+
"""
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from difflib import unified_diff
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
import sqlglot
|
|
13
|
+
from sqlglot import exp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_CONTEXT_SENSITIVE_FUNCTIONS = {
|
|
17
|
+
"CURRENT_DATE",
|
|
18
|
+
"CURRENT_TIME",
|
|
19
|
+
"CURRENT_TIMESTAMP",
|
|
20
|
+
"LOCALTIME",
|
|
21
|
+
"LOCALTIMESTAMP",
|
|
22
|
+
"NOW",
|
|
23
|
+
"RAND",
|
|
24
|
+
"RANDOM",
|
|
25
|
+
"UUID",
|
|
26
|
+
"CONCAT",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_CATEGORY_NODE_NAMES = {
|
|
30
|
+
"predicate": {
|
|
31
|
+
"And", "Between", "Eq", "Exists", "Greater", "GreaterThan", "GT",
|
|
32
|
+
"GTE", "In", "Is", "Like", "Not", "Or", "RegExp", "Where",
|
|
33
|
+
},
|
|
34
|
+
"projection": {"Alias", "Column", "Select", "Star"},
|
|
35
|
+
"grouping": {"Group", "Having"},
|
|
36
|
+
"ordering": {"Order", "Ordered"},
|
|
37
|
+
"row_limit": {"Fetch", "Limit", "Offset", "Top"},
|
|
38
|
+
"join": {"Join"},
|
|
39
|
+
"aggregate": {"AggFunc", "Count", "Sum", "Avg", "Min", "Max"},
|
|
40
|
+
"literal_or_type": {"Boolean", "DataType", "Date", "Interval", "Literal", "Null"},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class StructuredSemanticDifference:
|
|
46
|
+
"""Actionable description of one semantic difference category."""
|
|
47
|
+
|
|
48
|
+
category: str
|
|
49
|
+
severity: str
|
|
50
|
+
source_fragment: str
|
|
51
|
+
target_fragment: str
|
|
52
|
+
explanation: str
|
|
53
|
+
confidence: float = 1.0
|
|
54
|
+
evidence: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class SemanticDiff:
|
|
59
|
+
"""Comparison result with an explicit semantic confidence status."""
|
|
60
|
+
|
|
61
|
+
equivalent: bool
|
|
62
|
+
source_normalized: Optional[str]
|
|
63
|
+
target_normalized: Optional[str]
|
|
64
|
+
differences: List[str] = field(default_factory=list)
|
|
65
|
+
parse_error: Optional[str] = None
|
|
66
|
+
status: str = "different"
|
|
67
|
+
difference_categories: List[str] = field(default_factory=list)
|
|
68
|
+
structured_differences: List[StructuredSemanticDifference] = field(default_factory=list)
|
|
69
|
+
semantic_classification: str = "definitely_different"
|
|
70
|
+
confidence: float = 1.0
|
|
71
|
+
evidence: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _strip_redundant_parentheses(node: exp.Expression) -> exp.Expression:
|
|
75
|
+
"""Remove grouping around a single predicate without changing boolean precedence."""
|
|
76
|
+
if isinstance(node, exp.Paren) and not isinstance(node.this, (exp.And, exp.Or)):
|
|
77
|
+
return node.this.copy()
|
|
78
|
+
return node.copy()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_and_normalize(sql: str, dialect: str) -> exp.Expression:
|
|
82
|
+
if not isinstance(sql, str) or not sql.strip():
|
|
83
|
+
raise ValueError("SQL must be a non-empty string")
|
|
84
|
+
|
|
85
|
+
tree = sqlglot.parse_one(sql, read=dialect)
|
|
86
|
+
for node in list(tree.find_all(exp.Paren)):
|
|
87
|
+
if isinstance(node.this, (exp.And, exp.Or)):
|
|
88
|
+
continue
|
|
89
|
+
node.replace(node.this.copy())
|
|
90
|
+
return tree
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _canonical_sql(tree: exp.Expression) -> str:
|
|
94
|
+
return tree.sql(dialect="", pretty=False).strip()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _node_counts(tree: exp.Expression) -> Counter[str]:
|
|
98
|
+
return Counter(type(node).__name__ for node in tree.walk())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _function_names(tree: exp.Expression) -> List[str]:
|
|
102
|
+
names = []
|
|
103
|
+
for node in tree.walk():
|
|
104
|
+
sql_name = getattr(node, "sql_name", None)
|
|
105
|
+
if not callable(sql_name):
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
name = str(sql_name()).upper()
|
|
109
|
+
except (AttributeError, TypeError, ValueError):
|
|
110
|
+
continue
|
|
111
|
+
if name and name not in {"SELECT", "COLUMN", "TABLE", "LITERAL"}:
|
|
112
|
+
names.append(name)
|
|
113
|
+
return sorted(names)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _context_sensitive_functions(tree: exp.Expression) -> List[str]:
|
|
117
|
+
names = set()
|
|
118
|
+
for node in tree.walk():
|
|
119
|
+
sql_name = getattr(node, "sql_name", None)
|
|
120
|
+
if not callable(sql_name):
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
name = str(sql_name()).upper()
|
|
124
|
+
except (AttributeError, TypeError, ValueError):
|
|
125
|
+
name = ""
|
|
126
|
+
if name in _CONTEXT_SENSITIVE_FUNCTIONS:
|
|
127
|
+
names.add(name)
|
|
128
|
+
return sorted(names)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _fragment_sql(node: Optional[exp.Expression]) -> str:
|
|
132
|
+
if node is None:
|
|
133
|
+
return ""
|
|
134
|
+
return node.sql(dialect="", pretty=False).strip()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _list_fragment_sql(nodes) -> str:
|
|
138
|
+
return "\n".join(_fragment_sql(node) for node in nodes)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _difference_categories(
|
|
142
|
+
source_tree: exp.Expression,
|
|
143
|
+
target_tree: exp.Expression,
|
|
144
|
+
functions_differ: bool = False,
|
|
145
|
+
) -> List[str]:
|
|
146
|
+
"""Return categories whose semantic regions actually changed."""
|
|
147
|
+
categories = set()
|
|
148
|
+
source_select = source_tree if isinstance(source_tree, exp.Select) else source_tree.find(exp.Select)
|
|
149
|
+
target_select = target_tree if isinstance(target_tree, exp.Select) else target_tree.find(exp.Select)
|
|
150
|
+
|
|
151
|
+
if source_select is not None and target_select is not None:
|
|
152
|
+
if _list_fragment_sql(source_select.expressions) != _list_fragment_sql(target_select.expressions):
|
|
153
|
+
categories.add("projection")
|
|
154
|
+
source_where = _fragment_sql(source_select.args.get("where"))
|
|
155
|
+
target_where = _fragment_sql(target_select.args.get("where"))
|
|
156
|
+
if source_where != target_where:
|
|
157
|
+
categories.add("predicate")
|
|
158
|
+
if any("NULL" in fragment.upper() for fragment in (source_where, target_where)):
|
|
159
|
+
categories.add("null_semantics")
|
|
160
|
+
if _fragment_sql(source_select.args.get("having")) != _fragment_sql(target_select.args.get("having")):
|
|
161
|
+
categories.update({"predicate", "grouping"})
|
|
162
|
+
if _fragment_sql(source_select.args.get("group")) != _fragment_sql(target_select.args.get("group")):
|
|
163
|
+
categories.add("grouping")
|
|
164
|
+
if _fragment_sql(source_select.args.get("order")) != _fragment_sql(target_select.args.get("order")):
|
|
165
|
+
categories.add("ordering")
|
|
166
|
+
if _fragment_sql(source_select.args.get("limit")) != _fragment_sql(target_select.args.get("limit")):
|
|
167
|
+
categories.add("row_limit")
|
|
168
|
+
if _fragment_sql(source_select.args.get("offset")) != _fragment_sql(target_select.args.get("offset")):
|
|
169
|
+
categories.add("row_limit")
|
|
170
|
+
|
|
171
|
+
source_joins = list(source_tree.find_all(exp.Join))
|
|
172
|
+
target_joins = list(target_tree.find_all(exp.Join))
|
|
173
|
+
if _list_fragment_sql(source_joins) != _list_fragment_sql(target_joins):
|
|
174
|
+
categories.add("join")
|
|
175
|
+
|
|
176
|
+
source_aggregates = list(source_tree.find_all(exp.AggFunc))
|
|
177
|
+
target_aggregates = list(target_tree.find_all(exp.AggFunc))
|
|
178
|
+
if _list_fragment_sql(source_aggregates) != _list_fragment_sql(target_aggregates):
|
|
179
|
+
categories.add("aggregate")
|
|
180
|
+
|
|
181
|
+
source_literals = [node for node in source_tree.walk() if type(node).__name__ in _CATEGORY_NODE_NAMES["literal_or_type"]]
|
|
182
|
+
target_literals = [node for node in target_tree.walk() if type(node).__name__ in _CATEGORY_NODE_NAMES["literal_or_type"]]
|
|
183
|
+
if _list_fragment_sql(source_literals) != _list_fragment_sql(target_literals):
|
|
184
|
+
categories.add("literal_or_type")
|
|
185
|
+
|
|
186
|
+
if functions_differ:
|
|
187
|
+
categories.add("function")
|
|
188
|
+
|
|
189
|
+
return sorted(categories) or ["structure"]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _category_fragments(
|
|
193
|
+
category: str,
|
|
194
|
+
source_tree: exp.Expression,
|
|
195
|
+
target_tree: exp.Expression,
|
|
196
|
+
) -> tuple[str, str]:
|
|
197
|
+
source_select = source_tree if isinstance(source_tree, exp.Select) else source_tree.find(exp.Select)
|
|
198
|
+
target_select = target_tree if isinstance(target_tree, exp.Select) else target_tree.find(exp.Select)
|
|
199
|
+
if category in {"predicate", "null_semantics"} and source_select is not None and target_select is not None:
|
|
200
|
+
return (
|
|
201
|
+
_fragment_sql(source_select.args.get("where")),
|
|
202
|
+
_fragment_sql(target_select.args.get("where")),
|
|
203
|
+
)
|
|
204
|
+
if category == "projection" and source_select is not None and target_select is not None:
|
|
205
|
+
return _list_fragment_sql(source_select.expressions), _list_fragment_sql(target_select.expressions)
|
|
206
|
+
if category == "grouping" and source_select is not None and target_select is not None:
|
|
207
|
+
return _fragment_sql(source_select.args.get("group")), _fragment_sql(target_select.args.get("group"))
|
|
208
|
+
if category == "ordering" and source_select is not None and target_select is not None:
|
|
209
|
+
return _fragment_sql(source_select.args.get("order")), _fragment_sql(target_select.args.get("order"))
|
|
210
|
+
if category == "row_limit" and source_select is not None and target_select is not None:
|
|
211
|
+
source = _fragment_sql(source_select.args.get("limit")) or _fragment_sql(source_select.args.get("offset"))
|
|
212
|
+
target = _fragment_sql(target_select.args.get("limit")) or _fragment_sql(target_select.args.get("offset"))
|
|
213
|
+
return source, target
|
|
214
|
+
if category == "join":
|
|
215
|
+
return _list_fragment_sql(source_tree.find_all(exp.Join)), _list_fragment_sql(target_tree.find_all(exp.Join))
|
|
216
|
+
if category == "aggregate":
|
|
217
|
+
return _list_fragment_sql(source_tree.find_all(exp.AggFunc)), _list_fragment_sql(target_tree.find_all(exp.AggFunc))
|
|
218
|
+
return _canonical_sql(source_tree), _canonical_sql(target_tree)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _structured_explanations(
|
|
222
|
+
categories: List[str],
|
|
223
|
+
source_tree: exp.Expression,
|
|
224
|
+
target_tree: exp.Expression,
|
|
225
|
+
) -> List[StructuredSemanticDifference]:
|
|
226
|
+
explanations = {
|
|
227
|
+
"predicate": ("error", "Predicate logic changed; boolean precedence or filtering behavior may differ."),
|
|
228
|
+
"null_semantics": ("error", "NULL comparison semantics changed; SQL three-valued logic may produce different rows."),
|
|
229
|
+
"projection": ("warning", "Projected expressions changed, which can alter returned values or columns."),
|
|
230
|
+
"grouping": ("error", "Grouping or HAVING structure changed, which can alter row cardinality and aggregates."),
|
|
231
|
+
"ordering": ("warning", "Ordering changed; result sequence is not preserved."),
|
|
232
|
+
"row_limit": ("error", "Row limiting changed; the returned row set or pagination semantics may differ."),
|
|
233
|
+
"join": ("error", "JOIN structure changed; matching rows or row cardinality may differ."),
|
|
234
|
+
"aggregate": ("error", "Aggregate expressions changed; computed results may differ."),
|
|
235
|
+
"function": ("warning", "Function expressions differ and require dialect-specific semantic review."),
|
|
236
|
+
"literal_or_type": ("warning", "Literal or type representation changed and may affect coercion or comparison semantics."),
|
|
237
|
+
"structure": ("error", "AST structure changed outside a more specific semantic category."),
|
|
238
|
+
}
|
|
239
|
+
records: List[StructuredSemanticDifference] = []
|
|
240
|
+
for category in categories:
|
|
241
|
+
severity, explanation = explanations.get(category, ("warning", "The SQL structure differs and requires semantic review."))
|
|
242
|
+
source_fragment, target_fragment = _category_fragments(category, source_tree, target_tree)
|
|
243
|
+
records.append(
|
|
244
|
+
StructuredSemanticDifference(
|
|
245
|
+
category=category,
|
|
246
|
+
severity=severity,
|
|
247
|
+
source_fragment=source_fragment,
|
|
248
|
+
target_fragment=target_fragment,
|
|
249
|
+
explanation=explanation,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
return records
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def diff_sql_ast(
|
|
256
|
+
source_sql: str,
|
|
257
|
+
target_sql: str,
|
|
258
|
+
source_dialect: str = "",
|
|
259
|
+
target_dialect: str = "",
|
|
260
|
+
) -> SemanticDiff:
|
|
261
|
+
"""Compare two SQL ASTs and report semantic confidence explicitly.
|
|
262
|
+
|
|
263
|
+
``equivalent`` remains backward-compatible: it is true only when the ASTs
|
|
264
|
+
match and no known context-sensitive construct prevents a safe conclusion.
|
|
265
|
+
``status`` is one of ``equivalent``, ``different``, ``unknown``, or
|
|
266
|
+
``parse_error``. ``semantic_classification`` refines the result without
|
|
267
|
+
changing those existing status values.
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
source_tree = _parse_and_normalize(source_sql, source_dialect)
|
|
271
|
+
target_tree = _parse_and_normalize(target_sql, target_dialect)
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
return SemanticDiff(
|
|
274
|
+
equivalent=False,
|
|
275
|
+
source_normalized=None,
|
|
276
|
+
target_normalized=None,
|
|
277
|
+
differences=["Unable to parse one or both SQL statements"],
|
|
278
|
+
parse_error=str(exc),
|
|
279
|
+
status="parse_error",
|
|
280
|
+
semantic_classification="parse_error",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
source_normalized = _canonical_sql(source_tree)
|
|
284
|
+
target_normalized = _canonical_sql(target_tree)
|
|
285
|
+
source_counts = _node_counts(source_tree)
|
|
286
|
+
target_counts = _node_counts(target_tree)
|
|
287
|
+
source_functions = _function_names(source_tree)
|
|
288
|
+
target_functions = _function_names(target_tree)
|
|
289
|
+
functions_differ = source_functions != target_functions
|
|
290
|
+
|
|
291
|
+
differences: List[str] = []
|
|
292
|
+
categories: List[str] = []
|
|
293
|
+
if source_normalized != target_normalized:
|
|
294
|
+
diff_lines = list(
|
|
295
|
+
unified_diff(
|
|
296
|
+
source_normalized.splitlines(),
|
|
297
|
+
target_normalized.splitlines(),
|
|
298
|
+
fromfile="source",
|
|
299
|
+
tofile="target",
|
|
300
|
+
lineterm="",
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
differences.append("Canonical AST SQL differs")
|
|
304
|
+
differences.extend(diff_lines[:20])
|
|
305
|
+
categories = _difference_categories(source_tree, target_tree, functions_differ)
|
|
306
|
+
elif functions_differ:
|
|
307
|
+
differences.append("Function expression set differs")
|
|
308
|
+
categories = ["function"]
|
|
309
|
+
|
|
310
|
+
changed_nodes = sorted(set(source_counts) | set(target_counts))
|
|
311
|
+
for node_name in changed_nodes:
|
|
312
|
+
source_count = source_counts.get(node_name, 0)
|
|
313
|
+
target_count = target_counts.get(node_name, 0)
|
|
314
|
+
if source_count != target_count:
|
|
315
|
+
differences.append(
|
|
316
|
+
f"AST node count changed: {node_name} {source_count} -> {target_count}"
|
|
317
|
+
)
|
|
318
|
+
if not categories:
|
|
319
|
+
categories = ["structure"]
|
|
320
|
+
|
|
321
|
+
context_sensitive = sorted(
|
|
322
|
+
set(_context_sensitive_functions(source_tree))
|
|
323
|
+
| set(_context_sensitive_functions(target_tree))
|
|
324
|
+
)
|
|
325
|
+
if context_sensitive:
|
|
326
|
+
differences.append(
|
|
327
|
+
"Semantic equivalence is unknown for context-sensitive functions: "
|
|
328
|
+
+ ", ".join(context_sensitive)
|
|
329
|
+
)
|
|
330
|
+
categories = sorted(set(categories) | {"context_sensitive"})
|
|
331
|
+
|
|
332
|
+
if context_sensitive:
|
|
333
|
+
status = "unknown"
|
|
334
|
+
semantic_classification = "potentially_different"
|
|
335
|
+
elif differences:
|
|
336
|
+
status = "different"
|
|
337
|
+
semantic_classification = "definitely_different"
|
|
338
|
+
else:
|
|
339
|
+
status = "equivalent"
|
|
340
|
+
semantic_classification = "structurally_equivalent" if source_sql.strip() != target_sql.strip() else "equivalent"
|
|
341
|
+
|
|
342
|
+
structured = []
|
|
343
|
+
if categories and status != "unknown":
|
|
344
|
+
structured = _structured_explanations(categories, source_tree, target_tree)
|
|
345
|
+
elif status == "unknown":
|
|
346
|
+
structured = [
|
|
347
|
+
StructuredSemanticDifference(
|
|
348
|
+
category="context_sensitive",
|
|
349
|
+
severity="warning",
|
|
350
|
+
source_fragment=source_normalized,
|
|
351
|
+
target_fragment=target_normalized,
|
|
352
|
+
explanation=(
|
|
353
|
+
"The query contains context-sensitive functions whose values can vary by execution time or environment; "
|
|
354
|
+
"AST equality alone cannot prove runtime equivalence."
|
|
355
|
+
),
|
|
356
|
+
confidence=0.3,
|
|
357
|
+
evidence=", ".join(context_sensitive),
|
|
358
|
+
)
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
return SemanticDiff(
|
|
362
|
+
equivalent=status == "equivalent",
|
|
363
|
+
source_normalized=source_normalized,
|
|
364
|
+
target_normalized=target_normalized,
|
|
365
|
+
differences=differences,
|
|
366
|
+
status=status,
|
|
367
|
+
difference_categories=categories,
|
|
368
|
+
structured_differences=structured,
|
|
369
|
+
semantic_classification=semantic_classification,
|
|
370
|
+
confidence=0.95 if status == "equivalent" else (0.3 if status == "unknown" else 0.7),
|
|
371
|
+
evidence=None if status != "unknown" else "context-sensitive functions: " + ", ".join(context_sensitive),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class SQLSemanticDiffer:
|
|
376
|
+
"""Object-oriented facade for callers that prefer a reusable service."""
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def compare(
|
|
380
|
+
source_sql: str,
|
|
381
|
+
target_sql: str,
|
|
382
|
+
source_dialect: str = "",
|
|
383
|
+
target_dialect: str = "",
|
|
384
|
+
) -> SemanticDiff:
|
|
385
|
+
return diff_sql_ast(source_sql, target_sql, source_dialect, target_dialect)
|