blob: 519a81c24385d2fd319a56cef678253df5658104 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
354 if (mWhereClause.length() > 0) {
355 mWhereClause.append(')');
356 }
357
358 // Tack on the user's selection, if present.
359 if (selection != null && selection.length() > 0) {
360 if (mWhereClause.length() > 0) {
361 mWhereClause.append(" AND ");
362 }
363
364 mWhereClause.append('(');
365 mWhereClause.append(selection);
366 mWhereClause.append(')');
367 }
368
369 return buildQueryString(
370 mDistinct, mTables, projection, mWhereClause.toString(),
371 groupBy, having, sortOrder, limit);
372 }
373
374 /**
375 * Construct a SELECT statement suitable for use in a group of
376 * SELECT statements that will be joined through UNION operators
377 * in buildUnionQuery.
378 *
379 * @param typeDiscriminatorColumn the name of the result column
380 * whose cells will contain the name of the table from which
381 * each row was drawn.
382 * @param unionColumns the names of the columns to appear in the
383 * result. This may include columns that do not appear in the
384 * table this SELECT is querying (i.e. mTables), but that do
385 * appear in one of the other tables in the UNION query that we
386 * are constructing.
387 * @param columnsPresentInTable a Set of the names of the columns
388 * that appear in this table (i.e. in the table whose name is
389 * mTables). Since columns in unionColumns include columns that
390 * appear only in other tables, we use this array to distinguish
391 * which ones actually are present. Other columns will have
392 * NULL values for results from this subquery.
393 * @param computedColumnsOffset all columns in unionColumns before
394 * this index are included under the assumption that they're
395 * computed and therefore won't appear in columnsPresentInTable,
396 * e.g. "date * 1000 as normalized_date"
397 * @param typeDiscriminatorValue the value used for the
398 * type-discriminator column in this subquery
399 * @param selection A filter declaring which rows to return,
400 * formatted as an SQL WHERE clause (excluding the WHERE
401 * itself). Passing null will return all rows for the given
402 * URL.
403 * @param selectionArgs You may include ?s in selection, which
404 * will be replaced by the values from selectionArgs, in order
405 * that they appear in the selection. The values will be bound
406 * as Strings.
407 * @param groupBy A filter declaring how to group rows, formatted
408 * as an SQL GROUP BY clause (excluding the GROUP BY itself).
409 * Passing null will cause the rows to not be grouped.
410 * @param having A filter declare which row groups to include in
411 * the cursor, if row grouping is being used, formatted as an
412 * SQL HAVING clause (excluding the HAVING itself). Passing
413 * null will cause all row groups to be included, and is
414 * required when row grouping is not being used.
415 * @return the resulting SQL SELECT statement
416 */
417 public String buildUnionSubQuery(
418 String typeDiscriminatorColumn,
419 String[] unionColumns,
420 Set<String> columnsPresentInTable,
421 int computedColumnsOffset,
422 String typeDiscriminatorValue,
423 String selection,
424 String[] selectionArgs,
425 String groupBy,
426 String having) {
427 int unionColumnsCount = unionColumns.length;
428 String[] projectionIn = new String[unionColumnsCount];
429
430 for (int i = 0; i < unionColumnsCount; i++) {
431 String unionColumn = unionColumns[i];
432
433 if (unionColumn.equals(typeDiscriminatorColumn)) {
434 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
435 + typeDiscriminatorColumn;
436 } else if (i <= computedColumnsOffset
437 || columnsPresentInTable.contains(unionColumn)) {
438 projectionIn[i] = unionColumn;
439 } else {
440 projectionIn[i] = "NULL AS " + unionColumn;
441 }
442 }
443 return buildQuery(
444 projectionIn, selection, selectionArgs, groupBy, having,
445 null /* sortOrder */,
446 null /* limit */);
447 }
448
449 /**
450 * Given a set of subqueries, all of which are SELECT statements,
451 * construct a query that returns the union of what those
452 * subqueries return.
453 * @param subQueries an array of SQL SELECT statements, all of
454 * which must have the same columns as the same positions in
455 * their results
456 * @param sortOrder How to order the rows, formatted as an SQL
457 * ORDER BY clause (excluding the ORDER BY itself). Passing
458 * null will use the default sort order, which may be unordered.
459 * @param limit The limit clause, which applies to the entire union result set
460 *
461 * @return the resulting SQL SELECT statement
462 */
463 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
464 StringBuilder query = new StringBuilder(128);
465 int subQueryCount = subQueries.length;
466 String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
467
468 for (int i = 0; i < subQueryCount; i++) {
469 if (i > 0) {
470 query.append(unionOperator);
471 }
472 query.append(subQueries[i]);
473 }
474 appendClause(query, " ORDER BY ", sortOrder);
475 appendClause(query, " LIMIT ", limit);
476 return query.toString();
477 }
478
479 private String[] computeProjection(String[] projectionIn) {
480 if (projectionIn != null && projectionIn.length > 0) {
481 if (mProjectionMap != null) {
482 String[] projection = new String[projectionIn.length];
483 int length = projectionIn.length;
484
485 for (int i = 0; i < length; i++) {
486 String userColumn = projectionIn[i];
487 String column = mProjectionMap.get(userColumn);
488
489 if (column == null) {
490 throw new IllegalArgumentException(
491 "Invalid column " + projectionIn[i]);
492 } else {
493 projection[i] = column;
494 }
495 }
496 return projection;
497 } else {
498 return projectionIn;
499 }
500 } else if (mProjectionMap != null) {
501 // Return all columns in projection map.
502 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
503 String[] projection = new String[entrySet.size()];
504 Iterator<Entry<String, String>> entryIter = entrySet.iterator();
505 int i = 0;
506
507 while (entryIter.hasNext()) {
508 Entry<String, String> entry = entryIter.next();
509
510 // Don't include the _count column when people ask for no projection.
511 if (entry.getKey().equals(BaseColumns._COUNT)) {
512 continue;
513 }
514 projection[i++] = entry.getValue();
515 }
516 return projection;
517 }
518 return null;
519 }
520}