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,407 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Input validation utilities for SQL Dialect Master.
|
|
3
|
+
|
|
4
|
+
Provides validation functions for SQL, dialects, and other inputs.
|
|
5
|
+
"""
|
|
6
|
+
import re
|
|
7
|
+
from typing import Tuple, List
|
|
8
|
+
|
|
9
|
+
from backend.core.config import SUPPORTED_DIALECTS, settings
|
|
10
|
+
from backend.core.exceptions import (
|
|
11
|
+
ValidationError,
|
|
12
|
+
UnsupportedDialectError,
|
|
13
|
+
SecurityViolationError
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_dialect(dialect: str, param_name: str = "dialect") -> str:
|
|
18
|
+
"""Validate SQL dialect.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
dialect: Dialect to validate
|
|
22
|
+
param_name: Parameter name for error messages
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Normalized dialect name (lowercase)
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
UnsupportedDialectError: If dialect is not supported
|
|
29
|
+
"""
|
|
30
|
+
if not dialect:
|
|
31
|
+
raise ValidationError(f"{param_name} is required", field=param_name)
|
|
32
|
+
|
|
33
|
+
dialect_lower = dialect.lower().strip()
|
|
34
|
+
|
|
35
|
+
if dialect_lower not in SUPPORTED_DIALECTS:
|
|
36
|
+
raise UnsupportedDialectError(dialect, SUPPORTED_DIALECTS)
|
|
37
|
+
|
|
38
|
+
return dialect_lower
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_sql(sql: str, max_length: int = None) -> str:
|
|
42
|
+
"""Validate SQL input.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
sql: SQL to validate
|
|
46
|
+
max_length: Maximum allowed length
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Validated SQL string
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValidationError: If SQL is invalid
|
|
53
|
+
"""
|
|
54
|
+
if not sql:
|
|
55
|
+
raise ValidationError("SQL statement is required", field="sql")
|
|
56
|
+
|
|
57
|
+
sql = sql.strip()
|
|
58
|
+
|
|
59
|
+
if not sql:
|
|
60
|
+
raise ValidationError("SQL statement cannot be empty", field="sql")
|
|
61
|
+
|
|
62
|
+
max_len = max_length or settings.transpiler_max_sql_length
|
|
63
|
+
if len(sql) > max_len:
|
|
64
|
+
raise ValidationError(
|
|
65
|
+
f"SQL exceeds maximum length of {max_len} characters",
|
|
66
|
+
field="sql",
|
|
67
|
+
value=f"{len(sql)} characters"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return sql
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_sql_security(sql: str, block_dangerous: bool = None) -> Tuple[bool, List[str]]:
|
|
74
|
+
"""Validate SQL for security issues.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
sql: SQL to validate
|
|
78
|
+
block_dangerous: Whether to block dangerous patterns
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Tuple of (is_safe, list_of_warnings)
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
SecurityViolationError: If dangerous pattern detected and blocking enabled
|
|
85
|
+
"""
|
|
86
|
+
block = block_dangerous if block_dangerous is not None else settings.security_block_dangerous
|
|
87
|
+
warnings = []
|
|
88
|
+
|
|
89
|
+
# Dangerous patterns that may be blocked
|
|
90
|
+
dangerous_patterns = [
|
|
91
|
+
(r";\s*(DROP|DELETE|TRUNCATE|ALTER|CREATE|INSERT|UPDATE)\s+",
|
|
92
|
+
"Multiple statements with dangerous operations"),
|
|
93
|
+
(r"UNION\s+(ALL\s+)?SELECT\s+.*(FROM\s+information_schema|@@version|user\(\))",
|
|
94
|
+
"Potential SQL injection pattern"),
|
|
95
|
+
(r"(xp_cmdshell|sp_executesql|EXEC\s*\(|EXECUTE\s+IMMEDIATE)",
|
|
96
|
+
"Command execution attempt"),
|
|
97
|
+
(r"(INTO\s+OUTFILE|INTO\s+DUMPFILE|LOAD_FILE|UTL_FILE)",
|
|
98
|
+
"File operation detected"),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# Warning patterns (never blocked)
|
|
102
|
+
warning_patterns = [
|
|
103
|
+
(r"DELETE\s+FROM\s+\w+\s*(?!WHERE)",
|
|
104
|
+
"DELETE without WHERE clause"),
|
|
105
|
+
(r"UPDATE\s+\w+\s+SET\s+.*(?!WHERE)",
|
|
106
|
+
"UPDATE without WHERE clause"),
|
|
107
|
+
(r"DROP\s+(TABLE|DATABASE|SCHEMA|INDEX|VIEW)",
|
|
108
|
+
"DROP operation - data loss risk"),
|
|
109
|
+
(r"TRUNCATE\s+TABLE",
|
|
110
|
+
"TRUNCATE operation - data loss risk"),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# Check dangerous patterns
|
|
114
|
+
for pattern, message in dangerous_patterns:
|
|
115
|
+
if re.search(pattern, sql, re.IGNORECASE | re.MULTILINE):
|
|
116
|
+
if block:
|
|
117
|
+
raise SecurityViolationError(message, pattern=pattern)
|
|
118
|
+
warnings.append(f"🔒 Security: {message}")
|
|
119
|
+
|
|
120
|
+
# Check warning patterns
|
|
121
|
+
for pattern, message in warning_patterns:
|
|
122
|
+
if re.search(pattern, sql, re.IGNORECASE | re.MULTILINE):
|
|
123
|
+
warnings.append(f"⚠️ {message}")
|
|
124
|
+
|
|
125
|
+
return len(warnings) == 0, warnings
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def validate_batch_size(size: int, max_size: int = None) -> int:
|
|
129
|
+
"""Validate batch size.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
size: Requested batch size
|
|
133
|
+
max_size: Maximum allowed size
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Validated batch size
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValidationError: If size is invalid
|
|
140
|
+
"""
|
|
141
|
+
max_allowed = max_size or settings.max_batch_size
|
|
142
|
+
|
|
143
|
+
if size <= 0:
|
|
144
|
+
raise ValidationError("Batch size must be positive", field="batch_size", value=str(size))
|
|
145
|
+
|
|
146
|
+
if size > max_allowed:
|
|
147
|
+
return max_allowed
|
|
148
|
+
|
|
149
|
+
return size
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def validate_type_name(type_name: str) -> str:
|
|
153
|
+
"""Validate data type name.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
type_name: Type name to validate
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Normalized type name (uppercase)
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValidationError: If type name is invalid
|
|
163
|
+
"""
|
|
164
|
+
if not type_name:
|
|
165
|
+
raise ValidationError("Type name is required", field="type_name")
|
|
166
|
+
|
|
167
|
+
return type_name.upper().strip()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def validate_function_name(func_name: str) -> str:
|
|
171
|
+
"""Validate function name.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
func_name: Function name to validate
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Normalized function name (uppercase)
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ValidationError: If function name is invalid
|
|
181
|
+
"""
|
|
182
|
+
if not func_name:
|
|
183
|
+
raise ValidationError("Function name is required", field="function_name")
|
|
184
|
+
|
|
185
|
+
return func_name.upper().strip()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def sanitize_table_name(name: str) -> str:
|
|
189
|
+
"""Sanitize table name for safe use in SQL.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
name: Table name to sanitize
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Sanitized table name
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ValidationError: If name contains invalid characters
|
|
199
|
+
"""
|
|
200
|
+
if not name:
|
|
201
|
+
raise ValidationError("Table name is required", field="table_name")
|
|
202
|
+
|
|
203
|
+
# Allow only alphanumeric, underscore, and dot (for schema.table)
|
|
204
|
+
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_\.]*$', name):
|
|
205
|
+
raise ValidationError(
|
|
206
|
+
"Table name contains invalid characters",
|
|
207
|
+
field="table_name",
|
|
208
|
+
value=name
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return name
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def sanitize_column_name(name: str) -> str:
|
|
215
|
+
"""Sanitize column name for safe use in SQL.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
name: Column name to sanitize
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Sanitized column name
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
ValidationError: If name is invalid
|
|
225
|
+
"""
|
|
226
|
+
if not name:
|
|
227
|
+
raise ValidationError("Column name is required", field="column_name")
|
|
228
|
+
|
|
229
|
+
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name):
|
|
230
|
+
raise ValidationError(
|
|
231
|
+
"Column name contains invalid characters",
|
|
232
|
+
field="column_name",
|
|
233
|
+
value=name
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return name
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def split_sql_statements(sql: str, dialect: str = "postgres") -> list[str]:
|
|
243
|
+
"""Split SQL text into individual statements, preserving original text exactly.
|
|
244
|
+
|
|
245
|
+
Pure lexical splitter — no AST parsing, no re-rendering, no semantic analysis.
|
|
246
|
+
Finds statement boundaries by tracking quote/comment states so semicolons
|
|
247
|
+
inside literals/comments are not mistaken for separators.
|
|
248
|
+
|
|
249
|
+
Handles:
|
|
250
|
+
- Single-quoted strings with escaped quotes ('')
|
|
251
|
+
- Double-quoted identifiers
|
|
252
|
+
- Backtick identifiers (MySQL)
|
|
253
|
+
- Line comments (-- comment\n)
|
|
254
|
+
- Block comments (/* ... */)
|
|
255
|
+
- PostgreSQL dollar-quoted strings ($$...$$ or $tag$...$tag$)
|
|
256
|
+
- Empty statements and trailing semicolons filtered out
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
sql: SQL text potentially containing multiple statements
|
|
260
|
+
dialect: SQL dialect hint (not used for splitting logic)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
List of individual SQL statement strings (original text preserved)
|
|
264
|
+
"""
|
|
265
|
+
if not sql or not sql.strip():
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
statements: list[str] = []
|
|
269
|
+
stmt_chars: list[str] = []
|
|
270
|
+
i = 0
|
|
271
|
+
n = len(sql)
|
|
272
|
+
in_single = False
|
|
273
|
+
in_double = False
|
|
274
|
+
in_backtick = False
|
|
275
|
+
|
|
276
|
+
# Sentinel values to distinguish "inside $$" from "not in dollar quote"
|
|
277
|
+
_IN_DOLLAR_EMPTY = object() # inside $$...$$
|
|
278
|
+
_NOT_IN_DOLLAR = None # not inside any dollar quote
|
|
279
|
+
dollar_tag = _NOT_IN_DOLLAR
|
|
280
|
+
|
|
281
|
+
while i < n:
|
|
282
|
+
ch = sql[i]
|
|
283
|
+
|
|
284
|
+
# ── Dollar-quoted string content ──
|
|
285
|
+
if dollar_tag is not _NOT_IN_DOLLAR:
|
|
286
|
+
# We're inside a dollar-quoted string
|
|
287
|
+
if dollar_tag is _IN_DOLLAR_EMPTY:
|
|
288
|
+
# Looking for closing $$
|
|
289
|
+
if sql[i:i+2] == "$$":
|
|
290
|
+
stmt_chars.append("$$")
|
|
291
|
+
i += 2
|
|
292
|
+
dollar_tag = _NOT_IN_DOLLAR
|
|
293
|
+
else:
|
|
294
|
+
stmt_chars.append(ch)
|
|
295
|
+
i += 1
|
|
296
|
+
continue
|
|
297
|
+
else:
|
|
298
|
+
# Looking for closing $tag$
|
|
299
|
+
end_tag = "$" + dollar_tag + "$"
|
|
300
|
+
if sql[i:i + len(end_tag)] == end_tag:
|
|
301
|
+
stmt_chars.append(end_tag)
|
|
302
|
+
i += len(end_tag)
|
|
303
|
+
dollar_tag = _NOT_IN_DOLLAR
|
|
304
|
+
else:
|
|
305
|
+
stmt_chars.append(ch)
|
|
306
|
+
i += 1
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# ── Start of dollar-quoted string ──
|
|
310
|
+
if ch == "$" and not in_single and not in_double and not in_backtick:
|
|
311
|
+
# Check for $$ (empty tag)
|
|
312
|
+
if i + 1 < n and sql[i + 1] == "$":
|
|
313
|
+
stmt_chars.append("$$")
|
|
314
|
+
dollar_tag = _IN_DOLLAR_EMPTY
|
|
315
|
+
i += 2
|
|
316
|
+
continue
|
|
317
|
+
# Check for $tag$ format
|
|
318
|
+
if i + 1 < n:
|
|
319
|
+
next_ch = sql[i + 1]
|
|
320
|
+
if next_ch.isalnum() or next_ch == "_":
|
|
321
|
+
j = i + 1
|
|
322
|
+
while j < n and sql[j] != "$":
|
|
323
|
+
j += 1
|
|
324
|
+
if j < n and sql[j] == "$":
|
|
325
|
+
tag = sql[i + 1:j]
|
|
326
|
+
if tag:
|
|
327
|
+
# Append the opening $tag$ to preserve it
|
|
328
|
+
stmt_chars.append("$" + tag + "$")
|
|
329
|
+
dollar_tag = tag
|
|
330
|
+
i = j + 1
|
|
331
|
+
continue
|
|
332
|
+
# Not a dollar-quote start
|
|
333
|
+
stmt_chars.append(ch)
|
|
334
|
+
i += 1
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
# ── Single-quoted strings ──
|
|
338
|
+
if ch == "'" and not in_double and not in_backtick:
|
|
339
|
+
if i + 1 < n and sql[i + 1] == "'":
|
|
340
|
+
stmt_chars.append("''")
|
|
341
|
+
i += 2
|
|
342
|
+
continue
|
|
343
|
+
in_single = not in_single
|
|
344
|
+
stmt_chars.append(ch)
|
|
345
|
+
i += 1
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
# ── Double-quoted identifiers ──
|
|
349
|
+
if ch == '"' and not in_single and not in_backtick:
|
|
350
|
+
in_double = not in_double
|
|
351
|
+
stmt_chars.append(ch)
|
|
352
|
+
i += 1
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
# ── Backtick identifiers (MySQL) ──
|
|
356
|
+
if ch == "`" and not in_single and not in_double:
|
|
357
|
+
in_backtick = not in_backtick
|
|
358
|
+
stmt_chars.append(ch)
|
|
359
|
+
i += 1
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
# ── Line comments ──
|
|
363
|
+
if ch == "-" and i + 1 < n and sql[i + 1] == "-":
|
|
364
|
+
stmt_chars.append(ch)
|
|
365
|
+
i += 1
|
|
366
|
+
while i < n and sql[i] != "\n":
|
|
367
|
+
stmt_chars.append(sql[i])
|
|
368
|
+
i += 1
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
# ── Block comments ──
|
|
372
|
+
if ch == "/" and i + 1 < n and sql[i + 1] == "*":
|
|
373
|
+
stmt_chars.append("/*")
|
|
374
|
+
i += 2
|
|
375
|
+
while i + 1 < n:
|
|
376
|
+
if sql[i] == "*" and sql[i + 1] == "/":
|
|
377
|
+
stmt_chars.append("*/")
|
|
378
|
+
i += 2
|
|
379
|
+
break
|
|
380
|
+
stmt_chars.append(sql[i])
|
|
381
|
+
i += 1
|
|
382
|
+
else:
|
|
383
|
+
# Unterminated block comment
|
|
384
|
+
while i < n:
|
|
385
|
+
stmt_chars.append(sql[i])
|
|
386
|
+
i += 1
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
# ── Statement terminator ──
|
|
390
|
+
if ch == ";" and not in_single and not in_double and not in_backtick:
|
|
391
|
+
statement = "".join(stmt_chars).strip()
|
|
392
|
+
if statement:
|
|
393
|
+
statements.append(statement)
|
|
394
|
+
stmt_chars = []
|
|
395
|
+
i += 1
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
# ── Normal character ──
|
|
399
|
+
stmt_chars.append(ch)
|
|
400
|
+
i += 1
|
|
401
|
+
|
|
402
|
+
# Last statement without trailing semicolon
|
|
403
|
+
last = "".join(stmt_chars).strip()
|
|
404
|
+
if last:
|
|
405
|
+
statements.append(last)
|
|
406
|
+
|
|
407
|
+
return statements
|
frontend/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""SQL Dialect Master v2 — Frontend package.
|
|
2
|
+
|
|
3
|
+
Public exports for the v2 frontend architecture.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from frontend.core.design_tokens import DARK, LIGHT, THEMES, ColorTokens
|
|
8
|
+
from frontend.core.styles import generate_css
|
|
9
|
+
|
|
10
|
+
__all__ = ["DARK", "LIGHT", "THEMES", "ColorTokens", "generate_css"]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""App context for SQL Dialect Master v2 — the thin adapter layer between
|
|
2
|
+
frontend and backend that returns typed ViewModels instead of raw dicts.
|
|
3
|
+
|
|
4
|
+
All SQL conversion goes through the canonical SQLTranspiler to ensure
|
|
5
|
+
consistent security validation, stacked-statement detection, and
|
|
6
|
+
post-processing behavior.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
import streamlit as st
|
|
13
|
+
|
|
14
|
+
from backend.core.nl2sql import NL2SQLGenerator
|
|
15
|
+
from backend.core.nl2sql_legacy import NL2SQLResult
|
|
16
|
+
from backend.core.transpiler import SQLTranspiler, TranspileResult
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Singleton transpiler instance
|
|
21
|
+
_transpiler = SQLTranspiler()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def convert_sql(sql: str, src: str, tgt: str) -> TranspileResult:
|
|
25
|
+
"""Convert SQL through the canonical SQLTranspiler.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
sql: SQL statement to convert
|
|
29
|
+
src: Source dialect
|
|
30
|
+
tgt: Target dialect
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
TranspileResult from the backend
|
|
34
|
+
"""
|
|
35
|
+
return _transpiler.transpile(sql, src, tgt, pretty=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def batch_convert_sql(
|
|
39
|
+
statements: list[str], src: str, tgt: str, pretty: bool = True
|
|
40
|
+
) -> list[TranspileResult]:
|
|
41
|
+
"""Batch convert multiple SQL statements.
|
|
42
|
+
|
|
43
|
+
Uses the canonical batch API which properly handles semicolons in
|
|
44
|
+
strings, comments, and dollar-quoted literals.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
statements: List of SQL statement strings
|
|
48
|
+
src: Source dialect
|
|
49
|
+
tgt: Target dialect
|
|
50
|
+
pretty: Pretty-print output
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of TranspileResult objects
|
|
54
|
+
"""
|
|
55
|
+
return _transpiler.batch_transpile(statements, src, tgt, pretty=pretty)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def format_sql_local(sql: str, dialect: str) -> str:
|
|
59
|
+
"""Format SQL by round-tripping through the transpiler.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
sql: SQL to format
|
|
63
|
+
dialect: SQL dialect
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Formatted SQL or original if formatting fails
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
result = _transpiler.transpile(sql, dialect, dialect, pretty=True, validate=False)
|
|
70
|
+
if result.success:
|
|
71
|
+
return result.target_sql or sql
|
|
72
|
+
return sql
|
|
73
|
+
except Exception: # noqa: BLE001 — transpiler already handles errors; this is a fallback
|
|
74
|
+
return sql
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def generate_nl2sql(nl: str, dialect: str, table_hint: str | None = None) -> NL2SQLResult:
|
|
78
|
+
"""Generate SQL from natural language.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
nl: Natural language description
|
|
82
|
+
dialect: Target dialect
|
|
83
|
+
table_hint: Optional table name hint
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
NL2SQLResult from the backend
|
|
87
|
+
"""
|
|
88
|
+
gen = NL2SQLGenerator(default_dialect=dialect)
|
|
89
|
+
return gen.generate(nl, dialect=dialect, table_hint=table_hint)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _stable_key(sql: str) -> str:
|
|
93
|
+
"""Process-independent widget key (str.hash is salted per-process)."""
|
|
94
|
+
import hashlib
|
|
95
|
+
|
|
96
|
+
return f"copy_result_{hashlib.sha1(sql.encode('utf-8')).hexdigest()[:8]}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _html_attr(text: str) -> str:
|
|
100
|
+
"""Escape text for a double-quoted HTML attribute value."""
|
|
101
|
+
return (
|
|
102
|
+
text.replace("&", "&")
|
|
103
|
+
.replace('"', """)
|
|
104
|
+
.replace("<", "<")
|
|
105
|
+
.replace(">", ">")
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def copy_sql(sql: str, key: str | None = None) -> None:
|
|
110
|
+
"""Copy SQL to the clipboard.
|
|
111
|
+
|
|
112
|
+
Uses Streamlit's native ``copy_button`` when available; otherwise falls
|
|
113
|
+
back to a small HTML snippet with a Web-Clipboard call so result panels
|
|
114
|
+
keep a working copy affordance.
|
|
115
|
+
"""
|
|
116
|
+
if hasattr(st, "copy_button"):
|
|
117
|
+
st.copy_button("Copy", key=key or _stable_key(sql), data=sql)
|
|
118
|
+
return
|
|
119
|
+
snippet = (
|
|
120
|
+
'<div style="display:flex;justify-content:flex-end;margin:4px 0;">'
|
|
121
|
+
'<button onclick="navigator.clipboard.writeText(this.dataset.sql).then('
|
|
122
|
+
'()=>{this.textContent=\'Copied\';setTimeout(()=>this.textContent=\'Copy\',1500)})" '
|
|
123
|
+
f'data-sql="{_html_attr(sql)}" '
|
|
124
|
+
'style="padding:4px 10px;font-size:11px;cursor:pointer;">Copy</button></div>'
|
|
125
|
+
)
|
|
126
|
+
st.markdown(snippet, unsafe_allow_html=True)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Static data
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
from backend.core.config import SUPPORTED_DIALECTS, get_dialect_ui_info
|
|
133
|
+
from backend.utils.validators import split_sql_statements
|
|
134
|
+
|
|
135
|
+
DIALECTS = SUPPORTED_DIALECTS
|
|
136
|
+
DIALECT_INFO = get_dialect_ui_info()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_dialect_label(dialect: str) -> str:
|
|
140
|
+
"""Get formatted dialect label with icon."""
|
|
141
|
+
info = DIALECT_INFO.get(dialect, {})
|
|
142
|
+
icon = info.get("icon", "📄")
|
|
143
|
+
return f"{icon} {dialect.upper()}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Static data loading
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
import json
|
|
150
|
+
from dataclasses import dataclass
|
|
151
|
+
from pathlib import Path
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class AppData:
|
|
156
|
+
"""Registry-injected static data for data-dependent pages.
|
|
157
|
+
|
|
158
|
+
The unified PageRenderer contract is ``Callable[[ColorTokens], None]`` —
|
|
159
|
+
a renderer receives only the active theme. Pages that need static
|
|
160
|
+
data (Functions / Types) read it from this container instead of a
|
|
161
|
+
positional argument, which keeps every PAGE_MAP entry callable with
|
|
162
|
+
the same signature.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
types_data: dict
|
|
166
|
+
funcs_data: dict
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
_app_data: AppData | None = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@st.cache_data
|
|
173
|
+
def load_data_v2() -> tuple[dict, dict]:
|
|
174
|
+
"""Load static data (types, functions) with caching.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Tuple of (types_data, funcs_data)
|
|
178
|
+
"""
|
|
179
|
+
base = Path(__file__).parent.parent / "backend" / "core"
|
|
180
|
+
types_path = base / "type_mapping.json"
|
|
181
|
+
funcs_path = base / "functions_db.json"
|
|
182
|
+
|
|
183
|
+
if not types_path.exists():
|
|
184
|
+
raise FileNotFoundError(f"Missing runtime data file: {types_path}")
|
|
185
|
+
if not funcs_path.exists():
|
|
186
|
+
raise FileNotFoundError(f"Missing runtime data file: {funcs_path}")
|
|
187
|
+
|
|
188
|
+
types = json.loads(types_path.read_text(encoding="utf-8"))
|
|
189
|
+
funcs = json.loads(funcs_path.read_text(encoding="utf-8"))
|
|
190
|
+
return types, funcs
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_page_data() -> AppData:
|
|
194
|
+
"""Return the lazily loaded static data container.
|
|
195
|
+
|
|
196
|
+
Loaded once per process; Streamlit reruns share it.
|
|
197
|
+
"""
|
|
198
|
+
global _app_data
|
|
199
|
+
if _app_data is None:
|
|
200
|
+
_app_data = AppData(*load_data_v2())
|
|
201
|
+
return _app_data
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Shared imports for the frontend v2 architecture."""
|
|
2
|
+
|
|
3
|
+
from .design_tokens import (
|
|
4
|
+
DARK,
|
|
5
|
+
LIGHT,
|
|
6
|
+
SEMANTIC_STATUS_COLOR,
|
|
7
|
+
SEMANTIC_STATUS_LABELS,
|
|
8
|
+
STATUS_ERROR,
|
|
9
|
+
STATUS_INFO,
|
|
10
|
+
STATUS_NEUTRAL,
|
|
11
|
+
STATUS_VALID,
|
|
12
|
+
STATUS_WARNING,
|
|
13
|
+
THEMES,
|
|
14
|
+
ColorTokens,
|
|
15
|
+
ThemeName,
|
|
16
|
+
)
|
|
17
|
+
from .styles import generate_css
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DARK",
|
|
21
|
+
"LIGHT",
|
|
22
|
+
"SEMANTIC_STATUS_COLOR",
|
|
23
|
+
"SEMANTIC_STATUS_LABELS",
|
|
24
|
+
"STATUS_ERROR",
|
|
25
|
+
"STATUS_INFO",
|
|
26
|
+
"STATUS_NEUTRAL",
|
|
27
|
+
"STATUS_VALID",
|
|
28
|
+
"STATUS_WARNING",
|
|
29
|
+
"THEMES",
|
|
30
|
+
"ColorTokens",
|
|
31
|
+
"ThemeName",
|
|
32
|
+
"generate_css",
|
|
33
|
+
]
|