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,60 @@
|
|
|
1
|
+
"""Settings page for SQL Dialect Master v2."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import streamlit as st
|
|
5
|
+
|
|
6
|
+
from backend.core import metadata as meta
|
|
7
|
+
from frontend.core.design_tokens import ColorTokens
|
|
8
|
+
from frontend.core.state import SessionState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render_settings_page(theme: ColorTokens) -> None:
|
|
12
|
+
"""Render the Settings page."""
|
|
13
|
+
state = SessionState.get()
|
|
14
|
+
|
|
15
|
+
st.markdown(
|
|
16
|
+
f"""
|
|
17
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
18
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
19
|
+
Settings
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
""",
|
|
23
|
+
unsafe_allow_html=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Theme
|
|
27
|
+
st.markdown("##### Theme")
|
|
28
|
+
col_theme_sel, _ = st.columns([2, 1])
|
|
29
|
+
with col_theme_sel:
|
|
30
|
+
current = state.theme
|
|
31
|
+
selected = st.selectbox("Color theme", ["dark", "light"], index=["dark", "light"].index(current) if current in ["dark", "light"] else 0, key="settings_theme_sel")
|
|
32
|
+
with st.container():
|
|
33
|
+
if st.button("Apply Theme", key="settings_apply_theme"):
|
|
34
|
+
state.theme = selected
|
|
35
|
+
st.rerun()
|
|
36
|
+
|
|
37
|
+
# Keyboard shortcuts reference
|
|
38
|
+
st.markdown("---")
|
|
39
|
+
st.markdown("##### Notes")
|
|
40
|
+
st.markdown("""
|
|
41
|
+
Streamlit does not support native keyboard shortcuts.
|
|
42
|
+
Actions are performed via buttons and menu selections.
|
|
43
|
+
""")
|
|
44
|
+
|
|
45
|
+
# About
|
|
46
|
+
st.markdown("---")
|
|
47
|
+
st.markdown("##### About")
|
|
48
|
+
st.markdown(
|
|
49
|
+
f"""
|
|
50
|
+
**SQL Dialect Master {meta.PROJECT_VERSION}**
|
|
51
|
+
|
|
52
|
+
Professional SQL conversion workspace (Frontend v{meta.FRONTEND_ARCHITECTURE_VERSION})
|
|
53
|
+
powered by sqlglot and semantic analysis.
|
|
54
|
+
|
|
55
|
+
- {meta.SUPPORTED_DIALECTS_COUNT} supported dialects
|
|
56
|
+
- {meta.FUNCTION_COUNT}+ functions indexed
|
|
57
|
+
- {meta.TYPE_COUNT}+ type mappings
|
|
58
|
+
- Semantic diff analysis
|
|
59
|
+
"""
|
|
60
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Templates page for SQL Dialect Master v2."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import streamlit as st
|
|
5
|
+
|
|
6
|
+
from frontend.core.design_tokens import ColorTokens
|
|
7
|
+
from frontend.core.escaping import esc
|
|
8
|
+
from frontend.templates_v2 import TEMPLATES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render_templates_page(theme: ColorTokens) -> None:
|
|
12
|
+
"""Render the Templates page."""
|
|
13
|
+
|
|
14
|
+
st.markdown(
|
|
15
|
+
f"""
|
|
16
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
17
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
18
|
+
Templates
|
|
19
|
+
</div>
|
|
20
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
21
|
+
Quick-start SQL templates by category
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
""",
|
|
25
|
+
unsafe_allow_html=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Group by category (TEMPLATES maps category -> list of (label, sql)).
|
|
29
|
+
categories: dict[str, list[tuple[str, str]]] = {}
|
|
30
|
+
for cat, items in TEMPLATES.items():
|
|
31
|
+
if isinstance(items, list):
|
|
32
|
+
categories[cat] = list(items)
|
|
33
|
+
else:
|
|
34
|
+
categories[cat] = [(cat, items)]
|
|
35
|
+
|
|
36
|
+
for cat, items in categories.items():
|
|
37
|
+
st.markdown(f"##### {esc(cat)}")
|
|
38
|
+
for label, sql in items:
|
|
39
|
+
short_label = label.replace(f"{cat} · ", "")
|
|
40
|
+
with st.expander(f"{esc(short_label)}", expanded=False):
|
|
41
|
+
st.code(esc(sql), language="sql")
|
|
42
|
+
st.button(
|
|
43
|
+
"Use in Convert",
|
|
44
|
+
key=_deterministic_key("tpl", f"{cat}::{label}"),
|
|
45
|
+
on_click=_load_template,
|
|
46
|
+
args=(sql,),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _deterministic_key(prefix: str, value: str) -> str:
|
|
51
|
+
"""Build a stable, process-independent widget key.
|
|
52
|
+
|
|
53
|
+
``str.hash`` is salted per process on CPython, so it must never be
|
|
54
|
+
used for Streamlit widget keys. The previous code also applied
|
|
55
|
+
``hash()`` to a list of template tuples, which is unhashable and
|
|
56
|
+
crashed the page at runtime.
|
|
57
|
+
"""
|
|
58
|
+
import hashlib
|
|
59
|
+
|
|
60
|
+
digest = hashlib.sha1(value.encode("utf-8")).hexdigest()[:8]
|
|
61
|
+
return f"{prefix}_{digest}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_template(sql: str) -> None:
|
|
65
|
+
"""Load a template SQL into the Convert workspace."""
|
|
66
|
+
st.session_state.convert_src_sql = sql
|
|
67
|
+
st.query_params["page"] = "convert"
|
frontend/pages/types.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Type Mapping 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 DIALECTS, get_dialect_label, get_page_data
|
|
7
|
+
from frontend.core.design_tokens import ColorTokens
|
|
8
|
+
from frontend.core.escaping import esc
|
|
9
|
+
|
|
10
|
+
TYPE_CATEGORIES = {
|
|
11
|
+
"String Types": ["STRING", "VARCHAR", "CHAR", "TEXT", "MEDIUMTEXT", "LONGTEXT"],
|
|
12
|
+
"Numeric Types": ["BIGINT", "INT", "SMALLINT", "TINYINT", "DOUBLE", "FLOAT", "DECIMAL", "MONEY", "SERIAL"],
|
|
13
|
+
"Boolean & Bit": ["BOOLEAN", "BIT"],
|
|
14
|
+
"Date/Time Types": ["DATE", "TIME", "TIMESTAMP", "TIMESTAMP_TZ", "INTERVAL", "YEAR"],
|
|
15
|
+
"Complex Types": ["ARRAY", "MAP", "STRUCT", "JSON", "JSONB", "XML"],
|
|
16
|
+
"Binary Types": ["BINARY"],
|
|
17
|
+
"Special Types": ["UUID", "INET", "GEOMETRY", "GEOGRAPHY", "ENUM", "SET"],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render_types_page(theme: ColorTokens) -> None:
|
|
22
|
+
"""Render the Type Mapping page.
|
|
23
|
+
|
|
24
|
+
Data is injected by the page registry via ``get_page_data()`` — the
|
|
25
|
+
renderer keeps the unified PageRenderer contract of receiving only
|
|
26
|
+
the active theme.
|
|
27
|
+
"""
|
|
28
|
+
data = get_page_data()
|
|
29
|
+
types_data = data.types_data
|
|
30
|
+
|
|
31
|
+
st.markdown(
|
|
32
|
+
f"""
|
|
33
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
34
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
35
|
+
Type Mapping
|
|
36
|
+
</div>
|
|
37
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
38
|
+
Compare data types across database dialects
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
""",
|
|
42
|
+
unsafe_allow_html=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
mappings = types_data.get("mappings", {})
|
|
46
|
+
|
|
47
|
+
col_src, col_tgt = st.columns(2)
|
|
48
|
+
with col_src:
|
|
49
|
+
src_db = st.selectbox("Source", DIALECTS, index=DIALECTS.index("postgres") if "postgres" in DIALECTS else 0,
|
|
50
|
+
format_func=get_dialect_label, key="type_src")
|
|
51
|
+
with col_tgt:
|
|
52
|
+
tgt_db = st.selectbox("Target", DIALECTS, index=DIALECTS.index("mysql") if "mysql" in DIALECTS else 0,
|
|
53
|
+
format_func=get_dialect_label, key="type_tgt")
|
|
54
|
+
|
|
55
|
+
st.markdown(f"**{esc(src_db.upper())}** → **{esc(tgt_db.upper())}**")
|
|
56
|
+
|
|
57
|
+
# Build table data
|
|
58
|
+
table_data = []
|
|
59
|
+
for type_name, type_map in mappings.items():
|
|
60
|
+
src_type = type_map.get(src_db, "N/A")
|
|
61
|
+
tgt_type = type_map.get(tgt_db, "N/A")
|
|
62
|
+
if src_type == tgt_type:
|
|
63
|
+
status = "exact"
|
|
64
|
+
elif tgt_type != "N/A":
|
|
65
|
+
status = "mapped"
|
|
66
|
+
else:
|
|
67
|
+
status = "missing"
|
|
68
|
+
table_data.append({
|
|
69
|
+
"Type": type_name,
|
|
70
|
+
esc(src_db.upper()): src_type,
|
|
71
|
+
esc(tgt_db.upper()): tgt_type,
|
|
72
|
+
"Status": status,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Render as dataframe
|
|
76
|
+
import pandas as pd
|
|
77
|
+
df = pd.DataFrame(table_data)
|
|
78
|
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
|
79
|
+
|
|
80
|
+
# Detail explorer
|
|
81
|
+
st.markdown("---")
|
|
82
|
+
st.markdown("##### Type Details")
|
|
83
|
+
|
|
84
|
+
col_cat, col_type = st.columns([1, 2])
|
|
85
|
+
with col_cat:
|
|
86
|
+
cat_options = ["All"] + list(TYPE_CATEGORIES.keys())
|
|
87
|
+
selected_cat = st.selectbox("Category", cat_options, key="type_detail_cat")
|
|
88
|
+
|
|
89
|
+
if selected_cat == "All":
|
|
90
|
+
available = list(mappings.keys())
|
|
91
|
+
else:
|
|
92
|
+
available = [t for t in TYPE_CATEGORIES.get(selected_cat, []) if t in mappings]
|
|
93
|
+
|
|
94
|
+
with col_type:
|
|
95
|
+
selected_type = st.selectbox("Select Type", available, key="type_detail_sel")
|
|
96
|
+
|
|
97
|
+
if selected_type and selected_type in mappings:
|
|
98
|
+
m = mappings[selected_type]
|
|
99
|
+
st.markdown(f"### `{esc(selected_type)}`")
|
|
100
|
+
if "notes" in m:
|
|
101
|
+
st.info(esc(m["notes"]))
|
|
102
|
+
|
|
103
|
+
# Show all dialect mappings
|
|
104
|
+
dialect_items = [(d, t) for d, t in m.items() if d != "notes"]
|
|
105
|
+
for row_start in range(0, len(dialect_items), 4):
|
|
106
|
+
cols = st.columns(min(4, len(dialect_items) - row_start))
|
|
107
|
+
for j, (dialect, typ) in enumerate(dialect_items[row_start:row_start+4]):
|
|
108
|
+
with cols[j]:
|
|
109
|
+
status_icon = "✓" if typ not in ["N/A", "JSON", "STRING"] else "⚠"
|
|
110
|
+
st.markdown(f"**{esc(dialect.upper())}**\n`{esc(typ)}` {status_icon}")
|
frontend/templates_v2.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Templates for SQL Dialect Master v2.
|
|
2
|
+
|
|
3
|
+
Cleaned-up template library without emoji clutter.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
TEMPLATES: dict[str, str] = {
|
|
8
|
+
"Basic Queries": [
|
|
9
|
+
("Basic SELECT", "SELECT * FROM users WHERE id = 1"),
|
|
10
|
+
("DISTINCT with ORDER", "SELECT DISTINCT category, brand FROM products ORDER BY category, brand"),
|
|
11
|
+
("COALESCE null handling", "SELECT id, COALESCE(nickname, username, email, 'Anonymous') AS display_name FROM users"),
|
|
12
|
+
],
|
|
13
|
+
"Aggregations": [
|
|
14
|
+
("Group aggregation", "SELECT dept, COUNT(*) AS cnt, AVG(salary) AS avg_sal FROM employees GROUP BY dept"),
|
|
15
|
+
("HAVING filter", "SELECT dept, COUNT(*) AS emp_count FROM employees GROUP BY dept HAVING COUNT(*) > 5 ORDER BY emp_count DESC"),
|
|
16
|
+
("Multiple aggregates", "SELECT category, COUNT(*) AS cnt, SUM(price) AS total, AVG(price) AS avg_price FROM products GROUP BY category"),
|
|
17
|
+
],
|
|
18
|
+
"Joins": [
|
|
19
|
+
("Multi-table JOIN", "SELECT u.name, o.order_id, p.product_name FROM users u JOIN orders o ON u.id = o.user_id JOIN products p ON o.product_id = p.id"),
|
|
20
|
+
("LEFT JOIN with NULL", "SELECT u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.name"),
|
|
21
|
+
("Self JOIN", "SELECT e.name AS employee, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.id"),
|
|
22
|
+
],
|
|
23
|
+
"Window Functions": [
|
|
24
|
+
("ROW_NUMBER", "SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn FROM employees"),
|
|
25
|
+
("LAG/LEAD", "SELECT date, amount, LAG(amount, 1, 0) OVER (ORDER BY date) AS prev_amount FROM sales"),
|
|
26
|
+
("Running total", "SELECT date, amount, SUM(amount) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING) AS running_total FROM sales"),
|
|
27
|
+
],
|
|
28
|
+
"Subqueries": [
|
|
29
|
+
("Subquery IN clause", "SELECT * FROM products WHERE category_id IN (SELECT id FROM categories WHERE status = 'active')"),
|
|
30
|
+
("Correlated subquery", "SELECT * FROM employees e WHERE salary > (SELECT AVG(salary) FROM employees WHERE dept = e.dept)"),
|
|
31
|
+
("EXISTS subquery", "SELECT * FROM customers c WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.amount > 1000)"),
|
|
32
|
+
],
|
|
33
|
+
"CTEs": [
|
|
34
|
+
("Basic CTE", "WITH active_users AS (SELECT * FROM users WHERE status = 'active') SELECT * FROM active_users WHERE created_at > '2024-01-01'"),
|
|
35
|
+
("Multiple CTEs", "WITH sales_2024 AS (SELECT * FROM sales WHERE YEAR(date) = 2024), top_products AS (SELECT product_id, SUM(amount) AS total FROM sales_2024 GROUP BY product_id) SELECT * FROM top_products ORDER BY total DESC LIMIT 10"),
|
|
36
|
+
],
|
|
37
|
+
"Date Operations": [
|
|
38
|
+
("Date extraction", "SELECT YEAR(created_at) AS year, MONTH(created_at) AS month, COUNT(*) AS cnt FROM orders GROUP BY YEAR(created_at), MONTH(created_at)"),
|
|
39
|
+
("Date arithmetic", "SELECT id, created_at, DATE_ADD(created_at, 30) AS expires_at FROM subscriptions"),
|
|
40
|
+
("Date formatting", "SELECT id, DATE_FORMAT(created_at, '%Y-%m-%d') AS formatted_date FROM orders"),
|
|
41
|
+
],
|
|
42
|
+
"Set Operations": [
|
|
43
|
+
("UNION ALL", "SELECT 'Q1' AS quarter, SUM(amount) AS total FROM sales WHERE month <= 3 UNION ALL SELECT 'Q2', SUM(amount) FROM sales WHERE month BETWEEN 4 AND 6"),
|
|
44
|
+
("INTERSECT", "SELECT user_id FROM orders WHERE YEAR(created_at) = 2023 INTERSECT SELECT user_id FROM orders WHERE YEAR(created_at) = 2024"),
|
|
45
|
+
("EXCEPT", "SELECT user_id FROM users EXCEPT SELECT DISTINCT user_id FROM orders"),
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Flatten for compatibility with old lookup pattern
|
|
50
|
+
FLAT_TEMPLATES: dict[str, str] = {}
|
|
51
|
+
for cat, items in TEMPLATES.items():
|
|
52
|
+
for label, sql in items:
|
|
53
|
+
FLAT_TEMPLATES[f"{cat} · {label}"] = sql
|
frontend/ui/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Command Palette for SQL Dialect Master v2.
|
|
2
|
+
|
|
3
|
+
Provides a button-based command palette for quick access to all workspace actions.
|
|
4
|
+
All commands produce NavigationIntent objects — no direct routing.
|
|
5
|
+
|
|
6
|
+
Note: Streamlit does not support native keyboard shortcuts, so the ⌘K button
|
|
7
|
+
is used as the trigger instead of a keyboard listener.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
import streamlit as st
|
|
14
|
+
|
|
15
|
+
from frontend.core.design_tokens import ColorTokens
|
|
16
|
+
from frontend.core.navigation import create_navigation_intent
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Command:
|
|
21
|
+
"""A single command in the palette."""
|
|
22
|
+
|
|
23
|
+
label: str
|
|
24
|
+
description: str
|
|
25
|
+
category: str
|
|
26
|
+
action: str
|
|
27
|
+
shortcut: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Command registry ──────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
COMMANDS: list[Command] = [
|
|
33
|
+
# Workspace
|
|
34
|
+
Command("Convert SQL", "Open the SQL conversion workspace", "Workspace", "new_conversion", ""),
|
|
35
|
+
Command("NL2SQL", "Generate SQL from natural language", "Workspace", "open_nl2sql", ""),
|
|
36
|
+
Command("Semantic Diff", "Compare two SQL statements", "Workspace", "open_diff", ""),
|
|
37
|
+
Command("Lineage", "Visualize table dependencies", "Workspace", "open_lineage", ""),
|
|
38
|
+
Command("Runtime", "Execute and verify SQL against databases", "Workspace", "open_runtime", ""),
|
|
39
|
+
# Library
|
|
40
|
+
Command("Function Library", "Search and compare SQL functions", "Library", "open_functions", ""),
|
|
41
|
+
Command("Type Mapping", "Compare data types across dialects", "Library", "open_types", ""),
|
|
42
|
+
Command("Templates", "Browse SQL templates by category", "Library", "open_templates", ""),
|
|
43
|
+
Command("History", "View recent conversions", "Library", "open_history", ""),
|
|
44
|
+
# System
|
|
45
|
+
Command("Query Analysis", "Static analysis of SQL queries", "System", "open_query_analysis", ""),
|
|
46
|
+
Command("Settings", "Theme and configuration", "System", "open_settings", ""),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
CATEGORY_ORDER = ["Workspace", "Library", "System"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render_command_palette(theme: ColorTokens) -> None:
|
|
53
|
+
"""Render the command palette overlay."""
|
|
54
|
+
# Check for keyboard shortcut (⌘K / Ctrl+K)
|
|
55
|
+
# Streamlit doesn't support direct key listeners, so we use a text input
|
|
56
|
+
# as the palette trigger. The actual keyboard shortcut is documented.
|
|
57
|
+
|
|
58
|
+
# Palette trigger button in topbar
|
|
59
|
+
if st.button("⌘K", key="cmd_palette_trigger", help="Open command palette",
|
|
60
|
+
use_container_width=False):
|
|
61
|
+
_show_palette(theme)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _show_palette(theme: ColorTokens) -> None:
|
|
65
|
+
"""Show the command palette modal."""
|
|
66
|
+
query = st.text_input(
|
|
67
|
+
"Search commands...",
|
|
68
|
+
key="cmd_palette_query",
|
|
69
|
+
placeholder="Type a command name...",
|
|
70
|
+
label_visibility="collapsed",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Filter commands
|
|
74
|
+
q = query.lower().strip()
|
|
75
|
+
if q:
|
|
76
|
+
filtered = [
|
|
77
|
+
c for c in COMMANDS
|
|
78
|
+
if q in c.label.lower() or q in c.description.lower() or q in c.category.lower()
|
|
79
|
+
]
|
|
80
|
+
else:
|
|
81
|
+
# Group by category
|
|
82
|
+
filtered = list(COMMANDS)
|
|
83
|
+
|
|
84
|
+
# Render results
|
|
85
|
+
if not filtered:
|
|
86
|
+
st.caption("No commands found.")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Group by category
|
|
90
|
+
groups: dict[str, list[Command]] = {}
|
|
91
|
+
for cmd in filtered:
|
|
92
|
+
if cmd.category not in groups:
|
|
93
|
+
groups[cmd.category] = []
|
|
94
|
+
groups[cmd.category].append(cmd)
|
|
95
|
+
|
|
96
|
+
for cat in CATEGORY_ORDER:
|
|
97
|
+
if cat not in groups:
|
|
98
|
+
continue
|
|
99
|
+
st.markdown(
|
|
100
|
+
f'<div style="font-size:10px; font-weight:600; letter-spacing:0.08em; '
|
|
101
|
+
f'text-transform:uppercase; color:{theme.text_muted}; margin:8px 0 4px;">'
|
|
102
|
+
f'{cat}</div>',
|
|
103
|
+
unsafe_allow_html=True,
|
|
104
|
+
)
|
|
105
|
+
for cmd in groups[cat]:
|
|
106
|
+
_render_command_item(cmd, theme)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _render_command_item(cmd: Command, theme: ColorTokens) -> None:
|
|
110
|
+
"""Render a single command item."""
|
|
111
|
+
col_cmd, col_shortcut = st.columns([5, 1])
|
|
112
|
+
|
|
113
|
+
with col_cmd:
|
|
114
|
+
clicked = st.button(
|
|
115
|
+
f"**{cmd.label}**\n{cmd.description}",
|
|
116
|
+
key=f"cmd_{cmd.action}",
|
|
117
|
+
use_container_width=True,
|
|
118
|
+
help=f"{cmd.label} — {cmd.description}",
|
|
119
|
+
)
|
|
120
|
+
if clicked:
|
|
121
|
+
_execute_command(cmd)
|
|
122
|
+
|
|
123
|
+
with col_shortcut:
|
|
124
|
+
if cmd.shortcut:
|
|
125
|
+
st.caption(cmd.shortcut)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _execute_command(cmd: Command) -> None:
|
|
129
|
+
"""Execute a command by creating and storing a NavigationIntent."""
|
|
130
|
+
intent = create_navigation_intent(
|
|
131
|
+
target_page=_action_to_page(cmd.action),
|
|
132
|
+
action=cmd.action,
|
|
133
|
+
)
|
|
134
|
+
st.session_state.sdm_navigation_intent = intent
|
|
135
|
+
st.rerun()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _action_to_page(action: str) -> str:
|
|
139
|
+
"""Map action to target page."""
|
|
140
|
+
mapping = {
|
|
141
|
+
"new_conversion": "convert",
|
|
142
|
+
"open_nl2sql": "nl2sql",
|
|
143
|
+
"open_diff": "diff",
|
|
144
|
+
"open_lineage": "lineage",
|
|
145
|
+
"open_runtime": "runtime",
|
|
146
|
+
"open_functions": "functions",
|
|
147
|
+
"open_types": "types",
|
|
148
|
+
"open_templates": "templates",
|
|
149
|
+
"open_history": "history",
|
|
150
|
+
"open_query_analysis": "query_analysis",
|
|
151
|
+
"open_settings": "settings",
|
|
152
|
+
}
|
|
153
|
+
return mapping.get(action, "convert")
|