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}