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
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")
|