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.TLexerMdx;
009import gudusoft.gsqlparser.TParserMdx;
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;
026import gudusoft.gsqlparser.resolver.TSQLResolver;
027import gudusoft.gsqlparser.TLog;
028import gudusoft.gsqlparser.compiler.TASTEvaluator;
029
030import java.io.BufferedReader;
031import java.util.ArrayList;
032import java.util.List;
033import java.util.Stack;
034
035/**
036 * MDX (Multidimensional Expressions) SQL parser implementation.
037 *
038 * <p>This parser handles MDX-specific syntax including:
039 * <ul>
040 *   <li>MDX SELECT statements with axes specifications</li>
041 *   <li>SCOPE statements for calculated members</li>
042 *   <li>IF/CASE/END block structures</li>
043 *   <li>MDX-specific functions and operators</li>
044 * </ul>
045 *
046 * <p><b>Design Notes:</b>
047 * <ul>
048 *   <li>Extends {@link AbstractSqlParser} using the template method pattern</li>
049 *   <li>Uses {@link TLexerMdx} for tokenization</li>
050 *   <li>Uses {@link TParserMdx} for parsing</li>
051 *   <li>Delimiter character: ';' for MDX statements</li>
052 * </ul>
053 *
054 * <p><b>Usage Example:</b>
055 * <pre>
056 * // Get MDX parser from factory
057 * SqlParser parser = SqlParserFactory.get(EDbVendor.dbvmdx);
058 *
059 * // Build context
060 * ParserContext context = new ParserContext.Builder(EDbVendor.dbvmdx)
061 *     .sqlText("SELECT [Measures].[Sales] ON COLUMNS FROM [Cube]")
062 *     .build();
063 *
064 * // Parse
065 * SqlParseResult result = parser.parse(context);
066 *
067 * // Access statements
068 * TStatementList statements = result.getSqlStatements();
069 * </pre>
070 *
071 * @see SqlParser
072 * @see AbstractSqlParser
073 * @see TLexerMdx
074 * @see TParserMdx
075 * @since 3.2.0.0
076 */
077public class MdxSqlParser extends AbstractSqlParser {
078
079    /**
080     * Construct MDX SQL parser.
081     * <p>
082     * Configures the parser for MDX with default delimiter (;).
083     * <p>
084     * Following the original TGSqlParser pattern, the lexer and parser are
085     * created once in the constructor and reused for all parsing operations.
086     */
087    public MdxSqlParser() {
088        super(EDbVendor.dbvmdx);
089        this.delimiterChar = ';';
090        this.defaultDelimiterStr = ";";
091
092        // Create lexer once - will be reused for all parsing operations
093        this.flexer = new TLexerMdx();
094        this.flexer.delimiterchar = this.delimiterChar;
095        this.flexer.defaultDelimiterStr = this.defaultDelimiterStr;
096
097        // Set parent's lexer reference for shared tokenization logic
098        this.lexer = this.flexer;
099
100        // Create parser once - will be reused for all parsing operations
101        this.fparser = new TParserMdx(null);
102        this.fparser.lexer = this.flexer;
103    }
104
105    // ========== Parser Components ==========
106
107    /** The MDX lexer used for tokenization */
108    public TLexerMdx flexer;
109
110    /** MDX parser (for MDX statements) */
111    private TParserMdx fparser;
112
113    /** Current statement being built during extraction */
114    private TCustomSqlStatement gcurrentsqlstatement;
115
116    // Note: Global context and frame stack fields inherited from AbstractSqlParser:
117    // - protected TContext globalContext
118    // - protected TSQLEnv sqlEnv
119    // - protected Stack<TFrame> frameStack
120    // - protected TFrame globalFrame
121
122    // ========== AbstractSqlParser Abstract Methods Implementation ==========
123
124    /**
125     * Return the MDX lexer instance.
126     */
127    @Override
128    protected TCustomLexer getLexer(ParserContext context) {
129        return this.flexer;
130    }
131
132    /**
133     * Return the MDX parser instance with updated token list.
134     */
135    @Override
136    protected TCustomParser getParser(ParserContext context, TSourceTokenList tokens) {
137        this.fparser.sourcetokenlist = tokens;
138        return this.fparser;
139    }
140
141    /**
142     * MDX doesn't have a secondary parser.
143     * @return null
144     */
145    @Override
146    protected TCustomParser getSecondaryParser(ParserContext context, TSourceTokenList tokens) {
147        return null;
148    }
149
150    /**
151     * Call MDX-specific tokenization logic.
152     * <p>
153     * Delegates to domdxtexttotokenlist which handles MDX's
154     * specific keyword recognition and token generation.
155     */
156    @Override
157    protected void tokenizeVendorSql() {
158        domdxtexttotokenlist();
159    }
160
161    /**
162     * Setup MDX parser for raw statement extraction.
163     * <p>
164     * MDX uses a single parser, so we inject sqlcmds and update
165     * the token list for the main parser only.
166     */
167    @Override
168    protected void setupVendorParsersForExtraction() {
169        // Inject sqlcmds into parser (required for make_stmt)
170        this.fparser.sqlcmds = this.sqlcmds;
171
172        // Update token list for parser
173        this.fparser.sourcetokenlist = this.sourcetokenlist;
174    }
175
176    /**
177     * Call MDX-specific raw statement extraction logic.
178     * <p>
179     * Delegates to domdxgetrawsqlstatements which handles MDX's
180     * statement delimiters and SCOPE/END blocks.
181     */
182    @Override
183    protected void extractVendorRawStatements(SqlParseResult.Builder builder) {
184        domdxgetrawsqlstatements(builder);
185
186        // Set the extracted statements in the builder
187        builder.sqlStatements(this.sqlstatements);
188    }
189
190    /**
191     * Perform full parsing of statements with syntax checking.
192     * <p>
193     * This method orchestrates the parsing of all statements.
194     */
195    @Override
196    protected TStatementList performParsing(ParserContext context,
197                                           TCustomParser parser,
198                                           TCustomParser secondaryParser,
199                                           TSourceTokenList tokens,
200                                           TStatementList rawStatements) {
201        // Store references
202        this.fparser = (TParserMdx) parser;
203        this.sourcetokenlist = tokens;
204        this.parserContext = context;
205
206        // Use the raw statements passed from AbstractSqlParser.parse()
207        this.sqlstatements = rawStatements;
208
209        // Initialize sqlcmds
210        this.sqlcmds = SqlCmdsFactory.get(this.vendor);
211        this.fparser.sqlcmds = this.sqlcmds;
212
213        // Initialize global context (from AbstractSqlParser)
214        initializeGlobalContext();
215
216        // Parse each statement
217        for (int i = 0; i < sqlstatements.size(); i++) {
218            TCustomSqlStatement stmt = sqlstatements.getRawSql(i);
219
220            try {
221                // Set frame stack
222                stmt.setFrameStack(frameStack);
223
224                // Parse the statement
225                int parseResult = stmt.parsestatement(null, false, context.isOnlyNeedRawParseTree());
226
227                // Error recovery for CREATE TABLE statements
228                boolean doRecover = TBaseType.ENABLE_ERROR_RECOVER_IN_CREATE_TABLE;
229                if (doRecover && ((parseResult != 0) || (stmt.getErrorCount() > 0))) {
230                    handleCreateTableErrorRecovery(stmt);
231                }
232
233                // Collect errors
234                if ((parseResult != 0) || (stmt.getErrorCount() > 0)) {
235                    copyErrorsFromStatement(stmt);
236                }
237
238            } catch (Exception ex) {
239                // Use inherited exception handler
240                handleStatementParsingException(stmt, i, ex);
241                continue;
242            }
243        }
244
245        // Clean up frame stack
246        if (globalFrame != null) {
247            globalFrame.popMeFromStack(frameStack);
248        }
249
250        return sqlstatements;
251    }
252
253    /**
254     * Perform MDX-specific semantic analysis using TSQLResolver.
255     */
256    @Override
257    protected void performSemanticAnalysis(ParserContext context, TStatementList statements) {
258        if (TBaseType.isEnableResolver() && getSyntaxErrors().isEmpty()) {
259            TSQLResolver resolver = new TSQLResolver(globalContext, statements);
260            resolver.resolve();
261        }
262    }
263
264    /**
265     * Perform interpretation/evaluation on parsed statements.
266     */
267    @Override
268    protected void performInterpreter(ParserContext context, TStatementList statements) {
269        if (TBaseType.ENABLE_INTERPRETER && getSyntaxErrors().isEmpty()) {
270            TLog.clearLogs();
271            TGlobalScope interpreterScope = new TGlobalScope(sqlEnv);
272            TLog.enableInterpreterLogOnly();
273            TASTEvaluator astEvaluator = new TASTEvaluator(statements, interpreterScope);
274            astEvaluator.eval();
275        }
276    }
277
278    // ========== MDX-Specific Tokenization ==========
279
280    /**
281     * Tokenize MDX input into source tokens.
282     * <p>
283     * Migrated from TGSqlParser.doMdxtexttotokenlist() (lines 4824-4841)
284     * <p>
285     * MDX uses simple token-by-token processing without special handling.
286     */
287    private void domdxtexttotokenlist() {
288        TSourceToken asourcetoken;
289        int yychar;
290
291        asourcetoken = getanewsourcetoken();
292        if (asourcetoken == null) return;
293        yychar = asourcetoken.tokencode;
294
295        while (yychar > 0) {
296            sourcetokenlist.add(asourcetoken);
297            asourcetoken = getanewsourcetoken();
298            if (asourcetoken == null) break;
299            yychar = asourcetoken.tokencode;
300        }
301    }
302
303    // ========== MDX-Specific Raw Statement Extraction ==========
304
305    /**
306     * Extract raw MDX statements from token list.
307     * <p>
308     * Migrated from TGSqlParser.domdxgetrawsqlstatements() (lines 10861-11011)
309     * <p>
310     * Handles:
311     * <ul>
312     *   <li>Semicolon statement terminators</li>
313     *   <li>SCOPE...END blocks with nested IF/CASE/END support</li>
314     *   <li>Continuous semicolons treated as comments</li>
315     * </ul>
316     */
317    private void domdxgetrawsqlstatements(SqlParseResult.Builder builder) {
318        int waitingEnd = 0;
319        boolean foundEnd = false;
320
321        if (TBaseType.assigned(sqlstatements)) sqlstatements.clear();
322        if (!TBaseType.assigned(sourcetokenlist)) return;
323
324        gcurrentsqlstatement = null;
325        EFindSqlStateType gst = EFindSqlStateType.stnormal;
326        TSourceToken lcprevsolidtoken = null, ast = null;
327
328        for (int i = 0; i < sourcetokenlist.size(); i++) {
329
330            if ((ast != null) && (ast.issolidtoken()))
331                lcprevsolidtoken = ast;
332
333            ast = sourcetokenlist.get(i);
334            sourcetokenlist.curpos = i;
335
336            switch (gst) {
337                case sterror: {
338                    if (ast.tokentype == ETokenType.ttsemicolon) {
339                        gcurrentsqlstatement.sourcetokenlist.add(ast);
340                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
341                        gst = EFindSqlStateType.stnormal;
342                    } else {
343                        gcurrentsqlstatement.sourcetokenlist.add(ast);
344                    }
345                    break;
346                } //sterror
347
348                case stnormal: {
349                    if ((ast.tokencode == TBaseType.cmtdoublehyphen)
350                            || (ast.tokencode == TBaseType.cmtslashstar)
351                            || (ast.tokencode == TBaseType.lexspace)
352                            || (ast.tokencode == TBaseType.lexnewline)
353                            || (ast.tokentype == ETokenType.ttsemicolon)) {
354                        if (gcurrentsqlstatement != null) {
355                            gcurrentsqlstatement.sourcetokenlist.add(ast);
356                        }
357
358                        if ((lcprevsolidtoken != null) && (ast.tokentype == ETokenType.ttsemicolon)) {
359                            if (lcprevsolidtoken.tokentype == ETokenType.ttsemicolon) {
360                                // ;;;; continuous semicolon, treat it as comment
361                                ast.tokentype = ETokenType.ttsimplecomment;
362                                ast.tokencode = TBaseType.cmtdoublehyphen;
363                            }
364                        }
365
366                        continue;
367                    }
368
369                    // find a tokentext to start sql or plsql mode
370                    gcurrentsqlstatement = sqlcmds.issql(ast, gst, gcurrentsqlstatement);
371
372                    if (gcurrentsqlstatement != null) {
373                        if (gcurrentsqlstatement.sqlstatementtype == ESqlStatementType.sstmdxscope) {
374                            gst = EFindSqlStateType.ststoredprocedure;
375                            gcurrentsqlstatement.sourcetokenlist.add(ast);
376                            foundEnd = false;
377                            waitingEnd = 1;
378                        } else {
379                            gst = EFindSqlStateType.stsql;
380                            gcurrentsqlstatement.sourcetokenlist.add(ast);
381                        }
382                    } else {
383                        //error tokentext found
384
385                        this.syntaxErrors.add(new TSyntaxError(ast.getAstext(), ast.lineNo, (ast.columnNo < 0 ? 0 : ast.columnNo)
386                                , "Error when tokenlize", EErrorType.spwarning, TBaseType.MSG_WARNING_ERROR_WHEN_TOKENIZE, null, ast.posinlist));
387
388                        ast.tokentype = ETokenType.tttokenlizererrortoken;
389                        gst = EFindSqlStateType.sterror;
390
391                        gcurrentsqlstatement = new TUnknownSqlStatement(this.vendor);
392                        gcurrentsqlstatement.sqlstatementtype = ESqlStatementType.sstinvalid;
393                        gcurrentsqlstatement.sourcetokenlist.add(ast);
394
395                    }
396
397                    break;
398                } // stnormal
399
400                case stsql: {
401                    if (ast.tokentype == ETokenType.ttsemicolon) {
402                        gst = EFindSqlStateType.stnormal;
403                        gcurrentsqlstatement.sourcetokenlist.add(ast);
404                        gcurrentsqlstatement.semicolonended = ast;
405                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
406                        continue;
407                    }
408
409                    gcurrentsqlstatement.sourcetokenlist.add(ast);
410                    break;
411                }//case stsql
412
413                case ststoredprocedure: {
414                    if (ast.tokencode == TBaseType.rrw_if) {
415                        waitingEnd++;
416                    } else if (ast.tokencode == TBaseType.rrw_case) {
417                        waitingEnd++;
418                    } else if (ast.tokencode == TBaseType.rrw_scope) {
419                        // ignore scope after end keyword like this:
420                        // end scope
421                        if (lcprevsolidtoken.tokencode != TBaseType.rrw_end) {
422                            waitingEnd++;
423                        }
424                    } else if (ast.tokencode == TBaseType.rrw_end) {
425                        foundEnd = true;
426                        waitingEnd--;
427                        if (waitingEnd < 0) {
428                            waitingEnd = 0;
429                        }
430                    }
431
432                    gcurrentsqlstatement.sourcetokenlist.add(ast);
433                    if ((ast.tokentype == ETokenType.ttsemicolon) && (waitingEnd == 0) && (foundEnd)) {
434                        gst = EFindSqlStateType.stnormal;
435                        onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, false, builder);
436                    }
437
438                    break;
439                } //ststoredprocedure
440            } //switch
441        }//for
442
443        //last statement
444        if ((gcurrentsqlstatement != null) &&
445                ((gst == EFindSqlStateType.stsql) || (gst == EFindSqlStateType.ststoredprocedure) ||
446                        (gst == EFindSqlStateType.sterror))) {
447            onRawStatementComplete(parserContext, gcurrentsqlstatement, fparser, null, sqlstatements, true, builder);
448        }
449    }
450
451    /**
452     * Handle CREATE TABLE error recovery.
453     * <p>
454     * Attempts to recover from parsing errors in CREATE TABLE statements
455     * by marking unparseable tokens as ignoreable.
456     */
457    private void handleCreateTableErrorRecovery(TCustomSqlStatement stmt) {
458        // MDX doesn't typically have CREATE TABLE statements
459        // but we keep this method for consistency with the pattern
460        // No specific recovery logic needed for MDX
461    }
462}