Skip to content

Commit 699847c

Browse files
authored
feat: connect in a worker, show catalog loading (#399)
1 parent 793d805 commit 699847c

File tree

6 files changed

+98
-29
lines changed

6 files changed

+98
-29
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Features
8+
9+
- Harlequin now loads immediately and connects to your database in the background ([#393](https://github.com/tconbeer/harlequin/issues/393)).
10+
- Harlequin shows a loading indicator before the Data Catalog is hydrated for the first time ([#396](https://github.com/tconbeer/harlequin/issues/396)).
11+
712
## [1.9.2] - 2024-01-10
813

914
### Features

poetry.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ harlequin-trino = { version = "^0.1", optional = true }
5050
pre-commit = "^3.3.1"
5151
textual-dev = "^1.0.1"
5252
harlequin-postgres = "^0.2"
53+
harlequin-mysql = "^0.1.1"
5354

5455
[tool.poetry.group.static.dependencies]
5556
black = "^23.3.0"

src/harlequin/app.py

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from textual_fastdatatable import DataTable
2424
from textual_fastdatatable.backend import AutoBackendType
2525

26+
from harlequin import HarlequinConnection
2627
from harlequin.adapter import HarlequinAdapter, HarlequinCursor
2728
from harlequin.autocomplete import completer_factory
2829
from harlequin.cache import BufferState, Cache, write_cache
@@ -42,12 +43,20 @@
4243
from harlequin.exception import (
4344
HarlequinConfigError,
4445
HarlequinConnectionError,
46+
HarlequinError,
4547
HarlequinQueryError,
4648
HarlequinThemeError,
49+
pretty_error_message,
4750
pretty_print_error,
4851
)
4952

5053

54+
class DatabaseConnected(Message):
55+
def __init__(self, connection: HarlequinConnection) -> None:
56+
super().__init__()
57+
self.connection = connection
58+
59+
5160
class QuerySubmitted(Message):
5261
def __init__(self, query_text: str, limit: int | None) -> None:
5362
super().__init__()
@@ -126,14 +135,7 @@ def __init__(
126135
)
127136
self.exit(return_code=2)
128137
self.query_timer: Union[float, None] = None
129-
try:
130-
self.connection = self.adapter.connect()
131-
except HarlequinConnectionError as e:
132-
pretty_print_error(e)
133-
self.exit(return_code=2)
134-
else:
135-
if self.connection.init_message:
136-
self.notify(self.connection.init_message)
138+
self.connection: HarlequinConnection | None = None
137139

138140
try:
139141
self.app_colors = HarlequinColors.from_theme(theme)
@@ -150,9 +152,12 @@ def compose(self) -> ComposeResult:
150152
yield DataCatalog(type_color=self.app_colors.gray)
151153
with Vertical(id="main_panel"):
152154
yield EditorCollection(language="sql", theme=self.theme)
153-
yield RunQueryBar(max_results=self.max_results)
155+
yield RunQueryBar(
156+
max_results=self.max_results, classes="non-responsive"
157+
)
154158
yield ResultsViewer(
155-
max_results=self.max_results, type_color=self.app_colors.gray
159+
max_results=self.max_results,
160+
type_color=self.app_colors.gray,
156161
)
157162
yield Footer()
158163

@@ -186,8 +191,9 @@ async def on_mount(self) -> None:
186191

187192
self.editor.focus()
188193
self.run_query_bar.checkbox.value = False
194+
self.data_catalog.loading = True
189195

190-
self.update_schema_data()
196+
self._connect()
191197

192198
@on(Button.Pressed, "#run_query")
193199
def submit_query_from_run_query_bar(self, message: Button.Pressed) -> None:
@@ -208,6 +214,17 @@ def submit_query_from_editor(self, message: CodeEditor.Submitted) -> None:
208214
)
209215
)
210216

217+
@on(DatabaseConnected)
218+
def initialize_app(self, message: DatabaseConnected) -> None:
219+
self.connection = message.connection
220+
self.run_query_bar.set_responsive()
221+
self.results_viewer.show_table(did_run=False)
222+
if message.connection.init_message:
223+
self.notify(message.connection.init_message, title="Database Connected.")
224+
else:
225+
self.notify("Database Connected.")
226+
self.update_schema_data()
227+
211228
@on(DataCatalog.NodeSubmitted)
212229
def insert_node_into_editor(
213230
self, message: DataCatalog.NodeSubmitted[CatalogItem]
@@ -277,11 +294,11 @@ def copy_data_to_clipboard(self, message: DataTable.SelectionCopied) -> None:
277294
self.notify("Selected data copied to clipboard.")
278295

279296
@on(Worker.StateChanged)
280-
def handle_worker_error(self, message: Worker.StateChanged) -> None:
297+
async def handle_worker_error(self, message: Worker.StateChanged) -> None:
281298
if message.state == WorkerState.ERROR:
282-
self._handle_worker_error(message)
299+
await self._handle_worker_error(message)
283300

284-
def _handle_worker_error(self, message: Worker.StateChanged) -> None:
301+
async def _handle_worker_error(self, message: Worker.StateChanged) -> None:
285302
if (
286303
message.worker.name == "update_schema_data"
287304
and message.worker.error is not None
@@ -296,16 +313,33 @@ def _handle_worker_error(self, message: Worker.StateChanged) -> None:
296313
):
297314
self.run_query_bar.set_responsive()
298315
self.results_viewer.show_table()
299-
header = getattr(message.worker.error, "title", "Query Error")
316+
header = getattr(
317+
message.worker.error, "title", message.worker.error.__class__.__name__
318+
)
300319
self._push_error_modal(
301320
title="Query Error",
302321
header=header,
303322
error=message.worker.error,
304323
)
324+
elif message.worker.name == "_connect" and message.worker.error is not None:
325+
title = getattr(
326+
message.worker.error,
327+
"title",
328+
"Harlequin could not connect to your database.",
329+
)
330+
error = (
331+
message.worker.error
332+
if isinstance(message.worker.error, HarlequinError)
333+
else HarlequinConnectionError(
334+
msg=str(message.worker.error), title=title
335+
)
336+
)
337+
self.exit(return_code=2, message=pretty_error_message(error))
305338

306339
@on(NewCatalog)
307340
def update_tree_and_completers(self, message: NewCatalog) -> None:
308341
self.data_catalog.update_tree(message.catalog)
342+
self.data_catalog.loading = False
309343
self.update_completers(message.catalog)
310344

311345
@on(QueriesExecuted)
@@ -383,6 +417,8 @@ def watch_full_screen(self, full_screen: bool) -> None:
383417

384418
@on(QuerySubmitted)
385419
def execute_query(self, message: QuerySubmitted) -> None:
420+
if self.connection is None:
421+
return
386422
if message.query_text:
387423
self.full_screen = False
388424
self.run_query_bar.set_not_responsive()
@@ -472,6 +508,17 @@ def action_toggle_sidebar(self) -> None:
472508
else:
473509
self.sidebar_hidden = not self.sidebar_hidden
474510

511+
@work(
512+
thread=True,
513+
exclusive=True,
514+
exit_on_error=False,
515+
group="connect",
516+
description="Connecting to DB",
517+
)
518+
def _connect(self) -> None:
519+
connection = self.adapter.connect()
520+
self.post_message(DatabaseConnected(connection=connection))
521+
475522
@work(
476523
thread=True,
477524
exclusive=True,
@@ -480,6 +527,8 @@ def action_toggle_sidebar(self) -> None:
480527
description="Executing queries.",
481528
)
482529
def _execute_query(self, message: QuerySubmitted) -> None:
530+
if self.connection is None:
531+
return
483532
cursors: Dict[str, HarlequinCursor] = {}
484533
queries = self._split_query_text(message.query_text)
485534
for q in queries:
@@ -514,7 +563,7 @@ def _push_error_modal(self, title: str, header: str, error: BaseException) -> No
514563
title=title,
515564
header=header,
516565
error=error,
517-
)
566+
),
518567
)
519568

520569
@work(
@@ -543,6 +592,8 @@ def _fetch_data(
543592

544593
@work(thread=True, exclusive=True, exit_on_error=True, group="completer_builders")
545594
def update_completers(self, catalog: Catalog) -> None:
595+
if self.connection is None:
596+
return
546597
if (
547598
self.editor_collection.word_completer is not None
548599
and self.editor_collection.member_completer is not None
@@ -561,6 +612,8 @@ def update_completers(self, catalog: Catalog) -> None:
561612

562613
@work(thread=True, exclusive=True, exit_on_error=False, group="schema_updaters")
563614
def update_schema_data(self) -> None:
615+
if self.connection is None:
616+
return
564617
catalog = self.connection.get_catalog()
565618
self.post_message(NewCatalog(catalog=catalog))
566619

@@ -570,6 +623,8 @@ def _validate_selection(self) -> str:
570623
return the empty string.
571624
"""
572625
selection = self.editor.selected_text
626+
if self.connection is None:
627+
return selection
573628
if selection:
574629
try:
575630
return self.connection.validate_sql(selection)

src/harlequin/exception.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from rich.panel import Panel
2+
3+
14
class HarlequinExit(Exception):
25
pass
36

@@ -39,13 +42,14 @@ class HarlequinTzDataError(HarlequinError):
3942

4043
def pretty_print_error(error: HarlequinError) -> None:
4144
from rich import print
42-
from rich.panel import Panel
43-
44-
print(
45-
Panel.fit(
46-
str(error),
47-
title=error.title if error.title else ("Harlequin encountered an error."),
48-
title_align="left",
49-
border_style="red",
50-
)
45+
46+
print(pretty_error_message(error))
47+
48+
49+
def pretty_error_message(error: HarlequinError) -> Panel:
50+
return Panel.fit(
51+
str(error),
52+
title=error.title if error.title else ("Harlequin encountered an error."),
53+
title_align="left",
54+
border_style="red",
5155
)

src/harlequin/global.tcss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ Toast.-error {
7777
border-left: wide $error;
7878
}
7979

80+
Toast .toast--title {
81+
color: $primary;
82+
}
83+
8084
/* DATA CATALOG */
8185
DataCatalog {
8286
height: 1fr;

0 commit comments

Comments
 (0)