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.
Files changed (69) hide show
  1. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/PKG-INFO +87 -11
  2. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/README.md +86 -10
  3. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/__init__.py +1 -1
  4. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/execution.py +138 -24
  5. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/models.py +3 -11
  6. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/session.py +23 -7
  7. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/__init__.py +7 -0
  8. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app.py +37 -13
  9. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_db.py +368 -117
  10. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_editor.py +161 -40
  11. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_files.py +1 -4
  12. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_input.py +54 -2
  13. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_results.py +49 -21
  14. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_tabs_browser.py +25 -6
  15. plsqlwks-0.1.4/plsqlwks/ui/db_worker.py +228 -0
  16. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/results.py +6 -1
  17. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/state.py +10 -7
  18. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/PKG-INFO +87 -11
  19. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/SOURCES.txt +2 -0
  20. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_db_helpers.py +199 -2
  21. plsqlwks-0.1.4/tests/test_db_worker.py +464 -0
  22. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_oracle_integration.py +116 -0
  23. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_ui_flows.py +186 -61
  24. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_ui_keys.py +512 -3
  25. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/license.txt +0 -0
  26. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/__main__.py +0 -0
  27. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/__init__.py +0 -0
  28. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/loader.py +0 -0
  29. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/models.py +0 -0
  30. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/paths.py +0 -0
  31. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/session.py +0 -0
  32. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/config/settings.py +0 -0
  33. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/__init__.py +0 -0
  34. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/editing.py +0 -0
  35. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/explain.py +0 -0
  36. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/health.py +0 -0
  37. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/metadata.py +0 -0
  38. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/sql_analysis.py +0 -0
  39. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/db/transactions.py +0 -0
  40. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/sqlbinds.py +0 -0
  41. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/sqlsplit.py +0 -0
  42. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/app_render.py +0 -0
  43. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/browser.py +0 -0
  44. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/buffer.py +0 -0
  45. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/clipboard.py +0 -0
  46. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/commands.py +0 -0
  47. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/completion.py +0 -0
  48. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/constants.py +0 -0
  49. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/display.py +0 -0
  50. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/errors.py +0 -0
  51. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/help.py +0 -0
  52. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/keys.py +0 -0
  53. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/menu.py +0 -0
  54. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/sql.py +0 -0
  55. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/ui/syntax.py +0 -0
  56. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks/workspace.py +0 -0
  57. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/dependency_links.txt +0 -0
  58. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/entry_points.txt +0 -0
  59. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/requires.txt +0 -0
  60. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/plsqlwks.egg-info/top_level.txt +0 -0
  61. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/pyproject.toml +0 -0
  62. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/setup.cfg +0 -0
  63. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_config_workspace.py +0 -0
  64. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_db_editing.py +0 -0
  65. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_packaging.py +0 -0
  66. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_pty_keys.py +0 -0
  67. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_rchar.py +0 -0
  68. {plsqlwks-0.1.2 → plsqlwks-0.1.4}/tests/test_sqlbinds.py +0 -0
  69. {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.2
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 the local Oracle database.
26
+ An ncurses SQL and PL/SQL workspace for Oracle databases.
27
27
 
28
28
  ![PLSQLWKS PREVIEW](https://gitlab.com/unununu/plsqlwks/-/raw/main/img/preview.png)
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=/mnt/d/dev/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 UTF-8 locale for keyboard input and display. If a terminal starts in a plain `C` locale, the app falls back to `C.UTF-8` when available.
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` | Commit the current transaction |
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, current-schema object names, and table/view columns from cached or lazily loaded database metadata; multiple matches open a picker. Use `Alt-+` to reload schema-object metadata and clear cached column metadata.
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 appends the next result page. For simple single-table queries that select `ROWID`, press `Enter` on a non-`ROWID` cell to update that table column by rowid. Press `INS` in the grid to prepare a draft row at the top of the grid, edit its cells with `Enter`, use `Ctrl-Alt-C` to insert it, or use `Esc` to cancel it.
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 typed value that was originally loaded as well as the `ROWID`. If another session changes or deletes the row first, the edit is rejected and the query must be refreshed. NUMBER input uses decimal notation with a period; DATE and TIMESTAMP input uses ISO `YYYY-MM-DD[ HH:MM:SS[.ffffff]]`; RAW and BLOB input uses hexadecimal bytes. Character and CLOB input is preserved as entered. Types that cannot be compared without losing information, including time-zone timestamps and timestamps with precision above six, are read-only in the grid.
231
- Displayed CLOB and BLOB values are limited to the first 65,536 characters or bytes and include a truncation marker with the full size. Truncated LOB cells cannot be edited safely. CSV export uses the displayed result values and therefore retains the same explicit LOB limit; schema-browser DDL is always read in full.
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/ exported query output
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 the local Oracle database.
3
+ An ncurses SQL and PL/SQL workspace for Oracle databases.
4
4
 
5
5
  ![PLSQLWKS PREVIEW](https://gitlab.com/unununu/plsqlwks/-/raw/main/img/preview.png)
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=/mnt/d/dev/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 UTF-8 locale for keyboard input and display. If a terminal starts in a plain `C` locale, the app falls back to `C.UTF-8` when available.
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` | Commit the current transaction |
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, current-schema object names, and table/view columns from cached or lazily loaded database metadata; multiple matches open a picker. Use `Alt-+` to reload schema-object metadata and clear cached column metadata.
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 appends the next result page. For simple single-table queries that select `ROWID`, press `Enter` on a non-`ROWID` cell to update that table column by rowid. Press `INS` in the grid to prepare a draft row at the top of the grid, edit its cells with `Enter`, use `Ctrl-Alt-C` to insert it, or use `Esc` to cancel it.
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 typed value that was originally loaded as well as the `ROWID`. If another session changes or deletes the row first, the edit is rejected and the query must be refreshed. NUMBER input uses decimal notation with a period; DATE and TIMESTAMP input uses ISO `YYYY-MM-DD[ HH:MM:SS[.ffffff]]`; RAW and BLOB input uses hexadecimal bytes. Character and CLOB input is preserved as entered. Types that cannot be compared without losing information, including time-zone timestamps and timestamps with precision above six, are read-only in the grid.
208
- Displayed CLOB and BLOB values are limited to the first 65,536 characters or bytes and include a truncation marker with the full size. Truncated LOB cells cannot be edited safely. CSV export uses the displayed result values and therefore retains the same explicit LOB limit; schema-browser DDL is always read in full.
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/ exported query output
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,3 +1,3 @@
1
1
  """ncurses SQL and PL/SQL workspace."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
@@ -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 = QueryResultContinuation(cursor, fetched[self.config.max_rows], elapsed)
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(self, result: QueryResult) -> QueryResultPage:
150
- continuation = result.continuation
151
- if continuation is None:
152
- return QueryResultPage([], result.message)
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
- fetched = [continuation.lookahead_row, *continuation.cursor.fetchmany(self.config.max_rows)]
155
- elapsed = continuation.elapsed_seconds + (datetime.now() - started).total_seconds()
156
- result_rows = fetched[: self.config.max_rows]
157
- rows, original_rows = materialize_result_rows(result_rows)
158
- loaded_rows = len(result.rows) + len(rows)
159
- more = ""
160
- if len(fetched) > self.config.max_rows:
161
- continuation.lookahead_row = fetched[self.config.max_rows]
162
- continuation.elapsed_seconds = elapsed
163
- more = f" (limited to {loaded_rows} rows)"
164
- else:
165
- continuation.close()
166
- result.continuation = None
167
- return QueryResultPage(
168
- rows,
169
- f"{loaded_rows} row(s){more} in {elapsed:.2f}s",
170
- original_rows,
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
- results.append(self.execute_statement(statement.text, title=title))
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
- return _format_non_lob_value(value), value
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
- cursor: Any
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