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