Skip to content

fix(plugin-oracle): run PL/SQL blocks and units whole, with the terminator Oracle needs - #2988

Merged
datlechin merged 1 commit into
mainfrom
fix/oracle-plsql-units
Sep 18, 2026
Merged

datlechin merged 1 commit into
mainfrom
fix/oracle-plsql-units

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #2984

The bug

The editor split every Oracle script at every ;, so BEGIN DBMS_OUTPUT.PUT_LINE('x'); END; reached Oracle as two fragments and failed with ORA-06550 / PLS-00103. That was one symptom of a larger one:

  • A BEGIN only opened a block after CREATE, ALTER, REPLACE or DECLARE, and DECLARE itself opened nothing, so anonymous blocks, DECLARE blocks, procedures with a declaration section, package specifications (no BEGIN at all) and compound triggers were all cut at an inner ;.
  • Every statement had its final ; stripped. A PL/SQL unit needs it. Measured on Oracle 23ai through the app's own OracleCoreConnection: a block sent without it fails PLS-00103, and a CREATE PROCEDURE, PACKAGE or TRIGGER sent without it is stored INVALID while the statement reports success. oracle-nio drops the compile warning, so every PL/SQL unit created from the editor was broken with a green result.
  • The MCP / AppleScript / AI bridge stripped every trailing ;, SQL import had no block tracking at all, and folding disagreed with the run controls about where a unit ends.

The fix

One statement-boundary grammar that every reader feeds, instead of each reader guessing:

  • SQLStatementBoundaryTracking decides where a statement ends and whether its ; belongs to it. It is forward only, so the streaming SQL import parser can feed it across 64 KiB chunk boundaries.
  • PLSQLUnitTracker is Oracle's grammar, modelled on SQL*Plus's own rule for entering PL/SQL mode: anonymous blocks, stored units (PROCEDURE, FUNCTION, PACKAGE [BODY], TYPE [BODY], TRIGGER, LIBRARY), WITH FUNCTION queries, and CREATE JAVA / MLE MODULE / WRAPPED bodies that only a / line ends. It counts the constructs one END closes (a BEGIN continues a declaration section rather than nesting), treats END IF / END LOOP / END CASE, $IF…$END, labels, member names after . and call specs correctly, and keeps a unit's final ; except after a CALL-bodied trigger, which Oracle stores INVALID with one.
  • SQLRoutineBodyTracker is the existing rule for every other dialect, moved out of the scanner unchanged except for END CASE, which used to reopen a block and swallow everything after a MySQL procedure containing a CASE statement.
  • SqlDialect.oracle (PluginKit, additive, reuses the pending kit 33) carries Oracle's lexical rules: q'[…]' literals, no backslash escapes, / terminator lines.

Wired through the editor (run at cursor, selection, Run All, gutter, navigation), folding, SQL import, the external-client bridge and Compare & Sync (Oracle only).

Keeping a block whole must not weaken the execution gate, which tiers by leading keyword:

  • An Oracle anonymous block, and a query whose WITH clause declares a function or procedure, is classified as server-side code execution (parity with PostgreSQL DO), so MCP and the AI assistant refuse it and a Read-Only connection treats it as a write. Its tier is the worst of DROP/TRUNCATE in its body and the literals it passes to EXECUTE IMMEDIATE, DBMS_SQL.PARSE and EXEC_DDL_STATEMENT, read by Oracle's lexing rules (no backslash escapes, q'[…]'), and a DELETE without WHERE inside it raises the dangerous-query warning (a collection's v.DELETE does not).
  • A DECLARE block refreshes the sidebar like a BEGIN block; Oracle BEGIN is never read as a transaction.
  • A definition takes no bind parameters, so :NEW in a trigger body no longer opens the parameter panel or gets rewritten into a placeholder.

The Oracle plugin now reads ALL_ERRORS after a CREATE of a PL/SQL unit and fails with each line:column PLS-… instead of reporting success, and reports 0 rows affected for a block instead of oracle-nio's 1.

Evidence

Every corpus script, split by the real scanner sources from main and from this branch, each statement sent to Oracle 23ai Free through OracleCoreConnection:

TOTAL failed statements: before 82, after 1

(The one remaining failure is an INSERT that fired a trigger my earlier probing had left INVALID on that table.)

scripts/check-oracle-plsql-terminators.sh re-measures the keep/strip rule against a live server; on 23ai every shape agrees:

ok   procedure without ';': broken          ok   procedure with ';': works
ok   package body without ';': broken       ok   package body with ';': works
ok   CALL-bodied trigger without ';': works ok   CALL-bodied trigger with ';': broken
ok   anonymous block without ';': broken    ok   anonymous block with ';': works
ok   a '/' line sent to the server is refused
The terminator rules match this server.

Before / After

Running the block from the issue with Cmd+Enter:

What reaches Oracle Result
Before BEGIN\n DBMS_OUTPUT.PUT_LINE('Hello from PL/SQL') ORA-06550 … PLS-00103: Encountered the symbol "end-of-file"
After BEGIN\n DBMS_OUTPUT.PUT_LINE('Hello from PL/SQL');\nEND; Query executed successfully

No screenshots: the only new text on screen is the compilation-error message, shown in the existing error banner, and the change a reviewer needs to see is the statement text, which the measured run above covers statement by statement.

Review

Codex was at its usage limit, so the independent passes were the code-review and security-review skills.

  • code-review found four defects, all fixed with regression tests: the block classifier stripped literals with the generic lexer, so 'C:\temp\' or q'[it's]' could hide a DELETE; v.DELETE on a collection raised the dangerous-query warning; the caret on a / line ran nothing; and the import parser dropped a file's last character when it waited on lookahead at end of file. That last one predates this change for quotes and dashes, and the q-quote check widened it, so the end of input is now processed rather than left in the buffer.
  • security-review found one high-severity regression before it shipped: keeping WITH FUNCTION … END; SELECT … whole let the classifier tier it a safe read, so a read-only MCP client could run EXECUTE IMMEDIATE 'DROP …' inside it. It is now classified like an anonymous block, pinned by QueryClassifierPLSQLTests.

Tests

  • PLSQLScriptCorpus: 21 Oracle scripts with the exact text each statement is sent as, every one measured VALID or run on 23ai. Pinned by the scanner (SQLStatementPLSQLSplittingTests), SQL import at every possible chunk-boundary position (SQLFileParserPLSQLTests) and fold/run-control agreement (SQLFoldScannerTests).
  • QueryClassifierPLSQLTests: tiers, dynamic SQL, external gate refusal, catalog refresh, batch policy, bridge text.
  • OraclePLSQLUnitTests (package): header parsing, ALL_ERRORS query escaping, message format.
  • END CASE regression, q-quote coverage in SqlLexer, SQLNonCodeSpan, SQLTokenCursor, Oracle Compare & Sync terminators.

No UI automation: CI has no Oracle server, so a run of a PL/SQL block cannot be driven deterministically there. The live measurements above stand in for it.

Not in this PR

Reported separately rather than changed here: Dameng still maps to the generic dialect (no DM8 to measure against, and its backslash escaping is configurable); a column named begin merges CREATE TABLE …; DROP … into one statement on PostgreSQL/MySQL/SQLite; SQL import for other dialects still has no routine-body tracking; the iOS Oracle editor sends raw text; SQL*Plus client commands (SET SERVEROUTPUT, EXEC, SHOW ERRORS) are not interpreted. DBMS_OUTPUT display follows in a stacked PR.

@mintlify

mintlify Bot commented Sep 18, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 18, 2026, 5:30 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Oracle - PL/SQL blocks fail with ORA-06550 / PLS-00103 because editor splits on ;

1 participant