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,283 @@
|
|
|
1
|
+
"""Convert Workspace for SQL Dialect Master v2.
|
|
2
|
+
|
|
3
|
+
This is the primary workspace — a professional SQL IDE-style interface
|
|
4
|
+
for dialect conversion with semantic analysis integration.
|
|
5
|
+
|
|
6
|
+
Fixes the batch conversion bug from v1: properly delegates statement
|
|
7
|
+
splitting to the canonical backend API.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
|
|
13
|
+
import streamlit as st
|
|
14
|
+
|
|
15
|
+
from frontend.app_context_v2 import (
|
|
16
|
+
DIALECTS,
|
|
17
|
+
batch_convert_sql,
|
|
18
|
+
convert_sql,
|
|
19
|
+
copy_sql,
|
|
20
|
+
format_sql_local,
|
|
21
|
+
split_sql_statements,
|
|
22
|
+
)
|
|
23
|
+
from frontend.core.design_tokens import (
|
|
24
|
+
ColorTokens,
|
|
25
|
+
)
|
|
26
|
+
from frontend.core.escaping import esc
|
|
27
|
+
from frontend.core.navigation import create_navigation_intent
|
|
28
|
+
from frontend.core.state import SessionState
|
|
29
|
+
from frontend.core.viewmodels import (
|
|
30
|
+
BatchConversionViewModel,
|
|
31
|
+
ConversionViewModel,
|
|
32
|
+
)
|
|
33
|
+
from frontend.ui.status import render_batch_result, render_result_panel
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def render_convert_page(theme: ColorTokens) -> None:
|
|
37
|
+
"""Render the Convert workspace page."""
|
|
38
|
+
state = SessionState.get()
|
|
39
|
+
|
|
40
|
+
# ── Toolbar ──────────────────────────────────────────────────────
|
|
41
|
+
_render_toolbar(theme)
|
|
42
|
+
|
|
43
|
+
# ── Dialect selectors + SQL editors ────────────────────────────
|
|
44
|
+
src_dialect = st.selectbox(
|
|
45
|
+
"Source dialect",
|
|
46
|
+
DIALECTS,
|
|
47
|
+
index=DIALECTS.index("postgres") if "postgres" in DIALECTS else 0,
|
|
48
|
+
key="convert_src",
|
|
49
|
+
label_visibility="collapsed",
|
|
50
|
+
)
|
|
51
|
+
tgt_dialect = st.selectbox(
|
|
52
|
+
"Target dialect",
|
|
53
|
+
DIALECTS,
|
|
54
|
+
index=DIALECTS.index("mysql") if "mysql" in DIALECTS else 0,
|
|
55
|
+
key="convert_tgt",
|
|
56
|
+
label_visibility="collapsed",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Swap button between selectors
|
|
60
|
+
col_swap, _ = st.columns([1, 10])
|
|
61
|
+
with col_swap:
|
|
62
|
+
if st.button("⇄", key="swap_dialects", help="Swap source and target dialects"):
|
|
63
|
+
st.session_state.convert_src, st.session_state.convert_tgt = st.session_state.convert_tgt, st.session_state.convert_src
|
|
64
|
+
st.rerun()
|
|
65
|
+
|
|
66
|
+
# ── SQL editors (two-panel) ────────────────────────────────────
|
|
67
|
+
col_src, col_tgt = st.columns([1, 1])
|
|
68
|
+
|
|
69
|
+
with col_src:
|
|
70
|
+
st.markdown(
|
|
71
|
+
f'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
72
|
+
f'text-transform:uppercase; color:{theme.text_muted}; margin-bottom:6px;">'
|
|
73
|
+
f'Source · {esc(src_dialect.upper())}</div>',
|
|
74
|
+
unsafe_allow_html=True,
|
|
75
|
+
)
|
|
76
|
+
src_sql = st.text_area(
|
|
77
|
+
"Source SQL",
|
|
78
|
+
key="convert_src_sql",
|
|
79
|
+
height=300,
|
|
80
|
+
placeholder="SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'",
|
|
81
|
+
label_visibility="collapsed",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
col_fmt, col_clear = st.columns([1, 1])
|
|
85
|
+
with col_fmt:
|
|
86
|
+
if st.button("Format", key="format_src", use_container_width=True, type="secondary"):
|
|
87
|
+
formatted = format_sql_local(src_sql, src_dialect)
|
|
88
|
+
st.session_state.convert_src_sql = formatted
|
|
89
|
+
st.rerun()
|
|
90
|
+
with col_clear:
|
|
91
|
+
if st.button("Clear", key="clear_src", use_container_width=True):
|
|
92
|
+
st.session_state.convert_src_sql = ""
|
|
93
|
+
st.rerun()
|
|
94
|
+
|
|
95
|
+
with col_tgt:
|
|
96
|
+
st.markdown(
|
|
97
|
+
f'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
98
|
+
f'text-transform:uppercase; color:{theme.text_muted}; margin-bottom:6px;">'
|
|
99
|
+
f'Target · {esc(tgt_dialect.upper())}</div>',
|
|
100
|
+
unsafe_allow_html=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Show converted SQL or placeholder
|
|
104
|
+
target_sql_display = state.convert_target_sql
|
|
105
|
+
if target_sql_display:
|
|
106
|
+
st.code(target_sql_display, language="sql")
|
|
107
|
+
col_copy, col_dl = st.columns([1, 1])
|
|
108
|
+
with col_copy:
|
|
109
|
+
copy_sql(target_sql_display, key="copy_tgt")
|
|
110
|
+
with col_dl:
|
|
111
|
+
st.download_button(
|
|
112
|
+
"Download",
|
|
113
|
+
target_sql_display,
|
|
114
|
+
"converted.sql",
|
|
115
|
+
mime="text/sql",
|
|
116
|
+
key="download_tgt_sql",
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
st.markdown(
|
|
120
|
+
f'<div style="border:1px dashed {theme.border}; border-radius:6px; '
|
|
121
|
+
f'padding:40px; text-align:center; color:{theme.text_muted};">'
|
|
122
|
+
f'Converted SQL will appear here</div>',
|
|
123
|
+
unsafe_allow_html=True,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# ── Convert button ─────────────────────────────────────────────
|
|
127
|
+
convert_clicked = st.button("Convert", key="convert_btn", type="primary", use_container_width=True)
|
|
128
|
+
|
|
129
|
+
if convert_clicked and src_sql.strip():
|
|
130
|
+
with st.spinner("Converting..."):
|
|
131
|
+
result = convert_sql(src_sql, src_dialect, tgt_dialect)
|
|
132
|
+
|
|
133
|
+
vm = ConversionViewModel.from_transpile_result(result)
|
|
134
|
+
state.convert_last_vm = vm
|
|
135
|
+
state.convert_target_sql = vm.target_sql or ""
|
|
136
|
+
|
|
137
|
+
# Add to history
|
|
138
|
+
_add_to_history(state, vm, theme)
|
|
139
|
+
st.rerun()
|
|
140
|
+
elif convert_clicked and not src_sql.strip():
|
|
141
|
+
st.warning("Please enter SQL to convert.")
|
|
142
|
+
|
|
143
|
+
# ── Result display ─────────────────────────────────────────────
|
|
144
|
+
vm: ConversionViewModel | None = state.convert_last_vm
|
|
145
|
+
if vm is not None:
|
|
146
|
+
st.markdown("---")
|
|
147
|
+
render_result_panel(
|
|
148
|
+
sql=vm.target_sql or "",
|
|
149
|
+
source_dialect=vm.source_dialect,
|
|
150
|
+
target_dialect=vm.target_dialect,
|
|
151
|
+
warnings=vm.warnings,
|
|
152
|
+
transformations=vm.transformations,
|
|
153
|
+
compatibility_notes=vm.compatibility_notes,
|
|
154
|
+
error_message=vm.error_message,
|
|
155
|
+
semantic_label=vm.semantic_label,
|
|
156
|
+
semantic_status=vm.semantic_status,
|
|
157
|
+
theme=theme,
|
|
158
|
+
target_validation_state=vm.target_validation_state,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Navigate to Diff workspace
|
|
162
|
+
if vm.target_sql and vm.success:
|
|
163
|
+
col_diff, _ = st.columns([1, 3])
|
|
164
|
+
with col_diff:
|
|
165
|
+
if st.button("Semantic Diff →", key="run_semantic_diff", type="secondary", use_container_width=True):
|
|
166
|
+
intent = create_navigation_intent(
|
|
167
|
+
target_page="diff",
|
|
168
|
+
action="open_diff",
|
|
169
|
+
source=vm.source_sql,
|
|
170
|
+
target=vm.target_sql,
|
|
171
|
+
src_dialect=vm.source_dialect,
|
|
172
|
+
tgt_dialect=vm.target_dialect,
|
|
173
|
+
)
|
|
174
|
+
st.session_state.sdm_navigation_intent = intent
|
|
175
|
+
st.rerun()
|
|
176
|
+
|
|
177
|
+
# ── Batch conversion ───────────────────────────────────────────
|
|
178
|
+
with st.expander("Batch Conversion", expanded=False):
|
|
179
|
+
_render_batch_section(src_dialect, tgt_dialect, theme)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _render_toolbar(theme: ColorTokens) -> None:
|
|
183
|
+
"""Render the top toolbar for the Convert page."""
|
|
184
|
+
st.markdown(
|
|
185
|
+
f"""
|
|
186
|
+
<div style="display:flex; align-items:center; justify-content:space-between;
|
|
187
|
+
padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
188
|
+
<div>
|
|
189
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
190
|
+
Convert
|
|
191
|
+
</div>
|
|
192
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
193
|
+
Transform SQL between database dialects
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div style="display:flex; gap:8px;">
|
|
197
|
+
<span style="font-size:11px; color:{theme.text_muted}; font-family:monospace;">
|
|
198
|
+
Click Convert to run
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
""",
|
|
203
|
+
unsafe_allow_html=True,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _render_batch_section(src_dialect: str, tgt_dialect: str, theme: ColorTokens) -> None:
|
|
208
|
+
"""Render the batch conversion section."""
|
|
209
|
+
state = SessionState.get()
|
|
210
|
+
st.markdown(
|
|
211
|
+
'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
212
|
+
'text-transform:uppercase; color:' + theme.text_muted + '; margin-bottom:8px;">'
|
|
213
|
+
'Batch Mode</div>',
|
|
214
|
+
unsafe_allow_html=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
batch_sql = st.text_area(
|
|
218
|
+
"Enter SQL statements (one per line or separated by semicolons)",
|
|
219
|
+
key="batch_sql_input",
|
|
220
|
+
height=150,
|
|
221
|
+
placeholder="SELECT * FROM users;\nSELECT id, name FROM orders WHERE status = 'active';",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
col_bs, col_bt, col_bb = st.columns([1, 1, 2])
|
|
225
|
+
with col_bs:
|
|
226
|
+
batch_src = st.selectbox("Source", DIALECTS, index=DIALECTS.index("postgres") if "postgres" in DIALECTS else 0,
|
|
227
|
+
key="batch_src", label_visibility="collapsed")
|
|
228
|
+
with col_bt:
|
|
229
|
+
batch_tgt = st.selectbox("Target", DIALECTS, index=DIALECTS.index("mysql") if "mysql" in DIALECTS else 0,
|
|
230
|
+
key="batch_tgt", label_visibility="collapsed")
|
|
231
|
+
with col_bb:
|
|
232
|
+
if st.button("Convert All", key="batch_convert_btn", type="primary", use_container_width=True):
|
|
233
|
+
if not batch_sql.strip():
|
|
234
|
+
st.warning("Please enter SQL statements.")
|
|
235
|
+
else:
|
|
236
|
+
with st.spinner("Batch converting..."):
|
|
237
|
+
# Use canonical backend for proper semicolon splitting
|
|
238
|
+
statements = split_sql_statements(batch_sql, batch_src)
|
|
239
|
+
results = batch_convert_sql(statements, batch_src, batch_tgt)
|
|
240
|
+
bvm = BatchConversionViewModel.from_results(statements, results)
|
|
241
|
+
state.batch_last_vm = bvm
|
|
242
|
+
st.rerun()
|
|
243
|
+
|
|
244
|
+
bvm: BatchConversionViewModel | None = state.batch_last_vm
|
|
245
|
+
if bvm is not None:
|
|
246
|
+
render_batch_result(
|
|
247
|
+
total=bvm.total,
|
|
248
|
+
succeeded=bvm.succeeded,
|
|
249
|
+
failed=bvm.failed,
|
|
250
|
+
items=bvm.items,
|
|
251
|
+
theme=theme,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Download all
|
|
255
|
+
combined = "\n\n".join(
|
|
256
|
+
item.converted_sql or f"-- Statement {item.statement_index} failed"
|
|
257
|
+
for item in bvm.items
|
|
258
|
+
)
|
|
259
|
+
st.download_button(
|
|
260
|
+
"Download All",
|
|
261
|
+
combined,
|
|
262
|
+
"batch_converted.sql",
|
|
263
|
+
mime="text/sql",
|
|
264
|
+
use_container_width=True,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _add_to_history(
|
|
269
|
+
state: SessionState,
|
|
270
|
+
vm: ConversionViewModel,
|
|
271
|
+
theme: ColorTokens,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Add a successful conversion to session history via SessionState."""
|
|
274
|
+
if not vm.success:
|
|
275
|
+
return
|
|
276
|
+
state.add_history_entry(
|
|
277
|
+
sql=vm.source_sql,
|
|
278
|
+
source_dialect=vm.source_dialect,
|
|
279
|
+
target_dialect=vm.target_dialect,
|
|
280
|
+
result_sql=vm.target_sql,
|
|
281
|
+
status=vm.status,
|
|
282
|
+
created_at=datetime.now(UTC).isoformat(),
|
|
283
|
+
)
|
frontend/pages/diff.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Semantic Diff Workspace for SQL Dialect Master v2.
|
|
2
|
+
|
|
3
|
+
Full workspace with:
|
|
4
|
+
- Overall semantic classification banner
|
|
5
|
+
- Finding list (clickable, with severity indicators)
|
|
6
|
+
- Inspector panel (shows selected finding details)
|
|
7
|
+
- Semantic vs Text diff tab switcher
|
|
8
|
+
- Integration with Convert workspace via unified NavigationIntent
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import streamlit as st
|
|
13
|
+
|
|
14
|
+
from backend.core.semantic_diff import diff_sql_ast
|
|
15
|
+
from frontend.app_context_v2 import DIALECTS
|
|
16
|
+
from frontend.core.design_tokens import ColorTokens
|
|
17
|
+
from frontend.core.navigation import consume_navigation_intent
|
|
18
|
+
from frontend.core.state import SessionState
|
|
19
|
+
from frontend.core.viewmodels import SemanticDiffViewModel
|
|
20
|
+
|
|
21
|
+
_FINDING_KEY = "sdm_selected_finding_index"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def render_diff_page(theme: ColorTokens) -> None:
|
|
25
|
+
"""Render the Semantic Diff workspace."""
|
|
26
|
+
state = SessionState.get()
|
|
27
|
+
|
|
28
|
+
_render_header(theme)
|
|
29
|
+
_render_input_section(theme)
|
|
30
|
+
|
|
31
|
+
# Process navigation intent from Convert → Diff handoff
|
|
32
|
+
intent = consume_navigation_intent()
|
|
33
|
+
if intent and intent.action == "open_diff" and not state.diff_last_vm:
|
|
34
|
+
st.session_state.diff_src_sql = intent.get_payload("source", "")
|
|
35
|
+
st.session_state.diff_tgt_sql = intent.get_payload("target", "")
|
|
36
|
+
st.session_state.diff_src_dialect = intent.get_payload("src_dialect", "postgres")
|
|
37
|
+
st.session_state.diff_tgt_dialect = intent.get_payload("tgt_dialect", "mysql")
|
|
38
|
+
state.selected_finding_index = None
|
|
39
|
+
|
|
40
|
+
# Get saved inputs (widget keys — managed by Streamlit)
|
|
41
|
+
src_sql = st.session_state.get("diff_src_sql", "")
|
|
42
|
+
tgt_sql = st.session_state.get("diff_tgt_sql", "")
|
|
43
|
+
|
|
44
|
+
# Render results if available
|
|
45
|
+
vm: SemanticDiffViewModel | None = state.diff_last_vm
|
|
46
|
+
if vm is not None:
|
|
47
|
+
selected_idx = state.selected_finding_index
|
|
48
|
+
_render_results(vm, src_sql, tgt_sql, selected_idx, theme)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _render_header(theme: ColorTokens) -> None:
|
|
52
|
+
"""Render the page header."""
|
|
53
|
+
st.markdown(
|
|
54
|
+
f"""
|
|
55
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
56
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
57
|
+
Semantic Diff
|
|
58
|
+
</div>
|
|
59
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
60
|
+
AST-based semantic equivalence analysis between two SQL statements
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
""",
|
|
64
|
+
unsafe_allow_html=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _render_input_section(theme: ColorTokens) -> None:
|
|
69
|
+
"""Render the SQL input section."""
|
|
70
|
+
col_src, col_tgt = st.columns(2)
|
|
71
|
+
|
|
72
|
+
with col_src:
|
|
73
|
+
st.markdown(
|
|
74
|
+
'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
75
|
+
'text-transform:uppercase; color:' + theme.text_muted + '; margin-bottom:6px;">'
|
|
76
|
+
'Source SQL</div>',
|
|
77
|
+
unsafe_allow_html=True,
|
|
78
|
+
)
|
|
79
|
+
st.text_area(
|
|
80
|
+
"Source",
|
|
81
|
+
key="diff_src_sql",
|
|
82
|
+
height=160,
|
|
83
|
+
label_visibility="collapsed",
|
|
84
|
+
placeholder="SELECT DATE_ADD(created_at, INTERVAL 7 DAY) FROM users",
|
|
85
|
+
)
|
|
86
|
+
st.selectbox(
|
|
87
|
+
"Dialect",
|
|
88
|
+
DIALECTS,
|
|
89
|
+
key="diff_src_dialect",
|
|
90
|
+
label_visibility="collapsed",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with col_tgt:
|
|
94
|
+
st.markdown(
|
|
95
|
+
'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
96
|
+
'text-transform:uppercase; color:' + theme.text_muted + '; margin-bottom:6px;">'
|
|
97
|
+
'Target SQL</div>',
|
|
98
|
+
unsafe_allow_html=True,
|
|
99
|
+
)
|
|
100
|
+
st.text_area(
|
|
101
|
+
"Target",
|
|
102
|
+
key="diff_tgt_sql",
|
|
103
|
+
height=160,
|
|
104
|
+
label_visibility="collapsed",
|
|
105
|
+
placeholder="SELECT created_at + INTERVAL '7 days' FROM users",
|
|
106
|
+
)
|
|
107
|
+
st.selectbox(
|
|
108
|
+
"Dialect",
|
|
109
|
+
DIALECTS,
|
|
110
|
+
key="diff_tgt_dialect",
|
|
111
|
+
label_visibility="collapsed",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Compare button
|
|
115
|
+
col_btn, _ = st.columns([1, 5])
|
|
116
|
+
with col_btn:
|
|
117
|
+
if st.button("Compare", type="primary", key="diff_compare", use_container_width=True):
|
|
118
|
+
_run_comparison(theme)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _run_comparison(theme: ColorTokens) -> None:
|
|
122
|
+
"""Run semantic diff and store result in session state."""
|
|
123
|
+
state = SessionState.get()
|
|
124
|
+
src_sql = st.session_state.get("diff_src_sql", "")
|
|
125
|
+
tgt_sql = st.session_state.get("diff_tgt_sql", "")
|
|
126
|
+
src_dialect = st.session_state.get("diff_src_dialect", "postgres")
|
|
127
|
+
tgt_dialect = st.session_state.get("diff_tgt_dialect", "mysql")
|
|
128
|
+
|
|
129
|
+
if not src_sql.strip() or not tgt_sql.strip():
|
|
130
|
+
st.warning("Please enter both SQL statements.")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
with st.spinner("Analyzing semantic equivalence..."):
|
|
134
|
+
backend_result = diff_sql_ast(src_sql, tgt_sql, src_dialect, tgt_dialect)
|
|
135
|
+
|
|
136
|
+
vm = SemanticDiffViewModel.from_backend(
|
|
137
|
+
source_sql=src_sql,
|
|
138
|
+
target_sql=tgt_sql,
|
|
139
|
+
source_dialect=src_dialect,
|
|
140
|
+
target_dialect=tgt_dialect,
|
|
141
|
+
diff=backend_result,
|
|
142
|
+
)
|
|
143
|
+
state.diff_last_vm = vm
|
|
144
|
+
state.clear_finding_selection()
|
|
145
|
+
st.rerun()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _render_results(
|
|
149
|
+
vm: SemanticDiffViewModel,
|
|
150
|
+
source_sql: str,
|
|
151
|
+
target_sql: str,
|
|
152
|
+
selected_idx: int | None,
|
|
153
|
+
theme: ColorTokens,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Render the diff results with finding list and inspector."""
|
|
156
|
+
from frontend.ui.diff import render_semantic_diff_workspace
|
|
157
|
+
render_semantic_diff_workspace(vm, source_sql, target_sql, theme, selected_idx)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Function Library page for SQL Dialect Master v2."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import streamlit as st
|
|
5
|
+
|
|
6
|
+
from frontend.app_context_v2 import get_page_data
|
|
7
|
+
from frontend.core.design_tokens import ColorTokens
|
|
8
|
+
from frontend.core.escaping import esc
|
|
9
|
+
|
|
10
|
+
CATEGORY_ICONS = {
|
|
11
|
+
"string": "S",
|
|
12
|
+
"date": "D",
|
|
13
|
+
"math": "M",
|
|
14
|
+
"aggregate": "A",
|
|
15
|
+
"window": "W",
|
|
16
|
+
"conditional": "C",
|
|
17
|
+
"conversion": "V",
|
|
18
|
+
"json": "J",
|
|
19
|
+
"array": "L",
|
|
20
|
+
"system": "Y",
|
|
21
|
+
"geo": "G",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_functions_page(theme: ColorTokens) -> None:
|
|
26
|
+
"""Render the Function Library page.
|
|
27
|
+
|
|
28
|
+
Data is injected by the page registry via ``get_page_data()`` — the
|
|
29
|
+
renderer keeps the unified PageRenderer contract of receiving only
|
|
30
|
+
the active theme.
|
|
31
|
+
"""
|
|
32
|
+
data = get_page_data()
|
|
33
|
+
funcs_data = data.funcs_data
|
|
34
|
+
|
|
35
|
+
st.markdown(
|
|
36
|
+
f"""
|
|
37
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
38
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
39
|
+
Function Library
|
|
40
|
+
</div>
|
|
41
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
42
|
+
Search and compare SQL functions across dialects
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
""",
|
|
46
|
+
unsafe_allow_html=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
all_funcs = funcs_data.get("functions", [])
|
|
50
|
+
|
|
51
|
+
# Search and filter
|
|
52
|
+
col_search, col_cat = st.columns([2, 1])
|
|
53
|
+
with col_search:
|
|
54
|
+
query = st.text_input("Search", key="func_search", placeholder="Type function name...", label_visibility="collapsed")
|
|
55
|
+
with col_cat:
|
|
56
|
+
categories = sorted({f.get("category", "other") for f in all_funcs})
|
|
57
|
+
cat_options = ["All"] + categories
|
|
58
|
+
selected_cat = st.selectbox("Category", cat_options, key="func_cat")
|
|
59
|
+
|
|
60
|
+
# Filter
|
|
61
|
+
funcs = all_funcs
|
|
62
|
+
if query:
|
|
63
|
+
q = query.lower()
|
|
64
|
+
funcs = [f for f in funcs if q in f.get("name", "").lower() or q in f.get("description", "").lower()]
|
|
65
|
+
if selected_cat != "All":
|
|
66
|
+
funcs = [f for f in funcs if f.get("category", "") == selected_cat]
|
|
67
|
+
|
|
68
|
+
st.caption(f"{len(funcs)} functions" + (f" matching '{esc(query)}'" if query else ""))
|
|
69
|
+
|
|
70
|
+
if not funcs:
|
|
71
|
+
st.info("No functions found.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Display as compact table
|
|
75
|
+
for f in funcs[:50]:
|
|
76
|
+
_render_function_row(f, theme)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _render_function_row(func: dict, theme: ColorTokens) -> None:
|
|
80
|
+
"""Render a single function row in the library table."""
|
|
81
|
+
name = esc(func.get("name", ""))
|
|
82
|
+
category = func.get("category", "other")
|
|
83
|
+
icon = CATEGORY_ICONS.get(category, "?")
|
|
84
|
+
description = esc(func.get("description", ""))
|
|
85
|
+
dialect_count = len(func.get("dialects", {}))
|
|
86
|
+
|
|
87
|
+
with st.expander(f"**{name}** · `{icon}` {esc(category.title())} · {dialect_count} dialects", expanded=False):
|
|
88
|
+
st.markdown(f"**{description}**")
|
|
89
|
+
|
|
90
|
+
dialects = func.get("dialects", {})
|
|
91
|
+
if dialects:
|
|
92
|
+
st.markdown("**Syntax by Database:**")
|
|
93
|
+
items = list(dialects.items())
|
|
94
|
+
for i in range(0, len(items), 3):
|
|
95
|
+
cols = st.columns(min(3, len(items) - i))
|
|
96
|
+
for j, (dialect, syntax) in enumerate(items[i:i+3]):
|
|
97
|
+
with cols[j]:
|
|
98
|
+
st.markdown(
|
|
99
|
+
f'**{esc(dialect.upper())}**\n`{esc(syntax)}`'
|
|
100
|
+
)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""History page for SQL Dialect Master v2."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
from frontend.app_context_v2 import copy_sql
|
|
9
|
+
from frontend.core.design_tokens import ColorTokens
|
|
10
|
+
from frontend.core.escaping import esc
|
|
11
|
+
from frontend.core.state import SessionState
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def render_history_page(theme: ColorTokens) -> None:
|
|
15
|
+
"""Render the History page."""
|
|
16
|
+
state = SessionState.get()
|
|
17
|
+
|
|
18
|
+
st.markdown(
|
|
19
|
+
f"""
|
|
20
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
21
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
22
|
+
History
|
|
23
|
+
</div>
|
|
24
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
25
|
+
Recent conversions and saved queries
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
""",
|
|
29
|
+
unsafe_allow_html=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
history = state.history
|
|
33
|
+
|
|
34
|
+
if not history:
|
|
35
|
+
st.info("No conversion history yet.")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Search and filter controls (widget keys — managed by Streamlit)
|
|
39
|
+
col_search, col_filter = st.columns([2, 1])
|
|
40
|
+
with col_search:
|
|
41
|
+
search = st.text_input("Search", key="hist_search", placeholder="Search SQL...", label_visibility="collapsed")
|
|
42
|
+
with col_filter:
|
|
43
|
+
filter_status = st.selectbox("Status", ["All", "Success", "Error"], key="hist_filter")
|
|
44
|
+
|
|
45
|
+
# Apply filters
|
|
46
|
+
filtered = history
|
|
47
|
+
if search:
|
|
48
|
+
s = search.lower()
|
|
49
|
+
filtered = [h for h in filtered if s in h.get("sql", "").lower()]
|
|
50
|
+
if filter_status == "Success":
|
|
51
|
+
filtered = [h for h in filtered if h.get("status") == "valid"]
|
|
52
|
+
elif filter_status == "Error":
|
|
53
|
+
filtered = [h for h in filtered if h.get("status") == "error"]
|
|
54
|
+
|
|
55
|
+
st.caption(f"{len(filtered)} entries")
|
|
56
|
+
|
|
57
|
+
# Render entries
|
|
58
|
+
for entry in filtered:
|
|
59
|
+
_render_history_entry(state, entry, theme)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _render_history_entry(
|
|
63
|
+
state: SessionState,
|
|
64
|
+
entry: dict,
|
|
65
|
+
theme: ColorTokens,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Render a single history entry."""
|
|
68
|
+
src = esc(entry.get("src", "unknown"))
|
|
69
|
+
tgt = esc(entry.get("tgt", "unknown"))
|
|
70
|
+
sql_preview = esc(entry.get("sql", "")[:80])
|
|
71
|
+
if len(entry.get("sql", "")) > 80:
|
|
72
|
+
sql_preview += "..."
|
|
73
|
+
status = entry.get("status", "neutral")
|
|
74
|
+
created_at = entry.get("created_at", "")
|
|
75
|
+
identity = entry.get("identity", "")
|
|
76
|
+
|
|
77
|
+
# Status badge color
|
|
78
|
+
if status == "valid":
|
|
79
|
+
status_color = theme.success
|
|
80
|
+
status_text = "✓ Valid"
|
|
81
|
+
elif status == "error":
|
|
82
|
+
status_color = theme.danger
|
|
83
|
+
status_text = "✗ Error"
|
|
84
|
+
else:
|
|
85
|
+
status_color = theme.text_muted
|
|
86
|
+
status_text = "—"
|
|
87
|
+
|
|
88
|
+
# Time ago
|
|
89
|
+
time_str = _format_time_ago(created_at) if created_at else "recent"
|
|
90
|
+
|
|
91
|
+
with st.expander(f"{src} → {tgt} · {time_str}", expanded=False):
|
|
92
|
+
st.markdown(
|
|
93
|
+
f'<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">'
|
|
94
|
+
f'<span style="color:{status_color}; font-size:11px; font-weight:600;">{status_text}</span>'
|
|
95
|
+
f'</div>',
|
|
96
|
+
unsafe_allow_html=True,
|
|
97
|
+
)
|
|
98
|
+
st.code(sql_preview, language="sql")
|
|
99
|
+
|
|
100
|
+
col_load, col_copy, col_fav, col_del = st.columns([1, 1, 1, 1])
|
|
101
|
+
with col_load:
|
|
102
|
+
if st.button("Load", key=f"hist_load_{identity}"):
|
|
103
|
+
from frontend.core.navigation import create_navigation_intent
|
|
104
|
+
intent = create_navigation_intent(
|
|
105
|
+
target_page="convert",
|
|
106
|
+
action="open_conversion",
|
|
107
|
+
sql=entry.get("sql", ""),
|
|
108
|
+
dialect=entry.get("src", "postgres"),
|
|
109
|
+
origin="history",
|
|
110
|
+
)
|
|
111
|
+
st.session_state.sdm_navigation_intent = intent
|
|
112
|
+
st.rerun()
|
|
113
|
+
with col_copy:
|
|
114
|
+
if st.button("Copy", key=f"hist_copy_{identity}"):
|
|
115
|
+
copy_sql(entry.get("sql", ""), key=f"hist_copy_sql_{identity}")
|
|
116
|
+
with col_fav:
|
|
117
|
+
if not state.is_favorite(identity):
|
|
118
|
+
if st.button("☆", key=f"hist_fav_{identity}"):
|
|
119
|
+
state.add_favorite(identity)
|
|
120
|
+
st.rerun()
|
|
121
|
+
else:
|
|
122
|
+
st.button("★", key=f"hist_fav_{identity}", disabled=True)
|
|
123
|
+
with col_del:
|
|
124
|
+
if st.button("Del", key=f"hist_del_{identity}"):
|
|
125
|
+
state.remove_history_entry(identity)
|
|
126
|
+
st.rerun()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _format_time_ago(iso_str: str) -> str:
|
|
130
|
+
"""Format an ISO timestamp as 'X minutes ago' etc."""
|
|
131
|
+
try:
|
|
132
|
+
dt = datetime.fromisoformat(iso_str)
|
|
133
|
+
delta = datetime.now(UTC) - dt
|
|
134
|
+
seconds = int(delta.total_seconds())
|
|
135
|
+
if seconds < 60:
|
|
136
|
+
return f"{seconds}s ago"
|
|
137
|
+
elif seconds < 3600:
|
|
138
|
+
return f"{seconds // 60}m ago"
|
|
139
|
+
elif seconds < 86400:
|
|
140
|
+
return f"{seconds // 3600}h ago"
|
|
141
|
+
else:
|
|
142
|
+
return f"{seconds // 86400}d ago"
|
|
143
|
+
except (ValueError, TypeError):
|
|
144
|
+
return "recent"
|