From ef6b037e4bc8af59ed62ef749e63ffaebd9657dc Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 19 Sep 2026 00:27:58 +0700 Subject: [PATCH] fix(plugin-oracle): run PL/SQL blocks and units whole, with the terminator Oracle needs --- CHANGELOG.md | 6 + .../TableProOracleCore/OraclePLSQLUnit.swift | 212 ++++++++++ .../OraclePLSQLUnitTests.swift | 82 ++++ Plugins/OracleDriverPlugin/OraclePlugin.swift | 20 +- Plugins/TableProPluginKit/SqlDialect.swift | 15 + .../Core/Compare/CompareSyncExecutor.swift | 12 +- ...QueryExecutionCoordinator+Parameters.swift | 4 +- .../QueryExecutionCoordinator.swift | 2 +- .../Access/DatabaseAccessBridge.swift | 25 +- .../Core/MCP/MCPConnectionBridge+Data.swift | 2 +- TablePro/Core/MCP/MCPConnectionBridge.swift | 4 +- .../Execution/BatchTransactionPolicy.swift | 2 + .../SQL/CatalogChangeClassifier.swift | 3 + .../SQL/Folding/SQLFoldScanner.swift | 97 ++++- .../Core/Utilities/SQL/PLSQLUnitTracker.swift | 380 ++++++++++++++++++ .../Utilities/SQL/QueryClassifier+PLSQL.swift | 165 ++++++++ .../Core/Utilities/SQL/QueryClassifier.swift | 6 + .../Core/Utilities/SQL/SQLFileParser.swift | 259 +++++++++++- .../Core/Utilities/SQL/SQLNonCodeSpan.swift | 12 + .../Utilities/SQL/SQLParameterExtractor.swift | 8 + .../Utilities/SQL/SQLRoutineBodyTracker.swift | 112 ++++++ .../SQL/SQLStatementBoundaryTracking.swift | 60 +++ .../Utilities/SQL/SQLStatementScanner.swift | 373 ++++++++++------- .../Core/Utilities/SQL/SQLTokenCursor.swift | 5 + .../Utilities/SQL/SqlBlockStructure.swift | 91 ++++- TablePro/Core/Utilities/SQL/SqlLexer.swift | 56 ++- .../Views/Main/MainContentCoordinator.swift | 2 +- .../Compare/CompareSyncExecutorTests.swift | 32 +- .../DatabaseAccessBridgeStatementTests.swift | 6 +- .../Utilities/SQL/PLSQLScriptCorpus.swift | 377 +++++++++++++++++ .../SQL/QueryClassifierPLSQLTests.swift | 194 +++++++++ .../SQL/SQLFileParserPLSQLTests.swift | 94 +++++ .../Utilities/SQL/SQLFileParserTests.swift | 3 +- .../Utilities/SQL/SQLFoldScannerTests.swift | 30 ++ .../Utilities/SQL/SQLNonCodeSpanTests.swift | 8 + .../SQL/SQLStatementBlockSplittingTests.swift | 17 + .../SQL/SQLStatementPLSQLSplittingTests.swift | 108 +++++ .../Utilities/SQL/SQLTokenCursorTests.swift | 6 + .../Core/Utilities/SQL/SqlLexerTests.swift | 16 + docs/databases/oracle.mdx | 30 +- docs/features/sql-editor.mdx | 2 +- scripts/check-oracle-plsql-terminators.sh | 171 ++++++++ 42 files changed, 2910 insertions(+), 199 deletions(-) create mode 100644 Packages/TableProOracle/Sources/TableProOracleCore/OraclePLSQLUnit.swift create mode 100644 Packages/TableProOracle/Tests/TableProOracleCoreTests/OraclePLSQLUnitTests.swift create mode 100644 TablePro/Core/Utilities/SQL/PLSQLUnitTracker.swift create mode 100644 TablePro/Core/Utilities/SQL/QueryClassifier+PLSQL.swift create mode 100644 TablePro/Core/Utilities/SQL/SQLRoutineBodyTracker.swift create mode 100644 TablePro/Core/Utilities/SQL/SQLStatementBoundaryTracking.swift create mode 100644 TableProTests/Core/Utilities/SQL/PLSQLScriptCorpus.swift create mode 100644 TableProTests/Core/Utilities/SQL/QueryClassifierPLSQLTests.swift create mode 100644 TableProTests/Core/Utilities/SQL/SQLFileParserPLSQLTests.swift create mode 100644 TableProTests/Core/Utilities/SQL/SQLStatementPLSQLSplittingTests.swift create mode 100755 scripts/check-oracle-plsql-terminators.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index c71f41a983..15389e0d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Oracle PL/SQL blocks split at their inner semicolons and sent as fragments, failing with PLS-00103. (#2984) +- Oracle procedures, packages and triggers created from the editor stored INVALID while the run reported success. +- SQL*Plus `/` lines, `q'[…]'` literals and backslashes in strings misread in Oracle scripts. +- `:NEW` and `:OLD` in an Oracle trigger body opening the parameter panel. +- 1 row affected reported for every Oracle PL/SQL block. +- MySQL procedures with a `CASE` statement swallowing the statements after them in the editor. - Icon-only buttons announced as nothing by VoiceOver across the data grid, row inspector, editor find bar, filter bar, structure, dashboard and settings. - Status icons that carried a result only as a symbol and a colour, silent to VoiceOver, in the AWS and app import steps and the plugin lists. - Foreign key picker rows that could only be chosen with a mouse. diff --git a/Packages/TableProOracle/Sources/TableProOracleCore/OraclePLSQLUnit.swift b/Packages/TableProOracle/Sources/TableProOracleCore/OraclePLSQLUnit.swift new file mode 100644 index 0000000000..e8bfd0f7ca --- /dev/null +++ b/Packages/TableProOracle/Sources/TableProOracleCore/OraclePLSQLUnit.swift @@ -0,0 +1,212 @@ +import Foundation + +/// A stored PL/SQL unit a `CREATE` statement defines, read from the statement's header. +/// +/// Oracle answers a `CREATE PROCEDURE` whose body does not compile with success: the object is stored INVALID and the +/// failure travels only as a warning flag, which the driver drops. The errors are in `ALL_ERRORS`, keyed by owner, +/// name and type, so reporting them takes knowing which unit the statement defined. +public struct OraclePLSQLUnit: Sendable, Equatable { + /// The type as `ALL_ERRORS.TYPE` spells it, such as `PACKAGE BODY`. + public let type: String + + /// The schema the header names, or nil when the unit is created in the session's current schema. + public let owner: String? + public let name: String + + public init(type: String, owner: String?, name: String) { + self.type = type + self.owner = owner + self.name = name + } + + private static let modifiers: Set = ["OR", "REPLACE", "EDITIONABLE", "NONEDITIONABLE"] + private static let unitTypes: Set = ["PROCEDURE", "FUNCTION", "PACKAGE", "TRIGGER", "TYPE"] + + /// The unit `sql` creates, or nil when it creates something else or nothing at all. + public static func definition(in sql: String) -> OraclePLSQLUnit? { + var reader = HeaderReader(sql) + guard reader.nextWord() == "CREATE" else { return nil } + var word = reader.nextWord() + while let modifier = word, modifiers.contains(modifier) { + word = reader.nextWord() + } + guard let kind = word, unitTypes.contains(kind) else { return nil } + var type = kind + if kind == "PACKAGE" || kind == "TYPE", reader.peekWord() == "BODY" { + _ = reader.nextWord() + type = "\(kind) BODY" + } + if reader.peekWord() == "IF" { + _ = reader.nextWord() + guard reader.nextWord() == "NOT", reader.nextWord() == "EXISTS" else { return nil } + } + guard let first = reader.nextIdentifier() else { return nil } + guard reader.consumePeriod() else { + return OraclePLSQLUnit(type: type, owner: nil, name: first) + } + guard let second = reader.nextIdentifier() else { return nil } + return OraclePLSQLUnit(type: type, owner: first, name: second) + } + + /// Whether `sql` is an anonymous block, which opens with `DECLARE` or `BEGIN` after any `<