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.TLexerClickhouse;
009import gudusoft.gsqlparser.TParserClickhouse;
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.util.ArrayList;
028import java.util.List;
029import java.util.Stack;
030
031/**
032 * ClickHouse database SQL parser implementation.
033 *
034 * <p>This parser handles ClickHouse-specific SQL syntax including:
035 * <ul>
036 *   <li>Standard SQL DML/DDL (SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER)</li>
037 *   <li>ClickHouse-specific ENGINE clause in CREATE TABLE</li>
038 *   <li>PREWHERE, FINAL, SAMPLE, ARRAY JOIN clauses</li>
039 *   <li>FORMAT and SETTINGS clauses</li>
040 *   <li>ClickHouse data types (Array, Tuple, Map, Nullable, LowCardinality, etc.)</li>
041 * </ul>
042 *
043 * <p><b>Design Notes:</b>
044 * <ul>
045 *   <li>Extends {@link AbstractSqlParser}</li>
046 *   <li>Based on MySQL grammar (ClickHouse shares backtick quoting, # comments, ENGINE clause)</li>
047 *   <li>No stored procedure support (ClickHouse has no stored procedures)</li>
048 *   <li>Delimiter character: ';' for SQL statements</li>
049 * </ul>
050 *
051 * @see AbstractSqlParser
052 * @see TLexerClickhouse
053 * @see TParserClickhouse
054 * @since 3.2.0.0
055 */
056public class ClickhouseSqlParser extends AbstractSqlParser {
057
058    // ========== Lexer and Parser Instances ==========
059
060    /** The ClickHouse lexer used for tokenization (public for TGSqlParser.getFlexer()) */
061    public TLexerClickhouse flexer;
062    private TParserClickhouse fparser;
063
064    // ========== Constructor ==========
065
066    /**
067     * Construct ClickHouse SQL parser.
068     * <p>
069     * Configures the parser for ClickHouse database with default delimiter: semicolon (;)
070     */
071    public ClickhouseSqlParser() {
072        super(EDbVendor.dbvclickhouse);
073
074        // Set delimiter character - ClickHouse uses semicolon
075        this.delimiterChar = ';';
076        this.defaultDelimiterStr = ";";
077
078        // Create lexer once - will be reused for all parsing operations
079        this.flexer = new TLexerClickhouse();
080        this.flexer.delimiterchar = this.delimiterChar;
081        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
082
083        // CRITICAL: Set lexer for inherited getanewsourcetoken() method
084        this.lexer = this.flexer;
085
086        // Create parser once - will be reused for all parsing operations
087        this.fparser = new TParserClickhouse(null);
088        this.fparser.lexer = this.flexer;
089    }
090
091    // ========== AbstractSqlParser Abstract Methods Implementation ==========
092
093    @Override
094    protected TCustomLexer getLexer(ParserContext context) {
095        return this.flexer;
096    }
097
098    @Override
099    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
100        this.fparser.sourcetokenlist = tokens;
101        return this.fparser;
102    }
103
104    @Override
105    protected void tokenizeVendorSql() {
106        doclickhousetexttotokenlist();
107    }
108
109    @Override
110    protected void setupVendorParsersForExtraction() {
111        this.fparser.sqlcmds = this.sqlcmds;
112        this.fparser.sourcetokenlist = this.sourcetokenlist;
113    }
114
115    @Override
116    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
117        doclickhousegetrawsqlstatements(builder);
118    }
119
120    @Override
121    protected TStatementList performParsing(ParserContext context,
122                                           TCustomParser parser,
123                                           TCustomParser secondaryParser,
124                                           TSourceTokenList tokens,
125                                           TStatementList rawStatements) {
126        this.sourcetokenlist = tokens;
127        this.parserContext = context;
128        this.sqlstatements = rawStatements;
129
130        // Initialize sqlcmds for the parser
131        this.sqlcmds = SqlCmdsFactory.get(vendor);
132        this.fparser.sqlcmds = this.sqlcmds;
133
134        // Initialize global context for statement parsing
135        initializeGlobalContext();
136
137        // Parse each statement
138        for (int i = 0; i < sqlstatements.size(); i++) {
139            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
140
141            try {
142                stmt.setFrameStack(frameStack);
143                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
144
145                // Handle error recovery for CREATE TABLE statements if enabled
146                boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE;
147                if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) {
148                    handleCreateTableErrorRecovery(stmt);
149                }
150
151                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
152                    copyErrorsFromStatement(stmt);
153                }
154            } catch (Exception ex) {
155                handleStatementParsingException(stmt, i, ex);
156                continue;
157            }
158        }
159
160        // Clean up frame stack
161        if (globalFrame != null) {
162            globalFrame.popMeFromStack(frameStack);
163        }
164
165        return this.sqlstatements;
166    }
167
168    /**
169     * Handle error recovery for CREATE TABLE statements.
170     */
171    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
172        if ((stmt.sqlstatementtype != ESqlStatementType.sstcreatetable) || TBaseType.c_createTableStrictParsing) {
173            return;
174        }
175
176        int nested = 0;
177        boolean isIgnore = false, isFoundIgnoreToken = false;
178        TSourceToken firstIgnoreToken = null;
179
180        for (int k = 0; k < stmt.sourcetokenlist.size(); k++) {
181            TSourceToken st = stmt.sourcetokenlist.get(k);
182            if (isIgnore) {
183                if (st.issolidtoken() && (st.tokencode != ';')) {
184                    isFoundIgnoreToken = true;
185                    if (firstIgnoreToken == null) {
186                        firstIgnoreToken = st;
187                    }
188                }
189                if (st.tokencode != ';') {
190                    st.tokencode = TBaseType.sqlpluscmd;
191                }
192                continue;
193            }
194            if (st.tokencode == (int) ')') {
195                nested--;
196                if (nested == 0) {
197                    boolean isSelect = false;
198                    TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1);
199                    if (st1 != null) {
200                        TSourceToken st2 = st.searchToken((int) '(', 2);
201                        if (st2 != null) {
202                            TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3);
203                            isSelect = (st3 != null);
204                        }
205                    }
206                    if (!isSelect) isIgnore = true;
207                }
208            } else if (st.tokencode == (int) '(') {
209                nested++;
210            }
211        }
212
213        if (isFoundIgnoreToken) {
214            stmt.clearError();
215            stmt.parsestatement(null, false, this.parserContext.isOnlyNeedRawParseTree());
216        }
217    }
218
219    // ========== ClickHouse-Specific Tokenization ==========
220
221    /**
222     * Perform ClickHouse-specific tokenization.
223     * <p>
224     * Based on MySQL tokenization but simplified - no DELIMITER command,
225     * no stored procedure handling. Handles ClickHouse-specific token
226     * transformations including :: typecast operator splitting.
227     */
228    private void doclickhousetexttotokenlist() {
229        TSourceToken asourcetoken;
230        int yychar;
231
232        asourcetoken = getanewsourcetoken();
233        if (asourcetoken == null) return;
234        yychar = asourcetoken.tokencode;
235
236        while (yychar > 0) {
237            // Handle :: typecast operator: the lexer produces mysqllabel (identifier:)
238            // followed by bind_v (:name) for "identifier::TypeName". We need to
239            // rewrite as CAST(identifier AS TypeName).
240            if (yychar == TBaseType.mysqllabel) {
241                // Append BEFORE fetching the lookahead: getanewsourcetoken's
242                // whitespace merge reads sourcetokenlist.get(curpos), which is
243                // out of bounds while this token is held back — "SELECT {a: b}"
244                // (label then whitespace) crashed the whole tokenize and the
245                // parser silently returned zero tokens. The bind_v rewrite
246                // removes the token again below.
247                sourcetokenlist.add(asourcetoken);
248                TSourceToken nextToken = getanewsourcetoken();
249                if (nextToken != null && nextToken.tokencode == TBaseType.bind_v) {
250                    sourcetokenlist.remove(sourcetokenlist.size() - 1);
251                    // We have mysqllabel + bind_v pattern = "ident:" + ":TypeName"
252                    // Rewrite as: CAST ( identifier AS TypeName )
253                    String labelText = asourcetoken.toString();
254                    String identText = labelText.substring(0, labelText.length() - 1);
255                    String bindText = nextToken.toString();
256                    String typeText = bindText.substring(1);
257                    int line = (int) asourcetoken.lineNo;
258                    int col = (int) asourcetoken.columnNo;
259
260                    // Check if preceding token is '.' (qualified name like t1.col::Type)
261                    // If so, pull qualified name prefix into the CAST expression
262                    List<TSourceToken> qualifiedPrefix = new ArrayList<>();
263                    int lastIdx = sourcetokenlist.size() - 1;
264                    // Skip trailing whitespace
265                    while (lastIdx >= 0 && (sourcetokenlist.get(lastIdx).tokentype == ETokenType.ttwhitespace
266                            || sourcetokenlist.get(lastIdx).tokentype == ETokenType.ttreturn)) {
267                        lastIdx--;
268                    }
269                    if (lastIdx >= 0 && sourcetokenlist.get(lastIdx).tokentype == ETokenType.ttperiod) {
270                        // Walk back collecting name.name.name. pattern
271                        int prefixEnd = lastIdx;
272                        int idx = lastIdx;
273                        while (idx >= 0) {
274                            TSourceToken t = sourcetokenlist.get(idx);
275                            if (t.tokentype == ETokenType.ttperiod) {
276                                idx--;
277                            } else if (t.tokentype == ETokenType.ttidentifier || t.tokentype == ETokenType.ttkeyword) {
278                                idx--;
279                                // Skip whitespace between identifier and dot
280                                while (idx >= 0 && (sourcetokenlist.get(idx).tokentype == ETokenType.ttwhitespace
281                                        || sourcetokenlist.get(idx).tokentype == ETokenType.ttreturn)) {
282                                    idx--;
283                                }
284                                // Check if next non-ws token is another dot (continue) or stop
285                                if (idx >= 0 && sourcetokenlist.get(idx).tokentype == ETokenType.ttperiod) {
286                                    continue;
287                                } else {
288                                    break;
289                                }
290                            } else {
291                                break;
292                            }
293                        }
294                        int prefixStart = idx + 1;
295                        // Collect prefix tokens and remove from sourcetokenlist
296                        for (int i = prefixStart; i <= prefixEnd; i++) {
297                            qualifiedPrefix.add(sourcetokenlist.get(i));
298                        }
299                        // Remove prefix tokens from end of sourcetokenlist
300                        while (sourcetokenlist.size() > prefixStart) {
301                            sourcetokenlist.remove(sourcetokenlist.size() - 1);
302                        }
303                    }
304
305                    // Emit: CAST (
306                    sourcetokenlist.add(createSyntheticToken("CAST", 750, ETokenType.ttkeyword, line, col));
307                    sourcetokenlist.add(createSyntheticToken("(", 40, ETokenType.ttleftparenthesis, line, col));
308
309                    // Re-emit qualified prefix if any (e.g., "t1.")
310                    for (TSourceToken prefixToken : qualifiedPrefix) {
311                        prefixToken.posinlist = sourcetokenlist.size();
312                        sourcetokenlist.add(prefixToken);
313                    }
314
315                    // Emit: identifier or integer
316                    asourcetoken.setAstext(identText);
317                    if (identText.matches("\\d+")) {
318                        asourcetoken.tokencode = TBaseType.iconst;
319                        asourcetoken.tokentype = ETokenType.ttnumber;
320                    } else {
321                        asourcetoken.tokencode = TBaseType.ident;
322                        asourcetoken.tokentype = ETokenType.ttidentifier;
323                    }
324                    sourcetokenlist.add(asourcetoken);
325
326                    // Emit: AS
327                    sourcetokenlist.add(createSyntheticToken("AS", 341, ETokenType.ttkeyword, line, col));
328
329                    // Emit: TypeName
330                    nextToken.setAstext(typeText);
331                    nextToken.tokencode = TBaseType.ident;
332                    nextToken.tokentype = ETokenType.ttidentifier;
333                    sourcetokenlist.add(nextToken);
334
335                    // Check for parameterized types: TypeName(params)
336                    emitTypeParamsAndClose(line, col);
337
338                    asourcetoken = getanewsourcetoken();
339                    if (asourcetoken == null) break;
340                    yychar = asourcetoken.tokencode;
341                    continue;
342                } else {
343                    // Not followed by bind_v; the mysqllabel is already in the
344                    // list, continue with the lookahead token.
345                    if (nextToken == null) break;
346                    asourcetoken = nextToken;
347                    yychar = asourcetoken.tokencode;
348                    continue;
349                }
350            }
351
352            // Handle standalone :: typecast token (e.g., after string literals: '2024-01-15'::Date)
353            if (yychar == TBaseType.typecast) {
354                rewriteStandaloneTypecast(asourcetoken);
355                asourcetoken = getanewsourcetoken();
356                if (asourcetoken == null) break;
357                yychar = asourcetoken.tokencode;
358                continue;
359            }
360
361            if (yychar == TBaseType.bind_v) {
362                // ClickHouse has no :name bind variables; a bind_v that
363                // survived the :: rewrite above is a map-literal colon glued
364                // to its value ({'key1':1}). Split into ':' plus the value.
365                String bindText = asourcetoken.toString();
366                int bLine = (int) asourcetoken.lineNo;
367                int bCol = (int) asourcetoken.columnNo;
368                sourcetokenlist.add(createSyntheticToken(":", ':', ETokenType.ttcolon, bLine, bCol));
369                String valText = bindText.substring(1);
370                boolean allDigits = valText.length() > 0;
371                for (int ci = 0; ci < valText.length(); ci++) {
372                    if (!Character.isDigit(valText.charAt(ci))) { allDigits = false; break; }
373                }
374                if (allDigits) {
375                    sourcetokenlist.add(createSyntheticToken(valText, TBaseType.iconst, ETokenType.ttnumber, bLine, bCol + 1));
376                } else {
377                    sourcetokenlist.add(createSyntheticToken(valText, TBaseType.ident, ETokenType.ttidentifier, bLine, bCol + 1));
378                }
379                asourcetoken = getanewsourcetoken();
380                if (asourcetoken == null) break;
381                yychar = asourcetoken.tokencode;
382                continue;
383            }
384
385            // Tuple element access a.2: the lexer greedily reads ".2" as a
386            // float constant. When a float starting with '.' directly follows
387            // an identifier or a closing bracket, split it into a dedicated
388            // tuple-access dot token plus an integer constant. The dot gets
389            // its own token (CH_TUPLE_DOT) rather than '.' so the grammar
390            // rule cannot collide with qualified names like left.col.
391            if ((yychar == TBaseType.fconst) && asourcetoken.toString().startsWith(".")
392                    && asourcetoken.toString().indexOf('.', 1) < 0) {
393                TSourceToken lastSolid = null;
394                for (int li = sourcetokenlist.size() - 1; li >= 0; li--) {
395                    if (sourcetokenlist.get(li).issolidtoken()) { lastSolid = sourcetokenlist.get(li); break; }
396                }
397                if ((lastSolid != null)
398                        && ((lastSolid.tokentype == ETokenType.ttidentifier)
399                            || (lastSolid.tokencode == ')') || (lastSolid.tokencode == ']'))) {
400                    int line = (int) asourcetoken.lineNo;
401                    int col = (int) asourcetoken.columnNo;
402                    sourcetokenlist.add(createSyntheticToken(".", TBaseType.rrw_clickhouse_tuple_dot, ETokenType.ttperiod, line, col));
403                    TSourceToken num = createSyntheticToken(asourcetoken.toString().substring(1), TBaseType.iconst, ETokenType.ttnumber, line, col + 1);
404                    sourcetokenlist.add(num);
405                    asourcetoken = getanewsourcetoken();
406                    if (asourcetoken == null) break;
407                    yychar = asourcetoken.tokencode;
408                    continue;
409                }
410            }
411
412            sourcetokenlist.add(asourcetoken);
413
414            asourcetoken = getanewsourcetoken();
415            if (asourcetoken == null) break;
416
417            yychar = asourcetoken.tokencode;
418        }
419
420        clickhousePostTokenAdjust();
421    }
422
423    /**
424     * Post-tokenize adjustments that need lookahead/lookbehind over the whole
425     * list. Three rewrites, all ClickHouse-specific:
426     *
427     * 1. IN whose next solid token is PARTITION becomes RW_IN_BEFORE_PARTITION.
428     *    ALTER TABLE t UPDATE a = 1 IN PARTITION p WHERE ... is LALR(1)-blocked
429     *    with plain IN: "a = 1 IN (...)" is a legal value expression, so the
430     *    parser must shift IN into the expression and then dies at PARTITION.
431     *    "x IN PARTITION" is never a legal expression, so the rewrite is safe
432     *    globally; the grammar accepts both spellings via ch_in_before_partition.
433     *
434     * 2. FROM/TO demoted to identifiers where the keyword reading is impossible:
435     *    after '.' (g.from), or when the next solid token is ',', ';' or ')'
436     *    (SELECT from, to, label ... ORDER BY from). ClickHouse itself accepts
437     *    these as column names (WITH RECURSIVE documentation examples).
438     *
439     * 3. INSERT INTO t FORMAT <name> followed by inline data rows: everything
440     *    after the format name up to the statement's ';' is raw data in the
441     *    named format, not SQL. Those tokens are turned into sqlpluscmd, which
442     *    TCustomParser already skips (same mechanism as
443     *    handleCreateTableErrorRecovery above). Skipped when the tail starts
444     *    with SELECT/WITH so a query source is never eaten.
445     */
446    private void clickhousePostTokenAdjust() {
447        // Token-list surgery above (the :: CAST rewrite and the bind/float
448        // splits) inserts or removes tokens, but the lexer stamps posinlist
449        // from its own running count, so every token after the first edit
450        // carries a stale position. The statement splitter walks tokens BY
451        // posinlist (nextsolidtoken), so a stale value makes it read the
452        // wrong token and mistype the NEXT statement as sstinvalid — this
453        // predates the splits: SELECT a::UInt32; CREATE TABLE ... already
454        // mistyped the CREATE. Renumber once so positions match reality.
455        for (int ri = 0; ri < sourcetokenlist.size(); ri++) {
456            sourcetokenlist.get(ri).posinlist = ri;
457        }
458
459        TSourceToken prevSolid = null;
460        for (int i = 0; i < sourcetokenlist.size(); i++) {
461            TSourceToken st = sourcetokenlist.get(i);
462            if (!st.issolidtoken()) continue;
463
464            if (st.tokencode == TBaseType.rrw_clickhouse_in) {
465                TSourceToken nx = nextSolidToken(i);
466                if ((nx != null) && (nx.tokencode == TBaseType.rrw_clickhouse_partition)) {
467                    st.tokencode = TBaseType.rrw_clickhouse_in_before_partition;
468                }
469            } else if ((st.tokencode == TBaseType.rrw_from) || (st.tokencode == TBaseType.rrw_to)) {
470                TSourceToken nx = nextSolidToken(i);
471                boolean afterDot = (prevSolid != null) && (prevSolid.tokencode == '.');
472                boolean beforeCloser = (nx != null)
473                        && ((nx.tokencode == ',') || (nx.tokencode == ';') || (nx.tokencode == ')'));
474                if (afterDot || beforeCloser) {
475                    st.tokencode = TBaseType.ident;
476                    st.tokentype = ETokenType.ttidentifier;
477                }
478            } else if (st.tokencode == TBaseType.rrw_clickhouse_at) {
479                // AT is only a keyword in "AT TIME ZONE" / "AT LOCAL"; anywhere
480                // else it stays a plain identifier so aliases and columns named
481                // "at" keep parsing exactly as before AT was tokenized.
482                TSourceToken nx = nextSolidToken(i);
483                if ((nx == null) || ((nx.tokencode != TBaseType.rrw_clickhouse_time)
484                        && (nx.tokencode != TBaseType.rrw_clickhouse_local))) {
485                    st.tokencode = TBaseType.ident;
486                    st.tokentype = ETokenType.ttidentifier;
487                }
488            } else if (st.tokencode == TBaseType.rrw_clickhouse_zone) {
489                // ZONE is only a keyword directly after TIME.
490                if ((prevSolid == null) || (prevSolid.tokencode != TBaseType.rrw_clickhouse_time)) {
491                    st.tokencode = TBaseType.ident;
492                    st.tokentype = ETokenType.ttidentifier;
493                }
494            } else if (st.tokencode == '.') {
495                TSourceToken nx = nextSolidToken(i);
496                boolean prevOk = (prevSolid != null)
497                        && ((prevSolid.tokentype == ETokenType.ttidentifier)
498                            || (prevSolid.tokencode == ')') || (prevSolid.tokencode == ']'));
499                if (prevOk && (nx != null) && (nx.tokencode == TBaseType.iconst)) {
500                    st.tokencode = TBaseType.rrw_clickhouse_tuple_dot;
501                }
502            } else if (st.tokencode == TBaseType.rrw_insert) {
503                rewriteInsertFormatRawData(i);
504            } else if ((st.tokencode == TBaseType.ident) && st.toString().equalsIgnoreCase("PARALLEL")) {
505                // stmt1 PARALLEL WITH stmt2: an execution combinator GSP does not
506                // model. Rewriting PARALLEL into a statement separator (and WITH
507                // into an ignored token) lets both component statements parse
508                // with full ASTs; the concurrency annotation itself is dropped.
509                // Guarded on the token after WITH being a statement-starting
510                // keyword, so ORDER BY parallel WITH FILL, LIMIT parallel WITH
511                // TIES and GROUP BY parallel WITH TOTALS stay untouched.
512                TSourceToken w = nextSolidToken(i);
513                if ((w != null) && (w.tokencode == TBaseType.rrw_with)) {
514                    TSourceToken head = nextSolidToken(w.posinlist);
515                    if ((head != null) && isClickhouseStatementStart(head.tokencode)) {
516                        // Text is rewritten too: a statement's reconstructed text
517                        // includes its trailing separator, and a re-parse of
518                        // "DROP TABLE t1 PARALLEL" (trailing keyword, no WITH to
519                        // guard on) would fail. The combinator is dropped from
520                        // the token text exactly where it is dropped from the AST.
521                        st.setString(";");
522                        st.tokencode = ';';
523                        st.tokentype = ETokenType.ttsemicolon;
524                        w.setString(";");
525                        w.tokencode = ';';
526                        w.tokentype = ETokenType.ttsemicolon;
527                    }
528                }
529            }
530
531            prevSolid = st;
532        }
533    }
534
535    private boolean isClickhouseStatementStart(int tokencode) {
536        switch (tokencode) {
537            case TBaseType.rrw_select:
538            case TBaseType.rrw_insert:
539            case TBaseType.rrw_delete:
540            case TBaseType.rrw_create:
541            case TBaseType.rrw_drop:
542            case TBaseType.rrw_truncate:
543            case 304:  // UPDATE
544            case 426:  // ALTER
545            case 476:  // RENAME
546            case 504:  // OPTIMIZE
547            case 1181: // DETACH
548            case 1182: // ATTACH
549                return true;
550            default:
551                return false;
552        }
553    }
554
555    private TSourceToken nextSolidToken(int from) {
556        for (int j = from + 1; j < sourcetokenlist.size(); j++) {
557            TSourceToken t = sourcetokenlist.get(j);
558            if (t.issolidtoken()) return t;
559        }
560        return null;
561    }
562
563    private void rewriteInsertFormatRawData(int insertPos) {
564        int formatPos = -1;
565        for (int j = insertPos + 1; j < sourcetokenlist.size(); j++) {
566            TSourceToken t = sourcetokenlist.get(j);
567            if (!t.issolidtoken()) continue;
568            if (t.tokencode == ';') return;
569            if (t.tokencode == TBaseType.rrw_select) return; // INSERT ... SELECT: its FORMAT belongs to the query
570            if (t.tokencode == TBaseType.rrw_clickhouse_format) { formatPos = j; break; }
571        }
572        if (formatPos < 0) return;
573        int namePos = -1;
574        for (int j = formatPos + 1; j < sourcetokenlist.size(); j++) {
575            TSourceToken t = sourcetokenlist.get(j);
576            if (!t.issolidtoken()) continue;
577            if (t.tokencode == ';') return;
578            namePos = j;
579            break;
580        }
581        if (namePos < 0) return;
582        TSourceToken first = nextSolidToken(namePos);
583        if (first == null) return;
584        if ((first.tokencode == ';') || (first.tokencode == TBaseType.rrw_select)) return;
585        for (int j = namePos + 1; j < sourcetokenlist.size(); j++) {
586            TSourceToken t = sourcetokenlist.get(j);
587            if (t.tokencode == ';') break;
588            if (t.issolidtoken()) {
589                t.tokencode = TBaseType.sqlpluscmd;
590            }
591        }
592    }
593
594    /**
595     * Check if next token is '(' for parameterized types and emit matching tokens,
596     * then close with ')' for CAST.
597     */
598    private void emitTypeParamsAndClose(int line, int col) {
599        TSourceToken peekToken = getanewsourcetoken();
600        if (peekToken != null && peekToken.tokencode == 40) { // '('
601            sourcetokenlist.add(peekToken);
602            int depth = 1;
603            while (depth > 0) {
604                TSourceToken innerToken = getanewsourcetoken();
605                if (innerToken == null) break;
606                sourcetokenlist.add(innerToken);
607                if (innerToken.tokencode == 40) depth++;
608                else if (innerToken.tokencode == 41) depth--;
609            }
610            sourcetokenlist.add(createSyntheticToken(")", 41, ETokenType.ttrightparenthesis, line, col));
611        } else {
612            sourcetokenlist.add(createSyntheticToken(")", 41, ETokenType.ttrightparenthesis, line, col));
613            // The peeked token must be put back into the stream;
614            // since we can't push back, check if it needs further processing
615            if (peekToken != null) {
616                // Check if the peeked token is also a typecast (chained casts like x::A::B)
617                if (peekToken.tokencode == TBaseType.typecast) {
618                    rewriteStandaloneTypecast(peekToken);
619                } else {
620                    sourcetokenlist.add(peekToken);
621                }
622            }
623        }
624    }
625
626    /**
627     * Create a synthetic token for CAST() rewriting.
628     */
629    private TSourceToken createSyntheticToken(String text, int tokencode, ETokenType tokentype, int lineNo, int columnNo) {
630        TSourceToken token = new TSourceToken(text);
631        token.tokencode = tokencode;
632        token.tokentype = tokentype;
633        token.tokenstatus = ETokenStatus.tsoriginal;
634        token.lineNo = lineNo;
635        token.columnNo = columnNo;
636        token.container = sourcetokenlist;
637        token.posinlist = sourcetokenlist.size();
638        return token;
639    }
640
641    /**
642     * Handle standalone :: typecast (lexer correctly tokenized :: as typecast,
643     * e.g., after string literals or closing parentheses).
644     * The expression before :: is already in sourcetokenlist.
645     * Rewrites: ... expr :: TypeName ... → ... CAST( expr AS TypeName ) ...
646     */
647    private void rewriteStandaloneTypecast(TSourceToken typecastToken) {
648        int line = (int) typecastToken.lineNo;
649        int col = (int) typecastToken.columnNo;
650
651        // Find the preceding expression's last solid token
652        int exprEndIdx = sourcetokenlist.size() - 1;
653        while (exprEndIdx >= 0) {
654            TSourceToken t = sourcetokenlist.get(exprEndIdx);
655            if (t.tokentype != ETokenType.ttwhitespace && t.tokentype != ETokenType.ttreturn
656                && t.tokentype != ETokenType.ttsimplecomment && t.tokentype != ETokenType.ttbracketedcomment) {
657                break;
658            }
659            exprEndIdx--;
660        }
661        if (exprEndIdx < 0) return;
662
663        // Find expression start
664        int exprStartIdx = exprEndIdx;
665        TSourceToken exprEndToken = sourcetokenlist.get(exprEndIdx);
666
667        if (exprEndToken.tokentype == ETokenType.ttrightparenthesis) {
668            // Walk back to find matching '('
669            int depth = 1;
670            int idx = exprEndIdx - 1;
671            while (idx >= 0 && depth > 0) {
672                TSourceToken t = sourcetokenlist.get(idx);
673                if (t.tokentype == ETokenType.ttrightparenthesis) depth++;
674                else if (t.tokentype == ETokenType.ttleftparenthesis) depth--;
675                idx--;
676            }
677            exprStartIdx = idx + 1; // idx+1 because we decremented one extra
678            // Check for function name before '('
679            if (exprStartIdx > 0) {
680                int fnIdx = exprStartIdx - 1;
681                while (fnIdx >= 0) {
682                    TSourceToken t = sourcetokenlist.get(fnIdx);
683                    if (t.tokentype != ETokenType.ttwhitespace && t.tokentype != ETokenType.ttreturn) {
684                        break;
685                    }
686                    fnIdx--;
687                }
688                if (fnIdx >= 0) {
689                    TSourceToken fnToken = sourcetokenlist.get(fnIdx);
690                    if (fnToken.tokentype == ETokenType.ttidentifier || fnToken.tokentype == ETokenType.ttkeyword) {
691                        exprStartIdx = fnIdx;
692                    }
693                }
694            }
695        }
696
697        // Collect expression tokens and whitespace
698        List<TSourceToken> beforeExpr = new ArrayList<>();
699        for (int i = 0; i < exprStartIdx; i++) {
700            beforeExpr.add(sourcetokenlist.get(i));
701        }
702        // Collect whitespace just before expression that should go before CAST
703        List<TSourceToken> wsBeforeExpr = new ArrayList<>();
704        while (!beforeExpr.isEmpty()) {
705            TSourceToken last = beforeExpr.get(beforeExpr.size() - 1);
706            if (last.tokentype == ETokenType.ttwhitespace || last.tokentype == ETokenType.ttreturn) {
707                wsBeforeExpr.add(0, beforeExpr.remove(beforeExpr.size() - 1));
708            } else {
709                break;
710            }
711        }
712
713        List<TSourceToken> exprTokens = new ArrayList<>();
714        for (int i = exprStartIdx; i <= exprEndIdx; i++) {
715            exprTokens.add(sourcetokenlist.get(i));
716        }
717        List<TSourceToken> afterExpr = new ArrayList<>();
718        for (int i = exprEndIdx + 1; i < sourcetokenlist.size(); i++) {
719            afterExpr.add(sourcetokenlist.get(i));
720        }
721
722        // Clear and rebuild sourcetokenlist
723        sourcetokenlist.clear();
724        sourcetokenlist.curpos = -1;
725
726        // Re-add tokens before expression (without trailing whitespace)
727        for (TSourceToken t : beforeExpr) {
728            t.posinlist = sourcetokenlist.size();
729            sourcetokenlist.add(t);
730        }
731
732        // Add whitespace before CAST
733        for (TSourceToken t : wsBeforeExpr) {
734            t.posinlist = sourcetokenlist.size();
735            sourcetokenlist.add(t);
736        }
737
738        // Emit CAST(
739        sourcetokenlist.add(createSyntheticToken("CAST", 750, ETokenType.ttkeyword, line, col));
740        sourcetokenlist.add(createSyntheticToken("(", 40, ETokenType.ttleftparenthesis, line, col));
741
742        // Re-add expression tokens
743        for (TSourceToken t : exprTokens) {
744            t.posinlist = sourcetokenlist.size();
745            sourcetokenlist.add(t);
746        }
747
748        // Emit AS
749        sourcetokenlist.add(createSyntheticToken("AS", 341, ETokenType.ttkeyword, line, col));
750
751        // Read type name and emit closing paren
752        TSourceToken typeToken = getanewsourcetoken();
753        if (typeToken == null) {
754            sourcetokenlist.add(createSyntheticToken(")", 41, ETokenType.ttrightparenthesis, line, col));
755            return;
756        }
757
758        // Skip whitespace
759        while (typeToken != null && (typeToken.tokentype == ETokenType.ttwhitespace || typeToken.tokentype == ETokenType.ttreturn)) {
760            typeToken = getanewsourcetoken();
761        }
762        if (typeToken == null) {
763            sourcetokenlist.add(createSyntheticToken(")", 41, ETokenType.ttrightparenthesis, line, col));
764            return;
765        }
766
767        sourcetokenlist.add(typeToken);
768
769        // Check for parameterized types and close
770        emitTypeParamsAndClose(line, col);
771    }
772
773    /**
774     * Get previous non-whitespace token.
775     */
776    private TSourceToken getprevsolidtoken(TSourceToken ptoken) {
777        TSourceToken ret = null;
778        TSourceTokenList lctokenlist = ptoken.container;
779
780        if (lctokenlist != null) {
781            if ((ptoken.posinlist > 0) && (lctokenlist.size() > ptoken.posinlist - 1)) {
782                if (!(
783                        (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttwhitespace)
784                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttreturn)
785                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttsimplecomment)
786                        || (lctokenlist.get(ptoken.posinlist - 1).tokentype == ETokenType.ttbracketedcomment)
787                )) {
788                    ret = lctokenlist.get(ptoken.posinlist - 1);
789                } else {
790                    ret = lctokenlist.nextsolidtoken(ptoken.posinlist - 1, -1, false);
791                }
792            }
793        }
794        return ret;
795    }
796
797    // ========== ClickHouse-Specific Raw Statement Extraction ==========
798
799    /**
800     * Extract raw ClickHouse SQL statements from tokenized source.
801     * <p>
802     * Simplified from MySQL - no stored procedure states,
803     * no DELIMITER command, no custom delimiters.
804     * ClickHouse uses semicolon as the only delimiter.
805     */
806    private void doclickhousegetrawsqlstatements(SqlParseResult.Builder builder) {
807        if (TBaseType.assigned(sqlstatements)) sqlstatements.clear();
808        if (!TBaseType.assigned(sourcetokenlist)) {
809            builder.sqlStatements(this.sqlstatements);
810            builder.errorCode(1);
811            builder.errorMessage("No source token list available");
812            return;
813        }
814
815        TCustomSqlStatement gcurrentsqlstatement = null;
816        EFindSqlStateType gst = EFindSqlStateType.stnormal;
817
818        for (int i = 0; i < sourcetokenlist.size(); i++) {
819            TSourceToken ast = sourcetokenlist.get(i);
820            sourcetokenlist.curpos = i;
821
822            // Token transformations during raw statement extraction
823            performRawStatementTokenTransformations(ast);
824
825            switch (gst) {
826                case sterror: {
827                    if (ast.tokentype == ETokenType.ttsemicolon) {
828                        appendToken(gcurrentsqlstatement, ast);
829                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
830                        gst = EFindSqlStateType.stnormal;
831                    } else {
832                        appendToken(gcurrentsqlstatement, ast);
833                    }
834                    break;
835                }
836
837                case stnormal: {
838                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
839                            || (ast.tokencode == TBaseType.cmtslashstar)
840                            || (ast.tokencode == TBaseType.lexspace)
841                            || (ast.tokencode == TBaseType.lexnewline)
842                            || (ast.tokentype == ETokenType.ttsemicolon)) {
843                        if (gcurrentsqlstatement != null) {
844                            appendToken(gcurrentsqlstatement, ast);
845                        }
846                        continue;
847                    }
848
849                    // Find a token to start sql mode
850                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
851
852                    if (gcurrentsqlstatement != null) {
853                        gst = EFindSqlStateType.stsql;
854                        appendToken(gcurrentsqlstatement, ast);
855                    } else {
856                        // Error token found
857                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo,
858                                (ast.columnNo < 0 ? 0 : ast.columnNo),
859                                "Error when tokenize", EErrorType.spwarning,
860                                TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
861
862                        ast.tokentype = ETokenType.tttokenlizererrortoken;
863                        gst = EFindSqlStateType.sterror;
864
865                        gcurrentsqlstatement = new TUnknownSqlStatement(vendor);
866                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
867                        appendToken(gcurrentsqlstatement, ast);
868                    }
869
870                    break;
871                }
872
873                case stsql: {
874                    if (ast.tokentype == ETokenType.ttsemicolon) {
875                        gst = EFindSqlStateType.stnormal;
876                        appendToken(gcurrentsqlstatement, ast);
877                        gcurrentsqlstatement.semicolonended = ast;
878                        onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
879                        continue;
880                    }
881
882                    if (ast.tokencode == TBaseType.cmtdoublehyphen) {
883                        if (ast.toString().trim().endsWith(TBaseType.sqlflow_stmt_delimiter_str)) {
884                            gst = EFindSqlStateType.stnormal;
885                            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, false, builder);
886                            continue;
887                        }
888                    }
889
890                    appendToken(gcurrentsqlstatement, ast);
891                    break;
892                }
893
894                default:
895                    break;
896            }
897        }
898
899        // Last statement
900        if ((gcurrentsqlstatement != null) &&
901                ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.sterror))) {
902            onRawStatementComplete(this.parserContext, gcurrentsqlstatement, this.fparser, null, this.sqlstatements, true, builder);
903        }
904
905        // Populate builder with results
906        builder.sqlStatements(this.sqlstatements);
907        builder.syntaxErrors(syntaxErrors instanceof ArrayList ?
908                (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors));
909        builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size());
910    }
911
912    /**
913     * Handle token transformations during raw statement extraction.
914     * <p>
915     * Handles MySQL-inherited token transformations for DATE, TIME, TIMESTAMP keywords
916     * and other context-dependent token adjustments.
917     */
918    private void performRawStatementTokenTransformations(TSourceToken ast) {
919        if (ast.tokencode == TBaseType.rrw_date) {
920            TSourceToken st1 = ast.nextSolidToken();
921            if (st1 != null) {
922                if (st1.tokencode == '(') {
923                    ast.tokencode = TBaseType.rrw_mysql_date_function;
924                } else if (st1.tokencode == TBaseType.sconst) {
925                    ast.tokencode = TBaseType.rrw_mysql_date_const;
926                }
927            }
928        } else if (ast.tokencode == TBaseType.rrw_time) {
929            TSourceToken st1 = ast.nextSolidToken();
930            if (st1 != null) {
931                if (st1.tokencode == TBaseType.sconst) {
932                    ast.tokencode = TBaseType.rrw_mysql_time_const;
933                }
934            }
935        } else if (ast.tokencode == TBaseType.rrw_timestamp) {
936            TSourceToken st1 = ast.nextSolidToken();
937            if (st1 != null) {
938                if (st1.tokencode == TBaseType.sconst) {
939                    ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
940                } else if (st1.tokencode == TBaseType.ident) {
941                    if (st1.toString().startsWith("\"")) {
942                        ast.tokencode = TBaseType.rrw_mysql_timestamp_constant;
943                        st1.tokencode = TBaseType.sconst;
944                    }
945                }
946            }
947        } else if (ast.tokencode == TBaseType.rrw_mysql_position) {
948            TSourceToken st1 = ast.nextSolidToken();
949            if (st1 != null) {
950                if (st1.tokencode != '(') {
951                    ast.tokencode = TBaseType.ident;
952                }
953            }
954        } else if (ast.tokencode == TBaseType.rrw_interval) {
955            TSourceToken leftParen = ast.searchToken('(', 1);
956            if (leftParen != null) {
957                int k = leftParen.posinlist + 1;
958                int nested = 1;
959                boolean commaToken = false;
960                while (k < ast.container.size()) {
961                    if (ast.container.get(k).tokencode == '(') {
962                        nested++;
963                    }
964                    if (ast.container.get(k).tokencode == ')') {
965                        nested--;
966                        if (nested == 0) break;
967                    }
968                    if ((ast.container.get(k).tokencode == ',') && (nested == 1)) {
969                        commaToken = true;
970                        break;
971                    }
972                    k++;
973                }
974                if (commaToken) {
975                    ast.tokencode = TBaseType.rrw_mysql_interval_func;
976                }
977            }
978        }
979    }
980
981    private void appendToken(TCustomSqlStatement statement, TSourceToken token) {
982        if (statement == null || token == null) {
983            return;
984        }
985        token.stmt = statement;
986        statement.sourcetokenlist.add(token);
987    }
988
989    @Override
990    public String toString() {
991        return "ClickhouseSqlParser{vendor=" + vendor + "}";
992    }
993}