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 gst = EFindSqlStateType.stnormal; 478 } else { 479 appendToken(gcurrentsqlstatement, ast); 480 } 481 break; 482 } 483 484 case stnormal: { 485 if ((ast.tokencode == TBaseType.cmtdoublehyphen) 486 || (ast.tokencode == TBaseType.cmtslashstar) 487 || (ast.tokencode == TBaseType.lexspace) 488 || (ast.tokencode == TBaseType.lexnewline) 489 || (ast.tokentype == ETokenType.ttsemicolon)) { 490 if (TBaseType.assigned(gcurrentsqlstatement)) { 491 appendToken(gcurrentsqlstatement, ast); 492 } 493 continue; 494 } 495 496 if (ast.isFirstTokenOfLine() && (ast.toString().equalsIgnoreCase(userDelimiterStr))) { 497 ast.tokencode = ';';// treat it as semicolon 498 continue; 499 } 500 501 if ((ast.isFirstTokenOfLine()) && ((ast.tokencode == TBaseType.rrw_mysql_source) || (ast.tokencode == TBaseType.slash_dot))) { 502 gst = EFindSqlStateType.stsqlplus; 503 gcurrentsqlstatement = new TMySQLSource(vendor); 504 appendToken(gcurrentsqlstatement, ast); 505 continue; 506 } 507 508 // Find a token to start sql or plsql mode 509 gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement); 510 511 if (TBaseType.assigned(gcurrentsqlstatement)) { 512 ESqlStatementType[] ses = {ESqlStatementType.sstmysqlcreateprocedure, ESqlStatementType.sstmysqlcreatefunction, 513 ESqlStatementType.sstcreateprocedure, ESqlStatementType.sstcreatefunction, 514 ESqlStatementType.sstcreatetrigger}; 515 if (includesqlstatementtype(gcurrentsqlstatement.sqlstatementtype, ses)) { 516 gst = EFindSqlStateType.ststoredprocedure; 517 waitingDelimiter = false; 518 appendToken(gcurrentsqlstatement, ast); 519 curdelimiterchar = ';'; 520 // Only initialize userDelimiterStr if not already set by DELIMITER statement 521 if (userDelimiterStr == null || userDelimiterStr.isEmpty()) { 522 userDelimiterStr = ";"; 523 } 524 } else { 525 gst = EFindSqlStateType.stsql; 526 appendToken(gcurrentsqlstatement, ast); 527 } 528 } 529 530 if (!TBaseType.assigned(gcurrentsqlstatement)) { 531 // Error token found 532 this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo), 533 "Error when tokenize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist)); 534 535 ast.tokentype = ETokenType.tttokenlizererrortoken; 536 gst = EFindSqlStateType.sterror; 537 538 gcurrentsqlstatement = new TUnknownSqlStatement(vendor); 539 gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid; 540 appendToken(gcurrentsqlstatement, ast); 541 } 542 break; 543 } 544 545 case stsqlplus: { 546 if (ast.tokencode == TBaseType.lexnewline) { 547 gst = EFindSqlStateType.stnormal; 548 appendToken(gcurrentsqlstatement, ast); // so add it here 549 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 550 } else { 551 appendToken(gcurrentsqlstatement, ast); 552 } 553 break; 554 } 555 556 case stsql: { 557 if ((ast.tokentype == ETokenType.ttsemicolon) && (gcurrentsqlstatement.sqlstatementtype != ESqlStatementType.sstmysqldelimiter)) { 558 gst = EFindSqlStateType.stnormal; 559 appendToken(gcurrentsqlstatement, ast); 560 gcurrentsqlstatement.semicolonended = ast; 561 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 562 continue; 563 } 564 if (ast.toString().equalsIgnoreCase(userDelimiterStr)) { 565 gst = EFindSqlStateType.stnormal; 566 ast.tokencode = ';';// treat it as semicolon 567 appendToken(gcurrentsqlstatement, ast); 568 gcurrentsqlstatement.semicolonended = ast; 569 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 570 continue; 571 } 572 573 if (ast.tokencode == TBaseType.cmtdoublehyphen) { 574 if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter 575 gst = EFindSqlStateType.stnormal; 576 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 577 continue; 578 } 579 } 580 581 appendToken(gcurrentsqlstatement, ast); 582 583 if ((ast.tokencode == TBaseType.lexnewline) 584 && (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstmysqldelimiter)) { 585 gst = EFindSqlStateType.stnormal; 586 userDelimiterStr = ""; 587 for (int k = 0; k < gcurrentsqlstatement.sourcetokenlist.size(); k++) { 588 TSourceToken st = gcurrentsqlstatement.sourcetokenlist.get(k); 589 if ((st.tokencode == TBaseType.rrw_mysql_delimiter) 590 || (st.tokencode == TBaseType.lexnewline) 591 || (st.tokencode == TBaseType.lexspace) 592 || (st.tokencode == TBaseType.rrw_set)) // set delimiter // 593 { 594 continue; 595 } 596 597 userDelimiterStr += st.toString(); 598 } 599 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 600 continue; 601 } 602 603 break; 604 } 605 606 case ststoredprocedure: { 607 608 if ((gst == EFindSqlStateType.ststoredprocedure) && (ast.tokencode == TBaseType.cmtdoublehyphen)) { 609 if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter 610 gst = EFindSqlStateType.stnormal; 611 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 612 continue; 613 } 614 } 615 616 // Handle waitingDelimiter logic (when inside BEGIN...END block) 617 // Skip this check if delimiter is ";" since we need to check for END; pattern instead 618 if (waitingDelimiter && !userDelimiterStr.equals(";")) { 619 if (userDelimiterStr.equalsIgnoreCase(ast.toString())) { 620 gst = EFindSqlStateType.stnormal; 621 gcurrentsqlstatement.semicolonended = ast; 622 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 623 continue; 624 } else if (userDelimiterStr.startsWith(ast.toString())) { 625 String lcstr = ast.toString(); 626 for (int k = ast.posinlist + 1; k < ast.container.size(); k++) { 627 TSourceToken st = ast.container.get(k); 628 if ((st.tokencode == TBaseType.rrw_mysql_delimiter) || (st.tokencode == TBaseType.lexnewline) || (st.tokencode == TBaseType.lexspace)) { 629 break; 630 } 631 lcstr = lcstr + st.toString(); 632 } 633 634 if (userDelimiterStr.equalsIgnoreCase(lcstr)) { 635 for (int k = ast.posinlist; 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 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 641 } 642 gst = EFindSqlStateType.stnormal; 643 gcurrentsqlstatement.semicolonended = ast; 644 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 645 continue; 646 } 647 } 648 } 649 650 // Set waitingDelimiter when BEGIN is encountered 651 if (ast.tokencode == TBaseType.rrw_begin) { 652 waitingDelimiter = true; 653 } 654 655 // Main delimiter handling logic 656 // When not waiting for delimiter (no BEGIN block), complete at semicolon regardless of custom delimiter 657 if (!waitingDelimiter) { 658 appendToken(gcurrentsqlstatement, ast); 659 if (ast.tokentype == ETokenType.ttsemicolon) { 660 gst = EFindSqlStateType.stnormal; 661 gcurrentsqlstatement.semicolonended = ast; 662 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 663 continue; 664 } 665 } else { 666 // When waitingDelimiter is true AND delimiter is ";", only check for END; pattern 667 if (waitingDelimiter && userDelimiterStr.equals(";")) { 668 // Check for END; pattern 669 if ((ast.tokentype == ETokenType.ttsemicolon)) { 670 TSourceToken lcprevtoken = ast.container.nextsolidtoken(ast, -1, false); 671 if (lcprevtoken != null) { 672 if (lcprevtoken.tokencode == TBaseType.rrw_end) { 673 gst = EFindSqlStateType.stnormal; 674 gcurrentsqlstatement.semicolonended = ast; 675 appendToken(gcurrentsqlstatement, ast); 676 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 677 continue; 678 } 679 } 680 } 681 appendToken(gcurrentsqlstatement, ast); 682 } else { 683 // Custom delimiter handling (non-semicolon delimiters) 684 if (ast.toString().equals(userDelimiterStr)) { 685 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 686 appendToken(gcurrentsqlstatement, ast); 687 gst = EFindSqlStateType.stnormal; 688 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 689 } else { 690 appendToken(gcurrentsqlstatement, ast); 691 } 692 } 693 } 694 695 /* 696 // OLD LOGIC - replaced by above 697 if (curdelimiterchar == ';') { 698 appendToken(gcurrentsqlstatement, ast); 699 if (ast.tokentype == ETokenType.ttsemicolon) { 700 gst = EFindSqlStateType.stnormal; 701 gcurrentsqlstatement.semicolonended = ast; 702 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 703 continue; 704 } 705 } else { 706 // Handle multi-character delimiters 707 char ch; 708 if (ast.getAstext().length() == 1) { 709 ch = ast.getAstext().charAt(0); 710 } else if ((ast.getAstext().length() > 1) && (ast.issolidtoken())) { 711 ch = ast.getAstext().charAt(ast.getAstext().length() - 1); 712 } else { 713 ch = ' '; 714 } 715 716 if (ch == curdelimiterchar) { 717 if (ast.getAstext().length() > 1) { 718 String lcstr = ast.getAstext().substring(0, ast.getAstext().length() - 1); 719 int c = flexer.getkeywordvalue(lcstr); 720 if (c > 0) { 721 ast.tokencode = c; 722 } 723 } else { 724 // Mark single-character delimiter to be ignored by parser 725 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 726 gcurrentsqlstatement.semicolonended = ast; 727 } 728 appendToken(gcurrentsqlstatement, ast); 729 gst = EFindSqlStateType.stnormal; 730 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 731 } else { 732 appendToken(gcurrentsqlstatement, ast); 733 } 734 } 735 */ 736 break; 737 } 738 } 739 } 740 741 // Last statement 742 if (TBaseType.assigned(gcurrentsqlstatement) && ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) || (gst == EFindSqlStateType.sterror))) { 743 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, true, builder); 744 } 745 746 // Populate builder with results 747 builder.sqlStatements(this.sqlstatements); 748 builder.syntaxErrors(syntaxErrors instanceof ArrayList ? 749 (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors)); 750 builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size()); 751 } 752 753 /** 754 * Handle token transformations during raw statement extraction. 755 * 756 * @param ast current token being processed 757 */ 758 private void performRawStatementTokenTransformations(TSourceToken ast) { 759 if (ast.tokencode == TBaseType.rrw_date) { 760 TSourceToken st1 = ast.nextSolidToken(); 761 if (st1 != null) { 762 if (st1.tokencode == '(') { 763 ast.tokencode = TBaseType.rrw_mysql_date_function; 764 } else if (st1.tokencode == TBaseType.sconst) { 765 ast.tokencode = TBaseType.rrw_mysql_date_const; 766 } 767 } 768 } else if (ast.tokencode == TBaseType.rrw_time) { 769 TSourceToken st1 = ast.nextSolidToken(); 770 if (st1 != null) { 771 if (st1.tokencode == TBaseType.sconst) { 772 ast.tokencode = TBaseType.rrw_mysql_time_const; 773 } 774 } 775 } else if (ast.tokencode == TBaseType.rrw_timestamp) { 776 TSourceToken st1 = ast.nextSolidToken(); 777 if (st1 != null) { 778 if (st1.tokencode == TBaseType.sconst) { 779 ast.tokencode = TBaseType.rrw_mysql_timestamp_constant; 780 } else if (st1.tokencode == TBaseType.ident) { 781 if (st1.toString().startsWith("\"")) { 782 ast.tokencode = TBaseType.rrw_mysql_timestamp_constant; 783 st1.tokencode = TBaseType.sconst; 784 } 785 } 786 } 787 } else if (ast.tokencode == TBaseType.rrw_mysql_position) { 788 TSourceToken st1 = ast.nextSolidToken(); 789 if (st1 != null) { 790 if (st1.tokencode != '(') { 791 ast.tokencode = TBaseType.ident; // change position keyword to identifier if not followed by () 792 } 793 } 794 } else if (ast.tokencode == TBaseType.rrw_mysql_row) { 795 boolean isIdent = true; 796 TSourceToken st1 = ast.nextSolidToken(); 797 if (st1 != null) { 798 if (st1.tokencode == '(') { 799 isIdent = false; 800 } 801 } 802 st1 = ast.prevSolidToken(); 803 if (st1 != null) { 804 if ((st1.tokencode == TBaseType.rrw_mysql_each) || (st1.tokencode == TBaseType.rrw_mysql_current)) { 805 isIdent = false; 806 } 807 } 808 if (isIdent) ast.tokencode = TBaseType.ident; 809 } else if (ast.tokencode == TBaseType.rrw_interval) { 810 TSourceToken leftParen = ast.searchToken('(', 1); 811 if (leftParen != null) { 812 int k = leftParen.posinlist + 1; 813 int nested = 1; 814 boolean commaToken = false; 815 while (k < ast.container.size()) { 816 if (ast.container.get(k).tokencode == '(') { 817 nested++; 818 } 819 if (ast.container.get(k).tokencode == ')') { 820 nested--; 821 if (nested == 0) break; 822 } 823 if ((ast.container.get(k).tokencode == ',') && (nested == 1)) { 824 // only calculate the comma in the first level which is belong to interval 825 // don't count comma in the nested () like this: INTERVAL (SELECT IF(1=1,2,3)) 826 commaToken = true; 827 break; 828 } 829 k++; 830 } 831 if (commaToken) { 832 ast.tokencode = TBaseType.rrw_mysql_interval_func; 833 } 834 } 835 } 836 } 837 838 /** 839 * Helper method to check if a statement type is in an array of types. 840 * 841 * @param type the type to check 842 * @param types array of types to check against 843 * @return true if type is in the array 844 */ 845 private boolean includesqlstatementtype(ESqlStatementType type, ESqlStatementType[] types) { 846 for (ESqlStatementType t : types) { 847 if (type == t) return true; 848 } 849 return false; 850 } 851 852 private void appendToken(TCustomSqlStatement statement, TSourceToken token) { 853 if (statement == null || token == null) { 854 return; 855 } 856 token.stmt = statement; 857 statement.sourcetokenlist.add(token); 858 } 859 860 @Override 861 public String toString() { 862 return "MySqlSqlParser{vendor=" + vendor + "}"; 863 } 864}