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}