plsqlwks 0.1.2__tar.gz → 0.1.3__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.
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/PKG-INFO +1 -1
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/__init__.py +1 -1
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/execution.py +138 -24
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/models.py +3 -11
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/session.py +23 -7
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/__init__.py +7 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app.py +37 -13
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_db.py +324 -114
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_editor.py +161 -40
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_files.py +1 -4
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_input.py +28 -1
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_results.py +49 -21
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_tabs_browser.py +25 -6
- plsqlwks-0.1.3/plsqlwks/ui/db_worker.py +228 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/results.py +6 -1
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/state.py +10 -7
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/PKG-INFO +1 -1
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/SOURCES.txt +2 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_db_helpers.py +199 -2
- plsqlwks-0.1.3/tests/test_db_worker.py +464 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_oracle_integration.py +51 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_ui_flows.py +186 -61
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_ui_keys.py +203 -2
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/README.md +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/license.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/__main__.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/__init__.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/loader.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/models.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/paths.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/session.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/config/settings.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/__init__.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/editing.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/explain.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/health.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/metadata.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/sql_analysis.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/db/transactions.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/sqlbinds.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/sqlsplit.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/app_render.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/browser.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/buffer.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/clipboard.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/commands.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/completion.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/constants.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/display.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/errors.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/help.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/keys.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/menu.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/sql.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/ui/syntax.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks/workspace.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/dependency_links.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/entry_points.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/requires.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/plsqlwks.egg-info/top_level.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/pyproject.toml +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/setup.cfg +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_config_workspace.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_db_editing.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_packaging.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_pty_keys.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_rchar.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_sqlbinds.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.3}/tests/test_sqlsplit.py +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from decimal import Decimal
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
import re
|
|
7
8
|
from typing import Any, Mapping
|
|
9
|
+
from uuid import uuid4
|
|
8
10
|
|
|
9
11
|
import oracledb
|
|
10
12
|
|
|
@@ -42,6 +44,13 @@ CREATE_PLSQL_OBJECT_RE = re.compile(
|
|
|
42
44
|
LOB_DISPLAY_LIMIT = 64 * 1024
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
@dataclass
|
|
48
|
+
class _QueryResultContinuationState:
|
|
49
|
+
cursor: Any
|
|
50
|
+
lookahead_row: Any
|
|
51
|
+
elapsed_seconds: float
|
|
52
|
+
|
|
53
|
+
|
|
45
54
|
class ExecutionMixin:
|
|
46
55
|
def execute_statement(
|
|
47
56
|
self,
|
|
@@ -109,7 +118,11 @@ class ExecutionMixin:
|
|
|
109
118
|
more = ""
|
|
110
119
|
continuation = None
|
|
111
120
|
if len(fetched) > self.config.max_rows:
|
|
112
|
-
continuation =
|
|
121
|
+
continuation = self._register_result_continuation(
|
|
122
|
+
cursor,
|
|
123
|
+
fetched[self.config.max_rows],
|
|
124
|
+
elapsed,
|
|
125
|
+
)
|
|
113
126
|
keep_cursor_open = True
|
|
114
127
|
more = f" (limited to {len(rows)} rows)"
|
|
115
128
|
return QueryResult(
|
|
@@ -146,40 +159,104 @@ class ExecutionMixin:
|
|
|
146
159
|
if not keep_cursor_open:
|
|
147
160
|
cursor.close()
|
|
148
161
|
|
|
149
|
-
def fetch_more_rows(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
def fetch_more_rows(
|
|
163
|
+
self,
|
|
164
|
+
continuation: QueryResultContinuation,
|
|
165
|
+
loaded_rows: int,
|
|
166
|
+
) -> QueryResultPage:
|
|
167
|
+
state = self._result_continuations.get(continuation.token)
|
|
168
|
+
if state is None:
|
|
169
|
+
raise RuntimeError("Query result is stale or no longer available")
|
|
153
170
|
started = datetime.now()
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
try:
|
|
172
|
+
fetched = [state.lookahead_row, *state.cursor.fetchmany(self.config.max_rows)]
|
|
173
|
+
elapsed = state.elapsed_seconds + (datetime.now() - started).total_seconds()
|
|
174
|
+
result_rows = fetched[: self.config.max_rows]
|
|
175
|
+
rows, original_rows = materialize_result_rows(result_rows)
|
|
176
|
+
total_loaded_rows = loaded_rows + len(rows)
|
|
177
|
+
more = ""
|
|
178
|
+
next_continuation: QueryResultContinuation | None = None
|
|
179
|
+
if len(fetched) > self.config.max_rows:
|
|
180
|
+
state.lookahead_row = fetched[self.config.max_rows]
|
|
181
|
+
state.elapsed_seconds = elapsed
|
|
182
|
+
next_continuation = continuation
|
|
183
|
+
more = f" (limited to {total_loaded_rows} rows)"
|
|
184
|
+
else:
|
|
185
|
+
self.close_result_continuation(continuation)
|
|
186
|
+
return QueryResultPage(
|
|
187
|
+
rows,
|
|
188
|
+
f"{total_loaded_rows} row(s){more} in {elapsed:.2f}s",
|
|
189
|
+
original_rows,
|
|
190
|
+
next_continuation,
|
|
191
|
+
)
|
|
192
|
+
except BaseException:
|
|
193
|
+
try:
|
|
194
|
+
self.close_result_continuation(continuation)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
def close_result_continuation(self, continuation: QueryResultContinuation) -> None:
|
|
200
|
+
state = self._result_continuations.pop(continuation.token, None)
|
|
201
|
+
if state is not None:
|
|
202
|
+
state.cursor.close()
|
|
203
|
+
|
|
204
|
+
def close_all_result_continuations(self) -> None:
|
|
205
|
+
states = list(self._result_continuations.values())
|
|
206
|
+
self._result_continuations.clear()
|
|
207
|
+
first_error: Exception | None = None
|
|
208
|
+
for state in states:
|
|
209
|
+
try:
|
|
210
|
+
state.cursor.close()
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
if first_error is None:
|
|
213
|
+
first_error = exc
|
|
214
|
+
if first_error is not None:
|
|
215
|
+
raise first_error
|
|
216
|
+
|
|
217
|
+
def _register_result_continuation(
|
|
218
|
+
self,
|
|
219
|
+
cursor: Any,
|
|
220
|
+
lookahead_row: Any,
|
|
221
|
+
elapsed_seconds: float,
|
|
222
|
+
) -> QueryResultContinuation:
|
|
223
|
+
continuation = QueryResultContinuation(uuid4().hex)
|
|
224
|
+
self._result_continuations[continuation.token] = _QueryResultContinuationState(
|
|
225
|
+
cursor,
|
|
226
|
+
lookahead_row,
|
|
227
|
+
elapsed_seconds,
|
|
171
228
|
)
|
|
229
|
+
return continuation
|
|
172
230
|
|
|
173
231
|
def execute_script(self, script: str) -> list[QueryResult]:
|
|
174
232
|
results: list[QueryResult] = []
|
|
175
233
|
statements = split_script(script)
|
|
176
234
|
for idx, statement in enumerate(statements, start=1):
|
|
177
235
|
title = f"Statement {idx} lines {statement.start_line}-{statement.end_line}"
|
|
178
|
-
|
|
236
|
+
result = self.execute_statement(statement.text, title=title)
|
|
237
|
+
self._append_script_result(results, result)
|
|
179
238
|
if not statements:
|
|
180
239
|
results.append(QueryResult("Script", [], [], "No statements to execute."))
|
|
181
240
|
return results
|
|
182
241
|
|
|
242
|
+
def _append_script_result(
|
|
243
|
+
self,
|
|
244
|
+
results: list[QueryResult],
|
|
245
|
+
result: QueryResult,
|
|
246
|
+
) -> None:
|
|
247
|
+
if result.columns and not _is_dbms_output_result(result):
|
|
248
|
+
for previous in reversed(results):
|
|
249
|
+
if not previous.columns or _is_dbms_output_result(previous):
|
|
250
|
+
continue
|
|
251
|
+
if previous.continuation is not None:
|
|
252
|
+
try:
|
|
253
|
+
self.close_result_continuation(previous.continuation)
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
previous.continuation = None
|
|
257
|
+
break
|
|
258
|
+
results.append(result)
|
|
259
|
+
|
|
183
260
|
def export_result(self, result: QueryResult, path: Path) -> None:
|
|
184
261
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
185
262
|
with path.open("w", encoding="utf-8", newline="") as handle:
|
|
@@ -223,6 +300,10 @@ def format_value(value: Any, *, lob_limit: int = LOB_DISPLAY_LIMIT) -> str:
|
|
|
223
300
|
return display
|
|
224
301
|
|
|
225
302
|
|
|
303
|
+
def _is_dbms_output_result(result: QueryResult) -> bool:
|
|
304
|
+
return len(result.columns) == 1 and result.columns[0].upper() == "DBMS_OUTPUT"
|
|
305
|
+
|
|
306
|
+
|
|
226
307
|
def materialize_result_value(
|
|
227
308
|
value: Any,
|
|
228
309
|
*,
|
|
@@ -230,6 +311,9 @@ def materialize_result_value(
|
|
|
230
311
|
) -> tuple[str, Any]:
|
|
231
312
|
if value is None:
|
|
232
313
|
return NULL_DISPLAY_TOKEN, None
|
|
314
|
+
if isinstance(value, oracledb.Cursor):
|
|
315
|
+
value.close()
|
|
316
|
+
return "<REF CURSOR>", "<REF CURSOR>"
|
|
233
317
|
if isinstance(value, oracledb.LOB):
|
|
234
318
|
if lob_limit <= 0:
|
|
235
319
|
raise ValueError("LOB display limit must be positive")
|
|
@@ -243,7 +327,37 @@ def materialize_result_value(
|
|
|
243
327
|
return display, TruncatedLobValue(type_name, size)
|
|
244
328
|
content = value.read()
|
|
245
329
|
return _format_non_lob_value(content), content
|
|
246
|
-
|
|
330
|
+
materialized = _materialize_plain_result_value(value, lob_limit=lob_limit)
|
|
331
|
+
return _format_non_lob_value(materialized), materialized
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _materialize_plain_result_value(value: Any, *, lob_limit: int) -> Any:
|
|
335
|
+
"""Return result data that contains no live python-oracledb handles."""
|
|
336
|
+
if isinstance(value, oracledb.Cursor):
|
|
337
|
+
value.close()
|
|
338
|
+
return "<REF CURSOR>"
|
|
339
|
+
if isinstance(value, oracledb.LOB):
|
|
340
|
+
return materialize_result_value(value, lob_limit=lob_limit)[1]
|
|
341
|
+
if isinstance(value, list):
|
|
342
|
+
return [_materialize_plain_result_value(item, lob_limit=lob_limit) for item in value]
|
|
343
|
+
if isinstance(value, tuple):
|
|
344
|
+
return tuple(_materialize_plain_result_value(item, lob_limit=lob_limit) for item in value)
|
|
345
|
+
if isinstance(value, dict):
|
|
346
|
+
return {
|
|
347
|
+
_materialize_plain_result_value(key, lob_limit=lob_limit): _materialize_plain_result_value(
|
|
348
|
+
item, lob_limit=lob_limit
|
|
349
|
+
)
|
|
350
|
+
for key, item in value.items()
|
|
351
|
+
}
|
|
352
|
+
if isinstance(value, set):
|
|
353
|
+
return {_materialize_plain_result_value(item, lob_limit=lob_limit) for item in value}
|
|
354
|
+
if isinstance(value, frozenset):
|
|
355
|
+
return frozenset(
|
|
356
|
+
_materialize_plain_result_value(item, lob_limit=lob_limit) for item in value
|
|
357
|
+
)
|
|
358
|
+
if type(value).__module__.startswith("oracledb"):
|
|
359
|
+
return str(value)
|
|
360
|
+
return value
|
|
247
361
|
|
|
248
362
|
|
|
249
363
|
def _format_non_lob_value(value: Any) -> str:
|
|
@@ -39,18 +39,9 @@ class RowInsertResult:
|
|
|
39
39
|
display_values: list[str]
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
@dataclass
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
43
|
class QueryResultContinuation:
|
|
44
|
-
|
|
45
|
-
lookahead_row: Any
|
|
46
|
-
elapsed_seconds: float
|
|
47
|
-
closed: bool = False
|
|
48
|
-
|
|
49
|
-
def close(self) -> None:
|
|
50
|
-
if self.closed:
|
|
51
|
-
return
|
|
52
|
-
self.closed = True
|
|
53
|
-
self.cursor.close()
|
|
44
|
+
token: str = field(repr=False)
|
|
54
45
|
|
|
55
46
|
|
|
56
47
|
@dataclass(frozen=True)
|
|
@@ -58,6 +49,7 @@ class QueryResultPage:
|
|
|
58
49
|
rows: list[list[str]]
|
|
59
50
|
message: str
|
|
60
51
|
original_rows: list[list[Any]] = field(default_factory=list, repr=False, compare=False)
|
|
52
|
+
continuation: QueryResultContinuation | None = field(default=None, repr=False, compare=False)
|
|
61
53
|
|
|
62
54
|
|
|
63
55
|
@dataclass
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import oracledb
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from ..config import AppConfig
|
|
6
7
|
from .editing import EditingMixin
|
|
@@ -24,27 +25,41 @@ class OracleWorkspace(
|
|
|
24
25
|
self.read_only = config.read_only
|
|
25
26
|
self.pending_rows_changed = 0
|
|
26
27
|
self.pending_unknown_changes = False
|
|
28
|
+
self._result_continuations: dict[str, Any] = {}
|
|
27
29
|
|
|
28
30
|
def connect(self) -> None:
|
|
29
31
|
from . import read_password
|
|
30
32
|
|
|
31
33
|
password = read_password(self.config.password_file)
|
|
32
34
|
self.close()
|
|
33
|
-
|
|
35
|
+
connection = oracledb.connect(
|
|
34
36
|
user=self.config.user,
|
|
35
37
|
password=password,
|
|
36
38
|
dsn=self.config.dsn,
|
|
37
39
|
)
|
|
38
|
-
self.
|
|
39
|
-
|
|
40
|
+
self.connection = connection
|
|
41
|
+
try:
|
|
42
|
+
self.apply_autocommit()
|
|
43
|
+
self.enable_dbms_output()
|
|
44
|
+
except Exception:
|
|
45
|
+
try:
|
|
46
|
+
connection.close()
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
self.connection = None
|
|
50
|
+
raise
|
|
40
51
|
|
|
41
52
|
def close(self) -> None:
|
|
42
|
-
|
|
53
|
+
connection = self.connection
|
|
54
|
+
try:
|
|
55
|
+
self.close_all_result_continuations()
|
|
56
|
+
finally:
|
|
43
57
|
try:
|
|
44
|
-
|
|
58
|
+
if connection is not None:
|
|
59
|
+
connection.close()
|
|
45
60
|
finally:
|
|
46
61
|
self.connection = None
|
|
47
|
-
|
|
62
|
+
self.clear_pending_transaction()
|
|
48
63
|
|
|
49
64
|
def ensure_connected(self) -> oracledb.Connection:
|
|
50
65
|
if self.connection is None:
|
|
@@ -63,8 +78,9 @@ class OracleWorkspace(
|
|
|
63
78
|
return True
|
|
64
79
|
|
|
65
80
|
def set_autocommit(self, enabled: bool) -> None:
|
|
81
|
+
if self.connection is not None and hasattr(self.connection, "autocommit"):
|
|
82
|
+
self.connection.autocommit = enabled
|
|
66
83
|
self.autocommit = enabled
|
|
67
|
-
self.apply_autocommit()
|
|
68
84
|
|
|
69
85
|
def set_read_only(self, enabled: bool) -> None:
|
|
70
86
|
self.read_only = enabled
|
|
@@ -36,6 +36,7 @@ from .browser import *
|
|
|
36
36
|
from .menu import *
|
|
37
37
|
from .commands import *
|
|
38
38
|
from .results import *
|
|
39
|
+
from .db_worker import *
|
|
39
40
|
from .state import *
|
|
40
41
|
from .app import App, main, parse_args
|
|
41
42
|
|
|
@@ -130,6 +131,12 @@ __all__ = (
|
|
|
130
131
|
"DbOperationEvent",
|
|
131
132
|
"DbOperationFinished",
|
|
132
133
|
"DbOperationProgress",
|
|
134
|
+
"DatabaseWorker",
|
|
135
|
+
"DbCommandHandle",
|
|
136
|
+
"DbSessionState",
|
|
137
|
+
"DbWorkerEvent",
|
|
138
|
+
"DbWorkerFinished",
|
|
139
|
+
"DbWorkerProgress",
|
|
133
140
|
"ERROR_LOCATION_PRIORITY_EXACT",
|
|
134
141
|
"ERROR_LOCATION_PRIORITY_FALLBACK",
|
|
135
142
|
"ERROR_LOCATION_PRIORITY_OFFSET",
|
|
@@ -8,6 +8,7 @@ from ..config import AppConfig, load_config
|
|
|
8
8
|
from ..db import OracleWorkspace, workspace_health
|
|
9
9
|
from ..workspace import ensure_workspace, list_workspace_files
|
|
10
10
|
from .keys import configure_utf8_locale, disable_extended_keyboard_reporting, enable_extended_keyboard_reporting
|
|
11
|
+
from .db_worker import DatabaseWorker
|
|
11
12
|
from .state import UIState
|
|
12
13
|
from .app_render import AppRenderMixin
|
|
13
14
|
from .app_db import AppDbMixin
|
|
@@ -77,7 +78,8 @@ class App(
|
|
|
77
78
|
):
|
|
78
79
|
def __init__(self, screen: curses.window, config: AppConfig):
|
|
79
80
|
self.screen = screen
|
|
80
|
-
self.
|
|
81
|
+
self.db_worker = DatabaseWorker(OracleWorkspace(config))
|
|
82
|
+
self.state = UIState(config=config, db=self.db_worker.session_state)
|
|
81
83
|
self.state.files = list_workspace_files(config)
|
|
82
84
|
self.running = True
|
|
83
85
|
self.message_lines: list[str] = []
|
|
@@ -115,16 +117,38 @@ class App(
|
|
|
115
117
|
self.handle_key(key)
|
|
116
118
|
self.poll_db_operation()
|
|
117
119
|
finally:
|
|
118
|
-
self.
|
|
119
|
-
if extended_keyboard_enabled:
|
|
120
|
-
disable_extended_keyboard_reporting()
|
|
120
|
+
shutdown_timeout = 5.0 if self.state.db_operation is not None else None
|
|
121
121
|
try:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
try:
|
|
123
|
+
if self.state.db_operation is not None:
|
|
124
|
+
try:
|
|
125
|
+
self.db_worker.cancel_current_operation(
|
|
126
|
+
self.state.db_operation.handle.command_id
|
|
127
|
+
)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
if shutdown_timeout is None:
|
|
131
|
+
self.wait_for_db_operation()
|
|
132
|
+
else:
|
|
133
|
+
self.wait_for_db_operation(timeout=shutdown_timeout)
|
|
134
|
+
finally:
|
|
135
|
+
if extended_keyboard_enabled:
|
|
136
|
+
disable_extended_keyboard_reporting()
|
|
137
|
+
finally:
|
|
138
|
+
try:
|
|
139
|
+
try:
|
|
140
|
+
curses.nl()
|
|
141
|
+
except curses.error:
|
|
142
|
+
pass
|
|
143
|
+
try:
|
|
144
|
+
curses.noraw()
|
|
145
|
+
except curses.error:
|
|
146
|
+
pass
|
|
147
|
+
finally:
|
|
148
|
+
try:
|
|
149
|
+
self.close_all_result_continuations()
|
|
150
|
+
finally:
|
|
151
|
+
if shutdown_timeout is None:
|
|
152
|
+
self.shutdown_database_worker()
|
|
153
|
+
else:
|
|
154
|
+
self.shutdown_database_worker(timeout=shutdown_timeout)
|