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.TLexerOceanbasemysql;
009import gudusoft.gsqlparser.TParserOceanbasemysql;
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 * OceanBase user tenant — MySQL compatibility mode parser (Phase 2 fork).
036 *
037 * <p>This class is a verbatim fork of {@link MySqlSqlParser} with class
038 * names rewritten to point at the forked grammar artifacts under
039 * {@code gsp_java_parser/.../parser/oceanbasemysql/}. The structural
040 * intent is identical to the MySQL parser; what differs is:
041 *
042 * <ul>
043 *   <li>It uses {@link TLexerOceanbasemysql} and
044 *       {@link TParserOceanbasemysql} (generated from the forked
045 *       {@code lzlexoceanbasemysql.l} / {@code lzyaccoceanbasemysql.y}
046 *       sources by {@code parsetable}'s generic-vendor fallback path).</li>
047 *   <li>It registers as {@link EDbVendor#dbvoceanbase} so downstream
048 *       components (sqlcmds, IdentifierProfile, the various TSQLEnv
049 *       EnumMaps) consult OceanBase rules rather than MySQL rules.</li>
050 *   <li>It is selected by {@link OceanBaseSqlParser} when the active
051 *       {@link gudusoft.gsqlparser.EOBTenantMode} is
052 *       {@link gudusoft.gsqlparser.EOBTenantMode#MYSQL} or
053 *       {@link gudusoft.gsqlparser.EOBTenantMode#SYSTEM}.</li>
054 * </ul>
055 *
056 * <p>Phase 2 ships this fork at zero divergence from the MySQL base
057 * grammar — same shift/reduce conflicts, same token positions, same
058 * statement coverage. OceanBase-specific extensions (hints, partition
059 * extensions, tenant DDL, outlines, tablegroups) arrive incrementally
060 * in Phase 4 via additive edits to the {@code .y} source. Each Phase 4
061 * addition is tracked in {@code oceanbasemysql/FORK_DIVERGENCE.md}.
062 *
063 * <p>Backporting upstream MySQL grammar fixes is documented in the
064 * Phase 0 plan ({@code gsp_java_core/doc/oceanbase/PHASE0_REPORT.md})
065 * and the per-fork {@code oceanbasemysql/REGEN.md}.
066 *
067 * @see MySqlSqlParser the unmodified base parser this class was forked from
068 * @see OceanBaseSqlParser the mode-routing adapter that selects this parser
069 * @see TLexerOceanbasemysql forked lexer
070 * @see TParserOceanbasemysql forked parser
071 * @since 4.0.1.4
072 */
073public class OceanBaseMysqlSqlParser extends AbstractSqlParser {
074
075    // ========== Lexer and Parser Instances ==========
076    // Created once in constructor, reused for all parsing operations
077
078    /** The MySQL lexer used for tokenization (public for TGSqlParser.getFlexer()) */
079    public TLexerOceanbasemysql flexer;
080    private TParserOceanbasemysql fparser;
081
082    // ========== State Variables ==========
083    // NOTE: The following fields moved to AbstractSqlParser (inherited):
084    //   - sourcetokenlist (TSourceTokenList)
085    //   - sqlstatements (TStatementList)
086    //   - parserContext (ParserContext)
087    //   - sqlcmds (ISqlCmds)
088    //   - globalContext (TContext)
089    //   - sqlEnv (TSQLEnv)
090    //   - frameStack (Stack<TFrame>)
091    //   - globalFrame (TFrame)
092    //   - lexer (TCustomLexer)
093
094    // ========== State Variables for Raw Statement Extraction ==========
095    private String userDelimiterStr;
096    private char curdelimiterchar;
097    private boolean waitingDelimiter;
098
099    // ========== Constructor ==========
100
101    /**
102     * Construct MySQL SQL parser.
103     * <p>
104     * Configures the parser for MySQL database with default delimiter: semicolon (;)
105     * <p>
106     * Following the original TGSqlParser pattern, the lexer and parser are
107     * created once in the constructor and reused for all parsing operations.
108     */
109    public OceanBaseMysqlSqlParser() {
110        // Phase 2 fork: register as dbvoceanbase even though the lexer/parser
111        // are forked from MySQL. AST nodes built via this parser report
112        // dbvoceanbase via the NodeFactory back-reference fixup in
113        // TGSqlParser.doDelegatedRawParse(); the vendor passed here is the
114        // one consulted by sqlcmds, IdentifierProfile, and the various
115        // EnumMaps in TSQLEnv.
116        super(EDbVendor.dbvoceanbase);
117
118        // Set delimiter character
119        this.delimiterChar = '$';
120        this.defaultDelimiterStr = "$";
121
122        // Create lexer once - will be reused for all parsing operations
123        this.flexer = new TLexerOceanbasemysql();
124        this.flexer.delimiterchar = this.delimiterChar;
125        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
126
127        // CRITICAL: Set lexer for inherited getanewsourcetoken() method
128        this.lexer = this.flexer;
129
130        // Create parser once - will be reused for all parsing operations
131        this.fparser = new TParserOceanbasemysql(null);
132        this.fparser.lexer = this.flexer;
133
134        // NOTE: sourcetokenlist and sqlstatements are initialized in AbstractSqlParser constructor
135    }
136
137    // ========== AbstractSqlParser Abstract Methods Implementation ==========
138
139    /**
140     * Return the MySQL lexer instance.
141     * <p>
142     * The lexer is created once in the constructor and reused for all
143     * parsing operations. This method simply returns the existing instance,
144     * matching the original TGSqlParser pattern where the lexer is created
145     * once and reset before each use.
146     *
147     * @param context parser context (not used, lexer already created)
148     * @return the MySQL lexer instance created in constructor
149     */
150    @Override
151    protected TCustomLexer getLexer(ParserContext context) {
152        // Return existing lexer instance (created in constructor)
153        return this.flexer;
154    }
155
156    /**
157     * Return the MySQL SQL parser instance with updated token list.
158     * <p>
159     * The parser is created once in the constructor and reused for all
160     * parsing operations. This method updates the token list and returns
161     * the existing instance, matching the original TGSqlParser pattern.
162     *
163     * @param context parser context (not used, parser already created)
164     * @param tokens source token list to parse
165     * @return the MySQL SQL parser instance created in constructor
166     */
167    @Override
168    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
169        // Update token list for reused parser instance
170        this.fparser.sourcetokenlist = tokens;
171        return this.fparser;
172    }
173
174    /**
175     * Call MySQL-specific tokenization logic.
176     * <p>
177     * Delegates to domysqltexttotokenlist which handles MySQL's
178     * specific keyword recognition, delimiter handling, and token generation.
179     */
180    @Override
181    protected void tokenizeVendorSql() {
182        domysqltexttotokenlist();
183    }
184
185    /**
186     * Setup MySQL parser for raw statement extraction.
187     * <p>
188     * MySQL uses a single parser, so we inject sqlcmds and update
189     * the token list for the main parser only.
190     */
191    @Override
192    protected void setupVendorParsersForExtraction() {
193        this.fparser.sqlcmds = this.sqlcmds;
194        this.fparser.sourcetokenlist = this.sourcetokenlist;
195    }
196
197    /**
198     * Call MySQL-specific raw statement extraction logic.
199     * <p>
200     * Delegates to domysqlgetrawsqlstatements which handles MySQL's
201     * statement delimiters (semicolon by default, or custom delimiter via DELIMITER command).
202     */
203    @Override
204    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
205        domysqlgetrawsqlstatements(builder);
206    }
207
208    /**
209     * Perform full parsing of statements with syntax checking.
210     * <p>
211     * This method orchestrates the parsing of all statements.
212     *
213     * <p><b>Important:</b> This method does NOT extract raw statements - they are
214     * passed in as a parameter already extracted by {@link #extractRawStatements}.
215     *
216     * @param context parser context
217     * @param parser main SQL parser (TParserOceanbasemysql)
218     * @param secondaryParser not used for MySQL
219     * @param tokens source token list
220     * @param rawStatements raw statements already extracted (never null)
221     * @return list of fully parsed statements with AST built
222     */
223    @Override
224    protected TStatementList performParsing(ParserContext context,
225                                           TCustomParser parser,
226                                           TCustomParser secondaryParser,
227                                           TSourceTokenList tokens,
228                                           TStatementList rawStatements) {
229        // Store references (fparser is already set, don't reassign final variable)
230        this.sourcetokenlist = tokens;
231        this.parserContext = context;
232
233        // Use the raw statements passed from AbstractSqlParser.parse()
234        // (already extracted - DO NOT re-extract to avoid duplication)
235        this.sqlstatements = rawStatements;
236
237        // Initialize sqlcmds for the parser
238        this.sqlcmds = SqlCmdsFactory.get(vendor);
239        this.fparser.sqlcmds = this.sqlcmds;
240
241        // Initialize global context for statement parsing
242        initializeGlobalContext();
243
244        // Parse each statement
245        for (int i = 0; i < sqlstatements.size(); i++) {
246            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
247
248            try {
249                // Set frame stack for the statement (needed for parsing)
250                stmt.setFrameStack(frameStack);
251
252                // Parse the statement
253                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
254
255                // Handle error recovery for CREATE TABLE statements if enabled
256                boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE;
257                if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) {
258                    handleCreateTableErrorRecovery(stmt);
259                }
260
261                // Collect syntax errors
262                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
263                    copyErrorsFromStatement(stmt);
264                }
265            } catch (Exception ex) {
266                // Use inherited exception handler
267                handleStatementParsingException(stmt, i, ex);
268                continue;
269            }
270        }
271
272        // Clean up frame stack
273        if (globalFrame != null) {
274            globalFrame.popMeFromStack(frameStack);
275        }
276
277        return this.sqlstatements;
278    }
279
280    /**
281     * Handle error recovery for CREATE TABLE statements.
282     * <p>
283     * Migrated from TGSqlParser.handleCreateTableErrorRecovery()
284     * <p>
285     * This method marks unparseable table properties as sqlpluscmd tokens
286     * and retries parsing, similar to MSSQL error recovery.
287     *
288     * @param stmt the statement that failed to parse
289     */
290    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
291        if ((stmt.sqlstatementtype != ESqlStatementType.sstcreatetable) || TBaseType.c_createTableStrictParsing) {
292            return;
293        }
294
295        int nested = 0;
296        boolean isIgnore = false, isFoundIgnoreToken = false;
297        TSourceToken firstIgnoreToken = null;
298
299        for (int k = 0; k < stmt.sourcetokenlist.size(); k++) {
300            TSourceToken st = stmt.sourcetokenlist.get(k);
301            if (isIgnore) {
302                if (st.issolidtoken() && (st.tokencode != ';')) {
303                    isFoundIgnoreToken = true;
304                    if (firstIgnoreToken == null) {
305                        firstIgnoreToken = st;
306                    }
307                }
308                if (st.tokencode != ';') {
309                    st.tokencode = TBaseType.sqlpluscmd;
310                }
311                continue;
312            }
313            if (st.tokencode == (int) ')') {
314                nested--;
315                if (nested == 0) {
316                    boolean isSelect = false;
317                    TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1);
318                    if (st1 != null) {
319                        TSourceToken st2 = st.searchToken((int) '(', 2);
320                        if (st2 != null) {
321                            TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3);
322                            isSelect = (st3 != null);
323                        }
324                    }
325                    if (!isSelect) isIgnore = true;
326                }
327            } else if (st.tokencode == (int) '(') {
328                nested++;
329            }
330        }
331
332        if (isFoundIgnoreToken) {
333            stmt.clearError();
334            stmt.parsestatement(null, false, this.parserContext.isOnlyNeedRawParseTree());
335        }
336    }
337
338    // ========== MySQL-Specific Tokenization ==========
339
340    /**
341     * Perform MySQL-specific tokenization.
342     * <p>
343     * Extracted from TGSqlParser.domysqltexttotokenlist() (lines 4759-4822)
344     */
345    private void domysqltexttotokenlist() {
346        TSourceToken asourcetoken, lcprevst;
347        int yychar;
348        boolean startDelimiter = false;
349
350        flexer.tmpDelimiter = "";
351
352        asourcetoken = getanewsourcetoken();
353        if (asourcetoken == null) return;
354        yychar = asourcetoken.tokencode;
355        checkMySQLCommentToken(asourcetoken);
356
357        if ((asourcetoken.tokencode == TBaseType.rrw_mysql_delimiter)) {
358            startDelimiter = true;
359        }
360
361        while (yychar > 0) {
362            sourcetokenlist.add(asourcetoken);
363            asourcetoken = getanewsourcetoken();
364            if (asourcetoken == null) break;
365            checkMySQLCommentToken(asourcetoken);
366
367            if ((asourcetoken.tokencode == TBaseType.lexnewline) && (startDelimiter)) {
368                startDelimiter = false;
369                flexer.tmpDelimiter = sourcetokenlist.get(sourcetokenlist.size() - 1).getAstext();
370            }
371
372            if ((asourcetoken.tokencode == TBaseType.rrw_mysql_delimiter)) {
373                startDelimiter = true;
374            }
375
376            if (asourcetoken.tokencode == TBaseType.rrw_rollup) {
377                // with rollup
378                lcprevst = getprevsolidtoken(asourcetoken);
379                if (lcprevst != null) {
380                    if (lcprevst.tokencode == TBaseType.rrw_with)
381                        lcprevst.tokencode = TBaseType.with_rollup;
382                }
383            }
384
385            if ((asourcetoken.tokencode == TBaseType.rrw_mysql_d)
386                    || (asourcetoken.tokencode == TBaseType.rrw_mysql_t)
387                    || (asourcetoken.tokencode == TBaseType.rrw_mysql_ts)) {
388                // odbc date constant { d 'str' }
389                lcprevst = getprevsolidtoken(asourcetoken);
390                if (lcprevst != null) {
391                    if (lcprevst.tokencode != '{')
392                        asourcetoken.tokencode = TBaseType.ident;
393                }
394            }
395
396            if (asourcetoken.tokencode == (int) '=') {
397                // "SET role = ..." assigns a variable named role (parity with
398                // base MySQL); without this the grammar's SET ROLE statement
399                // path shifts ROLE and the assignment form fails to parse.
400                TSourceToken roleTk = lastSolidTokenInList();
401                if ((roleTk != null) && (roleTk.tokencode == TBaseType.rrw_oceanbase_role)) {
402                    TSourceToken setTk = getprevsolidtoken(roleTk);
403                    if ((setTk != null) && (setTk.tokencode == TBaseType.rrw_set)) {
404                        roleTk.tokencode = TBaseType.ident;
405                    }
406                }
407            }
408
409            yychar = asourcetoken.tokencode;
410        }
411    }
412
413    /**
414     * Check if MySQL comment token is valid.
415     * <p>
416     * MySQL requires a space after -- for double-hyphen comments.
417     * This method was present in TGSqlParser but the implementation
418     * was commented out, so we keep it as a placeholder.
419     *
420     * @param cmtToken comment token to check
421     */
422    private void checkMySQLCommentToken(TSourceToken cmtToken) {
423        // Implementation was commented out in original TGSqlParser
424        // Keeping this method as placeholder for future use
425    }
426
427    /**
428     * Get the last solid token already appended to sourcetokenlist. Used by
429     * the tokenize loop for lookbehind on the current (not yet appended)
430     * token, where getprevsolidtoken's container/posinlist are not set yet.
431     *
432     * @return last solid token in sourcetokenlist, or null
433     */
434    private TSourceToken lastSolidTokenInList() {
435        for (int i = sourcetokenlist.size() - 1; i >= 0; i--) {
436            TSourceToken st = sourcetokenlist.get(i);
437            if ((st.tokentype == ETokenType.ttwhitespace)
438                    || (st.tokentype == ETokenType.ttreturn)
439                    || (st.tokentype == ETokenType.ttsimplecomment)
440                    || (st.tokentype == ETokenType.ttbracketedcomment)) {
441                continue;
442            }
443            return st;
444        }
445        return null;
446    }
447
448    /**
449     * Get previous non-whitespace token.
450     *
451     * @param ptoken current token
452     * @return previous solid token, or null
453     */
454    private TSourceToken getprevsolidtoken(TSourceToken ptoken) {
455        TSourceToken ret = null;
456        TSourceTokenList lctokenlist = ptoken.container;
457
458        if (lctokenlist != null) {
459            if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) {
460                if (!(
461                        (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttwhitespace)
462                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttreturn)
463                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttsimplecomment)
464                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttbracketedcomment)
465                )) {
466                    ret = lctokenlist.get(ptoken.posinlist - 1);
467                } else {
468                    ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false);
469                }
470            }
471        }
472        return ret;
473    }
474
475    // ========== MySQL-Specific Raw Statement Extraction ==========
476
477    /**
478     * Extract raw MySQL SQL statements from tokenized source.
479     * <p>
480     * Extracted from TGSqlParser.domysqlgetrawsqlstatements() (lines 14979-15344)
481     *
482     * @param builder the result builder to populate with raw statements
483     */
484    private void domysqlgetrawsqlstatements(SqlParseResult.Builder builder) {
485        TCustomSqlStatement gcurrentsqlstatement = null;
486        EFindSqlStateType gst = EFindSqlStateType.stnormal;
487
488        // Reset delimiter
489        userDelimiterStr = defaultDelimiterStr;
490
491        if (TBaseType.assigned(sqlstatements)) sqlstatements.clear();
492        if (!TBaseType.assigned(sourcetokenlist)) {
493            // No tokens available - populate builder with empty results and return
494            builder.sqlStatements(this.sqlstatements);
495            builder.errorCode(1);
496            builder.errorMessage("No source token list available");
497            return;
498        }
499
500        for (int i = 0; i < sourcetokenlist.size(); i++) {
501            TSourceToken ast = sourcetokenlist.get(i);
502            sourcetokenlist.curpos = i;
503
504            // Token transformations during raw statement extraction
505            performRawStatementTokenTransformations(ast);
506
507            switch (gst) {
508                case sterror: {
509                    if (ast.tokentype == ETokenType.ttsemicolon) {
510                        appendToken(gcurrentsqlstatement, ast);
511                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
512                        gcurrentsqlstatement = null;
513                        gst = EFindSqlStateType.stnormal;
514                    } else {
515                        appendToken(gcurrentsqlstatement, ast);
516                    }
517                    break;
518                }
519
520                case stnormal: {
521                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
522                            || (ast.tokencode == TBaseType.cmtslashstar)
523                            || (ast.tokencode == TBaseType.lexspace)
524                            || (ast.tokencode == TBaseType.lexnewline)
525                            || (ast.tokentype == ETokenType.ttsemicolon)) {
526                        if (TBaseType.assigned(gcurrentsqlstatement)) {
527                            appendToken(gcurrentsqlstatement, ast);
528                        }
529                        continue;
530                    }
531
532                    if (ast.isFirstTokenOfLine() && (ast.toString().equalsIgnoreCase(userDelimiterStr))) {
533                        ast.tokencode = ';';// treat it as semicolon
534                        continue;
535                    }
536
537                    if ((ast.isFirstTokenOfLine()) && ((ast.tokencode == TBaseType.rrw_mysql_source) || (ast.tokencode == TBaseType.slash_dot))) {
538                        gst = EFindSqlStateType.stsqlplus;
539                        gcurrentsqlstatement = new TMySQLSource(vendor);
540                        appendToken(gcurrentsqlstatement, ast);
541                        continue;
542                    }
543
544                    // Find a token to start sql or plsql mode
545                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
546
547                    if (TBaseType.assigned(gcurrentsqlstatement)) {
548                        ESqlStatementType[] ses = {ESqlStatementType.sstmysqlcreateprocedure, ESqlStatementType.sstmysqlcreatefunction,
549                                ESqlStatementType.sstcreateprocedure, ESqlStatementType.sstcreatefunction,
550                                ESqlStatementType.sstcreatetrigger, ESqlStatementType.sstmysqlcreateevent,
551                                ESqlStatementType.sstmysqlalterevent};
552                        if (includesqlstatementtype(gcurrentsqlstatement.sqlstatementtype, ses)) {
553                            gst = EFindSqlStateType.ststoredprocedure;
554                            waitingDelimiter = false;
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                        userDelimiterStr = "";
628                        for (int k = 0; k < gcurrentsqlstatement.sourcetokenlist.size(); k++) {
629                            TSourceToken st = gcurrentsqlstatement.sourcetokenlist.get(k);
630                            if ((st.tokencode == TBaseType.rrw_mysql_delimiter)
631                                    || (st.tokencode == TBaseType.lexnewline)
632                                    || (st.tokencode == TBaseType.lexspace)
633                                    || (st.tokencode == TBaseType.rrw_set)) // set delimiter //
634                            {
635                                continue;
636                            }
637
638                            userDelimiterStr += st.toString();
639                        }
640                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
641                        gcurrentsqlstatement = null;
642                        continue;
643                    }
644
645                    break;
646                }
647
648                case ststoredprocedure: {
649
650                    if ((gst == EFindSqlStateType.ststoredprocedure) && (ast.tokencode == TBaseType.cmtdoublehyphen)) {
651                        if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter
652                            gst = EFindSqlStateType.stnormal;
653                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
654                            gcurrentsqlstatement = null;
655                            continue;
656                        }
657                    }
658
659                    // Handle waitingDelimiter logic (when inside BEGIN...END block)
660                    // Skip this check if delimiter is ";" since we need to check for END; pattern instead
661                    if (waitingDelimiter && !userDelimiterStr.equals(";")) {
662                        if (userDelimiterStr.equalsIgnoreCase(ast.toString())) {
663                            gst = EFindSqlStateType.stnormal;
664                            gcurrentsqlstatement.semicolonended = ast;
665                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
666                            gcurrentsqlstatement = null;
667                            continue;
668                        } else if (userDelimiterStr.startsWith(ast.toString())) {
669                            String lcstr = ast.toString();
670                            for (int k = ast.posinlist + 1; k < ast.container.size(); k++) {
671                                TSourceToken st = ast.container.get(k);
672                                if ((st.tokencode == TBaseType.rrw_mysql_delimiter) || (st.tokencode == TBaseType.lexnewline) || (st.tokencode == TBaseType.lexspace)) {
673                                    break;
674                                }
675                                lcstr = lcstr + st.toString();
676                            }
677
678                            if (userDelimiterStr.equalsIgnoreCase(lcstr)) {
679                                int lastDelimiterPos = ast.posinlist;
680                                for (int k = ast.posinlist; k < ast.container.size(); k++) {
681                                    TSourceToken st = ast.container.get(k);
682                                    if ((st.tokencode == TBaseType.rrw_mysql_delimiter) || (st.tokencode == TBaseType.lexnewline) || (st.tokencode == TBaseType.lexspace)) {
683                                        break;
684                                    }
685                                    st.tokenstatus = ETokenStatus.tsignorebyyacc;
686                                    lastDelimiterPos = k;
687                                }
688                                gst = EFindSqlStateType.stnormal;
689                                gcurrentsqlstatement.semicolonended = ast;
690                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
691                                gcurrentsqlstatement = null;
692                                i = lastDelimiterPos; // advance past all delimiter tokens
693                                continue;
694                            }
695                        }
696                    }
697
698                    // Set waitingDelimiter when BEGIN is encountered
699                    if (ast.tokencode == TBaseType.rrw_begin) {
700                        waitingDelimiter = true;
701                    }
702
703                    // Main delimiter handling logic
704                    // When not waiting for delimiter (no BEGIN block), complete at semicolon regardless of custom delimiter
705                    if (!waitingDelimiter) {
706                        appendToken(gcurrentsqlstatement, ast);
707                        if (ast.tokentype == ETokenType.ttsemicolon) {
708                            gst = EFindSqlStateType.stnormal;
709                            gcurrentsqlstatement.semicolonended = ast;
710                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
711                            gcurrentsqlstatement = null;
712                            continue;
713                        }
714                    } else {
715                        // When waitingDelimiter is true AND delimiter is ";", only check for END; pattern
716                        if (waitingDelimiter && userDelimiterStr.equals(";")) {
717                            // Check for END; pattern
718                            if ((ast.tokentype == ETokenType.ttsemicolon)) {
719                                TSourceToken lcprevtoken = ast.container.nextsolidtoken(ast, -1, false);
720                                if (lcprevtoken != null) {
721                                    if (lcprevtoken.tokencode == TBaseType.rrw_end) {
722                                        gst = EFindSqlStateType.stnormal;
723                                        gcurrentsqlstatement.semicolonended = ast;
724                                        appendToken(gcurrentsqlstatement, ast);
725                                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
726                                        gcurrentsqlstatement = null;
727                                        continue;
728                                    }
729                                }
730                            }
731                            appendToken(gcurrentsqlstatement, ast);
732                        } else {
733                            // Custom delimiter handling (non-semicolon delimiters)
734                            if (ast.toString().equals(userDelimiterStr)) {
735                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
736                                appendToken(gcurrentsqlstatement, ast);
737                                gst = EFindSqlStateType.stnormal;
738                                onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
739                                gcurrentsqlstatement = null;
740                            } else {
741                                appendToken(gcurrentsqlstatement, ast);
742                            }
743                        }
744                    }
745
746                    /*
747                    // OLD LOGIC - replaced by above
748                    if (curdelimiterchar == ';') {
749                        appendToken(gcurrentsqlstatement, ast);
750                        if (ast.tokentype == ETokenType.ttsemicolon) {
751                            gst = EFindSqlStateType.stnormal;
752                            gcurrentsqlstatement.semicolonended = ast;
753                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
754                            continue;
755                        }
756                    } else {
757                        // Handle multi-character delimiters
758                        char ch;
759                        if (ast.getAstext().length() == 1) {
760                            ch = ast.getAstext().charAt(0);
761                        } else if ((ast.getAstext().length() > 1) && (ast.issolidtoken())) {
762                            ch = ast.getAstext().charAt(ast.getAstext().length() - 1);
763                        } else {
764                            ch = ' ';
765                        }
766
767                        if (ch == curdelimiterchar) {
768                            if (ast.getAstext().length() > 1) {
769                                String lcstr = ast.getAstext().substring(0, ast.getAstext().length() - 1);
770                                int c = flexer.getkeywordvalue(lcstr);
771                                if (c > 0) {
772                                    ast.tokencode = c;
773                                }
774                            } else {
775                                // Mark single-character delimiter to be ignored by parser
776                                ast.tokenstatus = ETokenStatus.tsignorebyyacc;
777                                gcurrentsqlstatement.semicolonended = ast;
778                            }
779                            appendToken(gcurrentsqlstatement, ast);
780                            gst = EFindSqlStateType.stnormal;
781                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
782                        } else {
783                            appendToken(gcurrentsqlstatement, ast);
784                        }
785                    }
786                    */
787                    break;
788                }
789            }
790        }
791
792        // Last statement
793        if (TBaseType.assigned(gcurrentsqlstatement) && ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) || (gst == EFindSqlStateType.sterror))) {
794            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, true, builder);
795        }
796
797        // Populate builder with results
798        builder.sqlStatements(this.sqlstatements);
799        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
800                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
801        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
802    }
803
804    /**
805     * Handle token transformations during raw statement extraction.
806     *
807     * @param ast current token being processed
808     */
809    private void performRawStatementTokenTransformations(TSourceToken ast) {
810        if (ast.tokencode == TBaseType.rrw_date) {
811            TSourceToken st1 = ast.nextSolidToken();
812            if (st1 != null) {
813                if (st1.tokencode == '(') {
814                    ast.tokencode = TBaseType.rrw_mysql_date_function;
815                } else if (st1.tokencode == TBaseType.sconst) {
816                    ast.tokencode = TBaseType.rrw_mysql_date_const;
817                }
818            }
819        } else if (ast.tokencode == TBaseType.rrw_time) {
820            TSourceToken st1 = ast.nextSolidToken();
821            if (st1 != null) {
822                if (st1.tokencode == TBaseType.sconst) {
823                    ast.tokencode = TBaseType.rrw_mysql_time_const;
824                }
825            }
826        } else if (ast.tokencode == TBaseType.rrw_timestamp) {
827            TSourceToken st1 = ast.nextSolidToken();
828            if (st1 != null) {
829                if (st1.tokencode == TBaseType.sconst) {
830                    ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
831                } else if (st1.tokencode == TBaseType.ident) {
832                    if (st1.toString().startsWith("\"")) {
833                        ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
834                        st1.tokencode = TBaseType.sconst;
835                    }
836                }
837            }
838        } else if (ast.tokencode == TBaseType.rrw_mysql_position) {
839            TSourceToken st1 = ast.nextSolidToken();
840            if (st1 != null) {
841                if (st1.tokencode != '(') {
842                    ast.tokencode = TBaseType.ident; // change position keyword to identifier if not followed by ()
843                }
844            }
845        } else if (ast.tokencode == TBaseType.rrw_mysql_row) {
846            boolean isIdent = true;
847            TSourceToken st1 = ast.nextSolidToken();
848            if (st1 != null) {
849                if (st1.tokencode == '(') {
850                    isIdent = false;
851                }
852            }
853            st1 = ast.prevSolidToken();
854            if (st1 != null) {
855                if ((st1.tokencode == TBaseType.rrw_mysql_each) || (st1.tokencode == TBaseType.rrw_mysql_current)) {
856                    isIdent = false;
857                }
858            }
859            if (isIdent) ast.tokencode = TBaseType.ident;
860        } else if (ast.tokencode == TBaseType.rrw_interval) {
861            TSourceToken leftParen = ast.searchToken('(', 1);
862            if (leftParen != null) {
863                int k = leftParen.posinlist + 1;
864                int nested = 1;
865                boolean commaToken = false;
866                while (k < ast.container.size()) {
867                    if (ast.container.get(k).tokencode == '(') {
868                        nested++;
869                    }
870                    if (ast.container.get(k).tokencode == ')') {
871                        nested--;
872                        if (nested == 0) break;
873                    }
874                    if ((ast.container.get(k).tokencode == ',') && (nested == 1)) {
875                        // only calculate the comma in the first level which is belong to interval
876                        // don't count comma in the nested () like this: INTERVAL (SELECT IF(1=1,2,3))
877                        commaToken = true;
878                        break;
879                    }
880                    k++;
881                }
882                if (commaToken) {
883                    ast.tokencode = TBaseType.rrw_mysql_interval_func;
884                }
885            }
886        }
887    }
888
889    /**
890     * Helper method to check if a statement type is in an array of types.
891     *
892     * @param type the type to check
893     * @param types array of types to check against
894     * @return true if type is in the array
895     */
896    private boolean includesqlstatementtype(ESqlStatementType type, ESqlStatementType[] types) {
897        for (ESqlStatementType t : types) {
898            if (type == t) return true;
899        }
900        return false;
901    }
902
903    private void appendToken(TCustomSqlStatement statement, TSourceToken token) {
904        if (statement == null || token == null) {
905            return;
906        }
907        token.stmt = statement;
908        statement.sourcetokenlist.add(token);
909    }
910
911    @Override
912    public String toString() {
913        return "OceanBaseMysqlSqlParser{vendor=" + vendor + "}";
914    }
915}