diff --git a/customization/settings.mdx b/customization/settings.mdx
index 129224a..de91320 100644
--- a/customization/settings.mdx
+++ b/customization/settings.mdx
@@ -20,7 +20,7 @@ The settings window is app-wide: a value on any of these twelve tabs holds for e
Which finished operations notify you when TablePro is in the background.
Providers, inline suggestions, context, slash commands.
MCP server, tokens, clients, activity.
- Manage drivers and exporters.
+ Modules, drivers and exporters.
iCloud sync and what it carries.
Activation, devices, team.
@@ -105,8 +105,9 @@ What a fresh install ships with, so one glance down this column says what you ch
| MCP | Allow apps to list connections hidden from AI | Off |
| Sync | Sync this Mac with iCloud | Off |
| Sync | Passwords (keychain sync) | Off |
+| Extensions | Routine Debugger | Off |
-Three tabs are not in the table. Stock keyboard chords are on [Keyboard Shortcuts](/features/keyboard-shortcuts), and neither the Plugins nor the License tab holds a value of its own.
+Keyboard and License are not in the table. Stock keyboard chords are on [Keyboard Shortcuts](/features/keyboard-shortcuts), and the License tab holds no value of its own.
## Where settings live
diff --git a/databases/cloudflare-r2-sql.mdx b/databases/cloudflare-r2-sql.mdx
index a9fc721..00407c1 100644
--- a/databases/cloudflare-r2-sql.mdx
+++ b/databases/cloudflare-r2-sql.mdx
@@ -101,7 +101,7 @@ The bucket has R2 Data Catalog turned off, or the catalog holds no tables yet. T
### `SHOW TABLES returned columns TablePro does not recognize: …`
-R2 SQL answered a catalog statement in a shape this version of the driver cannot read. Update the plugin from **Settings > Plugins**.
+R2 SQL answered a catalog statement in a shape this version of the driver cannot read. Update the plugin from **Settings > Extensions**.
### `unsupported feature: OFFSET clause is not supported`
diff --git a/databases/oracle.mdx b/databases/oracle.mdx
index 82279f0..d481994 100644
--- a/databases/oracle.mdx
+++ b/databases/oracle.mdx
@@ -98,6 +98,8 @@ A `CREATE` whose unit does not compile fails with Oracle's own errors, one per l
`DBMS_OUTPUT.PUT_LINE` lines show with the result of the statement that printed them. See [Server output](/features/query-results#server-output).
+The [Routine Debugger](/features/routine-debugger) steps through a procedure, a function or a package with breakpoints. It needs `DEBUG CONNECT SESSION`.
+
In a trigger body, `:NEW` and `:OLD` are left as written. In an anonymous block, `:name` is a [query parameter](/features/query-parameters) filled from the panel.
An anonymous block, or a query whose `WITH` clause declares a function, runs code on the server. One that drops or truncates, including through `EXECUTE IMMEDIATE '…'`, raises the [dangerous query warning](/features/safe-mode) before it runs, and a Read-Only connection refuses it like any write. [MCP clients](/developers/mcp-tools) and the AI assistant cannot send either.
diff --git a/databases/postgresql.mdx b/databases/postgresql.mdx
index 8d0aca0..b4ea914 100644
--- a/databases/postgresql.mdx
+++ b/databases/postgresql.mdx
@@ -179,7 +179,7 @@ Sessions run in `UTF8` whatever encoding the database was created with, and the
## Tools
-`EXPLAIN` and `EXPLAIN ANALYZE` run with `FORMAT JSON` and render as a plan diagram or tree. See [EXPLAIN Visualization](/features/explain-visualization). **Database > Users & Roles** shows where each privilege comes from before you grant or revoke it. **Backup Dump** and **Restore Dump** shell out to your local `pg_dump` and `pg_restore`; see [Backup & Restore](/features/backup-restore).
+`EXPLAIN` and `EXPLAIN ANALYZE` run with `FORMAT JSON` and render as a plan diagram or tree. See [EXPLAIN Visualization](/features/explain-visualization). **Database > Users & Roles** shows where each privilege comes from before you grant or revoke it. **Backup Dump** and **Restore Dump** shell out to your local `pg_dump` and `pg_restore`; see [Backup & Restore](/features/backup-restore). The [Routine Debugger](/features/routine-debugger) steps through PL/pgSQL functions and procedures once the server has the `pldbgapi` extension.
## SSL/TLS
diff --git a/developers/url-scheme.mdx b/developers/url-scheme.mdx
index a8b1f9e..72e89ea 100644
--- a/developers/url-scheme.mdx
+++ b/developers/url-scheme.mdx
@@ -181,7 +181,7 @@ pane. The link only navigates and never changes a setting.
| Notifications | `notifications` |
| AI | `ai` |
| MCP | `mcp` |
-| Plugins | `plugins` |
+| Extensions | `plugins` |
| Sync | `sync` |
| License | `license` |
diff --git a/development/plugin-development.mdx b/development/plugin-development.mdx
index d55b978..6258e15 100644
--- a/development/plugin-development.mdx
+++ b/development/plugin-development.mdx
@@ -15,7 +15,7 @@ A plugin is a macOS loadable bundle target with `WRAPPER_EXTENSION = tableplugin
|-----|------|----------|---------|
| `TableProPluginKitVersion` | integer | Yes | The PluginKit ABI the plugin was built against. Current value: 41 |
| `TableProProvidesDatabaseTypeIds` | array of strings | Recommended | Database type IDs the plugin serves, which is what makes lazy loading possible |
-| `CFBundleShortVersionString` | string | Yes | Plugin version shown in **Settings > Plugins** |
+| `CFBundleShortVersionString` | string | Yes | Plugin version shown in **Settings > Extensions** |
| `CFBundleVersion` | string | Yes | Build number. Of two copies of a plugin the higher build loads, and a registry update needs a higher build |
Leave `TableProProvidesDatabaseTypeIds` out and the plugin loads eagerly at startup, blocking launch, and `PluginManager` logs a warning naming the key. With it, the app registers your metadata from `Info.plist` and loads the binary on first use.
@@ -86,7 +86,7 @@ Adopt these on top of `PluginDatabaseDriver` to reach more of the UI. The app fi
| `PluginDefaultSortProvider` | Per-table control of default sorting: `.useAppDefault`, `.suppress`, or `.forceColumns([String])` |
| `PluginPrincipalManagement` | Users, roles, and grants in the Users & Roles tab |
| `PluginBrowseFilterProvider` | Control of the sidebar object filter: glob syntax, case sensitivity, type scopes |
-| `SettablePlugin` | A settings pane of your own under **Settings > Plugins** |
+| `SettablePlugin` | A settings pane of your own under **Settings > Extensions** |
## ABI compatibility
diff --git a/development/plugin-registry.mdx b/development/plugin-registry.mdx
index d053852..709ce3c 100644
--- a/development/plugin-registry.mdx
+++ b/development/plugin-registry.mdx
@@ -100,4 +100,4 @@ A plugin loads when its `TableProPluginKitVersion` falls inside the app's range.
## Caching
-GitHub Pages serves the manifest with `Cache-Control: max-age=600`. The app keeps the response in its HTTP cache on disk and decodes it at launch, so the catalog is there before any request. At launch and when **Settings > Plugins** opens, the cached response answers while it is fresh. Before an install reports a plugin missing, and when the app repairs an outdated plugin, it revalidates with the server. A new release reaches the app within about ten minutes.
+GitHub Pages serves the manifest with `Cache-Control: max-age=600`. The app keeps the response in its HTTP cache on disk and decodes it at launch, so the catalog is there before any request. At launch and when **Settings > Extensions** opens, the cached response answers while it is fresh. Before an install reports a plugin missing, and when the app repairs an outdated plugin, it revalidates with the server. A new release reaches the app within about ten minutes.
diff --git a/development/testing-plugins.mdx b/development/testing-plugins.mdx
index 06efffa..7d28a45 100644
--- a/development/testing-plugins.mdx
+++ b/development/testing-plugins.mdx
@@ -60,7 +60,7 @@ This is the route for a bundle built on another machine or signed by another tea
## When it does not load
-The reason lands in a **Plugin Installation Failed** alert when installing, and in the banner in **Settings > Plugins** otherwise.
+The reason lands in a **Plugin Installation Failed** alert when installing, and in the banner in **Settings > Extensions** otherwise.
| Message | What it means | What to do |
|---------|---------------|------------|
diff --git a/docs.json b/docs.json
index 405d3d1..a84ebad 100644
--- a/docs.json
+++ b/docs.json
@@ -311,6 +311,7 @@
"pages": [
"features/table-structure",
"features/routines-triggers",
+ "features/routine-debugger",
"features/user-defined-types",
"features/table-operations",
"features/er-diagram",
diff --git a/faq.mdx b/faq.mdx
index bdd6424..f1dfc42 100644
--- a/faq.mdx
+++ b/faq.mdx
@@ -70,7 +70,7 @@ Each one must match the SHA-256 checksum in the registry manifest and carry a va
-An app update can raise the plugin interface version, and a plugin built against the old one is rejected at load rather than crashing. TablePro repairs it at the next launch by reinstalling a compatible build from the registry. Until then the plugin sits in a banner in **Settings > Plugins** with the reason and an action.
+An app update can raise the plugin interface version, and a plugin built against the old one is rejected at load rather than crashing. TablePro repairs it at the next launch by reinstalling a compatible build from the registry. Until then the plugin sits in a banner in **Settings > Extensions** with the reason and an action.
diff --git a/features/keyboard-shortcuts.mdx b/features/keyboard-shortcuts.mdx
index 7db6ecb..32953fe 100644
--- a/features/keyboard-shortcuts.mdx
+++ b/features/keyboard-shortcuts.mdx
@@ -327,6 +327,23 @@ In [Open Quickly](/features/open-quickly), `Ctrl+J`/`Ctrl+N` and `Ctrl+K`/`Ctrl+
See [Data Files](/features/data-files).
+## Debug
+
+The **Debug** menu, and the **Debug** section of **Settings > Keyboard**, exist only while **Routine Debugger** is selected in **Settings > Extensions**. See [Routine Debugger](/features/routine-debugger).
+
+| Action | Shortcut |
+|--------|----------|
+| Debug Routine… | none |
+| Continue | `Ctrl+Cmd+Y` |
+| Step Over | `F6` |
+| Step Into | `F7` |
+| Step Out | `F8` |
+| Stop | none |
+| Toggle Breakpoint | none by default, rebindable |
+| Remove All Breakpoints | none |
+
+Continue and the three steps are rebindable too. Debug Routine…, Stop and Remove All Breakpoints have no row in **Settings > Keyboard**.
+
## Vim and Emacs key bindings
Choose Vim or Emacs under **Settings > Editor > Key bindings**. Either applies only in the SQL editor; the data grid keeps its shortcuts, and `Cmd+Enter` runs the query in any mode. The key references are [Vim Mode](/features/vim-mode) and [Emacs Mode](/features/emacs-mode).
diff --git a/features/plugins.mdx b/features/plugins.mdx
index 3d84bf2..3acc67f 100644
--- a/features/plugins.mdx
+++ b/features/plugins.mdx
@@ -62,7 +62,7 @@ A registry plugin installs where you need it:
- A `.duckdb` file opened from Finder, which asks for the DuckDB driver.
- **Install Parquet Export…** at the end of the format menu in the Export dialog. Parquet is selected once the download finishes.
-On a Mac with no network, install from a file: drag a `.tableplugin` or `.zip` onto **Settings > Plugins**, or click the **+** button there. A file for a plugin that is already loaded replaces it the next time TablePro starts. User-installed plugins live in `~/Library/Application Support/TablePro/Plugins`.
+On a Mac with no network, install from a file: drag a `.tableplugin` or `.zip` onto **Settings > Extensions**, or click the **+** button there. A file for a plugin that is already loaded replaces it the next time TablePro starts. User-installed plugins live in `~/Library/Application Support/TablePro/Plugins`.
Every plugin is verified before it loads. A registry download must match the SHA-256 checksum in the registry manifest, and the bundle must be signed by TablePro. A bundle signed by anyone else, ad-hoc signed or unsigned is refused, with no override.
@@ -71,13 +71,13 @@ Every plugin is verified before it loads. A registry download must match the SHA
Registry plugins follow the app. After a TablePro update, each plugin installed from the registry with an older build than the app is fetched again at launch:
- A plugin that is not loaded, such as one rejected after the update, installs and loads at once.
-- A plugin that is loaded downloads in the background and replaces the old one the next time TablePro starts. Its row in **Settings > Plugins** reads "Updates when TablePro restarts" until then.
+- A plugin that is loaded downloads in the background and replaces the old one the next time TablePro starts. Its row in **Settings > Extensions** reads "Updates when TablePro restarts" until then.
Offline, the check waits and runs once the Mac is back online. If the registry has no newer build yet, the check tries again in the background, on the schedule in [A plugin will not install or load](/troubleshooting#a-plugin-will-not-install-or-load). Connecting to a database whose plugin is outdated checks again first.
Bundled plugins update with the app. A plugin installed from a file, including one installed over a registry plugin, updates from the registry only when an app update leaves it rejected and the registry has a compatible build of it.
-An app update can raise the plugin interface version. A plugin built for an older one is rejected at load instead of crashing, and stays in a banner in **Settings > Plugins** with the reason and an action until it is repaired:
+An app update can raise the plugin interface version. A plugin built for an older one is rejected at load instead of crashing, and stays in a banner in **Settings > Extensions** with the reason and an action until it is repaired:
| Action | Meaning |
|--------|---------|
@@ -88,7 +88,7 @@ An app update can raise the plugin interface version. A plugin built for an olde
## Managing plugins
-In **Settings > Plugins**:
+In **Settings > Extensions**:
- **Uninstall** removes a user-installed plugin from disk. Bundled plugins cannot be uninstalled.
- The detail pane shows version, bundle ID, capabilities, database type, and default port. Plugins with their own settings show them here.
@@ -100,9 +100,13 @@ Saved connections survive a removal. A connection whose driver cannot load stays
| Cause | Button |
|-------|--------|
| The driver is not installed | **Install Plugin…** downloads it and connects |
-| The plugin failed to load or needs an update | **Open Plugin Settings** selects it in **Settings > Plugins**. Click **Try Again** once it is fixed |
+| The plugin failed to load or needs an update | **Open Plugin Settings** selects it in **Settings > Extensions**. Click **Try Again** once it is fixed |
| The connection names a database type no plugin provides, which a connection imported from another app can | **Edit Connection…** opens the form to choose the type |
+## Modules
+
+**Settings > Extensions** lists **Modules** above the plugins. A module comes with the app and stays out of the menus until you select its checkbox. The checkbox is per Mac and does not sync. **Routine Debugger** is the one module; see [Routine Debugger](/features/routine-debugger#turn-it-on).
+
## Themes
Themes are not plugins. Choose, create, import and export them in **Settings > Appearance**. See [Appearance](/customization/appearance).
diff --git a/features/routine-debugger.mdx b/features/routine-debugger.mdx
new file mode 100644
index 0000000..e0a0a53
--- /dev/null
+++ b/features/routine-debugger.mdx
@@ -0,0 +1,256 @@
+---
+title: Routine Debugger
+description: Step through PostgreSQL PL/pgSQL and Oracle PL/SQL routines with breakpoints, the call stack and variable values
+---
+
+A debug run executes the routine on the server, with the arguments you type, in two sessions of its own. It does not see uncommitted work from your query tabs, and what it changes is committed when it finishes.
+
+## Turn it on
+
+The debugger is a module, off by default. Select **Routine Debugger** under **Modules** in **Settings > Extensions**. That adds:
+
+- the **Debug** menu, between Query and Window
+- **Debug…** on the right-click menu of a function or procedure in the sidebar
+- a **Debug** section in **Settings > Keyboard**
+- **Continue** and **Stop** in the toolbar while a debug tab is in front. Step Over, Step Into and Step Out are in **View > Customize Toolbar…**
+
+Deselecting the checkbox removes all of them, closes every debug tab and stops its run.
+
+
+
+
+
+
+## Engine support
+
+| Engine | What runs under the debugger | Step Out | Server side |
+|---|---|---|---|
+| PostgreSQL | PL/pgSQL functions and procedures. Not trigger functions | No | The pldebugger package and the `pldbgapi` extension. See [PostgreSQL setup](#postgresql-setup) |
+| Oracle | PL/SQL procedures, functions and packages | Yes | Built in. See [Oracle setup](#oracle-setup) |
+
+Other engines have no debugger, Redshift, CockroachDB and PGlite included.
+
+## Start a run
+
+
+
+ Right-click a function or procedure in the sidebar and choose **Debug…**. To debug an Oracle package, or to pick from a list, choose **Debug > Debug Routine…**: it lists what can be debugged in the current schema, with a search field.
+
+
+ Each parameter has a row with its name and type. Type a value, or select **NULL**. **Default** is selected for every parameter that has a default; deselect it to pass a value instead. An output parameter takes no value. For a package, pick the procedure or function to start from in **Entry Point**.
+
+
+ A debug tab opens, and the routine pauses where it starts: on its first statement on PostgreSQL, on the entry point's header line on Oracle.
+
+
+
+
+
+
+
+
+On Oracle the start sheet also carries **Protocol**, **Debugger Host** and **Debugger Ports**; see [Protocol](#protocol). Their values are remembered per connection on this Mac.
+
+The run goes through [Safe Mode](/features/safe-mode) as a write: **Read-Only** refuses it, and the Alert and Safe Mode levels ask first.
+
+## The debug tab
+
+
+
+
+
+
+| Pane | Shows |
+|---|---|
+| Source | The routine's source, read-only. A green arrow and band mark the line about to run |
+| Call Stack | One row per active call, innermost first. Select a row to show that call's source and variables |
+| Variables | Name, value and type of each parameter and local variable of the selected call. NULL reads the way the data grid shows it |
+| Output | `RAISE NOTICE` lines on PostgreSQL, `DBMS_OUTPUT` lines on Oracle. When the routine finishes, its result follows as a grid: the rows a PostgreSQL function returns, or an Oracle function's return value and OUT parameters |
+
+On PostgreSQL the source is the routine's body without its `CREATE FUNCTION` header, so its line numbers are the ones the server's error messages use. On Oracle, package variables are listed with the rest, a record shows as `(A => 21, B => x)`, a collection as **Collection** or, under DBMS_DEBUG_JDWP, **Collection of 3 elements**, and a value the debugger cannot read as **Not readable**.
+
+The bar between the panes holds the run's buttons and its status: **Paused at line 12 in …**, **Running…**, **Finished**, **Stopped**, or the error that ended it. A long status is cut to one line; hold the pointer over it to read all of it.
+
+A debug tab stays in the tab strip whichever database or schema the sidebar shows. It is not restored when TablePro relaunches, and **Reopen Closed Tab** skips it.
+
+### Breakpoints
+
+Click the narrow column left of the line numbers to set a breakpoint, a red circle, and click it again to remove it. **Debug > Toggle Breakpoint** does the same on the line holding the cursor, and **Debug > Remove All Breakpoints** clears them.
+
+- A breakpoint set while the routine runs takes effect at the next pause.
+- PostgreSQL accepts a breakpoint on any line. One on a line with no statement never stops.
+- Oracle moves a breakpoint to the next line with code and says so in Output. With no code after it, the breakpoint is removed.
+
+### Step through
+
+| Command | What it does |
+|---|---|
+| **Continue** | Runs to the next breakpoint, or to the end |
+| **Step Over** | Runs the current line without stopping inside the routines it calls |
+| **Step Into** | Runs the current line and stops inside a PL/pgSQL or PL/SQL routine it calls |
+| **Step Out** | Runs until the current routine returns to its caller. Oracle only; it stays dimmed on PostgreSQL |
+| **Stop** | Ends the run and rolls it back. See [How a run ends](#how-a-run-ends) |
+
+Each is in the **Debug** menu and on the bar. Their default keys are under [Debug](/features/keyboard-shortcuts#debug).
+
+## How a run ends
+
+Finishing commits what the routine changed. A PostgreSQL procedure's own `COMMIT` statements commit as they run, and **Stop** rolls back only what came after the last one. An error in the routine rolls the run back and shows on the bar. **Stop** rolls it back.
+
+**Stop** waits at most 3 seconds for the server to end the routine. When it cannot confirm the end, the status says so in place of **Stopped**:
+
+- On PostgreSQL it reads "The routine couldn't be stopped on the server." and names the server process, with the SQL that ends it. See [Troubleshooting](#troubleshooting).
+- On Oracle, a routine that Stop cannot reach, such as one running between breakpoints under DBMS_DEBUG, is ended with `ALTER SYSTEM KILL SESSION` when your account has `ALTER SYSTEM`. Without it, the status and Output say the routine is still running and give its SID, SERIAL# and the `ALTER SYSTEM KILL SESSION` statement that ends it, naming the instance on RAC. Its uncommitted work rolls back when it ends, and a user with `ALTER SYSTEM` can kill it sooner.
+
+Closing the debug tab, closing its window, disconnecting, deselecting the module and quitting TablePro each stop the run first. Quitting waits up to 5 seconds for that. When the tab is gone and the stop could not be confirmed, an alert says so instead of the status.
+
+A debug tab stays in the window that started it: it cannot be dragged into a window of its own.
+
+A crash or a force quit cannot stop the run. A PostgreSQL run then holds its server session and its locks until the server restarts. Oracle ends the run and rolls it back on its own, within 30 seconds under DBMS_DEBUG.
+
+## PostgreSQL setup
+
+
+
+ Install pldebugger for the server's PostgreSQL major version. With the PGDG packages on Debian or Ubuntu, that is `postgresql-17-pldebugger` for PostgreSQL 17.
+
+
+ As a superuser, in every database whose routines you debug:
+
+ ```sql
+ CREATE EXTENSION pldbgapi;
+ ```
+
+
+
+To debug a routine, connect as its owner or as a superuser. `shared_preload_libraries` needs no change. Add `plugin_debugger` to it and restart the server only if a run fails with "The server couldn't start the debugger."
+
+Amazon RDS for PostgreSQL 16 to 19 does not list `pldbgapi` among its extensions, so routines there cannot be debugged.
+
+## Oracle setup
+
+### Privileges
+
+The account you connect with needs `DEBUG CONNECT SESSION`. To debug a routine another schema owns, it also needs `DEBUG` on that routine, or `DEBUG ANY PROCEDURE`. A DBA grants them:
+
+```sql
+GRANT DEBUG CONNECT SESSION TO app_user;
+GRANT DEBUG ON hr.payroll_pkg TO app_user;
+```
+
+### Compile for debug
+
+A unit compiled without debug information cannot stop on a breakpoint. Starting a run on one asks to compile it first: **Compile for Debug** recompiles it with `PLSQL_OPTIMIZE_LEVEL=1` and keeps its other settings. Compiling commits. For a package, other sessions using it lose its state, and their next call to it fails once with ORA-04068. Safe Mode treats the compile as a schema change.
+
+### Protocol
+
+| Protocol | Who connects to whom | Needs |
+|---|---|---|
+| DBMS_DEBUG_JDWP | The database server connects to this Mac | A `jdwp` access control entry a DBA grants, and a route from the server to the Mac |
+| DBMS_DEBUG | TablePro connects to the database | Nothing more. Works through SSH tunnels and proxies |
+
+Leave **Protocol** on **Automatic**, the default. It tries DBMS_DEBUG_JDWP and falls back to DBMS_DEBUG when the server cannot reach the Mac: the access control list refuses it, the connection fails or takes more than 15 seconds, no port in the range is free, or the database has no DBMS_DEBUG_JDWP. Output names the protocol in use and the reason for a fallback. Choose **DBMS_DEBUG** when you know the server cannot reach this Mac, which skips that attempt. **DBMS_DEBUG_JDWP** never falls back, so a failure stops the run with its reason.
+
+**Debugger Host** is the address the server uses to reach this Mac. On a direct connection it starts as the address this Mac uses to reach the database, which is `127.0.0.1` when the database runs on this Mac or in a local container. Through an SSH tunnel or a proxy it starts empty. With no host, **Automatic** uses DBMS_DEBUG and **DBMS_DEBUG_JDWP** refuses to start.
+
+**Debugger Ports** takes one port or a range, `4000-4999` by default. A list of ports is refused. TablePro listens on a free port in it and accepts the connection only from the database server's address. If the macOS firewall is on, allow TablePro to accept incoming connections.
+
+For DBMS_DEBUG_JDWP, a DBA allows the database user to connect to the Mac's address and port range:
+
+```sql
+BEGIN
+ DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
+ host => '192.168.1.20',
+ lower_port => 4000,
+ upper_port => 4999,
+ ace => xs$ace_type(
+ privilege_list => xs$name_list('jdwp'),
+ principal_name => 'APP_USER',
+ principal_type => xs_acl.ptype_db));
+END;
+/
+```
+
+## Limitations
+
+- PostgreSQL does not offer routines with polymorphic parameters, such as `anyelement`, or `record` parameters: they have no **Debug…** item and are left out of **Debug Routine…**. Debug a routine that calls one with concrete types, and step into it.
+- On PostgreSQL 17 and later, a server-wide `transaction_timeout` ends the run when one step or Continue takes longer than the timeout. The run rolls back with "The debugger's session was interrupted while it waited…", and the debug sessions' own setting cannot lift it. A long pause does not count. Turn the server's `transaction_timeout` off, or raise it above the slowest statement you step across.
+- PostgreSQL does not list record and row variables. `RAISE NOTICE` a field to read it in Output.
+- Under DBMS_DEBUG, a whole record variable is not listed. DBMS_DEBUG_JDWP lists it with its fields.
+
+## Troubleshooting
+
+### The PL/pgSQL debugger isn't installed in “…”.
+
+The server has the pldebugger package, but this database has no `pldbgapi` extension. Ask a superuser to run `CREATE EXTENSION pldbgapi` in it.
+
+### This server has no PL/pgSQL debugger.
+
+The pldebugger package is not installed on the server. Install it, then create the extension. See [PostgreSQL setup](#postgresql-setup).
+
+### Only the owner of … or a superuser can debug it.
+
+pldebugger debugs a routine only for its owner or a superuser. Connect as one of them.
+
+### The server couldn't start the debugger.
+
+The server ran out of the shared memory pldebugger uses. Add `plugin_debugger` to `shared_preload_libraries` in `postgresql.conf` and restart the server.
+
+### This routine sets client_min_messages to …, which hides the notice the debugger starts with.
+
+The routine's own `SET client_min_messages` is `warning` or stricter, so the debugger never hears it start. Remove that `SET` from the routine to debug it.
+
+### This routine sets transaction_timeout to …, which would end it while it waits at a breakpoint.
+
+The routine's own `SET transaction_timeout` would end it during a pause. Remove that `SET` from the routine to debug it.
+
+### The debugger's session was interrupted while it waited, so the routine was stopped and its uncommitted changes were rolled back.
+
+A cancel reached the debugger's session, or a step outlasted the server's `transaction_timeout`. See [Limitations](#limitations).
+
+### The routine couldn't be stopped on the server.
+
+Stop could not confirm within its time limit that the routine ended, and the server process may still hold its locks. Hold the pointer over the status for the details. When they give a port, run the two statements they name in the same database, passing the number the first one returns to the second:
+
+```sql
+SELECT pldbg_attach_to_port(12);
+SELECT pldbg_abort_target(1);
+```
+
+When they name only the server process, find it in `pg_stat_activity`.
+
+### Debugging needs the DEBUG CONNECT SESSION privilege.
+
+The Oracle account lacks the privilege. A DBA grants it with `GRANT DEBUG CONNECT SESSION TO …`.
+
+### Debugging … needs the DEBUG privilege on it.
+
+The routine belongs to another schema. A DBA grants `DEBUG` on it, or `DEBUG ANY PROCEDURE`.
+
+### The database isn't allowed to connect to TablePro at … (ORA-24247).
+
+The access control list has no `jdwp` entry for this Mac's address and port. Under **Automatic** the line appears in Output and the run continues through DBMS_DEBUG. To use DBMS_DEBUG_JDWP, a DBA adds the entry shown under [Protocol](#protocol).
+
+### The database couldn't reach TablePro at ….
+
+The server has no route to **Debugger Host**, or a firewall blocks the port. Under **Automatic** the run continues through DBMS_DEBUG. Set **Debugger Host** to an address the server can reach, or choose **DBMS_DEBUG**.
+
+### A connection came from …, which isn't the database server's address, so TablePro closed it.
+
+The server reached this Mac from another address than the one TablePro connects to, as through NAT or a load balancer. Under **Automatic** the run continues through DBMS_DEBUG. Otherwise choose **DBMS_DEBUG**.
+
+### DBMS_DEBUG_JDWP needs a Debugger Host for the database to connect back to.
+
+**Debugger Host** is empty, which it is by default through an SSH tunnel or a proxy. Set it to an address the server can reach this Mac at, or choose **DBMS_DEBUG**.
+
+### “…” isn't a port or a range of ports, such as 4000-4999.
+
+**Debugger Ports** holds something other than one port or one range, such as a list. Enter a single range.
+
+### TablePro can't turn text into a … value for ….
+
+The parameter's type has no text form TablePro can convert. Pass **NULL** or leave it at **Default**.
+
+### TablePro can't pass any value of type … to ….
+
+The parameter's type cannot be declared in the block that starts the run. Debug a routine that calls this one with that value, and step into it.
diff --git a/features/routines-triggers.mdx b/features/routines-triggers.mdx
index 6b1c86e..b16d716 100644
--- a/features/routines-triggers.mdx
+++ b/features/routines-triggers.mdx
@@ -43,7 +43,7 @@ PostgreSQL lets several functions share a name with different argument lists. Wh
## Reading the source
-Return or a double-click opens the selected row. Right-click offers **Show DDL** for the same thing, plus **Copy Name** and **Copy with Signature**.
+Return or a double-click opens the selected row. Right-click offers **Show DDL** for the same thing, plus **Copy Name** and **Copy with Signature**. With the [Routine Debugger](/features/routine-debugger) turned on, a PL/pgSQL or PL/SQL routine also offers **Debug…**.
| Action | What it does |
|---|---|
diff --git a/features/safe-mode.mdx b/features/safe-mode.mdx
index 37be761..d07fe4e 100644
--- a/features/safe-mode.mdx
+++ b/features/safe-mode.mdx
@@ -52,7 +52,7 @@ It does not sit in front of reading metadata. Loading the sidebar, opening a tab
A statement runs as a read only when TablePro can tell for certain that it reads. Anything it cannot tell counts as a write: confirmed at the Alert and Safe Mode levels, blocked at Read-Only.
-On MongoDB, a read is one call on a collection or database with literal arguments, such as `db.orders.find({status: "open"}).sort({at: -1}).limit(50)`, an `aggregate` pipeline without `$out` or `$merge`, `countDocuments`, `distinct`, `explain`, `show collections` and `use`. Any other JavaScript counts as a write: a variable, a function, `forEach`, `runCommand`. Run several reads at once and each one is checked. Shell state lasts as long as the connection, so a function you redefine in one statement runs redefined in every later one. A read still cannot write through it: the MongoDB driver refuses any write while it runs a statement as a read, once the plugin in **Settings > Plugins** is updated for this release.
+On MongoDB, a read is one call on a collection or database with literal arguments, such as `db.orders.find({status: "open"}).sort({at: -1}).limit(50)`, an `aggregate` pipeline without `$out` or `$merge`, `countDocuments`, `distinct`, `explain`, `show collections` and `use`. Any other JavaScript counts as a write: a variable, a function, `forEach`, `runCommand`. Run several reads at once and each one is checked. Shell state lasts as long as the connection, so a function you redefine in one statement runs redefined in every later one. A read still cannot write through it: the MongoDB driver refuses any write while it runs a statement as a read, once the plugin in **Settings > Extensions** is updated for this release.
Redis reads come from a fixed list of read commands. etcd reads are `get`, `watch`, and the commands that only list or report, such as `member list` and `endpoint health`. SAP HANA is read like any other SQL database.
diff --git a/images/routine-debugger-start-dark.png b/images/routine-debugger-start-dark.png
new file mode 100644
index 0000000..9ff5769
Binary files /dev/null and b/images/routine-debugger-start-dark.png differ
diff --git a/images/routine-debugger-start.png b/images/routine-debugger-start.png
new file mode 100644
index 0000000..f081466
Binary files /dev/null and b/images/routine-debugger-start.png differ
diff --git a/images/routine-debugger-tab-dark.png b/images/routine-debugger-tab-dark.png
new file mode 100644
index 0000000..6c5fa30
Binary files /dev/null and b/images/routine-debugger-tab-dark.png differ
diff --git a/images/routine-debugger-tab.png b/images/routine-debugger-tab.png
new file mode 100644
index 0000000..8365ccc
Binary files /dev/null and b/images/routine-debugger-tab.png differ
diff --git a/images/settings-extensions-dark.png b/images/settings-extensions-dark.png
new file mode 100644
index 0000000..b9fe0a5
Binary files /dev/null and b/images/settings-extensions-dark.png differ
diff --git a/images/settings-extensions.png b/images/settings-extensions.png
new file mode 100644
index 0000000..0373609
Binary files /dev/null and b/images/settings-extensions.png differ
diff --git a/troubleshooting.mdx b/troubleshooting.mdx
index b099ae6..93612f0 100644
--- a/troubleshooting.mdx
+++ b/troubleshooting.mdx
@@ -40,7 +40,7 @@ A handshake failure names the cause and the SSL Mode to switch to, with the driv
## A plugin will not install or load
-A plugin that fails any of its checks appears as a banner in **Settings > Plugins** with the reason.
+A plugin that fails any of its checks appears as a banner in **Settings > Extensions** with the reason.
### "Plugin code signature verification failed: …"
@@ -113,6 +113,6 @@ Neither copy button includes a password. **Copy Details** carries the host, the
**Help > Report an Issue** opens the [issue tracker](https://github.com/TableProApp/TablePro/issues). A report that gets fixed quickly carries:
- The version from **TablePro > About TablePro**, your macOS version, and whether the Mac is Apple Silicon or Intel
-- The engine and server version, plus the plugin version from **Settings > Plugins**
+- The engine and server version, plus the plugin version from **Settings > Extensions**
- The error pasted from **Copy Details** or **Copy Diagnostic Info**, not retyped
- What you ran, what happened, and what you expected. Add the log lines when the failure left no dialog