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.TLexerAnsi;
009import gudusoft.gsqlparser.TParserAnsi;
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.ESqlStatementType;
017import gudusoft.gsqlparser.EErrorType;
018import gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement;
019import gudusoft.gsqlparser.stmt.TUnknownSqlStatement;
020import gudusoft.gsqlparser.sqlcmds.ISqlCmds;
021import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory;
022
023import java.util.ArrayList;
024
025/**
026 * ANSI SQL parser implementation.
027 *
028 * <p>This parser handles ANSI standard SQL syntax. It delegates tokenization
029 * and raw statement extraction to DB2 logic, as ANSI SQL shares similar
030 * parsing characteristics with DB2.
031 *
032 * <p><b>Design Notes:</b>
033 * <ul>
034 *   <li>Extends {@link AbstractSqlParser}</li>
035 *   <li>Can directly instantiate: {@link TLexerAnsi}, {@link TParserAnsi}</li>
036 *   <li>Uses single parser (no secondary parser)</li>
037 *   <li>Delimiter character: ';' (semicolon)</li>
038 *   <li>Shares tokenization and extraction logic with DB2</li>
039 * </ul>
040 *
041 * <p><b>Migrated from TGSqlParser:</b>
042 * <ul>
043 *   <li>doansitexttotokenlist() → delegates to dodb2sqltexttotokenlist()</li>
044 *   <li>doansigetrawsqlstatements() → delegates to dodb2getrawsqlstatements()</li>
045 * </ul>
046 *
047 * @see SqlParser
048 * @see AbstractSqlParser
049 * @see TLexerAnsi
050 * @see TParserAnsi
051 * @since 3.2.0.0
052 */
053public class AnsiSqlParser extends AbstractSqlParser {
054
055    // ========== Lexer and Parser Instances ==========
056    // Created once in constructor, reused for all parsing operations
057
058    private TLexerAnsi flexer;
059    private TParserAnsi fparser;
060
061    // ========== State Variables ==========
062    // NOTE: The following fields are inherited from AbstractSqlParser:
063    //   - sourcetokenlist (TSourceTokenList)
064    //   - sqlstatements (TStatementList)
065    //   - parserContext (ParserContext)
066    //   - sqlcmds (ISqlCmds)
067    //   - globalContext (TContext)
068    //   - sqlEnv (TSQLEnv)
069    //   - frameStack (Stack<TFrame>)
070    //   - globalFrame (TFrame)
071    //   - lexer (TCustomLexer)
072
073    // ========== Constructor ==========
074
075    /**
076     * Construct ANSI SQL parser.
077     * <p>
078     * Configures the parser for ANSI standard SQL with default delimiter: semicolon (;)
079     * <p>
080     * Following the original TGSqlParser pattern, the lexer and parser are
081     * created once in the constructor and reused for all parsing operations.
082     */
083    public AnsiSqlParser() {
084        super(EDbVendor.dbvansi);
085
086        // Set delimiter character - ANSI uses ';' (semicolon)
087        this.delimiterChar = ';';
088        this.defaultDelimiterStr = ";";
089
090        // Create lexer once - will be reused for all parsing operations
091        this.flexer = new TLexerAnsi();
092        this.flexer.delimiterchar = this.delimiterChar;
093        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
094
095        // CRITICAL: Set lexer for inherited getanewsourcetoken() method
096        this.lexer = this.flexer;
097
098        // Create parser once - will be reused for all parsing operations
099        this.fparser = new TParserAnsi(null);
100        this.fparser.lexer = this.flexer;
101
102        // NOTE: sourcetokenlist and sqlstatements are initialized in AbstractSqlParser constructor
103    }
104
105    // ========== AbstractSqlParser Abstract Methods Implementation ==========
106
107    /**
108     * Return the ANSI lexer instance.
109     * <p>
110     * The lexer is created once in the constructor and reused for all
111     * parsing operations. This method simply returns the existing instance,
112     * matching the original TGSqlParser pattern where the lexer is created
113     * once and reset before each use.
114     *
115     * @param context parser context (not used, lexer already created)
116     * @return the ANSI lexer instance created in constructor
117     */
118    @Override
119    protected TCustomLexer getLexer(ParserContext context) {
120        // Return existing lexer instance (created in constructor)
121        return this.flexer;
122    }
123
124    /**
125     * Return the ANSI SQL parser instance with updated token list.
126     * <p>
127     * The parser is created once in the constructor and reused for all
128     * parsing operations. This method updates the token list and returns
129     * the existing instance, matching the original TGSqlParser pattern.
130     *
131     * @param context parser context (not used, parser already created)
132     * @param tokens source token list to parse
133     * @return the ANSI SQL parser instance created in constructor
134     */
135    @Override
136    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
137        // Update token list for reused parser instance
138        this.fparser.sourcetokenlist = tokens;
139        return this.fparser;
140    }
141
142    /**
143     * Call ANSI-specific tokenization logic.
144     * <p>
145     * Delegates to dodb2sqltexttotokenlist which handles ANSI/DB2
146     * specific keyword recognition and token generation.
147     * <p>
148     * This follows the original TGSqlParser pattern where
149     * doansitexttotokenlist() delegates to dodb2sqltexttotokenlist().
150     */
151    @Override
152    protected void tokenizeVendorSql() {
153        dodb2sqltexttotokenlist();
154    }
155
156    /**
157     * Setup ANSI parser for raw statement extraction.
158     * <p>
159     * ANSI uses a single parser, so we inject sqlcmds and update
160     * the token list for the main parser only.
161     */
162    @Override
163    protected void setupVendorParsersForExtraction() {
164        this.fparser.sqlcmds = this.sqlcmds;
165        this.fparser.sourcetokenlist = this.sourcetokenlist;
166    }
167
168    /**
169     * Call ANSI-specific raw statement extraction logic.
170     * <p>
171     * Delegates to dodb2getrawsqlstatements which handles ANSI/DB2
172     * statement boundaries and compound blocks.
173     * <p>
174     * This follows the original TGSqlParser pattern where
175     * doansigetrawsqlstatements() delegates to dodb2getrawsqlstatements().
176     *
177     * @param builder result builder to populate
178     */
179    @Override
180    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
181        dodb2getrawsqlstatements(builder);
182    }
183
184    /**
185     * Perform full parsing on raw ANSI SQL statements.
186     * <p>
187     * This method parses each raw statement into a complete parse tree,
188     * handling stored procedures, functions, triggers, and nested blocks.
189     *
190     * @param context parser context
191     * @param parser main parser instance
192     * @param secondaryParser secondary parser (null for ANSI)
193     * @param tokens source token list
194     * @param rawStatements raw statements to parse
195     * @return list of parsed statements
196     */
197    @Override
198    protected TStatementList performParsing(ParserContext context,
199                                           TCustomParser parser,
200                                           TCustomParser secondaryParser,
201                                           TSourceTokenList tokens,
202                                           TStatementList rawStatements) {
203        // Store references
204        this.fparser = (TParserAnsi) parser;
205        this.sourcetokenlist = tokens;
206        this.parserContext = context;
207
208        // Use rawStatements if provided, otherwise use our own sqlstatements field
209        if (rawStatements != null) {
210            this.sqlstatements = rawStatements;
211        }
212
213        // Safety check - if sqlstatements is still null, return empty list
214        if (this.sqlstatements == null) {
215            return new TStatementList();
216        }
217
218        // Initialize sqlcmds if not already done
219        if (this.sqlcmds == null) {
220            this.sqlcmds = SqlCmdsFactory.get(vendor);
221        }
222        this.fparser.sqlcmds = this.sqlcmds;
223
224        // Use inherited method to initialize global context
225        initializeGlobalContext();
226
227        // Parse each statement with exception handling
228        for (int i = 0; i < sqlstatements.size(); i++) {
229            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
230            try {
231                stmt.setFrameStack(frameStack);
232                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
233
234                // Perform any ANSI-specific post-processing if needed
235                afterStatementParsed(stmt);
236
237                // Attempt error recovery for CREATE TABLE/INDEX with unsupported options
238                // This matches legacy TGSqlParser behavior that ignores vendor-specific
239                // table/index options not in the grammar
240                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
241                    parseResult = attemptErrorRecovery(stmt, parseResult, context.isOnlyNeedRawParseTree());
242                }
243
244                // Collect errors from statement (after recovery attempt)
245                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
246                    copyErrorsFromStatement(stmt);
247                }
248            } catch (Exception ex) {
249                // Use inherited exception handler
250                handleStatementParsingException(stmt, i, ex);
251                continue;
252            }
253        }
254
255        // Cleanup
256        if (globalFrame != null) {
257            globalFrame.popMeFromStack(frameStack);
258        }
259
260        return sqlstatements;
261    }
262
263    // ========== ANSI-Specific Tokenization (Delegates to DB2) ==========
264    // Migrated from TGSqlParser.dodb2sqltexttotokenlist()
265
266    /**
267     * Tokenize ANSI SQL text into source tokens.
268     * <p>
269     * Handles ANSI/DB2-specific features:
270     * <ul>
271     *   <li>Echo commands at line start treated as SQL*Plus commands</li>
272     *   <li>JDBC escape sequences {fn} and }</li>
273     *   <li>WITH isolation clauses (RR, RS, CS, UR)</li>
274     *   <li>ScriptOptions comment directive</li>
275     * </ul>
276     * <p>
277     * Migrated from TGSqlParser.dodb2sqltexttotokenlist (lines 4300-4408)
278     */
279    private void dodb2sqltexttotokenlist() {
280        // treat echo, ! of db2 at the begin of each line as oracle sqlplus command
281
282        boolean insqlpluscmd = false;
283        boolean isvalidplace = true;
284        int jdbc_escape_nest = 0;
285
286        TSourceToken asourcetoken, lcprevst;
287        int yychar;
288
289        asourcetoken = getanewsourcetoken();
290
291        if (asourcetoken == null) return;
292        yychar = asourcetoken.tokencode;
293
294        while (yychar > 0) {
295
296            switch (yychar) {
297                case TBaseType.lexnewline: {
298                    insqlpluscmd = false;
299                    isvalidplace = true;
300                    break;
301                }
302                case TBaseType.cmtdoublehyphen: {
303                    if (asourcetoken.toString().toLowerCase().indexOf("scriptoptions") >= 0) {
304                        asourcetoken.tokencode = TBaseType.scriptoptions;
305                        asourcetoken.tokentype = ETokenType.ttidentifier;
306                    }
307
308                    if (insqlpluscmd) {
309                        asourcetoken.insqlpluscmd = true;
310                    }
311                    break;
312                }
313                case TBaseType.rrw_jdbc_escape_fn: {
314                    jdbc_escape_nest++;
315                    asourcetoken.tokencode = TBaseType.lexspace;
316                    break;
317                }
318                case TBaseType.rrw_jdbc_escape_end: {
319                    jdbc_escape_nest--;
320                    asourcetoken.tokencode = TBaseType.lexspace;
321                    break;
322                }
323                default: // solid tokentext
324                {
325                    if ((asourcetoken.tokencode == TBaseType.rrw_rr)
326                            || (asourcetoken.tokencode == TBaseType.rrw_rs)
327                            || (asourcetoken.tokencode == TBaseType.rrw_cs)
328                            || (asourcetoken.tokencode == TBaseType.rrw_ur)) {
329                        // change with keyword in isolation clause to rrw_with_isolation
330                        // so opt_restriction_clause in create view worked correctly
331                        lcprevst = getprevsolidtoken(asourcetoken);
332                        if (lcprevst != null) {
333                            if (lcprevst.tokencode == TBaseType.rrw_with)
334                                lcprevst.tokencode = TBaseType.rrw_with_isolation;
335                        }
336                    }
337
338                    if (insqlpluscmd) {
339                        asourcetoken.insqlpluscmd = true;
340                    } else {
341                        if (isvalidplace) {
342                            if (TBaseType.mycomparetext(asourcetoken.toString().toLowerCase(), "echo") == 0) {
343                                asourcetoken.tokencode = TBaseType.sqlpluscmd;
344                                if (asourcetoken.tokentype != ETokenType.ttslash) {
345                                    asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
346                                }
347                                asourcetoken.insqlpluscmd = true;
348                                insqlpluscmd = true;
349                            }
350                        }
351                    }
352                    isvalidplace = false;
353
354                    break;
355                } // end of solid tokentext
356            } // switch
357
358            sourcetokenlist.add(asourcetoken);
359
360            asourcetoken = getanewsourcetoken();
361            if (asourcetoken == null) break;
362            yychar = asourcetoken.tokencode;
363
364        } // while
365    }
366
367    // ========== ANSI-Specific Raw Statement Extraction (Delegates to DB2) ==========
368    // Migrated from TGSqlParser.dodb2getrawsqlstatements()
369
370    /**
371     * Extract raw ANSI SQL statements from token list.
372     * <p>
373     * Handles ANSI/DB2-specific statement boundaries:
374     * <ul>
375     *   <li>Stored procedures, functions, triggers with BEGIN/END blocks</li>
376     *   <li>DECLARE CURSOR statements</li>
377     *   <li>Compound statements with nested blocks</li>
378     *   <li>Echo commands as SQL*Plus commands</li>
379     *   <li>Token transformations (DECLARE GLOBAL, YEAR as alias, TRIM L/R)</li>
380     * </ul>
381     * <p>
382     * Migrated from TGSqlParser.dodb2getrawsqlstatements (lines 15346-15622)
383     *
384     * @param builder result builder to populate with extracted statements
385     */
386    private void dodb2getrawsqlstatements(SqlParseResult.Builder builder) {
387        // Clear and initialize sqlstatements
388        if (TBaseType.assigned(sqlstatements)) {
389            sqlstatements.clear();
390        }
391
392        // Check if sourcetokenlist is available
393        if (!TBaseType.assigned(sourcetokenlist)) {
394            // No tokens available - populate builder with empty results and return
395            builder.sqlStatements(this.sqlstatements);
396            builder.errorCode(1);
397            builder.errorMessage("No source token list available");
398            return;
399        }
400
401        TCustomSqlStatement gcurrentsqlstatement = null;
402        EFindSqlStateType gst = EFindSqlStateType.stnormal;
403        int i, c;
404        TSourceToken ast;
405        int errorcount = 0;
406        char ch;
407        String lcstr;
408        int waitingEnds = 0;
409        boolean waitingForFirstBegin = false;
410        char curDelimiterChar = delimiterChar;
411
412        for (i = 0; i < sourcetokenlist.size(); i++) {
413            ast = sourcetokenlist.get(i);
414            sourcetokenlist.curpos = i;
415
416            // Token transformations
417            if (ast.tokencode == TBaseType.rrw_declare) {
418                TSourceToken st1 = ast.searchToken("global", 1);
419                if (st1 != null) {
420                    ast.tokencode = TBaseType.rrw_declare_global;
421                }
422            } else if ((ast.tokencode == TBaseType.rrw_year) || (ast.tokencode == TBaseType.rrw_db2_second)
423                    || (ast.tokencode == TBaseType.rrw_db2_current)) {
424                TSourceToken st1 = ast.searchToken('.', 1);
425                if (st1 != null) {
426                    // year.columnA, year is a table alias
427                    ast.tokencode = TBaseType.ident;
428                }
429            } else if ((ast.tokencode == TBaseType.rrw_db2_trim_l) || (ast.tokencode == TBaseType.rrw_db2_trim_r)) {
430                TSourceToken st1 = ast.searchToken(TBaseType.rrw_db2_trim, -2);
431                if (st1 == null) {
432                    // not L,R in TRIM (R 'Mr.' FROM name)
433                    ast.tokencode = TBaseType.ident;
434                }
435            }
436
437            switch (gst) {
438                case sterror: {
439                    if (ast.tokentype == ETokenType.ttsemicolon) {
440                        gcurrentsqlstatement.sourcetokenlist.add(ast);
441                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
442                        gst = EFindSqlStateType.stnormal;
443                    } else {
444                        gcurrentsqlstatement.sourcetokenlist.add(ast);
445                    }
446                    break;
447                }
448                case stnormal: {
449                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
450                            || (ast.tokencode == TBaseType.cmtslashstar)
451                            || (ast.tokencode == TBaseType.lexspace)
452                            || (ast.tokencode == TBaseType.lexnewline)
453                            || (ast.tokentype == ETokenType.ttsemicolon)) {
454                        if (TBaseType.assigned(gcurrentsqlstatement)) {
455                            gcurrentsqlstatement.sourcetokenlist.add(ast);
456                        }
457                        continue;
458                    }
459
460                    if (ast.tokencode == TBaseType.sqlpluscmd) {
461                        // echo in db2 is treated as sqlplus cmd
462                        gst = EFindSqlStateType.stsqlplus;
463                        gcurrentsqlstatement = new TSqlplusCmdStatement(vendor);
464                        gcurrentsqlstatement.sourcetokenlist.add(ast);
465                        continue;
466                    }
467
468                    // find a tokentext to start sql or plsql mode
469                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
470
471                    if (TBaseType.assigned(gcurrentsqlstatement)) {
472                        ESqlStatementType[] ses = {ESqlStatementType.sstdb2declarecursor, ESqlStatementType.sstcreateprocedure,
473                                ESqlStatementType.sstcreatefunction, ESqlStatementType.sstcreatetrigger, ESqlStatementType.sst_plsql_block};
474                        if (includesqlstatementtype(gcurrentsqlstatement.sqlstatementtype, ses)) {
475                            gst = EFindSqlStateType.ststoredprocedure;
476                            waitingEnds = 1; // need a END keyword to end this procedure
477                            waitingForFirstBegin = true;
478                            gcurrentsqlstatement.sourcetokenlist.add(ast);
479                            if ((gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreateprocedure)
480                                    || (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatefunction)
481                                    || (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatetrigger)) {
482                                curDelimiterChar = ';';
483                            }
484                        } else if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstdb2scriptOption) {
485                            gst = EFindSqlStateType.stnormal;
486                            gcurrentsqlstatement.sourcetokenlist.add(ast);
487                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
488                        } else {
489                            gst = EFindSqlStateType.stsql;
490                            gcurrentsqlstatement.sourcetokenlist.add(ast);
491                        }
492
493                    }
494
495                    if (!TBaseType.assigned(gcurrentsqlstatement)) // error tokentext found
496                    {
497                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo)
498                                , "Error when tokenlize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
499
500                        ast.tokentype = ETokenType.tttokenlizererrortoken;
501                        gst = EFindSqlStateType.sterror;
502
503                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
504                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
505                        gcurrentsqlstatement.sourcetokenlist.add(ast);
506
507                    }
508                    break;
509                }
510                case stsqlplus: // treat echo, ! of db2 at the begin of each line as oracle sqlplus command
511                {
512                    if (ast.insqlpluscmd) {
513                        gcurrentsqlstatement.sourcetokenlist.add(ast);
514                    } else {
515                        gst = EFindSqlStateType.stnormal; // this tokentext must be newline,
516                        gcurrentsqlstatement.sourcetokenlist.add(ast); // so add it here
517                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
518                    }
519                    break;
520                }
521                case stsql: {
522                    if (ast.tokentype == ETokenType.ttsemicolon) {
523                        gst = EFindSqlStateType.stnormal;
524                        gcurrentsqlstatement.sourcetokenlist.add(ast);
525                        gcurrentsqlstatement.semicolonended = ast;
526                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
527                        continue;
528                    }
529                    gcurrentsqlstatement.sourcetokenlist.add(ast);
530
531                    break;
532                }
533                case ststoredprocedure: {
534                    TCustomSqlStatement nextStmt = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
535                    if ((nextStmt != null)
536                            && ((nextStmt.sqlstatementtype == ESqlStatementType.sstcreateprocedure)
537                            || (nextStmt.sqlstatementtype == ESqlStatementType.sstcreatefunction)
538                            || (nextStmt.sqlstatementtype == ESqlStatementType.sstcreatetrigger))) {
539                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
540                        gcurrentsqlstatement = nextStmt;
541                        gst = EFindSqlStateType.ststoredprocedure;
542                        waitingEnds = 1; // need a END keyword to end this procedure
543                        waitingForFirstBegin = true;
544                        gcurrentsqlstatement.sourcetokenlist.add(ast);
545                        curDelimiterChar = ';';
546                        continue;
547                    }
548
549                    // single stmt in function/procedure/trigger may use ; as terminate char
550                    // so default terminate char is ;, if begin is found, then
551                    // set terminate char to DelimiterChar
552                    if (curDelimiterChar != delimiterChar) {
553                        if ((ast.tokencode == TBaseType.rrw_begin) || (ast.tokencode == TBaseType.rrw_declare)) {
554                            curDelimiterChar = delimiterChar;
555                        }
556                    }
557
558                    if (curDelimiterChar == ';') {
559                        gcurrentsqlstatement.sourcetokenlist.add(ast);
560                        if (ast.tokentype == ETokenType.ttsemicolon) {
561                            gst = EFindSqlStateType.stnormal;
562                            gcurrentsqlstatement.semicolonended = ast;
563                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
564                            continue;
565                        }
566                    } else {
567                        if (ast.getAstext().length() == 1) {
568                            ch = ast.getAstext().charAt(0);
569                        } else if ((ast.getAstext().length() > 1) && (ast.issolidtoken())) {
570                            ch = ast.getAstext().charAt(ast.getAstext().length() - 1);
571                        } else {
572                            ch = ' ';
573                        }
574
575                        if (ch == curDelimiterChar) {
576                            if (ast.getAstext().length() > 1) {
577                                lcstr = ast.getAstext().substring(0, ast.getAstext().length() - 1);
578                                if ((c = flexer.getkeywordvalue(lcstr)) > 0)
579                                    ast.tokencode = c;
580                            } else {
581                                gcurrentsqlstatement.semicolonended = ast;
582                            }
583                            gcurrentsqlstatement.sourcetokenlist.add(ast);
584                            gst = EFindSqlStateType.stnormal;
585                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
586                        } else {
587                            gcurrentsqlstatement.sourcetokenlist.add(ast);
588
589                            if ((ast.tokencode == TBaseType.rrw_case)
590                                    || (ast.tokencode == TBaseType.rrw_for)
591                                    || (ast.tokencode == TBaseType.rrw_if)
592                                    || (ast.tokencode == TBaseType.rrw_while)
593                                    || (ast.tokencode == TBaseType.rrw_repeat)
594                                    || (ast.tokencode == TBaseType.rrw_loop)) {
595                                TSourceToken nextst = ast.nextSolidToken();
596                                if (nextst != null) {
597                                    if (nextst.tokencode != ';') {
598                                        waitingEnds++;
599                                    }
600                                }
601
602                            } else if (ast.tokencode == TBaseType.rrw_begin) {
603                                if (waitingForFirstBegin) {
604                                    waitingForFirstBegin = false;
605                                } else {
606                                    waitingEnds++;
607                                }
608                            } else if (ast.tokencode == TBaseType.rrw_end) {
609                                waitingEnds--;
610                            }
611
612                            if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnds == 0)) {
613                                gst = EFindSqlStateType.stnormal;
614                                gcurrentsqlstatement.semicolonended = ast;
615                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
616                                continue;
617                            }
618
619                        }
620                    } // end of DelimiterChar
621
622                    break;
623                } // db2 sp
624            } // case
625        } // for
626
627        // last statement
628        if (TBaseType.assigned(gcurrentsqlstatement) && ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) || (gst == EFindSqlStateType.sterror))) {
629            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder);
630        }
631
632        // Populate builder with extracted statements and errors
633        builder.sqlStatements(this.sqlstatements);
634        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
635                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
636        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
637    }
638
639    /**
640     * Get previous non-whitespace/comment token.
641     * <p>
642     * Helper method for tokenization that finds the previous "solid" token
643     * (skipping whitespace, newlines, and comments).
644     *
645     * @param ptoken current token
646     * @return previous solid token, or null if not found
647     */
648    private TSourceToken getprevsolidtoken(TSourceToken ptoken) {
649        TSourceToken ret = null;
650        TSourceTokenList lctokenlist = ptoken.container;
651
652        if (lctokenlist != null) {
653            if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) {
654                if (!(
655                        (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttwhitespace)
656                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttreturn)
657                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttsimplecomment)
658                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttbracketedcomment)
659                )) {
660                    ret = lctokenlist.get(ptoken.posinlist - 1);
661                } else {
662                    ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false);
663                }
664            }
665        }
666        return ret;
667    }
668
669    /**
670     * Helper method to check if a statement type is in the given array.
671     * Migrated from TGSqlParser.
672     *
673     * @param type statement type to check
674     * @param types array of statement types
675     * @return true if type is in array
676     */
677    private boolean includesqlstatementtype(ESqlStatementType type, ESqlStatementType[] types) {
678        for (ESqlStatementType t : types) {
679            if (type == t) return true;
680        }
681        return false;
682    }
683
684    @Override
685    public String toString() {
686        return "AnsiSqlParser{vendor=" + vendor + "}";
687    }
688}