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                        gst = EFindSqlStateType.stnormal;
478                    } else {
479                        appendToken(gcurrentsqlstatement, ast);
480                    }
481                    break;
482                }
483
484                case stnormal: {
485                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
486                            || (ast.tokencode == TBaseType.cmtslashstar)
487                            || (ast.tokencode == TBaseType.lexspace)
488                            || (ast.tokencode == TBaseType.lexnewline)
489                            || (ast.tokentype == ETokenType.ttsemicolon)) {
490                        if (TBaseType.assigned(gcurrentsqlstatement)) {
491                            appendToken(gcurrentsqlstatement, ast);
492                        }
493                        continue;
494                    }
495
496                    if (ast.isFirstTokenOfLine() && (ast.toString().equalsIgnoreCase(userDelimiterStr))) {
497                        ast.tokencode = ';';// treat it as semicolon
498                        continue;
499                    }
500
501                    if ((ast.isFirstTokenOfLine()) && ((ast.tokencode == TBaseType.rrw_mysql_source) || (ast.tokencode == TBaseType.slash_dot))) {
502                        gst = EFindSqlStateType.stsqlplus;
503                        gcurrentsqlstatement = new TMySQLSource(vendor);
504                        appendToken(gcurrentsqlstatement, ast);
505                        continue;
506                    }
507
508                    // Find a token to start sql or plsql mode
509                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
510
511                    if (TBaseType.assigned(gcurrentsqlstatement)) {
512                        ESqlStatementType[] ses = {ESqlStatementType.sstmysqlcreateprocedure, ESqlStatementType.sstmysqlcreatefunction,
513                                ESqlStatementType.sstcreateprocedure, ESqlStatementType.sstcreatefunction,
514                                ESqlStatementType.sstcreatetrigger};
515                        if (includesqlstatementtype(gcurrentsqlstatement.sqlstatementtype, ses)) {
516                            gst = EFindSqlStateType.ststoredprocedure;
517                            waitingDelimiter = false;
518                            appendToken(gcurrentsqlstatement, ast);
519                            curdelimiterchar = ';';
520                            // Only initialize userDelimiterStr if not already set by DELIMITER statement
521                            if (userDelimiterStr == null || userDelimiterStr.isEmpty()) {
522                                userDelimiterStr = ";";
523                            }
524                        } else {
525                            gst = EFindSqlStateType.stsql;
526                            appendToken(gcurrentsqlstatement, ast);
527                        }
528                    }
529
530                    if (!TBaseType.assigned(gcurrentsqlstatement)) {
531                        // Error token found
532                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo),
533                                "Error when tokenize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
534
535                        ast.tokentype = ETokenType.tttokenlizererrortoken;
536                        gst = EFindSqlStateType.sterror;
537
538                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
539                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
540                        appendToken(gcurrentsqlstatement, ast);
541                    }
542                    break;
543                }
544
545                case stsqlplus: {
546                    if (ast.tokencode == TBaseType.lexnewline) {
547                        gst = EFindSqlStateType.stnormal;
548                        appendToken(gcurrentsqlstatement, ast); // so add it here
549                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
550                    } else {
551                        appendToken(gcurrentsqlstatement, ast);
552                    }
553                    break;
554                }
555
556                case stsql: {
557                    if ((ast.tokentype == ETokenType.ttsemicolon) && (gcurrentsqlstatement.sqlstatementtype != ESqlStatementType.sstmysqldelimiter)) {
558                        gst = EFindSqlStateType.stnormal;
559                        appendToken(gcurrentsqlstatement, ast);
560                        gcurrentsqlstatement.semicolonended = ast;
561                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
562                        continue;
563                    }
564                    if (ast.toString().equalsIgnoreCase(userDelimiterStr)) {
565                        gst = EFindSqlStateType.stnormal;
566                        ast.tokencode = ';';// treat it as semicolon
567                        appendToken(gcurrentsqlstatement, ast);
568                        gcurrentsqlstatement.semicolonended = ast;
569                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
570                        continue;
571                    }
572
573                    if (ast.tokencode == TBaseType.cmtdoublehyphen) {
574                        if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter
575                            gst = EFindSqlStateType.stnormal;
576                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
577                            continue;
578                        }
579                    }
580
581                    appendToken(gcurrentsqlstatement, ast);
582
583                    if ((ast.tokencode == TBaseType.lexnewline)
584                            && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstmysqldelimiter)) {
585                        gst = EFindSqlStateType.stnormal;
586                        userDelimiterStr = "";
587                        for (int k = 0; k < gcurrentsqlstatement.sourcetokenlist.size(); k++) {
588                            TSourceToken st = gcurrentsqlstatement.sourcetokenlist.get(k);
589                            if ((st.tokencode == TBaseType.rrw_mysql_delimiter)
590                                    || (st.tokencode == TBaseType.lexnewline)
591                                    || (st.tokencode == TBaseType.lexspace)
592                                    || (st.tokencode == TBaseType.rrw_set)) // set delimiter //
593                            {
594                                continue;
595                            }
596
597                            userDelimiterStr += st.toString();
598                        }
599                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
600                        continue;
601                    }
602
603                    break;
604                }
605
606                case ststoredprocedure: {
607
608                    if ((gst == EFindSqlStateType.ststoredprocedure) && (ast.tokencode == TBaseType.cmtdoublehyphen)) {
609                        if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter
610                            gst = EFindSqlStateType.stnormal;
611                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
612                            continue;
613                        }
614                    }
615
616                    // Handle waitingDelimiter logic (when inside BEGIN...END block)
617                    // Skip this check if delimiter is ";" since we need to check for END; pattern instead
618                    if (waitingDelimiter && !userDelimiterStr.equals(";")) {
619                        if (userDelimiterStr.equalsIgnoreCase(ast.toString())) {
620                            gst = EFindSqlStateType.stnormal;
621                            gcurrentsqlstatement.semicolonended = ast;
622                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
623                            continue;
624                        } else if (userDelimiterStr.startsWith(ast.toString())) {
625                            String lcstr = ast.toString();
626                            for (int k = ast.posinlist + 1; k < ast.container.size(); k++) {
627                                TSourceToken st = ast.container.get(k);
628                                if ((st.tokencode == TBaseType.rrw_mysql_delimiter) || (st.tokencode == TBaseType.lexnewline) || (st.tokencode == TBaseType.lexspace)) {
629                                    break;
630                                }
631                                lcstr = lcstr + st.toString();
632                            }
633
634                            if (userDelimiterStr.equalsIgnoreCase(lcstr)) {
635                                for (int k = ast.posinlist; 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                                    ast.tokenstatus = ETokenStatus.tsignorebyyacc;
641                                }
642                                gst = EFindSqlStateType.stnormal;
643                                gcurrentsqlstatement.semicolonended = ast;
644                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
645                                continue;
646                            }
647                        }
648                    }
649
650                    // Set waitingDelimiter when BEGIN is encountered
651                    if (ast.tokencode == TBaseType.rrw_begin) {
652                        waitingDelimiter = true;
653                    }
654
655                    // Main delimiter handling logic
656                    // When not waiting for delimiter (no BEGIN block), complete at semicolon regardless of custom delimiter
657                    if (!waitingDelimiter) {
658                        appendToken(gcurrentsqlstatement, ast);
659                        if (ast.tokentype == ETokenType.ttsemicolon) {
660                            gst = EFindSqlStateType.stnormal;
661                            gcurrentsqlstatement.semicolonended = ast;
662                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
663                            continue;
664                        }
665                    } else {
666                        // When waitingDelimiter is true AND delimiter is ";", only check for END; pattern
667                        if (waitingDelimiter && userDelimiterStr.equals(";")) {
668                            // Check for END; pattern
669                            if ((ast.tokentype == ETokenType.ttsemicolon)) {
670                                TSourceToken lcprevtoken = ast.container.nextsolidtoken(ast, -1, false);
671                                if (lcprevtoken != null) {
672                                    if (lcprevtoken.tokencode == TBaseType.rrw_end) {
673                                        gst = EFindSqlStateType.stnormal;
674                                        gcurrentsqlstatement.semicolonended = ast;
675                                        appendToken(gcurrentsqlstatement, ast);
676                                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
677                                        continue;
678                                    }
679                                }
680                            }
681                            appendToken(gcurrentsqlstatement, ast);
682                        } else {
683                            // Custom delimiter handling (non-semicolon delimiters)
684                            if (ast.toString().equals(userDelimiterStr)) {
685                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
686                                appendToken(gcurrentsqlstatement, ast);
687                                gst = EFindSqlStateType.stnormal;
688                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
689                            } else {
690                                appendToken(gcurrentsqlstatement, ast);
691                            }
692                        }
693                    }
694
695                    /*
696                    // OLD LOGIC - replaced by above
697                    if (curdelimiterchar == ';') {
698                        appendToken(gcurrentsqlstatement, ast);
699                        if (ast.tokentype == ETokenType.ttsemicolon) {
700                            gst = EFindSqlStateType.stnormal;
701                            gcurrentsqlstatement.semicolonended = ast;
702                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
703                            continue;
704                        }
705                    } else {
706                        // Handle multi-character delimiters
707                        char ch;
708                        if (ast.getAstext().length() == 1) {
709                            ch = ast.getAstext().charAt(0);
710                        } else if ((ast.getAstext().length() > 1) && (ast.issolidtoken())) {
711                            ch = ast.getAstext().charAt(ast.getAstext().length() - 1);
712                        } else {
713                            ch = ' ';
714                        }
715
716                        if (ch == curdelimiterchar) {
717                            if (ast.getAstext().length() > 1) {
718                                String lcstr = ast.getAstext().substring(0, ast.getAstext().length() - 1);
719                                int c = flexer.getkeywordvalue(lcstr);
720                                if (c > 0) {
721                                    ast.tokencode = c;
722                                }
723                            } else {
724                                // Mark single-character delimiter to be ignored by parser
725                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
726                                gcurrentsqlstatement.semicolonended = ast;
727                            }
728                            appendToken(gcurrentsqlstatement, ast);
729                            gst = EFindSqlStateType.stnormal;
730                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
731                        } else {
732                            appendToken(gcurrentsqlstatement, ast);
733                        }
734                    }
735                    */
736                    break;
737                }
738            }
739        }
740
741        // Last statement
742        if (TBaseType.assigned(gcurrentsqlstatement) && ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) || (gst == EFindSqlStateType.sterror))) {
743            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, true, builder);
744        }
745
746        // Populate builder with results
747        builder.sqlStatements(this.sqlstatements);
748        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
749                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
750        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
751    }
752
753    /**
754     * Handle token transformations during raw statement extraction.
755     *
756     * @param ast current token being processed
757     */
758    private void performRawStatementTokenTransformations(TSourceToken ast) {
759        if (ast.tokencode == TBaseType.rrw_date) {
760            TSourceToken st1 = ast.nextSolidToken();
761            if (st1 != null) {
762                if (st1.tokencode == '(') {
763                    ast.tokencode = TBaseType.rrw_mysql_date_function;
764                } else if (st1.tokencode == TBaseType.sconst) {
765                    ast.tokencode = TBaseType.rrw_mysql_date_const;
766                }
767            }
768        } else if (ast.tokencode == TBaseType.rrw_time) {
769            TSourceToken st1 = ast.nextSolidToken();
770            if (st1 != null) {
771                if (st1.tokencode == TBaseType.sconst) {
772                    ast.tokencode = TBaseType.rrw_mysql_time_const;
773                }
774            }
775        } else if (ast.tokencode == TBaseType.rrw_timestamp) {
776            TSourceToken st1 = ast.nextSolidToken();
777            if (st1 != null) {
778                if (st1.tokencode == TBaseType.sconst) {
779                    ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
780                } else if (st1.tokencode == TBaseType.ident) {
781                    if (st1.toString().startsWith("\"")) {
782                        ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
783                        st1.tokencode = TBaseType.sconst;
784                    }
785                }
786            }
787        } else if (ast.tokencode == TBaseType.rrw_mysql_position) {
788            TSourceToken st1 = ast.nextSolidToken();
789            if (st1 != null) {
790                if (st1.tokencode != '(') {
791                    ast.tokencode = TBaseType.ident; // change position keyword to identifier if not followed by ()
792                }
793            }
794        } else if (ast.tokencode == TBaseType.rrw_mysql_row) {
795            boolean isIdent = true;
796            TSourceToken st1 = ast.nextSolidToken();
797            if (st1 != null) {
798                if (st1.tokencode == '(') {
799                    isIdent = false;
800                }
801            }
802            st1 = ast.prevSolidToken();
803            if (st1 != null) {
804                if ((st1.tokencode == TBaseType.rrw_mysql_each) || (st1.tokencode == TBaseType.rrw_mysql_current)) {
805                    isIdent = false;
806                }
807            }
808            if (isIdent) ast.tokencode = TBaseType.ident;
809        } else if (ast.tokencode == TBaseType.rrw_interval) {
810            TSourceToken leftParen = ast.searchToken('(', 1);
811            if (leftParen != null) {
812                int k = leftParen.posinlist + 1;
813                int nested = 1;
814                boolean commaToken = false;
815                while (k < ast.container.size()) {
816                    if (ast.container.get(k).tokencode == '(') {
817                        nested++;
818                    }
819                    if (ast.container.get(k).tokencode == ')') {
820                        nested--;
821                        if (nested == 0) break;
822                    }
823                    if ((ast.container.get(k).tokencode == ',') && (nested == 1)) {
824                        // only calculate the comma in the first level which is belong to interval
825                        // don't count comma in the nested () like this: INTERVAL (SELECT IF(1=1,2,3))
826                        commaToken = true;
827                        break;
828                    }
829                    k++;
830                }
831                if (commaToken) {
832                    ast.tokencode = TBaseType.rrw_mysql_interval_func;
833                }
834            }
835        }
836    }
837
838    /**
839     * Helper method to check if a statement type is in an array of types.
840     *
841     * @param type the type to check
842     * @param types array of types to check against
843     * @return true if type is in the array
844     */
845    private boolean includesqlstatementtype(ESqlStatementType type, ESqlStatementType[] types) {
846        for (ESqlStatementType t : types) {
847            if (type == t) return true;
848        }
849        return false;
850    }
851
852    private void appendToken(TCustomSqlStatement statement, TSourceToken token) {
853        if (statement == null || token == null) {
854            return;
855        }
856        token.stmt = statement;
857        statement.sourcetokenlist.add(token);
858    }
859
860    @Override
861    public String toString() {
862        return "MySqlSqlParser{vendor=" + vendor + "}";
863    }
864}