jusi-postgres 0.2.0__tar.gz → 0.2.1__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 (23) hide show
  1. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/PKG-INFO +6 -2
  2. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/README.md +4 -0
  3. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/pyproject.toml +2 -2
  4. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/__init__.py +1 -1
  5. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/runner.py +37 -5
  6. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/tests/test_plugin.py +56 -1
  7. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/.gitignore +0 -0
  8. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/Dockerfile +0 -0
  9. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/LICENSE +0 -0
  10. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/docker/postgres/initdb.d/001_jusi_demo.sql +0 -0
  11. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/docker/postgres/initdb.d/002_jusi_blob_fixture.sql +0 -0
  12. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/examples/jusi.toml +0 -0
  13. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/fixtures/blob-readme.txt +0 -0
  14. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/fixtures/test-blob.zip +0 -0
  15. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/scripts/install-blob-fixture.sh +0 -0
  16. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/scripts/run-postgres.sh +0 -0
  17. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/catalog.py +0 -0
  18. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/config.py +0 -0
  19. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/constants.py +0 -0
  20. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/ipc.py +0 -0
  21. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/kernel.py +0 -0
  22. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/metadata.py +0 -0
  23. {jusi_postgres-0.2.0 → jusi_postgres-0.2.1}/src/jusi_postgres/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jusi-postgres
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: PostgreSQL provider plugin for Jusi SQL
5
5
  Author: Jusi contributors
6
6
  License: MIT License
@@ -27,7 +27,7 @@ License: MIT License
27
27
  License-File: LICENSE
28
28
  Requires-Python: >=3.9
29
29
  Requires-Dist: jusi-sql<0.3,>=0.2
30
- Requires-Dist: jusi<2,>=1.0
30
+ Requires-Dist: jusi<2,>=1.0.2
31
31
  Requires-Dist: psycopg[binary]>=3.1
32
32
  Requires-Dist: visidata<4,>=3
33
33
  Provides-Extra: test
@@ -91,6 +91,10 @@ transaction, and server-side cursors. `JusiInterrupt` requests cancellation on
91
91
  that connection. Closing the client closes its cursors, metadata connection,
92
92
  main connection, private control socket, and staged launch data.
93
93
 
94
+ The terminal application loads normal VisiData user configuration and plugins
95
+ before installing Jusi and PostgreSQL commands. This includes `~/.visidatarc`
96
+ and paths selected through `VD_CONFIG` and `VD_DIR`.
97
+
94
98
  ## Local development database
95
99
 
96
100
  Start a disposable PostgreSQL fixture:
@@ -55,6 +55,10 @@ transaction, and server-side cursors. `JusiInterrupt` requests cancellation on
55
55
  that connection. Closing the client closes its cursors, metadata connection,
56
56
  main connection, private control socket, and staged launch data.
57
57
 
58
+ The terminal application loads normal VisiData user configuration and plugins
59
+ before installing Jusi and PostgreSQL commands. This includes `~/.visidatarc`
60
+ and paths selected through `VD_CONFIG` and `VD_DIR`.
61
+
58
62
  ## Local development database
59
63
 
60
64
  Start a disposable PostgreSQL fixture:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jusi-postgres"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "PostgreSQL provider plugin for Jusi SQL"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -13,7 +13,7 @@ authors = [
13
13
  { name = "Jusi contributors" }
14
14
  ]
15
15
  dependencies = [
16
- "jusi>=1.0,<2",
16
+ "jusi>=1.0.2,<2",
17
17
  "jusi-sql>=0.2,<0.3",
18
18
  "psycopg[binary]>=3.1",
19
19
  "visidata>=3,<4",
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.2.1"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import curses
5
5
  from pathlib import Path
6
+ import re
6
7
  import sys
7
8
  import tempfile
8
9
  import threading
@@ -37,6 +38,9 @@ POSTGRES_KEYWORDS = (
37
38
  "VALUES", "UPDATE", "SET", "DELETE", "RETURNING", "WITH", "AS", "DISTINCT",
38
39
  "CREATE", "ALTER", "DROP", "TABLE", "VIEW", "INDEX", "BEGIN", "COMMIT", "ROLLBACK",
39
40
  )
41
+ POSTGRES_RELATION_KEYWORDS = frozenset({
42
+ "FROM", "JOIN", "INTO", "UPDATE", "TABLE", "VIEW", "DESCRIBE", "DESC",
43
+ })
40
44
  _PENDING_SHEETS: deque[Any] = deque()
41
45
 
42
46
 
@@ -129,7 +133,7 @@ class PostgresSession:
129
133
  def complete(self, request: SqlCompletionRequest) -> dict[str, Any]:
130
134
  self.show_metadata_warnings()
131
135
  snapshot = MetadataSnapshot() if not self.collect_metadata else self.metadata.snapshot()
132
- return complete_sql(snapshot, request, keywords=POSTGRES_KEYWORDS)
136
+ return _complete_postgres_sql(snapshot, request)
133
137
 
134
138
  def enter_cell(self) -> None:
135
139
  if not self.collect_metadata:
@@ -240,6 +244,30 @@ class PostgresSession:
240
244
  sheet.cursor_closed_reason = reason
241
245
 
242
246
 
247
+ def _complete_postgres_sql(
248
+ snapshot: MetadataSnapshot,
249
+ request: SqlCompletionRequest,
250
+ ) -> dict[str, list[dict[str, Any]]]:
251
+ if _is_blank_relation_context(request.prefix):
252
+ snapshot = MetadataSnapshot(
253
+ schemas=list(snapshot.schemas),
254
+ refreshed_at=snapshot.refreshed_at,
255
+ )
256
+ return complete_sql(
257
+ snapshot,
258
+ request,
259
+ keywords=POSTGRES_KEYWORDS,
260
+ relation_keywords=POSTGRES_RELATION_KEYWORDS,
261
+ )
262
+
263
+
264
+ def _is_blank_relation_context(prefix: str) -> bool:
265
+ if not prefix or not prefix[-1].isspace():
266
+ return False
267
+ words = re.findall(r"[A-Za-z_][A-Za-z0-9_]*", prefix)
268
+ return bool(words and words[-1].upper() in POSTGRES_RELATION_KEYWORDS)
269
+
270
+
243
271
  class PostgresResultSheet(SequenceSheet):
244
272
  rowtype = "rows"
245
273
 
@@ -396,6 +424,13 @@ def install_postgres_commands() -> None:
396
424
  setattr(visidata.BaseSheet, "_jusi_postgres_commands_v1", True)
397
425
 
398
426
 
427
+ def _initialize_visidata_application() -> None:
428
+ from jusi.visidata_support import initialize_visidata
429
+
430
+ initialize_visidata(open_name="selection.sql", open_filetype="sql")
431
+ install_postgres_commands()
432
+
433
+
399
434
  def _queue_sheet(sheet: PostgresResultSheet) -> None:
400
435
  _PENDING_SHEETS.append(sheet)
401
436
  vd.queueCommand("jusi-postgres-open-pending-sheet")
@@ -540,10 +575,7 @@ def _handle_application_operation(session: PostgresSession, operation: str, payl
540
575
  def run_postgres_application(payload_path: Path, socket_path: str) -> int:
541
576
  session: PostgresSession | None = None
542
577
  try:
543
- from jusi.plugins.vd.application import install_editor_actions
544
-
545
- install_postgres_commands()
546
- install_editor_actions()
578
+ _initialize_visidata_application()
547
579
  visidata.vd.timeouts_before_idle = -1
548
580
  payload = _read_payload(payload_path)
549
581
  query = str(payload.get("sql", "")).strip() or POSTGRES_BOOTSTRAP_SQL
@@ -12,7 +12,7 @@ import pytest
12
12
 
13
13
  from jusi.plugin_api import OperationRejected, WorkerContext
14
14
  from jusi.protocol import validate_plugin_kernel_message
15
- from jusi_sql import CompletionColumn, CompletionObject, MetadataSnapshot, find_sql_actions
15
+ from jusi_sql import CompletionColumn, CompletionObject, MetadataSnapshot, SqlCompletionRequest, find_sql_actions
16
16
  from jusi_sql.kernel import _reset_runtime_for_tests, dispatch_sql
17
17
 
18
18
  from jusi_postgres.catalog import catalog_entry
@@ -24,9 +24,11 @@ from jusi_postgres.runner import (
24
24
  PostgresResultSheet,
25
25
  PostgresSession,
26
26
  RawBinaryValue,
27
+ _complete_postgres_sql,
27
28
  _connect_psycopg,
28
29
  _display_row,
29
30
  _followup_sql,
31
+ _initialize_visidata_application,
30
32
  _looks_like_result_query,
31
33
  _write_raw_value_file,
32
34
  )
@@ -120,6 +122,59 @@ def test_followup_preserves_literal_sql_but_removes_magic_header() -> None:
120
122
  assert _followup_sql("%%sql analytics\nselect α") == "select α"
121
123
 
122
124
 
125
+ def test_blank_relation_completion_returns_only_schemas() -> None:
126
+ prefix = "select * from "
127
+ request = SqlCompletionRequest(prefix, prefix, len(prefix), 0, len(prefix))
128
+ snapshot = MetadataSnapshot(
129
+ schemas=["demo", "public"],
130
+ objects=[CompletionObject("accounts", "demo", "table")],
131
+ columns=[CompletionColumn("accounts", "email", "demo", "text")],
132
+ functions=[CompletionObject("decode_event", "demo", "function")],
133
+ )
134
+
135
+ result = _complete_postgres_sql(snapshot, request)
136
+
137
+ assert [(item["text"], item["kind"]) for item in result["items"]] == [
138
+ ("demo", "schema"),
139
+ ("public", "schema"),
140
+ ]
141
+ assert {(item["start"], item["end"]) for item in result["items"]} == {
142
+ (len(prefix), len(prefix)),
143
+ }
144
+
145
+
146
+ def test_typed_relation_completion_keeps_matching_metadata() -> None:
147
+ prefix = "select * from acc"
148
+ request = SqlCompletionRequest(prefix, prefix, len(prefix), 0, len(prefix))
149
+ snapshot = MetadataSnapshot(
150
+ schemas=["demo"],
151
+ objects=[CompletionObject("accounts", "demo", "table")],
152
+ )
153
+
154
+ result = _complete_postgres_sql(snapshot, request)
155
+
156
+ assert "accounts" in {item["text"] for item in result["items"]}
157
+
158
+
159
+ def test_application_loads_visidata_config_before_provider_commands(monkeypatch) -> None: # type: ignore[no-untyped-def]
160
+ calls = []
161
+ monkeypatch.setattr(
162
+ "jusi.visidata_support.initialize_visidata",
163
+ lambda **kwargs: calls.append(("initialize", kwargs)),
164
+ )
165
+ monkeypatch.setattr(
166
+ "jusi_postgres.runner.install_postgres_commands",
167
+ lambda: calls.append(("commands", {})),
168
+ )
169
+
170
+ _initialize_visidata_application()
171
+
172
+ assert calls == [
173
+ ("initialize", {"open_name": "selection.sql", "open_filetype": "sql"}),
174
+ ("commands", {}),
175
+ ]
176
+
177
+
123
178
  def test_worker_returns_one_terminal_and_delegates_to_family_router(monkeypatch) -> None: # type: ignore[no-untyped-def]
124
179
  monkeypatch.setattr("jusi_postgres.worker.find_spec", lambda _name: object())
125
180
  worker = create_worker(worker_context())
File without changes
File without changes
File without changes