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