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 `<