From e6d65ec8b38d3820a382fb060beaf0d889c0ae54 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 07:37:05 +0000 Subject: [PATCH 01/22] feat(oracle): parse subprogram heading clauses and PL/SQL CHARACTER SET (BYT-10301) Oracle-shipped PL/SQL that ANTLR parsed and omni rejected came down to the clauses of subprogram and package headings. Each rule below was checked on Oracle 23ai: - ACCESSIBLE BY ( [unit_kind] [schema.]name, ... ) on procedures, functions, package specifications, and package and local subprograms; a unit kind word is always the kind, and an accessor takes no dblink. - AGGREGATE USING type and PIPELINED ... USING type: the type implements the function, the clause list ends there, and there is no IS | AS body. Split ends such a function at its ';' like a call spec. - PIPELINED { ROW | TABLE } POLYMORPHIC, PARALLEL_ENABLE ( PARTITION arg BY { ANY | HASH | RANGE | VALUE } ... ), the ORDER and CLUSTER streaming clauses, RESULT_CACHE RELIES_ON, and SQL_MACRO ( [TYPE =>] SCALAR | TABLE ). - Every heading clause at most once (PLS-00371). - CHARACTER SET { name | item%CHARSET } after a PL/SQL datatype. ANY_CS stands only on a subprogram parameter (PLS-00551), a record field or collection element takes neither ANY_CS nor %CHARSET (PLS-00552), and built-in non-character types take none (PLS-00550, PLS-00554). - Parameter and function result types are unconstrained: p VARCHAR2(10) and RETURN NUMBER(5) are PLS-00103, as are constrained cursor parameters and REF CURSOR RETURN types. The dictionary manifest drops from 101 to 34 units, with none newly failing; units ANTLR parsed and omni did not drop from 38 to 9. Reference rows ref_147 to ref_165. Co-Authored-By: Claude Opus 5.5 --- oracle/ast/loc.go | 6 + oracle/ast/outfuncs.go | 84 ++++ oracle/ast/parsenodes.go | 95 +++-- oracle/ast/walk_generated.go | 19 + oracle/parser/compare_test.go | 4 +- oracle/parser/create_proc.go | 238 +++++++---- oracle/parser/create_type.go | 2 +- oracle/parser/parser.go | 7 +- oracle/parser/plsql_block.go | 25 +- oracle/parser/split.go | 15 +- oracle/parser/split_test.go | 16 + oracle/parser/subprogram_clause.go | 379 ++++++++++++++++++ oracle/parser/subprogram_clause_test.go | 195 +++++++++ .../testdata/coverage/loc_node_coverage.tsv | 3 + .../plsql_dictionary_known_failures.tsv | 77 +--- .../testdata/coverage/reference_oracle.tsv | 19 + oracle/parser/type.go | 157 +++++++- 17 files changed, 1121 insertions(+), 220 deletions(-) create mode 100644 oracle/parser/subprogram_clause.go create mode 100644 oracle/parser/subprogram_clause_test.go diff --git a/oracle/ast/loc.go b/oracle/ast/loc.go index 91d4c0a2..37463c2a 100644 --- a/oracle/ast/loc.go +++ b/oracle/ast/loc.go @@ -487,6 +487,12 @@ func NodeLoc(n Node) Loc { return v.Loc case *PLSQLSubtypeDecl: return v.Loc + case *Accessor: + return v.Loc + case *ParallelEnableClause: + return v.Loc + case *StreamingClause: + return v.Loc case *PLSQLAssign: return v.Loc case *PLSQLIf: diff --git a/oracle/ast/outfuncs.go b/oracle/ast/outfuncs.go index 4ace243e..cd751818 100644 --- a/oracle/ast/outfuncs.go +++ b/oracle/ast/outfuncs.go @@ -591,6 +591,12 @@ func writeNode(sb *strings.Builder, node Node) { writePLSQLTypeDecl(sb, n) case *PLSQLSubtypeDecl: writePLSQLSubtypeDecl(sb, n) + case *Accessor: + writeAccessor(sb, n) + case *ParallelEnableClause: + writeParallelEnableClause(sb, n) + case *StreamingClause: + writeStreamingClause(sb, n) case *PLSQLCall: writePLSQLCall(sb, n) @@ -670,6 +676,12 @@ func writeTypeName(sb *strings.Builder, n *TypeName) { sb.WriteString(" :arrayBounds ") writeNode(sb, n.ArrayBounds) } + if n.CharacterSet != "" { + sb.WriteString(fmt.Sprintf(" :characterSet %q", n.CharacterSet)) + } + if n.IsPercCharset { + sb.WriteString(" :pctCharset true") + } sb.WriteString(fmt.Sprintf(" :loc_start %d :loc_end %d", n.Loc.Start, n.Loc.End)) sb.WriteString("}") } @@ -3217,6 +3229,13 @@ func writeCreatePackageStmt(sb *strings.Builder, n *CreatePackageStmt) { sb.WriteString(" :name ") writeNode(sb, n.Name) } + if n.AuthID != "" { + sb.WriteString(fmt.Sprintf(" :authID %q", n.AuthID)) + } + if n.AccessibleBy != nil { + sb.WriteString(" :accessibleBy ") + writeNode(sb, n.AccessibleBy) + } if n.IsBody { sb.WriteString(" :isBody true") } @@ -3256,6 +3275,10 @@ func writeCreateProcedureStmt(sb *strings.Builder, n *CreateProcedureStmt) { sb.WriteString(" :parameters ") writeNode(sb, n.Parameters) } + if n.AccessibleBy != nil { + sb.WriteString(" :accessibleBy ") + writeNode(sb, n.AccessibleBy) + } if n.Wrapped { sb.WriteString(" :wrapped true") } @@ -3309,21 +3332,47 @@ func writeCreateFunctionStmt(sb *strings.Builder, n *CreateFunctionStmt) { if n.Pipelined { sb.WriteString(" :pipelined true") } + if n.Polymorphic != "" { + sb.WriteString(fmt.Sprintf(" :polymorphic %q", n.Polymorphic)) + } if n.Parallel { sb.WriteString(" :parallel true") } + if n.ParallelSpec != nil { + sb.WriteString(" :parallelSpec ") + writeNode(sb, n.ParallelSpec) + } + if n.Streaming != nil { + sb.WriteString(" :streaming ") + writeNode(sb, n.Streaming) + } if n.ResultCache { sb.WriteString(" :resultCache true") } + if n.ReliesOn != nil { + sb.WriteString(" :reliesOn ") + writeNode(sb, n.ReliesOn) + } if n.Aggregate { sb.WriteString(" :aggregate true") } + if n.Implementation != nil { + sb.WriteString(" :implementation ") + writeNode(sb, n.Implementation) + } if n.SqlMacro { sb.WriteString(" :sqlMacro true") } + if n.SqlMacroType != "" { + sb.WriteString(fmt.Sprintf(" :sqlMacroType %q", n.SqlMacroType)) + } if n.AuthID != "" { sb.WriteString(fmt.Sprintf(" :authID %q", n.AuthID)) } + if n.AccessibleBy != nil { + sb.WriteString(" :accessibleBy ") + writeNode(sb, n.AccessibleBy) + } if n.Body != nil { sb.WriteString(" :body ") writeNode(sb, n.Body) @@ -5391,6 +5440,41 @@ func writePLSQLSubtypeDecl(sb *strings.Builder, n *PLSQLSubtypeDecl) { sb.WriteString("}") } +func writeAccessor(sb *strings.Builder, n *Accessor) { + sb.WriteString("{ACCESSOR") + if n.UnitKind != "" { + sb.WriteString(fmt.Sprintf(" :unitKind %q", n.UnitKind)) + } + if n.Name != nil { + sb.WriteString(" :name ") + writeNode(sb, n.Name) + } + sb.WriteString(fmt.Sprintf(" :loc_start %d :loc_end %d", n.Loc.Start, n.Loc.End)) + sb.WriteString("}") +} + +func writeParallelEnableClause(sb *strings.Builder, n *ParallelEnableClause) { + sb.WriteString("{PARALLELENABLE") + sb.WriteString(fmt.Sprintf(" :argument %q :partitionBy %q", n.Argument, n.PartitionBy)) + if n.Columns != nil { + sb.WriteString(" :columns ") + writeNode(sb, n.Columns) + } + sb.WriteString(fmt.Sprintf(" :loc_start %d :loc_end %d", n.Loc.Start, n.Loc.End)) + sb.WriteString("}") +} + +func writeStreamingClause(sb *strings.Builder, n *StreamingClause) { + sb.WriteString("{STREAMING") + sb.WriteString(fmt.Sprintf(" :kind %q :argument %q", n.Kind, n.Argument)) + if n.Columns != nil { + sb.WriteString(" :columns ") + writeNode(sb, n.Columns) + } + 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 167ae710..19922df4 100644 --- a/oracle/ast/parsenodes.go +++ b/oracle/ast/parsenodes.go @@ -369,12 +369,14 @@ func (n *Hint) nodeTag() {} // TypeName represents a data type reference. type TypeName struct { - Names *List // type name parts (list of *String) - TypeMods *List // type modifiers (precision, scale) - IsPercType bool // %TYPE - IsPercRowtype bool // %ROWTYPE - ArrayBounds *List // array bounds if any - Loc Loc // start location + Names *List // type name parts (list of *String) + TypeMods *List // type modifiers (precision, scale) + IsPercType bool // %TYPE + IsPercRowtype bool // %ROWTYPE + ArrayBounds *List // array bounds if any + CharacterSet string // PL/SQL CHARACTER SET name, or the item named by item%CHARSET + IsPercCharset bool // CHARACTER SET item%CHARSET + Loc Loc // start location } func (n *TypeName) nodeTag() {} @@ -1992,6 +1994,8 @@ type CreatePackageStmt struct { NonEditionable bool // NONEDITIONABLE Sharing string // SHARING = METADATA | NONE Name *ObjectName // package name + AuthID string // AUTHID CURRENT_USER | DEFINER + AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) IsBody bool // PACKAGE BODY Body *List // package body declarations Loc Loc // start location @@ -2014,6 +2018,7 @@ type CreateProcedureStmt struct { AuthID string // AUTHID CURRENT_USER | DEFINER Name *ObjectName // procedure name Parameters *List // parameter list (list of *Parameter) + AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) Wrapped bool // WRAPPED procedure body WrappedSource string // raw text from WRAPPED through the wrapped payload Body StmtNode // procedure body (PL/SQL block); nil for a call spec @@ -2056,29 +2061,71 @@ func (n *CallSpec) nodeTag() {} // CreateFunctionStmt represents a CREATE FUNCTION statement. type CreateFunctionStmt struct { - OrReplace bool // OR REPLACE - IfNotExists bool // IF NOT EXISTS - Editionable bool // EDITIONABLE - NonEditionable bool // NONEDITIONABLE - Sharing string // SHARING = METADATA | NONE - Name *ObjectName // function name - Parameters *List // parameter list (list of *Parameter) - ReturnType *TypeName // RETURN type - Deterministic bool // DETERMINISTIC - Pipelined bool // PIPELINED - Parallel bool // PARALLEL_ENABLE - ResultCache bool // RESULT_CACHE - Aggregate bool // AGGREGATE USING - SqlMacro bool // SQL_MACRO - AuthID string // AUTHID CURRENT_USER | DEFINER - Body StmtNode // function body (PL/SQL block); nil for a call spec - CallSpec *CallSpec // call spec implementing the function in Java or C - Loc Loc // start location + OrReplace bool // OR REPLACE + IfNotExists bool // IF NOT EXISTS + Editionable bool // EDITIONABLE + NonEditionable bool // NONEDITIONABLE + Sharing string // SHARING = METADATA | NONE + Name *ObjectName // function name + Parameters *List // parameter list (list of *Parameter) + ReturnType *TypeName // RETURN type + Deterministic bool // DETERMINISTIC + Pipelined bool // PIPELINED + Polymorphic string // PIPELINED ROW or TABLE POLYMORPHIC: ROW or TABLE + Parallel bool // PARALLEL_ENABLE + ParallelSpec *ParallelEnableClause // PARALLEL_ENABLE ( PARTITION ... ), nil without one + Streaming *List // ORDER and CLUSTER streaming clauses (list of *StreamingClause) + ResultCache bool // RESULT_CACHE + ReliesOn *List // RESULT_CACHE RELIES_ON data sources (list of *ObjectName) + Aggregate bool // AGGREGATE USING + Implementation *ObjectName // AGGREGATE or PIPELINED USING implementation type; the function then has no body + SqlMacro bool // SQL_MACRO + SqlMacroType string // SQL_MACRO ( SCALAR | TABLE ), "" without the parenthesized type + AuthID string // AUTHID CURRENT_USER | DEFINER + AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) + Body StmtNode // function body (PL/SQL block); nil for a call spec + CallSpec *CallSpec // call spec implementing the function in Java or C + Loc Loc // start location } func (n *CreateFunctionStmt) nodeTag() {} func (n *CreateFunctionStmt) stmtNode() {} +// Accessor is one entry of an ACCESSIBLE BY clause. +// +// accessor ::= [ FUNCTION | PROCEDURE | PACKAGE | TRIGGER | TYPE ] [ schema. ] unit_name +type Accessor struct { + UnitKind string // FUNCTION, PROCEDURE, PACKAGE, TRIGGER, TYPE, or "" when omitted + Name *ObjectName // [schema.]unit_name + Loc Loc +} + +func (n *Accessor) nodeTag() {} + +// ParallelEnableClause is the partitioning of a PARALLEL_ENABLE function. +// +// ( PARTITION argument BY { ANY | { HASH | RANGE | VALUE } ( expr [, expr ]... ) } ) +type ParallelEnableClause struct { + Argument string // the partitioned cursor parameter + PartitionBy string // ANY, HASH, RANGE, or VALUE + Columns *List // HASH, RANGE, or VALUE expressions + Loc Loc +} + +func (n *ParallelEnableClause) nodeTag() {} + +// StreamingClause orders or clusters the rows a table function receives. +// +// { ORDER | CLUSTER } argument BY ( expr [, expr ]... ) +type StreamingClause struct { + Kind string // ORDER or CLUSTER + Argument string // the cursor parameter + Columns *List // the ordering or clustering expressions + Loc Loc +} + +func (n *StreamingClause) nodeTag() {} + // Parameter represents a procedure/function parameter. type Parameter struct { Name string // parameter name diff --git a/oracle/ast/walk_generated.go b/oracle/ast/walk_generated.go index 459d5b08..4a775301 100644 --- a/oracle/ast/walk_generated.go +++ b/oracle/ast/walk_generated.go @@ -6,6 +6,10 @@ package ast // for each child. This function is generated from parsenodes.go. func walkChildren(v Visitor, node Node) { switch n := node.(type) { + case *Accessor: + if n.Name != nil { + Walk(v, n.Name) + } case *AdminDDLStmt: if n.Name != nil { Walk(v, n.Name) @@ -503,6 +507,15 @@ func walkChildren(v Visitor, node Node) { if n.ReturnType != nil { Walk(v, n.ReturnType) } + if n.ParallelSpec != nil { + Walk(v, n.ParallelSpec) + } + walkList(v, n.Streaming) + walkList(v, n.ReliesOn) + if n.Implementation != nil { + Walk(v, n.Implementation) + } + walkList(v, n.AccessibleBy) Walk(v, n.Body) if n.CallSpec != nil { Walk(v, n.CallSpec) @@ -594,12 +607,14 @@ func walkChildren(v Visitor, node Node) { if n.Name != nil { Walk(v, n.Name) } + walkList(v, n.AccessibleBy) walkList(v, n.Body) case *CreateProcedureStmt: if n.Name != nil { Walk(v, n.Name) } walkList(v, n.Parameters) + walkList(v, n.AccessibleBy) Walk(v, n.Body) if n.CallSpec != nil { Walk(v, n.CallSpec) @@ -1229,6 +1244,8 @@ func walkChildren(v Visitor, node Node) { for _, item := range n.Stmts { Walk(v, item) } + case *ParallelEnableClause: + walkList(v, n.Columns) case *Parameter: if n.TypeName != nil { Walk(v, n.TypeName) @@ -1355,6 +1372,8 @@ func walkChildren(v Visitor, node Node) { } case *SortBy: Walk(v, n.Expr) + case *StreamingClause: + walkList(v, n.Columns) case *SubqueryExpr: Walk(v, n.Subquery) case *SubqueryRef: diff --git a/oracle/parser/compare_test.go b/oracle/parser/compare_test.go index bf4c1d13..153e6404 100644 --- a/oracle/parser/compare_test.go +++ b/oracle/parser/compare_test.go @@ -7703,7 +7703,9 @@ func TestParseBatch101_PLSQLObjects(t *testing.T) { // CREATE FUNCTION - EDITIONABLE with SHARING "CREATE OR REPLACE EDITIONABLE FUNCTION calc RETURN NUMBER SHARING = METADATA DETERMINISTIC IS BEGIN RETURN 1; END;", // CREATE FUNCTION - AGGREGATE USING - "CREATE FUNCTION agg_fn RETURN NUMBER AGGREGATE USING agg_impl IS BEGIN RETURN 0; END;", + // AGGREGATE USING ends the function: Oracle 23ai rejects an IS body + // after it (PLS-00103). + "CREATE FUNCTION agg_fn (x NUMBER) RETURN NUMBER AGGREGATE USING agg_impl;", // CREATE PACKAGE - IF NOT EXISTS with SHARING "CREATE IF NOT EXISTS PACKAGE my_pkg SHARING = METADATA IS\n PROCEDURE do_it;\nEND my_pkg;", // CREATE PACKAGE - EDITIONABLE with AUTHID diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index de39ed63..a20bf7e8 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -62,10 +62,8 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi } } - var parseErr456 error - stmt.AuthID, parseErr456 = p.parseOptionalAuthID() - if parseErr456 != nil { - return nil, parseErr456 + if err := p.parseProcedureProperties(stmt); err != nil { + return nil, err } if p.isIdentLikeStr("WRAPPED") { @@ -78,6 +76,7 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi p.advance() // PL/SQL block body (BEGIN ... END) or call spec + var parseErr456 error stmt.Body, stmt.CallSpec, parseErr456 = p.parseSubprogramImplementation() if parseErr456 != nil { return nil, parseErr456 @@ -191,7 +190,7 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit } p.advance() var parseErr459 error - stmt.ReturnType, parseErr459 = p.parseTypeName() + stmt.ReturnType, parseErr459 = p.parsePLSQLDatatype(typeModsNone, charsetFlexible) if parseErr459 != nil { return nil, parseErr459 } @@ -219,6 +218,9 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit nil { return nil, parseErr460 } + if stmt.Implementation != nil { + return p.finishTypeImplementedFunction(stmt) + } if p.cur.Type != kwIS && p.cur.Type != kwAS { return nil, p.syntaxErrorAtCur() @@ -236,92 +238,172 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit return stmt, nil } -// parseFunctionProperties parses optional DETERMINISTIC, PIPELINED, PARALLEL_ENABLE, RESULT_CACHE, -// AGGREGATE USING, SQL_MACRO, AUTHID, and other function property clauses. +// parseFunctionProperties parses the clauses between a function's RETURN +// type and its IS | AS or ';', in any order, each at most once (PLS-00371). +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/CREATE-FUNCTION-statement.html +// +// [ { invoker_rights_clause | accessible_by_clause | default_collation_clause +// | deterministic_clause | parallel_enable_clause | result_cache_clause +// | aggregate_clause | pipelined_clause | sql_macro_clause +// | streaming_clause }... ] +// pipelined_clause ::= PIPELINED [ { ROW | TABLE } POLYMORPHIC ] +// result_cache_clause ::= RESULT_CACHE [ RELIES_ON ( [ data_source [, data_source ]... ] ) ] +// sql_macro_clause ::= SQL_MACRO [ ( [ TYPE => ] { SCALAR | TABLE } ) ] +// aggregate_clause ::= AGGREGATE USING [ schema. ] implementation_type +// +// A type can implement the function: AGGREGATE USING type, or USING type +// after PIPELINED. The clause list ends there and the function has no IS | AS +// body; Oracle 23ai rejects a following IS or clause (PLS-00103), USING +// without PIPELINED (PLS-00624), and AGGREGATE without USING after it. func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt) error { + seen := clauseSeen{} for { switch { case p.cur.Type == kwDETERMINISTIC: + if err := p.firstClause(seen, "DETERMINISTIC"); err != nil { + return err + } stmt.Deterministic = true p.advance() case p.cur.Type == kwPIPELINED: + if err := p.firstClause(seen, "PIPELINED"); err != nil { + return err + } stmt.Pipelined = true p.advance() + if (p.cur.Type == kwROW || p.cur.Type == kwTABLE) && p.isIdentLikeStrAt(p.peekNext(), "POLYMORPHIC") { + stmt.Polymorphic = "ROW" + if p.cur.Type == kwTABLE { + stmt.Polymorphic = "TABLE" + } + p.advance() // consume ROW or TABLE + p.advance() // consume POLYMORPHIC + } case p.cur.Type == kwPARALLEL_ENABLE: + if err := p.firstClause(seen, "PARALLEL_ENABLE"); err != nil { + return err + } stmt.Parallel = true p.advance() + if p.cur.Type == '(' { + spec, err := p.parseParallelEnableSpec() + if err != nil { + return err + } + stmt.ParallelSpec = spec + } + case p.cur.Type == kwORDER, p.cur.Type == kwCLUSTER: + kind := "ORDER BY" + if p.cur.Type == kwCLUSTER { + kind = "CLUSTER BY" + } + if err := p.firstClause(seen, kind); err != nil { + return err + } + clause, err := p.parseStreamingClause() + if err != nil { + return err + } + if stmt.Streaming == nil { + stmt.Streaming = &nodes.List{} + } + stmt.Streaming.Items = append(stmt.Streaming.Items, clause) case p.cur.Type == kwRESULT_CACHE: + if err := p.firstClause(seen, "RESULT_CACHE"); err != nil { + return err + } stmt.ResultCache = true p.advance() - case p.isIdentLikeStr("AGGREGATE"): + if p.isKeywordStr("RELIES_ON") { + list, err := p.parseResultCacheReliesOn() + if err != nil { + return err + } + stmt.ReliesOn = list + } + case p.isKeywordStr("AGGREGATE"): stmt.Aggregate = true p.advance() // consume AGGREGATE - if p.cur.Type == kwUSING { - p.advance() - parseDiscard463, // consume USING - parseErr462 := p.parseObjectName() - _ = // consume implementation type - parseDiscard463 - if parseErr462 != nil { - return parseErr462 - } + if p.cur.Type != kwUSING { + return p.syntaxErrorAtCur() + } + return p.parseImplementationType(stmt) + case p.cur.Type == kwUSING: + if !stmt.Pipelined { + return p.syntaxErrorAtCur() + } + return p.parseImplementationType(stmt) + case p.isKeywordStr("SQL_MACRO"): + if err := p.firstClause(seen, "SQL_MACRO"); err != nil { + return err } - case p.isIdentLikeStr("SQL_MACRO"): stmt.SqlMacro = true p.advance() // consume SQL_MACRO - // Optional ( SCALAR | TABLE ) if p.cur.Type == '(' { - p.advance() - if p.isIdentLike() { - p.advance() // consume SCALAR or TABLE - } - if p.cur.Type == ')' { - p.advance() + kind, err := p.parseSqlMacroType() + if err != nil { + return err } + stmt.SqlMacroType = kind } case p.isIdentLikeStr("AUTHID"): - authID, parseErr464 := p.parseOptionalAuthID() - if parseErr464 != nil { - return parseErr464 + if err := p.firstClause(seen, "AUTHID"); err != nil { + return err + } + authID, err := p.parseOptionalAuthID() + if err != nil { + return err } stmt.AuthID = authID case p.isIdentLikeStr("ACCESSIBLE"): - // accessible_by_clause: ACCESSIBLE BY ( accessor [, ...] ) - p.advance() // consume ACCESSIBLE - if p.cur.Type == kwBY { - p.advance() // consume BY + if err := p.firstClause(seen, "ACCESSIBLE BY"); err != nil { + return err } - if p.cur.Type == '(' { - p.advance() - for p.cur.Type != ')' && p.cur.Type != tokEOF { - parseDiscard465, parseErr464 := p.parseObjectName() - _ = parseDiscard465 - if parseErr464 != nil { - return parseErr464 - } - if p.cur.Type != ',' { - break - } - p.advance() - } - if p.cur.Type == ')' { - p.advance() - } + list, err := p.parseAccessibleByClause() + if err != nil { + return err } - case p.cur.Type == kwDEFAULT && p.isIdentLikeStrAt(p.peekNext(), "COLLATION"): - // default_collation_clause: DEFAULT COLLATION collation_name - p.advance() // consume DEFAULT - p.advance() // consume COLLATION - if p.isIdentLike() { - p.advance() // consume collation name + stmt.AccessibleBy = list + case p.atDefaultCollation(): + if err := p.firstClause(seen, "DEFAULT COLLATION"); err != nil { + return err + } + if err := p.parseDefaultCollationClause(); err != nil { + return err } default: return nil } } +} + +// parseImplementationType parses USING [ schema. ] implementation_type +// [ @dblink ], the current token at USING. +func (p *Parser) parseImplementationType(stmt *nodes.CreateFunctionStmt) error { + p.advance() // consume USING + name, err := p.parseObjectName() + if err != nil { + return err + } + if name == nil || name.Name == "" { + return p.syntaxErrorAtCur() + } + stmt.Implementation = name return nil } +// finishTypeImplementedFunction ends a function a type implements, whose +// clause list ends at the type name: only its ';' follows. +func (p *Parser) finishTypeImplementedFunction(stmt *nodes.CreateFunctionStmt) (*nodes.CreateFunctionStmt, error) { + if p.cur.Type != ';' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + stmt.Loc.End = p.prev.End + return stmt, nil +} + func (p *Parser) parseOptionalAuthID() (string, error) { if !p.isIdentLikeStr("AUTHID") { return "", nil @@ -406,38 +488,11 @@ func (p *Parser) parseCreatePackageStmt(start int, orReplace, ifNotExists, editi } } - // Optional ACCESSIBLE BY ( ... ) — skip for package body - if !stmt.IsBody && p.isIdentLikeStr("ACCESSIBLE") { - p.advance() // consume ACCESSIBLE - if p.cur.Type == kwBY { - p.advance() // consume BY - } - if p.cur.Type == '(' { - p.advance() - for p.cur.Type != ')' && p.cur.Type != tokEOF { - parseDiscard468, - // Each accessor: [ unit_kind ] [ schema. ] unit_name - parseErr467 := p.parseObjectName() - _ = parseDiscard468 - if parseErr467 != nil { - return nil, parseErr467 - } - if p.cur.Type != ',' { - break - } - p.advance() - } - if p.cur.Type == ')' { - p.advance() - } - } - } - - // Optional invoker_rights_clause: AUTHID { CURRENT_USER | DEFINER } - if !stmt.IsBody && p.isIdentLikeStr("AUTHID") { - p.advance() // consume AUTHID - if p.isIdentLike() { - p.advance() // consume CURRENT_USER or DEFINER + // ACCESSIBLE BY, AUTHID, and DEFAULT COLLATION head a specification, + // in any order; a body takes none of them. + if !stmt.IsBody { + if err := p.parsePackageProperties(stmt); err != nil { + return nil, err } } @@ -555,6 +610,9 @@ func (p *Parser) parsePackageProcDecl() (*nodes.CreateProcedureStmt, error) { return nil, parseErr474 } } + if err := p.parseProcedureProperties(stmt); err != nil { + return nil, err + } if p.cur.Type == kwIS || p.cur.Type == kwAS { p.advance() @@ -608,7 +666,7 @@ func (p *Parser) parsePackageFuncDecl() (*nodes.CreateFunctionStmt, error) { if p.cur.Type == kwRETURN { p.advance() var parseErr478 error - stmt.ReturnType, parseErr478 = p.parseTypeName() + stmt.ReturnType, parseErr478 = p.parsePLSQLDatatype(typeModsNone, charsetFlexible) if parseErr478 != // Optional function properties @@ -628,6 +686,9 @@ func (p *Parser) parsePackageFuncDecl() (*nodes.CreateFunctionStmt, error) { nil { return nil, parseErr479 } + if stmt.Implementation != nil { + return p.finishTypeImplementedFunction(stmt) + } if p.cur.Type == kwIS || p.cur.Type == kwAS { p.advance() @@ -703,7 +764,8 @@ func (p *Parser) parseParameter() (*nodes.Parameter, error) { param.Mode = mode var parseErr484 error - param.TypeName, parseErr484 = p.parseTypeName() + // A parameter type is unconstrained and may name ANY_CS. + param.TypeName, parseErr484 = p.parsePLSQLDatatype(typeModsNone, charsetAnyCS) if parseErr484 != // Optional default value: := expr or DEFAULT expr diff --git a/oracle/parser/create_type.go b/oracle/parser/create_type.go index c2fa3942..efa8ca66 100644 --- a/oracle/parser/create_type.go +++ b/oracle/parser/create_type.go @@ -450,7 +450,7 @@ func (p *Parser) parseTypeBodyFunction(isConstructor, inSpec bool) (*nodes.Creat } } else { var parseErr587 error - stmt.ReturnType, parseErr587 = p.parseTypeName() + stmt.ReturnType, parseErr587 = p.parsePLSQLDatatype(typeModsNone, charsetFlexible) if parseErr587 != nil { return nil, parseErr587 } diff --git a/oracle/parser/parser.go b/oracle/parser/parser.go index 2bee07e4..7ef71c32 100644 --- a/oracle/parser/parser.go +++ b/oracle/parser/parser.go @@ -28,10 +28,9 @@ type Parser struct { // 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 + // typeMods says what the parenthesized modifiers of the datatype being + // parsed may hold; parsePLSQLDatatype sets it for PL/SQL datatypes. + typeMods typeModMode } // Parse parses a SQL string into an AST list. diff --git a/oracle/parser/plsql_block.go b/oracle/parser/plsql_block.go index 72d5189f..001dcc09 100644 --- a/oracle/parser/plsql_block.go +++ b/oracle/parser/plsql_block.go @@ -160,14 +160,15 @@ func (p *Parser) parsePLSQLDeclaration() (nodes.Node, error) { // Variable declaration: name [CONSTANT] type [NOT NULL] [:= | DEFAULT expr] ; if p.isIdentLike() { - return p.parsePLSQLVarDecl() + return p.parsePLSQLVarDecl(charsetFlexible) } return nil, nil } -// parsePLSQLVarDecl parses a variable declaration. -func (p *Parser) parsePLSQLVarDecl() (*nodes.PLSQLVarDecl, error) { +// parsePLSQLVarDecl parses a variable declaration, or a record field when +// charset is charsetFixed. +func (p *Parser) parsePLSQLVarDecl(charset plsqlCharsetUse) (*nodes.PLSQLVarDecl, error) { start := p.pos() decl := &nodes.PLSQLVarDecl{ Loc: nodes.Loc{Start: start}, @@ -189,7 +190,7 @@ func (p *Parser) parsePLSQLVarDecl() (*nodes.PLSQLVarDecl, error) { var parseErr835 error // Type name - decl.TypeName, parseErr835 = p.parsePLSQLTypeName() + decl.TypeName, parseErr835 = p.parsePLSQLDatatype(typeModsPLSQL, charset) if parseErr835 != // Optional NOT NULL @@ -323,7 +324,7 @@ func (p *Parser) parsePLSQLCursorParams() (*nodes.List, error) { p.advance() } var parseErr842 error - paramDecl.TypeName, parseErr842 = p.parseTypeName() + paramDecl.TypeName, parseErr842 = p.parsePLSQLDatatype(typeModsNone, charsetFlexible) if parseErr842 != // Optional default @@ -1786,8 +1787,8 @@ func plsqlStmtNodes(list *nodes.List) []nodes.StmtNode { // 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). +// Oracle 23ai rejects SUBTYPE s IS NUMBER 8, 2 (PLS-00103). The character set +// may be named or item%CHARSET, but not ANY_CS (PLS-00551). func (p *Parser) parsePLSQLSubtypeDecl() (*nodes.PLSQLSubtypeDecl, error) { decl := &nodes.PLSQLSubtypeDecl{Loc: nodes.Loc{Start: p.pos()}} p.advance() // consume SUBTYPE @@ -1805,7 +1806,7 @@ func (p *Parser) parsePLSQLSubtypeDecl() (*nodes.PLSQLSubtypeDecl, error) { } p.advance() - decl.BaseType, err = p.parsePLSQLTypeName() + decl.BaseType, err = p.parsePLSQLDatatype(typeModsPLSQL, charsetFlexible) if err != nil { return nil, err } @@ -1883,7 +1884,7 @@ func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { p.advance() } var parseErr894 error - decl.ElementType, parseErr894 = p.parsePLSQLTypeName() + decl.ElementType, parseErr894 = p.parsePLSQLDatatype(typeModsPLSQL, charsetFixed) if parseErr894 != // INDEX BY nil { @@ -1929,7 +1930,7 @@ func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { p.advance() } var parseErr897 error - decl.ElementType, parseErr897 = p.parsePLSQLTypeName() + decl.ElementType, parseErr897 = p.parsePLSQLDatatype(typeModsPLSQL, charsetFixed) if parseErr897 != nil { return nil, parseErr897 } @@ -1946,7 +1947,7 @@ func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { p.advance() decl.Fields = &nodes.List{} for p.cur.Type != ')' && p.cur.Type != tokEOF { - field, parseErr898 := p.parsePLSQLVarDecl() + field, parseErr898 := p.parsePLSQLVarDecl(charsetFixed) if parseErr898 != nil { return nil, parseErr898 } @@ -1974,7 +1975,7 @@ func (p *Parser) parsePLSQLTypeDecl() (*nodes.PLSQLTypeDecl, error) { if p.cur.Type == kwRETURN { p.advance() var parseErr899 error - decl.ReturnType, parseErr899 = p.parseTypeName() + decl.ReturnType, parseErr899 = p.parsePLSQLDatatype(typeModsNone, charsetNone) if parseErr899 != nil { return nil, parseErr899 } diff --git a/oracle/parser/split.go b/oracle/parser/split.go index ab4d88d4..7a28e678 100644 --- a/oracle/parser/split.go +++ b/oracle/parser/split.go @@ -459,9 +459,11 @@ func (s *splitState) observePLSQL(tok Token) { // observeSubprogramHead follows a stored unit or nested subprogram up to its // implementation. A call spec right after IS|AS replaces BEGIN ... END and -// ends at its ';'. WRAPPED replaces IS|AS in a wrapped stored unit. Both are -// recognized only outside the parameter list, so a parameter named LANGUAGE, -// EXTERNAL, or WRAPPED does not end the unit early. Like Oracle and the +// ends at its ';'. WRAPPED replaces IS|AS in a wrapped stored unit, and USING +// an implementation type replaces it in a function a type implements. All +// are recognized only outside the parameter list, so a parameter named +// LANGUAGE, EXTERNAL, or WRAPPED, or a default using USING, does not end the +// unit early. Like Oracle and the // parser, the splitter takes the word after IS|AS alone for a call spec, so // a malformed one still ends at its ';'. func (s *splitState) observeSubprogramHead(top *splitPLSQLFrame, tok Token) { @@ -492,6 +494,13 @@ func (s *splitState) observeSubprogramHead(top *splitPLSQLFrame, tok Token) { top.afterIsAs = true case tok.Type == tokIDENT && tok.Str == "WRAPPED" && len(s.frames) == 1: s.callSpecStarted = true + case tok.Type == kwUSING && len(s.frames) == 1: + // AGGREGATE USING type or PIPELINED ... USING type: a type + // implements the function, which has no IS|AS body and, like a + // call spec, ends at its ';'. SQL*Plus buffers either up to a "/" + // line; the splitter ends both where the parser does. + top.callSpec = true + s.callSpecStarted = true } } } diff --git a/oracle/parser/split_test.go b/oracle/parser/split_test.go index 536326bb..832d74ff 100644 --- a/oracle/parser/split_test.go +++ b/oracle/parser/split_test.go @@ -419,6 +419,22 @@ func TestSplitPLSQLBlocks(t *testing.T) { "\nSELECT 1 FROM dual", }, }, + { + // A type implements the function, which has no body: like a call + // spec, it ends at its ';', which it keeps. + name: "functions implemented by a type", + sql: "CREATE OR REPLACE FUNCTION agg (x NUMBER) RETURN NUMBER AGGREGATE USING agg_impl;\n" + + "CREATE FUNCTION pip RETURN t PIPELINED PARALLEL_ENABLE USING s.pip_impl;\n" + + "/\n" + + "CREATE FUNCTION f (u NUMBER DEFAULT 1) RETURN NUMBER IS BEGIN RETURN u; END;\n" + + "SELECT 1 FROM dual;", + want: []string{ + "CREATE OR REPLACE FUNCTION agg (x NUMBER) RETURN NUMBER AGGREGATE USING agg_impl;", + "\nCREATE FUNCTION pip RETURN t PIPELINED PARALLEL_ENABLE USING s.pip_impl;", + "\nCREATE FUNCTION f (u NUMBER DEFAULT 1) RETURN NUMBER IS BEGIN RETURN u; END;", + "\nSELECT 1 FROM dual", + }, + }, { name: "package with call spec members", sql: "CREATE OR REPLACE PACKAGE BODY pk AS\n" + diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go new file mode 100644 index 00000000..31930acc --- /dev/null +++ b/oracle/parser/subprogram_clause.go @@ -0,0 +1,379 @@ +package parser + +import ( + nodes "github.com/bytebase/omni/oracle/ast" +) + +// Clauses of subprogram and package headings. +// +// Each clause stands at most once in a heading: Oracle 23ai rejects a second +// AUTHID, ACCESSIBLE BY, DETERMINISTIC, PIPELINED, PARALLEL_ENABLE, +// RESULT_CACHE, SQL_MACRO, ORDER BY, or CLUSTER BY with PLS-00371. + +// clauseSeen records the clauses a heading has used, for the PLS-00371 check. +type clauseSeen map[string]bool + +// firstClause reports a syntax error at the current token, which starts clause, +// when the heading already has that clause. +func (p *Parser) firstClause(seen clauseSeen, clause string) error { + if seen[clause] { + return p.syntaxErrorAtCur() + } + seen[clause] = true + return nil +} + +// parseAccessibleByClause parses ACCESSIBLE BY ( accessor [, accessor ]... ), +// the current token at ACCESSIBLE. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/ACCESSIBLE-BY-clause.html +// +// accessor ::= [ unit_kind ] [ schema. ] unit_name +// unit_kind ::= { FUNCTION | PROCEDURE | PACKAGE | TRIGGER | TYPE } +// +// Oracle 23ai reads an unquoted unit kind word as the kind, so ACCESSIBLE BY +// (PACKAGE) is PLS-00103, as are an empty list, a missing parenthesis, and an +// accessor with a database link. +func (p *Parser) parseAccessibleByClause() (*nodes.List, error) { + p.advance() // consume ACCESSIBLE + if p.cur.Type != kwBY { + return nil, p.syntaxErrorAtCur() + } + p.advance() + if p.cur.Type != '(' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + list := &nodes.List{} + for { + acc := &nodes.Accessor{Loc: nodes.Loc{Start: p.pos()}} + switch p.cur.Type { + case kwFUNCTION: + acc.UnitKind = "FUNCTION" + case kwPROCEDURE: + acc.UnitKind = "PROCEDURE" + case kwPACKAGE: + acc.UnitKind = "PACKAGE" + case kwTRIGGER: + acc.UnitKind = "TRIGGER" + case kwTYPE: + acc.UnitKind = "TYPE" + } + if acc.UnitKind != "" { + p.advance() + } + name, err := p.parseUnitName() + if err != nil { + return nil, err + } + acc.Name = name + acc.Loc.End = p.prev.End + list.Items = append(list.Items, acc) + if p.cur.Type != ',' { + break + } + p.advance() + } + if p.cur.Type != ')' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + return list, nil +} + +// parseUnitName parses [ schema. ] name, without a database link. +func (p *Parser) parseUnitName() (*nodes.ObjectName, error) { + start := p.pos() + if !p.isIdentLike() { + return nil, p.syntaxErrorAtCur() + } + first, err := p.parseIdentifier() + if err != nil { + return nil, err + } + name := &nodes.ObjectName{Name: first, Loc: nodes.Loc{Start: start}} + if p.cur.Type == '.' { + p.advance() + if !p.isIdentLike() { + return nil, p.syntaxErrorAtCur() + } + second, err := p.parseIdentifier() + if err != nil { + return nil, err + } + name.Schema, name.Name = first, second + } + name.Loc.End = p.prev.End + return name, nil +} + +// parseDefaultCollationClause parses DEFAULT COLLATION collation_option, the +// current token at DEFAULT. +func (p *Parser) parseDefaultCollationClause() error { + p.advance() // consume DEFAULT + p.advance() // consume COLLATION + if !p.isIdentLike() { + return p.syntaxErrorAtCur() + } + p.advance() // consume the collation option (USING_NLS_COMP) + return nil +} + +// atDefaultCollation reports whether the current tokens are DEFAULT COLLATION. +func (p *Parser) atDefaultCollation() bool { + return p.cur.Type == kwDEFAULT && p.isIdentLikeStrAt(p.peekNext(), "COLLATION") +} + +// parseProcedureProperties parses the clauses between a procedure's +// parameters and its IS | AS or ';', in any order. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/CREATE-PROCEDURE-statement.html +// +// [ { invoker_rights_clause | accessible_by_clause | default_collation_clause }... ] +func (p *Parser) parseProcedureProperties(stmt *nodes.CreateProcedureStmt) error { + seen := clauseSeen{} + for { + switch { + case p.isIdentLikeStr("AUTHID"): + if err := p.firstClause(seen, "AUTHID"); err != nil { + return err + } + authID, err := p.parseOptionalAuthID() + if err != nil { + return err + } + stmt.AuthID = authID + case p.isIdentLikeStr("ACCESSIBLE"): + if err := p.firstClause(seen, "ACCESSIBLE BY"); err != nil { + return err + } + list, err := p.parseAccessibleByClause() + if err != nil { + return err + } + stmt.AccessibleBy = list + case p.atDefaultCollation(): + if err := p.firstClause(seen, "DEFAULT COLLATION"); err != nil { + return err + } + if err := p.parseDefaultCollationClause(); err != nil { + return err + } + default: + return nil + } + } +} + +// parsePackageProperties parses the clauses between a package +// specification's name and its IS | AS, in any order. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/CREATE-PACKAGE-statement.html +// +// [ { default_collation_clause | invoker_rights_clause | accessible_by_clause }... ] +func (p *Parser) parsePackageProperties(stmt *nodes.CreatePackageStmt) error { + seen := clauseSeen{} + for { + switch { + case p.isIdentLikeStr("AUTHID"): + if err := p.firstClause(seen, "AUTHID"); err != nil { + return err + } + authID, err := p.parseOptionalAuthID() + if err != nil { + return err + } + stmt.AuthID = authID + case p.isIdentLikeStr("ACCESSIBLE"): + if err := p.firstClause(seen, "ACCESSIBLE BY"); err != nil { + return err + } + list, err := p.parseAccessibleByClause() + if err != nil { + return err + } + stmt.AccessibleBy = list + case p.atDefaultCollation(): + if err := p.firstClause(seen, "DEFAULT COLLATION"); err != nil { + return err + } + if err := p.parseDefaultCollationClause(); err != nil { + return err + } + default: + return nil + } + } +} + +// parseParallelEnableSpec parses the partitioning of PARALLEL_ENABLE, the +// current token at '('. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/PARALLEL_ENABLE-clause.html +// +// ( PARTITION argument BY { ANY | { HASH | RANGE | VALUE } ( expr [, expr ]... ) } ) +// +// Oracle 23ai rejects an empty partitioning, PARTITION without BY, other +// methods (LIST), and an empty column list with PLS-00103. That VALUE takes a +// single column is checked when the unit compiles. +func (p *Parser) parseParallelEnableSpec() (*nodes.ParallelEnableClause, error) { + spec := &nodes.ParallelEnableClause{Loc: nodes.Loc{Start: p.pos()}} + p.advance() // consume '(' + if p.cur.Type != kwPARTITION { + return nil, p.syntaxErrorAtCur() + } + p.advance() + if !p.isIdentLike() { + return nil, p.syntaxErrorAtCur() + } + arg, err := p.parseIdentifier() + if err != nil { + return nil, err + } + spec.Argument = arg + if p.cur.Type != kwBY { + return nil, p.syntaxErrorAtCur() + } + p.advance() + switch { + case p.cur.Type == kwANY: + spec.PartitionBy = "ANY" + p.advance() + case p.cur.Type == kwHASH, p.cur.Type == kwRANGE, p.isKeywordStr("VALUE"): + spec.PartitionBy = p.cur.Str + if p.cur.Type == kwHASH { + spec.PartitionBy = "HASH" + } else if p.cur.Type == kwRANGE { + spec.PartitionBy = "RANGE" + } + p.advance() + cols, err := p.parseParenExprList() + if err != nil { + return nil, err + } + spec.Columns = cols + default: + return nil, p.syntaxErrorAtCur() + } + if p.cur.Type != ')' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + spec.Loc.End = p.prev.End + return spec, nil +} + +// parseStreamingClause parses { ORDER | CLUSTER } argument BY ( expr [, expr ]... ), +// the current token at ORDER or CLUSTER. Oracle 23ai takes it anywhere in a +// function's clause list, with or without PARALLEL_ENABLE, and requires a +// plain parameter name (ORDER c.x BY is PLS-00103). +func (p *Parser) parseStreamingClause() (*nodes.StreamingClause, error) { + clause := &nodes.StreamingClause{Kind: "ORDER", Loc: nodes.Loc{Start: p.pos()}} + if p.cur.Type == kwCLUSTER { + clause.Kind = "CLUSTER" + } + p.advance() + if !p.isIdentLike() { + return nil, p.syntaxErrorAtCur() + } + arg, err := p.parseIdentifier() + if err != nil { + return nil, err + } + clause.Argument = arg + if p.cur.Type != kwBY { + return nil, p.syntaxErrorAtCur() + } + p.advance() + cols, err := p.parseParenExprList() + if err != nil { + return nil, err + } + clause.Columns = cols + clause.Loc.End = p.prev.End + return clause, nil +} + +// parseParenExprList parses ( expr [, expr ]... ) with at least one +// expression, the current token at '('. +func (p *Parser) parseParenExprList() (*nodes.List, error) { + if p.cur.Type != '(' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + list := &nodes.List{} + for { + expr, err := p.parseExpr() + if err != nil { + return nil, err + } + if expr == nil { + return nil, p.syntaxErrorAtCur() + } + list.Items = append(list.Items, expr) + if p.cur.Type != ',' { + break + } + p.advance() + } + if p.cur.Type != ')' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + return list, nil +} + +// parseResultCacheReliesOn parses RELIES_ON ( [ data_source [, data_source ]... ] ), +// the current token at RELIES_ON. The list may be empty on Oracle 23ai. +func (p *Parser) parseResultCacheReliesOn() (*nodes.List, error) { + p.advance() // consume RELIES_ON + if p.cur.Type != '(' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + list := &nodes.List{} + for p.cur.Type != ')' { + name, err := p.parseObjectName() + if err != nil { + return nil, err + } + if name == nil || name.Name == "" { + return nil, p.syntaxErrorAtCur() + } + list.Items = append(list.Items, name) + if p.cur.Type != ',' { + break + } + p.advance() + } + if p.cur.Type != ')' { + return nil, p.syntaxErrorAtCur() + } + p.advance() + return list, nil +} + +// parseSqlMacroType parses ( [ TYPE => ] { SCALAR | TABLE } ) after SQL_MACRO, +// the current token at '('. +func (p *Parser) parseSqlMacroType() (string, error) { + p.advance() // consume '(' + if p.cur.Type == kwTYPE && p.peekNext().Type == tokASSOC { + p.advance() // consume TYPE + p.advance() // consume => + } + var kind string + switch { + case p.cur.Type == kwTABLE: + kind = "TABLE" + case p.isKeywordStr("SCALAR"): + kind = "SCALAR" + default: + return "", p.syntaxErrorAtCur() + } + p.advance() + if p.cur.Type != ')' { + return "", p.syntaxErrorAtCur() + } + p.advance() + return kind, nil +} diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go new file mode 100644 index 00000000..ad303aec --- /dev/null +++ b/oracle/parser/subprogram_clause_test.go @@ -0,0 +1,195 @@ +package parser + +import ( + "testing" + + "github.com/bytebase/omni/oracle/ast" +) + +// TestSubprogramHeadingClauses checks the clauses of procedure, function, +// and package headings against Oracle 23ai: every accepted form compiles +// there (or fails only on names it cannot resolve), and every rejected one +// fails with the error noted beside it. Rows ref_147 to ref_165 run the +// PLS-00103 cases against the engine; the others are errors Oracle reports +// from the text alone after parsing, which the reference harness counts as +// accepted. +func TestSubprogramHeadingClauses(t *testing.T) { + for _, sql := range []string{ + // ACCESSIBLE BY with every unit kind, in any order with AUTHID. + "CREATE PROCEDURE p ACCESSIBLE BY (PACKAGE x, PROCEDURE y, FUNCTION z, TRIGGER t, TYPE ty, plain, s.q) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a NUMBER) AUTHID DEFINER ACCESSIBLE BY (x) IS BEGIN NULL; END;", + "CREATE FUNCTION f RETURN NUMBER ACCESSIBLE BY (x) DETERMINISTIC IS BEGIN RETURN 1; END;", + "CREATE PACKAGE k AUTHID DEFINER ACCESSIBLE BY (PACKAGE x) AS PROCEDURE p; END;", + "CREATE PACKAGE k ACCESSIBLE BY (x) AUTHID CURRENT_USER AS PROCEDURE p ACCESSIBLE BY (PACKAGE x); FUNCTION f RETURN NUMBER ACCESSIBLE BY (y); END;", + "CREATE PACKAGE BODY k AS PROCEDURE p ACCESSIBLE BY (PACKAGE x) IS BEGIN NULL; END; END;", + "CREATE PROCEDURE p ACCESSIBLE BY (PACKAGE \"X\", s.x) IS BEGIN NULL; END;", + // A type implements the function, which then has no body. + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl;", + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER DETERMINISTIC AGGREGATE USING s.impl;", + "CREATE FUNCTION f RETURN t PIPELINED USING impl;", + "CREATE FUNCTION f RETURN t PIPELINED PARALLEL_ENABLE USING impl@lnk;", + "CREATE FUNCTION f RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) USING impl;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED TABLE POLYMORPHIC USING pkg;", + "CREATE PACKAGE k AS FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl; FUNCTION g RETURN t PIPELINED USING impl; END;", + // PARALLEL_ENABLE partitioning and streaming clauses. + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY RANGE (a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a + 1, b.c)) ORDER c BY (a, b) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t ORDER c BY (a) IS BEGIN RETURN; END;", + // RESULT_CACHE RELIES_ON and SQL_MACRO. + "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON (t1, s.t2) IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO (TYPE => TABLE) IS BEGIN RETURN 'x'; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO (SCALAR) IS BEGIN RETURN 'x'; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO IS BEGIN RETURN 'x'; END;", + // CHARACTER SET on parameters, results, and declarations. + "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET ANY_CS, b IN OUT NOCOPY CLOB CHARACTER SET ANY_CS, c CHAR CHARACTER SET ANY_CS DEFAULT 'x') IS BEGIN NULL; END;", + "CREATE FUNCTION f (a VARCHAR2 CHARACTER SET ANY_CS) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS v VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN RETURN a; END;", + "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET CHAR_CS, b VARCHAR2 CHARACTER SET NCHAR_CS) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET ANY_CS) IS c CONSTANT VARCHAR2(10) CHARACTER SET a%CHARSET NOT NULL := 'x'; SUBTYPE s IS VARCHAR2(10) CHARACTER SET a%CHARSET; TYPE r IS RECORD (f VARCHAR2(10) CHARACTER SET CHAR_CS); CURSOR cu (q VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", + "CREATE FUNCTION f RETURN VARCHAR2 CHARACTER SET CHAR_CS IS BEGIN RETURN NULL; END;", + // Unconstrained parameter and result types. + "CREATE PROCEDURE p (a TIMESTAMP WITH TIME ZONE, b INTERVAL DAY TO SECOND, c INTERVAL YEAR TO MONTH, d LONG RAW, e RAW) IS BEGIN NULL; END;", + } { + ParseAndCheck(t, sql) + if violations := CheckLocations(t, sql); len(violations) > 0 { + t.Errorf("Parse(%q) Loc violations: %v", sql, violations) + } + } + + for _, sql := range []string{ + // ACCESSIBLE BY (PLS-00103 unless noted). + "CREATE PROCEDURE p ACCESSIBLE BY () IS BEGIN NULL; END;", + "CREATE PROCEDURE p ACCESSIBLE BY x IS BEGIN NULL; END;", + "CREATE PROCEDURE p ACCESSIBLE BY (PACKAGE) IS BEGIN NULL; END;", + "CREATE PROCEDURE p ACCESSIBLE BY (VIEW x) IS BEGIN NULL; END;", + "CREATE FUNCTION f RETURN NUMBER ACCESSIBLE BY (x@lnk) IS BEGIN RETURN 1; END;", + "CREATE PROCEDURE p ACCESSIBLE BY (x) AUTHID DEFINER ACCESSIBLE BY (y) IS BEGIN NULL; END;", // PLS-00371 + "CREATE PACKAGE k ACCESSIBLE BY (x) ACCESSIBLE BY (y) AS PROCEDURE p; END;", // PLS-00371 + "CREATE PACKAGE k AUTHID DEFINER AUTHID DEFINER AS PROCEDURE p; END;", // PLS-00371 + "CREATE PROCEDURE p AUTHID DEFINER AUTHID CURRENT_USER IS BEGIN NULL; END;", // PLS-00371 + // A type implements the function: nothing may follow but ';'. + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f RETURN t PIPELINED USING impl IS BEGIN RETURN; END;", + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl DETERMINISTIC;", + "CREATE FUNCTION f RETURN t USING impl PIPELINED;", + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE PARALLEL_ENABLE USING impl;", + "CREATE FUNCTION f RETURN t PIPELINED USING;", + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl", + "CREATE FUNCTION f RETURN NUMBER USING impl;", // PLS-00624 + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE;", // PLS-00220 + // PARALLEL_ENABLE and streaming clauses. + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE () IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c BY LIST (a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c BY HASH ()) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY () IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c.x BY (a) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t ORDER c BY (a) ORDER c BY (b) IS BEGIN RETURN; END;", // PLS-00371 + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t CLUSTER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", // PLS-00371 + // SQL_MACRO and repeated clauses. + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO () IS BEGIN RETURN 'x'; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO (foo) IS BEGIN RETURN 'x'; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO (TYPE => foo) IS BEGIN RETURN 'x'; END;", + "CREATE FUNCTION f RETURN NUMBER DETERMINISTIC DETERMINISTIC IS BEGIN RETURN 1; END;", // PLS-00371 + "CREATE FUNCTION f RETURN t PIPELINED PIPELINED IS BEGIN RETURN; END;", // PLS-00371 + "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RESULT_CACHE IS BEGIN RETURN 1; END;", // PLS-00371 + "CREATE FUNCTION f RETURN NUMBER PARALLEL_ENABLE PARALLEL_ENABLE IS BEGIN RETURN 1; END;", // PLS-00371 + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO SQL_MACRO IS BEGIN RETURN 'x'; END;", // PLS-00371 + "CREATE FUNCTION f RETURN NUMBER AUTHID DEFINER AUTHID DEFINER IS BEGIN RETURN 1; END;", // PLS-00371 + // CHARACTER SET. + "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET a%TYPE) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a NUMBER CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00550 + "CREATE PROCEDURE p (a NVARCHAR2 CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00554 + "CREATE PROCEDURE p (a LONG CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00554 + "CREATE PROCEDURE p IS v VARCHAR2(10) CHARACTER SET ANY_CS; BEGIN NULL; END;", // PLS-00551 + "CREATE FUNCTION f RETURN VARCHAR2 CHARACTER SET ANY_CS IS BEGIN RETURN NULL; END;", // PLS-00551 + "CREATE PROCEDURE p IS CURSOR c (q VARCHAR2 CHARACTER SET ANY_CS) IS SELECT 1 FROM dual; BEGIN NULL; END;", // PLS-00551 + "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET ANY_CS) IS TYPE r IS RECORD (f VARCHAR2(10) CHARACTER SET a%CHARSET); BEGIN NULL; END;", // PLS-00552 + "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET ANY_CS) IS TYPE t IS TABLE OF VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END;", // PLS-00552 + "CREATE TABLE t (c VARCHAR2(10) CHARACTER SET ANY_CS)", + // Constrained parameter and result types. + "CREATE PROCEDURE p (a VARCHAR2(10)) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a NUMBER(5)) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a TIMESTAMP(3)) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a INTERVAL DAY(2) TO SECOND(6)) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a FLOAT(5)) IS BEGIN NULL; END;", + "CREATE FUNCTION f RETURN VARCHAR2(10) IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN NUMBER(5) IS BEGIN RETURN 1; END;", + "CREATE PACKAGE k AS PROCEDURE p (a VARCHAR2(10)); END;", + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER FUNCTION f (x VARCHAR2(10)) RETURN NUMBER)", + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER FUNCTION f RETURN VARCHAR2(10))", + "CREATE PROCEDURE p IS CURSOR c (q VARCHAR2(10)) IS SELECT 1 FROM dual; BEGIN NULL; END;", + "CREATE PROCEDURE p IS FUNCTION f (x NUMBER(5)) RETURN NUMBER IS BEGIN RETURN 1; END; BEGIN NULL; END;", + "CREATE PROCEDURE p IS TYPE rc IS REF CURSOR RETURN VARCHAR2(10); BEGIN NULL; END;", + } { + ParseShouldFail(t, sql) + } +} + +// TestSubprogramHeadingClausesAST checks what the heading clauses record. +func TestSubprogramHeadingClausesAST(t *testing.T) { + proc := parseOne[*ast.CreateProcedureStmt](t, "CREATE PROCEDURE p AUTHID DEFINER ACCESSIBLE BY (PACKAGE s.k, f) IS BEGIN NULL; END;") + if proc.AuthID != "DEFINER" || proc.AccessibleBy == nil || proc.AccessibleBy.Len() != 2 { + t.Fatalf("procedure AuthID %q, AccessibleBy %v", proc.AuthID, proc.AccessibleBy) + } + first := proc.AccessibleBy.Items[0].(*ast.Accessor) + second := proc.AccessibleBy.Items[1].(*ast.Accessor) + if first.UnitKind != "PACKAGE" || first.Name.Schema != "S" || first.Name.Name != "K" || second.UnitKind != "" || second.Name.Name != "F" { + t.Errorf("accessors = %+v %+v, want PACKAGE S.K and F", first, second) + } + + pkg := parseOne[*ast.CreatePackageStmt](t, "CREATE PACKAGE k AUTHID CURRENT_USER ACCESSIBLE BY (x) AS PROCEDURE p; END;") + if pkg.AuthID != "CURRENT_USER" || pkg.AccessibleBy == nil || pkg.AccessibleBy.Len() != 1 { + t.Errorf("package AuthID %q, AccessibleBy %v", pkg.AuthID, pkg.AccessibleBy) + } + + agg := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING s.impl;") + if !agg.Aggregate || agg.Implementation == nil || agg.Implementation.Schema != "S" || agg.Implementation.Name != "IMPL" || agg.Body != nil { + t.Errorf("aggregate function = %+v", agg) + } + + ptf := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;") + if !ptf.Pipelined || ptf.Polymorphic != "ROW" || ptf.Implementation == nil || ptf.Implementation.Name != "PKG" { + t.Errorf("polymorphic function = %+v", ptf) + } + + par := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) CLUSTER c BY (a) RESULT_CACHE RELIES_ON (t1) SQL_MACRO (TYPE => TABLE) IS BEGIN RETURN; END;") + if spec := par.ParallelSpec; spec == nil || spec.Argument != "C" || spec.PartitionBy != "HASH" || spec.Columns.Len() != 2 { + t.Errorf("parallel spec = %+v", par.ParallelSpec) + } + if par.Streaming == nil || par.Streaming.Len() != 1 { + t.Fatalf("streaming = %v", par.Streaming) + } + if s := par.Streaming.Items[0].(*ast.StreamingClause); s.Kind != "CLUSTER" || s.Argument != "C" || s.Columns.Len() != 1 { + t.Errorf("streaming clause = %+v", s) + } + if par.ReliesOn == nil || par.ReliesOn.Len() != 1 || par.SqlMacroType != "TABLE" { + t.Errorf("RELIES_ON %v, SQL_MACRO type %q", par.ReliesOn, par.SqlMacroType) + } + + cs := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (a VARCHAR2 CHARACTER SET ANY_CS) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN a; END;") + param := cs.Parameters.Items[0].(*ast.Parameter) + if param.TypeName.CharacterSet != "ANY_CS" || param.TypeName.IsPercCharset { + t.Errorf("parameter character set = %q (%%CHARSET %v), want ANY_CS", param.TypeName.CharacterSet, param.TypeName.IsPercCharset) + } + if cs.ReturnType.CharacterSet != "A" || !cs.ReturnType.IsPercCharset { + t.Errorf("result character set = %q (%%CHARSET %v), want A%%CHARSET", cs.ReturnType.CharacterSet, cs.ReturnType.IsPercCharset) + } +} + +// parseOne parses sql, which must hold one statement of type T. +func parseOne[T ast.Node](t *testing.T, sql string) T { + t.Helper() + result := ParseAndCheck(t, sql) + stmt, ok := result.Items[0].(*ast.RawStmt).Stmt.(T) + if !ok { + t.Fatalf("Parse(%q) = %T", sql, result.Items[0].(*ast.RawStmt).Stmt) + } + return stmt +} diff --git a/oracle/parser/testdata/coverage/loc_node_coverage.tsv b/oracle/parser/testdata/coverage/loc_node_coverage.tsv index fed2c270..f0e76bbe 100644 --- a/oracle/parser/testdata/coverage/loc_node_coverage.tsv +++ b/oracle/parser/testdata/coverage/loc_node_coverage.tsv @@ -235,6 +235,9 @@ node_type family status fixture notes debt_class approval next_action *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.Accessor plsql covered CREATE PROCEDURE p ACCESSIBLE BY (PACKAGE s.k, f) AS BEGIN NULL; END; direct parser fixture for ACCESSIBLE BY accessors none covered keep_regression_guard +*nodes.ParallelEnableClause plsql covered CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) IS BEGIN RETURN; END; direct parser fixture for PARALLEL_ENABLE partitioning none covered keep_regression_guard +*nodes.StreamingClause plsql covered CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED ORDER c BY (a, b) IS BEGIN RETURN; END; direct parser fixture for ORDER and CLUSTER streaming clauses 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 4eb8f767..151075a2 100644 --- a/oracle/parser/testdata/coverage/plsql_dictionary_known_failures.tsv +++ b/oracle/parser/testdata/coverage/plsql_dictionary_known_failures.tsv @@ -1,88 +1,26 @@ 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_GSM_UTILITY:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters, enumeration types) 1158:57 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 "(" -SYS.AVTUNE_LOG_LISTAGGCLOB$:FUNCTION PIPELINED / AGGREGATE USING implementation type 4:43 syntax error at or near ";" -SYS.JSON_DATAGUIDE:FUNCTION PIPELINED / AGGREGATE USING implementation type 6:38 syntax error at or near ";" -SYS.JSON_HIERDATAGUIDE:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:39 syntax error at or near ";" -SYS.MVAGGRAWBITOR:FUNCTION PIPELINED / AGGREGATE USING implementation type 3:34 syntax error at or near ";" -SYS.SCHEDULER$_BATCHERR_PIPE:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:50 syntax error at or near "USING" -SYS.STRAGG:FUNCTION PIPELINED / AGGREGATE USING implementation type 3:32 syntax error at or near ";" -SYS.SYS_DGAGG:FUNCTION PIPELINED / AGGREGATE USING implementation type 6:40 syntax error at or near ";" -SYS.SYS_HIERDGAGG:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:32 syntax error at or near ";" -SYS.SYS_IXMLAGG:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:26 syntax error at or near ";" -SYS.SYS_IXQAGG:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:25 syntax error at or near ";" -SYS.SYS_IXQAGGAVG:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:28 syntax error at or near ";" -SYS.SYS_IXQAGGSUM:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:28 syntax error at or near ";" -SYS.SYS_NT_COLLECT:FUNCTION PIPELINED / AGGREGATE USING implementation type 3:35 syntax error at or near ";" -SYS.V_LISTBACKUPPIPE:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:34 syntax error at or near "using" -SYS.V_LISTRSRANGEPIPE:FUNCTION PIPELINED / AGGREGATE USING implementation type 2:37 syntax error at or near "using" -SYS.XMLSEQUENCEFROMREFCURSOR:FUNCTION PIPELINED / AGGREGATE USING implementation type 4:11 syntax error at or near "using" -SYS.XMLSEQUENCEFROMREFCURSOR2:FUNCTION PIPELINED / AGGREGATE USING implementation type 3:11 syntax error at or near "using" -SYS.XMLSEQUENCEFROMXMLTYPE:FUNCTION PIPELINED / AGGREGATE USING implementation type 3:34 syntax error at or near "using" -SYS.XQSEQUENCEFROMXMLTYPE:FUNCTION PIPELINED / AGGREGATE USING implementation type 3:34 syntax error at or near "using" -SYS.XQWINDOWSEQUENCEFROMXMLTYPE:FUNCTION PIPELINED / AGGREGATE USING implementation type 9:12 syntax error at or near "using" -SYS.CDBVIEW_INTERNAL:PACKAGE ACCESSIBLE BY clause 2:26 syntax error at or near "sys" -SYS.DBMS_ASSERT:PACKAGE CHARACTER SET ANY_CS / %CHARSET 21:30 syntax error at or near "CHARACTER" -SYS.DBMS_AW_EXP:PACKAGE CHARACTER SET ANY_CS / %CHARSET 119:54 syntax error at or near "CHARACTER" -SYS.DBMS_CRYPTO:PACKAGE CHARACTER SET ANY_CS / %CHARSET 292:55 syntax error at or near "CHARACTER" -SYS.DBMS_DATA_MINING:PACKAGE PIPELINED / AGGREGATE USING implementation type 1267:13 syntax error at or near "USING" -SYS.DBMS_FREQUENT_ITEMSET:PACKAGE PIPELINED / AGGREGATE USING implementation type 13:49 syntax error at or near "using" -SYS.DBMS_GSM_FIXED:PACKAGE ACCESSIBLE BY clause 86:1 syntax error at or near "ACCESSIBLE" -SYS.DBMS_GSM_GSMUSER:PACKAGE ACCESSIBLE BY clause 13:1 syntax error at or near "ACCESSIBLE" -SYS.DBMS_HADOOP_INTERNAL:PACKAGE PIPELINED / AGGREGATE USING implementation type 408:30 syntax error at or near "USING" -SYS.DBMS_HASH:PACKAGE CHARACTER SET ANY_CS / %CHARSET 17:32 syntax error at or near "CHARACTER" SYS.DBMS_HCS_LOG:PACKAGE conditional compilation ($IF/$END) 1:1 split into 23 segments SYS.DBMS_LOB:PACKAGE conditional compilation ($IF/$END) 1:1 split into 219 segments -SYS.DBMS_LOBUTIL:PACKAGE CHARACTER SET ANY_CS / %CHARSET 10:39 syntax error at or near "CHARACTER" -SYS.DBMS_METADATA_INT:PACKAGE ACCESSIBLE BY clause 298:32 syntax error at or near "sys" -SYS.DBMS_METADATA_UTIL:PACKAGE ACCESSIBLE BY clause 818:20 syntax error at or near "ACCESSIBLE" 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, 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" SYS.DBMS_SQL:PACKAGE conditional compilation ($IF/$END) 1:1 split into 600 segments -SYS.DBMS_SQLTUNE_UTIL0:PACKAGE ACCESSIBLE BY clause 449:26 syntax error at or near "SYS" -SYS.DBMS_SQLTUNE_UTIL1:PACKAGE ACCESSIBLE BY clause 702:3 syntax error at or near "ACCESSIBLE" -SYS.DBMS_SQLTUNE_UTIL2:PACKAGE ACCESSIBLE BY clause 552:3 syntax error at or near "ACCESSIBLE" -SYS.DBMS_STREAMS:PACKAGE CHARACTER SET ANY_CS / %CHARSET 88:59 syntax error at or near "CHARACTER" -SYS.DBMS_TTS:PACKAGE ACCESSIBLE BY clause 289:6 syntax error at or near "ACCESSIBLE" -SYS.DBMS_UPG_JSON0:PACKAGE ACCESSIBLE BY clause 2:1 syntax error at or near "ACCESSIBLE" -SYS.DBMS_UPG_JSON_INT:PACKAGE ACCESSIBLE BY clause 2:1 syntax error at or near "ACCESSIBLE" SYS.DBMS_UTILITY:PACKAGE conditional compilation ($IF/$END) 1:1 split into 99 segments -SYS.DBMS_VECTOR:PACKAGE PARALLEL_ENABLE (PARTITION ...) 243:55 syntax error at or near "(" -SYS.DBMS_VECTOR_DATABASE_ADMIN:PACKAGE ACCESSIBLE BY clause 2:18 syntax error at or near "accessible" -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, 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" -SYS.UTL_LMS:PACKAGE CHARACTER SET ANY_CS / %CHARSET 37:54 syntax error at or near "CHARACTER" -SYS.UTL_RAW:PACKAGE CHARACTER SET ANY_CS / %CHARSET 80:38 syntax error at or near "CHARACTER" +SYS.UTL_LMS:PACKAGE Oracle-internal syntax (PRAGMA INTERFACE, ... parameters, enumeration types) 59:33 syntax error at or near ".." SYS.UTL_RECOMP:PACKAGE conditional compilation ($IF/$END) 1:1 split into 18 segments -SYS.UTL_SMTP:PACKAGE CHARACTER SET ANY_CS / %CHARSET 452:46 syntax error at or near "CHARACTER" -SYS.UTL_TCP:PACKAGE CHARACTER SET ANY_CS / %CHARSET 340:50 syntax error at or near "CHARACTER" -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_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.HTP:PACKAGE BODY package initialization section 1798:1 syntax error at or near "begin" SYS.OWA_COOKIE:PACKAGE BODY package initialization section 221:1 syntax error at or near "begin" SYS.OWA_CUSTOM:PACKAGE BODY package initialization section 15:1 syntax error at or near "begin" 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.STANDARD:PACKAGE BODY unclassified 85:11 syntax error at or near "'IS NAN'" 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'" @@ -90,13 +28,8 @@ SYS.DBMS_FEATURE_MLE:PROCEDURE JSON function clause 29:28 syntax error at or nea 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" -SYS.JSON_ARRAY_T:TYPE BODY CHARACTER SET ANY_CS / %CHARSET 111:39 syntax error at or near "character" -SYS.JSON_ELEMENT_T:TYPE BODY CHARACTER SET ANY_CS / %CHARSET 2:38 syntax error at or near "character" -SYS.JSON_OBJECT_T:TYPE BODY CHARACTER SET ANY_CS / %CHARSET 110:39 syntax error at or near "character" +SYS.JDOM_T:TYPE BODY unclassified 420:28 syntax error at or near "NATIVE" SYS.WRR$_ASH_TIME_PERIOD:TYPE BODY type body method modifiers 3:3 syntax error at or near "FINAL" -XDB.XMLINDEXINSFUNC:FUNCTION PIPELINED / AGGREGATE USING implementation type 4:11 syntax error at or near "using" -XDB.XMLINDEXLOADFUNC:FUNCTION PARALLEL_ENABLE (PARTITION ...) 5:17 syntax error at or near "(" XDB.DBMS_XDB:PACKAGE REF type 234:37 syntax error at or near "SYS" XDB.DBMS_XDBRESOURCE:PACKAGE REF type 488:56 syntax error at or near "SYS" XDB.DBMS_XDB_REPOS:PACKAGE REF type 223:37 syntax error at or near "SYS" diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 39f21090..5742c715 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -145,3 +145,22 @@ ref_143 dml reject LOCK TABLE t IN EXCLUSIVE MODE WAIT 2.5 WAIT interval that is ref_144 dml reject LOCK TABLE t PARTITION (p1, p2) IN EXCLUSIVE MODE PARTITION with more than one name (ORA-01738) ref_145 plsql accept BEGIN LOCK TABLE t IN SHARE UPDATE MODE WAIT 5; ROLLBACK; END; LOCK TABLE with WAIT in a block ref_146 plsql reject BEGIN LOCK TABLE t IN FOO MODE; END; unknown lock mode in a block +ref_147 plsql_body accept CREATE OR REPLACE PROCEDURE omni_ab_{run} (a NUMBER) AUTHID DEFINER ACCESSIBLE BY (PACKAGE omni_x, PROCEDURE omni_y, FUNCTION omni_z, TRIGGER omni_t, TYPE omni_ty, omni_plain, omni_s.omni_q) IS BEGIN NULL; END; ACCESSIBLE BY with every unit kind +ref_148 plsql_body accept CREATE OR REPLACE PACKAGE omni_abk_{run} ACCESSIBLE BY (omni_x) AUTHID CURRENT_USER AS PROCEDURE p ACCESSIBLE BY (PACKAGE omni_x); FUNCTION f RETURN NUMBER ACCESSIBLE BY (omni_y); END; ACCESSIBLE BY on a package and its subprograms +ref_149 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_ab_{run} ACCESSIBLE BY (PACKAGE) IS BEGIN NULL; END; accessor with a unit kind and no name +ref_150 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_abl_{run} RETURN NUMBER ACCESSIBLE BY (omni_x@omni_lnk) IS BEGIN RETURN 1; END; accessor with a database link +ref_151 plsql_body accept CREATE OR REPLACE FUNCTION omni_agg_{run} (x NUMBER) RETURN NUMBER DETERMINISTIC AGGREGATE USING omni_s.omni_impl; AGGREGATE USING implementation type, no body +ref_152 plsql_body accept CREATE OR REPLACE FUNCTION omni_pip_{run} RETURN omni_tab PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) USING omni_impl; PIPELINED ... USING implementation type +ref_153 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_agg_{run} (x NUMBER) RETURN NUMBER AGGREGATE USING omni_impl IS BEGIN RETURN 1; END; body after AGGREGATE USING +ref_154 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_pip_{run} RETURN omni_tab USING omni_impl PIPELINED; clause after USING implementation type +ref_155 plsql_body accept CREATE OR REPLACE FUNCTION omni_par_{run} (c SYS_REFCURSOR) RETURN omni_tab PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END; PARALLEL_ENABLE partitioning with streaming clauses +ref_156 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_par_{run} (c SYS_REFCURSOR) RETURN omni_tab PARALLEL_ENABLE (PARTITION c BY LIST (a)) IS BEGIN RETURN; END; unknown PARALLEL_ENABLE partitioning +ref_157 plsql_body accept CREATE OR REPLACE FUNCTION omni_ptf_{run} (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING omni_pkg; polymorphic table function +ref_158 plsql_body accept CREATE OR REPLACE FUNCTION omni_rc_{run} RETURN NUMBER RESULT_CACHE RELIES_ON (omni_t1, omni_s.omni_t2) IS BEGIN RETURN 1; END; RESULT_CACHE RELIES_ON +ref_159 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_sm_{run} RETURN VARCHAR2 SQL_MACRO (foo) IS BEGIN RETURN 'x'; END; unknown SQL_MACRO type +ref_160 plsql_body accept CREATE OR REPLACE FUNCTION omni_cs_{run} (p IN VARCHAR2 CHARACTER SET ANY_CS, q IN OUT NOCOPY CLOB CHARACTER SET ANY_CS) RETURN VARCHAR2 CHARACTER SET p%CHARSET IS v VARCHAR2(10) CHARACTER SET p%CHARSET; BEGIN RETURN p; END; CHARACTER SET ANY_CS and %CHARSET +ref_161 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_cs_{run} (p VARCHAR2 CHARACTER SET p%TYPE) IS BEGIN NULL; END; CHARACTER SET with %TYPE +ref_162 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_len_{run} (p VARCHAR2(10)) IS BEGIN NULL; END; constrained parameter type +ref_163 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_ret_{run} RETURN NUMBER(5) IS BEGIN RETURN 1; END; constrained result type +ref_164 plsql reject DECLARE CURSOR c (q VARCHAR2(10)) IS SELECT 1 FROM dual; BEGIN NULL; END; constrained cursor parameter type +ref_165 plsql_body accept CREATE OR REPLACE PROCEDURE omni_unc_{run} (p TIMESTAMP WITH TIME ZONE, q INTERVAL DAY TO SECOND, r LONG RAW, s RAW) IS BEGIN NULL; END; unconstrained parameter types diff --git a/oracle/parser/type.go b/oracle/parser/type.go index 6cfe752d..40da1c5d 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -1,6 +1,8 @@ package parser import ( + "strings" + nodes "github.com/bytebase/omni/oracle/ast" ) @@ -139,19 +141,138 @@ 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) +// typeModMode says what the parenthesized modifiers of a datatype (length, +// precision, scale) may hold. +type typeModMode int + +const ( + // typeModsSQL takes integer literals, as SQL does. + typeModsSQL typeModMode = iota + // typeModsPLSQL takes expressions: the datatype of a PL/SQL declaration. + // Where SQL takes an integer literal, PL/SQL takes a static expression, + // VARCHAR2(ORA_MAX_NAME_LEN + 2) or 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. + // + // Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/plsql-language-fundamentals.html (Static Expressions) + typeModsPLSQL + // typeModsNone takes no modifiers: the type of a parameter or of a + // function result is unconstrained. Oracle 23ai rejects p VARCHAR2(10), + // RETURN NUMBER(5), and INTERVAL DAY(2) TO SECOND there with PLS-00103, + // in standalone, package, local, and object type subprograms and cursor + // parameters alike. + typeModsNone +) + +// plsqlCharsetUse says which CHARACTER SET clause a PL/SQL datatype takes. +type plsqlCharsetUse int + +const ( + // charsetNone takes no CHARACTER SET clause. + charsetNone plsqlCharsetUse = iota + // charsetFixed takes a named character set only: a record field or a + // collection element (PLS-00552 for ANY_CS or item%CHARSET). + charsetFixed + // charsetFlexible also takes item%CHARSET: a declaration, a subtype, a + // function result, or a cursor parameter. + charsetFlexible + // charsetAnyCS also takes ANY_CS: a subprogram parameter, the one place + // Oracle allows it (PLS-00551 elsewhere). + charsetAnyCS +) + +// parsePLSQLTypeName parses the datatype of a PL/SQL declaration whose +// modifiers may be expressions and which takes no CHARACTER SET clause. func (p *Parser) parsePLSQLTypeName() (*nodes.TypeName, error) { - saved := p.plsqlTypeMods - p.plsqlTypeMods = true + return p.parsePLSQLDatatype(typeModsPLSQL, charsetNone) +} + +// parsePLSQLDatatype parses a PL/SQL datatype: mods says what its modifiers +// may hold, use which CHARACTER SET clause may follow it. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/datatype-attribute.html +// +// datatype [ CHARACTER SET { character_set | item%CHARSET } ] +func (p *Parser) parsePLSQLDatatype(mods typeModMode, use plsqlCharsetUse) (*nodes.TypeName, error) { + saved := p.typeMods + p.typeMods = mods tn, err := p.parseTypeName() - p.plsqlTypeMods = saved - return tn, err + p.typeMods = saved + if err != nil { + return nil, err + } + if use != charsetNone && p.cur.Type == tokIDENT && p.cur.Str == "CHARACTER" && p.peekNext().Type == kwSET { + if err := p.parsePLSQLCharacterSet(tn, use); err != nil { + return nil, err + } + } + return tn, nil +} + +// plsqlNonCharacterTypes are the built-in datatypes parseTypeName names that +// take no CHARACTER SET clause: Oracle 23ai rejects NUMBER CHARACTER SET +// ANY_CS with PLS-00550, and NCHAR, NVARCHAR2, NCLOB, and LONG ones, whose +// character set is fixed, with PLS-00554. A type the text does not settle, +// such as a subtype or t%TYPE, is left to the engine. +var plsqlNonCharacterTypes = map[string]bool{ + "NUMBER": true, "INTEGER": true, "SMALLINT": true, "DECIMAL": true, "FLOAT": true, + "NUMERIC": true, "DATE": true, "TIMESTAMP": true, "INTERVAL": true, "RAW": true, + "LONG": true, "ROWID": true, "BLOB": true, "JSON": true, "NCHAR": true, + "NVARCHAR2": true, "NCLOB": true, +} + +// parsePLSQLCharacterSet parses CHARACTER SET { character_set | item%CHARSET } +// after the datatype tn, the current token at CHARACTER. Each check below +// follows from the text alone and was confirmed on Oracle 23ai; whether a +// name is a known character set (PLS-00553) is left to the engine. +func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) error { + if !tn.IsPercType && !tn.IsPercRowtype && tn.Names.Len() > 0 { + if first, ok := tn.Names.Items[0].(*nodes.String); ok && plsqlNonCharacterTypes[first.Str] { + return p.syntaxErrorAtCur() + } + } + p.advance() // consume CHARACTER + p.advance() // consume SET + nameTok := p.cur + if !p.isIdentLike() { + return p.syntaxErrorAtCur() + } + name, err := p.parseIdentifier() + if err != nil { + return err + } + for p.cur.Type == '.' { + p.advance() + if !p.isIdentLike() { + return p.syntaxErrorAtCur() + } + part, err := p.parseIdentifier() + if err != nil { + return err + } + name += "." + part + } + if p.cur.Type == '%' { + p.advance() + if !p.isIdentLikeStr("CHARSET") { + return p.syntaxErrorAtCur() + } + p.advance() + tn.IsPercCharset = true + } else if strings.Contains(name, ".") { + return p.syntaxErrorAtCur() + } + switch { + case tn.IsPercCharset && use == charsetFixed: + // PLS-00552: flexible character set is not allowed on component element + return p.syntaxErrorAtTok(nameTok) + case !tn.IsPercCharset && nameTok.Type != tokQIDENT && name == "ANY_CS" && use != charsetAnyCS: + // PLS-00551: character set ANY_CS is only allowed on a subprogram parameter + return p.syntaxErrorAtTok(nameTok) + } + tn.CharacterSet = name + tn.Loc.End = p.prev.End + return nil } // parsePLSQLTypeMods parses the parenthesized modifiers of a datatype in a @@ -193,9 +314,9 @@ func (p *Parser) parsePLSQLTypeMods(tn *nodes.TypeName, sized bool) error { // parsePLSQLTypeMod parses one modifier expression. A datatype nested in // it, as in CAST(x AS NUMBER(5)), follows the SQL rules again. func (p *Parser) parsePLSQLTypeMod(tn *nodes.TypeName) error { - p.plsqlTypeMods = false + p.typeMods = typeModsSQL expr, err := p.parseExpr() - p.plsqlTypeMods = true + p.typeMods = typeModsPLSQL if err != nil { return err } @@ -215,8 +336,11 @@ func (p *Parser) parseOptionalPrecisionScale(tn *nodes.TypeName) error { if p.cur.Type != '(' { return nil } - if p.plsqlTypeMods { + switch p.typeMods { + case typeModsPLSQL: return p.parsePLSQLTypeMods(tn, false) + case typeModsNone: + return p.syntaxErrorAtCur() } p.advance() // consume '(' @@ -250,8 +374,11 @@ func (p *Parser) parseOptionalSizeWithSemantic(tn *nodes.TypeName) error { if p.cur.Type != '(' { return nil } - if p.plsqlTypeMods { + switch p.typeMods { + case typeModsPLSQL: return p.parsePLSQLTypeMods(tn, true) + case typeModsNone: + return p.syntaxErrorAtCur() } p.advance() // consume '(' From de7f539ac8e9e71b47f7e43c67cdcc8cca786c5d Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 07:50:15 +0000 Subject: [PATCH 02/22] fix(oracle): gate USING in Split, classify PL/SQL types for CHARACTER SET (BYT-10301) Review follow-up, each checked on Oracle 23ai: - Split ends a function at USING only after RETURN and AGGREGATE or PIPELINED: a function or parameter named USING compiles, and its unit ran on to END. - CHARACTER SET is rejected on the PL/SQL predefined non-character types too (BOOLEAN, PLS_INTEGER, BINARY_INTEGER, SIMPLE_INTEGER, NATURAL, POSITIVE, SIGNTYPE, INT, REAL, DEC, BINARY_*, SIMPLE_*, SYS_REFCURSOR, UROWID, BFILE, MLSLABEL, VECTOR: PLS-00550); STRING and CHARACTER take one. - %CHARSET must be unquoted: a%"CHARSET" is PLS-00103. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/split.go | 14 +++++++++++-- oracle/parser/split_test.go | 13 ++++++++++++ oracle/parser/subprogram_clause_test.go | 8 ++++++++ oracle/parser/type.go | 27 ++++++++++++++++--------- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/oracle/parser/split.go b/oracle/parser/split.go index 7a28e678..e016ac34 100644 --- a/oracle/parser/split.go +++ b/oracle/parser/split.go @@ -205,6 +205,11 @@ type splitPLSQLFrame struct { // callSpec marks an implementation that is a call spec: it has no END, // and its ';' ends it. callSpec bool + // afterReturn marks a function head past its RETURN, where the clause + // list starts; implClause marks AGGREGATE or PIPELINED in that list, + // after which USING names an implementation type. + afterReturn bool + implClause bool } type splitState struct { @@ -494,11 +499,16 @@ func (s *splitState) observeSubprogramHead(top *splitPLSQLFrame, tok Token) { top.afterIsAs = true case tok.Type == tokIDENT && tok.Str == "WRAPPED" && len(s.frames) == 1: s.callSpecStarted = true - case tok.Type == kwUSING && len(s.frames) == 1: + case tok.Type == kwRETURN: + top.afterReturn = true + case top.afterReturn && (tok.Type == kwPIPELINED || tok.Type == tokIDENT && tok.Str == "AGGREGATE"): + top.implClause = true + case tok.Type == kwUSING && top.implClause && len(s.frames) == 1: // AGGREGATE USING type or PIPELINED ... USING type: a type // implements the function, which has no IS|AS body and, like a // call spec, ends at its ';'. SQL*Plus buffers either up to a "/" - // line; the splitter ends both where the parser does. + // line; the splitter ends both where the parser does. USING + // elsewhere, as a function or parameter name, ends nothing. top.callSpec = true s.callSpecStarted = true } diff --git a/oracle/parser/split_test.go b/oracle/parser/split_test.go index 832d74ff..ea69cc19 100644 --- a/oracle/parser/split_test.go +++ b/oracle/parser/split_test.go @@ -435,6 +435,19 @@ func TestSplitPLSQLBlocks(t *testing.T) { "\nSELECT 1 FROM dual", }, }, + { + // USING ends a function only after AGGREGATE or PIPELINED: a + // function or parameter named USING compiles on Oracle 23ai. + name: "function named using", + sql: "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;\n" + + "CREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;\n" + + "SELECT 1 FROM dual;", + want: []string{ + "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;", + "\nCREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;", + "\nSELECT 1 FROM dual", + }, + }, { name: "package with call spec members", sql: "CREATE OR REPLACE PACKAGE BODY pk AS\n" + diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index ad303aec..83078ee5 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -50,8 +50,12 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET ANY_CS, b IN OUT NOCOPY CLOB CHARACTER SET ANY_CS, c CHAR CHARACTER SET ANY_CS DEFAULT 'x') IS BEGIN NULL; END;", "CREATE FUNCTION f (a VARCHAR2 CHARACTER SET ANY_CS) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS v VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN RETURN a; END;", "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET CHAR_CS, b VARCHAR2 CHARACTER SET NCHAR_CS) IS BEGIN NULL; END;", + "CREATE PROCEDURE p (a STRING CHARACTER SET ANY_CS, b CHARACTER CHARACTER SET ANY_CS, c my_subtype CHARACTER SET ANY_CS) IS BEGIN NULL; END;", "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET ANY_CS) IS c CONSTANT VARCHAR2(10) CHARACTER SET a%CHARSET NOT NULL := 'x'; SUBTYPE s IS VARCHAR2(10) CHARACTER SET a%CHARSET; TYPE r IS RECORD (f VARCHAR2(10) CHARACTER SET CHAR_CS); CURSOR cu (q VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", "CREATE FUNCTION f RETURN VARCHAR2 CHARACTER SET CHAR_CS IS BEGIN RETURN NULL; END;", + // USING as a name, not an implementation clause. + "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;", + "CREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;", // Unconstrained parameter and result types. "CREATE PROCEDURE p (a TIMESTAMP WITH TIME ZONE, b INTERVAL DAY TO SECOND, c INTERVAL YEAR TO MONTH, d LONG RAW, e RAW) IS BEGIN NULL; END;", } { @@ -105,6 +109,10 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET) IS BEGIN NULL; END;", "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET a%TYPE) IS BEGIN NULL; END;", "CREATE PROCEDURE p (a NUMBER CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00550 + "CREATE PROCEDURE p (a BOOLEAN CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00550 + "CREATE PROCEDURE p (a PLS_INTEGER CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00550 + "CREATE PROCEDURE p (a SYS_REFCURSOR CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00550 + "CREATE FUNCTION f (a VARCHAR2 CHARACTER SET ANY_CS) RETURN VARCHAR2 CHARACTER SET a%\"CHARSET\" IS BEGIN RETURN a; END;", // PLS-00550 "CREATE PROCEDURE p (a NVARCHAR2 CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00554 "CREATE PROCEDURE p (a LONG CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // PLS-00554 "CREATE PROCEDURE p IS v VARCHAR2(10) CHARACTER SET ANY_CS; BEGIN NULL; END;", // PLS-00551 diff --git a/oracle/parser/type.go b/oracle/parser/type.go index 40da1c5d..f3c001c6 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -209,16 +209,22 @@ func (p *Parser) parsePLSQLDatatype(mods typeModMode, use plsqlCharsetUse) (*nod return tn, nil } -// plsqlNonCharacterTypes are the built-in datatypes parseTypeName names that -// take no CHARACTER SET clause: Oracle 23ai rejects NUMBER CHARACTER SET -// ANY_CS with PLS-00550, and NCHAR, NVARCHAR2, NCLOB, and LONG ones, whose -// character set is fixed, with PLS-00554. A type the text does not settle, -// such as a subtype or t%TYPE, is left to the engine. +// plsqlNonCharacterTypes are the predefined datatypes, SQL and PL/SQL, that +// take no CHARACTER SET clause: Oracle 23ai rejects BOOLEAN or NUMBER +// CHARACTER SET ANY_CS with PLS-00550, and NCHAR, NVARCHAR2, NCLOB, and LONG +// ones, whose character set is fixed, with PLS-00554. CHAR, CHARACTER, +// VARCHAR2, VARCHAR, STRING, and CLOB take one. A name the text does not +// settle, such as a subtype, an object type, or t%TYPE, is left to the +// engine. var plsqlNonCharacterTypes = map[string]bool{ - "NUMBER": true, "INTEGER": true, "SMALLINT": true, "DECIMAL": true, "FLOAT": true, - "NUMERIC": true, "DATE": true, "TIMESTAMP": true, "INTERVAL": true, "RAW": true, - "LONG": true, "ROWID": true, "BLOB": true, "JSON": true, "NCHAR": true, - "NVARCHAR2": true, "NCLOB": true, + "NUMBER": true, "INTEGER": true, "INT": true, "SMALLINT": true, "DECIMAL": true, + "DEC": true, "NUMERIC": true, "FLOAT": true, "REAL": true, "DOUBLE": true, + "BINARY_FLOAT": true, "BINARY_DOUBLE": true, "SIMPLE_FLOAT": true, "SIMPLE_DOUBLE": true, + "PLS_INTEGER": true, "BINARY_INTEGER": true, "SIMPLE_INTEGER": true, "NATURAL": true, + "NATURALN": true, "POSITIVE": true, "POSITIVEN": true, "SIGNTYPE": true, "BOOLEAN": true, + "DATE": true, "TIMESTAMP": true, "INTERVAL": true, "RAW": true, "LONG": true, + "ROWID": true, "UROWID": true, "BLOB": true, "BFILE": true, "JSON": true, "VECTOR": true, + "MLSLABEL": true, "SYS_REFCURSOR": true, "NCHAR": true, "NVARCHAR2": true, "NCLOB": true, } // parsePLSQLCharacterSet parses CHARACTER SET { character_set | item%CHARSET } @@ -254,7 +260,8 @@ func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) } if p.cur.Type == '%' { p.advance() - if !p.isIdentLikeStr("CHARSET") { + // %CHARSET is an attribute keyword: Oracle 23ai rejects a%"CHARSET". + if !p.isKeywordStr("CHARSET") { return p.syntaxErrorAtCur() } p.advance() From 5c58d1228e55c9fe2521a0592ca36f609325a345 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 08:05:26 +0000 Subject: [PATCH 03/22] fix(oracle): scope heading clauses by subprogram level, tighten names (BYT-10301) Review follow-up, each checked on Oracle 23ai: - AUTHID stands only on a schema-level unit (PLS-00157 on packaged, nested, and type method subprograms), ACCESSIBLE BY not on a type method (PLS-00262), and DEFAULT COLLATION only on a schema-level unit (documented). The local-subprogram AUTHID over-accept is retired. - Clause keywords are unquoted words: "ACCESSIBLE" BY and "AUTHID" are PLS-00103. - RELIES_ON (t1,) and USING impl. or impl@ are PLS-00103; USING takes a dotted database link. - Split skips the result datatype's first word and name parts after '.' before looking for PIPELINED or AGGREGATE: RETURN pipelined.using names a type. A polymorphic table function with an IS body compiles on Oracle, so it stays accepted. Reference rows ref_166 to ref_170. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 49 ++++++++++++++----- oracle/parser/create_type.go | 2 +- oracle/parser/plsql_local_subprogram_test.go | 9 ++-- oracle/parser/split.go | 18 +++++-- oracle/parser/split_test.go | 2 + oracle/parser/subprogram_clause.go | 39 ++++++++++----- oracle/parser/subprogram_clause_test.go | 25 ++++++++++ .../testdata/coverage/reference_oracle.tsv | 5 ++ 8 files changed, 117 insertions(+), 32 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index a20bf7e8..b79e6f11 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -62,7 +62,7 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi } } - if err := p.parseProcedureProperties(stmt); err != nil { + if err := p.parseProcedureProperties(stmt, subprogramSchema); err != nil { return nil, err } @@ -211,7 +211,7 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit parseErr460 := // Optional function properties (can appear in any order before IS/AS) - p.parseFunctionProperties(stmt) + p.parseFunctionProperties(stmt, subprogramSchema) if parseErr460 != // IS | AS @@ -255,8 +255,9 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit // A type can implement the function: AGGREGATE USING type, or USING type // after PIPELINED. The clause list ends there and the function has no IS | AS // body; Oracle 23ai rejects a following IS or clause (PLS-00103), USING -// without PIPELINED (PLS-00624), and AGGREGATE without USING after it. -func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt) error { +// without PIPELINED (PLS-00624), and AGGREGATE without USING after it. level +// limits AUTHID, ACCESSIBLE BY, and DEFAULT COLLATION as subprogramLevel says. +func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level subprogramLevel) error { seen := clauseSeen{} for { switch { @@ -347,7 +348,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt) error { } stmt.SqlMacroType = kind } - case p.isIdentLikeStr("AUTHID"): + case p.isKeywordStr("AUTHID") && level == subprogramSchema: if err := p.firstClause(seen, "AUTHID"); err != nil { return err } @@ -356,7 +357,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt) error { return err } stmt.AuthID = authID - case p.isIdentLikeStr("ACCESSIBLE"): + case p.isKeywordStr("ACCESSIBLE") && level != subprogramMethod: if err := p.firstClause(seen, "ACCESSIBLE BY"); err != nil { return err } @@ -365,7 +366,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt) error { return err } stmt.AccessibleBy = list - case p.atDefaultCollation(): + case p.atDefaultCollation() && level == subprogramSchema: if err := p.firstClause(seen, "DEFAULT COLLATION"); err != nil { return err } @@ -379,15 +380,37 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt) error { } // parseImplementationType parses USING [ schema. ] implementation_type -// [ @dblink ], the current token at USING. +// [ @dblink ], the current token at USING. Every part needs its name: Oracle +// 23ai rejects USING impl. and USING impl@ (PLS-00103), and takes a dotted +// database link name (impl@lnk.dom). func (p *Parser) parseImplementationType(stmt *nodes.CreateFunctionStmt) error { p.advance() // consume USING - name, err := p.parseObjectName() + name, err := p.parseUnitName() if err != nil { return err } - if name == nil || name.Name == "" { - return p.syntaxErrorAtCur() + if p.cur.Type == '@' { + p.advance() + if !p.isIdentLike() { + return p.syntaxErrorAtCur() + } + link, err := p.parseIdentifier() + if err != nil { + return err + } + for p.cur.Type == '.' { + p.advance() + if !p.isIdentLike() { + return p.syntaxErrorAtCur() + } + part, err := p.parseIdentifier() + if err != nil { + return err + } + link += "." + part + } + name.DBLink = link + name.Loc.End = p.prev.End } stmt.Implementation = name return nil @@ -610,7 +633,7 @@ func (p *Parser) parsePackageProcDecl() (*nodes.CreateProcedureStmt, error) { return nil, parseErr474 } } - if err := p.parseProcedureProperties(stmt); err != nil { + if err := p.parseProcedureProperties(stmt, subprogramPackaged); err != nil { return nil, err } @@ -679,7 +702,7 @@ func (p *Parser) parsePackageFuncDecl() (*nodes.CreateFunctionStmt, error) { } else { return nil, p.syntaxErrorAtCur() } - parseErr479 := p.parseFunctionProperties(stmt) + parseErr479 := p.parseFunctionProperties(stmt, subprogramPackaged) if parseErr479 != // Check for IS|AS (definition) or ; (declaration) diff --git a/oracle/parser/create_type.go b/oracle/parser/create_type.go index efa8ca66..ba3867aa 100644 --- a/oracle/parser/create_type.go +++ b/oracle/parser/create_type.go @@ -463,7 +463,7 @@ func (p *Parser) parseTypeBodyFunction(isConstructor, inSpec bool) (*nodes.Creat // (PLS-00103 on Oracle 23ai). return nil, p.syntaxErrorAtCur() } - parseErr588 := p.parseFunctionProperties(stmt) + parseErr588 := p.parseFunctionProperties(stmt, subprogramMethod) if parseErr588 != // IS | AS diff --git a/oracle/parser/plsql_local_subprogram_test.go b/oracle/parser/plsql_local_subprogram_test.go index 8431a55e..9bfd940e 100644 --- a/oracle/parser/plsql_local_subprogram_test.go +++ b/oracle/parser/plsql_local_subprogram_test.go @@ -115,20 +115,21 @@ func TestPLSQLLocalSubprograms(t *testing.T) { // TestPLSQLLocalSubprogramDocumentedOverAccepts pins review-flagged shapes // that stay ACCEPTED, with dual-context engine evidence (Oracle 23ai): -// each hard-rejects inside an executed anonymous block (PLS-00157/00103), -// but the engine ACCEPTS the same shape in DDL contexts — CREATE +// each hard-rejects inside an executed anonymous block (PLS-00103), but the +// engine ACCEPTS the same shape in DDL contexts — CREATE // PACKAGE/PROCEDURE/FUNCTION succeed "with compilation errors" (statement // accepted, errors deferred to compilation). parsePLSQLDeclaration serves // both contexts, so rejecting would over-reject engine-accepted DDL — the // parser's fatal direction. Documented over-accepts per the // parser-unsure -> accept contract; revisit only with context-aware -// declaration parsing. +// declaration parsing. AUTHID on a local subprogram left this list: it is +// rejected now, as Oracle reports PLS-00157 for it from the text alone (see +// TestSubprogramHeadingClauses). func TestPLSQLLocalSubprogramDocumentedOverAccepts(t *testing.T) { for _, tc := range []struct { name string sql string }{ - {"authid on local function", "DECLARE FUNCTION f RETURN NUMBER AUTHID CURRENT_USER IS BEGIN RETURN 1; END; BEGIN NULL; END;"}, {"qualified local name", "DECLARE PROCEDURE sc.p IS BEGIN NULL; END; BEGIN NULL; END;"}, {"trailing comma in parameters", "DECLARE PROCEDURE p(x NUMBER,) IS BEGIN NULL; END; BEGIN NULL; END;"}, {"DECLARE after IS in routine body", "DECLARE FUNCTION f RETURN NUMBER IS DECLARE x NUMBER; BEGIN RETURN 1; END; BEGIN NULL; END;"}, diff --git a/oracle/parser/split.go b/oracle/parser/split.go index e016ac34..869311f2 100644 --- a/oracle/parser/split.go +++ b/oracle/parser/split.go @@ -205,10 +205,14 @@ type splitPLSQLFrame struct { // callSpec marks an implementation that is a call spec: it has no END, // and its ';' ends it. callSpec bool - // afterReturn marks a function head past its RETURN, where the clause - // list starts; implClause marks AGGREGATE or PIPELINED in that list, - // after which USING names an implementation type. + // afterReturn marks a function head past its RETURN; returnType marks + // the datatype's first word, which no clause follows yet; afterDot marks + // a token after '.', a name part. implClause marks AGGREGATE or + // PIPELINED in the clause list, after which USING names an + // implementation type. afterReturn bool + returnType bool + afterDot bool implClause bool } @@ -488,6 +492,10 @@ func (s *splitState) observeSubprogramHead(top *splitPLSQLFrame, tok Token) { } } case !top.isAs: + afterDot := top.afterDot + top.afterDot = tok.Type == '.' + inType := top.returnType + top.returnType = false switch { case tok.Type == '(': top.headDepth++ @@ -501,6 +509,10 @@ func (s *splitState) observeSubprogramHead(top *splitPLSQLFrame, tok Token) { s.callSpecStarted = true case tok.Type == kwRETURN: top.afterReturn = true + top.returnType = true + case inType || afterDot: + // The first word of the result datatype, or a name part after + // '.': RETURN pipelined.using names a type. case top.afterReturn && (tok.Type == kwPIPELINED || tok.Type == tokIDENT && tok.Str == "AGGREGATE"): top.implClause = true case tok.Type == kwUSING && top.implClause && len(s.frames) == 1: diff --git a/oracle/parser/split_test.go b/oracle/parser/split_test.go index ea69cc19..ea75ea88 100644 --- a/oracle/parser/split_test.go +++ b/oracle/parser/split_test.go @@ -441,10 +441,12 @@ func TestSplitPLSQLBlocks(t *testing.T) { name: "function named using", sql: "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;\n" + "CREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;\n" + + "CREATE FUNCTION h RETURN pipelined.using IS BEGIN RETURN NULL; END;\n" + "SELECT 1 FROM dual;", want: []string{ "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;", "\nCREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;", + "\nCREATE FUNCTION h RETURN pipelined.using IS BEGIN RETURN NULL; END;", "\nSELECT 1 FROM dual", }, }, diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 31930acc..8b812b12 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -13,6 +13,21 @@ import ( // clauseSeen records the clauses a heading has used, for the PLS-00371 check. type clauseSeen map[string]bool +// subprogramLevel says where a subprogram heading stands, which decides the +// clauses it takes. On Oracle 23ai AUTHID stands only on a schema-level unit +// (PLS-00157 on a packaged, nested, or object type method), and ACCESSIBLE +// BY not on an object type method (PLS-00262). DEFAULT COLLATION is +// documented for schema-level units only. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/DEFAULT-COLLATION-clause.html +type subprogramLevel int + +const ( + subprogramSchema subprogramLevel = iota // CREATE PROCEDURE or FUNCTION + subprogramPackaged // in a package, or nested in a block + subprogramMethod // an object type method +) + // firstClause reports a syntax error at the current token, which starts clause, // when the heading already has that clause. func (p *Parser) firstClause(seen clauseSeen, clause string) error { @@ -125,16 +140,17 @@ func (p *Parser) atDefaultCollation() bool { } // parseProcedureProperties parses the clauses between a procedure's -// parameters and its IS | AS or ';', in any order. +// parameters and its IS | AS or ';', in any order; level limits them as +// subprogramLevel says. // // Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/CREATE-PROCEDURE-statement.html // // [ { invoker_rights_clause | accessible_by_clause | default_collation_clause }... ] -func (p *Parser) parseProcedureProperties(stmt *nodes.CreateProcedureStmt) error { +func (p *Parser) parseProcedureProperties(stmt *nodes.CreateProcedureStmt, level subprogramLevel) error { seen := clauseSeen{} for { switch { - case p.isIdentLikeStr("AUTHID"): + case p.isKeywordStr("AUTHID") && level == subprogramSchema: if err := p.firstClause(seen, "AUTHID"); err != nil { return err } @@ -143,7 +159,7 @@ func (p *Parser) parseProcedureProperties(stmt *nodes.CreateProcedureStmt) error return err } stmt.AuthID = authID - case p.isIdentLikeStr("ACCESSIBLE"): + case p.isKeywordStr("ACCESSIBLE") && level != subprogramMethod: if err := p.firstClause(seen, "ACCESSIBLE BY"); err != nil { return err } @@ -152,7 +168,7 @@ func (p *Parser) parseProcedureProperties(stmt *nodes.CreateProcedureStmt) error return err } stmt.AccessibleBy = list - case p.atDefaultCollation(): + case p.atDefaultCollation() && level == subprogramSchema: if err := p.firstClause(seen, "DEFAULT COLLATION"); err != nil { return err } @@ -175,7 +191,7 @@ func (p *Parser) parsePackageProperties(stmt *nodes.CreatePackageStmt) error { seen := clauseSeen{} for { switch { - case p.isIdentLikeStr("AUTHID"): + case p.isKeywordStr("AUTHID"): if err := p.firstClause(seen, "AUTHID"); err != nil { return err } @@ -184,7 +200,7 @@ func (p *Parser) parsePackageProperties(stmt *nodes.CreatePackageStmt) error { return err } stmt.AuthID = authID - case p.isIdentLikeStr("ACCESSIBLE"): + case p.isKeywordStr("ACCESSIBLE"): if err := p.firstClause(seen, "ACCESSIBLE BY"); err != nil { return err } @@ -333,18 +349,19 @@ func (p *Parser) parseResultCacheReliesOn() (*nodes.List, error) { p.advance() list := &nodes.List{} for p.cur.Type != ')' { - name, err := p.parseObjectName() + // A ',' needs a data source after it: RELIES_ON (t1,) is PLS-00103. + name, err := p.parseUnitName() if err != nil { return nil, err } - if name == nil || name.Name == "" { - return nil, p.syntaxErrorAtCur() - } list.Items = append(list.Items, name) if p.cur.Type != ',' { break } p.advance() + if p.cur.Type == ')' { + return nil, p.syntaxErrorAtCur() + } } if p.cur.Type != ')' { return nil, p.syntaxErrorAtCur() diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 83078ee5..32940b93 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -53,6 +53,11 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE PROCEDURE p (a STRING CHARACTER SET ANY_CS, b CHARACTER CHARACTER SET ANY_CS, c my_subtype CHARACTER SET ANY_CS) IS BEGIN NULL; END;", "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET ANY_CS) IS c CONSTANT VARCHAR2(10) CHARACTER SET a%CHARSET NOT NULL := 'x'; SUBTYPE s IS VARCHAR2(10) CHARACTER SET a%CHARSET; TYPE r IS RECORD (f VARCHAR2(10) CHARACTER SET CHAR_CS); CURSOR cu (q VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", "CREATE FUNCTION f RETURN VARCHAR2 CHARACTER SET CHAR_CS IS BEGIN RETURN NULL; END;", + // A polymorphic table function may have a body (it compiles on + // Oracle 23ai), and an implementation type a dotted database link. + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN t PIPELINED USING impl@lnk.dom;", + "CREATE FUNCTION f RETURN pipelined.using IS BEGIN RETURN NULL; END;", // USING as a name, not an implementation clause. "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;", "CREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;", @@ -83,6 +88,8 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN t USING impl PIPELINED;", "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE PARALLEL_ENABLE USING impl;", "CREATE FUNCTION f RETURN t PIPELINED USING;", + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl.;", + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl@;", "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl", "CREATE FUNCTION f RETURN NUMBER USING impl;", // PLS-00624 "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE;", // PLS-00220 @@ -105,6 +112,24 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN NUMBER PARALLEL_ENABLE PARALLEL_ENABLE IS BEGIN RETURN 1; END;", // PLS-00371 "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO SQL_MACRO IS BEGIN RETURN 'x'; END;", // PLS-00371 "CREATE FUNCTION f RETURN NUMBER AUTHID DEFINER AUTHID DEFINER IS BEGIN RETURN 1; END;", // PLS-00371 + "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON (t1,) IS BEGIN RETURN 1; END;", + // Clause keywords are unquoted words. + "CREATE PROCEDURE p \"ACCESSIBLE\" BY (x) IS BEGIN NULL; END;", + "CREATE PROCEDURE p \"AUTHID\" DEFINER IS BEGIN NULL; END;", + "CREATE PACKAGE k \"AUTHID\" DEFINER AS PROCEDURE p; END;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED \"ROW\" POLYMORPHIC USING pkg;", + // AUTHID stands only on a schema-level unit (PLS-00157), ACCESSIBLE + // BY not on a type method (PLS-00262), DEFAULT COLLATION only on a + // schema-level unit (documented). + "CREATE PACKAGE k AS PROCEDURE p AUTHID DEFINER; END;", + "CREATE PACKAGE k AS FUNCTION f RETURN NUMBER AUTHID DEFINER; END;", + "CREATE PACKAGE k AS PROCEDURE p ACCESSIBLE BY (x) AUTHID DEFINER; END;", + "CREATE PROCEDURE p IS PROCEDURE q AUTHID DEFINER IS BEGIN NULL; END; BEGIN NULL; END;", + "DECLARE FUNCTION f RETURN NUMBER AUTHID CURRENT_USER IS BEGIN RETURN 1; END; BEGIN NULL; END;", + "CREATE PACKAGE k AS PROCEDURE p DEFAULT COLLATION USING_NLS_COMP; END;", + "CREATE PACKAGE k AS FUNCTION f RETURN NUMBER DEFAULT COLLATION USING_NLS_COMP; END;", + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER FUNCTION f RETURN NUMBER AUTHID DEFINER)", + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER FUNCTION f RETURN NUMBER ACCESSIBLE BY (x))", // CHARACTER SET. "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET) IS BEGIN NULL; END;", "CREATE PROCEDURE p (a VARCHAR2 CHARACTER SET a%TYPE) IS BEGIN NULL; END;", diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 5742c715..c58f65d2 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -164,3 +164,8 @@ ref_162 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_len_{run} (p VARC ref_163 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_ret_{run} RETURN NUMBER(5) IS BEGIN RETURN 1; END; constrained result type ref_164 plsql reject DECLARE CURSOR c (q VARCHAR2(10)) IS SELECT 1 FROM dual; BEGIN NULL; END; constrained cursor parameter type ref_165 plsql_body accept CREATE OR REPLACE PROCEDURE omni_unc_{run} (p TIMESTAMP WITH TIME ZONE, q INTERVAL DAY TO SECOND, r LONG RAW, s RAW) IS BEGIN NULL; END; unconstrained parameter types +ref_166 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_ro_{run} RETURN NUMBER RESULT_CACHE RELIES_ON (omni_t1,) IS BEGIN RETURN 1; END; RELIES_ON with a trailing comma +ref_167 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_qab_{run} "ACCESSIBLE" BY (omni_x) IS BEGIN NULL; END; quoted ACCESSIBLE is an identifier, not the clause +ref_168 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_impl_{run} (x NUMBER) RETURN NUMBER AGGREGATE USING omni_impl.; implementation type with an empty name part +ref_169 plsql_body accept CREATE OR REPLACE FUNCTION omni_ptfb_{run} (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC IS BEGIN RETURN NULL; END; polymorphic table function with a body +ref_170 plsql_body accept CREATE OR REPLACE FUNCTION omni_retdt_{run} RETURN pipelined.using IS BEGIN RETURN NULL; END; result datatype named pipelined.using From 56e084dedb0e66b72cc9e4b736b9104d318e7fac Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 08:20:17 +0000 Subject: [PATCH 04/22] fix(oracle): restrict polymorphic table function clauses, reserved unit names (BYT-10301) Review follow-up, each checked on Oracle 23ai: - A polymorphic table function takes no DETERMINISTIC, PARALLEL_ENABLE, ORDER BY, CLUSTER BY, or AUTHID (PLS-00760) and no RESULT_CACHE (PLS-00999, from its TABLE parameter), before or after PIPELINED ... POLYMORPHIC. ACCESSIBLE BY and an IS body compile. - ACCESSIBLE BY accessors, RELIES_ON data sources, and implementation type names are not PL/SQL reserved words: (SELECT), (BEGIN), and (sys.select) are PLS-00103; (loop) and "SELECT" compile. - DEFAULT COLLATION is recorded on procedure, function, and package statements. Reference rows ref_171 to ref_173. Co-Authored-By: Claude Opus 5.5 --- oracle/ast/outfuncs.go | 9 ++ oracle/ast/parsenodes.go | 103 +++++++++--------- oracle/parser/create_proc.go | 33 +++++- oracle/parser/subprogram_clause.go | 26 +++-- oracle/parser/subprogram_clause_test.go | 31 +++++- .../testdata/coverage/reference_oracle.tsv | 3 + 6 files changed, 138 insertions(+), 67 deletions(-) diff --git a/oracle/ast/outfuncs.go b/oracle/ast/outfuncs.go index cd751818..d43e054c 100644 --- a/oracle/ast/outfuncs.go +++ b/oracle/ast/outfuncs.go @@ -3236,6 +3236,9 @@ func writeCreatePackageStmt(sb *strings.Builder, n *CreatePackageStmt) { sb.WriteString(" :accessibleBy ") writeNode(sb, n.AccessibleBy) } + if n.DefaultCollation != "" { + sb.WriteString(fmt.Sprintf(" :defaultCollation %q", n.DefaultCollation)) + } if n.IsBody { sb.WriteString(" :isBody true") } @@ -3279,6 +3282,9 @@ func writeCreateProcedureStmt(sb *strings.Builder, n *CreateProcedureStmt) { sb.WriteString(" :accessibleBy ") writeNode(sb, n.AccessibleBy) } + if n.DefaultCollation != "" { + sb.WriteString(fmt.Sprintf(" :defaultCollation %q", n.DefaultCollation)) + } if n.Wrapped { sb.WriteString(" :wrapped true") } @@ -3373,6 +3379,9 @@ func writeCreateFunctionStmt(sb *strings.Builder, n *CreateFunctionStmt) { sb.WriteString(" :accessibleBy ") writeNode(sb, n.AccessibleBy) } + if n.DefaultCollation != "" { + sb.WriteString(fmt.Sprintf(" :defaultCollation %q", n.DefaultCollation)) + } if n.Body != nil { sb.WriteString(" :body ") writeNode(sb, n.Body) diff --git a/oracle/ast/parsenodes.go b/oracle/ast/parsenodes.go index 19922df4..6c6be1a7 100644 --- a/oracle/ast/parsenodes.go +++ b/oracle/ast/parsenodes.go @@ -1988,17 +1988,18 @@ func (n *TypeBodyMember) nodeTag() {} // CreatePackageStmt represents a CREATE PACKAGE statement. type CreatePackageStmt struct { - OrReplace bool // OR REPLACE - IfNotExists bool // IF NOT EXISTS - Editionable bool // EDITIONABLE - NonEditionable bool // NONEDITIONABLE - Sharing string // SHARING = METADATA | NONE - Name *ObjectName // package name - AuthID string // AUTHID CURRENT_USER | DEFINER - AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) - IsBody bool // PACKAGE BODY - Body *List // package body declarations - Loc Loc // start location + OrReplace bool // OR REPLACE + IfNotExists bool // IF NOT EXISTS + Editionable bool // EDITIONABLE + NonEditionable bool // NONEDITIONABLE + Sharing string // SHARING = METADATA | NONE + Name *ObjectName // package name + AuthID string // AUTHID CURRENT_USER | DEFINER + AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) + DefaultCollation string // DEFAULT COLLATION option + IsBody bool // PACKAGE BODY + Body *List // package body declarations + Loc Loc // start location } func (n *CreatePackageStmt) nodeTag() {} @@ -2010,20 +2011,21 @@ func (n *CreatePackageStmt) stmtNode() {} // CreateProcedureStmt represents a CREATE PROCEDURE statement. type CreateProcedureStmt struct { - OrReplace bool // OR REPLACE - IfNotExists bool // IF NOT EXISTS - Editionable bool // EDITIONABLE - NonEditionable bool // NONEDITIONABLE - Sharing string // SHARING = METADATA | NONE - AuthID string // AUTHID CURRENT_USER | DEFINER - Name *ObjectName // procedure name - Parameters *List // parameter list (list of *Parameter) - AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) - Wrapped bool // WRAPPED procedure body - WrappedSource string // raw text from WRAPPED through the wrapped payload - Body StmtNode // procedure body (PL/SQL block); nil for a call spec - CallSpec *CallSpec // call spec implementing the procedure in Java or C - Loc Loc // start location + OrReplace bool // OR REPLACE + IfNotExists bool // IF NOT EXISTS + Editionable bool // EDITIONABLE + NonEditionable bool // NONEDITIONABLE + Sharing string // SHARING = METADATA | NONE + AuthID string // AUTHID CURRENT_USER | DEFINER + Name *ObjectName // procedure name + Parameters *List // parameter list (list of *Parameter) + AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) + DefaultCollation string // DEFAULT COLLATION option + Wrapped bool // WRAPPED procedure body + WrappedSource string // raw text from WRAPPED through the wrapped payload + Body StmtNode // procedure body (PL/SQL block); nil for a call spec + CallSpec *CallSpec // call spec implementing the procedure in Java or C + Loc Loc // start location } func (n *CreateProcedureStmt) nodeTag() {} @@ -2061,31 +2063,32 @@ func (n *CallSpec) nodeTag() {} // CreateFunctionStmt represents a CREATE FUNCTION statement. type CreateFunctionStmt struct { - OrReplace bool // OR REPLACE - IfNotExists bool // IF NOT EXISTS - Editionable bool // EDITIONABLE - NonEditionable bool // NONEDITIONABLE - Sharing string // SHARING = METADATA | NONE - Name *ObjectName // function name - Parameters *List // parameter list (list of *Parameter) - ReturnType *TypeName // RETURN type - Deterministic bool // DETERMINISTIC - Pipelined bool // PIPELINED - Polymorphic string // PIPELINED ROW or TABLE POLYMORPHIC: ROW or TABLE - Parallel bool // PARALLEL_ENABLE - ParallelSpec *ParallelEnableClause // PARALLEL_ENABLE ( PARTITION ... ), nil without one - Streaming *List // ORDER and CLUSTER streaming clauses (list of *StreamingClause) - ResultCache bool // RESULT_CACHE - ReliesOn *List // RESULT_CACHE RELIES_ON data sources (list of *ObjectName) - Aggregate bool // AGGREGATE USING - Implementation *ObjectName // AGGREGATE or PIPELINED USING implementation type; the function then has no body - SqlMacro bool // SQL_MACRO - SqlMacroType string // SQL_MACRO ( SCALAR | TABLE ), "" without the parenthesized type - AuthID string // AUTHID CURRENT_USER | DEFINER - AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) - Body StmtNode // function body (PL/SQL block); nil for a call spec - CallSpec *CallSpec // call spec implementing the function in Java or C - Loc Loc // start location + OrReplace bool // OR REPLACE + IfNotExists bool // IF NOT EXISTS + Editionable bool // EDITIONABLE + NonEditionable bool // NONEDITIONABLE + Sharing string // SHARING = METADATA | NONE + Name *ObjectName // function name + Parameters *List // parameter list (list of *Parameter) + ReturnType *TypeName // RETURN type + Deterministic bool // DETERMINISTIC + Pipelined bool // PIPELINED + Polymorphic string // PIPELINED ROW or TABLE POLYMORPHIC: ROW or TABLE + Parallel bool // PARALLEL_ENABLE + ParallelSpec *ParallelEnableClause // PARALLEL_ENABLE ( PARTITION ... ), nil without one + Streaming *List // ORDER and CLUSTER streaming clauses (list of *StreamingClause) + ResultCache bool // RESULT_CACHE + ReliesOn *List // RESULT_CACHE RELIES_ON data sources (list of *ObjectName) + Aggregate bool // AGGREGATE USING + Implementation *ObjectName // AGGREGATE or PIPELINED USING implementation type; the function then has no body + SqlMacro bool // SQL_MACRO + SqlMacroType string // SQL_MACRO ( SCALAR | TABLE ), "" without the parenthesized type + AuthID string // AUTHID CURRENT_USER | DEFINER + AccessibleBy *List // ACCESSIBLE BY accessors (list of *Accessor) + DefaultCollation string // DEFAULT COLLATION option + Body StmtNode // function body (PL/SQL block); nil for a call spec + CallSpec *CallSpec // call spec implementing the function in Java or C + Loc Loc // start location } func (n *CreateFunctionStmt) nodeTag() {} diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index b79e6f11..34de6a95 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -259,12 +259,31 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit // limits AUTHID, ACCESSIBLE BY, and DEFAULT COLLATION as subprogramLevel says. func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level subprogramLevel) error { seen := clauseSeen{} + // notPolymorphic is the first clause a polymorphic table function does + // not take: Oracle 23ai rejects DETERMINISTIC, PARALLEL_ENABLE, ORDER + // BY, CLUSTER BY, and AUTHID on one with PLS-00760, and RESULT_CACHE + // with PLS-00999 (its TABLE parameter), before or after PIPELINED ... + // POLYMORPHIC. + var notPolymorphic *Token + note := func() { + if notPolymorphic == nil { + tok := p.cur + notPolymorphic = &tok + } + } + checkPolymorphic := func(err error) error { + if err == nil && stmt.Polymorphic != "" && notPolymorphic != nil { + return p.syntaxErrorAtTok(*notPolymorphic) + } + return err + } for { switch { case p.cur.Type == kwDETERMINISTIC: if err := p.firstClause(seen, "DETERMINISTIC"); err != nil { return err } + note() stmt.Deterministic = true p.advance() case p.cur.Type == kwPIPELINED: @@ -285,6 +304,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "PARALLEL_ENABLE"); err != nil { return err } + note() stmt.Parallel = true p.advance() if p.cur.Type == '(' { @@ -302,6 +322,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, kind); err != nil { return err } + note() clause, err := p.parseStreamingClause() if err != nil { return err @@ -314,6 +335,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "RESULT_CACHE"); err != nil { return err } + note() stmt.ResultCache = true p.advance() if p.isKeywordStr("RELIES_ON") { @@ -329,12 +351,12 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if p.cur.Type != kwUSING { return p.syntaxErrorAtCur() } - return p.parseImplementationType(stmt) + return checkPolymorphic(p.parseImplementationType(stmt)) case p.cur.Type == kwUSING: if !stmt.Pipelined { return p.syntaxErrorAtCur() } - return p.parseImplementationType(stmt) + return checkPolymorphic(p.parseImplementationType(stmt)) case p.isKeywordStr("SQL_MACRO"): if err := p.firstClause(seen, "SQL_MACRO"); err != nil { return err @@ -352,6 +374,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "AUTHID"); err != nil { return err } + note() authID, err := p.parseOptionalAuthID() if err != nil { return err @@ -370,11 +393,13 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "DEFAULT COLLATION"); err != nil { return err } - if err := p.parseDefaultCollationClause(); err != nil { + collation, err := p.parseDefaultCollationClause() + if err != nil { return err } + stmt.DefaultCollation = collation default: - return nil + return checkPolymorphic(nil) } } } diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 8b812b12..4660d32b 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -96,10 +96,13 @@ func (p *Parser) parseAccessibleByClause() (*nodes.List, error) { return list, nil } -// parseUnitName parses [ schema. ] name, without a database link. +// parseUnitName parses [ schema. ] name, without a database link. Neither +// part may be an unquoted PL/SQL reserved word: Oracle 23ai rejects +// ACCESSIBLE BY (SELECT), (BEGIN), and (sys.select), and RELIES_ON (BEGIN), +// with PLS-00103, and accepts (loop). func (p *Parser) parseUnitName() (*nodes.ObjectName, error) { start := p.pos() - if !p.isIdentLike() { + if !p.isPLSQLIdentifier() { return nil, p.syntaxErrorAtCur() } first, err := p.parseIdentifier() @@ -109,7 +112,7 @@ func (p *Parser) parseUnitName() (*nodes.ObjectName, error) { name := &nodes.ObjectName{Name: first, Loc: nodes.Loc{Start: start}} if p.cur.Type == '.' { p.advance() - if !p.isIdentLike() { + if !p.isPLSQLIdentifier() { return nil, p.syntaxErrorAtCur() } second, err := p.parseIdentifier() @@ -123,15 +126,14 @@ func (p *Parser) parseUnitName() (*nodes.ObjectName, error) { } // parseDefaultCollationClause parses DEFAULT COLLATION collation_option, the -// current token at DEFAULT. -func (p *Parser) parseDefaultCollationClause() error { +// current token at DEFAULT, and returns the option (USING_NLS_COMP). +func (p *Parser) parseDefaultCollationClause() (string, error) { p.advance() // consume DEFAULT p.advance() // consume COLLATION if !p.isIdentLike() { - return p.syntaxErrorAtCur() + return "", p.syntaxErrorAtCur() } - p.advance() // consume the collation option (USING_NLS_COMP) - return nil + return p.parseIdentifier() } // atDefaultCollation reports whether the current tokens are DEFAULT COLLATION. @@ -172,9 +174,11 @@ func (p *Parser) parseProcedureProperties(stmt *nodes.CreateProcedureStmt, level if err := p.firstClause(seen, "DEFAULT COLLATION"); err != nil { return err } - if err := p.parseDefaultCollationClause(); err != nil { + collation, err := p.parseDefaultCollationClause() + if err != nil { return err } + stmt.DefaultCollation = collation default: return nil } @@ -213,9 +217,11 @@ func (p *Parser) parsePackageProperties(stmt *nodes.CreatePackageStmt) error { if err := p.firstClause(seen, "DEFAULT COLLATION"); err != nil { return err } - if err := p.parseDefaultCollationClause(); err != nil { + collation, err := p.parseDefaultCollationClause() + if err != nil { return err } + stmt.DefaultCollation = collation default: return nil } diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 32940b93..59377e7b 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -57,6 +57,8 @@ func TestSubprogramHeadingClauses(t *testing.T) { // Oracle 23ai), and an implementation type a dotted database link. "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN t PIPELINED USING impl@lnk.dom;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC ACCESSIBLE BY (y) USING pkg;", + "CREATE PROCEDURE p ACCESSIBLE BY (loop, \"SELECT\") IS BEGIN NULL; END;", "CREATE FUNCTION f RETURN pipelined.using IS BEGIN RETURN NULL; END;", // USING as a name, not an implementation clause. "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;", @@ -113,6 +115,21 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO SQL_MACRO IS BEGIN RETURN 'x'; END;", // PLS-00371 "CREATE FUNCTION f RETURN NUMBER AUTHID DEFINER AUTHID DEFINER IS BEGIN RETURN 1; END;", // PLS-00371 "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON (t1,) IS BEGIN RETURN 1; END;", + // A polymorphic table function takes none of these (PLS-00760, and + // PLS-00999 for RESULT_CACHE), before or after POLYMORPHIC. + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC DETERMINISTIC USING pkg;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE DETERMINISTIC PIPELINED TABLE POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC PARALLEL_ENABLE USING pkg;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE AUTHID DEFINER PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC RESULT_CACHE USING pkg;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC ORDER t BY (a) USING pkg;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC DETERMINISTIC IS BEGIN RETURN NULL; END;", + // Names are not PL/SQL reserved words (PLS-00103). + "CREATE PROCEDURE p ACCESSIBLE BY (SELECT) IS BEGIN NULL; END;", + "CREATE PROCEDURE p ACCESSIBLE BY (BEGIN) IS BEGIN NULL; END;", + "CREATE PROCEDURE p ACCESSIBLE BY (PACKAGE SELECT) IS BEGIN NULL; END;", + "CREATE PROCEDURE p ACCESSIBLE BY (sys.select) IS BEGIN NULL; END;", + "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON (BEGIN) IS BEGIN RETURN 1; END;", // Clause keywords are unquoted words. "CREATE PROCEDURE p \"ACCESSIBLE\" BY (x) IS BEGIN NULL; END;", "CREATE PROCEDURE p \"AUTHID\" DEFINER IS BEGIN NULL; END;", @@ -177,9 +194,17 @@ func TestSubprogramHeadingClausesAST(t *testing.T) { t.Errorf("accessors = %+v %+v, want PACKAGE S.K and F", first, second) } - pkg := parseOne[*ast.CreatePackageStmt](t, "CREATE PACKAGE k AUTHID CURRENT_USER ACCESSIBLE BY (x) AS PROCEDURE p; END;") - if pkg.AuthID != "CURRENT_USER" || pkg.AccessibleBy == nil || pkg.AccessibleBy.Len() != 1 { - t.Errorf("package AuthID %q, AccessibleBy %v", pkg.AuthID, pkg.AccessibleBy) + pkg := parseOne[*ast.CreatePackageStmt](t, "CREATE PACKAGE k AUTHID CURRENT_USER ACCESSIBLE BY (x) DEFAULT COLLATION USING_NLS_COMP AS PROCEDURE p; END;") + if pkg.AuthID != "CURRENT_USER" || pkg.AccessibleBy == nil || pkg.AccessibleBy.Len() != 1 || pkg.DefaultCollation != "USING_NLS_COMP" { + t.Errorf("package AuthID %q, AccessibleBy %v, DefaultCollation %q", pkg.AuthID, pkg.AccessibleBy, pkg.DefaultCollation) + } + coll := parseOne[*ast.CreateProcedureStmt](t, "CREATE PROCEDURE p DEFAULT COLLATION USING_NLS_COMP IS BEGIN NULL; END;") + if coll.DefaultCollation != "USING_NLS_COMP" { + t.Errorf("procedure DefaultCollation = %q, want USING_NLS_COMP", coll.DefaultCollation) + } + fcoll := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f RETURN NUMBER DEFAULT COLLATION USING_NLS_COMP IS BEGIN RETURN 1; END;") + if fcoll.DefaultCollation != "USING_NLS_COMP" { + t.Errorf("function DefaultCollation = %q, want USING_NLS_COMP", fcoll.DefaultCollation) } agg := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING s.impl;") diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index c58f65d2..d7e44d9a 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -169,3 +169,6 @@ ref_167 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_qab_{run} "ACCESS ref_168 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_impl_{run} (x NUMBER) RETURN NUMBER AGGREGATE USING omni_impl.; implementation type with an empty name part ref_169 plsql_body accept CREATE OR REPLACE FUNCTION omni_ptfb_{run} (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC IS BEGIN RETURN NULL; END; polymorphic table function with a body ref_170 plsql_body accept CREATE OR REPLACE FUNCTION omni_retdt_{run} RETURN pipelined.using IS BEGIN RETURN NULL; END; result datatype named pipelined.using +ref_171 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_abr_{run} ACCESSIBLE BY (SELECT) IS BEGIN NULL; END; reserved word as an accessor +ref_172 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_ror_{run} RETURN NUMBER RESULT_CACHE RELIES_ON (BEGIN) IS BEGIN RETURN 1; END; PL/SQL reserved word as a RELIES_ON data source +ref_173 plsql_body accept CREATE OR REPLACE PROCEDURE omni_abn_{run} ACCESSIBLE BY (loop, "SELECT") IS BEGIN NULL; END; unreserved and quoted accessor names From 0b01856e172c12329367d7c935209df49ec5b248 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 08:33:14 +0000 Subject: [PATCH 05/22] fix(oracle): nested subprogram and SQL macro clause rules, aggregate arity (BYT-10301) Review follow-up, each checked on Oracle 23ai: - ACCESSIBLE BY stands on schema-level and package subprograms only; a nested one is PLS-00262 or PLS-00263. - A SQL macro takes no DETERMINISTIC, PARALLEL_ENABLE, RESULT_CACHE, AUTHID, or streaming clause (PLS-00778) and no PIPELINED (PLS-00630), and returns a character type: NUMBER, LONG, or %ROWTYPE is PLS-00776, NVARCHAR2, CLOB, CHAR, and STRING compile. - An aggregate needs a parameter (PLS-00652); several, or IN OUT, compile against a real ODCIAggregate type. - %ROWTYPE takes no CHARACTER SET (PLS-00550). - DEFAULT COLLATION takes USING_NLS_COMP unquoted, as documented. Reference rows ref_174 and ref_175. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 81 ++++++++++++++++--- oracle/parser/plsql_block.go | 4 +- oracle/parser/split_test.go | 2 + oracle/parser/subprogram_clause.go | 27 +++++-- oracle/parser/subprogram_clause_test.go | 41 +++++++++- .../testdata/coverage/reference_oracle.tsv | 2 + oracle/parser/type.go | 6 +- 7 files changed, 137 insertions(+), 26 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 34de6a95..bbb18d9b 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -264,18 +264,34 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s // BY, CLUSTER BY, and AUTHID on one with PLS-00760, and RESULT_CACHE // with PLS-00999 (its TABLE parameter), before or after PIPELINED ... // POLYMORPHIC. - var notPolymorphic *Token + // + // notMacro is the first clause a SQL macro does not take: those, and + // PIPELINED, whose collection result a macro cannot return (PLS-00778, + // PLS-00630). A macro returns a character type: NUMBER, LONG, or a + // %ROWTYPE result is PLS-00776, while CHAR, VARCHAR2, CLOB, and the + // national types compile. + var notPolymorphic, notMacro, macroTok *Token note := func() { + tok := p.cur if notPolymorphic == nil { - tok := p.cur notPolymorphic = &tok } + if notMacro == nil { + notMacro = &tok + } } checkPolymorphic := func(err error) error { - if err == nil && stmt.Polymorphic != "" && notPolymorphic != nil { + switch { + case err != nil: + return err + case stmt.Polymorphic != "" && notPolymorphic != nil: return p.syntaxErrorAtTok(*notPolymorphic) + case stmt.SqlMacro && notMacro != nil: + return p.syntaxErrorAtTok(*notMacro) + case stmt.SqlMacro && !isMacroResultType(stmt.ReturnType): + return p.syntaxErrorAtTok(*macroTok) } - return err + return nil } for { switch { @@ -290,6 +306,10 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "PIPELINED"); err != nil { return err } + if notMacro == nil { + tok := p.cur + notMacro = &tok + } stmt.Pipelined = true p.advance() if (p.cur.Type == kwROW || p.cur.Type == kwTABLE) && p.isIdentLikeStrAt(p.peekNext(), "POLYMORPHIC") { @@ -346,12 +366,22 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s stmt.ReliesOn = list } case p.isKeywordStr("AGGREGATE"): + aggTok := p.cur stmt.Aggregate = true p.advance() // consume AGGREGATE if p.cur.Type != kwUSING { return p.syntaxErrorAtCur() } - return checkPolymorphic(p.parseImplementationType(stmt)) + if err := p.parseImplementationType(stmt); err != nil { + return err + } + // An aggregate takes an argument: Oracle 23ai rejects one + // without parameters (PLS-00652) and compiles one with two, or + // with an OUT parameter, against its implementation type. + if stmt.Parameters == nil || stmt.Parameters.Len() == 0 { + return p.syntaxErrorAtTok(aggTok) + } + return checkPolymorphic(nil) case p.cur.Type == kwUSING: if !stmt.Pipelined { return p.syntaxErrorAtCur() @@ -361,6 +391,8 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "SQL_MACRO"); err != nil { return err } + tok := p.cur + macroTok = &tok stmt.SqlMacro = true p.advance() // consume SQL_MACRO if p.cur.Type == '(' { @@ -380,7 +412,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s return err } stmt.AuthID = authID - case p.isKeywordStr("ACCESSIBLE") && level != subprogramMethod: + case p.isKeywordStr("ACCESSIBLE") && level.takesAccessibleBy(): if err := p.firstClause(seen, "ACCESSIBLE BY"); err != nil { return err } @@ -404,6 +436,31 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s } } +// isMacroResultType reports whether tn may be a SQL macro's result type: not +// a predefined non-character type (the national character types are +// character types here) and not a %ROWTYPE record. A name the text does not +// settle is left to the engine. +func isMacroResultType(tn *nodes.TypeName) bool { + if tn == nil { + return true + } + if tn.IsPercRowtype { + return false + } + if tn.IsPercType || tn.Names.Len() != 1 { + return true + } + first, ok := tn.Names.Items[0].(*nodes.String) + if !ok { + return true + } + switch first.Str { + case "NCHAR", "NVARCHAR2", "NCLOB": + return true + } + return !plsqlNonCharacterTypes[first.Str] +} + // parseImplementationType parses USING [ schema. ] implementation_type // [ @dblink ], the current token at USING. Every part needs its name: Oracle // 23ai rejects USING impl. and USING impl@ (PLS-00103), and takes a dotted @@ -585,7 +642,7 @@ func (p *Parser) parsePackageBody() (*nodes.List, error) { for p.cur.Type != kwEND && p.cur.Type != tokEOF { // PROCEDURE declaration/definition in package if p.cur.Type == kwPROCEDURE { - decl, parseErr470 := p.parsePackageProcDecl() + decl, parseErr470 := p.parsePackageProcDecl(subprogramPackaged) if parseErr470 != nil { return nil, parseErr470 } @@ -597,7 +654,7 @@ func (p *Parser) parsePackageBody() (*nodes.List, error) { // FUNCTION declaration/definition in package if p.cur.Type == kwFUNCTION { - decl, parseErr471 := p.parsePackageFuncDecl() + decl, parseErr471 := p.parsePackageFuncDecl(subprogramPackaged) if parseErr471 != nil { return nil, parseErr471 } @@ -631,7 +688,7 @@ func (p *Parser) parsePackageBody() (*nodes.List, error) { // // PROCEDURE name [(params)] ; -- specification // PROCEDURE name [(params)] IS|AS body ; -- body definition -func (p *Parser) parsePackageProcDecl() (*nodes.CreateProcedureStmt, error) { +func (p *Parser) parsePackageProcDecl(level subprogramLevel) (*nodes.CreateProcedureStmt, error) { start := p.pos() p.advance() // consume PROCEDURE @@ -658,7 +715,7 @@ func (p *Parser) parsePackageProcDecl() (*nodes.CreateProcedureStmt, error) { return nil, parseErr474 } } - if err := p.parseProcedureProperties(stmt, subprogramPackaged); err != nil { + if err := p.parseProcedureProperties(stmt, level); err != nil { return nil, err } @@ -683,7 +740,7 @@ func (p *Parser) parsePackageProcDecl() (*nodes.CreateProcedureStmt, error) { // // FUNCTION name [(params)] RETURN type ; -- specification // FUNCTION name [(params)] RETURN type IS|AS body ; -- body definition -func (p *Parser) parsePackageFuncDecl() (*nodes.CreateFunctionStmt, error) { +func (p *Parser) parsePackageFuncDecl(level subprogramLevel) (*nodes.CreateFunctionStmt, error) { start := p.pos() p.advance() // consume FUNCTION @@ -727,7 +784,7 @@ func (p *Parser) parsePackageFuncDecl() (*nodes.CreateFunctionStmt, error) { } else { return nil, p.syntaxErrorAtCur() } - parseErr479 := p.parseFunctionProperties(stmt, subprogramPackaged) + parseErr479 := p.parseFunctionProperties(stmt, level) if parseErr479 != // Check for IS|AS (definition) or ; (declaration) diff --git a/oracle/parser/plsql_block.go b/oracle/parser/plsql_block.go index 001dcc09..b98119e5 100644 --- a/oracle/parser/plsql_block.go +++ b/oracle/parser/plsql_block.go @@ -152,10 +152,10 @@ func (p *Parser) parsePLSQLDeclaration() (nodes.Node, error) { // item declarations to precede subprograms (PLS-00103); order is not // enforced here. if p.cur.Type == kwPROCEDURE { - return p.parsePackageProcDecl() + return p.parsePackageProcDecl(subprogramNested) } if p.cur.Type == kwFUNCTION { - return p.parsePackageFuncDecl() + return p.parsePackageFuncDecl(subprogramNested) } // Variable declaration: name [CONSTANT] type [NOT NULL] [:= | DEFAULT expr] ; diff --git a/oracle/parser/split_test.go b/oracle/parser/split_test.go index ea75ea88..88dffca8 100644 --- a/oracle/parser/split_test.go +++ b/oracle/parser/split_test.go @@ -442,11 +442,13 @@ func TestSplitPLSQLBlocks(t *testing.T) { sql: "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;\n" + "CREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;\n" + "CREATE FUNCTION h RETURN pipelined.using IS BEGIN RETURN NULL; END;\n" + + "CREATE FUNCTION k RETURN t PIPELINED DEFAULT COLLATION USING_NLS_COMP IS BEGIN RETURN; END;\n" + "SELECT 1 FROM dual;", want: []string{ "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;", "\nCREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;", "\nCREATE FUNCTION h RETURN pipelined.using IS BEGIN RETURN NULL; END;", + "\nCREATE FUNCTION k RETURN t PIPELINED DEFAULT COLLATION USING_NLS_COMP IS BEGIN RETURN; END;", "\nSELECT 1 FROM dual", }, }, diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 4660d32b..4d3a496f 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -16,18 +16,25 @@ type clauseSeen map[string]bool // subprogramLevel says where a subprogram heading stands, which decides the // clauses it takes. On Oracle 23ai AUTHID stands only on a schema-level unit // (PLS-00157 on a packaged, nested, or object type method), and ACCESSIBLE -// BY not on an object type method (PLS-00262). DEFAULT COLLATION is -// documented for schema-level units only. +// BY only on a schema-level unit or a package subprogram (PLS-00262 on a +// type method, PLS-00262 or PLS-00263 on a nested subprogram). DEFAULT +// COLLATION is documented for schema-level units only. // // Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/DEFAULT-COLLATION-clause.html type subprogramLevel int const ( subprogramSchema subprogramLevel = iota // CREATE PROCEDURE or FUNCTION - subprogramPackaged // in a package, or nested in a block + subprogramPackaged // in a package specification or body + subprogramNested // declared in a block or another subprogram subprogramMethod // an object type method ) +// takesAccessibleBy reports whether a heading at level takes ACCESSIBLE BY. +func (l subprogramLevel) takesAccessibleBy() bool { + return l == subprogramSchema || l == subprogramPackaged +} + // firstClause reports a syntax error at the current token, which starts clause, // when the heading already has that clause. func (p *Parser) firstClause(seen clauseSeen, clause string) error { @@ -125,15 +132,19 @@ func (p *Parser) parseUnitName() (*nodes.ObjectName, error) { return name, nil } -// parseDefaultCollationClause parses DEFAULT COLLATION collation_option, the -// current token at DEFAULT, and returns the option (USING_NLS_COMP). +// parseDefaultCollationClause parses DEFAULT COLLATION USING_NLS_COMP, the +// current token at DEFAULT, and returns the option. USING_NLS_COMP, unquoted, +// is the one option a PL/SQL unit takes. +// +// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/DEFAULT-COLLATION-clause.html func (p *Parser) parseDefaultCollationClause() (string, error) { p.advance() // consume DEFAULT p.advance() // consume COLLATION - if !p.isIdentLike() { + if !p.isKeywordStr("USING_NLS_COMP") { return "", p.syntaxErrorAtCur() } - return p.parseIdentifier() + p.advance() + return "USING_NLS_COMP", nil } // atDefaultCollation reports whether the current tokens are DEFAULT COLLATION. @@ -161,7 +172,7 @@ func (p *Parser) parseProcedureProperties(stmt *nodes.CreateProcedureStmt, level return err } stmt.AuthID = authID - case p.isKeywordStr("ACCESSIBLE") && level != subprogramMethod: + case p.isKeywordStr("ACCESSIBLE") && level.takesAccessibleBy(): if err := p.firstClause(seen, "ACCESSIBLE BY"); err != nil { return err } diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 59377e7b..bb59a89c 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -59,6 +59,14 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN t PIPELINED USING impl@lnk.dom;", "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC ACCESSIBLE BY (y) USING pkg;", "CREATE PROCEDURE p ACCESSIBLE BY (loop, \"SELECT\") IS BEGIN NULL; END;", + // A SQL macro returns a character type, national ones included. + "CREATE FUNCTION f RETURN NVARCHAR2 SQL_MACRO IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN CLOB SQL_MACRO IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN CHAR SQL_MACRO (SCALAR) IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN STRING SQL_MACRO ACCESSIBLE BY (x) IS BEGIN RETURN NULL; END;", + // An aggregate compiles with several or OUT parameters. + "CREATE FUNCTION f (a NUMBER, b NUMBER) RETURN NUMBER AGGREGATE USING impl;", + "CREATE FUNCTION f (a IN OUT NUMBER) RETURN NUMBER AGGREGATE USING impl;", "CREATE FUNCTION f RETURN pipelined.using IS BEGIN RETURN NULL; END;", // USING as a name, not an implementation clause. "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;", @@ -124,6 +132,29 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC RESULT_CACHE USING pkg;", "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC ORDER t BY (a) USING pkg;", "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC DETERMINISTIC IS BEGIN RETURN NULL; END;", + // A SQL macro takes no DETERMINISTIC, PARALLEL_ENABLE, RESULT_CACHE, + // AUTHID, or streaming clause (PLS-00778), no PIPELINED (PLS-00630), + // and returns a character type (PLS-00776). + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO DETERMINISTIC IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN VARCHAR2 DETERMINISTIC SQL_MACRO IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO PARALLEL_ENABLE IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO RESULT_CACHE IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO AUTHID DEFINER IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN VARCHAR2 SQL_MACRO ORDER c BY (a) IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO PIPELINED IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN NUMBER SQL_MACRO IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f RETURN LONG SQL_MACRO IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN dual%ROWTYPE SQL_MACRO IS BEGIN RETURN NULL; END;", + // An aggregate takes an argument (PLS-00652). + "CREATE FUNCTION f RETURN NUMBER AGGREGATE USING impl;", + // ACCESSIBLE BY is not for a nested subprogram (PLS-00262, PLS-00263). + "DECLARE PROCEDURE p ACCESSIBLE BY (x) IS BEGIN NULL; END; BEGIN NULL; END;", + "CREATE PROCEDURE p IS FUNCTION f RETURN NUMBER ACCESSIBLE BY (x) IS BEGIN RETURN 1; END; BEGIN NULL; END;", + // DEFAULT COLLATION takes USING_NLS_COMP unquoted (documented). + "CREATE PROCEDURE p DEFAULT COLLATION BINARY IS BEGIN NULL; END;", + "CREATE PROCEDURE p DEFAULT COLLATION \"USING_NLS_COMP\" IS BEGIN NULL; END;", + // A %ROWTYPE record takes no character set (PLS-00550). + "CREATE PROCEDURE p (a dual%ROWTYPE CHARACTER SET ANY_CS) IS BEGIN NULL; END;", // Names are not PL/SQL reserved words (PLS-00103). "CREATE PROCEDURE p ACCESSIBLE BY (SELECT) IS BEGIN NULL; END;", "CREATE PROCEDURE p ACCESSIBLE BY (BEGIN) IS BEGIN NULL; END;", @@ -217,7 +248,7 @@ func TestSubprogramHeadingClausesAST(t *testing.T) { t.Errorf("polymorphic function = %+v", ptf) } - par := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) CLUSTER c BY (a) RESULT_CACHE RELIES_ON (t1) SQL_MACRO (TYPE => TABLE) IS BEGIN RETURN; END;") + par := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) CLUSTER c BY (a) RESULT_CACHE RELIES_ON (t1) IS BEGIN RETURN; END;") if spec := par.ParallelSpec; spec == nil || spec.Argument != "C" || spec.PartitionBy != "HASH" || spec.Columns.Len() != 2 { t.Errorf("parallel spec = %+v", par.ParallelSpec) } @@ -227,8 +258,12 @@ func TestSubprogramHeadingClausesAST(t *testing.T) { if s := par.Streaming.Items[0].(*ast.StreamingClause); s.Kind != "CLUSTER" || s.Argument != "C" || s.Columns.Len() != 1 { t.Errorf("streaming clause = %+v", s) } - if par.ReliesOn == nil || par.ReliesOn.Len() != 1 || par.SqlMacroType != "TABLE" { - t.Errorf("RELIES_ON %v, SQL_MACRO type %q", par.ReliesOn, par.SqlMacroType) + if par.ReliesOn == nil || par.ReliesOn.Len() != 1 { + t.Errorf("RELIES_ON %v", par.ReliesOn) + } + macro := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO (TYPE => TABLE) IS BEGIN RETURN 'x'; END;") + if !macro.SqlMacro || macro.SqlMacroType != "TABLE" { + t.Errorf("SQL_MACRO %v type %q", macro.SqlMacro, macro.SqlMacroType) } cs := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (a VARCHAR2 CHARACTER SET ANY_CS) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN a; END;") diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index d7e44d9a..5800be49 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -172,3 +172,5 @@ ref_170 plsql_body accept CREATE OR REPLACE FUNCTION omni_retdt_{run} RETURN pip ref_171 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_abr_{run} ACCESSIBLE BY (SELECT) IS BEGIN NULL; END; reserved word as an accessor ref_172 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_ror_{run} RETURN NUMBER RESULT_CACHE RELIES_ON (BEGIN) IS BEGIN RETURN 1; END; PL/SQL reserved word as a RELIES_ON data source ref_173 plsql_body accept CREATE OR REPLACE PROCEDURE omni_abn_{run} ACCESSIBLE BY (loop, "SELECT") IS BEGIN NULL; END; unreserved and quoted accessor names +ref_174 plsql_body accept CREATE OR REPLACE FUNCTION omni_agg2_{run} (a NUMBER, b IN OUT NUMBER) RETURN NUMBER AGGREGATE USING omni_impl; aggregate with several parameters +ref_175 plsql_body accept CREATE OR REPLACE FUNCTION omni_smn_{run} RETURN NVARCHAR2 SQL_MACRO IS BEGIN RETURN NULL; END; SQL macro returning a national character type diff --git a/oracle/parser/type.go b/oracle/parser/type.go index f3c001c6..0fd2623e 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -232,7 +232,11 @@ var plsqlNonCharacterTypes = map[string]bool{ // follows from the text alone and was confirmed on Oracle 23ai; whether a // name is a known character set (PLS-00553) is left to the engine. func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) error { - if !tn.IsPercType && !tn.IsPercRowtype && tn.Names.Len() > 0 { + // A %ROWTYPE record is never a character type (PLS-00550). + if tn.IsPercRowtype { + return p.syntaxErrorAtCur() + } + if !tn.IsPercType && tn.Names.Len() > 0 { if first, ok := tn.Names.Items[0].(*nodes.String); ok && plsqlNonCharacterTypes[first.Str] { return p.syntaxErrorAtCur() } From 3eced1a412eaff9b77770039650d1729701a5d27 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 08:44:14 +0000 Subject: [PATCH 06/22] fix(oracle): PIPELINED excludes AGGREGATE, no polymorphic type methods (BYT-10301) Review follow-up, each checked on Oracle 23ai: - PIPELINED and AGGREGATE exclude each other (PLS-00371) unless the function is polymorphic: PIPELINED ROW POLYMORPHIC AGGREGATE USING and SQL_MACRO AGGREGATE USING compile. - An object type method is not polymorphic (PLS-00765); a nested polymorphic table function compiles, in a stored unit and an anonymous block alike. PIPELINED POLYMORPHIC without ROW or TABLE stays rejected (PLS-00103). Reference rows ref_176 to ref_178. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 12 ++++++++++++ oracle/parser/subprogram_clause_test.go | 11 +++++++++++ oracle/parser/testdata/coverage/reference_oracle.tsv | 3 +++ 3 files changed, 26 insertions(+) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index bbb18d9b..6320ac86 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -313,6 +313,12 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s stmt.Pipelined = true p.advance() if (p.cur.Type == kwROW || p.cur.Type == kwTABLE) && p.isIdentLikeStrAt(p.peekNext(), "POLYMORPHIC") { + // The row kind is required (PIPELINED POLYMORPHIC is + // PLS-00103), and an object type method is not polymorphic + // (PLS-00765); a nested function may be. + if level == subprogramMethod { + return p.syntaxErrorAtCur() + } stmt.Polymorphic = "ROW" if p.cur.Type == kwTABLE { stmt.Polymorphic = "TABLE" @@ -367,6 +373,12 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s } case p.isKeywordStr("AGGREGATE"): aggTok := p.cur + // PIPELINED and AGGREGATE exclude each other (PLS-00371), unless + // the function is polymorphic: Oracle 23ai compiles PIPELINED + // ROW POLYMORPHIC AGGREGATE USING type, and SQL_MACRO with it. + if stmt.Pipelined && stmt.Polymorphic == "" { + return p.syntaxErrorAtCur() + } stmt.Aggregate = true p.advance() // consume AGGREGATE if p.cur.Type != kwUSING { diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index bb59a89c..7fd6aa87 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -64,6 +64,12 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN CLOB SQL_MACRO IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN CHAR SQL_MACRO (SCALAR) IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN STRING SQL_MACRO ACCESSIBLE BY (x) IS BEGIN RETURN NULL; END;", + // Oracle 23ai compiles these combinations and nested polymorphic + // table functions. + "CREATE FUNCTION f (a VARCHAR2) RETURN VARCHAR2 SQL_MACRO AGGREGATE USING impl;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC AGGREGATE USING pkg;", + "CREATE PROCEDURE p IS FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC IS BEGIN RETURN NULL; END; BEGIN NULL; END;", + "DECLARE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg; BEGIN NULL; END;", // An aggregate compiles with several or OUT parameters. "CREATE FUNCTION f (a NUMBER, b NUMBER) RETURN NUMBER AGGREGATE USING impl;", "CREATE FUNCTION f (a IN OUT NUMBER) RETURN NUMBER AGGREGATE USING impl;", @@ -145,6 +151,11 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN NUMBER SQL_MACRO IS BEGIN RETURN 1; END;", "CREATE FUNCTION f RETURN LONG SQL_MACRO IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN dual%ROWTYPE SQL_MACRO IS BEGIN RETURN NULL; END;", + // The row kind is required (PLS-00103); a type method is not + // polymorphic (PLS-00765); PIPELINED excludes AGGREGATE (PLS-00371). + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED POLYMORPHIC USING pkg;", + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC)", + "CREATE FUNCTION f (a NUMBER) RETURN NUMBER PIPELINED AGGREGATE USING impl;", // An aggregate takes an argument (PLS-00652). "CREATE FUNCTION f RETURN NUMBER AGGREGATE USING impl;", // ACCESSIBLE BY is not for a nested subprogram (PLS-00262, PLS-00263). diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 5800be49..134f0011 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -174,3 +174,6 @@ ref_172 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_ror_{run} RETURN N ref_173 plsql_body accept CREATE OR REPLACE PROCEDURE omni_abn_{run} ACCESSIBLE BY (loop, "SELECT") IS BEGIN NULL; END; unreserved and quoted accessor names ref_174 plsql_body accept CREATE OR REPLACE FUNCTION omni_agg2_{run} (a NUMBER, b IN OUT NUMBER) RETURN NUMBER AGGREGATE USING omni_impl; aggregate with several parameters ref_175 plsql_body accept CREATE OR REPLACE FUNCTION omni_smn_{run} RETURN NVARCHAR2 SQL_MACRO IS BEGIN RETURN NULL; END; SQL macro returning a national character type +ref_176 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_ptk_{run} (t TABLE) RETURN TABLE PIPELINED POLYMORPHIC USING omni_pkg; POLYMORPHIC without its row kind +ref_177 plsql accept DECLARE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC IS BEGIN RETURN NULL; END; BEGIN NULL; END; nested polymorphic table function +ref_178 plsql_body accept CREATE OR REPLACE FUNCTION omni_sma_{run} (a VARCHAR2) RETURN VARCHAR2 SQL_MACRO AGGREGATE USING omni_impl; SQL macro with AGGREGATE USING From 78b65a75b1690f2fdbddc81b360743e5fae829c2 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 08:58:47 +0000 Subject: [PATCH 07/22] fix(oracle): check PARALLEL_ENABLE arguments, TABLE pseudo-type, method macros (BYT-10301) Review follow-up, each checked on Oracle 23ai with real types behind the names (a strong REF CURSOR, a collection, a PTF implementation package): - The PARALLEL_ENABLE partition argument is an IN parameter of the function (PLS-00626, PLS-00625). An ORDER or CLUSTER BY clause needs PARALLEL_ENABLE with PARTITION BY (PLS-00654, PLS-00665) on the same argument (PLS-00631). An earlier probe masked by an unresolved return type had suggested the streaming clause stood alone. - The TABLE pseudo-type stands only in a polymorphic table function (PLS-00765): not in a procedure, an ordinary function, or a SQL macro. - An object type method is not a SQL macro (PLS-00781). A nested function may be an aggregate (it compiles), so it stays accepted. ref_152 now partitions a declared parameter. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 25 ++++- oracle/parser/subprogram_clause.go | 97 ++++++++++++++++--- oracle/parser/subprogram_clause_test.go | 28 +++++- .../testdata/coverage/loc_node_coverage.tsv | 2 +- .../testdata/coverage/reference_oracle.tsv | 2 +- 5 files changed, 132 insertions(+), 22 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 6320ac86..d9332dd4 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -62,6 +62,9 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi } } + if tn := tablePseudoType(stmt.Parameters, nil); tn != nil { + return nil, p.syntaxErrorAtType(tn) + } if err := p.parseProcedureProperties(stmt, subprogramSchema); err != nil { return nil, err } @@ -271,6 +274,8 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s // %ROWTYPE result is PLS-00776, while CHAR, VARCHAR2, CLOB, and the // national types compile. var notPolymorphic, notMacro, macroTok *Token + var partTok Token + var streamToks []Token note := func() { tok := p.cur if notPolymorphic == nil { @@ -291,7 +296,12 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s case stmt.SqlMacro && !isMacroResultType(stmt.ReturnType): return p.syntaxErrorAtTok(*macroTok) } - return nil + if stmt.Polymorphic == "" { + if tn := tablePseudoType(stmt.Parameters, stmt.ReturnType); tn != nil { + return p.syntaxErrorAtType(tn) + } + } + return p.checkParallelArguments(stmt, partTok, streamToks) } for { switch { @@ -334,11 +344,12 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s stmt.Parallel = true p.advance() if p.cur.Type == '(' { - spec, err := p.parseParallelEnableSpec() + spec, argTok, err := p.parseParallelEnableSpec() if err != nil { return err } stmt.ParallelSpec = spec + partTok = argTok } case p.cur.Type == kwORDER, p.cur.Type == kwCLUSTER: kind := "ORDER BY" @@ -349,10 +360,11 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s return err } note() - clause, err := p.parseStreamingClause() + clause, argTok, err := p.parseStreamingClause() if err != nil { return err } + streamToks = append(streamToks, argTok) if stmt.Streaming == nil { stmt.Streaming = &nodes.List{} } @@ -403,6 +415,10 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "SQL_MACRO"); err != nil { return err } + // A type method is not a SQL macro (PLS-00781). + if level == subprogramMethod { + return p.syntaxErrorAtCur() + } tok := p.cur macroTok = &tok stmt.SqlMacro = true @@ -727,6 +743,9 @@ func (p *Parser) parsePackageProcDecl(level subprogramLevel) (*nodes.CreateProce return nil, parseErr474 } } + if tn := tablePseudoType(stmt.Parameters, nil); tn != nil { + return nil, p.syntaxErrorAtType(tn) + } if err := p.parseProcedureProperties(stmt, level); err != nil { return nil, err } diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 4d3a496f..a3fda67a 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -249,23 +249,24 @@ func (p *Parser) parsePackageProperties(stmt *nodes.CreatePackageStmt) error { // Oracle 23ai rejects an empty partitioning, PARTITION without BY, other // methods (LIST), and an empty column list with PLS-00103. That VALUE takes a // single column is checked when the unit compiles. -func (p *Parser) parseParallelEnableSpec() (*nodes.ParallelEnableClause, error) { +func (p *Parser) parseParallelEnableSpec() (*nodes.ParallelEnableClause, Token, error) { spec := &nodes.ParallelEnableClause{Loc: nodes.Loc{Start: p.pos()}} p.advance() // consume '(' if p.cur.Type != kwPARTITION { - return nil, p.syntaxErrorAtCur() + return nil, Token{}, p.syntaxErrorAtCur() } p.advance() if !p.isIdentLike() { - return nil, p.syntaxErrorAtCur() + return nil, Token{}, p.syntaxErrorAtCur() } + argTok := p.cur arg, err := p.parseIdentifier() if err != nil { - return nil, err + return nil, Token{}, err } spec.Argument = arg if p.cur.Type != kwBY { - return nil, p.syntaxErrorAtCur() + return nil, Token{}, p.syntaxErrorAtCur() } p.advance() switch { @@ -282,49 +283,115 @@ func (p *Parser) parseParallelEnableSpec() (*nodes.ParallelEnableClause, error) p.advance() cols, err := p.parseParenExprList() if err != nil { - return nil, err + return nil, Token{}, err } spec.Columns = cols default: - return nil, p.syntaxErrorAtCur() + return nil, Token{}, p.syntaxErrorAtCur() } if p.cur.Type != ')' { - return nil, p.syntaxErrorAtCur() + return nil, Token{}, p.syntaxErrorAtCur() } p.advance() spec.Loc.End = p.prev.End - return spec, nil + return spec, argTok, nil } // parseStreamingClause parses { ORDER | CLUSTER } argument BY ( expr [, expr ]... ), // the current token at ORDER or CLUSTER. Oracle 23ai takes it anywhere in a // function's clause list, with or without PARALLEL_ENABLE, and requires a // plain parameter name (ORDER c.x BY is PLS-00103). -func (p *Parser) parseStreamingClause() (*nodes.StreamingClause, error) { +func (p *Parser) parseStreamingClause() (*nodes.StreamingClause, Token, error) { clause := &nodes.StreamingClause{Kind: "ORDER", Loc: nodes.Loc{Start: p.pos()}} if p.cur.Type == kwCLUSTER { clause.Kind = "CLUSTER" } p.advance() if !p.isIdentLike() { - return nil, p.syntaxErrorAtCur() + return nil, Token{}, p.syntaxErrorAtCur() } + argTok := p.cur arg, err := p.parseIdentifier() if err != nil { - return nil, err + return nil, Token{}, err } clause.Argument = arg if p.cur.Type != kwBY { - return nil, p.syntaxErrorAtCur() + return nil, Token{}, p.syntaxErrorAtCur() } p.advance() cols, err := p.parseParenExprList() if err != nil { - return nil, err + return nil, Token{}, err } clause.Columns = cols clause.Loc.End = p.prev.End - return clause, nil + return clause, argTok, nil +} + +// checkParallelArguments checks the arguments PARALLEL_ENABLE partitions and +// the streaming clauses order or cluster, each from the function's own text +// as Oracle 23ai does: the partitioned argument is one of the function's IN +// parameters (PLS-00626, PLS-00625); a streaming clause needs a PARTITION BY +// (PLS-00654, PLS-00665) on the same argument (PLS-00631). partTok and +// streamToks are the argument tokens, for the error position. +func (p *Parser) checkParallelArguments(stmt *nodes.CreateFunctionStmt, partTok Token, streamToks []Token) error { + if stmt.ParallelSpec == nil { + if len(streamToks) > 0 { + return p.syntaxErrorAtTok(streamToks[0]) + } + return nil + } + var param *nodes.Parameter + if stmt.Parameters != nil { + for _, item := range stmt.Parameters.Items { + if pr, ok := item.(*nodes.Parameter); ok && pr.Name == stmt.ParallelSpec.Argument { + param = pr + break + } + } + } + if param == nil || (param.Mode != "" && param.Mode != "IN") { + return p.syntaxErrorAtTok(partTok) + } + if stmt.Streaming != nil { + for i, item := range stmt.Streaming.Items { + if item.(*nodes.StreamingClause).Argument != stmt.ParallelSpec.Argument { + return p.syntaxErrorAtTok(streamToks[i]) + } + } + } + return nil +} + +// tablePseudoType returns the TABLE pseudo-type among a subprogram's +// parameter types and its result type, or nil. TABLE stands only in a +// polymorphic table function: Oracle 23ai rejects it elsewhere with +// PLS-00765, in a procedure, an ordinary function, or a SQL macro alike. +func tablePseudoType(params *nodes.List, result *nodes.TypeName) *nodes.TypeName { + isTable := func(tn *nodes.TypeName) bool { + if tn == nil || tn.IsPercType || tn.IsPercRowtype || tn.Names.Len() != 1 { + return false + } + s, ok := tn.Names.Items[0].(*nodes.String) + return ok && s.Str == "TABLE" + } + if params != nil { + for _, item := range params.Items { + if pr, ok := item.(*nodes.Parameter); ok && isTable(pr.TypeName) { + return pr.TypeName + } + } + } + if isTable(result) { + return result + } + return nil +} + +// syntaxErrorAtType returns a syntax error at datatype tn. +func (p *Parser) syntaxErrorAtType(tn *nodes.TypeName) *ParseError { + return p.syntaxErrorAtTok(Token{Type: tokIDENT, Loc: tn.Loc.Start, End: tn.Loc.End}) } // parseParenExprList parses ( expr [, expr ]... ) with at least one diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 7fd6aa87..13fd48e8 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -28,7 +28,7 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (x NUMBER) RETURN NUMBER DETERMINISTIC AGGREGATE USING s.impl;", "CREATE FUNCTION f RETURN t PIPELINED USING impl;", "CREATE FUNCTION f RETURN t PIPELINED PARALLEL_ENABLE USING impl@lnk;", - "CREATE FUNCTION f RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) USING impl;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) USING impl;", "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED TABLE POLYMORPHIC USING pkg;", "CREATE PACKAGE k AS FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl; FUNCTION g RETURN t PIPELINED USING impl; END;", @@ -39,7 +39,6 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a)) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a + 1, b.c)) ORDER c BY (a, b) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t ORDER c BY (a) IS BEGIN RETURN; END;", // RESULT_CACHE RELIES_ON and SQL_MACRO. "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON (t1, s.t2) IS BEGIN RETURN 1; END;", "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", @@ -64,6 +63,11 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN CLOB SQL_MACRO IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN CHAR SQL_MACRO (SCALAR) IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN STRING SQL_MACRO ACCESSIBLE BY (x) IS BEGIN RETURN NULL; END;", + // The partitioned argument matches a parameter however it is quoted, + // and may be IN; the streaming clauses may come first. + "CREATE FUNCTION f (c IN SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION \"C\" BY ANY) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t ORDER c BY (a) PARALLEL_ENABLE (PARTITION c BY ANY) PIPELINED IS BEGIN RETURN; END;", + "CREATE FUNCTION f (t TABLE, c COLUMNS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", // Oracle 23ai compiles these combinations and nested polymorphic // table functions. "CREATE FUNCTION f (a VARCHAR2) RETURN VARCHAR2 SQL_MACRO AGGREGATE USING impl;", @@ -156,6 +160,26 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED POLYMORPHIC USING pkg;", "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC)", "CREATE FUNCTION f (a NUMBER) RETURN NUMBER PIPELINED AGGREGATE USING impl;", + // PARALLEL_ENABLE partitions an IN parameter (PLS-00626, PLS-00625); + // a streaming clause needs PARALLEL_ENABLE with PARTITION BY on the + // same argument (PLS-00654, PLS-00665, PLS-00631). + "CREATE FUNCTION f RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c OUT SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c IN OUT SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t ORDER c BY (a) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE ORDER c BY (a) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR, d SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) CLUSTER d BY (a) IS BEGIN RETURN; END;", + "CREATE PACKAGE k AS FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED ORDER c BY (a); END;", + // TABLE stands only in a polymorphic table function (PLS-00765), and a + // type method is not a SQL macro (PLS-00781). + "CREATE PROCEDURE p (t TABLE) IS BEGIN NULL; END;", + "CREATE FUNCTION f (t TABLE) RETURN NUMBER IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f RETURN TABLE IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f (t TABLE) RETURN VARCHAR2 SQL_MACRO (SCALAR) IS BEGIN RETURN 'x'; END;", + "CREATE FUNCTION f (t TABLE) RETURN VARCHAR2 SQL_MACRO IS BEGIN RETURN 'x'; END;", + "CREATE PACKAGE k AS PROCEDURE p (t TABLE); END;", + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER FUNCTION f RETURN VARCHAR2 SQL_MACRO)", + "CREATE TYPE ty AS OBJECT (a NUMBER, STATIC FUNCTION f RETURN VARCHAR2 SQL_MACRO)", // An aggregate takes an argument (PLS-00652). "CREATE FUNCTION f RETURN NUMBER AGGREGATE USING impl;", // ACCESSIBLE BY is not for a nested subprogram (PLS-00262, PLS-00263). diff --git a/oracle/parser/testdata/coverage/loc_node_coverage.tsv b/oracle/parser/testdata/coverage/loc_node_coverage.tsv index f0e76bbe..5b82875c 100644 --- a/oracle/parser/testdata/coverage/loc_node_coverage.tsv +++ b/oracle/parser/testdata/coverage/loc_node_coverage.tsv @@ -237,7 +237,7 @@ node_type family status fixture notes debt_class approval next_action *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.Accessor plsql covered CREATE PROCEDURE p ACCESSIBLE BY (PACKAGE s.k, f) AS BEGIN NULL; END; direct parser fixture for ACCESSIBLE BY accessors none covered keep_regression_guard *nodes.ParallelEnableClause plsql covered CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) IS BEGIN RETURN; END; direct parser fixture for PARALLEL_ENABLE partitioning none covered keep_regression_guard -*nodes.StreamingClause plsql covered CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED ORDER c BY (a, b) IS BEGIN RETURN; END; direct parser fixture for ORDER and CLUSTER streaming clauses none covered keep_regression_guard +*nodes.StreamingClause plsql covered CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY (a, b) IS BEGIN RETURN; END; direct parser fixture for ORDER and CLUSTER streaming clauses 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/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 134f0011..184a404d 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -150,7 +150,7 @@ ref_148 plsql_body accept CREATE OR REPLACE PACKAGE omni_abk_{run} ACCESSIBLE BY ref_149 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_ab_{run} ACCESSIBLE BY (PACKAGE) IS BEGIN NULL; END; accessor with a unit kind and no name ref_150 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_abl_{run} RETURN NUMBER ACCESSIBLE BY (omni_x@omni_lnk) IS BEGIN RETURN 1; END; accessor with a database link ref_151 plsql_body accept CREATE OR REPLACE FUNCTION omni_agg_{run} (x NUMBER) RETURN NUMBER DETERMINISTIC AGGREGATE USING omni_s.omni_impl; AGGREGATE USING implementation type, no body -ref_152 plsql_body accept CREATE OR REPLACE FUNCTION omni_pip_{run} RETURN omni_tab PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) USING omni_impl; PIPELINED ... USING implementation type +ref_152 plsql_body accept CREATE OR REPLACE FUNCTION omni_pip_{run} (c SYS_REFCURSOR) RETURN omni_tab PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) USING omni_impl; PIPELINED ... USING implementation type ref_153 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_agg_{run} (x NUMBER) RETURN NUMBER AGGREGATE USING omni_impl IS BEGIN RETURN 1; END; body after AGGREGATE USING ref_154 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_pip_{run} RETURN omni_tab USING omni_impl PIPELINED; clause after USING implementation type ref_155 plsql_body accept CREATE OR REPLACE FUNCTION omni_par_{run} (c SYS_REFCURSOR) RETURN omni_tab PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END; PARALLEL_ENABLE partitioning with streaming clauses From 8c2c339ab72a6c9b93622bae84d883dec7a88aa2 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 09:16:40 +0000 Subject: [PATCH 08/22] fix(oracle): PTF signatures, partition cursor types, %CHARSET sources, Split (BYT-10301) Review follow-up, each checked on Oracle 23ai with real types behind the names: - A polymorphic table function returns TABLE (PLS-00767) and takes one TABLE parameter (PLS-00773, PLS-00766). - The PARALLEL_ENABLE argument is a REF CURSOR: a predefined non-cursor type, or SYS_REFCURSOR with HASH, RANGE, VALUE, or a streaming clause, is PLS-00627; SYS_REFCURSOR BY ANY compiles, and a named type is left to the engine. - item%CHARSET naming a parameter of the same heading needs a character parameter (PLS-00550 for NUMBER or DATE); VARCHAR2 without ANY_CS and NVARCHAR2 qualify, and items outside the heading are left to the engine. - Split ends a stored unit at a ';' in its head before IS|AS, so a malformed bodyless heading (AGGREGATE without USING) no longer runs on into the next statement. Fixtures that partitioned a weak cursor by HASH now use a strong one. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 24 +++- oracle/parser/split.go | 10 ++ oracle/parser/split_test.go | 11 ++ oracle/parser/subprogram_clause.go | 113 ++++++++++++++++-- oracle/parser/subprogram_clause_test.go | 56 ++++++--- .../testdata/coverage/loc_node_coverage.tsv | 4 +- .../testdata/coverage/reference_oracle.tsv | 2 +- 7 files changed, 185 insertions(+), 35 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index d9332dd4..5ac2723c 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -65,6 +65,9 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi if tn := tablePseudoType(stmt.Parameters, nil); tn != nil { return nil, p.syntaxErrorAtType(tn) } + if err := p.checkCharsetSources(stmt.Parameters, nil); err != nil { + return nil, err + } if err := p.parseProcedureProperties(stmt, subprogramSchema); err != nil { return nil, err } @@ -293,13 +296,18 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s return p.syntaxErrorAtTok(*notPolymorphic) case stmt.SqlMacro && notMacro != nil: return p.syntaxErrorAtTok(*notMacro) - case stmt.SqlMacro && !isMacroResultType(stmt.ReturnType): + case stmt.SqlMacro && !mayBeCharacterType(stmt.ReturnType): return p.syntaxErrorAtTok(*macroTok) } if stmt.Polymorphic == "" { if tn := tablePseudoType(stmt.Parameters, stmt.ReturnType); tn != nil { return p.syntaxErrorAtType(tn) } + } else if err := p.checkPolymorphicSignature(stmt); err != nil { + return err + } + if err := p.checkCharsetSources(stmt.Parameters, stmt.ReturnType); err != nil { + return err } return p.checkParallelArguments(stmt, partTok, streamToks) } @@ -464,11 +472,12 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s } } -// isMacroResultType reports whether tn may be a SQL macro's result type: not -// a predefined non-character type (the national character types are -// character types here) and not a %ROWTYPE record. A name the text does not -// settle is left to the engine. -func isMacroResultType(tn *nodes.TypeName) bool { +// mayBeCharacterType reports whether tn may hold character data: it is not a +// predefined non-character type (the national character types count as +// character types) and not a %ROWTYPE record. A SQL macro returns such a +// type (PLS-00776), and item%CHARSET takes its character set from one +// (PLS-00550). A name the text does not settle is left to the engine. +func mayBeCharacterType(tn *nodes.TypeName) bool { if tn == nil { return true } @@ -746,6 +755,9 @@ func (p *Parser) parsePackageProcDecl(level subprogramLevel) (*nodes.CreateProce if tn := tablePseudoType(stmt.Parameters, nil); tn != nil { return nil, p.syntaxErrorAtType(tn) } + if err := p.checkCharsetSources(stmt.Parameters, nil); err != nil { + return nil, err + } if err := p.parseProcedureProperties(stmt, level); err != nil { return nil, err } diff --git a/oracle/parser/split.go b/oracle/parser/split.go index 869311f2..100a9f49 100644 --- a/oracle/parser/split.go +++ b/oracle/parser/split.go @@ -408,6 +408,16 @@ func (s *splitState) observePLSQL(tok Token) { // A nested call spec has no END; its ';' closes it. s.frames = s.frames[:n-1] } + if len(s.frames) == 1 && s.frames[0].kind == splitPLSQLStoredUnit && + !s.frames[0].isAs && s.frames[0].headDepth == 0 && !s.callSpecStarted { + top := &s.frames[0] + // A ';' in a stored unit's head, before any IS|AS, ends a unit + // that has no body: a malformed heading such as AGGREGATE + // without USING ends here rather than running on to the next + // END, and keeps its ';' as a type-implemented function does. + top.callSpec = true + s.callSpecStarted = true + } return } diff --git a/oracle/parser/split_test.go b/oracle/parser/split_test.go index 88dffca8..c7faab19 100644 --- a/oracle/parser/split_test.go +++ b/oracle/parser/split_test.go @@ -435,6 +435,17 @@ func TestSplitPLSQLBlocks(t *testing.T) { "\nSELECT 1 FROM dual", }, }, + { + // A ';' in a function's head, before IS|AS, ends it even when the + // heading is malformed (AGGREGATE without USING). + name: "malformed bodyless heading", + sql: "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE;\n" + + "CREATE TABLE t (x NUMBER);", + want: []string{ + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE;", + "\nCREATE TABLE t (x NUMBER)", + }, + }, { // USING ends a function only after AGGREGATE or PIPELINED: a // function or parameter named USING compiles on Oracle 23ai. diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index a3fda67a..b7d997f5 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -354,6 +354,17 @@ func (p *Parser) checkParallelArguments(stmt *nodes.CreateFunctionStmt, partTok if param == nil || (param.Mode != "" && param.Mode != "IN") { return p.syntaxErrorAtTok(partTok) } + // The partitioned argument is a REF CURSOR, strongly typed unless + // partitioned BY ANY without a streaming clause (PLS-00627): a + // predefined non-cursor type never qualifies, nor SYS_REFCURSOR with + // HASH, RANGE, VALUE, ORDER BY, or CLUSTER BY. A named type is left to + // the engine. + if name, ok := predefinedTypeName(param.TypeName); ok { + weakAllowed := name == "SYS_REFCURSOR" && stmt.ParallelSpec.PartitionBy == "ANY" && len(streamToks) == 0 + if !weakAllowed { + return p.syntaxErrorAtTok(partTok) + } + } if stmt.Streaming != nil { for i, item := range stmt.Streaming.Items { if item.(*nodes.StreamingClause).Argument != stmt.ParallelSpec.Argument { @@ -364,31 +375,115 @@ func (p *Parser) checkParallelArguments(stmt *nodes.CreateFunctionStmt, partTok return nil } +// predefinedTypeName returns the name of tn when it is a predefined datatype +// that is not a REF CURSOR of the program's own: SYS_REFCURSOR, or a scalar, +// character, LOB, or %ROWTYPE record type. +func predefinedTypeName(tn *nodes.TypeName) (string, bool) { + if tn == nil { + return "", false + } + if tn.IsPercRowtype { + return "%ROWTYPE", true + } + if tn.IsPercType || tn.Names.Len() != 1 { + return "", false + } + s, ok := tn.Names.Items[0].(*nodes.String) + if !ok { + return "", false + } + switch s.Str { + case "CHAR", "CHARACTER", "VARCHAR2", "VARCHAR", "STRING", "CLOB": + return s.Str, true + } + return s.Str, plsqlNonCharacterTypes[s.Str] +} + +// checkPolymorphicSignature checks a polymorphic table function's signature +// as Oracle 23ai does from the text: it returns TABLE (PLS-00767) and takes +// exactly one TABLE parameter (PLS-00773, PLS-00766). +func (p *Parser) checkPolymorphicSignature(stmt *nodes.CreateFunctionStmt) error { + if !isTablePseudoType(stmt.ReturnType) { + return p.syntaxErrorAtType(stmt.ReturnType) + } + tables := 0 + if stmt.Parameters != nil { + for _, item := range stmt.Parameters.Items { + if pr, ok := item.(*nodes.Parameter); ok && isTablePseudoType(pr.TypeName) { + tables++ + if tables > 1 { + return p.syntaxErrorAtType(pr.TypeName) + } + } + } + } + if tables == 0 { + return p.syntaxErrorAtType(stmt.ReturnType) + } + return nil +} + +// checkCharsetSources checks each item%CHARSET in a subprogram heading that +// names one of its own parameters: Oracle 23ai rejects one whose parameter +// has a non-character type, such as NUMBER or DATE (PLS-00550). A CHAR, +// VARCHAR2, CLOB, or national character parameter qualifies with or +// without ANY_CS; an item outside the heading is left to the engine. +func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) error { + if params == nil { + return nil + } + byName := map[string]*nodes.Parameter{} + for _, item := range params.Items { + if pr, ok := item.(*nodes.Parameter); ok { + byName[pr.Name] = pr + } + } + check := func(tn *nodes.TypeName) error { + if tn == nil || !tn.IsPercCharset { + return nil + } + if src, ok := byName[tn.CharacterSet]; ok && !mayBeCharacterType(src.TypeName) { + return p.syntaxErrorAtType(tn) + } + return nil + } + for _, item := range params.Items { + if pr, ok := item.(*nodes.Parameter); ok { + if err := check(pr.TypeName); err != nil { + return err + } + } + } + return check(result) +} + // tablePseudoType returns the TABLE pseudo-type among a subprogram's // parameter types and its result type, or nil. TABLE stands only in a // polymorphic table function: Oracle 23ai rejects it elsewhere with // PLS-00765, in a procedure, an ordinary function, or a SQL macro alike. func tablePseudoType(params *nodes.List, result *nodes.TypeName) *nodes.TypeName { - isTable := func(tn *nodes.TypeName) bool { - if tn == nil || tn.IsPercType || tn.IsPercRowtype || tn.Names.Len() != 1 { - return false - } - s, ok := tn.Names.Items[0].(*nodes.String) - return ok && s.Str == "TABLE" - } if params != nil { for _, item := range params.Items { - if pr, ok := item.(*nodes.Parameter); ok && isTable(pr.TypeName) { + if pr, ok := item.(*nodes.Parameter); ok && isTablePseudoType(pr.TypeName) { return pr.TypeName } } } - if isTable(result) { + if isTablePseudoType(result) { return result } return nil } +// isTablePseudoType reports whether tn is the TABLE pseudo-type. +func isTablePseudoType(tn *nodes.TypeName) bool { + if tn == nil || tn.IsPercType || tn.IsPercRowtype || tn.Names.Len() != 1 { + return false + } + s, ok := tn.Names.Items[0].(*nodes.String) + return ok && s.Str == "TABLE" +} + // syntaxErrorAtType returns a syntax error at datatype tn. func (p *Parser) syntaxErrorAtType(tn *nodes.TypeName) *ParseError { return p.syntaxErrorAtTok(Token{Type: tokIDENT, Loc: tn.Loc.Start, End: tn.Loc.End}) diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 13fd48e8..172cb923 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -34,11 +34,11 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE PACKAGE k AS FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl; FUNCTION g RETURN t PIPELINED USING impl; END;", // PARALLEL_ENABLE partitioning and streaming clauses. "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY RANGE (a)) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a)) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a + 1, b.c)) ORDER c BY (a, b) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY RANGE (a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a + 1, b.c)) ORDER c BY (a, b) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", // RESULT_CACHE RELIES_ON and SQL_MACRO. "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON (t1, s.t2) IS BEGIN RETURN 1; END;", "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", @@ -66,8 +66,14 @@ func TestSubprogramHeadingClauses(t *testing.T) { // The partitioned argument matches a parameter however it is quoted, // and may be IN; the streaming clauses may come first. "CREATE FUNCTION f (c IN SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION \"C\" BY ANY) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t ORDER c BY (a) PARALLEL_ENABLE (PARTITION c BY ANY) PIPELINED IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t ORDER c BY (a) PARALLEL_ENABLE (PARTITION c BY ANY) PIPELINED IS BEGIN RETURN; END;", "CREATE FUNCTION f (t TABLE, c COLUMNS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (t TABLE, x NUMBER) RETURN TABLE PIPELINED TABLE POLYMORPHIC USING pkg;", + // item%CHARSET may name a character parameter without ANY_CS, a + // national one, or an item outside the heading. + "CREATE FUNCTION f (a VARCHAR2) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f (a NVARCHAR2) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN VARCHAR2 CHARACTER SET k.c%CHARSET IS BEGIN RETURN NULL; END;", // Oracle 23ai compiles these combinations and nested polymorphic // table functions. "CREATE FUNCTION f (a VARCHAR2) RETURN VARCHAR2 SQL_MACRO AGGREGATE USING impl;", @@ -117,11 +123,11 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE () IS BEGIN RETURN; END;", "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c BY LIST (a)) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c BY HASH ()) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY () IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c.x BY (a) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t ORDER c BY (a) ORDER c BY (b) IS BEGIN RETURN; END;", // PLS-00371 - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t CLUSTER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", // PLS-00371 + "CREATE FUNCTION f (c pkg.rc) RETURN t PARALLEL_ENABLE (PARTITION c BY HASH ()) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY () IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c.x BY (a) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t ORDER c BY (a) ORDER c BY (b) IS BEGIN RETURN; END;", // PLS-00371 + "CREATE FUNCTION f (c pkg.rc) RETURN t CLUSTER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", // PLS-00371 // SQL_MACRO and repeated clauses. "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO () IS BEGIN RETURN 'x'; END;", "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO (foo) IS BEGIN RETURN 'x'; END;", @@ -150,7 +156,7 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO PARALLEL_ENABLE IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO RESULT_CACHE IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO AUTHID DEFINER IS BEGIN RETURN NULL; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN VARCHAR2 SQL_MACRO ORDER c BY (a) IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN VARCHAR2 SQL_MACRO ORDER c BY (a) IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN VARCHAR2 SQL_MACRO PIPELINED IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN NUMBER SQL_MACRO IS BEGIN RETURN 1; END;", "CREATE FUNCTION f RETURN LONG SQL_MACRO IS BEGIN RETURN NULL; END;", @@ -166,10 +172,26 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c OUT SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c IN OUT SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t ORDER c BY (a) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE ORDER c BY (a) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c SYS_REFCURSOR, d SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) CLUSTER d BY (a) IS BEGIN RETURN; END;", - "CREATE PACKAGE k AS FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED ORDER c BY (a); END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t ORDER c BY (a) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE ORDER c BY (a) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc, d pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) CLUSTER d BY (a) IS BEGIN RETURN; END;", + // The partitioned argument is a strong REF CURSOR unless it is a + // SYS_REFCURSOR partitioned BY ANY without streaming (PLS-00627). + "CREATE FUNCTION f (c NUMBER) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c VARCHAR2) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY (a) IS BEGIN RETURN; END;", + // A polymorphic table function returns TABLE (PLS-00767) and takes one + // TABLE parameter (PLS-00773, PLS-00766). + "CREATE FUNCTION f (t TABLE) RETURN NUMBER PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (x NUMBER) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (t TABLE, u TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + // item%CHARSET of a non-character parameter (PLS-00550). + "CREATE FUNCTION f (a NUMBER) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f (a DATE) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN NULL; END;", + "CREATE PROCEDURE p (a NUMBER, b VARCHAR2 CHARACTER SET a%CHARSET) IS BEGIN NULL; END;", + "CREATE PACKAGE k AS FUNCTION f (c pkg.rc) RETURN t PIPELINED ORDER c BY (a); END;", // TABLE stands only in a polymorphic table function (PLS-00765), and a // type method is not a SQL macro (PLS-00781). "CREATE PROCEDURE p (t TABLE) IS BEGIN NULL; END;", @@ -283,7 +305,7 @@ func TestSubprogramHeadingClausesAST(t *testing.T) { t.Errorf("polymorphic function = %+v", ptf) } - par := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) CLUSTER c BY (a) RESULT_CACHE RELIES_ON (t1) IS BEGIN RETURN; END;") + par := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) CLUSTER c BY (a) RESULT_CACHE RELIES_ON (t1) IS BEGIN RETURN; END;") if spec := par.ParallelSpec; spec == nil || spec.Argument != "C" || spec.PartitionBy != "HASH" || spec.Columns.Len() != 2 { t.Errorf("parallel spec = %+v", par.ParallelSpec) } diff --git a/oracle/parser/testdata/coverage/loc_node_coverage.tsv b/oracle/parser/testdata/coverage/loc_node_coverage.tsv index 5b82875c..e02543b7 100644 --- a/oracle/parser/testdata/coverage/loc_node_coverage.tsv +++ b/oracle/parser/testdata/coverage/loc_node_coverage.tsv @@ -236,8 +236,8 @@ node_type family status fixture notes debt_class approval next_action *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.Accessor plsql covered CREATE PROCEDURE p ACCESSIBLE BY (PACKAGE s.k, f) AS BEGIN NULL; END; direct parser fixture for ACCESSIBLE BY accessors none covered keep_regression_guard -*nodes.ParallelEnableClause plsql covered CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) IS BEGIN RETURN; END; direct parser fixture for PARALLEL_ENABLE partitioning none covered keep_regression_guard -*nodes.StreamingClause plsql covered CREATE FUNCTION f (c SYS_REFCURSOR) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY (a, b) IS BEGIN RETURN; END; direct parser fixture for ORDER and CLUSTER streaming clauses none covered keep_regression_guard +*nodes.ParallelEnableClause plsql covered CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) IS BEGIN RETURN; END; direct parser fixture for PARALLEL_ENABLE partitioning none covered keep_regression_guard +*nodes.StreamingClause plsql covered CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY (a, b) IS BEGIN RETURN; END; direct parser fixture for ORDER and CLUSTER streaming clauses 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/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 184a404d..3f624abf 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -153,7 +153,7 @@ ref_151 plsql_body accept CREATE OR REPLACE FUNCTION omni_agg_{run} (x NUMBER) R ref_152 plsql_body accept CREATE OR REPLACE FUNCTION omni_pip_{run} (c SYS_REFCURSOR) RETURN omni_tab PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) USING omni_impl; PIPELINED ... USING implementation type ref_153 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_agg_{run} (x NUMBER) RETURN NUMBER AGGREGATE USING omni_impl IS BEGIN RETURN 1; END; body after AGGREGATE USING ref_154 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_pip_{run} RETURN omni_tab USING omni_impl PIPELINED; clause after USING implementation type -ref_155 plsql_body accept CREATE OR REPLACE FUNCTION omni_par_{run} (c SYS_REFCURSOR) RETURN omni_tab PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END; PARALLEL_ENABLE partitioning with streaming clauses +ref_155 plsql_body accept CREATE OR REPLACE FUNCTION omni_par_{run} (c omni_rcpkg.rc) RETURN omni_tab PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END; PARALLEL_ENABLE partitioning with streaming clauses ref_156 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_par_{run} (c SYS_REFCURSOR) RETURN omni_tab PARALLEL_ENABLE (PARTITION c BY LIST (a)) IS BEGIN RETURN; END; unknown PARALLEL_ENABLE partitioning ref_157 plsql_body accept CREATE OR REPLACE FUNCTION omni_ptf_{run} (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING omni_pkg; polymorphic table function ref_158 plsql_body accept CREATE OR REPLACE FUNCTION omni_rc_{run} RETURN NUMBER RESULT_CACHE RELIES_ON (omni_t1, omni_s.omni_t2) IS BEGIN RETURN 1; END; RESULT_CACHE RELIES_ON From ef953c213c2fb4698e727a108f0e9f88fe765503 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 09:28:24 +0000 Subject: [PATCH 09/22] fix(oracle): VALUE partition arity, quoted TABLE user type, BOOL (BYT-10301) Review follow-up, each checked on Oracle 23ai: - PARTITION BY VALUE takes one column (PLS-00757); HASH and RANGE take several. - A quoted "TABLE" names a user type, which a procedure or function parameter may have; only the unquoted word is the PTF pseudo-type. - BOOL, the 23ai alias of BOOLEAN, takes no CHARACTER SET (PLS-00550). A quoted "POLYMORPHIC" or "COLLATION" was already rejected, as Oracle does (PLS-00103); reference rows ref_179 to ref_181 pin these. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 6 ++--- oracle/parser/subprogram_clause.go | 25 +++++++++++++------ oracle/parser/subprogram_clause_test.go | 10 ++++++++ .../testdata/coverage/reference_oracle.tsv | 3 +++ oracle/parser/type.go | 1 + 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 5ac2723c..7b771107 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -62,7 +62,7 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi } } - if tn := tablePseudoType(stmt.Parameters, nil); tn != nil { + if tn := p.tablePseudoType(stmt.Parameters, nil); tn != nil { return nil, p.syntaxErrorAtType(tn) } if err := p.checkCharsetSources(stmt.Parameters, nil); err != nil { @@ -300,7 +300,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s return p.syntaxErrorAtTok(*macroTok) } if stmt.Polymorphic == "" { - if tn := tablePseudoType(stmt.Parameters, stmt.ReturnType); tn != nil { + if tn := p.tablePseudoType(stmt.Parameters, stmt.ReturnType); tn != nil { return p.syntaxErrorAtType(tn) } } else if err := p.checkPolymorphicSignature(stmt); err != nil { @@ -752,7 +752,7 @@ func (p *Parser) parsePackageProcDecl(level subprogramLevel) (*nodes.CreateProce return nil, parseErr474 } } - if tn := tablePseudoType(stmt.Parameters, nil); tn != nil { + if tn := p.tablePseudoType(stmt.Parameters, nil); tn != nil { return nil, p.syntaxErrorAtType(tn) } if err := p.checkCharsetSources(stmt.Parameters, nil); err != nil { diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index b7d997f5..6524d4f4 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -285,6 +285,12 @@ func (p *Parser) parseParallelEnableSpec() (*nodes.ParallelEnableClause, Token, if err != nil { return nil, Token{}, err } + // VALUE partitioning takes one column (PLS-00757); HASH and RANGE + // take several. + if spec.PartitionBy == "VALUE" && cols.Len() > 1 { + loc := nodes.NodeLoc(cols.Items[1]) + return nil, Token{}, p.syntaxErrorAtTok(Token{Type: tokIDENT, Loc: loc.Start, End: loc.End}) + } spec.Columns = cols default: return nil, Token{}, p.syntaxErrorAtCur() @@ -403,13 +409,13 @@ func predefinedTypeName(tn *nodes.TypeName) (string, bool) { // as Oracle 23ai does from the text: it returns TABLE (PLS-00767) and takes // exactly one TABLE parameter (PLS-00773, PLS-00766). func (p *Parser) checkPolymorphicSignature(stmt *nodes.CreateFunctionStmt) error { - if !isTablePseudoType(stmt.ReturnType) { + if !p.isTablePseudoType(stmt.ReturnType) { return p.syntaxErrorAtType(stmt.ReturnType) } tables := 0 if stmt.Parameters != nil { for _, item := range stmt.Parameters.Items { - if pr, ok := item.(*nodes.Parameter); ok && isTablePseudoType(pr.TypeName) { + if pr, ok := item.(*nodes.Parameter); ok && p.isTablePseudoType(pr.TypeName) { tables++ if tables > 1 { return p.syntaxErrorAtType(pr.TypeName) @@ -461,25 +467,30 @@ func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) // parameter types and its result type, or nil. TABLE stands only in a // polymorphic table function: Oracle 23ai rejects it elsewhere with // PLS-00765, in a procedure, an ordinary function, or a SQL macro alike. -func tablePseudoType(params *nodes.List, result *nodes.TypeName) *nodes.TypeName { +func (p *Parser) tablePseudoType(params *nodes.List, result *nodes.TypeName) *nodes.TypeName { if params != nil { for _, item := range params.Items { - if pr, ok := item.(*nodes.Parameter); ok && isTablePseudoType(pr.TypeName) { + if pr, ok := item.(*nodes.Parameter); ok && p.isTablePseudoType(pr.TypeName) { return pr.TypeName } } } - if isTablePseudoType(result) { + if p.isTablePseudoType(result) { return result } return nil } -// isTablePseudoType reports whether tn is the TABLE pseudo-type. -func isTablePseudoType(tn *nodes.TypeName) bool { +// isTablePseudoType reports whether tn is the TABLE pseudo-type: the +// unquoted reserved word. A quoted "TABLE" names a user type, which a +// procedure or function parameter may have (Oracle 23ai compiles one). +func (p *Parser) isTablePseudoType(tn *nodes.TypeName) bool { if tn == nil || tn.IsPercType || tn.IsPercRowtype || tn.Names.Len() != 1 { return false } + if tn.Loc.Start >= 0 && tn.Loc.Start < len(p.source) && p.source[tn.Loc.Start] == '"' { + return false + } s, ok := tn.Names.Items[0].(*nodes.String) return ok && s.Str == "TABLE" } diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 172cb923..26602f5d 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -69,6 +69,10 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (c pkg.rc) RETURN t ORDER c BY (a) PARALLEL_ENABLE (PARTITION c BY ANY) PIPELINED IS BEGIN RETURN; END;", "CREATE FUNCTION f (t TABLE, c COLUMNS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (t TABLE, x NUMBER) RETURN TABLE PIPELINED TABLE POLYMORPHIC USING pkg;", + // A quoted "TABLE" names a user type, not the pseudo-type. + "CREATE PROCEDURE p (a \"TABLE\") IS BEGIN NULL; END;", + "CREATE FUNCTION f (a \"TABLE\") RETURN NUMBER IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY RANGE (a, b)) IS BEGIN RETURN; END;", // item%CHARSET may name a character parameter without ANY_CS, a // national one, or an item outside the heading. "CREATE FUNCTION f (a VARCHAR2) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN NULL; END;", @@ -187,6 +191,12 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (t TABLE) RETURN NUMBER PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (x NUMBER) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (t TABLE, u TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + // VALUE partitioning takes one column (PLS-00757); BOOL is BOOLEAN + // (PLS-00550); clause keywords are unquoted (PLS-00103). + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a, b)) IS BEGIN RETURN; END;", + "CREATE PROCEDURE p (a BOOL CHARACTER SET ANY_CS) IS BEGIN NULL; END;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW \"POLYMORPHIC\" USING pkg;", + "CREATE PACKAGE k DEFAULT \"COLLATION\" USING_NLS_COMP AS PROCEDURE p; END;", // item%CHARSET of a non-character parameter (PLS-00550). "CREATE FUNCTION f (a NUMBER) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f (a DATE) RETURN VARCHAR2 CHARACTER SET a%CHARSET IS BEGIN RETURN NULL; END;", diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 3f624abf..ff26328f 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -177,3 +177,6 @@ ref_175 plsql_body accept CREATE OR REPLACE FUNCTION omni_smn_{run} RETURN NVARC ref_176 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_ptk_{run} (t TABLE) RETURN TABLE PIPELINED POLYMORPHIC USING omni_pkg; POLYMORPHIC without its row kind ref_177 plsql accept DECLARE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC IS BEGIN RETURN NULL; END; BEGIN NULL; END; nested polymorphic table function ref_178 plsql_body accept CREATE OR REPLACE FUNCTION omni_sma_{run} (a VARCHAR2) RETURN VARCHAR2 SQL_MACRO AGGREGATE USING omni_impl; SQL macro with AGGREGATE USING +ref_179 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_qpoly_{run} (t TABLE) RETURN TABLE PIPELINED ROW "POLYMORPHIC" USING omni_pkg; quoted POLYMORPHIC is an identifier +ref_180 plsql_body reject CREATE OR REPLACE PACKAGE omni_bad_qcoll_{run} DEFAULT "COLLATION" USING_NLS_COMP AS PROCEDURE p; END; quoted COLLATION is an identifier +ref_181 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qtab_{run} (a "TABLE") IS BEGIN NULL; END; quoted TABLE names a user type diff --git a/oracle/parser/type.go b/oracle/parser/type.go index 0fd2623e..1c9dd870 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -222,6 +222,7 @@ var plsqlNonCharacterTypes = map[string]bool{ "BINARY_FLOAT": true, "BINARY_DOUBLE": true, "SIMPLE_FLOAT": true, "SIMPLE_DOUBLE": true, "PLS_INTEGER": true, "BINARY_INTEGER": true, "SIMPLE_INTEGER": true, "NATURAL": true, "NATURALN": true, "POSITIVE": true, "POSITIVEN": true, "SIGNTYPE": true, "BOOLEAN": true, + "BOOL": true, "DATE": true, "TIMESTAMP": true, "INTERVAL": true, "RAW": true, "LONG": true, "ROWID": true, "UROWID": true, "BLOB": true, "BFILE": true, "JSON": true, "VECTOR": true, "MLSLABEL": true, "SYS_REFCURSOR": true, "NCHAR": true, "NVARCHAR2": true, "NCLOB": true, From fa2b93bc883d17018e43043b7e571acd7eeae9be Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 09:41:26 +0000 Subject: [PATCH 10/22] fix(oracle): COLUMNS pseudo-type and multiword predefined types (BYT-10301) Review follow-up, each checked on Oracle 23ai: - COLUMNS, like TABLE, stands only in a polymorphic table function (PLS-00765). No type can be named COLUMNS unquoted (CREATE TYPE columns is PLS-00103), so the unquoted word is always the pseudo-type; a quoted "COLUMNS" names a user type. - TIMESTAMP WITH [LOCAL] TIME ZONE, INTERVAL ... TO ..., and LONG RAW are predefined non-character types: a SQL macro cannot return one (PLS-00776), CHARACTER SET is not allowed on one (PLS-00550), and none is a partitionable REF CURSOR (PLS-00627). Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 12 ++----- oracle/parser/subprogram_clause.go | 47 ++++++++++++------------- oracle/parser/subprogram_clause_test.go | 15 +++++++- oracle/parser/type.go | 33 ++++++++++++++--- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 7b771107..f07ce9ea 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -484,18 +484,12 @@ func mayBeCharacterType(tn *nodes.TypeName) bool { if tn.IsPercRowtype { return false } - if tn.IsPercType || tn.Names.Len() != 1 { - return true - } - first, ok := tn.Names.Items[0].(*nodes.String) - if !ok { - return true - } - switch first.Str { + switch lead := predefinedTypeLead(tn); lead { case "NCHAR", "NVARCHAR2", "NCLOB": return true + default: + return !plsqlNonCharacterTypes[lead] } - return !plsqlNonCharacterTypes[first.Str] } // parseImplementationType parses USING [ schema. ] implementation_type diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 6524d4f4..8fd44866 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -391,31 +391,25 @@ func predefinedTypeName(tn *nodes.TypeName) (string, bool) { if tn.IsPercRowtype { return "%ROWTYPE", true } - if tn.IsPercType || tn.Names.Len() != 1 { - return "", false - } - s, ok := tn.Names.Items[0].(*nodes.String) - if !ok { - return "", false - } - switch s.Str { + switch lead := predefinedTypeLead(tn); lead { case "CHAR", "CHARACTER", "VARCHAR2", "VARCHAR", "STRING", "CLOB": - return s.Str, true + return lead, true + default: + return lead, plsqlNonCharacterTypes[lead] } - return s.Str, plsqlNonCharacterTypes[s.Str] } // checkPolymorphicSignature checks a polymorphic table function's signature // as Oracle 23ai does from the text: it returns TABLE (PLS-00767) and takes // exactly one TABLE parameter (PLS-00773, PLS-00766). func (p *Parser) checkPolymorphicSignature(stmt *nodes.CreateFunctionStmt) error { - if !p.isTablePseudoType(stmt.ReturnType) { + if p.pseudoType(stmt.ReturnType) != "TABLE" { return p.syntaxErrorAtType(stmt.ReturnType) } tables := 0 if stmt.Parameters != nil { for _, item := range stmt.Parameters.Items { - if pr, ok := item.(*nodes.Parameter); ok && p.isTablePseudoType(pr.TypeName) { + if pr, ok := item.(*nodes.Parameter); ok && p.pseudoType(pr.TypeName) == "TABLE" { tables++ if tables > 1 { return p.syntaxErrorAtType(pr.TypeName) @@ -463,36 +457,41 @@ func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) return check(result) } -// tablePseudoType returns the TABLE pseudo-type among a subprogram's -// parameter types and its result type, or nil. TABLE stands only in a -// polymorphic table function: Oracle 23ai rejects it elsewhere with +// tablePseudoType returns the first TABLE or COLUMNS pseudo-type among a +// subprogram's parameter types and its result type, or nil. Both stand only +// in a polymorphic table function: Oracle 23ai rejects them elsewhere with // PLS-00765, in a procedure, an ordinary function, or a SQL macro alike. func (p *Parser) tablePseudoType(params *nodes.List, result *nodes.TypeName) *nodes.TypeName { if params != nil { for _, item := range params.Items { - if pr, ok := item.(*nodes.Parameter); ok && p.isTablePseudoType(pr.TypeName) { + if pr, ok := item.(*nodes.Parameter); ok && p.pseudoType(pr.TypeName) != "" { return pr.TypeName } } } - if p.isTablePseudoType(result) { + if p.pseudoType(result) != "" { return result } return nil } -// isTablePseudoType reports whether tn is the TABLE pseudo-type: the -// unquoted reserved word. A quoted "TABLE" names a user type, which a -// procedure or function parameter may have (Oracle 23ai compiles one). -func (p *Parser) isTablePseudoType(tn *nodes.TypeName) bool { +// pseudoType returns TABLE or COLUMNS when tn is that polymorphic table +// function pseudo-type, "" otherwise. Only the unquoted word is the +// pseudo-type: a quoted "TABLE" or "COLUMNS" names a user type, which a +// procedure or function parameter may have, while no type can be named +// COLUMNS unquoted (CREATE TYPE columns is PLS-00103 on Oracle 23ai). +func (p *Parser) pseudoType(tn *nodes.TypeName) string { if tn == nil || tn.IsPercType || tn.IsPercRowtype || tn.Names.Len() != 1 { - return false + return "" } if tn.Loc.Start >= 0 && tn.Loc.Start < len(p.source) && p.source[tn.Loc.Start] == '"' { - return false + return "" } s, ok := tn.Names.Items[0].(*nodes.String) - return ok && s.Str == "TABLE" + if !ok || (s.Str != "TABLE" && s.Str != "COLUMNS") { + return "" + } + return s.Str } // syntaxErrorAtType returns a syntax error at datatype tn. diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 26602f5d..197fd4c5 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -69,7 +69,8 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (c pkg.rc) RETURN t ORDER c BY (a) PARALLEL_ENABLE (PARTITION c BY ANY) PIPELINED IS BEGIN RETURN; END;", "CREATE FUNCTION f (t TABLE, c COLUMNS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (t TABLE, x NUMBER) RETURN TABLE PIPELINED TABLE POLYMORPHIC USING pkg;", - // A quoted "TABLE" names a user type, not the pseudo-type. + // A quoted "TABLE" or "COLUMNS" names a user type, not the pseudo-type. + "CREATE PROCEDURE p (a \"COLUMNS\") IS BEGIN NULL; END;", "CREATE PROCEDURE p (a \"TABLE\") IS BEGIN NULL; END;", "CREATE FUNCTION f (a \"TABLE\") RETURN NUMBER IS BEGIN RETURN 1; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY RANGE (a, b)) IS BEGIN RETURN; END;", @@ -191,6 +192,18 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (t TABLE) RETURN NUMBER PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (x NUMBER) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (t TABLE, u TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + // COLUMNS stands only in a polymorphic table function (PLS-00765). + "CREATE PROCEDURE p (c COLUMNS) IS BEGIN NULL; END;", + "CREATE FUNCTION f (c COLUMNS) RETURN NUMBER IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f RETURN COLUMNS IS BEGIN RETURN NULL; END;", + // Multiword predefined types are not character types (PLS-00776, + // PLS-00550) or REF CURSORs (PLS-00627). + "CREATE FUNCTION f RETURN TIMESTAMP WITH TIME ZONE SQL_MACRO IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN TIMESTAMP WITH LOCAL TIME ZONE SQL_MACRO IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN LONG RAW SQL_MACRO IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN INTERVAL DAY TO SECOND SQL_MACRO IS BEGIN RETURN NULL; END;", + "CREATE PROCEDURE p (a TIMESTAMP WITH TIME ZONE CHARACTER SET ANY_CS) IS BEGIN NULL; END;", + "CREATE FUNCTION f (c TIMESTAMP WITH TIME ZONE) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", // VALUE partitioning takes one column (PLS-00757); BOOL is BOOLEAN // (PLS-00550); clause keywords are unquoted (PLS-00103). "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a, b)) IS BEGIN RETURN; END;", diff --git a/oracle/parser/type.go b/oracle/parser/type.go index 1c9dd870..f6bae298 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -228,6 +228,33 @@ var plsqlNonCharacterTypes = map[string]bool{ "MLSLABEL": true, "SYS_REFCURSOR": true, "NCHAR": true, "NVARCHAR2": true, "NCLOB": true, } +// predefinedTypeLead returns the name of the predefined datatype tn spells, +// "" for a name the text does not settle (a subtype, a schema-qualified +// type, t%TYPE). Multiword predefined types answer their first word: +// TIMESTAMP WITH [LOCAL] TIME ZONE, INTERVAL DAY|YEAR TO ..., LONG RAW. +func predefinedTypeLead(tn *nodes.TypeName) string { + if tn == nil || tn.IsPercType || tn.IsPercRowtype || tn.Names.Len() == 0 { + return "" + } + word := func(i int) string { + s, _ := tn.Names.Items[i].(*nodes.String) + if s == nil { + return "" + } + return s.Str + } + if tn.Names.Len() == 1 { + return word(0) + } + switch first, second := word(0), word(1); { + case first == "TIMESTAMP" && second == "WITH", + first == "INTERVAL" && (second == "DAY" || second == "YEAR"), + first == "LONG" && second == "RAW": + return first + } + return "" +} + // parsePLSQLCharacterSet parses CHARACTER SET { character_set | item%CHARSET } // after the datatype tn, the current token at CHARACTER. Each check below // follows from the text alone and was confirmed on Oracle 23ai; whether a @@ -237,10 +264,8 @@ func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) if tn.IsPercRowtype { return p.syntaxErrorAtCur() } - if !tn.IsPercType && tn.Names.Len() > 0 { - if first, ok := tn.Names.Items[0].(*nodes.String); ok && plsqlNonCharacterTypes[first.Str] { - return p.syntaxErrorAtCur() - } + if plsqlNonCharacterTypes[predefinedTypeLead(tn)] { + return p.syntaxErrorAtCur() } p.advance() // consume CHARACTER p.advance() // consume SET From db99faefb7c0c5eb600eae5ace7f37e22e48f2ee Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 09:53:51 +0000 Subject: [PATCH 11/22] fix(oracle): method heading checks, RESULT_CACHE parameter modes (BYT-10301) Review follow-up, each checked on Oracle 23ai: - Object type procedure methods and ALTER TYPE method signatures take the heading checks of other subprograms: no TABLE or COLUMNS pseudo-type (PLS-00765), item%CHARSET from a character parameter (PLS-00550), and an ALTER TYPE method result is unconstrained like a CREATE TYPE one. - RESULT_CACHE takes no OUT or IN OUT parameter (PLS-00999), standalone and in a package specification alike. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/alter_misc.go | 20 +++++++++++++------- oracle/parser/create_proc.go | 12 ++++++++++++ oracle/parser/create_type.go | 8 ++++++++ oracle/parser/subprogram_clause_test.go | 13 +++++++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/oracle/parser/alter_misc.go b/oracle/parser/alter_misc.go index 0cf05505..cd8ef0ca 100644 --- a/oracle/parser/alter_misc.go +++ b/oracle/parser/alter_misc.go @@ -3849,6 +3849,7 @@ func (p *Parser) parseAlterTypeAddDrop(stmt *nodes.AlterTypeStmt, action string) } func (p *Parser) parseAlterTypeMethodSig(stmt *nodes.AlterTypeStmt) error { + var params *nodes.List // FUNCTION or PROCEDURE if p.cur.Type == kwFUNCTION { stmt.MethodType = "FUNCTION" @@ -3869,7 +3870,8 @@ func (p *Parser) parseAlterTypeMethodSig(stmt *nodes.AlterTypeStmt) error { } if p.cur.Type == '(' { - params, parseErr126 := p.parseParameterList() + var parseErr126 error + params, parseErr126 = p.parseParameterList() if parseErr126 != nil { return parseErr126 } @@ -3899,18 +3901,22 @@ func (p *Parser) parseAlterTypeMethodSig(stmt *nodes.AlterTypeStmt) error { } } else { var parseErr127 error - stmt.MethodReturn, parseErr127 = p.parseTypeName() - if parseErr127 != - - // parseAlterTypeAttributes parses ( attribute [datatype] [, ...] ). - nil { + stmt.MethodReturn, parseErr127 = p.parsePLSQLDatatype(typeModsNone, charsetFlexible) + if parseErr127 != nil { return parseErr127 } } } - return nil + // A method signature takes the heading checks of CREATE TYPE methods: + // no TABLE or COLUMNS pseudo-type (PLS-00765), and item%CHARSET from a + // character parameter (PLS-00550). + if tn := p.tablePseudoType(params, stmt.MethodReturn); tn != nil { + return p.syntaxErrorAtType(tn) + } + return p.checkCharsetSources(params, stmt.MethodReturn) } +// parseAlterTypeAttributes parses ( attribute [datatype] [, ...] ). func (p *Parser) parseAlterTypeAttributes(withDatatype bool) ([]*nodes.TypeAttribute, error) { var attrs []*nodes.TypeAttribute diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index f07ce9ea..098f4718 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -279,6 +279,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s var notPolymorphic, notMacro, macroTok *Token var partTok Token var streamToks []Token + var resultCacheTok *Token note := func() { tok := p.cur if notPolymorphic == nil { @@ -309,6 +310,15 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.checkCharsetSources(stmt.Parameters, stmt.ReturnType); err != nil { return err } + // RESULT_CACHE takes no OUT or IN OUT parameter (PLS-00999 on + // Oracle 23ai, documented among the clause's restrictions). + if resultCacheTok != nil && stmt.Parameters != nil { + for _, item := range stmt.Parameters.Items { + if pr, ok := item.(*nodes.Parameter); ok && strings.Contains(pr.Mode, "OUT") { + return p.syntaxErrorAtTok(*resultCacheTok) + } + } + } return p.checkParallelArguments(stmt, partTok, streamToks) } for { @@ -382,6 +392,8 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s return err } note() + rcTok := p.cur + resultCacheTok = &rcTok stmt.ResultCache = true p.advance() if p.isKeywordStr("RELIES_ON") { diff --git a/oracle/parser/create_type.go b/oracle/parser/create_type.go index ba3867aa..cc7accb3 100644 --- a/oracle/parser/create_type.go +++ b/oracle/parser/create_type.go @@ -348,6 +348,14 @@ func (p *Parser) parseTypeBodyProcedure(inSpec bool) (*nodes.CreateProcedureStmt return nil, parseErr583 } } + // The heading checks of other procedures hold for a method too + // (PLS-00765, PLS-00550 on Oracle 23ai). + if tn := p.tablePseudoType(stmt.Parameters, nil); tn != nil { + return nil, p.syntaxErrorAtType(tn) + } + if err := p.checkCharsetSources(stmt.Parameters, nil); err != nil { + return nil, err + } if inSpec { var err error diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 197fd4c5..b9472cfe 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -69,6 +69,8 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (c pkg.rc) RETURN t ORDER c BY (a) PARALLEL_ENABLE (PARTITION c BY ANY) PIPELINED IS BEGIN RETURN; END;", "CREATE FUNCTION f (t TABLE, c COLUMNS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (t TABLE, x NUMBER) RETURN TABLE PIPELINED TABLE POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (a IN NUMBER) RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER PROCEDURE p (x VARCHAR2, b VARCHAR2 CHARACTER SET x%CHARSET))", // A quoted "TABLE" or "COLUMNS" names a user type, not the pseudo-type. "CREATE PROCEDURE p (a \"COLUMNS\") IS BEGIN NULL; END;", "CREATE PROCEDURE p (a \"TABLE\") IS BEGIN NULL; END;", @@ -204,6 +206,17 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN INTERVAL DAY TO SECOND SQL_MACRO IS BEGIN RETURN NULL; END;", "CREATE PROCEDURE p (a TIMESTAMP WITH TIME ZONE CHARACTER SET ANY_CS) IS BEGIN NULL; END;", "CREATE FUNCTION f (c TIMESTAMP WITH TIME ZONE) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN; END;", + // Type method procedures and ALTER TYPE signatures take the heading + // checks (PLS-00550, PLS-00765); RESULT_CACHE takes no OUT or IN OUT + // parameter (PLS-00999). + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER PROCEDURE p (x NUMBER, b VARCHAR2 CHARACTER SET x%CHARSET))", + "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER PROCEDURE p (t TABLE))", + "ALTER TYPE ty ADD MEMBER PROCEDURE p (x NUMBER, b VARCHAR2 CHARACTER SET x%CHARSET) CASCADE", + "ALTER TYPE ty ADD MEMBER FUNCTION f (t TABLE) RETURN NUMBER CASCADE", + "ALTER TYPE ty ADD MEMBER FUNCTION f RETURN VARCHAR2(10) CASCADE", + "CREATE FUNCTION f (a OUT NUMBER) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f (a IN OUT NUMBER) RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", + "CREATE PACKAGE k AS FUNCTION f (a IN OUT NOCOPY NUMBER) RETURN NUMBER RESULT_CACHE; END;", // VALUE partitioning takes one column (PLS-00757); BOOL is BOOLEAN // (PLS-00550); clause keywords are unquoted (PLS-00103). "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a, b)) IS BEGIN RETURN; END;", From bb4997f097eda3363796618cfd1206d926258617 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 10:09:10 +0000 Subject: [PATCH 12/22] fix(oracle): PTF parameter defaults, RESULT_CACHE types, AUTHID, sort columns (BYT-10301) Review follow-up, each checked on Oracle 23ai: - A polymorphic table function's TABLE parameter takes no default (PLS-00768); a COLUMNS one defaults to NULL if at all (PLS-00769). - RESULT_CACHE takes no parameter or result of a LOB, BFILE, or SYS_REFCURSOR type (PLS-00999); object types are left to the engine. - AUTHID CURRENT_USER is one word: AUTHID CURRENT and AUTHID CURRENT USER are PLS-00103 (a quoted "CURRENT_USER" compiles). - PARALLEL_ENABLE and streaming items are simple column names (PLS-00670): no expression, qualified name, or literal. Reference rows ref_182 and ref_183. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 38 +++++++++----- oracle/parser/subprogram_clause.go | 50 +++++++++++++++++-- oracle/parser/subprogram_clause_test.go | 25 +++++++++- .../testdata/coverage/reference_oracle.tsv | 2 + 4 files changed, 97 insertions(+), 18 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 098f4718..cd71001e 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -310,12 +310,19 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.checkCharsetSources(stmt.Parameters, stmt.ReturnType); err != nil { return err } - // RESULT_CACHE takes no OUT or IN OUT parameter (PLS-00999 on - // Oracle 23ai, documented among the clause's restrictions). - if resultCacheTok != nil && stmt.Parameters != nil { - for _, item := range stmt.Parameters.Items { - if pr, ok := item.(*nodes.Parameter); ok && strings.Contains(pr.Mode, "OUT") { - return p.syntaxErrorAtTok(*resultCacheTok) + // RESULT_CACHE takes no OUT or IN OUT parameter, and no parameter or + // result of a LOB, BFILE, or REF CURSOR type (PLS-00999 on Oracle + // 23ai, documented among the clause's restrictions). Object and + // record types are left to the engine. + if resultCacheTok != nil { + if notResultCacheable(stmt.ReturnType) { + return p.syntaxErrorAtTok(*resultCacheTok) + } + if stmt.Parameters != nil { + for _, item := range stmt.Parameters.Items { + if pr, ok := item.(*nodes.Parameter); ok && (strings.Contains(pr.Mode, "OUT") || notResultCacheable(pr.TypeName)) { + return p.syntaxErrorAtTok(*resultCacheTok) + } } } } @@ -484,6 +491,16 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s } } +// notResultCacheable reports whether tn is a predefined type a RESULT_CACHE +// function cannot take or return: a LOB, BFILE, or SYS_REFCURSOR. +func notResultCacheable(tn *nodes.TypeName) bool { + switch predefinedTypeLead(tn) { + case "BLOB", "CLOB", "NCLOB", "BFILE", "SYS_REFCURSOR": + return true + } + return false +} + // mayBeCharacterType reports whether tn may hold character data: it is not a // predefined non-character type (the national character types count as // character types) and not a %ROWTYPE record. A SQL macro returns such a @@ -557,17 +574,12 @@ func (p *Parser) parseOptionalAuthID() (string, error) { return "", nil } p.advance() + // CURRENT_USER is one word: Oracle 23ai rejects AUTHID CURRENT and + // AUTHID CURRENT USER with PLS-00103. if p.isIdentLikeStr("CURRENT_USER") { p.advance() return "CURRENT_USER", nil } - if p.cur.Type == kwCURRENT { - p.advance() - if p.isIdentLikeStr("USER") { - p.advance() - } - return "CURRENT_USER", nil - } if p.isIdentLikeStr("DEFINER") { p.advance() return "DEFINER", nil diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 8fd44866..5cc79326 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -288,8 +288,10 @@ func (p *Parser) parseParallelEnableSpec() (*nodes.ParallelEnableClause, Token, // VALUE partitioning takes one column (PLS-00757); HASH and RANGE // take several. if spec.PartitionBy == "VALUE" && cols.Len() > 1 { - loc := nodes.NodeLoc(cols.Items[1]) - return nil, Token{}, p.syntaxErrorAtTok(Token{Type: tokIDENT, Loc: loc.Start, End: loc.End}) + return nil, Token{}, p.syntaxErrorAtNode(cols.Items[1]) + } + if err := p.checkColumnNames(cols); err != nil { + return nil, Token{}, err } spec.Columns = cols default: @@ -330,11 +332,37 @@ func (p *Parser) parseStreamingClause() (*nodes.StreamingClause, Token, error) { if err != nil { return nil, Token{}, err } + if err := p.checkColumnNames(cols); err != nil { + return nil, Token{}, err + } clause.Columns = cols clause.Loc.End = p.prev.End return clause, argTok, nil } +// checkColumnNames checks that each item of a PARALLEL_ENABLE partitioning +// or a streaming clause is a simple column name: Oracle 23ai rejects an +// expression, a qualified name (c.a), or a literal with PLS-00670, and +// accepts a quoted name. +func (p *Parser) checkColumnNames(list *nodes.List) error { + if list == nil { + return nil + } + for _, item := range list.Items { + ref, ok := item.(*nodes.ColumnRef) + if !ok || ref.Table != "" || ref.Schema != "" || ref.Column == "*" || ref.OuterJoin { + return p.syntaxErrorAtNode(item) + } + } + return nil +} + +// syntaxErrorAtNode returns a syntax error at node n. +func (p *Parser) syntaxErrorAtNode(n nodes.Node) *ParseError { + loc := nodes.NodeLoc(n) + return p.syntaxErrorAtTok(Token{Type: tokIDENT, Loc: loc.Start, End: loc.End}) +} + // checkParallelArguments checks the arguments PARALLEL_ENABLE partitions and // the streaming clauses order or cluster, each from the function's own text // as Oracle 23ai does: the partitioned argument is one of the function's IN @@ -401,7 +429,9 @@ func predefinedTypeName(tn *nodes.TypeName) (string, bool) { // checkPolymorphicSignature checks a polymorphic table function's signature // as Oracle 23ai does from the text: it returns TABLE (PLS-00767) and takes -// exactly one TABLE parameter (PLS-00773, PLS-00766). +// exactly one TABLE parameter (PLS-00773, PLS-00766), which has no default +// (PLS-00768), while a COLUMNS parameter defaults to NULL if at all +// (PLS-00769). func (p *Parser) checkPolymorphicSignature(stmt *nodes.CreateFunctionStmt) error { if p.pseudoType(stmt.ReturnType) != "TABLE" { return p.syntaxErrorAtType(stmt.ReturnType) @@ -409,11 +439,23 @@ func (p *Parser) checkPolymorphicSignature(stmt *nodes.CreateFunctionStmt) error tables := 0 if stmt.Parameters != nil { for _, item := range stmt.Parameters.Items { - if pr, ok := item.(*nodes.Parameter); ok && p.pseudoType(pr.TypeName) == "TABLE" { + pr, ok := item.(*nodes.Parameter) + if !ok { + continue + } + switch p.pseudoType(pr.TypeName) { + case "TABLE": tables++ if tables > 1 { return p.syntaxErrorAtType(pr.TypeName) } + if pr.Default != nil { + return p.syntaxErrorAtNode(pr.Default) + } + case "COLUMNS": + if _, isNull := pr.Default.(*nodes.NullLiteral); pr.Default != nil && !isNull { + return p.syntaxErrorAtNode(pr.Default) + } } } } diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index b9472cfe..1a035269 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -37,7 +37,7 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY RANGE (a)) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a)) IS BEGIN RETURN; END;", - "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a + 1, b.c)) ORDER c BY (a, b) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) ORDER c BY (a, b) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", // RESULT_CACHE RELIES_ON and SQL_MACRO. "CREATE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON (t1, s.t2) IS BEGIN RETURN 1; END;", @@ -71,6 +71,10 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (t TABLE, x NUMBER) RETURN TABLE PIPELINED TABLE POLYMORPHIC USING pkg;", "CREATE FUNCTION f (a IN NUMBER) RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", "CREATE TYPE ty AS OBJECT (a NUMBER, MEMBER PROCEDURE p (x VARCHAR2, b VARCHAR2 CHARACTER SET x%CHARSET))", + "CREATE FUNCTION f (t TABLE, c COLUMNS DEFAULT NULL) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (a DATE, b VARCHAR2, c BOOLEAN) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", + "CREATE PROCEDURE p AUTHID \"CURRENT_USER\" IS BEGIN NULL; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (\"A\", b)) IS BEGIN RETURN; END;", // A quoted "TABLE" or "COLUMNS" names a user type, not the pseudo-type. "CREATE PROCEDURE p (a \"COLUMNS\") IS BEGIN NULL; END;", "CREATE PROCEDURE p (a \"TABLE\") IS BEGIN NULL; END;", @@ -217,6 +221,25 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (a OUT NUMBER) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE FUNCTION f (a IN OUT NUMBER) RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", "CREATE PACKAGE k AS FUNCTION f (a IN OUT NOCOPY NUMBER) RETURN NUMBER RESULT_CACHE; END;", + // TABLE takes no default (PLS-00768), COLUMNS only NULL (PLS-00769); + // RESULT_CACHE takes no LOB, BFILE, or REF CURSOR (PLS-00999); + // AUTHID CURRENT_USER is one word (PLS-00103); partition and + // streaming items are simple column names (PLS-00670). + "CREATE FUNCTION f (t TABLE DEFAULT dual) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (t TABLE DEFAULT NULL) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (t TABLE, c COLUMNS DEFAULT 1) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (a BLOB) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f (a NCLOB) RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f RETURN CLOB RESULT_CACHE IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f RETURN SYS_REFCURSOR RESULT_CACHE IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f (a BFILE) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", + "CREATE PROCEDURE p AUTHID CURRENT IS BEGIN NULL; END;", + "CREATE PROCEDURE p AUTHID CURRENT USER IS BEGIN NULL; END;", + "CREATE PACKAGE k AUTHID CURRENT AS PROCEDURE p; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a || 'x')) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY (lower(a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (c.a)) IS BEGIN RETURN; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY RANGE (1)) IS BEGIN RETURN; END;", // VALUE partitioning takes one column (PLS-00757); BOOL is BOOLEAN // (PLS-00550); clause keywords are unquoted (PLS-00103). "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a, b)) IS BEGIN RETURN; END;", diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index ff26328f..14b10f76 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -180,3 +180,5 @@ ref_178 plsql_body accept CREATE OR REPLACE FUNCTION omni_sma_{run} (a VARCHAR2) ref_179 plsql_body reject CREATE OR REPLACE FUNCTION omni_bad_qpoly_{run} (t TABLE) RETURN TABLE PIPELINED ROW "POLYMORPHIC" USING omni_pkg; quoted POLYMORPHIC is an identifier ref_180 plsql_body reject CREATE OR REPLACE PACKAGE omni_bad_qcoll_{run} DEFAULT "COLLATION" USING_NLS_COMP AS PROCEDURE p; END; quoted COLLATION is an identifier ref_181 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qtab_{run} (a "TABLE") IS BEGIN NULL; END; quoted TABLE names a user type +ref_182 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_aid_{run} AUTHID CURRENT USER IS BEGIN NULL; END; AUTHID CURRENT_USER written as two words +ref_183 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qaid_{run} AUTHID "CURRENT_USER" IS BEGIN NULL; END; quoted AUTHID value From 88f2fa2be0682c0ad94472b73476dc81ae8a226a Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 10:22:49 +0000 Subject: [PATCH 13/22] fix(oracle): leave quoted type names to the engine in heading checks (BYT-10301) Review follow-up, checked on Oracle 23ai: a package with SUBTYPE "NUMBER" IS VARCHAR2(100) as a SQL macro result, TYPE "SYS_REFCURSOR" IS REF CURSOR as a hash-partitioned argument, and SUBTYPE "CLOB" IS NUMBER as a RESULT_CACHE result compiles without errors. A quoted name is a user type, so the predefined-type checks (CHARACTER SET, SQL macro result, RESULT_CACHE, partition argument) now leave it to the engine. A polymorphic USING pkg@link compiles over a loopback link, so it stays accepted. Reference row ref_184. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 14 +++++++------- oracle/parser/subprogram_clause.go | 8 ++++---- oracle/parser/subprogram_clause_test.go | 7 +++++++ .../parser/testdata/coverage/reference_oracle.tsv | 1 + oracle/parser/type.go | 13 +++++++++---- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index cd71001e..69d91753 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -297,7 +297,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s return p.syntaxErrorAtTok(*notPolymorphic) case stmt.SqlMacro && notMacro != nil: return p.syntaxErrorAtTok(*notMacro) - case stmt.SqlMacro && !mayBeCharacterType(stmt.ReturnType): + case stmt.SqlMacro && !p.mayBeCharacterType(stmt.ReturnType): return p.syntaxErrorAtTok(*macroTok) } if stmt.Polymorphic == "" { @@ -315,12 +315,12 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s // 23ai, documented among the clause's restrictions). Object and // record types are left to the engine. if resultCacheTok != nil { - if notResultCacheable(stmt.ReturnType) { + if p.notResultCacheable(stmt.ReturnType) { return p.syntaxErrorAtTok(*resultCacheTok) } if stmt.Parameters != nil { for _, item := range stmt.Parameters.Items { - if pr, ok := item.(*nodes.Parameter); ok && (strings.Contains(pr.Mode, "OUT") || notResultCacheable(pr.TypeName)) { + if pr, ok := item.(*nodes.Parameter); ok && (strings.Contains(pr.Mode, "OUT") || p.notResultCacheable(pr.TypeName)) { return p.syntaxErrorAtTok(*resultCacheTok) } } @@ -493,8 +493,8 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s // notResultCacheable reports whether tn is a predefined type a RESULT_CACHE // function cannot take or return: a LOB, BFILE, or SYS_REFCURSOR. -func notResultCacheable(tn *nodes.TypeName) bool { - switch predefinedTypeLead(tn) { +func (p *Parser) notResultCacheable(tn *nodes.TypeName) bool { + switch p.predefinedTypeLead(tn) { case "BLOB", "CLOB", "NCLOB", "BFILE", "SYS_REFCURSOR": return true } @@ -506,14 +506,14 @@ func notResultCacheable(tn *nodes.TypeName) bool { // character types) and not a %ROWTYPE record. A SQL macro returns such a // type (PLS-00776), and item%CHARSET takes its character set from one // (PLS-00550). A name the text does not settle is left to the engine. -func mayBeCharacterType(tn *nodes.TypeName) bool { +func (p *Parser) mayBeCharacterType(tn *nodes.TypeName) bool { if tn == nil { return true } if tn.IsPercRowtype { return false } - switch lead := predefinedTypeLead(tn); lead { + switch lead := p.predefinedTypeLead(tn); lead { case "NCHAR", "NVARCHAR2", "NCLOB": return true default: diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 5cc79326..ff2b6267 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -393,7 +393,7 @@ func (p *Parser) checkParallelArguments(stmt *nodes.CreateFunctionStmt, partTok // predefined non-cursor type never qualifies, nor SYS_REFCURSOR with // HASH, RANGE, VALUE, ORDER BY, or CLUSTER BY. A named type is left to // the engine. - if name, ok := predefinedTypeName(param.TypeName); ok { + if name, ok := p.predefinedTypeName(param.TypeName); ok { weakAllowed := name == "SYS_REFCURSOR" && stmt.ParallelSpec.PartitionBy == "ANY" && len(streamToks) == 0 if !weakAllowed { return p.syntaxErrorAtTok(partTok) @@ -412,14 +412,14 @@ func (p *Parser) checkParallelArguments(stmt *nodes.CreateFunctionStmt, partTok // predefinedTypeName returns the name of tn when it is a predefined datatype // that is not a REF CURSOR of the program's own: SYS_REFCURSOR, or a scalar, // character, LOB, or %ROWTYPE record type. -func predefinedTypeName(tn *nodes.TypeName) (string, bool) { +func (p *Parser) predefinedTypeName(tn *nodes.TypeName) (string, bool) { if tn == nil { return "", false } if tn.IsPercRowtype { return "%ROWTYPE", true } - switch lead := predefinedTypeLead(tn); lead { + switch lead := p.predefinedTypeLead(tn); lead { case "CHAR", "CHARACTER", "VARCHAR2", "VARCHAR", "STRING", "CLOB": return lead, true default: @@ -484,7 +484,7 @@ func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) if tn == nil || !tn.IsPercCharset { return nil } - if src, ok := byName[tn.CharacterSet]; ok && !mayBeCharacterType(src.TypeName) { + if src, ok := byName[tn.CharacterSet]; ok && !p.mayBeCharacterType(src.TypeName) { return p.syntaxErrorAtType(tn) } return nil diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 1a035269..7f854445 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -75,6 +75,13 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (a DATE, b VARCHAR2, c BOOLEAN) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE PROCEDURE p AUTHID \"CURRENT_USER\" IS BEGIN NULL; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (\"A\", b)) IS BEGIN RETURN; END;", + // A quoted name is a user type, not the predefined one: Oracle 23ai + // compiles this package. + "CREATE PACKAGE k AS SUBTYPE \"NUMBER\" IS VARCHAR2(100); FUNCTION m RETURN \"NUMBER\" SQL_MACRO; " + + "TYPE \"SYS_REFCURSOR\" IS REF CURSOR RETURN dual%ROWTYPE; " + + "FUNCTION p (c \"SYS_REFCURSOR\") RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (dummy)); " + + "SUBTYPE \"CLOB\" IS NUMBER; FUNCTION r RETURN \"CLOB\" RESULT_CACHE; END;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg@lnk;", // A quoted "TABLE" or "COLUMNS" names a user type, not the pseudo-type. "CREATE PROCEDURE p (a \"COLUMNS\") IS BEGIN NULL; END;", "CREATE PROCEDURE p (a \"TABLE\") IS BEGIN NULL; END;", diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 14b10f76..26748dc2 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -182,3 +182,4 @@ ref_180 plsql_body reject CREATE OR REPLACE PACKAGE omni_bad_qcoll_{run} DEFAULT ref_181 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qtab_{run} (a "TABLE") IS BEGIN NULL; END; quoted TABLE names a user type ref_182 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_aid_{run} AUTHID CURRENT USER IS BEGIN NULL; END; AUTHID CURRENT_USER written as two words ref_183 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qaid_{run} AUTHID "CURRENT_USER" IS BEGIN NULL; END; quoted AUTHID value +ref_184 plsql_body accept CREATE OR REPLACE PACKAGE omni_quoted_{run} AS SUBTYPE "NUMBER" IS VARCHAR2(100); FUNCTION m RETURN "NUMBER" SQL_MACRO; SUBTYPE "CLOB" IS NUMBER; FUNCTION r RETURN "CLOB" RESULT_CACHE; END; quoted names of predefined types are user types diff --git a/oracle/parser/type.go b/oracle/parser/type.go index f6bae298..b513eb75 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -230,12 +230,17 @@ var plsqlNonCharacterTypes = map[string]bool{ // predefinedTypeLead returns the name of the predefined datatype tn spells, // "" for a name the text does not settle (a subtype, a schema-qualified -// type, t%TYPE). Multiword predefined types answer their first word: -// TIMESTAMP WITH [LOCAL] TIME ZONE, INTERVAL DAY|YEAR TO ..., LONG RAW. -func predefinedTypeLead(tn *nodes.TypeName) string { +// type, t%TYPE, or a quoted name: "NUMBER" may be a user subtype of +// VARCHAR2, and Oracle 23ai compiles one as a SQL macro result). Multiword +// predefined types answer their first word: TIMESTAMP WITH [LOCAL] TIME +// ZONE, INTERVAL DAY|YEAR TO ..., LONG RAW. +func (p *Parser) predefinedTypeLead(tn *nodes.TypeName) string { if tn == nil || tn.IsPercType || tn.IsPercRowtype || tn.Names.Len() == 0 { return "" } + if tn.Loc.Start >= 0 && tn.Loc.Start < len(p.source) && p.source[tn.Loc.Start] == '"' { + return "" + } word := func(i int) string { s, _ := tn.Names.Items[i].(*nodes.String) if s == nil { @@ -264,7 +269,7 @@ func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) if tn.IsPercRowtype { return p.syntaxErrorAtCur() } - if plsqlNonCharacterTypes[predefinedTypeLead(tn)] { + if plsqlNonCharacterTypes[p.predefinedTypeLead(tn)] { return p.syntaxErrorAtCur() } p.advance() // consume CHARACTER From 8f1030233e85a8e7c82aace2dcf13eb15bb032ed Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 10:38:28 +0000 Subject: [PATCH 14/22] fix(oracle): pipelined result types, RESULT_CACHE on pipelined, macro packages (BYT-10301) Review follow-up, each checked on Oracle 23ai: - An ordinary pipelined function returns a collection: a predefined scalar or %ROWTYPE result is PLS-00630, with or without USING; a named result type is left to the engine. - A pipelined function is not result-cached (PLS-00999). - A package holding a SQL macro cannot be AUTHID CURRENT_USER (PLS-00782). Nested SQL macros and nested RESULT_CACHE functions compile, so they stay accepted. Two fixtures that returned NUMBER from a pipelined function now return a collection type. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 24 ++++++++++++++++++- oracle/parser/create_proc_test.go | 4 +++- oracle/parser/subprogram_clause_test.go | 16 ++++++++++++- .../testdata/coverage/loc_node_coverage.tsv | 2 +- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 69d91753..7875b9df 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -279,7 +279,7 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s var notPolymorphic, notMacro, macroTok *Token var partTok Token var streamToks []Token - var resultCacheTok *Token + var resultCacheTok, pipelinedTok *Token note := func() { tok := p.cur if notPolymorphic == nil { @@ -310,6 +310,17 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.checkCharsetSources(stmt.Parameters, stmt.ReturnType); err != nil { return err } + // An ordinary pipelined function returns a collection (PLS-00630 for a + // predefined scalar or %ROWTYPE result) and is not result-cached + // (PLS-00999); a named result type is left to the engine. + if stmt.Pipelined && stmt.Polymorphic == "" { + if _, scalar := p.predefinedTypeName(stmt.ReturnType); scalar { + return p.syntaxErrorAtTok(*pipelinedTok) + } + if resultCacheTok != nil { + return p.syntaxErrorAtTok(*resultCacheTok) + } + } // RESULT_CACHE takes no OUT or IN OUT parameter, and no parameter or // result of a LOB, BFILE, or REF CURSOR type (PLS-00999 on Oracle // 23ai, documented among the clause's restrictions). Object and @@ -345,6 +356,8 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s tok := p.cur notMacro = &tok } + ptok := p.cur + pipelinedTok = &ptok stmt.Pipelined = true p.advance() if (p.cur.Type == kwROW || p.cur.Type == kwTABLE) && p.isIdentLikeStrAt(p.peekNext(), "POLYMORPHIC") { @@ -671,6 +684,15 @@ func (p *Parser) parseCreatePackageStmt(start int, orReplace, ifNotExists, editi nil { return nil, parseErr469 } + // A package holding a SQL macro has definer's rights: Oracle 23ai + // rejects AUTHID CURRENT_USER on one with PLS-00782. + if stmt.AuthID == "CURRENT_USER" && stmt.Body != nil { + for _, item := range stmt.Body.Items { + if fn, ok := item.(*nodes.CreateFunctionStmt); ok && fn.SqlMacro { + return nil, p.syntaxErrorAtTok(Token{Type: kwFUNCTION, Loc: fn.Loc.Start, End: fn.Loc.Start + len("FUNCTION")}) + } + } + } if p.cur.Type != kwEND { return nil, p.syntaxErrorAtCur() diff --git a/oracle/parser/create_proc_test.go b/oracle/parser/create_proc_test.go index 6cdc78af..e840502f 100644 --- a/oracle/parser/create_proc_test.go +++ b/oracle/parser/create_proc_test.go @@ -190,7 +190,9 @@ func TestParseCreateFunctionDeterministic(t *testing.T) { } func TestParseCreateFunctionPipelined(t *testing.T) { - sql := `CREATE FUNCTION pipe_fn RETURN NUMBER PIPELINED IS BEGIN NULL; END;` + // A pipelined function returns a collection: Oracle 23ai rejects a + // NUMBER result with PLS-00630. + sql := `CREATE FUNCTION pipe_fn RETURN num_tab PIPELINED IS BEGIN NULL; END;` result, err := Parse(sql) if err != nil { t.Fatalf("parse error: %v", err) diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 7f854445..a8bde44c 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -75,6 +75,11 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (a DATE, b VARCHAR2, c BOOLEAN) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE PROCEDURE p AUTHID \"CURRENT_USER\" IS BEGIN NULL; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (\"A\", b)) IS BEGIN RETURN; END;", + // Nested SQL macros and result-cached functions compile, and a + // definer's rights package may hold a SQL macro. + "CREATE PROCEDURE p IS FUNCTION f RETURN VARCHAR2 SQL_MACRO IS BEGIN RETURN NULL; END; BEGIN NULL; END;", + "CREATE PROCEDURE p IS FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END;", + "CREATE PACKAGE k AUTHID DEFINER AS FUNCTION f RETURN VARCHAR2 SQL_MACRO; END;", // A quoted name is a user type, not the predefined one: Oracle 23ai // compiles this package. "CREATE PACKAGE k AS SUBTYPE \"NUMBER\" IS VARCHAR2(100); FUNCTION m RETURN \"NUMBER\" SQL_MACRO; " + @@ -247,6 +252,15 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY ANY) ORDER c BY (lower(a)) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (c.a)) IS BEGIN RETURN; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY RANGE (1)) IS BEGIN RETURN; END;", + // An ordinary pipelined function returns a collection (PLS-00630) and + // is not result-cached (PLS-00999); a package holding a SQL macro is + // not AUTHID CURRENT_USER (PLS-00782). + "CREATE FUNCTION f RETURN NUMBER PIPELINED IS BEGIN RETURN; END;", + "CREATE FUNCTION f RETURN VARCHAR2 PIPELINED IS BEGIN RETURN; END;", + "CREATE FUNCTION f RETURN dual%ROWTYPE PIPELINED IS BEGIN RETURN; END;", + "CREATE FUNCTION f RETURN NUMBER PIPELINED USING impl;", + "CREATE FUNCTION f RETURN t PIPELINED RESULT_CACHE IS BEGIN RETURN; END;", + "CREATE PACKAGE k AUTHID CURRENT_USER AS FUNCTION f RETURN VARCHAR2 SQL_MACRO; END;", // VALUE partitioning takes one column (PLS-00757); BOOL is BOOLEAN // (PLS-00550); clause keywords are unquoted (PLS-00103). "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY VALUE (a, b)) IS BEGIN RETURN; END;", @@ -371,7 +385,7 @@ func TestSubprogramHeadingClausesAST(t *testing.T) { t.Errorf("polymorphic function = %+v", ptf) } - par := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) CLUSTER c BY (a) RESULT_CACHE RELIES_ON (t1) IS BEGIN RETURN; END;") + par := parseOne[*ast.CreateFunctionStmt](t, "CREATE FUNCTION f (c pkg.rc) RETURN t PARALLEL_ENABLE (PARTITION c BY HASH (a, b)) CLUSTER c BY (a) RESULT_CACHE RELIES_ON (t1) IS BEGIN RETURN NULL; END;") if spec := par.ParallelSpec; spec == nil || spec.Argument != "C" || spec.PartitionBy != "HASH" || spec.Columns.Len() != 2 { t.Errorf("parallel spec = %+v", par.ParallelSpec) } diff --git a/oracle/parser/testdata/coverage/loc_node_coverage.tsv b/oracle/parser/testdata/coverage/loc_node_coverage.tsv index e02543b7..4d7b0847 100644 --- a/oracle/parser/testdata/coverage/loc_node_coverage.tsv +++ b/oracle/parser/testdata/coverage/loc_node_coverage.tsv @@ -256,7 +256,7 @@ node_type family status fixture notes debt_class approval next_action *nodes.PLSQLFetch plsql covered DECLARE CURSOR c IS SELECT 1 FROM dual; x NUMBER; BEGIN OPEN c; FETCH c INTO x; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.PLSQLClose plsql covered DECLARE CURSOR c IS SELECT 1 FROM dual; BEGIN OPEN c; CLOSE c; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.PLSQLExecImmediate plsql covered BEGIN EXECUTE IMMEDIATE 'SELECT 1 FROM dual'; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard -*nodes.PLSQLPipeRow plsql covered CREATE FUNCTION f RETURN NUMBER PIPELINED IS BEGIN PIPE ROW (1); RETURN; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard +*nodes.PLSQLPipeRow plsql covered CREATE FUNCTION f RETURN num_tab PIPELINED IS BEGIN PIPE ROW (1); RETURN; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.PLSQLPragma plsql covered DECLARE PRAGMA AUTONOMOUS_TRANSACTION; BEGIN NULL; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.ExceptionHandler plsql covered BEGIN RAISE; EXCEPTION WHEN OTHERS THEN NULL; END; direct parser fixture added by strict Loc audit none covered keep_regression_guard *nodes.PLSQLLabeledStatement plsql covered BEGIN <> <> x := 1; END; labeled statement wrapper span (BYT-9909 label support) none covered keep_regression_guard From d671057928e39a34e36cc2e930603a83c1f0c223 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 11:16:53 +0000 Subject: [PATCH 15/22] fix(oracle): quoted names in %CHARSET and sort columns, %ROWTYPE result cache (BYT-10301) Review follow-up, each checked on Oracle 23ai: - A dotted %CHARSET source names an item outside the heading: a function with a quoted NUMBER formal "K.C" and a k.c%CHARSET result compiles. - A quoted "*" names a column: PARTITION BY HASH ("*") over such a column compiles; only the unquoted * is the wildcard. - RESULT_CACHE takes no %ROWTYPE record parameter (PLS-00999); a %ROWTYPE result compiles, so it stays accepted. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 12 +++++++----- oracle/parser/subprogram_clause.go | 12 ++++++++++-- oracle/parser/subprogram_clause_test.go | 6 ++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 7875b9df..00f1d83d 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -321,17 +321,19 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s return p.syntaxErrorAtTok(*resultCacheTok) } } - // RESULT_CACHE takes no OUT or IN OUT parameter, and no parameter or - // result of a LOB, BFILE, or REF CURSOR type (PLS-00999 on Oracle - // 23ai, documented among the clause's restrictions). Object and - // record types are left to the engine. + // RESULT_CACHE takes no OUT or IN OUT parameter, no parameter or + // result of a LOB, BFILE, or REF CURSOR type, and no %ROWTYPE record + // parameter (PLS-00999 on Oracle 23ai, documented among the clause's + // restrictions); a %ROWTYPE result compiles. Object types are left + // to the engine. if resultCacheTok != nil { if p.notResultCacheable(stmt.ReturnType) { return p.syntaxErrorAtTok(*resultCacheTok) } if stmt.Parameters != nil { for _, item := range stmt.Parameters.Items { - if pr, ok := item.(*nodes.Parameter); ok && (strings.Contains(pr.Mode, "OUT") || p.notResultCacheable(pr.TypeName)) { + if pr, ok := item.(*nodes.Parameter); ok && (strings.Contains(pr.Mode, "OUT") || p.notResultCacheable(pr.TypeName) || + (pr.TypeName != nil && pr.TypeName.IsPercRowtype)) { return p.syntaxErrorAtTok(*resultCacheTok) } } diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index ff2b6267..ddce6ef9 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -1,6 +1,8 @@ package parser import ( + "strings" + nodes "github.com/bytebase/omni/oracle/ast" ) @@ -350,7 +352,10 @@ func (p *Parser) checkColumnNames(list *nodes.List) error { } for _, item := range list.Items { ref, ok := item.(*nodes.ColumnRef) - if !ok || ref.Table != "" || ref.Schema != "" || ref.Column == "*" || ref.OuterJoin { + quoted := ok && ref.Loc.Start >= 0 && ref.Loc.Start < len(p.source) && p.source[ref.Loc.Start] == '"' + // A quoted "*" names a column (Oracle 23ai compiles HASH ("*")); + // only the unquoted * is the wildcard. + if !ok || ref.Table != "" || ref.Schema != "" || (ref.Column == "*" && !quoted) || ref.OuterJoin { return p.syntaxErrorAtNode(item) } } @@ -481,7 +486,10 @@ func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) } } check := func(tn *nodes.TypeName) error { - if tn == nil || !tn.IsPercCharset { + // A dotted source (k.c%CHARSET) names an item outside the heading, + // even when a quoted formal is spelled "K.C": Oracle 23ai compiles + // that function. + if tn == nil || !tn.IsPercCharset || strings.Contains(tn.CharacterSet, ".") { return nil } if src, ok := byName[tn.CharacterSet]; ok && !p.mayBeCharacterType(src.TypeName) { diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index a8bde44c..48eeb0b4 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -75,6 +75,11 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (a DATE, b VARCHAR2, c BOOLEAN) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE PROCEDURE p AUTHID \"CURRENT_USER\" IS BEGIN NULL; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (\"A\", b)) IS BEGIN RETURN; END;", + // A quoted formal spelled like a dotted %CHARSET source, a quoted "*" + // column, and a result-cached %ROWTYPE result compile. + "CREATE FUNCTION f (\"K.C\" NUMBER) RETURN VARCHAR2 CHARACTER SET k.c%CHARSET IS BEGIN RETURN NULL; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (\"*\")) IS BEGIN RETURN; END;", + "CREATE FUNCTION f RETURN dual%ROWTYPE RESULT_CACHE IS r dual%ROWTYPE; BEGIN RETURN r; END;", // Nested SQL macros and result-cached functions compile, and a // definer's rights package may hold a SQL macro. "CREATE PROCEDURE p IS FUNCTION f RETURN VARCHAR2 SQL_MACRO IS BEGIN RETURN NULL; END; BEGIN NULL; END;", @@ -245,6 +250,7 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN CLOB RESULT_CACHE IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f RETURN SYS_REFCURSOR RESULT_CACHE IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f (a BFILE) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f (a dual%ROWTYPE) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE PROCEDURE p AUTHID CURRENT IS BEGIN NULL; END;", "CREATE PROCEDURE p AUTHID CURRENT USER IS BEGIN NULL; END;", "CREATE PACKAGE k AUTHID CURRENT AS PROCEDURE p; END;", From c2edb488fbf3a6433bd7cac9a969fa81b81d0a22 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 11:56:23 +0000 Subject: [PATCH 16/22] fix(oracle): quoted %CHARSET formals, RESULT_CACHE in anonymous blocks (BYT-10301) Review follow-up, each checked on Oracle 23ai: - A single quoted %CHARSET source such as "K.C" names the formal of that spelling (PLS-00550 when it is numeric); only a qualified k.c source names an item outside the heading. The parser records which it was. - No subprogram in a top-level anonymous block is result-cached (PLS-00999, at any nesting depth); a DECLARE block or nested function inside a stored unit compiles, so those stay accepted. ORDER BY and CLUSTER BY together compile with HASH, RANGE, and ANY partitioning, so they stay accepted. Reference row ref_185. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 6 ++++ oracle/parser/parser.go | 28 ++++++++++++++++--- oracle/parser/subprogram_clause.go | 10 +++---- oracle/parser/subprogram_clause_test.go | 7 +++++ .../testdata/coverage/reference_oracle.tsv | 1 + oracle/parser/type.go | 12 ++++++-- 6 files changed, 51 insertions(+), 13 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 00f1d83d..5f3e5418 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -413,6 +413,12 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "RESULT_CACHE"); err != nil { return err } + // No subprogram in a top-level anonymous block is result-cached + // (PLS-00999); one nested in a stored unit, even in a DECLARE + // block there, compiles. + if p.inAnonymousBlock { + return p.syntaxErrorAtCur() + } note() rcTok := p.cur resultCacheTok = &rcTok diff --git a/oracle/parser/parser.go b/oracle/parser/parser.go index 7ef71c32..fc081dbb 100644 --- a/oracle/parser/parser.go +++ b/oracle/parser/parser.go @@ -31,6 +31,28 @@ type Parser struct { // typeMods says what the parenthesized modifiers of the datatype being // parsed may hold; parsePLSQLDatatype sets it for PL/SQL datatypes. typeMods typeModMode + + // qualifiedCharsets records the datatypes whose item%CHARSET source is a + // qualified name (k.c), as opposed to one identifier that may itself be + // a quoted "K.C". + qualifiedCharsets map[*nodes.TypeName]bool + + // inAnonymousBlock is set while a top-level anonymous block is parsed: + // Oracle 23ai rejects RESULT_CACHE on any subprogram in one (PLS-00999). + inAnonymousBlock bool +} + +// parseAnonymousBlock parses a top-level anonymous block, noting that its +// subprograms are in one. +func (p *Parser) parseAnonymousBlock() (nodes.StmtNode, error) { + saved := p.inAnonymousBlock + p.inAnonymousBlock = true + block, err := p.parsePLSQLBlock() + p.inAnonymousBlock = saved + if err != nil { + return nil, err + } + return block, nil } // Parse parses a SQL string into an AST list. @@ -304,10 +326,8 @@ func (p *Parser) parseStmt() (nodes.StmtNode, error) { return p.parseRevokeStmt() case kwCREATE: return p.parseCreateStmt() - case kwDECLARE, kwBEGIN: - return p.parsePLSQLBlock() - case tokLABELOPEN: - return p.parsePLSQLBlock() + case kwDECLARE, kwBEGIN, tokLABELOPEN: + return p.parseAnonymousBlock() default: // Handle identifier-based statements if p.isIdentLike() { diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index ddce6ef9..33085cb6 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -1,8 +1,6 @@ package parser import ( - "strings" - nodes "github.com/bytebase/omni/oracle/ast" ) @@ -486,10 +484,10 @@ func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) } } check := func(tn *nodes.TypeName) error { - // A dotted source (k.c%CHARSET) names an item outside the heading, - // even when a quoted formal is spelled "K.C": Oracle 23ai compiles - // that function. - if tn == nil || !tn.IsPercCharset || strings.Contains(tn.CharacterSet, ".") { + // A qualified source (k.c%CHARSET) names an item outside the + // heading, even when a quoted formal is spelled "K.C" (Oracle 23ai + // compiles that function); a single quoted "K.C" names the formal. + if tn == nil || !tn.IsPercCharset || p.qualifiedCharsets[tn] { return nil } if src, ok := byName[tn.CharacterSet]; ok && !p.mayBeCharacterType(src.TypeName) { diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index 48eeb0b4..a13f6d5a 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -80,6 +80,8 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (\"K.C\" NUMBER) RETURN VARCHAR2 CHARACTER SET k.c%CHARSET IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (\"*\")) IS BEGIN RETURN; END;", "CREATE FUNCTION f RETURN dual%ROWTYPE RESULT_CACHE IS r dual%ROWTYPE; BEGIN RETURN r; END;", + "CREATE PROCEDURE p IS BEGIN DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END; BEGIN NULL; END; END;", + "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", // Nested SQL macros and result-cached functions compile, and a // definer's rights package may hold a SQL macro. "CREATE PROCEDURE p IS FUNCTION f RETURN VARCHAR2 SQL_MACRO IS BEGIN RETURN NULL; END; BEGIN NULL; END;", @@ -251,6 +253,11 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN SYS_REFCURSOR RESULT_CACHE IS BEGIN RETURN NULL; END;", "CREATE FUNCTION f (a BFILE) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE FUNCTION f (a dual%ROWTYPE) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", + "CREATE FUNCTION f (\"K.C\" NUMBER) RETURN VARCHAR2 CHARACTER SET \"K.C\"%CHARSET IS BEGIN RETURN NULL; END;", + "DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END;", + "DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END; BEGIN NULL; END;", + "BEGIN DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END; END;", + "DECLARE PROCEDURE q IS FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END; BEGIN NULL; END;", "CREATE PROCEDURE p AUTHID CURRENT IS BEGIN NULL; END;", "CREATE PROCEDURE p AUTHID CURRENT USER IS BEGIN NULL; END;", "CREATE PACKAGE k AUTHID CURRENT AS PROCEDURE p; END;", diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 26748dc2..d41ef9ec 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -183,3 +183,4 @@ ref_181 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qtab_{run} (a "TABLE" ref_182 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_aid_{run} AUTHID CURRENT USER IS BEGIN NULL; END; AUTHID CURRENT_USER written as two words ref_183 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qaid_{run} AUTHID "CURRENT_USER" IS BEGIN NULL; END; quoted AUTHID value ref_184 plsql_body accept CREATE OR REPLACE PACKAGE omni_quoted_{run} AS SUBTYPE "NUMBER" IS VARCHAR2(100); FUNCTION m RETURN "NUMBER" SQL_MACRO; SUBTYPE "CLOB" IS NUMBER; FUNCTION r RETURN "CLOB" RESULT_CACHE; END; quoted names of predefined types are user types +ref_185 plsql reject DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END; RESULT_CACHE on a subprogram in an anonymous block (PLS-00999) diff --git a/oracle/parser/type.go b/oracle/parser/type.go index b513eb75..ac61cf4b 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -1,8 +1,6 @@ package parser import ( - "strings" - nodes "github.com/bytebase/omni/oracle/ast" ) @@ -282,6 +280,7 @@ func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) if err != nil { return err } + qualified := false for p.cur.Type == '.' { p.advance() if !p.isIdentLike() { @@ -292,6 +291,7 @@ func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) return err } name += "." + part + qualified = true } if p.cur.Type == '%' { p.advance() @@ -301,7 +301,13 @@ func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) } p.advance() tn.IsPercCharset = true - } else if strings.Contains(name, ".") { + if qualified { + if p.qualifiedCharsets == nil { + p.qualifiedCharsets = map[*nodes.TypeName]bool{} + } + p.qualifiedCharsets[tn] = true + } + } else if qualified { return p.syntaxErrorAtCur() } switch { From 29a5f40ebb06ec3992fb8a2c69860c2954fe219c Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 12:09:47 +0000 Subject: [PATCH 17/22] fix(oracle): CHARACTER SET on pseudo-types, PARALLEL_ENABLE on nested functions (BYT-10301) Review follow-up, each checked on Oracle 23ai: - CHARACTER SET is not allowed on the TABLE or COLUMNS pseudo-type, parameter or result (PLS-00550). - A nested function is not parallel-enabled (PLS-00712); a private package body function, and nested DETERMINISTIC, PIPELINED, and polymorphic functions, compile. A nested aggregate compiles, in a stored unit and an anonymous block, so it stays accepted. Reference row ref_186. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 5 +++++ oracle/parser/subprogram_clause_test.go | 9 +++++++++ oracle/parser/testdata/coverage/reference_oracle.tsv | 1 + oracle/parser/type.go | 4 +++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 5f3e5418..872721b8 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -380,6 +380,11 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if err := p.firstClause(seen, "PARALLEL_ENABLE"); err != nil { return err } + // A nested function is not parallel-enabled (PLS-00712 on + // Oracle 23ai); a private package body function may be. + if level == subprogramNested { + return p.syntaxErrorAtCur() + } note() stmt.Parallel = true p.advance() diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index a13f6d5a..ed3db0ee 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -82,6 +82,9 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f RETURN dual%ROWTYPE RESULT_CACHE IS r dual%ROWTYPE; BEGIN RETURN r; END;", "CREATE PROCEDURE p IS BEGIN DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END; BEGIN NULL; END; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", + "CREATE PACKAGE BODY k AS FUNCTION g RETURN NUMBER PARALLEL_ENABLE IS BEGIN RETURN 1; END; END;", + "CREATE PROCEDURE p IS FUNCTION f RETURN t PIPELINED DETERMINISTIC IS BEGIN RETURN; END; BEGIN NULL; END;", + "DECLARE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl; BEGIN NULL; END;", // Nested SQL macros and result-cached functions compile, and a // definer's rights package may hold a SQL macro. "CREATE PROCEDURE p IS FUNCTION f RETURN VARCHAR2 SQL_MACRO IS BEGIN RETURN NULL; END; BEGIN NULL; END;", @@ -255,6 +258,12 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (a dual%ROWTYPE) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE FUNCTION f (\"K.C\" NUMBER) RETURN VARCHAR2 CHARACTER SET \"K.C\"%CHARSET IS BEGIN RETURN NULL; END;", "DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END;", + // CHARACTER SET on a pseudo-type (PLS-00550); PARALLEL_ENABLE on a + // nested function (PLS-00712). + "CREATE FUNCTION f (t TABLE CHARACTER SET ANY_CS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE FUNCTION f (t TABLE) RETURN TABLE CHARACTER SET CHAR_CS PIPELINED ROW POLYMORPHIC USING pkg;", + "CREATE PROCEDURE p IS FUNCTION f RETURN NUMBER PARALLEL_ENABLE IS BEGIN RETURN 1; END; BEGIN NULL; END;", + "DECLARE FUNCTION f RETURN NUMBER PARALLEL_ENABLE IS BEGIN RETURN 1; END; BEGIN NULL; END;", "DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END; BEGIN NULL; END;", "BEGIN DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END; END;", "DECLARE PROCEDURE q IS FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END; BEGIN NULL; END;", diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index d41ef9ec..4ad9bf7f 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -184,3 +184,4 @@ ref_182 plsql_body reject CREATE OR REPLACE PROCEDURE omni_bad_aid_{run} AUTHID ref_183 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qaid_{run} AUTHID "CURRENT_USER" IS BEGIN NULL; END; quoted AUTHID value ref_184 plsql_body accept CREATE OR REPLACE PACKAGE omni_quoted_{run} AS SUBTYPE "NUMBER" IS VARCHAR2(100); FUNCTION m RETURN "NUMBER" SQL_MACRO; SUBTYPE "CLOB" IS NUMBER; FUNCTION r RETURN "CLOB" RESULT_CACHE; END; quoted names of predefined types are user types ref_185 plsql reject DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END; RESULT_CACHE on a subprogram in an anonymous block (PLS-00999) +ref_186 plsql reject DECLARE FUNCTION f RETURN NUMBER PARALLEL_ENABLE IS BEGIN RETURN 1; END; BEGIN NULL; END; PARALLEL_ENABLE on a nested function (PLS-00712) diff --git a/oracle/parser/type.go b/oracle/parser/type.go index ac61cf4b..1d94c23f 100644 --- a/oracle/parser/type.go +++ b/oracle/parser/type.go @@ -267,7 +267,9 @@ func (p *Parser) parsePLSQLCharacterSet(tn *nodes.TypeName, use plsqlCharsetUse) if tn.IsPercRowtype { return p.syntaxErrorAtCur() } - if plsqlNonCharacterTypes[p.predefinedTypeLead(tn)] { + // Neither is a predefined non-character type, nor is the TABLE or + // COLUMNS pseudo-type of a polymorphic table function (PLS-00550). + if plsqlNonCharacterTypes[p.predefinedTypeLead(tn)] || p.pseudoType(tn) != "" { return p.syntaxErrorAtCur() } p.advance() // consume CHARACTER From ca834a8e8387be6746485483bc7ea5b20c321bf0 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 12:24:58 +0000 Subject: [PATCH 18/22] fix(oracle): check %CHARSET sources in cursor parameter lists (BYT-10301) Review follow-up, checked on Oracle 23ai: a cursor parameter whose character set comes from a NUMBER or DATE cursor parameter is PLS-00550 in an anonymous block and a stored unit alike, while a VARCHAR2 source compiles. checkCharsetSources now takes cursor parameters as well as subprogram parameters. Reference rows ref_187 and ref_188. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/plsql_block.go | 3 +++ oracle/parser/subprogram_clause.go | 23 ++++++++++++++----- oracle/parser/subprogram_clause_test.go | 3 +++ .../testdata/coverage/reference_oracle.tsv | 2 ++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/oracle/parser/plsql_block.go b/oracle/parser/plsql_block.go index b98119e5..bc76556b 100644 --- a/oracle/parser/plsql_block.go +++ b/oracle/parser/plsql_block.go @@ -266,6 +266,9 @@ func (p *Parser) parsePLSQLCursorDecl() (*nodes.PLSQLCursorDecl, error) { nil { return nil, parseErr839 } + if err := p.checkCharsetSources(decl.Parameters, nil); err != nil { + return nil, err + } } if p.cur.Type == kwIS { diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 33085cb6..88b29617 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -477,10 +477,21 @@ func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) if params == nil { return nil } - byName := map[string]*nodes.Parameter{} + // params holds subprogram parameters (*Parameter) or cursor parameters + // (*PLSQLVarDecl); Oracle 23ai applies the rule to both. + formal := func(item nodes.Node) (string, *nodes.TypeName, bool) { + switch f := item.(type) { + case *nodes.Parameter: + return f.Name, f.TypeName, true + case *nodes.PLSQLVarDecl: + return f.Name, f.TypeName, true + } + return "", nil, false + } + byName := map[string]*nodes.TypeName{} for _, item := range params.Items { - if pr, ok := item.(*nodes.Parameter); ok { - byName[pr.Name] = pr + if name, tn, ok := formal(item); ok { + byName[name] = tn } } check := func(tn *nodes.TypeName) error { @@ -490,14 +501,14 @@ func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) if tn == nil || !tn.IsPercCharset || p.qualifiedCharsets[tn] { return nil } - if src, ok := byName[tn.CharacterSet]; ok && !p.mayBeCharacterType(src.TypeName) { + if src, ok := byName[tn.CharacterSet]; ok && !p.mayBeCharacterType(src) { return p.syntaxErrorAtType(tn) } return nil } for _, item := range params.Items { - if pr, ok := item.(*nodes.Parameter); ok { - if err := check(pr.TypeName); err != nil { + if _, tn, ok := formal(item); ok { + if err := check(tn); err != nil { return err } } diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index ed3db0ee..b7751d86 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -83,6 +83,7 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE PROCEDURE p IS BEGIN DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END; BEGIN NULL; END; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", "CREATE PACKAGE BODY k AS FUNCTION g RETURN NUMBER PARALLEL_ENABLE IS BEGIN RETURN 1; END; END;", + "DECLARE CURSOR c (a VARCHAR2, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", "CREATE PROCEDURE p IS FUNCTION f RETURN t PIPELINED DETERMINISTIC IS BEGIN RETURN; END; BEGIN NULL; END;", "DECLARE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl; BEGIN NULL; END;", // Nested SQL macros and result-cached functions compile, and a @@ -258,6 +259,8 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (a dual%ROWTYPE) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE FUNCTION f (\"K.C\" NUMBER) RETURN VARCHAR2 CHARACTER SET \"K.C\"%CHARSET IS BEGIN RETURN NULL; END;", "DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END;", + "DECLARE CURSOR c (a NUMBER, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", + "CREATE PROCEDURE p IS CURSOR c (a DATE, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", // CHARACTER SET on a pseudo-type (PLS-00550); PARALLEL_ENABLE on a // nested function (PLS-00712). "CREATE FUNCTION f (t TABLE CHARACTER SET ANY_CS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", diff --git a/oracle/parser/testdata/coverage/reference_oracle.tsv b/oracle/parser/testdata/coverage/reference_oracle.tsv index 4ad9bf7f..e7039c2c 100644 --- a/oracle/parser/testdata/coverage/reference_oracle.tsv +++ b/oracle/parser/testdata/coverage/reference_oracle.tsv @@ -185,3 +185,5 @@ ref_183 plsql_body accept CREATE OR REPLACE PROCEDURE omni_qaid_{run} AUTHID "CU ref_184 plsql_body accept CREATE OR REPLACE PACKAGE omni_quoted_{run} AS SUBTYPE "NUMBER" IS VARCHAR2(100); FUNCTION m RETURN "NUMBER" SQL_MACRO; SUBTYPE "CLOB" IS NUMBER; FUNCTION r RETURN "CLOB" RESULT_CACHE; END; quoted names of predefined types are user types ref_185 plsql reject DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END; RESULT_CACHE on a subprogram in an anonymous block (PLS-00999) ref_186 plsql reject DECLARE FUNCTION f RETURN NUMBER PARALLEL_ENABLE IS BEGIN RETURN 1; END; BEGIN NULL; END; PARALLEL_ENABLE on a nested function (PLS-00712) +ref_187 plsql reject DECLARE CURSOR c (a NUMBER, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END; cursor parameter %CHARSET from a numeric parameter (PLS-00550) +ref_188 plsql accept DECLARE CURSOR c (a VARCHAR2, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END; cursor parameter %CHARSET from a character parameter From dd0a5938d93559a4a0cb763f230c1bc21a3270af Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 12:33:30 +0000 Subject: [PATCH 19/22] test(oracle): pin DEFAULT (NULL) on a COLUMNS parameter as rejected (BYT-10301) Oracle 23ai rejects a parenthesized NULL default on a polymorphic table function's COLUMNS parameter with PLS-00769, as it does any default but a bare NULL. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/subprogram_clause_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index b7751d86..e56b694e 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -251,6 +251,8 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (t TABLE DEFAULT dual) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (t TABLE DEFAULT NULL) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (t TABLE, c COLUMNS DEFAULT 1) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", + // Only a bare NULL: Oracle 23ai rejects DEFAULT (NULL) too (PLS-00769). + "CREATE FUNCTION f (t TABLE, c COLUMNS DEFAULT (NULL)) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", "CREATE FUNCTION f (a BLOB) RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END;", "CREATE FUNCTION f (a NCLOB) RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END;", "CREATE FUNCTION f RETURN CLOB RESULT_CACHE IS BEGIN RETURN NULL; END;", From 25272c6a4eaa8ad7395042940fb1d5dc780942ce Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 12:48:00 +0000 Subject: [PATCH 20/22] fix(oracle): body %CHARSET sources, aggregate links, streaming USING in Split (BYT-10301) Review follow-up, each checked on Oracle 23ai: - A declaration in a subprogram body's own declaration section whose item%CHARSET source is a formal, or a preceding local variable, of a non-character type is PLS-00550; a VARCHAR2 local source compiles. Nested blocks are left to the engine. - An aggregate's implementation type cannot be reached over a database link (PLS-00331); a polymorphic function's package can. - Split no longer reads a streaming argument named USING (ORDER using BY ...) as the implementation-type marker. A scalar result with PARALLEL_ENABLE (PARTITION ...) compiles, so it stays accepted; DBMS_TF.TABLE_T and COLUMNS_T rules need the name resolved and are left to the engine. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 54 +++++++++++++++++++++++++ oracle/parser/split.go | 7 +++- oracle/parser/split_test.go | 2 + oracle/parser/subprogram_clause_test.go | 9 +++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 872721b8..e2e6f933 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -87,6 +87,9 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi if parseErr456 != nil { return nil, parseErr456 } + if err := p.checkBodyCharsetSources(stmt.Parameters, stmt.Body); err != nil { + return nil, err + } stmt.Loc.End = p.prev.End return stmt, nil @@ -239,6 +242,9 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit if parseErr461 != nil { return nil, parseErr461 } + if err := p.checkBodyCharsetSources(stmt.Parameters, stmt.Body); err != nil { + return nil, err + } stmt.Loc.End = p.prev.End return stmt, nil @@ -449,9 +455,16 @@ func (p *Parser) parseFunctionProperties(stmt *nodes.CreateFunctionStmt, level s if p.cur.Type != kwUSING { return p.syntaxErrorAtCur() } + implTok := p.cur if err := p.parseImplementationType(stmt); err != nil { return err } + // An aggregate's implementation type is local: Oracle 23ai + // rejects one over a database link with PLS-00331, while a + // polymorphic function's package may be remote. + if stmt.Implementation.DBLink != "" { + return p.syntaxErrorAtTok(implTok) + } // An aggregate takes an argument: Oracle 23ai rejects one // without parameters (PLS-00652) and compiles one with two, or // with an OUT parameter, against its implementation type. @@ -547,6 +560,41 @@ func (p *Parser) mayBeCharacterType(tn *nodes.TypeName) bool { } } +// checkBodyCharsetSources applies checkCharsetSources to the declarations +// of a subprogram body's own declaration section: a local item%CHARSET +// whose source is a formal, or a preceding local variable, of a +// non-character type is PLS-00550 on Oracle 23ai. A local cannot redeclare +// a formal there (PLS-00410). Nested blocks and subprograms are left to the +// engine. +func (p *Parser) checkBodyCharsetSources(params *nodes.List, body nodes.StmtNode) error { + block, ok := body.(*nodes.PLSQLBlock) + if !ok || block.Declarations == nil { + return nil + } + scope := &nodes.List{} + if params != nil { + scope.Items = append(scope.Items, params.Items...) + } + for _, item := range block.Declarations.Items { + var tn *nodes.TypeName + switch d := item.(type) { + case *nodes.PLSQLVarDecl: + tn = d.TypeName + case *nodes.PLSQLSubtypeDecl: + tn = d.BaseType + default: + continue + } + if err := p.checkCharsetSources(scope, tn); err != nil { + return err + } + if d, ok := item.(*nodes.PLSQLVarDecl); ok { + scope.Items = append(scope.Items, d) + } + } + return nil +} + // parseImplementationType parses USING [ schema. ] implementation_type // [ @dblink ], the current token at USING. Every part needs its name: Oracle // 23ai rejects USING impl. and USING impl@ (PLS-00103), and takes a dotted @@ -822,6 +870,9 @@ func (p *Parser) parsePackageProcDecl(level subprogramLevel) (*nodes.CreateProce if parseErr475 != nil { return nil, parseErr475 } + if err := p.checkBodyCharsetSources(stmt.Parameters, stmt.Body); err != nil { + return nil, err + } } else if p.cur.Type == ';' { p.advance() } else { @@ -898,6 +949,9 @@ func (p *Parser) parsePackageFuncDecl(level subprogramLevel) (*nodes.CreateFunct if parseErr480 != nil { return nil, parseErr480 } + if err := p.checkBodyCharsetSources(stmt.Parameters, stmt.Body); err != nil { + return nil, err + } } else if p.cur.Type == ';' { p.advance() } else { diff --git a/oracle/parser/split.go b/oracle/parser/split.go index 100a9f49..6d39ff8e 100644 --- a/oracle/parser/split.go +++ b/oracle/parser/split.go @@ -214,6 +214,9 @@ type splitPLSQLFrame struct { returnType bool afterDot bool implClause bool + // afterStream marks the token after ORDER or CLUSTER, the argument a + // streaming clause names, which may be a parameter named USING. + afterStream bool } type splitState struct { @@ -506,6 +509,8 @@ func (s *splitState) observeSubprogramHead(top *splitPLSQLFrame, tok Token) { top.afterDot = tok.Type == '.' inType := top.returnType top.returnType = false + afterStream := top.afterStream + top.afterStream = top.headDepth == 0 && (tok.Type == kwORDER || tok.Type == kwCLUSTER) switch { case tok.Type == '(': top.headDepth++ @@ -525,7 +530,7 @@ func (s *splitState) observeSubprogramHead(top *splitPLSQLFrame, tok Token) { // '.': RETURN pipelined.using names a type. case top.afterReturn && (tok.Type == kwPIPELINED || tok.Type == tokIDENT && tok.Str == "AGGREGATE"): top.implClause = true - case tok.Type == kwUSING && top.implClause && len(s.frames) == 1: + case tok.Type == kwUSING && top.implClause && !afterStream && len(s.frames) == 1: // AGGREGATE USING type or PIPELINED ... USING type: a type // implements the function, which has no IS|AS body and, like a // call spec, ends at its ';'. SQL*Plus buffers either up to a "/" diff --git a/oracle/parser/split_test.go b/oracle/parser/split_test.go index c7faab19..525c0d27 100644 --- a/oracle/parser/split_test.go +++ b/oracle/parser/split_test.go @@ -454,12 +454,14 @@ func TestSplitPLSQLBlocks(t *testing.T) { "CREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;\n" + "CREATE FUNCTION h RETURN pipelined.using IS BEGIN RETURN NULL; END;\n" + "CREATE FUNCTION k RETURN t PIPELINED DEFAULT COLLATION USING_NLS_COMP IS BEGIN RETURN; END;\n" + + "CREATE FUNCTION m (using pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION using BY HASH (a)) ORDER using BY (a) IS BEGIN NULL; END;\n" + "SELECT 1 FROM dual;", want: []string{ "CREATE FUNCTION using RETURN NUMBER IS BEGIN RETURN 1; END;", "\nCREATE FUNCTION g (using NUMBER) RETURN NUMBER IS BEGIN RETURN using; END;", "\nCREATE FUNCTION h RETURN pipelined.using IS BEGIN RETURN NULL; END;", "\nCREATE FUNCTION k RETURN t PIPELINED DEFAULT COLLATION USING_NLS_COMP IS BEGIN RETURN; END;", + "\nCREATE FUNCTION m (using pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION using BY HASH (a)) ORDER using BY (a) IS BEGIN NULL; END;", "\nSELECT 1 FROM dual", }, }, diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index e56b694e..c148f2ec 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -83,6 +83,8 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE PROCEDURE p IS BEGIN DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE RELIES_ON () IS BEGIN RETURN 1; END; BEGIN NULL; END; END;", "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", "CREATE PACKAGE BODY k AS FUNCTION g RETURN NUMBER PARALLEL_ENABLE IS BEGIN RETURN 1; END; END;", + "CREATE PROCEDURE p IS n VARCHAR2(10); v VARCHAR2(10) CHARACTER SET n%CHARSET; BEGIN NULL; END;", + "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN NUMBER PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN 1; END;", "DECLARE CURSOR c (a VARCHAR2, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", "CREATE PROCEDURE p IS FUNCTION f RETURN t PIPELINED DETERMINISTIC IS BEGIN RETURN; END; BEGIN NULL; END;", "DECLARE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl; BEGIN NULL; END;", @@ -263,6 +265,13 @@ func TestSubprogramHeadingClauses(t *testing.T) { "DECLARE FUNCTION f RETURN NUMBER RESULT_CACHE IS BEGIN RETURN 1; END; BEGIN NULL; END;", "DECLARE CURSOR c (a NUMBER, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", "CREATE PROCEDURE p IS CURSOR c (a DATE, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", + // A body declaration's %CHARSET source is a formal or preceding local + // of a non-character type (PLS-00550); an aggregate's type is local + // (PLS-00331). + "CREATE PROCEDURE p (a NUMBER) IS v VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END;", + "CREATE PROCEDURE p IS n NUMBER; v VARCHAR2(10) CHARACTER SET n%CHARSET; BEGIN NULL; END;", + "CREATE PACKAGE BODY k AS FUNCTION f (a DATE) RETURN NUMBER IS v VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN RETURN 1; END; END;", + "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl@lnk;", // CHARACTER SET on a pseudo-type (PLS-00550); PARALLEL_ENABLE on a // nested function (PLS-00712). "CREATE FUNCTION f (t TABLE CHARACTER SET ANY_CS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", From 56c28e40f6fb4d66590b726046c4be56ff665f62 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 13:09:56 +0000 Subject: [PATCH 21/22] fix(oracle): scope-aware %CHARSET source checks in one pass (BYT-10301) Check an item%CHARSET source against every declaration scope Oracle 23ai resolves it through: package items, enclosing blocks and subprograms, anonymous blocks, simple trigger bodies, and type-body methods, with an inner declaration shadowing an outer one. A source of a non-character type is PLS-00550. The walk visits each unit once and fills each scope's map as its declarations are read, replacing the per-declaration rescan that made long declaration sections quadratic. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 48 +------- oracle/parser/create_trigger.go | 3 + oracle/parser/create_type.go | 10 ++ oracle/parser/parser.go | 3 + oracle/parser/subprogram_clause.go | 146 ++++++++++++++++++++++++ oracle/parser/subprogram_clause_test.go | 21 ++++ 6 files changed, 188 insertions(+), 43 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index e2e6f933..3cf78b1f 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -87,7 +87,7 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi if parseErr456 != nil { return nil, parseErr456 } - if err := p.checkBodyCharsetSources(stmt.Parameters, stmt.Body); err != nil { + if err := p.checkCharsetScopes(stmt.Parameters, stmt.Body); err != nil { return nil, err } @@ -242,7 +242,7 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit if parseErr461 != nil { return nil, parseErr461 } - if err := p.checkBodyCharsetSources(stmt.Parameters, stmt.Body); err != nil { + if err := p.checkCharsetScopes(stmt.Parameters, stmt.Body); err != nil { return nil, err } @@ -560,41 +560,6 @@ func (p *Parser) mayBeCharacterType(tn *nodes.TypeName) bool { } } -// checkBodyCharsetSources applies checkCharsetSources to the declarations -// of a subprogram body's own declaration section: a local item%CHARSET -// whose source is a formal, or a preceding local variable, of a -// non-character type is PLS-00550 on Oracle 23ai. A local cannot redeclare -// a formal there (PLS-00410). Nested blocks and subprograms are left to the -// engine. -func (p *Parser) checkBodyCharsetSources(params *nodes.List, body nodes.StmtNode) error { - block, ok := body.(*nodes.PLSQLBlock) - if !ok || block.Declarations == nil { - return nil - } - scope := &nodes.List{} - if params != nil { - scope.Items = append(scope.Items, params.Items...) - } - for _, item := range block.Declarations.Items { - var tn *nodes.TypeName - switch d := item.(type) { - case *nodes.PLSQLVarDecl: - tn = d.TypeName - case *nodes.PLSQLSubtypeDecl: - tn = d.BaseType - default: - continue - } - if err := p.checkCharsetSources(scope, tn); err != nil { - return err - } - if d, ok := item.(*nodes.PLSQLVarDecl); ok { - scope.Items = append(scope.Items, d) - } - } - return nil -} - // parseImplementationType parses USING [ schema. ] implementation_type // [ @dblink ], the current token at USING. Every part needs its name: Oracle // 23ai rejects USING impl. and USING impl@ (PLS-00103), and takes a dotted @@ -745,6 +710,9 @@ func (p *Parser) parseCreatePackageStmt(start int, orReplace, ifNotExists, editi nil { return nil, parseErr469 } + if err := p.checkPackageCharsetScopes(stmt.Body); err != nil { + return nil, err + } // A package holding a SQL macro has definer's rights: Oracle 23ai // rejects AUTHID CURRENT_USER on one with PLS-00782. if stmt.AuthID == "CURRENT_USER" && stmt.Body != nil { @@ -870,9 +838,6 @@ func (p *Parser) parsePackageProcDecl(level subprogramLevel) (*nodes.CreateProce if parseErr475 != nil { return nil, parseErr475 } - if err := p.checkBodyCharsetSources(stmt.Parameters, stmt.Body); err != nil { - return nil, err - } } else if p.cur.Type == ';' { p.advance() } else { @@ -949,9 +914,6 @@ func (p *Parser) parsePackageFuncDecl(level subprogramLevel) (*nodes.CreateFunct if parseErr480 != nil { return nil, parseErr480 } - if err := p.checkBodyCharsetSources(stmt.Parameters, stmt.Body); err != nil { - return nil, err - } } else if p.cur.Type == ';' { p.advance() } else { diff --git a/oracle/parser/create_trigger.go b/oracle/parser/create_trigger.go index 232b4fe1..88890d60 100644 --- a/oracle/parser/create_trigger.go +++ b/oracle/parser/create_trigger.go @@ -201,6 +201,9 @@ func (p *Parser) parseCreateTriggerStmt(start int, orReplace, ifNotExists, editi if parseErr571 != nil { return nil, parseErr571 } + if err := p.checkCharsetScopes(nil, stmt.Body); err != nil { + return nil, err + } } stmt.Loc.End = p.prev.End diff --git a/oracle/parser/create_type.go b/oracle/parser/create_type.go index cc7accb3..f3e9f20a 100644 --- a/oracle/parser/create_type.go +++ b/oracle/parser/create_type.go @@ -380,6 +380,11 @@ func (p *Parser) parseTypeBodyProcedure(inSpec bool) (*nodes.CreateProcedureStmt if parseErr584 != nil { return nil, parseErr584 } + // A method's formals and locals shadow the type's attributes, so a + // %CHARSET source found among them is the one Oracle 23ai resolves. + if err := p.checkCharsetScopes(stmt.Parameters, stmt.Body); err != nil { + return nil, err + } stmt.Loc.End = p.prev.End return stmt, nil @@ -502,6 +507,11 @@ func (p *Parser) parseTypeBodyFunction(isConstructor, inSpec bool) (*nodes.Creat if parseErr589 != nil { return nil, parseErr589 } + // A method's formals and locals shadow the type's attributes, so a + // %CHARSET source found among them is the one Oracle 23ai resolves. + if err := p.checkCharsetScopes(stmt.Parameters, stmt.Body); err != nil { + return nil, err + } stmt.Loc.End = p.prev.End return stmt, nil diff --git a/oracle/parser/parser.go b/oracle/parser/parser.go index fc081dbb..69acd4e1 100644 --- a/oracle/parser/parser.go +++ b/oracle/parser/parser.go @@ -52,6 +52,9 @@ func (p *Parser) parseAnonymousBlock() (nodes.StmtNode, error) { if err != nil { return nil, err } + if err := p.checkCharsetScopes(nil, block); err != nil { + return nil, err + } return block, nil } diff --git a/oracle/parser/subprogram_clause.go b/oracle/parser/subprogram_clause.go index 88b29617..8a9808ae 100644 --- a/oracle/parser/subprogram_clause.go +++ b/oracle/parser/subprogram_clause.go @@ -516,6 +516,152 @@ func (p *Parser) checkCharsetSources(params *nodes.List, result *nodes.TypeName) return check(result) } +// charsetScopes checks the item%CHARSET source of each declaration in a +// unit against the declarations in scope, innermost first, as Oracle 23ai +// resolves it: a source of a non-character type is PLS-00550, from a +// package item, an outer block, or an enclosing subprogram alike, while an +// inner declaration of the same name shadows the outer one. A unit is +// walked once, each scope's map filled as its declarations are read. +type charsetScopes struct { + p *Parser + scopes []map[string]*nodes.TypeName +} + +// checkCharsetScopes walks a subprogram body (or an anonymous block, with +// no parameters): the formals and the body's declarations share a scope. +func (p *Parser) checkCharsetScopes(params *nodes.List, body nodes.StmtNode) error { + c := &charsetScopes{p: p} + return c.subprogram(params, body) +} + +// checkPackageCharsetScopes walks the items of a package specification or +// body, and the subprograms among them. +func (p *Parser) checkPackageCharsetScopes(items *nodes.List) error { + if items == nil { + return nil + } + c := &charsetScopes{p: p} + c.push() + return c.declarations(items) +} + +func (c *charsetScopes) push() { + c.scopes = append(c.scopes, map[string]*nodes.TypeName{}) +} + +func (c *charsetScopes) pop() { + c.scopes = c.scopes[:len(c.scopes)-1] +} + +// declare enters name in the innermost scope; tn is nil for an item that +// is not a typed variable (a cursor, a type, a subprogram), which shadows +// an outer name without being a %CHARSET source the text can judge. +func (c *charsetScopes) declare(name string, tn *nodes.TypeName) { + c.scopes[len(c.scopes)-1][name] = tn +} + +func (c *charsetScopes) check(tn *nodes.TypeName) error { + if tn == nil || !tn.IsPercCharset || c.p.qualifiedCharsets[tn] { + return nil + } + for i := len(c.scopes) - 1; i >= 0; i-- { + if src, ok := c.scopes[i][tn.CharacterSet]; ok { + if src != nil && !c.p.mayBeCharacterType(src) { + return c.p.syntaxErrorAtType(tn) + } + return nil + } + } + return nil +} + +func (c *charsetScopes) subprogram(params *nodes.List, body nodes.StmtNode) error { + block, ok := body.(*nodes.PLSQLBlock) + if !ok || block == nil { + return nil + } + c.push() + defer c.pop() + if params != nil { + for _, item := range params.Items { + if pr, ok := item.(*nodes.Parameter); ok { + c.declare(pr.Name, pr.TypeName) + } + } + } + return c.blockContents(block) +} + +func (c *charsetScopes) block(b *nodes.PLSQLBlock) error { + c.push() + defer c.pop() + return c.blockContents(b) +} + +// blockContents walks a block's declarations, then each block nested in its +// statements and handlers, without descending into those blocks twice. +func (c *charsetScopes) blockContents(b *nodes.PLSQLBlock) error { + if b.Declarations != nil { + if err := c.declarations(b.Declarations); err != nil { + return err + } + } + var err error + visit := func(n nodes.Node) bool { + if err != nil { + return false + } + if nested, ok := n.(*nodes.PLSQLBlock); ok { + err = c.block(nested) + return false + } + return true + } + if b.Statements != nil { + nodes.Inspect(b.Statements, visit) + } + if b.Exceptions != nil { + nodes.Inspect(b.Exceptions, visit) + } + return err +} + +func (c *charsetScopes) declarations(items *nodes.List) error { + for _, item := range items.Items { + switch d := item.(type) { + case *nodes.PLSQLVarDecl: + if err := c.check(d.TypeName); err != nil { + return err + } + c.declare(d.Name, d.TypeName) + case *nodes.PLSQLSubtypeDecl: + if err := c.check(d.BaseType); err != nil { + return err + } + c.declare(d.Name, nil) + case *nodes.PLSQLCursorDecl: + c.declare(d.Name, nil) + case *nodes.PLSQLTypeDecl: + c.declare(d.Name, nil) + case *nodes.CreateProcedureStmt: + if d.Name != nil { + c.declare(d.Name.Name, nil) + } + if err := c.subprogram(d.Parameters, d.Body); err != nil { + return err + } + case *nodes.CreateFunctionStmt: + if d.Name != nil { + c.declare(d.Name.Name, nil) + } + if err := c.subprogram(d.Parameters, d.Body); err != nil { + return err + } + } + } + return nil +} + // tablePseudoType returns the first TABLE or COLUMNS pseudo-type among a // subprogram's parameter types and its result type, or nil. Both stand only // in a polymorphic table function: Oracle 23ai rejects them elsewhere with diff --git a/oracle/parser/subprogram_clause_test.go b/oracle/parser/subprogram_clause_test.go index c148f2ec..58cccbf5 100644 --- a/oracle/parser/subprogram_clause_test.go +++ b/oracle/parser/subprogram_clause_test.go @@ -84,6 +84,14 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE FUNCTION f (c pkg.rc) RETURN t PIPELINED PARALLEL_ENABLE (PARTITION c BY HASH (a)) ORDER c BY (a) CLUSTER c BY (b) IS BEGIN RETURN; END;", "CREATE PACKAGE BODY k AS FUNCTION g RETURN NUMBER PARALLEL_ENABLE IS BEGIN RETURN 1; END; END;", "CREATE PROCEDURE p IS n VARCHAR2(10); v VARCHAR2(10) CHARACTER SET n%CHARSET; BEGIN NULL; END;", + // An inner declaration of the same name shadows the outer one. + "CREATE PROCEDURE p IS a NUMBER; BEGIN DECLARE a VARCHAR2(10); b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END; END;", + "CREATE PROCEDURE p (a NUMBER) IS PROCEDURE q (a VARCHAR2) IS b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END; BEGIN NULL; END;", + "CREATE TRIGGER t BEFORE INSERT ON tab FOR EACH ROW DECLARE a NVARCHAR2(10); b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END;", + // A method local shadows an attribute of the same name; an attribute + // itself is resolved by the engine. + "CREATE TYPE BODY ty AS MEMBER PROCEDURE m IS a VARCHAR2(10); b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END; END;", + "CREATE TYPE BODY ty AS MEMBER PROCEDURE m IS b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END; END;", "CREATE FUNCTION f (c SYS_REFCURSOR) RETURN NUMBER PARALLEL_ENABLE (PARTITION c BY ANY) IS BEGIN RETURN 1; END;", "DECLARE CURSOR c (a VARCHAR2, b VARCHAR2 CHARACTER SET a%CHARSET) IS SELECT 1 FROM dual; BEGIN NULL; END;", "CREATE PROCEDURE p IS FUNCTION f RETURN t PIPELINED DETERMINISTIC IS BEGIN RETURN; END; BEGIN NULL; END;", @@ -272,6 +280,19 @@ func TestSubprogramHeadingClauses(t *testing.T) { "CREATE PROCEDURE p IS n NUMBER; v VARCHAR2(10) CHARACTER SET n%CHARSET; BEGIN NULL; END;", "CREATE PACKAGE BODY k AS FUNCTION f (a DATE) RETURN NUMBER IS v VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN RETURN 1; END; END;", "CREATE FUNCTION f (x NUMBER) RETURN NUMBER AGGREGATE USING impl@lnk;", + // The %CHARSET source resolves through every scope: package items, + // outer blocks, enclosing subprograms, anonymous blocks (PLS-00550). + "CREATE PACKAGE k AS a NUMBER; b VARCHAR2(10) CHARACTER SET a%CHARSET; END;", + "CREATE PROCEDURE p IS a NUMBER; BEGIN DECLARE b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END; END;", + "CREATE PROCEDURE p IS a NUMBER; PROCEDURE q IS b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END; BEGIN NULL; END;", + "DECLARE a NUMBER; b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END;", + "CREATE PACKAGE BODY k AS c DATE; PROCEDURE q IS d VARCHAR2(10) CHARACTER SET c%CHARSET; BEGIN NULL; END; END;", + "CREATE PROCEDURE p IS a NUMBER; BEGIN FOR r IN 1..2 LOOP DECLARE b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END; END LOOP; END;", + "CREATE PROCEDURE p IS a NUMBER; BEGIN NULL; EXCEPTION WHEN OTHERS THEN DECLARE b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END; END;", + "CREATE PROCEDURE p IS \"A\" NUMBER; b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END;", + "CREATE TRIGGER t BEFORE INSERT ON tab FOR EACH ROW DECLARE a NUMBER; b VARCHAR2(10) CHARACTER SET a%CHARSET; BEGIN NULL; END;", + "CREATE TYPE BODY ty AS MEMBER PROCEDURE m (x NUMBER) IS b VARCHAR2(10) CHARACTER SET x%CHARSET; BEGIN NULL; END; END;", + "CREATE TYPE BODY ty AS MEMBER FUNCTION f (x NUMBER) RETURN NUMBER IS BEGIN DECLARE b VARCHAR2(10) CHARACTER SET x%CHARSET; BEGIN NULL; END; RETURN 1; END; END;", // CHARACTER SET on a pseudo-type (PLS-00550); PARALLEL_ENABLE on a // nested function (PLS-00712). "CREATE FUNCTION f (t TABLE CHARACTER SET ANY_CS) RETURN TABLE PIPELINED ROW POLYMORPHIC USING pkg;", From 7a4b76275d0f55d84e257f0632cf703ef0094e38 Mon Sep 17 00:00:00 2001 From: rebelice Date: Sat, 10 Oct 2026 13:33:14 +0000 Subject: [PATCH 22/22] fix(oracle): %CHARSET headings, cursor formals, qualified sources, type shadowing (BYT-10301) The %CHARSET scope walk now also checks subprogram headings, with or without a body, and cursor formals against the enclosing scopes, and resolves a source qualified by an enclosing subprogram, package, labeled block, or type-body method (p.a%CHARSET). Oracle 23ai rejects each with PLS-00550 when the source has a non-character type. A TYPE or SUBTYPE may redeclare any predefined type name but DOUBLE, NUMBER and SYS_REFCURSOR included, and the name then denotes the user's type. The predefined-type checks (RESULT_CACHE, PIPELINED, SQL_MACRO, PARALLEL_ENABLE, CHARACTER SET, %CHARSET sources) now skip a spelling shadowed by a declaration in scope, and every predefined spelling in a package body, whose specification may shadow it out of sight. A schema-level type does not shadow STANDARD's. Co-Authored-By: Claude Opus 5.5 --- oracle/parser/create_proc.go | 11 +- oracle/parser/create_trigger.go | 6 +- oracle/parser/create_type.go | 10 +- oracle/parser/parser.go | 24 +++- oracle/parser/plsql_block.go | 16 ++- oracle/parser/subprogram_clause.go | 178 +++++++++++++++++------- oracle/parser/subprogram_clause_test.go | 47 ++++++- oracle/parser/type.go | 57 +++++++- 8 files changed, 277 insertions(+), 72 deletions(-) diff --git a/oracle/parser/create_proc.go b/oracle/parser/create_proc.go index 3cf78b1f..ab2e5992 100644 --- a/oracle/parser/create_proc.go +++ b/oracle/parser/create_proc.go @@ -87,7 +87,7 @@ func (p *Parser) parseCreateProcedureStmt(start int, orReplace, ifNotExists, edi if parseErr456 != nil { return nil, parseErr456 } - if err := p.checkCharsetScopes(stmt.Parameters, stmt.Body); err != nil { + if err := p.checkCharsetScopes(subprogramName(stmt.Name), stmt.Parameters, nil, stmt.Body); err != nil { return nil, err } @@ -242,7 +242,7 @@ func (p *Parser) parseCreateFunctionStmt(start int, orReplace, ifNotExists, edit if parseErr461 != nil { return nil, parseErr461 } - if err := p.checkCharsetScopes(stmt.Parameters, stmt.Body); err != nil { + if err := p.checkCharsetScopes(subprogramName(stmt.Name), stmt.Parameters, stmt.ReturnType, stmt.Body); err != nil { return nil, err } @@ -703,14 +703,19 @@ func (p *Parser) parseCreatePackageStmt(start int, orReplace, ifNotExists, editi var parseErr469 error // Package declarations/body - collect everything until END + savedInBody := p.inPackageBody + p.inPackageBody = stmt.IsBody + p.typeScopes = append(p.typeScopes, map[string]bool{}) stmt.Body, parseErr469 = p.parsePackageBody() + p.typeScopes = p.typeScopes[:len(p.typeScopes)-1] + p.inPackageBody = savedInBody if parseErr469 != // END [name] ; nil { return nil, parseErr469 } - if err := p.checkPackageCharsetScopes(stmt.Body); err != nil { + if err := p.checkPackageCharsetScopes(subprogramName(stmt.Name), stmt.Body); err != nil { return nil, err } // A package holding a SQL macro has definer's rights: Oracle 23ai diff --git a/oracle/parser/create_trigger.go b/oracle/parser/create_trigger.go index 88890d60..352d21ba 100644 --- a/oracle/parser/create_trigger.go +++ b/oracle/parser/create_trigger.go @@ -196,14 +196,14 @@ func (p *Parser) parseCreateTriggerStmt(start int, orReplace, ifNotExists, editi Loc: callName.Loc, } } else if p.cur.Type == kwDECLARE || p.cur.Type == kwBEGIN || p.cur.Type == tokLABELOPEN { - var parseErr571 error - stmt.Body, parseErr571 = p.parsePLSQLBlock() + block, parseErr571 := p.parsePLSQLBlock() if parseErr571 != nil { return nil, parseErr571 } - if err := p.checkCharsetScopes(nil, stmt.Body); err != nil { + if err := p.checkBlockCharsetScopes(block); err != nil { return nil, err } + stmt.Body = block } stmt.Loc.End = p.prev.End diff --git a/oracle/parser/create_type.go b/oracle/parser/create_type.go index f3e9f20a..dd9e87af 100644 --- a/oracle/parser/create_type.go +++ b/oracle/parser/create_type.go @@ -381,8 +381,9 @@ func (p *Parser) parseTypeBodyProcedure(inSpec bool) (*nodes.CreateProcedureStmt return nil, parseErr584 } // A method's formals and locals shadow the type's attributes, so a - // %CHARSET source found among them is the one Oracle 23ai resolves. - if err := p.checkCharsetScopes(stmt.Parameters, stmt.Body); err != nil { + // %CHARSET source found among them, or qualified by the method name, is + // the one Oracle 23ai resolves. + if err := p.checkCharsetScopes(subprogramName(stmt.Name), stmt.Parameters, nil, stmt.Body); err != nil { return nil, err } @@ -508,8 +509,9 @@ func (p *Parser) parseTypeBodyFunction(isConstructor, inSpec bool) (*nodes.Creat return nil, parseErr589 } // A method's formals and locals shadow the type's attributes, so a - // %CHARSET source found among them is the one Oracle 23ai resolves. - if err := p.checkCharsetScopes(stmt.Parameters, stmt.Body); err != nil { + // %CHARSET source found among them, or qualified by the method name, is + // the one Oracle 23ai resolves. + if err := p.checkCharsetScopes(subprogramName(stmt.Name), stmt.Parameters, stmt.ReturnType, stmt.Body); err != nil { return nil, err } diff --git a/oracle/parser/parser.go b/oracle/parser/parser.go index 69acd4e1..afb4cd84 100644 --- a/oracle/parser/parser.go +++ b/oracle/parser/parser.go @@ -32,10 +32,24 @@ type Parser struct { // parsed may hold; parsePLSQLDatatype sets it for PL/SQL datatypes. typeMods typeModMode - // qualifiedCharsets records the datatypes whose item%CHARSET source is a - // qualified name (k.c), as opposed to one identifier that may itself be - // a quoted "K.C". - qualifiedCharsets map[*nodes.TypeName]bool + // qualifiedCharsets holds the components of each item%CHARSET source + // that is a qualified name (k.c), as opposed to one identifier that may + // itself be a quoted "K.C". + qualifiedCharsets map[*nodes.TypeName][]string + + // typeScopes holds, for each open PL/SQL declaration scope, the names + // its TYPE and SUBTYPE declarations have declared so far. Oracle 23ai + // lets one redeclare a predefined type name, NUMBER or SYS_REFCURSOR + // alike, and the redeclaration then shadows STANDARD's type. + typeScopes []map[string]bool + + // inPackageBody is set while a package body's items are parsed: its + // specification may redeclare any predefined type name out of sight. + inPackageBody bool + + // shadowedTypes records the datatypes spelled as a predefined type + // where a redeclaration of that name is, or may be, in scope. + shadowedTypes map[*nodes.TypeName]bool // inAnonymousBlock is set while a top-level anonymous block is parsed: // Oracle 23ai rejects RESULT_CACHE on any subprogram in one (PLS-00999). @@ -52,7 +66,7 @@ func (p *Parser) parseAnonymousBlock() (nodes.StmtNode, error) { if err != nil { return nil, err } - if err := p.checkCharsetScopes(nil, block); err != nil { + if err := p.checkBlockCharsetScopes(block); err != nil { return nil, err } return block, nil diff --git a/oracle/parser/plsql_block.go b/oracle/parser/plsql_block.go index bc76556b..c115feb3 100644 --- a/oracle/parser/plsql_block.go +++ b/oracle/parser/plsql_block.go @@ -8,6 +8,8 @@ import ( // // [<