blob: f71377052a712ea801dfcadc348b37005c69583a [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 Sharkeyae6a4662019-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 Sharkeyae6a4662019-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 Sharkeyae6a4662019-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 Sharkey82d783c2019-03-29 15:46:35 -0600216 /** {@hide} */
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600217 @Deprecated
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600218 public void setProjectionAggregationAllowed(boolean projectionAggregationAllowed) {
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600219 }
220
221 /** {@hide} */
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600222 @Deprecated
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600223 public boolean isProjectionAggregationAllowed() {
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600224 return true;
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600225 }
226
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700227 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700228 * Sets the cursor factory to be used for the query. You can use
229 * one factory for all queries on a database but it is normally
Jeff Brown75ea64f2012-01-25 19:37:13 -0800230 * easier to specify the factory when doing this query.
231 *
232 * @param factory the factory to use.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700233 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700234 public void setCursorFactory(@Nullable SQLiteDatabase.CursorFactory factory) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700235 mFactory = factory;
236 }
237
238 /**
Jeff Sharkeyaa28dff2019-03-25 09:28:34 -0600239 * Gets the cursor factory to be used for the query, as last configured by
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700240 * {@link #setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory)}.
241 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700242 public @Nullable SQLiteDatabase.CursorFactory getCursorFactory() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700243 return mFactory;
244 }
245
246 /**
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700247 * When set, the selection is verified against malicious arguments.
248 * When using this class to create a statement using
249 * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
250 * non-numeric limits will raise an exception. If a projection map is specified, fields
251 * not in that map will be ignored.
252 * If this class is used to execute the statement directly using
253 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
254 * or
255 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
256 * additionally also parenthesis escaping selection are caught.
257 *
258 * To summarize: To get maximum protection against malicious third party apps (for example
259 * content provider consumers), make sure to do the following:
260 * <ul>
261 * <li>Set this value to true</li>
262 * <li>Use a projection map</li>
263 * <li>Use one of the query overloads instead of getting the statement as a sql string</li>
264 * </ul>
265 * By default, this value is false.
266 */
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600267 public void setStrict(boolean strict) {
268 if (strict) {
269 mStrictFlags |= STRICT_PARENTHESES;
270 } else {
271 mStrictFlags &= ~STRICT_PARENTHESES;
272 }
Dmitri Plotnikov40eb4aa2010-04-14 16:09:46 -0700273 }
274
275 /**
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700276 * Get if the query is marked as strict, as last configured by
277 * {@link #setStrict(boolean)}.
278 */
Jeff Sharkeyaa28dff2019-03-25 09:28:34 -0600279 public boolean isStrict() {
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600280 return (mStrictFlags & STRICT_PARENTHESES) != 0;
281 }
282
283 /**
284 * When enabled, verify that all projections and {@link ContentValues} only
285 * contain valid columns as defined by {@link #setProjectionMap(Map)}.
286 * <p>
287 * This enforcement applies to {@link #insert}, {@link #query}, and
288 * {@link #update} operations. Any enforcement failures will throw an
289 * {@link IllegalArgumentException}.
290 */
291 public void setStrictColumns(boolean strictColumns) {
292 if (strictColumns) {
293 mStrictFlags |= STRICT_COLUMNS;
294 } else {
295 mStrictFlags &= ~STRICT_COLUMNS;
296 }
297 }
298
299 /**
300 * Get if the query is marked as strict, as last configured by
301 * {@link #setStrictColumns(boolean)}.
302 */
303 public boolean isStrictColumns() {
304 return (mStrictFlags & STRICT_COLUMNS) != 0;
305 }
306
307 /**
308 * When enabled, verify that all untrusted SQL conforms to a restricted SQL
309 * grammar. Here are the restrictions applied:
310 * <ul>
311 * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and
312 * windowing terms are rejected.
313 * <li>In {@code GROUP BY} clauses: only valid columns are allowed.
314 * <li>In {@code ORDER BY} clauses: only valid columns, collation, and
315 * ordering terms are allowed.
316 * <li>In {@code LIMIT} clauses: only numerical values and offset terms are
317 * allowed.
318 * </ul>
319 * All column references must be valid as defined by
320 * {@link #setProjectionMap(Map)}.
321 * <p>
322 * This enforcement applies to {@link #query}, {@link #update} and
323 * {@link #delete} operations. This enforcement does not apply to trusted
324 * inputs, such as those provided by {@link #appendWhere}. Any enforcement
325 * failures will throw an {@link IllegalArgumentException}.
326 */
327 public void setStrictGrammar(boolean strictGrammar) {
328 if (strictGrammar) {
329 mStrictFlags |= STRICT_GRAMMAR;
330 } else {
331 mStrictFlags &= ~STRICT_GRAMMAR;
332 }
333 }
334
335 /**
336 * Get if the query is marked as strict, as last configured by
337 * {@link #setStrictGrammar(boolean)}.
338 */
339 public boolean isStrictGrammar() {
340 return (mStrictFlags & STRICT_GRAMMAR) != 0;
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700341 }
342
343 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700344 * Build an SQL query string from the given clauses.
345 *
346 * @param distinct true if you want each row to be unique, false otherwise.
347 * @param tables The table names to compile the query against.
348 * @param columns A list of which columns to return. Passing null will
349 * return all columns, which is discouraged to prevent reading
350 * data from storage that isn't going to be used.
351 * @param where A filter declaring which rows to return, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700352 * {@code WHERE} clause (excluding the {@code WHERE} itself). Passing {@code null} will
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700353 * return all rows for the given URL.
354 * @param groupBy A filter declaring how to group rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700355 * {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). Passing {@code null}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700356 * will cause the rows to not be grouped.
357 * @param having A filter declare which row groups to include in the cursor,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700358 * if row grouping is being used, formatted as an SQL {@code HAVING}
359 * clause (excluding the {@code HAVING} itself). Passing null will cause
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700360 * all row groups to be included, and is required when row
361 * grouping is not being used.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700362 * @param orderBy How to order the rows, formatted as an SQL {@code ORDER BY} clause
363 * (excluding the {@code ORDER BY} itself). Passing null will use the
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700364 * default sort order, which may be unordered.
365 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700366 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700367 * @return the SQL query string
368 */
369 public static String buildQueryString(
370 boolean distinct, String tables, String[] columns, String where,
371 String groupBy, String having, String orderBy, String limit) {
372 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
373 throw new IllegalArgumentException(
374 "HAVING clauses are only permitted when using a groupBy clause");
375 }
376
377 StringBuilder query = new StringBuilder(120);
378
379 query.append("SELECT ");
380 if (distinct) {
381 query.append("DISTINCT ");
382 }
383 if (columns != null && columns.length != 0) {
384 appendColumns(query, columns);
385 } else {
386 query.append("* ");
387 }
388 query.append("FROM ");
389 query.append(tables);
390 appendClause(query, " WHERE ", where);
391 appendClause(query, " GROUP BY ", groupBy);
392 appendClause(query, " HAVING ", having);
393 appendClause(query, " ORDER BY ", orderBy);
Owen Linab18d1f2009-05-06 16:45:59 -0700394 appendClause(query, " LIMIT ", limit);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700395
396 return query.toString();
397 }
398
399 private static void appendClause(StringBuilder s, String name, String clause) {
400 if (!TextUtils.isEmpty(clause)) {
401 s.append(name);
402 s.append(clause);
403 }
404 }
405
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700406 /**
407 * Add the names that are non-null in columns to s, separating
408 * them with commas.
409 */
410 public static void appendColumns(StringBuilder s, String[] columns) {
411 int n = columns.length;
412
413 for (int i = 0; i < n; i++) {
414 String column = columns[i];
415
416 if (column != null) {
417 if (i > 0) {
418 s.append(", ");
419 }
420 s.append(column);
421 }
422 }
423 s.append(' ');
424 }
425
426 /**
427 * Perform a query by combining all current settings and the
428 * information passed into this method.
429 *
430 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600431 * @param projectionIn A list of which columns to return. Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700432 * null will return all columns, which is discouraged to prevent
433 * reading data from storage that isn't going to be used.
434 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700435 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700436 * itself). Passing null will return all rows for the given URL.
437 * @param selectionArgs You may include ?s in selection, which
438 * will be replaced by the values from selectionArgs, in order
439 * that they appear in the selection. The values will be bound
440 * as Strings.
441 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700442 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700443 * itself). Passing null will cause the rows to not be grouped.
444 * @param having A filter declare which row groups to include in
445 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700446 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700447 * null will cause all row groups to be included, and is
448 * required when row grouping is not being used.
449 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700450 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700451 * will use the default sort order, which may be unordered.
452 * @return a cursor over the result set
453 * @see android.content.ContentResolver#query(android.net.Uri, String[],
454 * String, String[], String)
455 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600456 public Cursor query(SQLiteDatabase db, String[] projectionIn,
457 String selection, String[] selectionArgs, String groupBy,
458 String having, String sortOrder) {
459 return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
Jeff Brown4c1241d2012-02-02 17:05:00 -0800460 null /* limit */, null /* cancellationSignal */);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700461 }
462
463 /**
464 * Perform a query by combining all current settings and the
465 * information passed into this method.
466 *
467 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600468 * @param projectionIn A list of which columns to return. Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700469 * null will return all columns, which is discouraged to prevent
470 * reading data from storage that isn't going to be used.
471 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700472 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700473 * itself). Passing null will return all rows for the given URL.
474 * @param selectionArgs You may include ?s in selection, which
475 * will be replaced by the values from selectionArgs, in order
476 * that they appear in the selection. The values will be bound
477 * as Strings.
478 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700479 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700480 * itself). Passing null will cause the rows to not be grouped.
481 * @param having A filter declare which row groups to include in
482 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700483 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700484 * null will cause all row groups to be included, and is
485 * required when row grouping is not being used.
486 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700487 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700488 * will use the default sort order, which may be unordered.
489 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700490 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700491 * @return a cursor over the result set
492 * @see android.content.ContentResolver#query(android.net.Uri, String[],
493 * String, String[], String)
494 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600495 public Cursor query(SQLiteDatabase db, String[] projectionIn,
496 String selection, String[] selectionArgs, String groupBy,
497 String having, String sortOrder, String limit) {
498 return query(db, projectionIn, selection, selectionArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800499 groupBy, having, sortOrder, limit, null);
500 }
501
502 /**
503 * Perform a query by combining all current settings and the
504 * information passed into this method.
505 *
506 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600507 * @param projectionIn A list of which columns to return. Passing
Jeff Brown75ea64f2012-01-25 19:37:13 -0800508 * null will return all columns, which is discouraged to prevent
509 * reading data from storage that isn't going to be used.
510 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700511 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Brown75ea64f2012-01-25 19:37:13 -0800512 * itself). Passing null will return all rows for the given URL.
513 * @param selectionArgs You may include ?s in selection, which
514 * will be replaced by the values from selectionArgs, in order
515 * that they appear in the selection. The values will be bound
516 * as Strings.
517 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700518 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
Jeff Brown75ea64f2012-01-25 19:37:13 -0800519 * itself). Passing null will cause the rows to not be grouped.
520 * @param having A filter declare which row groups to include in
521 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700522 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
Jeff Brown75ea64f2012-01-25 19:37:13 -0800523 * null will cause all row groups to be included, and is
524 * required when row grouping is not being used.
525 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700526 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
Jeff Brown75ea64f2012-01-25 19:37:13 -0800527 * will use the default sort order, which may be unordered.
528 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700529 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
Jeff Brown4c1241d2012-02-02 17:05:00 -0800530 * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
Jeff Brown75ea64f2012-01-25 19:37:13 -0800531 * If the operation is canceled, then {@link OperationCanceledException} will be thrown
532 * when the query is executed.
533 * @return a cursor over the result set
534 * @see android.content.ContentResolver#query(android.net.Uri, String[],
535 * String, String[], String)
536 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600537 public Cursor query(SQLiteDatabase db, String[] projectionIn,
538 String selection, String[] selectionArgs, String groupBy,
539 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
540 if (mTables == null) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700541 return null;
542 }
543
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600544 final String sql;
545 final String unwrappedSql = buildQuery(
546 projectionIn, selection, groupBy, having,
547 sortOrder, limit);
548
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600549 if (isStrictColumns()) {
550 enforceStrictColumns(projectionIn);
551 }
552 if (isStrictGrammar()) {
553 enforceStrictGrammar(selection, groupBy, having, sortOrder, limit);
554 }
555 if (isStrict()) {
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700556 // Validate the user-supplied selection to detect syntactic anomalies
557 // in the selection string that could indicate a SQL injection attempt.
558 // The idea is to ensure that the selection clause is a valid SQL expression
559 // by compiling it twice: once wrapped in parentheses and once as
560 // originally specified. An attacker cannot create an expression that
561 // would escape the SQL expression while maintaining balanced parentheses
562 // in both the wrapped and original forms.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700563
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600564 // NOTE: The ordering of the below operations is important; we must
565 // execute the wrapped query to ensure the untrusted clause has been
566 // fully isolated.
567
568 // Validate the unwrapped query
569 db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
570
571 // Execute wrapped query for extra protection
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600572 final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600573 wrap(having), sortOrder, limit);
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600574 sql = wrappedSql;
575 } else {
576 // Execute unwrapped query
577 sql = unwrappedSql;
578 }
Jeff Sharkey6adc98c2018-07-12 19:47:49 -0600579
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600580 final String[] sqlArgs = selectionArgs;
Jeff Sharkey91be9262018-07-19 09:30:16 -0600581 if (Log.isLoggable(TAG, Log.DEBUG)) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600582 if (Build.IS_DEBUGGABLE) {
583 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
584 } else {
585 Log.d(TAG, sql);
586 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700587 }
588 return db.rawQueryWithFactory(
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600589 mFactory, sql, sqlArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800590 SQLiteDatabase.findEditTable(mTables),
Jeff Brown4c1241d2012-02-02 17:05:00 -0800591 cancellationSignal); // will throw if query is invalid
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700592 }
593
594 /**
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600595 * Perform an insert by combining all current settings and the
596 * information passed into this method.
597 *
598 * @param db the database to insert on
599 * @return the row ID of the newly inserted row, or -1 if an error occurred
600 */
601 public long insert(@NonNull SQLiteDatabase db, @NonNull ContentValues values) {
602 Objects.requireNonNull(mTables, "No tables defined");
603 Objects.requireNonNull(db, "No database defined");
604 Objects.requireNonNull(values, "No values defined");
605
606 if (isStrictColumns()) {
607 enforceStrictColumns(values);
608 }
609
610 final String sql = buildInsert(values);
611
612 final ArrayMap<String, Object> rawValues = values.getValues();
613 final int valuesLength = rawValues.size();
614 final Object[] sqlArgs = new Object[valuesLength];
615 for (int i = 0; i < sqlArgs.length; i++) {
616 sqlArgs[i] = rawValues.valueAt(i);
617 }
618 if (Log.isLoggable(TAG, Log.DEBUG)) {
619 if (Build.IS_DEBUGGABLE) {
620 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
621 } else {
622 Log.d(TAG, sql);
623 }
624 }
625 return db.executeSql(sql, sqlArgs);
626 }
627
628 /**
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600629 * Perform an update by combining all current settings and the
630 * information passed into this method.
631 *
632 * @param db the database to update on
633 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700634 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600635 * itself). Passing null will return all rows for the given URL.
636 * @param selectionArgs You may include ?s in selection, which
637 * will be replaced by the values from selectionArgs, in order
638 * that they appear in the selection. The values will be bound
639 * as Strings.
640 * @return the number of rows updated
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600641 */
642 public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values,
643 @Nullable String selection, @Nullable String[] selectionArgs) {
644 Objects.requireNonNull(mTables, "No tables defined");
645 Objects.requireNonNull(db, "No database defined");
646 Objects.requireNonNull(values, "No values defined");
647
648 final String sql;
649 final String unwrappedSql = buildUpdate(values, selection);
650
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600651 if (isStrictColumns()) {
652 enforceStrictColumns(values);
653 }
654 if (isStrictGrammar()) {
655 enforceStrictGrammar(selection, null, null, null, null);
656 }
657 if (isStrict()) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600658 // Validate the user-supplied selection to detect syntactic anomalies
659 // in the selection string that could indicate a SQL injection attempt.
660 // The idea is to ensure that the selection clause is a valid SQL expression
661 // by compiling it twice: once wrapped in parentheses and once as
662 // originally specified. An attacker cannot create an expression that
663 // would escape the SQL expression while maintaining balanced parentheses
664 // in both the wrapped and original forms.
665
666 // NOTE: The ordering of the below operations is important; we must
667 // execute the wrapped query to ensure the untrusted clause has been
668 // fully isolated.
669
670 // Validate the unwrapped query
671 db.validateSql(unwrappedSql, null); // will throw if query is invalid
672
673 // Execute wrapped query for extra protection
674 final String wrappedSql = buildUpdate(values, wrap(selection));
675 sql = wrappedSql;
676 } else {
677 // Execute unwrapped query
678 sql = unwrappedSql;
679 }
680
Jeff Sharkey42122bf2018-07-26 09:39:18 -0600681 if (selectionArgs == null) {
682 selectionArgs = EmptyArray.STRING;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600683 }
Jeff Sharkey42122bf2018-07-26 09:39:18 -0600684 final ArrayMap<String, Object> rawValues = values.getValues();
685 final int valuesLength = rawValues.size();
686 final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length];
687 for (int i = 0; i < sqlArgs.length; i++) {
688 if (i < valuesLength) {
689 sqlArgs[i] = rawValues.valueAt(i);
690 } else {
691 sqlArgs[i] = selectionArgs[i - valuesLength];
692 }
693 }
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600694 if (Log.isLoggable(TAG, Log.DEBUG)) {
695 if (Build.IS_DEBUGGABLE) {
696 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
697 } else {
698 Log.d(TAG, sql);
699 }
700 }
701 return db.executeSql(sql, sqlArgs);
702 }
703
704 /**
705 * Perform a delete by combining all current settings and the
706 * information passed into this method.
707 *
708 * @param db the database to delete on
709 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700710 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600711 * itself). Passing null will return all rows for the given URL.
712 * @param selectionArgs You may include ?s in selection, which
713 * will be replaced by the values from selectionArgs, in order
714 * that they appear in the selection. The values will be bound
715 * as Strings.
716 * @return the number of rows deleted
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600717 */
718 public int delete(@NonNull SQLiteDatabase db, @Nullable String selection,
719 @Nullable String[] selectionArgs) {
720 Objects.requireNonNull(mTables, "No tables defined");
721 Objects.requireNonNull(db, "No database defined");
722
723 final String sql;
724 final String unwrappedSql = buildDelete(selection);
725
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600726 if (isStrictGrammar()) {
727 enforceStrictGrammar(selection, null, null, null, null);
728 }
729 if (isStrict()) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600730 // Validate the user-supplied selection to detect syntactic anomalies
731 // in the selection string that could indicate a SQL injection attempt.
732 // The idea is to ensure that the selection clause is a valid SQL expression
733 // by compiling it twice: once wrapped in parentheses and once as
734 // originally specified. An attacker cannot create an expression that
735 // would escape the SQL expression while maintaining balanced parentheses
736 // in both the wrapped and original forms.
737
738 // NOTE: The ordering of the below operations is important; we must
739 // execute the wrapped query to ensure the untrusted clause has been
740 // fully isolated.
741
742 // Validate the unwrapped query
743 db.validateSql(unwrappedSql, null); // will throw if query is invalid
744
745 // Execute wrapped query for extra protection
746 final String wrappedSql = buildDelete(wrap(selection));
747 sql = wrappedSql;
748 } else {
749 // Execute unwrapped query
750 sql = unwrappedSql;
751 }
752
753 final String[] sqlArgs = selectionArgs;
754 if (Log.isLoggable(TAG, Log.DEBUG)) {
755 if (Build.IS_DEBUGGABLE) {
756 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
757 } else {
758 Log.d(TAG, sql);
759 }
760 }
761 return db.executeSql(sql, sqlArgs);
762 }
763
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600764 private void enforceStrictColumns(@Nullable String[] projection) {
765 Objects.requireNonNull(mProjectionMap, "No projection map defined");
766
767 computeProjection(projection);
768 }
769
770 private void enforceStrictColumns(@NonNull ContentValues values) {
771 Objects.requireNonNull(mProjectionMap, "No projection map defined");
772
773 final ArrayMap<String, Object> rawValues = values.getValues();
774 for (int i = 0; i < rawValues.size(); i++) {
775 final String column = rawValues.keyAt(i);
776 if (!mProjectionMap.containsKey(column)) {
777 throw new IllegalArgumentException("Invalid column " + column);
778 }
779 }
780 }
781
782 private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy,
783 @Nullable String having, @Nullable String sortOrder, @Nullable String limit) {
784 SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE,
Jeff Sharkey19491022019-12-18 17:55:39 -0700785 this::enforceStrictToken);
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600786 SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE,
Jeff Sharkey19491022019-12-18 17:55:39 -0700787 this::enforceStrictToken);
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600788 SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE,
Jeff Sharkey19491022019-12-18 17:55:39 -0700789 this::enforceStrictToken);
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600790 SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE,
Jeff Sharkey19491022019-12-18 17:55:39 -0700791 this::enforceStrictToken);
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600792 SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE,
Jeff Sharkey19491022019-12-18 17:55:39 -0700793 this::enforceStrictToken);
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600794 }
795
Jeff Sharkey19491022019-12-18 17:55:39 -0700796 private void enforceStrictToken(@NonNull String token) {
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600797 if (isTableOrColumn(token)) return;
798 if (SQLiteTokenizer.isFunction(token)) return;
799 if (SQLiteTokenizer.isType(token)) return;
800
Jeff Sharkey19491022019-12-18 17:55:39 -0700801 // Carefully block any tokens that are attempting to jump across query
802 // clauses or create subqueries, since they could leak data that should
803 // have been filtered by the trusted where clause
804 boolean isAllowedKeyword = SQLiteTokenizer.isKeyword(token);
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600805 switch (token.toUpperCase(Locale.US)) {
Jeff Sharkey19491022019-12-18 17:55:39 -0700806 case "SELECT":
807 case "FROM":
808 case "WHERE":
809 case "GROUP":
810 case "HAVING":
811 case "WINDOW":
812 case "VALUES":
813 case "ORDER":
814 case "LIMIT":
815 isAllowedKeyword = false;
816 break;
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600817 }
Jeff Sharkey19491022019-12-18 17:55:39 -0700818 if (!isAllowedKeyword) {
819 throw new IllegalArgumentException("Invalid token " + token);
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600820 }
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600821 }
822
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600823 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700824 * Construct a {@code SELECT} statement suitable for use in a group of
825 * {@code SELECT} statements that will be joined through {@code UNION} operators
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700826 * in buildUnionQuery.
827 *
828 * @param projectionIn A list of which columns to return. Passing
829 * null will return all columns, which is discouraged to
830 * prevent reading data from storage that isn't going to be
831 * used.
832 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700833 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700834 * itself). Passing null will return all rows for the given
835 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700836 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700837 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself).
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700838 * Passing null will cause the rows to not be grouped.
839 * @param having A filter declare which row groups to include in
840 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700841 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700842 * null will cause all row groups to be included, and is
843 * required when row grouping is not being used.
844 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700845 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700846 * will use the default sort order, which may be unordered.
847 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700848 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
849 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700850 */
851 public String buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100852 String[] projectionIn, String selection, String groupBy,
853 String having, String sortOrder, String limit) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700854 String[] projection = computeProjection(projectionIn);
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600855 String where = computeWhere(selection);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700856
857 return buildQueryString(
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600858 mDistinct, mTables, projection, where,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700859 groupBy, having, sortOrder, limit);
860 }
861
862 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100863 * @deprecated This method's signature is misleading since no SQL parameter
864 * substitution is carried out. The selection arguments parameter does not get
865 * used at all. To avoid confusion, call
866 * {@link #buildQuery(String[], String, String, String, String, String)} instead.
867 */
868 @Deprecated
869 public String buildQuery(
870 String[] projectionIn, String selection, String[] selectionArgs,
871 String groupBy, String having, String sortOrder, String limit) {
872 return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
873 }
874
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600875 /** {@hide} */
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600876 public String buildInsert(ContentValues values) {
877 if (values == null || values.isEmpty()) {
878 throw new IllegalArgumentException("Empty values");
879 }
880
881 StringBuilder sql = new StringBuilder(120);
882 sql.append("INSERT INTO ");
883 sql.append(SQLiteDatabase.findEditTable(mTables));
884 sql.append(" (");
885
886 final ArrayMap<String, Object> rawValues = values.getValues();
887 for (int i = 0; i < rawValues.size(); i++) {
888 if (i > 0) {
889 sql.append(',');
890 }
891 sql.append(rawValues.keyAt(i));
892 }
893 sql.append(") VALUES (");
894 for (int i = 0; i < rawValues.size(); i++) {
895 if (i > 0) {
896 sql.append(',');
897 }
898 sql.append('?');
899 }
900 sql.append(")");
901 return sql.toString();
902 }
903
904 /** {@hide} */
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600905 public String buildUpdate(ContentValues values, String selection) {
906 if (values == null || values.isEmpty()) {
907 throw new IllegalArgumentException("Empty values");
908 }
909
910 StringBuilder sql = new StringBuilder(120);
911 sql.append("UPDATE ");
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600912 sql.append(SQLiteDatabase.findEditTable(mTables));
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600913 sql.append(" SET ");
914
915 final ArrayMap<String, Object> rawValues = values.getValues();
916 for (int i = 0; i < rawValues.size(); i++) {
917 if (i > 0) {
918 sql.append(',');
919 }
920 sql.append(rawValues.keyAt(i));
921 sql.append("=?");
922 }
923
924 final String where = computeWhere(selection);
925 appendClause(sql, " WHERE ", where);
926 return sql.toString();
927 }
928
929 /** {@hide} */
930 public String buildDelete(String selection) {
931 StringBuilder sql = new StringBuilder(120);
932 sql.append("DELETE FROM ");
Jeff Sharkeyae6a4662019-07-16 16:50:42 -0600933 sql.append(SQLiteDatabase.findEditTable(mTables));
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600934
935 final String where = computeWhere(selection);
936 appendClause(sql, " WHERE ", where);
937 return sql.toString();
938 }
939
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100940 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700941 * Construct a {@code SELECT} statement suitable for use in a group of
942 * {@code SELECT} statements that will be joined through {@code UNION} operators
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700943 * in buildUnionQuery.
944 *
945 * @param typeDiscriminatorColumn the name of the result column
946 * whose cells will contain the name of the table from which
947 * each row was drawn.
948 * @param unionColumns the names of the columns to appear in the
949 * result. This may include columns that do not appear in the
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700950 * table this {@code SELECT} is querying (i.e. mTables), but that do
951 * appear in one of the other tables in the {@code UNION} query that we
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700952 * are constructing.
953 * @param columnsPresentInTable a Set of the names of the columns
954 * that appear in this table (i.e. in the table whose name is
955 * mTables). Since columns in unionColumns include columns that
956 * appear only in other tables, we use this array to distinguish
957 * which ones actually are present. Other columns will have
958 * NULL values for results from this subquery.
959 * @param computedColumnsOffset all columns in unionColumns before
960 * this index are included under the assumption that they're
961 * computed and therefore won't appear in columnsPresentInTable,
962 * e.g. "date * 1000 as normalized_date"
963 * @param typeDiscriminatorValue the value used for the
964 * type-discriminator column in this subquery
965 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700966 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700967 * itself). Passing null will return all rows for the given
968 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700969 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700970 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself).
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700971 * Passing null will cause the rows to not be grouped.
972 * @param having A filter declare which row groups to include in
973 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700974 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700975 * null will cause all row groups to be included, and is
976 * required when row grouping is not being used.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700977 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700978 */
979 public String buildUnionSubQuery(
980 String typeDiscriminatorColumn,
981 String[] unionColumns,
982 Set<String> columnsPresentInTable,
983 int computedColumnsOffset,
984 String typeDiscriminatorValue,
985 String selection,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700986 String groupBy,
987 String having) {
988 int unionColumnsCount = unionColumns.length;
989 String[] projectionIn = new String[unionColumnsCount];
990
991 for (int i = 0; i < unionColumnsCount; i++) {
992 String unionColumn = unionColumns[i];
993
994 if (unionColumn.equals(typeDiscriminatorColumn)) {
995 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
996 + typeDiscriminatorColumn;
997 } else if (i <= computedColumnsOffset
998 || columnsPresentInTable.contains(unionColumn)) {
999 projectionIn[i] = unionColumn;
1000 } else {
1001 projectionIn[i] = "NULL AS " + unionColumn;
1002 }
1003 }
1004 return buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +01001005 projectionIn, selection, groupBy, having,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001006 null /* sortOrder */,
1007 null /* limit */);
1008 }
1009
1010 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +01001011 * @deprecated This method's signature is misleading since no SQL parameter
1012 * substitution is carried out. The selection arguments parameter does not get
1013 * used at all. To avoid confusion, call
Jean-Baptiste Queruf4072fc2010-11-17 16:47:59 -08001014 * {@link #buildUnionSubQuery}
Jonas Schwertfeger84029032010-11-12 11:42:28 +01001015 * instead.
1016 */
1017 @Deprecated
1018 public String buildUnionSubQuery(
1019 String typeDiscriminatorColumn,
1020 String[] unionColumns,
1021 Set<String> columnsPresentInTable,
1022 int computedColumnsOffset,
1023 String typeDiscriminatorValue,
1024 String selection,
1025 String[] selectionArgs,
1026 String groupBy,
1027 String having) {
1028 return buildUnionSubQuery(
1029 typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
1030 computedColumnsOffset, typeDiscriminatorValue, selection,
1031 groupBy, having);
1032 }
1033
1034 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001035 * Given a set of subqueries, all of which are {@code SELECT} statements,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001036 * construct a query that returns the union of what those
1037 * subqueries return.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001038 * @param subQueries an array of SQL {@code SELECT} statements, all of
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001039 * which must have the same columns as the same positions in
1040 * their results
1041 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001042 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001043 * null will use the default sort order, which may be unordered.
1044 * @param limit The limit clause, which applies to the entire union result set
1045 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -07001046 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001047 */
1048 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
1049 StringBuilder query = new StringBuilder(128);
1050 int subQueryCount = subQueries.length;
1051 String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
1052
1053 for (int i = 0; i < subQueryCount; i++) {
1054 if (i > 0) {
1055 query.append(unionOperator);
1056 }
1057 query.append(subQueries[i]);
1058 }
1059 appendClause(query, " ORDER BY ", sortOrder);
1060 appendClause(query, " LIMIT ", limit);
1061 return query.toString();
1062 }
1063
Jeff Sharkey82d783c2019-03-29 15:46:35 -06001064 private static @NonNull String maybeWithOperator(@Nullable String operator,
1065 @NonNull String column) {
1066 if (operator != null) {
1067 return operator + "(" + column + ")";
1068 } else {
1069 return column;
1070 }
1071 }
1072
1073 /** {@hide} */
Mathew Inwood31755f92018-12-20 13:53:36 +00001074 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Jeff Sharkeyae6a4662019-07-16 16:50:42 -06001075 public @Nullable String[] computeProjection(@Nullable String[] projectionIn) {
1076 if (!ArrayUtils.isEmpty(projectionIn)) {
1077 String[] projectionOut = new String[projectionIn.length];
1078 for (int i = 0; i < projectionIn.length; i++) {
1079 projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001080 }
Jeff Sharkeyae6a4662019-07-16 16:50:42 -06001081 return projectionOut;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001082 } else if (mProjectionMap != null) {
1083 // Return all columns in projection map.
1084 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
1085 String[] projection = new String[entrySet.size()];
1086 Iterator<Entry<String, String>> entryIter = entrySet.iterator();
1087 int i = 0;
1088
1089 while (entryIter.hasNext()) {
1090 Entry<String, String> entry = entryIter.next();
1091
1092 // Don't include the _count column when people ask for no projection.
1093 if (entry.getKey().equals(BaseColumns._COUNT)) {
1094 continue;
1095 }
1096 projection[i++] = entry.getValue();
1097 }
1098 return projection;
1099 }
1100 return null;
1101 }
Jeff Sharkeyb13ea302018-07-25 14:52:14 -06001102
Jeff Sharkeyae6a4662019-07-16 16:50:42 -06001103 private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) {
1104 final String column = computeSingleProjection(userColumn);
1105 if (column != null) {
1106 return column;
1107 } else {
1108 throw new IllegalArgumentException("Invalid column " + userColumn);
1109 }
1110 }
1111
1112 private @Nullable String computeSingleProjection(@NonNull String userColumn) {
1113 // When no mapping provided, anything goes
1114 if (mProjectionMap == null) {
1115 return userColumn;
1116 }
1117
1118 String operator = null;
1119 String column = mProjectionMap.get(userColumn);
1120
1121 // When no direct match found, look for aggregation
1122 if (column == null) {
1123 final Matcher matcher = sAggregationPattern.matcher(userColumn);
1124 if (matcher.matches()) {
1125 operator = matcher.group(1);
1126 userColumn = matcher.group(2);
1127 column = mProjectionMap.get(userColumn);
1128 }
1129 }
1130
1131 if (column != null) {
1132 return maybeWithOperator(operator, column);
1133 }
1134
1135 if (mStrictFlags == 0 &&
1136 (userColumn.contains(" AS ") || userColumn.contains(" as "))) {
1137 /* A column alias already exist */
1138 return maybeWithOperator(operator, userColumn);
1139 }
1140
1141 // If greylist is configured, we might be willing to let
1142 // this custom column bypass our strict checks.
1143 if (mProjectionGreylist != null) {
1144 boolean match = false;
1145 for (Pattern p : mProjectionGreylist) {
1146 if (p.matcher(userColumn).matches()) {
1147 match = true;
1148 break;
1149 }
1150 }
1151
1152 if (match) {
1153 Log.w(TAG, "Allowing abusive custom column: " + userColumn);
1154 return maybeWithOperator(operator, userColumn);
1155 }
1156 }
1157
1158 return null;
1159 }
1160
1161 private boolean isTableOrColumn(String token) {
1162 if (mTables.equals(token)) return true;
1163 return computeSingleProjection(token) != null;
1164 }
1165
Jeff Sharkey82d783c2019-03-29 15:46:35 -06001166 /** {@hide} */
1167 public @Nullable String computeWhere(@Nullable String selection) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -06001168 final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
1169 final boolean hasExternal = !TextUtils.isEmpty(selection);
1170
1171 if (hasInternal || hasExternal) {
1172 final StringBuilder where = new StringBuilder();
1173 if (hasInternal) {
1174 where.append('(').append(mWhereClause).append(')');
1175 }
1176 if (hasInternal && hasExternal) {
1177 where.append(" AND ");
1178 }
1179 if (hasExternal) {
1180 where.append('(').append(selection).append(')');
1181 }
1182 return where.toString();
1183 } else {
1184 return null;
1185 }
1186 }
1187
1188 /**
1189 * Wrap given argument in parenthesis, unless it's {@code null} or
1190 * {@code ()}, in which case return it verbatim.
1191 */
1192 private @Nullable String wrap(@Nullable String arg) {
1193 if (TextUtils.isEmpty(arg)) {
1194 return arg;
1195 } else {
1196 return "(" + arg + ")";
1197 }
1198 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001199}