sql-dialect-master 1.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. backend/__init__.py +1 -0
  2. backend/api/__init__.py +1 -0
  3. backend/api/main.py +732 -0
  4. backend/api/middleware.py +274 -0
  5. backend/api/rate_limit_store.py +105 -0
  6. backend/api/readiness.py +178 -0
  7. backend/core/__init__.py +57 -0
  8. backend/core/cache.py +261 -0
  9. backend/core/column_hint_validation.py +22 -0
  10. backend/core/config.py +546 -0
  11. backend/core/constants.py +84 -0
  12. backend/core/exceptions.py +124 -0
  13. backend/core/function_call_scanner.py +64 -0
  14. backend/core/function_semantics.py +148 -0
  15. backend/core/functions_db.json +601 -0
  16. backend/core/functions_lookup.py +332 -0
  17. backend/core/metadata.py +71 -0
  18. backend/core/nl2sql.py +85 -0
  19. backend/core/nl2sql_components/__init__.py +72 -0
  20. backend/core/nl2sql_components/aggregations.py +35 -0
  21. backend/core/nl2sql_components/boolean_conditions.py +279 -0
  22. backend/core/nl2sql_components/core_extract.py +106 -0
  23. backend/core/nl2sql_components/evidence.py +122 -0
  24. backend/core/nl2sql_components/mappings.py +185 -0
  25. backend/core/nl2sql_components/relations.py +180 -0
  26. backend/core/nl2sql_components/templates.py +130 -0
  27. backend/core/nl2sql_components/tokenizer.py +187 -0
  28. backend/core/nl2sql_legacy.py +22 -0
  29. backend/core/nl2sql_pipeline/__init__.py +13 -0
  30. backend/core/nl2sql_pipeline/aggregations.py +26 -0
  31. backend/core/nl2sql_pipeline/clause_extract.py +97 -0
  32. backend/core/nl2sql_pipeline/dialect.py +139 -0
  33. backend/core/nl2sql_pipeline/intent.py +85 -0
  34. backend/core/nl2sql_pipeline/ir.py +70 -0
  35. backend/core/nl2sql_pipeline/models.py +30 -0
  36. backend/core/nl2sql_pipeline/orchestrator.py +576 -0
  37. backend/core/nl2sql_pipeline/predicates.py +182 -0
  38. backend/core/nl2sql_pipeline/renderer.py +248 -0
  39. backend/core/nl2sql_pipeline/resolution.py +69 -0
  40. backend/core/nl2sql_pipeline/suggestions.py +30 -0
  41. backend/core/nl2sql_pipeline/templates.py +371 -0
  42. backend/core/nl2sql_pipeline/validation.py +28 -0
  43. backend/core/p1_sql_scanner.py +196 -0
  44. backend/core/p2_data_validation.py +72 -0
  45. backend/core/parser.py +280 -0
  46. backend/core/post_processor.py +129 -0
  47. backend/core/rules.py +808 -0
  48. backend/core/semantic_diff.py +385 -0
  49. backend/core/transpiler.py +791 -0
  50. backend/core/type_mapping.json +593 -0
  51. backend/core/type_mapping.py +765 -0
  52. backend/utils/__init__.py +2 -0
  53. backend/utils/validators.py +407 -0
  54. frontend/__init__.py +10 -0
  55. frontend/app_context_v2.py +201 -0
  56. frontend/core/__init__.py +33 -0
  57. frontend/core/design_tokens.py +127 -0
  58. frontend/core/escaping.py +13 -0
  59. frontend/core/navigation.py +135 -0
  60. frontend/core/state.py +612 -0
  61. frontend/core/styles.py +523 -0
  62. frontend/core/themes.py +69 -0
  63. frontend/core/viewmodels.py +499 -0
  64. frontend/pages/__init__.py +0 -0
  65. frontend/pages/convert.py +283 -0
  66. frontend/pages/diff.py +157 -0
  67. frontend/pages/functions.py +100 -0
  68. frontend/pages/history.py +144 -0
  69. frontend/pages/lineage.py +168 -0
  70. frontend/pages/nl2sql.py +305 -0
  71. frontend/pages/query_analysis.py +120 -0
  72. frontend/pages/runtime.py +66 -0
  73. frontend/pages/settings.py +60 -0
  74. frontend/pages/templates.py +67 -0
  75. frontend/pages/types.py +110 -0
  76. frontend/templates_v2.py +53 -0
  77. frontend/ui/__init__.py +0 -0
  78. frontend/ui/command_palette.py +153 -0
  79. frontend/ui/diff.py +275 -0
  80. frontend/ui/editor.py +102 -0
  81. frontend/ui/status.py +255 -0
  82. sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
  83. sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
  84. sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
  85. sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
  86. sql_dialect_master-1.1.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,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"
@@ -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}")
@@ -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
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")