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
frontend/ui/diff.py ADDED
@@ -0,0 +1,275 @@
1
+ """Semantic Diff UI components for SQL Dialect Master v2.
2
+
3
+ Renders the finding list, finding detail/inspector, and text diff view.
4
+ All data comes from SemanticDiffViewModel — no direct backend access.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import streamlit as st
9
+
10
+ from frontend.core.design_tokens import ColorTokens
11
+ from frontend.core.escaping import esc
12
+ from frontend.core.viewmodels import (
13
+ SemanticDiffViewModel,
14
+ SemanticFindingViewModel,
15
+ )
16
+
17
+
18
+ def render_semantic_diff_workspace(
19
+ vm: SemanticDiffViewModel,
20
+ source_sql: str,
21
+ target_sql: str,
22
+ theme: ColorTokens,
23
+ selected_finding_index: int | None = None,
24
+ ) -> None:
25
+ """Render the full Semantic Diff workspace with finding list and inspector.
26
+
27
+ Args:
28
+ vm: SemanticDiffViewModel from backend
29
+ source_sql: Raw source SQL for display
30
+ target_sql: Raw target SQL for display
31
+ theme: Current theme tokens
32
+ selected_finding_index: Index of selected finding (for inspector)
33
+ """
34
+ _render_overall_status(vm, theme)
35
+
36
+ if vm.parse_error:
37
+ st.error(f"Parse error: {esc(vm.parse_error)}")
38
+ return
39
+
40
+ col_findings, col_inspector = st.columns([2, 1])
41
+
42
+ with col_findings:
43
+ _render_finding_list(vm, selected_finding_index, theme)
44
+
45
+ with col_inspector:
46
+ if selected_finding_index is not None and 0 <= selected_finding_index < len(vm.findings):
47
+ _render_finding_inspector(vm.findings[selected_finding_index], theme)
48
+ else:
49
+ _render_inspector_placeholder(theme)
50
+
51
+ # Tabs for Semantic vs Text diff
52
+ st.markdown("---")
53
+ _render_diff_tabs(vm, source_sql, target_sql, theme)
54
+
55
+
56
+ def _render_overall_status(vm: SemanticDiffViewModel, theme: ColorTokens) -> None:
57
+ """Render the overall semantic classification banner."""
58
+ severity_colors = {
59
+ "valid": theme.success,
60
+ "warning": theme.warning,
61
+ "error": theme.danger,
62
+ "info": theme.info,
63
+ "neutral": theme.text_muted,
64
+ }
65
+ color = severity_colors.get(vm.overall_status, theme.text_muted)
66
+
67
+ # Finding summary
68
+ summary_parts = []
69
+ if vm.error_count:
70
+ summary_parts.append(f"{vm.error_count} error{'s' if vm.error_count > 1 else ''}")
71
+ if vm.warning_count:
72
+ summary_parts.append(f"{vm.warning_count} warning{'s' if vm.warning_count > 1 else ''}")
73
+ summary = " · ".join(summary_parts) if summary_parts else "No findings"
74
+
75
+ st.markdown(
76
+ f"""
77
+ <div style="display:flex; align-items:center; justify-content:space-between;
78
+ padding:12px 16px; background:{color}12; border:1px solid {color}30;
79
+ border-radius:6px; margin-bottom:16px;">
80
+ <div>
81
+ <div style="font-size:11px; font-weight:600; letter-spacing:0.06em;
82
+ text-transform:uppercase; color:{color};">
83
+ {esc(vm.classification_label)}
84
+ </div>
85
+ <div style="font-size:12px; color:{theme.text_secondary}; margin-top:2px;">
86
+ {esc(vm.source_dialect)} → {esc(vm.target_dialect)} · {summary}
87
+ </div>
88
+ </div>
89
+ <div style="text-align:right;">
90
+ <div style="font-size:11px; color:{theme.text_muted};">confidence</div>
91
+ <div style="font-size:18px; font-weight:700; color:{color}; font-family:monospace;">
92
+ {vm.confidence:.0%}
93
+ </div>
94
+ </div>
95
+ </div>
96
+ """,
97
+ unsafe_allow_html=True,
98
+ )
99
+
100
+
101
+ def _render_finding_list(vm: SemanticDiffViewModel, selected_idx: int | None, theme: ColorTokens) -> None:
102
+ """Render the findings list."""
103
+ st.markdown(
104
+ f'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
105
+ f'text-transform:uppercase; color:{theme.text_muted}; margin-bottom:8px;">'
106
+ f'Findings ({vm.finding_count})</div>',
107
+ unsafe_allow_html=True,
108
+ )
109
+
110
+ if not vm.findings:
111
+ st.info("No semantic differences detected.")
112
+ return
113
+
114
+ for i, finding in enumerate(vm.findings):
115
+ is_selected = i == selected_idx
116
+ bg = theme.selected_bg if is_selected else theme.surface
117
+ border_color = theme.accent if is_selected else theme.border
118
+ icon = "●" if finding.severity == "error" else "○"
119
+ icon_color = theme.danger if finding.severity == "error" else theme.warning
120
+
121
+ st.markdown(
122
+ f"""
123
+ <div class="sdm-finding-item" data-index="{i}"
124
+ style="padding:10px 12px; margin-bottom:4px; background:{bg};
125
+ border:1px solid {border_color}; border-radius:6px;
126
+ cursor:pointer; transition:all 0.15s;"
127
+ onmouseover="this.style.background='{theme.hover_bg}'"
128
+ onmouseout="this.style.background='{bg}'">
129
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
130
+ <span style="color:{icon_color}; font-size:10px;">{icon}</span>
131
+ <span style="font-size:12px; font-weight:600; color:{theme.text_primary};">
132
+ {esc(finding.category_label)}
133
+ </span>
134
+ <span style="margin-left:auto; font-size:10px; font-weight:600;
135
+ padding:2px 6px; border-radius:3px;
136
+ background:{theme.danger}15; color:{theme.danger};">
137
+ {esc(finding.severity_label)}
138
+ </span>
139
+ </div>
140
+ <div style="font-size:11px; color:{theme.text_secondary}; line-height:1.4;">
141
+ {esc(finding.explanation[:100])}
142
+ {'...' if len(finding.explanation) > 100 else ''}
143
+ </div>
144
+ </div>
145
+ """,
146
+ unsafe_allow_html=True,
147
+ )
148
+
149
+ if st.button("Select", key=f"find_{i}", help=f"View {finding.category} finding details"):
150
+ st.session_state.sdm_selected_finding_index = i
151
+ st.rerun()
152
+
153
+
154
+ def _render_finding_inspector(finding: SemanticFindingViewModel, theme: ColorTokens) -> None:
155
+ """Render the detail inspector for a selected finding."""
156
+ st.markdown(
157
+ f'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
158
+ f'text-transform:uppercase; color:{theme.text_muted}; margin-bottom:12px;">'
159
+ f'Finding #{finding.index + 1} · {esc(finding.category_label)}</div>',
160
+ unsafe_allow_html=True,
161
+ )
162
+
163
+ # Severity badge
164
+ sev_color = theme.danger if finding.severity == "error" else theme.warning
165
+ st.markdown(
166
+ f'<div style="display:inline-block; padding:3px 8px; border-radius:3px; '
167
+ f'background:{sev_color}15; color:{sev_color}; font-size:11px; font-weight:600; '
168
+ f'letter-spacing:0.05em; margin-bottom:12px;">'
169
+ f'{esc(finding.severity_label)}</div>',
170
+ unsafe_allow_html=True,
171
+ )
172
+
173
+ # Explanation
174
+ st.markdown(
175
+ '<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
176
+ 'text-transform:uppercase; color:' + theme.text_muted + '; margin:12px 0 6px;">'
177
+ 'Explanation</div>',
178
+ unsafe_allow_html=True,
179
+ )
180
+ st.markdown(f'<div style="font-size:12px; color:{theme.text_primary}; line-height:1.6;">'
181
+ f'{esc(finding.explanation)}</div>', unsafe_allow_html=True)
182
+
183
+ # Source fragment
184
+ st.markdown(
185
+ '<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
186
+ 'text-transform:uppercase; color:' + theme.text_muted + '; margin:12px 0 6px;">'
187
+ 'Source Fragment</div>',
188
+ unsafe_allow_html=True,
189
+ )
190
+ if finding.source_fragment:
191
+ st.code(finding.source_fragment, language="sql")
192
+ else:
193
+ st.caption("Not available")
194
+
195
+ # Target fragment
196
+ st.markdown(
197
+ '<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
198
+ 'text-transform:uppercase; color:' + theme.text_muted + '; margin:12px 0 6px;">'
199
+ 'Target Fragment</div>',
200
+ unsafe_allow_html=True,
201
+ )
202
+ if finding.target_fragment:
203
+ st.code(finding.target_fragment, language="sql")
204
+ else:
205
+ st.caption("Not available")
206
+
207
+ # Evidence
208
+ if finding.evidence:
209
+ st.markdown(
210
+ '<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
211
+ 'text-transform:uppercase; color:' + theme.text_muted + '; margin:12px 0 6px;">'
212
+ 'Evidence</div>',
213
+ unsafe_allow_html=True,
214
+ )
215
+ st.caption(f"`{esc(finding.evidence)}`")
216
+
217
+
218
+ def _render_inspector_placeholder(theme: ColorTokens) -> None:
219
+ """Render the empty inspector state."""
220
+ st.markdown(
221
+ f"""
222
+ <div style="padding:24px; border:1px dashed {theme.border}; border-radius:6px;
223
+ text-align:center; color:{theme.text_muted};">
224
+ <div style="font-size:12px;">Select a finding to inspect details</div>
225
+ </div>
226
+ """,
227
+ unsafe_allow_html=True,
228
+ )
229
+
230
+
231
+ def _render_diff_tabs(
232
+ vm: SemanticDiffViewModel,
233
+ source_sql: str,
234
+ target_sql: str,
235
+ theme: ColorTokens,
236
+ ) -> None:
237
+ """Render Semantic / Text diff tab switcher."""
238
+ tab_sem, tab_txt = st.tabs(["Semantic", "Text"])
239
+
240
+ with tab_sem:
241
+ # Already rendered above — show source/target side by side
242
+ col_s, col_t = st.columns(2)
243
+ with col_s:
244
+ st.markdown(
245
+ '<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
246
+ 'text-transform:uppercase; color:#' + theme.text_muted[1:] + '; margin-bottom:6px;">'
247
+ 'Source</div>',
248
+ unsafe_allow_html=True,
249
+ )
250
+ st.code(source_sql, language="sql")
251
+ with col_t:
252
+ st.markdown(
253
+ '<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
254
+ 'text-transform:uppercase; color:#' + theme.text_muted[1:] + '; margin-bottom:6px;">'
255
+ 'Target</div>',
256
+ unsafe_allow_html=True,
257
+ )
258
+ st.code(target_sql, language="sql")
259
+
260
+ with tab_txt:
261
+ import difflib
262
+ diff_lines = list(
263
+ difflib.unified_diff(
264
+ source_sql.splitlines(),
265
+ target_sql.splitlines(),
266
+ fromfile=f"Source ({esc(vm.source_dialect)})",
267
+ tofile=f"Target ({esc(vm.target_dialect)})",
268
+ lineterm="",
269
+ )
270
+ )
271
+ if diff_lines:
272
+ diff_text = "\n".join(diff_lines)
273
+ st.code(diff_text, language="diff")
274
+ else:
275
+ st.success("No textual differences found.")
frontend/ui/editor.py ADDED
@@ -0,0 +1,102 @@
1
+ """SQL editor component for SQL Dialect Master v2.
2
+
3
+ Provides a styled text area with monospace font, line-number-friendly
4
+ layout, and consistent styling across the application.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import streamlit as st
9
+
10
+ from frontend.core.design_tokens import ColorTokens
11
+ from frontend.core.escaping import esc
12
+
13
+
14
+ def render_sql_editor(
15
+ key: str,
16
+ label: str,
17
+ value: str = "",
18
+ height: int = 240,
19
+ dialect_key: str | None = None,
20
+ placeholder: str = "Enter SQL query...",
21
+ theme: ColorTokens | None = None,
22
+ disabled: bool = False,
23
+ ) -> str:
24
+ """Render a styled SQL editor text area.
25
+
26
+ Args:
27
+ key: Streamlit widget key
28
+ label: Display label above the editor
29
+ value: Initial SQL text
30
+ height: Editor height in pixels
31
+ dialect_key: Optional key for dialect selector (shown alongside label)
32
+ placeholder: Placeholder text
33
+ theme: Current theme tokens (uses DARK default if None)
34
+ disabled: Whether the editor is read-only
35
+
36
+ Returns:
37
+ The SQL text entered by the user (read from session state)
38
+ """
39
+ if theme is None:
40
+ from frontend.core.design_tokens import DARK
41
+ theme = DARK
42
+
43
+ # Build label HTML
44
+ label_html = f'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; ' \
45
+ f'text-transform:uppercase; color:{theme.text_muted}; margin-bottom:6px;">' \
46
+ f'{esc(label)}</div>'
47
+
48
+ # Dialect badge (if provided)
49
+ dialect_badge = ""
50
+ if dialect_key and dialect_key in st.session_state:
51
+ dialect_val = esc(str(st.session_state[dialect_key]))
52
+ dialect_badge = (
53
+ f'<span style="font-family:monospace; font-size:11px; color:{theme.accent}; '
54
+ f'background:{theme.selected_bg}; padding:2px 6px; border-radius:3px; '
55
+ f'border:1px solid {theme.border}; margin-left:8px;">{dialect_val}</span>'
56
+ )
57
+
58
+ st.markdown(f"{label_html}{dialect_badge}")
59
+
60
+ sql = st.text_area(
61
+ key=key,
62
+ value=value,
63
+ height=height,
64
+ placeholder=placeholder,
65
+ label_visibility="collapsed",
66
+ disabled=disabled,
67
+ horizontal=True,
68
+ )
69
+
70
+ return sql
71
+
72
+
73
+ def render_code_display(sql: str, language: str = "sql", theme: ColorTokens | None = None) -> None:
74
+ """Render SQL code in a styled code block.
75
+
76
+ Args:
77
+ sql: SQL text to display
78
+ language: Syntax highlighting language
79
+ theme: Current theme tokens
80
+ """
81
+ if theme is None:
82
+ from frontend.core.design_tokens import DARK
83
+ theme = DARK
84
+
85
+ st.markdown(
86
+ f"""
87
+ <div style="border:1px solid {theme.border}; border-radius:6px; overflow:hidden;
88
+ background:{theme.input_bg};">
89
+ <div style="display:flex; align-items:center; justify-content:space-between;
90
+ padding:6px 10px; background:{theme.surface}; border-bottom:1px solid {theme.border};
91
+ font-size:11px; font-weight:600; letter-spacing:0.06em; text-transform:uppercase;
92
+ color:{theme.text_muted};">
93
+ <span>SQL</span>
94
+ <span style="font-family:monospace; color:{theme.accent};">{esc(language)}</span>
95
+ </div>
96
+ <pre style="margin:0; padding:12px; background:{theme.input_bg};
97
+ color:{theme.text_primary}; font-family:monospace; font-size:13px;
98
+ line-height:1.6; white-space:pre; overflow:auto;">{esc(sql)}</pre>
99
+ </div>
100
+ """,
101
+ unsafe_allow_html=True,
102
+ )
frontend/ui/status.py ADDED
@@ -0,0 +1,255 @@
1
+ """Status and result display components for SQL Dialect Master v2."""
2
+ from __future__ import annotations
3
+
4
+ import streamlit as st
5
+
6
+ from frontend.app_context_v2 import copy_sql
7
+ from frontend.core.design_tokens import ColorTokens
8
+ from frontend.core.escaping import esc
9
+
10
+
11
+ def render_conversion_status(
12
+ status: str,
13
+ label: str,
14
+ detail: str | None = None,
15
+ theme: ColorTokens | None = None,
16
+ ) -> None:
17
+ """Render a single status indicator row.
18
+
19
+ Args:
20
+ status: One of 'valid', 'warning', 'error', 'info', 'neutral'
21
+ label: Status label text
22
+ detail: Optional detail text
23
+ theme: Current theme tokens
24
+ """
25
+ if theme is None:
26
+ from frontend.core.design_tokens import DARK
27
+ theme = DARK
28
+
29
+ colors = {
30
+ "valid": theme.success,
31
+ "warning": theme.warning,
32
+ "error": theme.danger,
33
+ "info": theme.info,
34
+ "neutral": theme.text_muted,
35
+ }
36
+ dots = {"valid": "●", "warning": "●", "error": "●", "info": "●", "neutral": "●"}
37
+ dot_color = colors.get(status, theme.text_muted)
38
+ dot = dots.get(status, "●")
39
+
40
+ detail_html = f'<span style="color:{theme.text_secondary}; font-size:12px;"> — {esc(detail)}</span>' if detail else ""
41
+
42
+ st.markdown(
43
+ f"""
44
+ <div style="display:flex; align-items:center; gap:8px; padding:6px 0;">
45
+ <span style="color:{dot_color}; font-size:10px;">{dot}</span>
46
+ <span style="font-size:12px; color:{theme.text_primary};">{esc(label)}</span>
47
+ {detail_html}
48
+ </div>
49
+ """,
50
+ unsafe_allow_html=True,
51
+ )
52
+
53
+
54
+ def render_result_panel(
55
+ sql: str,
56
+ source_dialect: str,
57
+ target_dialect: str,
58
+ warnings: list[str],
59
+ transformations: list[str],
60
+ compatibility_notes: list[str],
61
+ error_message: str | None,
62
+ semantic_label: str,
63
+ semantic_status: str,
64
+ theme: ColorTokens | None = None,
65
+ target_validation_state: str = "target_valid",
66
+ ) -> None:
67
+ """Render the full conversion result panel.
68
+
69
+ Args:
70
+ sql: Converted SQL
71
+ source_dialect: Source dialect name
72
+ target_dialect: Target dialect name
73
+ warnings: List of warning strings
74
+ transformations: List of transformation descriptions
75
+ compatibility_notes: List of compatibility notes
76
+ error_message: Error message if conversion failed
77
+ semantic_label: Semantic status label (e.g. "Equivalent")
78
+ semantic_status: Semantic status key for color
79
+ theme: Current theme tokens
80
+ """
81
+ if theme is None:
82
+ from frontend.core.design_tokens import DARK
83
+ theme = DARK
84
+
85
+ # Status badge
86
+ status_colors = {
87
+ "valid": (theme.success, f"background:{theme.success}15; color:{theme.success}; border-color:{theme.success}40;"),
88
+ "error": (theme.danger, f"background:{theme.danger}15; color:{theme.danger}; border-color:{theme.danger}40;"),
89
+ "warning": (theme.warning, f"background:{theme.warning}15; color:{theme.warning}; border-color:{theme.warning}40;"),
90
+ }
91
+ badge_style = status_colors.get(semantic_status, (theme.text_muted, f"background:{theme.hover_bg}; color:{theme.text_muted}; border-color:{theme.border};"))[1]
92
+ badge_text = esc(semantic_label) if semantic_status != "pending" else "Pending"
93
+
94
+ if error_message:
95
+ # Error state
96
+ st.markdown(
97
+ f"""
98
+ <div style="padding:12px 14px; background:{theme.danger}12; border:1px solid {theme.danger}40;
99
+ border-radius:6px; margin-bottom:16px;">
100
+ <div style="font-size:12px; font-weight:600; color:{theme.danger}; margin-bottom:4px;">
101
+ Conversion Failed
102
+ </div>
103
+ <div style="font-size:12px; color:{theme.text_secondary};">{esc(error_message)}</div>
104
+ </div>
105
+ """,
106
+ unsafe_allow_html=True,
107
+ )
108
+ return
109
+
110
+ # Success state with result panel
111
+ st.markdown(
112
+ f"""
113
+ <div style="display:flex; align-items:center; justify-content:space-between;
114
+ margin-bottom:12px;">
115
+ <div style="font-size:12px; font-weight:600; letter-spacing:0.06em;
116
+ text-transform:uppercase; color:{theme.text_muted};">
117
+ Conversion Result
118
+ </div>
119
+ <span style="font-size:11px; font-weight:600; padding:3px 8px; border-radius:4px;
120
+ border:1px solid {theme.border}; {badge_style};">
121
+ {badge_text}
122
+ </span>
123
+ </div>
124
+ """,
125
+ unsafe_allow_html=True,
126
+ )
127
+
128
+ # SQL output
129
+ st.code(sql, language="sql")
130
+
131
+ # Action buttons
132
+ col_copy, col_dl, col_diff = st.columns([1, 1, 1])
133
+ with col_copy:
134
+ copy_sql(sql, key=_stable_copy_key(sql))
135
+ with col_dl:
136
+ st.download_button(
137
+ "Download",
138
+ sql,
139
+ "converted.sql",
140
+ mime="text/sql",
141
+ key="download_result_panel",
142
+ )
143
+ with col_diff:
144
+ if st.button("Semantic Diff", key="to_diff_btn"):
145
+ from frontend.core.navigation import create_navigation_intent
146
+ intent = create_navigation_intent(
147
+ target_page="diff",
148
+ action="open_diff",
149
+ source=sql,
150
+ target=sql,
151
+ src_dialect=source_dialect,
152
+ tgt_dialect=target_dialect,
153
+ )
154
+ st.session_state.sdm_navigation_intent = intent
155
+ st.rerun()
156
+
157
+ # Transformations
158
+ if transformations:
159
+ st.markdown(
160
+ f'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
161
+ f'text-transform:uppercase; color:{theme.text_muted}; margin:16px 0 8px;">'
162
+ f'Transformations</div>',
163
+ unsafe_allow_html=True,
164
+ )
165
+ for t in transformations:
166
+ st.caption(f"→ {esc(t)}")
167
+
168
+ # Warnings
169
+ if warnings:
170
+ st.markdown(
171
+ f'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
172
+ f'text-transform:uppercase; color:{theme.text_muted}; margin:16px 0 8px;">'
173
+ f'Warnings ({len(warnings)})</div>',
174
+ unsafe_allow_html=True,
175
+ )
176
+ for w in warnings:
177
+ st.warning(esc(w))
178
+
179
+ # Compatibility notes
180
+ if compatibility_notes:
181
+ st.markdown(
182
+ f'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
183
+ f'text-transform:uppercase; color:{theme.text_muted}; margin:16px 0 8px;">'
184
+ f'Compatibility Notes</div>',
185
+ unsafe_allow_html=True,
186
+ )
187
+ for note in compatibility_notes:
188
+ st.info(esc(note))
189
+
190
+ # Target-dialect validation state: a generic-only fallback must be
191
+ # visibly distinguished from a target-validated conversion.
192
+ if target_validation_state == "generic_only":
193
+ st.warning(
194
+ "Output was not accepted by the target-dialect parser and is "
195
+ "retained via generic-parser fallback. Verify it against the "
196
+ "target database before relying on it.",
197
+ )
198
+
199
+
200
+ def _stable_copy_key(sql: str) -> str:
201
+ """Process-independent copy-button key (str.hash is salted)."""
202
+ import hashlib
203
+
204
+ return f"copy_result_{hashlib.sha1(sql.encode('utf-8')).hexdigest()[:8]}"
205
+
206
+
207
+ def render_batch_result(
208
+ total: int,
209
+ succeeded: int,
210
+ failed: int,
211
+ items: list,
212
+ theme: ColorTokens | None = None,
213
+ ) -> None:
214
+ """Render batch conversion results.
215
+
216
+ Args:
217
+ total: Total statements processed
218
+ succeeded: Number of successful conversions
219
+ failed: Number of failed conversions
220
+ items: List of BatchResultItem view models
221
+ theme: Current theme tokens
222
+ """
223
+ if theme is None:
224
+ from frontend.core.design_tokens import DARK
225
+ theme = DARK
226
+
227
+ rate = succeeded / total if total else 0
228
+
229
+ st.markdown(
230
+ f"""
231
+ <div style="display:flex; align-items:center; gap:16px; margin-bottom:12px;">
232
+ <span style="font-size:13px; font-weight:500; color:{theme.text_primary};">
233
+ {succeeded}/{total} statements converted
234
+ </span>
235
+ <div style="flex:1; height:4px; background:{theme.border}; border-radius:2px; overflow:hidden;">
236
+ <div style="width:{rate*100}%; height:100%; background:{theme.success}; border-radius:2px;"></div>
237
+ </div>
238
+ {f'<span style="font-size:12px; color:{theme.danger};">{failed} failed</span>' if failed else ''}
239
+ </div>
240
+ """,
241
+ unsafe_allow_html=True,
242
+ )
243
+
244
+ for item in items:
245
+ if item.success:
246
+ with st.expander(f"Statement {item.statement_index} ✓", expanded=False):
247
+ st.code(item.converted_sql or "", language="sql")
248
+ if item.warnings:
249
+ for w in item.warnings:
250
+ st.caption(f"⚠ {esc(w)}")
251
+ else:
252
+ with st.expander(f"Statement {item.statement_index} ✗ Error", expanded=False):
253
+ st.error(f"Error: {esc(item.error or 'Unknown error')}")
254
+ st.caption("Original:")
255
+ st.code(item.original_sql, language="sql")