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.TLexerPostgresql; 009import gudusoft.gsqlparser.TParserPostgresql; 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.TCommonBlock; 024import gudusoft.gsqlparser.stmt.TRoutine; 025import gudusoft.gsqlparser.compiler.TContext; 026import gudusoft.gsqlparser.sqlenv.TSQLEnv; 027import gudusoft.gsqlparser.compiler.TGlobalScope; 028import gudusoft.gsqlparser.compiler.TFrame; 029import gudusoft.gsqlparser.ETokenStatus; 030 031import java.io.BufferedReader; 032import java.util.ArrayList; 033import java.util.List; 034import java.util.Stack; 035 036/** 037 * PostgreSQL database SQL parser implementation. 038 * 039 * <p>This parser handles PostgreSQL-specific SQL syntax including: 040 * <ul> 041 * <li>PL/pgSQL blocks (functions, procedures, triggers)</li> 042 * <li>Dollar quoting ($$...$$)</li> 043 * <li>PostgreSQL-specific DML/DDL</li> 044 * <li>Special operators and functions</li> 045 * <li>Special token handling (%ROWTYPE, %TYPE, etc.)</li> 046 * </ul> 047 * 048 * <p><b>Design Notes:</b> 049 * <ul> 050 * <li>Extends {@link AbstractSqlParser}</li> 051 * <li>Can directly instantiate: {@link TLexerPostgresql}, {@link TParserPostgresqlSql}</li> 052 * <li>Uses single parser (no secondary parser like Oracle's PL/SQL)</li> 053 * <li>Delimiter character: ';' for SQL statements</li> 054 * </ul> 055 * 056 * <p><b>Usage Example:</b> 057 * <pre> 058 * // Get PostgreSQL parser from factory 059 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvpostgresql); 060 * 061 * // Build context 062 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvpostgresql) 063 * .sqlText("SELECT * FROM employees WHERE dept_id = 10") 064 * .build(); 065 * 066 * // Parse 067 * SqlParseResult result = parser.parse(context); 068 * 069 * // Access statements 070 * TStatementList statements = result.getSqlStatements(); 071 * </pre> 072 * 073 * @see SqlParser 074 * @see AbstractSqlParser 075 * @see TLexerPostgresql 076 * @see TParserPostgresql 077 * @since 3.2.0.0 078 */ 079public class PostgreSqlParser extends AbstractSqlParser { 080 081 // ========== Lexer and Parser Instances ========== 082 // Created once in constructor, reused for all parsing operations 083 084 /** The PostgreSQL lexer used for tokenization (public for TGSqlParser.getFlexer()) */ 085 public TLexerPostgresql flexer; 086 private TParserPostgresql fparser; 087 088 // ========== State Variables ========== 089 // NOTE: The following fields moved to AbstractSqlParser (inherited): 090 // - sourcetokenlist (TSourceTokenList) 091 // - sqlstatements (TStatementList) 092 // - parserContext (ParserContext) 093 // - sqlcmds (ISqlCmds) - to be added when PostgreSQL raw extraction is refactored 094 // - globalContext (TContext) 095 // - sqlEnv (TSQLEnv) 096 // - frameStack (Stack<TFrame>) 097 // - globalFrame (TFrame) 098 099 // ========== State Variables for Tokenization ========== 100 private boolean insqlpluscmd; 101 private boolean isvalidplace; 102 private boolean waitingreturnforsemicolon; 103 private boolean waitingreturnforfloatdiv; 104 private boolean continuesqlplusatnewline; 105 106 // ========== Constructor ========== 107 108 /** 109 * Construct PostgreSQL SQL parser. 110 * <p> 111 * Configures the parser for PostgreSQL database with default delimiter: semicolon (;) 112 * <p> 113 * Following the original TGSqlParser pattern, the lexer and parser are 114 * created once in the constructor and reused for all parsing operations. 115 */ 116 public PostgreSqlParser() { 117 super(EDbVendor.dbvpostgresql); 118 119 // Set delimiter character 120 this.delimiterChar = ';'; 121 this.defaultDelimiterStr = ";"; 122 123 // Create lexer once - will be reused for all parsing operations 124 this.flexer = new TLexerPostgresql(); 125 this.flexer.delimiterchar = this.delimiterChar; 126 this.flexer.defaultDelimiterStr = this.defaultDelimiterStr; 127 128 // CRITICAL: Set lexer for inherited getanewsourcetoken() method 129 this.lexer = this.flexer; 130 131 // Create parser once - will be reused for all parsing operations 132 this.fparser = new TParserPostgresql(null); 133 this.fparser.lexer = this.flexer; 134 135 // NOTE: sourcetokenlist and sqlstatements are initialized in AbstractSqlParser constructor 136 } 137 138 // ========== AbstractSqlParser Abstract Methods Implementation ========== 139 140 /** 141 * Return the PostgreSQL lexer instance. 142 * <p> 143 * The lexer is created once in the constructor and reused for all 144 * parsing operations. This method simply returns the existing instance, 145 * matching the original TGSqlParser pattern where the lexer is created 146 * once and reset before each use. 147 * 148 * @param context parser context (not used, lexer already created) 149 * @return the PostgreSQL lexer instance created in constructor 150 */ 151 @Override 152 protected TCustomLexer getLexer(ParserContext context) { 153 // Return existing lexer instance (created in constructor) 154 return this.flexer; 155 } 156 157 /** 158 * Return the PostgreSQL SQL parser instance with updated token list. 159 * <p> 160 * The parser is created once in the constructor and reused for all 161 * parsing operations. This method updates the token list and returns 162 * the existing instance, matching the original TGSqlParser pattern. 163 * 164 * @param context parser context (not used, parser already created) 165 * @param tokens source token list to parse 166 * @return the PostgreSQL SQL parser instance created in constructor 167 */ 168 @Override 169 protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) { 170 // Update token list for reused parser instance 171 this.fparser.sourcetokenlist = tokens; 172 return this.fparser; 173 } 174 175 /** 176 * Call PostgreSQL-specific tokenization logic. 177 * <p> 178 * Delegates to dopostgresqltexttotokenlist which handles PostgreSQL's 179 * specific keyword recognition, dollar quotes, and token generation. 180 */ 181 @Override 182 protected void tokenizeVendorSql() { 183 dopostgresqltexttotokenlist(); 184 } 185 186 /** 187 * Setup PostgreSQL parser for raw statement extraction. 188 * <p> 189 * PostgreSQL uses a single parser, so we inject sqlcmds and update 190 * the token list for the main parser only. 191 */ 192 @Override 193 protected void setupVendorParsersForExtraction() { 194 this.fparser.sqlcmds = this.sqlcmds; 195 this.fparser.sourcetokenlist = this.sourcetokenlist; 196 } 197 198 /** 199 * Call PostgreSQL-specific raw statement extraction logic. 200 * <p> 201 * Delegates to dopostgresqlgetrawsqlstatements which handles PostgreSQL's 202 * statement delimiters (semicolon for SQL, $$ for PL/pgSQL functions). 203 */ 204 @Override 205 protected void extractVendorRawStatements(SqlParseResult.Builder builder) { 206 dopostgresqlgetrawsqlstatements(builder); 207 } 208 209 /** 210 * Perform full parsing of statements with syntax checking. 211 * <p> 212 * This method orchestrates the parsing of all statements. 213 * 214 * <p><b>Important:</b> This method does NOT extract raw statements - they are 215 * passed in as a parameter already extracted by {@link #extractRawStatements}. 216 * 217 * @param context parser context 218 * @param parser main SQL parser (TParserPostgresql) 219 * @param secondaryParser not used for PostgreSQL 220 * @param tokens source token list 221 * @param rawStatements raw statements already extracted (never null) 222 * @return list of fully parsed statements with AST built 223 */ 224 @Override 225 protected TStatementList performParsing(ParserContext context, 226 TCustomParser parser, 227 TCustomParser secondaryParser, 228 TSourceTokenList tokens, 229 TStatementList rawStatements) { 230 // Store references (fparser is already set, don't reassign final variable) 231 this.sourcetokenlist = tokens; 232 this.parserContext = context; 233 234 // Use the raw statements passed from AbstractSqlParser.parse() 235 // (already extracted - DO NOT re-extract to avoid duplication) 236 this.sqlstatements = rawStatements; 237 238 // Initialize global context for statement parsing 239 initializeGlobalContext(); 240 241 // Parse each statement 242 for (int i = 0; i < sqlstatements.size(); i++) { 243 TCustomSqlStatement stmt = sqlstatements.getRawSql(i); 244 245 // Set frame stack for the statement (needed for parsing) 246 stmt.setFrameStack(frameStack); 247 248 // Parse the statement 249 int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree()); 250 251 // Collect syntax errors 252 if ((parseResult != 0) || (stmt.getErrorCount() > 0)) { 253 copyErrorsFromStatement(stmt); 254 } 255 } 256 257 // Clean up frame stack 258 if (globalFrame != null) { 259 globalFrame.popMeFromStack(frameStack); 260 } 261 262 return this.sqlstatements; 263 } 264 265 // ========== PostgreSQL-Specific Tokenization ========== 266 267 /** 268 * Perform PostgreSQL-specific tokenization. 269 * <p> 270 * Extracted from TGSqlParser.dopostgresqltexttotokenlist() (lines 3093-3287) 271 */ 272 private void dopostgresqltexttotokenlist() { 273 // Initialize state machine 274 insqlpluscmd = false; 275 isvalidplace = true; 276 waitingreturnforfloatdiv = false; 277 waitingreturnforsemicolon = false; 278 continuesqlplusatnewline = false; 279 280 TSourceToken lct = null, prevst = null; 281 TSourceToken asourcetoken, lcprevst; 282 int yychar; 283 284 asourcetoken = getanewsourcetoken(); 285 if (asourcetoken == null) return; 286 yychar = asourcetoken.tokencode; 287 288 while (yychar > 0) { 289 sourcetokenlist.add(asourcetoken); 290 291 switch (yychar) { 292 case TBaseType.cmtdoublehyphen: 293 case TBaseType.cmtslashstar: 294 case TBaseType.lexspace: { 295 if (insqlpluscmd) { 296 asourcetoken.insqlpluscmd = true; 297 } 298 break; 299 } 300 301 case TBaseType.lexnewline: { 302 if (insqlpluscmd) { 303 insqlpluscmd = false; 304 isvalidplace = true; 305 306 if (continuesqlplusatnewline) { 307 insqlpluscmd = true; 308 isvalidplace = false; 309 asourcetoken.insqlpluscmd = true; 310 } 311 } 312 313 if (waitingreturnforsemicolon) { 314 isvalidplace = true; 315 } 316 if (waitingreturnforfloatdiv) { 317 isvalidplace = true; 318 lct.tokencode = TBaseType.sqlpluscmd; 319 if (lct.tokentype != ETokenType.ttslash) { 320 lct.tokentype = ETokenType.ttsqlpluscmd; 321 } 322 } 323 flexer.insqlpluscmd = insqlpluscmd; 324 break; 325 } 326 327 default: { 328 // Solid token 329 continuesqlplusatnewline = false; 330 waitingreturnforsemicolon = false; 331 waitingreturnforfloatdiv = false; 332 333 if (insqlpluscmd) { 334 asourcetoken.insqlpluscmd = true; 335 if (asourcetoken.toString().equalsIgnoreCase("-")) { 336 continuesqlplusatnewline = true; 337 } 338 } else { 339 if (asourcetoken.tokentype == ETokenType.ttsemicolon) { 340 waitingreturnforsemicolon = true; 341 } 342 if ((asourcetoken.tokentype == ETokenType.ttslash) 343 && (isvalidplace || (isValidPlaceForDivToSqlplusCmd(sourcetokenlist, asourcetoken.posinlist)))) { 344 lct = asourcetoken; 345 waitingreturnforfloatdiv = true; 346 } 347 if ((isvalidplace) && isvalidsqlpluscmdInPostgresql(asourcetoken.toString())) { 348 asourcetoken.tokencode = TBaseType.sqlpluscmd; 349 if (asourcetoken.tokentype != ETokenType.ttslash) { 350 asourcetoken.tokentype = ETokenType.ttsqlpluscmd; 351 } 352 insqlpluscmd = true; 353 flexer.insqlpluscmd = insqlpluscmd; 354 } 355 } 356 isvalidplace = false; 357 358 // PostgreSQL-specific keyword handling 359 // (A NOT+DEFERRABLE merge ported from the .NET code base was 360 // removed here: the Java grammar has no NOT_DEFERRABLE token, 361 // so getkeywordvalue("NOT_DEFERRABLE") returned 0 and corrupted 362 // the NOT token; the grammar parses RW_NOT RW_DEFERRABLE 363 // directly. See MantisBT 4514.) 364 if (prevst != null) { 365 if (prevst.tokencode == TBaseType.rrw_inner) { 366 if (asourcetoken.tokencode != flexer.getkeywordvalue("JOIN")) { 367 prevst.tokencode = TBaseType.ident; 368 } 369 } 370 } 371 372 if (asourcetoken.tokencode == TBaseType.rrw_inner) { 373 prevst = asourcetoken; 374 } else { 375 prevst = null; 376 } 377 378 // Additional PostgreSQL transformations 379 if ((asourcetoken.tokencode == flexer.getkeywordvalue("DIRECT_LOAD")) 380 || (asourcetoken.tokencode == flexer.getkeywordvalue("ALL"))) { 381 lcprevst = getprevsolidtoken(asourcetoken); 382 if (lcprevst != null) { 383 if (lcprevst.tokencode == TBaseType.rrw_for) 384 lcprevst.tokencode = TBaseType.rw_for1; 385 } 386 } 387 388 if (asourcetoken.tokencode == TBaseType.rrw_dense_rank) { 389 TSourceToken stKeep = asourcetoken.searchToken(TBaseType.rrw_keep, -2); 390 if (stKeep != null) { 391 stKeep.tokencode = TBaseType.rrw_keep_before_dense_rank; 392 } 393 } 394 395 if ((asourcetoken.tokencode == TBaseType.rrw_postgresql_rowtype) 396 || (asourcetoken.tokencode == TBaseType.rrw_postgresql_type)) { 397 TSourceToken stPercent = asourcetoken.searchToken('%', -1); 398 if (stPercent != null) { 399 stPercent.tokencode = TBaseType.rowtype_operator; 400 } 401 } 402 403 if (asourcetoken.tokencode == TBaseType.JSON_EXIST) { 404 TSourceToken stPercent = asourcetoken.searchToken('=', -1); 405 if (stPercent != null) { 406 asourcetoken.tokencode = TBaseType.ident; 407 } 408 } 409 410 if (asourcetoken.tokencode == TBaseType.rrw_update) { 411 TSourceToken stDo = asourcetoken.searchToken(TBaseType.rrw_do, -1); 412 if (stDo != null) { 413 asourcetoken.tokencode = TBaseType.rrw_postgresql_do_update; 414 } 415 } 416 417 break; 418 } 419 } 420 421 // Get next token 422 asourcetoken = getanewsourcetoken(); 423 if (asourcetoken != null) { 424 yychar = asourcetoken.tokencode; 425 } else { 426 yychar = 0; 427 428 if (waitingreturnforfloatdiv) { 429 lct.tokencode = TBaseType.sqlpluscmd; 430 if (lct.tokentype != ETokenType.ttslash) { 431 lct.tokentype = ETokenType.ttsqlpluscmd; 432 } 433 } 434 } 435 436 if ((yychar == 0) && (prevst != null)) { 437 if (prevst.tokencode == TBaseType.rrw_inner) { 438 prevst.tokencode = TBaseType.ident; 439 } 440 } 441 } 442 } 443 444 /** 445 * Get next source token from the lexer. 446 * <p> 447 * This method wraps the lexer's yylexwrap() call. 448 * 449 * @return next source token, or null if end of input 450 */ 451 452 /** 453 * Check if token represents a valid SQL*Plus-like command in PostgreSQL. 454 * 455 * @param tokenText token text to check 456 * @return true if valid SQL*Plus command 457 */ 458 private boolean isvalidsqlpluscmdInPostgresql(String tokenText) { 459 // PostgreSQL supports psql meta-commands like \d, \dt, etc. 460 // For now, keep compatible with original implementation 461 return false; 462 } 463 464 465 /** 466 * Determine if forward slash should be treated as SQL*Plus command delimiter. 467 * 468 * @param pstlist token list 469 * @param pPos position of '/' token 470 * @return true if '/' should be SQL*Plus command 471 */ 472 private boolean isValidPlaceForDivToSqlplusCmd(TSourceTokenList pstlist, int pPos) { 473 boolean ret = false; 474 475 if ((pPos <= 0) || (pPos > pstlist.size() - 1)) return ret; 476 477 TSourceToken lcst = pstlist.get(pPos - 1); 478 if (lcst.tokentype != ETokenType.ttreturn) { 479 return ret; 480 } 481 482 if (!(lcst.getAstext().charAt(lcst.getAstext().length() - 1) == ' ')) { 483 ret = true; 484 } 485 486 return ret; 487 } 488 489 /** 490 * Get previous non-whitespace token. 491 * 492 * @param ptoken current token 493 * @return previous solid token, or null 494 */ 495 private TSourceToken getprevsolidtoken(TSourceToken ptoken) { 496 TSourceToken ret = null; 497 TSourceTokenList lctokenlist = ptoken.container; 498 499 if (lctokenlist != null) { 500 if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) { 501 if (!( 502 (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttwhitespace) 503 || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttreturn) 504 || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttsimplecomment) 505 || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttbracketedcomment) 506 )) { 507 ret = lctokenlist.get(ptoken.posinlist - 1); 508 } else { 509 ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false); 510 } 511 } 512 } 513 return ret; 514 } 515 516 // ========== PostgreSQL-Specific Raw Statement Extraction ========== 517 518 /** 519 * Extract raw PostgreSQL SQL statements from tokenized source. 520 * <p> 521 * Extracted from TGSqlParser.dopostgresqlgetrawsqlstatements() (lines 8051-8492) 522 * 523 * @param builder the result builder to populate with raw statements 524 */ 525 private void dopostgresqlgetrawsqlstatements(SqlParseResult.Builder builder) { 526 int waitingEnd = 0; 527 boolean foundEnd = false, enterDeclare = false; 528 boolean isSinglePLBlock = false; 529 530 if (TBaseType.assigned(sqlstatements)) sqlstatements.clear(); 531 if (!TBaseType.assigned(sourcetokenlist)) { 532 // No tokens available - populate builder with empty results and return 533 builder.sqlStatements(this.sqlstatements); 534 builder.errorCode(1); 535 builder.errorMessage("No source token list available"); 536 return; 537 } 538 539 TCustomSqlStatement gcurrentsqlstatement = null; 540 EFindSqlStateType gst = EFindSqlStateType.stnormal; 541 TSourceToken lcprevsolidtoken = null, ast = null; 542 543 if (isSinglePLBlock) { 544 gcurrentsqlstatement = new TCommonBlock(EDbVendor.dbvpostgresql); 545 } 546 547 // Mantis 4497: a '/' on its own line is tentatively flagged as an Oracle 548 // SQL*Plus statement terminator (tokencode sqlpluscmd, but tokentype still 549 // ttslash) by dopostgresqlsqltexttotokenlist(). PostgreSQL/psql has no '/' 550 // terminator -- '/' is the division operator. When such a slash sits between 551 // two operands (its next solid token can start a right-hand operand), revert 552 // it to a normal division operator so the surrounding expression is parsed as 553 // a single statement instead of being split. A genuine terminator is followed 554 // by end-of-input or a statement keyword, never a bare operand, so it keeps 555 // the sqlpluscmd flag and still splits. This pre-pass runs before the main 556 // extraction loop because sqlplusaftercurtoken() peeks ahead at the slash 557 // while processing the preceding token, so the revert must already be done. 558 for (int i = 0; i < sourcetokenlist.size(); i++) { 559 TSourceToken st = sourcetokenlist.get(i); 560 if ((st.tokencode == TBaseType.sqlpluscmd) 561 && (st.tokentype == ETokenType.ttslash) 562 && isDivisionOperatorContext(st)) { 563 st.tokencode = (int) '/'; 564 } 565 } 566 567 for (int i = 0; i < sourcetokenlist.size(); i++) { 568 569 if ((ast != null) && (ast.issolidtoken())) 570 lcprevsolidtoken = ast; 571 572 ast = sourcetokenlist.get(i); 573 sourcetokenlist.curpos = i; 574 575 if (isSinglePLBlock) { 576 gcurrentsqlstatement.sourcetokenlist.add(ast); 577 continue; 578 } 579 580 // Token transformations during raw statement extraction 581 performRawStatementTokenTransformations(ast); 582 583 switch (gst) { 584 case sterror: { 585 if (ast.tokentype == ETokenType.ttsemicolon) { 586 appendToken(gcurrentsqlstatement, ast); 587 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 588 gst = EFindSqlStateType.stnormal; 589 } else { 590 appendToken(gcurrentsqlstatement, ast); 591 } 592 break; 593 } 594 595 case stnormal: { 596 if ((ast.tokencode == TBaseType.cmtdoublehyphen) 597 || (ast.tokencode == TBaseType.cmtslashstar) 598 || (ast.tokencode == TBaseType.lexspace) 599 || (ast.tokencode == TBaseType.lexnewline) 600 || (ast.tokentype == ETokenType.ttsemicolon)) { 601 if (gcurrentsqlstatement != null) { 602 appendToken(gcurrentsqlstatement, ast); 603 } 604 605 if ((lcprevsolidtoken != null) && (ast.tokentype == ETokenType.ttsemicolon)) { 606 if (lcprevsolidtoken.tokentype == ETokenType.ttsemicolon) { 607 ast.tokentype = ETokenType.ttsimplecomment; 608 ast.tokencode = TBaseType.cmtdoublehyphen; 609 } 610 } 611 612 continue; 613 } 614 615 if (ast.tokencode == TBaseType.sqlpluscmd) { 616 gst = EFindSqlStateType.stsqlplus; 617 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 618 appendToken(gcurrentsqlstatement, ast); 619 continue; 620 } 621 622 // Handle psql meta-commands (\command) from pg_dump output 623 if (ast.tokencode == TBaseType.error 624 && "\\".equals(ast.getAstext()) 625 && (lcprevsolidtoken == null 626 || lcprevsolidtoken.tokentype == ETokenType.ttsemicolon)) { 627 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 628 appendToken(gcurrentsqlstatement, ast); 629 // Consume all tokens until newline 630 while (i + 1 < sourcetokenlist.size()) { 631 TSourceToken nextToken = sourcetokenlist.get(i + 1); 632 if (nextToken.tokentype == ETokenType.ttreturn) { 633 break; 634 } 635 i++; 636 ast = nextToken; 637 appendToken(gcurrentsqlstatement, ast); 638 } 639 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 640 gst = EFindSqlStateType.stnormal; 641 continue; 642 } 643 644 // Find a token to start sql or plsql mode 645 gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement); 646 647 if (gcurrentsqlstatement != null) { 648 enterDeclare = false; 649 if (gcurrentsqlstatement.ispgplsql()) { 650 gst = EFindSqlStateType.ststoredprocedure; 651 appendToken(gcurrentsqlstatement, ast); 652 foundEnd = false; 653 if ((ast.tokencode == TBaseType.rrw_begin) 654 || (ast.tokencode == TBaseType.rrw_package) 655 || (ast.searchToken(TBaseType.rrw_package, 4) != null)) { 656 waitingEnd = 1; 657 } else if (ast.tokencode == TBaseType.rrw_declare) { 658 enterDeclare = true; 659 } 660 } else { 661 gst = EFindSqlStateType.stsql; 662 appendToken(gcurrentsqlstatement, ast); 663 } 664 } else { 665 // Error token found 666 this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, 667 (ast.columnNo < 0 ? 0 : ast.columnNo), 668 "Error when tokenize", EErrorType.spwarning, 669 TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist)); 670 671 ast.tokentype = ETokenType.tttokenlizererrortoken; 672 gst = EFindSqlStateType.sterror; 673 674 gcurrentsqlstatement = new TUnknownSqlStatement(vendor); 675 gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid; 676 appendToken(gcurrentsqlstatement, ast); 677 } 678 679 break; 680 } 681 682 case stsqlplus: { 683 if (ast.insqlpluscmd) { 684 appendToken(gcurrentsqlstatement, ast); 685 } else { 686 gst = EFindSqlStateType.stnormal; 687 appendToken(gcurrentsqlstatement, ast); 688 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 689 } 690 691 break; 692 } 693 694 case stsql: { 695 if (ast.tokentype == ETokenType.ttsemicolon) { 696 gst = EFindSqlStateType.stnormal; 697 appendToken(gcurrentsqlstatement, ast); 698 gcurrentsqlstatement.semicolonended = ast; 699 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 700 continue; 701 } 702 703 if (sourcetokenlist.sqlplusaftercurtoken()) { 704 gst = EFindSqlStateType.stnormal; 705 appendToken(gcurrentsqlstatement, ast); 706 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 707 continue; 708 } 709 710 if (ast.tokencode == TBaseType.cmtdoublehyphen) { 711 if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { 712 gst = EFindSqlStateType.stnormal; 713 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 714 continue; 715 } 716 } 717 718 appendToken(gcurrentsqlstatement, ast); 719 break; 720 } 721 722 case ststoredprocedure: { 723 if (ast.tokencode == TBaseType.rrw_postgresql_function_delimiter) { 724 appendToken(gcurrentsqlstatement, ast); 725 gst = EFindSqlStateType.ststoredprocedurePgStartBody; 726 continue; 727 } 728 729 if (ast.tokencode == TBaseType.rrw_postgresql_language) { 730 TSourceToken nextSt = ast.nextSolidToken(); 731 if (nextSt != null) { 732 if (gcurrentsqlstatement instanceof TRoutine) { 733 TRoutine p = (TRoutine) gcurrentsqlstatement; 734 p.setRoutineLanguage(nextSt.toString()); 735 } 736 } 737 } 738 739 if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnd == 0) && (!enterDeclare)) { 740 gst = EFindSqlStateType.stnormal; 741 appendToken(gcurrentsqlstatement, ast); 742 gcurrentsqlstatement.semicolonended = ast; 743 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 744 continue; 745 } 746 747 if (ast.tokencode == TBaseType.rrw_begin) { 748 waitingEnd++; 749 enterDeclare = false; 750 } else if (ast.tokencode == TBaseType.rrw_declare) { 751 enterDeclare = true; 752 } else if (ast.tokencode == TBaseType.rrw_if) { 753 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 754 waitingEnd++; 755 } 756 } else if (ast.tokencode == TBaseType.rrw_case) { 757 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 758 waitingEnd++; 759 } 760 } else if (ast.tokencode == TBaseType.rrw_loop) { 761 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 762 waitingEnd++; 763 } 764 } else if (ast.tokencode == TBaseType.rrw_end) { 765 foundEnd = true; 766 waitingEnd--; 767 if (waitingEnd < 0) { 768 waitingEnd = 0; 769 } 770 } 771 772 if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) { 773 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 774 gst = EFindSqlStateType.stnormal; 775 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 776 777 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 778 appendToken(gcurrentsqlstatement, ast); 779 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 780 } else if ((ast.tokentype == ETokenType.ttperiod) 781 && (sourcetokenlist.returnaftercurtoken(false)) 782 && (sourcetokenlist.returnbeforecurtoken(false))) { 783 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 784 gst = EFindSqlStateType.stnormal; 785 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 786 787 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 788 appendToken(gcurrentsqlstatement, ast); 789 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 790 } else { 791 appendToken(gcurrentsqlstatement, ast); 792 if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnd == 0) && (foundEnd)) { 793 gst = EFindSqlStateType.stnormal; 794 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 795 } 796 } 797 798 if (ast.tokencode == TBaseType.sqlpluscmd) { 799 int m = flexer.getkeywordvalue(ast.getAstext()); 800 if (m != 0) { 801 ast.tokencode = m; 802 } else { 803 ast.tokencode = TBaseType.ident; 804 } 805 } 806 807 if ((gst == EFindSqlStateType.ststoredprocedure) && (ast.tokencode == TBaseType.cmtdoublehyphen)) { 808 if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { 809 gst = EFindSqlStateType.stnormal; 810 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 811 } 812 } 813 814 break; 815 } 816 817 case ststoredprocedurePgStartBody: { 818 appendToken(gcurrentsqlstatement, ast); 819 820 if (ast.tokencode == TBaseType.rrw_postgresql_function_delimiter) { 821 if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstDoExecuteBlock) { 822 // Check if DO block has trailing LANGUAGE clause 823 TSourceToken nextSolid = ast.nextSolidToken(); 824 if (nextSolid != null && nextSolid.tokencode == TBaseType.rrw_postgresql_language) { 825 gst = EFindSqlStateType.ststoredprocedurePgEndBody; 826 } else { 827 gst = EFindSqlStateType.stnormal; 828 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 829 } 830 continue; 831 } else { 832 gst = EFindSqlStateType.ststoredprocedurePgEndBody; 833 continue; 834 } 835 } 836 837 break; 838 } 839 840 case ststoredprocedurePgEndBody: { 841 if (ast.tokentype == ETokenType.ttsemicolon) { 842 gst = EFindSqlStateType.stnormal; 843 appendToken(gcurrentsqlstatement, ast); 844 gcurrentsqlstatement.semicolonended = ast; 845 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 846 continue; 847 } else if (ast.tokencode == TBaseType.cmtdoublehyphen) { 848 if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { 849 gst = EFindSqlStateType.stnormal; 850 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder); 851 continue; 852 } 853 } 854 855 appendToken(gcurrentsqlstatement, ast); 856 857 if (ast.tokencode == TBaseType.rrw_postgresql_language) { 858 TSourceToken nextSt = ast.nextSolidToken(); 859 if (nextSt != null) { 860 if (gcurrentsqlstatement instanceof TRoutine) { 861 TRoutine p = (TRoutine) gcurrentsqlstatement; 862 p.setRoutineLanguage(nextSt.toString()); 863 } 864 } 865 } 866 867 break; 868 } 869 } 870 } 871 872 // Last statement 873 if ((gcurrentsqlstatement != null) && 874 ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) 875 || (gst == EFindSqlStateType.ststoredprocedure) 876 || (gst == EFindSqlStateType.ststoredprocedurePgEndBody) 877 || (gst == EFindSqlStateType.sterror) || (isSinglePLBlock))) { 878 onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, true, builder); 879 } 880 881 // Populate builder with results 882 builder.sqlStatements(this.sqlstatements); 883 builder.syntaxErrors(syntaxErrors instanceof ArrayList ? 884 (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors)); 885 builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size()); 886 } 887 888 /** 889 * Handle token transformations during raw statement extraction. 890 * 891 * @param ast current token being processed 892 */ 893 private void performRawStatementTokenTransformations(TSourceToken ast) { 894 if (ast.tokencode == TBaseType.JSON_EXIST) { 895 TSourceToken stConstant = ast.searchToken(TBaseType.sconst, 1); 896 if (stConstant == null) { 897 ast.tokencode = TBaseType.ident; 898 } 899 } else if (ast.tokencode == TBaseType.rrw_postgresql_POSITION) { 900 TSourceToken st1 = ast.nextSolidToken(); 901 if (st1 != null) { 902 if (st1.tokencode == '(') { 903 ast.tokencode = TBaseType.rrw_postgresql_POSITION_FUNCTION; 904 } 905 } 906 } else if (ast.tokencode == TBaseType.rrw_postgresql_ordinality) { 907 TSourceToken lcprevst = getprevsolidtoken(ast); 908 909 if (lcprevst != null) { 910 if (lcprevst.tokencode == TBaseType.rrw_with) { 911 TSourceToken lcbeforewith = getprevsolidtoken(lcprevst); 912 if (lcbeforewith != null && lcbeforewith.tokencode == ')') { 913 // WITH ORDINALITY after function call - table modifier 914 lcprevst.tokencode = TBaseType.rrw_postgresql_with_lookahead; 915 } 916 // Otherwise keep as RW_WITH (CTE: WITH ordinality AS ...) 917 } 918 } 919 } else if (ast.tokencode == TBaseType.rrw_postgresql_filter) { 920 TSourceToken st1 = ast.nextSolidToken(); 921 if (st1 != null) { 922 if (st1.tokencode != '(') { 923 ast.tokencode = TBaseType.ident; 924 } 925 } 926 } else if (ast.tokencode == TBaseType.rrw_postgresql_jsonb) { 927 TSourceToken st1 = ast.nextSolidToken(); 928 if (st1 != null) { 929 if (st1.tokencode == '?') { 930 st1.tokencode = TBaseType.OP_JSONB_QUESTION; 931 } 932 } 933 } else if (ast.tokencode == '?') { 934 TSourceToken st1 = ast.nextSolidToken(); 935 if (st1 != null) { 936 if (st1.tokencode == TBaseType.sconst) { 937 ast.tokencode = TBaseType.OP_JSONB_QUESTION; 938 } 939 } 940 } else if (ast.tokencode == TBaseType.rrw_values) { 941 TSourceToken stParen = ast.searchToken('(', 1); 942 if (stParen != null) { 943 TSourceToken stInsert = ast.searchToken(TBaseType.rrw_insert, -ast.posinlist); 944 if (stInsert != null) { 945 TSourceToken stSemiColon = ast.searchToken(';', -ast.posinlist); 946 if ((stSemiColon != null) && (stSemiColon.posinlist > stInsert.posinlist)) { 947 // Don't treat values(1) as insert values 948 } else { 949 TSourceToken stFrom = ast.searchToken(TBaseType.rrw_from, -ast.posinlist); 950 if ((stFrom != null) && (stFrom.posinlist > stInsert.posinlist)) { 951 // Don't treat values after from keyword as an insert values 952 } else { 953 ast.tokencode = TBaseType.rrw_postgresql_insert_values; 954 } 955 } 956 } 957 } 958 } 959 } 960 961 private void appendToken(TCustomSqlStatement statement, TSourceToken token) { 962 if (statement == null || token == null) { 963 return; 964 } 965 token.stmt = statement; 966 statement.sourcetokenlist.add(token); 967 } 968 969 // Note: initializeGlobalContext() inherited from AbstractSqlParser 970 971 /** 972 * Override onRawStatementComplete to add PostgreSQL-specific processing. 973 * 974 * <p>This method handles special processing for stored procedures/functions 975 * whose body is written in non-SQL languages (e.g., PL/Python, PL/Perl, PL/R). 976 * 977 * <p>For such routines, the tokens between dollar-quote delimiters ($$, $function$, etc.) 978 * are marked as non-SQL content to prevent parsing errors. 979 * 980 * @param context parser context 981 * @param statement the completed statement 982 * @param mainParser main SQL parser 983 * @param secondaryParser secondary parser (not used for PostgreSQL) 984 * @param statementList list to add the statement to 985 * @param isLastStatement whether this is the last statement 986 * @param builder result builder for populating parse results 987 */ 988 @Override 989 protected void onRawStatementComplete(ParserContext context, 990 TCustomSqlStatement statement, 991 TCustomParser mainParser, 992 TCustomParser secondaryParser, 993 TStatementList statementList, 994 boolean isLastStatement, 995 SqlParseResult.Builder builder) { 996 // Call parent implementation for standard processing 997 super.onRawStatementComplete(context, statement, mainParser, secondaryParser, statementList, isLastStatement, builder); 998 999 // PostgreSQL-specific: Handle stored procedures with non-SQL bodies 1000 // (e.g., PL/Python, PL/Perl, PL/R, PL/Java, PL/Tcl) 1001 if (statement instanceof TRoutine) { 1002 TRoutine routine = (TRoutine) statement; 1003 1004 // Check if the routine body is NOT written in SQL/PLPGSQL 1005 if (!routine.isBodyInSQL()) { 1006 processNonSqlRoutineBody(routine); 1007 } 1008 } 1009 } 1010 1011 /** 1012 * Process a routine whose body is written in a non-SQL language. 1013 * 1014 * <p>This method: 1015 * <ul> 1016 * <li>Identifies the dollar-quote delimiters marking the routine body</li> 1017 * <li>Marks all tokens between delimiters as non-SQL (sqlpluscmd type)</li> 1018 * <li>Extracts and stores the complete routine body text</li> 1019 * </ul> 1020 * 1021 * <p>This prevents the parser from trying to parse Python, Perl, or other 1022 * language syntax as SQL, which would cause syntax errors. 1023 * 1024 * @param routine the routine statement to process 1025 */ 1026 private void processNonSqlRoutineBody(TRoutine routine) { 1027 if (routine.sourcetokenlist == null || routine.sourcetokenlist.size() == 0) { 1028 return; 1029 } 1030 1031 TSourceToken st; 1032 boolean inBody = false; 1033 StringBuilder routineBodyBuilder = new StringBuilder(); 1034 1035 // Scan through all tokens to find and mark the routine body 1036 for (int i = 0; i < routine.sourcetokenlist.size(); i++) { 1037 st = routine.sourcetokenlist.get(i); 1038 1039 // Check if this is a dollar-quote delimiter 1040 if (isDollarFunctionDelimiter(st.tokencode, this.vendor)) { 1041 if (!inBody) { 1042 // Start of body - record opening delimiter 1043 inBody = true; 1044 routineBodyBuilder.append(st.toString()); 1045 } else { 1046 // End of body - record closing delimiter 1047 inBody = false; 1048 routineBodyBuilder.append(st.toString()); 1049 break; 1050 } 1051 continue; 1052 } 1053 1054 // If we're inside the body, mark token as non-SQL and collect its text 1055 if (inBody) { 1056 st.tokencode = TBaseType.sqlpluscmd; 1057 routineBodyBuilder.append(st.toString()); 1058 } 1059 } 1060 1061 // Store the complete routine body text 1062 routine.setRoutineBody(routineBodyBuilder.toString()); 1063 } 1064 1065 // Note: isDollarFunctionDelimiter() is now inherited from AbstractSqlParser 1066 // The parent implementation handles all PostgreSQL-family databases 1067 1068 @Override 1069 public String toString() { 1070 return "PostgreSqlParser{vendor=" + vendor + "}"; 1071 } 1072}