Skip to content

Commit 4831ded

Browse files
authored
Autocomplete (#46)
* Add smart SQL autocomplete with context-aware suggestions - New SQL completion engine using sqlparse for context detection - Suggests tables after FROM/JOIN, columns after WHERE/SELECT, operators after column names - Lazy loading of columns to avoid UI lag during schema indexing - Scrollable autocomplete dropdown with keyboard navigation - Auto-hide when cursor moves away from typing position - Fuzzy matching and alias resolution support 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: prompt for password when connection password is empty Previously, an empty string password was treated differently from None, meaning connections with empty passwords would fail instead of prompting the user. Now both None and empty string trigger the password prompt. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: add Azure SQL Database compatibility for MSSQL adapter Azure SQL Database does not support cross-database queries or USE statements. This fix: - Adds _get_cursor_for_database() that tries USE first (fast for regular SQL Server), then falls back to creating a new connection with the target database (for Azure SQL) - Stores config and driver on connection object for reconnection - Removes all cross-database reference syntax ([Database].schema.table) - Updates all adapter methods to use the new approach Also adds unit tests verifying no cross-database references are used, and integration tests for Azure SQL Database. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * refactor: split SQL completion into separate modules by statement type Reorganizes sql_completion.py into a package with separate modules: - core.py: base classes and utilities - completion.py: main SQLCompletion class - alter_table.py, create_table.py, create_index.py, create_view.py, delete.py, drop.py, insert.py, truncate.py, update.py: statement handlers Also introduces QueryTextArea widget that wraps TextArea with SQL autocomplete functionality, simplifying the integration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: don't show completions without SQL context - Return empty completions when there's no SQL content yet (just whitespace). This prevents showing irrelevant keywords when pressing space/enter on an empty query. - Add handlers for IN ( and EXISTS ( patterns to suggest SELECT for subqueries. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: use correct database context when clicking tables in tree For Azure SQL Database, clicking a table from a different database now correctly executes the query against that database: - build_select_query no longer includes [database] prefix (breaks Azure) - Added _query_target_database to track which database to query - Query execution uses apply_database_override when target differs - Target database is cleared after use to not affect user-typed queries 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: use fully qualified table names in autocomplete when no default database When connected without a default database (multiple databases visible), autocomplete now suggests fully qualified names like [db].[schema].[table] instead of just table names. This allows queries to work correctly. Uses adapter.quote_identifier() for proper quoting (brackets for SQL Server). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * feat: idle scheduler for background schema loading and UX improvements Idle Scheduler: - Add idle scheduler inspired by browser's requestIdleCallback API - Execute background work during user idle periods (500ms threshold) - Queue schema loading (tables/views/procedures) as idle jobs - Add --debug-idle-scheduler flag to show scheduler status bar - Preload columns for tables found in query editor during idle Shared Cache: - Tree and autocomplete now share _db_object_cache for tables/views/procedures - Expanding tree nodes populates autocomplete cache and vice versa - Refresh ('f') clears both object cache and column cache Azure SQL: - Add separate AzureSQLAdapter with required database field - Simplify MSSQL adapter by removing Azure-specific fallback logic Autocomplete UX: - Add *, DISTINCT, TOP, ALL to SELECT clause completions - Remove table names from SELECT suggestions (belong after FROM) - Dropdown stays visible when window loses focus - Dropdown constrained to stay within bounds (no right-edge cutoff) * Enter key dismisses autocomplete instead of accepting - Enter now hides the autocomplete dropdown and inserts a newline - Only Tab accepts autocomplete suggestions - Added suppress flag to prevent autocomplete from re-triggering after Enter dismisses it (newline text change was re-showing dropdown) * Persist active database preference across sessions Introduces _active_database separate from current_config.database to preserve tree structure while allowing users to set a preferred database. - Added get/set_database_preference() to config.py for persistence - On connect: load cached preference into _active_database - On 'u' press: save preference and reload schema for new database - On disconnect: clear _active_database - Star display in tree now uses _active_database - Autocomplete schema loading now respects _active_database The connection config (and tree structure) remains unchanged - only the user's runtime preference is cached in settings.json. * Add system_databases property per adapter and fix ESC behavior - Add system_databases property to DatabaseAdapter base class - Define system databases for each adapter: - MSSQL: master, tempdb, model, msdb - PostgreSQL: template0, template1 - MySQL/MariaDB: mysql, information_schema, performance_schema, sys - ClickHouse: system, information_schema - Snowflake: SNOWFLAKE - Update autocomplete to filter using adapter.system_databases - ESC key now closes autocomplete AND exits to normal mode - Add unit tests for system_databases property * Fix ESC key priority on modal screens Add priority=True to escape bindings on all modal screens to ensure ESC key is handled correctly when other bindings are active. * Fix error display in results table Flatten multi-line error messages to single line since DataTable cells only display one line. Replaces textwrap with regex whitespace collapse. * Add supports_cross_database_queries property and auto-switch databases - Add supports_cross_database_queries property to DatabaseAdapter - Returns True for MSSQL, MySQL, ClickHouse, Snowflake (support USE) - Returns False for PostgreSQL, CockroachDB, D1 (require reconnection) - Add switch_database() method to ConnectionSession for reconnecting to a different database while reusing SSH tunnel - Auto-switch database when expanding database nodes or their contents - For cross-db adapters: sets active database (USE approach) - For non-cross-db adapters: reconnects to the database - Falls back to reconnection if USE fails (e.g., Azure SQL) - Remove 'u' keybinding - Enter on database node now sets it active - Apply active database to query execution context - Add helper functions in providers.py for database requirement checks - Update tests with new mock attributes * Remove Azure SQL adapter - use MSSQL with automatic fallback Azure SQL is now handled by the MSSQL adapter with automatic fallback: - If USE statement fails, automatically reconnect to target database - Retry the operation after reconnection - Show original error only if reconnection also fails This makes Azure SQL work transparently without a separate adapter. * Handle cross-db capability in autocomplete * Handle ESC in leader menu * Add tests for autocomplete database modes * Add autocomplete demo gifs * Hide autocomplete after statement terminator (semicolon) Previously, typing a semicolon after a complete statement would show irrelevant suggestions (tables or keywords). Now the autocomplete popup hides when the cursor is immediately after a semicolon. * Remove db caching * Fix password prompt for explicitly empty passwords - Only prompt for password when password is None (not set), not when it's an empty string (explicitly set to empty) - Some databases allow passwordless connections, so "" should be valid - Remove Azure SQL adapter tests (adapter was removed) --------- Co-authored-by: Peter Adams <18162810+Maxteabag@users.noreply.github.com>
1 parent 168efe4 commit 4831ded

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+7758
-358
lines changed

demos/autocomplete-01-basics.gif

220 KB
Loading
253 KB
Loading

demos/autocomplete-03-joins.gif

265 KB
Loading

demos/autocomplete-04-clauses.gif

266 KB
Loading

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies = [
3131
"pyperclip>=1.8.2",
3232
"keyring>=24.0.0",
3333
"docker>=7.0.0",
34+
"sqlparse>=0.5.0",
3435
]
3536
dynamic = ["version"]
3637

sqlit/app.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
from textual.lazy import Lazy
1818
from textual.screen import ModalScreen
1919
from textual.timer import Timer
20-
from textual.widgets import Static, TextArea, Tree
20+
from textual.widgets import Static, Tree
21+
22+
from .widgets import QueryTextArea
2123
from textual.worker import Worker
2224

2325
from .config import (
@@ -51,6 +53,7 @@
5153
TreeFilterInput,
5254
VimMode,
5355
)
56+
from .idle_scheduler import init_idle_scheduler, on_user_activity, IdleScheduler
5457

5558

5659
class SSMSTUI(
@@ -208,6 +211,17 @@ class SSMSTUI(
208211
padding: 0 1;
209212
}
210213
214+
#idle-scheduler-bar {
215+
height: 1;
216+
background: $primary-darken-3;
217+
padding: 0 1;
218+
display: none;
219+
}
220+
221+
#idle-scheduler-bar.visible {
222+
display: block;
223+
}
224+
211225
#sidebar,
212226
#query-area,
213227
#results-area {
@@ -260,7 +274,6 @@ class SSMSTUI(
260274
Binding("z", "collapse_tree", "Collapse", show=False),
261275
Binding("j", "tree_cursor_down", "Down", show=False),
262276
Binding("k", "tree_cursor_up", "Up", show=False),
263-
Binding("u", "use_database", "Use as default", show=False),
264277
Binding("v", "view_cell", "View cell", show=False),
265278
Binding("u", "edit_cell", "Update cell", show=False),
266279
Binding("h", "results_cursor_left", "Left", show=False),
@@ -297,6 +310,7 @@ def __init__(
297310
self._startup_connection = startup_connection
298311
self._startup_connect_config: ConnectionConfig | None = None
299312
self._debug_mode = os.environ.get("SQLIT_DEBUG") == "1"
313+
self._debug_idle_scheduler = os.environ.get("SQLIT_DEBUG_IDLE_SCHEDULER") == "1"
300314
self._startup_profile = os.environ.get("SQLIT_PROFILE_STARTUP") == "1"
301315
self._startup_mark = self._parse_startup_mark(os.environ.get("SQLIT_STARTUP_MARK"))
302316
self._startup_init_time = time.perf_counter()
@@ -355,7 +369,9 @@ def __init__(
355369
self._state_machine = UIStateMachine()
356370
self._session_factory: Any | None = None
357371
self._last_query_table: dict | None = None
358-
# Omarchy theme sync state
372+
self._query_target_database: str | None = None # Target DB for auto-generated queries
373+
# Idle scheduler for background work
374+
self._idle_scheduler: IdleScheduler | None = None
359375

360376
if mock_profile:
361377
self._session_factory = self._create_mock_session_factory(mock_profile)
@@ -387,8 +403,8 @@ def object_tree(self) -> Tree:
387403
return self.query_one("#object-tree", Tree)
388404

389405
@property
390-
def query_input(self) -> TextArea:
391-
return self.query_one("#query-input", TextArea)
406+
def query_input(self) -> QueryTextArea:
407+
return self.query_one("#query-input", QueryTextArea)
392408

393409
@property
394410
def results_table(self) -> SqlitDataTable:
@@ -416,6 +432,10 @@ def results_area(self) -> Any:
416432
def status_bar(self) -> Static:
417433
return self.query_one("#status-bar", Static)
418434

435+
@property
436+
def idle_scheduler_bar(self) -> Static:
437+
return self.query_one("#idle-scheduler-bar", Static)
438+
419439
@property
420440
def autocomplete_dropdown(self) -> Any:
421441
from .widgets import AutocompleteDropdown
@@ -509,7 +529,7 @@ def compose(self) -> ComposeResult:
509529

510530
with Vertical(id="main-panel"):
511531
with Container(id="query-area"):
512-
yield TextArea(
532+
yield QueryTextArea(
513533
"",
514534
language="sql",
515535
id="query-input",
@@ -521,6 +541,7 @@ def compose(self) -> ComposeResult:
521541
yield ResultsFilterInput(id="results-filter")
522542
yield Lazy(SqlitDataTable(id="results-table", zebra_stripes=True, show_header=False))
523543

544+
yield Static("", id="idle-scheduler-bar")
524545
yield Static("Not connected", id="status-bar")
525546

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

555+
# Initialize and start idle scheduler
556+
self._idle_scheduler = init_idle_scheduler(self)
557+
self._idle_scheduler.start()
558+
559+
# Show idle scheduler debug bar if enabled
560+
if self._debug_idle_scheduler:
561+
self.idle_scheduler_bar.add_class("visible")
562+
self._idle_scheduler_bar_timer = self.set_interval(0.1, self._update_idle_scheduler_bar)
563+
534564
self._theme_manager.register_builtin_themes()
535565
self._theme_manager.register_textarea_themes()
536566

sqlit/cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ def main() -> int:
161161
action="store_true",
162162
help="Show startup timing in the status bar.",
163163
)
164+
parser.add_argument(
165+
"--debug-idle-scheduler",
166+
action="store_true",
167+
help="Show idle scheduler status in the status bar.",
168+
)
164169

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

@@ -284,6 +289,10 @@ def main() -> int:
284289
os.environ["SQLIT_DEBUG"] = "1"
285290
else:
286291
os.environ.pop("SQLIT_DEBUG", None)
292+
if args.debug_idle_scheduler:
293+
os.environ["SQLIT_DEBUG_IDLE_SCHEDULER"] = "1"
294+
else:
295+
os.environ.pop("SQLIT_DEBUG_IDLE_SCHEDULER", None)
287296
if args.profile_startup or args.debug:
288297
os.environ["SQLIT_STARTUP_MARK"] = str(startup_mark)
289298
else:

sqlit/db/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def get_all_schemas() -> Any:
8888
"SchemaField": ("sqlit.db.schema", "SchemaField"),
8989
"SelectOption": ("sqlit.db.schema", "SelectOption"),
9090
# Adapters (through sqlit.db.adapters, which itself lazy-loads)
91+
"AzureSQLAdapter": ("sqlit.db.adapters", "AzureSQLAdapter"),
9192
"CockroachDBAdapter": ("sqlit.db.adapters", "CockroachDBAdapter"),
9293
"DuckDBAdapter": ("sqlit.db.adapters", "DuckDBAdapter"),
9394
"FirebirdAdapter": ("sqlit.db.adapters", "FirebirdAdapter"),

sqlit/db/adapters/base.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,37 @@ def supports_multiple_databases(self) -> bool:
182182
"""Whether this database type supports multiple databases."""
183183
pass
184184

185+
@property
186+
def supports_cross_database_queries(self) -> bool:
187+
"""Whether this database supports cross-database queries.
188+
189+
When True, queries can reference tables in other databases using
190+
fully qualified names (e.g., [db].[schema].[table] in SQL Server).
191+
192+
When False, each database is isolated and a specific database must
193+
be selected before querying. Connection creation will require a
194+
database to be specified.
195+
196+
Defaults to True. Override in subclasses for databases like PostgreSQL
197+
where each database is isolated.
198+
"""
199+
return True
200+
185201
@property
186202
@abstractmethod
187203
def supports_stored_procedures(self) -> bool:
188204
"""Whether this database type supports stored procedures."""
189205
pass
190206

207+
@property
208+
def system_databases(self) -> frozenset[str]:
209+
"""Set of system database names to exclude from user listings.
210+
211+
Override in subclasses for database-specific system databases.
212+
Returns lowercase names for case-insensitive comparison.
213+
"""
214+
return frozenset()
215+
191216
@property
192217
def default_schema(self) -> str:
193218
"""The default schema for this database type.
@@ -250,6 +275,10 @@ def validate_config(self, config: ConnectionConfig) -> None:
250275
"""Validate provider-specific config values."""
251276
return None
252277

278+
def detect_capabilities(self, conn: Any, config: ConnectionConfig) -> None:
279+
"""Detect runtime capabilities after establishing a connection."""
280+
return None
281+
253282
def get_auth_type(self, config: ConnectionConfig) -> Any | None:
254283
"""Return the provider-specific auth type, if applicable."""
255284
return None
@@ -567,6 +596,10 @@ class MySQLBaseAdapter(CursorBasedAdapter):
567596
def supports_multiple_databases(self) -> bool:
568597
return True
569598

599+
@property
600+
def system_databases(self) -> frozenset[str]:
601+
return frozenset({"mysql", "information_schema", "performance_schema", "sys"})
602+
570603
@property
571604
def supports_stored_procedures(self) -> bool:
572605
return True
@@ -821,6 +854,15 @@ def supports_multiple_databases(self) -> bool:
821854
def supports_stored_procedures(self) -> bool:
822855
return True
823856

857+
@property
858+
def system_databases(self) -> frozenset[str]:
859+
return frozenset({"template0", "template1"})
860+
861+
@property
862+
def supports_cross_database_queries(self) -> bool:
863+
"""PostgreSQL databases are isolated; cross-database queries not supported."""
864+
return False
865+
824866
@property
825867
def default_schema(self) -> str:
826868
return "public"

sqlit/db/adapters/clickhouse.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def driver_import_names(self) -> tuple[str, ...]:
6161
def supports_multiple_databases(self) -> bool:
6262
return True
6363

64+
@property
65+
def system_databases(self) -> frozenset[str]:
66+
return frozenset({"system", "information_schema", "INFORMATION_SCHEMA"})
67+
6468
@property
6569
def supports_stored_procedures(self) -> bool:
6670
# ClickHouse doesn't have traditional stored procedures

0 commit comments

Comments
 (0)