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.TLexerBigquery;
009import gudusoft.gsqlparser.TParserBigquery;
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.nodes.TTypeName;
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;
033
034import java.io.BufferedReader;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.List;
038import java.util.Stack;
039
040import static gudusoft.gsqlparser.ESqlStatementType.*;
041
042/**
043 * Google BigQuery SQL parser implementation.
044 *
045 * <p>This parser handles BigQuery-specific SQL syntax including:
046 * <ul>
047 *   <li>BigQuery scripting language (BEGIN...END blocks, IF/WHILE/FOR/LOOP/REPEAT statements)</li>
048 *   <li>BigQuery UDFs (CREATE FUNCTION with SQL or JavaScript)</li>
049 *   <li>BigQuery procedures (CREATE PROCEDURE with BEGIN...END blocks)</li>
050 *   <li>Special type handling (STRUCT, ARRAY, DATE/TIME/TIMESTAMP literals)</li>
051 *   <li>Backtick-quoted identifiers including qualified names (`schema.table`)</li>
052 *   <li>BigQuery-specific keywords and functions</li>
053 * </ul>
054 *
055 * <p><b>Design Notes:</b>
056 * <ul>
057 *   <li>Extends {@link AbstractSqlParser} using the template method pattern</li>
058 *   <li>Uses {@link TLexerBigquery} for tokenization</li>
059 *   <li>Uses {@link TParserBigquery} for parsing</li>
060 *   <li>Delimiter character: ';' for SQL statements</li>
061 *   <li>Splits backtick-quoted qualified names (`schema.table`) into individual tokens</li>
062 * </ul>
063 *
064 * <p><b>Usage Example:</b>
065 * <pre>
066 * // Get BigQuery parser from factory
067 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvbigquery);
068 *
069 * // Build context
070 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvbigquery)
071 *     .sqlText("SELECT * FROM `project.dataset.table` WHERE region = 'US'")
072 *     .build();
073 *
074 * // Parse
075 * SqlParseResult result = parser.parse(context);
076 *
077 * // Access statements
078 * TStatementList statements = result.getSqlStatements();
079 * </pre>
080 *
081 * @see SqlParser
082 * @see AbstractSqlParser
083 * @see TLexerBigquery
084 * @see TParserBigquery
085 * @since 3.2.0.0
086 */
087public class BigQuerySqlParser extends AbstractSqlParser {
088
089    /**
090     * Construct BigQuery SQL parser.
091     * <p>
092     * Configures the parser for BigQuery database with default delimiter (;).
093     * <p>
094     * Following the original TGSqlParser pattern, the lexer and parser are
095     * created once in the constructor and reused for all parsing operations.
096     */
097    public BigQuerySqlParser() {
098        super(EDbVendor.dbvbigquery);
099        this.delimiterChar = ';';
100        this.defaultDelimiterStr = ";";
101
102        // Create lexer once - will be reused for all parsing operations
103        this.flexer = new TLexerBigquery();
104        this.flexer.delimiterchar = this.delimiterChar;
105        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
106
107        // Set parent's lexer reference for shared tokenization logic
108        this.lexer = this.flexer;
109
110        // Create parser once - will be reused for all parsing operations
111        this.fparser = new TParserBigquery(null);
112        this.fparser.lexer = this.flexer;
113    }
114
115    // ========== Parser Components ==========
116
117    /** The BigQuery lexer used for tokenization */
118    public TLexerBigquery flexer;
119
120    /** SQL parser (for BigQuery statements) */
121    private TParserBigquery fparser;
122
123    /** Current statement being built during extraction */
124    private TCustomSqlStatement gcurrentsqlstatement;
125
126    /** Parser context for current operation */
127    private ParserContext parserContext;
128
129    /** User-defined delimiter string for MySQL DELIMITER command */
130    private String userDelimiterStr;
131
132    // Stored procedure parsing state tracking
133    private enum stored_procedure_type {
134        procedure, function, package_spec, package_body, block_with_declare,
135        block_with_begin, create_trigger, create_library, others
136    }
137
138    private enum stored_procedure_status {
139        start, is_as, body, bodyend, end
140    }
141
142    private static final int stored_procedure_nested_level = 50;
143
144    // Note: Global context and frame stack fields inherited from AbstractSqlParser:
145    // - protected TContext globalContext
146    // - protected TSQLEnv sqlEnv
147    // - protected Stack<TFrame> frameStack
148    // - protected TFrame globalFrame
149
150    // ========== AbstractSqlParser Abstract Methods Implementation ==========
151
152    /**
153     * Return the BigQuery lexer instance.
154     */
155    @Override
156    protected TCustomLexer getLexer(ParserContext context) {
157        return this.flexer;
158    }
159
160    /**
161     * Return the BigQuery SQL parser instance with updated token list.
162     */
163    @Override
164    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
165        this.fparser.sourcetokenlist = tokens;
166        return this.fparser;
167    }
168
169    /**
170     * BigQuery does not use a secondary parser (unlike Oracle with PL/SQL).
171     */
172    @Override
173    protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) {
174        return null;
175    }
176
177    /**
178     * Call BigQuery-specific tokenization logic.
179     * <p>
180     * Delegates to dobigquerysqltexttotokenlist which handles BigQuery's
181     * specific keyword recognition, backtick-quoted identifiers, and
182     * qualified name splitting.
183     */
184    @Override
185    protected void tokenizeVendorSql() {
186        dobigquerysqltexttotokenlist();
187    }
188
189    /**
190     * Setup BigQuery parser for raw statement extraction.
191     * <p>
192     * BigQuery uses a single parser, so we inject sqlcmds and update
193     * the token list for the main parser only.
194     */
195    @Override
196    protected void setupVendorParsersForExtraction() {
197        // Inject sqlcmds into parser (required for make_stmt)
198        this.fparser.sqlcmds = this.sqlcmds;
199
200        // Update token list for parser
201        this.fparser.sourcetokenlist = this.sourcetokenlist;
202    }
203
204    /**
205     * Call BigQuery-specific raw statement extraction logic.
206     * <p>
207     * Delegates to dobigquerygetrawsqlstatements which handles BigQuery's
208     * statement delimiters and scripting language boundaries (BEGIN...END blocks,
209     * IF/WHILE/FOR/LOOP statements).
210     */
211    @Override
212    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
213        int errorCount = dobigquerygetrawsqlstatements(builder);
214        // Error count is tracked internally; errors are already added to syntaxErrors list
215
216        // Set the extracted statements in the builder
217        builder.sqlStatements(this.sqlstatements);
218    }
219
220    // ========== Tokenization Methods ==========
221
222    /**
223     * Tokenize BigQuery SQL text into a list of tokens.
224     * <p>
225     * This method handles BigQuery-specific token processing:
226     * <ul>
227     *   <li>Splits backtick-quoted qualified names (`schema.table`) into separate tokens</li>
228     *   <li>Handles all standard SQL tokens (keywords, identifiers, operators, etc.)</li>
229     * </ul>
230     * <p>
231     * Migrated from TGSqlParser.dobigquerysqltexttotokenlist()
232     */
233    private void dobigquerysqltexttotokenlist() {
234        TSourceToken asourcetoken, lcprevst;
235        int yychar;
236
237        flexer.tmpDelimiter = "";
238
239        asourcetoken = getanewsourcetoken();
240        if (asourcetoken == null) return;
241        yychar = asourcetoken.tokencode;
242
243        while (yychar > 0) {
244            if (asourcetoken != null) {
245                sourcetokenlist.add(asourcetoken);
246            }
247
248            asourcetoken = getanewsourcetoken();
249            if (asourcetoken == null) break;
250            yychar = asourcetoken.tokencode;
251
252            // `schema.table_name` - split into separate tokens
253            if ((asourcetoken.tokencode == TBaseType.ident)
254                    && (asourcetoken.toString().startsWith("`")) && (asourcetoken.toString().endsWith("`"))
255                    && (asourcetoken.toString().indexOf(".") > 0)) {
256                yychar = splitQualifiedNameInBacktick(asourcetoken);
257                asourcetoken = null;
258            }
259        }
260    }
261
262    /**
263     * Turn one token: `schema.table_name` into 3 tokens: `schema` . `table_name`
264     * <p>
265     * This helper method splits backtick-quoted qualified names into individual
266     * identifier and period tokens, preserving line/column information for each part.
267     * <p>
268     * Migrated from TGSqlParser.splitQualifiedNameInBacktick()
269     *
270     * @param asourcetoken the token to split
271     * @return the token code of the last token created
272     */
273    private int splitQualifiedNameInBacktick(TSourceToken asourcetoken) {
274        int yychar = 0;
275
276        List<String> elephantList = Arrays.asList(TBaseType.getTextWithoutQuoted(asourcetoken.toString()).split("\\."));
277        int p = 0, offset = 0;
278        for (String s : elephantList) {
279            TSourceToken pst = new TSourceToken("`" + s + "`");
280            pst.tokencode = asourcetoken.tokencode;
281            pst.tokentype = asourcetoken.tokentype;
282            pst.tokenstatus = asourcetoken.tokenstatus;
283            pst.splitFrom = asourcetoken;
284            pst.lineNo = asourcetoken.lineNo;
285            pst.columnNo = asourcetoken.columnNo + offset;
286            if (p == 0) offset++; // this count the first ` token
287            offset = offset + s.length();
288            pst.container = sourcetokenlist;
289            if (p > 0) { // 第一个token使用被拆分前那个token的位置,从第二个开始的token,需要先把列表的位置指针加 1
290                sourcetokenlist.curpos = sourcetokenlist.curpos + 1;
291            }
292            pst.posinlist = sourcetokenlist.curpos;
293
294            sourcetokenlist.add(pst);
295            yychar = pst.tokencode;
296
297            if (p != elephantList.size() - 1) {
298                //`schema.table_name`, add period token in the middle of the backtick included identifier.
299                TSourceToken periodst = new TSourceToken(".");
300                periodst.tokencode = '.';
301                periodst.tokentype = ETokenType.ttperiod;
302                periodst.tokenstatus = asourcetoken.tokenstatus;
303                periodst.splitFrom = asourcetoken;
304                periodst.lineNo = asourcetoken.lineNo;
305                periodst.columnNo = asourcetoken.columnNo + offset;
306                offset++;
307                periodst.container = sourcetokenlist;
308                sourcetokenlist.curpos = sourcetokenlist.curpos + 1;
309                periodst.posinlist = sourcetokenlist.curpos;
310                sourcetokenlist.add(periodst);
311                yychar = periodst.tokencode;
312            }
313
314            p++;
315        }
316
317        return yychar;
318    }
319
320    // ========== Raw Statement Extraction ==========
321
322    /**
323     * Check if token code is one that pairs with END keyword.
324     * <p>
325     * Used for BigQuery scripting language to track nested BEGIN/IF/CASE/LOOP/WHILE/FOR/REPEAT blocks.
326     * <p>
327     * Migrated from TGSqlParser.checkTokenPairWithEnd()
328     */
329    private boolean checkTokenPairWithEnd(int tokencode) {
330        return ((tokencode == TBaseType.rrw_if) || (tokencode == TBaseType.rrw_case)
331                || (tokencode == TBaseType.rrw_loop) || (tokencode == TBaseType.rrw_repeat)
332                || (tokencode == TBaseType.rrw_while) || (tokencode == TBaseType.rrw_for)
333                || (tokencode == TBaseType.rrw_case));
334    }
335
336    /**
337     * Extract raw SQL statements from the token list.
338     * <p>
339     * This method separates individual SQL statements without full syntax checking.
340     * It handles BigQuery-specific syntax including:
341     * <ul>
342     *   <li>Stored procedures and functions with BEGIN...END blocks</li>
343     *   <li>Scripting language statements (IF, WHILE, FOR, LOOP, REPEAT, CASE)</li>
344     *   <li>STRUCT constructor special handling</li>
345     *   <li>Type casting with literals (DATE '2021-01-01')</li>
346     *   <li>Nested procedure tracking</li>
347     * </ul>
348     * <p>
349     * Migrated from TGSqlParser.dobigquerygetrawsqlstatements()
350     *
351     * @param builder the result builder to populate
352     * @return error count
353     */
354    private int dobigquerygetrawsqlstatements(SqlParseResult.Builder builder) {
355        int errorcount = 0;
356        gcurrentsqlstatement = null;
357        EFindSqlStateType gst = EFindSqlStateType.stnormal;
358        int i, c, beginNested = 0, waitingEnd = 0;
359        TSourceToken ast = null, lcprevsolidtoken = null;
360        boolean waitingDelimiter = false;
361
362        int waitingEnds[] = new int[stored_procedure_nested_level];
363        stored_procedure_type sptype[] = new stored_procedure_type[stored_procedure_nested_level];
364        stored_procedure_status procedure_status[] = new stored_procedure_status[stored_procedure_nested_level];
365        boolean endBySlashOnly = true;
366        int nestedProcedures = 0, nestedParenthesis = 0;
367
368        //reset delimiter
369        userDelimiterStr = defaultDelimiterStr;
370
371        for (i = 0; i < sourcetokenlist.size(); i++) {
372            if ((ast != null) && (ast.issolidtoken()))
373                lcprevsolidtoken = ast;
374
375            ast = sourcetokenlist.get(i);
376            sourcetokenlist.curpos = i;
377
378            // Handle STRUCT constructor: STRUCT(...) -> mark as struct constructor
379            if (ast.tokencode == TBaseType.rrw_bigquery_struct) {
380                TSourceToken st1 = ast.nextSolidToken();
381                if (st1 != null) {
382                    if (st1.tokencode == '(') {
383                        ast.tokencode = TBaseType.rrw_bigquery_struct_constructor;
384                    }
385                }
386            }
387            // Handle type casting: DATE '2021-01-01' -> mark DATE as datatype used to cast
388            else if ((ast.tokencode == TBaseType.sconst)) {
389                if (lcprevsolidtoken != null && TTypeName.searchTypeByName(lcprevsolidtoken.toString()) != null) {
390                    if (lcprevsolidtoken.tokencode != TBaseType.rrw_interval) {
391                        lcprevsolidtoken.tokencode = TBaseType.rrw_bigquery_datatype_used_to_cast;
392                    }
393                }
394            }
395            // Handle TIME/DATE: could be function or type literal
396            else if ((ast.tokencode == TBaseType.rrw_time) || (ast.tokencode == TBaseType.rrw_date)) {
397                TSourceToken st1 = ast.nextSolidToken();
398                if (st1 != null) {
399                    if (st1.tokencode == TBaseType.sconst) {
400                        // ast.tokencode = TBaseType.rrw_bigquery_time_before_const;
401                    } else if (st1.tokencode == '(') {
402                        ast.tokencode = TBaseType.ident;
403                    }
404                }
405            }
406            // Handle FROM after period: i.from -> treat FROM as identifier
407            else if (ast.tokencode == TBaseType.rrw_from) {
408                TSourceToken st1 = ast.prevSolidToken();
409                if (st1 != null) { // select i.from as `from` from t, treats from in i.from as identifier
410                    if (st1.tokencode == '.') {
411                        ast.tokencode = TBaseType.ident;
412                    }
413                }
414            }
415            // Handle TIMESTAMP: distinguish type from identifier based on context
416            else if (ast.tokencode == TBaseType.rrw_timestamp) {
417                TSourceToken prevToken = ast.prevSolidToken();
418                TSourceToken nextToken = ast.nextSolidToken();
419
420                // Check if preceded by period -> identifier (e.g., table.timestamp)
421                if (prevToken != null && prevToken.tokencode == '.') {
422                    // Keep as RW_TIMESTAMP (will be used as identifier via unreserved_keyword)
423                }
424                // Check type contexts where TIMESTAMP should be marked as type
425                else if (prevToken != null &&
426                        (prevToken.tokencode == TBaseType.rrw_as ||          // CAST(x AS TIMESTAMP)
427                         prevToken.tokencode == ',' ||                        // func(INT, TIMESTAMP)
428                         prevToken.tokencode == '(' ||                        // CAST(TIMESTAMP ...), STRUCT(col TIMESTAMP)
429                         prevToken.tokencode == '<' ||                        // ARRAY<TIMESTAMP>, STRUCT<col TIMESTAMP>
430                         prevToken.tokencode == TBaseType.ident)) {          // column_name TIMESTAMP (column definition)
431                    ast.tokencode = TBaseType.rrw_bigquery_timestamp_as_type;
432                }
433                // Check if followed by '(' with precision -> type constructor TIMESTAMP(6)
434                else if (nextToken != null && nextToken.tokencode == '(') {
435                    TSourceToken afterParen = nextToken.nextSolidToken();
436                    if (afterParen != null &&
437                        (afterParen.tokencode == TBaseType.iconst ||         // TIMESTAMP(6)
438                         afterParen.tokencode == ')')) {                      // TIMESTAMP()
439                        ast.tokencode = TBaseType.rrw_bigquery_timestamp_as_type;
440                    }
441                }
442                // Check if followed by common tokens after type declarations
443                else if (nextToken != null &&
444                        (nextToken.tokencode == ',' ||                        // col TIMESTAMP, another
445                         nextToken.tokencode == ')' ||                        // col TIMESTAMP)
446                         nextToken.tokencode == '>' ||                        // ARRAY<TIMESTAMP>
447                         nextToken.tokencode == TBaseType.rrw_not ||         // TIMESTAMP NOT NULL
448                         nextToken.tokencode == TBaseType.rrw_null)) {       // TIMESTAMP NULL
449                    ast.tokencode = TBaseType.rrw_bigquery_timestamp_as_type;
450                }
451            }
452            // Handle LEFT/INNER/FULL prefixing a set operation (LEFT [OUTER] UNION ...):
453            // rewrite to synthetic *_SET_OP tokens so the grammar can tell a set-op
454            // prefix apart from a join keyword after a parenthesized subquery
455            else if ((ast.tokencode == TBaseType.rrw_left)
456                    || (ast.tokencode == TBaseType.rrw_inner)
457                    || (ast.tokencode == TBaseType.rrw_full)) {
458                TSourceToken prevToken = ast.prevSolidToken();
459                // t.LEFT / t.FULL are qualified-name segments, never set-op prefixes
460                if (prevToken == null || prevToken.tokencode != '.') {
461                    TSourceToken st1 = ast.nextSolidToken();
462                    // LEFT/FULL allow an optional OUTER before UNION; INNER does not
463                    if ((st1 != null) && (st1.tokencode == TBaseType.rrw_outer)
464                            && (ast.tokencode != TBaseType.rrw_inner)) {
465                        st1 = st1.nextSolidToken();
466                    }
467                    if ((st1 != null) && (st1.tokencode == TBaseType.rrw_union)) {
468                        if (ast.tokencode == TBaseType.rrw_left) {
469                            ast.tokencode = TBaseType.rrw_bigquery_left_set_op;
470                        } else if (ast.tokencode == TBaseType.rrw_inner) {
471                            ast.tokencode = TBaseType.rrw_bigquery_inner_set_op;
472                        } else {
473                            ast.tokencode = TBaseType.rrw_bigquery_full_set_op;
474                        }
475                    }
476                }
477            }
478
479            switch (gst) {
480                case sterror: {
481                    if (ast.tokentype == ETokenType.ttsemicolon) {
482                        gcurrentsqlstatement.sourcetokenlist.add(ast);
483                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
484                        gst = EFindSqlStateType.stnormal;
485                    } else {
486                        gcurrentsqlstatement.sourcetokenlist.add(ast);
487                    }
488                    break;
489                }
490                case stnormal: {
491                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
492                            || (ast.tokencode == TBaseType.cmtslashstar)
493                            || (ast.tokencode == TBaseType.lexspace)
494                            || (ast.tokencode == TBaseType.lexnewline)
495                            || (ast.tokentype == ETokenType.ttsemicolon)) {
496                        if (TBaseType.assigned(gcurrentsqlstatement)) {
497                            gcurrentsqlstatement.sourcetokenlist.add(ast);
498                        }
499
500                        continue;
501                    }
502
503                    // find a token to start sql or plsql mode
504                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
505
506                    if (TBaseType.assigned(gcurrentsqlstatement)) {
507                        if (gcurrentsqlstatement.isBigQueryplsql()) {
508                            nestedProcedures = 0;
509                            gst = EFindSqlStateType.ststoredprocedure;
510                            gcurrentsqlstatement.sourcetokenlist.add(ast);
511
512                            switch (gcurrentsqlstatement.sqlstatementtype) {
513                                case sstplsql_createprocedure:
514                                case sstcreateprocedure:
515                                    sptype[nestedProcedures] = stored_procedure_type.procedure;
516                                    break;
517                                case sstplsql_createfunction:
518                                    sptype[nestedProcedures] = stored_procedure_type.function;
519                                    break;
520                                case sstplsql_createpackage:
521                                    sptype[nestedProcedures] = stored_procedure_type.package_spec;
522                                    if (ast.searchToken(TBaseType.rrw_body, 5) != null) {
523                                        sptype[nestedProcedures] = stored_procedure_type.package_body;
524                                    }
525                                    break;
526                                case sst_plsql_block:
527                                    sptype[nestedProcedures] = stored_procedure_type.block_with_declare;
528                                    if (ast.tokencode == TBaseType.rrw_begin) {
529                                        sptype[nestedProcedures] = stored_procedure_type.block_with_begin;
530                                    }
531                                    break;
532                                case sstplsql_createtrigger:
533                                    sptype[nestedProcedures] = stored_procedure_type.create_trigger;
534                                    break;
535                                case sstoraclecreatelibrary:
536                                    sptype[nestedProcedures] = stored_procedure_type.create_library;
537                                    break;
538                                case sstplsql_createtype_placeholder:
539                                    gst = EFindSqlStateType.stsql;
540                                    break;
541                                default:
542                                    sptype[nestedProcedures] = stored_procedure_type.others;
543                                    break;
544                            }
545
546                            if (sptype[0] == stored_procedure_type.block_with_declare) {
547                                // sd
548                                endBySlashOnly = false;
549                                procedure_status[0] = stored_procedure_status.is_as;
550                            } else if (sptype[0] == stored_procedure_type.block_with_begin) {
551                                // sb
552                                endBySlashOnly = false;
553                                procedure_status[0] = stored_procedure_status.body;
554                            } else if (sptype[0] == stored_procedure_type.procedure) {
555                                // ss
556                                endBySlashOnly = false;
557                                procedure_status[0] = stored_procedure_status.start;
558                            } else if (sptype[0] == stored_procedure_type.function) {
559                                // ss
560                                endBySlashOnly = false;
561                                procedure_status[0] = stored_procedure_status.start;
562                            } else if (sptype[0] == stored_procedure_type.package_spec) {
563                                // ss
564                                endBySlashOnly = false;
565                                procedure_status[0] = stored_procedure_status.start;
566                            } else if (sptype[0] == stored_procedure_type.package_body) {
567                                // ss
568                                endBySlashOnly = false;
569                                procedure_status[0] = stored_procedure_status.start;
570                            } else if (sptype[0] == stored_procedure_type.create_trigger) {
571                                // ss
572                                endBySlashOnly = false;
573                                procedure_status[0] = stored_procedure_status.start;
574                            } else if (sptype[0] == stored_procedure_type.create_library) {
575                                // ss
576                                endBySlashOnly = false;
577                                procedure_status[0] = stored_procedure_status.bodyend;
578                            } else {
579                                // so
580                                endBySlashOnly = true;
581                                procedure_status[0] = stored_procedure_status.bodyend;
582                            }
583                            if ((ast.tokencode == TBaseType.rrw_begin)
584                                    || (ast.tokencode == TBaseType.rrw_package)
585                                    || (ast.searchToken(TBaseType.rrw_package, 4) != null)) {
586                                waitingEnds[nestedProcedures] = 1;
587                            }
588
589                        } else if ((gcurrentsqlstatement.sqlstatementtype == sst_ifstmt)
590                                || (gcurrentsqlstatement.sqlstatementtype == sst_loopstmt)
591                                || (gcurrentsqlstatement.sqlstatementtype == sstRepeat)
592                                || (gcurrentsqlstatement.sqlstatementtype == sstWhilestmt)
593                                || (gcurrentsqlstatement.sqlstatementtype == sstForStmt)
594                                || (gcurrentsqlstatement.sqlstatementtype == sst_plsql_block)
595                                || (gcurrentsqlstatement.sqlstatementtype == sst_casestmt)) {
596                            gst = EFindSqlStateType.stBigQueryIf;
597                            // For labeled statements (label: LOOP/WHILE/etc.), the first token
598                            // is the label identifier, and the keyword comes later in stBigQueryIf
599                            // where it will increment waitingEnd. So start at 0.
600                            waitingEnd = (ast.tokencode == TBaseType.ident) ? 0 : 1;
601                            gcurrentsqlstatement.sourcetokenlist.add(ast);
602                        } else {
603                            gst = EFindSqlStateType.stsql;
604                            gcurrentsqlstatement.sourcetokenlist.add(ast);
605                        }
606
607                    }
608
609                    if (!TBaseType.assigned(gcurrentsqlstatement)) //error token found
610                    {
611                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo)
612                                , "Error when tokenlize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
613
614                        ast.tokentype = ETokenType.tttokenlizererrortoken;
615                        gst = EFindSqlStateType.sterror;
616
617                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
618                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
619                        gcurrentsqlstatement.sourcetokenlist.add(ast);
620
621                    }
622                    break;
623                }
624                case stBigQueryIf: {
625                    gcurrentsqlstatement.sourcetokenlist.add(ast);
626
627                    if (checkTokenPairWithEnd(ast.tokencode)) { // if... end if
628                        if (ast.searchToken(TBaseType.rrw_end, -1) == null) {
629                            //this is not if after END
630                            waitingEnd++;
631                        }
632                    } else if (ast.tokencode == TBaseType.rrw_end) { // if ... end if
633                        TSourceToken next = ast.nextSolidToken();
634                        if (next != null) {
635                            if (checkTokenPairWithEnd(next.tokencode)) { // if ... end if;
636                                waitingEnd--;
637                            } else if (next.tokencode == ';') { // begin ... end ;
638                                waitingEnd--;
639                            } else if (next.tokencode == TBaseType.ident) {
640                                // begin ... end label_name ; (labeled block end)
641                                TSourceToken afterLabel = next.nextSolidToken();
642                                if (afterLabel != null && afterLabel.tokencode == ';') {
643                                    waitingEnd--;
644                                }
645                            }
646
647                        }
648                    } else if ((ast.tokencode == ';') && (waitingEnd == 0)) {
649                        gst = EFindSqlStateType.stnormal;
650
651                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
652                    }
653                    break;
654                }
655                case stsqlplus: {
656                    if (ast.tokencode == TBaseType.lexnewline) {
657                        gst = EFindSqlStateType.stnormal;
658                        gcurrentsqlstatement.sourcetokenlist.add(ast); // so add it here
659                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
660                    } else {
661                        {
662                            gcurrentsqlstatement.sourcetokenlist.add(ast);
663                        }
664                    }
665
666                    break;
667                }//case source command or \. command
668                case stsql: {
669                    if ((ast.tokentype == ETokenType.ttsemicolon) && (gcurrentsqlstatement.sqlstatementtype != ESqlStatementType.sstmysqldelimiter)) {
670                        gst = EFindSqlStateType.stnormal;
671                        gcurrentsqlstatement.sourcetokenlist.add(ast);
672                        gcurrentsqlstatement.semicolonended = ast;
673                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
674                        continue;
675                    }
676                    if (ast.toString().equalsIgnoreCase(userDelimiterStr)) {
677                        gst = EFindSqlStateType.stnormal;
678                        ast.tokencode = ';';// treat it as semicolon
679                        gcurrentsqlstatement.sourcetokenlist.add(ast);
680                        gcurrentsqlstatement.semicolonended = ast;
681                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
682                        continue;
683                    }
684                    gcurrentsqlstatement.sourcetokenlist.add(ast);
685
686                    if ((ast.tokencode == TBaseType.lexnewline)
687                            && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstmysqldelimiter)) {
688                        gst = EFindSqlStateType.stnormal;
689                        userDelimiterStr = "";
690                        for (int k = 0; k < gcurrentsqlstatement.sourcetokenlist.size(); k++) {
691                            TSourceToken st = gcurrentsqlstatement.sourcetokenlist.get(k);
692                            if ((st.tokencode == TBaseType.rrw_mysql_delimiter)
693                                    || (st.tokencode == TBaseType.lexnewline)
694                                    || (st.tokencode == TBaseType.lexspace)) {
695                                continue;
696                            }
697
698                            userDelimiterStr += st.toString();
699                        }
700                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
701
702                        continue;
703                    }
704
705                    break;
706                }
707                case ststoredprocedure: {
708                    if (procedure_status[nestedProcedures] != stored_procedure_status.bodyend) {
709                        gcurrentsqlstatement.sourcetokenlist.add(ast);
710                    }
711
712                    switch (procedure_status[nestedProcedures]) {
713                        case start:
714                            if ((ast.tokencode == TBaseType.rrw_as) || (ast.tokencode == TBaseType.rrw_is)) {
715                                // s1
716                                if (sptype[nestedProcedures] != stored_procedure_type.create_trigger) {
717                                    if ((sptype[0] == stored_procedure_type.package_spec) && (nestedProcedures > 0)) {
718                                        //when it's a package specification, only top level accept as/is
719                                    } else {
720                                        procedure_status[nestedProcedures] = stored_procedure_status.is_as;
721                                        if (ast.searchToken("language", 1) != null) {
722                                            if (nestedProcedures == 0) {
723                                                gst = EFindSqlStateType.stsql;
724                                            } else {
725                                                procedure_status[nestedProcedures] = stored_procedure_status.body;
726                                                nestedProcedures--;
727                                            }
728
729                                        }
730                                    }
731                                }
732                            } else if (ast.tokencode == TBaseType.rrw_begin) {
733                                // s4
734                                if (sptype[nestedProcedures] == stored_procedure_type.create_trigger)
735                                    waitingEnds[nestedProcedures]++;
736
737                                if (nestedProcedures > 0) {
738                                    nestedProcedures--;
739                                }
740                                procedure_status[nestedProcedures] = stored_procedure_status.body;
741                                waitingEnds[nestedProcedures] = 1;
742                            } else if (ast.tokencode == TBaseType.rrw_end) {
743                                //s10
744                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures - 1] == 1)
745                                        && ((sptype[nestedProcedures - 1] == stored_procedure_type.package_body)
746                                        || (sptype[nestedProcedures - 1] == stored_procedure_type.package_spec))) {
747                                    nestedProcedures--;
748                                    procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
749                                }
750                            } else if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) {
751                                //s3
752                                if ((nestedProcedures > 0) && (waitingEnds[nestedProcedures] == 0)
753                                        && (procedure_status[nestedProcedures - 1] == stored_procedure_status.is_as)) {
754                                    nestedProcedures--;
755                                    nestedProcedures++;
756                                    waitingEnds[nestedProcedures] = 0;
757                                    procedure_status[nestedProcedures] = stored_procedure_status.start;
758                                }
759                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger) && (ast.tokencode == TBaseType.rrw_declare)) {
760                                procedure_status[nestedProcedures] = stored_procedure_status.is_as;
761                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger) && (ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) {
762                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
763                                gst = EFindSqlStateType.stnormal;
764                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
765
766                                //make / a sqlplus cmd
767                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
768                                gcurrentsqlstatement.sourcetokenlist.add(ast);
769                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
770                            } else if ((sptype[nestedProcedures] == stored_procedure_type.create_trigger)) {
771                                if (ast.tokencode == TBaseType.rrw_trigger) {
772                                    TSourceToken compoundSt = ast.searchToken(TBaseType.rrw_oracle_compound, -1);
773                                    if (compoundSt != null) {
774                                        //it's trigger with compound trigger block
775                                        procedure_status[nestedProcedures] = stored_procedure_status.body;
776                                        waitingEnds[nestedProcedures]++;
777                                    }
778                                }
779                            } else if ((sptype[nestedProcedures] == stored_procedure_type.function) && (ast.tokencode == TBaseType.rrw_teradata_using)) {
780                                if ((ast.searchToken("aggregate", -1) != null) || (ast.searchToken("pipelined", -1) != null)) {
781                                    if (nestedProcedures == 0) {
782                                        gst = EFindSqlStateType.stsql;
783                                    } else {
784                                        procedure_status[nestedProcedures] = stored_procedure_status.body;
785                                        nestedProcedures--;
786                                    }
787                                }
788
789                            } else {
790                                //other tokens, do nothing
791                            }
792                            break;
793                        case is_as:
794                            if ((ast.tokencode == TBaseType.rrw_procedure) || (ast.tokencode == TBaseType.rrw_function)) {
795                                // s2
796                                nestedProcedures++;
797                                if (nestedProcedures > stored_procedure_nested_level - 1) {
798                                    gst = EFindSqlStateType.sterror;
799                                    nestedProcedures--;
800                                } else {
801                                    waitingEnds[nestedProcedures] = 0;
802                                    procedure_status[nestedProcedures] = stored_procedure_status.start;
803                                }
804
805                            } else if (ast.tokencode == TBaseType.rrw_begin) {
806                                // s5
807                                if ((nestedProcedures == 0) &&
808                                        ((sptype[nestedProcedures] == stored_procedure_type.package_body)
809                                                || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) {
810                                    //top level package or package body's BEGIN keyword already count,
811                                    // so don't increase waitingEnds[nestedProcedures] here
812
813                                } else {
814                                    waitingEnds[nestedProcedures]++;
815                                }
816                                procedure_status[nestedProcedures] = stored_procedure_status.body;
817                            } else if (ast.tokencode == TBaseType.rrw_end) {
818                                // s6
819                                if ((nestedProcedures == 0) && (waitingEnds[nestedProcedures] == 1) &&
820                                        ((sptype[nestedProcedures] == stored_procedure_type.package_body) || (sptype[nestedProcedures] == stored_procedure_type.package_spec))) {
821                                    procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
822                                    waitingEnds[nestedProcedures]--;
823                                } else {
824                                    waitingEnds[nestedProcedures]--;
825                                }
826                            } else if (ast.tokencode == TBaseType.rrw_case) {
827                                if (ast.searchToken(';', 1) == null) {
828                                    //this is not case before ;
829                                    waitingEnds[nestedProcedures]++;
830                                }
831                            } else {
832                                //other tokens, do nothing
833                            }
834                            break;
835                        case body:
836                            if ((ast.tokencode == TBaseType.rrw_begin)) {
837                                waitingEnds[nestedProcedures]++;
838                            } else if (ast.tokencode == TBaseType.rrw_if) {
839
840                                if (ast.searchToken(';', 2) == null) {
841                                    //this is not if before ;
842
843                                    // 2015-02-27, change 1 to 2 make it able to detect label name after case
844                                    // like this: END CASE l1;
845                                    waitingEnds[nestedProcedures]++;
846                                }
847                            } else if (ast.tokencode == TBaseType.rrw_case) {
848                                if (ast.searchToken(';', 2) == null) {
849                                    //this is not case before ;
850                                    if (ast.searchToken(TBaseType.rrw_end, -1) == null) {
851                                        waitingEnds[nestedProcedures]++;
852                                    }
853                                }
854                            } else if ((ast.tokencode == TBaseType.rrw_loop)
855                                    || (ast.tokencode == TBaseType.rrw_while) || (ast.tokencode == TBaseType.rrw_repeat)
856                                    || (ast.tokencode == TBaseType.rrw_for)) {
857                                if (!((ast.searchToken(TBaseType.rrw_end, -1) != null)
858                                        && (ast.searchToken(';', 2) != null))) {
859                                    // exclude loop like this:
860                                    // end loop [labelname];
861                                    waitingEnds[nestedProcedures]++;
862                                }
863
864                            } else if (ast.tokencode == TBaseType.rrw_end) {
865                                //foundEnd = true;
866                                waitingEnds[nestedProcedures]--;
867                                //if (waitingEnd < 0) { waitingEnd = 0;}
868                                if (waitingEnds[nestedProcedures] == 0) {
869                                    if (nestedProcedures == 0) {
870                                        // s7
871                                        procedure_status[nestedProcedures] = stored_procedure_status.bodyend;
872                                    } else {
873                                        // s71
874                                        nestedProcedures--;
875                                        procedure_status[nestedProcedures] = stored_procedure_status.is_as;
876                                    }
877                                }
878                            } else if ((waitingEnds[nestedProcedures] == 0) && (ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) {
879                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
880                                gst = EFindSqlStateType.stnormal;
881                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
882
883                                //make / a sqlplus cmd
884                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
885                                gcurrentsqlstatement.sourcetokenlist.add(ast);
886                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
887                            }
888                            break;
889                        case bodyend:
890                            if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) {
891                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
892                                gst = EFindSqlStateType.stnormal;
893                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
894
895                                //make / a sqlplus cmd
896                                gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
897                                gcurrentsqlstatement.sourcetokenlist.add(ast);
898                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
899                            } else if ((ast.tokencode == ';')) {
900                                gst = EFindSqlStateType.stnormal;
901                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
902
903                            } else if ((ast.searchToken(TBaseType.rrw_package, 1) != null) && (!endBySlashOnly)) {
904                                gcurrentsqlstatement.sourcetokenlist.add(ast);
905                                gst = EFindSqlStateType.stnormal;
906                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
907                            } else if ((ast.searchToken(TBaseType.rrw_procedure, 1) != null) && (!endBySlashOnly)) {
908                                gcurrentsqlstatement.sourcetokenlist.add(ast);
909                                gst = EFindSqlStateType.stnormal;
910                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
911                            } else if ((ast.searchToken(TBaseType.rrw_function, 1) != null) && (!endBySlashOnly)) {
912                                gcurrentsqlstatement.sourcetokenlist.add(ast);
913                                gst = EFindSqlStateType.stnormal;
914                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
915                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) && (ast.searchToken(TBaseType.rrw_package, 4) != null) && (!endBySlashOnly)) {
916                                gcurrentsqlstatement.sourcetokenlist.add(ast);
917                                gst = EFindSqlStateType.stnormal;
918                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
919                            } else if ((ast.searchToken(TBaseType.rrw_create, 1) != null) && (ast.searchToken(TBaseType.rrw_library, 4) != null) && (!endBySlashOnly)) {
920                                gcurrentsqlstatement.sourcetokenlist.add(ast);
921                                gst = EFindSqlStateType.stnormal;
922                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
923                            } else if ((ast.searchToken(TBaseType.rrw_alter, 1) != null) && (ast.searchToken(TBaseType.rrw_trigger, 2) != null) && (!endBySlashOnly)) {
924                                gcurrentsqlstatement.sourcetokenlist.add(ast);
925                                gst = EFindSqlStateType.stnormal;
926                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
927                            } else if ((ast.searchToken(TBaseType.rrw_select, 1) != null) && (!endBySlashOnly)) {
928                                gcurrentsqlstatement.sourcetokenlist.add(ast);
929                                gst = EFindSqlStateType.stnormal;
930                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
931                            } else if ((ast.searchToken(TBaseType.rrw_commit, 1) != null) && (!endBySlashOnly)) {
932                                gcurrentsqlstatement.sourcetokenlist.add(ast);
933                                gst = EFindSqlStateType.stnormal;
934                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
935                            } else if ((ast.searchToken(TBaseType.rrw_grant, 1) != null) &&
936                                    (ast.searchToken(TBaseType.rrw_execute, 2) != null) && (!endBySlashOnly)) {
937                                gcurrentsqlstatement.sourcetokenlist.add(ast);
938                                gst = EFindSqlStateType.stnormal;
939                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
940                            } else {
941                                gcurrentsqlstatement.sourcetokenlist.add(ast);
942                            }
943                            break;
944                        case end:
945                            break;
946                        default:
947                            break;
948                    }
949
950                    if (ast.tokencode == TBaseType.sqlpluscmd) {
951                        //change tokencode back to keyword or TBaseType.ident, because sqlplus cmd
952                        //in a sql statement(almost is plsql block) is not really a sqlplus cmd
953                        int m = flexer.getkeywordvalue(ast.getAstext());
954                        if (m != 0) {
955                            ast.tokencode = m;
956                        } else if (ast.tokentype == ETokenType.ttslash) {
957                            ast.tokencode = '/';
958                        } else {
959                            ast.tokencode = TBaseType.ident;
960                        }
961                    }
962
963                    final int wrapped_keyword_max_pos = 20;
964                    if ((ast.tokencode == TBaseType.rrw_wrapped) && (ast.posinlist - gcurrentsqlstatement.sourcetokenlist.get(0).posinlist < wrapped_keyword_max_pos)) {
965                        if (gcurrentsqlstatement instanceof TCommonStoredProcedureSqlStatement) {
966                            ((TCommonStoredProcedureSqlStatement) gcurrentsqlstatement).setWrapped(true);
967                        }
968
969                        if (gcurrentsqlstatement instanceof TPlsqlCreatePackage) {
970                            if (ast.prevSolidToken() != null) {
971                                ((TPlsqlCreatePackage) gcurrentsqlstatement).setPackageName(fparser.getNf().createObjectNameWithPart(ast.prevSolidToken()));
972                            }
973                        }
974                    }
975
976                    break;
977                } //ststoredprocedure
978
979            } //case
980        } //for
981
982        //last statement
983        if (TBaseType.assigned(gcurrentsqlstatement) &&
984                ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure)
985                        || (gst == EFindSqlStateType.stBigQueryIf) || (gst == EFindSqlStateType.sterror))) {
986            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder);
987        }
988
989        return errorcount;
990    }
991
992    // ========== Statement Parsing ==========
993
994    /**
995     * Parse all SQL statements after tokenization and raw extraction.
996     * <p>
997     * This method iterates through all statements, calls parsestatement on each,
998     * and handles error recovery for CREATE TABLE statements if enabled.
999     * <p>
1000     * Inherited performParsing pattern from AbstractSqlParser, specialized for BigQuery.
1001     */
1002    @Override
1003    protected TStatementList performParsing(ParserContext context, TCustomParser parser,
1004                                            TCustomParser secondaryParser, TSourceTokenList tokens,
1005                                            TStatementList rawStatements) {
1006        // Store references for error handling
1007        this.fparser = (TParserBigquery) parser;
1008        this.sourcetokenlist = tokens;
1009        this.parserContext = context;
1010        this.sqlstatements = rawStatements;
1011
1012        // Initialize sqlcmds for BigQuery
1013        if (this.sqlcmds == null) {
1014            this.sqlcmds = SqlCmdsFactory.get(vendor);
1015        }
1016        this.fparser.sqlcmds = this.sqlcmds;
1017
1018        // Initialize global context using inherited method
1019        initializeGlobalContext();
1020
1021        // Parse each statement
1022        for (int i = 0; i < sqlstatements.size(); i++) {
1023            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
1024
1025            try {
1026                // Set frame stack for variable scope tracking
1027                stmt.setFrameStack(frameStack);
1028
1029                // Parse the statement
1030                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
1031
1032                // Vendor-specific post-processing (none needed for BigQuery currently)
1033                afterStatementParsed(stmt);
1034
1035                // Collect errors from the statement
1036                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
1037                    copyErrorsFromStatement(stmt);
1038                }
1039
1040            } catch (Exception ex) {
1041                // Use inherited exception handler
1042                handleStatementParsingException(stmt, i, ex);
1043                continue;
1044            }
1045        }
1046
1047        // Clean up frame stack
1048        if (globalFrame != null) {
1049            globalFrame.popMeFromStack(frameStack);
1050        }
1051
1052        return sqlstatements;
1053    }
1054
1055    /**
1056     * Post-process statement after parsing (hook method).
1057     * <p>
1058     * BigQuery does not require special post-processing, so this is a no-op.
1059     * Override if BigQuery-specific validation is needed in the future.
1060     */
1061    protected void afterStatementParsed(TCustomSqlStatement stmt) {
1062        // No special post-processing needed for BigQuery
1063    }
1064
1065    // ========== Semantic Analysis ==========
1066
1067    /**
1068     * Perform semantic analysis on parsed statements.
1069     * <p>
1070     * Runs TSQLResolver to build relationships between tables and columns,
1071     * resolve references, and perform type checking.
1072     */
1073    @Override
1074    protected void performSemanticAnalysis(ParserContext context, TStatementList statements) {
1075        if (TBaseType.isEnableResolver() && getSyntaxErrors().isEmpty()) {
1076            TSQLResolver resolver = new TSQLResolver(globalContext, statements);
1077            resolver.resolve();
1078        }
1079    }
1080
1081    /**
1082     * Perform interpretation/evaluation on statements.
1083     * <p>
1084     * Runs TASTEvaluator to execute constant expressions and compile-time
1085     * evaluations.
1086     */
1087    @Override
1088    protected void performInterpreter(ParserContext context, TStatementList statements) {
1089        // BigQuery does not require interpretation currently
1090    }
1091
1092    @Override
1093    public String toString() {
1094        return "BigQuerySqlParser{vendor=" + vendor + "}";
1095    }
1096}