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.
Files changed (86) hide show
  1. backend/__init__.py +1 -0
  2. backend/api/__init__.py +1 -0
  3. backend/api/main.py +732 -0
  4. backend/api/middleware.py +274 -0
  5. backend/api/rate_limit_store.py +105 -0
  6. backend/api/readiness.py +178 -0
  7. backend/core/__init__.py +57 -0
  8. backend/core/cache.py +261 -0
  9. backend/core/column_hint_validation.py +22 -0
  10. backend/core/config.py +546 -0
  11. backend/core/constants.py +84 -0
  12. backend/core/exceptions.py +124 -0
  13. backend/core/function_call_scanner.py +64 -0
  14. backend/core/function_semantics.py +148 -0
  15. backend/core/functions_db.json +601 -0
  16. backend/core/functions_lookup.py +332 -0
  17. backend/core/metadata.py +71 -0
  18. backend/core/nl2sql.py +85 -0
  19. backend/core/nl2sql_components/__init__.py +72 -0
  20. backend/core/nl2sql_components/aggregations.py +35 -0
  21. backend/core/nl2sql_components/boolean_conditions.py +279 -0
  22. backend/core/nl2sql_components/core_extract.py +106 -0
  23. backend/core/nl2sql_components/evidence.py +122 -0
  24. backend/core/nl2sql_components/mappings.py +185 -0
  25. backend/core/nl2sql_components/relations.py +180 -0
  26. backend/core/nl2sql_components/templates.py +130 -0
  27. backend/core/nl2sql_components/tokenizer.py +187 -0
  28. backend/core/nl2sql_legacy.py +22 -0
  29. backend/core/nl2sql_pipeline/__init__.py +13 -0
  30. backend/core/nl2sql_pipeline/aggregations.py +26 -0
  31. backend/core/nl2sql_pipeline/clause_extract.py +97 -0
  32. backend/core/nl2sql_pipeline/dialect.py +139 -0
  33. backend/core/nl2sql_pipeline/intent.py +85 -0
  34. backend/core/nl2sql_pipeline/ir.py +70 -0
  35. backend/core/nl2sql_pipeline/models.py +30 -0
  36. backend/core/nl2sql_pipeline/orchestrator.py +576 -0
  37. backend/core/nl2sql_pipeline/predicates.py +182 -0
  38. backend/core/nl2sql_pipeline/renderer.py +248 -0
  39. backend/core/nl2sql_pipeline/resolution.py +69 -0
  40. backend/core/nl2sql_pipeline/suggestions.py +30 -0
  41. backend/core/nl2sql_pipeline/templates.py +371 -0
  42. backend/core/nl2sql_pipeline/validation.py +28 -0
  43. backend/core/p1_sql_scanner.py +196 -0
  44. backend/core/p2_data_validation.py +72 -0
  45. backend/core/parser.py +280 -0
  46. backend/core/post_processor.py +129 -0
  47. backend/core/rules.py +808 -0
  48. backend/core/semantic_diff.py +385 -0
  49. backend/core/transpiler.py +791 -0
  50. backend/core/type_mapping.json +593 -0
  51. backend/core/type_mapping.py +765 -0
  52. backend/utils/__init__.py +2 -0
  53. backend/utils/validators.py +407 -0
  54. frontend/__init__.py +10 -0
  55. frontend/app_context_v2.py +201 -0
  56. frontend/core/__init__.py +33 -0
  57. frontend/core/design_tokens.py +127 -0
  58. frontend/core/escaping.py +13 -0
  59. frontend/core/navigation.py +135 -0
  60. frontend/core/state.py +612 -0
  61. frontend/core/styles.py +523 -0
  62. frontend/core/themes.py +69 -0
  63. frontend/core/viewmodels.py +499 -0
  64. frontend/pages/__init__.py +0 -0
  65. frontend/pages/convert.py +283 -0
  66. frontend/pages/diff.py +157 -0
  67. frontend/pages/functions.py +100 -0
  68. frontend/pages/history.py +144 -0
  69. frontend/pages/lineage.py +168 -0
  70. frontend/pages/nl2sql.py +305 -0
  71. frontend/pages/query_analysis.py +120 -0
  72. frontend/pages/runtime.py +66 -0
  73. frontend/pages/settings.py +60 -0
  74. frontend/pages/templates.py +67 -0
  75. frontend/pages/types.py +110 -0
  76. frontend/templates_v2.py +53 -0
  77. frontend/ui/__init__.py +0 -0
  78. frontend/ui/command_palette.py +153 -0
  79. frontend/ui/diff.py +275 -0
  80. frontend/ui/editor.py +102 -0
  81. frontend/ui/status.py +255 -0
  82. sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
  83. sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
  84. sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
  85. sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
  86. 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
+ }