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}