From 19967dcecf82eb89ee3f3942befdbf7fc837bdaa Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 06:34:39 +0000 Subject: [PATCH 1/3] fix(oracle): require ';' after PL/SQL statements, declarations, and END (BYT-10301) omni accepted PL/SQL that Oracle rejects with PLS-00103 because three parsers recovered silently instead of failing: - the statement loop resumed after a statement that stopped early, so the words left over parsed as procedure calls (BEGIN NULL END;); - the declaration loops and the package item loop skipped what they did not recognize, and stray ';' between items; - END of a block, package, type body, and compound trigger took its ';' only when present. Each statement, declaration, package item, and END now requires its ';', and a stray ';' is an error, as on Oracle 23ai. Missing ';' is reported at the token Oracle reports: a PL/SQL reserved word after END or END LOOP is no longer taken as a label (END LOOP END;), using the reserved set measured on the engine. The leniency hid constructs omni did not parse, now implemented: - COMMIT, ROLLBACK, SAVEPOINT, SET TRANSACTION, and LOCK TABLE inside a PL/SQL body, and the INLINE, COVERAGE, DEPRECATE, and SUPPRESSES_WARNING_6009 pragmas as statements (other pragmas fail there, as in Oracle); - SUBTYPE name IS base_type [RANGE low .. high] [NOT NULL] (PLSQLSubtypeDecl); - NOT NULL on the element type of TABLE OF and VARRAY OF; - expressions as a length, precision, or scale in PL/SQL declarations (VARCHAR2(ORA_MAX_NAME_LEN + 2), varchar2(pkg.c)); SQL keeps integer literals. Reference rows ref_105 to ref_128 record the engine's answers; main disagreed with Oracle on 14 of them. In the dictionary manifest, DBMS_HADOOP's body now parses; four JSON units fail at the unsupported RETURNING clause instead of after it; STANDARD stops at its enumeration type, Oracle-internal syntax that Oracle rejects elsewhere (PLS-00505). Co-Authored-By: Claude Opus 5.5 --- oracle/ast/loc.go | 2 + oracle/ast/outfuncs.go | 27 +++ oracle/ast/parsenodes.go | 34 ++- oracle/ast/walk_generated.go | 6 + oracle/parser/create_proc.go | 27 +-- oracle/parser/create_trigger.go | 9 +- oracle/parser/create_type.go | 7 +- oracle/parser/create_type_test.go | 2 +- oracle/parser/keywords.go | 43 ++++ oracle/parser/parser.go | 15 +- oracle/parser/plsql_block.go | 155 ++++++++++++- oracle/parser/plsql_dictionary_oracle_test.go | 2 +- oracle/parser/plsql_terminator_test.go | 203 ++++++++++++++++++ .../testdata/coverage/loc_node_coverage.tsv | 1 + .../plsql_dictionary_known_failures.tsv | 19 +- .../testdata/coverage/reference_oracle.tsv | 24 +++ oracle/parser/transaction.go | 14 ++ oracle/parser/type.go | 73 +++++++ 18 files changed, 600 insertions(+), 63 deletions(-) create mode 100644 oracle/parser/plsql_terminator_test.go diff --git a/oracle/ast/loc.go b/oracle/ast/loc.go index 395b2617..91d4c0a2 100644 --- a/oracle/ast/loc.go +++ b/oracle/ast/loc.go @@ -485,6 +485,8 @@ func NodeLoc(n Node) Loc { return v.Loc case *PLSQLTypeDecl: return v.Loc + case *PLSQLSubtypeDecl: + return v.Loc case *PLSQLAssign: return v.Loc case *PLSQLIf: diff --git a/oracle/ast/outfuncs.go b/oracle/ast/outfuncs.go index 8fb9b07b..4ace243e 100644 --- a/oracle/ast/outfuncs.go +++ b/oracle/ast/outfuncs.go @@ -589,6 +589,8 @@ func writeNode(sb *strings.Builder, node Node) { writePLSQLWhen(sb, n) case *PLSQLTypeDecl: writePLSQLTypeDecl(sb, n) + case *PLSQLSubtypeDecl: + writePLSQLSubtypeDecl(sb, n) case *PLSQLCall: writePLSQLCall(sb, n) @@ -5344,6 +5346,9 @@ func writePLSQLTypeDecl(sb *strings.Builder, n *PLSQLTypeDecl) { sb.WriteString(" :elementType ") writeNode(sb, n.ElementType) } + if n.ElementNotNull { + sb.WriteString(" :elementNotNull true") + } if n.IndexBy != nil { sb.WriteString(" :indexBy ") writeNode(sb, n.IndexBy) @@ -5364,6 +5369,28 @@ func writePLSQLTypeDecl(sb *strings.Builder, n *PLSQLTypeDecl) { sb.WriteString("}") } +func writePLSQLSubtypeDecl(sb *strings.Builder, n *PLSQLSubtypeDecl) { + sb.WriteString("{PLSQLSUBTYPEDECL") + sb.WriteString(fmt.Sprintf(" :name %q", n.Name)) + if n.BaseType != nil { + sb.WriteString(" :baseType ") + writeNode(sb, n.BaseType) + } + if n.RangeLow != nil { + sb.WriteString(" :rangeLow ") + writeNode(sb, n.RangeLow) + } + if n.RangeHigh != nil { + sb.WriteString(" :rangeHigh ") + writeNode(sb, n.RangeHigh) + } + if n.NotNull { + sb.WriteString(" :notNull true") + } + sb.WriteString(fmt.Sprintf(" :loc_start %d :loc_end %d", n.Loc.Start, n.Loc.End)) + sb.WriteString("}") +} + func writePLSQLCall(sb *strings.Builder, n *PLSQLCall) { sb.WriteString("{PLSQLCALL") if n.Name != nil { diff --git a/oracle/ast/parsenodes.go b/oracle/ast/parsenodes.go index da0878b5..167ae710 100644 --- a/oracle/ast/parsenodes.go +++ b/oracle/ast/parsenodes.go @@ -2561,7 +2561,8 @@ type PLSQLPragma struct { Loc Loc } -func (n *PLSQLPragma) nodeTag() {} +func (n *PLSQLPragma) nodeTag() {} +func (n *PLSQLPragma) stmtNode() {} // INLINE and COVERAGE also stand as statements // PLSQLCase represents a PL/SQL CASE statement (distinct from CASE expression). type PLSQLCase struct { @@ -2590,18 +2591,33 @@ func (n *PLSQLWhen) nodeTag() {} // TYPE name IS RECORD (field type [,...]) // TYPE name IS REF CURSOR [RETURN type] type PLSQLTypeDecl struct { - Name string // type name - Kind string // TABLE, VARRAY, RECORD, REF_CURSOR - ElementType *TypeName // element type (TABLE OF/VARRAY OF) - IndexBy *TypeName // INDEX BY type (associative arrays) - Limit ExprNode // VARRAY limit - Fields *List // RECORD fields - ReturnType *TypeName // REF CURSOR RETURN type - Loc Loc + Name string // type name + Kind string // TABLE, VARRAY, RECORD, REF_CURSOR + ElementType *TypeName // element type (TABLE OF/VARRAY OF) + ElementNotNull bool // TABLE OF/VARRAY OF type NOT NULL + IndexBy *TypeName // INDEX BY type (associative arrays) + Limit ExprNode // VARRAY limit + Fields *List // RECORD fields + ReturnType *TypeName // REF CURSOR RETURN type + Loc Loc } func (n *PLSQLTypeDecl) nodeTag() {} +// PLSQLSubtypeDecl represents a PL/SQL SUBTYPE declaration. +// +// SUBTYPE name IS base_type [RANGE low .. high] [NOT NULL] +type PLSQLSubtypeDecl struct { + Name string // subtype name + BaseType *TypeName // base type, with its precision, scale, or size + RangeLow ExprNode // RANGE lower bound, nil without RANGE + RangeHigh ExprNode // RANGE upper bound, nil without RANGE + NotNull bool // NOT NULL + Loc Loc +} + +func (n *PLSQLSubtypeDecl) nodeTag() {} + // PLSQLCall represents a PL/SQL standalone procedure call statement. type PLSQLCall struct { Name ExprNode // procedure/function reference (could be dotted name) diff --git a/oracle/ast/walk_generated.go b/oracle/ast/walk_generated.go index 82df5aca..459d5b08 100644 --- a/oracle/ast/walk_generated.go +++ b/oracle/ast/walk_generated.go @@ -1201,6 +1201,12 @@ func walkChildren(v Visitor, node Node) { walkList(v, n.Args) case *PLSQLReturn: Walk(v, n.Expr) + case *PLSQLSubtypeDecl: + if n.BaseType != nil { + Walk(v, n.BaseType) + } + Walk(v, n.RangeLow) + Walk(v, n.RangeHigh) case *PLSQLTypeDecl: if n.ElementType != nil { Walk(v, n.ElementType) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index e25cc789..de39ed63 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -462,12 +462,13 @@ func (p *Parser) parseCreatePackageStmt(start int, orReplace, ifNotExists, editi } p.advance() // consume END // Optional package name after END - if p.isIdentLike() && p.cur.Type != ';' && p.cur.Type != tokEOF { + if p.atEndName() { p.advance() // consume name } - if p.cur.Type == ';' { - p.advance() // consume ; + if p.cur.Type != ';' { + return nil, p.syntaxErrorAtCur() } + p.advance() // consume ; stmt.Loc.End = p.prev.End return stmt, nil @@ -479,12 +480,6 @@ func (p *Parser) parsePackageBody() (*nodes.List, error) { decls := &nodes.List{} for p.cur.Type != kwEND && p.cur.Type != tokEOF { - // Skip standalone semicolons - if p.cur.Type == ';' { - p.advance() - continue - } - // PROCEDURE declaration/definition in package if p.cur.Type == kwPROCEDURE { decl, parseErr470 := p.parsePackageProcDecl() @@ -514,18 +509,14 @@ func (p *Parser) parsePackageBody() (*nodes.List, error) { break } - // Variable/type/cursor declarations + // Variable/type/cursor declarations. Each ends with ';', and a + // stray ';' between items is PLS-00103 on Oracle 23ai. decl, parseErr472 := p.parsePLSQLDeclaration() if parseErr472 != nil { - return nil, - - // If we can't parse anything, skip a token to avoid infinite loop - parseErr472 + return nil, parseErr472 } - if decl == nil { - - p.advance() - continue + if decl == nil || p.prev.Type != ';' { + return nil, p.syntaxErrorAtCur() } decls.Items = append(decls.Items, decl) } diff --git a/oracle/parser/create_trigger.go b/oracle/parser/create_trigger.go index c9a2546b..232b4fe1 100644 --- a/oracle/parser/create_trigger.go +++ b/oracle/parser/create_trigger.go @@ -164,11 +164,16 @@ func (p *Parser) parseCreateTriggerStmt(start int, orReplace, ifNotExists, editi if depth > 0 { depth-- } else { - // Outer END — consume END, optional name, done. + // Outer END — consume END, optional name, and the ';' + // that ends the trigger (PLS-00103 without it). p.advance() // consume END - if p.isIdentLike() { + if p.atEndName() { p.advance() // consume trigger name } + if p.cur.Type != ';' { + return nil, p.syntaxErrorAtCur() + } + p.advance() // consume ; break } } diff --git a/oracle/parser/create_type.go b/oracle/parser/create_type.go index 7640ec5d..c2fa3942 100644 --- a/oracle/parser/create_type.go +++ b/oracle/parser/create_type.go @@ -153,12 +153,13 @@ func (p *Parser) parseCreateTypeStmt(start int, orReplace, ifNotExists, editiona } p.advance() // Optional type name after END - if p.isIdentLike() && p.cur.Type != ';' && p.cur.Type != tokEOF { + if p.atEndName() { p.advance() } - if p.cur.Type == ';' { - p.advance() + if p.cur.Type != ';' { + return nil, p.syntaxErrorAtCur() } + p.advance() } } diff --git a/oracle/parser/create_type_test.go b/oracle/parser/create_type_test.go index 33d0b3dc..111e89e5 100644 --- a/oracle/parser/create_type_test.go +++ b/oracle/parser/create_type_test.go @@ -80,7 +80,7 @@ func TestParseCreateTypeVarray(t *testing.T) { func TestParseCreateTypeBody(t *testing.T) { // A type body holds subprogram definitions and ends with END; Oracle // rejects a body without it (PLS-00103), and so does omni. - p := newTestParser("TYPE BODY my_type IS MEMBER FUNCTION f RETURN NUMBER IS BEGIN RETURN 1; END; END my_type") + p := newTestParser("TYPE BODY my_type IS MEMBER FUNCTION f RETURN NUMBER IS BEGIN RETURN 1; END; END my_type;") stmt, parseErr5 := p.parseCreateTypeStmt(0, false, false, false, false) if parseErr5 != nil { t.Fatalf("parse: %v", parseErr5) diff --git a/oracle/parser/keywords.go b/oracle/parser/keywords.go index 1ab4bbfe..91f7354c 100644 --- a/oracle/parser/keywords.go +++ b/oracle/parser/keywords.go @@ -126,6 +126,49 @@ func isOracleContextKeyword(tokenType int) bool { } } +// plsqlReservedWords are the words Oracle 23ai rejects, unquoted, where +// PL/SQL expects a name, such as the label after END (PLS-00103). They were +// found by compiling BEGIN BEGIN NULL; END word; END; for each word of the +// documented PL/SQL reserved words, each SQL reserved word, and each PL/SQL +// statement keyword; the engine, not the list, decides. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/plsql-reserved-words-keywords.html +var plsqlReservedWords = map[string]bool{ + "ALL": true, "ALTER": true, "AND": true, "ANY": true, "AS": true, "ASC": true, "AT": true, + "BEGIN": true, "BETWEEN": true, "BY": true, "CASE": true, "CHECK": true, "CLUSTER": true, + "CLUSTERS": true, "COLAUTH": true, "COLUMNS": true, "COMPRESS": true, "CONNECT": true, + "CRASH": true, "CREATE": true, "CURRENT": true, "DECLARE": true, "DEFAULT": true, + "DELETE": true, "DESC": true, "DISTINCT": true, "DROP": true, "ELSE": true, "END": true, + "EXCEPTION": true, "EXCLUSIVE": true, "EXISTS": true, "FETCH": true, "FOR": true, + "FROM": true, "GOTO": true, "GRANT": true, "GROUP": true, "HAVING": true, + "IDENTIFIED": true, "IF": true, "IN": true, "INDEX": true, "INDEXES": true, "INSERT": true, + "INTERSECT": true, "INTO": true, "IS": true, "LIKE": true, "LOCK": true, "MINUS": true, + "MODE": true, "NOCOMPRESS": true, "NOT": true, "NOWAIT": true, "NULL": true, "OF": true, + "ON": true, "OPTION": true, "OR": true, "ORDER": true, "OVERLAPS": true, "PRIOR": true, + "PROCEDURE": true, "PUBLIC": true, "RESOURCE": true, "REVOKE": true, "SELECT": true, + "SHARE": true, "SIZE": true, "SQL": true, "START": true, "TABAUTH": true, "TABLE": true, + "THEN": true, "TO": true, "UNION": true, "UNIQUE": true, "UPDATE": true, "VALUES": true, + "VIEW": true, "VIEWS": true, "WHEN": true, "WHERE": true, "WITH": true, +} + +// isPLSQLIdentifier reports whether the current token can name a PL/SQL +// item: a quoted identifier, or a word that is not PL/SQL reserved. SUBTYPE +// loop IS NUMBER compiles on Oracle 23ai; SUBTYPE prior IS NUMBER does not. +func (p *Parser) isPLSQLIdentifier() bool { + if p.cur.Type == tokQIDENT { + return true + } + return p.isIdentLike() && !plsqlReservedWords[p.cur.Str] +} + +// atEndName reports whether the current token, just after END or END LOOP, +// is the optional label or unit name. A PL/SQL reserved word is not: in +// END LOOP END; the second END closes the enclosing block, and Oracle reports +// the missing ';' at it. +func (p *Parser) atEndName() bool { + return p.isPLSQLIdentifier() +} + // isOracleSQLReservedKeyword returns true when tok is an Oracle SQL reserved word // that cannot be used as a nonquoted object or column identifier. func isOracleSQLReservedKeyword(tok Token) bool { diff --git a/oracle/parser/parser.go b/oracle/parser/parser.go index ecc2e154..2bee07e4 100644 --- a/oracle/parser/parser.go +++ b/oracle/parser/parser.go @@ -27,6 +27,11 @@ type Parser struct { // met to the end of the range, whether it opens a CASE expression (true) // or names a column (false). decideCaseTokens fills it in one pass. caseKinds map[int]bool + + // plsqlTypeMods is set while parsePLSQLTypeName parses the datatype of + // a PL/SQL declaration, where a length, precision, or scale may be an + // expression rather than an integer literal. + plsqlTypeMods bool } // Parse parses a SQL string into an AST list. @@ -252,15 +257,7 @@ func (p *Parser) parseStmt() (nodes.StmtNode, error) { case kwSET: next := p.peekNext() if next.Type == kwTRANSACTION { - start := p.pos() - p.advance() // consume SET - p.advance() // consume TRANSACTION - stmt, parseErr828 := p.parseSetTransactionStmt() - if parseErr828 != nil { - return nil, parseErr828 - } - stmt.(*nodes.SetTransactionStmt).Loc.Start = start - return stmt, nil + return p.parseSetTransactionAtSet() } if next.Type == kwROLE { return p.parseSetRoleStmt() diff --git a/oracle/parser/plsql_block.go b/oracle/parser/plsql_block.go index c0f3ce10..f40d4fec 100644 --- a/oracle/parser/plsql_block.go +++ b/oracle/parser/plsql_block.go @@ -80,12 +80,15 @@ func (p *Parser) parsePLSQLBlock() (*nodes.PLSQLBlock, error) { } p.advance() // consume END // Optional label after END - if p.isIdentLike() && p.cur.Type != ';' && p.cur.Type != tokEOF { + if p.atEndName() { p.advance() // consume label } - if p.cur.Type == ';' { - p.advance() // consume ; + // END [label] ends with ';', for a nested block and for a whole unit + // alike: Oracle compiles BEGIN NULL; END with PLS-00103. + if p.cur.Type != ';' { + return nil, p.syntaxErrorAtCur() } + p.advance() // consume ; block.Loc.End = p.prev.End return block, nil @@ -104,6 +107,10 @@ func (p *Parser) parsePLSQLDeclarations() (*nodes.List, error) { if decl == nil { break } + if p.prev.Type != ';' { + // Every declaration ends with ';' (PLS-00103 otherwise). + return nil, p.syntaxErrorAtCur() + } decls.Items = append(decls.Items, decl) } @@ -130,6 +137,10 @@ func (p *Parser) parsePLSQLDeclaration() (nodes.Node, error) { } } + if p.cur.Type == kwSUBTYPE { + return p.parsePLSQLSubtypeDecl() + } + // Local subprogram declaration or definition. The package-item parser // surface is reused (name, optional parameters, IS|AS body or // forward-declaration semicolon) and is intentionally broad: it also @@ -178,7 +189,7 @@ func (p *Parser) parsePLSQLVarDecl() (*nodes.PLSQLVarDecl, error) { var parseErr835 error // Type name - decl.TypeName, parseErr835 = p.parseTypeName() + decl.TypeName, parseErr835 = p.parsePLSQLTypeName() if parseErr835 != // Optional NOT NULL @@ -369,6 +380,12 @@ func (p *Parser) parsePLSQLStatements() (*nodes.List, error) { if stmt == nil { return stmts, nil } + // Every PL/SQL statement ends with ';' (PLS-00103 otherwise). Without + // this check the words after a statement that stopped early parsed + // as further procedure calls. + if p.prev.Type != ';' { + return nil, p.syntaxErrorAtCur() + } stmts.Items = append(stmts.Items, stmt) } return nil, @@ -488,6 +505,24 @@ func (p *Parser) parsePLSQLStatement() (nodes.StmtNode, error) { case kwCASE: return p.parsePLSQLCaseStmt() + // SQL statements PL/SQL runs as they are, each ended by its ';'. + case kwCOMMIT: + return p.parsePLSQLSQLStmt(p.parseCommitStmt) + case kwROLLBACK: + return p.parsePLSQLSQLStmt(p.parseRollbackStmt) + case kwSAVEPOINT: + return p.parsePLSQLSQLStmt(p.parseSavepointStmt) + case kwLOCK: + return p.parsePLSQLSQLStmt(p.parseLockTableStmt) + case kwSET: + if p.peekNext().Type == kwTRANSACTION { + return p.parsePLSQLSQLStmt(p.parseSetTransactionAtSet) + } + return nil, p.syntaxErrorAtCur() + + case kwPRAGMA: + return p.parsePLSQLPragmaStmt() + case kwMERGE: stmt, parseErr846 := p.parseMergeStmt() if parseErr846 != nil { @@ -549,6 +584,34 @@ func (p *Parser) parsePLSQLStatement() (nodes.StmtNode, error) { } } +// parsePLSQLSQLStmt parses a SQL statement in a PL/SQL body with parse and +// consumes the ';' that ends it. +func (p *Parser) parsePLSQLSQLStmt(parse func() (nodes.StmtNode, error)) (nodes.StmtNode, error) { + stmt, err := parse() + if err != nil { + return nil, err + } + if p.cur.Type == ';' { + p.advance() + } + return stmt, nil +} + +// parsePLSQLPragmaStmt parses a pragma that stands as a statement. Oracle +// 23ai accepts INLINE, COVERAGE, DEPRECATE, and SUPPRESSES_WARNING_6009 there; +// the others belong to a declarative section or a package and fail in a body +// (AUTONOMOUS_TRANSACTION, UDF: PLS-00710; SERIALLY_REUSABLE: PLS-00708; +// EXCEPTION_INIT: PLS-00700; RESTRICT_REFERENCES: PLS-00115). +func (p *Parser) parsePLSQLPragmaStmt() (nodes.StmtNode, error) { + switch p.peekNext().Str { + case "INLINE", "COVERAGE", "DEPRECATE", "SUPPRESSES_WARNING_6009": + default: + p.advance() // consume PRAGMA + return nil, p.syntaxErrorAtCur() + } + return p.parsePLSQLPragma() +} + // parsePLSQLAssignOrCall parses an assignment statement (target := expr ;) // or a procedure call (name(args) ;). func (p *Parser) parsePLSQLAssignOrCall() (nodes.StmtNode, error) { @@ -865,7 +928,7 @@ func (p *Parser) consumeEndLoop() error { } p.advance() // Optional label after END LOOP - if p.isIdentLike() && p.cur.Type != ';' && p.cur.Type != tokEOF { + if p.atEndName() { p.advance() } return nil @@ -1689,10 +1752,80 @@ func (p *Parser) parsePLSQLCaseStmt() (nodes.StmtNode, error) { return stmt, nil } +// parsePLSQLSubtypeDecl parses a SUBTYPE declaration, the current token at +// SUBTYPE. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/block.html (subtype_definition) +// +// SUBTYPE subtype IS base_type +// [ constraint | CHARACTER SET character_set ] [ NOT NULL ] ; +// constraint ::= { precision [, scale ] | RANGE low_value .. high_value } +// +// Precision and scale are written in parentheses as part of base_type: +// Oracle 23ai rejects SUBTYPE s IS NUMBER 8, 2 (PLS-00103). CHARACTER SET is +// not parsed; Oracle allows it only on a subprogram parameter (PLS-00551). +func (p *Parser) parsePLSQLSubtypeDecl() (*nodes.PLSQLSubtypeDecl, error) { + decl := &nodes.PLSQLSubtypeDecl{Loc: nodes.Loc{Start: p.pos()}} + p.advance() // consume SUBTYPE + + if !p.isPLSQLIdentifier() { + return nil, p.syntaxErrorAtCur() + } + name, err := p.parseIdentifier() + if err != nil { + return nil, err + } + decl.Name = name + if p.cur.Type != kwIS { + return nil, p.syntaxErrorAtCur() + } + p.advance() + + decl.BaseType, err = p.parsePLSQLTypeName() + if err != nil { + return nil, err + } + if decl.BaseType.Names.Len() == 0 { + return nil, p.syntaxErrorAtCur() + } + + if p.cur.Type == kwRANGE { + p.advance() + if decl.RangeLow, err = p.parseExpr(); err != nil { + return nil, err + } + if p.cur.Type != tokDOTDOT { + return nil, p.syntaxErrorAtCur() + } + p.advance() + if decl.RangeHigh, err = p.parseExpr(); err != nil { + return nil, err + } + } + decl.NotNull = p.consumeNotNull() + + if p.cur.Type != ';' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + decl.Loc.End = p.prev.End + return decl, nil +} + +// consumeNotNull consumes NOT NULL and reports whether it was there. +func (p *Parser) consumeNotNull() bool { + if p.cur.Type != kwNOT || p.peekNext().Type != kwNULL { + return false + } + p.advance() // consume NOT + p.advance() // consume NULL + return true +} + // parsePLSQLTypeDecl parses a PL/SQL TYPE declaration. // -// TYPE name IS TABLE OF type [INDEX BY type] ; -// TYPE name IS VARRAY(n) OF type ; +// TYPE name IS TABLE OF type [NOT NULL] [INDEX BY type] ; +// TYPE name IS VARRAY(n) OF type [NOT NULL] ; // TYPE name IS RECORD (field type [,...]) ; // TYPE name IS REF CURSOR [RETURN type] ; func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { @@ -1723,7 +1856,7 @@ func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { p.advance() } var parseErr894 error - decl.ElementType, parseErr894 = p.parseTypeName() + decl.ElementType, parseErr894 = p.parsePLSQLTypeName() if parseErr894 != // INDEX BY nil { @@ -1732,6 +1865,7 @@ func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { if decl.ElementType == nil || decl.ElementType.Names.Len() == 0 { return nil, p.syntaxErrorAtCur() } + decl.ElementNotNull = p.consumeNotNull() if p.cur.Type == kwINDEX { p.advance() @@ -1739,7 +1873,7 @@ func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { p.advance() } var parseErr895 error - decl.IndexBy, parseErr895 = p.parseTypeName() + decl.IndexBy, parseErr895 = p.parsePLSQLTypeName() if parseErr895 != nil { return nil, parseErr895 } @@ -1768,13 +1902,14 @@ func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { p.advance() } var parseErr897 error - decl.ElementType, parseErr897 = p.parseTypeName() + decl.ElementType, parseErr897 = p.parsePLSQLTypeName() if parseErr897 != nil { return nil, parseErr897 } if decl.ElementType == nil || decl.ElementType.Names.Len() == 0 { return nil, p.syntaxErrorAtCur() } + decl.ElementNotNull = p.consumeNotNull() case p.isIdentLike() && p.cur.Str == "RECORD": decl.Kind = "RECORD" diff --git a/oracle/parser/plsql_dictionary_oracle_test.go b/oracle/parser/plsql_dictionary_oracle_test.go index 2f15756a..066f7e6f 100644 --- a/oracle/parser/plsql_dictionary_oracle_test.go +++ b/oracle/parser/plsql_dictionary_oracle_test.go @@ -248,7 +248,7 @@ var dictionaryCategories = []struct { {"PARALLEL_ENABLE (PARTITION ...)", "", regexp.MustCompile(`(?i)parallel_enable\s*\(`)}, {"package initialization section", "PACKAGE BODY", regexp.MustCompile(`(?i)^\s*begin\b`)}, {"REF type", "", regexp.MustCompile(`(?i)\bref\s`)}, - {"Oracle-internal syntax (PRAGMA INTERFACE, ... parameters)", "", regexp.MustCompile(`(?i)pragma\s+interface|\.\.\.`)}, + {"Oracle-internal syntax (PRAGMA INTERFACE, ... parameters, enumeration types)", "", regexp.MustCompile(`(?i)pragma\s+interface|\.\.\.|^\s*type\s+\w+\s+is\s*\(`)}, {"JSON function clause", "", regexp.MustCompile(`(?i)\breturning\b|json_|^\s*key\b`)}, {"trigger CALL with arguments", "TRIGGER", regexp.MustCompile(`(?i)^\s*call\b|\(\s*:(new|old)\.`)}, {"qualified expression (=>)", "", regexp.MustCompile(`=>`)}, diff --git a/oracle/parser/plsql_terminator_test.go b/oracle/parser/plsql_terminator_test.go new file mode 100644 index 00000000..baa9d14d --- /dev/null +++ b/oracle/parser/plsql_terminator_test.go @@ -0,0 +1,203 @@ +package parser + +import ( + "errors" + "testing" + + "github.com/bytebase/omni/oracle/ast" +) + +// TestPLSQLTerminatorsRequired checks that every PL/SQL statement, +// declaration, package item, and END carries its ';', and that a stray ';' +// is an error, as on Oracle 23ai (PLS-00103; reference rows ref_108 to +// ref_118). at is the text from the error onward, "" for the end of input. +func TestPLSQLTerminatorsRequired(t *testing.T) { + cases := []struct { + sql string + at string + }{ + {"BEGIN NULL END;", "END;"}, + {"BEGIN x := 1 y := 2; END;", "y := 2; END;"}, + {"BEGIN IF 1 = 1 THEN NULL; END IF END;", "END;"}, + {"BEGIN LOOP NULL; END LOOP END;", "END;"}, + {"BEGIN CASE 1 WHEN 1 THEN NULL; END CASE END;", "END;"}, + {"BEGIN BEGIN NULL; END END;", "END;"}, + {"BEGIN BEGIN NULL; END IF; END;", "IF; END;"}, + {"BEGIN <> BEGIN NULL; END l; END", ""}, + {"BEGIN NULL; END", ""}, + {"BEGIN NULL; END lbl", ""}, + {"BEGIN NULL;; END;", "; END;"}, + {"BEGIN COMMIT END;", "END;"}, + {"BEGIN PRAGMA AUTONOMOUS_TRANSACTION; NULL; END;", "AUTONOMOUS_TRANSACTION; NULL; END;"}, + {"BEGIN SET ROLE ALL; END;", "SET ROLE ALL; END;"}, + {"DECLARE x NUMBER y NUMBER; BEGIN NULL; END;", "y NUMBER; BEGIN NULL; END;"}, + {"DECLARE x NUMBER;; BEGIN NULL; END;", "; BEGIN NULL; END;"}, + {"CREATE PROCEDURE p AS BEGIN NULL; END", ""}, + {"CREATE PACKAGE k AS PROCEDURE q; END", ""}, + {"CREATE PACKAGE k AS PROCEDURE q END;", "END;"}, + {"CREATE PACKAGE k AS c CONSTANT PLS_INTEGER := 1;; END;", "; END;"}, + {"CREATE PACKAGE k AS ; c CONSTANT PLS_INTEGER := 1; END;", "; c CONSTANT PLS_INTEGER := 1; END;"}, + {"CREATE PACKAGE k AS c CONSTANT PLS_INTEGER := 1; 42; END;", "42; END;"}, + {"CREATE PACKAGE BODY k AS PROCEDURE q IS BEGIN NULL; END; END", ""}, + {"CREATE TYPE BODY ty AS MEMBER FUNCTION f RETURN NUMBER IS BEGIN RETURN 1; END; END", ""}, + {"CREATE TRIGGER trg FOR INSERT ON t COMPOUND TRIGGER BEFORE STATEMENT IS BEGIN NULL; END BEFORE STATEMENT; END trg", ""}, + } + for _, tc := range cases { + _, err := Parse(tc.sql) + var pe *ParseError + if !errors.As(err, &pe) { + t.Errorf("Parse(%q) = %v, want a ParseError", tc.sql, err) + continue + } + if want := len(tc.sql) - len(tc.at); pe.Position != want { + t.Errorf("Parse(%q) error at %d (%v), want %d, at %q", tc.sql, pe.Position, err, want, tc.at) + } + } +} + +// TestPLSQLTransactionStatements checks the SQL statements a PL/SQL body runs +// as they are, which the parser used to skip as unknown words (ref_105 to +// ref_107). +func TestPLSQLTransactionStatements(t *testing.T) { + sql := "BEGIN SAVEPOINT s; ROLLBACK TO SAVEPOINT s; ROLLBACK WORK; COMMIT WORK; SET TRANSACTION READ ONLY; " + + "LOCK TABLE t IN EXCLUSIVE MODE NOWAIT; PRAGMA INLINE (pr, 'YES'); PRAGMA COVERAGE ('NOT_FEASIBLE'); NULL; END;" + result := ParseAndCheck(t, sql) + block := result.Items[0].(*ast.RawStmt).Stmt.(*ast.PLSQLBlock) + var got []string + for _, item := range block.Statements.Items { + switch n := item.(type) { + case *ast.SavepointStmt: + got = append(got, "SAVEPOINT") + case *ast.RollbackStmt: + got = append(got, "ROLLBACK") + case *ast.CommitStmt: + got = append(got, "COMMIT") + case *ast.SetTransactionStmt: + got = append(got, "SET TRANSACTION") + case *ast.LockTableStmt: + got = append(got, "LOCK TABLE") + case *ast.PLSQLPragma: + got = append(got, "PRAGMA "+n.Name) + case *ast.PLSQLNull: + got = append(got, "NULL") + default: + got = append(got, "?") + } + } + want := []string{"SAVEPOINT", "ROLLBACK", "ROLLBACK", "COMMIT", "SET TRANSACTION", "LOCK TABLE", + "PRAGMA INLINE", "PRAGMA COVERAGE", "NULL"} + if len(got) != len(want) { + t.Fatalf("statements = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("statements = %v, want %v", got, want) + } + } + if violations := CheckLocations(t, sql); len(violations) > 0 { + t.Errorf("Loc violations: %v", violations) + } +} + +// TestPLSQLSubtypeDecl checks SUBTYPE declarations (ref_119 to ref_121). +func TestPLSQLSubtypeDecl(t *testing.T) { + sql := "DECLARE SUBTYPE s IS PLS_INTEGER RANGE 0 .. 3 NOT NULL; SUBTYPE v IS VARCHAR2(10); SUBTYPE r IS t.c%TYPE; " + + "SUBTYPE loop IS NUMBER; SUBTYPE \"SELECT\" IS NUMBER; BEGIN NULL; END;" + result := ParseAndCheck(t, sql) + decls := result.Items[0].(*ast.RawStmt).Stmt.(*ast.PLSQLBlock).Declarations + if decls.Len() != 5 { + t.Fatalf("declarations = %d, want 5", decls.Len()) + } + s := decls.Items[0].(*ast.PLSQLSubtypeDecl) + if s.Name != "S" || s.RangeLow == nil || s.RangeHigh == nil || !s.NotNull { + t.Errorf("SUBTYPE s = %+v, want RANGE 0 .. 3 NOT NULL", s) + } + if got := sql[s.Loc.Start:s.Loc.End]; got != "SUBTYPE s IS PLS_INTEGER RANGE 0 .. 3 NOT NULL;" { + t.Errorf("SUBTYPE s Loc covers %q", got) + } + v := decls.Items[1].(*ast.PLSQLSubtypeDecl) + if v.RangeLow != nil || v.NotNull || v.BaseType.TypeMods.Len() != 1 { + t.Errorf("SUBTYPE v = %+v, want VARCHAR2(10)", v) + } + if r := decls.Items[2].(*ast.PLSQLSubtypeDecl); !r.BaseType.IsPercType { + t.Errorf("SUBTYPE r base type = %+v, want t.c%%TYPE", r.BaseType) + } + if violations := CheckLocations(t, sql); len(violations) > 0 { + t.Errorf("Loc violations: %v", violations) + } + + for _, bad := range []string{ + "DECLARE SUBTYPE s NUMBER; BEGIN NULL; END;", + "DECLARE SUBTYPE s IS NUMBER 8, 2; BEGIN NULL; END;", + "DECLARE SUBTYPE s IS PLS_INTEGER RANGE 1; BEGIN NULL; END;", + "DECLARE SUBTYPE s IS; BEGIN NULL; END;", + "DECLARE SUBTYPE IS IS NUMBER; BEGIN NULL; END;", + "DECLARE SUBTYPE prior IS NUMBER; BEGIN NULL; END;", + } { + ParseShouldFail(t, bad) + } +} + +// TestPLSQLTypeLengthExpressions checks that a PL/SQL declaration takes an +// expression for a length, precision, or scale, while SQL keeps to integer +// literals (ref_122, ref_123). +func TestPLSQLTypeLengthExpressions(t *testing.T) { + sql := "DECLARE v VARCHAR2(ORA_MAX_NAME_LEN + 2); w NUMBER(k.p, 2); c CHAR(k.n BYTE); z NUMBER(*, 2); BEGIN NULL; END;" + result := ParseAndCheck(t, sql) + decls := result.Items[0].(*ast.RawStmt).Stmt.(*ast.PLSQLBlock).Declarations + mods := func(i int) []ast.Node { + return decls.Items[i].(*ast.PLSQLVarDecl).TypeName.TypeMods.Items + } + if m := mods(0); len(m) != 1 || isIntegerMod(m[0]) { + t.Errorf("VARCHAR2(ORA_MAX_NAME_LEN + 2) mods = %#v, want one expression", m) + } + if m := mods(1); len(m) != 2 || isIntegerMod(m[0]) || !isIntegerMod(m[1]) { + t.Errorf("NUMBER(k.p, 2) mods = %#v, want an expression and an integer", m) + } + if m := mods(2); len(m) != 2 || isIntegerMod(m[0]) { + t.Errorf("CHAR(k.n BYTE) mods = %#v, want an expression and BYTE", m) + } else if s, ok := m[1].(*ast.String); !ok || s.Str != "BYTE" { + t.Errorf("CHAR(k.n BYTE) length semantics = %#v, want BYTE", m[1]) + } + if m := mods(3); len(m) != 2 || !isIntegerMod(m[1]) { + t.Errorf("NUMBER(*, 2) mods = %#v, want * and an integer", m) + } + if violations := CheckLocations(t, sql); len(violations) > 0 { + t.Errorf("Loc violations: %v", violations) + } + + for _, bad := range []string{ + "DECLARE v VARCHAR2(10 x); BEGIN NULL; END;", + "DECLARE v VARCHAR2(); BEGIN NULL; END;", + "CREATE TABLE t (c VARCHAR2(n))", + "CREATE TABLE t (c VARCHAR2(10 + 2))", + } { + ParseShouldFail(t, bad) + } +} + +func isIntegerMod(n ast.Node) bool { + _, ok := n.(*ast.Integer) + return ok +} + +// TestPLSQLCollectionElementNotNull checks NOT NULL on the element type of a +// nested table, associative array, and varray (ref_124). +func TestPLSQLCollectionElementNotNull(t *testing.T) { + sql := "DECLARE TYPE a IS TABLE OF NUMBER NOT NULL INDEX BY PLS_INTEGER; TYPE b IS VARRAY(3) OF BLOB NOT NULL; " + + "TYPE c IS TABLE OF VARCHAR2(ORA_MAX_NAME_LEN); BEGIN NULL; END;" + result := ParseAndCheck(t, sql) + decls := result.Items[0].(*ast.RawStmt).Stmt.(*ast.PLSQLBlock).Declarations + for i, want := range []bool{true, true, false} { + decl := decls.Items[i].(*ast.PLSQLTypeDecl) + if decl.ElementNotNull != want { + t.Errorf("TYPE %s ElementNotNull = %v, want %v", decl.Name, decl.ElementNotNull, want) + } + } + if decls.Items[0].(*ast.PLSQLTypeDecl).IndexBy == nil { + t.Error("TYPE a lost its INDEX BY after NOT NULL") + } + if violations := CheckLocations(t, sql); len(violations) > 0 { + t.Errorf("Loc violations: %v", violations) + } +} diff --git a/oracle/parser/testdata/coverage/loc_node_coverage.tsv b/oracle/parser/testdata/coverage/loc_node_coverage.tsv index 55d05639..fed2c270 100644 --- a/oracle/parser/testdata/coverage/loc_node_coverage.tsv +++ b/oracle/parser/testdata/coverage/loc_node_coverage.tsv @@ -234,6 +234,7 @@ node_type family status fixture notes debt_class approval next_action *nodes.PLSQLVarDecl plsql covered DECLARE x NUMBER; BEGIN NULL; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.PLSQLCursorDecl plsql covered DECLARE CURSOR c IS SELECT 1 FROM dual; BEGIN NULL; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.PLSQLTypeDecl plsql covered DECLARE TYPE nt IS TABLE OF NUMBER; BEGIN NULL; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard +*nodes.PLSQLSubtypeDecl plsql covered DECLARE SUBTYPE s IS PLS_INTEGER RANGE 1 .. 31 NOT NULL; BEGIN NULL; END; direct parser fixture for SUBTYPE declarations none covered keep_regression_guard *nodes.PLSQLAssign plsql covered BEGIN x := 1; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.PLSQLIf plsql covered BEGIN IF 1 = 1 THEN NULL; END IF; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.PLSQLElsIf plsql covered BEGIN IF 1 = 0 THEN NULL; ELSIF 1 = 1 THEN NULL; END IF; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard diff --git a/oracle/parser/testdata/coverage/plsql_dictionary_known_failures.tsv b/oracle/parser/testdata/coverage/plsql_dictionary_known_failures.tsv index abc0b824..4eb8f767 100644 --- a/oracle/parser/testdata/coverage/plsql_dictionary_known_failures.tsv +++ b/oracle/parser/testdata/coverage/plsql_dictionary_known_failures.tsv @@ -2,7 +2,7 @@ unit category position error GSMADMIN_INTERNAL.DBMS_GSM_COMMON:PACKAGE ACCESSIBLE BY clause 567:26 syntax error at or near "dbms_gsm_dbadmin" GSMADMIN_INTERNAL.DBMS_GSM_DBADMIN:PACKAGE ACCESSIBLE BY clause 71:24 syntax error at or near "sys" GSMADMIN_INTERNAL.DBMS_GSM_UTILITY:PACKAGE ACCESSIBLE BY clause 938:1 syntax error at or near "ACCESSIBLE" -GSMADMIN_INTERNAL.DBMS_SHARDING_DIRECTORY:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters) 42:34 syntax error at or near ".." +GSMADMIN_INTERNAL.DBMS_SHARDING_DIRECTORY:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters, enumeration types) 42:34 syntax error at or near ".." GSMADMIN_INTERNAL.CAT_ROLLBACK_TRIGGER:TRIGGER trigger CALL with arguments 3:40 syntax error at or near "(" GSMADMIN_INTERNAL.DONE_TRIGGER:TRIGGER trigger CALL with arguments 3:40 syntax error at or near "(" GSMADMIN_INTERNAL.REQUEST_DELETE_TRIGGER:TRIGGER trigger CALL with arguments 3:36 syntax error at or near "(" @@ -44,7 +44,7 @@ SYS.DBMS_METADATA_UTIL:PACKAGE ACCESSIBLE BY clause 818:20 syntax error at or ne SYS.DBMS_MLE:PACKAGE conditional compilation ($IF/$END) 1:1 split into 96 segments SYS.DBMS_PIPE:PACKAGE CHARACTER SET ANY_CS / %CHARSET 246:43 syntax error at or near "character" SYS.DBMS_PLUGTS:PACKAGE ACCESSIBLE BY clause 121:28 syntax error at or near "sys" -SYS.DBMS_REPORT:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters) 175:22 syntax error at or near ".." +SYS.DBMS_REPORT:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters, enumeration types) 175:22 syntax error at or near ".." SYS.DBMS_RULE_INTERNAL:PACKAGE ACCESSIBLE BY clause 46:3 syntax error at or near "ACCESSIBLE" SYS.DBMS_SCHEDULER:PACKAGE CHARACTER SET ANY_CS / %CHARSET 1076:40 syntax error at or near "CHARACTER" SYS.DBMS_SODA_UTIL:PACKAGE ACCESSIBLE BY clause 1:61 syntax error at or near "accessible" @@ -62,8 +62,8 @@ SYS.DBMS_VECTOR_DATABASE_ADMIN:PACKAGE ACCESSIBLE BY clause 2:18 syntax error at SYS.HTF:PACKAGE CHARACTER SET ANY_CS / %CHARSET 43:35 syntax error at or near "character" SYS.HTP:PACKAGE CHARACTER SET ANY_CS / %CHARSET 17:38 syntax error at or near "character" SYS.OWA_UTIL:PACKAGE CHARACTER SET ANY_CS / %CHARSET 46:42 syntax error at or near "character" -SYS.PLITBLM:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters) 20:29 syntax error at or near ")" -SYS.STANDARD:PACKAGE CHARACTER SET ANY_CS / %CHARSET 255:39 syntax error at or near "CHARACTER" +SYS.PLITBLM:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters, enumeration types) 20:29 syntax error at or near ")" +SYS.STANDARD:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters, enumeration types) 4:19 syntax error at or near "(" SYS.UTL_ENCODE:PACKAGE CHARACTER SET ANY_CS / %CHARSET 55:51 syntax error at or near "character" SYS.UTL_HTTP:PACKAGE CHARACTER SET ANY_CS / %CHARSET 1456:52 syntax error at or near "CHARACTER" SYS.UTL_I18N:PACKAGE CHARACTER SET ANY_CS / %CHARSET 109:43 syntax error at or near "CHARACTER" @@ -75,8 +75,7 @@ SYS.UTL_TCP:PACKAGE CHARACTER SET ANY_CS / %CHARSET 340:50 syntax error at or ne SYS.UTL_URL:PACKAGE CHARACTER SET ANY_CS / %CHARSET 147:53 syntax error at or near "CHARACTER" SYS.UTL_XML:PACKAGE ACCESSIBLE BY clause 129:3 syntax error at or near "ACCESSIBLE" SYS.CDBVIEW_INTERNAL:PACKAGE BODY qualified expression (=>) 12:57 syntax error at or near "=>" -SYS.DBMS_HADOOP:PACKAGE BODY type length expression 98:40 syntax error at or near "+" -SYS.DBMS_HADOOP_INTERNAL:PACKAGE BODY type length expression 62:37 syntax error at or near "+" +SYS.DBMS_HADOOP_INTERNAL:PACKAGE BODY unclassified 254:7 syntax error at or near "union" SYS.HTF:PACKAGE BODY CHARACTER SET ANY_CS / %CHARSET 40:32 syntax error at or near "CHARACTER" SYS.HTP:PACKAGE BODY CHARACTER SET ANY_CS / %CHARSET 68:39 syntax error at or near "character" SYS.OWA_COOKIE:PACKAGE BODY package initialization section 221:1 syntax error at or near "begin" @@ -84,11 +83,11 @@ SYS.OWA_CUSTOM:PACKAGE BODY package initialization section 15:1 syntax error at SYS.OWA_IMAGE:PACKAGE BODY package initialization section 14:1 syntax error at or near "begin" SYS.OWA_UTIL:PACKAGE BODY CHARACTER SET ANY_CS / %CHARSET 24:26 syntax error at or near "character" SYS.STANDARD:PACKAGE BODY CHARACTER SET ANY_CS / %CHARSET 16:31 syntax error at or near "CHARACTER" -SYS.DBMS_FEATURE_ADBDCAT:PROCEDURE JSON function clause 181:49 syntax error at or near ")" -SYS.DBMS_FEATURE_ADBXTTAB:PROCEDURE JSON function clause 320:49 syntax error at or near ")" +SYS.DBMS_FEATURE_ADBDCAT:PROCEDURE JSON function clause 181:28 syntax error at or near "RETURNING" +SYS.DBMS_FEATURE_ADBXTTAB:PROCEDURE JSON function clause 320:28 syntax error at or near "RETURNING" SYS.DBMS_FEATURE_JOB_CLOUDSQL:PROCEDURE JSON function clause 54:13 syntax error at or near "'type'" -SYS.DBMS_FEATURE_MLE:PROCEDURE JSON function clause 29:49 syntax error at or near ")" -SYS.DBMS_FEATURE_SQL_FIREWALL:PROCEDURE JSON function clause 56:19 syntax error at or near ")" +SYS.DBMS_FEATURE_MLE:PROCEDURE JSON function clause 29:28 syntax error at or near "RETURNING" +SYS.DBMS_FEATURE_SQL_FIREWALL:PROCEDURE JSON function clause 56:5 syntax error at or near "returning" SYS.OBJNUM_REUSE_HOLES:PROCEDURE SQL%BULK_EXCEPTIONS 107:30 syntax error at or near "%" SYS.DBMS_KAFKA_DROP_USER:TRIGGER conditional compilation ($IF/$END) 57:7 syntax error at or near "$" SYS.JDOM_T:TYPE BODY CHARACTER SET ANY_CS / %CHARSET 9:41 syntax error at or near "character" diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 46e1df91..df21abdc 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -103,3 +103,27 @@ ref_101 select accept SELECT CASE likec WHEN 'x' THEN 2 END FROM t_kwcol column ref_102 select accept SELECT CASE likec = 'x' WHEN TRUE THEN 1 ELSE 0 END FROM t_kwcol simple CASE with a Boolean comparison selector ref_103 select accept SELECT k, j, v FROM (SELECT 1 k, 1 j, 10 v FROM dual) MODEL DIMENSION BY (k, j) MEASURES (v) RULES (v[2, 1] = CASE v[1, 1] WHEN 10 THEN 20 END) MODEL cell reference as a simple CASE selector ref_104 select accept SELECT k, case FROM (SELECT 1 k, 10 v FROM dual) MODEL DIMENSION BY (k) MEASURES (v case) RULES (case[2] = CASE case[1] WHEN 10 THEN 20 END) MODEL measure named CASE +ref_105 plsql accept BEGIN SAVEPOINT s; ROLLBACK TO SAVEPOINT s; ROLLBACK WORK; COMMIT WORK; SET TRANSACTION READ ONLY; COMMIT; END; transaction statements in a block +ref_106 plsql accept BEGIN LOCK TABLE t IN EXCLUSIVE MODE NOWAIT; ROLLBACK; END; LOCK TABLE in a block +ref_107 plsql accept DECLARE PROCEDURE pr (x VARCHAR2) IS BEGIN NULL; END; BEGIN PRAGMA INLINE (pr, 'YES'); pr('a'); PRAGMA COVERAGE ('NOT_FEASIBLE'); NULL; END; INLINE and COVERAGE pragmas as statements +ref_108 plsql reject BEGIN PRAGMA AUTONOMOUS_TRANSACTION; NULL; END; AUTONOMOUS_TRANSACTION is a declaration, not a statement +ref_109 plsql reject BEGIN NULL END; statement without its semicolon +ref_110 plsql reject BEGIN IF 1 = 1 THEN NULL; END IF END; END IF without its semicolon +ref_111 plsql reject BEGIN BEGIN NULL; END END; inner block END without its semicolon +ref_112 plsql reject BEGIN NULL;; END; stray semicolon between statements +ref_113 plsql reject DECLARE x NUMBER y NUMBER; BEGIN NULL; END; declaration without its semicolon +ref_114 plsql reject DECLARE x NUMBER;; BEGIN NULL; END; stray semicolon between declarations +ref_115 plsql_body reject CREATE OR REPLACE PROCEDURE omni_nosemi_p_{run} AS BEGIN NULL; END procedure END without its semicolon +ref_116 plsql_body reject CREATE OR REPLACE PACKAGE omni_nosemi_pk_{run} AS PROCEDURE q; END package END without its semicolon +ref_117 plsql_body reject CREATE OR REPLACE PACKAGE omni_semi_pk_{run} AS c CONSTANT PLS_INTEGER := 1;; END; stray semicolon between package items +ref_118 plsql_body reject CREATE OR REPLACE PACKAGE omni_noitem_pk_{run} AS c CONSTANT PLS_INTEGER := 1; 42; END; package item that is not a declaration +ref_119 plsql accept DECLARE SUBTYPE s IS PLS_INTEGER RANGE 0 .. 3 NOT NULL; SUBTYPE v IS VARCHAR2(10); SUBTYPE n IS NUMBER(8, 2); x s := 1; BEGIN NULL; END; SUBTYPE declarations +ref_120 plsql reject DECLARE SUBTYPE n IS NUMBER 8, 2; BEGIN NULL; END; SUBTYPE precision outside parentheses +ref_121 plsql reject DECLARE SUBTYPE s IS PLS_INTEGER RANGE 1; BEGIN NULL; END; SUBTYPE RANGE without its upper bound +ref_122 plsql accept DECLARE v VARCHAR2(ORA_MAX_NAME_LEN + 2); r RAW(2 * 8); c CHAR(ORA_MAX_NAME_LEN BYTE); BEGIN NULL; END; static expressions as PL/SQL type lengths +ref_123 plsql reject DECLARE v VARCHAR2(10 x); BEGIN NULL; END; PL/SQL type length followed by a stray word +ref_124 plsql accept DECLARE TYPE a IS TABLE OF NUMBER NOT NULL INDEX BY PLS_INTEGER; TYPE b IS VARRAY(3) OF BLOB NOT NULL; TYPE c IS TABLE OF VARCHAR2(ORA_MAX_NAME_LEN) NOT NULL; BEGIN NULL; END; collection element types with NOT NULL +ref_125 plsql_body accept CREATE OR REPLACE PACKAGE omni_subtype_pk_{run} AS SUBTYPE s IS PLS_INTEGER RANGE 1 .. 31; c CONSTANT PLS_INTEGER := 10; TYPE r IS RECORD (a VARCHAR2(c), b NUMBER(c, 2)); END; package with SUBTYPE and constant type lengths +ref_126 plsql reject DECLARE TYPE e IS (a, b); BEGIN NULL; END; enumeration type outside STANDARD (PLS-00505) +ref_127 plsql accept BEGIN <> BEGIN NULL; END l; <> LOOP EXIT; END LOOP m; BEGIN NULL; END loop; BEGIN NULL; END type; END; labels after END, including unreserved keywords +ref_128 plsql reject BEGIN BEGIN NULL; END if; END; reserved word as the label after END diff --git a/oracle/parser/transaction.go b/oracle/parser/transaction.go index 68c7e02e..deef33eb 100644 --- a/oracle/parser/transaction.go +++ b/oracle/parser/transaction.go @@ -151,6 +151,20 @@ func (p *Parser) parseSavepointStmt() (nodes.StmtNode, error) { return stmt, nil } +// parseSetTransactionAtSet parses SET TRANSACTION ... with the current token +// at SET. +func (p *Parser) parseSetTransactionAtSet() (nodes.StmtNode, error) { + start := p.pos() + p.advance() // consume SET + p.advance() // consume TRANSACTION + stmt, err := p.parseSetTransactionStmt() + if err != nil { + return nil, err + } + stmt.(*nodes.SetTransactionStmt).Loc.Start = start + return stmt, nil +} + // parseSetTransactionStmt parses a SET TRANSACTION statement. // // BNF: oracle/parser/bnf/SET-TRANSACTION.bnf diff --git a/oracle/parser/type.go b/oracle/parser/type.go index a9ba38f0..d71c7138 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -139,11 +139,81 @@ func (p *Parser) parseTypeName() (*nodes.TypeName, error) { return tn, nil } +// parsePLSQLTypeName parses the datatype of a PL/SQL declaration. Where SQL +// takes an integer literal for a length, precision, or scale, PL/SQL takes a +// static expression: VARCHAR2(ORA_MAX_NAME_LEN + 2), NUMBER(pkg.p, 2). That +// the expression is static is checked when the unit compiles (PLS-00491 on +// Oracle 23ai), not when it parses, so any expression parses here. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/plsql-language-fundamentals.html (Static Expressions) +func (p *Parser) parsePLSQLTypeName() (*nodes.TypeName, error) { + saved := p.plsqlTypeMods + p.plsqlTypeMods = true + tn, err := p.parseTypeName() + p.plsqlTypeMods = saved + return tn, err +} + +// parsePLSQLTypeMods parses the parenthesized modifiers of a datatype in a +// PL/SQL declaration, the current token at '(': +// +// ( { expr | * } [ , expr ] ) precision and scale (sized is false) +// ( expr [ BYTE | CHAR ] ) size and length semantics (sized is true) +// +// An integer literal is recorded as *Integer, as in SQL; any other modifier +// is recorded as its expression. +func (p *Parser) parsePLSQLTypeMods(tn *nodes.TypeName, sized bool) error { + p.advance() // consume '(' + if !sized && p.cur.Type == '*' { + tn.TypeMods.Items = append(tn.TypeMods.Items, &nodes.String{Str: "*"}) + p.advance() + } else if err := p.parsePLSQLTypeMod(tn); err != nil { + return err + } + switch { + case sized && p.isIdentLikeStr("BYTE"): + tn.TypeMods.Items = append(tn.TypeMods.Items, &nodes.String{Str: "BYTE"}) + p.advance() + case sized && p.cur.Type == kwCHAR: + tn.TypeMods.Items = append(tn.TypeMods.Items, &nodes.String{Str: "CHAR"}) + p.advance() + case !sized && p.cur.Type == ',': + p.advance() + if err := p.parsePLSQLTypeMod(tn); err != nil { + return err + } + } + if p.cur.Type != ')' { + return p.syntaxErrorAtCur() + } + p.advance() + return nil +} + +func (p *Parser) parsePLSQLTypeMod(tn *nodes.TypeName) error { + expr, err := p.parseExpr() + if err != nil { + return err + } + if expr == nil { + return p.syntaxErrorAtCur() + } + if lit, ok := expr.(*nodes.NumberLiteral); ok && !lit.IsFloat { + tn.TypeMods.Items = append(tn.TypeMods.Items, &nodes.Integer{Ival: lit.Ival}) + } else { + tn.TypeMods.Items = append(tn.TypeMods.Items, expr) + } + return nil +} + // parseOptionalPrecisionScale parses optional ( precision [, scale ] ). func (p *Parser) parseOptionalPrecisionScale(tn *nodes.TypeName) error { if p.cur.Type != '(' { return nil } + if p.plsqlTypeMods { + return p.parsePLSQLTypeMods(tn, false) + } p.advance() // consume '(' // precision @@ -176,6 +246,9 @@ func (p *Parser) parseOptionalSizeWithSemantic(tn *nodes.TypeName) error { if p.cur.Type != '(' { return nil } + if p.plsqlTypeMods { + return p.parsePLSQLTypeMods(tn, true) + } p.advance() // consume '(' // size From f77182651846344164941503263c5f9020113a7a Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 06:54:27 +0000 Subject: [PATCH 2/3] fix(oracle): close CASE, IF, handler, and LOCK gaps in PL/SQL strictness (BYT-10301) Review follow-up, each form checked on Oracle 23ai: - CASE statement arms parse through the statement list, so every arm statement needs its ';' (THEN NULL x := 1 was accepted). - END CASE and END IF are required (END; alone is PLS-00103), take an optional label as Oracle allows (END IF l;), and need their ';'. - A block body, IF and CASE branch, loop body, and exception handler hold at least one statement (BEGIN END; is PLS-00103), a CASE statement at least one WHEN, and a handler requires THEN. - SUBTYPE ... RANGE needs both bounds. - LOCK requires TABLE (ORA-00966), in SQL and in PL/SQL. - A datatype nested in a PL/SQL type-length expression, as in VARCHAR2(CAST(1 AS NUMBER(foo))), follows the SQL rules again. Reference rows ref_129 to ref_139 record the engine's answers. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/plsql_block.go | 109 +++++++++++------- oracle/parser/plsql_terminator_test.go | 47 ++++++++ .../testdata/coverage/reference_oracle.tsv | 11 ++ oracle/parser/type.go | 4 + oracle/parser/utility.go | 7 +- 5 files changed, 135 insertions(+), 43 deletions(-) diff --git a/oracle/parser/plsql_block.go b/oracle/parser/plsql_block.go index f40d4fec..72d5189f 100644 --- a/oracle/parser/plsql_block.go +++ b/oracle/parser/plsql_block.go @@ -370,7 +370,7 @@ func (p *Parser) parsePLSQLStatements() (*nodes.List, error) { // Check for terminators switch p.cur.Type { case kwEND, kwEXCEPTION, kwELSIF, kwELSE, kwWHEN, tokEOF: - return stmts, nil + return stmts, p.requirePLSQLStatement(stmts) } stmt, parseErr845 := p.parsePLSQLStatement() @@ -378,7 +378,7 @@ func (p *Parser) parsePLSQLStatements() (*nodes.List, error) { return nil, parseErr845 } if stmt == nil { - return stmts, nil + return stmts, p.requirePLSQLStatement(stmts) } // Every PL/SQL statement ends with ';' (PLS-00103 otherwise). Without // this check the words after a statement that stopped early parsed @@ -394,6 +394,18 @@ func (p *Parser) parsePLSQLStatements() (*nodes.List, error) { nil } +// requirePLSQLStatement reports a syntax error at the current token when +// stmts is empty. A block body, a branch of IF or CASE, a loop body, and an +// exception handler each hold at least one statement; Oracle 23ai rejects +// BEGIN END; and IF c THEN END IF; with PLS-00103 (NULL; is the empty +// statement). +func (p *Parser) requirePLSQLStatement(stmts *nodes.List) error { + if stmts.Len() == 0 { + return p.syntaxErrorAtCur() + } + return nil +} + func (p *Parser) parsePLSQLStatement() (nodes.StmtNode, error) { // One or more <