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}