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