harlequin 2.5.2__tar.gz → 2.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {harlequin-2.5.2 → harlequin-2.6.0}/CHANGELOG.md +12 -1
  2. {harlequin-2.5.2 → harlequin-2.6.0}/PKG-INFO +1 -1
  3. {harlequin-2.5.2 → harlequin-2.6.0}/pyproject.toml +8 -3
  4. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/app.py +25 -4
  5. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/autocomplete/completers.py +20 -13
  6. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/code_editor.py +2 -0
  7. {harlequin-2.5.2 → harlequin-2.6.0}/.gitignore +0 -0
  8. {harlequin-2.5.2 → harlequin-2.6.0}/LICENSE +0 -0
  9. {harlequin-2.5.2 → harlequin-2.6.0}/README.md +0 -0
  10. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/__init__.py +0 -0
  11. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/__main__.py +0 -0
  12. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/actions.py +0 -0
  13. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/adapter.py +0 -0
  14. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/app.tcss +0 -0
  15. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/app_base.py +0 -0
  16. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/autocomplete/__init__.py +0 -0
  17. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/autocomplete/completion.py +0 -0
  18. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/autocomplete/constants.py +0 -0
  19. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/bindings.py +0 -0
  20. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/cache.py +0 -0
  21. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/catalog.py +0 -0
  22. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/catalog_cache.py +0 -0
  23. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/cli.py +0 -0
  24. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/colors.py +0 -0
  25. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/__init__.py +0 -0
  26. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/confirm_modal.py +0 -0
  27. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/__init__.py +0 -0
  28. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/database_tree.py +0 -0
  29. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/file_tree.py +0 -0
  30. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/s3_tree.py +0 -0
  31. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/tree.py +0 -0
  32. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/debug_info.py +0 -0
  33. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/error_modal.py +0 -0
  34. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/export_screen.py +0 -0
  35. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/help_screen.md +0 -0
  36. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/help_screen.py +0 -0
  37. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/history_screen.py +0 -0
  38. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/results_viewer.py +0 -0
  39. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/run_query_bar.py +0 -0
  40. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/config.py +0 -0
  41. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/config_wizard.py +0 -0
  42. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/copy_formats.py +0 -0
  43. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/copy_widgets.py +0 -0
  44. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/driver.py +0 -0
  45. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/editor_cache.py +0 -0
  46. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/exception.py +0 -0
  47. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/export.py +0 -0
  48. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/global.tcss +0 -0
  49. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/history.py +0 -0
  50. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/keymap.py +0 -0
  51. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/keys_app.py +0 -0
  52. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/keys_app.tcss +0 -0
  53. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/locale_manager.py +0 -0
  54. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/messages.py +0 -0
  55. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/options.py +0 -0
  56. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/plugins.py +0 -0
  57. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/py.typed +0 -0
  58. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/transaction_mode.py +0 -0
  59. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/windows_timezone.py +0 -0
  60. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/__init__.py +0 -0
  61. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/adapter.py +0 -0
  62. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/catalog.py +0 -0
  63. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/cli_options.py +0 -0
  64. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/completions.py +0 -0
  65. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/interactions.py +0 -0
  66. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/py.typed +0 -0
  67. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/__init__.py +0 -0
  68. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/adapter.py +0 -0
  69. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/catalog.py +0 -0
  70. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/cli_options.py +0 -0
  71. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/completions.py +0 -0
  72. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/interactions.py +0 -0
  73. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/py.typed +0 -0
  74. {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_vscode/__init__.py +0 -0
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.0] - 2026-08-03
8
+
9
+ ### Performance
10
+
11
+ - Autocomplete no longer re-sorts the full completion list every time the Data Catalog lazy-loads a node's children; new completions are staged and merged at most once per second. This keeps the UI responsive while the catalog loads in the background, especially against high-latency (remote) databases.
12
+
13
+ ### Bug Fixes
14
+
15
+ - Refreshes the file tree in the Data Catalog after Harlequin writes a file, either by saving from the editor or exporting data, so newly written files appear without a manual refresh ([#871](https://github.com/tconbeer/harlequin/issues/871)).
16
+
7
17
  ## [2.5.2] - 2026-03-25
8
18
 
9
19
  ### Bug Fixes
@@ -873,7 +883,8 @@ All notable changes to this project will be documented in this file.
873
883
 
874
884
  - Use the DuckDB CLI.
875
885
 
876
- [unreleased]: https://github.com/tconbeer/harlequin/compare/2.5.2...HEAD
886
+ [unreleased]: https://github.com/tconbeer/harlequin/compare/2.6.0...HEAD
887
+ [2.6.0]: https://github.com/tconbeer/harlequin/compare/2.5.2...2.6.0
877
888
  [2.5.2]: https://github.com/tconbeer/harlequin/compare/2.5.1...2.5.2
878
889
  [2.5.1]: https://github.com/tconbeer/harlequin/compare/2.5.0...2.5.1
879
890
  [2.5.0]: https://github.com/tconbeer/harlequin/compare/2.4.1...2.5.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harlequin
3
- Version: 2.5.2
3
+ Version: 2.6.0
4
4
  Summary: The SQL IDE for Your Terminal.
5
5
  Project-URL: Homepage, https://harlequin.sh
6
6
  Project-URL: Documentation, https://harlequin.sh/docs/getting-started/index
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
 
3
3
  name = "harlequin"
4
- version = "2.5.2"
4
+ version = "2.6.0"
5
5
  requires-python = ">=3.10"
6
6
  description = "The SQL IDE for Your Terminal."
7
7
  authors = [
@@ -68,14 +68,15 @@ dev = [
68
68
  ]
69
69
  static = [
70
70
  "ruff>=0.14.1",
71
- "mypy==1.18.2,<2",
71
+ "mypy==2.0.0,<3",
72
72
  "pandas-stubs>=2,<3",
73
73
  "boto3-stubs>=1.34.23,<2",
74
74
  ]
75
75
  test = [
76
76
  "pytest>=9.0.2,<9.1.0",
77
77
  "pytest-asyncio==1.3.0",
78
- "pytest-textual-snapshot @ https://github.com/tconbeer/pytest-textual-snapshot.git#main",
78
+ "pytest-xdist>=3.6",
79
+ "pytest-textual-snapshot @ git+https://github.com/tconbeer/pytest-textual-snapshot.git@a2c6763b45283f76757a123342900accefcf04e8",
79
80
  "boto3==1.34.22",
80
81
  # extension tests require consistent version of duckdb.
81
82
  "duckdb==1.5.1",
@@ -186,6 +187,10 @@ strict_equality = true
186
187
 
187
188
  [tool.pytest.ini_options]
188
189
 
190
+ # Tests run in parallel, one worker per CPU. Pass -n0 to run them in a single
191
+ # process, e.g. when using a debugger or -s.
192
+ addopts = ["-n", "auto"]
193
+
189
194
  markers = [
190
195
  "online: requeries internet connection (deselect with '-m \"not online\"')",
191
196
  "use_cache: override the autouse fixture the disables the buffer cache",
@@ -27,6 +27,7 @@ from textual.lazy import Lazy
27
27
  from textual.message import Message
28
28
  from textual.reactive import reactive
29
29
  from textual.screen import Screen, ScreenResultCallbackType, ScreenResultType
30
+ from textual.timer import Timer
30
31
  from textual.types import CSSPathType
31
32
  from textual.widget import AwaitMount, Widget
32
33
  from textual.widgets import Button, Footer, Input
@@ -229,6 +230,7 @@ class Harlequin(AppBase):
229
230
  self.query_timer: Union[float, None] = None
230
231
  self.connection: HarlequinConnection | None = None
231
232
  self.harlequin_driver = HarlequinDriver(app=self)
233
+ self._completer_merge_timer: Timer | None = None
232
234
 
233
235
  if keymap_names is None:
234
236
  keymap_names = ("vscode",)
@@ -889,11 +891,15 @@ class Harlequin(AppBase):
889
891
  if table is None:
890
892
  show_export_error(error=ValueError("You must execute a query first."))
891
893
  return
892
- notify = partial(self.notify, "Data exported successfully.")
894
+
895
+ def on_export_success() -> None:
896
+ self.notify("Data exported successfully.")
897
+ self.data_catalog.update_file_tree()
898
+
893
899
  callback = partial(
894
900
  export_callback,
895
901
  table=table,
896
- success_callback=notify,
902
+ success_callback=on_export_success,
897
903
  error_callback=show_export_error,
898
904
  )
899
905
  self.app.push_screen(
@@ -1168,12 +1174,27 @@ class Harlequin(AppBase):
1168
1174
  self.editor_collection.word_completer is not None
1169
1175
  and self.editor_collection.member_completer is not None
1170
1176
  ):
1177
+ # appending to the catalog is cheap, but merging the full
1178
+ # completion list is O(n log n), so when the Data Catalog's lazy
1179
+ # loader delivers a stream of items (one message per node), we
1180
+ # defer the merge and run it at most once per second.
1171
1181
  self.editor_collection.word_completer.extend_catalog(
1172
- parent=parent, items=items
1182
+ parent=parent, items=items, defer_merge=True
1173
1183
  )
1174
1184
  self.editor_collection.member_completer.extend_catalog(
1175
- parent=parent, items=items
1185
+ parent=parent, items=items, defer_merge=True
1176
1186
  )
1187
+ if self._completer_merge_timer is None:
1188
+ self._completer_merge_timer = self.set_timer(
1189
+ 1.0, self._merge_completers
1190
+ )
1191
+
1192
+ def _merge_completers(self) -> None:
1193
+ self._completer_merge_timer = None
1194
+ if self.editor_collection.word_completer is not None:
1195
+ self.editor_collection.word_completer.merge()
1196
+ if self.editor_collection.member_completer is not None:
1197
+ self.editor_collection.member_completer.merge()
1177
1198
 
1178
1199
  def update_completers(self, catalog: Catalog) -> None:
1179
1200
  if self.connection is None:
@@ -25,12 +25,8 @@ class WordCompleter:
25
25
  self._function_completions = function_completions
26
26
  self._catalog_completions = catalog_completions
27
27
  self._extra_completions = extra_completions or []
28
- self.completions: list[HarlequinCompletion] = self._merge_completions(
29
- self._keyword_completions,
30
- self._function_completions,
31
- self._catalog_completions,
32
- self._extra_completions,
33
- )
28
+ self.completions: list[HarlequinCompletion] = []
29
+ self.merge()
34
30
 
35
31
  def __call__(self, prefix: str) -> list[tuple[tuple[str, str], str]]:
36
32
  """
@@ -64,18 +60,29 @@ class WordCompleter:
64
60
 
65
61
  def update_catalog(self, catalog: Catalog) -> None:
66
62
  self._catalog_completions = build_catalog_completions(catalog=catalog)
67
- self.completions = self._merge_completions(
68
- self._keyword_completions,
69
- self._function_completions,
70
- self._catalog_completions,
71
- self._extra_completions,
72
- )
63
+ self.merge()
73
64
 
74
- def extend_catalog(self, parent: CatalogItem, items: list[CatalogItem]) -> None:
65
+ def extend_catalog(
66
+ self,
67
+ parent: CatalogItem,
68
+ items: list[CatalogItem],
69
+ defer_merge: bool = False,
70
+ ) -> None:
75
71
  # TODO: dedupe/merge on the parent's unique key, so we can load items from
76
72
  # a cache and update them later when they are lazy-loaded.
77
73
  new_completions = _build_children_completions(items=items, context=parent.label)
78
74
  self._catalog_completions.extend(new_completions)
75
+ if not defer_merge:
76
+ self.merge()
77
+
78
+ def merge(self) -> None:
79
+ """
80
+ Rebuild the merged, sorted completions list. This is O(n log n) in the
81
+ total number of completions, so callers that extend the catalog many
82
+ times in quick succession (e.g., the Data Catalog's lazy loader) should
83
+ pass defer_merge=True to extend_catalog and call this once, on a
84
+ debounce.
85
+ """
79
86
  self.completions = self._merge_completions(
80
87
  self._keyword_completions,
81
88
  self._function_completions,
@@ -81,6 +81,8 @@ class CodeEditor(TextEditor, inherit_bindings=False):
81
81
 
82
82
  def on_text_area_saved(self, message: TextAreaSaved) -> None:
83
83
  self.app.notify(f"Editor contents saved to {message.path}")
84
+ if hasattr(self.app, "data_catalog"):
85
+ self.app.data_catalog.update_file_tree()
84
86
 
85
87
  def on_text_area_clipboard_error(self) -> None:
86
88
  if not self.has_shown_clipboard_error:
File without changes
File without changes
File without changes