Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions sqlit/domains/explorer/app/schema_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ def list_folder_items(self, folder_type: str, database: str | None) -> list[Any]
cache_key = database or "__default__"
obj_cache = self.object_cache

def cached(key: str, loader: Callable[[], Any]) -> Any:
def cached(key: str, loader: Callable[[], Any], *, allow_empty: bool = True) -> Any:
if cache_key in obj_cache and key in obj_cache[cache_key]:
return obj_cache[cache_key][key]
data = obj_cache[cache_key][key]
if allow_empty or data:
return data
data = loader()
if cache_key not in obj_cache:
obj_cache[cache_key] = {}
Expand All @@ -110,6 +112,7 @@ def cached(key: str, loader: Callable[[], Any]) -> Any:
lambda: inspector.get_tables(self.session.connection, db_arg),
database,
),
allow_empty=self.session.provider.metadata.db_type != "duckdb",
)
return [("table", schema, name) for schema, name in raw_data]
if folder_type == "views":
Expand All @@ -119,6 +122,7 @@ def cached(key: str, loader: Callable[[], Any]) -> Any:
lambda: inspector.get_views(self.session.connection, db_arg),
database,
),
allow_empty=self.session.provider.metadata.db_type != "duckdb",
)
return [("view", schema, name) for schema, name in raw_data]
if folder_type == "databases":
Expand Down
17 changes: 15 additions & 2 deletions sqlit/domains/explorer/ui/mixins/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,21 @@ def on_tree_node_highlighted(self: TreeMixinHost, event: Tree.NodeHighlighted) -

def action_refresh_tree(self: TreeMixinHost) -> None:
"""Refresh the explorer."""
self._refresh_tree_common(notify=True)

def _refresh_tree_after_schema_change(self: TreeMixinHost) -> None:
"""Refresh tree after DDL without showing a notification."""
self._refresh_tree_common(notify=False)

def _refresh_tree_common(self: TreeMixinHost, *, notify: bool) -> None:
self._get_object_cache().clear()
if hasattr(self, "_schema_cache") and "columns" in self._schema_cache:
if hasattr(self, "_schema_cache") and isinstance(self._schema_cache, dict):
self._schema_cache["columns"] = {}
self._schema_cache["tables"] = []
self._schema_cache["views"] = []
self._schema_cache["procedures"] = []
if hasattr(self, "_db_object_cache"):
self._db_object_cache = {}
if hasattr(self, "_loading_nodes"):
self._loading_nodes.clear()
self._schema_service = None
Expand Down Expand Up @@ -241,7 +253,8 @@ def run_loader() -> None:
)
else:
self._schedule_timer(MIN_TIMER_DELAY_S, run_loader)
self.notify("Refreshed")
if notify:
self.notify("Refreshed")

def refresh_tree(self: TreeMixinHost) -> None:
tree_builder.refresh_tree_chunked(self)
Expand Down
36 changes: 36 additions & 0 deletions sqlit/domains/query/ui/mixins/query_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

import re

from typing import TYPE_CHECKING, Any, Callable

from sqlit.domains.explorer.ui.tree import db_switching as tree_db_switching
Expand All @@ -21,6 +23,19 @@
from sqlit.domains.query.app.transaction import TransactionExecutor


_SCHEMA_CHANGE_RE = re.compile(
r"\b(create|alter|drop|truncate|rename|comment|grant|revoke)\b",
re.IGNORECASE,
)
_SQL_COMMENT_RE = re.compile(r"(--[^\n]*|/\*.*?\*/)", re.DOTALL)
_SQL_LITERAL_RE = re.compile(r"('([^']|'')*'|\"([^\"]|\"\")*\"|`[^`]*`|\[[^\]]*\])", re.DOTALL)


def _strip_sql_comments_and_literals(sql: str) -> str:
sql = _SQL_COMMENT_RE.sub(" ", sql)
return _SQL_LITERAL_RE.sub(" ", sql)


class QueryExecutionMixin(ProcessWorkerLifecycleMixin):
"""Mixin providing query execution actions."""

Expand Down Expand Up @@ -216,6 +231,21 @@ def _on_result(confirmed: bool | None) -> None:
_on_result,
)

def _query_changes_schema(self: QueryMixinHost, query: str) -> bool:
cleaned = _strip_sql_comments_and_literals(query)
return bool(_SCHEMA_CHANGE_RE.search(cleaned))

def _maybe_refresh_explorer_after_query(self: QueryMixinHost, query: str) -> None:
if not self._query_changes_schema(query):
return
refresh = getattr(self, "_refresh_tree_after_schema_change", None)
if callable(refresh):
refresh()
return
action = getattr(self, "action_refresh_tree", None)
if callable(action):
action()

def _start_query_spinner(self: QueryMixinHost) -> None:
"""Start the query execution spinner animation."""
import time
Expand Down Expand Up @@ -470,6 +500,7 @@ async def _run_query_async(self: QueryMixinHost, query: str, keep_insert_mode: b
)
else:
self._display_non_query_result(result.rows_affected, elapsed_ms)
self._maybe_refresh_explorer_after_query(query)
if keep_insert_mode:
self._restore_insert_mode()
return
Expand All @@ -489,6 +520,7 @@ async def _run_query_async(self: QueryMixinHost, query: str, keep_insert_mode: b
except Exception:
pass
self._display_multi_statement_results(multi_result, elapsed_ms)
self._maybe_refresh_explorer_after_query(query)
else:
# Single statement - existing behavior
result = await asyncio.to_thread(
Expand All @@ -509,6 +541,7 @@ async def _run_query_async(self: QueryMixinHost, query: str, keep_insert_mode: b
)
else:
self._display_non_query_result(result.rows_affected, elapsed_ms)
self._maybe_refresh_explorer_after_query(query)

if keep_insert_mode:
self._restore_insert_mode()
Expand Down Expand Up @@ -573,14 +606,17 @@ async def _run_query_atomic_async(self: QueryMixinHost, query: str) -> None:
self.notify("Transaction rolled back (error in statement)", severity="error")
else:
self.notify("Query executed atomically (committed)", severity="information")
self._maybe_refresh_explorer_after_query(query)
elif isinstance(result, QueryResult):
await self._display_query_results(
result.columns, result.rows, result.row_count, result.truncated, elapsed_ms
)
self.notify("Query executed atomically (committed)", severity="information")
self._maybe_refresh_explorer_after_query(query)
else:
self._display_non_query_result(result.rows_affected, elapsed_ms)
self.notify("Query executed atomically (committed)", severity="information")
self._maybe_refresh_explorer_after_query(query)

except Exception as e:
self._display_query_error(f"Transaction rolled back: {e}")
Expand Down
Loading
Loading