001package gudusoft.gsqlparser.parser;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.TBaseType;
005import gudusoft.gsqlparser.TCustomLexer;
006import gudusoft.gsqlparser.TCustomParser;
007import gudusoft.gsqlparser.TCustomSqlStatement;
008import gudusoft.gsqlparser.TLexerOracle;
009import gudusoft.gsqlparser.TParserOracleSql;
010import gudusoft.gsqlparser.TParserOraclePLSql;
011import gudusoft.gsqlparser.TSourceToken;
012import gudusoft.gsqlparser.TSourceTokenList;
013import gudusoft.gsqlparser.TStatementList;
014import gudusoft.gsqlparser.TSyntaxError;
015import gudusoft.gsqlparser.EFindSqlStateType;
016import gudusoft.gsqlparser.ESqlPlusCmd;
017import gudusoft.gsqlparser.ETokenType;
018import gudusoft.gsqlparser.ETokenStatus;
019import gudusoft.gsqlparser.ESqlStatementType;
020import gudusoft.gsqlparser.EErrorType;
021import gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement;
022import gudusoft.gsqlparser.stmt.TUnknownSqlStatement;
023import gudusoft.gsqlparser.sqlcmds.ISqlCmds;
024import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory;
025import gudusoft.gsqlparser.compiler.TContext;
026import gudusoft.gsqlparser.sqlenv.TSQLEnv;
027import gudusoft.gsqlparser.compiler.TGlobalScope;
028import gudusoft.gsqlparser.compiler.TFrame;
029import gudusoft.gsqlparser.resolver.TSQLResolver;
030import gudusoft.gsqlparser.TLog;
031import gudusoft.gsqlparser.TGSqlParser;
032import gudusoft.gsqlparser.compiler.TASTEvaluator;
033import java.util.Stack;
034
035import java.io.BufferedReader;
036import java.util.ArrayList;
037import java.util.List;
038
039/**
040 * Oracle database SQL parser implementation.
041 *
042 * <p>This parser handles Oracle-specific SQL syntax including:
043 * <ul>
044 *   <li>PL/SQL blocks (procedures, functions, packages, triggers)</li>
045 *   <li>SQL*Plus commands (spool, set, show, etc.)</li>
046 *   <li>Oracle-specific DML/DDL (MERGE, flashback, etc.)</li>
047 *   <li>Oracle analytical functions and extensions</li>
048 *   <li>Special token handling (INNER, NOT DEFERRABLE, etc.)</li>
049 * </ul>
050 *
051 * <p><b>Implementation Status:</b> PHASE 3 - IN PROGRESS
052 * <ul>
053 *   <li><b>Completed:</b> Oracle classes (TLexerOracle, TParserOracleSql, TParserOraclePLSql) are now PUBLIC</li>
054 *   <li><b>Current:</b> Skeleton implementation delegates to legacy TGSqlParser</li>
055 *   <li><b>Next:</b> Extract vendor-specific logic from TGSqlParser into this class</li>
056 *   <li><b>Goal:</b> Fully self-contained Oracle parser using AbstractSqlParser template</li>
057 * </ul>
058 *
059 * <p><b>Design Notes:</b>
060 * <ul>
061 *   <li>Implements {@link SqlParser} directly (will extend {@link AbstractSqlParser} in Phase 4)</li>
062 *   <li>Can now directly instantiate: {@link TLexerOracle}, {@link TParserOracleSql}, {@link TParserOraclePLSql}</li>
063 *   <li>Uses two parsers: TParserOracleSql (SQL) + TParserOraclePLSql (PL/SQL blocks)</li>
064 *   <li>Handles SQL*Plus commands via special tokenization logic</li>
065 *   <li>Delimiter character: '/' for PL/SQL blocks, ';' for SQL statements</li>
066 * </ul>
067 *
068 * <p><b>Usage Example:</b>
069 * <pre>
070 * // Get Oracle parser from factory
071 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvoracle);
072 *
073 * // Build context
074 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvoracle)
075 *     .sqlText("SELECT * FROM emp WHERE deptno = 10")
076 *     .build();
077 *
078 * // Parse
079 * SqlParseResult result = parser.parse(context);
080 *
081 * // Access statements
082 * TStatementList statements = result.getSqlStatements();
083 * </pre>
084 *
085 * <p><b>Phase 3 Extraction Roadmap:</b>
086 * <ol>
087 *   <li>✅ DONE: Make TLexerOracle, TParserOracleSql, TParserOraclePLSql public</li>
088 *   <li>⏳ TODO: Extract tokenization logic (~367 lines from TGSqlParser.dooraclesqltexttotokenlist())</li>
089 *   <li>⏳ TODO: Extract raw statement logic (~200 lines from TGSqlParser.dooraclegetrawsqlstatements())</li>
090 *   <li>⏳ TODO: Extract parsing orchestration (SQL vs PL/SQL parser selection)</li>
091 *   <li>⏳ TODO: Extract helper methods (getanewsourcetoken, getprevsolidtoken, etc.)</li>
092 *   <li>⏳ TODO: Extend AbstractSqlParser and use template method pattern fully</li>
093 *   <li>⏳ TODO: Remove all delegation to TGSqlParser</li>
094 * </ol>
095 *
096 * <p><b>Key Methods to Extract from TGSqlParser:</b>
097 * <ul>
098 *   <li>{@code dooraclesqltexttotokenlist()} - Oracle tokenization with SQL*Plus command detection</li>
099 *   <li>{@code dooraclegetrawsqlstatements()} - Oracle raw statement boundaries (handles PL/SQL blocks)</li>
100 *   <li>{@code getanewsourcetoken()} - Token iterator from lexer</li>
101 *   <li>{@code getprevsolidtoken()} - Navigate token list backwards</li>
102 *   <li>{@code IsValidPlaceForDivToSqlplusCmd()} - Slash vs divide operator disambiguation</li>
103 *   <li>{@code countLines()} - Multi-line token handling</li>
104 *   <li>{@code spaceAtTheEndOfReturnToken()} - SQL*Plus command validation</li>
105 * </ul>
106 *
107 * @see SqlParser
108 * @see AbstractSqlParser
109 * @see TLexerOracle
110 * @see TParserOracleSql
111 * @see TParserOraclePLSql
112 * @since 3.2.0.0
113 */
114public class OracleSqlParser extends AbstractSqlParser {
115
116    /**
117     * Construct Oracle SQL parser.
118     * <p>
119     * Configures the parser for Oracle database with default delimiters:
120     * <ul>
121     *   <li>SQL statements: semicolon (;)</li>
122     *   <li>PL/SQL blocks: forward slash (/)</li>
123     * </ul>
124     * <p>
125     * Following the original TGSqlParser pattern, the lexer and parsers are
126     * created once in the constructor and reused for all parsing operations.
127     * This avoids unnecessary object allocation overhead since the parser
128     * is not thread-safe and designed for single-use per instance.
129     */
130    public OracleSqlParser() {
131        super(EDbVendor.dbvoracle);
132        this.delimiterChar = '/';  // PL/SQL delimiter
133        this.defaultDelimiterStr = ";";  // SQL delimiter
134
135        // Create lexer once - will be reused for all parsing operations
136        // (matches original TGSqlParser constructor pattern at line 1033)
137        this.flexer = new TLexerOracle();
138        this.flexer.delimiterchar = this.delimiterChar;
139        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
140
141        // Set parent's lexer reference for shared tokenization logic
142        this.lexer = this.flexer;
143
144        // Create parsers once - will be reused for all parsing operations
145        // Token list will be set/updated when parsing begins
146        // (matches original TGSqlParser constructor pattern at lines 1036-1040)
147        this.fparser = new TParserOracleSql(null);
148        this.fplsqlparser = new TParserOraclePLSql(null);
149        this.fparser.lexer = this.flexer;
150        this.fplsqlparser.lexer = this.flexer;
151
152        // NOTE: sourcetokenlist and sqlstatements are initialized in AbstractSqlParser constructor
153    }
154
155    // ========== Tokenization State (used during tokenization) ==========
156    // These instance variables are used during the tokenization process
157    // and are set up at the beginning of tokenization
158
159    /** The Oracle lexer used for tokenization */
160    public TLexerOracle flexer;  // Package-accessible for TGSqlParser integration
161
162    // NOTE: sourcetokenlist moved to AbstractSqlParser (inherited)
163
164    /** Optional callback for token processing (can be null) */
165    private Object tokenHandle;  // TTokenCallback interface - keeping as Object for now
166
167    // State variables for tokenization (set during dooraclesqltexttotokenlist())
168    private boolean continuesqlplusatnewline;
169    private boolean waitingreturnforsemicolon;
170    private boolean waitingreturnforfloatdiv;
171    private boolean isvalidplace;
172    private boolean insqlpluscmd;
173    // Brace nesting inside CREATE JAVA SOURCE ... AS <java code>: the Java
174    // body contains its own semicolons, which must not end the SQL statement.
175    private int javaBraceNesting = 0;
176
177    // ========== Statement Parsing State (used during statement parsing) ==========
178    // These instance variables are used during the statement parsing process
179
180    // NOTE: The following fields moved to AbstractSqlParser (inherited):
181    //   - sqlcmds (ISqlCmds)
182    //   - sqlstatements (TStatementList)
183    //   - parserContext (ParserContext)
184
185    /** Current statement being built */
186    private TCustomSqlStatement gcurrentsqlstatement;
187
188    /** SQL parser (for regular SQL statements) */
189    private TParserOracleSql fparser;
190
191    /** PL/SQL parser (for PL/SQL blocks) */
192    private TParserOraclePLSql fplsqlparser;
193
194    // Note: Global context and frame stack fields inherited from AbstractSqlParser:
195    // - protected TContext globalContext
196    // - protected TSQLEnv sqlEnv
197    // - protected Stack<TFrame> frameStack
198    // - protected TFrame globalFrame
199
200    // ========== Enums for State Machine ==========
201    // These enums are used by the dooraclegetrawsqlstatements state machine
202
203    enum stored_procedure_status {start,is_as,body,bodyend,end, cursor_declare};
204    enum stored_procedure_type {function,procedure,package_spec,package_body, block_with_begin,block_with_declare,
205        create_trigger,create_library,cursor_in_package_spec,others};
206
207    static final int stored_procedure_nested_level = 1024;
208
209    // ========== AbstractSqlParser Abstract Methods Implementation ==========
210
211    /**
212     * Return the Oracle lexer instance.
213     * <p>
214     * The lexer is created once in the constructor and reused for all
215     * parsing operations. This method simply returns the existing instance,
216     * matching the original TGSqlParser pattern where the lexer is created
217     * once and reset before each use.
218     *
219     * @param context parser context (not used, lexer already created)
220     * @return the Oracle lexer instance created in constructor
221     */
222    @Override
223    protected TCustomLexer getLexer(ParserContext context) {
224        // Return existing lexer instance (created in constructor)
225        // No need to create new instance - matches original TGSqlParser pattern
226        return this.flexer;
227    }
228
229    /**
230     * Return the Oracle SQL parser instance with updated token list.
231     * <p>
232     * The parser is created once in the constructor and reused for all
233     * parsing operations. This method updates the token list and returns
234     * the existing instance, matching the original TGSqlParser pattern.
235     *
236     * @param context parser context (not used, parser already created)
237     * @param tokens source token list to parse
238     * @return the Oracle SQL parser instance created in constructor
239     */
240    @Override
241    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
242        // Update token list for reused parser instance
243        this.fparser.sourcetokenlist = tokens;
244        return this.fparser;
245    }
246
247    /**
248     * Return the Oracle PL/SQL parser instance with updated token list.
249     * <p>
250     * Oracle needs a secondary parser (TParserOraclePLSql) for PL/SQL blocks
251     * (procedures, functions, packages, triggers, anonymous blocks).
252     * <p>
253     * The parser is created once in the constructor and reused for all
254     * parsing operations. This method updates the token list and returns
255     * the existing instance, matching the original TGSqlParser pattern.
256     *
257     * @param context parser context (not used, parser already created)
258     * @param tokens source token list to parse
259     * @return the Oracle PL/SQL parser instance created in constructor
260     */
261    @Override
262    protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) {
263        // Update token list for reused parser instance
264        this.fplsqlparser.sourcetokenlist = tokens;
265        return this.fplsqlparser;
266    }
267
268    /**
269     * Call Oracle-specific tokenization logic.
270     * <p>
271     * Delegates to dooraclesqltexttotokenlist which handles Oracle's
272     * specific keyword recognition, SQL*Plus commands, forward slash
273     * disambiguation, and token generation.
274     */
275    @Override
276    protected void tokenizeVendorSql() {
277        dooraclesqltexttotokenlist();
278        restoreSqlSetStatements();
279    }
280
281    /**
282     * SET is both a SQL*Plus command (SET PAGESIZE ..., line-terminated) and
283     * the head of the SQL statements SET CONSTRAINT[S] / SET ROLE /
284     * SET TRANSACTION (semicolon-terminated). Tokenization cannot look ahead
285     * (the token list is built incrementally), so every line-leading SET is
286     * first marked sqlpluscmd; a multi-line SET CONSTRAINTS was then truncated
287     * at the newline. This pass runs on the complete token list and restores
288     * the SQL reading when the word after SET is one of the SQL forms: the
289     * SET token gets its saved keyword code back and the rest of its line
290     * loses the insqlpluscmd flag, so the statement splitter routes it through
291     * the command table (sstoraclesetconstraint etc.) and runs to ';'.
292     */
293    private void restoreSqlSetStatements() {
294        for (int i = 0; i < sourcetokenlist.size(); i++) {
295            TSourceToken st = sourcetokenlist.get(i);
296            if (st.tokencode != TBaseType.sqlpluscmd) continue;
297            if (!st.toString().equalsIgnoreCase("SET")) continue;
298            if (st.prevTokenCode != TBaseType.rrw_set) continue;
299            TSourceToken nx = st.nextSolidToken();
300            if (nx == null) continue;
301            String w = nx.toString();
302            if (!(w.equalsIgnoreCase("CONSTRAINT") || w.equalsIgnoreCase("CONSTRAINTS")
303                    || w.equalsIgnoreCase("ROLE") || w.equalsIgnoreCase("TRANSACTION"))) continue;
304            st.tokencode = st.prevTokenCode;
305            st.tokentype = ETokenType.ttkeyword;
306            st.insqlpluscmd = false;
307            for (int j = i + 1; j < sourcetokenlist.size(); j++) {
308                TSourceToken t = sourcetokenlist.get(j);
309                if (t.tokencode == TBaseType.lexnewline) break;
310                t.insqlpluscmd = false;
311            }
312        }
313    }
314
315    /**
316     * Post-tokenization: merge ${...} template variable tokens into single IDENT tokens.
317     * Template syntax like ${if(len(X) == 0, "", "...")} is used by BI tools.
318     */
319    @Override
320    protected void doAfterTokenize(TSourceTokenList tokens) {
321        super.doAfterTokenize(tokens);
322        mergeTemplateVariableTokens(tokens);
323    }
324
325    private void mergeTemplateVariableTokens(TSourceTokenList tokens) {
326        for (int i = 0; i < tokens.size() - 1; i++) {
327            TSourceToken dollar = tokens.get(i);
328
329            // Match either bare '$' (self-char) or '$IDENT' like $P, $X (identifier starting with $)
330            boolean isDollarChar = (dollar.tokencode == '$');
331            boolean isDollarIdent = (dollar.tokencode == TBaseType.ident
332                    && dollar.astext != null && dollar.astext.startsWith("$"));
333            if (!isDollarChar && !isDollarIdent) continue;
334
335            // Find next non-whitespace token — for $IDENT pattern, require immediate '{' (no whitespace)
336            int braceIdx = i + 1;
337            if (isDollarChar) {
338                while (braceIdx < tokens.size() && tokens.get(braceIdx).tokentype == ETokenType.ttwhitespace) {
339                    braceIdx++;
340                }
341            }
342            if (braceIdx >= tokens.size() || tokens.get(braceIdx).tokencode != '{') continue;
343
344            // Found ${ pattern — find matching } with depth tracking
345            int depth = 1;
346            int endIdx = braceIdx + 1;
347            boolean isComplex = false;
348            while (endIdx < tokens.size() && depth > 0) {
349                int code = tokens.get(endIdx).tokencode;
350                if (code == '{') depth++;
351                else if (code == '}') depth--;
352                else if (code == '(' || code == ',' || code == '\'' || code == '"') isComplex = true;
353                if (depth > 0) endIdx++;
354            }
355            if (depth != 0) continue; // unclosed
356
357            // Build merged token text
358            StringBuilder sb = new StringBuilder();
359            for (int j = i; j <= endIdx; j++) {
360                sb.append(tokens.get(j).astext);
361            }
362
363            if (isComplex && isDollarChar) {
364                // Complex template starting with bare $ like ${if(len(X)==0,...)}
365                // These expand to SQL fragments (e.g., AND clauses) at runtime,
366                // so convert to whitespace to let parser skip them entirely.
367                for (int j = i; j <= endIdx; j++) {
368                    tokens.get(j).tokentype = ETokenType.ttwhitespace;
369                    tokens.get(j).tokencode = TBaseType.lexspace;
370                }
371            } else {
372                // Simple template like ${NAME}, or JasperReports $P{VAR}/$X{IN,COL,PARAM}
373                // These expand to single values/expressions, so merge into IDENT placeholder.
374                dollar.astext = sb.toString();
375                dollar.tokencode = TBaseType.ident;
376                dollar.tokentype = ETokenType.ttidentifier;
377                // Convert remaining tokens to whitespace so parser skips them even if
378                // tokenstatus is overwritten by statement splitter (tsignoredbygetrawstatement)
379                for (int j = i + 1; j <= endIdx; j++) {
380                    tokens.get(j).tokentype = ETokenType.ttwhitespace;
381                    tokens.get(j).tokencode = TBaseType.lexspace;
382                }
383            }
384
385            i = endIdx; // skip past merged tokens
386        }
387    }
388
389    /**
390     * Setup Oracle parsers for raw statement extraction.
391     * <p>
392     * Oracle uses dual parsers (SQL + PL/SQL), so we inject sqlcmds and
393     * update token lists for both parsers.
394     */
395    @Override
396    protected void setupVendorParsersForExtraction() {
397        // Inject sqlcmds into BOTH parsers (SQL + PL/SQL)
398        this.fparser.sqlcmds = this.sqlcmds;
399        this.fplsqlparser.sqlcmds = this.sqlcmds;
400
401        // Update token list for BOTH parsers
402        this.fparser.sourcetokenlist = this.sourcetokenlist;
403        this.fplsqlparser.sourcetokenlist = this.sourcetokenlist;
404    }
405
406    /**
407     * Call Oracle-specific raw statement extraction logic.
408     * <p>
409     * Delegates to dooraclegetrawsqlstatements which handles Oracle's
410     * statement delimiters (semicolon and forward slash).
411     */
412    @Override
413    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
414        dooraclegetrawsqlstatements(builder);
415    }
416
417    /**
418     * Perform full parsing of statements with syntax checking.
419     * <p>
420     * This method orchestrates the parsing of all statements by:
421     * <ul>
422     *   <li>Using the raw statements passed from AbstractSqlParser.parse()</li>
423     *   <li>Initializing SQL and PL/SQL parsers</li>
424     *   <li>Creating global context and frame stack</li>
425     *   <li>Looping through each raw statement</li>
426     *   <li>Calling parsestatement() on each to build AST</li>
427     *   <li>Handling error recovery for CREATE TABLE/INDEX</li>
428     *   <li>Collecting syntax errors</li>
429     * </ul>
430     *
431     * <p><b>Important:</b> This method does NOT extract raw statements - they are
432     * passed in as a parameter already extracted by {@link #extractRawStatements}.
433     * This eliminates duplicate extraction that was occurring in the old design.
434     *
435     * <p>Extracted from: TGSqlParser.doparse() lines 16903-17026
436     *
437     * @param context parser context
438     * @param parser main SQL parser (TParserOracleSql)
439     * @param secondaryParser PL/SQL parser (TParserOraclePLSql)
440     * @param tokens source token list
441     * @param rawStatements raw statements already extracted (never null)
442     * @return list of fully parsed statements with AST built
443     */
444    @Override
445    protected TStatementList performParsing(ParserContext context,
446                                           TCustomParser parser,
447                                           TCustomParser secondaryParser,
448                                           TSourceTokenList tokens,
449                                           TStatementList rawStatements) {
450        // Store references
451        this.fparser = (TParserOracleSql) parser;
452        this.fplsqlparser = (TParserOraclePLSql) secondaryParser;
453        this.sourcetokenlist = tokens;
454        this.parserContext = context;
455
456        // Use the raw statements passed from AbstractSqlParser.parse()
457        // (already extracted - DO NOT re-extract to avoid duplication)
458        this.sqlstatements = rawStatements;
459
460        // Initialize statement parsing infrastructure
461        this.sqlcmds = SqlCmdsFactory.get(vendor);
462
463        // Inject sqlcmds into parsers (required for make_stmt and other methods)
464        this.fparser.sqlcmds = this.sqlcmds;
465        this.fplsqlparser.sqlcmds = this.sqlcmds;
466
467        // Initialize global context for semantic analysis
468        // CRITICAL: When delegated from TGSqlParser, use TGSqlParser's frameStack
469        // so that variables set in statements can be found by other statements
470        if (context != null && context.getGsqlparser() != null) {
471            TGSqlParser gsqlparser = (TGSqlParser) context.getGsqlparser();
472            this.frameStack = gsqlparser.getFrameStack();
473
474            // CRITICAL: Set gsqlparser on the NodeFactory - matches TGSqlParser behavior
475            // This is needed for proper AST node creation during parsing
476            // Without this, expression traversal order may differ, causing
477            // dataflow constant ordering issues
478            this.fparser.getNf().setGsqlParser(gsqlparser);
479            this.fplsqlparser.getNf().setGsqlParser(gsqlparser);
480
481            // Create global context if needed
482            this.globalContext = new TContext();
483            this.sqlEnv = new TSQLEnv(this.vendor) {
484                @Override
485                public void initSQLEnv() {
486                }
487            };
488            this.globalContext.setSqlEnv(this.sqlEnv, this.sqlstatements);
489        } else {
490            initializeGlobalContext();
491        }
492
493        // Parse each statement with exception handling for robustness
494        for (int i = 0; i < sqlstatements.size(); i++) {
495            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
496
497            try {
498                stmt.setFrameStack(frameStack);
499
500                // Parse the statement
501                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
502
503                // Oracle-specific post-processing (overridden hook method)
504                afterStatementParsed(stmt);
505
506                // Handle error recovery for CREATE TABLE/INDEX
507                boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE;
508                if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) {
509                    handleCreateTableErrorRecovery(stmt);
510                }
511
512                // Collect syntax errors
513                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
514                    copyErrorsFromStatement(stmt);
515                }
516
517            } catch (Exception ex) {
518                // Use inherited exception handler from AbstractSqlParser
519                // This provides consistent error handling across all database parsers
520                handleStatementParsingException(stmt, i, ex);
521                continue;
522            }
523        }
524
525        // Clean up frame stack
526        if (globalFrame != null) {
527            globalFrame.popMeFromStack(frameStack);
528        }
529
530        return this.sqlstatements;
531    }
532
533    // Note: initializeGlobalContext() inherited from AbstractSqlParser
534
535    /**
536     * Override to provide Oracle-specific post-processing after statement parsing.
537     * <p>
538     * For Oracle, we check if the statement is PL/SQL and recursively find syntax
539     * errors in nested PL/SQL statements.
540     */
541    @Override
542    protected void afterStatementParsed(TCustomSqlStatement stmt) {
543        if (stmt.isoracleplsql()) {
544            findAllSyntaxErrorsInPlsql(stmt);
545        }
546    }
547
548    /**
549     * Perform Oracle-specific semantic analysis using TSQLResolver.
550     *
551     * <p>This includes:
552     * <ul>
553     *   <li>Column-to-table resolution</li>
554     *   <li>Dataflow analysis</li>
555     *   <li>Reference resolution</li>
556     *   <li>Scope resolution</li>
557     * </ul>
558     *
559     * @param context the parser context
560     * @param statements the parsed statements
561     */
562    @Override
563    protected void performSemanticAnalysis(ParserContext context, TStatementList statements) {
564        if (TBaseType.isEnableResolver() && getSyntaxErrors().isEmpty()) {
565            TSQLResolver resolver = new TSQLResolver(globalContext, statements);
566            resolver.resolve();
567        }
568    }
569
570    /**
571     * Perform Oracle-specific AST interpretation/evaluation using TASTEvaluator.
572     *
573     * <p>This executes simple SQL statements and evaluates expressions
574     * for static analysis and constant folding.
575     *
576     * @param context the parser context
577     * @param statements the parsed statements
578     */
579    @Override
580    protected void performInterpreter(ParserContext context, TStatementList statements) {
581        if (TBaseType.ENABLE_INTERPRETER && getSyntaxErrors().isEmpty()) {
582            TLog.clearLogs();
583            TGlobalScope interpreterScope = new TGlobalScope(sqlEnv);
584            TLog.enableInterpreterLogOnly();
585            TASTEvaluator astEvaluator = new TASTEvaluator(statements, interpreterScope);
586            astEvaluator.eval();
587        }
588    }
589
590    // ========== Raw Statement Extraction ==========
591    // These methods extract raw SQL statements from tokens without full parsing
592    // Extracted from TGSqlParser.dooraclegetrawsqlstatements() and related methods
593
594    /**
595     * Extract raw Oracle SQL statements from tokenized source.
596     * <p>
597     * This is the main Oracle statement extraction state machine that:
598     * <ul>
599     *   <li>Groups tokens into statement boundaries</li>
600     *   <li>Identifies statement types (SQL vs PL/SQL, SQL*Plus commands)</li>
601     *   <li>Handles nested PL/SQL blocks (procedures, functions, packages, triggers)</li>
602     *   <li>Tracks BEGIN/END pairs and other block delimiters</li>
603     *   <li>Detects statement terminators (semicolon, forward slash, period)</li>
604     * </ul>
605     *
606     * <p><b>State Machine:</b> Uses 4 main states:
607     * <ul>
608     *   <li>{@code stnormal} - Between statements, looking for start of next statement</li>
609     *   <li>{@code stsql} - Inside a SQL statement</li>
610     *   <li>{@code stsqlplus} - Inside a SQL*Plus command</li>
611     *   <li>{@code ststoredprocedure} - Inside a PL/SQL block (procedure/function/package/trigger)</li>
612     *   <li>{@code sterror} - Error recovery mode</li>
613     * </ul>
614     *
615     * <p><b>Extracted from:</b> TGSqlParser.dooraclegetrawsqlstatements() (lines 10071-10859)
616     *
617     * <p><b>Design Note:</b> This method now receives a builder to populate with results,
618     * following Option A design where the vendor-specific method focuses on parsing logic
619     * while extractRawStatements() handles result construction.
620     *
621     * @param builder the result builder to populate with statements and error information
622     */
623    private void dooraclegetrawsqlstatements(SqlParseResult.Builder builder) {
624        int waitingEnds[] = new int[stored_procedure_nested_level];
625        stored_procedure_type sptype[] = new stored_procedure_type[stored_procedure_nested_level];
626        stored_procedure_status procedure_status[] = new stored_procedure_status[stored_procedure_nested_level];
627        boolean endBySlashOnly = true;
628        int nestedProcedures = 0, nestedParenthesis = 0;
629        // Flag for CREATE MLE MODULE with AS clause - terminates with / not ;
630        boolean mleModuleWithAs = false;
631        // Flag for WITH FUNCTION/PROCEDURE - track BEGIN/END nesting to handle embedded semicolons
632        boolean withPlsqlDefinition = false;
633        int withPlsqlBeginEndNesting = 0;
634        boolean withPlsqlFoundSelect = false;  // True when SELECT has been found after WITH FUNCTION
635        // Track whether the current CTE statement's main SELECT has been found
636        // (i.e., the SELECT after WITH name AS (...) at paren level 0)
637        boolean cteMainSelectFound = false;
638
639        if (TBaseType.assigned(sqlstatements)) sqlstatements.clear();
640        if (!TBaseType.assigned(sourcetokenlist)) {
641            // No tokens available - populate builder with error and return
642            builder.errorCode(1);
643            builder.errorMessage("No source token list available");
644            builder.sqlStatements(new TStatementList());
645            return;
646        }
647
648        gcurrentsqlstatement = null;
649        EFindSqlStateType gst = EFindSqlStateType.stnormal;
650        TSourceToken lcprevsolidtoken = null, ast = null;
651
652        // Main tokenization loop
653        for (int i = 0; i < sourcetokenlist.size(); i++) {
654
655            if ((ast != null) && (ast.issolidtoken()))
656                lcprevsolidtoken = ast;
657
658            ast = sourcetokenlist.get(i);
659            sourcetokenlist.curpos = i;
660
661            // Token-specific keyword transformations for Oracle
662            performRawStatementTokenTransformations(ast);
663
664            // State machine processing
665            switch (gst) {
666                case sterror: {
667                    if (ast.tokentype == ETokenType.ttsemicolon) {
668                        appendToken(gcurrentsqlstatement, ast);
669                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
670                        gst = EFindSqlStateType.stnormal;
671                    } else {
672                        appendToken(gcurrentsqlstatement, ast);
673                    }
674                    break;
675                } //sterror
676
677                case stnormal: {
678                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
679                            || (ast.tokencode == TBaseType.cmtslashstar)
680                            || (ast.tokencode == TBaseType.lexspace)
681                            || (ast.tokencode == TBaseType.lexnewline)
682                            || (ast.tokentype == ETokenType.ttsemicolon)) {
683                        if (gcurrentsqlstatement != null) {
684                            appendToken(gcurrentsqlstatement, ast);
685                        }
686
687                        if ((lcprevsolidtoken != null) && (ast.tokentype == ETokenType.ttsemicolon)) {
688                            if (lcprevsolidtoken.tokentype == ETokenType.ttsemicolon) {
689                                // ;;;; continuous semicolon, treat it as comment
690                                ast.tokentype = ETokenType.ttsimplecomment;
691                                ast.tokencode = TBaseType.cmtdoublehyphen;
692                            }
693                        }
694
695                        continue;
696                    }
697
698                    if (ast.tokencode == TBaseType.sqlpluscmd) {
699                        gst = EFindSqlStateType.stsqlplus;
700                        gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
701                        appendToken(gcurrentsqlstatement, ast);
702                        continue;
703                    }
704
705                    // find a token to start sql or plsql mode
706                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
707
708                    if (gcurrentsqlstatement != null) {
709                        if (gcurrentsqlstatement.isoracleplsql()) {
710                            nestedProcedures = 0;
711                            gst = EFindSqlStateType.ststoredprocedure;
712                            appendToken(gcurrentsqlstatement, ast);
713
714                            switch (gcurrentsqlstatement.sqlstatementtype) {
715                                case sstplsql_createprocedure:
716                                    sptype[nestedProcedures] = stored_procedure_type.procedure;
717                                    break;
718                                case sstplsql_createfunction:
719                                    sptype[nestedProcedures] = stored_procedure_type.function;
720                                    break;
721                                case sstplsql_createpackage:
722                                    sptype[nestedProcedures] = stored_procedure_type.package_spec;
723                                    if (ast.searchToken(TBaseType.rrw_body, 5) != null) {
724                                        sptype[nestedProcedures] = stored_procedure_type.package_body;
725                                    }
726                                    break;
727                                case sst_plsql_block:
728                                    sptype[nestedProcedures] = stored_procedure_type.block_with_declare;
729                                    if (ast.tokencode == TBaseType.rrw_begin) {
730                                        sptype[nestedProcedures] = stored_procedure_type.block_with_begin;
731                                    }
732                                    break;
733                                case sstplsql_createtrigger:
734                                    sptype[nestedProcedures] = stored_procedure_type.create_trigger;
735                                    break;
736                                case sstoraclecreatelibrary:
737                                    sptype[nestedProcedures] = stored_procedure_type.create_library;
738                                    break;
739                                case sstplsql_createtype_placeholder:
740                                    gst = EFindSqlStateType.stsql;
741                                    break;
742                                default:
743                                    sptype[nestedProcedures] = stored_procedure_type.others;
744                                    break;
745                            }
746
747                            if (sptype[0] == stored_procedure_type.block_with_declare) {
748                                endBySlashOnly = false;
749                                procedure_status[0] = stored_procedure_status.is_as;
750                            } else if (sptype[0] == stored_procedure_type.block_with_begin) {
751                                endBySlashOnly = false;
752                                procedure_status[0] = stored_procedure_status.body;
753                            } else if (sptype[0] == stored_procedure_type.procedure) {
754                                endBySlashOnly = false;
755                                procedure_status[0] = stored_procedure_status.start;
756                            } else if (sptype[0] == stored_procedure_type.function) {
757                                endBySlashOnly = false;
758                                procedure_status[0] = stored_procedure_status.start;
759                            } else if (sptype[0] == stored_procedure_type.package_spec) {
760                                endBySlashOnly = false;
761                                procedure_status[0] = stored_procedure_status.start;
762                            } else if (sptype[0] == stored_procedure_type.package_body) {
763                                endBySlashOnly = false;
764                                procedure_status[0] = stored_procedure_status.start;
765                            } else if (sptype[0] == stored_procedure_type.create_trigger) {
766                                endBySlashOnly = false;
767                                procedure_status[0] = stored_procedure_status.start;
768                            } else if (sptype[0] == stored_procedure_type.create_library) {
769                                endBySlashOnly = false;
770                                procedure_status[0] = stored_procedure_status.bodyend;
771                            } else {
772                                endBySlashOnly = true;
773                                procedure_status[0] = stored_procedure_status.bodyend;
774                            }
775
776                            if ((ast.tokencode == TBaseType.rrw_begin)
777                                    || (ast.tokencode == TBaseType.rrw_package)
778                                    || (ast.searchToken(TBaseType.rrw_package, 4) != null)) {
779                                waitingEnds[nestedProcedures] = 1;
780                            }
781                        } else {
782                            gst = EFindSqlStateType.stsql;
783                            appendToken(gcurrentsqlstatement, ast);
784                            nestedParenthesis = 0;
785                            // Check if this is CREATE MLE MODULE with AS clause (JavaScript code)
786                            // If AS is found after LANGUAGE JAVASCRIPT, it terminates with / not ;
787                            if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstoraclecreatemlemodule) {
788                                // Look ahead to see if there's an AS keyword
789                                TSourceToken asToken = ast.searchToken(TBaseType.rrw_as, 10);
790                                mleModuleWithAs = (asToken != null);
791                            } else {
792                                mleModuleWithAs = false;
793                            }
794
795                            // Check if this is WITH FUNCTION/PROCEDURE (Oracle 12c inline PL/SQL)
796                            // Need to track BEGIN/END nesting to handle embedded semicolons
797                            if (ast.tokencode == TBaseType.rrw_with && gcurrentsqlstatement.isctequery) {
798                                // Look ahead for FUNCTION or PROCEDURE keyword
799                                TSourceToken nextSolid = ast.nextSolidToken();
800                                if (nextSolid != null && (nextSolid.tokencode == TBaseType.rrw_function
801                                        || nextSolid.tokencode == TBaseType.rrw_procedure)) {
802                                    withPlsqlDefinition = true;
803                                    withPlsqlBeginEndNesting = 0;
804                                }
805                            }
806                        }
807                    } else {
808                        //error token found
809                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo)
810                                , "Error when tokenize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
811
812                        ast.tokentype = ETokenType.tttokenlizererrortoken;
813                        gst = EFindSqlStateType.sterror;
814
815                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
816                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
817                        appendToken(gcurrentsqlstatement, ast);
818                    }
819
820                    break;
821                } // stnormal
822
823                case stsqlplus: {
824                    if (ast.insqlpluscmd) {
825                        appendToken(gcurrentsqlstatement, ast);
826                    } else {
827                        gst = EFindSqlStateType.stnormal; //this token must be newline,
828                        appendToken(gcurrentsqlstatement, ast); // so add it here
829                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
830                    }
831
832                    break;
833                }//case stsqlplus
834
835                case stsql: {
836                    // For WITH FUNCTION/PROCEDURE, track BEGIN/END nesting and when SELECT is found
837                    if (withPlsqlDefinition) {
838                        if (ast.tokencode == TBaseType.rrw_begin) {
839                            withPlsqlBeginEndNesting++;
840                        } else if (ast.tokencode == TBaseType.rrw_end) {
841                            withPlsqlBeginEndNesting--;
842                            if (withPlsqlBeginEndNesting < 0) withPlsqlBeginEndNesting = 0;
843                        } else if (ast.tokencode == TBaseType.rrw_select && withPlsqlBeginEndNesting == 0) {
844                            // Found SELECT after all function definitions are done
845                            withPlsqlFoundSelect = true;
846                        }
847                    }
848
849                    // For CREATE MLE MODULE with AS clause, don't terminate on semicolon
850                    // The JavaScript code may contain semicolons; wait for / to terminate
851                    // For WITH FUNCTION/PROCEDURE, don't terminate on semicolon until SELECT is found
852                    // (the semicolons in function body and after END are part of the function definition)
853                    // CREATE JAVA SOURCE ... AS <java code>: semicolons inside
854                    // the Java class body (brace depth > 0) are Java statement
855                    // terminators, not SQL statement terminators.
856                    if ((gcurrentsqlstatement != null)
857                            && (gcurrentsqlstatement.sqlstatementtype == gudusoft.gsqlparser.ESqlStatementType.sstoraclecreatejava)) {
858                        if ("{".equals(ast.toString())) {
859                            javaBraceNesting++;
860                        } else if ("}".equals(ast.toString()) && (javaBraceNesting > 0)) {
861                            javaBraceNesting--;
862                        }
863                    }
864
865                    boolean skipSemicolonTermination = mleModuleWithAs || (javaBraceNesting > 0) || (withPlsqlDefinition && !withPlsqlFoundSelect);
866                    if (ast.tokentype == ETokenType.ttsemicolon && !skipSemicolonTermination) {
867                        gst = EFindSqlStateType.stnormal;
868                        appendToken(gcurrentsqlstatement, ast);
869                        gcurrentsqlstatement.semicolonended = ast;
870                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
871                        mleModuleWithAs = false; // Reset flag
872                        javaBraceNesting = 0;
873                        withPlsqlDefinition = false; // Reset WITH FUNCTION flag
874                        withPlsqlBeginEndNesting = 0;
875                        cteMainSelectFound = false;
876                        withPlsqlFoundSelect = false;
877                        continue;
878                    }
879
880                    if (sourcetokenlist.sqlplusaftercurtoken()) //most probably is / cmd
881                    {
882                        gst = EFindSqlStateType.stnormal;
883                        appendToken(gcurrentsqlstatement, ast);
884                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
885                        mleModuleWithAs = false; // Reset flag
886                        javaBraceNesting = 0;
887                        continue;
888                    }
889
890                    if (ast.tokencode == '(') nestedParenthesis++;
891                    if (ast.tokencode == ')') {
892                        nestedParenthesis--;
893                        if (nestedParenthesis < 0) nestedParenthesis = 0;
894                    }
895
896                    Boolean findNewStmt = false;
897                    TCustomSqlStatement lcStmt = null;
898                    // Check for new statement: CREATE TABLE (original), or SELECT inside a non-CTE SELECT
899                    boolean shouldCheckNewStmt = false;
900                    if ((nestedParenthesis == 0) && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatetable)) {
901                        shouldCheckNewStmt = true;
902                    } else if ((nestedParenthesis == 0) && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstselect)
903                            && (ast.tokencode == TBaseType.rrw_select || ast.tokencode == TBaseType.rrw_with)) {
904                        // Check if current statement is a CTE (starts with WITH)
905                        boolean isCteContext = false;
906                        for (int si = 0; si < gcurrentsqlstatement.sourcetokenlist.size(); si++) {
907                            TSourceToken st = gcurrentsqlstatement.sourcetokenlist.get(si);
908                            if (st.tokentype == ETokenType.ttwhitespace || st.tokentype == ETokenType.ttreturn
909                                    || st.tokencode == TBaseType.cmtdoublehyphen || st.tokencode == TBaseType.cmtslashstar) {
910                                continue;
911                            }
912                            if (st.tokencode == TBaseType.rrw_with) {
913                                isCteContext = true;
914                            }
915                            break;
916                        }
917                        // Don't split if previous token makes this SELECT part of current statement:
918                        // - Set operators: UNION, INTERSECT, MINUS, EXCEPT, ALL
919                        // - Left paren: (SELECT ...) — SELECT is main query of parenthesized expr
920                        boolean suppressSplit = false;
921                        if (ast.tokencode == TBaseType.rrw_select && lcprevsolidtoken != null) {
922                            int prevCode = lcprevsolidtoken.tokencode;
923                            if (prevCode == TBaseType.rrw_union || prevCode == TBaseType.rrw_intersect
924                                    || prevCode == TBaseType.rrw_minus || prevCode == TBaseType.rrw_except
925                                    || prevCode == TBaseType.rrw_all
926                                    || prevCode == '(') {
927                                suppressSplit = true;
928                            }
929                        }
930                        if (suppressSplit) {
931                            // SELECT is part of current statement — don't split
932                        } else if (!isCteContext) {
933                            // Non-CTE SELECT: any SELECT/WITH at paren level 0 starts a new statement
934                            shouldCheckNewStmt = true;
935                        } else if (cteMainSelectFound) {
936                            // CTE context: main SELECT already consumed, so this SELECT/WITH
937                            // at paren level 0 is a new statement
938                            shouldCheckNewStmt = true;
939                        } else if (ast.tokencode == TBaseType.rrw_select) {
940                            // CTE context: this is the main SELECT after WITH name AS (...)
941                            cteMainSelectFound = true;
942                            // Don't split — this SELECT is part of the CTE statement
943                        }
944                        // If ast is WITH and main SELECT not yet found, it could be another
945                        // CTE definition (WITH a AS (...), b AS (...)) — don't split
946                    }
947                    if (shouldCheckNewStmt) {
948                        // For SELECT-after-SELECT/WITH splitting, use stnormal so issql can detect CTE starts.
949                        // For CREATE TABLE, preserve original stsql state to avoid false positives
950                        // (e.g., INSERT/DELETE keywords in blockchain table clauses).
951                        EFindSqlStateType issqlState = (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstselect)
952                                ? EFindSqlStateType.stnormal : gst;
953                        lcStmt = sqlcmds.issql(ast, issqlState, gcurrentsqlstatement);
954                        if (lcStmt != null) {
955                            findNewStmt = true;
956                            if (lcStmt.sqlstatementtype == ESqlStatementType.sstselect) {
957                                TSourceToken prevst = ast.prevSolidToken();
958                                if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatetable) {
959                                    // For CREATE TABLE, suppress split when SELECT follows AS/(/): AS (SELECT ...)
960                                    if ((prevst.tokencode == TBaseType.rrw_as) || (prevst.tokencode == '(') || (prevst.tokencode == ')')) {
961                                        findNewStmt = false;
962                                    }
963                                }
964                                // For SELECT-after-SELECT/WITH splitting at paren level 0,
965                                // no suppression needed — the new SELECT/WITH is a new statement
966                            }
967                        }
968                    }
969
970                    if (findNewStmt) {
971                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
972                        gcurrentsqlstatement = lcStmt;
973                        cteMainSelectFound = false; // Reset for new statement
974                        nestedParenthesis = 0; // Reset paren tracking for new statement
975                        appendToken(gcurrentsqlstatement, ast);
976                        continue;
977                    } else
978                        appendToken(gcurrentsqlstatement, ast);
979
980                    break;
981                }//case stsql
982
983                case ststoredprocedure: {
984
985                    if (procedure_status[nestedProcedures] != stored_procedure_status.bodyend) {
986                        appendToken(gcurrentsqlstatement, ast);
987                    }
988
989                    switch (procedure_status[nestedProcedures]) {
990                        case cursor_declare:
991                            if (ast.tokencode == ';') {
992                                nestedProcedures--;
993                                if (nestedProcedures < 0) {
994                                    nestedProcedures = 0;
995                                }
996                            }
997                            break;
998                        case start:
999                            if ((ast.tokencode == TBaseType.rrw_as) || (ast.tokencode == TBaseType.rrw_is)) {
1000                                if (sptype[nestedProcedures] != stored_procedure_type.create_trigger) {
1001                                    if ((sptype[0] == stored_procedure_type.package_spec) && (nestedProcedures > 0)) {
1002                                        // when it's a package specification, only top level accept as/is
1003                                    } else {
1004                                        procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1005                                        if (ast.searchToken("language", 1) != null) {
1006                                            if (nestedProcedures == 0) {
1007                                                gst = EFindSqlStateType.stsql;
1008                                            } else {
1009                                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1010                                                nestedProcedures--;
1011                                            }
1012                                        }
1013                                    }
1014                                }
1015                            } else if (ast.tokencode == TBaseType.rrw_begin) {
1016                                if (sptype[nestedProcedures] == stored_procedure_type.create_trigger) {
1017                                    waitingEnds[nestedProcedures]++;
1018                                }
1019                                if (nestedProcedures > 0) {
1020                                    nestedProcedures--;
1021                                }
1022                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1023                            } else if (ast.tokencode == TBaseType.rrw_end) {
1024                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures - 1] == 1)
1025                                        && ((sptype[nestedProcedures - 1] == stored_procedure_type.package_body)
1026                                        || (sptype[nestedProcedures - 1] == stored_procedure_type.package_spec))) {
1027                                    nestedProcedures--;
1028                                    procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1029                                }
1030                            } else if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) {
1031                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures] == 0)
1032                                        && (procedure_status[nestedProcedures - 1] == stored_procedure_status.is_as)) {
1033                                    nestedProcedures--;
1034                                    nestedProcedures++;
1035                                    waitingEnds[nestedProcedures] = 0;
1036                                    procedure_status[nestedProcedures] = stored_procedure_status.start;
1037                                }
1038                            } else if (ast.tokencode == TBaseType.rrw_oracle_cursor) {
1039                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures] == 0)
1040                                        && (procedure_status[nestedProcedures - 1] == stored_procedure_status.is_as)) {
1041                                    nestedProcedures--;
1042                                    nestedProcedures++;
1043                                    waitingEnds[nestedProcedures] = 0;
1044                                    procedure_status[nestedProcedures] = stored_procedure_status.cursor_declare;
1045                                }
1046                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger) && (ast.tokencode == TBaseType.rrw_declare)) {
1047                                procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1048                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger)
1049                                    && (ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) {
1050                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1051                                gst = EFindSqlStateType.stnormal;
1052                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1053
1054                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1055                                appendToken(gcurrentsqlstatement, ast);
1056                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1057                            } else if (sptype[nestedProcedures] == stored_procedure_type.create_trigger) {
1058                                if (ast.tokencode == TBaseType.rrw_trigger) {
1059                                    TSourceToken compoundSt = ast.searchToken(TBaseType.rrw_oracle_compound, -1);
1060                                    if (compoundSt != null) {
1061                                        procedure_status[nestedProcedures] = stored_procedure_status.body;
1062                                        waitingEnds[nestedProcedures]++;
1063                                    }
1064                                }
1065                            } else if ((sptype[nestedProcedures] == stored_procedure_type.function)
1066                                    && (ast.tokencode == TBaseType.rrw_teradata_using)) {
1067                                if ((ast.searchToken("aggregate", -1) != null) || (ast.searchToken("pipelined", -1) != null)) {
1068                                    if (nestedProcedures == 0) {
1069                                        gst = EFindSqlStateType.stsql;
1070                                    } else {
1071                                        procedure_status[nestedProcedures] = stored_procedure_status.body;
1072                                        nestedProcedures--;
1073                                    }
1074                                }
1075                            }
1076                            break;
1077                        case is_as:
1078                            if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) {
1079                                nestedProcedures++;
1080                                if (nestedProcedures > stored_procedure_nested_level - 1) {
1081                                    gst = EFindSqlStateType.sterror;
1082                                    nestedProcedures--;
1083                                } else {
1084                                    waitingEnds[nestedProcedures] = 0;
1085                                    procedure_status[nestedProcedures] = stored_procedure_status.start;
1086                                }
1087                            } else if (ast.tokencode == TBaseType.rrw_begin) {
1088                                if ((nestedProcedures == 0) &&
1089                                        ((sptype[nestedProcedures] == stored_procedure_type.package_body)
1090                                                || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) {
1091                                    // top level package begin already counted
1092                                } else {
1093                                    waitingEnds[nestedProcedures]++;
1094                                }
1095                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1096                            } else if (ast.tokencode == TBaseType.rrw_end) {
1097                                if ((nestedProcedures == 0) && (waitingEnds[nestedProcedures] == 1)
1098                                        && ((sptype[nestedProcedures] == stored_procedure_type.package_body)
1099                                        || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) {
1100                                    procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1101                                    waitingEnds[nestedProcedures]--;
1102                                } else {
1103                                    waitingEnds[nestedProcedures]--;
1104                                }
1105                            } else if (ast.tokencode == TBaseType.rrw_case) {
1106                                if (ast.searchToken(';', 1) == null) {
1107                                    waitingEnds[nestedProcedures]++;
1108                                }
1109                            }
1110                            break;
1111                        case body:
1112                            if (ast.tokencode == TBaseType.rrw_begin) {
1113                                waitingEnds[nestedProcedures]++;
1114                            } else if (ast.tokencode == TBaseType.rrw_if) {
1115                                if (ast.searchToken(';', 2) == null) {
1116                                    waitingEnds[nestedProcedures]++;
1117                                }
1118                            } else if (ast.tokencode == TBaseType.rrw_case) {
1119                                if (ast.searchToken(';', 2) == null) {
1120                                    if (ast.searchToken(TBaseType.rrw_end, -1) == null) {
1121                                        waitingEnds[nestedProcedures]++;
1122                                    }
1123                                }
1124                            } else if (ast.tokencode == TBaseType.rrw_loop) {
1125                                if (!((ast.searchToken(TBaseType.rrw_end, -1) != null)
1126                                        && (ast.searchToken(';', 2) != null))) {
1127                                    waitingEnds[nestedProcedures]++;
1128                                }
1129                            } else if (ast.tokencode == TBaseType.rrw_end) {
1130                                waitingEnds[nestedProcedures]--;
1131                                if (waitingEnds[nestedProcedures] == 0) {
1132                                    if (nestedProcedures == 0) {
1133                                        procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1134                                    } else {
1135                                        nestedProcedures--;
1136                                        procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1137                                    }
1138                                }
1139                            } else if ((waitingEnds[nestedProcedures] == 0)
1140                                    && (ast.tokentype == ETokenType.ttslash)
1141                                    && (ast.tokencode == TBaseType.sqlpluscmd)) {
1142                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1143                                gst = EFindSqlStateType.stnormal;
1144                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1145
1146                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1147                                appendToken(gcurrentsqlstatement, ast);
1148                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1149                            }
1150                            break;
1151                        case bodyend:
1152                            if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) {
1153                                // TPlsqlStatementParse(asqlstatement).TerminatorToken := ast;
1154                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1155                                gst = EFindSqlStateType.stnormal;
1156                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1157
1158                                //make / a sqlplus cmd
1159                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1160                                appendToken(gcurrentsqlstatement, ast);
1161                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1162                            } else if ((ast.tokentype == ETokenType.ttperiod) && (sourcetokenlist.returnaftercurtoken(false)) && (sourcetokenlist.returnbeforecurtoken(false))) {
1163                                // single dot at a seperate line
1164                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1165                                gst = EFindSqlStateType.stnormal;
1166                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1167
1168                                //make ttperiod a sqlplus cmd
1169                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1170                                appendToken(gcurrentsqlstatement, ast);
1171                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1172                            } else if ((ast.searchToken(TBaseType.rrw_package, 1) != null) && (!endBySlashOnly)) {
1173                                appendToken(gcurrentsqlstatement, ast);
1174                                gst = EFindSqlStateType.stnormal;
1175                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1176                            } else if ((ast.searchToken(TBaseType.rrw_procedure, 1) != null) && (!endBySlashOnly)) {
1177                                appendToken(gcurrentsqlstatement, ast);
1178                                gst = EFindSqlStateType.stnormal;
1179                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1180                            } else if ((ast.searchToken(TBaseType.rrw_function, 1) != null) && (!endBySlashOnly)) {
1181                                appendToken(gcurrentsqlstatement, ast);
1182                                gst = EFindSqlStateType.stnormal;
1183                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1184                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null)
1185                                    && ((ast.searchToken(TBaseType.rrw_package, 4) != null) || (ast.searchToken(TBaseType.rrw_package, 5) != null))
1186                                    && (!endBySlashOnly)) {
1187                                appendToken(gcurrentsqlstatement, ast);
1188                                gst = EFindSqlStateType.stnormal;
1189                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1190                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null)
1191                                    && ((ast.searchToken(TBaseType.rrw_procedure, 4) != null)
1192                                            || (ast.searchToken(TBaseType.rrw_function, 4) != null)
1193                                            || (ast.searchToken(TBaseType.rrw_view, 4) != null)
1194                                            || (ast.searchToken(TBaseType.rrw_oracle_synonym, 4) != null)
1195                                            || (ast.searchToken(TBaseType.rrw_trigger, 4) != null))
1196                                    && (!endBySlashOnly)) {
1197                                appendToken(gcurrentsqlstatement, ast);
1198                                gst = EFindSqlStateType.stnormal;
1199                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1200                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) && (ast.searchToken(TBaseType.rrw_library, 4) != null) && (!endBySlashOnly)) {
1201                                appendToken(gcurrentsqlstatement, ast);
1202                                gst = EFindSqlStateType.stnormal;
1203                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1204                            } else if ((ast.searchToken(TBaseType.rrw_alter, 1) != null) && (ast.searchToken(TBaseType.rrw_trigger, 2) != null) && (!endBySlashOnly)) {
1205                                appendToken(gcurrentsqlstatement, ast);
1206                                gst = EFindSqlStateType.stnormal;
1207                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1208                            } else if ((ast.searchToken(TBaseType.rrw_select, 1) != null) && (!endBySlashOnly)) {
1209                                appendToken(gcurrentsqlstatement, ast);
1210                                gst = EFindSqlStateType.stnormal;
1211                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1212                            } else if ((ast.searchToken(TBaseType.rrw_call, 1) != null) && (!endBySlashOnly)) {
1213                                appendToken(gcurrentsqlstatement, ast);
1214                                gst = EFindSqlStateType.stnormal;
1215                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1216                            } else if ((ast.searchToken(TBaseType.rrw_commit, 1) != null) && (!endBySlashOnly)) {
1217                                appendToken(gcurrentsqlstatement, ast);
1218                                gst = EFindSqlStateType.stnormal;
1219                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1220                            } else if ((ast.searchToken(TBaseType.rrw_declare, 1) != null) && (!endBySlashOnly)) {
1221                                appendToken(gcurrentsqlstatement, ast);
1222                                gst = EFindSqlStateType.stnormal;
1223                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1224                            } else if ((ast.searchToken(TBaseType.rrw_grant, 1) != null)
1225                                    && (ast.searchToken(TBaseType.rrw_execute, 2) != null) && (!endBySlashOnly)) {
1226                                appendToken(gcurrentsqlstatement, ast);
1227                                gst = EFindSqlStateType.stnormal;
1228                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1229                            } else if ((ast.searchToken(TBaseType.rrw_alter, 1) != null)
1230                                    && (ast.searchToken(TBaseType.rrw_table, 2) != null) && (!endBySlashOnly)) {
1231                                appendToken(gcurrentsqlstatement, ast);
1232                                gst = EFindSqlStateType.stnormal;
1233                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1234                            } else {
1235                                appendToken(gcurrentsqlstatement, ast);
1236                            }
1237                            break;
1238                        case end:
1239                            break;
1240                        default:
1241                            break;
1242                    }
1243
1244                    if (ast.tokencode == TBaseType.sqlpluscmd) {
1245                        int m = flexer.getkeywordvalue(ast.getAstext());
1246                        if (m != 0) {
1247                            ast.tokencode = m;
1248                        } else if (ast.tokentype == ETokenType.ttslash) {
1249                            ast.tokencode = '/';
1250                        } else {
1251                            ast.tokencode = TBaseType.ident;
1252                        }
1253                    }
1254
1255                    final int wrapped_keyword_max_pos = 20;
1256                    if ((ast.tokencode == TBaseType.rrw_wrapped)
1257                            && (ast.posinlist - gcurrentsqlstatement.sourcetokenlist.get(0).posinlist < wrapped_keyword_max_pos)) {
1258                        if (gcurrentsqlstatement instanceof gudusoft.gsqlparser.stmt.TCommonStoredProcedureSqlStatement) {
1259                            ((gudusoft.gsqlparser.stmt.TCommonStoredProcedureSqlStatement) gcurrentsqlstatement).setWrapped(true);
1260                        }
1261
1262                        if (gcurrentsqlstatement instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage) {
1263                            if (ast.prevSolidToken() != null) {
1264                                ((gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage) gcurrentsqlstatement)
1265                                        .setPackageName(fparser.getNf().createObjectNameWithPart(ast.prevSolidToken()));
1266                            }
1267                        }
1268                    }
1269
1270                    break;
1271                } //ststoredprocedure
1272
1273            } //switch
1274        }//for
1275
1276        //last statement
1277        if ((gcurrentsqlstatement != null) &&
1278                ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) ||
1279                        (gst == EFindSqlStateType.sterror))) {
1280            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, true, builder);
1281        }
1282
1283        // Populate builder with results
1284        builder.sqlStatements(this.sqlstatements);
1285        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
1286                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
1287        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
1288        builder.errorMessage(syntaxErrors.isEmpty() ? "" :
1289                String.format("Raw extraction completed with %d error(s)", syntaxErrors.size()));
1290    }
1291
1292    /**
1293     * Handle token transformations during raw statement extraction.
1294     * <p>
1295     * This performs Oracle-specific keyword disambiguation that must happen
1296     * before statement boundary detection. Examples:
1297     * <ul>
1298     *   <li>RETURN after WHERE → treat as identifier</li>
1299     *   <li>VALUE after BY → mark as value_after_by</li>
1300     *   <li>NEW → treat as identifier or constructor based on context</li>
1301     *   <li>And many more Oracle-specific cases</li>
1302     * </ul>
1303     *
1304     * @param ast current token being processed
1305     */
1306    private void performRawStatementTokenTransformations(TSourceToken ast) {
1307        // This method contains the keyword transformation logic from dooraclegetrawsqlstatements
1308        // It's been extracted to keep the main method more readable
1309
1310        if (ast.tokencode == TBaseType.rrw_return) {
1311            TSourceToken stMatch = ast.searchToken(TBaseType.rrw_where, 1);
1312            if (stMatch != null) {
1313                ast.tokencode = TBaseType.ident;
1314            }
1315        } else if (ast.tokencode == TBaseType.rrw_value_oracle) {
1316            TSourceToken stBy = ast.searchToken(TBaseType.rrw_by, -1);
1317            if (stBy != null) {
1318                ast.tokencode = TBaseType.rrw_value_after_by;
1319            }
1320        } else if (ast.tokencode == TBaseType.rrw_new_oracle) {
1321            TSourceToken stRightParen = ast.searchToken(')', -1);
1322            if (stRightParen != null) {
1323                ast.tokencode = TBaseType.ident;
1324            }
1325            TSourceToken stDot = ast.searchToken('.', 1);
1326            if (stDot != null) {
1327                ast.tokencode = TBaseType.ident;
1328            }
1329
1330            TSourceToken stNext = ast.searchTokenAfterObjectName();
1331            stDot = ast.searchToken('.', 1);
1332            if ((stDot == null) && (stNext != null) && (stNext.tokencode == '(')) {
1333                ast.tokencode = TBaseType.rrw_oracle_new_constructor;
1334            }
1335        } else if (ast.tokencode == TBaseType.rrw_chr_oracle) {
1336            TSourceToken stLeftParen = ast.searchToken('(', 1);
1337            if (stLeftParen == null) {
1338                ast.tokencode = TBaseType.ident;
1339            }
1340        } else if (ast.tokencode == TBaseType.rrw_log_oracle) {
1341            TSourceToken stNext = ast.searchToken(TBaseType.rrw_errors_oracle, 1);
1342            TSourceToken stPrev = ast.searchToken(TBaseType.rrw_view, -1);
1343            if (stPrev == null) {
1344                stPrev = ast.searchToken(TBaseType.rrw_oracle_supplemental, -1);
1345            }
1346            if ((stNext == null) && (stPrev == null)) {
1347                ast.tokencode = TBaseType.ident;
1348            }
1349        } else if (ast.tokencode == TBaseType.rrw_delete) {
1350            TSourceToken stPrev = ast.searchToken('.', -1);
1351            if (stPrev != null) {
1352                ast.tokencode = TBaseType.ident;
1353            }
1354        } else if (ast.tokencode == TBaseType.rrw_partition) {
1355            TSourceToken stPrev = ast.searchToken(TBaseType.rrw_add, -1);
1356            if (stPrev != null) {
1357                stPrev.tokencode = TBaseType.rrw_add_p;
1358            }
1359        } else if (ast.tokencode == TBaseType.rrw_oracle_column) {
1360            TSourceToken stPrev = ast.searchToken(TBaseType.rrw_oracle_modify, -1);
1361            if (stPrev != null) {
1362                ast.tokencode = TBaseType.rrw_oracle_column_after_modify;
1363            }
1364        } else if (ast.tokencode == TBaseType.rrw_oracle_apply) {
1365            TSourceToken stPrev = ast.searchToken(TBaseType.rrw_outer, -1);
1366            if (stPrev != null) {
1367                stPrev.tokencode = TBaseType.ORACLE_OUTER2;
1368            }
1369        } else if (ast.tokencode == TBaseType.rrw_oracle_subpartition) {
1370            TSourceToken stNext = ast.searchToken("(", 2);
1371            if (stNext != null) {
1372                TSourceToken st1 = ast.nextSolidToken();
1373                if (st1.toString().equalsIgnoreCase("template")) {
1374                    // don't change, keep as RW_SUBPARTITION
1375                } else {
1376                    ast.tokencode = TBaseType.rrw_oracle_subpartition_tablesample;
1377                }
1378            }
1379        } else if (ast.tokencode == TBaseType.rrw_primary) {
1380            TSourceToken stNext = ast.searchToken("key", 1);
1381            if (stNext == null) {
1382                ast.tokencode = TBaseType.ident;
1383            }
1384        } else if (ast.tokencode == TBaseType.rrw_oracle_offset) {
1385            TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_row, 2);
1386            if (stNext == null) {
1387                stNext = ast.searchToken(TBaseType.rrw_oracle_rows, 2);
1388            }
1389            if (stNext == null) {
1390                stNext = searchRowAfterOffsetExpression(ast);
1391            }
1392            if (stNext != null) {
1393                ast.tokencode = TBaseType.rrw_oracle_offset_row;
1394            }
1395        } else if (ast.tokencode == TBaseType.rrw_translate) {
1396            TSourceToken stNext = ast.searchToken("(", 2);
1397            if (stNext == null) {
1398                ast.tokencode = TBaseType.ident;
1399            }
1400        } else if (ast.tokencode == TBaseType.rrw_constraint) {
1401            TSourceToken stNext = ast.nextSolidToken();
1402            if (stNext == null) {
1403                ast.tokencode = TBaseType.ident;
1404            } else {
1405                if (stNext.tokencode != TBaseType.ident) {
1406                    ast.tokencode = TBaseType.ident;
1407                }
1408            }
1409        } else if (ast.tokencode == TBaseType.rrw_oracle_without) {
1410            TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_count, 1);
1411            if (stNext != null) {
1412                ast.tokencode = TBaseType.rrw_oracle_without_before_count;
1413            }
1414        } else if (ast.tokencode == TBaseType.rrw_bulk) {
1415            TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_collect, 1);
1416            if (stNext == null) {
1417                ast.tokencode = TBaseType.ident;
1418            }
1419        } else if (ast.tokencode == TBaseType.rrw_oracle_model) {
1420            TSourceToken stNext = ast.nextSolidToken();
1421            if (stNext != null) {
1422                switch (stNext.toString().toUpperCase()) {
1423                    case "RETURN":
1424                    case "REFERENCE":
1425                    case "IGNORE":
1426                    case "KEEP":
1427                    case "UNIQUE":
1428                    case "PARTITION":
1429                    case "DIMENSION":
1430                    case "MEASURES":
1431                    case "RULES":
1432                        ast.tokencode = TBaseType.rrw_oracle_model_in_model_clause;
1433                        break;
1434                    default:
1435                        ;
1436                }
1437            }
1438        }
1439    }
1440
1441    /**
1442     * Look for the ROW/ROWS keyword terminating an OFFSET clause whose value is
1443     * a multi-token arithmetic expression, e.g. <code>OFFSET 1 * 10 ROWS</code>
1444     * (Mantis 4614). The single-token case is handled by the caller via
1445     * {@link TSourceToken#searchToken(int, int)}; this scan only accepts a
1446     * strictly alternating operand/operator token run (with balanced
1447     * parentheses), so a bare <code>OFFSET</code> used as a column name is not
1448     * rewritten.
1449     *
1450     * @param offsetToken the OFFSET keyword token
1451     * @return the terminating ROW/ROWS token, or null if the following tokens
1452     *         do not form an arithmetic expression ended by ROW/ROWS
1453     */
1454    private TSourceToken searchRowAfterOffsetExpression(TSourceToken offsetToken) {
1455        // generous cap: only bounds runaway scans on token runs that never hit
1456        // ROW/ROWS; any realistic offset expression is far shorter (a tight cap
1457        // would reject valid long expressions before the grammar sees them)
1458        final int maxTokens = 512;
1459        TSourceToken t = offsetToken.nextSolidToken();
1460        int parenDepth = 0;
1461        boolean expectOperand = true;
1462        int steps = 0;
1463        while (t != null && steps++ < maxTokens) {
1464            String text = t.toString();
1465            if (expectOperand) {
1466                if (text.equals("(")) {
1467                    parenDepth++;
1468                } else if (text.equals("+") || text.equals("-")) {
1469                    // unary sign, keep expecting an operand
1470                } else if (t.tokencode == TBaseType.iconst
1471                        || t.tokencode == TBaseType.fconst
1472                        || t.tokencode == TBaseType.ident
1473                        || text.startsWith(":")) {
1474                    expectOperand = false;
1475                } else {
1476                    return null;
1477                }
1478            } else {
1479                if (parenDepth == 0
1480                        && (t.tokencode == TBaseType.rrw_oracle_row || t.tokencode == TBaseType.rrw_oracle_rows)) {
1481                    return t;
1482                }
1483                if (text.equals(")")) {
1484                    if (--parenDepth < 0) {
1485                        return null;
1486                    }
1487                } else if (text.equals("+") || text.equals("-") || text.equals("*") || text.equals("/")) {
1488                    expectOperand = true;
1489                } else {
1490                    return null;
1491                }
1492            }
1493            t = t.nextSolidToken();
1494        }
1495        return null;
1496    }
1497
1498    private void appendToken(TCustomSqlStatement statement, TSourceToken token) {
1499        if (statement == null || token == null) {
1500            return;
1501        }
1502        token.stmt = statement;
1503        statement.sourcetokenlist.add(token);
1504    }
1505
1506    // ========== Error Handling and Recovery ==========
1507
1508    /**
1509     * Find all syntax errors in PL/SQL statements recursively.
1510     * Extracted from TGSqlParser.findAllSyntaxErrorsInPlsql().
1511     */
1512    private void findAllSyntaxErrorsInPlsql(TCustomSqlStatement psql) {
1513        if (psql.getErrorCount() > 0) {
1514            copyErrorsFromStatement(psql);
1515        }
1516
1517        for (int k = 0; k < psql.getStatements().size(); k++) {
1518            findAllSyntaxErrorsInPlsql(psql.getStatements().get(k));
1519        }
1520    }
1521
1522    /**
1523     * Handle error recovery for CREATE TABLE/INDEX statements.
1524     * Oracle allows table properties that may not be fully parsed.
1525     * This method marks unparseable properties as SQL*Plus commands to skip them.
1526     *
1527     * <p>Extracted from TGSqlParser.doparse() lines 16916-16971
1528     */
1529    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
1530        if (((stmt.sqlstatementtype == ESqlStatementType.sstcreatetable) ||
1531             (stmt.sqlstatementtype == ESqlStatementType.sstcreateindex)) &&
1532            (!TBaseType.c_createTableStrictParsing)) {
1533
1534            // Find the closing parenthesis of table definition
1535            int nested = 0;
1536            boolean isIgnore = false, isFoundIgnoreToken = false;
1537            TSourceToken firstIgnoreToken = null;
1538
1539            for (int k = 0; k < stmt.sourcetokenlist.size(); k++) {
1540                TSourceToken st = stmt.sourcetokenlist.get(k);
1541
1542                if (isIgnore) {
1543                    if (st.issolidtoken() && (st.tokencode != ';')) {
1544                        isFoundIgnoreToken = true;
1545                        if (firstIgnoreToken == null) {
1546                            firstIgnoreToken = st;
1547                        }
1548                    }
1549                    if (st.tokencode != ';') {
1550                        st.tokencode = TBaseType.sqlpluscmd;
1551                    }
1552                    continue;
1553                }
1554
1555                if (st.tokencode == (int) ')') {
1556                    nested--;
1557                    if (nested == 0) {
1558                        // Check if next token is "AS ( SELECT"
1559                        boolean isSelect = false;
1560                        TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1);
1561                        if (st1 != null) {
1562                            TSourceToken st2 = st.searchToken((int) '(', 2);
1563                            if (st2 != null) {
1564                                TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3);
1565                                isSelect = (st3 != null);
1566                            }
1567                        }
1568                        if (!isSelect) isIgnore = true;
1569                    }
1570                }
1571
1572                if ((st.tokencode == (int) '(') || (st.tokencode == TBaseType.left_parenthesis_2)) {
1573                    nested++;
1574                }
1575            }
1576
1577            // Verify it's a valid Oracle table property
1578            if ((firstIgnoreToken != null) &&
1579                (!TBaseType.searchOracleTablePros(firstIgnoreToken.toString()))) {
1580                // Not a valid property, keep the error
1581                isFoundIgnoreToken = false;
1582            }
1583
1584            // Retry parsing if we found ignoreable properties
1585            if (isFoundIgnoreToken) {
1586                stmt.clearError();
1587                stmt.parsestatement(null, false);
1588            }
1589        }
1590    }
1591
1592    /**
1593     * Copy syntax errors from a statement to our error list.
1594     * Extracted from TGSqlParser.copyerrormsg().
1595     */
1596
1597    @Override
1598    public String toString() {
1599        return "OracleSqlParser{vendor=" + vendor + "}";
1600    }
1601
1602    // ========== Main Oracle Tokenization ==========
1603    // Core tokenization logic extracted from TGSqlParser.dooraclesqltexttotokenlist()
1604
1605    /**
1606     * Perform Oracle-specific tokenization with SQL*Plus command detection.
1607     * <p>
1608     * This method implements Oracle's complex tokenization rules including:
1609     * <ul>
1610     *   <li>SQL*Plus command detection (SPOOL, SET, START, etc.)</li>
1611     *   <li>Forward slash disambiguation (division vs PL/SQL delimiter)</li>
1612     *   <li>Oracle-specific keyword transformations (INNER, TYPE, FULL, etc.)</li>
1613     *   <li>Context-dependent token code modifications</li>
1614     * </ul>
1615     *
1616     * <p><b>State Machine:</b> Uses 5 boolean flags to track tokenization state:
1617     * <ul>
1618     *   <li>{@code insqlpluscmd} - Currently inside SQL*Plus command</li>
1619     *   <li>{@code isvalidplace} - Valid place to start SQL*Plus command</li>
1620     *   <li>{@code waitingreturnforfloatdiv} - Slash seen, waiting for newline</li>
1621     *   <li>{@code waitingreturnforsemicolon} - Semicolon seen, waiting for newline</li>
1622     *   <li>{@code continuesqlplusatnewline} - SQL*Plus command continues to next line</li>
1623     * </ul>
1624     *
1625     * <p><b>Extracted from:</b> TGSqlParser.dooraclesqltexttotokenlist() (lines 3931-4298)
1626     *
1627     * @throws RuntimeException if tokenization fails
1628     */
1629    private void dooraclesqltexttotokenlist() {
1630        // Initialize state machine for SQL*Plus command detection
1631        insqlpluscmd = false;
1632        javaBraceNesting = 0;
1633        isvalidplace = true;
1634        waitingreturnforfloatdiv = false;
1635        waitingreturnforsemicolon = false;
1636        continuesqlplusatnewline = false;
1637
1638        ESqlPlusCmd currentCmdType = ESqlPlusCmd.spcUnknown;
1639
1640        TSourceToken lct = null, prevst = null;
1641
1642        TSourceToken asourcetoken, lcprevst;
1643        int yychar;
1644
1645        asourcetoken = getanewsourcetoken();
1646        if (asourcetoken == null) return;
1647        yychar = asourcetoken.tokencode;
1648
1649        while (yychar > 0) {
1650            sourcetokenlist.add(asourcetoken);
1651
1652            switch (yychar) {
1653                case TBaseType.cmtdoublehyphen:
1654                case TBaseType.cmtslashstar:
1655                case TBaseType.lexspace: {
1656                    if (insqlpluscmd) {
1657                        asourcetoken.insqlpluscmd = true;
1658                    }
1659                    break;
1660                }
1661
1662                case TBaseType.lexnewline: {
1663                    if (insqlpluscmd) {
1664                        insqlpluscmd = false;
1665                        isvalidplace = true;
1666
1667                        if (continuesqlplusatnewline) {
1668                            insqlpluscmd = true;
1669                            isvalidplace = false;
1670                            asourcetoken.insqlpluscmd = true;
1671                        }
1672
1673                        if (!insqlpluscmd) {
1674                            currentCmdType = ESqlPlusCmd.spcUnknown;
1675                        }
1676                    }
1677
1678                    if (waitingreturnforsemicolon) {
1679                        isvalidplace = true;
1680                    }
1681
1682                    if (waitingreturnforfloatdiv) {
1683                        isvalidplace = true;
1684                        lct.tokencode = TBaseType.sqlpluscmd;
1685                        if (lct.tokentype != ETokenType.ttslash) {
1686                            lct.tokentype = ETokenType.ttsqlpluscmd;
1687                        }
1688                    }
1689
1690                    if (countLines(asourcetoken.toString()) > 1) {
1691                        // There is a line after select, so spool is the right place to start a sqlplus command
1692                        isvalidplace = true;
1693                    }
1694
1695                    flexer.insqlpluscmd = insqlpluscmd;
1696                    break;
1697                }
1698
1699                default: {
1700                    // Solid token
1701                    // Save semicolon flag before clearing: slash after semicolon on
1702                    // the same line (e.g. "END; /") should be a SQL*Plus delimiter,
1703                    // not division.
1704                    boolean prevWasSemicolon = waitingreturnforsemicolon;
1705                    continuesqlplusatnewline = false;
1706                    waitingreturnforsemicolon = false;
1707                    waitingreturnforfloatdiv = false;
1708
1709                    if (insqlpluscmd) {
1710                        asourcetoken.insqlpluscmd = true;
1711                        if (asourcetoken.toString().equalsIgnoreCase("-")) {
1712                            continuesqlplusatnewline = true;
1713                        }
1714                    } else {
1715                        if (asourcetoken.tokentype == ETokenType.ttsemicolon) {
1716                            waitingreturnforsemicolon = true;
1717                        }
1718
1719                        if ((asourcetoken.tokentype == ETokenType.ttslash)
1720                                && (isvalidplace || prevWasSemicolon || (isValidPlaceForDivToSqlplusCmd(sourcetokenlist, asourcetoken.posinlist)))) {
1721                            lct = asourcetoken;
1722                            waitingreturnforfloatdiv = true;
1723                        }
1724
1725                        currentCmdType = TSqlplusCmdStatement.searchCmd(asourcetoken.toString(), asourcetoken.nextToken());
1726                        if (currentCmdType != ESqlPlusCmd.spcUnknown) {
1727                            if (isvalidplace) {
1728                                TSourceToken lnbreak = null;
1729                                boolean aRealSqlplusCmd = true;
1730                                if (sourcetokenlist.curpos > 0) {
1731                                    lnbreak = sourcetokenlist.get(sourcetokenlist.curpos - 1);
1732                                    aRealSqlplusCmd = !spaceAtTheEndOfReturnToken(lnbreak.toString());
1733                                }
1734
1735                                if (aRealSqlplusCmd) {
1736                                    asourcetoken.prevTokenCode = asourcetoken.tokencode;
1737                                    asourcetoken.tokencode = TBaseType.sqlpluscmd;
1738                                    if (asourcetoken.tokentype != ETokenType.ttslash) {
1739                                        asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
1740                                    }
1741                                    insqlpluscmd = true;
1742                                    flexer.insqlpluscmd = insqlpluscmd;
1743                                }
1744                            } else if ((asourcetoken.tokencode == TBaseType.rrw_connect) && (sourcetokenlist.returnbeforecurtoken(true))) {
1745                                asourcetoken.tokencode = TBaseType.sqlpluscmd;
1746                                if (asourcetoken.tokentype != ETokenType.ttslash) {
1747                                    asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
1748                                }
1749                                insqlpluscmd = true;
1750                                flexer.insqlpluscmd = insqlpluscmd;
1751                            } else if (sourcetokenlist.returnbeforecurtoken(true)) {
1752                                TSourceToken lnbreak = sourcetokenlist.get(sourcetokenlist.curpos - 1);
1753
1754                                if ((countLines(lnbreak.toString()) > 1) && (!spaceAtTheEndOfReturnToken(lnbreak.toString()))) {
1755                                    asourcetoken.tokencode = TBaseType.sqlpluscmd;
1756                                    if (asourcetoken.tokentype != ETokenType.ttslash) {
1757                                        asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
1758                                    }
1759                                    insqlpluscmd = true;
1760                                    flexer.insqlpluscmd = insqlpluscmd;
1761                                }
1762                            }
1763                        }
1764                    }
1765
1766                    isvalidplace = false;
1767
1768                    // Oracle-specific keyword handling (inline to match legacy behavior)
1769                    if (prevst != null) {
1770                        if (prevst.tokencode == TBaseType.rrw_inner) {
1771                            if (asourcetoken.tokencode != flexer.getkeywordvalue("JOIN")) {
1772                                prevst.tokencode = TBaseType.ident;
1773                            }
1774                        } else if ((prevst.tokencode == TBaseType.rrw_not)
1775                                && (asourcetoken.tokencode == flexer.getkeywordvalue("DEFERRABLE"))) {
1776                            prevst.tokencode = flexer.getkeywordvalue("NOT_DEFERRABLE");
1777                        }
1778                    }
1779
1780                    if (asourcetoken.tokencode == TBaseType.rrw_inner) {
1781                        prevst = asourcetoken;
1782                    } else if (asourcetoken.tokencode == TBaseType.rrw_not) {
1783                        prevst = asourcetoken;
1784                    } else {
1785                        prevst = null;
1786                    }
1787
1788                    // Oracle keyword transformations that rely on prev token state
1789                    if ((asourcetoken.tokencode == flexer.getkeywordvalue("DIRECT_LOAD"))
1790                            || (asourcetoken.tokencode == flexer.getkeywordvalue("ALL"))) {
1791                        lcprevst = getprevsolidtoken(asourcetoken);
1792                        if (lcprevst != null) {
1793                            if (lcprevst.tokencode == TBaseType.rrw_for)
1794                                lcprevst.tokencode = TBaseType.rw_for1;
1795                        }
1796                    } else if (asourcetoken.tokencode == TBaseType.rrw_dense_rank) {
1797                        TSourceToken stKeep = asourcetoken.searchToken(TBaseType.rrw_keep, -2);
1798                        if (stKeep != null) {
1799                            stKeep.tokencode = TBaseType.rrw_keep_before_dense_rank;
1800                        }
1801                    } else if (asourcetoken.tokencode == TBaseType.rrw_full) {
1802                        TSourceToken stMatch = asourcetoken.searchToken(TBaseType.rrw_match, -1);
1803                        if (stMatch != null) {
1804                            asourcetoken.tokencode = TBaseType.RW_FULL2;
1805                        }
1806                    } else if (asourcetoken.tokencode == TBaseType.rrw_join) {
1807                        TSourceToken stFull = asourcetoken.searchToken(TBaseType.rrw_full, -1);
1808                        if (stFull != null) {
1809                            stFull.tokencode = TBaseType.RW_FULL2;
1810                        } else {
1811                            TSourceToken stNatural = asourcetoken.searchToken(TBaseType.rrw_natural, -4);
1812                            if (stNatural != null) {
1813                                stNatural.tokencode = TBaseType.RW_NATURAL2;
1814                            }
1815                        }
1816                    } else if (asourcetoken.tokencode == TBaseType.rrw_outer) {
1817                        TSourceToken stFull = asourcetoken.searchToken(TBaseType.rrw_full, -1);
1818                        if (stFull != null) {
1819                            stFull.tokencode = TBaseType.RW_FULL2;
1820                        }
1821                    } else if (asourcetoken.tokencode == TBaseType.rrw_is) {
1822                        TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2);
1823                        if (stType != null) {
1824                            stType.tokencode = TBaseType.rrw_type2;
1825                        }
1826                    } else if (asourcetoken.tokencode == TBaseType.rrw_as) {
1827                        TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2);
1828                        if (stType != null) {
1829                            stType.tokencode = TBaseType.rrw_type2;
1830                        }
1831                    } else if (asourcetoken.tokencode == TBaseType.rrw_oid) {
1832                        TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2);
1833                        if (stType != null) {
1834                            stType.tokencode = TBaseType.rrw_type2;
1835                        }
1836                    } else if (asourcetoken.tokencode == TBaseType.rrw_type) {
1837                        TSourceToken stPrev;
1838                        stPrev = asourcetoken.searchToken(TBaseType.rrw_drop, -1);
1839                        if (stPrev != null) {
1840                            asourcetoken.tokencode = TBaseType.rrw_type2;
1841                        }
1842                        if (asourcetoken.tokencode == TBaseType.rrw_type) {
1843                            stPrev = asourcetoken.searchToken(TBaseType.rrw_of, -1);
1844                            if (stPrev != null) {
1845                                asourcetoken.tokencode = TBaseType.rrw_type2;
1846                            }
1847                        }
1848                        if (asourcetoken.tokencode == TBaseType.rrw_type) {
1849                            stPrev = asourcetoken.searchToken(TBaseType.rrw_create, -1);
1850                            if (stPrev != null) {
1851                                asourcetoken.tokencode = TBaseType.rrw_type2;
1852                            }
1853                        }
1854                        if (asourcetoken.tokencode == TBaseType.rrw_type) {
1855                            stPrev = asourcetoken.searchToken(TBaseType.rrw_replace, -1);
1856                            if (stPrev != null) {
1857                                asourcetoken.tokencode = TBaseType.rrw_type2;
1858                            }
1859                        }
1860                        if (asourcetoken.tokencode == TBaseType.rrw_type) {
1861                            stPrev = asourcetoken.searchToken('%', -1);
1862                            if (stPrev != null) {
1863                                asourcetoken.tokencode = TBaseType.rrw_type2;
1864                            }
1865                        }
1866                    } else if ((asourcetoken.tokencode == TBaseType.rrw_by) || (asourcetoken.tokencode == TBaseType.rrw_to)) {
1867                        lcprevst = getprevsolidtoken(asourcetoken);
1868                        if (lcprevst != null) {
1869                            if ((lcprevst.tokencode == TBaseType.sqlpluscmd) && (lcprevst.toString().equalsIgnoreCase("connect"))) {
1870                                lcprevst.tokencode = TBaseType.rrw_connect;
1871                                lcprevst.tokentype = ETokenType.ttkeyword;
1872                                flexer.insqlpluscmd = false;
1873
1874                                continuesqlplusatnewline = false;
1875                                waitingreturnforsemicolon = false;
1876                                waitingreturnforfloatdiv = false;
1877                                isvalidplace = false;
1878                                insqlpluscmd = false;
1879                            }
1880                        }
1881                    } else if (asourcetoken.tokencode == TBaseType.rrw_with) {
1882                        lcprevst = getprevsolidtoken(asourcetoken);
1883                        if (lcprevst != null) {
1884                            if ((lcprevst.tokencode == TBaseType.sqlpluscmd) && (lcprevst.toString().equalsIgnoreCase("start"))) {
1885                                lcprevst.tokencode = TBaseType.rrw_start;
1886                                lcprevst.tokentype = ETokenType.ttkeyword;
1887                                flexer.insqlpluscmd = false;
1888
1889                                continuesqlplusatnewline = false;
1890                                waitingreturnforsemicolon = false;
1891                                waitingreturnforfloatdiv = false;
1892                                isvalidplace = false;
1893                                insqlpluscmd = false;
1894                            }
1895                        }
1896                    } else if (asourcetoken.tokencode == TBaseType.rrw_set) {
1897                        lcprevst = getprevsolidtoken(asourcetoken);
1898                        if (lcprevst != null) {
1899                            if (lcprevst.getAstext().equalsIgnoreCase("a")) {
1900                                TSourceToken lcpp = getprevsolidtoken(lcprevst);
1901                                if (lcpp != null) {
1902                                    if ((lcpp.tokencode == TBaseType.rrw_not) || (lcpp.tokencode == TBaseType.rrw_is)) {
1903                                        lcprevst.tokencode = TBaseType.rrw_oracle_a_in_aset;
1904                                        asourcetoken.tokencode = TBaseType.rrw_oracle_set_in_aset;
1905                                    }
1906                                }
1907                            }
1908                        }
1909                    }
1910
1911                    break;
1912                }
1913            }
1914
1915            // Get next token
1916            asourcetoken = getanewsourcetoken();
1917            if (asourcetoken != null) {
1918                yychar = asourcetoken.tokencode;
1919
1920                // Handle special case: dot after SQL*Plus commands
1921                if ((asourcetoken.tokencode == '.') && (getprevsolidtoken(asourcetoken) != null)
1922                        && ((currentCmdType == ESqlPlusCmd.spcAppend)
1923                        || (currentCmdType == ESqlPlusCmd.spcChange) || (currentCmdType == ESqlPlusCmd.spcInput)
1924                        || (currentCmdType == ESqlPlusCmd.spcList) || (currentCmdType == ESqlPlusCmd.spcRun))) {
1925                    // a.ent_rp_usr_id is not a real sqlplus command
1926                    TSourceToken lcprevst2 = getprevsolidtoken(asourcetoken);
1927                    lcprevst2.insqlpluscmd = false;
1928                    if (lcprevst2.prevTokenCode != 0) {
1929                        lcprevst2.tokencode = lcprevst2.prevTokenCode;
1930                    } else {
1931                        lcprevst2.tokencode = TBaseType.ident;
1932                    }
1933
1934                    flexer.insqlpluscmd = false;
1935                    continuesqlplusatnewline = false;
1936                    waitingreturnforsemicolon = false;
1937                    waitingreturnforfloatdiv = false;
1938                    isvalidplace = false;
1939                    insqlpluscmd = false;
1940                }
1941            } else {
1942                yychar = 0;
1943
1944                if (waitingreturnforfloatdiv) {
1945                    // / at the end of line treat as sqlplus command
1946                    lct.tokencode = TBaseType.sqlpluscmd;
1947                    if (lct.tokentype != ETokenType.ttslash) {
1948                        lct.tokentype = ETokenType.ttsqlpluscmd;
1949                    }
1950                }
1951            }
1952
1953            if ((yychar == 0) && (prevst != null)) {
1954                if (prevst.tokencode == TBaseType.rrw_inner) {
1955                    prevst.tokencode = TBaseType.ident;
1956                }
1957            }
1958        }
1959    }
1960
1961    // ========== Helper Methods for Tokenization ==========
1962    // These methods support Oracle-specific tokenization logic
1963
1964    /**
1965     * Count number of newlines in a string.
1966     *
1967     * @param s string to analyze
1968     * @return number of line breaks (LF or CR)
1969     */
1970    private int countLines(String s) {
1971        int pos = 0, lf = 0, cr = 0;
1972
1973        while (pos < s.length()) {
1974            if (s.charAt(pos) == '\r') {
1975                cr++;
1976                pos++;
1977                continue;
1978            }
1979            if (s.charAt(pos) == '\n') {
1980                lf++;
1981                pos++;
1982                continue;
1983            }
1984
1985            if (s.charAt(pos) == ' ') {
1986                pos++;
1987                continue;
1988            }
1989            break;
1990        }
1991
1992        if (lf >= cr) return lf;
1993        else return cr;
1994    }
1995
1996    /**
1997     * Check if return token ends with space or tab.
1998     *
1999     * @param s token text
2000     * @return true if ends with space/tab
2001     */
2002    private boolean spaceAtTheEndOfReturnToken(String s) {
2003        if (s == null) return false;
2004        if (s.length() == 0) return false;
2005
2006        return ((s.charAt(s.length() - 1) == ' ') || (s.charAt(s.length() - 1) == '\t'));
2007    }
2008
2009    /**
2010     * Determine if forward slash should be treated as SQL*Plus command delimiter.
2011     * <p>
2012     * Oracle uses '/' as both division operator and SQL*Plus block delimiter.
2013     * This method disambiguates by checking if the '/' appears at the beginning
2014     * of a line (after a return token without trailing whitespace).
2015     *
2016     * @param pstlist token list
2017     * @param pPos position of '/' token
2018     * @return true if '/' should be SQL*Plus command
2019     */
2020    private boolean isValidPlaceForDivToSqlplusCmd(TSourceTokenList pstlist, int pPos) {
2021        boolean ret = false;
2022
2023        if ((pPos <= 0) || (pPos > pstlist.size() - 1)) return ret;
2024
2025        // Token directly before div must be ttreturn without space appending it
2026        gudusoft.gsqlparser.TSourceToken lcst = pstlist.get(pPos - 1);
2027        if (lcst.tokentype != gudusoft.gsqlparser.ETokenType.ttreturn) {
2028            return ret;
2029        }
2030
2031        if (!(lcst.getAstext().charAt(lcst.getAstext().length() - 1) == ' ')) {
2032            ret = true;
2033        }
2034
2035        return ret;
2036    }
2037
2038    /**
2039     * Get previous non-whitespace token.
2040     *
2041     * @param ptoken current token
2042     * @return previous solid token, or null
2043     */
2044    private gudusoft.gsqlparser.TSourceToken getprevsolidtoken(gudusoft.gsqlparser.TSourceToken ptoken) {
2045        gudusoft.gsqlparser.TSourceToken ret = null;
2046        TSourceTokenList lctokenlist = ptoken.container;
2047
2048        if (lctokenlist != null) {
2049            if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) {
2050                if (!(
2051                        (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttwhitespace)
2052                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttreturn)
2053                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttsimplecomment)
2054                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttbracketedcomment)
2055                )) {
2056                    ret = lctokenlist.get(ptoken.posinlist - 1);
2057                } else {
2058                    ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false);
2059                }
2060            }
2061        }
2062        return ret;
2063    }
2064}