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.TLexerStarrocks;
009import gudusoft.gsqlparser.TParserStarrocksSql;
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.sqlcmds.ISqlCmds;
021import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory;
022import gudusoft.gsqlparser.compiler.TContext;
023import gudusoft.gsqlparser.sqlenv.TSQLEnv;
024import gudusoft.gsqlparser.compiler.TGlobalScope;
025import gudusoft.gsqlparser.compiler.TFrame;
026
027import java.io.BufferedReader;
028import java.util.ArrayList;
029import java.util.List;
030import java.util.Stack;
031
032/**
033 * StarRocks SQL parser implementation.
034 *
035 * <p>StarRocks is a fork of Apache Doris (from 2020) and maintains MySQL protocol
036 * compatibility. This parser is based on the Doris parser with extensions for
037 * StarRocks-specific features including:
038 * <ul>
039 *   <li>OLAP data models (DUPLICATE KEY, AGGREGATE KEY, UNIQUE KEY, PRIMARY KEY)</li>
040 *   <li>DISTRIBUTED BY HASH/RANDOM clause</li>
041 *   <li>PROPERTIES clause</li>
042 *   <li>StarRocks-specific data types (LARGEINT, HLL, BITMAP, JSON, ARRAY, MAP, STRUCT)</li>
043 *   <li>External catalogs (CREATE EXTERNAL CATALOG)</li>
044 *   <li>Asynchronous materialized views (REFRESH ASYNC/MANUAL)</li>
045 *   <li>SUBMIT TASK for async ETL</li>
046 * </ul>
047 *
048 * <p><b>Design Notes:</b>
049 * <ul>
050 *   <li>Extends {@link AbstractSqlParser}</li>
051 *   <li>Uses {@link TLexerStarrocks} and {@link TParserStarrocksSql}</li>
052 *   <li>Delimiter character: ';'</li>
053 * </ul>
054 *
055 * @see SqlParser
056 * @see AbstractSqlParser
057 * @see TLexerStarrocks
058 * @see TParserStarrocksSql
059 * @since 4.0.2.0
060 */
061public class StarrocksSqlParser extends AbstractSqlParser {
062
063    // ========== Lexer and Parser Instances ==========
064
065    /** The StarRocks lexer used for tokenization (public for TGSqlParser.getFlexer()) */
066    public TLexerStarrocks flexer;
067    private TParserStarrocksSql fparser;
068
069    // ========== State Variables for Raw Statement Extraction ==========
070    private String userDelimiterStr;
071    private char curdelimiterchar;
072    private boolean waitingDelimiter;
073
074    // ========== Constructor ==========
075
076    /**
077     * Construct StarRocks SQL parser.
078     * <p>
079     * Configures the parser for StarRocks database with default delimiter: semicolon (;)
080     */
081    public StarrocksSqlParser() {
082        super(EDbVendor.dbvstarrocks);
083
084        // Set delimiter character - StarRocks uses semicolon like standard MySQL
085        this.delimiterChar = ';';
086        this.defaultDelimiterStr = ";";
087
088        // Create lexer once - will be reused for all parsing operations
089        this.flexer = new TLexerStarrocks();
090        this.flexer.delimiterchar = this.delimiterChar;
091        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
092
093        // CRITICAL: Set lexer for inherited getanewsourcetoken() method
094        this.lexer = this.flexer;
095
096        // Create parser once - will be reused for all parsing operations
097        this.fparser = new TParserStarrocksSql(null);
098        this.fparser.lexer = this.flexer;
099    }
100
101    // ========== AbstractSqlParser Abstract Methods Implementation ==========
102
103    @Override
104    protected TCustomLexer getLexer(ParserContext context) {
105        return this.flexer;
106    }
107
108    @Override
109    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
110        this.fparser.sourcetokenlist = tokens;
111        return this.fparser;
112    }
113
114    @Override
115    protected void tokenizeVendorSql() {
116        dostarrockstexttotokenlist();
117    }
118
119    @Override
120    protected void setupVendorParsersForExtraction() {
121        this.fparser.sqlcmds = this.sqlcmds;
122        this.fparser.sourcetokenlist = this.sourcetokenlist;
123    }
124
125    @Override
126    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
127        dostarrocksgetrawsqlstatements(builder);
128    }
129
130    @Override
131    protected TStatementList performParsing(ParserContext context,
132                                           TCustomParser parser,
133                                           TCustomParser secondaryParser,
134                                           TSourceTokenList tokens,
135                                           TStatementList rawStatements) {
136        this.sourcetokenlist = tokens;
137        this.parserContext = context;
138        this.sqlstatements = rawStatements;
139
140        this.sqlcmds = SqlCmdsFactory.get(vendor);
141        this.fparser.sqlcmds = this.sqlcmds;
142
143        initializeGlobalContext();
144
145        for (int i = 0; i < sqlstatements.size(); i++) {
146            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
147
148            try {
149                stmt.setFrameStack(frameStack);
150                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
151
152                boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE;
153                if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) {
154                    handleCreateTableErrorRecovery(stmt);
155                }
156
157                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
158                    copyErrorsFromStatement(stmt);
159                }
160            } catch (Exception ex) {
161                handleStatementParsingException(stmt, i, ex);
162                continue;
163            }
164        }
165
166        if (globalFrame != null) {
167            globalFrame.popMeFromStack(frameStack);
168        }
169
170        return this.sqlstatements;
171    }
172
173    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
174        if ((stmt.sqlstatementtype != ESqlStatementType.sstcreatetable) || TBaseType.c_createTableStrictParsing) {
175            return;
176        }
177
178        // A CREATE TABLE AS SELECT keeps its query; the recovery below would drop it or cut it short.
179        int queryStart = CreateTableAsSelectRecovery.queryStart(stmt.sourcetokenlist);
180        if (queryStart >= 0) {
181            if (CreateTableAsSelectRecovery.ignoreClausesBeforeQuery(stmt.sourcetokenlist, queryStart)) {
182                stmt.clearError();
183                stmt.parsestatement(null, false, this.parserContext.isOnlyNeedRawParseTree());
184            }
185            return;
186        }
187
188        int nested = 0;
189        boolean isIgnore = false, isFoundIgnoreToken = false;
190        TSourceToken firstIgnoreToken = null;
191
192        for (int k = 0; k < stmt.sourcetokenlist.size(); k++) {
193            TSourceToken st = stmt.sourcetokenlist.get(k);
194            if (isIgnore) {
195                if (st.issolidtoken() && (st.tokencode != ';')) {
196                    isFoundIgnoreToken = true;
197                    if (firstIgnoreToken == null) {
198                        firstIgnoreToken = st;
199                    }
200                }
201                if (st.tokencode != ';') {
202                    st.tokencode = TBaseType.sqlpluscmd;
203                }
204                continue;
205            }
206            if (st.tokencode == (int) ')') {
207                nested--;
208                if (nested == 0) {
209                    boolean isSelect = false;
210                    TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1);
211                    if (st1 != null) {
212                        TSourceToken st2 = st.searchToken((int) '(', 2);
213                        if (st2 != null) {
214                            TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3);
215                            isSelect = (st3 != null);
216                        }
217                    }
218                    if (!isSelect) isIgnore = true;
219                }
220            } else if (st.tokencode == (int) '(') {
221                nested++;
222            }
223        }
224
225        if (isFoundIgnoreToken) {
226            stmt.clearError();
227            stmt.parsestatement(null, false, this.parserContext.isOnlyNeedRawParseTree());
228        }
229    }
230
231    // ========== StarRocks-Specific Tokenization ==========
232
233    private void dostarrockstexttotokenlist() {
234        TSourceToken asourcetoken, lcprevst;
235        int yychar;
236
237        asourcetoken = getanewsourcetoken();
238        if (asourcetoken == null) return;
239        yychar = asourcetoken.tokencode;
240
241        while (yychar > 0) {
242            sourcetokenlist.add(asourcetoken);
243            asourcetoken = getanewsourcetoken();
244            if (asourcetoken == null) break;
245
246            if (asourcetoken.tokencode == TBaseType.rrw_rollup) {
247                lcprevst = getprevsolidtoken(asourcetoken);
248                if (lcprevst != null) {
249                    if (lcprevst.tokencode == TBaseType.rrw_with)
250                        lcprevst.tokencode = TBaseType.with_rollup;
251                }
252            }
253
254            yychar = asourcetoken.tokencode;
255        }
256    }
257
258    private TSourceToken getprevsolidtoken(TSourceToken ptoken) {
259        TSourceToken ret = null;
260        TSourceTokenList lctokenlist = ptoken.container;
261
262        if (lctokenlist != null) {
263            if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) {
264                if (!(
265                        (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttwhitespace)
266                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttreturn)
267                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttsimplecomment)
268                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttbracketedcomment)
269                )) {
270                    ret = lctokenlist.get(ptoken.posinlist - 1);
271                } else {
272                    ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false);
273                }
274            }
275        }
276        return ret;
277    }
278
279    // ========== StarRocks-Specific Raw Statement Extraction ==========
280
281    private void dostarrocksgetrawsqlstatements(SqlParseResult.Builder builder) {
282        TCustomSqlStatement gcurrentsqlstatement = null;
283        EFindSqlStateType gst = EFindSqlStateType.stnormal;
284
285        userDelimiterStr = defaultDelimiterStr;
286
287        if (TBaseType.assigned(sqlstatements)) sqlstatements.clear();
288        if (!TBaseType.assigned(sourcetokenlist)) {
289            builder.sqlStatements(this.sqlstatements);
290            builder.errorCode(1);
291            builder.errorMessage("No source token list available");
292            return;
293        }
294
295        for (int i = 0; i < sourcetokenlist.size(); i++) {
296            TSourceToken ast = sourcetokenlist.get(i);
297            sourcetokenlist.curpos = i;
298
299            performRawStatementTokenTransformations(ast);
300
301            switch (gst) {
302                case sterror: {
303                    if (ast.tokentype == ETokenType.ttsemicolon) {
304                        appendToken(gcurrentsqlstatement, ast);
305                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
306                        gst = EFindSqlStateType.stnormal;
307                    } else {
308                        appendToken(gcurrentsqlstatement, ast);
309                    }
310                    break;
311                }
312
313                case stnormal: {
314                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
315                            || (ast.tokencode == TBaseType.cmtslashstar)
316                            || (ast.tokencode == TBaseType.lexspace)
317                            || (ast.tokencode == TBaseType.lexnewline)
318                            || (ast.tokentype == ETokenType.ttsemicolon)) {
319                        if (TBaseType.assigned(gcurrentsqlstatement)) {
320                            appendToken(gcurrentsqlstatement, ast);
321                        }
322                        continue;
323                    }
324
325                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
326
327                    if (TBaseType.assigned(gcurrentsqlstatement)) {
328                        gst = EFindSqlStateType.stsql;
329                        appendToken(gcurrentsqlstatement, ast);
330                    }
331
332                    if (!TBaseType.assigned(gcurrentsqlstatement)) {
333                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo),
334                                "Error when tokenize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
335
336                        ast.tokentype = ETokenType.tttokenlizererrortoken;
337                        gst = EFindSqlStateType.sterror;
338
339                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
340                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
341                        appendToken(gcurrentsqlstatement, ast);
342                    }
343                    break;
344                }
345
346                case stsql: {
347                    if (ast.tokentype == ETokenType.ttsemicolon) {
348                        gst = EFindSqlStateType.stnormal;
349                        appendToken(gcurrentsqlstatement, ast);
350                        gcurrentsqlstatement.semicolonended = ast;
351                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
352                        continue;
353                    }
354
355                    if (ast.tokencode == TBaseType.cmtdoublehyphen) {
356                        if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) {
357                            gst = EFindSqlStateType.stnormal;
358                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
359                            continue;
360                        }
361                    }
362
363                    appendToken(gcurrentsqlstatement, ast);
364                    break;
365                }
366
367                default:
368                    break;
369            }
370        }
371
372        // Last statement
373        if (TBaseType.assigned(gcurrentsqlstatement) && ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.sterror))) {
374            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, true, builder);
375        }
376
377        builder.sqlStatements(this.sqlstatements);
378        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
379                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
380        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
381    }
382
383    private void performRawStatementTokenTransformations(TSourceToken ast) {
384        // StarRocks-specific token transformations (MySQL-compatible)
385        if (ast.tokencode == TBaseType.rrw_date) {
386            TSourceToken st1 = ast.nextSolidToken();
387            if (st1 != null) {
388                if (st1.tokencode == '(') {
389                    ast.tokencode = TBaseType.rrw_mysql_date_function;
390                } else if (st1.tokencode == TBaseType.sconst) {
391                    ast.tokencode = TBaseType.rrw_mysql_date_const;
392                }
393            }
394        } else if (ast.tokencode == TBaseType.rrw_time) {
395            TSourceToken st1 = ast.nextSolidToken();
396            if (st1 != null) {
397                if (st1.tokencode == TBaseType.sconst) {
398                    ast.tokencode = TBaseType.rrw_mysql_time_const;
399                }
400            }
401        } else if (ast.tokencode == TBaseType.rrw_timestamp) {
402            TSourceToken st1 = ast.nextSolidToken();
403            if (st1 != null) {
404                if (st1.tokencode == TBaseType.sconst) {
405                    ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
406                }
407            }
408        }
409    }
410
411    private void appendToken(TCustomSqlStatement statement, TSourceToken token) {
412        if (statement == null || token == null) {
413            return;
414        }
415        token.stmt = statement;
416        statement.sourcetokenlist.add(token);
417    }
418
419    @Override
420    public String toString() {
421        return "StarrocksSqlParser{vendor=" + vendor + "}";
422    }
423}