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,168 @@
|
|
|
1
|
+
"""Lineage Workspace for SQL Dialect Master v2."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sqlglot
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from frontend.app_context_v2 import DIALECTS, get_dialect_label
|
|
8
|
+
from frontend.core.design_tokens import ColorTokens
|
|
9
|
+
from frontend.core.escaping import esc
|
|
10
|
+
from frontend.core.state import SessionState
|
|
11
|
+
from frontend.core.viewmodels import (
|
|
12
|
+
LineageOutputColumn,
|
|
13
|
+
LineageTable,
|
|
14
|
+
LineageViewModel,
|
|
15
|
+
sanitize_mermaid_id,
|
|
16
|
+
sanitize_mermaid_label,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def render_lineage_page(theme: ColorTokens) -> None:
|
|
21
|
+
"""Render the Lineage workspace."""
|
|
22
|
+
state = SessionState.get()
|
|
23
|
+
|
|
24
|
+
st.markdown(
|
|
25
|
+
f"""
|
|
26
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
27
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">Lineage</div>
|
|
28
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
29
|
+
Table dependencies and column references from AST analysis
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
""",
|
|
33
|
+
unsafe_allow_html=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
dialect = st.selectbox(
|
|
37
|
+
"SQL Dialect",
|
|
38
|
+
DIALECTS,
|
|
39
|
+
index=DIALECTS.index("postgres") if "postgres" in DIALECTS else 0,
|
|
40
|
+
format_func=get_dialect_label,
|
|
41
|
+
key="lineage_dialect",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
sql = st.text_area(
|
|
45
|
+
"SQL",
|
|
46
|
+
key="lineage_sql",
|
|
47
|
+
height=180,
|
|
48
|
+
placeholder="SELECT u.name AS user_name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.name",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if st.button("Analyze", type="primary", key="lineage_analyze", use_container_width=True):
|
|
52
|
+
if not sql.strip():
|
|
53
|
+
st.warning("Please enter SQL to analyze.")
|
|
54
|
+
else:
|
|
55
|
+
try:
|
|
56
|
+
vm = _build_lineage(sql, dialect)
|
|
57
|
+
state.lineage_last_vm = vm
|
|
58
|
+
st.rerun()
|
|
59
|
+
except Exception as e: # noqa: BLE001
|
|
60
|
+
st.error(f"Parse error: {esc(str(e))}")
|
|
61
|
+
|
|
62
|
+
vm: LineageViewModel | None = state.lineage_last_vm
|
|
63
|
+
if vm is None:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
col_diagram, col_details = st.columns([2, 1])
|
|
67
|
+
|
|
68
|
+
with col_diagram:
|
|
69
|
+
st.markdown("### Lineage Diagram")
|
|
70
|
+
mermaid = _build_mermaid(vm)
|
|
71
|
+
st.markdown(mermaid)
|
|
72
|
+
|
|
73
|
+
with col_details:
|
|
74
|
+
st.markdown("### Tables")
|
|
75
|
+
for t in vm.tables:
|
|
76
|
+
alias_str = f" as {esc(t.alias)}" if t.alias and t.alias != t.name else ""
|
|
77
|
+
st.caption(f"`{esc(t.name)}`{alias_str}")
|
|
78
|
+
|
|
79
|
+
st.markdown("### Output Columns")
|
|
80
|
+
for oc in vm.output_columns:
|
|
81
|
+
st.caption(f"`{esc(oc.name)}`")
|
|
82
|
+
if oc.expression:
|
|
83
|
+
st.caption(f" Expr: {esc(oc.expression)}")
|
|
84
|
+
|
|
85
|
+
if vm.column_references:
|
|
86
|
+
st.markdown("### Column References")
|
|
87
|
+
for tbl, cols in list(vm.column_references.items())[:5]:
|
|
88
|
+
st.caption(f"**{esc(tbl)}**: {', '.join(esc(c) for c in cols)}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _build_lineage(sql: str, dialect: str) -> LineageViewModel:
|
|
92
|
+
"""Build a LineageViewModel from SQL using sqlglot AST."""
|
|
93
|
+
parsed = sqlglot.parse_one(sql, read=dialect)
|
|
94
|
+
|
|
95
|
+
# Extract tables
|
|
96
|
+
tables = []
|
|
97
|
+
seen = set()
|
|
98
|
+
for t in parsed.find_all(sqlglot.exp.Table):
|
|
99
|
+
name = t.name
|
|
100
|
+
alias = t.alias or name
|
|
101
|
+
if name not in seen:
|
|
102
|
+
tables.append(LineageTable(name=name, alias=alias))
|
|
103
|
+
seen.add(name)
|
|
104
|
+
|
|
105
|
+
# Extract output columns from SELECT
|
|
106
|
+
output_columns = []
|
|
107
|
+
select = parsed.find(sqlglot.exp.Select)
|
|
108
|
+
if select:
|
|
109
|
+
for expr in select.expressions:
|
|
110
|
+
alias = getattr(expr, 'alias', None)
|
|
111
|
+
col_name = alias if alias else getattr(expr, 'name', None)
|
|
112
|
+
if col_name:
|
|
113
|
+
output_columns.append(LineageOutputColumn(
|
|
114
|
+
name=str(col_name),
|
|
115
|
+
expression=str(expr),
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
# Extract column references
|
|
119
|
+
col_refs: dict[str, list[str]] = {}
|
|
120
|
+
for col in parsed.find_all(sqlglot.exp.Column):
|
|
121
|
+
table_ref = col.table or "unknown"
|
|
122
|
+
if table_ref not in col_refs:
|
|
123
|
+
col_refs[table_ref] = []
|
|
124
|
+
if col.name not in col_refs[table_ref]:
|
|
125
|
+
col_refs[table_ref].append(col.name)
|
|
126
|
+
|
|
127
|
+
return LineageViewModel(
|
|
128
|
+
dialect=dialect,
|
|
129
|
+
sql=sql,
|
|
130
|
+
tables=tables,
|
|
131
|
+
output_columns=output_columns,
|
|
132
|
+
column_references=col_refs,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _build_mermaid(vm: LineageViewModel) -> str:
|
|
137
|
+
"""Build a Mermaid flowchart from LineageViewModel."""
|
|
138
|
+
lines = ["```mermaid", "flowchart LR"]
|
|
139
|
+
|
|
140
|
+
# Source tables
|
|
141
|
+
lines.append(" subgraph sources[\"Sources\"]")
|
|
142
|
+
for t in vm.tables:
|
|
143
|
+
safe_id = sanitize_mermaid_id(t.name)
|
|
144
|
+
lines.append(f" {safe_id}[\"{esc(sanitize_mermaid_label(t.name))}\"]")
|
|
145
|
+
lines.append(" end")
|
|
146
|
+
|
|
147
|
+
# Query node
|
|
148
|
+
lines.append(' QUERY{"SQL Query"}')
|
|
149
|
+
|
|
150
|
+
# Output
|
|
151
|
+
lines.append(" subgraph output[\"Output\"]")
|
|
152
|
+
lines.append(' RESULT["Result Set"]')
|
|
153
|
+
for oc in vm.output_columns[:6]:
|
|
154
|
+
safe_id = sanitize_mermaid_id(oc.name)
|
|
155
|
+
lines.append(f" {safe_id}[\"{esc(sanitize_mermaid_label(oc.name))}\"]")
|
|
156
|
+
lines.append(" end")
|
|
157
|
+
|
|
158
|
+
# Edges
|
|
159
|
+
for t in vm.tables:
|
|
160
|
+
safe_id = sanitize_mermaid_id(t.name)
|
|
161
|
+
lines.append(f" {safe_id} --> QUERY")
|
|
162
|
+
lines.append(" QUERY --> RESULT")
|
|
163
|
+
for oc in vm.output_columns[:6]:
|
|
164
|
+
safe_id = sanitize_mermaid_id(oc.name)
|
|
165
|
+
lines.append(f" RESULT --> {safe_id}")
|
|
166
|
+
|
|
167
|
+
lines.append("```")
|
|
168
|
+
return "\n".join(lines)
|
frontend/pages/nl2sql.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""NL2SQL Workspace for SQL Dialect Master v2.
|
|
2
|
+
|
|
3
|
+
Professional workspace for natural-language-to-SQL generation.
|
|
4
|
+
Shows result with confidence, explanation, parsed elements, and
|
|
5
|
+
conversion/diff handoff buttons.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import streamlit as st
|
|
10
|
+
|
|
11
|
+
from frontend.app_context_v2 import DIALECTS, copy_sql, generate_nl2sql, get_dialect_label
|
|
12
|
+
from frontend.core.design_tokens import ColorTokens
|
|
13
|
+
from frontend.core.escaping import esc
|
|
14
|
+
from frontend.core.state import SessionState
|
|
15
|
+
from frontend.core.viewmodels import NL2SQLViewModel
|
|
16
|
+
|
|
17
|
+
# ── Example queries (no emoji) ─────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
EXAMPLE_QUERIES: dict[str, str] = {
|
|
20
|
+
"Count all users": "统计所有用户的数量",
|
|
21
|
+
"Date filter": "查询最近7天的订单",
|
|
22
|
+
"Conditional": "查询金额大于100的订单",
|
|
23
|
+
"Group by": "按部门分组统计员工数量",
|
|
24
|
+
"Order and limit": "查询前10个用户按得分降序排列",
|
|
25
|
+
"Join query": "查询用户和订单关联的数据",
|
|
26
|
+
"Average": "查询员工的平均薪资",
|
|
27
|
+
"Like search": "查询名字包含张的用户",
|
|
28
|
+
"This month": "查询本月所有订单的总金额",
|
|
29
|
+
"Top N": "查询销售额最高的前5个产品",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_nl2sql_page(theme: ColorTokens) -> None:
|
|
34
|
+
"""Render the NL2SQL workspace."""
|
|
35
|
+
state = SessionState.get()
|
|
36
|
+
|
|
37
|
+
_render_header(theme)
|
|
38
|
+
_render_input_section(theme)
|
|
39
|
+
_render_result_section(state, theme)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _render_header(theme: ColorTokens) -> None:
|
|
43
|
+
"""Render the page header."""
|
|
44
|
+
st.markdown(
|
|
45
|
+
f"""
|
|
46
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
47
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
48
|
+
NL2SQL
|
|
49
|
+
</div>
|
|
50
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
51
|
+
Generate SQL from natural language descriptions
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
""",
|
|
55
|
+
unsafe_allow_html=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _render_input_section(theme: ColorTokens) -> None:
|
|
60
|
+
"""Render the input section: NL text, dialect, table hint."""
|
|
61
|
+
col_nl, col_dialect = st.columns([3, 1])
|
|
62
|
+
|
|
63
|
+
with col_nl:
|
|
64
|
+
# Example selector
|
|
65
|
+
examples = list(EXAMPLE_QUERIES.keys())
|
|
66
|
+
selected_example = st.selectbox(
|
|
67
|
+
"Quick example",
|
|
68
|
+
[""] + examples,
|
|
69
|
+
key="nl_example_sel",
|
|
70
|
+
label_visibility="collapsed",
|
|
71
|
+
)
|
|
72
|
+
if selected_example:
|
|
73
|
+
st.session_state.nl_input_text = EXAMPLE_QUERIES[selected_example]
|
|
74
|
+
|
|
75
|
+
st.text_area(
|
|
76
|
+
"Describe your query",
|
|
77
|
+
key="nl_input_text",
|
|
78
|
+
height=100,
|
|
79
|
+
label_visibility="collapsed",
|
|
80
|
+
placeholder="e.g., Get users who placed orders in the last 7 days",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
with col_dialect:
|
|
84
|
+
st.markdown(
|
|
85
|
+
'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
86
|
+
'text-transform:uppercase; color:' + theme.text_muted + '; margin-bottom:6px;">'
|
|
87
|
+
'Target dialect</div>',
|
|
88
|
+
unsafe_allow_html=True,
|
|
89
|
+
)
|
|
90
|
+
st.selectbox(
|
|
91
|
+
"Dialect",
|
|
92
|
+
DIALECTS,
|
|
93
|
+
index=DIALECTS.index("postgres") if "postgres" in DIALECTS else 0,
|
|
94
|
+
format_func=get_dialect_label,
|
|
95
|
+
key="nl_dialect",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Table hint + generate
|
|
99
|
+
col_hint, col_gen = st.columns([3, 1])
|
|
100
|
+
with col_hint:
|
|
101
|
+
st.text_input(
|
|
102
|
+
"Table hint (optional)",
|
|
103
|
+
key="nl_table_hint",
|
|
104
|
+
label_visibility="collapsed",
|
|
105
|
+
placeholder="e.g., users, orders, products",
|
|
106
|
+
)
|
|
107
|
+
with col_gen:
|
|
108
|
+
st.markdown("")
|
|
109
|
+
if st.button("Generate SQL", type="primary", key="nl_generate", use_container_width=True):
|
|
110
|
+
_run_generation(theme)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _run_generation(theme: ColorTokens) -> None:
|
|
114
|
+
"""Run NL2SQL generation and store result."""
|
|
115
|
+
state = SessionState.get()
|
|
116
|
+
nl_input = st.session_state.get("nl_input_text", "")
|
|
117
|
+
dialect = st.session_state.get("nl_dialect", "postgres")
|
|
118
|
+
table_hint = st.session_state.get("nl_table_hint", "")
|
|
119
|
+
|
|
120
|
+
if not nl_input.strip():
|
|
121
|
+
st.warning("Please describe your query.")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
with st.spinner("Generating SQL..."):
|
|
125
|
+
result = generate_nl2sql(nl_input, dialect, table_hint or None)
|
|
126
|
+
|
|
127
|
+
vm = NL2SQLViewModel.from_nl2sql_result(result)
|
|
128
|
+
state.nl_last_vm = vm
|
|
129
|
+
state.nl_generation_error = None
|
|
130
|
+
st.rerun()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _render_result_section(state: SessionState, theme: ColorTokens) -> None:
|
|
134
|
+
"""Render the generation result."""
|
|
135
|
+
vm: NL2SQLViewModel | None = state.nl_last_vm
|
|
136
|
+
error: str | None = state.nl_generation_error
|
|
137
|
+
|
|
138
|
+
if error:
|
|
139
|
+
st.error(f"Generation failed: {esc(error)}")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if vm is None:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
if not vm.success:
|
|
146
|
+
st.error(f"Generation failed: {esc(vm.explanation or 'No SQL produced')}")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
_render_generated_sql(vm, theme)
|
|
150
|
+
_render_confidence(vm, theme)
|
|
151
|
+
_render_explanation(vm, theme)
|
|
152
|
+
_render_parsed_elements(vm, theme)
|
|
153
|
+
_render_action_buttons(vm, theme)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _render_generated_sql(vm: NL2SQLViewModel, theme: ColorTokens) -> None:
|
|
157
|
+
"""Render the generated SQL code block."""
|
|
158
|
+
sql = vm.clean_sql()
|
|
159
|
+
if not sql:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
st.markdown(
|
|
163
|
+
'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
164
|
+
'text-transform:uppercase; color:' + theme.text_muted + '; margin:16px 0 8px;">'
|
|
165
|
+
'Generated SQL</div>',
|
|
166
|
+
unsafe_allow_html=True,
|
|
167
|
+
)
|
|
168
|
+
st.code(sql, language="sql")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _render_confidence(vm: NL2SQLViewModel, theme: ColorTokens) -> None:
|
|
172
|
+
"""Render the overall confidence indicator."""
|
|
173
|
+
level_colors = {
|
|
174
|
+
"high": theme.success,
|
|
175
|
+
"review": theme.warning,
|
|
176
|
+
"low": theme.danger,
|
|
177
|
+
}
|
|
178
|
+
color = level_colors.get(vm.confidence_level, theme.text_muted)
|
|
179
|
+
|
|
180
|
+
st.markdown(
|
|
181
|
+
'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
182
|
+
'text-transform:uppercase; color:' + theme.text_muted + '; margin:12px 0 8px;">'
|
|
183
|
+
'Confidence</div>',
|
|
184
|
+
unsafe_allow_html=True,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
st.markdown(
|
|
188
|
+
f"""
|
|
189
|
+
<div style="display:flex; align-items:center; gap:12px; padding:8px 12px;
|
|
190
|
+
background:{color}12; border:1px solid {color}30; border-radius:6px;">
|
|
191
|
+
<span style="font-size:20px; font-weight:700; color:{color}; font-family:monospace;">
|
|
192
|
+
{vm.confidence:.0%}
|
|
193
|
+
</span>
|
|
194
|
+
<span style="font-size:12px; color:{color}; font-weight:500;">
|
|
195
|
+
{esc(vm.confidence_label)}
|
|
196
|
+
</span>
|
|
197
|
+
<span style="font-size:11px; color:{theme.text_muted}; margin-left:auto;">
|
|
198
|
+
Overall
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
""",
|
|
202
|
+
unsafe_allow_html=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _render_explanation(vm: NL2SQLViewModel, theme: ColorTokens) -> None:
|
|
207
|
+
"""Render the generation explanation."""
|
|
208
|
+
if not vm.explanation:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
st.markdown(
|
|
212
|
+
'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
213
|
+
'text-transform:uppercase; color:' + theme.text_muted + '; margin:12px 0 8px;">'
|
|
214
|
+
'Explanation</div>',
|
|
215
|
+
unsafe_allow_html=True,
|
|
216
|
+
)
|
|
217
|
+
st.info(esc(vm.explanation))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _render_parsed_elements(vm: NL2SQLViewModel, theme: ColorTokens) -> None:
|
|
221
|
+
"""Render parsed elements: tables, columns, values."""
|
|
222
|
+
has_elements = vm.tables or vm.columns or vm.values or vm.token_count > 0
|
|
223
|
+
|
|
224
|
+
if not has_elements:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
st.markdown(
|
|
228
|
+
'<div style="font-size:11px; font-weight:600; letter-spacing:0.06em; '
|
|
229
|
+
'text-transform:uppercase; color:' + theme.text_muted + '; margin:12px 0 8px;">'
|
|
230
|
+
'Parsed Elements</div>',
|
|
231
|
+
unsafe_allow_html=True,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
cols = st.columns(4)
|
|
235
|
+
with cols[0]:
|
|
236
|
+
if vm.tables:
|
|
237
|
+
st.markdown("**Tables**")
|
|
238
|
+
for t in vm.tables[:5]:
|
|
239
|
+
st.caption(f"`{esc(t)}`")
|
|
240
|
+
else:
|
|
241
|
+
st.caption("—")
|
|
242
|
+
|
|
243
|
+
with cols[1]:
|
|
244
|
+
if vm.columns:
|
|
245
|
+
st.markdown("**Columns**")
|
|
246
|
+
for c in vm.columns[:5]:
|
|
247
|
+
st.caption(f"`{esc(c)}`")
|
|
248
|
+
else:
|
|
249
|
+
st.caption("—")
|
|
250
|
+
|
|
251
|
+
with cols[2]:
|
|
252
|
+
if vm.values:
|
|
253
|
+
st.markdown("**Values**")
|
|
254
|
+
for v in vm.values[:5]:
|
|
255
|
+
st.caption(f"`{esc(v)}`")
|
|
256
|
+
else:
|
|
257
|
+
st.caption("—")
|
|
258
|
+
|
|
259
|
+
with cols[3]:
|
|
260
|
+
st.markdown("**Metadata**")
|
|
261
|
+
if vm.is_chinese:
|
|
262
|
+
st.caption("Language: Chinese")
|
|
263
|
+
st.caption(f"Tokens: {vm.token_count}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _render_action_buttons(vm: NL2SQLViewModel, theme: ColorTokens) -> None:
|
|
267
|
+
"""Render action buttons: Copy, Download, Open in Converter, Semantic Diff."""
|
|
268
|
+
sql = vm.clean_sql()
|
|
269
|
+
if not sql:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
col_copy, col_dl, col_convert, col_diff = st.columns(4)
|
|
273
|
+
|
|
274
|
+
with col_copy:
|
|
275
|
+
copy_sql(sql)
|
|
276
|
+
|
|
277
|
+
with col_dl:
|
|
278
|
+
st.download_button("Download", sql, "generated.sql", mime="text/sql")
|
|
279
|
+
|
|
280
|
+
with col_convert:
|
|
281
|
+
if st.button("Open in Converter", key="nl_to_convert", use_container_width=True):
|
|
282
|
+
from frontend.core.navigation import create_navigation_intent
|
|
283
|
+
intent = create_navigation_intent(
|
|
284
|
+
target_page="convert",
|
|
285
|
+
action="open_conversion",
|
|
286
|
+
sql=sql,
|
|
287
|
+
dialect=vm.dialect,
|
|
288
|
+
origin="nl2sql",
|
|
289
|
+
)
|
|
290
|
+
st.session_state.sdm_navigation_intent = intent
|
|
291
|
+
st.rerun()
|
|
292
|
+
|
|
293
|
+
with col_diff:
|
|
294
|
+
if st.button("Semantic Diff", key="nl_to_diff", use_container_width=True):
|
|
295
|
+
from frontend.core.navigation import create_navigation_intent
|
|
296
|
+
intent = create_navigation_intent(
|
|
297
|
+
target_page="diff",
|
|
298
|
+
action="open_diff",
|
|
299
|
+
source=sql,
|
|
300
|
+
target=sql,
|
|
301
|
+
src_dialect=vm.dialect,
|
|
302
|
+
tgt_dialect=vm.dialect,
|
|
303
|
+
)
|
|
304
|
+
st.session_state.sdm_navigation_intent = intent
|
|
305
|
+
st.rerun()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Query Analysis (formerly Explain) Workspace for SQL Dialect Master v2.
|
|
2
|
+
|
|
3
|
+
This is static analysis only — no real execution plans are generated.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sqlglot
|
|
8
|
+
import streamlit as st
|
|
9
|
+
|
|
10
|
+
from frontend.app_context_v2 import DIALECTS, get_dialect_label
|
|
11
|
+
from frontend.core.design_tokens import ColorTokens
|
|
12
|
+
from frontend.core.escaping import esc
|
|
13
|
+
from frontend.core.state import SessionState
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def render_query_analysis_page(theme: ColorTokens) -> None:
|
|
17
|
+
"""Render the Query Analysis workspace."""
|
|
18
|
+
state = SessionState.get()
|
|
19
|
+
|
|
20
|
+
st.markdown(
|
|
21
|
+
f"""
|
|
22
|
+
<div style="padding:12px 0; margin-bottom:16px; border-bottom:1px solid {theme.border};">
|
|
23
|
+
<div style="font-size:18px; font-weight:700; color:{theme.text_primary};">
|
|
24
|
+
Query Analysis
|
|
25
|
+
</div>
|
|
26
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
27
|
+
Static structural analysis of SQL queries
|
|
28
|
+
</div>
|
|
29
|
+
<div style="font-size:11px; color:{theme.warning}; margin-top:4px;">
|
|
30
|
+
Note: This is static analysis only — not a real execution plan.
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
""",
|
|
34
|
+
unsafe_allow_html=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
dialect = st.selectbox(
|
|
38
|
+
"SQL Dialect",
|
|
39
|
+
DIALECTS,
|
|
40
|
+
index=DIALECTS.index("postgres") if "postgres" in DIALECTS else 0,
|
|
41
|
+
format_func=get_dialect_label,
|
|
42
|
+
key="qa_dialect",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
sql = st.text_area(
|
|
46
|
+
"SQL",
|
|
47
|
+
key="qa_sql",
|
|
48
|
+
height=150,
|
|
49
|
+
placeholder="SELECT u.name, COUNT(o.id) AS order_count FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name ORDER BY order_count DESC LIMIT 10",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if st.button("Analyze", type="primary", key="qa_analyze", use_container_width=True):
|
|
53
|
+
if not sql.strip():
|
|
54
|
+
st.warning("Please enter SQL to analyze.")
|
|
55
|
+
else:
|
|
56
|
+
try:
|
|
57
|
+
result = _analyze_sql(sql, dialect)
|
|
58
|
+
state.qa_last_result = result
|
|
59
|
+
st.rerun()
|
|
60
|
+
except Exception as e: # noqa: BLE001
|
|
61
|
+
st.error(f"Parse error: {esc(str(e))}")
|
|
62
|
+
|
|
63
|
+
result = state.qa_last_result
|
|
64
|
+
if result is None:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Display results
|
|
68
|
+
cols = st.columns(4)
|
|
69
|
+
with cols[0]:
|
|
70
|
+
st.metric("Tables", len(result["tables"]))
|
|
71
|
+
with cols[1]:
|
|
72
|
+
st.metric("Joins", result["join_count"])
|
|
73
|
+
with cols[2]:
|
|
74
|
+
st.metric("Aggregation", "Yes" if result["has_aggregation"] else "No")
|
|
75
|
+
with cols[3]:
|
|
76
|
+
st.metric("Sorting", "Yes" if result["has_sorting"] else "No")
|
|
77
|
+
|
|
78
|
+
if result["tables"]:
|
|
79
|
+
st.markdown("##### Tables")
|
|
80
|
+
for t in result["tables"]:
|
|
81
|
+
st.caption(f"`{esc(t)}`")
|
|
82
|
+
|
|
83
|
+
if result["hints"]:
|
|
84
|
+
st.markdown("##### Dialect Characteristics")
|
|
85
|
+
st.caption("These are general characteristics of how this dialect handles queries, not optimizer output.")
|
|
86
|
+
for hint in result["hints"][:5]:
|
|
87
|
+
st.info(esc(hint))
|
|
88
|
+
|
|
89
|
+
if result.get("limit"):
|
|
90
|
+
st.markdown(f'**Limit:** `{esc(result["limit"])}`')
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _analyze_sql(sql: str, dialect: str) -> dict:
|
|
94
|
+
"""Perform static analysis on SQL."""
|
|
95
|
+
parsed = sqlglot.parse_one(sql, read=dialect)
|
|
96
|
+
|
|
97
|
+
tables = list({t.name for t in parsed.find_all(sqlglot.exp.Table)})
|
|
98
|
+
joins = list(parsed.find_all(sqlglot.exp.Join))
|
|
99
|
+
groups = list(parsed.find_all(sqlglot.exp.Group))
|
|
100
|
+
orders = list(parsed.find_all(sqlglot.exp.Order))
|
|
101
|
+
limit = parsed.find(sqlglot.exp.Limit)
|
|
102
|
+
|
|
103
|
+
dialect_hints: dict[str, list[str]] = {
|
|
104
|
+
"postgres": ["Seq/Index scan", "Hash/Merge join", "Parallel workers"],
|
|
105
|
+
"mysql": ["Index scan", "Nested loop join", "InnoDB buffer pool"],
|
|
106
|
+
"hive": ["MapReduce/Tez job", "Shuffle for GROUP BY"],
|
|
107
|
+
"spark": ["Exchange shuffle", "HashAggregate", "Broadcast join"],
|
|
108
|
+
"snowflake": ["Virtual warehouse", "Micro-partition pruning"],
|
|
109
|
+
"duckdb": ["Vectorized execution", "Parallel pipeline"],
|
|
110
|
+
}
|
|
111
|
+
hints = dialect_hints.get(dialect, ["Standard processing"])
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"tables": tables,
|
|
115
|
+
"join_count": len(joins),
|
|
116
|
+
"has_aggregation": bool(groups),
|
|
117
|
+
"has_sorting": bool(orders),
|
|
118
|
+
"limit": str(limit.expression) if limit else None,
|
|
119
|
+
"hints": hints,
|
|
120
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Runtime Verification page for SQL Dialect Master v2.
|
|
2
|
+
|
|
3
|
+
This page is a placeholder — real runtime verification requires a
|
|
4
|
+
connected database, which is not currently available in the Streamlit
|
|
5
|
+
environment. The UI clearly signals "not available" rather than
|
|
6
|
+
fabricating results.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import streamlit as st
|
|
11
|
+
|
|
12
|
+
from frontend.core.design_tokens import ColorTokens
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_runtime_page(theme: ColorTokens) -> None:
|
|
16
|
+
"""Render the Runtime Verification page."""
|
|
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
|
+
Runtime Verification
|
|
23
|
+
</div>
|
|
24
|
+
<div style="font-size:12px; color:{theme.text_muted}; margin-top:2px;">
|
|
25
|
+
Execute SQL against connected databases to verify semantics
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
""",
|
|
29
|
+
unsafe_allow_html=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Not available notice
|
|
33
|
+
st.markdown(
|
|
34
|
+
f"""
|
|
35
|
+
<div style="padding:24px; border:1px dashed {theme.border}; border-radius:8px;
|
|
36
|
+
text-align:center; color:{theme.text_muted};">
|
|
37
|
+
<div style="font-size:16px; font-weight:600; margin-bottom:8px;">
|
|
38
|
+
Not Available
|
|
39
|
+
</div>
|
|
40
|
+
<div style="font-size:12px;">
|
|
41
|
+
Runtime verification requires a connected database. This feature
|
|
42
|
+
is available via the API when a database connection is configured.
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
""",
|
|
46
|
+
unsafe_allow_html=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Architecture info
|
|
50
|
+
st.markdown("---")
|
|
51
|
+
st.markdown("### Architecture")
|
|
52
|
+
st.markdown(
|
|
53
|
+
f"""
|
|
54
|
+
<div style="font-size:12px; color:{theme.text_secondary}; line-height:1.8;">
|
|
55
|
+
<p>When a database connection is available, the Runtime workspace will:</p>
|
|
56
|
+
<ul style="margin:8px 0 8px 20px; padding:0;">
|
|
57
|
+
<li>Execute source and target SQL against connected databases</li>
|
|
58
|
+
<li>Compare result sets for semantic equivalence</li>
|
|
59
|
+
<li>Report execution time and row count differences</li>
|
|
60
|
+
<li>Flag cases where AST analysis was insufficient</li>
|
|
61
|
+
</ul>
|
|
62
|
+
<p style="color:{theme.text_muted};">Current status: No database connections configured.</p>
|
|
63
|
+
</div>
|
|
64
|
+
""",
|
|
65
|
+
unsafe_allow_html=True,
|
|
66
|
+
)
|