001package gudusoft.gsqlparser.pp2.token; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.ETokenType; 005import gudusoft.gsqlparser.TBaseType; 006import gudusoft.gsqlparser.TSourceToken; 007import gudusoft.gsqlparser.TSourceTokenList; 008import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 009import gudusoft.gsqlparser.sqlenv.IdentifierCodec; 010import gudusoft.gsqlparser.sqlenv.IdentifierProfile; 011 012import java.util.Arrays; 013import java.util.HashSet; 014import java.util.Set; 015 016/** 017 * Which double-quoted texts may be character strings rather than delimited names. Changing the case 018 * of a delimited name is a formatting choice; changing the case of a string changes the data. So the 019 * formatter leaves every double-quoted text this class reports as written, and pp2's content checks 020 * refuse any change to one. 021 * 022 * <p>A double-quoted text may be a string 023 * <ul> 024 * <li>anywhere, in a dialect whose identifier rules do not make a double quote a name delimiter 025 * (MDX, for one), or in which a setting the SQL does not show can make it a string: MySQL and its 026 * relatives without ANSI_QUOTES, BigQuery, Hive, Spark, Databricks, Impala, Flink, Couchbase, 027 * Informix without DELIMIDENT, Sybase with quoted_identifier off (its default), SQLite (which 028 * falls back to a string), SOQL, and Access (whose criteria take {@code "Smith"});</li> 029 * <li>after the statement that turns the quoted-identifier option off: {@code SET QUOTED_IDENTIFIER OFF}, 030 * {@code SET ANSI_NULLS, QUOTED_IDENTIFIER OFF} or {@code SET ANSI_DEFAULTS OFF} (which sets 031 * QUOTED_IDENTIFIER with it) in SQL Server, Azure SQL and SQL Anywhere, 032 * {@code SET [TEMPORARY] OPTION quoted_identifier = 'Off'} in SQL Anywhere, and 033 * {@code SET SQL DIALECT 1} in Firebird, whose dialects before 3 read double quotes as strings.</li> 034 * </ul> 035 * 036 * <p>Only the SQL text is seen. A setting it does not show is the dialect's default, which is how GSP's 037 * parser reads the text too: in SQL Server and Azure SQL QUOTED_IDENTIFIER is on (Microsoft's ODBC and 038 * OLE DB drivers turn it on when they connect), in SQL Anywhere quoted_identifier is on, and Firebird 039 * uses dialect 3. A script for a connection that has the option off must turn it off itself; without 040 * that, its double-quoted texts are names here. 041 * 042 * <p>A later statement that turns the option back on is not followed. In a SQL Server routine body it 043 * does not change how the body is read (the setting of the batch that created the routine does), and 044 * the tokens alone cannot tell a routine body from a batch; leaving a name's case as written is safe, 045 * re-casing a string is not. A script that never turns the option off keeps the identifier reading. 046 */ 047public final class DoubleQuotedStrings { 048 049 /** No double-quoted text may be a string. */ 050 public static final long NONE = Long.MAX_VALUE; 051 052 /** Every double-quoted text may be a string. */ 053 public static final long ALL = -1L; 054 055 /** 056 * Dialects in which a double-quoted text may be a string although their identifier profile reads it 057 * as a name: a setting outside the SQL decides. 058 */ 059 private static final Set<EDbVendor> DOUBLE_QUOTE_MAY_BE_STRING = new HashSet<EDbVendor>(Arrays.asList( 060 EDbVendor.dbvmysql, EDbVendor.dbvdoris, EDbVendor.dbvstarrocks, EDbVendor.dbvoceanbase, 061 EDbVendor.dbvbigquery, EDbVendor.dbvhive, EDbVendor.dbvsparksql, EDbVendor.dbvdatabricks, 062 EDbVendor.dbvimpala, EDbVendor.dbvflink, EDbVendor.dbvcouchbase, EDbVendor.dbvinformix, 063 EDbVendor.dbvsybase, EDbVendor.dbvsybasease, EDbVendor.dbvsybaseiq, EDbVendor.dbvsqlite, 064 EDbVendor.dbvsoql, EDbVendor.dbvaccess)); 065 066 /** Dialects in which a SET statement can make the double-quoted texts after it strings. */ 067 private static final Set<EDbVendor> QUOTED_IDENTIFIER_OPTION = new HashSet<EDbVendor>(Arrays.asList( 068 EDbVendor.dbvmssql, EDbVendor.dbvazuresql, EDbVendor.dbvsqlanywhere, EDbVendor.dbvfirebird)); 069 070 /** Per vendor ordinal: whether every double-quoted text may be a string. */ 071 private static volatile boolean[] everywhere; 072 073 private DoubleQuotedStrings() { 074 } 075 076 /** 077 * The offset after which every double-quoted text of {@code tokens} may be a string: 078 * {@link #ALL}, the offset of the value that turns the quoted-identifier option off, or 079 * {@link #NONE}. A double-quoted token may be a string when its offset is greater. 080 */ 081 public static long stringsAfter(EDbVendor vendor, TSourceTokenList tokens) { 082 if (vendor == null || tokens == null) return NONE; 083 if (everywhere(vendor)) return ALL; 084 if (!QUOTED_IDENTIFIER_OPTION.contains(vendor)) return NONE; 085 OptionReader reader = new OptionReader(); 086 for (int i = 0; i < tokens.size(); i++) { 087 TSourceToken t = tokens.get(i); 088 if (t != null && reader.turnsOff(t)) return t.offset; 089 } 090 return NONE; 091 } 092 093 /** {@link #stringsAfter(EDbVendor, TSourceTokenList)} over a pp2 token stream. */ 094 public static long stringsAfter(EDbVendor vendor, Pp2TokenStream stream) { 095 if (vendor == null || stream == null) return NONE; 096 if (everywhere(vendor)) return ALL; 097 if (!QUOTED_IDENTIFIER_OPTION.contains(vendor)) return NONE; 098 OptionReader reader = new OptionReader(); 099 for (int i = 0; i < stream.size(); i++) { 100 TSourceToken t = stream.get(i).getSourceToken(); 101 if (t != null && reader.turnsOff(t)) return t.offset; 102 } 103 return NONE; 104 } 105 106 private static boolean everywhere(EDbVendor vendor) { 107 boolean[] known = everywhere; 108 if (known == null) { 109 EDbVendor[] all = EDbVendor.values(); 110 known = new boolean[all.length]; 111 for (EDbVendor v : all) { 112 known[v.ordinal()] = DOUBLE_QUOTE_MAY_BE_STRING.contains(v) || !doubleQuoteDelimitsNames(v); 113 } 114 everywhere = known; 115 } 116 return known[vendor.ordinal()]; 117 } 118 119 private static boolean doubleQuoteDelimitsNames(EDbVendor vendor) { 120 try { 121 IdentifierProfile profile = IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults()); 122 return IdentifierCodec.isQuoted(profile, ESQLDataObjectType.dotColumn, "\"x\""); 123 } catch (RuntimeException e) { 124 return false; 125 } 126 } 127 128 /** 129 * Reads the statements that set the quoted-identifier option, one solid token at a time: 130 * {@code SET option [, option ...] ON|OFF}, where QUOTED_IDENTIFIER or ANSI_DEFAULTS sets it, 131 * {@code SET [EXISTING] [TEMPORARY] OPTION [owner.]option = value} and {@code SET SQL DIALECT n}. 132 */ 133 private static final class OptionReader { 134 private static final int IDLE = 0; 135 /** After SET, or after SET EXISTING or SET TEMPORARY. */ 136 private static final int AFTER_SET = 1; 137 /** After an option name of the ON|OFF form. */ 138 private static final int AFTER_NAME = 2; 139 /** After a comma of that form's option list. */ 140 private static final int AFTER_COMMA = 3; 141 /** After SET ... OPTION, or after the dot of a qualified option name. */ 142 private static final int AFTER_OPTION = 4; 143 /** After the option name of the SET OPTION form. */ 144 private static final int AFTER_OPTION_NAME = 5; 145 /** After its equals sign. */ 146 private static final int AFTER_EQUALS = 6; 147 /** After SET SQL. */ 148 private static final int AFTER_SQL = 7; 149 /** After SET SQL DIALECT. */ 150 private static final int AFTER_DIALECT = 8; 151 152 private int state = IDLE; 153 private boolean quotedIdentifier; 154 155 /** True when {@code t} is the value that turns the quoted-identifier option off. */ 156 boolean turnsOff(TSourceToken t) { 157 if (!t.issolidtoken()) return false; 158 String text = t.toString(); 159 boolean word = t.tokentype == ETokenType.ttkeyword || t.tokentype == ETokenType.ttidentifier; 160 switch (state) { 161 case AFTER_SET: 162 if (word && (same("existing", text) || same("temporary", text))) return false; 163 if (word && same("option", text)) { 164 state = AFTER_OPTION; 165 return false; 166 } 167 if (word && same("sql", text)) { 168 state = AFTER_SQL; 169 return false; 170 } 171 if (word) { 172 quotedIdentifier = setsQuotedIdentifier(text); 173 state = AFTER_NAME; 174 return false; 175 } 176 break; 177 case AFTER_SQL: 178 if (word && same("dialect", text)) { 179 state = AFTER_DIALECT; 180 return false; 181 } 182 break; 183 case AFTER_DIALECT: 184 state = IDLE; 185 // dialects 1 and 2 read a double-quoted text as a string, or refuse it 186 return t.tokentype == ETokenType.ttnumber && !same("3", text); 187 case AFTER_NAME: 188 if (same(",", text)) { 189 state = AFTER_COMMA; 190 return false; 191 } 192 if (word && (same("on", text) || same("off", text))) { 193 state = IDLE; 194 return quotedIdentifier && same("off", text); 195 } 196 break; 197 case AFTER_COMMA: 198 if (word) { 199 quotedIdentifier |= setsQuotedIdentifier(text); 200 state = AFTER_NAME; 201 return false; 202 } 203 break; 204 case AFTER_OPTION: 205 if (word) { 206 quotedIdentifier = same("quoted_identifier", text); 207 state = AFTER_OPTION_NAME; 208 return false; 209 } 210 break; 211 case AFTER_OPTION_NAME: 212 if (same(".", text)) { 213 state = AFTER_OPTION; 214 return false; 215 } 216 if (same("=", text)) { 217 state = AFTER_EQUALS; 218 return false; 219 } 220 break; 221 case AFTER_EQUALS: 222 state = IDLE; 223 // no value resets the option to its default, on 224 return quotedIdentifier && isValue(t) && !turnsOn(text); 225 default: 226 break; 227 } 228 state = IDLE; 229 quotedIdentifier = false; 230 if (word && (t.tokencode == TBaseType.rrw_set || same("set", text))) { 231 state = AFTER_SET; 232 } 233 return false; 234 } 235 236 /** QUOTED_IDENTIFIER, or ANSI_DEFAULTS, which sets QUOTED_IDENTIFIER with the other ISO options. */ 237 private static boolean setsQuotedIdentifier(String option) { 238 return same("quoted_identifier", option) || same("ansi_defaults", option); 239 } 240 241 private static boolean isValue(TSourceToken t) { 242 return t.tokentype == ETokenType.ttkeyword || t.tokentype == ETokenType.ttidentifier 243 || t.tokentype == ETokenType.ttsqstring || t.tokentype == ETokenType.ttnumber; 244 } 245 246 /** On, Yes, True or 1, in quotes or not, turn a switch on; any other value turns it off. */ 247 private static boolean turnsOn(String value) { 248 String v = value; 249 if (v.length() >= 2 && v.charAt(0) == '\'' && v.charAt(v.length() - 1) == '\'') { 250 v = v.substring(1, v.length() - 1); 251 } 252 return same("on", v) || same("yes", v) || same("true", v) || same("1", v); 253 } 254 255 private static boolean same(String a, String b) { 256 return a.equalsIgnoreCase(b); // non-identifier-compare: keywords and option values 257 } 258 } 259}