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                    // WITH FUNCTION/PROCEDURE can also open the query of a statement that does not
837                    // start with WITH: CREATE VIEW ... AS WITH FUNCTION ..., CREATE TABLE ... AS,
838                    // INSERT ... WITH FUNCTION ... SELECT. The semicolons of the inline PL/SQL belong
839                    // to that statement as well (Mantis 4949).
840                    if ((ast.tokencode == TBaseType.rrw_with) && isInlinePlsqlWith(ast)) {
841                        withPlsqlDefinition = true;
842                        withPlsqlBeginEndNesting = 0;
843                        withPlsqlFoundSelect = false;
844                    }
845
846                    // For WITH FUNCTION/PROCEDURE, track BEGIN/END nesting and when SELECT is found
847                    if (withPlsqlDefinition) {
848                        if (ast.tokencode == TBaseType.rrw_begin) {
849                            withPlsqlBeginEndNesting++;
850                        } else if (ast.tokencode == TBaseType.rrw_end) {
851                            withPlsqlBeginEndNesting--;
852                            if (withPlsqlBeginEndNesting < 0) withPlsqlBeginEndNesting = 0;
853                        } else if (ast.tokencode == TBaseType.rrw_select && withPlsqlBeginEndNesting == 0) {
854                            // Found SELECT after all function definitions are done
855                            withPlsqlFoundSelect = true;
856                        }
857                    }
858
859                    // For CREATE MLE MODULE with AS clause, don't terminate on semicolon
860                    // The JavaScript code may contain semicolons; wait for / to terminate
861                    // For WITH FUNCTION/PROCEDURE, don't terminate on semicolon until SELECT is found
862                    // (the semicolons in function body and after END are part of the function definition)
863                    // CREATE JAVA SOURCE ... AS <java code>: semicolons inside
864                    // the Java class body (brace depth > 0) are Java statement
865                    // terminators, not SQL statement terminators.
866                    if ((gcurrentsqlstatement != null)
867                            && (gcurrentsqlstatement.sqlstatementtype == gudusoft.gsqlparser.ESqlStatementType.sstoraclecreatejava)) {
868                        if ("{".equals(ast.toString())) {
869                            javaBraceNesting++;
870                        } else if ("}".equals(ast.toString()) && (javaBraceNesting > 0)) {
871                            javaBraceNesting--;
872                        }
873                    }
874
875                    boolean skipSemicolonTermination = mleModuleWithAs || (javaBraceNesting > 0) || (withPlsqlDefinition && !withPlsqlFoundSelect);
876                    if (ast.tokentype == ETokenType.ttsemicolon && !skipSemicolonTermination) {
877                        gst = EFindSqlStateType.stnormal;
878                        appendToken(gcurrentsqlstatement, ast);
879                        gcurrentsqlstatement.semicolonended = ast;
880                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
881                        mleModuleWithAs = false; // Reset flag
882                        javaBraceNesting = 0;
883                        withPlsqlDefinition = false; // Reset WITH FUNCTION flag
884                        withPlsqlBeginEndNesting = 0;
885                        cteMainSelectFound = false;
886                        withPlsqlFoundSelect = false;
887                        continue;
888                    }
889
890                    if (sourcetokenlist.sqlplusaftercurtoken()) //most probably is / cmd
891                    {
892                        gst = EFindSqlStateType.stnormal;
893                        appendToken(gcurrentsqlstatement, ast);
894                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
895                        mleModuleWithAs = false; // Reset flag
896                        javaBraceNesting = 0;
897                        continue;
898                    }
899
900                    if (ast.tokencode == '(') nestedParenthesis++;
901                    if (ast.tokencode == ')') {
902                        nestedParenthesis--;
903                        if (nestedParenthesis < 0) nestedParenthesis = 0;
904                    }
905
906                    Boolean findNewStmt = false;
907                    TCustomSqlStatement lcStmt = null;
908                    // Check for new statement: CREATE TABLE (original), or SELECT inside a non-CTE SELECT
909                    boolean shouldCheckNewStmt = false;
910                    if ((nestedParenthesis == 0) && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatetable)) {
911                        shouldCheckNewStmt = true;
912                    } else if ((nestedParenthesis == 0) && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstselect)
913                            && (ast.tokencode == TBaseType.rrw_select || ast.tokencode == TBaseType.rrw_with)) {
914                        // Check if current statement is a CTE (starts with WITH)
915                        boolean isCteContext = false;
916                        for (int si = 0; si < gcurrentsqlstatement.sourcetokenlist.size(); si++) {
917                            TSourceToken st = gcurrentsqlstatement.sourcetokenlist.get(si);
918                            if (st.tokentype == ETokenType.ttwhitespace || st.tokentype == ETokenType.ttreturn
919                                    || st.tokencode == TBaseType.cmtdoublehyphen || st.tokencode == TBaseType.cmtslashstar) {
920                                continue;
921                            }
922                            if (st.tokencode == TBaseType.rrw_with) {
923                                isCteContext = true;
924                            }
925                            break;
926                        }
927                        // Don't split if previous token makes this SELECT part of current statement:
928                        // - Set operators: UNION, INTERSECT, MINUS, EXCEPT, ALL
929                        // - Left paren: (SELECT ...) — SELECT is main query of parenthesized expr
930                        boolean suppressSplit = false;
931                        if (ast.tokencode == TBaseType.rrw_select && lcprevsolidtoken != null) {
932                            int prevCode = lcprevsolidtoken.tokencode;
933                            if (prevCode == TBaseType.rrw_union || prevCode == TBaseType.rrw_intersect
934                                    || prevCode == TBaseType.rrw_minus || prevCode == TBaseType.rrw_except
935                                    || prevCode == TBaseType.rrw_all
936                                    || prevCode == '(') {
937                                suppressSplit = true;
938                            }
939                        }
940                        if (suppressSplit) {
941                            // SELECT is part of current statement — don't split
942                        } else if (!isCteContext) {
943                            // Non-CTE SELECT: any SELECT/WITH at paren level 0 starts a new statement
944                            shouldCheckNewStmt = true;
945                        } else if (cteMainSelectFound) {
946                            // CTE context: main SELECT already consumed, so this SELECT/WITH
947                            // at paren level 0 is a new statement
948                            shouldCheckNewStmt = true;
949                        } else if (ast.tokencode == TBaseType.rrw_select) {
950                            // CTE context: this is the main SELECT after WITH name AS (...)
951                            cteMainSelectFound = true;
952                            // Don't split — this SELECT is part of the CTE statement
953                        }
954                        // If ast is WITH and main SELECT not yet found, it could be another
955                        // CTE definition (WITH a AS (...), b AS (...)) — don't split
956                    }
957                    if (shouldCheckNewStmt) {
958                        // For SELECT-after-SELECT/WITH splitting, use stnormal so issql can detect CTE starts.
959                        // For CREATE TABLE, preserve original stsql state to avoid false positives
960                        // (e.g., INSERT/DELETE keywords in blockchain table clauses).
961                        EFindSqlStateType issqlState = (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstselect)
962                                ? EFindSqlStateType.stnormal : gst;
963                        lcStmt = sqlcmds.issql(ast, issqlState, gcurrentsqlstatement);
964                        if (lcStmt != null) {
965                            findNewStmt = true;
966                            if (lcStmt.sqlstatementtype == ESqlStatementType.sstselect) {
967                                TSourceToken prevst = ast.prevSolidToken();
968                                if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatetable) {
969                                    // For CREATE TABLE, suppress split when SELECT follows AS/(/): AS (SELECT ...)
970                                    if ((prevst.tokencode == TBaseType.rrw_as) || (prevst.tokencode == '(') || (prevst.tokencode == ')')) {
971                                        findNewStmt = false;
972                                    }
973                                }
974                                // For SELECT-after-SELECT/WITH splitting at paren level 0,
975                                // no suppression needed — the new SELECT/WITH is a new statement
976                            }
977                        }
978                    }
979
980                    if (findNewStmt) {
981                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
982                        gcurrentsqlstatement = lcStmt;
983                        cteMainSelectFound = false; // Reset for new statement
984                        nestedParenthesis = 0; // Reset paren tracking for new statement
985                        appendToken(gcurrentsqlstatement, ast);
986                        continue;
987                    } else
988                        appendToken(gcurrentsqlstatement, ast);
989
990                    break;
991                }//case stsql
992
993                case ststoredprocedure: {
994
995                    if (procedure_status[nestedProcedures] != stored_procedure_status.bodyend) {
996                        appendToken(gcurrentsqlstatement, ast);
997                    }
998
999                    switch (procedure_status[nestedProcedures]) {
1000                        case cursor_declare:
1001                            if (ast.tokencode == ';') {
1002                                nestedProcedures--;
1003                                if (nestedProcedures < 0) {
1004                                    nestedProcedures = 0;
1005                                }
1006                            }
1007                            break;
1008                        case start:
1009                            if ((ast.tokencode == TBaseType.rrw_as) || (ast.tokencode == TBaseType.rrw_is)) {
1010                                if (sptype[nestedProcedures] != stored_procedure_type.create_trigger) {
1011                                    if ((sptype[0] == stored_procedure_type.package_spec) && (nestedProcedures > 0)) {
1012                                        // when it's a package specification, only top level accept as/is
1013                                    } else {
1014                                        procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1015                                        if (ast.searchToken("language", 1) != null) {
1016                                            if (nestedProcedures == 0) {
1017                                                gst = EFindSqlStateType.stsql;
1018                                            } else {
1019                                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1020                                                nestedProcedures--;
1021                                            }
1022                                        }
1023                                    }
1024                                }
1025                            } else if (ast.tokencode == TBaseType.rrw_begin) {
1026                                if (sptype[nestedProcedures] == stored_procedure_type.create_trigger) {
1027                                    waitingEnds[nestedProcedures]++;
1028                                }
1029                                if (nestedProcedures > 0) {
1030                                    nestedProcedures--;
1031                                }
1032                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1033                            } else if (ast.tokencode == TBaseType.rrw_end) {
1034                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures - 1] == 1)
1035                                        && ((sptype[nestedProcedures - 1] == stored_procedure_type.package_body)
1036                                        || (sptype[nestedProcedures - 1] == stored_procedure_type.package_spec))) {
1037                                    nestedProcedures--;
1038                                    procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1039                                }
1040                            } else if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) {
1041                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures] == 0)
1042                                        && (procedure_status[nestedProcedures - 1] == stored_procedure_status.is_as)) {
1043                                    nestedProcedures--;
1044                                    nestedProcedures++;
1045                                    waitingEnds[nestedProcedures] = 0;
1046                                    procedure_status[nestedProcedures] = stored_procedure_status.start;
1047                                }
1048                            } else if (ast.tokencode == TBaseType.rrw_oracle_cursor) {
1049                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures] == 0)
1050                                        && (procedure_status[nestedProcedures - 1] == stored_procedure_status.is_as)) {
1051                                    nestedProcedures--;
1052                                    nestedProcedures++;
1053                                    waitingEnds[nestedProcedures] = 0;
1054                                    procedure_status[nestedProcedures] = stored_procedure_status.cursor_declare;
1055                                }
1056                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger) && (ast.tokencode == TBaseType.rrw_declare)) {
1057                                procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1058                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger)
1059                                    && (ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) {
1060                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1061                                gst = EFindSqlStateType.stnormal;
1062                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1063
1064                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1065                                appendToken(gcurrentsqlstatement, ast);
1066                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1067                            } else if (sptype[nestedProcedures] == stored_procedure_type.create_trigger) {
1068                                if (ast.tokencode == TBaseType.rrw_trigger) {
1069                                    TSourceToken compoundSt = ast.searchToken(TBaseType.rrw_oracle_compound, -1);
1070                                    if (compoundSt != null) {
1071                                        procedure_status[nestedProcedures] = stored_procedure_status.body;
1072                                        waitingEnds[nestedProcedures]++;
1073                                    }
1074                                }
1075                            } else if ((sptype[nestedProcedures] == stored_procedure_type.function)
1076                                    && (ast.tokencode == TBaseType.rrw_teradata_using)) {
1077                                if ((ast.searchToken("aggregate", -1) != null) || (ast.searchToken("pipelined", -1) != null)) {
1078                                    if (nestedProcedures == 0) {
1079                                        gst = EFindSqlStateType.stsql;
1080                                    } else {
1081                                        procedure_status[nestedProcedures] = stored_procedure_status.body;
1082                                        nestedProcedures--;
1083                                    }
1084                                }
1085                            }
1086                            break;
1087                        case is_as:
1088                            if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) {
1089                                nestedProcedures++;
1090                                if (nestedProcedures > stored_procedure_nested_level - 1) {
1091                                    gst = EFindSqlStateType.sterror;
1092                                    nestedProcedures--;
1093                                } else {
1094                                    waitingEnds[nestedProcedures] = 0;
1095                                    procedure_status[nestedProcedures] = stored_procedure_status.start;
1096                                }
1097                            } else if (ast.tokencode == TBaseType.rrw_begin) {
1098                                if ((nestedProcedures == 0) &&
1099                                        ((sptype[nestedProcedures] == stored_procedure_type.package_body)
1100                                                || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) {
1101                                    // top level package begin already counted
1102                                } else {
1103                                    waitingEnds[nestedProcedures]++;
1104                                }
1105                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1106                            } else if (ast.tokencode == TBaseType.rrw_end) {
1107                                if ((nestedProcedures == 0) && (waitingEnds[nestedProcedures] == 1)
1108                                        && ((sptype[nestedProcedures] == stored_procedure_type.package_body)
1109                                        || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) {
1110                                    procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1111                                    waitingEnds[nestedProcedures]--;
1112                                } else {
1113                                    waitingEnds[nestedProcedures]--;
1114                                }
1115                            } else if (ast.tokencode == TBaseType.rrw_case) {
1116                                if (ast.searchToken(';', 1) == null) {
1117                                    waitingEnds[nestedProcedures]++;
1118                                }
1119                            }
1120                            break;
1121                        case body:
1122                            if (ast.tokencode == TBaseType.rrw_begin) {
1123                                waitingEnds[nestedProcedures]++;
1124                            } else if (ast.tokencode == TBaseType.rrw_if) {
1125                                if (ast.searchToken(';', 2) == null) {
1126                                    waitingEnds[nestedProcedures]++;
1127                                }
1128                            } else if (ast.tokencode == TBaseType.rrw_case) {
1129                                if (ast.searchToken(';', 2) == null) {
1130                                    if (ast.searchToken(TBaseType.rrw_end, -1) == null) {
1131                                        waitingEnds[nestedProcedures]++;
1132                                    }
1133                                }
1134                            } else if (ast.tokencode == TBaseType.rrw_loop) {
1135                                if (!((ast.searchToken(TBaseType.rrw_end, -1) != null)
1136                                        && (ast.searchToken(';', 2) != null))) {
1137                                    waitingEnds[nestedProcedures]++;
1138                                }
1139                            } else if (ast.tokencode == TBaseType.rrw_end) {
1140                                waitingEnds[nestedProcedures]--;
1141                                if (waitingEnds[nestedProcedures] == 0) {
1142                                    if (nestedProcedures == 0) {
1143                                        procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1144                                    } else {
1145                                        nestedProcedures--;
1146                                        procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1147                                    }
1148                                }
1149                            } else if ((waitingEnds[nestedProcedures] == 0)
1150                                    && (ast.tokentype == ETokenType.ttslash)
1151                                    && (ast.tokencode == TBaseType.sqlpluscmd)) {
1152                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1153                                gst = EFindSqlStateType.stnormal;
1154                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1155
1156                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1157                                appendToken(gcurrentsqlstatement, ast);
1158                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1159                            }
1160                            break;
1161                        case bodyend:
1162                            if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) {
1163                                // TPlsqlStatementParse(asqlstatement).TerminatorToken := ast;
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 / 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.tokentype == ETokenType.ttperiod) && (sourcetokenlist.returnaftercurtoken(false)) && (sourcetokenlist.returnbeforecurtoken(false))) {
1173                                // single dot at a seperate line
1174                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1175                                gst = EFindSqlStateType.stnormal;
1176                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1177
1178                                //make ttperiod a sqlplus cmd
1179                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1180                                appendToken(gcurrentsqlstatement, ast);
1181                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1182                            } else if ((ast.searchToken(TBaseType.rrw_package, 1) != null) && (!endBySlashOnly)) {
1183                                appendToken(gcurrentsqlstatement, ast);
1184                                gst = EFindSqlStateType.stnormal;
1185                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1186                            } else if ((ast.searchToken(TBaseType.rrw_procedure, 1) != null) && (!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_function, 1) != null) && (!endBySlashOnly)) {
1191                                appendToken(gcurrentsqlstatement, ast);
1192                                gst = EFindSqlStateType.stnormal;
1193                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1194                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null)
1195                                    && ((ast.searchToken(TBaseType.rrw_package, 4) != null) || (ast.searchToken(TBaseType.rrw_package, 5) != 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)
1201                                    && ((ast.searchToken(TBaseType.rrw_procedure, 4) != null)
1202                                            || (ast.searchToken(TBaseType.rrw_function, 4) != null)
1203                                            || (ast.searchToken(TBaseType.rrw_view, 4) != null)
1204                                            || (ast.searchToken(TBaseType.rrw_oracle_synonym, 4) != null)
1205                                            || (ast.searchToken(TBaseType.rrw_trigger, 4) != null))
1206                                    && (!endBySlashOnly)) {
1207                                appendToken(gcurrentsqlstatement, ast);
1208                                gst = EFindSqlStateType.stnormal;
1209                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1210                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) && (ast.searchToken(TBaseType.rrw_library, 4) != null) && (!endBySlashOnly)) {
1211                                appendToken(gcurrentsqlstatement, ast);
1212                                gst = EFindSqlStateType.stnormal;
1213                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1214                            } else if ((ast.searchToken(TBaseType.rrw_alter, 1) != null) && (ast.searchToken(TBaseType.rrw_trigger, 2) != null) && (!endBySlashOnly)) {
1215                                appendToken(gcurrentsqlstatement, ast);
1216                                gst = EFindSqlStateType.stnormal;
1217                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1218                            } else if ((ast.searchToken(TBaseType.rrw_select, 1) != null) && (!endBySlashOnly)) {
1219                                appendToken(gcurrentsqlstatement, ast);
1220                                gst = EFindSqlStateType.stnormal;
1221                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1222                            } else if ((ast.searchToken(TBaseType.rrw_call, 1) != null) && (!endBySlashOnly)) {
1223                                appendToken(gcurrentsqlstatement, ast);
1224                                gst = EFindSqlStateType.stnormal;
1225                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1226                            } else if ((ast.searchToken(TBaseType.rrw_commit, 1) != null) && (!endBySlashOnly)) {
1227                                appendToken(gcurrentsqlstatement, ast);
1228                                gst = EFindSqlStateType.stnormal;
1229                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1230                            } else if ((ast.searchToken(TBaseType.rrw_declare, 1) != 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 if ((ast.searchToken(TBaseType.rrw_grant, 1) != null)
1235                                    && (ast.searchToken(TBaseType.rrw_execute, 2) != null) && (!endBySlashOnly)) {
1236                                appendToken(gcurrentsqlstatement, ast);
1237                                gst = EFindSqlStateType.stnormal;
1238                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1239                            } else if ((ast.searchToken(TBaseType.rrw_alter, 1) != null)
1240                                    && (ast.searchToken(TBaseType.rrw_table, 2) != null) && (!endBySlashOnly)) {
1241                                appendToken(gcurrentsqlstatement, ast);
1242                                gst = EFindSqlStateType.stnormal;
1243                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, false, builder);
1244                            } else {
1245                                appendToken(gcurrentsqlstatement, ast);
1246                            }
1247                            break;
1248                        case end:
1249                            break;
1250                        default:
1251                            break;
1252                    }
1253
1254                    if (ast.tokencode == TBaseType.sqlpluscmd) {
1255                        int m = flexer.getkeywordvalue(ast.getAstext());
1256                        if (m != 0) {
1257                            ast.tokencode = m;
1258                        } else if (ast.tokentype == ETokenType.ttslash) {
1259                            ast.tokencode = '/';
1260                        } else {
1261                            ast.tokencode = TBaseType.ident;
1262                        }
1263                    }
1264
1265                    final int wrapped_keyword_max_pos = 20;
1266                    if ((ast.tokencode == TBaseType.rrw_wrapped)
1267                            && (ast.posinlist - gcurrentsqlstatement.sourcetokenlist.get(0).posinlist < wrapped_keyword_max_pos)) {
1268                        if (gcurrentsqlstatement instanceof gudusoft.gsqlparser.stmt.TCommonStoredProcedureSqlStatement) {
1269                            ((gudusoft.gsqlparser.stmt.TCommonStoredProcedureSqlStatement) gcurrentsqlstatement).setWrapped(true);
1270                        }
1271
1272                        if (gcurrentsqlstatement instanceof gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage) {
1273                            if (ast.prevSolidToken() != null) {
1274                                ((gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage) gcurrentsqlstatement)
1275                                        .setPackageName(fparser.getNf().createObjectNameWithPart(ast.prevSolidToken()));
1276                            }
1277                        }
1278                    }
1279
1280                    break;
1281                } //ststoredprocedure
1282
1283            } //switch
1284        }//for
1285
1286        //last statement
1287        if ((gcurrentsqlstatement != null) &&
1288                ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) ||
1289                        (gst == EFindSqlStateType.sterror))) {
1290            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, this.fplsqlparser, this.sqlstatements, true, builder);
1291        }
1292
1293        // Populate builder with results
1294        builder.sqlStatements(this.sqlstatements);
1295        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
1296                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
1297        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
1298        builder.errorMessage(syntaxErrors.isEmpty() ? "" :
1299                String.format("Raw extraction completed with %d error(s)", syntaxErrors.size()));
1300    }
1301
1302    /**
1303     * True when a WITH inside a statement opens inline PL/SQL declarations (Mantis 4949): it has
1304     * the routine shape {@code WITH FUNCTION name (} or {@code WITH FUNCTION name RETURN}, or
1305     * {@code WITH PROCEDURE name} followed by {@code (}, {@code IS} or {@code AS}. FUNCTION and
1306     * PROCEDURE are legal column and CTE names, so {@code START WITH function = 1} and
1307     * {@code WITH function AS (...)} are not inline PL/SQL.
1308     */
1309    private static boolean isInlinePlsqlWith(TSourceToken withToken) {
1310        TSourceToken prev = withToken.prevSolidToken();
1311        if ((prev != null) && (prev.tokencode == TBaseType.rrw_start)) return false;
1312        TSourceToken routine = withToken.nextSolidToken();
1313        if (routine == null) return false;
1314        boolean isFunction = (routine.tokencode == TBaseType.rrw_function);
1315        if (!isFunction && (routine.tokencode != TBaseType.rrw_procedure)) return false;
1316        TSourceToken name = routine.nextSolidToken();
1317        if (name == null) return false;
1318        if ((name.tokentype != ETokenType.ttidentifier) && (name.tokentype != ETokenType.ttdqstring)
1319                && (name.tokentype != ETokenType.ttkeyword)) return false;
1320        if ((name.tokencode == TBaseType.rrw_as) || (name.tokencode == TBaseType.rrw_is)
1321                || (name.tokencode == TBaseType.rrw_return)) return false;
1322        TSourceToken next = name.nextSolidToken();
1323        if (next == null) return false;
1324        if (next.tokencode == '(') return true;
1325        if (isFunction) return (next.tokencode == TBaseType.rrw_return);
1326        return (next.tokencode == TBaseType.rrw_is) || (next.tokencode == TBaseType.rrw_as);
1327    }
1328
1329    /**
1330     * Handle token transformations during raw statement extraction.
1331     * <p>
1332     * This performs Oracle-specific keyword disambiguation that must happen
1333     * before statement boundary detection. Examples:
1334     * <ul>
1335     *   <li>RETURN after WHERE → treat as identifier</li>
1336     *   <li>VALUE after BY → mark as value_after_by</li>
1337     *   <li>NEW → treat as identifier or constructor based on context</li>
1338     *   <li>And many more Oracle-specific cases</li>
1339     * </ul>
1340     *
1341     * @param ast current token being processed
1342     */
1343    private void performRawStatementTokenTransformations(TSourceToken ast) {
1344        // This method contains the keyword transformation logic from dooraclegetrawsqlstatements
1345        // It's been extracted to keep the main method more readable
1346
1347        if (ast.tokencode == TBaseType.rrw_return) {
1348            TSourceToken stMatch = ast.searchToken(TBaseType.rrw_where, 1);
1349            if (stMatch != null) {
1350                ast.tokencode = TBaseType.ident;
1351            }
1352        } else if (ast.tokencode == TBaseType.rrw_value_oracle) {
1353            TSourceToken stBy = ast.searchToken(TBaseType.rrw_by, -1);
1354            if (stBy != null) {
1355                ast.tokencode = TBaseType.rrw_value_after_by;
1356            }
1357        } else if (ast.tokencode == TBaseType.rrw_new_oracle) {
1358            TSourceToken stRightParen = ast.searchToken(')', -1);
1359            if (stRightParen != null) {
1360                ast.tokencode = TBaseType.ident;
1361            }
1362            TSourceToken stDot = ast.searchToken('.', 1);
1363            if (stDot != null) {
1364                ast.tokencode = TBaseType.ident;
1365            }
1366
1367            TSourceToken stNext = ast.searchTokenAfterObjectName();
1368            stDot = ast.searchToken('.', 1);
1369            if ((stDot == null) && (stNext != null) && (stNext.tokencode == '(')) {
1370                ast.tokencode = TBaseType.rrw_oracle_new_constructor;
1371            }
1372        } else if (ast.tokencode == TBaseType.rrw_chr_oracle) {
1373            TSourceToken stLeftParen = ast.searchToken('(', 1);
1374            if (stLeftParen == null) {
1375                ast.tokencode = TBaseType.ident;
1376            }
1377        } else if (ast.tokencode == TBaseType.rrw_log_oracle) {
1378            TSourceToken stNext = ast.searchToken(TBaseType.rrw_errors_oracle, 1);
1379            TSourceToken stPrev = ast.searchToken(TBaseType.rrw_view, -1);
1380            if (stPrev == null) {
1381                stPrev = ast.searchToken(TBaseType.rrw_oracle_supplemental, -1);
1382            }
1383            if ((stNext == null) && (stPrev == null)) {
1384                ast.tokencode = TBaseType.ident;
1385            }
1386        } else if (ast.tokencode == TBaseType.rrw_delete) {
1387            TSourceToken stPrev = ast.searchToken('.', -1);
1388            if (stPrev != null) {
1389                ast.tokencode = TBaseType.ident;
1390            }
1391        } else if (ast.tokencode == TBaseType.rrw_partition) {
1392            TSourceToken stPrev = ast.searchToken(TBaseType.rrw_add, -1);
1393            if (stPrev != null) {
1394                stPrev.tokencode = TBaseType.rrw_add_p;
1395            }
1396        } else if (ast.tokencode == TBaseType.rrw_oracle_column) {
1397            TSourceToken stPrev = ast.searchToken(TBaseType.rrw_oracle_modify, -1);
1398            if (stPrev != null) {
1399                ast.tokencode = TBaseType.rrw_oracle_column_after_modify;
1400            }
1401        } else if (ast.tokencode == TBaseType.rrw_oracle_apply) {
1402            TSourceToken stPrev = ast.searchToken(TBaseType.rrw_outer, -1);
1403            if (stPrev != null) {
1404                stPrev.tokencode = TBaseType.ORACLE_OUTER2;
1405            }
1406        } else if (ast.tokencode == TBaseType.rrw_oracle_subpartition) {
1407            TSourceToken stNext = ast.searchToken("(", 2);
1408            if (stNext != null) {
1409                TSourceToken st1 = ast.nextSolidToken();
1410                if (st1.toString().equalsIgnoreCase("template")) {
1411                    // don't change, keep as RW_SUBPARTITION
1412                } else {
1413                    ast.tokencode = TBaseType.rrw_oracle_subpartition_tablesample;
1414                }
1415            }
1416        } else if (ast.tokencode == TBaseType.rrw_primary) {
1417            TSourceToken stNext = ast.searchToken("key", 1);
1418            if (stNext == null) {
1419                ast.tokencode = TBaseType.ident;
1420            }
1421        } else if (ast.tokencode == TBaseType.rrw_oracle_offset) {
1422            TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_row, 2);
1423            if (stNext == null) {
1424                stNext = ast.searchToken(TBaseType.rrw_oracle_rows, 2);
1425            }
1426            if (stNext == null) {
1427                stNext = searchRowAfterOffsetExpression(ast);
1428            }
1429            if (stNext != null) {
1430                ast.tokencode = TBaseType.rrw_oracle_offset_row;
1431            }
1432        } else if (ast.tokencode == TBaseType.rrw_translate) {
1433            TSourceToken stNext = ast.searchToken("(", 2);
1434            if (stNext == null) {
1435                ast.tokencode = TBaseType.ident;
1436            }
1437        } else if (ast.tokencode == TBaseType.rrw_constraint) {
1438            TSourceToken stNext = ast.nextSolidToken();
1439            if (stNext == null) {
1440                ast.tokencode = TBaseType.ident;
1441            } else {
1442                if (stNext.tokencode != TBaseType.ident) {
1443                    ast.tokencode = TBaseType.ident;
1444                }
1445            }
1446        } else if (ast.tokencode == TBaseType.rrw_oracle_without) {
1447            TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_count, 1);
1448            if (stNext != null) {
1449                ast.tokencode = TBaseType.rrw_oracle_without_before_count;
1450            }
1451        } else if (ast.tokencode == TBaseType.rrw_bulk) {
1452            TSourceToken stNext = ast.searchToken(TBaseType.rrw_oracle_collect, 1);
1453            if (stNext == null) {
1454                ast.tokencode = TBaseType.ident;
1455            }
1456        } else if (ast.tokencode == TBaseType.rrw_oracle_model) {
1457            TSourceToken stNext = ast.nextSolidToken();
1458            if (stNext != null) {
1459                switch (stNext.toString().toUpperCase(java.util.Locale.ROOT)) {
1460                    case "RETURN":
1461                    case "REFERENCE":
1462                    case "IGNORE":
1463                    case "KEEP":
1464                    case "UNIQUE":
1465                    case "PARTITION":
1466                    case "DIMENSION":
1467                    case "MEASURES":
1468                    case "RULES":
1469                        ast.tokencode = TBaseType.rrw_oracle_model_in_model_clause;
1470                        break;
1471                    default:
1472                        ;
1473                }
1474            }
1475        }
1476    }
1477
1478    /**
1479     * Look for the ROW/ROWS keyword terminating an OFFSET clause whose value is
1480     * a multi-token arithmetic expression, e.g. <code>OFFSET 1 * 10 ROWS</code>
1481     * (Mantis 4614). The single-token case is handled by the caller via
1482     * {@link TSourceToken#searchToken(int, int)}; this scan only accepts a
1483     * strictly alternating operand/operator token run (with balanced
1484     * parentheses), so a bare <code>OFFSET</code> used as a column name is not
1485     * rewritten.
1486     *
1487     * @param offsetToken the OFFSET keyword token
1488     * @return the terminating ROW/ROWS token, or null if the following tokens
1489     *         do not form an arithmetic expression ended by ROW/ROWS
1490     */
1491    private TSourceToken searchRowAfterOffsetExpression(TSourceToken offsetToken) {
1492        // generous cap: only bounds runaway scans on token runs that never hit
1493        // ROW/ROWS; any realistic offset expression is far shorter (a tight cap
1494        // would reject valid long expressions before the grammar sees them)
1495        final int maxTokens = 512;
1496        TSourceToken t = offsetToken.nextSolidToken();
1497        int parenDepth = 0;
1498        boolean expectOperand = true;
1499        int steps = 0;
1500        while (t != null && steps++ < maxTokens) {
1501            String text = t.toString();
1502            if (expectOperand) {
1503                if (text.equals("(")) {
1504                    parenDepth++;
1505                } else if (text.equals("+") || text.equals("-")) {
1506                    // unary sign, keep expecting an operand
1507                } else if (t.tokencode == TBaseType.iconst
1508                        || t.tokencode == TBaseType.fconst
1509                        || t.tokencode == TBaseType.ident
1510                        || text.startsWith(":")) {
1511                    expectOperand = false;
1512                } else {
1513                    return null;
1514                }
1515            } else {
1516                if (parenDepth == 0
1517                        && (t.tokencode == TBaseType.rrw_oracle_row || t.tokencode == TBaseType.rrw_oracle_rows)) {
1518                    return t;
1519                }
1520                if (text.equals(")")) {
1521                    if (--parenDepth < 0) {
1522                        return null;
1523                    }
1524                } else if (text.equals("+") || text.equals("-") || text.equals("*") || text.equals("/")) {
1525                    expectOperand = true;
1526                } else {
1527                    return null;
1528                }
1529            }
1530            t = t.nextSolidToken();
1531        }
1532        return null;
1533    }
1534
1535    private void appendToken(TCustomSqlStatement statement, TSourceToken token) {
1536        if (statement == null || token == null) {
1537            return;
1538        }
1539        token.stmt = statement;
1540        statement.sourcetokenlist.add(token);
1541    }
1542
1543    // ========== Error Handling and Recovery ==========
1544
1545    /**
1546     * Find all syntax errors in PL/SQL statements recursively.
1547     * Extracted from TGSqlParser.findAllSyntaxErrorsInPlsql().
1548     */
1549    private void findAllSyntaxErrorsInPlsql(TCustomSqlStatement psql) {
1550        if (psql.getErrorCount() > 0) {
1551            copyErrorsFromStatement(psql);
1552        }
1553
1554        for (int k = 0; k < psql.getStatements().size(); k++) {
1555            findAllSyntaxErrorsInPlsql(psql.getStatements().get(k));
1556        }
1557    }
1558
1559    /**
1560     * Handle error recovery for CREATE TABLE/INDEX statements.
1561     * Oracle allows table properties that may not be fully parsed.
1562     * This method marks unparseable properties as SQL*Plus commands to skip them.
1563     *
1564     * <p>Extracted from TGSqlParser.doparse() lines 16916-16971
1565     */
1566    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
1567        if (((stmt.sqlstatementtype == ESqlStatementType.sstcreatetable) ||
1568             (stmt.sqlstatementtype == ESqlStatementType.sstcreateindex)) &&
1569            (!TBaseType.c_createTableStrictParsing)) {
1570
1571            // Find the closing parenthesis of table definition
1572            int nested = 0;
1573            boolean isIgnore = false, isFoundIgnoreToken = false;
1574            TSourceToken firstIgnoreToken = null;
1575
1576            for (int k = 0; k < stmt.sourcetokenlist.size(); k++) {
1577                TSourceToken st = stmt.sourcetokenlist.get(k);
1578
1579                if (isIgnore) {
1580                    if (st.issolidtoken() && (st.tokencode != ';')) {
1581                        isFoundIgnoreToken = true;
1582                        if (firstIgnoreToken == null) {
1583                            firstIgnoreToken = st;
1584                        }
1585                    }
1586                    if (st.tokencode != ';') {
1587                        st.tokencode = TBaseType.sqlpluscmd;
1588                    }
1589                    continue;
1590                }
1591
1592                if (st.tokencode == (int) ')') {
1593                    nested--;
1594                    if (nested == 0) {
1595                        // Check if next token is "AS ( SELECT"
1596                        boolean isSelect = false;
1597                        TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1);
1598                        if (st1 != null) {
1599                            TSourceToken st2 = st.searchToken((int) '(', 2);
1600                            if (st2 != null) {
1601                                TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3);
1602                                isSelect = (st3 != null);
1603                            }
1604                        }
1605                        if (!isSelect) isIgnore = true;
1606                    }
1607                }
1608
1609                if ((st.tokencode == (int) '(') || (st.tokencode == TBaseType.left_parenthesis_2)) {
1610                    nested++;
1611                }
1612            }
1613
1614            // Verify it's a valid Oracle table property
1615            if ((firstIgnoreToken != null) &&
1616                (!TBaseType.searchOracleTablePros(firstIgnoreToken.toString()))) {
1617                // Not a valid property, keep the error
1618                isFoundIgnoreToken = false;
1619            }
1620
1621            // Retry parsing if we found ignoreable properties
1622            if (isFoundIgnoreToken) {
1623                stmt.clearError();
1624                stmt.parsestatement(null, false);
1625            }
1626        }
1627    }
1628
1629    /**
1630     * Copy syntax errors from a statement to our error list.
1631     * Extracted from TGSqlParser.copyerrormsg().
1632     */
1633
1634    @Override
1635    public String toString() {
1636        return "OracleSqlParser{vendor=" + vendor + "}";
1637    }
1638
1639    // ========== Main Oracle Tokenization ==========
1640    // Core tokenization logic extracted from TGSqlParser.dooraclesqltexttotokenlist()
1641
1642    /**
1643     * Perform Oracle-specific tokenization with SQL*Plus command detection.
1644     * <p>
1645     * This method implements Oracle's complex tokenization rules including:
1646     * <ul>
1647     *   <li>SQL*Plus command detection (SPOOL, SET, START, etc.)</li>
1648     *   <li>Forward slash disambiguation (division vs PL/SQL delimiter)</li>
1649     *   <li>Oracle-specific keyword transformations (INNER, TYPE, FULL, etc.)</li>
1650     *   <li>Context-dependent token code modifications</li>
1651     * </ul>
1652     *
1653     * <p><b>State Machine:</b> Uses 5 boolean flags to track tokenization state:
1654     * <ul>
1655     *   <li>{@code insqlpluscmd} - Currently inside SQL*Plus command</li>
1656     *   <li>{@code isvalidplace} - Valid place to start SQL*Plus command</li>
1657     *   <li>{@code waitingreturnforfloatdiv} - Slash seen, waiting for newline</li>
1658     *   <li>{@code waitingreturnforsemicolon} - Semicolon seen, waiting for newline</li>
1659     *   <li>{@code continuesqlplusatnewline} - SQL*Plus command continues to next line</li>
1660     * </ul>
1661     *
1662     * <p><b>Extracted from:</b> TGSqlParser.dooraclesqltexttotokenlist() (lines 3931-4298)
1663     *
1664     * @throws RuntimeException if tokenization fails
1665     */
1666    private void dooraclesqltexttotokenlist() {
1667        // Initialize state machine for SQL*Plus command detection
1668        insqlpluscmd = false;
1669        javaBraceNesting = 0;
1670        isvalidplace = true;
1671        waitingreturnforfloatdiv = false;
1672        waitingreturnforsemicolon = false;
1673        continuesqlplusatnewline = false;
1674
1675        ESqlPlusCmd currentCmdType = ESqlPlusCmd.spcUnknown;
1676
1677        TSourceToken lct = null, prevst = null;
1678
1679        TSourceToken asourcetoken, lcprevst;
1680        int yychar;
1681
1682        asourcetoken = getanewsourcetoken();
1683        if (asourcetoken == null) return;
1684        yychar = asourcetoken.tokencode;
1685
1686        while (yychar > 0) {
1687            sourcetokenlist.add(asourcetoken);
1688
1689            switch (yychar) {
1690                case TBaseType.cmtdoublehyphen:
1691                case TBaseType.cmtslashstar:
1692                case TBaseType.lexspace: {
1693                    if (insqlpluscmd) {
1694                        asourcetoken.insqlpluscmd = true;
1695                    }
1696                    break;
1697                }
1698
1699                case TBaseType.lexnewline: {
1700                    if (insqlpluscmd) {
1701                        insqlpluscmd = false;
1702                        isvalidplace = true;
1703
1704                        if (continuesqlplusatnewline) {
1705                            insqlpluscmd = true;
1706                            isvalidplace = false;
1707                            asourcetoken.insqlpluscmd = true;
1708                        }
1709
1710                        if (!insqlpluscmd) {
1711                            currentCmdType = ESqlPlusCmd.spcUnknown;
1712                        }
1713                    }
1714
1715                    if (waitingreturnforsemicolon) {
1716                        isvalidplace = true;
1717                    }
1718
1719                    if (waitingreturnforfloatdiv) {
1720                        isvalidplace = true;
1721                        lct.tokencode = TBaseType.sqlpluscmd;
1722                        if (lct.tokentype != ETokenType.ttslash) {
1723                            lct.tokentype = ETokenType.ttsqlpluscmd;
1724                        }
1725                    }
1726
1727                    if (countLines(asourcetoken.toString()) > 1) {
1728                        // There is a line after select, so spool is the right place to start a sqlplus command
1729                        isvalidplace = true;
1730                    }
1731
1732                    flexer.insqlpluscmd = insqlpluscmd;
1733                    break;
1734                }
1735
1736                default: {
1737                    // Solid token
1738                    // Save semicolon flag before clearing: slash after semicolon on
1739                    // the same line (e.g. "END; /") should be a SQL*Plus delimiter,
1740                    // not division.
1741                    boolean prevWasSemicolon = waitingreturnforsemicolon;
1742                    continuesqlplusatnewline = false;
1743                    waitingreturnforsemicolon = false;
1744                    waitingreturnforfloatdiv = false;
1745
1746                    if (insqlpluscmd) {
1747                        asourcetoken.insqlpluscmd = true;
1748                        if (asourcetoken.toString().equalsIgnoreCase("-")) {
1749                            continuesqlplusatnewline = true;
1750                        }
1751                    } else {
1752                        if (asourcetoken.tokentype == ETokenType.ttsemicolon) {
1753                            waitingreturnforsemicolon = true;
1754                        }
1755
1756                        if ((asourcetoken.tokentype == ETokenType.ttslash)
1757                                && (isvalidplace || prevWasSemicolon || (isValidPlaceForDivToSqlplusCmd(sourcetokenlist, asourcetoken.posinlist)))) {
1758                            lct = asourcetoken;
1759                            waitingreturnforfloatdiv = true;
1760                        }
1761
1762                        currentCmdType = TSqlplusCmdStatement.searchCmd(asourcetoken.toString(), asourcetoken.nextToken());
1763                        if (currentCmdType != ESqlPlusCmd.spcUnknown) {
1764                            if (isvalidplace) {
1765                                TSourceToken lnbreak = null;
1766                                boolean aRealSqlplusCmd = true;
1767                                if (sourcetokenlist.curpos > 0) {
1768                                    lnbreak = sourcetokenlist.get(sourcetokenlist.curpos - 1);
1769                                    aRealSqlplusCmd = !spaceAtTheEndOfReturnToken(lnbreak.toString());
1770                                }
1771
1772                                if (aRealSqlplusCmd) {
1773                                    asourcetoken.prevTokenCode = asourcetoken.tokencode;
1774                                    asourcetoken.tokencode = TBaseType.sqlpluscmd;
1775                                    if (asourcetoken.tokentype != ETokenType.ttslash) {
1776                                        asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
1777                                    }
1778                                    insqlpluscmd = true;
1779                                    flexer.insqlpluscmd = insqlpluscmd;
1780                                }
1781                            } else if ((asourcetoken.tokencode == TBaseType.rrw_connect) && (sourcetokenlist.returnbeforecurtoken(true))) {
1782                                asourcetoken.tokencode = TBaseType.sqlpluscmd;
1783                                if (asourcetoken.tokentype != ETokenType.ttslash) {
1784                                    asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
1785                                }
1786                                insqlpluscmd = true;
1787                                flexer.insqlpluscmd = insqlpluscmd;
1788                            } else if (sourcetokenlist.returnbeforecurtoken(true)) {
1789                                TSourceToken lnbreak = sourcetokenlist.get(sourcetokenlist.curpos - 1);
1790
1791                                if ((countLines(lnbreak.toString()) > 1) && (!spaceAtTheEndOfReturnToken(lnbreak.toString()))) {
1792                                    asourcetoken.tokencode = TBaseType.sqlpluscmd;
1793                                    if (asourcetoken.tokentype != ETokenType.ttslash) {
1794                                        asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
1795                                    }
1796                                    insqlpluscmd = true;
1797                                    flexer.insqlpluscmd = insqlpluscmd;
1798                                }
1799                            }
1800                        }
1801                    }
1802
1803                    isvalidplace = false;
1804
1805                    // Oracle-specific keyword handling (inline to match legacy behavior)
1806                    if (prevst != null) {
1807                        if (prevst.tokencode == TBaseType.rrw_inner) {
1808                            if (asourcetoken.tokencode != flexer.getkeywordvalue("JOIN")) {
1809                                prevst.tokencode = TBaseType.ident;
1810                            }
1811                        } else if ((prevst.tokencode == TBaseType.rrw_not)
1812                                && (asourcetoken.tokencode == flexer.getkeywordvalue("DEFERRABLE"))) {
1813                            prevst.tokencode = flexer.getkeywordvalue("NOT_DEFERRABLE");
1814                        }
1815                    }
1816
1817                    if (asourcetoken.tokencode == TBaseType.rrw_inner) {
1818                        prevst = asourcetoken;
1819                    } else if (asourcetoken.tokencode == TBaseType.rrw_not) {
1820                        prevst = asourcetoken;
1821                    } else {
1822                        prevst = null;
1823                    }
1824
1825                    // Oracle keyword transformations that rely on prev token state
1826                    if ((asourcetoken.tokencode == flexer.getkeywordvalue("DIRECT_LOAD"))
1827                            || (asourcetoken.tokencode == flexer.getkeywordvalue("ALL"))) {
1828                        lcprevst = getprevsolidtoken(asourcetoken);
1829                        if (lcprevst != null) {
1830                            if (lcprevst.tokencode == TBaseType.rrw_for)
1831                                lcprevst.tokencode = TBaseType.rw_for1;
1832                        }
1833                    } else if (asourcetoken.tokencode == TBaseType.rrw_dense_rank) {
1834                        TSourceToken stKeep = asourcetoken.searchToken(TBaseType.rrw_keep, -2);
1835                        if (stKeep != null) {
1836                            stKeep.tokencode = TBaseType.rrw_keep_before_dense_rank;
1837                        }
1838                    } else if (asourcetoken.tokencode == TBaseType.rrw_full) {
1839                        TSourceToken stMatch = asourcetoken.searchToken(TBaseType.rrw_match, -1);
1840                        if (stMatch != null) {
1841                            asourcetoken.tokencode = TBaseType.RW_FULL2;
1842                        }
1843                    } else if (asourcetoken.tokencode == TBaseType.rrw_join) {
1844                        TSourceToken stFull = asourcetoken.searchToken(TBaseType.rrw_full, -1);
1845                        if (stFull != null) {
1846                            stFull.tokencode = TBaseType.RW_FULL2;
1847                        } else {
1848                            TSourceToken stNatural = asourcetoken.searchToken(TBaseType.rrw_natural, -4);
1849                            if (stNatural != null) {
1850                                stNatural.tokencode = TBaseType.RW_NATURAL2;
1851                            }
1852                        }
1853                    } else if (asourcetoken.tokencode == TBaseType.rrw_outer) {
1854                        TSourceToken stFull = asourcetoken.searchToken(TBaseType.rrw_full, -1);
1855                        if (stFull != null) {
1856                            stFull.tokencode = TBaseType.RW_FULL2;
1857                        }
1858                    } else if (asourcetoken.tokencode == TBaseType.rrw_is) {
1859                        TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2);
1860                        if (stType != null) {
1861                            stType.tokencode = TBaseType.rrw_type2;
1862                        }
1863                    } else if (asourcetoken.tokencode == TBaseType.rrw_as) {
1864                        TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2);
1865                        if (stType != null) {
1866                            stType.tokencode = TBaseType.rrw_type2;
1867                        }
1868                    } else if (asourcetoken.tokencode == TBaseType.rrw_oid) {
1869                        TSourceToken stType = asourcetoken.searchToken(TBaseType.rrw_type, -2);
1870                        if (stType != null) {
1871                            stType.tokencode = TBaseType.rrw_type2;
1872                        }
1873                    } else if (asourcetoken.tokencode == TBaseType.rrw_type) {
1874                        TSourceToken stPrev;
1875                        stPrev = asourcetoken.searchToken(TBaseType.rrw_drop, -1);
1876                        if (stPrev != null) {
1877                            asourcetoken.tokencode = TBaseType.rrw_type2;
1878                        }
1879                        if (asourcetoken.tokencode == TBaseType.rrw_type) {
1880                            stPrev = asourcetoken.searchToken(TBaseType.rrw_of, -1);
1881                            if (stPrev != null) {
1882                                asourcetoken.tokencode = TBaseType.rrw_type2;
1883                            }
1884                        }
1885                        if (asourcetoken.tokencode == TBaseType.rrw_type) {
1886                            stPrev = asourcetoken.searchToken(TBaseType.rrw_create, -1);
1887                            if (stPrev != null) {
1888                                asourcetoken.tokencode = TBaseType.rrw_type2;
1889                            }
1890                        }
1891                        if (asourcetoken.tokencode == TBaseType.rrw_type) {
1892                            stPrev = asourcetoken.searchToken(TBaseType.rrw_replace, -1);
1893                            if (stPrev != null) {
1894                                asourcetoken.tokencode = TBaseType.rrw_type2;
1895                            }
1896                        }
1897                        if (asourcetoken.tokencode == TBaseType.rrw_type) {
1898                            stPrev = asourcetoken.searchToken('%', -1);
1899                            if (stPrev != null) {
1900                                asourcetoken.tokencode = TBaseType.rrw_type2;
1901                            }
1902                        }
1903                    } else if ((asourcetoken.tokencode == TBaseType.rrw_by) || (asourcetoken.tokencode == TBaseType.rrw_to)) {
1904                        lcprevst = getprevsolidtoken(asourcetoken);
1905                        if (lcprevst != null) {
1906                            if ((lcprevst.tokencode == TBaseType.sqlpluscmd) && (lcprevst.toString().equalsIgnoreCase("connect"))) {
1907                                lcprevst.tokencode = TBaseType.rrw_connect;
1908                                lcprevst.tokentype = ETokenType.ttkeyword;
1909                                flexer.insqlpluscmd = false;
1910
1911                                continuesqlplusatnewline = false;
1912                                waitingreturnforsemicolon = false;
1913                                waitingreturnforfloatdiv = false;
1914                                isvalidplace = false;
1915                                insqlpluscmd = false;
1916                            }
1917                        }
1918                    } else if (asourcetoken.tokencode == TBaseType.rrw_with) {
1919                        lcprevst = getprevsolidtoken(asourcetoken);
1920                        if (lcprevst != null) {
1921                            if ((lcprevst.tokencode == TBaseType.sqlpluscmd) && (lcprevst.toString().equalsIgnoreCase("start"))) {
1922                                lcprevst.tokencode = TBaseType.rrw_start;
1923                                lcprevst.tokentype = ETokenType.ttkeyword;
1924                                flexer.insqlpluscmd = false;
1925
1926                                continuesqlplusatnewline = false;
1927                                waitingreturnforsemicolon = false;
1928                                waitingreturnforfloatdiv = false;
1929                                isvalidplace = false;
1930                                insqlpluscmd = false;
1931                            }
1932                        }
1933                    } else if (asourcetoken.tokencode == TBaseType.rrw_set) {
1934                        lcprevst = getprevsolidtoken(asourcetoken);
1935                        if (lcprevst != null) {
1936                            if (lcprevst.getAstext().equalsIgnoreCase("a")) {
1937                                TSourceToken lcpp = getprevsolidtoken(lcprevst);
1938                                if (lcpp != null) {
1939                                    if ((lcpp.tokencode == TBaseType.rrw_not) || (lcpp.tokencode == TBaseType.rrw_is)) {
1940                                        lcprevst.tokencode = TBaseType.rrw_oracle_a_in_aset;
1941                                        asourcetoken.tokencode = TBaseType.rrw_oracle_set_in_aset;
1942                                    }
1943                                }
1944                            }
1945                        }
1946                    }
1947
1948                    break;
1949                }
1950            }
1951
1952            // Get next token
1953            asourcetoken = getanewsourcetoken();
1954            if (asourcetoken != null) {
1955                yychar = asourcetoken.tokencode;
1956
1957                // Handle special case: dot after SQL*Plus commands
1958                if ((asourcetoken.tokencode == '.') && (getprevsolidtoken(asourcetoken) != null)
1959                        && ((currentCmdType == ESqlPlusCmd.spcAppend)
1960                        || (currentCmdType == ESqlPlusCmd.spcChange) || (currentCmdType == ESqlPlusCmd.spcInput)
1961                        || (currentCmdType == ESqlPlusCmd.spcList) || (currentCmdType == ESqlPlusCmd.spcRun))) {
1962                    // a.ent_rp_usr_id is not a real sqlplus command
1963                    TSourceToken lcprevst2 = getprevsolidtoken(asourcetoken);
1964                    lcprevst2.insqlpluscmd = false;
1965                    if (lcprevst2.prevTokenCode != 0) {
1966                        lcprevst2.tokencode = lcprevst2.prevTokenCode;
1967                    } else {
1968                        lcprevst2.tokencode = TBaseType.ident;
1969                    }
1970
1971                    flexer.insqlpluscmd = false;
1972                    continuesqlplusatnewline = false;
1973                    waitingreturnforsemicolon = false;
1974                    waitingreturnforfloatdiv = false;
1975                    isvalidplace = false;
1976                    insqlpluscmd = false;
1977                }
1978            } else {
1979                yychar = 0;
1980
1981                if (waitingreturnforfloatdiv) {
1982                    // / at the end of line treat as sqlplus command
1983                    lct.tokencode = TBaseType.sqlpluscmd;
1984                    if (lct.tokentype != ETokenType.ttslash) {
1985                        lct.tokentype = ETokenType.ttsqlpluscmd;
1986                    }
1987                }
1988            }
1989
1990            if ((yychar == 0) && (prevst != null)) {
1991                if (prevst.tokencode == TBaseType.rrw_inner) {
1992                    prevst.tokencode = TBaseType.ident;
1993                }
1994            }
1995        }
1996    }
1997
1998    // ========== Helper Methods for Tokenization ==========
1999    // These methods support Oracle-specific tokenization logic
2000
2001    /**
2002     * Count number of newlines in a string.
2003     *
2004     * @param s string to analyze
2005     * @return number of line breaks (LF or CR)
2006     */
2007    private int countLines(String s) {
2008        int pos = 0, lf = 0, cr = 0;
2009
2010        while (pos < s.length()) {
2011            if (s.charAt(pos) == '\r') {
2012                cr++;
2013                pos++;
2014                continue;
2015            }
2016            if (s.charAt(pos) == '\n') {
2017                lf++;
2018                pos++;
2019                continue;
2020            }
2021
2022            if (s.charAt(pos) == ' ') {
2023                pos++;
2024                continue;
2025            }
2026            break;
2027        }
2028
2029        if (lf >= cr) return lf;
2030        else return cr;
2031    }
2032
2033    /**
2034     * Check if return token ends with space or tab.
2035     *
2036     * @param s token text
2037     * @return true if ends with space/tab
2038     */
2039    private boolean spaceAtTheEndOfReturnToken(String s) {
2040        if (s == null) return false;
2041        if (s.length() == 0) return false;
2042
2043        return ((s.charAt(s.length() - 1) == ' ') || (s.charAt(s.length() - 1) == '\t'));
2044    }
2045
2046    /**
2047     * Determine if forward slash should be treated as SQL*Plus command delimiter.
2048     * <p>
2049     * Oracle uses '/' as both division operator and SQL*Plus block delimiter.
2050     * This method disambiguates by checking if the '/' appears at the beginning
2051     * of a line (after a return token without trailing whitespace).
2052     *
2053     * @param pstlist token list
2054     * @param pPos position of '/' token
2055     * @return true if '/' should be SQL*Plus command
2056     */
2057    private boolean isValidPlaceForDivToSqlplusCmd(TSourceTokenList pstlist, int pPos) {
2058        boolean ret = false;
2059
2060        if ((pPos <= 0) || (pPos > pstlist.size() - 1)) return ret;
2061
2062        // Token directly before div must be ttreturn without space appending it
2063        gudusoft.gsqlparser.TSourceToken lcst = pstlist.get(pPos - 1);
2064        if (lcst.tokentype != gudusoft.gsqlparser.ETokenType.ttreturn) {
2065            return ret;
2066        }
2067
2068        if (!(lcst.getAstext().charAt(lcst.getAstext().length() - 1) == ' ')) {
2069            ret = true;
2070        }
2071
2072        return ret;
2073    }
2074
2075    /**
2076     * Get previous non-whitespace token.
2077     *
2078     * @param ptoken current token
2079     * @return previous solid token, or null
2080     */
2081    private gudusoft.gsqlparser.TSourceToken getprevsolidtoken(gudusoft.gsqlparser.TSourceToken ptoken) {
2082        gudusoft.gsqlparser.TSourceToken ret = null;
2083        TSourceTokenList lctokenlist = ptoken.container;
2084
2085        if (lctokenlist != null) {
2086            if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) {
2087                if (!(
2088                        (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttwhitespace)
2089                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttreturn)
2090                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttsimplecomment)
2091                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == gudusoft.gsqlparser.ETokenType.ttbracketedcomment)
2092                )) {
2093                    ret = lctokenlist.get(ptoken.posinlist - 1);
2094                } else {
2095                    ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false);
2096                }
2097            }
2098        }
2099        return ret;
2100    }
2101}