blob: 5e7d5853483123bd7347caa092868367325d3313 [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
Mathew Inwoodf86bea92018-08-10 16:10:20 +010019import android.annotation.UnsupportedAppUsage;
Jeff Sharkeye56d8b82018-07-25 14:52:14 -060020import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.content.ContentValues;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070023import android.database.Cursor;
24import android.database.DatabaseUtils;
Jeff Sharkeye56d8b82018-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 Sharkeye56d8b82018-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 Sharkey8a634372018-07-26 09:39:18 -060033import libcore.util.EmptyArray;
Jeff Sharkeye56d8b82018-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 Sharkeye56d8b82018-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 Sharkey0da04832018-07-26 14:36:59 -060047public class SQLiteQueryBuilder {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070048 private static final String TAG = "SQLiteQueryBuilder";
Owen Linab18d1f2009-05-06 16:45:59 -070049 private static final Pattern sLimitPattern =
50 Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070051
52 private Map<String, String> mProjectionMap = null;
Mathew Inwoodf86bea92018-08-10 16:10:20 +010053 @UnsupportedAppUsage
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070054 private String mTables = "";
Mathew Inwoodf86bea92018-08-10 16:10:20 +010055 @UnsupportedAppUsage
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -070056 private StringBuilder mWhereClause = null; // lazily created
Mathew Inwoodf86bea92018-08-10 16:10:20 +010057 @UnsupportedAppUsage
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070058 private boolean mDistinct;
59 private SQLiteDatabase.CursorFactory mFactory;
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -070060 private boolean mStrict;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070061
62 public SQLiteQueryBuilder() {
63 mDistinct = false;
64 mFactory = null;
65 }
66
67 /**
68 * Mark the query as DISTINCT.
69 *
70 * @param distinct if true the query is DISTINCT, otherwise it isn't
71 */
72 public void setDistinct(boolean distinct) {
73 mDistinct = distinct;
74 }
75
76 /**
77 * Returns the list of tables being queried
78 *
79 * @return the list of tables being queried
80 */
81 public String getTables() {
82 return mTables;
83 }
84
85 /**
86 * Sets the list of tables to query. Multiple tables can be specified to perform a join.
87 * For example:
88 * setTables("foo, bar")
89 * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
90 *
91 * @param inTables the list of tables to query on
92 */
93 public void setTables(String inTables) {
94 mTables = inTables;
95 }
96
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070097 /**
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -060098 * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
99 * by parenthesis and ANDed with the selection passed to {@link #query}. The final
100 * WHERE clause looks like:
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700101 *
102 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
103 *
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600104 * @param inWhere the chunk of text to append to the WHERE clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700105 */
Jeff Sharkey0da04832018-07-26 14:36:59 -0600106 public void appendWhere(@NonNull CharSequence inWhere) {
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -0700107 if (mWhereClause == null) {
108 mWhereClause = new StringBuilder(inWhere.length() + 16);
109 }
Jeff Sharkey3e26b7d2018-07-12 19:47:49 -0600110 mWhereClause.append(inWhere);
Jeff Sharkey3e26b7d2018-07-12 19:47:49 -0600111 }
112
113 /**
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600114 * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
115 * by parenthesis and ANDed with the selection passed to {@link #query}. The final
116 * WHERE clause looks like:
Jeff Sharkey55777e52018-07-16 14:34:21 -0600117 *
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600118 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
Jeff Sharkey55777e52018-07-16 14:34:21 -0600119 *
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600120 * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
121 * to avoid SQL injection attacks
Jeff Sharkey55777e52018-07-16 14:34:21 -0600122 */
Jeff Sharkey0da04832018-07-26 14:36:59 -0600123 public void appendWhereEscapeString(@NonNull String inWhere) {
Jeff Sharkey55777e52018-07-16 14:34:21 -0600124 if (mWhereClause == null) {
125 mWhereClause = new StringBuilder(inWhere.length() + 16);
126 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700127 DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
128 }
129
130 /**
Jeff Sharkey0da04832018-07-26 14:36:59 -0600131 * Add a standalone chunk to the {@code WHERE} clause of this query.
132 * <p>
133 * This method differs from {@link #appendWhere(CharSequence)} in that it
134 * automatically appends {@code AND} to any existing {@code WHERE} clause
135 * already under construction before appending the given standalone
136 * expression wrapped in parentheses.
137 *
138 * @param inWhere the standalone expression to append to the {@code WHERE}
139 * clause. It will be wrapped in parentheses when it's appended.
140 */
141 public void appendWhereStandalone(@NonNull CharSequence inWhere) {
142 if (mWhereClause == null) {
143 mWhereClause = new StringBuilder(inWhere.length() + 16);
144 }
145 if (mWhereClause.length() > 0) {
146 mWhereClause.append(" AND ");
147 }
148 mWhereClause.append('(').append(inWhere).append(')');
149 }
150
151 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700152 * Sets the projection map for the query. The projection map maps
153 * from column names that the caller passes into query to database
154 * column names. This is useful for renaming columns as well as
155 * disambiguating column names when doing joins. For example you
156 * could map "name" to "people.name". If a projection map is set
157 * it must contain all column names the user may request, even if
158 * the key and value are the same.
159 *
160 * @param columnMap maps from the user column names to the database column names
161 */
162 public void setProjectionMap(Map<String, String> columnMap) {
163 mProjectionMap = columnMap;
164 }
165
166 /**
167 * Sets the cursor factory to be used for the query. You can use
168 * one factory for all queries on a database but it is normally
Jeff Brown75ea64f2012-01-25 19:37:13 -0800169 * easier to specify the factory when doing this query.
170 *
171 * @param factory the factory to use.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700172 */
173 public void setCursorFactory(SQLiteDatabase.CursorFactory factory) {
174 mFactory = factory;
175 }
176
177 /**
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700178 * When set, the selection is verified against malicious arguments.
179 * When using this class to create a statement using
180 * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
181 * non-numeric limits will raise an exception. If a projection map is specified, fields
182 * not in that map will be ignored.
183 * If this class is used to execute the statement directly using
184 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
185 * or
186 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
187 * additionally also parenthesis escaping selection are caught.
188 *
189 * To summarize: To get maximum protection against malicious third party apps (for example
190 * content provider consumers), make sure to do the following:
191 * <ul>
192 * <li>Set this value to true</li>
193 * <li>Use a projection map</li>
194 * <li>Use one of the query overloads instead of getting the statement as a sql string</li>
195 * </ul>
196 * By default, this value is false.
197 */
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600198 public void setStrict(boolean flag) {
199 mStrict = flag;
Dmitri Plotnikov40eb4aa2010-04-14 16:09:46 -0700200 }
201
202 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700203 * Build an SQL query string from the given clauses.
204 *
205 * @param distinct true if you want each row to be unique, false otherwise.
206 * @param tables The table names to compile the query against.
207 * @param columns A list of which columns to return. Passing null will
208 * return all columns, which is discouraged to prevent reading
209 * data from storage that isn't going to be used.
210 * @param where A filter declaring which rows to return, formatted as an SQL
211 * WHERE clause (excluding the WHERE itself). Passing null will
212 * return all rows for the given URL.
213 * @param groupBy A filter declaring how to group rows, formatted as an SQL
214 * GROUP BY clause (excluding the GROUP BY itself). Passing null
215 * will cause the rows to not be grouped.
216 * @param having A filter declare which row groups to include in the cursor,
217 * if row grouping is being used, formatted as an SQL HAVING
218 * clause (excluding the HAVING itself). Passing null will cause
219 * all row groups to be included, and is required when row
220 * grouping is not being used.
221 * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
222 * (excluding the ORDER BY itself). Passing null will use the
223 * default sort order, which may be unordered.
224 * @param limit Limits the number of rows returned by the query,
225 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
226 * @return the SQL query string
227 */
228 public static String buildQueryString(
229 boolean distinct, String tables, String[] columns, String where,
230 String groupBy, String having, String orderBy, String limit) {
231 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
232 throw new IllegalArgumentException(
233 "HAVING clauses are only permitted when using a groupBy clause");
234 }
Owen Linab18d1f2009-05-06 16:45:59 -0700235 if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) {
236 throw new IllegalArgumentException("invalid LIMIT clauses:" + limit);
237 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700238
239 StringBuilder query = new StringBuilder(120);
240
241 query.append("SELECT ");
242 if (distinct) {
243 query.append("DISTINCT ");
244 }
245 if (columns != null && columns.length != 0) {
246 appendColumns(query, columns);
247 } else {
248 query.append("* ");
249 }
250 query.append("FROM ");
251 query.append(tables);
252 appendClause(query, " WHERE ", where);
253 appendClause(query, " GROUP BY ", groupBy);
254 appendClause(query, " HAVING ", having);
255 appendClause(query, " ORDER BY ", orderBy);
Owen Linab18d1f2009-05-06 16:45:59 -0700256 appendClause(query, " LIMIT ", limit);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700257
258 return query.toString();
259 }
260
261 private static void appendClause(StringBuilder s, String name, String clause) {
262 if (!TextUtils.isEmpty(clause)) {
263 s.append(name);
264 s.append(clause);
265 }
266 }
267
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700268 /**
269 * Add the names that are non-null in columns to s, separating
270 * them with commas.
271 */
272 public static void appendColumns(StringBuilder s, String[] columns) {
273 int n = columns.length;
274
275 for (int i = 0; i < n; i++) {
276 String column = columns[i];
277
278 if (column != null) {
279 if (i > 0) {
280 s.append(", ");
281 }
282 s.append(column);
283 }
284 }
285 s.append(' ');
286 }
287
288 /**
289 * Perform a query by combining all current settings and the
290 * information passed into this method.
291 *
292 * @param db the database to query on
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600293 * @param projectionIn A list of which columns to return. Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700294 * null will return all columns, which is discouraged to prevent
295 * reading data from storage that isn't going to be used.
296 * @param selection A filter declaring which rows to return,
297 * formatted as an SQL WHERE clause (excluding the WHERE
298 * itself). Passing null will return all rows for the given URL.
299 * @param selectionArgs You may include ?s in selection, which
300 * will be replaced by the values from selectionArgs, in order
301 * that they appear in the selection. The values will be bound
302 * as Strings.
303 * @param groupBy A filter declaring how to group rows, formatted
304 * as an SQL GROUP BY clause (excluding the GROUP BY
305 * itself). Passing null will cause the rows to not be grouped.
306 * @param having A filter declare which row groups to include in
307 * the cursor, if row grouping is being used, formatted as an
308 * SQL HAVING clause (excluding the HAVING itself). Passing
309 * null will cause all row groups to be included, and is
310 * required when row grouping is not being used.
311 * @param sortOrder How to order the rows, formatted as an SQL
312 * ORDER BY clause (excluding the ORDER BY itself). Passing null
313 * will use the default sort order, which may be unordered.
314 * @return a cursor over the result set
315 * @see android.content.ContentResolver#query(android.net.Uri, String[],
316 * String, String[], String)
317 */
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600318 public Cursor query(SQLiteDatabase db, String[] projectionIn,
319 String selection, String[] selectionArgs, String groupBy,
320 String having, String sortOrder) {
321 return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
Jeff Brown4c1241d2012-02-02 17:05:00 -0800322 null /* limit */, null /* cancellationSignal */);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700323 }
324
325 /**
326 * Perform a query by combining all current settings and the
327 * information passed into this method.
328 *
329 * @param db the database to query on
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600330 * @param projectionIn A list of which columns to return. Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700331 * null will return all columns, which is discouraged to prevent
332 * reading data from storage that isn't going to be used.
333 * @param selection A filter declaring which rows to return,
334 * formatted as an SQL WHERE clause (excluding the WHERE
335 * itself). Passing null will return all rows for the given URL.
336 * @param selectionArgs You may include ?s in selection, which
337 * will be replaced by the values from selectionArgs, in order
338 * that they appear in the selection. The values will be bound
339 * as Strings.
340 * @param groupBy A filter declaring how to group rows, formatted
341 * as an SQL GROUP BY clause (excluding the GROUP BY
342 * itself). Passing null will cause the rows to not be grouped.
343 * @param having A filter declare which row groups to include in
344 * the cursor, if row grouping is being used, formatted as an
345 * SQL HAVING clause (excluding the HAVING itself). Passing
346 * null will cause all row groups to be included, and is
347 * required when row grouping is not being used.
348 * @param sortOrder How to order the rows, formatted as an SQL
349 * ORDER BY clause (excluding the ORDER BY itself). Passing null
350 * will use the default sort order, which may be unordered.
351 * @param limit Limits the number of rows returned by the query,
352 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
353 * @return a cursor over the result set
354 * @see android.content.ContentResolver#query(android.net.Uri, String[],
355 * String, String[], String)
356 */
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600357 public Cursor query(SQLiteDatabase db, String[] projectionIn,
358 String selection, String[] selectionArgs, String groupBy,
359 String having, String sortOrder, String limit) {
360 return query(db, projectionIn, selection, selectionArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800361 groupBy, having, sortOrder, limit, null);
362 }
363
364 /**
365 * Perform a query by combining all current settings and the
366 * information passed into this method.
367 *
368 * @param db the database to query on
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600369 * @param projectionIn A list of which columns to return. Passing
Jeff Brown75ea64f2012-01-25 19:37:13 -0800370 * null will return all columns, which is discouraged to prevent
371 * reading data from storage that isn't going to be used.
372 * @param selection A filter declaring which rows to return,
373 * formatted as an SQL WHERE clause (excluding the WHERE
374 * itself). Passing null will return all rows for the given URL.
375 * @param selectionArgs You may include ?s in selection, which
376 * will be replaced by the values from selectionArgs, in order
377 * that they appear in the selection. The values will be bound
378 * as Strings.
379 * @param groupBy A filter declaring how to group rows, formatted
380 * as an SQL GROUP BY clause (excluding the GROUP BY
381 * itself). Passing null will cause the rows to not be grouped.
382 * @param having A filter declare which row groups to include in
383 * the cursor, if row grouping is being used, formatted as an
384 * SQL HAVING clause (excluding the HAVING itself). Passing
385 * null will cause all row groups to be included, and is
386 * required when row grouping is not being used.
387 * @param sortOrder How to order the rows, formatted as an SQL
388 * ORDER BY clause (excluding the ORDER BY itself). Passing null
389 * will use the default sort order, which may be unordered.
390 * @param limit Limits the number of rows returned by the query,
391 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
Jeff Brown4c1241d2012-02-02 17:05:00 -0800392 * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
Jeff Brown75ea64f2012-01-25 19:37:13 -0800393 * If the operation is canceled, then {@link OperationCanceledException} will be thrown
394 * when the query is executed.
395 * @return a cursor over the result set
396 * @see android.content.ContentResolver#query(android.net.Uri, String[],
397 * String, String[], String)
398 */
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600399 public Cursor query(SQLiteDatabase db, String[] projectionIn,
400 String selection, String[] selectionArgs, String groupBy,
401 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
402 if (mTables == null) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700403 return null;
404 }
405
Jeff Sharkeyb17c8e72018-07-25 14:01:59 -0600406 final String sql;
407 final String unwrappedSql = buildQuery(
408 projectionIn, selection, groupBy, having,
409 sortOrder, limit);
410
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600411 if (mStrict && selection != null && selection.length() > 0) {
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700412 // Validate the user-supplied selection to detect syntactic anomalies
413 // in the selection string that could indicate a SQL injection attempt.
414 // The idea is to ensure that the selection clause is a valid SQL expression
415 // by compiling it twice: once wrapped in parentheses and once as
416 // originally specified. An attacker cannot create an expression that
417 // would escape the SQL expression while maintaining balanced parentheses
418 // in both the wrapped and original forms.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700419
Jeff Sharkeyb17c8e72018-07-25 14:01:59 -0600420 // NOTE: The ordering of the below operations is important; we must
421 // execute the wrapped query to ensure the untrusted clause has been
422 // fully isolated.
423
424 // Validate the unwrapped query
425 db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
426
427 // Execute wrapped query for extra protection
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600428 final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
Jeff Sharkeyb17c8e72018-07-25 14:01:59 -0600429 having, sortOrder, limit);
430 sql = wrappedSql;
431 } else {
432 // Execute unwrapped query
433 sql = unwrappedSql;
434 }
Jeff Sharkey3e26b7d2018-07-12 19:47:49 -0600435
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600436 final String[] sqlArgs = selectionArgs;
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600437 if (Log.isLoggable(TAG, Log.DEBUG)) {
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600438 if (Build.IS_DEBUGGABLE) {
439 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
440 } else {
441 Log.d(TAG, sql);
442 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700443 }
444 return db.rawQueryWithFactory(
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600445 mFactory, sql, sqlArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800446 SQLiteDatabase.findEditTable(mTables),
Jeff Brown4c1241d2012-02-02 17:05:00 -0800447 cancellationSignal); // will throw if query is invalid
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700448 }
449
450 /**
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600451 * Perform an update by combining all current settings and the
452 * information passed into this method.
453 *
454 * @param db the database to update on
455 * @param selection A filter declaring which rows to return,
456 * formatted as an SQL WHERE clause (excluding the WHERE
457 * itself). Passing null will return all rows for the given URL.
458 * @param selectionArgs You may include ?s in selection, which
459 * will be replaced by the values from selectionArgs, in order
460 * that they appear in the selection. The values will be bound
461 * as Strings.
462 * @return the number of rows updated
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600463 */
464 public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values,
465 @Nullable String selection, @Nullable String[] selectionArgs) {
466 Objects.requireNonNull(mTables, "No tables defined");
467 Objects.requireNonNull(db, "No database defined");
468 Objects.requireNonNull(values, "No values defined");
469
470 final String sql;
471 final String unwrappedSql = buildUpdate(values, selection);
472
473 if (mStrict) {
474 // Validate the user-supplied selection to detect syntactic anomalies
475 // in the selection string that could indicate a SQL injection attempt.
476 // The idea is to ensure that the selection clause is a valid SQL expression
477 // by compiling it twice: once wrapped in parentheses and once as
478 // originally specified. An attacker cannot create an expression that
479 // would escape the SQL expression while maintaining balanced parentheses
480 // in both the wrapped and original forms.
481
482 // NOTE: The ordering of the below operations is important; we must
483 // execute the wrapped query to ensure the untrusted clause has been
484 // fully isolated.
485
486 // Validate the unwrapped query
487 db.validateSql(unwrappedSql, null); // will throw if query is invalid
488
489 // Execute wrapped query for extra protection
490 final String wrappedSql = buildUpdate(values, wrap(selection));
491 sql = wrappedSql;
492 } else {
493 // Execute unwrapped query
494 sql = unwrappedSql;
495 }
496
Jeff Sharkey8a634372018-07-26 09:39:18 -0600497 if (selectionArgs == null) {
498 selectionArgs = EmptyArray.STRING;
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600499 }
Jeff Sharkey8a634372018-07-26 09:39:18 -0600500 final ArrayMap<String, Object> rawValues = values.getValues();
501 final int valuesLength = rawValues.size();
502 final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length];
503 for (int i = 0; i < sqlArgs.length; i++) {
504 if (i < valuesLength) {
505 sqlArgs[i] = rawValues.valueAt(i);
506 } else {
507 sqlArgs[i] = selectionArgs[i - valuesLength];
508 }
509 }
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600510 if (Log.isLoggable(TAG, Log.DEBUG)) {
511 if (Build.IS_DEBUGGABLE) {
512 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
513 } else {
514 Log.d(TAG, sql);
515 }
516 }
517 return db.executeSql(sql, sqlArgs);
518 }
519
520 /**
521 * Perform a delete by combining all current settings and the
522 * information passed into this method.
523 *
524 * @param db the database to delete on
525 * @param selection A filter declaring which rows to return,
526 * formatted as an SQL WHERE clause (excluding the WHERE
527 * itself). Passing null will return all rows for the given URL.
528 * @param selectionArgs You may include ?s in selection, which
529 * will be replaced by the values from selectionArgs, in order
530 * that they appear in the selection. The values will be bound
531 * as Strings.
532 * @return the number of rows deleted
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600533 */
534 public int delete(@NonNull SQLiteDatabase db, @Nullable String selection,
535 @Nullable String[] selectionArgs) {
536 Objects.requireNonNull(mTables, "No tables defined");
537 Objects.requireNonNull(db, "No database defined");
538
539 final String sql;
540 final String unwrappedSql = buildDelete(selection);
541
542 if (mStrict) {
543 // Validate the user-supplied selection to detect syntactic anomalies
544 // in the selection string that could indicate a SQL injection attempt.
545 // The idea is to ensure that the selection clause is a valid SQL expression
546 // by compiling it twice: once wrapped in parentheses and once as
547 // originally specified. An attacker cannot create an expression that
548 // would escape the SQL expression while maintaining balanced parentheses
549 // in both the wrapped and original forms.
550
551 // NOTE: The ordering of the below operations is important; we must
552 // execute the wrapped query to ensure the untrusted clause has been
553 // fully isolated.
554
555 // Validate the unwrapped query
556 db.validateSql(unwrappedSql, null); // will throw if query is invalid
557
558 // Execute wrapped query for extra protection
559 final String wrappedSql = buildDelete(wrap(selection));
560 sql = wrappedSql;
561 } else {
562 // Execute unwrapped query
563 sql = unwrappedSql;
564 }
565
566 final String[] sqlArgs = selectionArgs;
567 if (Log.isLoggable(TAG, Log.DEBUG)) {
568 if (Build.IS_DEBUGGABLE) {
569 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
570 } else {
571 Log.d(TAG, sql);
572 }
573 }
574 return db.executeSql(sql, sqlArgs);
575 }
576
577 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700578 * Construct a SELECT statement suitable for use in a group of
579 * SELECT statements that will be joined through UNION operators
580 * in buildUnionQuery.
581 *
582 * @param projectionIn A list of which columns to return. Passing
583 * null will return all columns, which is discouraged to
584 * prevent reading data from storage that isn't going to be
585 * used.
586 * @param selection A filter declaring which rows to return,
587 * formatted as an SQL WHERE clause (excluding the WHERE
588 * itself). Passing null will return all rows for the given
589 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700590 * @param groupBy A filter declaring how to group rows, formatted
591 * as an SQL GROUP BY clause (excluding the GROUP BY itself).
592 * Passing null will cause the rows to not be grouped.
593 * @param having A filter declare which row groups to include in
594 * the cursor, if row grouping is being used, formatted as an
595 * SQL HAVING clause (excluding the HAVING itself). Passing
596 * null will cause all row groups to be included, and is
597 * required when row grouping is not being used.
598 * @param sortOrder How to order the rows, formatted as an SQL
599 * ORDER BY clause (excluding the ORDER BY itself). Passing null
600 * will use the default sort order, which may be unordered.
601 * @param limit Limits the number of rows returned by the query,
602 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
603 * @return the resulting SQL SELECT statement
604 */
605 public String buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100606 String[] projectionIn, String selection, String groupBy,
607 String having, String sortOrder, String limit) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700608 String[] projection = computeProjection(projectionIn);
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600609 String where = computeWhere(selection);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700610
611 return buildQueryString(
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600612 mDistinct, mTables, projection, where,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700613 groupBy, having, sortOrder, limit);
614 }
615
616 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100617 * @deprecated This method's signature is misleading since no SQL parameter
618 * substitution is carried out. The selection arguments parameter does not get
619 * used at all. To avoid confusion, call
620 * {@link #buildQuery(String[], String, String, String, String, String)} instead.
621 */
622 @Deprecated
623 public String buildQuery(
624 String[] projectionIn, String selection, String[] selectionArgs,
625 String groupBy, String having, String sortOrder, String limit) {
626 return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
627 }
628
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600629 /** {@hide} */
630 public String buildUpdate(ContentValues values, String selection) {
631 if (values == null || values.isEmpty()) {
632 throw new IllegalArgumentException("Empty values");
633 }
634
635 StringBuilder sql = new StringBuilder(120);
636 sql.append("UPDATE ");
637 sql.append(mTables);
638 sql.append(" SET ");
639
640 final ArrayMap<String, Object> rawValues = values.getValues();
641 for (int i = 0; i < rawValues.size(); i++) {
642 if (i > 0) {
643 sql.append(',');
644 }
645 sql.append(rawValues.keyAt(i));
646 sql.append("=?");
647 }
648
649 final String where = computeWhere(selection);
650 appendClause(sql, " WHERE ", where);
651 return sql.toString();
652 }
653
654 /** {@hide} */
655 public String buildDelete(String selection) {
656 StringBuilder sql = new StringBuilder(120);
657 sql.append("DELETE FROM ");
658 sql.append(mTables);
659
660 final String where = computeWhere(selection);
661 appendClause(sql, " WHERE ", where);
662 return sql.toString();
663 }
664
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100665 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700666 * Construct a SELECT statement suitable for use in a group of
667 * SELECT statements that will be joined through UNION operators
668 * in buildUnionQuery.
669 *
670 * @param typeDiscriminatorColumn the name of the result column
671 * whose cells will contain the name of the table from which
672 * each row was drawn.
673 * @param unionColumns the names of the columns to appear in the
674 * result. This may include columns that do not appear in the
675 * table this SELECT is querying (i.e. mTables), but that do
676 * appear in one of the other tables in the UNION query that we
677 * are constructing.
678 * @param columnsPresentInTable a Set of the names of the columns
679 * that appear in this table (i.e. in the table whose name is
680 * mTables). Since columns in unionColumns include columns that
681 * appear only in other tables, we use this array to distinguish
682 * which ones actually are present. Other columns will have
683 * NULL values for results from this subquery.
684 * @param computedColumnsOffset all columns in unionColumns before
685 * this index are included under the assumption that they're
686 * computed and therefore won't appear in columnsPresentInTable,
687 * e.g. "date * 1000 as normalized_date"
688 * @param typeDiscriminatorValue the value used for the
689 * type-discriminator column in this subquery
690 * @param selection A filter declaring which rows to return,
691 * formatted as an SQL WHERE clause (excluding the WHERE
692 * itself). Passing null will return all rows for the given
693 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700694 * @param groupBy A filter declaring how to group rows, formatted
695 * as an SQL GROUP BY clause (excluding the GROUP BY itself).
696 * Passing null will cause the rows to not be grouped.
697 * @param having A filter declare which row groups to include in
698 * the cursor, if row grouping is being used, formatted as an
699 * SQL HAVING clause (excluding the HAVING itself). Passing
700 * null will cause all row groups to be included, and is
701 * required when row grouping is not being used.
702 * @return the resulting SQL SELECT statement
703 */
704 public String buildUnionSubQuery(
705 String typeDiscriminatorColumn,
706 String[] unionColumns,
707 Set<String> columnsPresentInTable,
708 int computedColumnsOffset,
709 String typeDiscriminatorValue,
710 String selection,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700711 String groupBy,
712 String having) {
713 int unionColumnsCount = unionColumns.length;
714 String[] projectionIn = new String[unionColumnsCount];
715
716 for (int i = 0; i < unionColumnsCount; i++) {
717 String unionColumn = unionColumns[i];
718
719 if (unionColumn.equals(typeDiscriminatorColumn)) {
720 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
721 + typeDiscriminatorColumn;
722 } else if (i <= computedColumnsOffset
723 || columnsPresentInTable.contains(unionColumn)) {
724 projectionIn[i] = unionColumn;
725 } else {
726 projectionIn[i] = "NULL AS " + unionColumn;
727 }
728 }
729 return buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100730 projectionIn, selection, groupBy, having,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700731 null /* sortOrder */,
732 null /* limit */);
733 }
734
735 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100736 * @deprecated This method's signature is misleading since no SQL parameter
737 * substitution is carried out. The selection arguments parameter does not get
738 * used at all. To avoid confusion, call
Jean-Baptiste Queruf4072fc2010-11-17 16:47:59 -0800739 * {@link #buildUnionSubQuery}
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100740 * instead.
741 */
742 @Deprecated
743 public String buildUnionSubQuery(
744 String typeDiscriminatorColumn,
745 String[] unionColumns,
746 Set<String> columnsPresentInTable,
747 int computedColumnsOffset,
748 String typeDiscriminatorValue,
749 String selection,
750 String[] selectionArgs,
751 String groupBy,
752 String having) {
753 return buildUnionSubQuery(
754 typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
755 computedColumnsOffset, typeDiscriminatorValue, selection,
756 groupBy, having);
757 }
758
759 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700760 * Given a set of subqueries, all of which are SELECT statements,
761 * construct a query that returns the union of what those
762 * subqueries return.
763 * @param subQueries an array of SQL SELECT statements, all of
764 * which must have the same columns as the same positions in
765 * their results
766 * @param sortOrder How to order the rows, formatted as an SQL
767 * ORDER BY clause (excluding the ORDER BY itself). Passing
768 * null will use the default sort order, which may be unordered.
769 * @param limit The limit clause, which applies to the entire union result set
770 *
771 * @return the resulting SQL SELECT statement
772 */
773 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
774 StringBuilder query = new StringBuilder(128);
775 int subQueryCount = subQueries.length;
776 String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
777
778 for (int i = 0; i < subQueryCount; i++) {
779 if (i > 0) {
780 query.append(unionOperator);
781 }
782 query.append(subQueries[i]);
783 }
784 appendClause(query, " ORDER BY ", sortOrder);
785 appendClause(query, " LIMIT ", limit);
786 return query.toString();
787 }
788
Mathew Inwoodf86bea92018-08-10 16:10:20 +0100789 @UnsupportedAppUsage
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600790 private String[] computeProjection(String[] projectionIn) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700791 if (projectionIn != null && projectionIn.length > 0) {
792 if (mProjectionMap != null) {
793 String[] projection = new String[projectionIn.length];
794 int length = projectionIn.length;
795
796 for (int i = 0; i < length; i++) {
797 String userColumn = projectionIn[i];
798 String column = mProjectionMap.get(userColumn);
799
Michael Chan99c44832009-04-27 16:28:51 -0700800 if (column != null) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700801 projection[i] = column;
Michael Chan99c44832009-04-27 16:28:51 -0700802 continue;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700803 }
Michael Chan99c44832009-04-27 16:28:51 -0700804
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700805 if (!mStrict &&
Dmitri Plotnikov40eb4aa2010-04-14 16:09:46 -0700806 ( userColumn.contains(" AS ") || userColumn.contains(" as "))) {
Michael Chan99c44832009-04-27 16:28:51 -0700807 /* A column alias already exist */
808 projection[i] = userColumn;
809 continue;
810 }
811
812 throw new IllegalArgumentException("Invalid column "
Jeff Sharkey6c90f1d2018-07-19 09:30:16 -0600813 + projectionIn[i]);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700814 }
815 return projection;
816 } else {
817 return projectionIn;
818 }
819 } else if (mProjectionMap != null) {
820 // Return all columns in projection map.
821 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
822 String[] projection = new String[entrySet.size()];
823 Iterator<Entry<String, String>> entryIter = entrySet.iterator();
824 int i = 0;
825
826 while (entryIter.hasNext()) {
827 Entry<String, String> entry = entryIter.next();
828
829 // Don't include the _count column when people ask for no projection.
830 if (entry.getKey().equals(BaseColumns._COUNT)) {
831 continue;
832 }
833 projection[i++] = entry.getValue();
834 }
835 return projection;
836 }
837 return null;
838 }
Jeff Sharkeye56d8b82018-07-25 14:52:14 -0600839
840 private @Nullable String computeWhere(@Nullable String selection) {
841 final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
842 final boolean hasExternal = !TextUtils.isEmpty(selection);
843
844 if (hasInternal || hasExternal) {
845 final StringBuilder where = new StringBuilder();
846 if (hasInternal) {
847 where.append('(').append(mWhereClause).append(')');
848 }
849 if (hasInternal && hasExternal) {
850 where.append(" AND ");
851 }
852 if (hasExternal) {
853 where.append('(').append(selection).append(')');
854 }
855 return where.toString();
856 } else {
857 return null;
858 }
859 }
860
861 /**
862 * Wrap given argument in parenthesis, unless it's {@code null} or
863 * {@code ()}, in which case return it verbatim.
864 */
865 private @Nullable String wrap(@Nullable String arg) {
866 if (TextUtils.isEmpty(arg)) {
867 return arg;
868 } else {
869 return "(" + arg + ")";
870 }
871 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700872}