blob: 139fcba95b69c1bd4f20dd39417224ab1530bf11 [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
19import android.database.Cursor;
20import android.database.DatabaseUtils;
21import android.database.sqlite.SQLiteDatabase;
22import android.provider.BaseColumns;
23import android.text.TextUtils;
24import android.util.Config;
25import android.util.Log;
26
27import java.util.Iterator;
28import java.util.Map;
29import java.util.Set;
30import java.util.Map.Entry;
31
32/**
33 * This is a convience class that helps build SQL queries to be sent to
34 * {@link SQLiteDatabase} objects.
35 */
36public class SQLiteQueryBuilder
37{
38 private static final String TAG = "SQLiteQueryBuilder";
39
40 private Map<String, String> mProjectionMap = null;
41 private String mTables = "";
42 private StringBuilder mWhereClause = new StringBuilder(64);
43 private boolean mDistinct;
44 private SQLiteDatabase.CursorFactory mFactory;
45
46 public SQLiteQueryBuilder() {
47 mDistinct = false;
48 mFactory = null;
49 }
50
51 /**
52 * Mark the query as DISTINCT.
53 *
54 * @param distinct if true the query is DISTINCT, otherwise it isn't
55 */
56 public void setDistinct(boolean distinct) {
57 mDistinct = distinct;
58 }
59
60 /**
61 * Returns the list of tables being queried
62 *
63 * @return the list of tables being queried
64 */
65 public String getTables() {
66 return mTables;
67 }
68
69 /**
70 * Sets the list of tables to query. Multiple tables can be specified to perform a join.
71 * For example:
72 * setTables("foo, bar")
73 * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
74 *
75 * @param inTables the list of tables to query on
76 */
77 public void setTables(String inTables) {
78 mTables = inTables;
79 }
80
81 /**
82 * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
83 * by parenthesis and ANDed with the selection passed to {@link #query}. The final
84 * WHERE clause looks like:
85 *
86 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
87 *
88 * @param inWhere the chunk of text to append to the WHERE clause.
89 */
90 public void appendWhere(CharSequence inWhere) {
91 if (mWhereClause.length() == 0) {
92 mWhereClause.append('(');
93 }
94 mWhereClause.append(inWhere);
95 }
96
97 /**
98 * 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:
101 *
102 * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
103 *
104 * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
105 * to avoid SQL injection attacks
106 */
107 public void appendWhereEscapeString(String inWhere) {
108 if (mWhereClause.length() == 0) {
109 mWhereClause.append('(');
110 }
111 DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
112 }
113
114 /**
115 * Sets the projection map for the query. The projection map maps
116 * from column names that the caller passes into query to database
117 * column names. This is useful for renaming columns as well as
118 * disambiguating column names when doing joins. For example you
119 * could map "name" to "people.name". If a projection map is set
120 * it must contain all column names the user may request, even if
121 * the key and value are the same.
122 *
123 * @param columnMap maps from the user column names to the database column names
124 */
125 public void setProjectionMap(Map<String, String> columnMap) {
126 mProjectionMap = columnMap;
127 }
128
129 /**
130 * Sets the cursor factory to be used for the query. You can use
131 * one factory for all queries on a database but it is normally
132 * easier to specify the factory when doing this query. @param
133 * factory the factor to use
134 */
135 public void setCursorFactory(SQLiteDatabase.CursorFactory factory) {
136 mFactory = factory;
137 }
138
139 /**
140 * Build an SQL query string from the given clauses.
141 *
142 * @param distinct true if you want each row to be unique, false otherwise.
143 * @param tables The table names to compile the query against.
144 * @param columns A list of which columns to return. Passing null will
145 * return all columns, which is discouraged to prevent reading
146 * data from storage that isn't going to be used.
147 * @param where A filter declaring which rows to return, formatted as an SQL
148 * WHERE clause (excluding the WHERE itself). Passing null will
149 * return all rows for the given URL.
150 * @param groupBy A filter declaring how to group rows, formatted as an SQL
151 * GROUP BY clause (excluding the GROUP BY itself). Passing null
152 * will cause the rows to not be grouped.
153 * @param having A filter declare which row groups to include in the cursor,
154 * if row grouping is being used, formatted as an SQL HAVING
155 * clause (excluding the HAVING itself). Passing null will cause
156 * all row groups to be included, and is required when row
157 * grouping is not being used.
158 * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
159 * (excluding the ORDER BY itself). Passing null will use the
160 * default sort order, which may be unordered.
161 * @param limit Limits the number of rows returned by the query,
162 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
163 * @return the SQL query string
164 */
165 public static String buildQueryString(
166 boolean distinct, String tables, String[] columns, String where,
167 String groupBy, String having, String orderBy, String limit) {
168 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
169 throw new IllegalArgumentException(
170 "HAVING clauses are only permitted when using a groupBy clause");
171 }
172
173 StringBuilder query = new StringBuilder(120);
174
175 query.append("SELECT ");
176 if (distinct) {
177 query.append("DISTINCT ");
178 }
179 if (columns != null && columns.length != 0) {
180 appendColumns(query, columns);
181 } else {
182 query.append("* ");
183 }
184 query.append("FROM ");
185 query.append(tables);
186 appendClause(query, " WHERE ", where);
187 appendClause(query, " GROUP BY ", groupBy);
188 appendClause(query, " HAVING ", having);
189 appendClause(query, " ORDER BY ", orderBy);
190 appendClauseEscapeClause(query, " LIMIT ", limit);
191
192 return query.toString();
193 }
194
195 private static void appendClause(StringBuilder s, String name, String clause) {
196 if (!TextUtils.isEmpty(clause)) {
197 s.append(name);
198 s.append(clause);
199 }
200 }
201
202 private static void appendClauseEscapeClause(StringBuilder s, String name, String clause) {
203 if (!TextUtils.isEmpty(clause)) {
204 s.append(name);
205 DatabaseUtils.appendEscapedSQLString(s, clause);
206 }
207 }
208
209 /**
210 * Add the names that are non-null in columns to s, separating
211 * them with commas.
212 */
213 public static void appendColumns(StringBuilder s, String[] columns) {
214 int n = columns.length;
215
216 for (int i = 0; i < n; i++) {
217 String column = columns[i];
218
219 if (column != null) {
220 if (i > 0) {
221 s.append(", ");
222 }
223 s.append(column);
224 }
225 }
226 s.append(' ');
227 }
228
229 /**
230 * Perform a query by combining all current settings and the
231 * information passed into this method.
232 *
233 * @param db the database to query on
234 * @param projectionIn A list of which columns to return. Passing
235 * null will return all columns, which is discouraged to prevent
236 * reading data from storage that isn't going to be used.
237 * @param selection A filter declaring which rows to return,
238 * formatted as an SQL WHERE clause (excluding the WHERE
239 * itself). Passing null will return all rows for the given URL.
240 * @param selectionArgs You may include ?s in selection, which
241 * will be replaced by the values from selectionArgs, in order
242 * that they appear in the selection. The values will be bound
243 * as Strings.
244 * @param groupBy A filter declaring how to group rows, formatted
245 * as an SQL GROUP BY clause (excluding the GROUP BY
246 * itself). Passing null will cause the rows to not be grouped.
247 * @param having A filter declare which row groups to include in
248 * the cursor, if row grouping is being used, formatted as an
249 * SQL HAVING clause (excluding the HAVING itself). Passing
250 * null will cause all row groups to be included, and is
251 * required when row grouping is not being used.
252 * @param sortOrder How to order the rows, formatted as an SQL
253 * ORDER BY clause (excluding the ORDER BY itself). Passing null
254 * will use the default sort order, which may be unordered.
255 * @return a cursor over the result set
256 * @see android.content.ContentResolver#query(android.net.Uri, String[],
257 * String, String[], String)
258 */
259 public Cursor query(SQLiteDatabase db, String[] projectionIn,
260 String selection, String[] selectionArgs, String groupBy,
261 String having, String sortOrder) {
262 return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
263 null /* limit */);
264 }
265
266 /**
267 * Perform a query by combining all current settings and the
268 * information passed into this method.
269 *
270 * @param db the database to query on
271 * @param projectionIn A list of which columns to return. Passing
272 * null will return all columns, which is discouraged to prevent
273 * reading data from storage that isn't going to be used.
274 * @param selection A filter declaring which rows to return,
275 * formatted as an SQL WHERE clause (excluding the WHERE
276 * itself). Passing null will return all rows for the given URL.
277 * @param selectionArgs You may include ?s in selection, which
278 * will be replaced by the values from selectionArgs, in order
279 * that they appear in the selection. The values will be bound
280 * as Strings.
281 * @param groupBy A filter declaring how to group rows, formatted
282 * as an SQL GROUP BY clause (excluding the GROUP BY
283 * itself). Passing null will cause the rows to not be grouped.
284 * @param having A filter declare which row groups to include in
285 * the cursor, if row grouping is being used, formatted as an
286 * SQL HAVING clause (excluding the HAVING itself). Passing
287 * null will cause all row groups to be included, and is
288 * required when row grouping is not being used.
289 * @param sortOrder How to order the rows, formatted as an SQL
290 * ORDER BY clause (excluding the ORDER BY itself). Passing null
291 * will use the default sort order, which may be unordered.
292 * @param limit Limits the number of rows returned by the query,
293 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
294 * @return a cursor over the result set
295 * @see android.content.ContentResolver#query(android.net.Uri, String[],
296 * String, String[], String)
297 */
298 public Cursor query(SQLiteDatabase db, String[] projectionIn,
299 String selection, String[] selectionArgs, String groupBy,
300 String having, String sortOrder, String limit) {
301 if (mTables == null) {
302 return null;
303 }
304
305 String sql = buildQuery(
306 projectionIn, selection, selectionArgs, groupBy, having,
307 sortOrder, limit);
308
309 if (Log.isLoggable(TAG, Log.DEBUG)) {
310 Log.d(TAG, "Performing query: " + sql);
311 }
312 return db.rawQueryWithFactory(
313 mFactory, sql, selectionArgs,
314 SQLiteDatabase.findEditTable(mTables));
315 }
316
317 /**
318 * Construct a SELECT statement suitable for use in a group of
319 * SELECT statements that will be joined through UNION operators
320 * in buildUnionQuery.
321 *
322 * @param projectionIn A list of which columns to return. Passing
323 * null will return all columns, which is discouraged to
324 * prevent reading data from storage that isn't going to be
325 * used.
326 * @param selection A filter declaring which rows to return,
327 * formatted as an SQL WHERE clause (excluding the WHERE
328 * itself). Passing null will return all rows for the given
329 * URL.
330 * @param selectionArgs You may include ?s in selection, which
331 * will be replaced by the values from selectionArgs, in order
332 * that they appear in the selection. The values will be bound
333 * as Strings.
334 * @param groupBy A filter declaring how to group rows, formatted
335 * as an SQL GROUP BY clause (excluding the GROUP BY itself).
336 * Passing null will cause the rows to not be grouped.
337 * @param having A filter declare which row groups to include in
338 * the cursor, if row grouping is being used, formatted as an
339 * SQL HAVING clause (excluding the HAVING itself). Passing
340 * null will cause all row groups to be included, and is
341 * required when row grouping is not being used.
342 * @param sortOrder How to order the rows, formatted as an SQL
343 * ORDER BY clause (excluding the ORDER BY itself). Passing null
344 * will use the default sort order, which may be unordered.
345 * @param limit Limits the number of rows returned by the query,
346 * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
347 * @return the resulting SQL SELECT statement
348 */
349 public String buildQuery(
350 String[] projectionIn, String selection, String[] selectionArgs,
351 String groupBy, String having, String sortOrder, String limit) {
352 String[] projection = computeProjection(projectionIn);
353
Yusuke Ohmichi(maimuzo)accbade2008-12-19 19:41:46 +0900354 StringBuilder where = new StringBuilder();
355
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700356 if (mWhereClause.length() > 0) {
Yusuke Ohmichi(maimuzo)accbade2008-12-19 19:41:46 +0900357 where.append(mWhereClause.toString());
358 where.append(')');
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700359 }
360
361 // Tack on the user's selection, if present.
362 if (selection != null && selection.length() > 0) {
363 if (mWhereClause.length() > 0) {
Yusuke Ohmichi(maimuzo)accbade2008-12-19 19:41:46 +0900364 where.append(" AND ");
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700365 }
366
Yusuke Ohmichi(maimuzo)accbade2008-12-19 19:41:46 +0900367 where.append('(');
368 where.append(selection);
369 where.append(')');
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700370 }
371
372 return buildQueryString(
Yusuke Ohmichi(maimuzo)accbade2008-12-19 19:41:46 +0900373 mDistinct, mTables, projection, where.toString(),
The Android Open Source Project54b6cfa2008-10-21 07:00:00 -0700374 groupBy, having, sortOrder, limit);
375 }
376
377 /**
378 * Construct a SELECT statement suitable for use in a group of
379 * SELECT statements that will be joined through UNION operators
380 * in buildUnionQuery.
381 *
382 * @param typeDiscriminatorColumn the name of the result column
383 * whose cells will contain the name of the table from which
384 * each row was drawn.
385 * @param unionColumns the names of the columns to appear in the
386 * result. This may include columns that do not appear in the
387 * table this SELECT is querying (i.e. mTables), but that do
388 * appear in one of the other tables in the UNION query that we
389 * are constructing.
390 * @param columnsPresentInTable a Set of the names of the columns
391 * that appear in this table (i.e. in the table whose name is
392 * mTables). Since columns in unionColumns include columns that
393 * appear only in other tables, we use this array to distinguish
394 * which ones actually are present. Other columns will have
395 * NULL values for results from this subquery.
396 * @param computedColumnsOffset all columns in unionColumns before
397 * this index are included under the assumption that they're
398 * computed and therefore won't appear in columnsPresentInTable,
399 * e.g. "date * 1000 as normalized_date"
400 * @param typeDiscriminatorValue the value used for the
401 * type-discriminator column in this subquery
402 * @param selection A filter declaring which rows to return,
403 * formatted as an SQL WHERE clause (excluding the WHERE
404 * itself). Passing null will return all rows for the given
405 * URL.
406 * @param selectionArgs You may include ?s in selection, which
407 * will be replaced by the values from selectionArgs, in order
408 * that they appear in the selection. The values will be bound
409 * as Strings.
410 * @param groupBy A filter declaring how to group rows, formatted
411 * as an SQL GROUP BY clause (excluding the GROUP BY itself).
412 * Passing null will cause the rows to not be grouped.
413 * @param having A filter declare which row groups to include in
414 * the cursor, if row grouping is being used, formatted as an
415 * SQL HAVING clause (excluding the HAVING itself). Passing
416 * null will cause all row groups to be included, and is
417 * required when row grouping is not being used.
418 * @return the resulting SQL SELECT statement
419 */
420 public String buildUnionSubQuery(
421 String typeDiscriminatorColumn,
422 String[] unionColumns,
423 Set<String> columnsPresentInTable,
424 int computedColumnsOffset,
425 String typeDiscriminatorValue,
426 String selection,
427 String[] selectionArgs,
428 String groupBy,
429 String having) {
430 int unionColumnsCount = unionColumns.length;
431 String[] projectionIn = new String[unionColumnsCount];
432
433 for (int i = 0; i < unionColumnsCount; i++) {
434 String unionColumn = unionColumns[i];
435
436 if (unionColumn.equals(typeDiscriminatorColumn)) {
437 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
438 + typeDiscriminatorColumn;
439 } else if (i <= computedColumnsOffset
440 || columnsPresentInTable.contains(unionColumn)) {
441 projectionIn[i] = unionColumn;
442 } else {
443 projectionIn[i] = "NULL AS " + unionColumn;
444 }
445 }
446 return buildQuery(
447 projectionIn, selection, selectionArgs, groupBy, having,
448 null /* sortOrder */,
449 null /* limit */);
450 }
451
452 /**
453 * Given a set of subqueries, all of which are SELECT statements,
454 * construct a query that returns the union of what those
455 * subqueries return.
456 * @param subQueries an array of SQL SELECT statements, all of
457 * which must have the same columns as the same positions in
458 * their results
459 * @param sortOrder How to order the rows, formatted as an SQL
460 * ORDER BY clause (excluding the ORDER BY itself). Passing
461 * null will use the default sort order, which may be unordered.
462 * @param limit The limit clause, which applies to the entire union result set
463 *
464 * @return the resulting SQL SELECT statement
465 */
466 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
467 StringBuilder query = new StringBuilder(128);
468 int subQueryCount = subQueries.length;
469 String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
470
471 for (int i = 0; i < subQueryCount; i++) {
472 if (i > 0) {
473 query.append(unionOperator);
474 }
475 query.append(subQueries[i]);
476 }
477 appendClause(query, " ORDER BY ", sortOrder);
478 appendClause(query, " LIMIT ", limit);
479 return query.toString();
480 }
481
482 private String[] computeProjection(String[] projectionIn) {
483 if (projectionIn != null && projectionIn.length > 0) {
484 if (mProjectionMap != null) {
485 String[] projection = new String[projectionIn.length];
486 int length = projectionIn.length;
487
488 for (int i = 0; i < length; i++) {
489 String userColumn = projectionIn[i];
490 String column = mProjectionMap.get(userColumn);
491
492 if (column == null) {
493 throw new IllegalArgumentException(
494 "Invalid column " + projectionIn[i]);
495 } else {
496 projection[i] = column;
497 }
498 }
499 return projection;
500 } else {
501 return projectionIn;
502 }
503 } else if (mProjectionMap != null) {
504 // Return all columns in projection map.
505 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
506 String[] projection = new String[entrySet.size()];
507 Iterator<Entry<String, String>> entryIter = entrySet.iterator();
508 int i = 0;
509
510 while (entryIter.hasNext()) {
511 Entry<String, String> entry = entryIter.next();
512
513 // Don't include the _count column when people ask for no projection.
514 if (entry.getKey().equals(BaseColumns._COUNT)) {
515 continue;
516 }
517 projection[i++] = entry.getValue();
518 }
519 return projection;
520 }
521 return null;
522 }
523}