blob: 3523e956656ac65eba458e92a4446a44614e3796 [file] [log] [blame]
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -07001/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.database.sqlite;
18
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060019import android.annotation.NonNull;
20import android.annotation.Nullable;
Mathew Inwood41b31942018-08-10 16:00:53 +010021import android.annotation.UnsupportedAppUsage;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060022import android.content.ContentValues;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070023import android.database.Cursor;
24import android.database.DatabaseUtils;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060025import android.os.Build;
Jeff Browna7771df2012-05-07 20:06:46 -070026import android.os.CancellationSignal;
27import android.os.OperationCanceledException;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070028import android.provider.BaseColumns;
29import android.text.TextUtils;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060030import android.util.ArrayMap;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070031import android.util.Log;
32
Jeff Sharkey42122bf2018-07-26 09:39:18 -060033import libcore.util.EmptyArray;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060034
35import java.util.Arrays;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070036import java.util.Iterator;
Jeff Sharkey7049e652018-09-13 17:05:07 -060037import java.util.List;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070038import java.util.Map;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070039import java.util.Map.Entry;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -060040import java.util.Objects;
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -070041import java.util.Set;
Jeff Sharkey82d783c2019-03-29 15:46:35 -060042import java.util.regex.Matcher;
Owen Linab18d1f2009-05-06 16:45:59 -070043import java.util.regex.Pattern;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070044
45/**
Joshua Baxter3639e2f2018-03-26 14:55:14 -070046 * This is a convenience class that helps build SQL queries to be sent to
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070047 * {@link SQLiteDatabase} objects.
48 */
Jeff Sharkeyb89df9e2018-07-26 14:36:59 -060049public class SQLiteQueryBuilder {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070050 private static final String TAG = "SQLiteQueryBuilder";
Jeff Sharkey82d783c2019-03-29 15:46:35 -060051
Owen Linab18d1f2009-05-06 16:45:59 -070052 private static final Pattern sLimitPattern =
53 Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
Jeff Sharkey82d783c2019-03-29 15:46:35 -060054 private static final Pattern sAggregationPattern = Pattern.compile(
Jeff Sharkey0ab6feb2019-04-10 10:27:15 -060055 "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)");
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070056
57 private Map<String, String> mProjectionMap = null;
Jeff Sharkey7049e652018-09-13 17:05:07 -060058 private List<Pattern> mProjectionGreylist = null;
Jeff Sharkey82d783c2019-03-29 15:46:35 -060059 private boolean mProjectionAggregationAllowed = false;
Jeff Sharkey7049e652018-09-13 17:05:07 -060060
Mathew Inwood31755f92018-12-20 13:53:36 +000061 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070062 private String mTables = "";
Mathew Inwood31755f92018-12-20 13:53:36 +000063 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -070064 private StringBuilder mWhereClause = null; // lazily created
Mathew Inwood31755f92018-12-20 13:53:36 +000065 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070066 private boolean mDistinct;
67 private SQLiteDatabase.CursorFactory mFactory;
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -070068 private boolean mStrict;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070069
70 public SQLiteQueryBuilder() {
71 mDistinct = false;
72 mFactory = null;
73 }
74
75 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -070076 * Mark the query as {@code DISTINCT}.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070077 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -070078 * @param distinct if true the query is {@code DISTINCT}, otherwise it isn't
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070079 */
80 public void setDistinct(boolean distinct) {
81 mDistinct = distinct;
82 }
83
84 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -070085 * Get if the query is marked as {@code DISTINCT}, as last configured by
Jeff Sharkey0ec586b2019-02-14 15:29:16 -070086 * {@link #setDistinct(boolean)}.
87 */
Jeff Sharkeyaa28dff2019-03-25 09:28:34 -060088 public boolean isDistinct() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -070089 return mDistinct;
90 }
91
92 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070093 * Returns the list of tables being queried
94 *
95 * @return the list of tables being queried
96 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -070097 public @Nullable String getTables() {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -070098 return mTables;
99 }
100
101 /**
102 * Sets the list of tables to query. Multiple tables can be specified to perform a join.
103 * For example:
104 * setTables("foo, bar")
105 * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
106 *
107 * @param inTables the list of tables to query on
108 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700109 public void setTables(@Nullable String inTables) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700110 mTables = inTables;
111 }
112
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700113 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700114 * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded
115 * by parenthesis and {@code AND}ed with the selection passed to {@link #query}. The final
116 * {@code WHERE} clause looks like:
117 * <p>
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700118 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
119 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700120 * @param inWhere the chunk of text to append to the {@code WHERE} clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700121 */
Jeff Sharkeyb89df9e2018-07-26 14:36:59 -0600122 public void appendWhere(@NonNull CharSequence inWhere) {
Brad Fitzpatrickae6cdd12010-03-14 11:38:06 -0700123 if (mWhereClause == null) {
124 mWhereClause = new StringBuilder(inWhere.length() + 16);
125 }
Jeff Sharkey6adc98c2018-07-12 19:47:49 -0600126 mWhereClause.append(inWhere);
Jeff Sharkey6adc98c2018-07-12 19:47:49 -0600127 }
128
129 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700130 * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded
Jeff Sharkey91be9262018-07-19 09:30:16 -0600131 * by parenthesis and ANDed with the selection passed to {@link #query}. The final
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700132 * {@code WHERE} clause looks like:
133 * <p>
Jeff Sharkey91be9262018-07-19 09:30:16 -0600134 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
Jeff Sharkey99cc1182018-07-16 14:34:21 -0600135 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700136 * @param inWhere the chunk of text to append to the {@code WHERE} clause. it will be escaped
Jeff Sharkey91be9262018-07-19 09:30:16 -0600137 * to avoid SQL injection attacks
Jeff Sharkey99cc1182018-07-16 14:34:21 -0600138 */
Jeff Sharkeyb89df9e2018-07-26 14:36:59 -0600139 public void appendWhereEscapeString(@NonNull String inWhere) {
Jeff Sharkey99cc1182018-07-16 14:34:21 -0600140 if (mWhereClause == null) {
141 mWhereClause = new StringBuilder(inWhere.length() + 16);
142 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700143 DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
144 }
145
146 /**
Jeff Sharkeyb89df9e2018-07-26 14:36:59 -0600147 * Add a standalone chunk to the {@code WHERE} clause of this query.
148 * <p>
149 * This method differs from {@link #appendWhere(CharSequence)} in that it
150 * automatically appends {@code AND} to any existing {@code WHERE} clause
151 * already under construction before appending the given standalone
152 * expression wrapped in parentheses.
153 *
154 * @param inWhere the standalone expression to append to the {@code WHERE}
155 * clause. It will be wrapped in parentheses when it's appended.
156 */
157 public void appendWhereStandalone(@NonNull CharSequence inWhere) {
158 if (mWhereClause == null) {
159 mWhereClause = new StringBuilder(inWhere.length() + 16);
160 }
161 if (mWhereClause.length() > 0) {
162 mWhereClause.append(" AND ");
163 }
164 mWhereClause.append('(').append(inWhere).append(')');
165 }
166
167 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700168 * Sets the projection map for the query. The projection map maps
169 * from column names that the caller passes into query to database
170 * column names. This is useful for renaming columns as well as
171 * disambiguating column names when doing joins. For example you
172 * could map "name" to "people.name". If a projection map is set
173 * it must contain all column names the user may request, even if
174 * the key and value are the same.
175 *
176 * @param columnMap maps from the user column names to the database column names
177 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700178 public void setProjectionMap(@Nullable Map<String, String> columnMap) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700179 mProjectionMap = columnMap;
180 }
181
182 /**
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700183 * Gets the projection map for the query, as last configured by
184 * {@link #setProjectionMap(Map)}.
185 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700186 public @Nullable Map<String, String> getProjectionMap() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700187 return mProjectionMap;
188 }
189
190 /**
Jeff Sharkey7049e652018-09-13 17:05:07 -0600191 * Sets a projection greylist of columns that will be allowed through, even
192 * when {@link #setStrict(boolean)} is enabled. This provides a way for
193 * abusive custom columns like {@code COUNT(*)} to continue working.
194 *
195 * @hide
196 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700197 public void setProjectionGreylist(@Nullable List<Pattern> projectionGreylist) {
Jeff Sharkey7049e652018-09-13 17:05:07 -0600198 mProjectionGreylist = projectionGreylist;
199 }
200
201 /**
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700202 * Gets the projection greylist for the query, as last configured by
203 * {@link #setProjectionGreylist(List)}.
204 *
205 * @hide
206 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700207 public @Nullable List<Pattern> getProjectionGreylist() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700208 return mProjectionGreylist;
209 }
210
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600211 /** {@hide} */
212 public void setProjectionAggregationAllowed(boolean projectionAggregationAllowed) {
213 mProjectionAggregationAllowed = projectionAggregationAllowed;
214 }
215
216 /** {@hide} */
217 public boolean isProjectionAggregationAllowed() {
218 return mProjectionAggregationAllowed;
219 }
220
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700221 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700222 * Sets the cursor factory to be used for the query. You can use
223 * one factory for all queries on a database but it is normally
Jeff Brown75ea64f2012-01-25 19:37:13 -0800224 * easier to specify the factory when doing this query.
225 *
226 * @param factory the factory to use.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700227 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700228 public void setCursorFactory(@Nullable SQLiteDatabase.CursorFactory factory) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700229 mFactory = factory;
230 }
231
232 /**
Jeff Sharkeyaa28dff2019-03-25 09:28:34 -0600233 * Gets the cursor factory to be used for the query, as last configured by
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700234 * {@link #setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory)}.
235 */
Jeff Sharkeya30e5c32019-02-28 12:02:10 -0700236 public @Nullable SQLiteDatabase.CursorFactory getCursorFactory() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700237 return mFactory;
238 }
239
240 /**
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700241 * When set, the selection is verified against malicious arguments.
242 * When using this class to create a statement using
243 * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
244 * non-numeric limits will raise an exception. If a projection map is specified, fields
245 * not in that map will be ignored.
246 * If this class is used to execute the statement directly using
247 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
248 * or
249 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
250 * additionally also parenthesis escaping selection are caught.
251 *
252 * To summarize: To get maximum protection against malicious third party apps (for example
253 * content provider consumers), make sure to do the following:
254 * <ul>
255 * <li>Set this value to true</li>
256 * <li>Use a projection map</li>
257 * <li>Use one of the query overloads instead of getting the statement as a sql string</li>
258 * </ul>
259 * By default, this value is false.
260 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600261 public void setStrict(boolean flag) {
262 mStrict = flag;
Dmitri Plotnikov40eb4aa2010-04-14 16:09:46 -0700263 }
264
265 /**
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700266 * Get if the query is marked as strict, as last configured by
267 * {@link #setStrict(boolean)}.
268 */
Jeff Sharkeyaa28dff2019-03-25 09:28:34 -0600269 public boolean isStrict() {
Jeff Sharkey0ec586b2019-02-14 15:29:16 -0700270 return mStrict;
271 }
272
273 /**
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700274 * Build an SQL query string from the given clauses.
275 *
276 * @param distinct true if you want each row to be unique, false otherwise.
277 * @param tables The table names to compile the query against.
278 * @param columns A list of which columns to return. Passing null will
279 * return all columns, which is discouraged to prevent reading
280 * data from storage that isn't going to be used.
281 * @param where A filter declaring which rows to return, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700282 * {@code WHERE} clause (excluding the {@code WHERE} itself). Passing {@code null} will
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700283 * return all rows for the given URL.
284 * @param groupBy A filter declaring how to group rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700285 * {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). Passing {@code null}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700286 * will cause the rows to not be grouped.
287 * @param having A filter declare which row groups to include in the cursor,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700288 * if row grouping is being used, formatted as an SQL {@code HAVING}
289 * clause (excluding the {@code HAVING} itself). Passing null will cause
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700290 * all row groups to be included, and is required when row
291 * grouping is not being used.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700292 * @param orderBy How to order the rows, formatted as an SQL {@code ORDER BY} clause
293 * (excluding the {@code ORDER BY} itself). Passing null will use the
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700294 * default sort order, which may be unordered.
295 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700296 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700297 * @return the SQL query string
298 */
299 public static String buildQueryString(
300 boolean distinct, String tables, String[] columns, String where,
301 String groupBy, String having, String orderBy, String limit) {
302 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
303 throw new IllegalArgumentException(
304 "HAVING clauses are only permitted when using a groupBy clause");
305 }
Owen Linab18d1f2009-05-06 16:45:59 -0700306 if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) {
307 throw new IllegalArgumentException("invalid LIMIT clauses:" + limit);
308 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700309
310 StringBuilder query = new StringBuilder(120);
311
312 query.append("SELECT ");
313 if (distinct) {
314 query.append("DISTINCT ");
315 }
316 if (columns != null && columns.length != 0) {
317 appendColumns(query, columns);
318 } else {
319 query.append("* ");
320 }
321 query.append("FROM ");
322 query.append(tables);
323 appendClause(query, " WHERE ", where);
324 appendClause(query, " GROUP BY ", groupBy);
325 appendClause(query, " HAVING ", having);
326 appendClause(query, " ORDER BY ", orderBy);
Owen Linab18d1f2009-05-06 16:45:59 -0700327 appendClause(query, " LIMIT ", limit);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700328
329 return query.toString();
330 }
331
332 private static void appendClause(StringBuilder s, String name, String clause) {
333 if (!TextUtils.isEmpty(clause)) {
334 s.append(name);
335 s.append(clause);
336 }
337 }
338
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700339 /**
340 * Add the names that are non-null in columns to s, separating
341 * them with commas.
342 */
343 public static void appendColumns(StringBuilder s, String[] columns) {
344 int n = columns.length;
345
346 for (int i = 0; i < n; i++) {
347 String column = columns[i];
348
349 if (column != null) {
350 if (i > 0) {
351 s.append(", ");
352 }
353 s.append(column);
354 }
355 }
356 s.append(' ');
357 }
358
359 /**
360 * Perform a query by combining all current settings and the
361 * information passed into this method.
362 *
363 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600364 * @param projectionIn A list of which columns to return. Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700365 * null will return all columns, which is discouraged to prevent
366 * reading data from storage that isn't going to be used.
367 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700368 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700369 * itself). Passing null will return all rows for the given URL.
370 * @param selectionArgs You may include ?s in selection, which
371 * will be replaced by the values from selectionArgs, in order
372 * that they appear in the selection. The values will be bound
373 * as Strings.
374 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700375 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700376 * itself). Passing null will cause the rows to not be grouped.
377 * @param having A filter declare which row groups to include in
378 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700379 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700380 * null will cause all row groups to be included, and is
381 * required when row grouping is not being used.
382 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700383 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700384 * will use the default sort order, which may be unordered.
385 * @return a cursor over the result set
386 * @see android.content.ContentResolver#query(android.net.Uri, String[],
387 * String, String[], String)
388 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600389 public Cursor query(SQLiteDatabase db, String[] projectionIn,
390 String selection, String[] selectionArgs, String groupBy,
391 String having, String sortOrder) {
392 return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
Jeff Brown4c1241d2012-02-02 17:05:00 -0800393 null /* limit */, null /* cancellationSignal */);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700394 }
395
396 /**
397 * Perform a query by combining all current settings and the
398 * information passed into this method.
399 *
400 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600401 * @param projectionIn A list of which columns to return. Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700402 * null will return all columns, which is discouraged to prevent
403 * reading data from storage that isn't going to be used.
404 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700405 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700406 * itself). Passing null will return all rows for the given URL.
407 * @param selectionArgs You may include ?s in selection, which
408 * will be replaced by the values from selectionArgs, in order
409 * that they appear in the selection. The values will be bound
410 * as Strings.
411 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700412 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700413 * itself). Passing null will cause the rows to not be grouped.
414 * @param having A filter declare which row groups to include in
415 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700416 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700417 * null will cause all row groups to be included, and is
418 * required when row grouping is not being used.
419 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700420 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700421 * will use the default sort order, which may be unordered.
422 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700423 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700424 * @return a cursor over the result set
425 * @see android.content.ContentResolver#query(android.net.Uri, String[],
426 * String, String[], String)
427 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600428 public Cursor query(SQLiteDatabase db, String[] projectionIn,
429 String selection, String[] selectionArgs, String groupBy,
430 String having, String sortOrder, String limit) {
431 return query(db, projectionIn, selection, selectionArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800432 groupBy, having, sortOrder, limit, null);
433 }
434
435 /**
436 * Perform a query by combining all current settings and the
437 * information passed into this method.
438 *
439 * @param db the database to query on
Jeff Sharkey91be9262018-07-19 09:30:16 -0600440 * @param projectionIn A list of which columns to return. Passing
Jeff Brown75ea64f2012-01-25 19:37:13 -0800441 * null will return all columns, which is discouraged to prevent
442 * reading data from storage that isn't going to be used.
443 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700444 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Brown75ea64f2012-01-25 19:37:13 -0800445 * itself). Passing null will return all rows for the given URL.
446 * @param selectionArgs You may include ?s in selection, which
447 * will be replaced by the values from selectionArgs, in order
448 * that they appear in the selection. The values will be bound
449 * as Strings.
450 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700451 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
Jeff Brown75ea64f2012-01-25 19:37:13 -0800452 * itself). Passing null will cause the rows to not be grouped.
453 * @param having A filter declare which row groups to include in
454 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700455 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
Jeff Brown75ea64f2012-01-25 19:37:13 -0800456 * null will cause all row groups to be included, and is
457 * required when row grouping is not being used.
458 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700459 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
Jeff Brown75ea64f2012-01-25 19:37:13 -0800460 * will use the default sort order, which may be unordered.
461 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700462 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
Jeff Brown4c1241d2012-02-02 17:05:00 -0800463 * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
Jeff Brown75ea64f2012-01-25 19:37:13 -0800464 * If the operation is canceled, then {@link OperationCanceledException} will be thrown
465 * when the query is executed.
466 * @return a cursor over the result set
467 * @see android.content.ContentResolver#query(android.net.Uri, String[],
468 * String, String[], String)
469 */
Jeff Sharkey91be9262018-07-19 09:30:16 -0600470 public Cursor query(SQLiteDatabase db, String[] projectionIn,
471 String selection, String[] selectionArgs, String groupBy,
472 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
473 if (mTables == null) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700474 return null;
475 }
476
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600477 final String sql;
478 final String unwrappedSql = buildQuery(
479 projectionIn, selection, groupBy, having,
480 sortOrder, limit);
481
Jeff Sharkey91be9262018-07-19 09:30:16 -0600482 if (mStrict && selection != null && selection.length() > 0) {
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700483 // Validate the user-supplied selection to detect syntactic anomalies
484 // in the selection string that could indicate a SQL injection attempt.
485 // The idea is to ensure that the selection clause is a valid SQL expression
486 // by compiling it twice: once wrapped in parentheses and once as
487 // originally specified. An attacker cannot create an expression that
488 // would escape the SQL expression while maintaining balanced parentheses
489 // in both the wrapped and original forms.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700490
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600491 // NOTE: The ordering of the below operations is important; we must
492 // execute the wrapped query to ensure the untrusted clause has been
493 // fully isolated.
494
495 // Validate the unwrapped query
496 db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
497
498 // Execute wrapped query for extra protection
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600499 final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
Jeff Sharkeybe8e0d02018-07-25 14:01:59 -0600500 having, sortOrder, limit);
501 sql = wrappedSql;
502 } else {
503 // Execute unwrapped query
504 sql = unwrappedSql;
505 }
Jeff Sharkey6adc98c2018-07-12 19:47:49 -0600506
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600507 final String[] sqlArgs = selectionArgs;
Jeff Sharkey91be9262018-07-19 09:30:16 -0600508 if (Log.isLoggable(TAG, Log.DEBUG)) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600509 if (Build.IS_DEBUGGABLE) {
510 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
511 } else {
512 Log.d(TAG, sql);
513 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700514 }
515 return db.rawQueryWithFactory(
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600516 mFactory, sql, sqlArgs,
Jeff Brown75ea64f2012-01-25 19:37:13 -0800517 SQLiteDatabase.findEditTable(mTables),
Jeff Brown4c1241d2012-02-02 17:05:00 -0800518 cancellationSignal); // will throw if query is invalid
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700519 }
520
521 /**
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600522 * Perform an update by combining all current settings and the
523 * information passed into this method.
524 *
525 * @param db the database to update on
526 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700527 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600528 * 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 updated
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600534 */
535 public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values,
536 @Nullable String selection, @Nullable String[] selectionArgs) {
537 Objects.requireNonNull(mTables, "No tables defined");
538 Objects.requireNonNull(db, "No database defined");
539 Objects.requireNonNull(values, "No values defined");
540
541 final String sql;
542 final String unwrappedSql = buildUpdate(values, selection);
543
544 if (mStrict) {
545 // Validate the user-supplied selection to detect syntactic anomalies
546 // in the selection string that could indicate a SQL injection attempt.
547 // The idea is to ensure that the selection clause is a valid SQL expression
548 // by compiling it twice: once wrapped in parentheses and once as
549 // originally specified. An attacker cannot create an expression that
550 // would escape the SQL expression while maintaining balanced parentheses
551 // in both the wrapped and original forms.
552
553 // NOTE: The ordering of the below operations is important; we must
554 // execute the wrapped query to ensure the untrusted clause has been
555 // fully isolated.
556
557 // Validate the unwrapped query
558 db.validateSql(unwrappedSql, null); // will throw if query is invalid
559
560 // Execute wrapped query for extra protection
561 final String wrappedSql = buildUpdate(values, wrap(selection));
562 sql = wrappedSql;
563 } else {
564 // Execute unwrapped query
565 sql = unwrappedSql;
566 }
567
Jeff Sharkey42122bf2018-07-26 09:39:18 -0600568 if (selectionArgs == null) {
569 selectionArgs = EmptyArray.STRING;
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600570 }
Jeff Sharkey42122bf2018-07-26 09:39:18 -0600571 final ArrayMap<String, Object> rawValues = values.getValues();
572 final int valuesLength = rawValues.size();
573 final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length];
574 for (int i = 0; i < sqlArgs.length; i++) {
575 if (i < valuesLength) {
576 sqlArgs[i] = rawValues.valueAt(i);
577 } else {
578 sqlArgs[i] = selectionArgs[i - valuesLength];
579 }
580 }
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600581 if (Log.isLoggable(TAG, Log.DEBUG)) {
582 if (Build.IS_DEBUGGABLE) {
583 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
584 } else {
585 Log.d(TAG, sql);
586 }
587 }
588 return db.executeSql(sql, sqlArgs);
589 }
590
591 /**
592 * Perform a delete by combining all current settings and the
593 * information passed into this method.
594 *
595 * @param db the database to delete on
596 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700597 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600598 * itself). Passing null will return all rows for the given URL.
599 * @param selectionArgs You may include ?s in selection, which
600 * will be replaced by the values from selectionArgs, in order
601 * that they appear in the selection. The values will be bound
602 * as Strings.
603 * @return the number of rows deleted
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600604 */
605 public int delete(@NonNull SQLiteDatabase db, @Nullable String selection,
606 @Nullable String[] selectionArgs) {
607 Objects.requireNonNull(mTables, "No tables defined");
608 Objects.requireNonNull(db, "No database defined");
609
610 final String sql;
611 final String unwrappedSql = buildDelete(selection);
612
613 if (mStrict) {
614 // Validate the user-supplied selection to detect syntactic anomalies
615 // in the selection string that could indicate a SQL injection attempt.
616 // The idea is to ensure that the selection clause is a valid SQL expression
617 // by compiling it twice: once wrapped in parentheses and once as
618 // originally specified. An attacker cannot create an expression that
619 // would escape the SQL expression while maintaining balanced parentheses
620 // in both the wrapped and original forms.
621
622 // NOTE: The ordering of the below operations is important; we must
623 // execute the wrapped query to ensure the untrusted clause has been
624 // fully isolated.
625
626 // Validate the unwrapped query
627 db.validateSql(unwrappedSql, null); // will throw if query is invalid
628
629 // Execute wrapped query for extra protection
630 final String wrappedSql = buildDelete(wrap(selection));
631 sql = wrappedSql;
632 } else {
633 // Execute unwrapped query
634 sql = unwrappedSql;
635 }
636
637 final String[] sqlArgs = selectionArgs;
638 if (Log.isLoggable(TAG, Log.DEBUG)) {
639 if (Build.IS_DEBUGGABLE) {
640 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
641 } else {
642 Log.d(TAG, sql);
643 }
644 }
645 return db.executeSql(sql, sqlArgs);
646 }
647
648 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700649 * Construct a {@code SELECT} statement suitable for use in a group of
650 * {@code SELECT} statements that will be joined through {@code UNION} operators
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700651 * in buildUnionQuery.
652 *
653 * @param projectionIn A list of which columns to return. Passing
654 * null will return all columns, which is discouraged to
655 * prevent reading data from storage that isn't going to be
656 * used.
657 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700658 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700659 * itself). Passing null will return all rows for the given
660 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700661 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700662 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself).
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700663 * Passing null will cause the rows to not be grouped.
664 * @param having A filter declare which row groups to include in
665 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700666 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700667 * null will cause all row groups to be included, and is
668 * required when row grouping is not being used.
669 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700670 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700671 * will use the default sort order, which may be unordered.
672 * @param limit Limits the number of rows returned by the query,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700673 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
674 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700675 */
676 public String buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100677 String[] projectionIn, String selection, String groupBy,
678 String having, String sortOrder, String limit) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700679 String[] projection = computeProjection(projectionIn);
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600680 String where = computeWhere(selection);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700681
682 return buildQueryString(
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600683 mDistinct, mTables, projection, where,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700684 groupBy, having, sortOrder, limit);
685 }
686
687 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100688 * @deprecated This method's signature is misleading since no SQL parameter
689 * substitution is carried out. The selection arguments parameter does not get
690 * used at all. To avoid confusion, call
691 * {@link #buildQuery(String[], String, String, String, String, String)} instead.
692 */
693 @Deprecated
694 public String buildQuery(
695 String[] projectionIn, String selection, String[] selectionArgs,
696 String groupBy, String having, String sortOrder, String limit) {
697 return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
698 }
699
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600700 /** {@hide} */
701 public String buildUpdate(ContentValues values, String selection) {
702 if (values == null || values.isEmpty()) {
703 throw new IllegalArgumentException("Empty values");
704 }
705
706 StringBuilder sql = new StringBuilder(120);
707 sql.append("UPDATE ");
708 sql.append(mTables);
709 sql.append(" SET ");
710
711 final ArrayMap<String, Object> rawValues = values.getValues();
712 for (int i = 0; i < rawValues.size(); i++) {
713 if (i > 0) {
714 sql.append(',');
715 }
716 sql.append(rawValues.keyAt(i));
717 sql.append("=?");
718 }
719
720 final String where = computeWhere(selection);
721 appendClause(sql, " WHERE ", where);
722 return sql.toString();
723 }
724
725 /** {@hide} */
726 public String buildDelete(String selection) {
727 StringBuilder sql = new StringBuilder(120);
728 sql.append("DELETE FROM ");
729 sql.append(mTables);
730
731 final String where = computeWhere(selection);
732 appendClause(sql, " WHERE ", where);
733 return sql.toString();
734 }
735
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100736 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700737 * Construct a {@code SELECT} statement suitable for use in a group of
738 * {@code SELECT} statements that will be joined through {@code UNION} operators
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700739 * in buildUnionQuery.
740 *
741 * @param typeDiscriminatorColumn the name of the result column
742 * whose cells will contain the name of the table from which
743 * each row was drawn.
744 * @param unionColumns the names of the columns to appear in the
745 * result. This may include columns that do not appear in the
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700746 * table this {@code SELECT} is querying (i.e. mTables), but that do
747 * appear in one of the other tables in the {@code UNION} query that we
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700748 * are constructing.
749 * @param columnsPresentInTable a Set of the names of the columns
750 * that appear in this table (i.e. in the table whose name is
751 * mTables). Since columns in unionColumns include columns that
752 * appear only in other tables, we use this array to distinguish
753 * which ones actually are present. Other columns will have
754 * NULL values for results from this subquery.
755 * @param computedColumnsOffset all columns in unionColumns before
756 * this index are included under the assumption that they're
757 * computed and therefore won't appear in columnsPresentInTable,
758 * e.g. "date * 1000 as normalized_date"
759 * @param typeDiscriminatorValue the value used for the
760 * type-discriminator column in this subquery
761 * @param selection A filter declaring which rows to return,
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700762 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700763 * itself). Passing null will return all rows for the given
764 * URL.
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700765 * @param groupBy A filter declaring how to group rows, formatted
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700766 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself).
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700767 * Passing null will cause the rows to not be grouped.
768 * @param having A filter declare which row groups to include in
769 * the cursor, if row grouping is being used, formatted as an
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700770 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700771 * null will cause all row groups to be included, and is
772 * required when row grouping is not being used.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700773 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700774 */
775 public String buildUnionSubQuery(
776 String typeDiscriminatorColumn,
777 String[] unionColumns,
778 Set<String> columnsPresentInTable,
779 int computedColumnsOffset,
780 String typeDiscriminatorValue,
781 String selection,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700782 String groupBy,
783 String having) {
784 int unionColumnsCount = unionColumns.length;
785 String[] projectionIn = new String[unionColumnsCount];
786
787 for (int i = 0; i < unionColumnsCount; i++) {
788 String unionColumn = unionColumns[i];
789
790 if (unionColumn.equals(typeDiscriminatorColumn)) {
791 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
792 + typeDiscriminatorColumn;
793 } else if (i <= computedColumnsOffset
794 || columnsPresentInTable.contains(unionColumn)) {
795 projectionIn[i] = unionColumn;
796 } else {
797 projectionIn[i] = "NULL AS " + unionColumn;
798 }
799 }
800 return buildQuery(
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100801 projectionIn, selection, groupBy, having,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700802 null /* sortOrder */,
803 null /* limit */);
804 }
805
806 /**
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100807 * @deprecated This method's signature is misleading since no SQL parameter
808 * substitution is carried out. The selection arguments parameter does not get
809 * used at all. To avoid confusion, call
Jean-Baptiste Queruf4072fc2010-11-17 16:47:59 -0800810 * {@link #buildUnionSubQuery}
Jonas Schwertfeger84029032010-11-12 11:42:28 +0100811 * instead.
812 */
813 @Deprecated
814 public String buildUnionSubQuery(
815 String typeDiscriminatorColumn,
816 String[] unionColumns,
817 Set<String> columnsPresentInTable,
818 int computedColumnsOffset,
819 String typeDiscriminatorValue,
820 String selection,
821 String[] selectionArgs,
822 String groupBy,
823 String having) {
824 return buildUnionSubQuery(
825 typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
826 computedColumnsOffset, typeDiscriminatorValue, selection,
827 groupBy, having);
828 }
829
830 /**
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700831 * Given a set of subqueries, all of which are {@code SELECT} statements,
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700832 * construct a query that returns the union of what those
833 * subqueries return.
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700834 * @param subQueries an array of SQL {@code SELECT} statements, all of
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700835 * which must have the same columns as the same positions in
836 * their results
837 * @param sortOrder How to order the rows, formatted as an SQL
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700838 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700839 * null will use the default sort order, which may be unordered.
840 * @param limit The limit clause, which applies to the entire union result set
841 *
Jeff Sharkeyb91eaa52019-02-19 11:09:13 -0700842 * @return the resulting SQL {@code SELECT} statement
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700843 */
844 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
845 StringBuilder query = new StringBuilder(128);
846 int subQueryCount = subQueries.length;
847 String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
848
849 for (int i = 0; i < subQueryCount; i++) {
850 if (i > 0) {
851 query.append(unionOperator);
852 }
853 query.append(subQueries[i]);
854 }
855 appendClause(query, " ORDER BY ", sortOrder);
856 appendClause(query, " LIMIT ", limit);
857 return query.toString();
858 }
859
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600860 private static @NonNull String maybeWithOperator(@Nullable String operator,
861 @NonNull String column) {
862 if (operator != null) {
863 return operator + "(" + column + ")";
864 } else {
865 return column;
866 }
867 }
868
869 /** {@hide} */
Mathew Inwood31755f92018-12-20 13:53:36 +0000870 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600871 public String[] computeProjection(String[] projectionIn) {
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700872 if (projectionIn != null && projectionIn.length > 0) {
873 if (mProjectionMap != null) {
874 String[] projection = new String[projectionIn.length];
875 int length = projectionIn.length;
876
877 for (int i = 0; i < length; i++) {
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600878 String operator = null;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700879 String userColumn = projectionIn[i];
880 String column = mProjectionMap.get(userColumn);
881
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600882 // If aggregation is allowed, extract the underlying column
883 // that may be aggregated
884 if (mProjectionAggregationAllowed) {
885 final Matcher matcher = sAggregationPattern.matcher(userColumn);
886 if (matcher.matches()) {
887 operator = matcher.group(1);
888 userColumn = matcher.group(2);
889 column = mProjectionMap.get(userColumn);
890 }
891 }
892
Michael Chan99c44832009-04-27 16:28:51 -0700893 if (column != null) {
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600894 projection[i] = maybeWithOperator(operator, column);
Michael Chan99c44832009-04-27 16:28:51 -0700895 continue;
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700896 }
Michael Chan99c44832009-04-27 16:28:51 -0700897
Daniel Lehmann50b1f8d2011-06-01 15:24:23 -0700898 if (!mStrict &&
Dmitri Plotnikov40eb4aa2010-04-14 16:09:46 -0700899 ( userColumn.contains(" AS ") || userColumn.contains(" as "))) {
Michael Chan99c44832009-04-27 16:28:51 -0700900 /* A column alias already exist */
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600901 projection[i] = maybeWithOperator(operator, userColumn);
Michael Chan99c44832009-04-27 16:28:51 -0700902 continue;
903 }
904
Jeff Sharkey7049e652018-09-13 17:05:07 -0600905 // If greylist is configured, we might be willing to let
906 // this custom column bypass our strict checks.
907 if (mProjectionGreylist != null) {
908 boolean match = false;
909 for (Pattern p : mProjectionGreylist) {
910 if (p.matcher(userColumn).matches()) {
911 match = true;
912 break;
913 }
914 }
915
916 if (match) {
917 Log.w(TAG, "Allowing abusive custom column: " + userColumn);
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600918 projection[i] = maybeWithOperator(operator, userColumn);
Jeff Sharkey7049e652018-09-13 17:05:07 -0600919 continue;
920 }
921 }
922
Michael Chan99c44832009-04-27 16:28:51 -0700923 throw new IllegalArgumentException("Invalid column "
Jeff Sharkey91be9262018-07-19 09:30:16 -0600924 + projectionIn[i]);
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700925 }
926 return projection;
927 } else {
928 return projectionIn;
929 }
930 } else if (mProjectionMap != null) {
931 // Return all columns in projection map.
932 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
933 String[] projection = new String[entrySet.size()];
934 Iterator<Entry<String, String>> entryIter = entrySet.iterator();
935 int i = 0;
936
937 while (entryIter.hasNext()) {
938 Entry<String, String> entry = entryIter.next();
939
940 // Don't include the _count column when people ask for no projection.
941 if (entry.getKey().equals(BaseColumns._COUNT)) {
942 continue;
943 }
944 projection[i++] = entry.getValue();
945 }
946 return projection;
947 }
948 return null;
949 }
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600950
Jeff Sharkey82d783c2019-03-29 15:46:35 -0600951 /** {@hide} */
952 public @Nullable String computeWhere(@Nullable String selection) {
Jeff Sharkeyb13ea302018-07-25 14:52:14 -0600953 final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
954 final boolean hasExternal = !TextUtils.isEmpty(selection);
955
956 if (hasInternal || hasExternal) {
957 final StringBuilder where = new StringBuilder();
958 if (hasInternal) {
959 where.append('(').append(mWhereClause).append(')');
960 }
961 if (hasInternal && hasExternal) {
962 where.append(" AND ");
963 }
964 if (hasExternal) {
965 where.append('(').append(selection).append(')');
966 }
967 return where.toString();
968 } else {
969 return null;
970 }
971 }
972
973 /**
974 * Wrap given argument in parenthesis, unless it's {@code null} or
975 * {@code ()}, in which case return it verbatim.
976 */
977 private @Nullable String wrap(@Nullable String arg) {
978 if (TextUtils.isEmpty(arg)) {
979 return arg;
980 } else {
981 return "(" + arg + ")";
982 }
983 }
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700984}