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