001package gudusoft.gsqlparser.parser; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.EFindSqlStateType; 005import gudusoft.gsqlparser.EErrorType; 006import gudusoft.gsqlparser.ESqlStatementType; 007import gudusoft.gsqlparser.ETokenStatus; 008import gudusoft.gsqlparser.ETokenType; 009import gudusoft.gsqlparser.TBaseType; 010import gudusoft.gsqlparser.TCustomLexer; 011import gudusoft.gsqlparser.TCustomParser; 012import gudusoft.gsqlparser.TCustomSqlStatement; 013import gudusoft.gsqlparser.TLexerDatabricks; 014import gudusoft.gsqlparser.TParserDatabricks; 015import gudusoft.gsqlparser.TSourceToken; 016import gudusoft.gsqlparser.TSourceTokenList; 017import gudusoft.gsqlparser.TStatementList; 018import gudusoft.gsqlparser.TSyntaxError; 019import gudusoft.gsqlparser.TLog; 020import gudusoft.gsqlparser.stmt.oracle.TSqlplusCmdStatement; 021import gudusoft.gsqlparser.stmt.TUnknownSqlStatement; 022import gudusoft.gsqlparser.sqlcmds.ISqlCmds; 023import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory; 024import gudusoft.gsqlparser.compiler.TContext; 025import gudusoft.gsqlparser.compiler.TGlobalScope; 026import gudusoft.gsqlparser.sqlenv.TSQLEnv; 027import gudusoft.gsqlparser.compiler.TFrame; 028import gudusoft.gsqlparser.resolver.TSQLResolver; 029import gudusoft.gsqlparser.compiler.TASTEvaluator; 030import gudusoft.gsqlparser.nodes.TTypeName; 031import gudusoft.gsqlparser.EDataType; 032 033import java.util.Stack; 034import java.util.ArrayList; 035 036/** 037 * Databricks SQL parser implementation. 038 * 039 * <p>This parser handles Databricks-specific SQL syntax including: 040 * <ul> 041 * <li>Databricks SQL dialect and extensions</li> 042 * <li>Databricks PL/SQL blocks</li> 043 * <li>Special handling for VALUES keyword in INSERT statements</li> 044 * <li>Datatype casting with literals (e.g., DATE '2021-2-1')</li> 045 * </ul> 046 * 047 * <p><b>Implementation Status:</b> MIGRATED 048 * <ul> 049 * <li><b>Phase:</b> Complete migration from delegation to full AbstractSqlParser implementation</li> 050 * <li><b>Current:</b> Self-contained Databricks parser using AbstractSqlParser template</li> 051 * <li><b>Goal:</b> No delegation to legacy TGSqlParser</li> 052 * </ul> 053 * 054 * @see SqlParser 055 * @see AbstractSqlParser 056 * @see TLexerDatabricks 057 * @see TParserDatabricks 058 * @since 3.2.0.0 059 */ 060public class DatabricksSqlParser extends AbstractSqlParser { 061 062 /** 063 * Construct Databricks SQL parser. 064 * <p> 065 * Configures the parser for Databricks database with default delimiter: semicolon (;) 066 * <p> 067 * Following the original TGSqlParser pattern, the lexer and parser are 068 * created once in the constructor and reused for all parsing operations. 069 */ 070 public DatabricksSqlParser() { 071 super(EDbVendor.dbvdatabricks); 072 this.delimiterChar = ';'; 073 this.defaultDelimiterStr = ";"; 074 075 // Create lexer once - will be reused for all parsing operations 076 this.flexer = new TLexerDatabricks(); 077 this.flexer.delimiterchar = this.delimiterChar; 078 this.flexer.defaultDelimiterStr = this.defaultDelimiterStr; 079 080 // Set parent's lexer reference for shared tokenization logic 081 this.lexer = this.flexer; 082 083 // Create parser once - will be reused for all parsing operations 084 this.fparser = new TParserDatabricks(null); 085 this.fparser.lexer = this.flexer; 086 } 087 088 // ========== Tokenization State (used during tokenization) ========== 089 090 /** The Databricks lexer used for tokenization */ 091 public TLexerDatabricks flexer; 092 093 // ========== Statement Parsing State (used during statement parsing) ========== 094 095 /** Current statement being built */ 096 private TCustomSqlStatement gcurrentsqlstatement; 097 098 /** SQL parser (for Databricks SQL statements) */ 099 private TParserDatabricks fparser; 100 101 // ========== AbstractSqlParser Abstract Methods Implementation ========== 102 103 /** 104 * Return the Databricks lexer instance. 105 * <p> 106 * The lexer is created once in the constructor and reused for all 107 * parsing operations. 108 * 109 * @param context parser context (not used, lexer already created) 110 * @return the Databricks lexer instance created in constructor 111 */ 112 @Override 113 protected TCustomLexer getLexer(ParserContext context) { 114 return this.flexer; 115 } 116 117 /** 118 * Return the Databricks SQL parser instance with updated token list. 119 * <p> 120 * The parser is created once in the constructor and reused for all 121 * parsing operations. 122 * 123 * @param context parser context (not used, parser already created) 124 * @param tokens source token list to parse 125 * @return the Databricks SQL parser instance created in constructor 126 */ 127 @Override 128 protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) { 129 this.fparser.sourcetokenlist = tokens; 130 return this.fparser; 131 } 132 133 /** 134 * Databricks uses a single parser, no secondary parser needed. 135 * 136 * @param context parser context 137 * @param tokens source token list 138 * @return null (no secondary parser) 139 */ 140 @Override 141 protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) { 142 return null; 143 } 144 145 /** 146 * Hook method: Tokenize Databricks SQL by calling vendor-specific tokenization. 147 */ 148 @Override 149 protected void tokenizeVendorSql() { 150 dodatabrickstexttotokenlist(); 151 } 152 153 /** 154 * Hook method: Setup parsers for raw statement extraction. 155 * Inject sqlcmds and sourcetokenlist into parser. 156 */ 157 @Override 158 protected void setupVendorParsersForExtraction() { 159 this.fparser.sqlcmds = this.sqlcmds; 160 this.fparser.sourcetokenlist = this.sourcetokenlist; 161 } 162 163 /** 164 * Hook method: Extract raw Databricks SQL statements. 165 * 166 * @param builder the result builder to populate 167 */ 168 @Override 169 protected void extractVendorRawStatements(SqlParseResult.Builder builder) { 170 dodatabricksgetrawsqlstatements(builder); 171 } 172 173 // ========== Databricks-Specific Tokenization Logic ========== 174 175 /** 176 * Tokenize Databricks SQL text to token list. 177 * <p> 178 * This method processes the input SQL text and converts it into a sequence 179 * of tokens. It handles Databricks-specific token processing including 180 * MySQL-style comments and delimiter handling. 181 * <p> 182 * Migrated from TGSqlParser.dodatabrickstexttotokenlist() at line 4696. 183 */ 184 private void dodatabrickstexttotokenlist() { 185 TSourceToken asourcetoken, lcprevst; 186 int yychar; 187 boolean startDelimiter = false; 188 189 flexer.tmpDelimiter = ""; 190 191 asourcetoken = getanewsourcetoken(); 192 if (asourcetoken == null) return; 193 yychar = asourcetoken.tokencode; 194 195 while (yychar > 0) { 196 sourcetokenlist.add(asourcetoken); 197 asourcetoken = getanewsourcetoken(); 198 if (asourcetoken == null) break; 199 checkMySQLCommentToken(asourcetoken); 200 201 if ((asourcetoken.tokencode == TBaseType.lexnewline) && (startDelimiter)) { 202 startDelimiter = false; 203 flexer.tmpDelimiter = sourcetokenlist.get(sourcetokenlist.size() - 1).getAstext(); 204 } 205 206 yychar = asourcetoken.tokencode; 207 } 208 } 209 210 /** 211 * Check for MySQL-style comments in tokens. 212 * <p> 213 * This method is used to handle MySQL comment syntax which is also 214 * supported by Databricks. 215 * 216 * @param asourcetoken the token to check 217 */ 218 private void checkMySQLCommentToken(TSourceToken asourcetoken) { 219 // MySQL comment handling - placeholder for now 220 // The actual implementation would check for MySQL-style comments 221 // This matches the pattern from TGSqlParser 222 } 223 224 // ========== Databricks-Specific Raw Statement Extraction Logic ========== 225 226 /** 227 * Extract raw SQL statements from token list for Databricks. 228 * <p> 229 * This method separates the token list into individual SQL statements 230 * without performing full parsing. It handles Databricks-specific syntax: 231 * <ul> 232 * <li>VALUES keyword disambiguation for INSERT statements</li> 233 * <li>Datatype casting with literals (DATE '2021-2-1')</li> 234 * <li>PL/SQL block detection with BEGIN/END</li> 235 * <li>Statement terminators (semicolons, slash, period)</li> 236 * </ul> 237 * <p> 238 * Migrated from TGSqlParser.dodatabricksgetrawsqlstatements() at line 6944. 239 * 240 * @param builder the result builder to populate with raw statements 241 */ 242 private void dodatabricksgetrawsqlstatements(SqlParseResult.Builder builder) { 243 int waitingEnd = 0; 244 boolean foundEnd = false; 245 EDataType tmpDatatype = null; 246 247 if (TBaseType.assigned(sqlstatements)) sqlstatements.clear(); 248 if (!TBaseType.assigned(sourcetokenlist)) { 249 builder.errorCode(-1); 250 builder.errorMessage("Source token list not assigned"); 251 return; 252 } 253 254 gcurrentsqlstatement = null; 255 EFindSqlStateType gst = EFindSqlStateType.stnormal; 256 TSourceToken lcprevsolidtoken = null, ast = null; 257 258 for (int i = 0; i < sourcetokenlist.size(); i++) { 259 260 if ((ast != null) && (ast.issolidtoken())) 261 lcprevsolidtoken = ast; 262 263 ast = sourcetokenlist.get(i); 264 sourcetokenlist.curpos = i; 265 266 // Databricks-specific token adjustments 267 if (ast.tokencode == TBaseType.rrw_values) { 268 TSourceToken stParen = ast.searchToken('(', 1); 269 if (stParen != null) { 270 TSourceToken stInsert = ast.searchToken(TBaseType.rrw_insert, -ast.posinlist, ';', true); 271 if (stInsert != null) { 272 TSourceToken stSemiColon = ast.searchToken(';', -ast.posinlist); 273 if ((stSemiColon != null) && (stSemiColon.posinlist > stInsert.posinlist)) { 274 // INSERT INTO test values (16,1), (8,2), (4,4), (2,0), (97, 16); 275 // VALUES (1); 276 // don't treat values(1) as insert values 277 } else { 278 TSourceToken stFrom = ast.searchToken(TBaseType.rrw_from, -ast.posinlist, ';', true); 279 if (stFrom != null) { 280 // don't treat values after from keyword as an insert values 281 // insert into inserttest values(10, 20, '40'), (-1, 2, DEFAULT), 282 // ((select 2), (select i from (values(3) ) as foo (i)), 'values are fun!'); 283 284 // let check the INSERT keyword is close to VALUES than FROM keyword, if yes, treat it as insert values 285 if (stInsert.posinlist > stFrom.posinlist) { 286 // https://www.sqlparser.com/bugs/mantisbt/view.php?id=3354 287 ast.tokencode = TBaseType.rrw_databricks_values_insert; 288 } 289 } else { 290 ast.tokencode = TBaseType.rrw_databricks_values_insert; 291 } 292 } 293 } 294 } 295 } else if ((ast.tokencode == TBaseType.sconst) || (ast.tokencode == '+') || (ast.tokencode == '-')) { 296 if ((lcprevsolidtoken != null) && (TTypeName.searchTypeByName(lcprevsolidtoken.toString()) != null)) { 297 // date '2021-2-1', turn date to TBaseType.rrw_databricks_datatype_used_to_cast 298 if (lcprevsolidtoken.tokencode != TBaseType.rrw_interval) { 299 lcprevsolidtoken.tokencode = TBaseType.rrw_databricks_datatype_used_to_cast; 300 } 301 } 302 } else if (ast.tokencode == TBaseType.rrw_databricks_POSITION) { 303 // Databricks POSITION has two forms: 304 // 1. POSITION(substr IN str) -- SQL-standard form 305 // 2. POSITION(substr, str[, pos]) -- comma form 306 // The comma form parses fine as a generic function call, but the IN 307 // form parses as func(<general IN predicate>), whose RHS (in_expr) is 308 // restricted to scalars and cannot be a function/CASE/CAST. Route only 309 // the IN form to the dedicated RW_POSITION_FUNCTION rule, which accepts 310 // a full expression on both sides of IN. Leave the comma form untouched. 311 if (isPositionInForm(ast)) { 312 ast.tokencode = TBaseType.rrw_databricks_POSITION_FUNCTION; 313 } 314 } else if ((ast.tokencode == TBaseType.ident) && ast.toString().equalsIgnoreCase("identifier")) { 315 // Databricks IDENTIFIER(expr) as a DDL/DML write target: 316 // CREATE TABLE IDENTIFIER(:p || '_t') AS ... 317 // INSERT OVERWRITE IDENTIFIER(:cat || '.s.t') SELECT ... 318 // MERGE INTO IDENTIFIER(...) ... 319 // In those positions the grammar expects a qualified_name, so the 320 // generic function-call route used by FROM IDENTIFIER(...) is not 321 // available. Route to the dedicated RW_IDENTIFIER_FUNC rule, but 322 // only when (a) the previous solid token is TABLE/INTO/OVERWRITE/ 323 // EXISTS (write-target context; FROM and expression positions 324 // already parse via the function-call path and stay untouched) 325 // and (b) the argument starts with a parameter marker or string 326 // literal - so a table literally named "identifier" followed by 327 // a column-definition list is not captured. Mantis 4638. 328 if (lcprevsolidtoken != null) { 329 String prevText = lcprevsolidtoken.toString(); 330 // non-identifier-compare: matching SQL keywords (TABLE/INTO/OVERWRITE/EXISTS), not DB object names 331 if (prevText.equalsIgnoreCase("table") || prevText.equalsIgnoreCase("into") 332 || prevText.equalsIgnoreCase("overwrite") || prevText.equalsIgnoreCase("exists")) { 333 TSourceToken openParen = ast.nextSolidToken(); 334 if ((openParen != null) && (openParen.tokencode == (int) '(')) { 335 TSourceToken firstArg = openParen.nextSolidToken(); 336 if ((firstArg != null) 337 && ((firstArg.tokencode == (int) ':') || (firstArg.tokencode == TBaseType.sconst))) { 338 ast.tokencode = TBaseType.rrw_databricks_identifier_func; 339 } 340 } 341 } 342 } 343 } else if (ast.tokencode == TBaseType.rrw_with) { 344 // A plain RW_WITH introducing a schema/metrics clause poisons the 345 // LALR tables, so route it to the synthetic RW_WITH_SCHEMA_OPTION 346 // token when the following solid tokens spell one of: 347 // WITH SCHEMA {BINDING | COMPENSATION | EVOLUTION | TYPE EVOLUTION} 348 // (CREATE VIEW, Mantis 4644 / MERGE, Mantis 4645) 349 // WITH METRICS LANGUAGE ... (metric views, Mantis 4643) 350 // A CTE named "schema" or "metrics" is not captured: its name is 351 // followed by AS or '(', never by BINDING/EVOLUTION/... or LANGUAGE. 352 TSourceToken next1 = ast.nextSolidToken(); 353 TSourceToken next2 = (next1 == null) ? null : next1.nextSolidToken(); 354 if ((next1 != null) && (next2 != null)) { 355 String next1Text = next1.toString(); 356 String next2Text = next2.toString(); 357 // non-identifier-compare: matching SQL keywords of the schema/metrics clause, not DB object names 358 if (next1Text.equalsIgnoreCase("schema") 359 && (next2Text.equalsIgnoreCase("binding") || next2Text.equalsIgnoreCase("compensation") 360 || next2Text.equalsIgnoreCase("evolution"))) { 361 ast.tokencode = TBaseType.rrw_databricks_with_schema_option; 362 } else if (next1Text.equalsIgnoreCase("schema") && next2Text.equalsIgnoreCase("type")) { 363 // WITH SCHEMA TYPE is only valid as WITH SCHEMA TYPE EVOLUTION 364 TSourceToken next3 = next2.nextSolidToken(); 365 // non-identifier-compare: matching SQL keywords of the schema clause, not DB object names 366 if ((next3 != null) && next3.toString().equalsIgnoreCase("evolution")) { 367 ast.tokencode = TBaseType.rrw_databricks_with_schema_option; 368 } 369 } else if (next1Text.equalsIgnoreCase("metrics") && next2Text.equalsIgnoreCase("language")) { 370 // the language name itself is left open (YAML today; new 371 // languages would otherwise need a parser change) 372 ast.tokencode = TBaseType.rrw_databricks_with_schema_option; 373 } 374 } 375 } 376 377 switch (gst) { 378 case sterror: { 379 if (ast.tokentype == ETokenType.ttsemicolon) { 380 appendToken(gcurrentsqlstatement, ast); 381 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 382 gst = EFindSqlStateType.stnormal; 383 } else { 384 appendToken(gcurrentsqlstatement, ast); 385 } 386 break; 387 } //sterror 388 389 case stnormal: { 390 if ((ast.tokencode == TBaseType.cmtdoublehyphen) 391 || (ast.tokencode == TBaseType.cmtslashstar) 392 || (ast.tokencode == TBaseType.lexspace) 393 || (ast.tokencode == TBaseType.lexnewline) 394 || (ast.tokentype == ETokenType.ttsemicolon)) { 395 if (gcurrentsqlstatement != null) { 396 appendToken(gcurrentsqlstatement, ast); 397 } 398 399 if ((lcprevsolidtoken != null) && (ast.tokentype == ETokenType.ttsemicolon)) { 400 if (lcprevsolidtoken.tokentype == ETokenType.ttsemicolon) { 401 // ;;;; continuous semicolon, treat it as comment 402 ast.tokentype = ETokenType.ttsimplecomment; 403 ast.tokencode = TBaseType.cmtdoublehyphen; 404 } 405 } 406 407 continue; 408 } 409 410 // find a tokentext to start sql or plsql mode 411 gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement); 412 413 if (gcurrentsqlstatement != null) { 414 if (gcurrentsqlstatement.isdatabricksplsql()) { 415 gst = EFindSqlStateType.ststoredprocedure; 416 appendToken(gcurrentsqlstatement, ast); 417 foundEnd = false; 418 if ((ast.tokencode == TBaseType.rrw_begin) 419 || (ast.tokencode == TBaseType.rrw_package) 420 || (ast.searchToken(TBaseType.rrw_package, 4) != null)) { 421 waitingEnd = 1; 422 } 423 } else { 424 gst = EFindSqlStateType.stsql; 425 appendToken(gcurrentsqlstatement, ast); 426 } 427 } else { 428 //error tokentext found 429 this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo) 430 , "Error when tokenlize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist)); 431 432 ast.tokentype = ETokenType.tttokenlizererrortoken; 433 gst = EFindSqlStateType.sterror; 434 435 gcurrentsqlstatement = new TUnknownSqlStatement(vendor); 436 gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid; 437 appendToken(gcurrentsqlstatement, ast); 438 } 439 440 break; 441 } // stnormal 442 443 case stsql: { 444 if (ast.tokentype == ETokenType.ttsemicolon) { 445 gst = EFindSqlStateType.stnormal; 446 appendToken(gcurrentsqlstatement, ast); 447 gcurrentsqlstatement.semicolonended = ast; 448 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 449 continue; 450 } 451 452 if (sourcetokenlist.sqlplusaftercurtoken()) { //most probably is / cmd 453 gst = EFindSqlStateType.stnormal; 454 appendToken(gcurrentsqlstatement, ast); 455 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 456 continue; 457 } 458 appendToken(gcurrentsqlstatement, ast); 459 break; 460 }//case stsql 461 462 case ststoredprocedure: { 463 if (ast.tokencode == TBaseType.rrw_begin) { 464 waitingEnd++; 465 } else if (ast.tokencode == TBaseType.rrw_if) { 466 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 467 //this is not if after END 468 waitingEnd++; 469 } 470 } else if (ast.tokencode == TBaseType.rrw_case) { 471 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 472 //this is not case after END 473 waitingEnd++; 474 } 475 } else if (ast.tokencode == TBaseType.rrw_loop) { 476 if (ast.searchToken(TBaseType.rrw_end, -1) == null) { 477 //this is not loop after END 478 waitingEnd++; 479 } 480 } else if (ast.tokencode == TBaseType.rrw_end) { 481 foundEnd = true; 482 waitingEnd--; 483 if (waitingEnd < 0) { 484 waitingEnd = 0; 485 } 486 } 487 488 if ((ast.tokentype == ETokenType.ttslash) && (ast.tokencode == TBaseType.sqlpluscmd)) { 489 // TPlsqlStatementParse(asqlstatement).TerminatorToken := ast; 490 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 491 gst = EFindSqlStateType.stnormal; 492 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 493 494 //make / a sqlplus cmd 495 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 496 appendToken(gcurrentsqlstatement, ast); 497 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 498 } else if ((ast.tokentype == ETokenType.ttperiod) && (sourcetokenlist.returnaftercurtoken(false)) && (sourcetokenlist.returnbeforecurtoken(false))) { 499 // single dot at a separate line 500 ast.tokenstatus = ETokenStatus.tsignorebyyacc; 501 gst = EFindSqlStateType.stnormal; 502 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 503 504 //make ttperiod a sqlplus cmd 505 gcurrentsqlstatement = new TSqlplusCmdStatement(vendor); 506 appendToken(gcurrentsqlstatement, ast); 507 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 508 } else { 509 appendToken(gcurrentsqlstatement, ast); 510 if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnd == 0) 511 && (foundEnd)) { 512 gst = EFindSqlStateType.stnormal; 513 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 514 } 515 } 516 517 if (ast.tokencode == TBaseType.sqlpluscmd) { 518 //change tokencode back to keyword or TBaseType.ident, because sqlplus cmd 519 //in a sql statement(almost is plsql block) is not really a sqlplus cmd 520 int m = flexer.getkeywordvalue(ast.getAstext()); 521 if (m != 0) { 522 ast.tokencode = m; 523 } else { 524 ast.tokencode = TBaseType.ident; 525 } 526 } 527 528 break; 529 } //ststoredprocedure 530 } //switch 531 }//for 532 533 //last statement 534 if ((gcurrentsqlstatement != null) && 535 ((gst == EFindSqlStateType.stsqlplus) || (gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) || 536 (gst == EFindSqlStateType.sterror))) { 537 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder); 538 } 539 540 // Populate builder with results 541 builder.sqlStatements(this.sqlstatements); 542 builder.syntaxErrors(syntaxErrors instanceof ArrayList ? 543 (ArrayList<TSyntaxError>) syntaxErrors : new ArrayList<>(syntaxErrors)); 544 builder.errorCode(syntaxErrors.isEmpty() ? 0 : syntaxErrors.size()); 545 if (!syntaxErrors.isEmpty()) { 546 builder.errorMessage(String.format("Extraction completed with %d error(s)", syntaxErrors.size())); 547 } 548 } 549 550 /** 551 * Helper method to append token to statement. 552 * Sets the token's stmt reference and adds it to the statement's token list. 553 * 554 * @param statement the statement to append to 555 * @param token the token to append 556 */ 557 private void appendToken(TCustomSqlStatement statement, TSourceToken token) { 558 if (statement == null || token == null) { 559 return; 560 } 561 token.stmt = statement; 562 statement.sourcetokenlist.add(token); 563 } 564 565 // ========== Statement Parsing Logic ========== 566 567 /** 568 * Parse all raw statements to build AST. 569 * <p> 570 * This method iterates through all raw statements and calls parsestatement() 571 * on each one to build the Abstract Syntax Tree. It handles error recovery 572 * for CREATE TABLE statements and collects syntax errors. 573 * 574 * @param context parser context with configuration 575 * @param parser primary parser instance 576 * @param secondaryParser secondary parser (null for Databricks) 577 * @param tokens source token list 578 * @param rawStatements raw statements from extraction phase 579 * @return statement list with parsed AST 580 */ 581 @Override 582 protected TStatementList performParsing(ParserContext context, TCustomParser parser, 583 TCustomParser secondaryParser, TSourceTokenList tokens, 584 TStatementList rawStatements) { 585 // Store references 586 this.fparser = (TParserDatabricks) parser; 587 this.sourcetokenlist = tokens; 588 this.parserContext = context; 589 this.sqlstatements = rawStatements; 590 591 // Initialize sqlcmds 592 this.sqlcmds = SqlCmdsFactory.get(vendor); 593 this.fparser.sqlcmds = this.sqlcmds; 594 595 // Initialize global context (inherited from AbstractSqlParser) 596 initializeGlobalContext(); 597 598 // Parse each statement 599 for (int i = 0; i < sqlstatements.size(); i++) { 600 TCustomSqlStatement stmt = sqlstatements.getRawSql(i); 601 try { 602 stmt.setFrameStack(frameStack); 603 int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree()); 604 605 // Vendor-specific post-processing (override hook if needed) 606 afterStatementParsed(stmt); 607 608 // Error recovery 609 boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE; 610 if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) { 611 handleCreateTableErrorRecovery(stmt); 612 } 613 614 // Collect errors 615 if ((parseResult != 0) || (stmt.getErrorCount() > 0)) { 616 copyErrorsFromStatement(stmt); 617 } 618 } catch (Exception ex) { 619 // Use inherited exception handler 620 handleStatementParsingException(stmt, i, ex); 621 continue; 622 } 623 } 624 625 // Clean up frame stack 626 if (globalFrame != null) globalFrame.popMeFromStack(frameStack); 627 628 return sqlstatements; 629 } 630 631 /** 632 * Post-processing hook after each statement is parsed. 633 * <p> 634 * Default implementation does nothing. Override if needed for vendor-specific 635 * post-processing. 636 * 637 * @param stmt the statement that was just parsed 638 */ 639 protected void afterStatementParsed(TCustomSqlStatement stmt) { 640 // Default: no post-processing needed for Databricks 641 } 642 643 /** 644 * Handle error recovery for CREATE TABLE statements. 645 * <p> 646 * This method attempts to recover from parse errors in CREATE TABLE statements 647 * by marking unparseable table properties (like ROW FORMAT, STORED AS, etc.) 648 * as sqlpluscmd and retrying. 649 * <p> 650 * Databricks/Hive DDL allows complex table properties after the column definition 651 * that may not be fully supported in the grammar. This error recovery allows 652 * partial parsing of the main table structure. 653 * <p> 654 * Extracted from TGSqlParser.doparse() lines 16916-16971 655 * 656 * @param stmt the statement with errors 657 */ 658 protected void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) { 659 if (((stmt.sqlstatementtype == ESqlStatementType.sstcreatetable) || 660 (stmt.sqlstatementtype == ESqlStatementType.sstcreateindex)) && 661 (!TBaseType.c_createTableStrictParsing)) { 662 663 // Find the closing parenthesis of table/column definition 664 // Mark everything after it as sqlpluscmd (ignored table properties) 665 int nested = 0; 666 boolean isIgnore = false, isFoundIgnoreToken = false; 667 TSourceToken firstIgnoreToken = null; 668 669 for (int k = 0; k < stmt.sourcetokenlist.size(); k++) { 670 TSourceToken st = stmt.sourcetokenlist.get(k); 671 672 if (isIgnore) { 673 // Mark tokens after closing paren as sqlpluscmd (to be ignored) 674 if (st.issolidtoken() && (st.tokencode != ';')) { 675 isFoundIgnoreToken = true; 676 if (firstIgnoreToken == null) { 677 firstIgnoreToken = st; 678 } 679 } 680 if (st.tokencode != ';') { 681 st.tokencode = TBaseType.sqlpluscmd; 682 } 683 continue; 684 } 685 686 // Track nested parentheses to find the matching closing paren 687 if (st.tokencode == (int) ')') { 688 nested--; 689 if (nested == 0) { 690 // Check if next token is "AS ( SELECT" - don't ignore CTAS subquery 691 boolean isSelect = false; 692 TSourceToken st1 = st.searchToken(TBaseType.rrw_as, 1); 693 if (st1 != null) { 694 TSourceToken st2 = st.searchToken((int) '(', 2); 695 if (st2 != null) { 696 TSourceToken st3 = st.searchToken(TBaseType.rrw_select, 3); 697 isSelect = (st3 != null); 698 } 699 } 700 if (!isSelect) { 701 // Found the closing paren, start ignoring subsequent tokens 702 isIgnore = true; 703 } 704 } 705 } 706 707 if ((st.tokencode == (int) '(') || (st.tokencode == TBaseType.left_parenthesis_2)) { 708 nested++; 709 } 710 } 711 712 // For Databricks, we don't validate specific table properties 713 // (unlike Oracle which checks TBaseType.searchOracleTablePros) 714 // This allows any Hive/Databricks DDL syntax like: 715 // ROW FORMAT, STORED AS, TBLPROPERTIES, LOCATION, etc. 716 717 // Retry parsing if we found ignoreable properties 718 if (isFoundIgnoreToken) { 719 stmt.clearError(); 720 stmt.parsestatement(null, false); 721 } 722 } 723 } 724 725 /** 726 * Determine whether a POSITION token starts the SQL-standard 727 * {@code POSITION(substr IN str)} form (as opposed to the comma form 728 * {@code POSITION(substr, str[, pos])}). 729 * 730 * <p>Scans the parenthesized argument group that immediately follows the 731 * POSITION keyword and returns {@code true} only when the <em>top level</em> 732 * of the argument list is an {@code IN} expression — i.e. the first 733 * top-level separator encountered is the {@code IN} keyword rather than a 734 * comma. "Top level" means paren-depth 1 and outside any {@code CASE ... END} 735 * block or {@code [ ... ]} subscript, so an {@code IN} nested inside a 736 * sub-expression does not misclassify a comma-form call. This handles: 737 * <ul> 738 * <li>{@code POSITION(a, f(x IN y))} — IN inside nested parens (skipped)</li> 739 * <li>{@code POSITION('a', CASE WHEN x IN (..) THEN b END)} — IN inside a 740 * CASE, with a preceding top-level comma (comma form wins)</li> 741 * <li>{@code POSITION(CASE WHEN x IN (..) THEN y END)} — IN inside a CASE, 742 * no top-level IN (comma/generic form)</li> 743 * <li>{@code POSITION(arr[x IN y], str)} — IN inside a subscript (skipped)</li> 744 * <li>{@code POSITION(x::map<string,string>::string IN lower(str))} — the 745 * comma inside the complex-type parameters is not the separator</li> 746 * </ul> 747 * A quoted identifier named {@code in} is ignored because it lexes as an 748 * identifier ({@code ttidentifier}), not the {@code IN} keyword. Angle 749 * brackets are only treated as type parameters when the {@code <} follows a 750 * complex-type keyword (MAP/ARRAY/STRUCT), so comparison operators such as 751 * {@code a < b} are unaffected. 752 * 753 * @param positionTok the POSITION source token 754 * @return {@code true} if the argument list uses the {@code IN} form 755 */ 756 private boolean isPositionInForm(TSourceToken positionTok) { 757 TSourceToken open = positionTok.nextSolidToken(); 758 if ((open == null) || (open.tokencode != (int) '(')) { 759 return false; 760 } 761 int parenDepth = 0; 762 int bracketDepth = 0; 763 int angleDepth = 0; 764 int caseDepth = 0; 765 TSourceToken prevSolid = null; 766 for (int i = open.posinlist; i < sourcetokenlist.size(); i++) { 767 TSourceToken st = sourcetokenlist.get(i); 768 if (!st.issolidtoken()) { 769 continue; 770 } 771 if ((st.tokencode == (int) '(') || (st.tokencode == TBaseType.left_parenthesis_2)) { 772 parenDepth++; 773 } else if (st.tokencode == (int) ')') { 774 parenDepth--; 775 if (parenDepth == 0) { 776 // reached the matching close paren without a top-level IN 777 return false; 778 } 779 } else if (st.tokencode == (int) '[') { 780 bracketDepth++; 781 } else if (st.tokencode == (int) ']') { 782 if (bracketDepth > 0) { 783 bracketDepth--; 784 } 785 } else if ((st.tokentype == ETokenType.ttlessthan) && isComplexTypeKeyword(prevSolid)) { 786 // opening a MAP<..>/ARRAY<..>/STRUCT<..> type parameter list 787 angleDepth++; 788 } else if ((st.tokentype == ETokenType.ttgreaterthan) && (angleDepth > 0)) { 789 // nested types lex as separate '>' tokens (e.g. map<..,array<..>>), so 790 // each closer decrements one level 791 angleDepth--; 792 } else if ((parenDepth == 1) && (bracketDepth == 0) && (angleDepth == 0)) { 793 if (st.tokentype == ETokenType.ttkeyword) { 794 // non-identifier-compare: matching SQL keywords (CASE/END/IN), not DB object names 795 if (st.toString().equalsIgnoreCase("case")) { 796 caseDepth++; 797 } else if (st.toString().equalsIgnoreCase("end")) { 798 if (caseDepth > 0) { 799 caseDepth--; 800 } 801 } else if ((caseDepth == 0) && st.toString().equalsIgnoreCase("in")) { 802 // top-level IN => POSITION(substr IN str) 803 return true; 804 } 805 } else if ((caseDepth == 0) && (st.tokencode == (int) ',')) { 806 // top-level comma => POSITION(substr, str[, pos]); keep generic 807 return false; 808 } 809 } 810 prevSolid = st; 811 } 812 return false; 813 } 814 815 /** 816 * @return {@code true} if the token is a complex-type keyword whose type 817 * parameters are written with angle brackets (MAP/ARRAY/STRUCT). 818 */ 819 private static boolean isComplexTypeKeyword(TSourceToken st) { 820 if ((st == null) || (st.tokentype != ETokenType.ttkeyword)) { 821 return false; 822 } 823 // non-identifier-compare: matching type keywords, not DB object names 824 return st.toString().equalsIgnoreCase("map") 825 || st.toString().equalsIgnoreCase("array") 826 || st.toString().equalsIgnoreCase("struct"); 827 } 828 829 // ========== Semantic Analysis and Interpretation ========== 830 831 /** 832 * Perform semantic analysis (resolve column-table relationships, etc.). 833 * <p> 834 * This method runs the TSQLResolver to build semantic relationships 835 * between columns and tables, among other analysis. 836 * 837 * @param context parser context 838 * @param statements statement list to analyze 839 */ 840 @Override 841 protected void performSemanticAnalysis(ParserContext context, TStatementList statements) { 842 if (!TBaseType.isEnableResolver()) { 843 return; 844 } 845 846 if (!getSyntaxErrors().isEmpty()) { 847 return; 848 } 849 850 try { 851 TSQLResolver resolver = new TSQLResolver(globalContext, statements); 852 resolver.resolve(); 853 } catch (Exception e) { 854 // Log but don't fail - semantic analysis is optional 855 System.err.println("Semantic analysis failed: " + e.getMessage()); 856 } 857 } 858 859 /** 860 * Perform interpretation (execute SQL in interpreter mode). 861 * <p> 862 * This method runs the TASTEvaluator to interpret/execute the SQL. 863 * 864 * @param context parser context 865 * @param statements statement list to interpret 866 */ 867 @Override 868 protected void performInterpreter(ParserContext context, TStatementList statements) { 869 if (!TBaseType.ENABLE_INTERPRETER) { 870 return; 871 } 872 873 try { 874 TGlobalScope interpreterScope = new TGlobalScope(sqlEnv); 875 TLog.enableInterpreterLogOnly(); 876 TASTEvaluator astEvaluator = new TASTEvaluator(statements, interpreterScope); 877 astEvaluator.eval(); 878 } catch (Exception e) { 879 // Log but don't fail - interpretation is optional 880 System.err.println("Interpretation failed: " + e.getMessage()); 881 } 882 } 883 884 @Override 885 public String toString() { 886 return "DatabricksSqlParser{vendor=" + vendor + "}"; 887 } 888}