blob: 58901798b5f79928298c83f9f884cd1088681ad5 [file] [log] [blame]
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.database.sqlite;
18
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060019import android.annotation.NonNull;
20import android.annotation.Nullable;
Mathew Inwood41b31942018-08-10 16:00:53 +010021import android.annotation.UnsupportedAppUsage;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060022import android.content.ContentValues;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070023import android.database.Cursor;
24import android.database.DatabaseUtils;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060025import android.os.Build;
Jeff Browna7771df2012-05-07 20:06:46 -070026import android.os.CancellationSignal;
27import android.os.OperationCanceledException;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070028import android.provider.BaseColumns;
29import android.text.TextUtils;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060030import android.util.ArrayMap;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070031import android.util.Log;
32
Jeff Sharkey0e66ea62019-07-16 16:50:42 -060033import com.android.internal.util.ArrayUtils;
34
Jeff Sharkey42122bf2018-07-26 09:39:18 -060035import libcore.util.EmptyArray;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060036
37import java.util.Arrays;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070038import java.util.Iterator;
Jeff Sharkey7049e652018-09-13 17:05:07 -060039import java.util.List;
Jeff Sharkey0e66ea62019-07-16 16:50:42 -060040import java.util.Locale;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070041import java.util.Map;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070042import java.util.Map.Entry;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060043import java.util.Objects;
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -070044import java.util.Set;
Jeff Sharkey82d783c2019-03-29 15:46:35 -060045import java.util.regex.Matcher;
Owen Linab18d1f2009-05-06 16:45:59 -070046import java.util.regex.Pattern;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070047
48/**
Joshua Baxter3639e2f2018-03-26 14:55:14 -070049 * This is a convenience class that helps build SQL queries to be sent to
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070050 * {@link SQLiteDatabase} objects.
51 */
Jeff Sharkeyb89df9e2018-07-26 14:36:59 -060052public class SQLiteQueryBuilder {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070053 private static final String TAG = "SQLiteQueryBuilder";
Jeff Sharkey82d783c2019-03-29 15:46:35 -060054
Jeff Sharkey82d783c2019-03-29 15:46:35 -060055 private static final Pattern sAggregationPattern = Pattern.compile(
Jeff Sharkey0ab6feb2019-04-10 10:27:15 -060056 "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)");
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070057
58 private Map<String, String> mProjectionMap = null;
Jeff Sharkey7049e652018-09-13 17:05:07 -060059 private List<Pattern> mProjectionGreylist = null;
60
Mathew Inwood31755f92018-12-20 13:53:36 +000061 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070062 private String mTables = "";
Mathew Inwood31755f92018-12-20 13:53:36 +000063 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -070064 private StringBuilder mWhereClause = null; // lazily created
Mathew Inwood31755f92018-12-20 13:53:36 +000065 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070066 private boolean mDistinct;
67 private SQLiteDatabase.CursorFactory mFactory;
Jeff Sharkey0e66ea62019-07-16 16:50:42 -060068
69 private static final int STRICT_PARENTHESES = 1 << 0;
70 private static final int STRICT_COLUMNS = 1 << 1;
71 private static final int STRICT_GRAMMAR = 1 << 2;
72
73 private int mStrictFlags;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070074
75 public SQLiteQueryBuilder() {
76 mDistinct = false;
77 mFactory = null;
78 }
79
80 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -070081 * Mark the query as {@code DISTINCT}.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070082 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -070083 * @param distinct if true the query is {@code DISTINCT}, otherwise it isn't
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070084 */
85 public void setDistinct(boolean distinct) {
86 mDistinct = distinct;
87 }
88
89 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -070090 * Get if the query is marked as {@code DISTINCT}, as last configured by
Jeff Sharkey0ec586b2019-02-14 15:29:16 -070091 * {@link #setDistinct(boolean)}.
92 */
Jeff Sharkeyaa28dff2019-03-25 09:28:34 -060093 public boolean isDistinct() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -070094 return mDistinct;
95 }
96
97 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070098 * Returns the list of tables being queried
99 *
100 * @return the list of tables being queried
101 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700102 public @Nullable String getTables() {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700103 return mTables;
104 }
105
106 /**
107 * Sets the list of tables to query. Multiple tables can be specified to perform a join.
108 * For example:
109 * setTables("foo, bar")
110 * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
111 *
112 * @param inTables the list of tables to query on
113 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700114 public void setTables(@Nullable String inTables) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700115 mTables = inTables;
116 }
117
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700118 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700119 * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded
120 * by parenthesis and {@code AND}ed with the selection passed to {@link #query}. The final
121 * {@code WHERE} clause looks like:
122 * <p>
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700123 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
124 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700125 * @param inWhere the chunk of text to append to the {@code WHERE} clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700126 */
Jeff Sharkeyb89df9e2018-07-26 14:36:59 -0600127 public void appendWhere(@NonNull CharSequence inWhere) {
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -0700128 if (mWhereClause == null) {
129 mWhereClause = new StringBuilder(inWhere.length() + 16);
130 }
Jeff Sharkey6adc98c2018-07-12 19:47:49 -0600131 mWhereClause.append(inWhere);
Jeff Sharkey6adc98c2018-07-12 19:47:49 -0600132 }
133
134 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700135 * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded
Jeff Sharkey91be9262018-07-19 09:30:16 -0600136 * by parenthesis and ANDed with the selection passed to {@link #query}. The final
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700137 * {@code WHERE} clause looks like:
138 * <p>
Jeff Sharkey91be9262018-07-19 09:30:16 -0600139 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
Jeff Sharkey99cc1182018-07-16 14:34:21 -0600140 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700141 * @param inWhere the chunk of text to append to the {@code WHERE} clause. it will be escaped
Jeff Sharkey91be9262018-07-19 09:30:16 -0600142 * to avoid SQL injection attacks
Jeff Sharkey99cc1182018-07-16 14:34:21 -0600143 */
Jeff Sharkeyb89df9e2018-07-26 14:36:59 -0600144 public void appendWhereEscapeString(@NonNull String inWhere) {
Jeff Sharkey99cc1182018-07-16 14:34:21 -0600145 if (mWhereClause == null) {
146 mWhereClause = new StringBuilder(inWhere.length() + 16);
147 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700148 DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
149 }
150
151 /**
Jeff Sharkeyb89df9e2018-07-26 14:36:59 -0600152 * Add a standalone chunk to the {@code WHERE} clause of this query.
153 * <p>
154 * This method differs from {@link #appendWhere(CharSequence)} in that it
155 * automatically appends {@code AND} to any existing {@code WHERE} clause
156 * already under construction before appending the given standalone
157 * expression wrapped in parentheses.
158 *
159 * @param inWhere the standalone expression to append to the {@code WHERE}
160 * clause. It will be wrapped in parentheses when it's appended.
161 */
162 public void appendWhereStandalone(@NonNull CharSequence inWhere) {
163 if (mWhereClause == null) {
164 mWhereClause = new StringBuilder(inWhere.length() + 16);
165 }
166 if (mWhereClause.length() > 0) {
167 mWhereClause.append(" AND ");
168 }
169 mWhereClause.append('(').append(inWhere).append(')');
170 }
171
172 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700173 * Sets the projection map for the query. The projection map maps
174 * from column names that the caller passes into query to database
175 * column names. This is useful for renaming columns as well as
176 * disambiguating column names when doing joins. For example you
177 * could map "name" to "people.name". If a projection map is set
178 * it must contain all column names the user may request, even if
179 * the key and value are the same.
180 *
181 * @param columnMap maps from the user column names to the database column names
182 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700183 public void setProjectionMap(@Nullable Map<String, String> columnMap) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700184 mProjectionMap = columnMap;
185 }
186
187 /**
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700188 * Gets the projection map for the query, as last configured by
189 * {@link #setProjectionMap(Map)}.
190 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700191 public @Nullable Map<String, String> getProjectionMap() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700192 return mProjectionMap;
193 }
194
195 /**
Jeff Sharkey7049e652018-09-13 17:05:07 -0600196 * Sets a projection greylist of columns that will be allowed through, even
197 * when {@link #setStrict(boolean)} is enabled. This provides a way for
198 * abusive custom columns like {@code COUNT(*)} to continue working.
199 *
200 * @hide
201 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700202 public void setProjectionGreylist(@Nullable List<Pattern> projectionGreylist) {
Jeff Sharkey7049e652018-09-13 17:05:07 -0600203 mProjectionGreylist = projectionGreylist;
204 }
205
206 /**
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700207 * Gets the projection greylist for the query, as last configured by
208 * {@link #setProjectionGreylist(List)}.
209 *
210 * @hide
211 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700212 public @Nullable List<Pattern> getProjectionGreylist() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700213 return mProjectionGreylist;
214 }
215
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600216 /**
217 * @deprecated Projection aggregation is now always allowed
218 *
219 * @hide
220 */
221 @Deprecated
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600222 public void setProjectionAggregationAllowed(boolean projectionAggregationAllowed) {
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600223 }
224
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600225 /**
226 * @deprecated Projection aggregation is now always allowed
227 *
228 * @hide
229 */
230 @Deprecated
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600231 public boolean isProjectionAggregationAllowed() {
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600232 return true;
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600233 }
234
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700235 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700236 * Sets the cursor factory to be used for the query. You can use
237 * one factory for all queries on a database but it is normally
Jeff Brown75ea64f2012-01-25 19:37:13 -0800238 * easier to specify the factory when doing this query.
239 *
240 * @param factory the factory to use.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700241 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700242 public void setCursorFactory(@Nullable SQLiteDatabase.CursorFactory factory) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700243 mFactory = factory;
244 }
245
246 /**
Jeff Sharkeyaa28dff2019-03-25 09:28:34 -0600247 * Gets the cursor factory to be used for the query, as last configured by
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700248 * {@link #setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory)}.
249 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700250 public @Nullable SQLiteDatabase.CursorFactory getCursorFactory() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700251 return mFactory;
252 }
253
254 /**
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700255 * When set, the selection is verified against malicious arguments.
256 * When using this class to create a statement using
257 * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
258 * non-numeric limits will raise an exception. If a projection map is specified, fields
259 * not in that map will be ignored.
260 * If this class is used to execute the statement directly using
261 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
262 * or
263 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
264 * additionally also parenthesis escaping selection are caught.
265 *
266 * To summarize: To get maximum protection against malicious third party apps (for example
267 * content provider consumers), make sure to do the following:
268 * <ul>
269 * <li>Set this value to true</li>
270 * <li>Use a projection map</li>
271 * <li>Use one of the query overloads instead of getting the statement as a sql string</li>
272 * </ul>
273 * By default, this value is false.
274 */
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600275 public void setStrict(boolean strict) {
276 if (strict) {
277 mStrictFlags |= STRICT_PARENTHESES;
278 } else {
279 mStrictFlags &= ~STRICT_PARENTHESES;
280 }
Dmitri Plotnikov40eb4aa2010-04-14 16:09:46 -0700281 }
282
283 /**
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700284 * Get if the query is marked as strict, as last configured by
285 * {@link #setStrict(boolean)}.
286 */
Jeff Sharkeyaa28dff2019-03-25 09:28:34 -0600287 public boolean isStrict() {
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600288 return (mStrictFlags & STRICT_PARENTHESES) != 0;
289 }
290
291 /**
292 * When enabled, verify that all projections and {@link ContentValues} only
293 * contain valid columns as defined by {@link #setProjectionMap(Map)}.
294 * <p>
295 * This enforcement applies to {@link #insert}, {@link #query}, and
296 * {@link #update} operations. Any enforcement failures will throw an
297 * {@link IllegalArgumentException}.
298 *
299 * {@hide}
300 */
301 public void setStrictColumns(boolean strictColumns) {
302 if (strictColumns) {
303 mStrictFlags |= STRICT_COLUMNS;
304 } else {
305 mStrictFlags &= ~STRICT_COLUMNS;
306 }
307 }
308
309 /**
310 * Get if the query is marked as strict, as last configured by
311 * {@link #setStrictColumns(boolean)}.
312 *
313 * {@hide}
314 */
315 public boolean isStrictColumns() {
316 return (mStrictFlags & STRICT_COLUMNS) != 0;
317 }
318
319 /**
320 * When enabled, verify that all untrusted SQL conforms to a restricted SQL
321 * grammar. Here are the restrictions applied:
322 * <ul>
323 * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and
324 * windowing terms are rejected.
325 * <li>In {@code GROUP BY} clauses: only valid columns are allowed.
326 * <li>In {@code ORDER BY} clauses: only valid columns, collation, and
327 * ordering terms are allowed.
328 * <li>In {@code LIMIT} clauses: only numerical values and offset terms are
329 * allowed.
330 * </ul>
331 * All column references must be valid as defined by
332 * {@link #setProjectionMap(Map)}.
333 * <p>
334 * This enforcement applies to {@link #query}, {@link #update} and
335 * {@link #delete} operations. This enforcement does not apply to trusted
336 * inputs, such as those provided by {@link #appendWhere}. Any enforcement
337 * failures will throw an {@link IllegalArgumentException}.
338 *
339 * {@hide}
340 */
341 public void setStrictGrammar(boolean strictGrammar) {
342 if (strictGrammar) {
343 mStrictFlags |= STRICT_GRAMMAR;
344 } else {
345 mStrictFlags &= ~STRICT_GRAMMAR;
346 }
347 }
348
349 /**
350 * Get if the query is marked as strict, as last configured by
351 * {@link #setStrictGrammar(boolean)}.
352 *
353 * {@hide}
354 */
355 public boolean isStrictGrammar() {
356 return (mStrictFlags & STRICT_GRAMMAR) != 0;
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700357 }
358
359 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700360 * Build an SQL query string from the given clauses.
361 *
362 * @param distinct true if you want each row to be unique, false otherwise.
363 * @param tables The table names to compile the query against.
364 * @param columns A list of which columns to return. Passing null will
365 * return all columns, which is discouraged to prevent reading
366 * data from storage that isn't going to be used.
367 * @param where A filter declaring which rows to return, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700368 * {@code WHERE} clause (excluding the {@code WHERE} itself). Passing {@code null} will
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700369 * return all rows for the given URL.
370 * @param groupBy A filter declaring how to group rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700371 * {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). Passing {@code null}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700372 * will cause the rows to not be grouped.
373 * @param having A filter declare which row groups to include in the cursor,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700374 * if row grouping is being used, formatted as an SQL {@code HAVING}
375 * clause (excluding the {@code HAVING} itself). Passing null will cause
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700376 * all row groups to be included, and is required when row
377 * grouping is not being used.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700378 * @param orderBy How to order the rows, formatted as an SQL {@code ORDER BY} clause
379 * (excluding the {@code ORDER BY} itself). Passing null will use the
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700380 * default sort order, which may be unordered.
381 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700382 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700383 * @return the SQL query string
384 */
385 public static String buildQueryString(
386 boolean distinct, String tables, String[] columns, String where,
387 String groupBy, String having, String orderBy, String limit) {
388 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
389 throw new IllegalArgumentException(
390 "HAVING clauses are only permitted when using a groupBy clause");
391 }
392
393 StringBuilder query = new StringBuilder(120);
394
395 query.append("SELECT ");
396 if (distinct) {
397 query.append("DISTINCT ");
398 }
399 if (columns != null && columns.length != 0) {
400 appendColumns(query, columns);
401 } else {
402 query.append("* ");
403 }
404 query.append("FROM ");
405 query.append(tables);
406 appendClause(query, " WHERE ", where);
407 appendClause(query, " GROUP BY ", groupBy);
408 appendClause(query, " HAVING ", having);
409 appendClause(query, " ORDER BY ", orderBy);
Owen Linab18d1f2009-05-06 16:45:59 -0700410 appendClause(query, " LIMIT ", limit);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700411
412 return query.toString();
413 }
414
415 private static void appendClause(StringBuilder s, String name, String clause) {
416 if (!TextUtils.isEmpty(clause)) {
417 s.append(name);
418 s.append(clause);
419 }
420 }
421
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700422 /**
423 * Add the names that are non-null in columns to s, separating
424 * them with commas.
425 */
426 public static void appendColumns(StringBuilder s, String[] columns) {
427 int n = columns.length;
428
429 for (int i = 0; i < n; i++) {
430 String column = columns[i];
431
432 if (column != null) {
433 if (i > 0) {
434 s.append(", ");
435 }
436 s.append(column);
437 }
438 }
439 s.append(' ');
440 }
441
442 /**
443 * Perform a query by combining all current settings and the
444 * information passed into this method.
445 *
446 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600447 * @param projectionIn A list of which columns to return. Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700448 * null will return all columns, which is discouraged to prevent
449 * reading data from storage that isn't going to be used.
450 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700451 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700452 * itself). Passing null will return all rows for the given URL.
453 * @param selectionArgs You may include ?s in selection, which
454 * will be replaced by the values from selectionArgs, in order
455 * that they appear in the selection. The values will be bound
456 * as Strings.
457 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700458 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700459 * itself). Passing null will cause the rows to not be grouped.
460 * @param having A filter declare which row groups to include in
461 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700462 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700463 * null will cause all row groups to be included, and is
464 * required when row grouping is not being used.
465 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700466 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700467 * will use the default sort order, which may be unordered.
468 * @return a cursor over the result set
469 * @see android.content.ContentResolver#query(android.net.Uri, String[],
470 * String, String[], String)
471 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600472 public Cursor query(SQLiteDatabase db, String[] projectionIn,
473 String selection, String[] selectionArgs, String groupBy,
474 String having, String sortOrder) {
475 return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
Jeff Brown4c1241d2012-02-02 17:05:00 -0800476 null /* limit */, null /* cancellationSignal */);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700477 }
478
479 /**
480 * Perform a query by combining all current settings and the
481 * information passed into this method.
482 *
483 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600484 * @param projectionIn A list of which columns to return. Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700485 * null will return all columns, which is discouraged to prevent
486 * reading data from storage that isn't going to be used.
487 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700488 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700489 * itself). Passing null will return all rows for the given URL.
490 * @param selectionArgs You may include ?s in selection, which
491 * will be replaced by the values from selectionArgs, in order
492 * that they appear in the selection. The values will be bound
493 * as Strings.
494 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700495 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700496 * itself). Passing null will cause the rows to not be grouped.
497 * @param having A filter declare which row groups to include in
498 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700499 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700500 * null will cause all row groups to be included, and is
501 * required when row grouping is not being used.
502 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700503 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700504 * will use the default sort order, which may be unordered.
505 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700506 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700507 * @return a cursor over the result set
508 * @see android.content.ContentResolver#query(android.net.Uri, String[],
509 * String, String[], String)
510 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600511 public Cursor query(SQLiteDatabase db, String[] projectionIn,
512 String selection, String[] selectionArgs, String groupBy,
513 String having, String sortOrder, String limit) {
514 return query(db, projectionIn, selection, selectionArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800515 groupBy, having, sortOrder, limit, null);
516 }
517
518 /**
519 * Perform a query by combining all current settings and the
520 * information passed into this method.
521 *
522 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600523 * @param projectionIn A list of which columns to return. Passing
Jeff Brown75ea64f2012-01-25 19:37:13 -0800524 * null will return all columns, which is discouraged to prevent
525 * reading data from storage that isn't going to be used.
526 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700527 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Brown75ea64f2012-01-25 19:37:13 -0800528 * itself). Passing null will return all rows for the given URL.
529 * @param selectionArgs You may include ?s in selection, which
530 * will be replaced by the values from selectionArgs, in order
531 * that they appear in the selection. The values will be bound
532 * as Strings.
533 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700534 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
Jeff Brown75ea64f2012-01-25 19:37:13 -0800535 * itself). Passing null will cause the rows to not be grouped.
536 * @param having A filter declare which row groups to include in
537 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700538 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
Jeff Brown75ea64f2012-01-25 19:37:13 -0800539 * null will cause all row groups to be included, and is
540 * required when row grouping is not being used.
541 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700542 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
Jeff Brown75ea64f2012-01-25 19:37:13 -0800543 * will use the default sort order, which may be unordered.
544 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700545 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
Jeff Brown4c1241d2012-02-02 17:05:00 -0800546 * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
Jeff Brown75ea64f2012-01-25 19:37:13 -0800547 * If the operation is canceled, then {@link OperationCanceledException} will be thrown
548 * when the query is executed.
549 * @return a cursor over the result set
550 * @see android.content.ContentResolver#query(android.net.Uri, String[],
551 * String, String[], String)
552 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600553 public Cursor query(SQLiteDatabase db, String[] projectionIn,
554 String selection, String[] selectionArgs, String groupBy,
555 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
556 if (mTables == null) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700557 return null;
558 }
559
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600560 final String sql;
561 final String unwrappedSql = buildQuery(
562 projectionIn, selection, groupBy, having,
563 sortOrder, limit);
564
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600565 if (isStrictColumns()) {
566 enforceStrictColumns(projectionIn);
567 }
568 if (isStrictGrammar()) {
569 enforceStrictGrammar(selection, groupBy, having, sortOrder, limit);
570 }
571 if (isStrict()) {
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700572 // Validate the user-supplied selection to detect syntactic anomalies
573 // in the selection string that could indicate a SQL injection attempt.
574 // The idea is to ensure that the selection clause is a valid SQL expression
575 // by compiling it twice: once wrapped in parentheses and once as
576 // originally specified. An attacker cannot create an expression that
577 // would escape the SQL expression while maintaining balanced parentheses
578 // in both the wrapped and original forms.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700579
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600580 // NOTE: The ordering of the below operations is important; we must
581 // execute the wrapped query to ensure the untrusted clause has been
582 // fully isolated.
583
584 // Validate the unwrapped query
585 db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
586
587 // Execute wrapped query for extra protection
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600588 final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600589 wrap(having), sortOrder, limit);
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600590 sql = wrappedSql;
591 } else {
592 // Execute unwrapped query
593 sql = unwrappedSql;
594 }
Jeff Sharkey6adc98c2018-07-12 19:47:49 -0600595
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600596 final String[] sqlArgs = selectionArgs;
Jeff Sharkey91be9262018-07-19 09:30:16 -0600597 if (Log.isLoggable(TAG, Log.DEBUG)) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600598 if (Build.IS_DEBUGGABLE) {
599 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
600 } else {
601 Log.d(TAG, sql);
602 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700603 }
604 return db.rawQueryWithFactory(
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600605 mFactory, sql, sqlArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800606 SQLiteDatabase.findEditTable(mTables),
Jeff Brown4c1241d2012-02-02 17:05:00 -0800607 cancellationSignal); // will throw if query is invalid
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700608 }
609
610 /**
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600611 * Perform an insert by combining all current settings and the
612 * information passed into this method.
613 *
614 * @param db the database to insert on
615 * @return the row ID of the newly inserted row, or -1 if an error occurred
616 *
617 * {@hide}
618 */
619 public long insert(@NonNull SQLiteDatabase db, @NonNull ContentValues values) {
620 Objects.requireNonNull(mTables, "No tables defined");
621 Objects.requireNonNull(db, "No database defined");
622 Objects.requireNonNull(values, "No values defined");
623
624 if (isStrictColumns()) {
625 enforceStrictColumns(values);
626 }
627
628 final String sql = buildInsert(values);
629
630 final ArrayMap<String, Object> rawValues = values.getValues();
631 final int valuesLength = rawValues.size();
632 final Object[] sqlArgs = new Object[valuesLength];
633 for (int i = 0; i < sqlArgs.length; i++) {
634 sqlArgs[i] = rawValues.valueAt(i);
635 }
636 if (Log.isLoggable(TAG, Log.DEBUG)) {
637 if (Build.IS_DEBUGGABLE) {
638 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
639 } else {
640 Log.d(TAG, sql);
641 }
642 }
643 return db.executeSql(sql, sqlArgs);
644 }
645
646 /**
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600647 * Perform an update by combining all current settings and the
648 * information passed into this method.
649 *
650 * @param db the database to update on
651 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700652 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600653 * itself). Passing null will return all rows for the given URL.
654 * @param selectionArgs You may include ?s in selection, which
655 * will be replaced by the values from selectionArgs, in order
656 * that they appear in the selection. The values will be bound
657 * as Strings.
658 * @return the number of rows updated
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600659 */
660 public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values,
661 @Nullable String selection, @Nullable String[] selectionArgs) {
662 Objects.requireNonNull(mTables, "No tables defined");
663 Objects.requireNonNull(db, "No database defined");
664 Objects.requireNonNull(values, "No values defined");
665
666 final String sql;
667 final String unwrappedSql = buildUpdate(values, selection);
668
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600669 if (isStrictColumns()) {
670 enforceStrictColumns(values);
671 }
672 if (isStrictGrammar()) {
673 enforceStrictGrammar(selection, null, null, null, null);
674 }
675 if (isStrict()) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600676 // Validate the user-supplied selection to detect syntactic anomalies
677 // in the selection string that could indicate a SQL injection attempt.
678 // The idea is to ensure that the selection clause is a valid SQL expression
679 // by compiling it twice: once wrapped in parentheses and once as
680 // originally specified. An attacker cannot create an expression that
681 // would escape the SQL expression while maintaining balanced parentheses
682 // in both the wrapped and original forms.
683
684 // NOTE: The ordering of the below operations is important; we must
685 // execute the wrapped query to ensure the untrusted clause has been
686 // fully isolated.
687
688 // Validate the unwrapped query
689 db.validateSql(unwrappedSql, null); // will throw if query is invalid
690
691 // Execute wrapped query for extra protection
692 final String wrappedSql = buildUpdate(values, wrap(selection));
693 sql = wrappedSql;
694 } else {
695 // Execute unwrapped query
696 sql = unwrappedSql;
697 }
698
Jeff Sharkey42122bf2018-07-26 09:39:18 -0600699 if (selectionArgs == null) {
700 selectionArgs = EmptyArray.STRING;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600701 }
Jeff Sharkey42122bf2018-07-26 09:39:18 -0600702 final ArrayMap<String, Object> rawValues = values.getValues();
703 final int valuesLength = rawValues.size();
704 final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length];
705 for (int i = 0; i < sqlArgs.length; i++) {
706 if (i < valuesLength) {
707 sqlArgs[i] = rawValues.valueAt(i);
708 } else {
709 sqlArgs[i] = selectionArgs[i - valuesLength];
710 }
711 }
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600712 if (Log.isLoggable(TAG, Log.DEBUG)) {
713 if (Build.IS_DEBUGGABLE) {
714 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
715 } else {
716 Log.d(TAG, sql);
717 }
718 }
719 return db.executeSql(sql, sqlArgs);
720 }
721
722 /**
723 * Perform a delete by combining all current settings and the
724 * information passed into this method.
725 *
726 * @param db the database to delete on
727 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700728 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600729 * itself). Passing null will return all rows for the given URL.
730 * @param selectionArgs You may include ?s in selection, which
731 * will be replaced by the values from selectionArgs, in order
732 * that they appear in the selection. The values will be bound
733 * as Strings.
734 * @return the number of rows deleted
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600735 */
736 public int delete(@NonNull SQLiteDatabase db, @Nullable String selection,
737 @Nullable String[] selectionArgs) {
738 Objects.requireNonNull(mTables, "No tables defined");
739 Objects.requireNonNull(db, "No database defined");
740
741 final String sql;
742 final String unwrappedSql = buildDelete(selection);
743
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600744 if (isStrictGrammar()) {
745 enforceStrictGrammar(selection, null, null, null, null);
746 }
747 if (isStrict()) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600748 // Validate the user-supplied selection to detect syntactic anomalies
749 // in the selection string that could indicate a SQL injection attempt.
750 // The idea is to ensure that the selection clause is a valid SQL expression
751 // by compiling it twice: once wrapped in parentheses and once as
752 // originally specified. An attacker cannot create an expression that
753 // would escape the SQL expression while maintaining balanced parentheses
754 // in both the wrapped and original forms.
755
756 // NOTE: The ordering of the below operations is important; we must
757 // execute the wrapped query to ensure the untrusted clause has been
758 // fully isolated.
759
760 // Validate the unwrapped query
761 db.validateSql(unwrappedSql, null); // will throw if query is invalid
762
763 // Execute wrapped query for extra protection
764 final String wrappedSql = buildDelete(wrap(selection));
765 sql = wrappedSql;
766 } else {
767 // Execute unwrapped query
768 sql = unwrappedSql;
769 }
770
771 final String[] sqlArgs = selectionArgs;
772 if (Log.isLoggable(TAG, Log.DEBUG)) {
773 if (Build.IS_DEBUGGABLE) {
774 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
775 } else {
776 Log.d(TAG, sql);
777 }
778 }
779 return db.executeSql(sql, sqlArgs);
780 }
781
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600782 private void enforceStrictColumns(@Nullable String[] projection) {
783 Objects.requireNonNull(mProjectionMap, "No projection map defined");
784
785 computeProjection(projection);
786 }
787
788 private void enforceStrictColumns(@NonNull ContentValues values) {
789 Objects.requireNonNull(mProjectionMap, "No projection map defined");
790
791 final ArrayMap<String, Object> rawValues = values.getValues();
792 for (int i = 0; i < rawValues.size(); i++) {
793 final String column = rawValues.keyAt(i);
794 if (!mProjectionMap.containsKey(column)) {
795 throw new IllegalArgumentException("Invalid column " + column);
796 }
797 }
798 }
799
800 private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy,
801 @Nullable String having, @Nullable String sortOrder, @Nullable String limit) {
802 SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE,
803 this::enforceStrictGrammarWhereHaving);
804 SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE,
805 this::enforceStrictGrammarGroupBy);
806 SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE,
807 this::enforceStrictGrammarWhereHaving);
808 SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE,
809 this::enforceStrictGrammarOrderBy);
810 SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE,
811 this::enforceStrictGrammarLimit);
812 }
813
814 private void enforceStrictGrammarWhereHaving(@NonNull String token) {
815 if (isTableOrColumn(token)) return;
816 if (SQLiteTokenizer.isFunction(token)) return;
817 if (SQLiteTokenizer.isType(token)) return;
818
819 // NOTE: we explicitly don't allow SELECT subqueries, since they could
820 // leak data that should have been filtered by the trusted where clause
821 switch (token.toUpperCase(Locale.US)) {
822 case "AND": case "AS": case "BETWEEN": case "BINARY":
823 case "CASE": case "CAST": case "COLLATE": case "DISTINCT":
824 case "ELSE": case "END": case "ESCAPE": case "EXISTS":
825 case "GLOB": case "IN": case "IS": case "ISNULL":
826 case "LIKE": case "MATCH": case "NOCASE": case "NOT":
827 case "NOTNULL": case "NULL": case "OR": case "REGEXP":
828 case "RTRIM": case "THEN": case "WHEN":
829 return;
830 }
831 throw new IllegalArgumentException("Invalid token " + token);
832 }
833
834 private void enforceStrictGrammarGroupBy(@NonNull String token) {
835 if (isTableOrColumn(token)) return;
836 throw new IllegalArgumentException("Invalid token " + token);
837 }
838
839 private void enforceStrictGrammarOrderBy(@NonNull String token) {
840 if (isTableOrColumn(token)) return;
841 switch (token.toUpperCase(Locale.US)) {
842 case "COLLATE": case "ASC": case "DESC":
843 case "BINARY": case "RTRIM": case "NOCASE":
844 return;
845 }
846 throw new IllegalArgumentException("Invalid token " + token);
847 }
848
849 private void enforceStrictGrammarLimit(@NonNull String token) {
850 switch (token.toUpperCase(Locale.US)) {
851 case "OFFSET":
852 return;
853 }
854 throw new IllegalArgumentException("Invalid token " + token);
855 }
856
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600857 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700858 * Construct a {@code SELECT} statement suitable for use in a group of
859 * {@code SELECT} statements that will be joined through {@code UNION} operators
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700860 * in buildUnionQuery.
861 *
862 * @param projectionIn A list of which columns to return. Passing
863 * null will return all columns, which is discouraged to
864 * prevent reading data from storage that isn't going to be
865 * used.
866 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700867 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700868 * itself). Passing null will return all rows for the given
869 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700870 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700871 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself).
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700872 * Passing null will cause the rows to not be grouped.
873 * @param having A filter declare which row groups to include in
874 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700875 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700876 * null will cause all row groups to be included, and is
877 * required when row grouping is not being used.
878 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700879 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700880 * will use the default sort order, which may be unordered.
881 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700882 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
883 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700884 */
885 public String buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100886 String[] projectionIn, String selection, String groupBy,
887 String having, String sortOrder, String limit) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700888 String[] projection = computeProjection(projectionIn);
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600889 String where = computeWhere(selection);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700890
891 return buildQueryString(
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600892 mDistinct, mTables, projection, where,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700893 groupBy, having, sortOrder, limit);
894 }
895
896 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100897 * @deprecated This method's signature is misleading since no SQL parameter
898 * substitution is carried out. The selection arguments parameter does not get
899 * used at all. To avoid confusion, call
900 * {@link #buildQuery(String[], String, String, String, String, String)} instead.
901 */
902 @Deprecated
903 public String buildQuery(
904 String[] projectionIn, String selection, String[] selectionArgs,
905 String groupBy, String having, String sortOrder, String limit) {
906 return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
907 }
908
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600909 /** {@hide} */
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600910 public String buildInsert(ContentValues values) {
911 if (values == null || values.isEmpty()) {
912 throw new IllegalArgumentException("Empty values");
913 }
914
915 StringBuilder sql = new StringBuilder(120);
916 sql.append("INSERT INTO ");
917 sql.append(SQLiteDatabase.findEditTable(mTables));
918 sql.append(" (");
919
920 final ArrayMap<String, Object> rawValues = values.getValues();
921 for (int i = 0; i < rawValues.size(); i++) {
922 if (i > 0) {
923 sql.append(',');
924 }
925 sql.append(rawValues.keyAt(i));
926 }
927 sql.append(") VALUES (");
928 for (int i = 0; i < rawValues.size(); i++) {
929 if (i > 0) {
930 sql.append(',');
931 }
932 sql.append('?');
933 }
934 sql.append(")");
935 return sql.toString();
936 }
937
938 /** {@hide} */
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600939 public String buildUpdate(ContentValues values, String selection) {
940 if (values == null || values.isEmpty()) {
941 throw new IllegalArgumentException("Empty values");
942 }
943
944 StringBuilder sql = new StringBuilder(120);
945 sql.append("UPDATE ");
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600946 sql.append(SQLiteDatabase.findEditTable(mTables));
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600947 sql.append(" SET ");
948
949 final ArrayMap<String, Object> rawValues = values.getValues();
950 for (int i = 0; i < rawValues.size(); i++) {
951 if (i > 0) {
952 sql.append(',');
953 }
954 sql.append(rawValues.keyAt(i));
955 sql.append("=?");
956 }
957
958 final String where = computeWhere(selection);
959 appendClause(sql, " WHERE ", where);
960 return sql.toString();
961 }
962
963 /** {@hide} */
964 public String buildDelete(String selection) {
965 StringBuilder sql = new StringBuilder(120);
966 sql.append("DELETE FROM ");
Jeff Sharkey0e66ea62019-07-16 16:50:42 -0600967 sql.append(SQLiteDatabase.findEditTable(mTables));
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600968
969 final String where = computeWhere(selection);
970 appendClause(sql, " WHERE ", where);
971 return sql.toString();
972 }
973
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100974 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700975 * Construct a {@code SELECT} statement suitable for use in a group of
976 * {@code SELECT} statements that will be joined through {@code UNION} operators
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700977 * in buildUnionQuery.
978 *
979 * @param typeDiscriminatorColumn the name of the result column
980 * whose cells will contain the name of the table from which
981 * each row was drawn.
982 * @param unionColumns the names of the columns to appear in the
983 * result. This may include columns that do not appear in the
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700984 * table this {@code SELECT} is querying (i.e. mTables), but that do
985 * appear in one of the other tables in the {@code UNION} query that we
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700986 * are constructing.
987 * @param columnsPresentInTable a Set of the names of the columns
988 * that appear in this table (i.e. in the table whose name is
989 * mTables). Since columns in unionColumns include columns that
990 * appear only in other tables, we use this array to distinguish
991 * which ones actually are present. Other columns will have
992 * NULL values for results from this subquery.
993 * @param computedColumnsOffset all columns in unionColumns before
994 * this index are included under the assumption that they're
995 * computed and therefore won't appear in columnsPresentInTable,
996 * e.g. "date * 1000 as normalized_date"
997 * @param typeDiscriminatorValue the value used for the
998 * type-discriminator column in this subquery
999 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001000 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001001 * itself). Passing null will return all rows for the given
1002 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001003 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001004 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself).
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001005 * Passing null will cause the rows to not be grouped.
1006 * @param having A filter declare which row groups to include in
1007 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001008 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001009 * null will cause all row groups to be included, and is
1010 * required when row grouping is not being used.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001011 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001012 */
1013 public String buildUnionSubQuery(
1014 String typeDiscriminatorColumn,
1015 String[] unionColumns,
1016 Set<String> columnsPresentInTable,
1017 int computedColumnsOffset,
1018 String typeDiscriminatorValue,
1019 String selection,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001020 String groupBy,
1021 String having) {
1022 int unionColumnsCount = unionColumns.length;
1023 String[] projectionIn = new String[unionColumnsCount];
1024
1025 for (int i = 0; i < unionColumnsCount; i++) {
1026 String unionColumn = unionColumns[i];
1027
1028 if (unionColumn.equals(typeDiscriminatorColumn)) {
1029 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
1030 + typeDiscriminatorColumn;
1031 } else if (i <= computedColumnsOffset
1032 || columnsPresentInTable.contains(unionColumn)) {
1033 projectionIn[i] = unionColumn;
1034 } else {
1035 projectionIn[i] = "NULL AS " + unionColumn;
1036 }
1037 }
1038 return buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +01001039 projectionIn, selection, groupBy, having,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001040 null /* sortOrder */,
1041 null /* limit */);
1042 }
1043
1044 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +01001045 * @deprecated This method's signature is misleading since no SQL parameter
1046 * substitution is carried out. The selection arguments parameter does not get
1047 * used at all. To avoid confusion, call
Jean-Baptiste Queruf4072fc2010-11-17 16:47:59 -08001048 * {@link #buildUnionSubQuery}
Jonas Schwertfeger84029032010-11-12 11:42:28 +01001049 * instead.
1050 */
1051 @Deprecated
1052 public String buildUnionSubQuery(
1053 String typeDiscriminatorColumn,
1054 String[] unionColumns,
1055 Set<String> columnsPresentInTable,
1056 int computedColumnsOffset,
1057 String typeDiscriminatorValue,
1058 String selection,
1059 String[] selectionArgs,
1060 String groupBy,
1061 String having) {
1062 return buildUnionSubQuery(
1063 typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
1064 computedColumnsOffset, typeDiscriminatorValue, selection,
1065 groupBy, having);
1066 }
1067
1068 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001069 * Given a set of subqueries, all of which are {@code SELECT} statements,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001070 * construct a query that returns the union of what those
1071 * subqueries return.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001072 * @param subQueries an array of SQL {@code SELECT} statements, all of
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001073 * which must have the same columns as the same positions in
1074 * their results
1075 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001076 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001077 * null will use the default sort order, which may be unordered.
1078 * @param limit The limit clause, which applies to the entire union result set
1079 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001080 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001081 */
1082 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
1083 StringBuilder query = new StringBuilder(128);
1084 int subQueryCount = subQueries.length;
1085 String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
1086
1087 for (int i = 0; i < subQueryCount; i++) {
1088 if (i > 0) {
1089 query.append(unionOperator);
1090 }
1091 query.append(subQueries[i]);
1092 }
1093 appendClause(query, " ORDER BY ", sortOrder);
1094 appendClause(query, " LIMIT ", limit);
1095 return query.toString();
1096 }
1097
Jeff Sharkey82d783c2019-03-29 15:46:35 -06001098 private static @NonNull String maybeWithOperator(@Nullable String operator,
1099 @NonNull String column) {
1100 if (operator != null) {
1101 return operator + "(" + column + ")";
1102 } else {
1103 return column;
1104 }
1105 }
1106
1107 /** {@hide} */
Mathew Inwood31755f92018-12-20 13:53:36 +00001108 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Jeff Sharkey0e66ea62019-07-16 16:50:42 -06001109 public @Nullable String[] computeProjection(@Nullable String[] projectionIn) {
1110 if (!ArrayUtils.isEmpty(projectionIn)) {
1111 String[] projectionOut = new String[projectionIn.length];
1112 for (int i = 0; i < projectionIn.length; i++) {
1113 projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001114 }
Jeff Sharkey0e66ea62019-07-16 16:50:42 -06001115 return projectionOut;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001116 } else if (mProjectionMap != null) {
1117 // Return all columns in projection map.
1118 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
1119 String[] projection = new String[entrySet.size()];
1120 Iterator<Entry<String, String>> entryIter = entrySet.iterator();
1121 int i = 0;
1122
1123 while (entryIter.hasNext()) {
1124 Entry<String, String> entry = entryIter.next();
1125
1126 // Don't include the _count column when people ask for no projection.
1127 if (entry.getKey().equals(BaseColumns._COUNT)) {
1128 continue;
1129 }
1130 projection[i++] = entry.getValue();
1131 }
1132 return projection;
1133 }
1134 return null;
1135 }
Jeff Sharkeyb13ea302018-07-25 14:52:14 -06001136
Jeff Sharkey0e66ea62019-07-16 16:50:42 -06001137 private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) {
1138 final String column = computeSingleProjection(userColumn);
1139 if (column != null) {
1140 return column;
1141 } else {
1142 throw new IllegalArgumentException("Invalid column " + userColumn);
1143 }
1144 }
1145
1146 private @Nullable String computeSingleProjection(@NonNull String userColumn) {
1147 // When no mapping provided, anything goes
1148 if (mProjectionMap == null) {
1149 return userColumn;
1150 }
1151
1152 String operator = null;
1153 String column = mProjectionMap.get(userColumn);
1154
1155 // When no direct match found, look for aggregation
1156 if (column == null) {
1157 final Matcher matcher = sAggregationPattern.matcher(userColumn);
1158 if (matcher.matches()) {
1159 operator = matcher.group(1);
1160 userColumn = matcher.group(2);
1161 column = mProjectionMap.get(userColumn);
1162 }
1163 }
1164
1165 if (column != null) {
1166 return maybeWithOperator(operator, column);
1167 }
1168
1169 if (mStrictFlags == 0
1170 && (userColumn.contains(" AS ") || userColumn.contains(" as "))) {
1171 /* A column alias already exist */
1172 return maybeWithOperator(operator, userColumn);
1173 }
1174
1175 // If greylist is configured, we might be willing to let
1176 // this custom column bypass our strict checks.
1177 if (mProjectionGreylist != null) {
1178 boolean match = false;
1179 for (Pattern p : mProjectionGreylist) {
1180 if (p.matcher(userColumn).matches()) {
1181 match = true;
1182 break;
1183 }
1184 }
1185
1186 if (match) {
1187 Log.w(TAG, "Allowing abusive custom column: " + userColumn);
1188 return maybeWithOperator(operator, userColumn);
1189 }
1190 }
1191
1192 return null;
1193 }
1194
1195 private boolean isTableOrColumn(String token) {
1196 if (mTables.equals(token)) return true;
1197 return computeSingleProjection(token) != null;
1198 }
1199
Jeff Sharkey82d783c2019-03-29 15:46:35 -06001200 /** {@hide} */
1201 public @Nullable String computeWhere(@Nullable String selection) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -06001202 final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
1203 final boolean hasExternal = !TextUtils.isEmpty(selection);
1204
1205 if (hasInternal || hasExternal) {
1206 final StringBuilder where = new StringBuilder();
1207 if (hasInternal) {
1208 where.append('(').append(mWhereClause).append(')');
1209 }
1210 if (hasInternal && hasExternal) {
1211 where.append(" AND ");
1212 }
1213 if (hasExternal) {
1214 where.append('(').append(selection).append(')');
1215 }
1216 return where.toString();
1217 } else {
1218 return null;
1219 }
1220 }
1221
1222 /**
1223 * Wrap given argument in parenthesis, unless it's {@code null} or
1224 * {@code ()}, in which case return it verbatim.
1225 */
1226 private @Nullable String wrap(@Nullable String arg) {
1227 if (TextUtils.isEmpty(arg)) {
1228 return arg;
1229 } else {
1230 return "(" + arg + ")";
1231 }
1232 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001233}