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}