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}