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                                ESqlStatementType.sstdb2alterfunction, ESqlStatementType.sstdb2alterprocedure};
477                        if (includesqlstatementtype(gcurrentsqlstatement.sqlstatementtype, ses)) {
478                            gst = EFindSqlStateType.ststoredprocedure;
479                            waitingEnds = 1; // need a END keyword to end this procedure
480                            waitingForFirstBegin = true;
481                            gcurrentsqlstatement.sourcetokenlist.add(ast);
482                            if ((gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreateprocedure)
483                                    || (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatefunction)
484                                    || (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstcreatetrigger)
485                                    || (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstdb2alterfunction)
486                                    || (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstdb2alterprocedure)) {
487                                curDelimiterChar = ';';
488                            }
489                        } else if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstdb2scriptOption) {
490                            gst = EFindSqlStateType.stnormal;
491                            gcurrentsqlstatement.sourcetokenlist.add(ast);
492                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
493                        } else {
494                            gst = EFindSqlStateType.stsql;
495                            gcurrentsqlstatement.sourcetokenlist.add(ast);
496                        }
497
498                    }
499
500                    if (!TBaseType.assigned(gcurrentsqlstatement)) // error tokentext found
501                    {
502                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo)
503                                , "Error when tokenlize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
504
505                        ast.tokentype = ETokenType.tttokenlizererrortoken;
506                        gst = EFindSqlStateType.sterror;
507
508                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
509                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
510                        gcurrentsqlstatement.sourcetokenlist.add(ast);
511
512                    }
513                    break;
514                }
515                case stsqlplus: // treat echo, ! of db2 at the begin of each line as oracle sqlplus command
516                {
517                    if (ast.insqlpluscmd) {
518                        gcurrentsqlstatement.sourcetokenlist.add(ast);
519                    } else {
520                        gst = EFindSqlStateType.stnormal; // this tokentext must be newline,
521                        gcurrentsqlstatement.sourcetokenlist.add(ast); // so add it here
522                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
523                    }
524                    break;
525                }
526                case stsql: {
527                    if (ast.tokentype == ETokenType.ttsemicolon) {
528                        gst = EFindSqlStateType.stnormal;
529                        gcurrentsqlstatement.sourcetokenlist.add(ast);
530                        gcurrentsqlstatement.semicolonended = ast;
531                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
532                        continue;
533                    }
534                    gcurrentsqlstatement.sourcetokenlist.add(ast);
535
536                    break;
537                }
538                case ststoredprocedure: {
539                    TCustomSqlStatement nextStmt = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
540                    if ((nextStmt != null)
541                            && ((nextStmt.sqlstatementtype == ESqlStatementType.sstcreateprocedure)
542                            || (nextStmt.sqlstatementtype == ESqlStatementType.sstcreatefunction)
543                            || (nextStmt.sqlstatementtype == ESqlStatementType.sstcreatetrigger))) {
544                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
545                        gcurrentsqlstatement = nextStmt;
546                        gst = EFindSqlStateType.ststoredprocedure;
547                        waitingEnds = 1; // need a END keyword to end this procedure
548                        waitingForFirstBegin = true;
549                        gcurrentsqlstatement.sourcetokenlist.add(ast);
550                        curDelimiterChar = ';';
551                        continue;
552                    }
553
554                    // single stmt in function/procedure/trigger may use ; as terminate char
555                    // so default terminate char is ;, if begin is found, then
556                    // set terminate char to DelimiterChar
557                    if (curDelimiterChar != delimiterChar) {
558                        if ((ast.tokencode == TBaseType.rrw_begin) || (ast.tokencode == TBaseType.rrw_declare)) {
559                            curDelimiterChar = delimiterChar;
560                        }
561                    }
562
563                    if (curDelimiterChar == ';') {
564                        gcurrentsqlstatement.sourcetokenlist.add(ast);
565                        if (ast.tokentype == ETokenType.ttsemicolon) {
566                            gst = EFindSqlStateType.stnormal;
567                            gcurrentsqlstatement.semicolonended = ast;
568                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
569                            continue;
570                        }
571                    } else {
572                        if (ast.getAstext().length() == 1) {
573                            ch = ast.getAstext().charAt(0);
574                        } else if ((ast.getAstext().length() > 1) && (ast.issolidtoken())) {
575                            ch = ast.getAstext().charAt(ast.getAstext().length() - 1);
576                        } else {
577                            ch = ' ';
578                        }
579
580                        if (ch == curDelimiterChar) {
581                            if (ast.getAstext().length() > 1) {
582                                lcstr = ast.getAstext().substring(0, ast.getAstext().length() - 1);
583                                if ((c = flexer.getkeywordvalue(lcstr)) > 0)
584                                    ast.tokencode = c;
585                            } else {
586                                gcurrentsqlstatement.semicolonended = ast;
587                            }
588                            gcurrentsqlstatement.sourcetokenlist.add(ast);
589                            gst = EFindSqlStateType.stnormal;
590                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
591                        } else {
592                            gcurrentsqlstatement.sourcetokenlist.add(ast);
593
594                            if ((ast.tokencode == TBaseType.rrw_case)
595                                    || (ast.tokencode == TBaseType.rrw_for)
596                                    || (ast.tokencode == TBaseType.rrw_if)
597                                    || (ast.tokencode == TBaseType.rrw_while)
598                                    || (ast.tokencode == TBaseType.rrw_repeat)
599                                    || (ast.tokencode == TBaseType.rrw_loop)) {
600                                TSourceToken nextst = ast.nextSolidToken();
601                                if (nextst != null) {
602                                    if (nextst.tokencode != ';') {
603                                        waitingEnds++;
604                                    }
605                                }
606
607                            } else if (ast.tokencode == TBaseType.rrw_begin) {
608                                if (waitingForFirstBegin) {
609                                    waitingForFirstBegin = false;
610                                } else {
611                                    waitingEnds++;
612                                }
613                            } else if (ast.tokencode == TBaseType.rrw_end) {
614                                waitingEnds--;
615                            }
616
617                            if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnds == 0)) {
618                                gst = EFindSqlStateType.stnormal;
619                                gcurrentsqlstatement.semicolonended = ast;
620                                onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
621                                continue;
622                            }
623
624                        }
625                    } // end of DelimiterChar
626
627                    break;
628                } // db2 sp
629            } // case
630        } // for
631
632        // last statement
633        if (TBaseType.assigned(gcurrentsqlstatement) && ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) || (gst == EFindSqlStateType.sterror))) {
634            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder);
635        }
636
637        // Populate builder with extracted statements and errors
638        builder.sqlStatements(this.sqlstatements);
639        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
640                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
641        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
642    }
643
644    /**
645     * Get previous non-whitespace/comment token.
646     * <p>
647     * Helper method for tokenization that finds the previous "solid" token
648     * (skipping whitespace, newlines, and comments).
649     *
650     * @param ptoken current token
651     * @return previous solid token, or null if not found
652     */
653    private TSourceToken getprevsolidtoken(TSourceToken ptoken) {
654        TSourceToken ret = null;
655        TSourceTokenList lctokenlist = ptoken.container;
656
657        if (lctokenlist != null) {
658            if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) {
659                if (!(
660                        (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttwhitespace)
661                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttreturn)
662                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttsimplecomment)
663                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttbracketedcomment)
664                )) {
665                    ret = lctokenlist.get(ptoken.posinlist - 1);
666                } else {
667                    ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false);
668                }
669            }
670        }
671        return ret;
672    }
673
674    /**
675     * Helper method to check if a statement type is in the given array.
676     * Migrated from TGSqlParser.
677     *
678     * @param type statement type to check
679     * @param types array of statement types
680     * @return true if type is in array
681     */
682    private boolean includesqlstatementtype(ESqlStatementType type, ESqlStatementType[] types) {
683        for (ESqlStatementType t : types) {
684            if (type == t) return true;
685        }
686        return false;
687    }
688
689    @Override
690    public String toString() {
691        return "Db2SqlParser{vendor=" + vendor + "}";
692    }
693}