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