001package gudusoft.gsqlparser.stmt.snowflake;
002
003import gudusoft.gsqlparser.TSourceToken;
004import gudusoft.gsqlparser.TSourceTokenList;
005
006/**
007 * Which object a keyword-less Snowflake {@code USE} switches — MantisBT 4677.
008 *
009 * <p>Snowflake makes the object keyword optional on both sides, so
010 * {@code USE a} and {@code USE a.b} must be told apart without one. The rule
011 * comes from the USE SCHEMA reference: <em>"The SCHEMA keyword is optional if
012 * the schema name is fully qualified (in the form of db_name.schema_name)"</em>.
013 * Combined with USE DATABASE's own optional keyword:
014 *
015 * <ul>
016 *   <li>unqualified name → {@link #DATABASE}</li>
017 *   <li>qualified name   → {@link #SCHEMA}</li>
018 * </ul>
019 *
020 * <p><b>Why this lives in one class.</b> Two independent code paths need the
021 * answer: the statement splitter picks the statement class <em>before</em>
022 * parsing, and the grammar action stamps the {@code EDbObjectType} that drives
023 * {@code TUseDatabase.isSchema()} and resolver2's {@code DatabaseContextTracker}.
024 * Two separate implementations drifted apart twice during this fix (once via a
025 * part-count that is never populated, once on the {@code IDENTIFIER(...)}
026 * form), and a disagreement is invisible through the public API — the class
027 * says one thing, the node says another, and nothing reports it. Both callers
028 * now pass the same evidence (the token list plus the position of the
029 * {@code USE} token) to {@link #of}, so there is exactly one rule.
030 */
031public enum SnowflakeUseTarget {
032    DATABASE,
033    SCHEMA;
034
035    /**
036     * Classify a keyword-less {@code USE}. Callers must have already ruled out
037     * the explicit forms ({@code USE DATABASE/SCHEMA/ROLE/WAREHOUSE ...}).
038     *
039     * @param tokens the statement's token list
040     * @param usePos position of the {@code USE} token within {@code tokens}
041     * @return {@link #SCHEMA} when the name is qualified, else {@link #DATABASE}
042     */
043    public static SnowflakeUseTarget of(TSourceTokenList tokens, int usePos) {
044        if (tokens == null) {
045            return DATABASE;
046        }
047        TSourceToken first = tokens.nextsolidtoken(usePos, 1, false);
048        if (first == null) {
049            return DATABASE;
050        }
051        if ("IDENTIFIER".equalsIgnoreCase(first.toString())) { // non-identifier-compare: function keyword token text, not an object name
052            return ofIdentifierCall(tokens, usePos);
053        }
054        // Plain name. A qualified one separates its parts with a dot TOKEN, so a
055        // delimited identifier that merely CONTAINS a dot ("my.db") is a single
056        // token and correctly stays unqualified.
057        TSourceToken afterName = tokens.nextsolidtoken(usePos, 2, false);
058        return (afterName != null && ".".equals(afterName.toString()))
059                ? SCHEMA : DATABASE;
060    }
061
062    /**
063     * {@code USE IDENTIFIER(<arg>)} — Snowflake's way of supplying an object
064     * name as a value, so the name can be computed rather than written out.
065     *
066     * <p>The dot that would qualify the name lives INSIDE the argument, not in
067     * the token stream, so the plain-name rule cannot see it and would call
068     * every such statement a database switch.
069     *
070     * <ul>
071     *   <li>A string literal argument is known at parse time and is decided
072     *       properly: {@code IDENTIFIER('D1.S2')} is a schema switch.</li>
073     *   <li>A session variable or bind ({@code IDENTIFIER($db)},
074     *       {@code IDENTIFIER(?)}) has no value until run time, so the form is
075     *       genuinely undecidable statically. It falls back to
076     *       {@link #DATABASE} — the documented limitation, matching the bare
077     *       {@code USE <name>} default rather than inventing a guess.</li>
078     * </ul>
079     */
080    private static SnowflakeUseTarget ofIdentifierCall(TSourceTokenList tokens, int usePos) {
081        TSourceToken open = tokens.nextsolidtoken(usePos, 2, false);
082        TSourceToken arg = tokens.nextsolidtoken(usePos, 3, false);
083        if (open == null || arg == null || !"(".equals(open.toString())) {
084            return DATABASE;
085        }
086        String body = literalBody(arg.toString());
087        if (body == null) {
088            // Not a literal: value unknown until run time.
089            return DATABASE;
090        }
091        return nameIsQualified(body) ? SCHEMA : DATABASE;
092    }
093
094    /**
095     * Strip a string literal's delimiters and return its content, or
096     * {@code null} when the token is not a literal at all (a session variable
097     * or a bind, whose value only exists at run time).
098     *
099     * <p>Snowflake accepts both spellings, and the dollar-quoted forms are just
100     * as knowable at parse time as the single-quoted one:
101     * {@code 'D1.S2'}, {@code $$D1.S2$$}, and the tagged {@code $tag$D1.S2$tag$}.
102     * Testing only for a leading quote would send every dollar-quoted name down
103     * the "unknown at parse time" path and misclassify it as a database.
104     */
105    private static String literalBody(String text) {
106        if (text == null || text.length() < 2) {
107            return null;
108        }
109        if (text.charAt(0) == '\'' && text.charAt(text.length() - 1) == '\'') {
110            return text.substring(1, text.length() - 1);
111        }
112        if (text.charAt(0) == '$') {
113            // $tag$...$tag$ (tag may be empty, giving $$...$$).
114            int open = text.indexOf('$', 1);
115            if (open < 0) {
116                return null;
117            }
118            String delimiter = text.substring(0, open + 1);
119            if (text.length() >= 2 * delimiter.length()
120                    && text.endsWith(delimiter)) {
121                return text.substring(delimiter.length(),
122                        text.length() - delimiter.length());
123            }
124        }
125        return null;
126    }
127
128    /**
129     * Is a bare name qualified? Scans for a dot outside any delimited
130     * identifier, so {@code "my.db"} stays unqualified for the same reason
131     * {@code USE "my.db"} does.
132     */
133    private static boolean nameIsQualified(String body) {
134        boolean inQuotes = false;
135        for (int i = 0; i < body.length(); i++) {
136            char c = body.charAt(i);
137            if (c == '"') {
138                inQuotes = !inQuotes;
139            } else if (c == '.' && !inQuotes) {
140                return true;
141            }
142        }
143        return false;
144    }
145}