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.TLexerGreenplum;
009import gudusoft.gsqlparser.TParserGreenplum;
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.greenplum.TSlashCommand;
021import gudusoft.gsqlparser.stmt.TRoutine;
022import gudusoft.gsqlparser.sqlcmds.ISqlCmds;
023import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory;
024import gudusoft.gsqlparser.compiler.TContext;
025import gudusoft.gsqlparser.sqlenv.TSQLEnv;
026import gudusoft.gsqlparser.compiler.TGlobalScope;
027import gudusoft.gsqlparser.compiler.TFrame;
028import gudusoft.gsqlparser.resolver.TSQLResolver;
029import gudusoft.gsqlparser.TLog;
030import gudusoft.gsqlparser.compiler.TASTEvaluator;
031
032import java.io.BufferedReader;
033import java.util.ArrayList;
034import java.util.List;
035import java.util.Stack;
036
037/**
038 * Greenplum database SQL parser implementation.
039 *
040 * <p>This parser handles Greenplum-specific SQL syntax including:
041 * <ul>
042 *   <li>PL/pgSQL blocks (procedures, functions, triggers)</li>
043 *   <li>Dollar-quoted strings ($$ ... $$)</li>
044 *   <li>Backslash meta commands (\d, \dt, etc.)</li>
045 *   <li>PostgreSQL-compatible syntax extensions</li>
046 *   <li>Special token handling (INNER, NOT DEFERRABLE, etc.)</li>
047 * </ul>
048 *
049 * <p><b>Design Notes:</b>
050 * <ul>
051 *   <li>Extends {@link AbstractSqlParser} using the template method pattern</li>
052 *   <li>Uses {@link TLexerGreenplum} for tokenization</li>
053 *   <li>Uses {@link TParserGreenplum} for parsing</li>
054 *   <li>Delimiter character: '/' for PL/pgSQL blocks</li>
055 * </ul>
056 *
057 * <p><b>Usage Example:</b>
058 * <pre>
059 * // Get Greenplum parser from factory
060 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvgreenplum);
061 *
062 * // Build context
063 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvgreenplum)
064 *     .sqlText("SELECT * FROM employees WHERE dept_id = 10")
065 *     .build();
066 *
067 * // Parse
068 * SqlParseResult result = parser.parse(context);
069 *
070 * // Access statements
071 * TStatementList statements = result.getSqlStatements();
072 * </pre>
073 *
074 * @see SqlParser
075 * @see AbstractSqlParser
076 * @see TLexerGreenplum
077 * @see TParserGreenplum
078 * @since 3.3.1.0
079 */
080public class GreenplumSqlParser extends AbstractSqlParser {
081
082    /**
083     * Construct Greenplum SQL parser.
084     * <p>
085     * Configures the parser for Greenplum database with default delimiter (/).
086     * <p>
087     * Following the original TGSqlParser pattern, the lexer and parser are
088     * created once in the constructor and reused for all parsing operations.
089     */
090    public GreenplumSqlParser() {
091        super(EDbVendor.dbvgreenplum);
092        this.delimiterChar = '/';
093        this.defaultDelimiterStr = "/";
094
095        // Create lexer once - will be reused for all parsing operations
096        this.flexer = new TLexerGreenplum();
097        this.flexer.delimiterchar = this.delimiterChar;
098        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
099
100        // Set parent's lexer reference for shared tokenization logic
101        this.lexer = this.flexer;
102
103        // Create parser once - will be reused for all parsing operations
104        this.fparser = new TParserGreenplum(null);
105        this.fparser.lexer = this.flexer;
106    }
107
108    // ========== Parser Components ==========
109
110    /** The Greenplum lexer used for tokenization */
111    public TLexerGreenplum flexer;
112
113    /** SQL parser (for Greenplum statements) */
114    private TParserGreenplum fparser;
115
116    /** Current statement being built during extraction */
117    private TCustomSqlStatement gcurrentsqlstatement;
118
119    // Note: Global context and frame stack fields inherited from AbstractSqlParser:
120    // - protected TContext globalContext
121    // - protected TSQLEnv sqlEnv
122    // - protected Stack<TFrame> frameStack
123    // - protected TFrame globalFrame
124
125    // ========== AbstractSqlParser Abstract Methods Implementation ==========
126
127    /**
128     * Return the Greenplum lexer instance.
129     */
130    @Override
131    protected TCustomLexer getLexer(ParserContext context) {
132        return this.flexer;
133    }
134
135    /**
136     * Return the Greenplum SQL parser instance with updated token list.
137     */
138    @Override
139    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
140        this.fparser.sourcetokenlist = tokens;
141        return this.fparser;
142    }
143
144    /**
145     * Greenplum doesn't use secondary parser (unlike Oracle with PL/SQL parser).
146     */
147    @Override
148    protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) {
149        return null;
150    }
151
152    /**
153     * Call Greenplum-specific tokenization logic.
154     * <p>
155     * Delegates to dogreenplumtexttotokenlist which handles Greenplum's
156     * tokenization including backslash commands, dollar-quoted strings, and
157     * PostgreSQL-compatible syntax.
158     */
159    @Override
160    protected void tokenizeVendorSql() {
161        dogreenplumtexttotokenlist();
162    }
163
164    /**
165     * Setup vendor parsers before raw statement extraction.
166     * <p>
167     * Injects sqlcmds and sourcetokenlist into the Greenplum parser.
168     */
169    @Override
170    protected void setupVendorParsersForExtraction() {
171        this.fparser.sqlcmds = this.sqlcmds;
172        this.fparser.sourcetokenlist = this.sourcetokenlist;
173    }
174
175    /**
176     * Call Greenplum-specific raw statement extraction.
177     * <p>
178     * Delegates to dogreenplumgetrawsqlstatements which handles:
179     * - PL/pgSQL block boundaries (dollar-quoted strings)
180     * - Backslash meta commands
181     * - Semicolon-separated statements
182     */
183    @Override
184    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
185        dogreenplumgetrawsqlstatements(builder);
186    }
187
188    /**
189     * Parse all statements in the statement list.
190     * <p>
191     * This is the main parsing loop that processes each raw statement
192     * and converts it into a fully parsed AST.
193     */
194    @Override
195    protected TStatementList performParsing(ParserContext context,
196                                           TCustomParser mainParser,
197                                           TCustomParser secondaryParser,
198                                           TSourceTokenList tokens,
199                                           TStatementList rawStatements) {
200        // Store references
201        this.fparser = (TParserGreenplum) mainParser;
202        this.sourcetokenlist = tokens;
203        this.parserContext = context;
204
205        // Initialize sqlcmds for this parsing session
206        this.sqlcmds = SqlCmdsFactory.get(vendor);
207        this.fparser.sqlcmds = this.sqlcmds;
208
209        // Initialize global context and frame stack
210        initializeGlobalContext();
211
212        // Parse each statement
213        for (int i = 0; i < rawStatements.size(); i++) {
214            TCustomSqlStatement stmt = rawStatements.getRawSql(i);
215            try {
216                stmt.setFrameStack(frameStack);
217                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
218
219                // Vendor-specific post-processing (if needed)
220                afterStatementParsed(stmt);
221
222                // Error recovery for CREATE TABLE statements
223                boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE;
224                if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) {
225                    handleCreateTableErrorRecovery(stmt);
226                }
227
228                // Collect syntax errors from the statement
229                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
230                    copyErrorsFromStatement(stmt);
231                }
232            } catch (Exception ex) {
233                // Use inherited exception handler
234                handleStatementParsingException(stmt, i, ex);
235                continue;
236            }
237        }
238
239        // Clean up frame stack
240        if (globalFrame != null) {
241            globalFrame.popMeFromStack(frameStack);
242        }
243
244        return rawStatements;
245    }
246
247    /**
248     * Perform semantic analysis on parsed statements.
249     * <p>
250     * This runs the TSQLResolver to resolve column-to-table relationships,
251     * data flow analysis, and other semantic checks.
252     */
253    @Override
254    protected void performSemanticAnalysis(ParserContext context, TStatementList statements) {
255        if (TBaseType.isEnableResolver() && getSyntaxErrors().isEmpty()) {
256            TSQLResolver resolver = new TSQLResolver(globalContext, statements);
257            resolver.resolve();
258        }
259    }
260
261
262    // ========== Greenplum-Specific Tokenization ==========
263
264    /**
265     * Tokenize Greenplum SQL text into tokens.
266     * <p>
267     * This method handles Greenplum-specific tokenization including:
268     * - Backslash meta commands (\d, \dt, etc.)
269     * - Forward slash as SQL*Plus-like command delimiter
270     * - INNER keyword disambiguation
271     * - NOT DEFERRABLE keyword combination
272     * - Special operators (%, ROWTYPE, etc.)
273     */
274    private void dogreenplumtexttotokenlist() {
275        boolean insqlpluscmd = false;
276        boolean isvalidplace = true;
277        boolean waitingreturnforfloatdiv = false;
278        boolean waitingreturnforsemicolon = false;
279        boolean continuesqlplusatnewline = false;
280
281        TSourceToken lct = null, prevst = null;
282
283        TSourceToken asourcetoken, lcprevst;
284        int yychar;
285
286        asourcetoken = getanewsourcetoken();
287        if (asourcetoken == null) return;
288        yychar = asourcetoken.tokencode;
289
290        while (yychar > 0) {
291            sourcetokenlist.add(asourcetoken);
292            switch (yychar) {
293                case TBaseType.cmtdoublehyphen:
294                case TBaseType.cmtslashstar:
295                case TBaseType.lexspace: {
296                    if (insqlpluscmd) {
297                        asourcetoken.insqlpluscmd = true;
298                    }
299                    break;
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                } //case newline
326                default: {
327                    //solid token
328                    continuesqlplusatnewline = false;
329                    waitingreturnforsemicolon = false;
330                    waitingreturnforfloatdiv = false;
331                    if (insqlpluscmd) {
332                        asourcetoken.insqlpluscmd = true;
333                        if (asourcetoken.toString().equalsIgnoreCase("-")) {
334                            continuesqlplusatnewline = true;
335                        }
336                    } else {
337                        if (asourcetoken.tokentype == ETokenType.ttsemicolon) {
338                            waitingreturnforsemicolon = true;
339                        }
340                        if ((asourcetoken.tokentype == ETokenType.ttslash)
341                                && (isvalidplace || (IsValidPlaceForDivToSqlplusCmd(sourcetokenlist, asourcetoken.posinlist)))) {
342                            lct = asourcetoken;
343                            waitingreturnforfloatdiv = true;
344                        }
345                        if ((isvalidplace) && isvalidsqlpluscmdInPostgresql(asourcetoken.toString())) {
346                            asourcetoken.tokencode = TBaseType.sqlpluscmd;
347                            if (asourcetoken.tokentype != ETokenType.ttslash) {
348                                asourcetoken.tokentype = ETokenType.ttsqlpluscmd;
349                            }
350                            insqlpluscmd = true;
351                            flexer.insqlpluscmd = insqlpluscmd;
352                        }
353                    }
354                    isvalidplace = false;
355
356                    // the inner keyword token should be convert to ident when
357                    // next solid token is not join
358
359                    if (prevst != null) {
360                        if (prevst.tokencode == TBaseType.rrw_inner) {
361                            if (asourcetoken.tokencode != flexer.getkeywordvalue("JOIN")) {
362                                prevst.tokencode = TBaseType.ident;
363                            }
364                        }
365
366                        if ((prevst.tokencode == TBaseType.rrw_not)
367                                && (asourcetoken.tokencode == flexer.getkeywordvalue("DEFERRABLE"))) {
368                            prevst.tokencode = flexer.getkeywordvalue("NOT_DEFERRABLE");
369                        }
370                    }
371
372                    if (asourcetoken.tokencode == TBaseType.rrw_inner) {
373                        prevst = asourcetoken;
374                    } else if (asourcetoken.tokencode == TBaseType.rrw_not) {
375                        prevst = asourcetoken;
376                    } else {
377                        prevst = null;
378                    }
379
380                    if ((asourcetoken.tokencode == flexer.getkeywordvalue("DIRECT_LOAD"))
381                            || (asourcetoken.tokencode == flexer.getkeywordvalue("ALL"))) {
382                        // RW_COMPRESS RW_FOR RW_ALL RW_OPERATIONS
383                        // RW_COMPRESS RW_FOR RW_DIRECT_LOAD RW_OPERATIONS
384                        // change rw_for to rw_for1, it conflicts with compress for update in create materialized view
385
386                        lcprevst = getprevsolidtoken(asourcetoken);
387                        if (lcprevst != null) {
388                            if (lcprevst.tokencode == TBaseType.rrw_for)
389                                lcprevst.tokencode = TBaseType.rw_for1;
390                        }
391                    }
392
393                    if (asourcetoken.tokencode == TBaseType.rrw_dense_rank) {
394                        //keep keyword can be column alias, make keep in keep_denserankclause as a different token code
395                        TSourceToken stKeep = asourcetoken.searchToken(TBaseType.rrw_keep, -2);
396                        if (stKeep != null) {
397                            stKeep.tokencode = TBaseType.rrw_keep_before_dense_rank;
398                        }
399                    }
400
401                    if (asourcetoken.tokencode == TBaseType.rrw_greenplum_rowtype) {
402                        TSourceToken stPercent = asourcetoken.searchToken('%', -1);
403                        if (stPercent != null) {
404                            stPercent.tokencode = TBaseType.rowtype_operator;
405                        }
406                    }
407
408                }
409            }
410
411            //flexer.yylexwrap(asourcetoken);
412            asourcetoken = getanewsourcetoken();
413            if (asourcetoken != null) {
414                yychar = asourcetoken.tokencode;
415            } else {
416                yychar = 0;
417
418                if (waitingreturnforfloatdiv) { // / at the end of line treat as sqlplus command
419                    //isvalidplace = true;
420                    lct.tokencode = TBaseType.sqlpluscmd;
421                    if (lct.tokentype != ETokenType.ttslash) {
422                        lct.tokentype = ETokenType.ttsqlpluscmd;
423                    }
424                }
425
426            }
427
428            if ((yychar == 0) && (prevst != null)) {
429                if (prevst.tokencode == TBaseType.rrw_inner) {
430                    prevst.tokencode = TBaseType.ident;
431                }
432            }
433
434        }
435    }
436
437    // ========== Greenplum-Specific Raw Statement Extraction ==========
438
439    /**
440     * Extract raw SQL statements from token list.
441     * <p>
442     * This method handles Greenplum-specific statement boundaries:
443     * - Semicolon (;) for regular SQL statements
444     * - Dollar-quoted strings ($$ ... $$) for PL/pgSQL function bodies
445     * - Backslash commands (\d, \dt, etc.)
446     * - BEGIN/END blocks for stored procedures
447     */
448    private int dogreenplumgetrawsqlstatements(SqlParseResult.Builder builder) {
449        int waitingEnd = 0;
450        boolean foundEnd = false, enterDeclare = false;
451
452        if (TBaseType.assigned(sqlstatements)) sqlstatements.clear();
453        if (!TBaseType.assigned(sourcetokenlist)) return -1;
454
455        gcurrentsqlstatement = null;
456        EFindSqlStateType gst = EFindSqlStateType.stnormal;
457        TSourceToken lcprevsolidtoken = null, ast = null;
458        TSourceToken dollarStringToken = null;
459
460        // Mantis 4497: a '/' on its own line is tentatively flagged as an Oracle
461        // SQL*Plus statement terminator (tokencode sqlpluscmd, but tokentype still
462        // ttslash) by dogreenplumsqltexttotokenlist(). Greenplum/psql has no '/'
463        // terminator -- '/' is the division operator. When such a slash sits between
464        // two operands (its next solid token can start a right-hand operand), revert
465        // it to a normal division operator so the surrounding expression is parsed as
466        // a single statement instead of being split. A genuine terminator is followed
467        // by end-of-input or a statement keyword, never a bare operand, so it keeps
468        // the sqlpluscmd flag and still splits. This pre-pass runs before the main
469        // extraction loop because sqlplusaftercurtoken() peeks ahead at the slash
470        // while processing the preceding token, so the revert must already be done.
471        for (int i = 0; i < sourcetokenlist.size(); i++) {
472            TSourceToken st = sourcetokenlist.get(i);
473            if ((st.tokencode == TBaseType.sqlpluscmd)
474                    && (st.tokentype == ETokenType.ttslash)
475                    && isDivisionOperatorContext(st)) {
476                st.tokencode = (int) '/';
477            }
478        }
479
480        for (int i = 0; i < sourcetokenlist.size(); i++) {
481
482            if ((ast != null) && (ast.issolidtoken()))
483                lcprevsolidtoken = ast;
484
485            ast = sourcetokenlist.get(i);
486            sourcetokenlist.curpos = i;
487
488            // Special token adjustments
489            if (ast.tokencode == TBaseType.rrw_date) {
490                TSourceToken st1 = ast.nextSolidToken();
491                if (st1 != null) {
492                    if (st1.tokencode == '(') {
493                        ast.tokencode = TBaseType.rrw_greenplum_DATE_FUNCTION;
494                    }
495                }
496            } else if (ast.tokencode == TBaseType.rrw_greenplum_POSITION) {
497                TSourceToken st1 = ast.nextSolidToken();
498                if (st1 != null) {
499                    if (st1.tokencode == '(') {
500                        ast.tokencode = TBaseType.rrw_greenplum_POSITION_FUNCTION;
501                    }
502                }
503            } else if (ast.tokencode == TBaseType.rrw_greenplum_filter) {
504                TSourceToken st1 = ast.nextSolidToken();
505                if (st1 != null) {
506                    if (st1.tokencode == '(') {
507
508                    } else {
509                        ast.tokencode = TBaseType.ident;
510                    }
511                }
512            } else if (ast.tokencode == TBaseType.rrw_values) {
513                TSourceToken stParen = ast.searchToken('(', 1);
514                if (stParen != null) {
515                    TSourceToken stInsert = ast.searchToken(TBaseType.rrw_insert, -ast.posinlist);
516                    if (stInsert != null) {
517                        TSourceToken stSemiColon = ast.searchToken(';', -ast.posinlist);
518                        if ((stSemiColon != null) && (stSemiColon.posinlist > stInsert.posinlist)) {
519//                            INSERT INTO test values (16,1), (8,2), (4,4), (2,0), (97, 16);
520//                            VALUES (1);
521                            // don't treat values(1) as insert values
522
523                        } else {
524                            TSourceToken stFrom = ast.searchToken(TBaseType.rrw_from, -ast.posinlist);
525                            if (stFrom != null) {
526                                // don't treat values after from keyword as a insert values
527                                // insert into inserttest values(10, 20, '40'), (-1, 2, DEFAULT),  ((select 2), (select i from (values(3) ) as foo (i)), 'values are fun!');
528
529                            } else {
530                                ast.tokencode = TBaseType.rrw_greenplum_values_insert;
531                            }
532
533                        }
534
535                    }
536                }
537            }
538
539            switch (gst) {
540                case sterror: {
541                    if (ast.tokentype == ETokenType.ttsemicolon) {
542                        gcurrentsqlstatement.sourcetokenlist.add(ast);
543                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
544                        gst = EFindSqlStateType.stnormal;
545                    } else {
546                        gcurrentsqlstatement.sourcetokenlist.add(ast);
547                    }
548                    break;
549                } //sterror
550
551                case stnormal: {
552                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
553                            || (ast.tokencode == TBaseType.cmtslashstar)
554                            || (ast.tokencode == TBaseType.lexspace)
555                            || (ast.tokencode == TBaseType.lexnewline)
556                            || (ast.tokentype == ETokenType.ttsemicolon)) {
557                        if (gcurrentsqlstatement != null) {
558                            gcurrentsqlstatement.sourcetokenlist.add(ast);
559                        }
560
561                        if ((lcprevsolidtoken != null) && (ast.tokentype == ETokenType.ttsemicolon)) {
562                            if (lcprevsolidtoken.tokentype == ETokenType.ttsemicolon) {
563                                // ;;;; continuous semicolon,treat it as comment
564                                ast.tokentype = ETokenType.ttsimplecomment;
565                                ast.tokencode = TBaseType.cmtdoublehyphen;
566                            }
567                        }
568
569                        continue;
570                    }
571
572                    if (ast.tokencode == '\\') {
573                        gst = EFindSqlStateType.stsqlplus;
574                        gcurrentsqlstatement = new TSlashCommand(vendor);
575                        gcurrentsqlstatement.sourcetokenlist.add(ast);
576                        continue;
577                    }
578
579                    // find a token to start sql or plsql mode
580                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
581
582                    if (gcurrentsqlstatement != null) {
583                        if (gcurrentsqlstatement.isgreeplumplsql()) {
584                            gst = EFindSqlStateType.ststoredprocedure;
585                            gcurrentsqlstatement.sourcetokenlist.add(ast);
586                            foundEnd = false;
587                            if ((ast.tokencode == TBaseType.rrw_begin)
588                                    || (ast.tokencode == TBaseType.rrw_package)
589                                    || (ast.searchToken(TBaseType.rrw_package, 4) != null)) {
590                                waitingEnd = 1;
591                            } else if (ast.tokencode == TBaseType.rrw_declare) {
592                                enterDeclare = true;
593                            }
594                        } else {
595                            gst = EFindSqlStateType.stsql;
596                            gcurrentsqlstatement.sourcetokenlist.add(ast);
597                        }
598                    } else {
599                        //error token found
600
601                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo)
602                                , "Error when tokenlize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
603
604                        ast.tokentype = ETokenType.tttokenlizererrortoken;
605                        gst = EFindSqlStateType.sterror;
606
607                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
608                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
609                        gcurrentsqlstatement.sourcetokenlist.add(ast);
610
611                    }
612
613                    break;
614                } // stnormal
615
616                case stsqlplus: {
617                    if (ast.tokencode == TBaseType.lexnewline) {
618                        gst = EFindSqlStateType.stnormal; //this token must be newline,
619                        gcurrentsqlstatement.sourcetokenlist.add(ast); // so add it here
620                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
621                    }
622                    if (ast.tokencode == '\\') {
623                        TSourceToken nextst = ast.searchToken('\\', 1);
624                        if (nextst != null) {
625                            gst = EFindSqlStateType.stnormal;
626                            gcurrentsqlstatement.sourcetokenlist.add(ast);
627                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
628                        } else {
629                            gst = EFindSqlStateType.stsqlplus;
630                            gcurrentsqlstatement = new TSlashCommand(vendor);
631                            gcurrentsqlstatement.sourcetokenlist.add(ast);
632                            continue;
633                        }
634                    } else {
635                        {
636                            gcurrentsqlstatement.sourcetokenlist.add(ast);
637                        }
638                    }
639
640                    break;
641                }//case greenplum meta command
642
643                case stsql: {
644                    if (ast.tokentype == ETokenType.ttsemicolon) {
645                        gst = EFindSqlStateType.stnormal;
646                        gcurrentsqlstatement.sourcetokenlist.add(ast);
647                        gcurrentsqlstatement.semicolonended = ast;
648                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
649                        continue;
650                    }
651
652                    if (sourcetokenlist.sqlplusaftercurtoken()) //most probably is / cmd
653                    {
654                        gst = EFindSqlStateType.stnormal;
655                        gcurrentsqlstatement.sourcetokenlist.add(ast);
656                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
657                        continue;
658                    }
659                    gcurrentsqlstatement.sourcetokenlist.add(ast);
660                    break;
661                }//case stsql
662
663                case ststoredprocedure: {
664                    if (ast.tokencode == TBaseType.rrw_greenplum_function_delimiter) {
665                        gcurrentsqlstatement.sourcetokenlist.add(ast);
666                        gst = EFindSqlStateType.ststoredprocedurePgStartBody;
667                        dollarStringToken = ast;
668                        continue;
669                    }
670
671                    if (ast.tokencode == TBaseType.rrw_greenplum_language) {
672                        // check next token which is the language used by this stored procedure
673                        TSourceToken nextSt = ast.nextSolidToken();
674                        if (nextSt != null) {
675                            if (gcurrentsqlstatement instanceof TRoutine) {  // can be TCreateProcedureStmt or TCreateFunctionStmt
676                                TRoutine p = (TRoutine) gcurrentsqlstatement;
677                                p.setRoutineLanguage(nextSt.toString());
678                            }
679                        }
680                    }
681
682                    if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnd == 0) && (!enterDeclare)) {
683                        gst = EFindSqlStateType.stnormal;
684                        gcurrentsqlstatement.sourcetokenlist.add(ast);
685                        gcurrentsqlstatement.semicolonended = ast;
686                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
687                        continue;
688                    }
689
690
691                    if ((ast.tokencode == TBaseType.rrw_begin)) {
692                        waitingEnd++;
693                        enterDeclare = false;
694                    } else if ((ast.tokencode == TBaseType.rrw_declare)) {
695                        enterDeclare = true;
696                    } else if ((ast.tokencode == TBaseType.rrw_if)) {
697                        if (ast.searchToken(TBaseType.rrw_end, -1) == null) {
698                            //this is not if after END
699                            if (!((ast.searchToken(TBaseType.rrw_greenplum_exits, 1) != null)
700                                    || (ast.searchToken(TBaseType.rrw_greenplum_exits, 2) != null))) // if not exists, if exists is not a real if statement, so skip those
701                            {
702                                waitingEnd++;
703                            }
704                        }
705                    } else if ((ast.tokencode == TBaseType.rrw_case)) {
706                        if (ast.searchToken(TBaseType.rrw_end, -1) == null) {
707                            //this is not case after END
708                            waitingEnd++;
709                        }
710                    } else if ((ast.tokencode == TBaseType.rrw_loop)) {
711                        if (ast.searchToken(TBaseType.rrw_end, -1) == null) {
712                            //this is not loop after END
713                            waitingEnd++;
714                        }
715                    } else if (ast.tokencode == TBaseType.rrw_end) {
716                        foundEnd = true;
717                        waitingEnd--;
718                        if (waitingEnd < 0) {
719                            waitingEnd = 0;
720                        }
721                    }
722
723                    if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) //and (prevst.NewlineIsLastTokenInTailerToken)) then
724                    {
725                        // TPlsqlStatementParse(asqlstatement).TerminatorToken := ast;
726                        ast.tokenstatus = ETokenStatus.tsignorebyyacc;
727                        gst = EFindSqlStateType.stnormal;
728                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
729
730                        //make / a sqlplus cmd
731                        gcurrentsqlstatement = new gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement(vendor);
732                        gcurrentsqlstatement.sourcetokenlist.add(ast);
733                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
734                    } else if ((ast.tokentype == ETokenType.ttperiod) && (sourcetokenlist.returnaftercurtoken(false)) && (sourcetokenlist.returnbeforecurtoken(false))) {    // single dot at a separate line
735                        ast.tokenstatus = ETokenStatus.tsignorebyyacc;
736                        gst = EFindSqlStateType.stnormal;
737                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
738
739                        //make ttperiod a sqlplus cmd
740                        gcurrentsqlstatement = new gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement(vendor);
741                        gcurrentsqlstatement.sourcetokenlist.add(ast);
742                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
743                    } else if ((ast.tokencode == TBaseType.rrw_declare) && (waitingEnd == 0)) {
744                        i--;
745                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
746                        gst = EFindSqlStateType.stnormal;
747                    } else {
748                        gcurrentsqlstatement.sourcetokenlist.add(ast);
749                        if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnd == 0) && (foundEnd) && (gcurrentsqlstatement.OracleStatementCanBeSeparatedByBeginEndPair())) {
750                            gst = EFindSqlStateType.stnormal;
751                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
752                        }
753                    }
754
755                    if (ast.tokencode == TBaseType.sqlpluscmd) {
756                        //change tokencode back to keyword or ident, because sqlplus cmd
757                        //in a sql statement(almost is plsql block) is not really a sqlplus cmd
758                        int m = flexer.getkeywordvalue(ast.getAstext());
759                        if (m != 0) {
760                            ast.tokencode = m;
761                        } else {
762                            ast.tokencode = TBaseType.ident;
763                        }
764                    }
765
766                    break;
767                } //ststoredprocedure
768
769                case ststoredprocedurePgStartBody: {
770                    gcurrentsqlstatement.sourcetokenlist.add(ast);
771
772                    if (ast.tokencode == TBaseType.rrw_greenplum_function_delimiter) {
773                        if (dollarStringToken.toString().equalsIgnoreCase(ast.toString())) {
774                            // must match the $$ token
775                            gst = EFindSqlStateType.ststoredprocedurePgEndBody;
776                            continue;
777                        }
778                    }
779
780                    break;
781                }
782
783                case ststoredprocedurePgEndBody: {
784
785                    if (ast.tokentype == ETokenType.ttsemicolon) {
786                        gst = EFindSqlStateType.stnormal;
787                        gcurrentsqlstatement.sourcetokenlist.add(ast);
788                        gcurrentsqlstatement.semicolonended = ast;
789                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
790                        continue;
791                    } else if (ast.tokencode == TBaseType.cmtdoublehyphen) {
792                        if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) { // -- sqlflow-delimiter
793                            gst = EFindSqlStateType.stnormal;
794                            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
795                            continue;
796                        }
797                    }
798
799                    gcurrentsqlstatement.sourcetokenlist.add(ast);
800
801                    if (ast.tokencode == TBaseType.rrw_greenplum_language) {
802                        // check next token which is the language used by this stored procedure
803                        TSourceToken nextSt = ast.nextSolidToken();
804                        if (nextSt != null) {
805                            if (gcurrentsqlstatement instanceof TRoutine) {  // can be TCreateProcedureStmt or TCreateFunctionStmt
806                                TRoutine p = (TRoutine) gcurrentsqlstatement;
807                                p.setRoutineLanguage(nextSt.toString());
808                            }
809                        }
810                    }
811
812                    break;
813                }
814
815            } //switch
816        }//for
817
818        //last statement
819        if ((gcurrentsqlstatement != null) &&
820                ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql)
821                        || (gst == EFindSqlStateType.ststoredprocedure) || (gst == EFindSqlStateType.ststoredprocedurePgEndBody)
822                        || (gst == EFindSqlStateType.sterror))) {
823            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder);
824        }
825
826        // Set results in builder
827        builder.sqlStatements(this.sqlstatements);
828        builder.errorCode(0);
829        builder.errorMessage("");
830
831        return 0;
832    }
833
834    // ========== Helper Methods ==========
835
836    /**
837     * Get previous solid token (non-whitespace, non-comment).
838     */
839    private TSourceToken getprevsolidtoken(TSourceToken asourcetoken) {
840        int i = asourcetoken.posinlist;
841        TSourceToken st;
842        for (i = i - 1; i >= 0; i--) {
843            st = sourcetokenlist.get(i);
844            if (st.issolidtoken()) return st;
845        }
846        return null;
847    }
848
849    /**
850     * Placeholder function for PostgreSQL-style meta command validation.
851     * Always returns false - Greenplum doesn't use SQL*Plus commands.
852     */
853    private boolean isvalidsqlpluscmdInPostgresql(String astr) {
854        return false;
855    }
856
857
858    /**
859     * Check if forward slash (/) is in a valid position to be treated as a SQL*Plus command.
860     */
861    private boolean IsValidPlaceForDivToSqlplusCmd(TSourceTokenList tokenList, int pos) {
862        // Implementation similar to TSourceTokenList.TokenBeforeCurToken
863        // This checks if there's a newline before the current token
864        if (pos <= 0) return false;
865        for (int i = pos - 1; i >= 0; i--) {
866            TSourceToken st = tokenList.get(i);
867            if (st.tokencode == TBaseType.lexnewline) {
868                return true;
869            }
870            if (st.issolidtoken()) {
871                return false;
872            }
873        }
874        return false;
875    }
876
877    /**
878     * Hook method called after each statement is parsed.
879     * <p>
880     * Greenplum doesn't need special post-processing, so this is a no-op.
881     */
882    @Override
883    protected void afterStatementParsed(TCustomSqlStatement stmt) {
884        // No special post-processing needed for Greenplum
885    }
886
887    /**
888     * Handle error recovery for CREATE TABLE statements.
889     * <p>
890     * This attempts to recover from syntax errors in table properties
891     * by marking unparseable tokens as SQL*Plus commands and retrying.
892     * <p>
893     * Migrated from TGSqlParser.doparse() lines 16914-16971
894     */
895    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
896        // Only handle CREATE TABLE and CREATE INDEX (but not for Couchbase)
897        if (!((stmt.sqlstatementtype == ESqlStatementType.sstcreatetable)
898                || ((stmt.sqlstatementtype == ESqlStatementType.sstcreateindex) && (vendor != EDbVendor.dbvcouchbase)))) {
899            return;
900        }
901
902        // Don't recover if strict parsing is enabled
903        if (TBaseType.c_createTableStrictParsing) {
904            return;
905        }
906
907        // only parse main body of create table,
908        // ignore vendor-specific table properties after closing parenthesis
909        TCustomSqlStatement errorSqlStatement = stmt;
910
911        int nested = 0;
912        boolean isIgnore = false, isFoundIgnoreToken = false;
913        TSourceToken firstIgnoreToken = null;
914
915        for (int k = 0; k < errorSqlStatement.sourcetokenlist.size(); k++) {
916            TSourceToken st = errorSqlStatement.sourcetokenlist.get(k);
917
918            if (isIgnore) {
919                if (st.issolidtoken() && (st.tokencode != ';')) {
920                    isFoundIgnoreToken = true;
921                    if (firstIgnoreToken == null) {
922                        firstIgnoreToken = st;
923                    }
924                }
925                if (st.tokencode != ';') {
926                    st.tokencode = TBaseType.sqlpluscmd;
927                }
928                continue;
929            }
930
931            if (st.tokencode == (int) ')') {
932                nested--;
933                if (nested == 0) {
934                    //let's check if next token is: AS ( SELECT
935                    boolean isSelect = false;
936                    TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1);
937                    if (st1 != null) {
938                        TSourceToken st2 = st.searchToken((int) '(', 2);
939                        if (st2 != null) {
940                            TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3);
941                            isSelect = (st3 != null);
942                        }
943                    }
944                    if (!isSelect) isIgnore = true;
945                }
946            }
947
948            if ((st.tokencode == (int) '(') || (st.tokencode == TBaseType.left_parenthesis_2)) {
949                nested++;
950            }
951        }
952
953        // For Oracle, check if the first ignored token is a valid table property
954        if ((vendor == EDbVendor.dbvoracle) && ((firstIgnoreToken != null) && (!TBaseType.searchOracleTablePros(firstIgnoreToken.toString())))) {
955            // if it is not a valid Oracle table properties option, let raise the error.
956            isFoundIgnoreToken = false;
957        }
958
959        if (isFoundIgnoreToken) {
960            errorSqlStatement.clearError();
961            stmt.parsestatement(null, false, parserContext.isOnlyNeedRawParseTree());
962        }
963    }
964}