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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env python3
2
+ """Utility modules for SQL Dialect Master."""
@@ -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("&", "&amp;")
103
+ .replace('"', "&quot;")
104
+ .replace("<", "&lt;")
105
+ .replace(">", "&gt;")
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
+ ]