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