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.TLexerMysql;
009import gudusoft.gsqlparser.TParserMysqlSql;
010import gudusoft.gsqlparser.TSourceToken;
011import gudusoft.gsqlparser.TSourceTokenList;
012import gudusoft.gsqlparser.TStatementList;
013import gudusoft.gsqlparser.TSyntaxError;
014import gudusoft.gsqlparser.EFindSqlStateType;
015import gudusoft.gsqlparser.ETokenType;
016import gudusoft.gsqlparser.ETokenStatus;
017import gudusoft.gsqlparser.ESqlStatementType;
018import gudusoft.gsqlparser.EErrorType;
019import gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement;
020import gudusoft.gsqlparser.stmt.TUnknownSqlStatement;
021import gudusoft.gsqlparser.sqlcmds.ISqlCmds;
022import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory;
023import gudusoft.gsqlparser.stmt.mysql.TMySQLSource;
024import gudusoft.gsqlparser.compiler.TContext;
025import gudusoft.gsqlparser.sqlenv.TSQLEnv;
026import gudusoft.gsqlparser.compiler.TGlobalScope;
027import gudusoft.gsqlparser.compiler.TFrame;
028
029import java.io.BufferedReader;
030import java.util.ArrayList;
031import java.util.List;
032import java.util.Stack;
033
034/**
035 * MySQL database SQL parser implementation.
036 *
037 * <p>This parser handles MySQL-specific SQL syntax including:
038 * <ul>
039 *   <li>MySQL stored procedures and functions</li>
040 *   <li>MySQL triggers</li>
041 *   <li>Custom delimiter support (DELIMITER command)</li>
042 *   <li>MySQL-specific DML/DDL</li>
043 *   <li>Special operators and functions</li>
044 *   <li>SOURCE command and \\. command</li>
045 * </ul>
046 *
047 * <p><b>Design Notes:</b>
048 * <ul>
049 *   <li>Extends {@link AbstractSqlParser}</li>
050 *   <li>Can directly instantiate: {@link TLexerMysql}, {@link TParserMysqlSql}</li>
051 *   <li>Uses single parser (no secondary parser like Oracle's PL/SQL)</li>
052 *   <li>Delimiter character: ';' for SQL statements (configurable via DELIMITER command)</li>
053 * </ul>
054 *
055 * <p><b>Usage Example:</b>
056 * <pre>
057 * // Get MySQL parser from factory
058 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvmysql);
059 *
060 * // Build context
061 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvmysql)
062 *     .sqlText("SELECT * FROM employees WHERE dept_id = 10")
063 *     .build();
064 *
065 * // Parse
066 * SqlParseResult result = parser.parse(context);
067 *
068 * // Access statements
069 * TStatementList statements = result.getSqlStatements();
070 * </pre>
071 *
072 * @see SqlParser
073 * @see AbstractSqlParser
074 * @see TLexerMysql
075 * @see TParserMysqlSql
076 * @since 3.2.0.0
077 */
078public class MySqlSqlParser extends AbstractSqlParser {
079
080    // ========== Lexer and Parser Instances ==========
081    // Created once in constructor, reused for all parsing operations
082
083    /** The MySQL lexer used for tokenization (public for TGSqlParser.getFlexer()) */
084    public TLexerMysql flexer;
085    private TParserMysqlSql fparser;
086
087    // ========== State Variables ==========
088    // NOTE: The following fields moved to AbstractSqlParser (inherited):
089    //   - sourcetokenlist (TSourceTokenList)
090    //   - sqlstatements (TStatementList)
091    //   - parserContext (ParserContext)
092    //   - sqlcmds (ISqlCmds)
093    //   - globalContext (TContext)
094    //   - sqlEnv (TSQLEnv)
095    //   - frameStack (Stack<TFrame>)
096    //   - globalFrame (TFrame)
097    //   - lexer (TCustomLexer)
098
099    // ========== State Variables for Raw Statement Extraction ==========
100    private String userDelimiterStr;
101    // True only after an explicit DELIMITER command; while false, a stored
102    // routine may also terminate at a nesting-balanced END; (scripts written
103    // without DELIMITER cannot produce the default '$' terminator at all).
104    private boolean userDelimiterExplicit;
105    // BEGIN/END nesting inside the current stored routine.
106    private int spBeginEndNesting;
107    private char curdelimiterchar;
108    private boolean waitingDelimiter;
109
110    // ========== Constructor ==========
111
112    /**
113     * Construct MySQL SQL parser.
114     * <p>
115     * Configures the parser for MySQL database with default delimiter: semicolon (;)
116     * <p>
117     * Following the original TGSqlParser pattern, the lexer and parser are
118     * created once in the constructor and reused for all parsing operations.
119     */
120    public MySqlSqlParser() {
121        super(EDbVendor.dbvmysql);
122
123        // Set delimiter character
124        this.delimiterChar = '$';
125        this.defaultDelimiterStr = "$";
126
127        // Create lexer once - will be reused for all parsing operations
128        this.flexer = new TLexerMysql();
129        this.flexer.delimiterchar = this.delimiterChar;
130        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
131
132        // CRITICAL: Set lexer for inherited getanewsourcetoken() method
133        this.lexer = this.flexer;
134
135        // Create parser once - will be reused for all parsing operations
136        this.fparser = new TParserMysqlSql(null);
137        this.fparser.lexer = this.flexer;
138
139        // NOTE: sourcetokenlist and sqlstatements are initialized in AbstractSqlParser constructor
140    }
141
142    // ========== AbstractSqlParser Abstract Methods Implementation ==========
143
144    /**
145     * Return the MySQL lexer instance.
146     * <p>
147     * The lexer is created once in the constructor and reused for all
148     * parsing operations. This method simply returns the existing instance,
149     * matching the original TGSqlParser pattern where the lexer is created
150     * once and reset before each use.
151     *
152     * @param context parser context (not used, lexer already created)
153     * @return the MySQL lexer instance created in constructor
154     */
155    @Override
156    protected TCustomLexer getLexer(ParserContext context) {
157        // Return existing lexer instance (created in constructor)
158        return this.flexer;
159    }
160
161    /**
162     * Return the MySQL SQL parser instance with updated token list.
163     * <p>
164     * The parser is created once in the constructor and reused for all
165     * parsing operations. This method updates the token list and returns
166     * the existing instance, matching the original TGSqlParser pattern.
167     *
168     * @param context parser context (not used, parser already created)
169     * @param tokens source token list to parse
170     * @return the MySQL SQL parser instance created in constructor
171     */
172    @Override
173    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
174        // Update token list for reused parser instance
175        this.fparser.sourcetokenlist = tokens;
176        return this.fparser;
177    }
178
179    /**
180     * Call MySQL-specific tokenization logic.
181     * <p>
182     * Delegates to domysqltexttotokenlist which handles MySQL's
183     * specific keyword recognition, delimiter handling, and token generation.
184     */
185    @Override
186    protected void tokenizeVendorSql() {
187        domysqltexttotokenlist();
188    }
189
190    /**
191     * Setup MySQL parser for raw statement extraction.
192     * <p>
193     * MySQL uses a single parser, so we inject sqlcmds and update
194     * the token list for the main parser only.
195     */
196    @Override
197    protected void setupVendorParsersForExtraction() {
198        this.fparser.sqlcmds = this.sqlcmds;
199        this.fparser.sourcetokenlist = this.sourcetokenlist;
200    }
201
202    /**
203     * Call MySQL-specific raw statement extraction logic.
204     * <p>
205     * Delegates to domysqlgetrawsqlstatements which handles MySQL's
206     * statement delimiters (semicolon by default, or custom delimiter via DELIMITER command).
207     */
208    @Override
209    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
210        domysqlgetrawsqlstatements(builder);
211    }
212
213    /**
214     * Perform full parsing of statements with syntax checking.
215     * <p>
216     * This method orchestrates the parsing of all statements.
217     *
218     * <p><b>Important:</b> This method does NOT extract raw statements - they are
219     * passed in as a parameter already extracted by {@link #extractRawStatements}.
220     *
221     * @param context parser context
222     * @param parser main SQL parser (TParserMysqlSql)
223     * @param secondaryParser not used for MySQL
224     * @param tokens source token list
225     * @param rawStatements raw statements already extracted (never null)
226     * @return list of fully parsed statements with AST built
227     */
228    @Override
229    protected TStatementList performParsing(ParserContext context,
230                                           TCustomParser parser,
231                                           TCustomParser secondaryParser,
232                                           TSourceTokenList tokens,
233                                           TStatementList rawStatements) {
234        // Store references (fparser is already set, don't reassign final variable)
235        this.sourcetokenlist = tokens;
236        this.parserContext = context;
237
238        // Use the raw statements passed from AbstractSqlParser.parse()
239        // (already extracted - DO NOT re-extract to avoid duplication)
240        this.sqlstatements = rawStatements;
241
242        // Initialize sqlcmds for the parser
243        this.sqlcmds = SqlCmdsFactory.get(vendor);
244        this.fparser.sqlcmds = this.sqlcmds;
245
246        // Initialize global context for statement parsing
247        initializeGlobalContext();
248
249        // Parse each statement
250        for (int i = 0; i < sqlstatements.size(); i++) {
251            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
252
253            try {
254                // Set frame stack for the statement (needed for parsing)
255                stmt.setFrameStack(frameStack);
256
257                // Parse the statement
258                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
259
260                // Handle error recovery for CREATE TABLE statements if enabled
261                boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE;
262                if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) {
263                    handleCreateTableErrorRecovery(stmt);
264                }
265
266                // Collect syntax errors
267                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
268                    copyErrorsFromStatement(stmt);
269                }
270            } catch (Exception ex) {
271                // Use inherited exception handler
272                handleStatementParsingException(stmt, i, ex);
273                continue;
274            }
275        }
276
277        // Clean up frame stack
278        if (globalFrame != null) {
279            globalFrame.popMeFromStack(frameStack);
280        }
281
282        return this.sqlstatements;
283    }
284
285    /**
286     * Handle error recovery for CREATE TABLE statements.
287     * <p>
288     * Migrated from TGSqlParser.handleCreateTableErrorRecovery()
289     * <p>
290     * This method marks unparseable table properties as sqlpluscmd tokens
291     * and retries parsing, similar to MSSQL error recovery.
292     *
293     * @param stmt the statement that failed to parse
294     */
295    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
296        if ((stmt.sqlstatementtype != ESqlStatementType.sstcreatetable) || TBaseType.c_createTableStrictParsing) {
297            return;
298        }
299
300        int nested = 0;
301        boolean isIgnore = false, isFoundIgnoreToken = false;
302        TSourceToken firstIgnoreToken = null;
303
304        for (int k = 0; k < stmt.sourcetokenlist.size(); k++) {
305            TSourceToken st = stmt.sourcetokenlist.get(k);
306            if (isIgnore) {
307                if (st.issolidtoken() && (st.tokencode != ';')) {
308                    isFoundIgnoreToken = true;
309                    if (firstIgnoreToken == null) {
310                        firstIgnoreToken = st;
311                    }
312                }
313                if (st.tokencode != ';') {
314                    st.tokencode = TBaseType.sqlpluscmd;
315                }
316                continue;
317            }
318            if (st.tokencode == (int) ')') {
319                nested--;
320                if (nested == 0) {
321                    boolean isSelect = false;
322                    TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1);
323                    if (st1 != null) {
324                        TSourceToken st2 = st.searchToken((int) '(', 2);
325                        if (st2 != null) {
326                            TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3);
327                            isSelect = (st3 != null);
328                        }
329                    }
330                    if (!isSelect) isIgnore = true;
331                }
332            } else if (st.tokencode == (int) '(') {
333                nested++;
334            }
335        }
336
337        if (isFoundIgnoreToken) {
338            stmt.clearError();
339            stmt.parsestatement(null, false, this.parserContext.isOnlyNeedRawParseTree());
340        }
341    }
342
343    // ========== MySQL-Specific Tokenization ==========
344
345    /**
346     * Perform MySQL-specific tokenization.
347     * <p>
348     * Extracted from TGSqlParser.domysqltexttotokenlist() (lines 4759-4822)
349     */
350    private void domysqltexttotokenlist() {
351        TSourceToken asourcetoken, lcprevst;
352        int yychar;
353        boolean startDelimiter = false;
354        // DELIMITER is an unreserved keyword, so it only introduces a directive when it
355        // is the first SOLID token on its line; elsewhere it is an ordinary identifier,
356        // as in "SELECT delimiter AS marker". Tracked by line number rather than by
357        // newline tokens, because a multi-line comment is emitted as a single non-solid
358        // token carrying no newline -- keying off newlines would hide a directive
359        // written as "SELECT 1; /* comment\n*/ DELIMITER $$".
360        long lastSolidLineNo = -1;
361
362        flexer.tmpDelimiter = "";
363
364        asourcetoken = getanewsourcetoken();
365        if (asourcetoken == null) return;
366        yychar = asourcetoken.tokencode;
367        checkMySQLCommentToken(asourcetoken);
368
369        if ((asourcetoken.tokencode == TBaseType.rrw_mysql_delimiter)) {
370            startDelimiter = true;
371        }
372        if (!asourcetoken.isnonsolidtoken()) {
373            lastSolidLineNo = asourcetoken.lineNo;
374        }
375
376        while (yychar > 0) {
377            sourcetokenlist.add(asourcetoken);
378            asourcetoken = getanewsourcetoken();
379            if (asourcetoken == null) break;
380            checkMySQLCommentToken(asourcetoken);
381
382            if ((asourcetoken.tokencode == TBaseType.lexnewline) && (startDelimiter)) {
383                startDelimiter = false;
384                // The delimiter is the last SOLID token on the DELIMITER line. Trailing
385                // spaces or tabs before the newline must not become the delimiter, or a
386                // body terminated by END$$ would no longer match. Stop at the DELIMITER
387                // keyword itself so a directive with no argument leaves the delimiter
388                // unchanged instead of setting it to "delimiter".
389                for (int i = sourcetokenlist.size() - 1; i >= 0; i--) {
390                    TSourceToken st = sourcetokenlist.get(i);
391                    if (st.tokencode == TBaseType.rrw_mysql_delimiter) break;
392                    if (!st.isnonsolidtoken()) {
393                        flexer.tmpDelimiter = st.getAstext();
394                        break;
395                    }
396                }
397            }
398
399            if (!asourcetoken.isnonsolidtoken()) {
400                if ((asourcetoken.tokencode == TBaseType.rrw_mysql_delimiter)
401                        && (asourcetoken.lineNo != lastSolidLineNo)) {
402                    startDelimiter = true;
403                }
404                lastSolidLineNo = asourcetoken.lineNo;
405            }
406
407            if (asourcetoken.tokencode == TBaseType.rrw_rollup) {
408                // with rollup
409                lcprevst = getprevsolidtoken(asourcetoken);
410                if (lcprevst != null) {
411                    if (lcprevst.tokencode == TBaseType.rrw_with)
412                        lcprevst.tokencode = TBaseType.with_rollup;
413                }
414            }
415
416            if ((asourcetoken.tokencode == TBaseType.rrw_mysql_d)
417                    || (asourcetoken.tokencode == TBaseType.rrw_mysql_t)
418                    || (asourcetoken.tokencode == TBaseType.rrw_mysql_ts)) {
419                // odbc date constant { d 'str' }
420                lcprevst = getprevsolidtoken(asourcetoken);
421                if (lcprevst != null) {
422                    if (lcprevst.tokencode != '{')
423                        asourcetoken.tokencode = TBaseType.ident;
424                }
425            }
426
427            yychar = asourcetoken.tokencode;
428        }
429    }
430
431    /**
432     * Check if MySQL comment token is valid.
433     * <p>
434     * MySQL requires a space after -- for double-hyphen comments.
435     * This method was present in TGSqlParser but the implementation
436     * was commented out, so we keep it as a placeholder.
437     *
438     * @param cmtToken comment token to check
439     */
440    private void checkMySQLCommentToken(TSourceToken cmtToken) {
441        // Implementation was commented out in original TGSqlParser
442        // Keeping this method as placeholder for future use
443    }
444
445    /**
446     * Get previous non-whitespace token.
447     *
448     * @param ptoken current token
449     * @return previous solid token, or null
450     */
451    private TSourceToken getprevsolidtoken(TSourceToken ptoken) {
452        TSourceToken ret = null;
453        TSourceTokenList lctokenlist = ptoken.container;
454
455        if (lctokenlist != null) {
456            if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) {
457                if (!(
458                        (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttwhitespace)
459                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttreturn)
460                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttsimplecomment)
461                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttbracketedcomment)
462                )) {
463                    ret = lctokenlist.get(ptoken.posinlist - 1);
464                } else {
465                    ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false);
466                }
467            }
468        }
469        return ret;
470    }
471
472    // ========== MySQL-Specific Raw Statement Extraction ==========
473
474    /**
475     * Extract raw MySQL SQL statements from tokenized source.
476     * <p>
477     * Extracted from TGSqlParser.domysqlgetrawsqlstatements() (lines 14979-15344)
478     *
479     * @param builder the result builder to populate with raw statements
480     */
481    private void domysqlgetrawsqlstatements(SqlParseResult.Builder builder) {
482        TCustomSqlStatement gcurrentsqlstatement = null;
483        EFindSqlStateType gst = EFindSqlStateType.stnormal;
484
485        // Reset delimiter
486        userDelimiterStr = defaultDelimiterStr;
487        userDelimiterExplicit = false;
488        spBeginEndNesting = 0;
489
490        if (TBaseType.assigned(sqlstatements)) sqlstatements.clear();
491        if (!TBaseType.assigned(sourcetokenlist)) {
492            // No tokens available - populate builder with empty results and return
493            builder.sqlStatements(this.sqlstatements);
494            builder.errorCode(1);
495            builder.errorMessage("No source token list available");
496            return;
497        }
498
499        for (int i = 0; i < sourcetokenlist.size(); i++) {
500            TSourceToken ast = sourcetokenlist.get(i);
501            sourcetokenlist.curpos = i;
502
503            // Token transformations during raw statement extraction
504            performRawStatementTokenTransformations(ast);
505
506            switch (gst) {
507                case sterror: {
508                    if (ast.tokentype == ETokenType.ttsemicolon) {
509                        appendToken(gcurrentsqlstatement, ast);
510                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
511                        gcurrentsqlstatement = null;
512                        gst = EFindSqlStateType.stnormal;
513                    } else {
514                        appendToken(gcurrentsqlstatement, ast);
515                    }
516                    break;
517                }
518
519                case stnormal: {
520                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
521                            || (ast.tokencode == TBaseType.cmtslashstar)
522                            || (ast.tokencode == TBaseType.lexspace)
523                            || (ast.tokencode == TBaseType.lexnewline)
524                            || (ast.tokentype == ETokenType.ttsemicolon)) {
525                        if (TBaseType.assigned(gcurrentsqlstatement)) {
526                            appendToken(gcurrentsqlstatement, ast);
527                        }
528                        continue;
529                    }
530
531                    if (ast.isFirstTokenOfLine() && (ast.toString().equalsIgnoreCase(userDelimiterStr))) {
532                        ast.tokencode = ';';// treat it as semicolon
533                        continue;
534                    }
535
536                    if ((ast.isFirstTokenOfLine()) && ((ast.tokencode == TBaseType.rrw_mysql_source) || (ast.tokencode == TBaseType.slash_dot))) {
537                        gst = EFindSqlStateType.stsqlplus;
538                        gcurrentsqlstatement = new TMySQLSource(vendor);
539                        appendToken(gcurrentsqlstatement, ast);
540                        continue;
541                    }
542
543                    // Find a token to start sql or plsql mode
544                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
545
546                    if (TBaseType.assigned(gcurrentsqlstatement)) {
547                        ESqlStatementType[] ses = {ESqlStatementType.sstmysqlcreateprocedure, ESqlStatementType.sstmysqlcreatefunction,
548                                ESqlStatementType.sstcreateprocedure, ESqlStatementType.sstcreatefunction,
549                                ESqlStatementType.sstcreatetrigger, ESqlStatementType.sstmysqlcreateevent,
550                                ESqlStatementType.sstmysqlalterevent};
551                        if (includesqlstatementtype(gcurrentsqlstatement.sqlstatementtype, ses)) {
552                            gst = EFindSqlStateType.ststoredprocedure;
553                            waitingDelimiter = false;
554                            spBeginEndNesting = 0;
555                            appendToken(gcurrentsqlstatement, ast);
556                            curdelimiterchar = ';';
557                            // Only initialize userDelimiterStr if not already set by DELIMITER statement
558                            if (userDelimiterStr == null || userDelimiterStr.isEmpty()) {
559                                userDelimiterStr = ";";
560                            }
561                        } else {
562                            gst = EFindSqlStateType.stsql;
563                            appendToken(gcurrentsqlstatement, ast);
564                        }
565                    }
566
567                    if (!TBaseType.assigned(gcurrentsqlstatement)) {
568                        // Error token found
569                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo),
570                                "Error when tokenize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
571
572                        ast.tokentype = ETokenType.tttokenlizererrortoken;
573                        gst = EFindSqlStateType.sterror;
574
575                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
576                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
577                        appendToken(gcurrentsqlstatement, ast);
578                    }
579                    break;
580                }
581
582                case stsqlplus: {
583                    if (ast.tokencode == TBaseType.lexnewline) {
584                        gst = EFindSqlStateType.stnormal;
585                        appendToken(gcurrentsqlstatement, ast); // so add it here
586                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
587                        gcurrentsqlstatement = null;
588                    } else {
589                        appendToken(gcurrentsqlstatement, ast);
590                    }
591                    break;
592                }
593
594                case stsql: {
595                    if ((ast.tokentype == ETokenType.ttsemicolon) && (gcurrentsqlstatement.sqlstatementtype != ESqlStatementType.sstmysqldelimiter)) {
596                        gst = EFindSqlStateType.stnormal;
597                        appendToken(gcurrentsqlstatement, ast);
598                        gcurrentsqlstatement.semicolonended = ast;
599                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
600                        gcurrentsqlstatement = null;
601                        continue;
602                    }
603                    if (ast.toString().equalsIgnoreCase(userDelimiterStr)) {
604                        gst = EFindSqlStateType.stnormal;
605                        ast.tokencode = ';';// treat it as semicolon
606                        appendToken(gcurrentsqlstatement, ast);
607                        gcurrentsqlstatement.semicolonended = ast;
608                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
609                        gcurrentsqlstatement = null;
610                        continue;
611                    }
612
613                    if (ast.tokencode == TBaseType.cmtdoublehyphen) {
614                        if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter
615                            gst = EFindSqlStateType.stnormal;
616                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
617                            gcurrentsqlstatement = null;
618                            continue;
619                        }
620                    }
621
622                    appendToken(gcurrentsqlstatement, ast);
623
624                    if ((ast.tokencode == TBaseType.lexnewline)
625                            && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstmysqldelimiter)) {
626                        gst = EFindSqlStateType.stnormal;
627                        userDelimiterExplicit = true;
628                        userDelimiterStr = "";
629                        for (int k = 0; k < gcurrentsqlstatement.sourcetokenlist.size(); k++) {
630                            TSourceToken st = gcurrentsqlstatement.sourcetokenlist.get(k);
631                            if ((st.tokencode == TBaseType.rrw_mysql_delimiter)
632                                    || (st.tokencode == TBaseType.lexnewline)
633                                    || (st.tokencode == TBaseType.lexspace)
634                                    || (st.tokencode == TBaseType.rrw_set)) // set delimiter //
635                            {
636                                continue;
637                            }
638
639                            userDelimiterStr += st.toString();
640                        }
641                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
642                        gcurrentsqlstatement = null;
643                        continue;
644                    }
645
646                    break;
647                }
648
649                case ststoredprocedure: {
650
651                    if ((gst == EFindSqlStateType.ststoredprocedure) && (ast.tokencode == TBaseType.cmtdoublehyphen)) {
652                        if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter
653                            gst = EFindSqlStateType.stnormal;
654                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
655                            gcurrentsqlstatement = null;
656                            continue;
657                        }
658                    }
659
660                    // Handle waitingDelimiter logic (when inside BEGIN...END block)
661                    // Skip this check if delimiter is ";" since we need to check for END; pattern instead
662                    if (waitingDelimiter && !userDelimiterStr.equals(";")) {
663                        if (userDelimiterStr.equalsIgnoreCase(ast.toString())) {
664                            gst = EFindSqlStateType.stnormal;
665                            gcurrentsqlstatement.semicolonended = ast;
666                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
667                            gcurrentsqlstatement = null;
668                            continue;
669                        } else if (userDelimiterStr.startsWith(ast.toString())) {
670                            String lcstr = ast.toString();
671                            for (int k = ast.posinlist + 1; k < ast.container.size(); k++) {
672                                TSourceToken st = ast.container.get(k);
673                                if ((st.tokencode == TBaseType.rrw_mysql_delimiter) || (st.tokencode == TBaseType.lexnewline) || (st.tokencode == TBaseType.lexspace)) {
674                                    break;
675                                }
676                                lcstr = lcstr + st.toString();
677                            }
678
679                            if (userDelimiterStr.equalsIgnoreCase(lcstr)) {
680                                int lastDelimiterPos = ast.posinlist;
681                                for (int k = ast.posinlist; k < ast.container.size(); k++) {
682                                    TSourceToken st = ast.container.get(k);
683                                    if ((st.tokencode == TBaseType.rrw_mysql_delimiter) || (st.tokencode == TBaseType.lexnewline) || (st.tokencode == TBaseType.lexspace)) {
684                                        break;
685                                    }
686                                    st.tokenstatus = ETokenStatus.tsignorebyyacc;
687                                    lastDelimiterPos = k;
688                                }
689                                gst = EFindSqlStateType.stnormal;
690                                gcurrentsqlstatement.semicolonended = ast;
691                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
692                                gcurrentsqlstatement = null;
693                                i = lastDelimiterPos; // advance past all delimiter tokens
694                                continue;
695                            }
696                        }
697                    }
698
699                    // Set waitingDelimiter when BEGIN is encountered
700                    if (ast.tokencode == TBaseType.rrw_begin) {
701                        waitingDelimiter = true;
702                        spBeginEndNesting++;
703                    }
704
705                    // Main delimiter handling logic
706                    // When not waiting for delimiter (no BEGIN block), complete at semicolon regardless of custom delimiter
707                    if (!waitingDelimiter) {
708                        appendToken(gcurrentsqlstatement, ast);
709                        if (ast.tokentype == ETokenType.ttsemicolon) {
710                            gst = EFindSqlStateType.stnormal;
711                            gcurrentsqlstatement.semicolonended = ast;
712                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
713                            gcurrentsqlstatement = null;
714                            continue;
715                        }
716                    } else {
717                        // When waitingDelimiter is true AND delimiter is ";", only check for END; pattern
718                        if (waitingDelimiter && userDelimiterStr.equals(";")) {
719                            // Check for END; pattern
720                            if ((ast.tokentype == ETokenType.ttsemicolon)) {
721                                TSourceToken lcprevtoken = ast.container.nextsolidtoken(ast, -1, false);
722                                if ((lcprevtoken != null) && (lcprevtoken.tokencode == TBaseType.rrw_end)) {
723                                    spBeginEndNesting--;
724                                    if (spBeginEndNesting <= 0) {
725                                        gst = EFindSqlStateType.stnormal;
726                                        gcurrentsqlstatement.semicolonended = ast;
727                                        appendToken(gcurrentsqlstatement, ast);
728                                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
729                                        gcurrentsqlstatement = null;
730                                        continue;
731                                    }
732                                }
733                            }
734                            appendToken(gcurrentsqlstatement, ast);
735                        } else {
736                            // Custom delimiter handling (non-semicolon delimiters)
737                            if (ast.toString().equals(userDelimiterStr)) {
738                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
739                                appendToken(gcurrentsqlstatement, ast);
740                                gst = EFindSqlStateType.stnormal;
741                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
742                                gcurrentsqlstatement = null;
743                            } else if ((ast.getAstext().length() > 1) && ast.issolidtoken()
744                                    && ast.getAstext().endsWith(userDelimiterStr)) {
745                                // A token that fused with the delimiter (END$): split
746                                // logically — restore the keyword code of the leading
747                                // part and end the statement (the pre-rewrite splitter
748                                // handled this shape the same way).
749                                String lcstr = ast.getAstext().substring(0, ast.getAstext().length() - userDelimiterStr.length());
750                                int c = flexer.getkeywordvalue(lcstr);
751                                if (c > 0) {
752                                    ast.tokencode = c;
753                                }
754                                appendToken(gcurrentsqlstatement, ast);
755                                gst = EFindSqlStateType.stnormal;
756                                gcurrentsqlstatement.semicolonended = ast;
757                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
758                                gcurrentsqlstatement = null;
759                            } else if (!userDelimiterExplicit && (ast.tokentype == ETokenType.ttsemicolon)) {
760                                // No DELIMITER command was ever issued, so the
761                                // default '$' can never appear; a script written
762                                // that way ends its routines at a balanced END;.
763                                TSourceToken lcprevtoken = ast.container.nextsolidtoken(ast, -1, false);
764                                if ((lcprevtoken != null) && (lcprevtoken.tokencode == TBaseType.rrw_end)) {
765                                    spBeginEndNesting--;
766                                    if (spBeginEndNesting <= 0) {
767                                        gst = EFindSqlStateType.stnormal;
768                                        gcurrentsqlstatement.semicolonended = ast;
769                                        appendToken(gcurrentsqlstatement, ast);
770                                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
771                                        gcurrentsqlstatement = null;
772                                        continue;
773                                    }
774                                }
775                                appendToken(gcurrentsqlstatement, ast);
776                            } else {
777                                appendToken(gcurrentsqlstatement, ast);
778                            }
779                        }
780                    }
781
782                    /*
783                    // OLD LOGIC - replaced by above
784                    if (curdelimiterchar == ';') {
785                        appendToken(gcurrentsqlstatement, ast);
786                        if (ast.tokentype == ETokenType.ttsemicolon) {
787                            gst = EFindSqlStateType.stnormal;
788                            gcurrentsqlstatement.semicolonended = ast;
789                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
790                            continue;
791                        }
792                    } else {
793                        // Handle multi-character delimiters
794                        char ch;
795                        if (ast.getAstext().length() == 1) {
796                            ch = ast.getAstext().charAt(0);
797                        } else if ((ast.getAstext().length() > 1) && (ast.issolidtoken())) {
798                            ch = ast.getAstext().charAt(ast.getAstext().length() - 1);
799                        } else {
800                            ch = ' ';
801                        }
802
803                        if (ch == curdelimiterchar) {
804                            if (ast.getAstext().length() > 1) {
805                                String lcstr = ast.getAstext().substring(0, ast.getAstext().length() - 1);
806                                int c = flexer.getkeywordvalue(lcstr);
807                                if (c > 0) {
808                                    ast.tokencode = c;
809                                }
810                            } else {
811                                // Mark single-character delimiter to be ignored by parser
812                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
813                                gcurrentsqlstatement.semicolonended = ast;
814                            }
815                            appendToken(gcurrentsqlstatement, ast);
816                            gst = EFindSqlStateType.stnormal;
817                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
818                        } else {
819                            appendToken(gcurrentsqlstatement, ast);
820                        }
821                    }
822                    */
823                    break;
824                }
825            }
826        }
827
828        // Last statement
829        if (TBaseType.assigned(gcurrentsqlstatement) && ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) || (gst == EFindSqlStateType.sterror))) {
830            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, true, builder);
831        }
832
833        // Populate builder with results
834        builder.sqlStatements(this.sqlstatements);
835        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
836                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
837        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
838    }
839
840    /**
841     * Handle token transformations during raw statement extraction.
842     *
843     * @param ast current token being processed
844     */
845    private void performRawStatementTokenTransformations(TSourceToken ast) {
846        if (ast.tokencode == TBaseType.rrw_date) {
847            TSourceToken st1 = ast.nextSolidToken();
848            if (st1 != null) {
849                if (st1.tokencode == '(') {
850                    ast.tokencode = TBaseType.rrw_mysql_date_function;
851                } else if (st1.tokencode == TBaseType.sconst) {
852                    ast.tokencode = TBaseType.rrw_mysql_date_const;
853                }
854            }
855        } else if (ast.tokencode == TBaseType.rrw_time) {
856            TSourceToken st1 = ast.nextSolidToken();
857            if (st1 != null) {
858                if (st1.tokencode == TBaseType.sconst) {
859                    ast.tokencode = TBaseType.rrw_mysql_time_const;
860                }
861            }
862        } else if (ast.tokencode == TBaseType.rrw_timestamp) {
863            TSourceToken st1 = ast.nextSolidToken();
864            if (st1 != null) {
865                if (st1.tokencode == TBaseType.sconst) {
866                    ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
867                } else if (st1.tokencode == TBaseType.ident) {
868                    if (st1.toString().startsWith("\"")) {
869                        ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
870                        st1.tokencode = TBaseType.sconst;
871                    }
872                }
873            }
874        } else if (ast.tokencode == TBaseType.rrw_mysql_position) {
875            TSourceToken st1 = ast.nextSolidToken();
876            if (st1 != null) {
877                if (st1.tokencode != '(') {
878                    ast.tokencode = TBaseType.ident; // change position keyword to identifier if not followed by ()
879                }
880            }
881        } else if (ast.tokencode == TBaseType.rrw_mysql_row) {
882            boolean isIdent = true;
883            TSourceToken st1 = ast.nextSolidToken();
884            if (st1 != null) {
885                if (st1.tokencode == '(') {
886                    isIdent = false;
887                }
888            }
889            st1 = ast.prevSolidToken();
890            if (st1 != null) {
891                if ((st1.tokencode == TBaseType.rrw_mysql_each) || (st1.tokencode == TBaseType.rrw_mysql_current)) {
892                    isIdent = false;
893                }
894            }
895            if (isIdent) ast.tokencode = TBaseType.ident;
896        } else if (ast.tokencode == TBaseType.rrw_interval) {
897            TSourceToken leftParen = ast.searchToken('(', 1);
898            if (leftParen != null) {
899                int k = leftParen.posinlist + 1;
900                int nested = 1;
901                boolean commaToken = false;
902                while (k < ast.container.size()) {
903                    if (ast.container.get(k).tokencode == '(') {
904                        nested++;
905                    }
906                    if (ast.container.get(k).tokencode == ')') {
907                        nested--;
908                        if (nested == 0) break;
909                    }
910                    if ((ast.container.get(k).tokencode == ',') && (nested == 1)) {
911                        // only calculate the comma in the first level which is belong to interval
912                        // don't count comma in the nested () like this: INTERVAL (SELECT IF(1=1,2,3))
913                        commaToken = true;
914                        break;
915                    }
916                    k++;
917                }
918                if (commaToken) {
919                    ast.tokencode = TBaseType.rrw_mysql_interval_func;
920                }
921            }
922        }
923    }
924
925    /**
926     * Helper method to check if a statement type is in an array of types.
927     *
928     * @param type the type to check
929     * @param types array of types to check against
930     * @return true if type is in the array
931     */
932    private boolean includesqlstatementtype(ESqlStatementType type, ESqlStatementType[] types) {
933        for (ESqlStatementType t : types) {
934            if (type == t) return true;
935        }
936        return false;
937    }
938
939    private void appendToken(TCustomSqlStatement statement, TSourceToken token) {
940        if (statement == null || token == null) {
941            return;
942        }
943        token.stmt = statement;
944        statement.sourcetokenlist.add(token);
945    }
946
947    @Override
948    public String toString() {
949        return "MySqlSqlParser{vendor=" + vendor + "}";
950    }
951}