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