Spaces:
Running
Running
| import duckdb | |
| import pandas as pd | |
| import streamlit as st | |
| import time | |
| from datetime import datetime | |
| from utils.data_utils import get_numeric_columns, get_categorical_columns, get_datetime_columns | |
| from services.sql_service import execute_sql_query, get_table_schema | |
| from utils.ui_utils import log_change | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| SECTION_HEADER_CLASS = "section-header" | |
| def render_sql_tab(df) -> None: | |
| """ | |
| SQL Query Interface - allows users to query their data using SQL | |
| """ | |
| # Check if df is None or empty | |
| # If it's a DatasetContext, check its filtered_df property | |
| if df is None: | |
| st.warning("β οΈ No data loaded. Please upload data first.") | |
| return | |
| # Check if it's a DatasetContext object and get the DataFrame | |
| if hasattr(df, 'filtered_df'): | |
| df_to_use = df.filtered_df | |
| else: | |
| df_to_use = df | |
| if df_to_use.empty: | |
| st.warning("β οΈ No data loaded. Please upload data first.") | |
| return | |
| st.markdown(f'<div class="{SECTION_HEADER_CLASS}">π Dynamic SQL Studio</div>', unsafe_allow_html=True) | |
| st.caption("Interactive SQL editor with data exploration tools β powered by DuckDB") | |
| # Register the DataFrame as a table | |
| conn = duckdb.connect() | |
| conn.register("data", df_to_use) | |
| # Sidebar with data exploration tools | |
| with st.sidebar: | |
| st.markdown("### π οΈ SQL Toolkit") | |
| # Data preview | |
| with st.expander("π Data Preview", expanded=False): | |
| st.write(f"Shape: {df_to_use.shape[0]} Γ {df_to_use.shape[1]}") | |
| st.dataframe(df_to_use.head(3), use_container_width=True) | |
| # Schema explorer | |
| with st.expander("π Schema Explorer", expanded=True): | |
| try: | |
| schema = conn.execute("DESCRIBE data").fetchdf() | |
| st.dataframe(schema, use_container_width=True) | |
| # Column type summary | |
| col_types = df_to_use.dtypes.value_counts() | |
| st.write("**Column Types Summary:**") | |
| for dtype, count in col_types.items(): | |
| st.caption(f"{dtype}: {count} columns") | |
| except Exception as e: | |
| st.error(f"Schema error: {e}") | |
| # Quick stats | |
| with st.expander("π Quick Stats", expanded=False): | |
| st.write("**Numeric Columns:**") | |
| num_cols = get_numeric_columns(df_to_use) | |
| if num_cols: | |
| st.write(", ".join(num_cols[:5])) # Show first 5 | |
| if len(num_cols) > 5: | |
| st.caption(f"... and {len(num_cols)-5} more") | |
| else: | |
| st.info("No numeric columns found") | |
| st.write("**Categorical Columns:**") | |
| cat_cols = get_categorical_columns(df_to_use) | |
| if cat_cols: | |
| st.write(", ".join(cat_cols[:5])) # Show first 5 | |
| if len(cat_cols) > 5: | |
| st.caption(f"... and {len(cat_cols)-5} more") | |
| else: | |
| st.info("No categorical columns found") | |
| # Main content area | |
| tab1, tab2, tab3, tab4 = st.tabs([ | |
| "βοΈ Query Editor", | |
| "π‘ Query Builder", | |
| "π Visualization", | |
| "πΎ History & Export" | |
| ]) | |
| # Initialize session state for query history | |
| if 'query_history' not in st.session_state: | |
| st.session_state.query_history = [] | |
| with tab1: # Query Editor | |
| # Layout for query editor | |
| editor_col1, editor_col2 = st.columns([4, 1]) | |
| with editor_col1: | |
| query = st.text_area( | |
| "SQL Query", | |
| value="SELECT *\nFROM data\nLIMIT 100", | |
| height=300, | |
| key="sql_query", | |
| placeholder="Enter your SQL query here...\n\nExample: SELECT column1, AVG(column2) FROM data GROUP BY column1" | |
| ) | |
| with editor_col2: | |
| st.write("") # Spacer | |
| run = st.button("βΆ Execute Query", type="primary", use_container_width=True) | |
| if st.button("π§Ή Clear", use_container_width=True): | |
| st.session_state.sql_query = "SELECT *\nFROM data\nLIMIT 100" | |
| # Execute query | |
| if run and query.strip(): | |
| try: | |
| start_time = time.time() | |
| result = conn.execute(query).fetchdf() | |
| execution_time = time.time() - start_time | |
| # Add to history | |
| st.session_state.query_history.append({ | |
| 'query': query.strip(), | |
| 'rows': len(result), | |
| 'time': execution_time, | |
| 'timestamp': datetime.now().strftime("%H:%M:%S") | |
| }) | |
| # Stats | |
| st.success(f"β Query executed in {execution_time:.2f}s. {len(result):,} rows returned.") | |
| stats_col1, stats_col2, stats_col3, stats_col4 = st.columns(4) | |
| with stats_col1: | |
| st.metric("Rows", f"{len(result):,}") | |
| with stats_col2: | |
| st.metric("Cols", f"{len(result.columns)}") | |
| with stats_col3: | |
| st.metric("Time", f"{execution_time:.2f}s") | |
| with stats_col4: | |
| st.metric("Size", f"{result.memory_usage(deep=True).sum() / 1024:.1f} KB") | |
| # Results display | |
| st.subheader("Query Results") | |
| # Show different views based on data size | |
| if len(result) > 10000: | |
| st.warning(f"Large result set ({len(result):,} rows) detected. Showing first 10,000 rows.") | |
| result_display = result.head(10000) | |
| elif len(result) > 1000: | |
| result_display = result | |
| else: | |
| result_display = result | |
| # Interactive dataframe with copy/paste | |
| st.dataframe(result_display, use_container_width=True, height=400) | |
| log_change("Executed SQL query", f"Rows returned: {len(result)}, Execution time: {execution_time:.2f}s") | |
| except Exception as e: | |
| st.error(f"β Query failed: {e}") | |
| logger.error(f"SQL query error: {e}") | |
| with tab2: # Query Builder | |
| st.markdown("### ποΈ Visual Query Builder") | |
| # Query building components | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| selected_cols = st.multiselect( | |
| "Select Columns", | |
| options=["*"] + list(df_to_use.columns), | |
| default=["*"] | |
| ) | |
| with col2: | |
| agg_options = { | |
| "None": "", | |
| "COUNT": "COUNT({})", | |
| "SUM": "SUM({})", | |
| "AVG": "AVG({})", | |
| "MIN": "MIN({})", | |
| "MAX": "MAX({})" | |
| } | |
| aggregation = st.selectbox("Aggregation", options=list(agg_options.keys())) | |
| # Where clause builder | |
| st.subheader("FilterWhere Conditions") | |
| where_col1, where_op1, where_val1 = st.columns(3) | |
| with where_col1: | |
| where_col = st.selectbox("Column", options=[""] + list(df_to_use.columns), key="where_col") | |
| with where_op1: | |
| operator = st.selectbox("Operator", ["=", "!=", ">", "<", ">=", "<=", "IN", "LIKE"], key="where_op") | |
| with where_val1: | |
| if where_col: | |
| # Provide appropriate input based on column type | |
| if df_to_use[where_col].dtype in ['int64', 'float64']: | |
| where_val = st.number_input("Value", key="where_val") | |
| else: | |
| where_val = st.text_input("Value", key="where_val") | |
| else: | |
| where_val = "" | |
| # Group by | |
| group_by_cols = st.multiselect("GROUP BY", options=[""] + list(df_to_use.columns), key="group_by") | |
| # Order by | |
| order_col, order_dir = st.columns(2) | |
| with order_col: | |
| order_by = st.selectbox("ORDER BY", options=[""] + list(df_to_use.columns), key="order_by") | |
| with order_dir: | |
| order_direction = st.selectbox("Direction", ["ASC", "DESC"], key="order_dir") | |
| # Limit | |
| limit = st.slider("LIMIT", 1, 10000, 100, key="limit") | |
| # Generate query button | |
| if st.button("β‘ Generate Query"): | |
| # Build query | |
| if selected_cols: | |
| if "*" in selected_cols: | |
| select_clause = "*" | |
| else: | |
| if aggregation != "None" and selected_cols: | |
| select_clause = ", ".join([agg_options[aggregation].format(col) for col in selected_cols if col]) | |
| else: | |
| select_clause = ", ".join(selected_cols) | |
| else: | |
| select_clause = "*" | |
| query_parts = [f"SELECT {select_clause}", "FROM data"] | |
| if where_col and where_val: | |
| if operator == "IN": | |
| query_parts.append(f"WHERE {where_col} IN ({where_val})") | |
| elif operator == "LIKE": | |
| query_parts.append(f"WHERE {where_col} LIKE '%{where_val}%'") | |
| else: | |
| query_parts.append(f"WHERE {where_col} {operator} {repr(where_val) if isinstance(where_val, str) else where_val}") | |
| if group_by_cols and any(group_by_cols): | |
| query_parts.append(f"GROUP BY {', '.join([col for col in group_by_cols if col])}") | |
| if order_by: | |
| query_parts.append(f"ORDER BY {order_by} {order_direction}") | |
| query_parts.append(f"LIMIT {limit}") | |
| generated_query = " ".join(query_parts) | |
| # Set the generated query in the main editor | |
| st.session_state.sql_query = generated_query | |
| st.code(generated_query, language="sql") | |
| with tab3: # Visualization | |
| st.markdown("### π Data Visualization") | |
| if 'result' in locals() and not locals()['result'].empty: | |
| viz_data = locals()['result'] | |
| elif 'result_display' in locals() and not locals()['result_display'].empty: | |
| viz_data = locals()['result_display'] | |
| else: | |
| st.info("Run a query in the Query Editor tab to visualize results") | |
| st.stop() | |
| viz_col1, viz_col2 = st.columns(2) | |
| with viz_col1: | |
| x_axis = st.selectbox("X-Axis", options=viz_data.columns) | |
| with viz_col2: | |
| y_axis = st.selectbox("Y-Axis", options=["None"] + list(viz_data.columns)) | |
| chart_type = st.selectbox("Chart Type", ["Line", "Bar", "Scatter", "Histogram"]) | |
| if x_axis: | |
| import plotly.express as px | |
| if chart_type == "Line": | |
| if y_axis and y_axis != "None": | |
| fig = px.line(viz_data, x=x_axis, y=y_axis, title=f"Line Chart: {x_axis} vs {y_axis}") | |
| else: | |
| fig = px.line(viz_data, x=x_axis, title=f"Line Chart: {x_axis}") | |
| elif chart_type == "Bar": | |
| if y_axis and y_axis != "None": | |
| fig = px.bar(viz_data, x=x_axis, y=y_axis, title=f"Bar Chart: {x_axis} vs {y_axis}") | |
| else: | |
| # Count occurrences of x-axis values | |
| counts = viz_data[x_axis].value_counts().reset_index() | |
| counts.columns = [x_axis, 'count'] | |
| fig = px.bar(counts, x=x_axis, y='count', title=f"Bar Chart: {x_axis} Counts") | |
| elif chart_type == "Scatter": | |
| if y_axis and y_axis != "None": | |
| fig = px.scatter(viz_data, x=x_axis, y=y_axis, title=f"Scatter Plot: {x_axis} vs {y_axis}") | |
| else: | |
| st.warning("Scatter plots require both X and Y axes") | |
| elif chart_type == "Histogram": | |
| fig = px.histogram(viz_data, x=x_axis, title=f"Histogram: {x_axis}") | |
| if 'fig' in locals(): | |
| st.plotly_chart(fig, use_container_width=True) | |
| with tab4: # History & Export | |
| st.markdown("### π Query History") | |
| if st.session_state.query_history: | |
| # Show recent queries | |
| for i, record in enumerate(reversed(st.session_state.query_history[-10:])): # Last 10 queries | |
| with st.expander(f"Query at {record['timestamp']} ({record['rows']} rows, {record['time']:.2f}s)", expanded=False): | |
| st.code(record['query'], language='sql') | |
| if st.button(f"Reuse Query #{len(st.session_state.query_history)-i}", key=f"reuse_{i}"): | |
| st.session_state.sql_query = record['query'] | |
| st.success("Query copied to editor!") | |
| else: | |
| st.info("No queries executed yet") | |
| st.markdown("### πΎ Export Options") | |
| # Export query results if available | |
| if 'result' in locals() and not locals()['result'].empty: | |
| result_export = locals()['result'] | |
| export_col1, export_col2, export_col3 = st.columns(3) | |
| with export_col1: | |
| csv = result_export.to_csv(index=False) | |
| st.download_button( | |
| label="π₯ CSV Export", | |
| data=csv, | |
| file_name=f"sql_query_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", | |
| mime="text/csv" | |
| ) | |
| with export_col2: | |
| json_str = result_export.to_json(orient='records', date_format='iso', indent=2) | |
| st.download_button( | |
| label="π₯ JSON Export", | |
| data=json_str, | |
| file_name=f"sql_query_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", | |
| mime="application/json" | |
| ) | |
| with export_col3: | |
| try: | |
| from io import BytesIO | |
| buffer = BytesIO() | |
| with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer: | |
| result_export.to_excel(writer, index=False, sheet_name='QueryResult') | |
| buffer.seek(0) | |
| st.download_button( | |
| label="π₯ Excel Export", | |
| data=buffer.getvalue(), | |
| file_name=f"sql_query_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx", | |
| mime="application/vnd.ms-excel" | |
| ) | |
| except ImportError: | |
| st.info("Install `xlsxwriter` for Excel export") | |
| else: | |
| st.info("Run a query to enable export options") | |
| conn.close() |