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
backend/core/parser.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SQL Parser - Parse and analyze SQL statements using sqlglot.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive SQL parsing with:
|
|
5
|
+
- 12 database dialects support
|
|
6
|
+
- AST extraction and analysis
|
|
7
|
+
- Element extraction (tables, columns, functions, joins)
|
|
8
|
+
- Query type detection
|
|
9
|
+
"""
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
import sqlglot
|
|
14
|
+
from sqlglot import exp
|
|
15
|
+
|
|
16
|
+
from .config import SUPPORTED_DIALECTS, settings
|
|
17
|
+
|
|
18
|
+
__all__ = ['SUPPORTED_DIALECTS', 'ParseResult', 'SQLParser']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ParseResult:
|
|
23
|
+
"""Result of SQL parsing operation."""
|
|
24
|
+
success: bool
|
|
25
|
+
ast: Optional[exp.Expression] = None
|
|
26
|
+
dialect: str = ""
|
|
27
|
+
error: Optional[str] = None
|
|
28
|
+
tables: List[Dict[str, Any]] = field(default_factory=list)
|
|
29
|
+
columns: List[Dict[str, Any]] = field(default_factory=list)
|
|
30
|
+
functions: List[Dict[str, Any]] = field(default_factory=list)
|
|
31
|
+
joins: List[Dict[str, Any]] = field(default_factory=list)
|
|
32
|
+
query_type: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SQLParser:
|
|
36
|
+
"""SQL Parser using sqlglot for multi-dialect support."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, dialect: str = "hive"):
|
|
39
|
+
"""Initialize parser with a normalized supported dialect."""
|
|
40
|
+
dialect_lower = dialect.strip().lower()
|
|
41
|
+
if dialect_lower not in SUPPORTED_DIALECTS:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Unsupported dialect: {dialect}. "
|
|
44
|
+
f"Supported: {', '.join(SUPPORTED_DIALECTS)}"
|
|
45
|
+
)
|
|
46
|
+
self.dialect = dialect_lower
|
|
47
|
+
|
|
48
|
+
def parse(self, sql: str) -> ParseResult:
|
|
49
|
+
"""Parse SQL string into AST."""
|
|
50
|
+
if not isinstance(sql, str):
|
|
51
|
+
return ParseResult(success=False, error="SQL statement must be a string", dialect=self.dialect)
|
|
52
|
+
max_sql_length = getattr(settings, "parser_max_sql_length", settings.transpiler_max_sql_length)
|
|
53
|
+
if len(sql) > max_sql_length:
|
|
54
|
+
return ParseResult(
|
|
55
|
+
success=False,
|
|
56
|
+
error=f"SQL exceeds maximum length of {max_sql_length} characters",
|
|
57
|
+
dialect=self.dialect,
|
|
58
|
+
)
|
|
59
|
+
if not sql or not sql.strip():
|
|
60
|
+
return ParseResult(
|
|
61
|
+
success=False,
|
|
62
|
+
error="Empty SQL statement",
|
|
63
|
+
dialect=self.dialect
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
ast = sqlglot.parse_one(sql, read=self.dialect)
|
|
68
|
+
result = ParseResult(
|
|
69
|
+
success=True,
|
|
70
|
+
ast=ast,
|
|
71
|
+
dialect=self.dialect,
|
|
72
|
+
query_type=self._get_query_type(ast)
|
|
73
|
+
)
|
|
74
|
+
result.tables = self._extract_tables(ast)
|
|
75
|
+
result.columns = self._extract_columns(ast)
|
|
76
|
+
result.functions = self._extract_functions(ast)
|
|
77
|
+
result.joins = self._extract_joins(ast)
|
|
78
|
+
return result
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return ParseResult(
|
|
81
|
+
success=False,
|
|
82
|
+
error=str(e),
|
|
83
|
+
dialect=self.dialect
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def validate(self, sql: str) -> tuple:
|
|
87
|
+
"""Validate SQL syntax."""
|
|
88
|
+
if not isinstance(sql, str):
|
|
89
|
+
return False, "SQL statement must be a string"
|
|
90
|
+
max_sql_length = getattr(settings, "parser_max_sql_length", settings.transpiler_max_sql_length)
|
|
91
|
+
if len(sql) > max_sql_length:
|
|
92
|
+
return False, f"SQL exceeds maximum length of {max_sql_length} characters"
|
|
93
|
+
if not sql or not sql.strip():
|
|
94
|
+
return False, "Empty SQL statement"
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
sqlglot.parse_one(sql, read=self.dialect)
|
|
98
|
+
return True, None
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return False, str(e)
|
|
101
|
+
|
|
102
|
+
def extract_elements(self, sql: str) -> Dict[str, Any]:
|
|
103
|
+
"""Extract all SQL elements for analysis."""
|
|
104
|
+
result = self.parse(sql)
|
|
105
|
+
if not result.success:
|
|
106
|
+
return {"error": result.error}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"query_type": result.query_type,
|
|
110
|
+
"tables": result.tables,
|
|
111
|
+
"columns": result.columns,
|
|
112
|
+
"functions": result.functions,
|
|
113
|
+
"joins": result.joins,
|
|
114
|
+
"has_subquery": self._has_subquery(result.ast),
|
|
115
|
+
"has_cte": self._has_cte(result.ast),
|
|
116
|
+
"has_window": self._has_window_function(result.ast),
|
|
117
|
+
"has_aggregation": self._has_aggregation(result.ast),
|
|
118
|
+
"has_distinct": self._has_distinct(result.ast),
|
|
119
|
+
"has_union": self._has_union(result.ast),
|
|
120
|
+
"has_order_by": self._has_order_by(result.ast),
|
|
121
|
+
"has_limit": self._has_limit(result.ast),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def get_complexity_score(self, sql: str) -> Dict[str, Any]:
|
|
125
|
+
"""Calculate SQL complexity score."""
|
|
126
|
+
result = self.parse(sql)
|
|
127
|
+
if not result.success:
|
|
128
|
+
return {"error": result.error, "score": 0}
|
|
129
|
+
|
|
130
|
+
score = 1
|
|
131
|
+
factors = []
|
|
132
|
+
if result.joins:
|
|
133
|
+
join_score = len(result.joins) * 2
|
|
134
|
+
score += join_score
|
|
135
|
+
factors.append(f"Joins: +{join_score}")
|
|
136
|
+
if self._has_subquery(result.ast):
|
|
137
|
+
score += 3
|
|
138
|
+
factors.append("Subquery: +3")
|
|
139
|
+
if self._has_cte(result.ast):
|
|
140
|
+
score += 2
|
|
141
|
+
factors.append("CTE: +2")
|
|
142
|
+
if self._has_window_function(result.ast):
|
|
143
|
+
score += 2
|
|
144
|
+
factors.append("Window function: +2")
|
|
145
|
+
if self._has_aggregation(result.ast):
|
|
146
|
+
score += 1
|
|
147
|
+
factors.append("Aggregation: +1")
|
|
148
|
+
if self._has_union(result.ast):
|
|
149
|
+
score += 2
|
|
150
|
+
factors.append("UNION: +2")
|
|
151
|
+
|
|
152
|
+
if score <= 2:
|
|
153
|
+
level = "Simple"
|
|
154
|
+
elif score <= 5:
|
|
155
|
+
level = "Moderate"
|
|
156
|
+
elif score <= 10:
|
|
157
|
+
level = "Complex"
|
|
158
|
+
else:
|
|
159
|
+
level = "Very Complex"
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
"score": score,
|
|
163
|
+
"level": level,
|
|
164
|
+
"factors": factors,
|
|
165
|
+
"tables_count": len(result.tables),
|
|
166
|
+
"columns_count": len(result.columns),
|
|
167
|
+
"functions_count": len(result.functions),
|
|
168
|
+
"joins_count": len(result.joins)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def _get_query_type(self, ast: exp.Expression) -> str:
|
|
172
|
+
"""Determine the type of SQL query."""
|
|
173
|
+
type_map = {
|
|
174
|
+
exp.Select: "SELECT",
|
|
175
|
+
exp.Insert: "INSERT",
|
|
176
|
+
exp.Update: "UPDATE",
|
|
177
|
+
exp.Delete: "DELETE",
|
|
178
|
+
exp.Create: "CREATE",
|
|
179
|
+
exp.Drop: "DROP",
|
|
180
|
+
exp.Alter: "ALTER",
|
|
181
|
+
exp.Merge: "MERGE",
|
|
182
|
+
exp.Union: "UNION",
|
|
183
|
+
}
|
|
184
|
+
for exp_type, name in type_map.items():
|
|
185
|
+
if isinstance(ast, exp_type):
|
|
186
|
+
return name
|
|
187
|
+
return "UNKNOWN"
|
|
188
|
+
|
|
189
|
+
def _extract_tables(self, ast: exp.Expression) -> List[Dict[str, Any]]:
|
|
190
|
+
"""Extract all table references."""
|
|
191
|
+
tables = []
|
|
192
|
+
seen = set()
|
|
193
|
+
for table in ast.find_all(exp.Table):
|
|
194
|
+
key = (table.name, table.alias)
|
|
195
|
+
if key not in seen:
|
|
196
|
+
seen.add(key)
|
|
197
|
+
tables.append({
|
|
198
|
+
"name": table.name,
|
|
199
|
+
"alias": table.alias if table.alias else None,
|
|
200
|
+
"schema": table.db if hasattr(table, 'db') and table.db else None,
|
|
201
|
+
"catalog": table.catalog if hasattr(table, 'catalog') and table.catalog else None
|
|
202
|
+
})
|
|
203
|
+
return tables
|
|
204
|
+
|
|
205
|
+
def _extract_columns(self, ast: exp.Expression) -> List[Dict[str, Any]]:
|
|
206
|
+
"""Extract all column references."""
|
|
207
|
+
columns = []
|
|
208
|
+
seen = set()
|
|
209
|
+
for col in ast.find_all(exp.Column):
|
|
210
|
+
key = (col.name, col.table)
|
|
211
|
+
if key not in seen:
|
|
212
|
+
seen.add(key)
|
|
213
|
+
columns.append({
|
|
214
|
+
"name": col.name,
|
|
215
|
+
"table": col.table if col.table else None,
|
|
216
|
+
"alias": None
|
|
217
|
+
})
|
|
218
|
+
return columns
|
|
219
|
+
|
|
220
|
+
def _extract_functions(self, ast: exp.Expression) -> List[Dict[str, Any]]:
|
|
221
|
+
"""Extract all function calls."""
|
|
222
|
+
functions = []
|
|
223
|
+
seen = set()
|
|
224
|
+
for func in ast.find_all(exp.Func):
|
|
225
|
+
name = func.sql_name() if hasattr(func, 'sql_name') else type(func).__name__
|
|
226
|
+
if name not in seen:
|
|
227
|
+
seen.add(name)
|
|
228
|
+
functions.append({
|
|
229
|
+
"name": name,
|
|
230
|
+
"args_count": len(func.args) if hasattr(func, 'args') else 0,
|
|
231
|
+
"is_aggregate": isinstance(func, (exp.AggFunc,)),
|
|
232
|
+
"is_window": isinstance(getattr(func, 'parent', None), exp.Window)
|
|
233
|
+
})
|
|
234
|
+
return functions
|
|
235
|
+
|
|
236
|
+
def _extract_joins(self, ast: exp.Expression) -> List[Dict[str, Any]]:
|
|
237
|
+
"""Extract all JOIN operations."""
|
|
238
|
+
joins = []
|
|
239
|
+
for join in ast.find_all(exp.Join):
|
|
240
|
+
join_type = "INNER"
|
|
241
|
+
if join.kind:
|
|
242
|
+
join_type = join.kind.upper()
|
|
243
|
+
elif join.side:
|
|
244
|
+
join_type = f"{join.side.upper()} OUTER"
|
|
245
|
+
if hasattr(join.this, 'name'):
|
|
246
|
+
table_name = join.this.name
|
|
247
|
+
elif hasattr(join.this, 'alias'):
|
|
248
|
+
table_name = str(join.this)
|
|
249
|
+
else:
|
|
250
|
+
table_name = str(join.this)
|
|
251
|
+
joins.append({
|
|
252
|
+
"type": join_type,
|
|
253
|
+
"table": table_name,
|
|
254
|
+
"on_condition": str(join.args.get('on')) if join.args.get('on') else None
|
|
255
|
+
})
|
|
256
|
+
return joins
|
|
257
|
+
|
|
258
|
+
def _has_subquery(self, ast: exp.Expression) -> bool:
|
|
259
|
+
return len(list(ast.find_all(exp.Subquery))) > 0
|
|
260
|
+
|
|
261
|
+
def _has_cte(self, ast: exp.Expression) -> bool:
|
|
262
|
+
return ast.find(exp.With) is not None
|
|
263
|
+
|
|
264
|
+
def _has_window_function(self, ast: exp.Expression) -> bool:
|
|
265
|
+
return len(list(ast.find_all(exp.Window))) > 0
|
|
266
|
+
|
|
267
|
+
def _has_aggregation(self, ast: exp.Expression) -> bool:
|
|
268
|
+
return ast.find(exp.Group) is not None or ast.find(exp.AggFunc) is not None
|
|
269
|
+
|
|
270
|
+
def _has_distinct(self, ast: exp.Expression) -> bool:
|
|
271
|
+
return ast.find(exp.Distinct) is not None
|
|
272
|
+
|
|
273
|
+
def _has_union(self, ast: exp.Expression) -> bool:
|
|
274
|
+
return isinstance(ast, exp.Union) or ast.find(exp.Union) is not None
|
|
275
|
+
|
|
276
|
+
def _has_order_by(self, ast: exp.Expression) -> bool:
|
|
277
|
+
return ast.find(exp.Order) is not None
|
|
278
|
+
|
|
279
|
+
def _has_limit(self, ast: exp.Expression) -> bool:
|
|
280
|
+
return ast.find(exp.Limit) is not None or ast.find(exp.Fetch) is not None
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Post-processor for dialect-specific SQL transformations.
|
|
3
|
+
|
|
4
|
+
Uses the rule engine for declarative transformations and handles
|
|
5
|
+
complex cases that require custom logic.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Tuple, List, Callable
|
|
9
|
+
|
|
10
|
+
import sqlglot
|
|
11
|
+
from sqlglot import exp
|
|
12
|
+
|
|
13
|
+
from .rules import rule_engine, RuleEngine
|
|
14
|
+
from .function_call_scanner import replace_function_calls
|
|
15
|
+
from .p1_sql_scanner import mask_non_executable
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PostProcessor:
|
|
21
|
+
"""Apply dialect-specific fixes after sqlglot transpilation."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, engine: RuleEngine = None):
|
|
24
|
+
self.engine = engine or rule_engine
|
|
25
|
+
|
|
26
|
+
def process(self, sql: str, source: str, target: str) -> Tuple[str, List[str]]:
|
|
27
|
+
result = sql
|
|
28
|
+
all_notes = []
|
|
29
|
+
result, rule_notes = self.engine.apply_rules(result, source, target)
|
|
30
|
+
all_notes.extend(rule_notes)
|
|
31
|
+
result, custom_notes = self._apply_custom_transformations(result, source, target)
|
|
32
|
+
all_notes.extend(custom_notes)
|
|
33
|
+
warnings = self._check_warnings(sql, source, target)
|
|
34
|
+
all_notes.extend(warnings)
|
|
35
|
+
return result, all_notes
|
|
36
|
+
|
|
37
|
+
def _apply_custom_transformations(self, sql: str, source: str, target: str) -> Tuple[str, List[str]]:
|
|
38
|
+
result = sql
|
|
39
|
+
notes = []
|
|
40
|
+
if source == "oracle" and target in ("hive", "spark", "databricks"):
|
|
41
|
+
result, listagg_notes = self._convert_listagg_to_array_join(result)
|
|
42
|
+
notes.extend(listagg_notes)
|
|
43
|
+
return result, notes
|
|
44
|
+
|
|
45
|
+
def _replace_function_calls(self, sql: str, function_name: str, replacer: Callable[[str, str], str]) -> str:
|
|
46
|
+
"""Replace function calls using the shared quote/comment-aware scanner."""
|
|
47
|
+
return replace_function_calls(sql, function_name, replacer)
|
|
48
|
+
|
|
49
|
+
def _check_warnings(self, sql: str, source: str, target: str) -> List[str]:
|
|
50
|
+
warnings = []
|
|
51
|
+
sql_upper = mask_non_executable(sql).upper()
|
|
52
|
+
if source == "hive" and "INSERT OVERWRITE" in sql_upper and target in ("mysql", "postgres", "tsql", "oracle"):
|
|
53
|
+
warnings.append(f"WARNING: INSERT OVERWRITE not supported in {target}. Use TRUNCATE + INSERT or MERGE instead.")
|
|
54
|
+
if source == "oracle" and "CONNECT BY" in sql_upper and target in ("hive", "postgres", "mysql"):
|
|
55
|
+
warnings.append("WARNING: CONNECT BY requires manual conversion to WITH RECURSIVE CTE")
|
|
56
|
+
if source == "hive" and target in ("mysql", "postgres", "oracle", "tsql") and ("DISTRIBUTE BY" in sql_upper or "CLUSTER BY" in sql_upper):
|
|
57
|
+
warnings.append("WARNING: DISTRIBUTE BY/CLUSTER BY are Hive-specific hints, removed in target")
|
|
58
|
+
if any(kw in sql_upper for kw in ["CREATE PROCEDURE", "CREATE FUNCTION", "BEGIN", "DECLARE"]):
|
|
59
|
+
warnings.append("WARNING: Procedural code detected. Stored procedure syntax varies significantly between databases.")
|
|
60
|
+
if "MATERIALIZED VIEW" in sql_upper:
|
|
61
|
+
warnings.append("WARNING: Materialized view syntax and refresh mechanisms vary by database.")
|
|
62
|
+
return warnings
|
|
63
|
+
|
|
64
|
+
def _convert_listagg_to_array_join(self, sql: str) -> Tuple[str, List[str]]:
|
|
65
|
+
"""Convert Oracle LISTAGG→Hive-family aggregated output to ARRAY_JOIN(COLLECT_LIST(...)).
|
|
66
|
+
|
|
67
|
+
sqlglot transpiles Oracle LISTAGG to GROUP_CONCAT for hive/spark/databricks,
|
|
68
|
+
but Hive does not natively support GROUP_CONCAT. This method rewrites the
|
|
69
|
+
sqlglot-produced GROUP_CONCAT(... ORDER BY ..., sep) form to
|
|
70
|
+
ARRAY_JOIN(COLLECT_LIST(expr), sep). Ordering semantics are explicitly
|
|
71
|
+
noted as lost because Hive's COLLECT_LIST does not preserve input order.
|
|
72
|
+
|
|
73
|
+
The separator is the last Ordered expression when it is a string Literal.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
tree = sqlglot.parse_one(sql, read="hive")
|
|
77
|
+
except sqlglot.errors.ParseError:
|
|
78
|
+
return sql, []
|
|
79
|
+
|
|
80
|
+
notes = []
|
|
81
|
+
rewritten = False
|
|
82
|
+
|
|
83
|
+
for node in list(tree.walk()):
|
|
84
|
+
if not isinstance(node, exp.GroupConcat):
|
|
85
|
+
continue
|
|
86
|
+
expr = node.this
|
|
87
|
+
sep = node.args.get("separator")
|
|
88
|
+
|
|
89
|
+
has_order = isinstance(expr, exp.Order)
|
|
90
|
+
if has_order:
|
|
91
|
+
inner_expr = expr.this
|
|
92
|
+
# The separator is encoded as the last Ordered expression when it's a string literal
|
|
93
|
+
order_exprs = expr.expressions
|
|
94
|
+
if (
|
|
95
|
+
order_exprs
|
|
96
|
+
and isinstance(order_exprs[-1].this, exp.Literal)
|
|
97
|
+
and order_exprs[-1].this.is_string
|
|
98
|
+
):
|
|
99
|
+
sep = order_exprs[-1].this
|
|
100
|
+
else:
|
|
101
|
+
inner_expr = expr
|
|
102
|
+
|
|
103
|
+
collect_list = exp.Anonymous(
|
|
104
|
+
this="COLLECT_LIST",
|
|
105
|
+
expressions=[inner_expr],
|
|
106
|
+
)
|
|
107
|
+
array_join = exp.Anonymous(
|
|
108
|
+
this="ARRAY_JOIN",
|
|
109
|
+
expressions=[collect_list, sep or exp.Literal.string(",")],
|
|
110
|
+
)
|
|
111
|
+
node.replace(array_join)
|
|
112
|
+
if has_order:
|
|
113
|
+
notes.append(
|
|
114
|
+
"LISTAGG ordering semantics cannot be preserved in Hive; "
|
|
115
|
+
"converted to ARRAY_JOIN(COLLECT_LIST()) which does not guarantee order"
|
|
116
|
+
)
|
|
117
|
+
rewritten = True
|
|
118
|
+
|
|
119
|
+
if rewritten:
|
|
120
|
+
return tree.sql(dialect="hive"), notes
|
|
121
|
+
return sql, notes
|
|
122
|
+
|
|
123
|
+
def get_stats(self) -> dict:
|
|
124
|
+
return {
|
|
125
|
+
"rule_engine": self.engine.get_stats(),
|
|
126
|
+
"custom_handlers": [
|
|
127
|
+
"LISTAGG to ARRAY_JOIN (Hive)",
|
|
128
|
+
],
|
|
129
|
+
}
|