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