blob: 71760f28782bb6a6515de58f938558c7da3160d2 [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 Sharkey234de022018-07-25 14:52:14 -060019import android.annotation.NonNull;
20import android.annotation.Nullable;
Kenny Guy6cbfa792019-01-21 13:16:11 +000021import android.annotation.UnsupportedAppUsage;
Jeff Sharkey234de022018-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 Sharkey234de022018-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;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070030import android.util.Log;
31
Jeff Sharkey0f732192018-07-26 09:39:18 -060032import libcore.util.EmptyArray;
Jeff Sharkey234de022018-07-25 14:52:14 -060033
34import java.util.Arrays;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070035import java.util.Iterator;
36import java.util.Map;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070037import java.util.Map.Entry;
Jeff Sharkey234de022018-07-25 14:52:14 -060038import java.util.Objects;
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -070039import java.util.Set;
Owen Linab18d1f2009-05-06 16:45:59 -070040import java.util.regex.Pattern;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070041
42/**
Joshua Baxter3639e2f2018-03-26 14:55:14 -070043 * This is a convenience class that helps build SQL queries to be sent to
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070044 * {@link SQLiteDatabase} objects.
45 */
Jeff Sharkey0da04832018-07-26 14:36:59 -060046public class SQLiteQueryBuilder {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070047 private static final String TAG = "SQLiteQueryBuilder";
Owen Linab18d1f2009-05-06 16:45:59 -070048 private static final Pattern sLimitPattern =
49 Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070050
51 private Map<String, String> mProjectionMap = null;
Mathew Inwood55418ea2018-12-20 15:30:45 +000052 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070053 private String mTables = "";
Mathew Inwood55418ea2018-12-20 15:30:45 +000054 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -070055 private StringBuilder mWhereClause = null; // lazily created
Mathew Inwood55418ea2018-12-20 15:30:45 +000056 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070057 private boolean mDistinct;
58 private SQLiteDatabase.CursorFactory mFactory;
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -070059 private boolean mStrict;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070060
61 public SQLiteQueryBuilder() {
62 mDistinct = false;
63 mFactory = null;
64 }
65
66 /**
67 * Mark the query as DISTINCT.
68 *
69 * @param distinct if true the query is DISTINCT, otherwise it isn't
70 */
71 public void setDistinct(boolean distinct) {
72 mDistinct = distinct;
73 }
74
75 /**
76 * Returns the list of tables being queried
77 *
78 * @return the list of tables being queried
79 */
80 public String getTables() {
81 return mTables;
82 }
83
84 /**
85 * Sets the list of tables to query. Multiple tables can be specified to perform a join.
86 * For example:
87 * setTables("foo, bar")
88 * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
89 *
90 * @param inTables the list of tables to query on
91 */
92 public void setTables(String inTables) {
93 mTables = inTables;
94 }
95
96 /**
97 * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
98 * by parenthesis and ANDed with the selection passed to {@link #query}. The final
99 * WHERE clause looks like:
100 *
101 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
102 *
103 * @param inWhere the chunk of text to append to the WHERE clause.
104 */
Kenny Guy6cbfa792019-01-21 13:16:11 +0000105 public void appendWhere(@NonNull CharSequence inWhere) {
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -0700106 if (mWhereClause == null) {
107 mWhereClause = new StringBuilder(inWhere.length() + 16);
108 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700109 mWhereClause.append(inWhere);
110 }
111
112 /**
113 * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
114 * by parenthesis and ANDed with the selection passed to {@link #query}. The final
115 * WHERE clause looks like:
116 *
117 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
118 *
119 * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
120 * to avoid SQL injection attacks
121 */
Kenny Guy6cbfa792019-01-21 13:16:11 +0000122 public void appendWhereEscapeString(@NonNull String inWhere) {
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -0700123 if (mWhereClause == null) {
124 mWhereClause = new StringBuilder(inWhere.length() + 16);
125 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700126 DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
127 }
128
129 /**
Jeff Sharkey0da04832018-07-26 14:36:59 -0600130 * Add a standalone chunk to the {@code WHERE} clause of this query.
131 * <p>
132 * This method differs from {@link #appendWhere(CharSequence)} in that it
133 * automatically appends {@code AND} to any existing {@code WHERE} clause
134 * already under construction before appending the given standalone
135 * expression wrapped in parentheses.
136 *
137 * @param inWhere the standalone expression to append to the {@code WHERE}
138 * clause. It will be wrapped in parentheses when it's appended.
139 */
140 public void appendWhereStandalone(@NonNull CharSequence inWhere) {
141 if (mWhereClause == null) {
142 mWhereClause = new StringBuilder(inWhere.length() + 16);
143 }
144 if (mWhereClause.length() > 0) {
145 mWhereClause.append(" AND ");
146 }
147 mWhereClause.append('(').append(inWhere).append(')');
148 }
149
150 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700151 * Sets the projection map for the query. The projection map maps
152 * from column names that the caller passes into query to database
153 * column names. This is useful for renaming columns as well as
154 * disambiguating column names when doing joins. For example you
155 * could map "name" to "people.name". If a projection map is set
156 * it must contain all column names the user may request, even if
157 * the key and value are the same.
158 *
159 * @param columnMap maps from the user column names to the database column names
160 */
161 public void setProjectionMap(Map<String, String> columnMap) {
162 mProjectionMap = columnMap;
163 }
164
165 /**
166 * Sets the cursor factory to be used for the query. You can use
167 * one factory for all queries on a database but it is normally
Jeff Brown75ea64f2012-01-25 19:37:13 -0800168 * easier to specify the factory when doing this query.
169 *
170 * @param factory the factory to use.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700171 */
172 public void setCursorFactory(SQLiteDatabase.CursorFactory factory) {
173 mFactory = factory;
174 }
175
176 /**
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700177 * When set, the selection is verified against malicious arguments.
178 * When using this class to create a statement using
179 * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
180 * non-numeric limits will raise an exception. If a projection map is specified, fields
181 * not in that map will be ignored.
182 * If this class is used to execute the statement directly using
183 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
184 * or
185 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
186 * additionally also parenthesis escaping selection are caught.
187 *
188 * To summarize: To get maximum protection against malicious third party apps (for example
189 * content provider consumers), make sure to do the following:
190 * <ul>
191 * <li>Set this value to true</li>
192 * <li>Use a projection map</li>
193 * <li>Use one of the query overloads instead of getting the statement as a sql string</li>
194 * </ul>
195 * By default, this value is false.
196 */
197 public void setStrict(boolean flag) {
198 mStrict = flag;
Dmitri Plotnikov40eb4aa2010-04-14 16:09:46 -0700199 }
200
201 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700202 * Build an SQL query string from the given clauses.
203 *
204 * @param distinct true if you want each row to be unique, false otherwise.
205 * @param tables The table names to compile the query against.
206 * @param columns A list of which columns to return. Passing null will
207 * return all columns, which is discouraged to prevent reading
208 * data from storage that isn't going to be used.
209 * @param where A filter declaring which rows to return, formatted as an SQL
210 * WHERE clause (excluding the WHERE itself). Passing null will
211 * return all rows for the given URL.
212 * @param groupBy A filter declaring how to group rows, formatted as an SQL
213 * GROUP BY clause (excluding the GROUP BY itself). Passing null
214 * will cause the rows to not be grouped.
215 * @param having A filter declare which row groups to include in the cursor,
216 * if row grouping is being used, formatted as an SQL HAVING
217 * clause (excluding the HAVING itself). Passing null will cause
218 * all row groups to be included, and is required when row
219 * grouping is not being used.
220 * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
221 * (excluding the ORDER BY itself). Passing null will use the
222 * default sort order, which may be unordered.
223 * @param limit Limits the number of rows returned by the query,
224 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
225 * @return the SQL query string
226 */
227 public static String buildQueryString(
228 boolean distinct, String tables, String[] columns, String where,
229 String groupBy, String having, String orderBy, String limit) {
230 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
231 throw new IllegalArgumentException(
232 "HAVING clauses are only permitted when using a groupBy clause");
233 }
Owen Linab18d1f2009-05-06 16:45:59 -0700234 if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) {
235 throw new IllegalArgumentException("invalid LIMIT clauses:" + limit);
236 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700237
238 StringBuilder query = new StringBuilder(120);
239
240 query.append("SELECT ");
241 if (distinct) {
242 query.append("DISTINCT ");
243 }
244 if (columns != null && columns.length != 0) {
245 appendColumns(query, columns);
246 } else {
247 query.append("* ");
248 }
249 query.append("FROM ");
250 query.append(tables);
251 appendClause(query, " WHERE ", where);
252 appendClause(query, " GROUP BY ", groupBy);
253 appendClause(query, " HAVING ", having);
254 appendClause(query, " ORDER BY ", orderBy);
Owen Linab18d1f2009-05-06 16:45:59 -0700255 appendClause(query, " LIMIT ", limit);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700256
257 return query.toString();
258 }
259
260 private static void appendClause(StringBuilder s, String name, String clause) {
261 if (!TextUtils.isEmpty(clause)) {
262 s.append(name);
263 s.append(clause);
264 }
265 }
266
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700267 /**
268 * Add the names that are non-null in columns to s, separating
269 * them with commas.
270 */
271 public static void appendColumns(StringBuilder s, String[] columns) {
272 int n = columns.length;
273
274 for (int i = 0; i < n; i++) {
275 String column = columns[i];
276
277 if (column != null) {
278 if (i > 0) {
279 s.append(", ");
280 }
281 s.append(column);
282 }
283 }
284 s.append(' ');
285 }
286
287 /**
288 * Perform a query by combining all current settings and the
289 * information passed into this method.
290 *
291 * @param db the database to query on
292 * @param projectionIn A list of which columns to return. Passing
293 * null will return all columns, which is discouraged to prevent
294 * reading data from storage that isn't going to be used.
295 * @param selection A filter declaring which rows to return,
296 * formatted as an SQL WHERE clause (excluding the WHERE
297 * itself). Passing null will return all rows for the given URL.
298 * @param selectionArgs You may include ?s in selection, which
299 * will be replaced by the values from selectionArgs, in order
300 * that they appear in the selection. The values will be bound
301 * as Strings.
302 * @param groupBy A filter declaring how to group rows, formatted
303 * as an SQL GROUP BY clause (excluding the GROUP BY
304 * itself). Passing null will cause the rows to not be grouped.
305 * @param having A filter declare which row groups to include in
306 * the cursor, if row grouping is being used, formatted as an
307 * SQL HAVING clause (excluding the HAVING itself). Passing
308 * null will cause all row groups to be included, and is
309 * required when row grouping is not being used.
310 * @param sortOrder How to order the rows, formatted as an SQL
311 * ORDER BY clause (excluding the ORDER BY itself). Passing null
312 * will use the default sort order, which may be unordered.
313 * @return a cursor over the result set
314 * @see android.content.ContentResolver#query(android.net.Uri, String[],
315 * String, String[], String)
316 */
317 public Cursor query(SQLiteDatabase db, String[] projectionIn,
318 String selection, String[] selectionArgs, String groupBy,
319 String having, String sortOrder) {
320 return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
Jeff Brown4c1241d2012-02-02 17:05:00 -0800321 null /* limit */, null /* cancellationSignal */);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700322 }
323
324 /**
325 * Perform a query by combining all current settings and the
326 * information passed into this method.
327 *
328 * @param db the database to query on
329 * @param projectionIn A list of which columns to return. Passing
330 * null will return all columns, which is discouraged to prevent
331 * reading data from storage that isn't going to be used.
332 * @param selection A filter declaring which rows to return,
333 * formatted as an SQL WHERE clause (excluding the WHERE
334 * itself). Passing null will return all rows for the given URL.
335 * @param selectionArgs You may include ?s in selection, which
336 * will be replaced by the values from selectionArgs, in order
337 * that they appear in the selection. The values will be bound
338 * as Strings.
339 * @param groupBy A filter declaring how to group rows, formatted
340 * as an SQL GROUP BY clause (excluding the GROUP BY
341 * itself). Passing null will cause the rows to not be grouped.
342 * @param having A filter declare which row groups to include in
343 * the cursor, if row grouping is being used, formatted as an
344 * SQL HAVING clause (excluding the HAVING itself). Passing
345 * null will cause all row groups to be included, and is
346 * required when row grouping is not being used.
347 * @param sortOrder How to order the rows, formatted as an SQL
348 * ORDER BY clause (excluding the ORDER BY itself). Passing null
349 * will use the default sort order, which may be unordered.
350 * @param limit Limits the number of rows returned by the query,
351 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
352 * @return a cursor over the result set
353 * @see android.content.ContentResolver#query(android.net.Uri, String[],
354 * String, String[], String)
355 */
356 public Cursor query(SQLiteDatabase db, String[] projectionIn,
357 String selection, String[] selectionArgs, String groupBy,
358 String having, String sortOrder, String limit) {
Jeff Brown75ea64f2012-01-25 19:37:13 -0800359 return query(db, projectionIn, selection, selectionArgs,
360 groupBy, having, sortOrder, limit, null);
361 }
362
363 /**
364 * Perform a query by combining all current settings and the
365 * information passed into this method.
366 *
367 * @param db the database to query on
368 * @param projectionIn A list of which columns to return. Passing
369 * null will return all columns, which is discouraged to prevent
370 * reading data from storage that isn't going to be used.
371 * @param selection A filter declaring which rows to return,
372 * formatted as an SQL WHERE clause (excluding the WHERE
373 * itself). Passing null will return all rows for the given URL.
374 * @param selectionArgs You may include ?s in selection, which
375 * will be replaced by the values from selectionArgs, in order
376 * that they appear in the selection. The values will be bound
377 * as Strings.
378 * @param groupBy A filter declaring how to group rows, formatted
379 * as an SQL GROUP BY clause (excluding the GROUP BY
380 * itself). Passing null will cause the rows to not be grouped.
381 * @param having A filter declare which row groups to include in
382 * the cursor, if row grouping is being used, formatted as an
383 * SQL HAVING clause (excluding the HAVING itself). Passing
384 * null will cause all row groups to be included, and is
385 * required when row grouping is not being used.
386 * @param sortOrder How to order the rows, formatted as an SQL
387 * ORDER BY clause (excluding the ORDER BY itself). Passing null
388 * will use the default sort order, which may be unordered.
389 * @param limit Limits the number of rows returned by the query,
390 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
Jeff Brown4c1241d2012-02-02 17:05:00 -0800391 * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
Jeff Brown75ea64f2012-01-25 19:37:13 -0800392 * If the operation is canceled, then {@link OperationCanceledException} will be thrown
393 * when the query is executed.
394 * @return a cursor over the result set
395 * @see android.content.ContentResolver#query(android.net.Uri, String[],
396 * String, String[], String)
397 */
398 public Cursor query(SQLiteDatabase db, String[] projectionIn,
399 String selection, String[] selectionArgs, String groupBy,
Jeff Brown4c1241d2012-02-02 17:05:00 -0800400 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700401 if (mTables == null) {
402 return null;
403 }
404
Jeff Sharkey234de022018-07-25 14:52:14 -0600405 final String sql;
406 final String unwrappedSql = buildQuery(
407 projectionIn, selection, groupBy, having,
408 sortOrder, limit);
409
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700410 if (mStrict && selection != null && selection.length() > 0) {
411 // Validate the user-supplied selection to detect syntactic anomalies
412 // in the selection string that could indicate a SQL injection attempt.
413 // The idea is to ensure that the selection clause is a valid SQL expression
414 // by compiling it twice: once wrapped in parentheses and once as
415 // originally specified. An attacker cannot create an expression that
416 // would escape the SQL expression while maintaining balanced parentheses
417 // in both the wrapped and original forms.
Jeff Sharkey234de022018-07-25 14:52:14 -0600418
419 // NOTE: The ordering of the below operations is important; we must
420 // execute the wrapped query to ensure the untrusted clause has been
421 // fully isolated.
422
423 // Validate the unwrapped query
424 db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
425
426 // Execute wrapped query for extra protection
427 final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700428 having, sortOrder, limit);
Jeff Sharkey234de022018-07-25 14:52:14 -0600429 sql = wrappedSql;
430 } else {
431 // Execute unwrapped query
432 sql = unwrappedSql;
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700433 }
434
Jeff Sharkey234de022018-07-25 14:52:14 -0600435 final String[] sqlArgs = selectionArgs;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700436 if (Log.isLoggable(TAG, Log.DEBUG)) {
Jeff Sharkey234de022018-07-25 14:52:14 -0600437 if (Build.IS_DEBUGGABLE) {
438 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
439 } else {
440 Log.d(TAG, sql);
441 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700442 }
443 return db.rawQueryWithFactory(
Jeff Sharkey234de022018-07-25 14:52:14 -0600444 mFactory, sql, sqlArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800445 SQLiteDatabase.findEditTable(mTables),
Jeff Brown4c1241d2012-02-02 17:05:00 -0800446 cancellationSignal); // will throw if query is invalid
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700447 }
448
449 /**
Jeff Sharkey234de022018-07-25 14:52:14 -0600450 * Perform an update by combining all current settings and the
451 * information passed into this method.
452 *
453 * @param db the database to update on
454 * @param selection A filter declaring which rows to return,
455 * formatted as an SQL WHERE clause (excluding the WHERE
456 * itself). Passing null will return all rows for the given URL.
457 * @param selectionArgs You may include ?s in selection, which
458 * will be replaced by the values from selectionArgs, in order
459 * that they appear in the selection. The values will be bound
460 * as Strings.
461 * @return the number of rows updated
Jeff Sharkey234de022018-07-25 14:52:14 -0600462 */
463 public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values,
464 @Nullable String selection, @Nullable String[] selectionArgs) {
465 Objects.requireNonNull(mTables, "No tables defined");
466 Objects.requireNonNull(db, "No database defined");
467 Objects.requireNonNull(values, "No values defined");
468
469 final String sql;
470 final String unwrappedSql = buildUpdate(values, selection);
471
472 if (mStrict) {
473 // Validate the user-supplied selection to detect syntactic anomalies
474 // in the selection string that could indicate a SQL injection attempt.
475 // The idea is to ensure that the selection clause is a valid SQL expression
476 // by compiling it twice: once wrapped in parentheses and once as
477 // originally specified. An attacker cannot create an expression that
478 // would escape the SQL expression while maintaining balanced parentheses
479 // in both the wrapped and original forms.
480
481 // NOTE: The ordering of the below operations is important; we must
482 // execute the wrapped query to ensure the untrusted clause has been
483 // fully isolated.
484
485 // Validate the unwrapped query
486 db.validateSql(unwrappedSql, null); // will throw if query is invalid
487
488 // Execute wrapped query for extra protection
489 final String wrappedSql = buildUpdate(values, wrap(selection));
490 sql = wrappedSql;
491 } else {
492 // Execute unwrapped query
493 sql = unwrappedSql;
494 }
495
Jeff Sharkey0f732192018-07-26 09:39:18 -0600496 if (selectionArgs == null) {
497 selectionArgs = EmptyArray.STRING;
Jeff Sharkey234de022018-07-25 14:52:14 -0600498 }
Jeff Sharkey50699422018-07-25 14:52:14 -0600499 final String[] rawKeys = values.keySet().toArray(EmptyArray.STRING);
500 final int valuesLength = rawKeys.length;
Jeff Sharkey0f732192018-07-26 09:39:18 -0600501 final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length];
502 for (int i = 0; i < sqlArgs.length; i++) {
503 if (i < valuesLength) {
Jeff Sharkey50699422018-07-25 14:52:14 -0600504 sqlArgs[i] = values.get(rawKeys[i]);
Jeff Sharkey0f732192018-07-26 09:39:18 -0600505 } else {
506 sqlArgs[i] = selectionArgs[i - valuesLength];
507 }
508 }
Jeff Sharkey234de022018-07-25 14:52:14 -0600509 if (Log.isLoggable(TAG, Log.DEBUG)) {
510 if (Build.IS_DEBUGGABLE) {
511 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
512 } else {
513 Log.d(TAG, sql);
514 }
515 }
516 return db.executeSql(sql, sqlArgs);
517 }
518
519 /**
520 * Perform a delete by combining all current settings and the
521 * information passed into this method.
522 *
523 * @param db the database to delete on
524 * @param selection A filter declaring which rows to return,
525 * formatted as an SQL WHERE clause (excluding the WHERE
526 * itself). Passing null will return all rows for the given URL.
527 * @param selectionArgs You may include ?s in selection, which
528 * will be replaced by the values from selectionArgs, in order
529 * that they appear in the selection. The values will be bound
530 * as Strings.
531 * @return the number of rows deleted
Jeff Sharkey234de022018-07-25 14:52:14 -0600532 */
533 public int delete(@NonNull SQLiteDatabase db, @Nullable String selection,
534 @Nullable String[] selectionArgs) {
535 Objects.requireNonNull(mTables, "No tables defined");
536 Objects.requireNonNull(db, "No database defined");
537
538 final String sql;
539 final String unwrappedSql = buildDelete(selection);
540
541 if (mStrict) {
542 // Validate the user-supplied selection to detect syntactic anomalies
543 // in the selection string that could indicate a SQL injection attempt.
544 // The idea is to ensure that the selection clause is a valid SQL expression
545 // by compiling it twice: once wrapped in parentheses and once as
546 // originally specified. An attacker cannot create an expression that
547 // would escape the SQL expression while maintaining balanced parentheses
548 // in both the wrapped and original forms.
549
550 // NOTE: The ordering of the below operations is important; we must
551 // execute the wrapped query to ensure the untrusted clause has been
552 // fully isolated.
553
554 // Validate the unwrapped query
555 db.validateSql(unwrappedSql, null); // will throw if query is invalid
556
557 // Execute wrapped query for extra protection
558 final String wrappedSql = buildDelete(wrap(selection));
559 sql = wrappedSql;
560 } else {
561 // Execute unwrapped query
562 sql = unwrappedSql;
563 }
564
565 final String[] sqlArgs = selectionArgs;
566 if (Log.isLoggable(TAG, Log.DEBUG)) {
567 if (Build.IS_DEBUGGABLE) {
568 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
569 } else {
570 Log.d(TAG, sql);
571 }
572 }
573 return db.executeSql(sql, sqlArgs);
574 }
575
576 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700577 * Construct a SELECT statement suitable for use in a group of
578 * SELECT statements that will be joined through UNION operators
579 * in buildUnionQuery.
580 *
581 * @param projectionIn A list of which columns to return. Passing
582 * null will return all columns, which is discouraged to
583 * prevent reading data from storage that isn't going to be
584 * used.
585 * @param selection A filter declaring which rows to return,
586 * formatted as an SQL WHERE clause (excluding the WHERE
587 * itself). Passing null will return all rows for the given
588 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700589 * @param groupBy A filter declaring how to group rows, formatted
590 * as an SQL GROUP BY clause (excluding the GROUP BY itself).
591 * Passing null will cause the rows to not be grouped.
592 * @param having A filter declare which row groups to include in
593 * the cursor, if row grouping is being used, formatted as an
594 * SQL HAVING clause (excluding the HAVING itself). Passing
595 * null will cause all row groups to be included, and is
596 * required when row grouping is not being used.
597 * @param sortOrder How to order the rows, formatted as an SQL
598 * ORDER BY clause (excluding the ORDER BY itself). Passing null
599 * will use the default sort order, which may be unordered.
600 * @param limit Limits the number of rows returned by the query,
601 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
602 * @return the resulting SQL SELECT statement
603 */
604 public String buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100605 String[] projectionIn, String selection, String groupBy,
606 String having, String sortOrder, String limit) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700607 String[] projection = computeProjection(projectionIn);
Jeff Sharkey234de022018-07-25 14:52:14 -0600608 String where = computeWhere(selection);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700609
610 return buildQueryString(
Jeff Sharkey234de022018-07-25 14:52:14 -0600611 mDistinct, mTables, projection, where,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700612 groupBy, having, sortOrder, limit);
613 }
614
615 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100616 * @deprecated This method's signature is misleading since no SQL parameter
617 * substitution is carried out. The selection arguments parameter does not get
618 * used at all. To avoid confusion, call
619 * {@link #buildQuery(String[], String, String, String, String, String)} instead.
620 */
621 @Deprecated
622 public String buildQuery(
623 String[] projectionIn, String selection, String[] selectionArgs,
624 String groupBy, String having, String sortOrder, String limit) {
625 return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
626 }
627
Jeff Sharkey234de022018-07-25 14:52:14 -0600628 /** {@hide} */
629 public String buildUpdate(ContentValues values, String selection) {
Jeff Sharkey50699422018-07-25 14:52:14 -0600630 if (values == null || values.size() == 0) {
Jeff Sharkey234de022018-07-25 14:52:14 -0600631 throw new IllegalArgumentException("Empty values");
632 }
633
634 StringBuilder sql = new StringBuilder(120);
635 sql.append("UPDATE ");
636 sql.append(mTables);
637 sql.append(" SET ");
638
Jeff Sharkey50699422018-07-25 14:52:14 -0600639 final String[] rawKeys = values.keySet().toArray(EmptyArray.STRING);
640 for (int i = 0; i < rawKeys.length; i++) {
Jeff Sharkey234de022018-07-25 14:52:14 -0600641 if (i > 0) {
642 sql.append(',');
643 }
Jeff Sharkey50699422018-07-25 14:52:14 -0600644 sql.append(rawKeys[i]);
Jeff Sharkey234de022018-07-25 14:52:14 -0600645 sql.append("=?");
646 }
647
648 final String where = computeWhere(selection);
649 appendClause(sql, " WHERE ", where);
650 return sql.toString();
651 }
652
653 /** {@hide} */
654 public String buildDelete(String selection) {
655 StringBuilder sql = new StringBuilder(120);
656 sql.append("DELETE FROM ");
657 sql.append(mTables);
658
659 final String where = computeWhere(selection);
660 appendClause(sql, " WHERE ", where);
661 return sql.toString();
662 }
663
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100664 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700665 * Construct a SELECT statement suitable for use in a group of
666 * SELECT statements that will be joined through UNION operators
667 * in buildUnionQuery.
668 *
669 * @param typeDiscriminatorColumn the name of the result column
670 * whose cells will contain the name of the table from which
671 * each row was drawn.
672 * @param unionColumns the names of the columns to appear in the
673 * result. This may include columns that do not appear in the
674 * table this SELECT is querying (i.e. mTables), but that do
675 * appear in one of the other tables in the UNION query that we
676 * are constructing.
677 * @param columnsPresentInTable a Set of the names of the columns
678 * that appear in this table (i.e. in the table whose name is
679 * mTables). Since columns in unionColumns include columns that
680 * appear only in other tables, we use this array to distinguish
681 * which ones actually are present. Other columns will have
682 * NULL values for results from this subquery.
683 * @param computedColumnsOffset all columns in unionColumns before
684 * this index are included under the assumption that they're
685 * computed and therefore won't appear in columnsPresentInTable,
686 * e.g. "date * 1000 as normalized_date"
687 * @param typeDiscriminatorValue the value used for the
688 * type-discriminator column in this subquery
689 * @param selection A filter declaring which rows to return,
690 * formatted as an SQL WHERE clause (excluding the WHERE
691 * itself). Passing null will return all rows for the given
692 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700693 * @param groupBy A filter declaring how to group rows, formatted
694 * as an SQL GROUP BY clause (excluding the GROUP BY itself).
695 * Passing null will cause the rows to not be grouped.
696 * @param having A filter declare which row groups to include in
697 * the cursor, if row grouping is being used, formatted as an
698 * SQL HAVING clause (excluding the HAVING itself). Passing
699 * null will cause all row groups to be included, and is
700 * required when row grouping is not being used.
701 * @return the resulting SQL SELECT statement
702 */
703 public String buildUnionSubQuery(
704 String typeDiscriminatorColumn,
705 String[] unionColumns,
706 Set<String> columnsPresentInTable,
707 int computedColumnsOffset,
708 String typeDiscriminatorValue,
709 String selection,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700710 String groupBy,
711 String having) {
712 int unionColumnsCount = unionColumns.length;
713 String[] projectionIn = new String[unionColumnsCount];
714
715 for (int i = 0; i < unionColumnsCount; i++) {
716 String unionColumn = unionColumns[i];
717
718 if (unionColumn.equals(typeDiscriminatorColumn)) {
719 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
720 + typeDiscriminatorColumn;
721 } else if (i <= computedColumnsOffset
722 || columnsPresentInTable.contains(unionColumn)) {
723 projectionIn[i] = unionColumn;
724 } else {
725 projectionIn[i] = "NULL AS " + unionColumn;
726 }
727 }
728 return buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100729 projectionIn, selection, groupBy, having,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700730 null /* sortOrder */,
731 null /* limit */);
732 }
733
734 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100735 * @deprecated This method's signature is misleading since no SQL parameter
736 * substitution is carried out. The selection arguments parameter does not get
737 * used at all. To avoid confusion, call
Jean-Baptiste Queruf4072fc2010-11-17 16:47:59 -0800738 * {@link #buildUnionSubQuery}
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100739 * instead.
740 */
741 @Deprecated
742 public String buildUnionSubQuery(
743 String typeDiscriminatorColumn,
744 String[] unionColumns,
745 Set<String> columnsPresentInTable,
746 int computedColumnsOffset,
747 String typeDiscriminatorValue,
748 String selection,
749 String[] selectionArgs,
750 String groupBy,
751 String having) {
752 return buildUnionSubQuery(
753 typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
754 computedColumnsOffset, typeDiscriminatorValue, selection,
755 groupBy, having);
756 }
757
758 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700759 * Given a set of subqueries, all of which are SELECT statements,
760 * construct a query that returns the union of what those
761 * subqueries return.
762 * @param subQueries an array of SQL SELECT statements, all of
763 * which must have the same columns as the same positions in
764 * their results
765 * @param sortOrder How to order the rows, formatted as an SQL
766 * ORDER BY clause (excluding the ORDER BY itself). Passing
767 * null will use the default sort order, which may be unordered.
768 * @param limit The limit clause, which applies to the entire union result set
769 *
770 * @return the resulting SQL SELECT statement
771 */
772 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
773 StringBuilder query = new StringBuilder(128);
774 int subQueryCount = subQueries.length;
775 String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
776
777 for (int i = 0; i < subQueryCount; i++) {
778 if (i > 0) {
779 query.append(unionOperator);
780 }
781 query.append(subQueries[i]);
782 }
783 appendClause(query, " ORDER BY ", sortOrder);
784 appendClause(query, " LIMIT ", limit);
785 return query.toString();
786 }
787
Mathew Inwood55418ea2018-12-20 15:30:45 +0000788 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700789 private String[] computeProjection(String[] projectionIn) {
790 if (projectionIn != null && projectionIn.length > 0) {
791 if (mProjectionMap != null) {
792 String[] projection = new String[projectionIn.length];
793 int length = projectionIn.length;
794
795 for (int i = 0; i < length; i++) {
796 String userColumn = projectionIn[i];
797 String column = mProjectionMap.get(userColumn);
798
Michael Chan99c44832009-04-27 16:28:51 -0700799 if (column != null) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700800 projection[i] = column;
Michael Chan99c44832009-04-27 16:28:51 -0700801 continue;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700802 }
Michael Chan99c44832009-04-27 16:28:51 -0700803
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700804 if (!mStrict &&
Dmitri Plotnikov40eb4aa2010-04-14 16:09:46 -0700805 ( userColumn.contains(" AS ") || userColumn.contains(" as "))) {
Michael Chan99c44832009-04-27 16:28:51 -0700806 /* A column alias already exist */
807 projection[i] = userColumn;
808 continue;
809 }
810
811 throw new IllegalArgumentException("Invalid column "
812 + projectionIn[i]);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700813 }
814 return projection;
815 } else {
816 return projectionIn;
817 }
818 } else if (mProjectionMap != null) {
819 // Return all columns in projection map.
820 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
821 String[] projection = new String[entrySet.size()];
822 Iterator<Entry<String, String>> entryIter = entrySet.iterator();
823 int i = 0;
824
825 while (entryIter.hasNext()) {
826 Entry<String, String> entry = entryIter.next();
827
828 // Don't include the _count column when people ask for no projection.
829 if (entry.getKey().equals(BaseColumns._COUNT)) {
830 continue;
831 }
832 projection[i++] = entry.getValue();
833 }
834 return projection;
835 }
836 return null;
837 }
Jeff Sharkey234de022018-07-25 14:52:14 -0600838
839 private @Nullable String computeWhere(@Nullable String selection) {
840 final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
841 final boolean hasExternal = !TextUtils.isEmpty(selection);
842
843 if (hasInternal || hasExternal) {
844 final StringBuilder where = new StringBuilder();
845 if (hasInternal) {
846 where.append('(').append(mWhereClause).append(')');
847 }
848 if (hasInternal && hasExternal) {
849 where.append(" AND ");
850 }
851 if (hasExternal) {
852 where.append('(').append(selection).append(')');
853 }
854 return where.toString();
855 } else {
856 return null;
857 }
858 }
859
860 /**
861 * Wrap given argument in parenthesis, unless it's {@code null} or
862 * {@code ()}, in which case return it verbatim.
863 */
864 private @Nullable String wrap(@Nullable String arg) {
865 if (TextUtils.isEmpty(arg)) {
866 return arg;
867 } else {
868 return "(" + arg + ")";
869 }
870 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700871}