Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b28a2df
Add smart SQL autocomplete with context-aware suggestions
Maxteabag Dec 26, 2025
05ea429
fix: prompt for password when connection password is empty
Maxteabag Dec 26, 2025
442561b
fix: add Azure SQL Database compatibility for MSSQL adapter
Maxteabag Dec 26, 2025
f5b4a0c
refactor: split SQL completion into separate modules by statement type
Maxteabag Dec 26, 2025
1f70964
fix: don't show completions without SQL context
Maxteabag Dec 26, 2025
d250dbe
fix: use correct database context when clicking tables in tree
Maxteabag Dec 26, 2025
c2f8cd4
fix: use fully qualified table names in autocomplete when no default …
Maxteabag Dec 26, 2025
7969efc
feat: idle scheduler for background schema loading and UX improvements
Maxteabag Dec 26, 2025
47c776e
Enter key dismisses autocomplete instead of accepting
Maxteabag Dec 26, 2025
1fee591
Persist active database preference across sessions
Maxteabag Dec 26, 2025
4de1071
Add system_databases property per adapter and fix ESC behavior
Maxteabag Dec 26, 2025
645bb3d
Fix ESC key priority on modal screens
Maxteabag Dec 26, 2025
6605b40
Fix error display in results table
Maxteabag Dec 26, 2025
512d9f8
Add supports_cross_database_queries property and auto-switch databases
Maxteabag Dec 26, 2025
065142e
Remove Azure SQL adapter - use MSSQL with automatic fallback
Maxteabag Dec 26, 2025
2a6e68e
Handle cross-db capability in autocomplete
Maxteabag Dec 27, 2025
82c74f9
Handle ESC in leader menu
Maxteabag Dec 27, 2025
40336c7
Add tests for autocomplete database modes
Maxteabag Dec 27, 2025
45781f5
Add autocomplete demo gifs
Maxteabag Dec 27, 2025
29c4fb0
Hide autocomplete after statement terminator (semicolon)
Maxteabag Dec 27, 2025
31f9e35
Remove db caching
Maxteabag Dec 27, 2025
6758c07
Merge branch 'main' into autocomplete
Maxteabag Dec 27, 2025
2935aac
Fix password prompt for explicitly empty passwords
Maxteabag Dec 27, 2025
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
Binary file added demos/autocomplete-01-basics.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/autocomplete-02-select-from.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/autocomplete-03-joins.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demos/autocomplete-04-clauses.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies = [
"pyperclip>=1.8.2",
"keyring>=24.0.0",
"docker>=7.0.0",
"sqlparse>=0.5.0",
]
dynamic = ["version"]

Expand Down
42 changes: 36 additions & 6 deletions sqlit/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from textual.lazy import Lazy
from textual.screen import ModalScreen
from textual.timer import Timer
from textual.widgets import Static, TextArea, Tree
from textual.widgets import Static, Tree

from .widgets import QueryTextArea
from textual.worker import Worker

from .config import (
Expand Down Expand Up @@ -51,6 +53,7 @@
TreeFilterInput,
VimMode,
)
from .idle_scheduler import init_idle_scheduler, on_user_activity, IdleScheduler


class SSMSTUI(
Expand Down Expand Up @@ -208,6 +211,17 @@ class SSMSTUI(
padding: 0 1;
}

#idle-scheduler-bar {
height: 1;
background: $primary-darken-3;
padding: 0 1;
display: none;
}

#idle-scheduler-bar.visible {
display: block;
}

#sidebar,
#query-area,
#results-area {
Expand Down Expand Up @@ -260,7 +274,6 @@ class SSMSTUI(
Binding("z", "collapse_tree", "Collapse", show=False),
Binding("j", "tree_cursor_down", "Down", show=False),
Binding("k", "tree_cursor_up", "Up", show=False),
Binding("u", "use_database", "Use as default", show=False),
Binding("v", "view_cell", "View cell", show=False),
Binding("u", "edit_cell", "Update cell", show=False),
Binding("h", "results_cursor_left", "Left", show=False),
Expand Down Expand Up @@ -297,6 +310,7 @@ def __init__(
self._startup_connection = startup_connection
self._startup_connect_config: ConnectionConfig | None = None
self._debug_mode = os.environ.get("SQLIT_DEBUG") == "1"
self._debug_idle_scheduler = os.environ.get("SQLIT_DEBUG_IDLE_SCHEDULER") == "1"
self._startup_profile = os.environ.get("SQLIT_PROFILE_STARTUP") == "1"
self._startup_mark = self._parse_startup_mark(os.environ.get("SQLIT_STARTUP_MARK"))
self._startup_init_time = time.perf_counter()
Expand Down Expand Up @@ -355,7 +369,9 @@ def __init__(
self._state_machine = UIStateMachine()
self._session_factory: Any | None = None
self._last_query_table: dict | None = None
# Omarchy theme sync state
self._query_target_database: str | None = None # Target DB for auto-generated queries
# Idle scheduler for background work
self._idle_scheduler: IdleScheduler | None = None

if mock_profile:
self._session_factory = self._create_mock_session_factory(mock_profile)
Expand Down Expand Up @@ -387,8 +403,8 @@ def object_tree(self) -> Tree:
return self.query_one("#object-tree", Tree)

@property
def query_input(self) -> TextArea:
return self.query_one("#query-input", TextArea)
def query_input(self) -> QueryTextArea:
return self.query_one("#query-input", QueryTextArea)

@property
def results_table(self) -> SqlitDataTable:
Expand Down Expand Up @@ -416,6 +432,10 @@ def results_area(self) -> Any:
def status_bar(self) -> Static:
return self.query_one("#status-bar", Static)

@property
def idle_scheduler_bar(self) -> Static:
return self.query_one("#idle-scheduler-bar", Static)

@property
def autocomplete_dropdown(self) -> Any:
from .widgets import AutocompleteDropdown
Expand Down Expand Up @@ -509,7 +529,7 @@ def compose(self) -> ComposeResult:

with Vertical(id="main-panel"):
with Container(id="query-area"):
yield TextArea(
yield QueryTextArea(
"",
language="sql",
id="query-input",
Expand All @@ -521,6 +541,7 @@ def compose(self) -> ComposeResult:
yield ResultsFilterInput(id="results-filter")
yield Lazy(SqlitDataTable(id="results-table", zebra_stripes=True, show_header=False))

yield Static("", id="idle-scheduler-bar")
yield Static("Not connected", id="status-bar")

yield ContextFooter()
Expand All @@ -531,6 +552,15 @@ def on_mount(self) -> None:
self._startup_stamp("on_mount_start")
self._restart_argv = self._compute_restart_argv()

# Initialize and start idle scheduler
self._idle_scheduler = init_idle_scheduler(self)
self._idle_scheduler.start()

# Show idle scheduler debug bar if enabled
if self._debug_idle_scheduler:
self.idle_scheduler_bar.add_class("visible")
self._idle_scheduler_bar_timer = self.set_interval(0.1, self._update_idle_scheduler_bar)

self._theme_manager.register_builtin_themes()
self._theme_manager.register_textarea_themes()

Expand Down
9 changes: 9 additions & 0 deletions sqlit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ def main() -> int:
action="store_true",
help="Show startup timing in the status bar.",
)
parser.add_argument(
"--debug-idle-scheduler",
action="store_true",
help="Show idle scheduler status in the status bar.",
)

subparsers = parser.add_subparsers(dest="command", help="Available commands")

Expand Down Expand Up @@ -284,6 +289,10 @@ def main() -> int:
os.environ["SQLIT_DEBUG"] = "1"
else:
os.environ.pop("SQLIT_DEBUG", None)
if args.debug_idle_scheduler:
os.environ["SQLIT_DEBUG_IDLE_SCHEDULER"] = "1"
else:
os.environ.pop("SQLIT_DEBUG_IDLE_SCHEDULER", None)
if args.profile_startup or args.debug:
os.environ["SQLIT_STARTUP_MARK"] = str(startup_mark)
else:
Expand Down
1 change: 1 addition & 0 deletions sqlit/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def get_all_schemas() -> Any:
"SchemaField": ("sqlit.db.schema", "SchemaField"),
"SelectOption": ("sqlit.db.schema", "SelectOption"),
# Adapters (through sqlit.db.adapters, which itself lazy-loads)
"AzureSQLAdapter": ("sqlit.db.adapters", "AzureSQLAdapter"),
"CockroachDBAdapter": ("sqlit.db.adapters", "CockroachDBAdapter"),
"DuckDBAdapter": ("sqlit.db.adapters", "DuckDBAdapter"),
"FirebirdAdapter": ("sqlit.db.adapters", "FirebirdAdapter"),
Expand Down
42 changes: 42 additions & 0 deletions sqlit/db/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,37 @@ def supports_multiple_databases(self) -> bool:
"""Whether this database type supports multiple databases."""
pass

@property
def supports_cross_database_queries(self) -> bool:
"""Whether this database supports cross-database queries.

When True, queries can reference tables in other databases using
fully qualified names (e.g., [db].[schema].[table] in SQL Server).

When False, each database is isolated and a specific database must
be selected before querying. Connection creation will require a
database to be specified.

Defaults to True. Override in subclasses for databases like PostgreSQL
where each database is isolated.
"""
return True

@property
@abstractmethod
def supports_stored_procedures(self) -> bool:
"""Whether this database type supports stored procedures."""
pass

@property
def system_databases(self) -> frozenset[str]:
"""Set of system database names to exclude from user listings.

Override in subclasses for database-specific system databases.
Returns lowercase names for case-insensitive comparison.
"""
return frozenset()

@property
def default_schema(self) -> str:
"""The default schema for this database type.
Expand Down Expand Up @@ -250,6 +275,10 @@ def validate_config(self, config: ConnectionConfig) -> None:
"""Validate provider-specific config values."""
return None

def detect_capabilities(self, conn: Any, config: ConnectionConfig) -> None:
"""Detect runtime capabilities after establishing a connection."""
return None

def get_auth_type(self, config: ConnectionConfig) -> Any | None:
"""Return the provider-specific auth type, if applicable."""
return None
Expand Down Expand Up @@ -567,6 +596,10 @@ class MySQLBaseAdapter(CursorBasedAdapter):
def supports_multiple_databases(self) -> bool:
return True

@property
def system_databases(self) -> frozenset[str]:
return frozenset({"mysql", "information_schema", "performance_schema", "sys"})

@property
def supports_stored_procedures(self) -> bool:
return True
Expand Down Expand Up @@ -821,6 +854,15 @@ def supports_multiple_databases(self) -> bool:
def supports_stored_procedures(self) -> bool:
return True

@property
def system_databases(self) -> frozenset[str]:
return frozenset({"template0", "template1"})

@property
def supports_cross_database_queries(self) -> bool:
"""PostgreSQL databases are isolated; cross-database queries not supported."""
return False

@property
def default_schema(self) -> str:
return "public"
Expand Down
4 changes: 4 additions & 0 deletions sqlit/db/adapters/clickhouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ def driver_import_names(self) -> tuple[str, ...]:
def supports_multiple_databases(self) -> bool:
return True

@property
def system_databases(self) -> frozenset[str]:
return frozenset({"system", "information_schema", "INFORMATION_SCHEMA"})

@property
def supports_stored_procedures(self) -> bool:
# ClickHouse doesn't have traditional stored procedures
Expand Down
5 changes: 5 additions & 0 deletions sqlit/db/adapters/d1.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ def supports_stored_procedures(self) -> bool:
"""D1 is SQLite-based and does not support stored procedures."""
return False

@property
def supports_cross_database_queries(self) -> bool:
"""D1 databases are isolated; cross-database queries not supported."""
return False

def connect(self, config: ConnectionConfig) -> D1Connection:
"""Establishes a 'connection' to D1 by preparing authenticated session."""
requests = import_driver_module(
Expand Down
Loading