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.TLexerSnowflake;
009import gudusoft.gsqlparser.TParserSnowflake;
010import gudusoft.gsqlparser.TSourceToken;
011import gudusoft.gsqlparser.TSourceTokenList;
012import gudusoft.gsqlparser.TStatementList;
013import gudusoft.gsqlparser.TSyntaxError;
014import gudusoft.gsqlparser.EFindSqlStateType;
015import gudusoft.gsqlparser.ETokenType;
016import gudusoft.gsqlparser.ETokenStatus;
017import gudusoft.gsqlparser.ESqlStatementType;
018import gudusoft.gsqlparser.EErrorType;
019import gudusoft.gsqlparser.stmt.TUnknownSqlStatement;
020import gudusoft.gsqlparser.stmt.TCommonStoredProcedureSqlStatement;
021import gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement;
022import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage;
023import gudusoft.gsqlparser.stmt.snowflake.TCreateTaskStmt;
024import gudusoft.gsqlparser.sqlcmds.ISqlCmds;
025import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory;
026import gudusoft.gsqlparser.compiler.TContext;
027import gudusoft.gsqlparser.sqlenv.TSQLEnv;
028import gudusoft.gsqlparser.compiler.TGlobalScope;
029import gudusoft.gsqlparser.compiler.TFrame;
030import gudusoft.gsqlparser.resolver.TSQLResolver;
031import gudusoft.gsqlparser.TLog;
032import gudusoft.gsqlparser.compiler.TASTEvaluator;
033import gudusoft.gsqlparser.stmt.TRoutine;
034import gudusoft.gsqlparser.util.TSnowflakeParameterChecker;
035
036import java.io.BufferedReader;
037import java.io.IOException;
038import java.io.StringReader;
039import java.util.ArrayList;
040import java.util.List;
041import java.util.Stack;
042
043import static gudusoft.gsqlparser.ESqlStatementType.*;
044
045/**
046 * Snowflake database SQL parser implementation.
047 *
048 * <p>This parser handles Snowflake-specific SQL syntax including:
049 * <ul>
050 *   <li>Snowflake stored procedures (SQL and JavaScript)</li>
051 *   <li>Snowflake-specific functions (FLATTEN, PIVOT, UNPIVOT, etc.)</li>
052 *   <li>Snowflake tasks and streams</li>
053 *   <li>Snowflake semi-structured data handling (VARIANT, ARRAY, OBJECT)</li>
054 *   <li>Special token handling (AT, LEFT/RIGHT joins, DATE/TIME functions)</li>
055 *   <li>Transaction control (BEGIN TRANSACTION, COMMIT, ROLLBACK)</li>
056 * </ul>
057 *
058 * <p><b>Design Notes:</b>
059 * <ul>
060 *   <li>Extends {@link AbstractSqlParser} using the template method pattern</li>
061 *   <li>Uses {@link TLexerSnowflake} for tokenization</li>
062 *   <li>Uses {@link TParserSnowflake} for parsing</li>
063 *   <li>Delimiter character: ';' for SQL statements</li>
064 * </ul>
065 *
066 * <p><b>Usage Example:</b>
067 * <pre>
068 * // Get Snowflake parser from factory
069 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvsnowflake);
070 *
071 * // Build context
072 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvsnowflake)
073 *     .sqlText("SELECT * FROM customers WHERE region = 'US'")
074 *     .build();
075 *
076 * // Parse
077 * SqlParseResult result = parser.parse(context);
078 *
079 * // Access statements
080 * TStatementList statements = result.getSqlStatements();
081 * </pre>
082 *
083 * @see SqlParser
084 * @see AbstractSqlParser
085 * @see TLexerSnowflake
086 * @see TParserSnowflake
087 * @since 3.2.0.0
088 */
089public class SnowflakeSqlParser extends AbstractSqlParser {
090
091    /**
092     * Construct Snowflake SQL parser.
093     * <p>
094     * Configures the parser for Snowflake database with default delimiter (;).
095     * <p>
096     * Following the original TGSqlParser pattern, the lexer and parser are
097     * created once in the constructor and reused for all parsing operations.
098     */
099    public SnowflakeSqlParser() {
100        super(EDbVendor.dbvsnowflake);
101        this.delimiterChar = ';';
102        this.defaultDelimiterStr = ";";
103
104        // Create lexer once - will be reused for all parsing operations
105        this.flexer = new TLexerSnowflake();
106        this.flexer.delimiterchar = this.delimiterChar;
107        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
108
109        // Set parent's lexer reference for shared tokenization logic
110        this.lexer = this.flexer;
111
112        // Create parser once - will be reused for all parsing operations
113        this.fparser = new TParserSnowflake(null);
114        this.fparser.lexer = this.flexer;
115    }
116
117    // ========== Parser Components ==========
118
119    /** The Snowflake lexer used for tokenization */
120    public TLexerSnowflake flexer;
121
122    /** SQL parser (for Snowflake statements) */
123    private TParserSnowflake fparser;
124
125    /** Current statement being built during extraction */
126    private TCustomSqlStatement gcurrentsqlstatement;
127
128    // Stored procedure parsing state tracking
129    private enum stored_procedure_type {
130        procedure, function, package_spec, package_body, block_with_declare,
131        block_with_begin, create_trigger, create_library, others
132    }
133
134    private enum stored_procedure_status {
135        start, is_as, body, bodyend, end
136    }
137
138    private static final int stored_procedure_nested_level = 50;
139
140    // Note: Global context and frame stack fields inherited from AbstractSqlParser:
141    // - protected TContext globalContext
142    // - protected TSQLEnv sqlEnv
143    // - protected Stack<TFrame> frameStack
144    // - protected TFrame globalFrame
145
146    // ========== AbstractSqlParser Abstract Methods Implementation ==========
147
148    /**
149     * Return the Snowflake lexer instance.
150     */
151    @Override
152    protected TCustomLexer getLexer(ParserContext context) {
153        return this.flexer;
154    }
155
156    /**
157     * Return the Snowflake SQL parser instance with updated token list.
158     */
159    @Override
160    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
161        this.fparser.sourcetokenlist = tokens;
162        this.fparser.reclassifyStagePathKeywords();
163        return this.fparser;
164    }
165
166    /**
167     * Snowflake does not use a secondary parser (unlike Oracle with PL/SQL).
168     */
169    @Override
170    protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) {
171        return null;
172    }
173
174    /**
175     * Call Snowflake-specific tokenization logic.
176     * <p>
177     * Delegates to dosnowflakesqltexttotokenlist which handles Snowflake's
178     * specific keyword recognition and token generation.
179     */
180    @Override
181    protected void tokenizeVendorSql() {
182        preprocessBackslashEscapedQuotes();
183        dosnowflakesqltexttotokenlist();
184    }
185
186    /**
187     * Preprocess SQL input to convert \\' (backslash-escaped quotes from serialized SQL)
188     * into regular single quotes before tokenization.
189     * <p>
190     * Some SQL export tools serialize single quotes as \\' when embedding SQL in strings.
191     * The Snowflake lexer does not handle \\ in init state, so we normalize them here.
192     */
193    private void preprocessBackslashEscapedQuotes() {
194        if (this.flexer.yyinput == null) return;
195        try {
196            StringBuilder sb = new StringBuilder();
197            char[] buf = new char[4096];
198            int n;
199            while ((n = this.flexer.yyinput.read(buf)) != -1) {
200                sb.append(buf, 0, n);
201            }
202            String sql = sb.toString();
203            if (sql.contains("\\\\'")) {
204                sql = sql.replace("\\\\'", "'");
205            }
206            this.flexer.yyinput = new BufferedReader(new StringReader(sql));
207        } catch (IOException e) {
208            // If reading fails, leave the original reader in place
209        }
210    }
211
212    /**
213     * Post-tokenization: merge {identifier} template variable tokens into IDENT tokens.
214     * Handles patterns like kayodatalake__{env}.schema.table where {env} is a
215     * dbt/Jinja template variable. Also handles standalone {var} patterns.
216     */
217    @Override
218    protected void doAfterTokenize(TSourceTokenList tokens) {
219        super.doAfterTokenize(tokens);
220        mergeBraceTemplateVariables(tokens);
221    }
222
223    private static boolean isIdentOrKeyword(TSourceToken t) {
224        return t.tokencode == TBaseType.ident
225            || t.tokentype == ETokenType.ttkeyword
226            || t.tokentype == ETokenType.ttidentifier;
227    }
228
229    private void mergeBraceTemplateVariables(TSourceTokenList tokens) {
230        for (int i = 0; i < tokens.size() - 2; i++) {
231            TSourceToken tok = tokens.get(i);
232            if (tok.tokencode != '{') continue;
233
234            // Look for pattern: '{' IDENT_OR_KEYWORD '}'
235            int identIdx = i + 1;
236            TSourceToken identTok = tokens.get(identIdx);
237            if (!isIdentOrKeyword(identTok)) continue;
238
239            int closeIdx = identIdx + 1;
240            if (closeIdx >= tokens.size()) continue;
241            TSourceToken closeTok = tokens.get(closeIdx);
242            if (closeTok.tokencode != '}') continue;
243
244            // Found {name} pattern. Check if preceding token is an IDENT to merge with.
245            String mergedText = "{" + identTok.astext + "}";
246            int mergeStart = i;
247
248            // Check preceding token for an identifier to merge with (e.g., kayodatalake__{env})
249            int prevIdx = i - 1;
250            if (prevIdx >= 0 && isIdentOrKeyword(tokens.get(prevIdx))) {
251                mergedText = tokens.get(prevIdx).astext + mergedText;
252                mergeStart = prevIdx;
253            }
254
255            // Set the first token of the merge range to the merged IDENT
256            TSourceToken anchor = tokens.get(mergeStart);
257            anchor.astext = mergedText;
258            anchor.tokencode = TBaseType.ident;
259            anchor.tokentype = ETokenType.ttidentifier;
260
261            // Convert remaining tokens to whitespace
262            for (int j = mergeStart + 1; j <= closeIdx; j++) {
263                tokens.get(j).tokentype = ETokenType.ttwhitespace;
264                tokens.get(j).tokencode = TBaseType.lexspace;
265            }
266
267            i = closeIdx; // skip past merged tokens
268        }
269    }
270
271    /**
272     * Setup Snowflake parser for raw statement extraction.
273     * <p>
274     * Snowflake uses a single parser, so we inject sqlcmds and update
275     * the token list for the main parser only.
276     */
277    @Override
278    protected void setupVendorParsersForExtraction() {
279        // Inject sqlcmds into parser (required for make_stmt)
280        this.fparser.sqlcmds = this.sqlcmds;
281
282        // Update token list for parser
283        this.fparser.sourcetokenlist = this.sourcetokenlist;
284    }
285
286    /**
287     * Call Snowflake-specific raw statement extraction logic.
288     * <p>
289     * Delegates to dosnowflakegetrawsqlstatements which handles Snowflake's
290     * statement delimiters and stored procedure boundaries.
291     */
292    @Override
293    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
294        int errorCount = dosnowflakegetrawsqlstatements(builder);
295        // Error count is tracked internally; errors are already added to syntaxErrors list
296
297        // Expand dollar string and single-quoted procedure bodies into token lists
298        expandDollarString();
299
300        // Set the extracted statements in the builder
301        builder.sqlStatements(this.sqlstatements);
302    }
303
304    /**
305     * Expand dollar-delimited and single-quoted string literals in Snowflake procedure/function bodies.
306     * <p>
307     * For CREATE PROCEDURE/FUNCTION statements with LANGUAGE SQL, this method:
308     * 1. Finds string literals (starting with $$ or ') that follow AS keyword
309     * 2. Extracts and tokenizes the SQL code inside the quotes
310     * 3. Replaces the single string token with expanded tokens for proper parsing
311     * <p>
312     * This is essential for Snowflake's syntax: AS '...' or AS $$...$$
313     */
314    private void expandDollarString() {
315        TSourceToken st;
316        TCustomSqlStatement sql;
317        ArrayList<TSourceToken> dollarTokens = new ArrayList<>();
318        boolean isSQLLanguage = true;
319
320        // Iterate all create procedure and create function, other sql statement just skipped
321        for (int i = 0; i < sqlstatements.size(); i++) {
322            sql = sqlstatements.get(i);
323            if (!((sql.sqlstatementtype == ESqlStatementType.sstcreateprocedure) ||
324                  (sql.sqlstatementtype == ESqlStatementType.sstcreatefunction))) continue;
325
326            isSQLLanguage = true;
327            for (int j = 0; j < sql.sourcetokenlist.size(); j++) {
328                st = sql.sourcetokenlist.get(j);
329
330                if (sql.sqlstatementtype == ESqlStatementType.sstcreateprocedure) {
331                    if (st.tokencode == TBaseType.rrw_snowflake_language) {
332                        TSourceToken lang = st.nextSolidToken();
333                        if ((lang != null) && (!lang.toString().equalsIgnoreCase("sql"))) {
334                            isSQLLanguage = false;
335                        }
336                    }
337                }
338
339                if (!isSQLLanguage) break;
340
341                if (st.tokencode == TBaseType.sconst) {
342                    if (st.toString().startsWith("$$")) {
343                        dollarTokens.add(st);
344                    } else if (st.toString().startsWith("'")) {
345                        // https://docs.snowflake.com/en/sql-reference/sql/create-procedure
346                        // string literal delimiter can be $ or '
347                        if (st.prevSolidToken().tokencode == TBaseType.rrw_as) {
348                            dollarTokens.add(st);
349                        }
350                    }
351                }
352            }//check tokens
353
354            for (int m = dollarTokens.size() - 1; m >= 0; m--) {
355                // Token Expansion:
356                // For each identified string literal:
357                // Extracts the content between the quotes
358                // Tokenizes the extracted SQL code
359                // Verifies it's a valid code block (starts with DECLARE or BEGIN)
360                // Replaces the original quoted string with expanded tokens in the source token list
361
362                st = dollarTokens.get(m);
363
364                // Create a new parser to tokenize the procedure body
365                // Use TGSqlParser for internal tokenization (simpler than using SnowflakeSqlParser for this snippet)
366                gudusoft.gsqlparser.TGSqlParser parser = new gudusoft.gsqlparser.TGSqlParser(this.vendor);
367
368                // Extract the body content from the string literal
369                // For procedure bodies, we need special handling for single-quoted strings:
370                // - Remove outer quotes
371                // - Unescape '' to ' (SQL standard)
372                // - Preserve backslash-quote-quote patterns: \'' -> \'
373                //   (backslash + escaped quote should become backslash + single quote for re-parsing)
374                // See mantisbt issue 4298 for details
375                String tokenStr = st.toString();
376                String bodyContent;
377                if (tokenStr.startsWith("$$")) {
378                    // Dollar-quoted string: just strip the $$ delimiters
379                    bodyContent = TBaseType.getStringInsideLiteral(tokenStr);
380                } else if (tokenStr.startsWith("'")) {
381                    // Single-quoted string: custom unescaping
382                    // Do NOT use getStringInsideLiteral() as it incorrectly handles \'
383                    bodyContent = tokenStr.substring(1, tokenStr.length() - 1);
384                    // Unescape body-level escapes for re-parsing:
385                    // \\'' (2 backslashes + 2 quotes) = escaped backslash + escaped quote
386                    //   at the body level = one literal quote char in the body SQL.
387                    // \'' (1 backslash + 2 quotes) = backslash + escaped quote
388                    //   preserved as \' (C-style escaped quote) for re-parsing.
389                    // '' (2 quotes) = escaped quote = one literal quote char.
390                    bodyContent = bodyContent.replace("\\\\''", "\u0000BSQQ2\u0000");
391                    bodyContent = bodyContent.replace("\\''", "\u0000BSQQ\u0000");
392                    bodyContent = bodyContent.replace("''", "'");
393                    bodyContent = bodyContent.replace("\u0000BSQQ\u0000", "\\'");
394                    bodyContent = bodyContent.replace("\u0000BSQQ2\u0000", "'");
395                } else {
396                    bodyContent = tokenStr;
397                }
398                parser.sqltext = bodyContent;
399
400                TSourceToken startQuote = new TSourceToken(st.toString().substring(0, 1));
401                startQuote.tokencode = TBaseType.lexspace; // Set as space, can be ignored during parsing, but preserved in toString()
402                TSourceToken endQuote = new TSourceToken(st.toString().substring(0, 1));
403                endQuote.tokencode = TBaseType.lexspace;
404
405                // use getrawsqlstatements() instead of tokenizeSqltext() to get the source token list because
406                // some token will be transformed to other token, which will be processed in dosnowflakegetrawsqlstatements()
407                parser.getrawsqlstatements();
408
409                TSourceToken st2;
410                boolean isValidBlock = false;
411                for (int k = 0; k < parser.sourcetokenlist.size(); k++) {
412                    st2 = parser.sourcetokenlist.get(k);
413                    if (st2.isnonsolidtoken()) continue;
414                    if ((st2.tokencode == TBaseType.rrw_declare) || (st2.tokencode == TBaseType.rrw_begin)) {
415                        isValidBlock = true;
416                    }
417                    break;
418                }
419
420                if (isValidBlock) {
421                    TSourceToken semiColon = null;
422                    st.tokenstatus = ETokenStatus.tsdeleted;
423                    int startPosOfThisSQL = sql.getStartToken().posinlist;
424
425                    sql.sourcetokenlist.add((st.posinlist++) - startPosOfThisSQL, startQuote); // Add opening quote
426                    for (int k = 0; k < parser.sourcetokenlist.size(); k++) {
427                        st2 = parser.sourcetokenlist.get(k);
428                        if (st2.tokencode == ';') {
429                            semiColon = st2;
430                            TSourceToken prevSolidToken = st2.prevSolidToken();
431                            if ((prevSolidToken != null) && (prevSolidToken.tokencode == TBaseType.rrw_begin)) {
432                                // begin;  => begin transaction;
433                                prevSolidToken.tokencode = TBaseType.rrw_snowflake_begin_transaction;
434                            }
435                        }
436                        if ((st2.tokencode == TBaseType.rrw_snowflake_work) || (st2.tokencode == TBaseType.rrw_snowflake_transaction)) {
437                            // begin work;  => begin transaction;
438                            TSourceToken prevSolidToken = st2.prevSolidToken();
439                            if ((prevSolidToken != null) && (prevSolidToken.tokencode == TBaseType.rrw_begin)) {
440                                // begin;  => begin transaction;
441                                prevSolidToken.tokencode = TBaseType.rrw_snowflake_begin_transaction;
442                            }
443                        }
444                        sql.sourcetokenlist.add((st.posinlist++) - startPosOfThisSQL, st2);
445                    }
446                    if (semiColon != null) {
447                        if (semiColon.prevSolidToken().tokencode == TBaseType.rrw_end) {
448                            // Set as space, can be ignored during parsing, but preserved in toString()
449                            semiColon.tokencode = TBaseType.lexspace;
450                        }
451                    }
452
453                    sql.sourcetokenlist.add((st.posinlist++) - startPosOfThisSQL, endQuote); // Add closing quote
454                    TBaseType.resetTokenChain(sql.sourcetokenlist, 0); // Reset token chain to ensure new tokens are accessible in toString()
455                }
456            }
457
458            dollarTokens.clear();
459        }//statement
460    }
461
462    /**
463     * Perform full parsing of statements with syntax checking.
464     * <p>
465     * This method orchestrates the parsing of all statements.
466     */
467    @Override
468    protected TStatementList performParsing(ParserContext context,
469                                           TCustomParser parser,
470                                           TCustomParser secondaryParser,
471                                           TSourceTokenList tokens,
472                                           TStatementList rawStatements) {
473        // Store references
474        this.fparser = (TParserSnowflake) parser;
475        this.sourcetokenlist = tokens;
476        this.parserContext = context;
477
478        // Use the raw statements passed from AbstractSqlParser.parse()
479        this.sqlstatements = rawStatements;
480
481        // Initialize statement parsing infrastructure
482        this.sqlcmds = SqlCmdsFactory.get(vendor);
483
484        // Inject sqlcmds into parser (required for make_stmt and other methods)
485        this.fparser.sqlcmds = this.sqlcmds;
486
487        // Initialize global context for semantic analysis
488        initializeGlobalContext();
489
490        // Parse each statement with exception handling for robustness
491        for (int i = 0; i < sqlstatements.size(); i++) {
492            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
493
494            try {
495                stmt.setFrameStack(frameStack);
496
497                // Parse the statement
498                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
499
500                // Handle error recovery for CREATE TABLE/INDEX
501                boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE;
502                if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) {
503                    handleCreateTableErrorRecovery(stmt);
504                }
505
506                // Collect syntax errors
507                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
508                    copyErrorsFromStatement(stmt);
509                }
510
511            } catch (Exception ex) {
512                // Use inherited exception handler from AbstractSqlParser
513                // This provides consistent error handling across all database parsers
514                handleStatementParsingException(stmt, i, ex);
515                continue;
516            }
517        }
518
519        // Clean up frame stack
520        if (globalFrame != null) {
521            globalFrame.popMeFromStack(frameStack);
522        }
523
524        return this.sqlstatements;
525    }
526
527    // Note: initializeGlobalContext() inherited from AbstractSqlParser
528    // Note: No override of afterStatementParsed() needed - default (no-op) is appropriate for Snowflake
529
530    /**
531     * Handle error recovery for CREATE TABLE/INDEX statements.
532     */
533    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
534        if (((stmt.sqlstatementtype == ESqlStatementType.sstcreatetable)
535                || (stmt.sqlstatementtype == ESqlStatementType.sstcreateindex))
536                && (!TBaseType.c_createTableStrictParsing)) {
537
538            int nested = 0;
539            boolean isIgnore = false, isFoundIgnoreToken = false;
540            TSourceToken firstIgnoreToken = null;
541
542            for (int k = 0; k < stmt.sourcetokenlist.size(); k++) {
543                TSourceToken st = stmt.sourcetokenlist.get(k);
544                if (isIgnore) {
545                    if (st.issolidtoken() && (st.tokencode != ';')) {
546                        isFoundIgnoreToken = true;
547                        if (firstIgnoreToken == null) {
548                            firstIgnoreToken = st;
549                        }
550                    }
551                    if (st.tokencode != ';') {
552                        st.tokencode = TBaseType.sqlpluscmd;
553                    }
554                    continue;
555                }
556                if (st.tokencode == (int) ')') {
557                    nested--;
558                    if (nested == 0) {
559                        boolean isSelect = false;
560                        TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1);
561                        if (st1 != null) {
562                            TSourceToken st2 = st.searchToken((int) '(', 2);
563                            if (st2 != null) {
564                                TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3);
565                                isSelect = (st3 != null);
566                            }
567                        }
568                        if (!isSelect) isIgnore = true;
569                    }
570                } else if (st.tokencode == (int) '(') {
571                    nested++;
572                }
573            }
574
575            if (isFoundIgnoreToken) {
576                stmt.clearError();
577                stmt.parsestatement(null, false);
578            }
579        }
580    }
581
582    /**
583     * Perform Snowflake-specific semantic analysis using TSQLResolver.
584     */
585    @Override
586    protected void performSemanticAnalysis(ParserContext context, TStatementList statements) {
587        if (TBaseType.isEnableResolver() && getSyntaxErrors().isEmpty()) {
588            TSQLResolver resolver = new TSQLResolver(globalContext, statements);
589            resolver.resolve();
590        }
591    }
592
593    /**
594     * Perform interpretation/evaluation on parsed statements.
595     */
596    @Override
597    protected void performInterpreter(ParserContext context, TStatementList statements) {
598        if (TBaseType.ENABLE_INTERPRETER && getSyntaxErrors().isEmpty()) {
599            TLog.clearLogs();
600            TGlobalScope interpreterScope = new TGlobalScope(sqlEnv);
601            TLog.enableInterpreterLogOnly();
602            TASTEvaluator astEvaluator = new TASTEvaluator(statements, interpreterScope);
603            astEvaluator.eval();
604        }
605    }
606
607    // ========== Snowflake-Specific Tokenization ==========
608
609    /**
610     * Snowflake-specific tokenization logic.
611     * <p>
612     * Extracted from: TGSqlParser.dosnowflakesqltexttotokenlist() (lines 3289-3439)
613     */
614    private void dosnowflakesqltexttotokenlist() {
615
616        boolean insqlpluscmd = false;
617        boolean isvalidplace = true;
618        boolean waitingreturnforfloatdiv = false;
619        boolean waitingreturnforsemicolon = false;
620        boolean continuesqlplusatnewline = false;
621
622        TSourceToken lct = null, prevst = null;
623
624        TSourceToken asourcetoken, lcprevst;
625        int yychar;
626
627        asourcetoken = getanewsourcetoken();
628        if (asourcetoken == null) return;
629        yychar = asourcetoken.tokencode;
630
631        while (yychar > 0) {
632            sourcetokenlist.add(asourcetoken);
633            switch (yychar) {
634                case TBaseType.cmtdoublehyphen:
635                case TBaseType.cmtslashstar:
636                case TBaseType.lexspace: {
637                    if (insqlpluscmd) {
638                        asourcetoken.insqlpluscmd = true;
639                    }
640                    break;
641                }
642                case TBaseType.lexnewline: {
643                    if (insqlpluscmd) {
644                        insqlpluscmd = false;
645                        isvalidplace = true;
646
647                        if (continuesqlplusatnewline) {
648                            insqlpluscmd = true;
649                            isvalidplace = false;
650                            asourcetoken.insqlpluscmd = true;
651                        }
652                    }
653
654                    if (waitingreturnforsemicolon) {
655                        isvalidplace = true;
656                    }
657                    if (waitingreturnforfloatdiv) {
658                        isvalidplace = true;
659                        lct.tokencode = TBaseType.sqlpluscmd;
660                        if (lct.tokentype != ETokenType.ttslash) {
661                            lct.tokentype = ETokenType.ttsqlpluscmd;
662                        }
663                    }
664                    flexer.insqlpluscmd = insqlpluscmd;
665                    break;
666                } //case newline
667                default: {
668                    //solid tokentext
669                    continuesqlplusatnewline = false;
670                    waitingreturnforsemicolon = false;
671                    waitingreturnforfloatdiv = false;
672                    if (insqlpluscmd) {
673                        asourcetoken.insqlpluscmd = true;
674                        if (asourcetoken.getAstext().equalsIgnoreCase("-")) {
675                            continuesqlplusatnewline = true;
676                        }
677                    } else {
678                        if (asourcetoken.tokentype == ETokenType.ttsemicolon) {
679                            waitingreturnforsemicolon = true;
680                        }
681                        if ((asourcetoken.tokentype == ETokenType.ttslash)
682                                // and (isvalidplace or sourcetokenlist.TokenBeforeCurToken(#10,false,false,false)) then
683                                && (isvalidplace || (IsValidPlaceForDivToSqlplusCmd(sourcetokenlist, asourcetoken.posinlist)))) {
684                            lct = asourcetoken;
685                            waitingreturnforfloatdiv = true;
686                        }
687                        if ((isvalidplace) && isvalidsqlpluscmdInPostgresql(asourcetoken.toString())) {
688                            asourcetoken.tokencode = TBaseType.sqlpluscmd;
689                            if (asourcetoken.tokentype != ETokenType.ttslash) {
690                                asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
691                            }
692                            insqlpluscmd = true;
693                            flexer.insqlpluscmd = insqlpluscmd;
694                        }
695                    }
696                    isvalidplace = false;
697
698                    // the inner keyword tokentext should be convert to TBaseType.ident when
699                    // next solid tokentext is not join
700
701                    if (prevst != null) {
702                        if (prevst.tokencode == TBaseType.rrw_inner)//flexer.getkeywordvalue("INNER"))
703                        {
704                            if (asourcetoken.tokencode != flexer.getkeywordvalue("JOIN")
705                                    && asourcetoken.tokencode != flexer.getkeywordvalue("DIRECTED")) {
706                                prevst.tokencode = TBaseType.ident;
707                            }
708                        }
709
710
711                        if ((prevst.tokencode == TBaseType.rrw_not)
712                                && (asourcetoken.tokencode == flexer.getkeywordvalue("DEFERRABLE"))) {
713                            prevst.tokencode = flexer.getkeywordvalue("NOT_DEFERRABLE");
714                        }
715
716                    }
717
718                    if (asourcetoken.tokencode == TBaseType.rrw_inner) {
719                        prevst = asourcetoken;
720                    } else if (asourcetoken.tokencode == TBaseType.rrw_not) {
721                        prevst = asourcetoken;
722                    } else {
723                        prevst = null;
724                    }
725
726
727                }
728            }
729
730            //flexer.yylexwrap(asourcetoken);
731            asourcetoken = getanewsourcetoken();
732            if (asourcetoken != null) {
733                yychar = asourcetoken.tokencode;
734            } else {
735                yychar = 0;
736
737                if (waitingreturnforfloatdiv) { // / at the end of line treat as sqlplus command
738                    //isvalidplace = true;
739                    lct.tokencode = TBaseType.sqlpluscmd;
740                    if (lct.tokentype != ETokenType.ttslash) {
741                        lct.tokentype = ETokenType.ttsqlpluscmd;
742                    }
743                }
744
745            }
746
747            if ((yychar == 0) && (prevst != null)) {
748                if (prevst.tokencode == TBaseType.rrw_inner)// flexer.getkeywordvalue("RW_INNER"))
749                {
750                    prevst.tokencode = TBaseType.ident;
751                }
752            }
753
754
755        } // while
756
757
758    }
759
760    // ========== Snowflake-Specific Raw Statement Extraction ==========
761
762    /**
763     * Snowflake-specific raw statement extraction logic.
764     * <p>
765     * Extracted from: TGSqlParser.dosnowflakegetrawsqlstatements() (lines 8646-9388)
766     */
767    private int dosnowflakegetrawsqlstatements(SqlParseResult.Builder builder) {
768        int waitingEnd = 0;
769        boolean foundEnd = false;
770
771        int waitingEnds[] = new int[stored_procedure_nested_level];
772        stored_procedure_type sptype[] = new stored_procedure_type[stored_procedure_nested_level];
773        stored_procedure_status procedure_status[] = new stored_procedure_status[stored_procedure_nested_level];
774        boolean endBySlashOnly = true;
775        int nestedProcedures = 0, nestedParenthesis = 0;
776        boolean inDollarBody = false;
777
778        if (TBaseType.assigned(sqlstatements)) sqlstatements.clear();
779        if (!TBaseType.assigned(sourcetokenlist)) return -1;
780
781        gcurrentsqlstatement = null;
782        EFindSqlStateType gst = EFindSqlStateType.stnormal;
783        TSourceToken lcprevsolidtoken = null, ast = null;
784
785        for (int i = 0; i < sourcetokenlist.size(); i++) {
786
787            if ((ast != null) && (ast.issolidtoken()))
788                lcprevsolidtoken = ast;
789
790            ast = sourcetokenlist.get(i);
791            sourcetokenlist.curpos = i;
792
793            if ((ast.tokencode == TBaseType.rrw_right) || (ast.tokencode == TBaseType.rrw_left)) {
794                TSourceToken stLparen = ast.searchToken('(', 1);
795                if (stLparen != null) {   //match (
796                    ast.tokencode = TBaseType.ident;
797                }
798                TSourceToken stNextToken = ast.nextSolidToken();
799                if ((stNextToken != null) && ((stNextToken.tokencode == TBaseType.rrw_join) || (stNextToken.tokencode == TBaseType.rrw_outer))) {
800                    if (ast.tokencode == TBaseType.rrw_left) {
801                        ast.tokencode = TBaseType.rrw_snowflake_left_join;
802                    } else {
803                        ast.tokencode = TBaseType.rrw_snowflake_right_join;
804                    }
805                }
806            } else if (ast.tokencode == TBaseType.rrw_snowflake_at) {
807                TSourceToken stLparen = ast.searchToken('(', 1);
808                if (stLparen != null) {   //match (
809                    ast.tokencode = TBaseType.rrw_snowflake_at_before_parenthesis;
810                }
811            } else if (ast.tokencode == TBaseType.rrw_snowflake_changes) {
812                TSourceToken stLparen = ast.searchToken('(', 1);
813                if (stLparen != null) {   //changes (
814                    ast.tokencode = TBaseType.rrw_snowflake_changes_parenthesis;
815                }
816            } else if (ast.tokencode == TBaseType.rrw_date) {
817                TSourceToken stLparen = ast.searchToken('(', 1);
818                if (stLparen != null) {   //date (
819                    ast.tokencode = TBaseType.rrw_snowflake_date;
820                } else {
821                    stLparen = ast.searchToken('.', 1);
822                    if (stLparen != null) {   //date (
823                        ast.tokencode = TBaseType.ident;
824                    }
825                }
826            } else if (ast.tokencode == TBaseType.rrw_time) {
827                TSourceToken stLparen = ast.searchToken('(', 1);
828                if (stLparen != null) {   //date (
829                    ast.tokencode = TBaseType.rrw_snowflake_time;
830                } else {
831                    stLparen = ast.searchToken('.', 1);
832                    if (stLparen != null) {   //date (
833                        ast.tokencode = TBaseType.ident;
834                    }
835                }
836            } else if (ast.tokencode == TBaseType.rrw_char) {
837                TSourceToken stLparen = ast.searchToken('(', 1);
838                if (stLparen != null) {   //date (
839                    ast.tokencode = TBaseType.rrw_snowflake_char;
840                } else {
841                    stLparen = ast.searchToken('.', 1);
842                    if (stLparen != null) {   //date (
843                        ast.tokencode = TBaseType.ident;
844                    }
845                }
846            } else if (ast.tokencode == TBaseType.rrw_snowflake_window) {
847                TSourceToken stAs = ast.searchToken(TBaseType.rrw_as, 2);
848                if (stAs != null) {   //date (
849                    ast.tokencode = TBaseType.rrw_snowflake_window_as;
850                } else {
851                }
852            } else if ((ast.tokencode == TBaseType.rrw_snowflake_pivot) || (ast.tokencode == TBaseType.rrw_snowflake_unpivot)) {
853                // For UNPIVOT, search range 3 to look past INCLUDE/EXCLUDE NULLS before (
854                int searchRange = (ast.tokencode == TBaseType.rrw_snowflake_unpivot) ? 3 : 1;
855                TSourceToken stLparen = ast.searchToken('(', searchRange);
856                if (stLparen != null) {   //pivot (, unpivot (, unpivot include nulls (
857
858                } else {
859                    ast.tokencode = TBaseType.ident;
860                }
861            } else if (ast.tokencode == TBaseType.rrw_snowflake_flatten) {
862                TSourceToken stLeftParens = ast.searchToken('(', 1);
863                if (stLeftParens != null) {   //flatten (
864
865                } else {
866                    ast.tokencode = TBaseType.ident; // change it to an identifier, can be used as db object name.
867                }
868            } else if (ast.tokencode == TBaseType.rrw_snowflake_offset) {
869                TSourceToken stFrom = ast.searchToken(TBaseType.rrw_from, -ast.posinlist, TBaseType.rrw_select, true);
870                if (stFrom == null) {
871                    // FORM keyword before OFFSET is not found, then offset must be a column name,
872                    // just like this: SELECT column1 offset FROM table2
873                    ast.tokencode = TBaseType.ident; // change it to an identifier, can be used as db object name.
874                }
875            } else if (ast.tokencode == TBaseType.rrw_replace) {
876                TSourceToken stStar = ast.prevSolidToken();
877                if (stStar.tokencode == '*') {
878                    ast.tokencode = TBaseType.rrw_snowflake_replace_after_star;
879                }
880            } else if (ast.tokencode == TBaseType.rrw_snowflake_transaction) {
881                TSourceToken stBegin = ast.prevSolidToken();
882                if ((stBegin != null) && (stBegin.tokencode == TBaseType.rrw_begin)) {
883                    stBegin.tokencode = TBaseType.rrw_snowflake_begin_transaction;
884                }
885            } else if (ast.tokencode == TBaseType.rrw_begin) {
886                // begin;
887                // begin work;
888                // begin transaction;
889                TSourceToken stNext = ast.nextSolidToken();
890                if ((stNext != null) && ((stNext.tokencode == ';')
891                        || (stNext.tokencode == TBaseType.rrw_snowflake_work) || (stNext.tokencode == TBaseType.rrw_snowflake_transaction))
892                ) {
893                    ast.tokencode = TBaseType.rrw_snowflake_begin_transaction;
894                }
895            } else if ((ast.tokencode == TBaseType.rrw_snowflake_top) || (ast.tokencode == TBaseType.rrw_text) || (ast.tokencode == TBaseType.rrw_snowflake_default)) {
896                TSourceToken stPeriod = ast.nextSolidToken();
897                if ((stPeriod != null) && (stPeriod.tokencode == '.')) {
898                    ast.tokencode = TBaseType.ident;
899                }
900            } else if (ast.tokencode == TBaseType.rrw_snowflake_limit) {
901                TSourceToken stPrev = ast.prevSolidToken();
902                if ((stPrev != null) && (stPrev.tokencode == ',')) {
903                    ast.tokencode = TBaseType.ident;
904                }
905            } else if (ast.tokencode == TBaseType.ident) {
906                // check whether it is a snowflake parameter name
907                // 这个调用可能会有性能问题,因为每个ident都会调用一次
908                if (TSnowflakeParameterChecker.isSnowflakeParameter(ast.toString())) {
909                    ast.tokencode = TBaseType.rrw_snowflake_parameter_name;
910                }
911            }
912
913
914            switch (gst) {
915                case sterror: {
916                    if (ast.tokentype == ETokenType.ttsemicolon) {
917                        gcurrentsqlstatement.sourcetokenlist.add(ast);
918                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
919                        gst = EFindSqlStateType.stnormal;
920                    } else {
921                        gcurrentsqlstatement.sourcetokenlist.add(ast);
922                    }
923                    break;
924                } //sterror
925
926                case stnormal: {
927                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
928                            || (ast.tokencode == TBaseType.cmtslashstar)
929                            || (ast.tokencode == TBaseType.lexspace)
930                            || (ast.tokencode == TBaseType.lexnewline)
931                            || (ast.tokentype == ETokenType.ttsemicolon)) {
932                        if (gcurrentsqlstatement != null) {
933                            gcurrentsqlstatement.sourcetokenlist.add(ast);
934                        }
935
936                        if ((lcprevsolidtoken != null) && (ast.tokentype == ETokenType.ttsemicolon)) {
937                            if (lcprevsolidtoken.tokentype == ETokenType.ttsemicolon) {
938                                // ;;;; continuous semicolon,treat it as comment
939                                ast.tokentype = ETokenType.ttsimplecomment;
940                                ast.tokencode = TBaseType.cmtdoublehyphen;
941                            }
942                        }
943
944                        continue;
945                    }
946
947                    if (ast.tokencode == TBaseType.sqlpluscmd) {
948                        gst = EFindSqlStateType.stsqlplus;
949                        gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
950                        gcurrentsqlstatement.sourcetokenlist.add(ast);
951                        continue;
952                    }
953
954                    // find a tokentext to start sql or plsql mode
955                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
956
957                    if (gcurrentsqlstatement != null) {
958                        // WITH...AS PROCEDURE uses stsql mode (not ststoredprocedure)
959                        // because the $$ body is handled by TRoutine's dollar delimiter logic in stsql mode,
960                        // and the ststoredprocedure state machine misinterprets the early AS token in "WITH name AS PROCEDURE"
961                        boolean isWithProcedure = (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreateprocedure)
962                                && (ast.tokencode == TBaseType.rrw_with);
963
964                        if (gcurrentsqlstatement.issnowflakeplsql() && !isWithProcedure) {
965                            nestedProcedures = 0;
966                            gst = EFindSqlStateType.ststoredprocedure;
967                            gcurrentsqlstatement.sourcetokenlist.add(ast);
968
969                            switch (gcurrentsqlstatement.sqlstatementtype) {
970                                case sstplsql_createprocedure:
971                                case sstcreateprocedure:
972                                    sptype[nestedProcedures] = stored_procedure_type.procedure;
973                                    break;
974                                case sstplsql_createfunction:
975                                    sptype[nestedProcedures] = stored_procedure_type.function;
976                                    break;
977                                case sstplsql_createpackage:
978                                    sptype[nestedProcedures] = stored_procedure_type.package_spec;
979                                    if (ast.searchToken(TBaseType.rrw_body, 5) != null) {
980                                        sptype[nestedProcedures] = stored_procedure_type.package_body;
981                                    }
982                                    break;
983                                case sst_plsql_block:
984                                    sptype[nestedProcedures] = stored_procedure_type.block_with_declare;
985                                    if (ast.tokencode == TBaseType.rrw_begin) {
986                                        sptype[nestedProcedures] = stored_procedure_type.block_with_begin;
987                                    } else if (ast.tokencode == TBaseType.rrw_while
988                                            || ast.tokencode == TBaseType.rrw_for
989                                            || ast.tokencode == TBaseType.rrw_loop
990                                            || ast.tokencode == TBaseType.rrw_if) {
991                                        sptype[nestedProcedures] = stored_procedure_type.block_with_begin;
992                                    }
993                                    break;
994                                case sstplsql_createtrigger:
995                                    sptype[nestedProcedures] = stored_procedure_type.create_trigger;
996                                    break;
997                                case sstoraclecreatelibrary:
998                                    sptype[nestedProcedures] = stored_procedure_type.create_library;
999                                    break;
1000                                case sstplsql_createtype_placeholder:
1001                                    gst = EFindSqlStateType.stsql;
1002                                    break;
1003                                default:
1004                                    sptype[nestedProcedures] = stored_procedure_type.others;
1005                                    break;
1006                            }
1007
1008                            if (sptype[0] == stored_procedure_type.block_with_declare) {
1009                                // sd
1010                                endBySlashOnly = false;
1011                                procedure_status[0] = stored_procedure_status.is_as;
1012                            } else if (sptype[0] == stored_procedure_type.block_with_begin) {
1013                                // sb
1014                                endBySlashOnly = false;
1015                                procedure_status[0] = stored_procedure_status.body;
1016                            } else if (sptype[0] == stored_procedure_type.procedure) {
1017                                // ss
1018                                endBySlashOnly = false;
1019                                procedure_status[0] = stored_procedure_status.start;
1020                            } else if (sptype[0] == stored_procedure_type.function) {
1021                                // ss
1022                                endBySlashOnly = false;
1023                                procedure_status[0] = stored_procedure_status.start;
1024                            } else if (sptype[0] == stored_procedure_type.package_spec) {
1025                                // ss
1026                                endBySlashOnly = false;
1027                                procedure_status[0] = stored_procedure_status.start;
1028                            } else if (sptype[0] == stored_procedure_type.package_body) {
1029                                // ss
1030                                endBySlashOnly = false;
1031                                procedure_status[0] = stored_procedure_status.start;
1032                            } else if (sptype[0] == stored_procedure_type.create_trigger) {
1033                                // ss
1034                                endBySlashOnly = false;
1035                                procedure_status[0] = stored_procedure_status.start;
1036                                //procedure_status[0] = stored_procedure_status.body;
1037                            } else if (sptype[0] == stored_procedure_type.create_library) {
1038                                // ss
1039                                endBySlashOnly = false;
1040                                procedure_status[0] = stored_procedure_status.bodyend;
1041                            } else {
1042                                // so
1043                                endBySlashOnly = true;
1044                                procedure_status[0] = stored_procedure_status.bodyend;
1045                            }
1046                            //foundEnd = false;
1047                            if ((ast.tokencode == TBaseType.rrw_begin)
1048                                    || (ast.tokencode == TBaseType.rrw_package)
1049                                    //||(ast.tokencode == TBaseType.rrw_procedure)
1050                                    || (ast.searchToken(TBaseType.rrw_package, 4) != null)
1051                                    || (ast.tokencode == TBaseType.rrw_while)
1052                                    || (ast.tokencode == TBaseType.rrw_for)
1053                                    || (ast.tokencode == TBaseType.rrw_loop)
1054                                    || (ast.tokencode == TBaseType.rrw_if)
1055                            ) {
1056                                //waitingEnd = 1;
1057                                waitingEnds[nestedProcedures] = 1;
1058                            }
1059
1060                        } else {
1061                            gst = EFindSqlStateType.stsql;
1062                            if (isWithProcedure) {
1063                                // Mark WITH and first AS tokens so parser skips them
1064                                // This avoids S/R conflict with CTE (WITH name AS SELECT)
1065                                // Parser skips tokens with tsignorebyyacc status (TCustomParser line 420)
1066                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1067                                // Also mark the first AS (WITH name(...) AS PROCEDURE) so parser sees IDENT (...) PROCEDURE...
1068                                TSourceToken nameToken = ast.nextSolidToken();
1069                                if (nameToken != null) {
1070                                    TSourceToken afterName = nameToken.nextSolidToken();
1071                                    // Skip past parenthesized parameter list if present
1072                                    if (afterName != null && afterName.tokentype == ETokenType.ttleftparenthesis) {
1073                                        int parenDepth = 1;
1074                                        TSourceToken t = afterName.nextSolidToken();
1075                                        while (t != null && parenDepth > 0) {
1076                                            if (t.tokentype == ETokenType.ttleftparenthesis) parenDepth++;
1077                                            else if (t.tokentype == ETokenType.ttrightparenthesis) parenDepth--;
1078                                            if (parenDepth > 0) t = t.nextSolidToken();
1079                                        }
1080                                        if (t != null) afterName = t.nextSolidToken();
1081                                        else afterName = null;
1082                                    }
1083                                    if (afterName != null && afterName.tokencode == TBaseType.rrw_as) {
1084                                        afterName.tokenstatus = ETokenStatus.tsignorebyyacc;
1085                                    }
1086                                }
1087                            }
1088                            gcurrentsqlstatement.sourcetokenlist.add(ast);
1089                        }
1090                    } else {
1091                        //error tokentext found
1092
1093                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo)
1094                                , "Error when tokenlize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
1095
1096                        ast.tokentype = ETokenType.tttokenlizererrortoken;
1097                        gst = EFindSqlStateType.sterror;
1098
1099                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
1100                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
1101                        gcurrentsqlstatement.sourcetokenlist.add(ast);
1102
1103                    }
1104
1105                    break;
1106                } // stnormal
1107
1108                case stsqlplus: {
1109                    if (ast.insqlpluscmd) {
1110                        gcurrentsqlstatement.sourcetokenlist.add(ast);
1111                    } else {
1112                        gst = EFindSqlStateType.stnormal; //this tokentext must be newline,
1113                        gcurrentsqlstatement.sourcetokenlist.add(ast); // so add it here
1114                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1115                    }
1116
1117                    break;
1118                }//case stsqlplus
1119
1120                case stsql: {
1121                    if (gcurrentsqlstatement instanceof TRoutine) {
1122                        if (isDollarFunctionDelimiter(ast.tokencode, this.vendor)) {
1123                            if (inDollarBody) {
1124                                inDollarBody = false;
1125                            } else {
1126                                inDollarBody = true;
1127                            }
1128                        }
1129
1130                        if (inDollarBody) {
1131                            gcurrentsqlstatement.sourcetokenlist.add(ast);
1132                            continue;
1133                        }
1134
1135                        // Handle inline scripting body (AS DECLARE ... BEGIN ... END or AS BEGIN ... END)
1136                        // for CREATE FUNCTION with LANGUAGE SQL
1137                        if (ast.tokencode == TBaseType.rrw_as) {
1138                            TSourceToken tmpNext = ast.nextSolidToken();
1139                            if ((tmpNext != null) && ((tmpNext.tokencode == TBaseType.rrw_begin) || (tmpNext.tokencode == TBaseType.rrw_declare))) {
1140                                gst = EFindSqlStateType.ststoredprocedure;
1141                                nestedProcedures = 0;
1142                                if (tmpNext.tokencode == TBaseType.rrw_begin) {
1143                                    procedure_status[nestedProcedures] = stored_procedure_status.body;
1144                                } else {
1145                                    procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1146                                }
1147                                waitingEnds[nestedProcedures] = 0;
1148                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1149                                continue;
1150                            }
1151                        }
1152                    } else if (gcurrentsqlstatement instanceof TCreateTaskStmt) {
1153                        if (ast.tokencode == TBaseType.rrw_as) {
1154                            TSourceToken tmpNext = ast.nextSolidToken();
1155                            if ((tmpNext != null) && (tmpNext.tokencode == TBaseType.rrw_begin)) {
1156                                // begin ... end block in create task statement, mantisbt/view.php?id=3531
1157
1158                                gst = EFindSqlStateType.ststoredprocedure;
1159                                nestedProcedures = 0;
1160                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1161                                waitingEnds[nestedProcedures] = 0;
1162                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1163                                continue;
1164                            }
1165                        }
1166                    }
1167
1168                    if (ast.tokentype == ETokenType.ttsemicolon) {
1169                        gst = EFindSqlStateType.stnormal;
1170                        gcurrentsqlstatement.sourcetokenlist.add(ast);
1171                        gcurrentsqlstatement.semicolonended = ast;
1172                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1173                        continue;
1174                    }
1175
1176                    if (sourcetokenlist.sqlplusaftercurtoken()) //most probaly is / cmd
1177                    {
1178                        gst = EFindSqlStateType.stnormal;
1179                        gcurrentsqlstatement.sourcetokenlist.add(ast);
1180                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1181                        continue;
1182                    }
1183
1184                    // Check if a DDL keyword starts a new SQL statement without semicolon separator.
1185                    // Guard against false positives:
1186                    // - GRANT/REVOKE use CREATE/ALTER/DROP as privilege names
1187                    // - CREATE TASK body can contain any DDL
1188                    // - CREATE OR ALTER pattern: ALTER follows OR inside a CREATE statement
1189                    if (ast.tokencode == TBaseType.rrw_create
1190                            || ast.tokencode == TBaseType.rrw_alter
1191                            || ast.tokencode == TBaseType.rrw_drop) {
1192                        boolean shouldCheckSplit = true;
1193
1194                        // Don't split inside GRANT/REVOKE (CREATE/ALTER/DROP are privilege names)
1195                        if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstoraclegrant
1196                                || gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstoraclerevoke
1197                                || gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstGrant
1198                                || gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstRevoke) {
1199                            shouldCheckSplit = false;
1200                        }
1201
1202                        // Don't split inside CREATE TASK (task body can contain DDL)
1203                        if (gcurrentsqlstatement instanceof TCreateTaskStmt) {
1204                            shouldCheckSplit = false;
1205                        }
1206
1207                        // Don't split ALTER/DROP when preceded by OR (CREATE OR ALTER pattern)
1208                        if (shouldCheckSplit && (ast.tokencode == TBaseType.rrw_alter || ast.tokencode == TBaseType.rrw_drop)) {
1209                            TSourceToken prevSolid = ast.prevSolidToken();
1210                            if (prevSolid != null && prevSolid.tokencode == TBaseType.rrw_or) {
1211                                shouldCheckSplit = false;
1212                            }
1213                        }
1214
1215                        if (shouldCheckSplit) {
1216                            TCustomSqlStatement lcnextsqlstmt = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
1217                            if (lcnextsqlstmt != null) {
1218                                // Finalize current statement and start the new one
1219                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1220                                gcurrentsqlstatement = lcnextsqlstmt;
1221                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1222                                if (gcurrentsqlstatement.issnowflakeplsql()) {
1223                                    nestedProcedures = 0;
1224                                    gst = EFindSqlStateType.ststoredprocedure;
1225                                }
1226                                // else stay in stsql
1227                                continue;
1228                            }
1229                        }
1230                    }
1231
1232                    // Check if SET starts a new statement without semicolon separator.
1233                    // Guard against false positives:
1234                    // - UPDATE ... SET is the SET clause of an UPDATE statement
1235                    // - ALTER SESSION/TABLE ... SET is part of the ALTER statement
1236                    // - CREATE TASK body can contain SET
1237                    if (ast.tokencode == TBaseType.rrw_set) {
1238                        boolean shouldCheckSplit = true;
1239
1240                        // Don't split SET inside UPDATE (SET clause)
1241                        if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstupdate) {
1242                            shouldCheckSplit = false;
1243                        }
1244
1245                        // Don't split SET inside any ALTER statement (ALTER SESSION SET, ALTER TABLE SET,
1246                        // ALTER VIEW SET, ALTER FUNCTION SET, etc.)
1247                        {
1248                            String typeName = gcurrentsqlstatement.sqlstatementtype.name().toLowerCase();
1249                            if (typeName.startsWith("sstalter") || typeName.startsWith("sst_alter")) {
1250                                shouldCheckSplit = false;
1251                            }
1252                        }
1253
1254                        // Don't split inside CREATE TASK (task body can contain SET)
1255                        if (gcurrentsqlstatement instanceof TCreateTaskStmt) {
1256                            shouldCheckSplit = false;
1257                        }
1258
1259                        // Don't split inside MERGE (SET clause in WHEN MATCHED THEN UPDATE SET)
1260                        if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstmerge) {
1261                            shouldCheckSplit = false;
1262                        }
1263
1264                        if (shouldCheckSplit) {
1265                            TCustomSqlStatement lcnextsqlstmt = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
1266                            if (lcnextsqlstmt != null) {
1267                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1268                                gcurrentsqlstatement = lcnextsqlstmt;
1269                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1270                                continue;
1271                            }
1272                        }
1273                    }
1274
1275                    gcurrentsqlstatement.sourcetokenlist.add(ast);
1276                    break;
1277                }//case stsql
1278
1279                case ststoredprocedure: {
1280                    if (procedure_status[nestedProcedures] != stored_procedure_status.bodyend) {
1281                        gcurrentsqlstatement.sourcetokenlist.add(ast);
1282                    }
1283
1284                    switch (procedure_status[nestedProcedures]) {
1285                        case start:
1286                            if ((ast.tokencode == TBaseType.rrw_as) || (ast.tokencode == TBaseType.rrw_is)) {
1287                                // s1
1288                                if (sptype[nestedProcedures] != stored_procedure_type.create_trigger) {
1289                                    if ((sptype[0] == stored_procedure_type.package_spec) && (nestedProcedures > 0)) {
1290                                        //when it's a package specification, only top level accept as/is
1291                                    } else {
1292                                        procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1293                                        if (ast.searchToken("language", 1) != null) {
1294                                            // if as language is used in create function, then switch state to stored_procedure_status.body directly.
1295//                                        CREATE OR REPLACE FUNCTION THING.addressparse(p_addressline1 VARCHAR2) RETURN VARCHAR2 AUTHID DEFINER
1296//                                        as Language JAVA NAME 'AddressParser.parse(java.lang.String) return java.lang.String';
1297//                                        /
1298                                            if (nestedProcedures == 0) {
1299                                                //  procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1300                                                gst = EFindSqlStateType.stsql;
1301                                            } else {
1302                                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1303                                                nestedProcedures--;
1304                                                //if (nestedProcedures > 0){ nestedProcedures--;}
1305                                            }
1306
1307                                        }
1308                                    }
1309                                }
1310                            } else if (ast.tokencode == TBaseType.rrw_begin) {
1311                                // s4
1312                                if (sptype[nestedProcedures] == stored_procedure_type.create_trigger)
1313                                    waitingEnds[nestedProcedures]++;
1314
1315                                if (nestedProcedures > 0) {
1316                                    nestedProcedures--;
1317                                }
1318                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1319                            } else if (ast.tokencode == TBaseType.rrw_end) {
1320                                //s10
1321                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures - 1] == 1)
1322                                        && ((sptype[nestedProcedures - 1] == stored_procedure_type.package_body)
1323                                        || (sptype[nestedProcedures - 1] == stored_procedure_type.package_spec))) {
1324                                    nestedProcedures--;
1325                                    procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1326                                }
1327                            } else if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) {
1328                                //s3
1329                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures] == 0)
1330                                        && (procedure_status[nestedProcedures - 1] == stored_procedure_status.is_as)) {
1331                                    nestedProcedures--;
1332                                    nestedProcedures++;
1333                                    waitingEnds[nestedProcedures] = 0;
1334                                    procedure_status[nestedProcedures] = stored_procedure_status.start;
1335                                }
1336                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger) && (ast.tokencode == TBaseType.rrw_declare)) {
1337                                procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1338                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger) && (ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) {
1339                                // TPlsqlStatementParse(asqlstatement).TerminatorToken := ast;
1340                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1341                                gst = EFindSqlStateType.stnormal;
1342                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1343
1344                                //make / a sqlplus cmd
1345                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1346                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1347                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1348                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger)) {
1349                                if (ast.tokencode == TBaseType.rrw_trigger) {
1350                                    TSourceToken compoundSt = ast.searchToken(TBaseType.rrw_oracle_compound, -1);
1351                                    if (compoundSt != null) {
1352                                        //it's trigger with compound trigger block
1353                                        procedure_status[nestedProcedures] = stored_procedure_status.body;
1354                                        waitingEnds[nestedProcedures]++;
1355                                    }
1356                                }
1357                            } else if ((sptype[nestedProcedures] == stored_procedure_type.function) && (ast.tokencode == TBaseType.rrw_teradata_using)) {
1358                                if ((ast.searchToken("aggregate", -1) != null) || (ast.searchToken("pipelined", -1) != null)) {
1359                                    if (nestedProcedures == 0) {
1360                                        //  procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1361                                        gst = EFindSqlStateType.stsql;
1362                                    } else {
1363                                        procedure_status[nestedProcedures] = stored_procedure_status.body;
1364                                        nestedProcedures--;
1365                                    }
1366                                }
1367
1368                            } else {
1369                                //other tokens, do nothing
1370                                if (ast.tokencode == TBaseType.rrw_snowflake_language) {
1371                                    // check next token which is the language used by this stored procedure
1372                                    TSourceToken nextSt = ast.nextSolidToken();
1373                                    if (nextSt != null) {
1374                                        if (gcurrentsqlstatement instanceof TRoutine) {  // can be TCreateProcedureStmt or TCreateFunctionStmt
1375                                            TRoutine p = (TRoutine) gcurrentsqlstatement;
1376                                            p.setRoutineLanguage(nextSt.toString());
1377                                            // Switch to stsql for all languages (SQL, JavaScript, Python, etc.)
1378                                            // The stsql state has proper $$ dollar delimiter handling for procedure bodies.
1379                                            // For inline scripting bodies (AS BEGIN...END), stsql detects AS+BEGIN/DECLARE
1380                                            // and switches back to ststoredprocedure as needed.
1381                                            gst = EFindSqlStateType.stsql;
1382                                        }
1383                                    }
1384                                }
1385                            }
1386                            break;
1387                        case is_as:
1388                            if (isDollarFunctionDelimiter(ast.tokencode, this.vendor)) {
1389                                // Procedure body is dollar-quoted (AS $$...$$).
1390                                // Switch to stsql mode which has proper dollar delimiter tracking.
1391                                gst = EFindSqlStateType.stsql;
1392                                inDollarBody = true;
1393                                continue;
1394                            } else if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) {
1395                                // s2
1396                                nestedProcedures++;
1397                                if (nestedProcedures > stored_procedure_nested_level - 1) {
1398                                    gst = EFindSqlStateType.sterror;
1399                                    nestedProcedures--;
1400                                } else {
1401                                    waitingEnds[nestedProcedures] = 0;
1402                                    procedure_status[nestedProcedures] = stored_procedure_status.start;
1403                                }
1404
1405                            } else if (ast.tokencode == TBaseType.rrw_begin) {
1406                                // s5
1407                                if ((nestedProcedures == 0) &&
1408                                        ((sptype[nestedProcedures] == stored_procedure_type.package_body)
1409                                                || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) {
1410                                    //top level package or package body's BEGIN keyword already count,
1411                                    // so don't increase waitingEnds[nestedProcedures] here
1412
1413                                } else {
1414                                    waitingEnds[nestedProcedures]++;
1415                                }
1416                                procedure_status[nestedProcedures] = stored_procedure_status.body;
1417                            } else if (ast.tokencode == TBaseType.rrw_end) {
1418                                // s6
1419                                if ((nestedProcedures == 0) && (waitingEnds[nestedProcedures] == 1) &&
1420                                        ((sptype[nestedProcedures] == stored_procedure_type.package_body) || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) {
1421                                    procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1422                                    waitingEnds[nestedProcedures]--;
1423                                } else {
1424                                    waitingEnds[nestedProcedures]--;
1425                                }
1426                            } else if (ast.tokencode == TBaseType.rrw_case) {
1427//                            if (ast.searchToken(TBaseType.rrw_end,-1) == null){
1428//                                //this is not case after END
1429//                             waitingEnds[nestedProcedures]++;
1430//                            }
1431                                if (ast.searchToken(';', 1) == null) {
1432                                    //this is not case before ;
1433                                    waitingEnds[nestedProcedures]++;
1434                                }
1435                            } else {
1436                                //other tokens, do nothing
1437                            }
1438                            break;
1439                        case body:
1440                            if ((ast.tokencode == TBaseType.rrw_begin)) {
1441                                waitingEnds[nestedProcedures]++;
1442                            } else if (ast.tokencode == TBaseType.rrw_if) {
1443                                if (ast.searchToken(TBaseType.rrw_snowflake_exists, 1) != null) {
1444                                    //drop table if exists SANDBOX.ANALYSIS_CONTENT.TABLEAU_DATES;
1445                                    // don't need END for the above if exists clause
1446                                } else if (ast.searchToken(';', 2) == null) {
1447                                    //this is not if before ;
1448
1449                                    // 2015-02-27, change 1 to 2 make it able to detect label name after case
1450                                    // like this: END CASE l1;
1451                                    waitingEnds[nestedProcedures]++;
1452                                }
1453                            } else if (ast.tokencode == TBaseType.rrw_for) {
1454                                if (ast.searchToken(';', 2) == null) {
1455                                    //this is not for before ;
1456
1457                                    // 2015-02-27, change 1 to 2 make it able to detect label name after case
1458                                    // like this: END CASE l1;
1459                                    waitingEnds[nestedProcedures]++;
1460                                }
1461                            } else if (ast.tokencode == TBaseType.rrw_case) {
1462//                            if (ast.searchToken(TBaseType.rrw_end,-1) == null){
1463//                                //this is not case after END
1464//                             waitingEnds[nestedProcedures]++;
1465//                            }
1466                                if (ast.searchToken(';', 2) == null) {
1467                                    //this is not case before ;
1468                                    if (ast.searchToken(TBaseType.rrw_end, -1) == null) {
1469                                        waitingEnds[nestedProcedures]++;
1470                                    }
1471                                }
1472                            } else if (ast.tokencode == TBaseType.rrw_while) {
1473                                if (!((ast.searchToken(TBaseType.rrw_end, -1) != null)
1474                                        && (ast.searchToken(';', 2) != null))) {
1475                                    // exclude while like this:
1476                                    // end while [labelname];
1477                                    waitingEnds[nestedProcedures]++;
1478                                }
1479                            } else if (ast.tokencode == TBaseType.rrw_loop) {
1480                                if (!((ast.searchToken(TBaseType.rrw_end, -1) != null)
1481                                        && (ast.searchToken(';', 2) != null))) {
1482                                    // exclude loop like this:
1483                                    // end loop [labelname];
1484
1485                                    // Also exclude LOOP when it follows a WHILE or FOR condition
1486                                    // (e.g., WHILE condition LOOP, FOR var IN ... LOOP)
1487                                    // In those cases, WHILE/FOR already incremented waitingEnds.
1488                                    // Only standalone LOOP (preceded by ; or block start) should increment.
1489                                    TSourceToken prevSolid = ast.prevSolidToken();
1490                                    boolean isStandaloneLoop = (prevSolid == null)
1491                                            || (prevSolid.tokentype == ETokenType.ttsemicolon)
1492                                            || (prevSolid.tokencode == TBaseType.rrw_begin)
1493                                            || (prevSolid.tokencode == TBaseType.rrw_then)
1494                                            || (prevSolid.tokencode == TBaseType.rrw_else)
1495                                            || (prevSolid.tokencode == TBaseType.rrw_loop);
1496                                    if (isStandaloneLoop) {
1497                                        waitingEnds[nestedProcedures]++;
1498                                    }
1499                                }
1500
1501//                            if (ast.searchToken(TBaseType.rrw_end,-1) == null){
1502//                                //this is not loop after END
1503//                             waitingEnds[nestedProcedures]++;
1504////                            }
1505////                            if (ast.searchToken(';',2) == null){
1506////                                //this is no loop before ;
1507////                             waitingEnds[nestedProcedures]++;
1508//                            } else if (ast.searchToken(TBaseType.rrw_null,1) != null){
1509//                                // mantis bug tracking system:   #65
1510//                                waitingEnds[nestedProcedures]++;
1511//                            }
1512                            } else if (ast.tokencode == TBaseType.rrw_end) {
1513                                //foundEnd = true;
1514                                waitingEnds[nestedProcedures]--;
1515                                //if (waitingEnd < 0) { waitingEnd = 0;}
1516                                if (waitingEnds[nestedProcedures] == 0) {
1517                                    if (nestedProcedures == 0) {
1518                                        // s7
1519                                        procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
1520                                    } else {
1521                                        // s71
1522                                        nestedProcedures--;
1523                                        procedure_status[nestedProcedures] = stored_procedure_status.is_as;
1524                                    }
1525                                }
1526                            } else if ((waitingEnds[nestedProcedures] == 0) && (ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) //and (prevst.NewlineIsLastTokenInTailerToken)) then
1527                            {
1528                                //sql ref: c:\prg\gsqlparser\Test\TestCases\oracle\createtrigger.sql, line 53
1529                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1530                                gst = EFindSqlStateType.stnormal;
1531                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1532
1533                                //make / a sqlplus cmd
1534                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1535                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1536                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1537                            }
1538                            break;
1539                        case bodyend:
1540                            if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) //and (prevst.NewlineIsLastTokenInTailerToken)) then
1541                            {
1542                                // TPlsqlStatementParse(asqlstatement).TerminatorToken := ast;
1543                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1544                                gst = EFindSqlStateType.stnormal;
1545                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1546
1547                                //make / a sqlplus cmd
1548                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1549                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1550                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1551                            } else if ((ast.tokentype == ETokenType.ttperiod) && (sourcetokenlist.returnaftercurtoken(false)) && (sourcetokenlist.returnbeforecurtoken(false))) {    // single dot at a seperate line
1552                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
1553                                gst = EFindSqlStateType.stnormal;
1554                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1555
1556                                //make ttperiod a sqlplus cmd
1557                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
1558                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1559                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1560                            } else if ((ast.searchToken(TBaseType.rrw_package, 1) != null) && (!endBySlashOnly)) {
1561                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1562                                gst = EFindSqlStateType.stnormal;
1563                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1564                            } else if ((ast.searchToken(TBaseType.rrw_procedure, 1) != null) && (!endBySlashOnly)) {
1565                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1566                                gst = EFindSqlStateType.stnormal;
1567                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1568                            } else if ((ast.searchToken(TBaseType.rrw_function, 1) != null) && (!endBySlashOnly)) {
1569                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1570                                gst = EFindSqlStateType.stnormal;
1571                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1572                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) && (ast.searchToken(TBaseType.rrw_package, 4) != null) && (!endBySlashOnly)) {
1573                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1574                                gst = EFindSqlStateType.stnormal;
1575                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1576                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) && (ast.searchToken(TBaseType.rrw_library, 4) != null) && (!endBySlashOnly)) {
1577                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1578                                gst = EFindSqlStateType.stnormal;
1579                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1580                            } else if ((ast.searchToken(TBaseType.rrw_alter, 1) != null) && (ast.searchToken(TBaseType.rrw_trigger, 2) != null) && (!endBySlashOnly)) {
1581                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1582                                gst = EFindSqlStateType.stnormal;
1583                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1584                            } else if ((ast.searchToken(TBaseType.rrw_select, 1) != null) && (!endBySlashOnly)) {
1585                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1586                                gst = EFindSqlStateType.stnormal;
1587                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1588                            } else if ((ast.searchToken(TBaseType.rrw_commit, 1) != null) && (!endBySlashOnly)) {
1589                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1590                                gst = EFindSqlStateType.stnormal;
1591                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1592                            } else if (((ast.searchToken(TBaseType.rrw_grant, 1) != null)
1593                                    || (ast.searchToken(TBaseType.rrw_revoke, 1) != null)) && (!endBySlashOnly)) {
1594                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1595                                gst = EFindSqlStateType.stnormal;
1596                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1597                            } else if ((gcurrentsqlstatement instanceof TCreateTaskStmt) && (ast.tokencode == ';')) {
1598                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1599                                gst = EFindSqlStateType.stnormal;
1600                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1601                            } else if ((ast.tokentype == ETokenType.ttsemicolon) && (!endBySlashOnly)
1602                                    && isStandaloneScriptingBlock(gcurrentsqlstatement)) {
1603                                // Semicolon terminates standalone scripting blocks (WHILE/FOR/LOOP/IF)
1604                                // Wrap with synthetic BEGIN...END so the parser sees pl_block: BEGIN proc_stmt END
1605                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1606
1607                                TSourceToken synBegin = new TSourceToken("BEGIN");
1608                                synBegin.tokencode = TBaseType.rrw_begin;
1609                                synBegin.tokentype = ETokenType.ttkeyword;
1610                                gcurrentsqlstatement.sourcetokenlist.add(0, synBegin);
1611
1612                                TSourceToken synEnd = new TSourceToken("END");
1613                                synEnd.tokencode = TBaseType.rrw_end;
1614                                synEnd.tokentype = ETokenType.ttkeyword;
1615                                gcurrentsqlstatement.sourcetokenlist.add(synEnd);
1616
1617                                TSourceToken synSemicolon = new TSourceToken(";");
1618                                synSemicolon.tokencode = ';';
1619                                synSemicolon.tokentype = ETokenType.ttsemicolon;
1620                                gcurrentsqlstatement.sourcetokenlist.add(synSemicolon);
1621
1622                                gst = EFindSqlStateType.stnormal;
1623                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
1624                            } else {
1625                                gcurrentsqlstatement.sourcetokenlist.add(ast);
1626                            }
1627                            break;
1628                        case end:
1629                            break;
1630                        default:
1631                            break;
1632                    }
1633
1634
1635                    if (ast.tokencode == TBaseType.sqlpluscmd) {
1636                        //change tokencode back to keyword or TBaseType.ident, because sqlplus cmd
1637                        //in a sql statement(almost is plsql block) is not really a sqlplus cmd
1638                        int m = flexer.getkeywordvalue(ast.getAstext());
1639                        if (m != 0) {
1640                            ast.tokencode = m;
1641                        } else if (ast.tokentype == ETokenType.ttslash) {
1642                            ast.tokencode = '/';
1643                        } else {
1644                            ast.tokencode = TBaseType.ident;
1645                        }
1646                    }
1647
1648                    final int wrapped_keyword_max_pos = 20;
1649                    if ((ast.tokencode == TBaseType.rrw_wrapped) && (ast.posinlist - gcurrentsqlstatement.sourcetokenlist.get(0).posinlist < wrapped_keyword_max_pos)) {
1650                        if (gcurrentsqlstatement instanceof TCommonStoredProcedureSqlStatement) {
1651                            ((TCommonStoredProcedureSqlStatement) gcurrentsqlstatement).setWrapped(true);
1652                        }
1653
1654                        if (gcurrentsqlstatement instanceof TPlsqlCreatePackage) {
1655                            if (ast.prevSolidToken() != null) {
1656                                ((TPlsqlCreatePackage) gcurrentsqlstatement).setPackageName(fparser.getNf().createObjectNameWithPart(ast.prevSolidToken()));
1657                            }
1658                        }
1659                    }
1660
1661                    break;
1662                } //ststoredprocedure
1663            } //switch
1664        }//for
1665
1666        //last statement
1667        if ((gcurrentsqlstatement != null) &&
1668                ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) ||
1669                        (gst == EFindSqlStateType.sterror))) {
1670            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder);
1671        }
1672
1673        return syntaxErrors.size();
1674    }
1675
1676    // ========== Helper Methods ==========
1677
1678    /**
1679     * Check if a scripting block statement (WHILE/FOR/LOOP/IF) needs synthetic BEGIN...END wrapping.
1680     * Returns true if the first solid token in the token list is a scripting keyword.
1681     */
1682    private boolean isStandaloneScriptingBlock(TCustomSqlStatement stmt) {
1683        for (int i = 0; i < stmt.sourcetokenlist.size(); i++) {
1684            TSourceToken t = stmt.sourcetokenlist.get(i);
1685            if (t.tokentype == ETokenType.ttwhitespace || t.tokentype == ETokenType.ttreturn) continue;
1686            if (t.tokencode == TBaseType.cmtdoublehyphen || t.tokencode == TBaseType.cmtslashstar) continue;
1687            return t.tokencode == TBaseType.rrw_while
1688                    || t.tokencode == TBaseType.rrw_for
1689                    || t.tokencode == TBaseType.rrw_loop
1690                    || t.tokencode == TBaseType.rrw_if;
1691        }
1692        return false;
1693    }
1694
1695    /**
1696     * Check if a position is valid for treating division operator as SQL*Plus command.
1697     */
1698    private boolean IsValidPlaceForDivToSqlplusCmd(TSourceTokenList pstlist, int pPos) {
1699        boolean ret = false;
1700
1701        if ((pPos <= 0) || (pPos > pstlist.size() - 1)) return ret;
1702        //tokentext directly before div must be ttreturn without space appending it
1703        TSourceToken lcst = pstlist.get(pPos - 1);
1704        if (lcst.tokencode == TBaseType.lexnewline) {
1705            String astext = lcst.getAstext();
1706            ret = (astext.length() > 0 && astext.charAt(astext.length() - 1) == '\n');
1707        }
1708
1709        return ret;
1710    }
1711
1712    /**
1713     * Placeholder function for PostgreSQL-style SQL*Plus commands.
1714     * Always returns false for Snowflake (not applicable).
1715     */
1716    private boolean isvalidsqlpluscmdInPostgresql(String astr) {
1717        return false;
1718    }
1719
1720    // Note: isDollarFunctionDelimiter() is now inherited from AbstractSqlParser
1721    // The parent implementation handles all PostgreSQL-family databases including Snowflake
1722
1723    @Override
1724    public String toString() {
1725        return "SnowflakeSqlParser{vendor=" + vendor + "}";
1726    }
1727}