plsqlwks 0.1.2__tar.gz → 0.1.4__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.4}/PKG-INFO +87 -11
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/README.md +86 -10
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/__init__.py +1 -1
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/execution.py +138 -24
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/models.py +3 -11
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/session.py +23 -7
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/__init__.py +7 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app.py +37 -13
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_db.py +368 -117
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_editor.py +161 -40
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_files.py +1 -4
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_input.py +54 -2
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_results.py +49 -21
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_tabs_browser.py +25 -6
- plsqlwks-0.1.4/plsqlwks/ui/db_worker.py +228 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/results.py +6 -1
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/state.py +10 -7
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/PKG-INFO +87 -11
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/SOURCES.txt +2 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_db_helpers.py +199 -2
- plsqlwks-0.1.4/tests/test_db_worker.py +464 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_oracle_integration.py +116 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_ui_flows.py +186 -61
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_ui_keys.py +512 -3
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/license.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/__main__.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/__init__.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/loader.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/models.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/paths.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/session.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/settings.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/__init__.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/editing.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/explain.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/health.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/metadata.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/sql_analysis.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/transactions.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/sqlbinds.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/sqlsplit.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_render.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/browser.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/buffer.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/clipboard.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/commands.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/completion.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/constants.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/display.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/errors.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/help.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/keys.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/menu.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/sql.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/syntax.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/workspace.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/dependency_links.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/entry_points.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/requires.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/top_level.txt +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/pyproject.toml +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/setup.cfg +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_config_workspace.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_db_editing.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_packaging.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_pty_keys.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_rchar.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_sqlbinds.py +0 -0
- {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_sqlsplit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plsqlwks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: An ncurses SQL and PL/SQL workspace for Oracle databases.
|
|
5
5
|
License-Expression: LicenseRef-plsqlwks-Donationware
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/unununu/plsqlwks
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
# plsqlwks
|
|
25
25
|
|
|
26
|
-
An ncurses SQL and PL/SQL workspace for
|
|
26
|
+
An ncurses SQL and PL/SQL workspace for Oracle databases.
|
|
27
27
|
|
|
28
28
|

|
|
29
29
|
|
|
@@ -48,15 +48,24 @@ Override the defaults with environment variables:
|
|
|
48
48
|
export ORACLE_USER=hr
|
|
49
49
|
export ORACLE_PASSWORD_FILE=~/.config/plsqlwks/orapass
|
|
50
50
|
export ORACLE_DSN='127.0.0.1:1521/free'
|
|
51
|
-
export PLSQLWKS_WORKSPACE=/
|
|
51
|
+
export PLSQLWKS_WORKSPACE=/path/to/workspace
|
|
52
|
+
export PLSQLWKS_MAX_ROWS=200
|
|
53
|
+
export PLSQLWKS_ARRAYSIZE=100
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
Password files preserve leading and trailing spaces; only final CR/LF line
|
|
55
57
|
endings are removed. On POSIX systems, create the file for the current user and
|
|
56
58
|
protect it with `chmod 600`.
|
|
57
59
|
|
|
60
|
+
`PLSQLWKS_MAX_ROWS` sets the maximum number of rows in each result page, while
|
|
61
|
+
`PLSQLWKS_ARRAYSIZE` sets the Oracle cursor array size. Both values must be
|
|
62
|
+
positive integers.
|
|
63
|
+
|
|
58
64
|
## Install
|
|
59
65
|
|
|
66
|
+
Python 3.10 or newer is required, along with a compatible terminal and a Python
|
|
67
|
+
build that provides the standard-library `curses` module.
|
|
68
|
+
|
|
60
69
|
For development, install the package in editable mode:
|
|
61
70
|
|
|
62
71
|
```bash
|
|
@@ -74,6 +83,21 @@ For a regular local install, use:
|
|
|
74
83
|
python3 -m pip install .
|
|
75
84
|
```
|
|
76
85
|
|
|
86
|
+
Run the default test and lint checks from a development checkout with:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
python3 -m pytest -q
|
|
90
|
+
python3 -m ruff check .
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Oracle integration tests require the connection environment variables shown
|
|
94
|
+
above and an accessible password file. Enable every test group with:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
PLSQLWKS_TEST_ORACLE=1 PLSQLWKS_TEST_PTY=1 PLSQLWKS_TEST_SLOW=1 \
|
|
98
|
+
python3 -m pytest -q -rs
|
|
99
|
+
```
|
|
100
|
+
|
|
77
101
|
## Run
|
|
78
102
|
|
|
79
103
|
After installation, start the workspace with:
|
|
@@ -92,6 +116,15 @@ The CLI option takes precedence over `PLSQLWKS_WORKSPACE`. For compatibility, a
|
|
|
92
116
|
source checkout that already contains a configured `workspace/` continues to
|
|
93
117
|
use it and displays a migration notice; files are never moved automatically.
|
|
94
118
|
|
|
119
|
+
Choose the initial transaction mode for one invocation with `--manual` or
|
|
120
|
+
`--autocommit`. These options override `[database] autocommit` from the active
|
|
121
|
+
`config.ini`:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
plsqlwks --manual
|
|
125
|
+
plsqlwks --autocommit
|
|
126
|
+
```
|
|
127
|
+
|
|
95
128
|
You can also run it directly as a module from the source tree:
|
|
96
129
|
|
|
97
130
|
```bash
|
|
@@ -104,7 +137,25 @@ The app opens a split-screen terminal workspace:
|
|
|
104
137
|
- results/messages below
|
|
105
138
|
- command/status bar at the bottom
|
|
106
139
|
|
|
107
|
-
The terminal UI uses the active
|
|
140
|
+
The terminal UI uses the active locale for keyboard input and display. If that
|
|
141
|
+
locale cannot be initialized, the app tries `C.UTF-8`, `en_US.UTF-8`, and
|
|
142
|
+
`UTF-8`. A valid plain `C` locale remains active, so configure a UTF-8 locale in
|
|
143
|
+
the shell when non-ASCII input or display is required.
|
|
144
|
+
|
|
145
|
+
Oracle operations run serially on one persistent background worker. The UI
|
|
146
|
+
continues to redraw and accept input while an operation runs, but it rejects a
|
|
147
|
+
second database operation until the active one finishes. `Ctrl-C` requests
|
|
148
|
+
interruption of the active operation. Reconnecting closes open result pages and
|
|
149
|
+
clears query results loaded by the previous connection. If a manual transaction
|
|
150
|
+
has pending changes, reconnect first asks whether to commit, roll back, discard
|
|
151
|
+
the session, or cancel. Commit or rollback must succeed before reconnect closes
|
|
152
|
+
the old connection. If either action fails, plsqlwks does not close the current
|
|
153
|
+
connection or clear its loaded results; reconnect can be tried again with an
|
|
154
|
+
explicit discard. Discard closes the session without resolving the transaction
|
|
155
|
+
explicitly, so Oracle rolls its uncommitted work back.
|
|
156
|
+
If the old connection is already dead and reports an error while closing,
|
|
157
|
+
plsqlwks still attempts the replacement connection and reports the close error
|
|
158
|
+
as a warning after a successful reconnect.
|
|
108
159
|
|
|
109
160
|
After a successful quit, plsqlwks records the open file-backed tabs, active tab, and cursor positions in the managed `[session.tabs]` section of `config.ini`. Fresh platform-default workspaces use the user-config directory; explicitly selected and legacy workspaces keep `config.ini` inside the workspace for compatibility. On the next start the app tries each saved path independently, silently skips files that are missing or unreadable, and leaves the initial empty tab in place when none can be opened. A saved cursor position that no longer exists in its file starts at the beginning instead. Untitled, template, and generated schema tabs are not persisted because they do not have a file to reopen.
|
|
110
161
|
|
|
@@ -127,7 +178,7 @@ After a successful quit, plsqlwks records the open file-backed tabs, active tab,
|
|
|
127
178
|
| `Alt-1`..`Alt-9` | Jump to visible file tab |
|
|
128
179
|
| `Ctrl-Q` | Quit |
|
|
129
180
|
| `Ctrl-C` while running | Interrupt the active database operation |
|
|
130
|
-
| `Ctrl-Alt-C` |
|
|
181
|
+
| `Ctrl-Alt-C` | Insert the active result-grid draft, or commit when no draft is active |
|
|
131
182
|
| `Ctrl-Alt-R` | Roll back the current transaction |
|
|
132
183
|
|
|
133
184
|
### Editor
|
|
@@ -182,7 +233,12 @@ Set `[database] remember_bind_values = yes` in the active `config.ini` to prefil
|
|
|
182
233
|
|
|
183
234
|
Editor search is literal and case-insensitive. `Ctrl-F` prompts for text and selects the found occurrence; `Ctrl-N` and `Ctrl-P` repeat the last search forward or backward with wraparound.
|
|
184
235
|
|
|
185
|
-
Editor autocomplete is available with `Shift-Tab`. It completes PL/SQL keywords,
|
|
236
|
+
Editor autocomplete is available with `Shift-Tab`. It completes PL/SQL keywords,
|
|
237
|
+
current-schema object names, and table/view columns from cached or lazily loaded
|
|
238
|
+
database metadata; multiple matches open a picker. Database-object and column
|
|
239
|
+
completion currently supports conventional unquoted identifiers; quoted
|
|
240
|
+
mixed-case names are not preserved. Use `Alt-+` to reload schema-object metadata
|
|
241
|
+
and clear cached column metadata.
|
|
186
242
|
|
|
187
243
|
Editor syntax and explain-plan colors can be overridden in the active `config.ini` with color names or numeric curses color indexes. Unsupported values fall back to the built-in palette for the current terminal:
|
|
188
244
|
|
|
@@ -225,16 +281,35 @@ Read-only mode is a client-side guardrail that rejects statements which the SQL
|
|
|
225
281
|
| `PageUp` / `PageDown` | Scroll explain-plan lines by page |
|
|
226
282
|
| `Home` / `End` | Move to the first/last explain-plan line |
|
|
227
283
|
|
|
228
|
-
When more result rows are available, `PageDown` at the loaded end fetches and
|
|
284
|
+
When more result rows are available, `PageDown` at the loaded end fetches and
|
|
285
|
+
appends the next result page. For simple queries against one conventional
|
|
286
|
+
unquoted current-schema table that include `ROWID`, press `Enter` on a directly
|
|
287
|
+
selected unquoted table column to update it by rowid. Press `INS` in the grid to
|
|
288
|
+
prepare a draft row at the top of the grid, edit its cells with `Enter`, use
|
|
289
|
+
`Ctrl-Alt-C` to insert it, or use `Esc` to cancel it.
|
|
229
290
|
Database null values are displayed and entered as `<NULL>` in editable results. Plain `NULL` is stored as literal text.
|
|
230
|
-
Grid edits compare the
|
|
231
|
-
|
|
291
|
+
Grid edits compare the selected cell's originally loaded typed value as well as
|
|
292
|
+
the `ROWID`. If another session changes that cell or deletes the row first, the
|
|
293
|
+
edit is rejected and the query must be refreshed. Changes to other cells in the
|
|
294
|
+
same row do not cause a conflict. NUMBER input uses decimal notation with a
|
|
295
|
+
period; DATE and TIMESTAMP input uses ISO
|
|
296
|
+
`YYYY-MM-DD[ HH:MM:SS[.ffffff]]`; RAW and BLOB input uses hexadecimal bytes.
|
|
297
|
+
Character and CLOB input is preserved as entered. Types that cannot be compared
|
|
298
|
+
without losing information, including time-zone timestamps and timestamps with
|
|
299
|
+
precision above six, are read-only in the grid.
|
|
300
|
+
Displayed CLOB and BLOB values are limited to the first 65,536 characters or
|
|
301
|
+
bytes and include a truncation marker with the full size. Truncated LOB cells
|
|
302
|
+
cannot be edited safely. Schema-browser DDL is always read in full.
|
|
232
303
|
`F7` cycles a grid fullscreen view that starts with the table header on the first terminal line and uses the last line for data, an editor-only fullscreen view, and a split layout with 2/3 editor and 1/3 data grid.
|
|
233
304
|
|
|
234
305
|
### Schema Browser
|
|
235
306
|
|
|
236
307
|
The `F9` schema browser groups tables, views, procedures, functions, packages, triggers, sequences, indexes, and private synonyms from the current schema. Type while the browser is focused to filter object names with a case-insensitive substring match; groups without matches are hidden and matching groups expand automatically without changing their saved expansion state.
|
|
237
308
|
|
|
309
|
+
Definition loading supports conventional unquoted identifiers. Quoted
|
|
310
|
+
mixed-case objects can appear in the browser but their definitions cannot
|
|
311
|
+
currently be loaded from it.
|
|
312
|
+
|
|
238
313
|
| Key | Action |
|
|
239
314
|
| --- | --- |
|
|
240
315
|
| Printable text | Filter object names |
|
|
@@ -272,7 +347,8 @@ The `F9` schema browser groups tables, views, procedures, functions, packages, t
|
|
|
272
347
|
workspace/
|
|
273
348
|
sql/ saved SQL files
|
|
274
349
|
plsql/ saved PL/SQL files
|
|
275
|
-
results/
|
|
350
|
+
results/ reserved output directory
|
|
276
351
|
```
|
|
277
352
|
|
|
278
|
-
The first launch creates the workspace folders and starter SQL/PLSQL files.
|
|
353
|
+
The first launch creates the workspace folders and starter SQL/PLSQL files. The
|
|
354
|
+
terminal UI does not currently provide a query-export command.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# plsqlwks
|
|
2
2
|
|
|
3
|
-
An ncurses SQL and PL/SQL workspace for
|
|
3
|
+
An ncurses SQL and PL/SQL workspace for Oracle databases.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -25,15 +25,24 @@ Override the defaults with environment variables:
|
|
|
25
25
|
export ORACLE_USER=hr
|
|
26
26
|
export ORACLE_PASSWORD_FILE=~/.config/plsqlwks/orapass
|
|
27
27
|
export ORACLE_DSN='127.0.0.1:1521/free'
|
|
28
|
-
export PLSQLWKS_WORKSPACE=/
|
|
28
|
+
export PLSQLWKS_WORKSPACE=/path/to/workspace
|
|
29
|
+
export PLSQLWKS_MAX_ROWS=200
|
|
30
|
+
export PLSQLWKS_ARRAYSIZE=100
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
Password files preserve leading and trailing spaces; only final CR/LF line
|
|
32
34
|
endings are removed. On POSIX systems, create the file for the current user and
|
|
33
35
|
protect it with `chmod 600`.
|
|
34
36
|
|
|
37
|
+
`PLSQLWKS_MAX_ROWS` sets the maximum number of rows in each result page, while
|
|
38
|
+
`PLSQLWKS_ARRAYSIZE` sets the Oracle cursor array size. Both values must be
|
|
39
|
+
positive integers.
|
|
40
|
+
|
|
35
41
|
## Install
|
|
36
42
|
|
|
43
|
+
Python 3.10 or newer is required, along with a compatible terminal and a Python
|
|
44
|
+
build that provides the standard-library `curses` module.
|
|
45
|
+
|
|
37
46
|
For development, install the package in editable mode:
|
|
38
47
|
|
|
39
48
|
```bash
|
|
@@ -51,6 +60,21 @@ For a regular local install, use:
|
|
|
51
60
|
python3 -m pip install .
|
|
52
61
|
```
|
|
53
62
|
|
|
63
|
+
Run the default test and lint checks from a development checkout with:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python3 -m pytest -q
|
|
67
|
+
python3 -m ruff check .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Oracle integration tests require the connection environment variables shown
|
|
71
|
+
above and an accessible password file. Enable every test group with:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
PLSQLWKS_TEST_ORACLE=1 PLSQLWKS_TEST_PTY=1 PLSQLWKS_TEST_SLOW=1 \
|
|
75
|
+
python3 -m pytest -q -rs
|
|
76
|
+
```
|
|
77
|
+
|
|
54
78
|
## Run
|
|
55
79
|
|
|
56
80
|
After installation, start the workspace with:
|
|
@@ -69,6 +93,15 @@ The CLI option takes precedence over `PLSQLWKS_WORKSPACE`. For compatibility, a
|
|
|
69
93
|
source checkout that already contains a configured `workspace/` continues to
|
|
70
94
|
use it and displays a migration notice; files are never moved automatically.
|
|
71
95
|
|
|
96
|
+
Choose the initial transaction mode for one invocation with `--manual` or
|
|
97
|
+
`--autocommit`. These options override `[database] autocommit` from the active
|
|
98
|
+
`config.ini`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
plsqlwks --manual
|
|
102
|
+
plsqlwks --autocommit
|
|
103
|
+
```
|
|
104
|
+
|
|
72
105
|
You can also run it directly as a module from the source tree:
|
|
73
106
|
|
|
74
107
|
```bash
|
|
@@ -81,7 +114,25 @@ The app opens a split-screen terminal workspace:
|
|
|
81
114
|
- results/messages below
|
|
82
115
|
- command/status bar at the bottom
|
|
83
116
|
|
|
84
|
-
The terminal UI uses the active
|
|
117
|
+
The terminal UI uses the active locale for keyboard input and display. If that
|
|
118
|
+
locale cannot be initialized, the app tries `C.UTF-8`, `en_US.UTF-8`, and
|
|
119
|
+
`UTF-8`. A valid plain `C` locale remains active, so configure a UTF-8 locale in
|
|
120
|
+
the shell when non-ASCII input or display is required.
|
|
121
|
+
|
|
122
|
+
Oracle operations run serially on one persistent background worker. The UI
|
|
123
|
+
continues to redraw and accept input while an operation runs, but it rejects a
|
|
124
|
+
second database operation until the active one finishes. `Ctrl-C` requests
|
|
125
|
+
interruption of the active operation. Reconnecting closes open result pages and
|
|
126
|
+
clears query results loaded by the previous connection. If a manual transaction
|
|
127
|
+
has pending changes, reconnect first asks whether to commit, roll back, discard
|
|
128
|
+
the session, or cancel. Commit or rollback must succeed before reconnect closes
|
|
129
|
+
the old connection. If either action fails, plsqlwks does not close the current
|
|
130
|
+
connection or clear its loaded results; reconnect can be tried again with an
|
|
131
|
+
explicit discard. Discard closes the session without resolving the transaction
|
|
132
|
+
explicitly, so Oracle rolls its uncommitted work back.
|
|
133
|
+
If the old connection is already dead and reports an error while closing,
|
|
134
|
+
plsqlwks still attempts the replacement connection and reports the close error
|
|
135
|
+
as a warning after a successful reconnect.
|
|
85
136
|
|
|
86
137
|
After a successful quit, plsqlwks records the open file-backed tabs, active tab, and cursor positions in the managed `[session.tabs]` section of `config.ini`. Fresh platform-default workspaces use the user-config directory; explicitly selected and legacy workspaces keep `config.ini` inside the workspace for compatibility. On the next start the app tries each saved path independently, silently skips files that are missing or unreadable, and leaves the initial empty tab in place when none can be opened. A saved cursor position that no longer exists in its file starts at the beginning instead. Untitled, template, and generated schema tabs are not persisted because they do not have a file to reopen.
|
|
87
138
|
|
|
@@ -104,7 +155,7 @@ After a successful quit, plsqlwks records the open file-backed tabs, active tab,
|
|
|
104
155
|
| `Alt-1`..`Alt-9` | Jump to visible file tab |
|
|
105
156
|
| `Ctrl-Q` | Quit |
|
|
106
157
|
| `Ctrl-C` while running | Interrupt the active database operation |
|
|
107
|
-
| `Ctrl-Alt-C` |
|
|
158
|
+
| `Ctrl-Alt-C` | Insert the active result-grid draft, or commit when no draft is active |
|
|
108
159
|
| `Ctrl-Alt-R` | Roll back the current transaction |
|
|
109
160
|
|
|
110
161
|
### Editor
|
|
@@ -159,7 +210,12 @@ Set `[database] remember_bind_values = yes` in the active `config.ini` to prefil
|
|
|
159
210
|
|
|
160
211
|
Editor search is literal and case-insensitive. `Ctrl-F` prompts for text and selects the found occurrence; `Ctrl-N` and `Ctrl-P` repeat the last search forward or backward with wraparound.
|
|
161
212
|
|
|
162
|
-
Editor autocomplete is available with `Shift-Tab`. It completes PL/SQL keywords,
|
|
213
|
+
Editor autocomplete is available with `Shift-Tab`. It completes PL/SQL keywords,
|
|
214
|
+
current-schema object names, and table/view columns from cached or lazily loaded
|
|
215
|
+
database metadata; multiple matches open a picker. Database-object and column
|
|
216
|
+
completion currently supports conventional unquoted identifiers; quoted
|
|
217
|
+
mixed-case names are not preserved. Use `Alt-+` to reload schema-object metadata
|
|
218
|
+
and clear cached column metadata.
|
|
163
219
|
|
|
164
220
|
Editor syntax and explain-plan colors can be overridden in the active `config.ini` with color names or numeric curses color indexes. Unsupported values fall back to the built-in palette for the current terminal:
|
|
165
221
|
|
|
@@ -202,16 +258,35 @@ Read-only mode is a client-side guardrail that rejects statements which the SQL
|
|
|
202
258
|
| `PageUp` / `PageDown` | Scroll explain-plan lines by page |
|
|
203
259
|
| `Home` / `End` | Move to the first/last explain-plan line |
|
|
204
260
|
|
|
205
|
-
When more result rows are available, `PageDown` at the loaded end fetches and
|
|
261
|
+
When more result rows are available, `PageDown` at the loaded end fetches and
|
|
262
|
+
appends the next result page. For simple queries against one conventional
|
|
263
|
+
unquoted current-schema table that include `ROWID`, press `Enter` on a directly
|
|
264
|
+
selected unquoted table column to update it by rowid. Press `INS` in the grid to
|
|
265
|
+
prepare a draft row at the top of the grid, edit its cells with `Enter`, use
|
|
266
|
+
`Ctrl-Alt-C` to insert it, or use `Esc` to cancel it.
|
|
206
267
|
Database null values are displayed and entered as `<NULL>` in editable results. Plain `NULL` is stored as literal text.
|
|
207
|
-
Grid edits compare the
|
|
208
|
-
|
|
268
|
+
Grid edits compare the selected cell's originally loaded typed value as well as
|
|
269
|
+
the `ROWID`. If another session changes that cell or deletes the row first, the
|
|
270
|
+
edit is rejected and the query must be refreshed. Changes to other cells in the
|
|
271
|
+
same row do not cause a conflict. NUMBER input uses decimal notation with a
|
|
272
|
+
period; DATE and TIMESTAMP input uses ISO
|
|
273
|
+
`YYYY-MM-DD[ HH:MM:SS[.ffffff]]`; RAW and BLOB input uses hexadecimal bytes.
|
|
274
|
+
Character and CLOB input is preserved as entered. Types that cannot be compared
|
|
275
|
+
without losing information, including time-zone timestamps and timestamps with
|
|
276
|
+
precision above six, are read-only in the grid.
|
|
277
|
+
Displayed CLOB and BLOB values are limited to the first 65,536 characters or
|
|
278
|
+
bytes and include a truncation marker with the full size. Truncated LOB cells
|
|
279
|
+
cannot be edited safely. Schema-browser DDL is always read in full.
|
|
209
280
|
`F7` cycles a grid fullscreen view that starts with the table header on the first terminal line and uses the last line for data, an editor-only fullscreen view, and a split layout with 2/3 editor and 1/3 data grid.
|
|
210
281
|
|
|
211
282
|
### Schema Browser
|
|
212
283
|
|
|
213
284
|
The `F9` schema browser groups tables, views, procedures, functions, packages, triggers, sequences, indexes, and private synonyms from the current schema. Type while the browser is focused to filter object names with a case-insensitive substring match; groups without matches are hidden and matching groups expand automatically without changing their saved expansion state.
|
|
214
285
|
|
|
286
|
+
Definition loading supports conventional unquoted identifiers. Quoted
|
|
287
|
+
mixed-case objects can appear in the browser but their definitions cannot
|
|
288
|
+
currently be loaded from it.
|
|
289
|
+
|
|
215
290
|
| Key | Action |
|
|
216
291
|
| --- | --- |
|
|
217
292
|
| Printable text | Filter object names |
|
|
@@ -249,7 +324,8 @@ The `F9` schema browser groups tables, views, procedures, functions, packages, t
|
|
|
249
324
|
workspace/
|
|
250
325
|
sql/ saved SQL files
|
|
251
326
|
plsql/ saved PL/SQL files
|
|
252
|
-
results/
|
|
327
|
+
results/ reserved output directory
|
|
253
328
|
```
|
|
254
329
|
|
|
255
|
-
The first launch creates the workspace folders and starter SQL/PLSQL files.
|
|
330
|
+
The first launch creates the workspace folders and starter SQL/PLSQL files. The
|
|
331
|
+
terminal UI does not currently provide a query-export command.
|
|
@@ -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
|