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}