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.TLexerDax; 009import gudusoft.gsqlparser.TParserDax; 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.stmt.dax.TDaxExprStmt; 021import gudusoft.gsqlparser.stmt.dax.TDaxEvaluateStmt; 022import gudusoft.gsqlparser.sqlcmds.ISqlCmds; 023import gudusoft.gsqlparser.sqlcmds.SqlCmdsFactory; 024import gudusoft.gsqlparser.compiler.TContext; 025import gudusoft.gsqlparser.sqlenv.TSQLEnv; 026import gudusoft.gsqlparser.compiler.TGlobalScope; 027import gudusoft.gsqlparser.compiler.TFrame; 028import gudusoft.gsqlparser.resolver.TSQLResolver; 029import gudusoft.gsqlparser.TLog; 030import gudusoft.gsqlparser.compiler.TASTEvaluator; 031 032import java.io.BufferedReader; 033import java.util.ArrayList; 034import java.util.List; 035import java.util.Stack; 036 037/** 038 * DAX (Data Analysis Expressions) database SQL parser implementation. 039 * 040 * <p>This parser handles DAX-specific SQL syntax including: 041 * <ul> 042 * <li>DAX expressions starting with '=' at beginning of line</li> 043 * <li>DEFINE...EVALUATE statements</li> 044 * <li>EVALUATE statements</li> 045 * <li>DAX function calls and expressions</li> 046 * <li>Special handling of DAX keywords (PRODUCT, TRUE, FALSE, CALENDAR, SECOND)</li> 047 * </ul> 048 * 049 * <p><b>Design Notes:</b> 050 * <ul> 051 * <li>Extends {@link AbstractSqlParser} using the template method pattern</li> 052 * <li>Uses {@link TLexerDax} for tokenization</li> 053 * <li>Uses {@link TParserDax} for parsing</li> 054 * <li>Delimiter character: ';' for DAX statements</li> 055 * </ul> 056 * 057 * <p><b>Usage Example:</b> 058 * <pre> 059 * // Get DAX parser from factory 060 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvdax); 061 * 062 * // Build context 063 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvdax) 064 * .sqlText("= CALCULATE(SUM(Sales[Amount]), ALL(Sales))") 065 * .build(); 066 * 067 * // Parse 068 * SqlParseResult result = parser.parse(context); 069 * 070 * // Access statements 071 * TStatementList statements = result.getSqlStatements(); 072 * </pre> 073 * 074 * @see SqlParser 075 * @see AbstractSqlParser 076 * @see TLexerDax 077 * @see TParserDax 078 * @since 3.3.1 079 */ 080public class DaxSqlParser extends AbstractSqlParser { 081 082 /** 083 * Construct DAX SQL parser. 084 * <p> 085 * Configures the parser for DAX database with default delimiter (;). 086 * <p> 087 * Following the original TGSqlParser pattern, the lexer and parser are 088 * created once in the constructor and reused for all parsing operations. 089 */ 090 public DaxSqlParser() { 091 super(EDbVendor.dbvdax); 092 this.delimiterChar = ';'; 093 this.defaultDelimiterStr = ";"; 094 095 // Create lexer once - will be reused for all parsing operations 096 this.flexer = new TLexerDax(); 097 this.flexer.delimiterchar = this.delimiterChar; 098 this.flexer.defaultDelimiterStr = this.defaultDelimiterStr; 099 100 // Set parent's lexer reference for shared tokenization logic 101 this.lexer = this.flexer; 102 103 // Create parser once - will be reused for all parsing operations 104 this.fparser = new TParserDax(null); 105 this.fparser.lexer = this.flexer; 106 } 107 108 // ========== Parser Components ========== 109 110 /** The DAX lexer used for tokenization */ 111 public TLexerDax flexer; 112 113 /** DAX parser (for DAX statements) */ 114 private TParserDax fparser; 115 116 /** Current statement being built during extraction */ 117 private TCustomSqlStatement gcurrentsqlstatement; 118 119 // Note: Global context and frame stack fields inherited from AbstractSqlParser: 120 // - protected TContext globalContext 121 // - protected TSQLEnv sqlEnv 122 // - protected Stack<TFrame> frameStack 123 // - protected TFrame globalFrame 124 125 // ========== AbstractSqlParser Abstract Methods Implementation ========== 126 127 /** 128 * Return the DAX lexer instance. 129 */ 130 @Override 131 protected TCustomLexer getLexer(ParserContext context) { 132 return this.flexer; 133 } 134 135 /** 136 * Return the DAX SQL parser instance with updated token list. 137 */ 138 @Override 139 protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) { 140 this.fparser.sourcetokenlist = tokens; 141 return this.fparser; 142 } 143 144 /** 145 * DAX does not use a secondary parser. 146 * @return null (no secondary parser) 147 */ 148 @Override 149 protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) { 150 return null; 151 } 152 153 /** 154 * Call DAX-specific tokenization logic. 155 * <p> 156 * Delegates to dodaxsqltexttotokenlist which handles DAX's 157 * simple token iteration. 158 */ 159 @Override 160 protected void tokenizeVendorSql() { 161 dodaxsqltexttotokenlist(); 162 } 163 164 /** 165 * Setup DAX parser for raw statement extraction. 166 * <p> 167 * Injects shared state (sqlcmds, sourcetokenlist) into the DAX parser 168 * so it can access tokens and SQL command definitions during parsing. 169 */ 170 @Override 171 protected void setupVendorParsersForExtraction() { 172 this.fparser.sqlcmds = this.sqlcmds; 173 this.fparser.sourcetokenlist = this.sourcetokenlist; 174 } 175 176 /** 177 * Call DAX-specific raw statement extraction logic. 178 * <p> 179 * Delegates to dodaxgetrawsqlstatements which handles DAX's 180 * statement boundary detection (based on nested parentheses and 181 * special DAX keywords). 182 */ 183 @Override 184 protected void extractVendorRawStatements(SqlParseResult.Builder builder) { 185 dodaxgetrawsqlstatements(builder); 186 } 187 188 // ========== DAX-Specific Tokenization ========== 189 190 /** 191 * Tokenize DAX SQL text to token list. 192 * <p> 193 * This is a simple tokenization process - just iterate through all tokens 194 * from the lexer and add them to the source token list. 195 * <p> 196 * Migrated from TGSqlParser.dodaxsqltexttotokenlist() 197 */ 198 private void dodaxsqltexttotokenlist() { 199 TSourceToken asourcetoken; 200 int yychar; 201 202 asourcetoken = getanewsourcetoken(); 203 if (asourcetoken == null) return; 204 yychar = asourcetoken.tokencode; 205 206 while (yychar > 0) { 207 sourcetokenlist.add(asourcetoken); 208 asourcetoken = getanewsourcetoken(); 209 if (asourcetoken == null) break; 210 yychar = asourcetoken.tokencode; 211 } 212 } 213 214 // ========== DAX-Specific Raw Statement Extraction ========== 215 216 /** 217 * Extract raw DAX statements from token list. 218 * <p> 219 * DAX statement boundaries are determined by: 220 * <ul> 221 * <li>Parentheses nesting level (statements are only complete at nesting level 0)</li> 222 * <li>Special DAX keywords that start new statements (=, DEFINE, EVALUATE)</li> 223 * <li>DAX keywords that might be identifiers (PRODUCT, TRUE, FALSE, CALENDAR, SECOND)</li> 224 * </ul> 225 * <p> 226 * Migrated from TGSqlParser.dodaxgetrawsqlstatements() 227 * 228 * @param builder Result builder to collect parse errors and timing 229 */ 230 private void dodaxgetrawsqlstatements(SqlParseResult.Builder builder) { 231 gcurrentsqlstatement = null; 232 TCustomSqlStatement tmpStmt = null; 233 EFindSqlStateType gst = EFindSqlStateType.stnormal; 234 int i; 235 TSourceToken ast; 236 int errorcount = 0; 237 int nestedParens = 0; 238 239 for (i = 0; i < sourcetokenlist.size(); i++) { 240 ast = sourcetokenlist.get(i); 241 sourcetokenlist.curpos = i; 242 243 // Handle DAX keywords that might be identifiers depending on context 244 if ((ast.tokencode == TBaseType.rrw_dax_product) 245 || (ast.tokencode == TBaseType.rrw_dax_true) 246 || (ast.tokencode == TBaseType.rrw_dax_false) 247 || (ast.tokencode == TBaseType.rrw_dax_calendar) 248 || (ast.tokencode == TBaseType.rrw_dax_second) 249 ) { 250 TSourceToken st1 = ast.searchToken("(", 1); 251 if (st1 == null) { 252 // Not followed by '(' - treat as identifier, not keyword 253 ast.tokencode = TBaseType.ident; 254 } 255 } 256 257 // Track parentheses nesting 258 if (ast.tokencode == '(') nestedParens++; 259 if (ast.tokencode == ')') nestedParens--; 260 261 // If we're inside parentheses, just add token to current statement 262 if (nestedParens > 0) { 263 if (gcurrentsqlstatement != null) { 264 gcurrentsqlstatement.sourcetokenlist.add(ast); 265 } 266 continue; 267 } 268 269 switch (gst) { 270 case sterror: { 271 tmpStmt = startDaxStmt(ast, gcurrentsqlstatement); 272 if (tmpStmt != null) { 273 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 274 275 gcurrentsqlstatement = tmpStmt; 276 gcurrentsqlstatement.sourcetokenlist.add(ast); 277 gst = EFindSqlStateType.stsql; 278 } else { 279 gcurrentsqlstatement.sourcetokenlist.add(ast); 280 } 281 break; 282 } 283 case stnormal: { 284 if ((ast.tokencode == TBaseType.cmtdoublehyphen) 285 || (ast.tokencode == TBaseType.cmtslashstar) 286 || (ast.tokencode == TBaseType.lexspace) 287 || (ast.tokencode == TBaseType.lexnewline) 288 || (ast.tokentype == ETokenType.ttsemicolon)) { 289 if (TBaseType.assigned(gcurrentsqlstatement)) { 290 gcurrentsqlstatement.sourcetokenlist.add(ast); 291 } 292 continue; 293 } 294 295 gcurrentsqlstatement = startDaxStmt(ast, gcurrentsqlstatement); 296 297 if (TBaseType.assigned(gcurrentsqlstatement)) { 298 gst = EFindSqlStateType.stsql; 299 gcurrentsqlstatement.sourcetokenlist.add(ast); 300 } else { 301 this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo) 302 , "Error when tokenlize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist)); 303 304 ast.tokentype = ETokenType.tttokenlizererrortoken; 305 gst = EFindSqlStateType.sterror; 306 307 gcurrentsqlstatement = new TUnknownSqlStatement(vendor); 308 gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid; 309 gcurrentsqlstatement.sourcetokenlist.add(ast); 310 } 311 break; 312 } 313 case stsql: { 314 tmpStmt = startDaxStmt(ast, gcurrentsqlstatement); 315 if (tmpStmt != null) { 316 gst = EFindSqlStateType.stsql; 317 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder); 318 319 gcurrentsqlstatement = tmpStmt; 320 gcurrentsqlstatement.sourcetokenlist.add(ast); 321 continue; 322 } 323 324 gcurrentsqlstatement.sourcetokenlist.add(ast); 325 326 break; 327 } 328 329 } //switch 330 } //for 331 332 // Last statement 333 if (TBaseType.assigned(gcurrentsqlstatement) && ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.sterror))) { 334 onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder); 335 } 336 337 // Add the sqlstatements to the builder so they can be returned 338 builder.sqlStatements(this.sqlstatements); 339 } 340 341 /** 342 * Determine if a token starts a new DAX statement. 343 * <p> 344 * DAX statements can start with: 345 * <ul> 346 * <li>'=' at the beginning of a line - creates TDaxExprStmt</li> 347 * <li>DEFINE at the beginning of a line - creates TDaxEvaluateStmt with DEFINE flag</li> 348 * <li>EVALUATE at the beginning of a line - creates TDaxEvaluateStmt (but not if we're already in a DEFINE...EVALUATE block)</li> 349 * <li>Any token at the very beginning of the query (first token) - creates TDaxExprStmt</li> 350 * </ul> 351 * <p> 352 * Migrated from TGSqlParser.startDaxStmt() 353 * 354 * @param currToken Current token to check 355 * @param currStmt Current statement being built 356 * @return New statement if this token starts one, null otherwise 357 */ 358 private TCustomSqlStatement startDaxStmt(TSourceToken currToken, TCustomSqlStatement currStmt) { 359 TCustomSqlStatement newStmt = null; 360 if (currToken == null) return null; 361 362 if ((currToken.tokencode == '=') && (currToken.isFirstTokenOfLine())) { 363 // '=' at beginning of line starts a DAX expression 364 currToken.tokencode = TBaseType.equal_start_expr; 365 newStmt = new TDaxExprStmt(EDbVendor.dbvdax); 366 } else if ((currToken.tokencode == TBaseType.rrw_dax_define) && (currToken.isFirstTokenOfLine())) { 367 // DEFINE at beginning of line starts a DEFINE...EVALUATE block 368 newStmt = new TDaxEvaluateStmt(EDbVendor.dbvdax); 369 ((TDaxEvaluateStmt) newStmt).setStartWithDefine(true); 370 } else if ((currToken.tokencode == TBaseType.rrw_dax_evaluate) && (currToken.isFirstTokenOfLine())) { 371 // EVALUATE at beginning of line - but not if we're inside a DEFINE block 372 if ((currStmt != null) && (currStmt instanceof TDaxEvaluateStmt)) { 373 TDaxEvaluateStmt tmp = (TDaxEvaluateStmt) currStmt; 374 if (tmp.isStartWithDefine()) { 375 // We're inside a DEFINE...EVALUATE block, so EVALUATE is part of the current statement 376 return null; 377 } 378 } 379 newStmt = new TDaxEvaluateStmt(EDbVendor.dbvdax); 380 } 381 382 if (newStmt == null) { 383 // Check if this is the first token of the query 384 boolean isFirst = currToken.isFirstTokenOfLine(); 385 TSourceToken prevToken = currToken.prevSolidToken(); 386 if ((isFirst) && (prevToken == null)) { 387 // First token of query - start a DAX expression 388 newStmt = new TDaxExprStmt(EDbVendor.dbvdax); 389 } 390 } 391 return newStmt; 392 } 393 394 // ========== Statement Parsing ========== 395 396 /** 397 * Parse all raw DAX statements. 398 * <p> 399 * Iterates through all raw statements extracted by dodaxgetrawsqlstatements() 400 * and parses each one using the DAX parser. 401 * <p> 402 * Follows the pattern from MssqlSqlParser.performParsing() 403 */ 404 @Override 405 protected TStatementList performParsing(ParserContext context, TCustomParser parser, 406 TCustomParser secondaryParser, TSourceTokenList tokens, 407 TStatementList rawStatements) { 408 // Store references to key objects 409 this.fparser = (TParserDax) parser; 410 this.sourcetokenlist = tokens; 411 this.parserContext = context; 412 413 // Use the raw statements passed from AbstractSqlParser.parse() 414 this.sqlstatements = rawStatements; 415 416 // Initialize sqlcmds for this vendor 417 this.sqlcmds = SqlCmdsFactory.get(vendor); 418 419 // CRITICAL: Inject sqlcmds into parser 420 this.fparser.sqlcmds = this.sqlcmds; 421 422 // Initialize global context for semantic analysis 423 initializeGlobalContext(); 424 425 // Parse each statement 426 for (int i = 0; i < sqlstatements.size(); i++) { 427 TCustomSqlStatement stmt = sqlstatements.getRawSql(i); 428 try { 429 stmt.setFrameStack(frameStack); 430 int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree()); 431 432 // Error recovery 433 boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE; 434 if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) { 435 handleCreateTableErrorRecovery(stmt); 436 } 437 438 // Collect errors 439 if ((parseResult != 0) || (stmt.getErrorCount() > 0)) { 440 copyErrorsFromStatement(stmt); 441 } 442 } catch (Exception ex) { 443 // Use inherited exception handler 444 handleStatementParsingException(stmt, i, ex); 445 continue; 446 } 447 } 448 449 // Clean up frame stack 450 if (globalFrame != null) globalFrame.popMeFromStack(frameStack); 451 452 return sqlstatements; 453 } 454 455 /** 456 * Handle CREATE TABLE error recovery. 457 * <p> 458 * DAX doesn't have CREATE TABLE statements, so this is a no-op. 459 * However, we implement it to satisfy the AbstractSqlParser contract. 460 */ 461 private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) { 462 // No CREATE TABLE in DAX, so no error recovery needed 463 } 464}