plsqlwks 0.2.1__tar.gz → 0.2.2__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 (131) hide show
  1. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/PKG-INFO +6 -2
  2. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/README.md +5 -1
  3. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/doc/API.md +5 -1
  4. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/doc/ARCHITECTURE.md +30 -2
  5. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/doc/PLUGIN_CATALOG.md +53 -7
  6. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/doc/QUICKSTART.md +3 -2
  7. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/__init__.py +1 -1
  8. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/table_export.py +79 -38
  9. plsqlwks-0.2.2/plsqlwks/db/table_export_catalog.py +488 -0
  10. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/sqlsplit.py +10 -0
  11. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/app.py +6 -1
  12. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/buffer.py +47 -1
  13. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/clipboard.py +22 -5
  14. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/dialogs.py +17 -0
  15. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/editor_controller.py +1 -0
  16. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/ports.py +2 -0
  17. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/query_controller.py +3 -0
  18. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/renderer.py +102 -16
  19. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/syntax.py +140 -0
  20. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/table_export.py +10 -4
  21. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks.egg-info/SOURCES.txt +1 -0
  22. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/doc/COMPATIBILITY.md +0 -0
  23. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/doc/PLUGINS.md +0 -0
  24. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/doc/QUICKSTART.html +0 -0
  25. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/license.txt +0 -0
  26. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/__main__.py +0 -0
  27. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/cli.py +0 -0
  28. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/config/__init__.py +0 -0
  29. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/config/loader.py +0 -0
  30. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/config/models.py +0 -0
  31. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/config/paths.py +0 -0
  32. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/config/session.py +0 -0
  33. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/config/settings.py +0 -0
  34. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/__init__.py +0 -0
  35. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/ddl_remap.py +0 -0
  36. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/dependencies.py +0 -0
  37. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/editing.py +0 -0
  38. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/execution.py +0 -0
  39. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/explain.py +0 -0
  40. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/health.py +0 -0
  41. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/identifiers.py +0 -0
  42. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/metadata.py +0 -0
  43. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/models.py +0 -0
  44. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/session.py +0 -0
  45. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/sql_analysis.py +0 -0
  46. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/timestamps.py +0 -0
  47. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/db/transactions.py +0 -0
  48. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/exporting.py +0 -0
  49. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/plugins/__init__.py +0 -0
  50. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/plugins/api.py +0 -0
  51. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/plugins/loader.py +0 -0
  52. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/plugins/management.py +0 -0
  53. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/sqlbinds.py +0 -0
  54. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/__init__.py +0 -0
  55. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/application_controller.py +0 -0
  56. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/browser.py +0 -0
  57. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/catalog.py +0 -0
  58. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/code_generation.py +0 -0
  59. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/command_dispatcher.py +0 -0
  60. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/commands.py +0 -0
  61. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/completion.py +0 -0
  62. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/constants.py +0 -0
  63. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/db_operations.py +0 -0
  64. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/db_session.py +0 -0
  65. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/db_worker.py +0 -0
  66. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/dependency_walker.py +0 -0
  67. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/display.py +0 -0
  68. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/documents.py +0 -0
  69. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/errors.py +0 -0
  70. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/help.py +0 -0
  71. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/input_controller.py +0 -0
  72. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/key_reader.py +0 -0
  73. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/keys.py +0 -0
  74. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/menu.py +0 -0
  75. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/output_selection.py +0 -0
  76. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/plugin_host.py +0 -0
  77. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/result_controller.py +0 -0
  78. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/result_export.py +0 -0
  79. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/result_presenter.py +0 -0
  80. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/results.py +0 -0
  81. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/sql.py +0 -0
  82. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/sqlplus.py +0 -0
  83. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/state.py +0 -0
  84. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/ui/viewport.py +0 -0
  85. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plsqlwks/workspace.py +0 -0
  86. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv-export/install.plw +0 -0
  87. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv-export/payload/__init__.py +0 -0
  88. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv-export/payload/plugin.py +0 -0
  89. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv-export/payload/support.py +0 -0
  90. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv-export/payload/writer.py +0 -0
  91. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv2oratab/install.plw +0 -0
  92. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv2oratab/payload/__init__.py +0 -0
  93. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv2oratab/payload/csv2oratab.py +0 -0
  94. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/csv2oratab/payload/plugin.py +0 -0
  95. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/dependencies-walker/install.plw +0 -0
  96. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/dependencies-walker/payload/__init__.py +0 -0
  97. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/dependencies-walker/payload/plugin.py +0 -0
  98. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/html-export/install.plw +0 -0
  99. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/html-export/payload/__init__.py +0 -0
  100. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/html-export/payload/html_exporting.py +0 -0
  101. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/html-export/payload/plugin.py +0 -0
  102. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/html-export/payload/support.py +0 -0
  103. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/html-export/payload/writer.py +0 -0
  104. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/packages/csv-export-1.0.0.plwgn +0 -0
  105. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/packages/csv2oratab-0.1.0.plwgn +0 -0
  106. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/packages/dependencies-walker-0.1.0.plwgn +0 -0
  107. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/packages/html-export-1.0.0.plwgn +0 -0
  108. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/packages/sqlplus-0.1.0.plwgn +0 -0
  109. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/packages/table-ddl-export-0.1.1.plwgn +0 -0
  110. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/packages/xlsx-export-1.0.0.plwgn +0 -0
  111. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/sqlplus/install.plw +0 -0
  112. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/sqlplus/payload/__init__.py +0 -0
  113. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/sqlplus/payload/plugin.py +0 -0
  114. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/table-ddl-export/install.plw +0 -0
  115. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/table-ddl-export/payload/__init__.py +0 -0
  116. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/table-ddl-export/payload/plugin.py +0 -0
  117. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/xlsx-export/install.plw +0 -0
  118. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/xlsx-export/payload/__init__.py +0 -0
  119. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/xlsx-export/payload/plugin.py +0 -0
  120. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/xlsx-export/payload/support.py +0 -0
  121. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/plugins/xlsx-export/payload/xlsx_exporting.py +0 -0
  122. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/pyproject.toml +0 -0
  123. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/pytest.ini +0 -0
  124. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/rchar.py +0 -0
  125. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/requirements.txt +0 -0
  126. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/setup.cfg +0 -0
  127. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/tests/conftest.py +0 -0
  128. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/tests/fixtures/config_exports.txt +0 -0
  129. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/tests/fixtures/ui_exports.txt +0 -0
  130. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/tools/putty_keys.ps1 +0 -0
  131. {plsqlwks-0.2.1 → plsqlwks-0.2.2}/tools/windows_keys.cmd +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plsqlwks
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: An ncurses SQL and PL/SQL workspace for Oracle databases.
5
5
  Author: unu2000
6
6
  License-Expression: LicenseRef-plsqlwks-Donationware
@@ -749,12 +749,16 @@ also works from Command Prompt and continues to support PuTTY.
749
749
  | `Ctrl+=` | Reconnect |
750
750
 
751
751
  Use `/` on a line by itself after PL/SQL objects or anonymous blocks when running a script.
752
+ With no selection, `F11` asks: **To run the whole buffer, press Enter. Esc cancels.**
753
+ Press Enter to continue or Esc/Ctrl+C to cancel before bind prompts and execution.
754
+ Executing selected text does not require this additional confirmation.
752
755
  Script execution reports statement progress as `n/total` and stops at the first
753
756
  failed statement while preserving earlier results. Scripts are sent directly to
754
757
  Oracle, not through SQL*Plus or SQLcl: client commands such as `PROMPT`, `SPOOL`,
755
758
  and `SET SERVEROUTPUT`, and `&`/`&&` substitution variables, are rejected before
756
759
  bind prompts or execution. Oracle SQL statements such as `SET TRANSACTION`
757
- remain supported.
760
+ remain supported. A standalone `SET DEFINE OFF` line is accepted locally and
761
+ omitted from database execution. It does not enable substitution variables.
758
762
  Execution and explain errors report mapped editor line and column diagnostics.
759
763
  The cursor moves only when the buffer still matches the source that was sent to
760
764
  Oracle; if it changed while the operation ran, the result warns about the stale
@@ -715,12 +715,16 @@ also works from Command Prompt and continues to support PuTTY.
715
715
  | `Ctrl+=` | Reconnect |
716
716
 
717
717
  Use `/` on a line by itself after PL/SQL objects or anonymous blocks when running a script.
718
+ With no selection, `F11` asks: **To run the whole buffer, press Enter. Esc cancels.**
719
+ Press Enter to continue or Esc/Ctrl+C to cancel before bind prompts and execution.
720
+ Executing selected text does not require this additional confirmation.
718
721
  Script execution reports statement progress as `n/total` and stops at the first
719
722
  failed statement while preserving earlier results. Scripts are sent directly to
720
723
  Oracle, not through SQL*Plus or SQLcl: client commands such as `PROMPT`, `SPOOL`,
721
724
  and `SET SERVEROUTPUT`, and `&`/`&&` substitution variables, are rejected before
722
725
  bind prompts or execution. Oracle SQL statements such as `SET TRANSACTION`
723
- remain supported.
726
+ remain supported. A standalone `SET DEFINE OFF` line is accepted locally and
727
+ omitted from database execution. It does not enable substitution variables.
724
728
  Execution and explain errors report mapped editor line and column diagnostics.
725
729
  The cursor moves only when the buffer still matches the source that was sent to
726
730
  Oracle; if it changed while the operation ran, the result warns about the stale
@@ -547,7 +547,11 @@ Required source-schema tables and views are followed recursively; tables include
547
547
  their constraints, indexes, triggers, and detected sequences. Selection of callers stays
548
548
  limited to direct references to the initially checked tables. Definitions are
549
549
  ordered by creation dependencies across object types; unresolved prerequisites
550
- and genuine dependency cycles are reported in the generated script.
550
+ and genuine dependency cycles are reported in the generated script. Discovery
551
+ prefers ALL_* catalogs and supplements them with accessible DBA_* views.
552
+ Cross-schema definitions use catalog rendering for supported objects and
553
+ DBMS_METADATA fallback for advanced or incomplete metadata. Own-schema
554
+ definitions retain DBMS_METADATA extraction.
551
555
 
552
556
  On success the host opens and activates a new unsaved tab containing the generated
553
557
  SQL, with keyboard focus in the editor. Cancellation,
@@ -47,6 +47,32 @@ UI command as force-disconnected, quarantines that worker, and closes the exact
47
47
  captured client connection on a best-effort control thread. No server-side kill
48
48
  or process termination is attempted, and other tab workers are unaffected.
49
49
 
50
+ ## Editor rendering
51
+
52
+ The SQL document remains in memory, with a lazy `ViewportSyntax` cache owned by
53
+ each `Buffer`. The cache keeps line-boundary lexer states and code-bracket
54
+ positions for scanned lines, but retains tokens only for the visible rows and
55
+ one screen above and below. Buffer edits, completion, and undo/redo report their
56
+ changed line ranges. Parsing resumes from the first invalid line and reuses the
57
+ unchanged suffix once its incoming lexer state agrees again. Display and cursor
58
+ movement do not join or tokenize the entire document.
59
+
60
+ Cold jumps and edits that change multiline strings or comments may need context
61
+ from earlier lines. Parsing and distant bracket searches run in cooperative
62
+ 8 ms batches on the curses thread. Unresolved rows appear immediately in the
63
+ default text style; accurate highlighting follows as context becomes available.
64
+ The event loop polls input and database completions between batches, without
65
+ the normal 200 ms idle delay. The budget is checked between lines and bracket
66
+ positions; an exceptionally long individual line can exceed it.
67
+
68
+ `Renderer` retains a curses window sized to the editor viewport. Small vertical
69
+ scrolls move its existing rows, and row signatures limit painting to exposed or
70
+ changed text, selection, and bracket overlays. Geometry or active-buffer changes
71
+ invalidate the appropriate rows. The root screen and editor window compose via
72
+ `noutrefresh()` and one `doupdate()`, restoring the active cursor after composition.
73
+ Recomposing the retained editor also restores it after overlapping dialogs and
74
+ external terminal use. No document-sized curses pad or rendering thread is used.
75
+
50
76
  ## Transaction state
51
77
 
52
78
  Each worker-owned `OracleWorkspace` is the detailed source of truth. It tracks
@@ -116,8 +142,10 @@ The Table DDL Export command delegates through `start_table_ddl_export()`.
116
142
  Its host controller owns the table checklist with incremental substring filtering
117
143
  and cancellable worker jobs. Its database service discovers related sequences
118
144
  and directly referencing source-schema program units using bound dictionary
119
- reads, orders included routines by dependency, and extracts definitions using
120
- per-handle DBMS_METADATA transforms. Generation creates a fresh unsaved tab only for the originating
145
+ reads, orders included routines by dependency, and supplements ALL_* discovery
146
+ with accessible DBA_* views. Cross-schema definitions use conservative catalog
147
+ renderers; own-schema and unsupported definitions use per-handle DBMS_METADATA
148
+ transforms. Generation creates a fresh unsaved tab only for the originating
121
149
  editor/session. Identifier-aware remapping preserves literals and comments and
122
150
  reports ambiguous references. The plugin itself never receives database or UI
123
151
  internals and generated SQL is never automatically executed.
@@ -70,12 +70,21 @@ Type in the table list to filter names immediately by a case-insensitive literal
70
70
  substring. **Backspace** edits the filter, **Ctrl+L** clears it, and **/** opens
71
71
  an optional filter prompt for text containing spaces. An empty filter shows all
72
72
  tables. Use arrows/Page Up/Page Down/Home/End to navigate, **Space** to toggle
73
- an entry, **Ctrl+A** to select matching tables, and **Ctrl+U** to deselect them.
73
+ an entry, **F2** to select matching tables, and **Ctrl+U** to deselect them.
74
74
  Checked tables stay selected when hidden by a filter; the screen shows both
75
75
  total and visible selection counts. **Enter** exports the checked tables,
76
76
  including hidden selections. **Esc** or **Ctrl+C** cancels, including during
77
77
  metadata reads. At least one table must be checked.
78
78
 
79
+ During metadata reads and DDL generation, the screen shows **Elapsed: HH:MM:SS**.
80
+ The timer resets for each operation and excludes time spent selecting tables
81
+ or answering prompts. There is no percentage estimate.
82
+
83
+ Generated scripts begin with `SET DEFINE OFF` so SQL*Plus does not prompt for
84
+ ampersands in definitions, such as an `&afcid` string literal. PLSQLWKS accepts
85
+ this standalone directive locally without sending it to Oracle. Generation
86
+ opens the script in a new unsaved editor tab; it does not execute it.
87
+
79
88
  There is no sequence-selection dialog. Source-schema sequences referenced by
80
89
  defaults and Oracle-recorded table/trigger/program-unit dependencies are included
81
90
  automatically. Trigger bodies are also scanned for explicit NEXTVAL/CURRVAL
@@ -89,7 +98,11 @@ For ordinary sequences, START WITH uses the exporting session's CURRVAL plus
89
98
  INCREMENT BY when CURRVAL is defined, taking priority over the metadata cache
90
99
  boundary. A cycling sequence wraps at its configured bounds; an exhausted
91
100
  noncycling sequence stops the export with an error. If CURRVAL is undefined in
92
- that session (ORA-08002), the original DBMS_METADATA START WITH is preserved.
101
+ that session (ORA-08002), the metadata START WITH is preserved. Cross-schema
102
+ catalog rendering obtains that snapshot from ALL_SEQUENCES.LAST_NUMBER or,
103
+ when available, DBA_SEQUENCES.LAST_NUMBER. If the account cannot read CURRVAL,
104
+ the snapshot is retained after confirming the sequence remains catalog-visible.
105
+ Dictionary access alone does not grant access to sequence values.
93
106
  Scalable/sharded sequences retain their metadata restart position with a review
94
107
  comment because raw CURRVAL includes offsets. Other sequence options remain intact.
95
108
 
@@ -147,11 +160,44 @@ SQL, ambiguous qualifiers, and source qualifiers in strings receive review
147
160
  notes. Dependencies outside the export appear as prerequisites in the script
148
161
  header; Oracle metadata cannot discover every dynamic or indirect dependency.
149
162
 
150
- The source account must be able to retrieve each requested object's metadata.
151
- Normally this means owning the objects; cross-schema extraction requires
152
- appropriate DBMS_METADATA catalog privileges, such as SELECT_CATALOG_ROLE.
153
- Having SELECT on another schema's table alone is insufficient. Metadata errors
154
- abort generation instead of presenting an incomplete export as successful.
163
+ The host prefers ALL_* catalog views and supplements discovery with readable
164
+ DBA_* views, even when ALL_* returns only part of the schema. For example, a
165
+ sequence absent from ALL_SEQUENCES can be exported using DBA_SEQUENCES. Access
166
+ is checked per view using actual queries; the exporter neither enables roles
167
+ nor requests grants. Catalog visibility limits which dependencies can be found.
168
+
169
+ For objects owned by the session user, definitions still come from per-handle
170
+ DBMS_METADATA calls. Cross-schema exports first render ordinary permanent heap
171
+ tables, scalar columns/defaults, constraints, B-tree/function-based indexes,
172
+ global sequences, ordinary views, and readable PL/SQL source from catalog
173
+ metadata. Stored source and view text are read in full. Changing CURRENT_SCHEMA
174
+ does not change ownership or metadata privileges.
175
+
176
+ Catalog rendering supports identity, virtual, invisible, and collated table
177
+ columns. Identity generation modes and sequence options are preserved; restart
178
+ uses this session's readable CURRVAL plus the increment, or the backing
179
+ sequence's catalog LAST_NUMBER snapshot if CURRVAL is undefined or inaccessible.
180
+ Export never calls NEXTVAL and does not recreate the internal identity sequence
181
+ separately. These snapshots may leave cache gaps and do not track concurrent
182
+ sessions. If neither restart source is usable, export requires metadata instead
183
+ of resetting numbering to the identity's initial value.
184
+
185
+ Virtual expressions are read in full. Visible columns retain their current
186
+ SELECT * order, including columns made visible again; invisible columns follow
187
+ in internal-column order. System-generated hidden columns are recreated by their
188
+ constraints/indexes. Named table and column collations are preserved, including
189
+ the original columns behind internal collation keys. Named collations require
190
+ the destination database to support data-bound collation (MAX_STRING_SIZE=EXTENDED).
191
+
192
+ Advanced features use DBMS_METADATA instead, including partitioned/IOT/external/
193
+ object tables, encrypted columns, edition-specific virtual-column clauses, specialized indexes
194
+ (including descending indexes), constrained or specialized views, and session/
195
+ scalable/sharded sequences. This fallback requires the object owner or appropriate
196
+ DBMS_METADATA catalog privileges, such as SELECT_CATALOG_ROLE. Object grants or
197
+ readable DBA_* views do not by themselves guarantee DBMS_METADATA access.
198
+ If both paths fail for an included object, generation stops with its name and
199
+ the reasons, including the column and unsupported feature or missing column
200
+ metadata where applicable; no incomplete export buffer is opened.
155
201
 
156
202
  Review and save the script normally. Export does not execute SQL, create the
157
203
  destination schema, copy rows, grant privileges, commit, roll back, or drain
@@ -157,8 +157,9 @@ select count(*) as visible_objects
157
157
  from user_objects;
158
158
  ```
159
159
 
160
- `F11` runs the whole buffer (or the selection). Press `F6` to switch between
161
- normal results and captured `DBMS_OUTPUT`.
160
+ `F11` runs the whole buffer (or the selection). When running the whole buffer,
161
+ press Enter at the confirmation prompt to continue, or Esc/Ctrl+C to cancel.
162
+ Press `F6` to switch between normal results and captured `DBMS_OUTPUT`.
162
163
 
163
164
  ### Page through a larger result
164
165
 
@@ -1,3 +1,3 @@
1
1
  """ncurses SQL and PL/SQL workspace."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
@@ -12,6 +12,7 @@ import oracledb
12
12
 
13
13
  from .ddl_remap import ddl_tokens, function_references, remap_sql, sequence_references
14
14
  from .identifiers import normalize_identifier, quote_identifier
15
+ from .table_export_catalog import CatalogUnavailable, ExportCatalog, error_code, sequence_restart_value
15
16
 
16
17
  _METADATA_TYPES = {'PACKAGE': 'PACKAGE_SPEC', 'PACKAGE BODY': 'PACKAGE_BODY'}
17
18
  _SEQUENCE_START = re.compile(r'START\s+WITH\s+([+-]?\d+)', re.IGNORECASE)
@@ -125,6 +126,8 @@ class TableExportService:
125
126
  def __init__(self, connection: oracledb.Connection, cancelled: Callable[[], bool] = lambda: False):
126
127
  self.connection = connection
127
128
  self.cancelled = cancelled
129
+ self.catalog = ExportCatalog(lambda sql, **binds: self.rows(sql, **binds))
130
+ self._session_user: str | None = None
128
131
 
129
132
  def check_cancelled(self) -> None:
130
133
  if self.cancelled():
@@ -139,27 +142,35 @@ class TableExportService:
139
142
  def current_schema(self) -> str:
140
143
  return str(self.rows("select sys_context('USERENV', 'CURRENT_SCHEMA') from dual")[0][0])
141
144
 
145
+ def session_user(self) -> str:
146
+ if self._session_user is None:
147
+ self._session_user = str(self.rows("select sys_context('USERENV', 'SESSION_USER') from dual")[0][0])
148
+ return self._session_user
149
+
142
150
  def tables(self, schema: str) -> tuple[str, ...]:
143
- return tuple(row[0] for row in self.rows(
151
+ materialized = {row[0] for row in self.catalog.rows(
152
+ 'select mview_name from all_mviews where owner = :owner', owner=schema,
153
+ )}
154
+ return tuple(sorted(row[0] for row in self.catalog.rows(
144
155
  """select table_name from all_tables t where owner = :owner
145
156
  and nested = 'NO' and secondary = 'N'
146
157
  and (iot_type is null or iot_type = 'IOT')
147
158
  and table_name not like 'BIN$%'
148
- and not exists (select 1 from all_mviews m where m.owner = t.owner and m.mview_name = t.table_name)
149
159
  order by table_name""", owner=schema,
150
- ))
160
+ ) if row[0] not in materialized))
151
161
 
152
162
  def sequences(self, schema: str) -> tuple[str, ...]:
153
- return tuple(row[0] for row in self.rows(
163
+ identities = {row[0] for row in self.catalog.rows(
164
+ 'select sequence_name from all_tab_identity_cols where owner = :owner', owner=schema,
165
+ )}
166
+ return tuple(sorted(row[0] for row in self.catalog.rows(
154
167
  """select s.sequence_name from all_sequences s where s.sequence_owner = :owner
155
- and not exists (select 1 from all_tab_identity_cols i
156
- where i.owner = s.sequence_owner and i.sequence_name = s.sequence_name)
157
168
  order by s.sequence_name""", owner=schema,
158
- ))
169
+ ) if row[0] not in identities))
159
170
 
160
171
  def table_dependencies(self, obj: ExportObject, known_objects: set[ExportObject]) -> set[ExportObject]:
161
172
  dependencies = self.object_dependencies(obj)
162
- for (default,) in self.rows(
173
+ for (default,) in self.catalog.rows(
163
174
  "select data_default from all_tab_cols where owner = :owner and table_name = :name and default_length > 0",
164
175
  owner=obj.owner, name=obj.name,
165
176
  ):
@@ -173,7 +184,7 @@ class TableExportService:
173
184
  dependencies = self.object_dependencies(obj)
174
185
  # Both FOLLOWS and PRECEDES require the named trigger to exist first;
175
186
  # this is creation order, not the firing order of the resulting triggers.
176
- dependencies.update(ExportObject(owner, name, 'TRIGGER') for owner, name in self.rows(
187
+ dependencies.update(ExportObject(owner, name, 'TRIGGER') for owner, name in self.catalog.rows(
177
188
  '''select referenced_trigger_owner, referenced_trigger_name from all_trigger_ordering
178
189
  where trigger_owner = :owner and trigger_name = :name''', owner=obj.owner, name=obj.name,
179
190
  ))
@@ -183,7 +194,7 @@ class TableExportService:
183
194
  for sequence in sorted(candidates - dependencies):
184
195
  # A token may name a synonym or a PL/SQL record rather than a
185
196
  # sequence. Keep dictionary-resolved synonym dependencies intact.
186
- if self.rows(
197
+ if self.catalog.rows(
187
198
  'select sequence_name from all_sequences where sequence_owner = :owner and sequence_name = :name',
188
199
  owner=sequence.owner, name=sequence.name,
189
200
  ):
@@ -204,7 +215,7 @@ class TableExportService:
204
215
  visible = known_objects
205
216
  if candidate.owner != schema:
206
217
  visible = {ExportObject(candidate.owner, candidate.name, _METADATA_TYPES.get(kind, kind))
207
- for (kind,) in self.rows(
218
+ for (kind,) in self.catalog.rows(
208
219
  'select object_type from all_objects where owner = :owner and object_name = :name',
209
220
  owner=candidate.owner, name=candidate.name,
210
221
  )}
@@ -218,7 +229,7 @@ class TableExportService:
218
229
  return dependencies
219
230
 
220
231
  def table_users(self, schema: str, table: str) -> set[ExportObject]:
221
- return {ExportObject(schema, name, _METADATA_TYPES.get(kind, kind)) for name, kind in self.rows(
232
+ return {ExportObject(schema, name, _METADATA_TYPES.get(kind, kind)) for name, kind in self.catalog.rows(
222
233
  """select distinct name, type from all_dependencies
223
234
  where owner = :owner and referenced_owner = :owner and referenced_name = :name
224
235
  and referenced_type = 'TABLE' and referenced_link_name is null
@@ -228,7 +239,7 @@ class TableExportService:
228
239
 
229
240
  def object_dependencies(self, obj: ExportObject) -> set[ExportObject]:
230
241
  dictionary_kind = {'PACKAGE_SPEC': 'PACKAGE', 'PACKAGE_BODY': 'PACKAGE BODY'}.get(obj.kind, obj.kind)
231
- references = {ExportObject(owner, name, _METADATA_TYPES.get(kind, kind)) for owner, name, kind in self.rows(
242
+ references = {ExportObject(owner, name, _METADATA_TYPES.get(kind, kind)) for owner, name, kind in self.catalog.rows(
232
243
  """select distinct referenced_owner, referenced_name, referenced_type from all_dependencies
233
244
  where owner = :owner and name = :name and type = :kind
234
245
  and referenced_owner is not null and referenced_link_name is null""",
@@ -239,6 +250,19 @@ class TableExportService:
239
250
  return references
240
251
 
241
252
  def ddl(self, obj: ExportObject, source: str, target: str) -> str:
253
+ self.check_cancelled()
254
+ if obj.owner == self.session_user():
255
+ return self.metadata_ddl(obj, source, target)
256
+ try:
257
+ return self.catalog.ddl(obj.owner, obj.name, obj.kind)
258
+ except CatalogUnavailable as catalog_error:
259
+ self.check_cancelled()
260
+ try:
261
+ return self.metadata_ddl(obj, source, target)
262
+ except RuntimeError as metadata_error:
263
+ raise RuntimeError(f'{obj.label}: {catalog_error}; DBMS_METADATA: {metadata_error}') from metadata_error
264
+
265
+ def metadata_ddl(self, obj: ExportObject, source: str, target: str) -> str:
242
266
  self.check_cancelled()
243
267
  with self.connection.cursor() as cursor:
244
268
  result = cursor.var(oracledb.DB_TYPE_CLOB)
@@ -298,13 +322,21 @@ class TableExportService:
298
322
  try:
299
323
  current = int(self.rows(f"select to_char({qualified}.currval, 'TM9') from dual")[0][0])
300
324
  except oracledb.DatabaseError as exc:
301
- if getattr(exc.args[0], 'code', None) != 8002:
325
+ code = error_code(exc)
326
+ if code not in {8002, 1031, 2289}:
302
327
  raise RuntimeError(f'{obj.label}: cannot read CURRVAL: {exc}') from exc
303
- return f'-- START WITH {saved} from metadata; CURRVAL is undefined in this session.\n{ddl}'
304
- rows = self.rows(
305
- '''select increment_by, min_value, max_value, cycle_flag, scale_flag, sharded_flag
328
+ if code != 8002 and not self.catalog.rows(
329
+ 'select sequence_name from all_sequences where sequence_owner = :owner and sequence_name = :name',
330
+ owner=obj.owner, name=obj.name,
331
+ ):
332
+ raise RuntimeError(f'{obj.label}: sequence disappeared or metadata is no longer accessible') from exc
333
+ reason = 'undefined in this session' if code == 8002 else 'not accessible to this account'
334
+ return f'-- START WITH {saved} from metadata; CURRVAL is {reason}.\n{ddl}'
335
+ rows = self.catalog.rows(
336
+ '''select to_char(increment_by, 'TM9'), to_char(min_value, 'TM9'), to_char(max_value, 'TM9'),
337
+ cycle_flag, scale_flag, sharded_flag
306
338
  from all_sequences where sequence_owner = :owner and sequence_name = :name''',
307
- owner=obj.owner, name=obj.name,
339
+ key=(), owner=obj.owner, name=obj.name,
308
340
  )
309
341
  if not rows:
310
342
  raise RuntimeError(f'{obj.label}: sequence metadata is no longer accessible')
@@ -312,11 +344,10 @@ class TableExportService:
312
344
  if scale == 'Y' or sharded == 'Y':
313
345
  return f'-- START WITH {saved} from metadata; SCALE/SHARD CURRVAL offsets require manual review.\n{ddl}'
314
346
  increment, minimum, maximum = int(increment), int(minimum), int(maximum)
315
- restart = current + increment
316
- if not minimum <= restart <= maximum:
317
- if cycle != 'Y':
318
- raise RuntimeError(f'{obj.label}: CURRVAL {current} has exhausted the sequence bounds')
319
- restart = minimum if increment > 0 else maximum
347
+ try:
348
+ restart = sequence_restart_value(current, increment, minimum, maximum, cycle)
349
+ except CatalogUnavailable as exc:
350
+ raise RuntimeError(f'{obj.label}: {exc}') from exc
320
351
  ddl = ddl[:start.start(1)] + str(restart) + ddl[start.end(1):]
321
352
  return (f'-- START WITH {restart} from session CURRVAL {current} and INCREMENT BY {increment}'
322
353
  f' (metadata START WITH {saved}).\n{ddl}')
@@ -331,7 +362,7 @@ class TableExportService:
331
362
  available_tables = set(self.tables(source))
332
363
  if not set(tables) <= available_tables:
333
364
  raise ValueError('A selected table disappeared or is no longer accessible; restart the export')
334
- schema_objects = self.rows('select object_name, object_type from all_objects where owner = :owner', owner=source)
365
+ schema_objects = self.catalog.rows('select object_name, object_type from all_objects where owner = :owner', owner=source)
335
366
  known_names = {name for name, kind in schema_objects}
336
367
  known_objects = {ExportObject(source, name, _METADATA_TYPES.get(kind, kind)) for name, kind in schema_objects}
337
368
  package_bodies = {name for name, kind in schema_objects if kind == 'PACKAGE BODY'}
@@ -377,43 +408,51 @@ class TableExportService:
377
408
  table_obj = ExportObject(source, table, 'TABLE')
378
409
  progress(f'Reading dependencies: {table_obj.label}', completed_tables, len(included_tables))
379
410
  add_references(table_obj, self.table_dependencies(table_obj, known_objects))
380
- for owner, name in self.rows(
411
+ key_indexes = set(self.catalog.rows(
412
+ """select index_owner, index_name from all_constraints where owner = :owner
413
+ and table_name = :name and constraint_type in ('P','U') and index_name is not null""",
414
+ owner=source, name=table,
415
+ ))
416
+ for owner, name in self.catalog.rows(
381
417
  """select i.owner, i.index_name from all_indexes i
382
418
  where i.table_owner = :owner and i.table_name = :name
383
- and i.index_type not in ('LOB', 'IOT - TOP')
384
- and not exists (select 1 from all_constraints c where c.owner = :owner
385
- and c.table_name = :name and c.constraint_type in ('P','U')
386
- and c.index_owner = i.owner and c.index_name = i.index_name)""", owner=source, name=table,
419
+ and i.index_type not in ('LOB', 'IOT - TOP')""", owner=source, name=table,
387
420
  ):
421
+ if (owner, name) in key_indexes:
422
+ continue
388
423
  obj = ExportObject(owner, name, 'INDEX')
389
424
  refs = self.object_dependencies(obj) | {table_obj}
390
- for (expression,) in self.rows(
425
+ for (expression,) in self.catalog.rows(
391
426
  'select column_expression from all_ind_expressions where index_owner = :owner and index_name = :name',
392
427
  owner=owner, name=name,
393
428
  ):
394
429
  if expression:
395
430
  refs.update(self.expression_dependencies(expression, owner, known_objects))
396
431
  add_references(obj, refs)
397
- for owner, name, body in self.rows(
432
+ for owner, name, body in self.catalog.rows(
398
433
  'select owner, trigger_name, trigger_body from all_triggers where table_owner = :owner and table_name = :name',
399
- owner=source, name=table,
434
+ key=(0, 1), owner=source, name=table,
400
435
  ):
401
436
  obj = ExportObject(owner, name, 'TRIGGER')
402
437
  add_references(obj, self.trigger_dependencies(obj, body) | {table_obj})
403
- for owner, name, ref_owner, ref_table in self.rows(
404
- """select c.owner, c.constraint_name, c.r_owner, p.table_name
405
- from all_constraints c left join all_constraints p
406
- on p.owner = c.r_owner and p.constraint_name = c.r_constraint_name
438
+ for owner, name, ref_owner, ref_constraint in self.catalog.rows(
439
+ """select c.owner, c.constraint_name, c.r_owner, c.r_constraint_name
440
+ from all_constraints c
407
441
  where c.owner = :owner and c.table_name = :name and c.constraint_type = 'R'""",
408
- owner=source, name=table,
442
+ key=(0, 1), owner=source, name=table,
409
443
  ):
444
+ parent = self.catalog.rows(
445
+ 'select table_name from all_constraints where owner = :owner and constraint_name = :name',
446
+ key=(), owner=ref_owner, name=ref_constraint,
447
+ )
448
+ ref_table = parent[0][0] if parent else None
410
449
  add_references(ExportObject(owner, name, 'REF_CONSTRAINT'), {
411
450
  table_obj, ExportObject(ref_owner, ref_table or '<inaccessible referenced table>', 'TABLE'),
412
451
  })
413
452
  completed_tables += 1
414
453
  dependencies = {ref for refs in references.values() for ref in refs}
415
454
  # Identity backing sequences are part of CREATE TABLE, never separate exports.
416
- identities = {ExportObject(source, row[0], 'SEQUENCE') for table in sorted(included_tables) for row in self.rows(
455
+ identities = {ExportObject(source, row[0], 'SEQUENCE') for table in sorted(included_tables) for row in self.catalog.rows(
417
456
  'select sequence_name from all_tab_identity_cols where owner = :owner and table_name = :name',
418
457
  owner=source, name=table,
419
458
  )}
@@ -441,6 +480,7 @@ class TableExportService:
441
480
  self.check_cancelled()
442
481
  warnings = sorted(set(warnings))
443
482
  header = [
483
+ 'SET DEFINE OFF',
444
484
  '-- Table DDL Export: definitions only; review before execution.',
445
485
  f'-- Source: {quote_identifier(source)}; target: {quote_identifier(target)}',
446
486
  f'-- {len(included_tables)} table(s), {len(sequence_objects)} sequence(s). Destination schema must already exist.',
@@ -452,6 +492,7 @@ class TableExportService:
452
492
  ' directly referencing package(s)/function(s)/procedure(s).',
453
493
  f'-- {sum(obj.kind == "VIEW" for obj in table_users)} directly referencing view(s).',
454
494
  '-- Definitions follow creation dependencies; cycles and external prerequisites are listed below.',
495
+ '-- Discovery prefers ALL_* catalogs, supplemented by accessible DBA_* views; invisible dependencies may be absent.',
455
496
  '-- Tablespaces/storage omitted. Sequence starts prefer session CURRVAL + increment, else metadata.',
456
497
  '-- CURRVAL is session-local; other sessions may have consumed different values.',
457
498
  '-- Restart positions are export-time snapshots; cache gaps may occur. No source NEXTVAL was called.',