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.TLexerRedshift; 009import gudusoft.gsqlparser.TParserRedshift; 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.TUnknownSqlStatement; 020import gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement; 021import gudusoft.gsqlparser.stmt.TCommonBlock; 022import gudusoft.gsqlparser.stmt.TRoutine; 023import gudusoft.gsqlparser.sqlcmds.ISqlCmds; 024import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory; 025import gudusoft.gsqlparser.compiler.TContext; 026import gudusoft.gsqlparser.sqlenv.TSQLEnv; 027import gudusoft.gsqlparser.compiler.TGlobalScope; 028import gudusoft.gsqlparser.compiler.TFrame; 029import gudusoft.gsqlparser.resolver.TSQLResolver; 030import gudusoft.gsqlparser.TLog; 031import gudusoft.gsqlparser.compiler.TASTEvaluator; 032 033import java.io.BufferedReader; 034import java.util.ArrayList; 035import java.util.List; 036import java.util.Stack; 037 038/** 039 * Amazon Redshift SQL parser implementation. 040 * 041 * <p>This parser handles Redshift-specific SQL syntax including: 042 * <ul> 043 * <li>PostgreSQL-based syntax (Redshift is based on PostgreSQL 8.0.2)</li> 044 * <li>PL/pgSQL functions and procedures</li> 045 * <li>CREATE FUNCTION with LANGUAGE clause</li> 046 * <li>Function body delimiters ($$)</li> 047 * <li>Redshift-specific types (ARRAY<type>, %ROWTYPE, etc.)</li> 048 * <li>Redshift-specific keywords (FILTER, LANGUAGE, etc.)</li> 049 * </ul> 050 * 051 * <p><b>Design Notes:</b> 052 * <ul> 053 * <li>Extends {@link AbstractSqlParser} using the template method pattern</li> 054 * <li>Uses {@link TLexerRedshift} for tokenization</li> 055 * <li>Uses {@link TParserRedshift} for parsing</li> 056 * <li>Delimiter character: ';' for SQL statements</li> 057 * </ul> 058 * 059 * <p><b>Usage Example:</b> 060 * <pre> 061 * // Get Redshift parser from factory 062 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvredshift); 063 * 064 * // Build context 065 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvredshift) 066 * .sqlText("SELECT * FROM orders WHERE order_date > CURRENT_DATE - 7") 067 * .build(); 068 * 069 * // Parse 070 * SqlParseResult result = parser.parse(context); 071 * 072 * // Access statements 073 * TStatementList statements = result.getSqlStatements(); 074 * </pre> 075 * 076 * @see SqlParser 077 * @see AbstractSqlParser 078 * @see TLexerRedshift 079 * @see TParserRedshift 080 * @since 3.2.0.0 081 */ 082public class RedshiftSqlParser extends AbstractSqlParser { 083 084 /** 085 * Construct Redshift SQL parser. 086 * <p> 087 * Configures the parser for Redshift database with default delimiter (;). 088 * <p> 089 * Following the original TGSqlParser pattern, the lexer and parser are 090 * created once in the constructor and reused for all parsing operations. 091 */ 092 public RedshiftSqlParser() { 093 super(EDbVendor.dbvredshift); 094 this.delimiterChar = ';'; 095 this.defaultDelimiterStr = ";"; 096 097 // Create lexer once - will be reused for all parsing operations 098 this.flexer = new TLexerRedshift(); 099 this.flexer.delimiterchar = this.delimiterChar; 100 this.flexer.defaultDelimiterStr = this.defaultDelimiterStr; 101 102 // CRITICAL: Set parent's lexer reference for shared tokenization logic 103 this.lexer = this.flexer; 104 105 // Create parser once - will be reused for all parsing operations 106 this.fparser = new TParserRedshift(null); 107 this.fparser.lexer = this.flexer; 108 } 109 110 // ========== Parser Components ========== 111 112 /** The Redshift lexer used for tokenization */ 113 public TLexerRedshift flexer; 114 115 /** SQL parser (for Redshift statements) */ 116 private TParserRedshift fparser; 117 118 /** Current statement being built during extraction */ 119 private TCustomSqlStatement gcurrentsqlstatement; 120 121 // Note: Global context and frame stack fields inherited from AbstractSqlParser: 122 // - protected TContext globalContext 123 // - protected TSQLEnv sqlEnv 124 // - protected Stack<TFrame> frameStack 125 // - protected TFrame globalFrame 126 127 // ========== AbstractSqlParser Abstract Methods Implementation ========== 128 129 /** 130 * Return the Redshift lexer instance. 131 */ 132 @Override 133 protected TCustomLexer getLexer(ParserContext context) { 134 return this.flexer; 135 } 136 137 /** 138 * Return the Redshift SQL parser instance with updated token list. 139 */ 140 @Override 141 protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) { 142 this.fparser.sourcetokenlist = tokens; 143 return this.fparser; 144 } 145 146 /** 147 * Redshift doesn't have a secondary parser. 148 * <p> 149 * Only Oracle uses a secondary parser (PL/SQL parser). 150 */ 151 @Override 152 protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) { 153 return null; 154 } 155 156 /** 157 * Call Redshift-specific tokenization logic. 158 * <p> 159 * Delegates to doredshiftsqltexttotokenlist which handles Redshift's 160 * specific keyword recognition, PostgreSQL commands, and token generation. 161 */ 162 @Override 163 protected void tokenizeVendorSql() { 164 doredshiftsqltexttotokenlist(); 165 } 166 167 /** 168 * Setup Redshift parser for raw statement extraction. 169 * <p> 170 * Redshift uses a single parser, so we inject sqlcmds and update 171 * the token list for the main parser only. 172 */ 173 @Override 174 protected void setupVendorParsersForExtraction() { 175 // Inject sqlcmds into parser (required for make_stmt) 176 this.fparser.sqlcmds = this.sqlcmds; 177 178 // Update token list for parser 179 this.fparser.sourcetokenlist = this.sourcetokenlist; 180 } 181 182 /** 183 * Call Redshift-specific raw statement extraction logic. 184 * <p> 185 * Delegates to doredshiftgetrawsqlstatements which handles Redshift's 186 * statement delimiters (semicolon, function delimiters $$, etc.). 187 */ 188 @Override 189 protected void extractVendorRawStatements(SqlParseResult.Builder builder) { 190 doredshiftgetrawsqlstatements(builder); 191 192 // Set the extracted statements in the builder 193 builder.sqlStatements(this.sqlstatements); 194 } 195 196 /** 197 * Perform full parsing of statements with syntax checking. 198 * <p> 199 * This method orchestrates the parsing of all statements. 200 */ 201 @Override 202 protected TStatementList performParsing(ParserContext context, 203 TCustomParser parser, 204 TCustomParser secondaryParser, 205 TSourceTokenList tokens, 206 TStatementList rawStatements) { 207 // Store references 208 this.fparser = (TParserRedshift) parser; 209 this.sourcetokenlist = tokens; 210 this.parserContext = context; 211 212 // Use the raw statements passed from AbstractSqlParser.parse() 213 this.sqlstatements = rawStatements; 214 215 // Initialize sqlcmds (required for parsing) 216 this.sqlcmds = SqlCmdsFactory.get(vendor); 217 218 // CRITICAL: Inject sqlcmds into parser (required for make_stmt) 219 this.fparser.sqlcmds = this.sqlcmds; 220 221 // Initialize global context for semantic analysis 222 initializeGlobalContext(); 223 224 // Parse each statement 225 for (int i = 0; i < sqlstatements.size(); i++) { 226 TCustomSqlStatement stmt = sqlstatements.getRawSql(i); 227 228 try { 229 // Set frame stack for this statement 230 stmt.setFrameStack(frameStack); 231 232 // Parse the statement 233 int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree()); 234 235 // Vendor-specific post-processing (override hook if needed) 236 afterStatementParsed(stmt); 237 238 // Error recovery 239 boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE; 240 if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) { 241 handleCreateTableErrorRecovery(stmt); 242 } 243 244 // Collect errors 245 if ((parseResult != 0) || (stmt.getErrorCount() > 0)) { 246 copyErrorsFromStatement(stmt); 247 } 248 249 } catch (Exception ex) { 250 // Use inherited exception handler 251 handleStatementParsingException(stmt, i, ex); 252 continue; 253 } 254 } 255 256 // Clean up frame stack 257 if (globalFrame != null) { 258 globalFrame.popMeFromStack(frameStack); 259 } 260 261 return sqlstatements; 262 } 263 264 /** 265 * Perform semantic analysis on parsed statements. 266 * <p> 267 * This step resolves column-to-table relationships and performs type checking. 268 */ 269 @Override 270 protected void performSemanticAnalysis(ParserContext context, TStatementList statements) { 271 if (TBaseType.isEnableResolver() && getSyntaxErrors().isEmpty()) { 272 TSQLResolver resolver = new TSQLResolver(globalContext, statements); 273 resolver.resolve(); 274 } 275 } 276 277 /** 278 * Perform interpretation on parsed statements. 279 * <p> 280 * This step evaluates constant expressions and performs other interpretation tasks. 281 */ 282 @Override 283 protected void performInterpreter(ParserContext context, TStatementList statements) { 284 if (TBaseType.ENABLE_INTERPRETER && getSyntaxErrors().isEmpty()) { 285 TLog.clearLogs(); 286 TGlobalScope interpreterScope = new TGlobalScope(sqlEnv); 287 TLog.enableInterpreterLogOnly(); 288 TASTEvaluator astEvaluator = new TASTEvaluator(statements, interpreterScope); 289 astEvaluator.eval(); 290 } 291 } 292 293 // ========== Redshift-Specific Tokenization ========== 294 295 /** 296 * Tokenize Redshift SQL text to token list. 297 * <p> 298 * Migrated from TGSqlParser.doredshiftsqltexttotokenlist(). 299 * <p> 300 * This method handles Redshift-specific token processing: 301 * <ul> 302 * <li>SQL*Plus-like commands detection</li> 303 * <li>Forward slash (/) disambiguation</li> 304 * <li>%ROWTYPE operator detection</li> 305 * <li>Continuation lines (hyphen at end of line)</li> 306 * </ul> 307 */ 308 private void doredshiftsqltexttotokenlist() { 309 boolean insqlpluscmd = false; 310 boolean isvalidplace = true; 311 boolean waitingreturnforfloatdiv = false; 312 boolean waitingreturnforsemicolon = false; 313 boolean continuesqlplusatnewline = false; 314 315 TSourceToken lct = null, prevst = null; 316 317 TSourceToken asourcetoken, lcprevst; 318 int yychar; 319 320 asourcetoken = getanewsourcetoken(); 321 if (asourcetoken == null) return; 322 yychar = asourcetoken.tokencode; 323 324 while (yychar > 0) { 325 sourcetokenlist.add(asourcetoken); 326 switch (yychar) { 327 case TBaseType.cmtdoublehyphen: 328 case TBaseType.cmtslashstar: 329 case TBaseType.lexspace: { 330 if (insqlpluscmd) { 331 asourcetoken.insqlpluscmd = true; 332 } 333 break; 334 } 335 case TBaseType.lexnewline: { 336 if (insqlpluscmd) { 337 insqlpluscmd = false; 338 isvalidplace = true; 339 340 if (continuesqlplusatnewline) { 341 insqlpluscmd = true; 342 isvalidplace = false; 343 asourcetoken.insqlpluscmd = true; 344 } 345 } 346 347 if (waitingreturnforsemicolon) { 348 isvalidplace = true; 349 } 350 if (waitingreturnforfloatdiv) { 351 isvalidplace = true; 352 lct.tokencode = TBaseType.sqlpluscmd; 353 if (lct.tokentype != ETokenType.ttslash) { 354 lct.tokentype = ETokenType.ttsqlpluscmd; 355 } 356 } 357 flexer.insqlpluscmd = insqlpluscmd; 358 break; 359 } //case newline 360 default: { 361 //solid token 362 continuesqlplusatnewline = false; 363 waitingreturnforsemicolon = false; 364 waitingreturnforfloatdiv = false; 365 if (insqlpluscmd) { 366 asourcetoken.insqlpluscmd = true; 367 if (asourcetoken.toString().equalsIgnoreCase("-")) { 368 continuesqlplusatnewline = true; 369 } 370 } else { 371 if (asourcetoken.tokentype == ETokenType.ttsemicolon) { 372 waitingreturnforsemicolon = true; 373 } 374 if ((asourcetoken.tokentype == ETokenType.ttslash) 375 && (isvalidplace || (IsValidPlaceForDivToSqlplusCmd(sourcetokenlist, asourcetoken.posinlist)))) { 376 lct = asourcetoken; 377 waitingreturnforfloatdiv = true; 378 } 379 if ((isvalidplace) && isvalidsqlpluscmdInPostgresql(asourcetoken.toString())) { 380 asourcetoken.tokencode = TBaseType.sqlpluscmd; 381 if (asourcetoken.tokentype != ETokenType.ttslash) { 382 asourcetoken.tokentype = ETokenType.ttsqlpluscmd; 383 } 384 insqlpluscmd = true; 385 flexer.insqlpluscmd = insqlpluscmd; 386 } 387 } 388 isvalidplace = false; 389 390 // Redshift-specific: Handle %ROWTYPE operator 391 if (asourcetoken.tokencode == TBaseType.rrw_redshift_rowtype) { 392 TSourceToken stPercent = asourcetoken.searchToken('%', -1); 393 if (stPercent != null) { 394 stPercent.tokencode = TBaseType.rowtype_operator; 395 } 396 } 397 } 398 } 399 400 //flexer.yylexwrap(asourcetoken); 401 asourcetoken = getanewsourcetoken(); 402 if (asourcetoken != null) { 403 yychar = asourcetoken.tokencode; 404 } else { 405 yychar = 0; 406 407 if (waitingreturnforfloatdiv) { 408 // / at the end of line treat as sqlplus command 409 lct.tokencode = TBaseType.sqlpluscmd; 410 if (lct.tokentype != ETokenType.ttslash) { 411 lct.tokentype = ETokenType.ttsqlpluscmd; 412 } 413 } 414 } 415 416 if ((yychar == 0) && (prevst != null)) { 417 // End of input 418 } 419 } // while 420 } 421 422 /** 423 * Check if this is a valid place for a forward slash to be treated as a SQL*Plus command. 424 * <p> 425 * Migrated from TGSqlParser.IsValidPlaceForDivToSqlplusCmd(). 426 */ 427 private boolean IsValidPlaceForDivToSqlplusCmd(TSourceTokenList tokenlist, int pos) { 428 if (tokenlist == null) return false; 429 if (pos <= 0) return true; 430 431 for (int i = pos - 1; i >= 0; i--) { 432 TSourceToken st = tokenlist.get(i); 433 if (st.tokencode == TBaseType.lexnewline) { 434 return true; 435 } 436 if ((st.tokencode != TBaseType.lexspace) 437 && (st.tokencode != TBaseType.cmtdoublehyphen) 438 && (st.tokencode != TBaseType.cmtslashstar)) { 439 return false; 440 } 441 } 442 return true; 443 } 444 445 /** 446 * Check if this token is a valid PostgreSQL-like command. 447 * <p> 448 * Migrated from TGSqlParser.isvalidsqlpluscmdInPostgresql(). 449 */ 450 private boolean isvalidsqlpluscmdInPostgresql(String str) { 451 if (str == null) return false; 452 if (str.length() == 0) return false; 453 454 String s = str.trim().toLowerCase(); 455 return s.startsWith("\\"); 456 } 457 458 // ========== Redshift-Specific Raw Statement Extraction ========== 459 460 /** 461 * Extract raw SQL statements from token list. 462 * <p> 463 * Migrated from TGSqlParser.doredshiftgetrawsqlstatements(). 464 * <p> 465 * This method handles Redshift-specific statement boundaries: 466 * <ul> 467 * <li>Semicolon (;) for regular SQL statements</li> 468 * <li>Function delimiter ($$) for function bodies</li> 469 * <li>BEGIN/END blocks for PL/pgSQL</li> 470 * <li>DECLARE blocks</li> 471 * </ul> 472 */ 473 private void doredshiftgetrawsqlstatements(SqlParseResult.Builder builder) { 474 int waitingEnd = 0; 475 boolean foundEnd = false, enterDeclare = false; 476 477 if (TBaseType.assigned(sqlstatements)) sqlstatements.clear(); 478 if (!TBaseType.assigned(sourcetokenlist)) { 479 builder.errorCode(-1); 480 return; 481 } 482 483 gcurrentsqlstatement = null; 484 EFindSqlStateType gst = EFindSqlStateType.stnormal; 485 TSourceToken lcprevsolidtoken = null, ast = null; 486 487 if (parserContext.isSinglePLBlock()) { 488 gcurrentsqlstatement = new TCommonBlock(EDbVendor.dbvpostgresql); 489 } 490 491 // Mantis 4497: a '/' on its own line is tentatively flagged as an Oracle 492 // SQL*Plus statement terminator (tokencode sqlpluscmd, but tokentype still 493 // ttslash) by doredshiftsqltexttotokenlist(). Redshift has no '/' terminator 494 // -- '/' is the division operator. When such a slash sits between two 495 // operands (its next solid token can start a right-hand operand), revert it 496 // to a normal division operator so the surrounding expression is parsed as a 497 // single statement instead of being split. A genuine terminator is followed 498 // by end-of-input or a statement keyword, never a bare operand, so it keeps 499 // the sqlpluscmd flag and still splits. This pre-pass runs before the main 500 // extraction loop because sqlplusaftercurtoken() peeks ahead at the slash 501 // while processing the preceding token, so the revert must already be done. 502 for (int i = 0; i < sourcetokenlist.size(); i++) { 503 TSourceToken st = sourcetokenlist.get(i); 504 if ((st.tokencode == TBaseType.sqlpluscmd) 505 && (st.tokentype == ETokenType.ttslash) 506 && isDivisionOperatorContext(st)) { 507 st.tokencode = (int) '/'; 508 } 509 } 510 511 for (int i = 0; i < sourcetokenlist.size(); i++) { 512 if ((ast != null) && (ast.issolidtoken())) 513 lcprevsolidtoken = ast; 514 515 ast = sourcetokenlist.get(i); 516 sourcetokenlist.curpos = i; 517 518 // Redshift-specific token adjustments 519 if (ast.tokencode == TBaseType.rrw_redshift_filter) { 520 TSourceToken st1 = ast.nextSolidToken(); 521 if (st1 != null) { 522 if (st1.tokencode != '(') { 523 ast.tokencode = TBaseType.ident; 524 } 525 } 526 } else if (ast.tokencode == TBaseType.rrw_redshift_array) { 527 TSourceToken st1 = ast.searchToken('<', 1); 528 if (st1 != null) { // array<varchar(20)> 529 ast.tokencode = TBaseType.rrw_redshift_array_type; 530 } 531 } else if (ast.tokencode == TBaseType.rrw_redshift_prior) { 532 // PRIOR is a valid identifier/alias in Redshift. When followed by '.', 533 // it can only start a qualified column reference (prior.column_a) — 534 // never the CONNECT BY unary operator (PRIOR expr) or the FETCH 535 // direction (PRIOR FROM), and a float literal like .5 lexes as a 536 // single numeric token, not a bare '.'. 537 TSourceToken st1 = ast.nextSolidToken(); 538 if ((st1 != null) && (st1.tokencode == '.')) { 539 ast.tokencode = TBaseType.ident; 540 } 541 } else if (ast.tokencode == TBaseType.rrw_binary) { 542 // Distinguish BINARY as data type from BINARY as identifier 543 // BINARY is a type when: 544 // - Preceded by: AS (CAST), comma, left paren, column name 545 // - Followed by: VARYING, left paren, comma, right paren, NOT, NULL 546 TSourceToken prevToken = ast.prevSolidToken(); 547 TSourceToken nextToken = ast.nextSolidToken(); 548 549 // Check if preceded by a period -> identifier (e.g., table.binary) 550 if (prevToken != null && prevToken.tokencode == '.') { 551 // Keep as identifier, no change 552 } 553 // Check type contexts by previous token 554 else if (prevToken != null && 555 (prevToken.tokencode == TBaseType.rrw_as || // CAST(x AS BINARY) 556 prevToken.tokencode == ',' || // func(INT, BINARY) 557 prevToken.tokencode == '(' || // (col BINARY), CAST(BINARY ... 558 prevToken.tokentype == ETokenType.ttidentifier)) { // column_name BINARY 559 ast.tokencode = TBaseType.rrw_redshift_binary_as_type; 560 } 561 // Check type contexts by next token 562 else if (nextToken != null && 563 (nextToken.tokencode == TBaseType.rrw_varying || // BINARY VARYING 564 nextToken.tokencode == '(' || // BINARY(10) 565 nextToken.tokencode == ',' || // col BINARY, col2 566 nextToken.tokencode == ')' || // col BINARY) 567 nextToken.tokencode == TBaseType.rrw_not || // BINARY NOT NULL 568 nextToken.tokencode == TBaseType.rrw_null)) { // BINARY NULL 569 ast.tokencode = TBaseType.rrw_redshift_binary_as_type; 570 } 571 } else if (ast.tokencode == TBaseType.rrw_values) { 572 TSourceToken stParen = ast.searchToken('(', 1); 573 if (stParen != null) { 574 TSourceToken stInsert = ast.searchToken(TBaseType.rrw_insert, -ast.posinlist); 575 if (stInsert != null) { 576 TSourceToken stSemiColon = ast.searchToken(';', -ast.posinlist); 577 if ((stSemiColon != null) && (stSemiColon.posinlist > stInsert.posinlist)) { 578 // INSERT INTO test values (16,1), (8,2), (4,4), (2,0), (97, 16); 579 // VALUES (1); 580 // don't treat values(1) as insert values 581 } else { 582 TSourceToken stFrom = ast.searchToken(TBaseType.rrw_from, -ast.posinlist); 583 if ((stFrom != null) && (stFrom.posinlist > stInsert.posinlist)) { 584 // don't treat values after from keyword as an insert values 585 // insert into inserttest values(10, 20, '40'), (-1, 2, DEFAULT), 586 // ((select 2), (select i from (values(3) ) as foo (i)), 'values are fun!'); 587 } else { 588 ast.tokencode = TBaseType.rrw_postgresql_insert_values; 589 } 590 } 591 } 592 } 593 } 594 595 switch (gst) { 596 case sterror: { 597 if (ast.tokentype == ETokenType.ttsemicolon) { 598 gcurrentsqlstatement.sourcetokenlist.add(ast); 599 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 600 gst = EFindSqlStateType.stnormal; 601 } else { 602 gcurrentsqlstatement.sourcetokenlist.add(ast); 603 } 604 break; 605 } //sterror 606 607 case stnormal: { 608 if ((ast.tokencode == TBaseType.cmtdoublehyphen) 609 || (ast.tokencode == TBaseType.cmtslashstar) 610 || (ast.tokencode == TBaseType.lexspace) 611 || (ast.tokencode == TBaseType.lexnewline) 612 || (ast.tokentype == ETokenType.ttsemicolon)) { 613 if (gcurrentsqlstatement != null) { 614 gcurrentsqlstatement.sourcetokenlist.add(ast); 615 } 616 617 if ((lcprevsolidtoken != null) && (ast.tokentype == ETokenType.ttsemicolon)) { 618 if (lcprevsolidtoken.tokentype == ETokenType.ttsemicolon) { 619 // ;;;; continuous semicolon, treat it as comment 620 ast.tokentype = ETokenType.ttsimplecomment; 621 ast.tokencode = TBaseType.cmtdoublehyphen; 622 } 623 } 624 625 continue; 626 } 627 628 if (ast.tokencode == TBaseType.sqlpluscmd) { 629 gst = EFindSqlStateType.stsqlplus; 630 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 631 gcurrentsqlstatement.sourcetokenlist.add(ast); 632 continue; 633 } 634 635 // find a token to start sql or plsql mode 636 gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement); 637 638 if (gcurrentsqlstatement != null) { 639 enterDeclare = false; 640 if (gcurrentsqlstatement.ispgplsql()) { 641 gst = EFindSqlStateType.ststoredprocedure; 642 gcurrentsqlstatement.sourcetokenlist.add(ast); 643 foundEnd = false; 644 if ((ast.tokencode == TBaseType.rrw_begin) 645 || (ast.tokencode == TBaseType.rrw_package) 646 || (ast.searchToken(TBaseType.rrw_package, 4) != null)) { 647 waitingEnd = 1; 648 } else if (ast.tokencode == TBaseType.rrw_declare) { 649 enterDeclare = true; 650 } 651 } else { 652 gst = EFindSqlStateType.stsql; 653 gcurrentsqlstatement.sourcetokenlist.add(ast); 654 } 655 } else { 656 //error token found 657 this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo), 658 "Error when tokenize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist)); 659 660 ast.tokentype = ETokenType.tttokenlizererrortoken; 661 gst = EFindSqlStateType.sterror; 662 663 gcurrentsqlstatement = new TUnknownSqlStatement(vendor); 664 gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid; 665 gcurrentsqlstatement.sourcetokenlist.add(ast); 666 } 667 668 break; 669 } // stnormal 670 671 case stsqlplus: { 672 if (ast.insqlpluscmd) { 673 gcurrentsqlstatement.sourcetokenlist.add(ast); 674 } else { 675 gst = EFindSqlStateType.stnormal; //this token must be newline, 676 gcurrentsqlstatement.sourcetokenlist.add(ast); // so add it here 677 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 678 } 679 680 break; 681 }//case stsqlplus 682 683 case stsql: { 684 if (ast.tokentype == ETokenType.ttsemicolon) { 685 gst = EFindSqlStateType.stnormal; 686 gcurrentsqlstatement.sourcetokenlist.add(ast); 687 gcurrentsqlstatement.semicolonended = ast; 688 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 689 continue; 690 } 691 692 if (sourcetokenlist.sqlplusaftercurtoken()) { //most probably is / cmd 693 gst = EFindSqlStateType.stnormal; 694 gcurrentsqlstatement.sourcetokenlist.add(ast); 695 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 696 continue; 697 } 698 699 if (ast.tokencode == TBaseType.cmtdoublehyphen) { 700 if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter 701 gst = EFindSqlStateType.stnormal; 702 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 703 continue; 704 } 705 } 706 707 // Check if a DDL keyword starts a new SQL statement without semicolon separator. 708 // Guard against false positives: 709 // - GRANT/REVOKE use CREATE/ALTER/DROP as privilege names 710 // - EXPLAIN statement can contain CREATE/ALTER/DROP (e.g. EXPLAIN CREATE TABLE) 711 // - CREATE OR ALTER pattern: ALTER follows OR inside a CREATE statement 712 if (ast.tokencode == TBaseType.rrw_create 713 || ast.tokencode == TBaseType.rrw_alter 714 || ast.tokencode == TBaseType.rrw_drop) { 715 boolean shouldCheckSplit = true; 716 717 // Don't split inside GRANT/REVOKE (CREATE/ALTER/DROP are privilege names) 718 if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstoraclegrant 719 || gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstoraclerevoke 720 || gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstGrant 721 || gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstRevoke) { 722 shouldCheckSplit = false; 723 } 724 725 // Don't split inside EXPLAIN (EXPLAIN CREATE TABLE, EXPLAIN ALTER, etc.) 726 if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstExplain) { 727 shouldCheckSplit = false; 728 } 729 730 // Don't split ALTER/DROP when preceded by OR (CREATE OR ALTER pattern) 731 if (shouldCheckSplit && (ast.tokencode == TBaseType.rrw_alter || ast.tokencode == TBaseType.rrw_drop)) { 732 TSourceToken prevSolid = ast.prevSolidToken(); 733 if (prevSolid != null && prevSolid.tokencode == TBaseType.rrw_or) { 734 shouldCheckSplit = false; 735 } 736 } 737 738 if (shouldCheckSplit) { 739 TCustomSqlStatement lcnextsqlstmt = sqlcmds.issql(ast, gst, gcurrentsqlstatement); 740 if (lcnextsqlstmt != null) { 741 // Finalize current statement and start the new one 742 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 743 gcurrentsqlstatement = lcnextsqlstmt; 744 gcurrentsqlstatement.sourcetokenlist.add(ast); 745 if (gcurrentsqlstatement.ispgplsql()) { 746 gst = EFindSqlStateType.ststoredprocedure; 747 } 748 // else stay in stsql 749 continue; 750 } 751 } 752 } 753 754 gcurrentsqlstatement.sourcetokenlist.add(ast); 755 break; 756 }//case stsql 757 758 case ststoredprocedure: { 759 if (ast.tokencode == TBaseType.rrw_redshift_function_delimiter) { 760 gcurrentsqlstatement.sourcetokenlist.add(ast); 761 gst = EFindSqlStateType.ststoredprocedurePgStartBody; 762 continue; 763 } 764 765 if (ast.tokencode == TBaseType.rrw_redshift_language) { 766 // check next token which is the language used by this stored procedure 767 TSourceToken nextSt = ast.nextSolidToken(); 768 if (nextSt != null) { 769 if (gcurrentsqlstatement instanceof TRoutine) { // can be TCreateProcedureStmt or TCreateFunctionStmt 770 TRoutine p = (TRoutine) gcurrentsqlstatement; 771 p.setRoutineLanguage(nextSt.toString()); 772 } 773 } 774 } 775 776 if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnd == 0) && (!enterDeclare)) { 777 gst = EFindSqlStateType.stnormal; 778 gcurrentsqlstatement.sourcetokenlist.add(ast); 779 gcurrentsqlstatement.semicolonended = ast; 780 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 781 continue; 782 } 783 784 if ((ast.tokencode == TBaseType.rrw_begin)) { 785 waitingEnd++; 786 enterDeclare = false; 787 } else if ((ast.tokencode == TBaseType.rrw_declare)) { 788 enterDeclare = true; 789 } else if ((ast.tokencode == TBaseType.rrw_if)) { 790 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 791 //this is not if after END 792 waitingEnd++; 793 } 794 } else if ((ast.tokencode == TBaseType.rrw_case)) { 795 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 796 //this is not case after END 797 waitingEnd++; 798 } 799 } else if ((ast.tokencode == TBaseType.rrw_loop)) { 800 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 801 //this is not loop after END 802 waitingEnd++; 803 } 804 } else if (ast.tokencode == TBaseType.rrw_end) { 805 foundEnd = true; 806 waitingEnd--; 807 if (waitingEnd < 0) { 808 waitingEnd = 0; 809 } 810 } 811 812 if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) { 813 // TPlsqlStatementParse(asqlstatement).TerminatorToken := ast; 814 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 815 gst = EFindSqlStateType.stnormal; 816 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 817 818 //make / a sqlplus cmd 819 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 820 gcurrentsqlstatement.sourcetokenlist.add(ast); 821 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 822 } else if ((ast.tokentype == ETokenType.ttperiod) && (sourcetokenlist.returnaftercurtoken(false)) && (sourcetokenlist.returnbeforecurtoken(false))) { 823 // single dot at a separate line 824 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 825 gst = EFindSqlStateType.stnormal; 826 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 827 828 //make ttperiod a sqlplus cmd 829 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 830 gcurrentsqlstatement.sourcetokenlist.add(ast); 831 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 832 } else { 833 gcurrentsqlstatement.sourcetokenlist.add(ast); 834 if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnd == 0) 835 && (foundEnd)) { 836 gst = EFindSqlStateType.stnormal; 837 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 838 } 839 } 840 841 if (ast.tokencode == TBaseType.sqlpluscmd) { 842 //change tokencode back to keyword or TBaseType.ident, because sqlplus cmd 843 //in a sql statement(almost is plsql block) is not really a sqlplus cmd 844 int m = flexer.getkeywordvalue(ast.getAstext()); 845 if (m != 0) { 846 ast.tokencode = m; 847 } else { 848 ast.tokencode = TBaseType.ident; 849 } 850 } 851 852 if ((gst == EFindSqlStateType.ststoredprocedure) && (ast.tokencode == TBaseType.cmtdoublehyphen)) { 853 if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter 854 gst = EFindSqlStateType.stnormal; 855 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 856 } 857 } 858 859 break; 860 } //ststoredprocedure 861 862 case ststoredprocedurePgStartBody: { 863 // Check if this is the closing delimiter 864 if (ast.tokencode == TBaseType.rrw_redshift_function_delimiter) { 865 gcurrentsqlstatement.sourcetokenlist.add(ast); 866 gst = EFindSqlStateType.ststoredprocedurePgEndBody; 867 continue; 868 } 869 870 // Only add function body tokens if language is SQL or PLPGSQL 871 // For other languages (e.g., plpythonu, plperl), skip tokens so the 872 // parser sees two consecutive delimiters and matches the empty body rule 873 boolean shouldAddToken = true; // Default: add tokens (assume SQL/PLPGSQL) 874 875 // Look ahead to find the LANGUAGE keyword after the closing $$ 876 // to determine if we should skip these tokens 877 TSourceToken languageToken = null; 878 for (int j = i + 1; j < sourcetokenlist.size(); j++) { 879 TSourceToken lookahead = sourcetokenlist.get(j); 880 if (lookahead.tokencode == TBaseType.rrw_redshift_function_delimiter) { 881 // Found closing delimiter, now look for LANGUAGE keyword 882 for (int k = j + 1; k < sourcetokenlist.size(); k++) { 883 TSourceToken st = sourcetokenlist.get(k); 884 if (st.tokencode == TBaseType.rrw_redshift_language) { 885 // Found LANGUAGE, check next solid token for the language name 886 languageToken = st.nextSolidToken(); 887 break; 888 } 889 if (st.tokentype == ETokenType.ttsemicolon) { 890 break; // Reached end of statement 891 } 892 } 893 break; 894 } 895 } 896 897 if (languageToken != null) { 898 String language = languageToken.toString().toLowerCase().trim(); 899 // Remove quotes if present 900 if (language.startsWith("'") && language.endsWith("'")) { 901 language = language.substring(1, language.length() - 1); 902 } 903 // Skip tokens for non-SQL/non-PLPGSQL languages 904 if (!language.equals("sql") && !language.equals("plpgsql")) { 905 shouldAddToken = false; 906 } 907 } 908 909 if (shouldAddToken) { 910 gcurrentsqlstatement.sourcetokenlist.add(ast); 911 } 912 913 break; 914 } 915 916 case ststoredprocedurePgEndBody: { 917 if (ast.tokentype == ETokenType.ttsemicolon) { 918 gst = EFindSqlStateType.stnormal; 919 gcurrentsqlstatement.sourcetokenlist.add(ast); 920 gcurrentsqlstatement.semicolonended = ast; 921 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 922 continue; 923 } else if (ast.tokencode == TBaseType.cmtdoublehyphen) { 924 if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter 925 gst = EFindSqlStateType.stnormal; 926 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 927 continue; 928 } 929 } 930 931 gcurrentsqlstatement.sourcetokenlist.add(ast); 932 933 if (ast.tokencode == TBaseType.rrw_redshift_language) { 934 // check next token which is the language used by this stored procedure 935 TSourceToken nextSt = ast.nextSolidToken(); 936 if (nextSt != null) { 937 if (gcurrentsqlstatement instanceof TRoutine) { // can be TCreateProcedureStmt or TCreateFunctionStmt 938 TRoutine p = (TRoutine) gcurrentsqlstatement; 939 p.setRoutineLanguage(nextSt.toString()); 940 } 941 } 942 } 943 944 break; 945 } 946 } //switch 947 }//for 948 949 //last statement 950 if ((gcurrentsqlstatement != null) && 951 ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) 952 || (gst == EFindSqlStateType.ststoredprocedure) 953 || (gst == EFindSqlStateType.ststoredprocedurePgEndBody) 954 || (gst == EFindSqlStateType.sterror) || (parserContext.isSinglePLBlock()))) { 955 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder); 956 } 957 958 builder.errorCode(syntaxErrors.size()); 959 } 960 961 /** 962 * Handle CREATE TABLE error recovery. 963 * <p> 964 * Migrated from TGSqlParser.handleCreateTableErrorRecovery(). 965 */ 966 private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) { 967 if (((stmt.sqlstatementtype == ESqlStatementType.sstcreatetable) 968 || (stmt.sqlstatementtype == ESqlStatementType.sstcreateindex)) 969 && (!TBaseType.c_createTableStrictParsing)) { 970 971 int nested = 0; 972 boolean isIgnore = false, isFoundIgnoreToken = false; 973 TSourceToken firstIgnoreToken = null; 974 975 for (int k = 0; k < stmt.sourcetokenlist.size(); k++) { 976 TSourceToken st = stmt.sourcetokenlist.get(k); 977 if (isIgnore) { 978 if (st.issolidtoken() && (st.tokencode != ';')) { 979 isFoundIgnoreToken = true; 980 if (firstIgnoreToken == null) { 981 firstIgnoreToken = st; 982 } 983 } 984 if (st.tokencode != ';') { 985 st.tokencode = TBaseType.sqlpluscmd; 986 } 987 continue; 988 } 989 if (st.tokencode == (int) ')') { 990 nested--; 991 if (nested == 0) { 992 boolean isSelect = false; 993 TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1); 994 if (st1 != null) { 995 TSourceToken st2 = st.searchToken((int) '(', 2); 996 if (st2 != null) { 997 TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3); 998 isSelect = (st3 != null); 999 } 1000 } 1001 if (!isSelect) isIgnore = true; 1002 } 1003 } else if (st.tokencode == (int) '(') { 1004 nested++; 1005 } 1006 } 1007 1008 if (isFoundIgnoreToken) { 1009 stmt.clearError(); 1010 stmt.parsestatement(null, false); 1011 } 1012 } 1013 } 1014 1015 @Override 1016 public String toString() { 1017 return "RedshiftSqlParser{vendor=" + vendor + "}"; 1018 } 1019}