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