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}