plsqlwks 0.1.7__tar.gz → 0.1.8__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 (91) hide show
  1. plsqlwks-0.1.8/ARCHITECTURE.md +86 -0
  2. plsqlwks-0.1.8/COMPATIBILITY.md +498 -0
  3. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/MANIFEST.in +5 -0
  4. plsqlwks-0.1.7/README.md → plsqlwks-0.1.8/PKG-INFO +178 -49
  5. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/PLUGINS.md +39 -31
  6. plsqlwks-0.1.8/QUICKSTART.html +597 -0
  7. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/QUICKSTART.md +11 -5
  8. plsqlwks-0.1.7/PKG-INFO → plsqlwks-0.1.8/README.md +147 -76
  9. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/__init__.py +1 -1
  10. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/__main__.py +0 -1
  11. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/config/__init__.py +3 -4
  12. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/config/loader.py +2 -3
  13. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/config/paths.py +2 -3
  14. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/config/session.py +2 -3
  15. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/config/settings.py +4 -6
  16. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/__init__.py +14 -5
  17. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/editing.py +155 -109
  18. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/execution.py +74 -60
  19. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/explain.py +3 -2
  20. plsqlwks-0.1.8/plsqlwks/db/identifiers.py +64 -0
  21. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/metadata.py +47 -8
  22. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/models.py +4 -7
  23. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/session.py +16 -9
  24. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/sql_analysis.py +39 -2
  25. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/transactions.py +28 -26
  26. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/exporting.py +45 -25
  27. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/html_exporting.py +21 -14
  28. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/plugins/__init__.py +0 -1
  29. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/plugins/_result_export.py +58 -19
  30. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/plugins/api.py +2 -2
  31. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/plugins/csv_export.py +58 -23
  32. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/plugins/html_export.py +66 -28
  33. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/plugins/loader.py +17 -15
  34. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/plugins/xlsx_export.py +65 -30
  35. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/sqlbinds.py +3 -9
  36. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/sqlsplit.py +25 -25
  37. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/__init__.py +12 -12
  38. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/app.py +38 -35
  39. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/application_controller.py +16 -2
  40. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/browser.py +7 -4
  41. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/buffer.py +6 -7
  42. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/catalog.py +188 -42
  43. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/clipboard.py +5 -5
  44. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/commands.py +9 -3
  45. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/completion.py +93 -34
  46. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/constants.py +16 -1
  47. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/db_operations.py +143 -65
  48. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/db_session.py +21 -45
  49. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/db_worker.py +36 -31
  50. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/dialogs.py +13 -37
  51. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/documents.py +8 -30
  52. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/editor_controller.py +73 -71
  53. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/errors.py +9 -20
  54. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/help.py +13 -2
  55. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/input_controller.py +15 -17
  56. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/keys.py +88 -1
  57. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/menu.py +6 -18
  58. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/plugin_host.py +56 -17
  59. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/ports.py +10 -2
  60. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/query_controller.py +32 -59
  61. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/renderer.py +43 -99
  62. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/result_controller.py +53 -67
  63. plsqlwks-0.1.8/plsqlwks/ui/result_export.py +404 -0
  64. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/result_presenter.py +62 -99
  65. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/results.py +44 -17
  66. plsqlwks-0.1.8/plsqlwks/ui/sql.py +226 -0
  67. plsqlwks-0.1.8/plsqlwks/ui/state.py +841 -0
  68. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/syntax.py +19 -6
  69. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/viewport.py +254 -17
  70. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/workspace.py +0 -1
  71. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/xlsx_exporting.py +177 -79
  72. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks.egg-info/SOURCES.txt +5 -0
  73. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/pyproject.toml +31 -20
  74. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/pytest.ini +1 -0
  75. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/tests/conftest.py +6 -16
  76. plsqlwks-0.1.7/plsqlwks/ui/sql.py +0 -158
  77. plsqlwks-0.1.7/plsqlwks/ui/state.py +0 -284
  78. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/license.txt +0 -0
  79. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/config/models.py +0 -0
  80. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/db/health.py +0 -0
  81. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/command_dispatcher.py +0 -0
  82. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/display.py +0 -0
  83. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plsqlwks/ui/key_reader.py +0 -0
  84. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plugin-requirements/csv-export/requirements.txt +0 -0
  85. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plugin-requirements/html-export/requirements.txt +0 -0
  86. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/plugin-requirements/xlsx-export/requirements.txt +0 -0
  87. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/rchar.py +0 -0
  88. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/requirements.txt +0 -0
  89. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/setup.cfg +0 -0
  90. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/tests/fixtures/config_exports.txt +0 -0
  91. {plsqlwks-0.1.7 → plsqlwks-0.1.8}/tests/fixtures/ui_exports.txt +0 -0
@@ -0,0 +1,86 @@
1
+ # Architecture
2
+
3
+ `plsqlwks` is a terminal-first application with one curses thread, one
4
+ long-lived database worker thread, and explicit boundaries around Oracle and
5
+ installed plugins.
6
+
7
+ ## Runtime flow
8
+
9
+ ```text
10
+ KeyReader
11
+ -> InputController / CommandDispatcher
12
+ -> focused controller
13
+ -> DatabaseOperations
14
+ -> DatabaseWorker (OracleWorkspace)
15
+ -> completion event
16
+ -> DatabaseOperations.poll
17
+ -> ResultPresenter or operation callback
18
+ -> UIState
19
+ -> ViewportController
20
+ -> Renderer
21
+ ```
22
+
23
+ `ui.app.App` is the composition root and owns the event loop. `KeyReader`
24
+ normalizes terminal input, `InputController` routes it by focus, and
25
+ `CommandDispatcher` resolves stable command IDs. A focused controller validates
26
+ input and prepares an operation on the curses thread. Local document, editor,
27
+ and plugin actions may finish there without crossing the database boundary.
28
+
29
+ Database work goes through `DatabaseOperations`, which allows one foreground
30
+ operation and submits a database-only callable to `DatabaseWorker`. The worker
31
+ owns the single `OracleWorkspace`, connection, cursors, and continuation
32
+ registry; it executes commands in FIFO order and emits progress or completion
33
+ events. Polling those events on the curses thread updates `UIState` and invokes
34
+ `ResultPresenter` or an operation-specific callback. The presenter materializes
35
+ status, focus, errors, and per-tab results. On the next frame,
36
+ `ViewportController` prepares a layout snapshot and `Renderer` reads that
37
+ snapshot plus `UIState` to draw curses. The curses thread never calls Oracle
38
+ directly, and the worker never draws or prompts.
39
+
40
+ ## Transaction state
41
+
42
+ The worker-owned `OracleWorkspace` is the detailed source of truth. It tracks
43
+ the connection, `autocommit`, the client-side `read_only` guardrail, known
44
+ pending row changes, and whether uncommitted work has an unknown size or
45
+ outcome. After every command the UI receives only an immutable
46
+ `DbSessionState(connected, autocommit, read_only, has_uncommitted_changes)`
47
+ snapshot.
48
+
49
+ - In manual mode, successful DML adds its row count and PL/SQL marks pending
50
+ work as unknown. Rollback to a savepoint is also treated conservatively as
51
+ unknown.
52
+ - Successful commit, rollback, and DDL clear tracked pending state. Driver
53
+ transaction status may clear stale state or mark an otherwise untracked live
54
+ transaction as unknown. State is cleared only after the driver operation
55
+ succeeds.
56
+ - Changing from manual mode to autocommit and quitting with pending work require
57
+ an explicit commit, rollback, or cancel decision. Reconnect may additionally
58
+ discard a dead session. Interrupts and disconnects preserve conservative
59
+ warnings; live cursor and editing handles are detached while already
60
+ materialized rows remain viewable.
61
+
62
+ Database privileges remain the security boundary. Read-only mode only rejects
63
+ statements the client recognizes as writing.
64
+
65
+ ## Plugin API boundary
66
+
67
+ The supported public surface is exactly the names exported by
68
+ `plsqlwks.plugins`. Installed factories are discovered through the
69
+ `plsqlwks.plugins` entry-point group, return `Plugin` and `PluginCommand`
70
+ metadata, and must match Plugin API version 1. The loader validates IDs,
71
+ versions, command uniqueness, and callability; a broken external plugin becomes
72
+ a startup warning instead of preventing the application from starting.
73
+
74
+ `PluginHost` and `UIPluginContext` are the sole UI adapters. A synchronous
75
+ handler can inspect an immutable snapshot of already loaded display rows, read
76
+ the results directory, detect an insert draft, prompt for text or overwrite
77
+ confirmation, set status, and report an error. A snapshot exposes no cursor or
78
+ continuation token, and only aligned immutable numeric source values may cross
79
+ alongside display text.
80
+
81
+ Plugin API version 1 does not expose the database worker, `OracleWorkspace`,
82
+ mutable results or `UIState`, curses objects, global shortcut registration,
83
+ lifecycle events, background jobs, settings schemas, hot reload, or
84
+ workspace-local executable modules. Plugins are trusted in-process Python code,
85
+ not a security sandbox. See [PLUGINS.md](PLUGINS.md) for the complete author
86
+ contract and examples.
@@ -0,0 +1,498 @@
1
+ # Oracle compatibility
2
+
3
+ This document is the canonical compatibility and integration-test contract for
4
+ plsqlwks. Oracle Database 19c and Oracle AI Database 26ai are mandatory
5
+ release-gate targets. A target is described as continuously tested only after
6
+ both protected CI systems record ten consecutive qualifying green runs spanning
7
+ at least 30 days; until then, the narrower release-gate-target wording applies.
8
+
9
+ ## Compatibility matrix
10
+
11
+ Both releases use python-oracledb Thin mode with password-file authentication.
12
+ `Easy Connect` means a `host:port/service` DSN; `full descriptor` means a
13
+ complete `(DESCRIPTION=...)` connect descriptor for the same service.
14
+
15
+ | Profile | Easy Connect coverage | Full-descriptor coverage |
16
+ | --- | --- | --- |
17
+ | Developer | Safety preflight and the complete live Oracle suite | Connection, identity, version, endpoint-fingerprint, and driver-mode smoke test |
18
+ | DML-only | Qualified SELECT/INSERT/UPDATE/DELETE against the developer-owned fixture, followed by cleanup and rollback; DDL must be denied | Connection, identity, version, endpoint-fingerprint, and driver-mode smoke test |
19
+ | Read-only | Qualified SELECT succeeds; DML and DDL must be denied by Oracle | Connection, identity, version, endpoint-fingerprint, and driver-mode smoke test |
20
+
21
+ The complete six-cell matrix runs for each release-gate target:
22
+
23
+ | Target | Accepted server version |
24
+ | --- | --- |
25
+ | Oracle Database 19c | Reported numeric major version is `19` |
26
+ | Oracle AI Database 26ai | Reported numeric release is `23.26` or later in the `23.x` family |
27
+
28
+ The developer suite creates, discovers, loads, and removes representative
29
+ tables, views, procedures, functions, packages, triggers, sequences, indexes,
30
+ and private synonyms, covering every object type advertised by the schema
31
+ browser. It also covers two contrasting session NLS profiles, quoted Unicode
32
+ identifiers, DATE and timestamp types, exact-boundary and approximately 1 MiB
33
+ CLOB/BLOB values, compiler warnings, cancellation of an active long-running
34
+ query, and bounded connection-failure recovery. These mutation-heavy scenarios
35
+ run in the developer/Easy Connect cell; the other five cells retain their
36
+ connection, identity, fingerprint, and privilege-boundary purposes.
37
+
38
+ A qualifying protected pipeline must select both Oracle target jobs after all
39
+ required hygiene, quality, Python 3.10/3.14 test, and build jobs; every selected
40
+ Oracle test must pass without a skip, allowed failure, cancellation, or pending
41
+ gate. A failed or incomplete qualifying pipeline restarts the consecutive-run
42
+ count. Ad-hoc local runs and experimental connection jobs never count toward
43
+ the public claim.
44
+
45
+ Other Oracle releases may be used for ad-hoc development, but are not part of
46
+ the compatibility claim. Thick mode, TNS aliases, wallets, TCPS-specific
47
+ configuration, external authentication, proxy authentication, and driver or
48
+ database failover configurations are not release-gating combinations.
49
+
50
+ ### Time-zone timestamp behavior
51
+
52
+ Oracle `DATE` has no time-zone component. Plain `DATE` and `TIMESTAMP(6)` values
53
+ are materialized as NLS-independent Python datetimes and ISO display strings.
54
+ The current pinned python-oracledb Thin driver does not preserve the offset or
55
+ zone of `TIMESTAMP WITH TIME ZONE` and `TIMESTAMP WITH LOCAL TIME ZONE` values,
56
+ and it truncates precision beyond Python microseconds. Returning the resulting
57
+ naive datetime would falsely imply a lossless value.
58
+
59
+ For that reason, non-null zoned timestamp cells are read-only and use these
60
+ fail-safe display and export values:
61
+
62
+ - `<TIMESTAMP WITH TIME ZONE: lossless Thin-mode fetch unavailable; use TO_CHAR>`
63
+ - `<TIMESTAMP WITH LOCAL TIME ZONE: lossless Thin-mode fetch unavailable; use TO_CHAR>`
64
+
65
+ The result also carries a warning. Select a lossless companion string explicitly
66
+ when exact data is required, for example:
67
+
68
+ ```sql
69
+ to_char(zoned_timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.FF9 TZH:TZM')
70
+ ```
71
+
72
+ For a local-time-zone column, first cast it at the required precision to
73
+ `TIMESTAMP(9) WITH TIME ZONE`; its rendered value follows `SESSIONTIMEZONE`.
74
+ Named-zone timestamp fetches remain an explicit Thin-mode limitation.
75
+
76
+ ### Experimental connection matrices
77
+
78
+ No executable experimental connection job or runtime switch is part of the
79
+ current release gate. The following names reserve separate future contracts so
80
+ their results cannot be mistaken for coverage of the mandatory Thin matrix:
81
+
82
+ | Reserved matrix | Required isolated infrastructure |
83
+ | --- | --- |
84
+ | `oracle-experimental-thick` | A separate process and protected runner image with reviewed Oracle Instant Client libraries |
85
+ | `oracle-experimental-wallet-tcps` | A dedicated TCPS endpoint and disposable protected wallet/configuration files |
86
+ | `oracle-experimental-tns-alias` | A disposable `tnsnames.ora`/`TNS_ADMIN` configuration and a dedicated alias |
87
+
88
+ Each future experiment must retain the identity, version, endpoint-fingerprint,
89
+ privilege, guard-token, and database-lock checks used by the base matrix. It
90
+ starts as a separate non-gating job with separate secrets and no artifacts or
91
+ caches. It can be added to the compatibility claim only after becoming
92
+ compulsory on protected pipelines and independently meeting the same ten-run,
93
+ 30-day evidence rule.
94
+
95
+ The schema browser and metadata completion deliberately enumerate the current
96
+ login schema. Qualified cross-schema SQL is covered by the DML-only and
97
+ read-only profiles, but it does not add cross-schema objects to the browser.
98
+ ROWID-backed grid editing and insertion also remain limited to one base table
99
+ in the current schema. Exact quoted and Unicode table/column names are covered;
100
+ qualified cross-schema results are viewable but not grid-editable.
101
+
102
+ ## Fail-closed safety gate
103
+
104
+ Matrix endpoints must be isolated, disposable databases containing no
105
+ production data. Before any selected Oracle test executes, the session-level
106
+ preflight performs all of these checks:
107
+
108
+ - every required value is present and every password/token path is a nonempty
109
+ regular file;
110
+ - the three usernames are distinct conventional unquoted identifiers;
111
+ - every profile connects through both DSN forms as its expected session user
112
+ and current schema, without proxy authentication, and each account is local
113
+ and not Oracle-maintained, inherited, implicit, or shard-wide;
114
+ - server version, `DB_UNIQUE_NAME`, container name, and service name match the
115
+ protected expected values, and the database role is `PRIMARY`;
116
+ - python-oracledb is using Thin mode with password authentication, and the
117
+ guard token matches the protected digest in the developer schema;
118
+ - effective/direct system privileges, direct/effective object grants, column
119
+ grants, outgoing grants, and empty roles exactly match the contracts below;
120
+ restricted schemas own no objects, every grant option is `NO`, and DBA,
121
+ user-administration, `ANY`, unlimited-tablespace, and public mutation paths
122
+ are rejected;
123
+ - on 26ai, both effective and directly received schema privileges are empty.
124
+ - the developer's effective `CONNECT_TIME` and `IDLE_TIME` profile limits are
125
+ both `UNLIMITED`, so they cannot terminate and roll back the guard session.
126
+
127
+ Any mismatch is an infrastructure failure, not a skipped test. Preflight errors
128
+ identify the failed setting but do not print usernames, DSNs, password paths,
129
+ passwords, or the guard token. The token is bound into this comparison and is
130
+ never stored or logged as plaintext:
131
+
132
+ ```sql
133
+ lower(rawtohex(standard_hash(:token, 'SHA256')))
134
+ ```
135
+
136
+ Only after every non-mutating check succeeds, a dedicated developer connection
137
+ acquires the verified `PLSQLWKS_ORACLE_MATRIX` guard row with
138
+ `SELECT ... FOR UPDATE NOWAIT` (the zero-wait equivalent of `WAIT 0`) and holds
139
+ that row lock for the entire pytest session. If another
140
+ GitHub, GitLab, or local run already holds it, the newcomer fails before test
141
+ mutation instead of waiting or overlapping. Teardown rolls back and closes the
142
+ dedicated connection; Oracle also releases the lock automatically if the test
143
+ process or connection dies. This database-level mutex is the cross-platform
144
+ serialization boundary.
145
+ Before and after every selected Oracle test, the harness performs another
146
+ token-bound `SELECT ... FOR UPDATE NOWAIT` on the existing lock connection. It
147
+ first requires the driver's transaction-in-progress flag and never reconnects
148
+ that guard session. A lost connection, rolled-back lock, or competing holder
149
+ therefore fails closed before the next test can mutate.
150
+
151
+ DML probes use a new 32-character UUID hex key, delete their own probe row, and
152
+ roll back. DDL-denial probes run only after rollback because Oracle DDL can
153
+ implicitly commit pending work. If a denial probe unexpectedly creates an
154
+ object, the test removes that object in `finally` and fails. Read-only denial
155
+ probes disable the plsqlwks client guard so the Oracle privilege boundary is
156
+ what rejects DML and DDL.
157
+
158
+ CI contains application credentials only. It must never contain or use a DBA,
159
+ `SYS`, `SYSTEM`, user-management, grant-management, or schema-bootstrap
160
+ credential. Account creation and grants are a separate, deliberate DBA action.
161
+ The live harness proves that its ordinary sessions are not DBA sessions and
162
+ rejects every unexpected normal-session privilege. It intentionally cannot
163
+ query the password file and does not make repeated administrative-login
164
+ attempts, which could trigger authentication controls. Before storing any test
165
+ credential, a DBA must therefore verify that none of the three accounts has a
166
+ password-file administrative authorization:
167
+
168
+ ```sql
169
+ select username, sysdba, sysoper, sysasm, sysbackup, sysdg, syskm
170
+ from v$pwfile_users
171
+ where username in (upper('<DEV_USER>'),
172
+ upper('<DML_USER>'),
173
+ upper('<READ_ONLY_USER>'));
174
+ ```
175
+
176
+ This query must return no rows. The private runner host must not be a database
177
+ host and its service accounts must not belong to Oracle operating-system
178
+ administration groups.
179
+
180
+ ## Pre-provisioning contract
181
+
182
+ Provision three different conventional unquoted users on each disposable
183
+ target: `<DEV_USER>`, `<DML_USER>`, and `<READ_ONLY_USER>`. Use the site's secret
184
+ management process to create password-authenticated accounts. Give only the
185
+ developer exactly one positive finite quota, no greater than 64 MiB, on a
186
+ dedicated test tablespace; the other accounts must have no tablespace quota.
187
+
188
+ The effective rows from `SESSION_PRIVS` must be exactly:
189
+
190
+ - developer: `CREATE SESSION`, `ALTER SESSION`, `CREATE TABLE`, `CREATE VIEW`,
191
+ `CREATE PROCEDURE`, `CREATE TRIGGER`, `CREATE SEQUENCE`, and `CREATE SYNONYM`;
192
+ - DML-only and read-only: `CREATE SESSION`.
193
+
194
+ `SESSION_ROLES` must be empty for all three profiles, and all required
195
+ privileges must be direct grants. In particular, do not grant `CONNECT`,
196
+ `RESOURCE`, `DBA`, `UNLIMITED TABLESPACE`, catalog roles, or a privilege
197
+ containing `ANY`. This avoids trusting object privileges inherited through a
198
+ role that `SESSION_PRIVS` cannot expose.
199
+
200
+ The DML-only and read-only schemas must have zero rows in `USER_OBJECTS` and no
201
+ column-level grants. On 26ai, `SESSION_SCHEMA_PRIVS` and `USER_SCHEMA_PRIVS`
202
+ must also be empty for every profile. The developer's outgoing object grants
203
+ must be exactly the five fixture grants shown below; it must have no outgoing
204
+ column grants, no grant on the guard table, and no grant to `PUBLIC`. The
205
+ preflight also rejects any effective public table/column mutation privilege
206
+ visible to a profile.
207
+
208
+ Assign the developer a dedicated profile whose `CONNECT_TIME` and `IDLE_TIME`
209
+ are both `UNLIMITED`; the preflight verifies the effective values through
210
+ `USER_RESOURCE_LIMITS`. A DBA must also ensure that no Database Resource Manager
211
+ `MAX_IDLE_BLOCKER_TIME` or priority-transaction policy targets this isolated
212
+ test account, because such a policy could roll back the guard-row transaction.
213
+
214
+ After creating the accounts, a DBA applies the following template. Angle
215
+ brackets are deliberate non-executable placeholders and must be replaced with
216
+ the chosen safe identifiers.
217
+
218
+ ```sql
219
+ grant create session, alter session, create table, create view,
220
+ create procedure, create trigger, create sequence, create synonym
221
+ to <DEV_USER>;
222
+ alter user <DEV_USER> quota 64M on <TEST_TABLESPACE>;
223
+
224
+ grant create session to <DML_USER>;
225
+ grant create session to <READ_ONLY_USER>;
226
+
227
+ grant execute on sys.dbms_output to <DEV_USER>;
228
+ grant execute on sys.dbms_metadata to <DEV_USER>;
229
+ grant execute on sys.dbms_xplan to <DEV_USER>;
230
+ grant execute on sys.dbms_session to <DEV_USER>;
231
+ grant execute on sys.dbms_utility to <DEV_USER>;
232
+ grant execute on sys.dbms_lob to <DEV_USER>;
233
+
234
+ grant execute on sys.dbms_output to <DML_USER>;
235
+ grant execute on sys.dbms_output to <READ_ONLY_USER>;
236
+
237
+ grant select on sys.v_$sql to <DEV_USER>;
238
+ grant select on sys.v_$sql_plan to <DEV_USER>;
239
+ grant select on sys.v_$sql_plan_statistics_all to <DEV_USER>;
240
+ grant select on sys.v_$session to <DEV_USER>;
241
+ ```
242
+
243
+ These are exact allowlists, not minimums. An additional direct system or object
244
+ grant, an admin/grant option, or any received role makes preflight fail.
245
+
246
+ Generate a high-entropy token outside Oracle, keep its plaintext only in a
247
+ protected local file or CI secret, and calculate its lowercase SHA-256 hex
248
+ digest.
249
+ Then connect as `<DEV_USER>` and create the two permanent fixtures. Replace
250
+ `<LOWERCASE_TOKEN_SHA256>` with the digest, never the plaintext token.
251
+
252
+ For example, this standard-library command creates a new token file
253
+ exclusively, sets mode `0600`, and prints only the digest to copy into the SQL:
254
+
255
+ ```bash
256
+ python3 - /protected/guard-token <<'PY'
257
+ from hashlib import sha256
258
+ from pathlib import Path
259
+ from secrets import token_hex
260
+ import sys
261
+
262
+ path = Path(sys.argv[1])
263
+ token = token_hex(32)
264
+ with path.open("x", encoding="ascii") as stream:
265
+ stream.write(token)
266
+ path.chmod(0o600)
267
+ print(sha256(token.encode("ascii")).hexdigest())
268
+ PY
269
+ ```
270
+
271
+ ```sql
272
+ create table plsqlwks_compat_guard (
273
+ guard_name varchar2(30 char) primary key,
274
+ token_sha256 varchar2(64 char) not null,
275
+ constraint plsqlwks_guard_token_sha_ck check (
276
+ regexp_like(token_sha256, '^[0-9a-f]{64}$', 'c')
277
+ )
278
+ );
279
+
280
+ insert into plsqlwks_compat_guard (guard_name, token_sha256)
281
+ values ('PLSQLWKS_ORACLE_MATRIX', '<LOWERCASE_TOKEN_SHA256>');
282
+
283
+ create table plsqlwks_compat_fixture (
284
+ probe_id varchar2(32 char) primary key,
285
+ probe_value varchar2(200 char) not null
286
+ );
287
+
288
+ insert into plsqlwks_compat_fixture (probe_id, probe_value)
289
+ values ('READ_ONLY_BASELINE', 'compatibility fixture');
290
+
291
+ grant select, insert, update, delete on plsqlwks_compat_fixture to <DML_USER>;
292
+ grant select on plsqlwks_compat_fixture to <READ_ONLY_USER>;
293
+ commit;
294
+ ```
295
+
296
+ Do not grant either limited user access to `PLSQLWKS_COMPAT_GUARD`. Verify each
297
+ profile in a new session with `SESSION_PRIVS`, `SESSION_ROLES`,
298
+ `USER_SYS_PRIVS`, `USER_ROLE_PRIVS`, `USER_TAB_PRIVS_RECD`,
299
+ `USER_COL_PRIVS_RECD`, `USER_TAB_PRIVS_MADE`, `USER_COL_PRIVS_MADE`, and
300
+ `USER_OBJECTS`, `USER_TS_QUOTAS`, and `USER_RESOURCE_LIMITS`. On 26ai also inspect `SESSION_SCHEMA_PRIVS` and
301
+ `USER_SCHEMA_PRIVS`. Obtain the fingerprint values used by CI with:
302
+
303
+ ```sql
304
+ select sys_context('USERENV', 'DB_UNIQUE_NAME') as db_unique_name,
305
+ sys_context('USERENV', 'CON_NAME') as con_name,
306
+ sys_context('USERENV', 'SERVICE_NAME') as service_name
307
+ from dual;
308
+ ```
309
+
310
+ ## Running the matrix locally
311
+
312
+ The existing developer-only live suite remains available through
313
+ `python3 tools/dev.py test oracle`. A complete matrix run uses the
314
+ `oracle-matrix` profile, chooses `19c` or `26ai`, and supplies all of the
315
+ following generic variables:
316
+
317
+ ```bash
318
+ export ORACLE_USER='<developer user>'
319
+ export ORACLE_DSN='db.example.test:1521/service'
320
+ export ORACLE_PASSWORD_FILE='/protected/dev-password'
321
+ export PLSQLWKS_TEST_ORACLE_DESCRIPTOR_DSN='(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=db.example.test)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=service)))'
322
+ export PLSQLWKS_TEST_ORACLE_DML_USER='<dml user>'
323
+ export PLSQLWKS_TEST_ORACLE_DML_PASSWORD_FILE='/protected/dml-password'
324
+ export PLSQLWKS_TEST_ORACLE_READ_ONLY_USER='<read-only user>'
325
+ export PLSQLWKS_TEST_ORACLE_READ_ONLY_PASSWORD_FILE='/protected/read-only-password'
326
+ export PLSQLWKS_TEST_ORACLE_EXPECTED_DB_UNIQUE_NAME='<DB_UNIQUE_NAME>'
327
+ export PLSQLWKS_TEST_ORACLE_EXPECTED_CON_NAME='<container name>'
328
+ export PLSQLWKS_TEST_ORACLE_EXPECTED_SERVICE_NAME='<service name>'
329
+ export PLSQLWKS_TEST_ORACLE_GUARD_TOKEN_FILE='/protected/guard-token'
330
+
331
+ PLSQLWKS_TEST_ORACLE_TARGET=19c \
332
+ python3 tools/dev.py test oracle-matrix
333
+ ```
334
+
335
+ Password and token files must be nonempty regular files; protect them with
336
+ mode `0600` on POSIX systems. The developer DSN must be Easy Connect and the
337
+ descriptor variable must be a full descriptor for the same endpoint. Omitting
338
+ the matrix flag preserves the existing ad-hoc developer-suite behavior, but it
339
+ does not acquire the guard-row mutex. Never point an unguarded ad-hoc run at a
340
+ shared release-gating endpoint; use a separate disposable database or enable
341
+ the complete matrix.
342
+
343
+ ## Protected CI release gates
344
+
345
+ Both CI systems define the same required 19c and 26ai matrix, use only private
346
+ self-hosted runners, and have no schedule or nightly trigger. GitLab explicitly
347
+ rejects scheduled pipelines and fork merge-request pipelines. Missing
348
+ configuration fails the selected job. Test output stays in the job log; the CI
349
+ definitions do not upload artifacts or use vendor-hosted remote caches from the
350
+ live Oracle jobs. Separate non-Oracle Python 3.10 and 3.14 gates retain JUnit,
351
+ Cobertura XML, and coverage JSON for seven days. Build and installed-wheel smoke
352
+ verification stay in one job so no distribution artifact transfer is needed.
353
+ The shared GitLab Oracle template explicitly sets both live-test opt-in flags,
354
+ declares blocking `needs` edges to repository hygiene, quality, both parallel
355
+ Python test instances, and build smoke, disables dependency artifact downloads,
356
+ and is non-interruptible. The GitHub jobs use the equivalent explicit flags,
357
+ blocking dependencies, and non-cancelling concurrency groups.
358
+
359
+ The authoritative CI definitions are `.github/workflows/ci.yml` and
360
+ `.gitlab-ci.yml`; a duplicate root `ci.yml` is intentionally not maintained.
361
+ Both definitions call the versioned `tools/dev.py` commands for installation,
362
+ linting, test profiles, hygiene, and build smoke checks, keeping shared behavior
363
+ in one locally runnable source. Installs and nested wheel smoke environments use
364
+ the exact Linux/Python 3.10/3.14 dependency set in `constraints/ci.txt`; dependency
365
+ updates are deliberate reviewed changes rather than upgrades performed by each job.
366
+
367
+ Every GitLab job runs a standard-library preflight before installation or test
368
+ work. It verifies the requested Python version, `curses`, usable PTYs, the selected
369
+ Python image, a disposable Docker environment, both required runner tags, and
370
+ required checkout inputs. Oracle jobs additionally validate every mapped matrix
371
+ variable, the job-specific workspace prerequisites, and all four private secret
372
+ files. Failures name only the unmet condition or variable label: the preflight
373
+ never prints a username, DSN, secret-file path or contents, raw operating-system
374
+ error, or configuration representation.
375
+
376
+ Every live Oracle job creates a new mode-`0700` workspace with a job-unique,
377
+ validated path and exports it as `PLSQLWKS_WORKSPACE`. The always-run cleanup
378
+ deletes it only after verifying a job-created ownership marker; GitLab binds
379
+ that marker to the job token with an HMAC. A pre-existing path fails the job
380
+ and is not deleted. This prevents stale `config.ini` settings on a persistent
381
+ private runner from changing transaction or read-only behavior.
382
+
383
+ ### GitLab variables and jobs
384
+
385
+ For each prefix `ORACLE_19C` and `ORACLE_26AI`, configure these protected CI/CD
386
+ variables:
387
+
388
+ - `<PREFIX>_USER`, `<PREFIX>_DSN`, and file-type
389
+ `<PREFIX>_PASSWORD_FILE` for the developer profile;
390
+ - `<PREFIX>_DESCRIPTOR_DSN`;
391
+ - `<PREFIX>_DML_USER` and file-type `<PREFIX>_DML_PASSWORD_FILE`;
392
+ - `<PREFIX>_READ_ONLY_USER` and file-type
393
+ `<PREFIX>_READ_ONLY_PASSWORD_FILE`;
394
+ - `<PREFIX>_EXPECTED_DB_UNIQUE_NAME`, `<PREFIX>_EXPECTED_CON_NAME`, and
395
+ `<PREFIX>_EXPECTED_SERVICE_NAME`;
396
+ - file-type `<PREFIX>_GUARD_TOKEN_FILE`.
397
+
398
+ Mark values protected and mask values where GitLab supports masking. The two
399
+ explicit jobs, `oracle-integration-19c` and `oracle-integration-26ai`, run only
400
+ on protected refs, fail on an incomplete matrix, and use per-release resource
401
+ groups so two GitLab pipelines cannot mutate one endpoint at the same time.
402
+ They are release-gating jobs, not scheduled or nightly jobs. The guard-row
403
+ mutex also prevents overlap with GitHub or local runs.
404
+
405
+ All GitLab jobs require both private runner tags, `plsqlwks` and `docker`. Project
406
+ maintainers must disable GitLab instance runners and every non-compliant group or
407
+ project runner, protect the selected project runner, and disable **Run untagged
408
+ jobs**. Without a runner carrying both tags, jobs intentionally remain pending,
409
+ so hosted-runner minutes are never consumed.
410
+
411
+ ### GitHub secrets and jobs
412
+
413
+ For each prefix `ORACLE_19C` and `ORACLE_26AI`, configure these protected
414
+ GitHub Actions secrets:
415
+
416
+ - `<PREFIX>_USER`, `<PREFIX>_DSN`, `<PREFIX>_PASSWORD`, and
417
+ `<PREFIX>_DESCRIPTOR_DSN`;
418
+ - `<PREFIX>_DML_USER` and `<PREFIX>_DML_PASSWORD`;
419
+ - `<PREFIX>_READ_ONLY_USER` and `<PREFIX>_READ_ONLY_PASSWORD`;
420
+ - `<PREFIX>_EXPECTED_DB_UNIQUE_NAME`, `<PREFIX>_EXPECTED_CON_NAME`, and
421
+ `<PREFIX>_EXPECTED_SERVICE_NAME`;
422
+ - `<PREFIX>_GUARD_TOKEN`.
423
+
424
+ Store these secrets in the protected `oracle-integration` environment and
425
+ restrict that environment to trusted branches and reviewers. The workflow
426
+ writes each password and token value to a temporary mode-`0600` file, maps that
427
+ path to the generic test variable, and removes the file during job cleanup.
428
+ Live Oracle jobs run only for repository-owned protected refs or a trusted
429
+ manual dispatch; they have no schedule, do not run for pull requests from
430
+ forks, and never expose these secrets to fork code. All other workflow jobs
431
+ also skip fork pull requests before selecting the private runner. Each job uses
432
+ `runs-on: [self-hosted, linux, x64, plsqlwks]`; no job names a GitHub-hosted
433
+ runner label. The two live jobs have separate per-release concurrency groups
434
+ and do not cancel an in-progress database run. Disable any hosted-runner
435
+ fallback allowed by the repository or organization policy. With no matching
436
+ private runner, a job must remain queued instead of consuming hosted-runner
437
+ minutes. The guard-row mutex additionally prevents overlap with GitLab or local
438
+ runs that use the same endpoint. Every referenced GitHub action is pinned to a
439
+ reviewed full commit SHA; update those pins only as a deliberate dependency
440
+ change.
441
+
442
+ ### Private runner setup and first run
443
+
444
+ Use a dedicated Linux x64 runner host that contains no production credentials
445
+ or data and can reach only the required disposable Oracle endpoints and package
446
+ sources. Do not reuse a credentialed runner for untrusted fork code.
447
+
448
+ 1. Install Python 3.10 and 3.14, Python build tooling, a compiler/toolchain, and
449
+ the platform prerequisites needed by this repository. For GitLab, use the
450
+ Docker executor because the Python 3.10/3.14 matrix selects
451
+ `python:<version>-slim` images and rejects other executor environments.
452
+ 2. In the GitHub repository or organization runner UI, follow the official
453
+ [self-hosted runner setup](https://docs.github.com/en/actions/reference/runners/self-hosted-runners),
454
+ create a runner, and run the displayed registration command and short-lived
455
+ token on the host. Keep the default `self-hosted`, `linux`, and `x64` labels
456
+ and add `plsqlwks`; never store the registration token in the repository.
457
+ 3. In the GitLab project or group runner UI, follow the official
458
+ [runner setup](https://docs.gitlab.com/ci/runners/), create a protected project
459
+ runner with tags `plsqlwks` and `docker`, disable **Run untagged jobs**, and run
460
+ the displayed registration command and token on the same host or a separately
461
+ isolated host. Select the Docker executor and `python:3.14-slim` as its default
462
+ image. Do not retain the token outside the runner's protected configuration.
463
+ 4. Install both runner agents as system services under dedicated unprivileged
464
+ operating-system accounts. Do not store Oracle passwords or guard tokens in
465
+ the runner checkout or service environment. From the configured GitHub
466
+ runner directory, `sudo ./svc.sh install`, `sudo ./svc.sh start`, and
467
+ `sudo ./svc.sh status` install, start, and inspect that service. For a
468
+ package-installed GitLab runner, use `sudo gitlab-runner start`,
469
+ `sudo gitlab-runner status`, and `sudo gitlab-runner verify`.
470
+ 5. Disable GitLab instance runners, non-compliant group/project runners, and
471
+ GitHub-hosted fallback in project, organization, and billing/policy settings.
472
+ Disable vendor-hosted remote-cache use for these workflows. Confirm a
473
+ deliberately unmatched label leaves a harmless test job pending/queued rather
474
+ than selecting a hosted runner, then cancel that job.
475
+ 6. Add the protected GitLab variables and GitHub secrets listed above. Protect
476
+ the GitLab runner and variables, and configure the GitHub
477
+ `oracle-integration` environment for trusted branches or tags with the
478
+ desired approval rules.
479
+ 7. Start the services. In GitHub open **Actions > CI > Run workflow** and choose
480
+ a protected branch. In GitLab open **Build > Pipelines > New pipeline** and
481
+ choose a protected ref. Verify in both job UIs that the selected runner has
482
+ the expected private identity and the required `plsqlwks` and `docker` tags
483
+ before allowing the Oracle commands to run.
484
+ 8. Confirm both Oracle version jobs pass, leave only secret-free test output in
485
+ the logs, remove temporary credential files, and upload no Oracle artifacts
486
+ or caches.
487
+ Stop and investigate immediately if either platform selects an unexpected
488
+ runner.
489
+
490
+ Self-hosted runner hardware or cloud instances, network traffic, electricity,
491
+ maintenance, and Oracle licensing remain the operator's responsibility. The
492
+ private-runner guarantee is specifically that these definitions consume no
493
+ GitHub/GitLab hosted-runner minutes and upload no Oracle artifacts or remote
494
+ caches; only non-Oracle machine-readable test and coverage reports are retained
495
+ for seven days. A job waits when the private runner is unavailable. Confirm the
496
+ account-side boundary in the current [GitHub Actions billing documentation](https://docs.github.com/en/billing/concepts/product-billing/github-actions)
497
+ and [GitLab compute-minute documentation](https://docs.gitlab.com/ci/pipelines/instance_runner_compute_minutes/)
498
+ before the first run.
@@ -3,7 +3,10 @@
3
3
  global-exclude *
4
4
 
5
5
  include MANIFEST.in
6
+ include ARCHITECTURE.md
7
+ include COMPATIBILITY.md
6
8
  include PLUGINS.md
9
+ include QUICKSTART.html
7
10
  include QUICKSTART.md
8
11
  include README.md
9
12
  include license.txt
@@ -33,6 +36,7 @@ include plsqlwks/db/editing.py
33
36
  include plsqlwks/db/execution.py
34
37
  include plsqlwks/db/explain.py
35
38
  include plsqlwks/db/health.py
39
+ include plsqlwks/db/identifiers.py
36
40
  include plsqlwks/db/metadata.py
37
41
  include plsqlwks/db/models.py
38
42
  include plsqlwks/db/session.py
@@ -76,6 +80,7 @@ include plsqlwks/ui/ports.py
76
80
  include plsqlwks/ui/query_controller.py
77
81
  include plsqlwks/ui/renderer.py
78
82
  include plsqlwks/ui/result_controller.py
83
+ include plsqlwks/ui/result_export.py
79
84
  include plsqlwks/ui/result_presenter.py
80
85
  include plsqlwks/ui/results.py
81
86
  include plsqlwks/ui/sql.py