Home | History | Annotate | Download | only in sqlite
      1 /*
      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 
     17 package android.database.sqlite;
     18 
     19 import android.database.Cursor;
     20 import android.database.DatabaseUtils;
     21 import android.os.CancellationSignal;
     22 import android.os.OperationCanceledException;
     23 import android.provider.BaseColumns;
     24 import android.text.TextUtils;
     25 import android.util.Log;
     26 
     27 import java.util.Iterator;
     28 import java.util.Map;
     29 import java.util.Map.Entry;
     30 import java.util.Set;
     31 import java.util.regex.Pattern;
     32 
     33 /**
     34  * This is a convenience class that helps build SQL queries to be sent to
     35  * {@link SQLiteDatabase} objects.
     36  */
     37 public class SQLiteQueryBuilder
     38 {
     39     private static final String TAG = "SQLiteQueryBuilder";
     40     private static final Pattern sLimitPattern =
     41             Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
     42 
     43     private Map<String, String> mProjectionMap = null;
     44     private String mTables = "";
     45     private StringBuilder mWhereClause = null;  // lazily created
     46     private boolean mDistinct;
     47     private SQLiteDatabase.CursorFactory mFactory;
     48     private boolean mStrict;
     49 
     50     public SQLiteQueryBuilder() {
     51         mDistinct = false;
     52         mFactory = null;
     53     }
     54 
     55     /**
     56      * Mark the query as DISTINCT.
     57      *
     58      * @param distinct if true the query is DISTINCT, otherwise it isn't
     59      */
     60     public void setDistinct(boolean distinct) {
     61         mDistinct = distinct;
     62     }
     63 
     64     /**
     65      * Returns the list of tables being queried
     66      *
     67      * @return the list of tables being queried
     68      */
     69     public String getTables() {
     70         return mTables;
     71     }
     72 
     73     /**
     74      * Sets the list of tables to query. Multiple tables can be specified to perform a join.
     75      * For example:
     76      *   setTables("foo, bar")
     77      *   setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
     78      *
     79      * @param inTables the list of tables to query on
     80      */
     81     public void setTables(String inTables) {
     82         mTables = inTables;
     83     }
     84 
     85     /**
     86      * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
     87      * by parenthesis and ANDed with the selection passed to {@link #query}. The final
     88      * WHERE clause looks like:
     89      *
     90      * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
     91      *
     92      * @param inWhere the chunk of text to append to the WHERE clause.
     93      */
     94     public void appendWhere(CharSequence inWhere) {
     95         if (mWhereClause == null) {
     96             mWhereClause = new StringBuilder(inWhere.length() + 16);
     97         }
     98         if (mWhereClause.length() == 0) {
     99             mWhereClause.append('(');
    100         }
    101         mWhereClause.append(inWhere);
    102     }
    103 
    104     /**
    105      * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
    106      * by parenthesis and ANDed with the selection passed to {@link #query}. The final
    107      * WHERE clause looks like:
    108      *
    109      * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
    110      *
    111      * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
    112      * to avoid SQL injection attacks
    113      */
    114     public void appendWhereEscapeString(String inWhere) {
    115         if (mWhereClause == null) {
    116             mWhereClause = new StringBuilder(inWhere.length() + 16);
    117         }
    118         if (mWhereClause.length() == 0) {
    119             mWhereClause.append('(');
    120         }
    121         DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
    122     }
    123 
    124     /**
    125      * Sets the projection map for the query.  The projection map maps
    126      * from column names that the caller passes into query to database
    127      * column names. This is useful for renaming columns as well as
    128      * disambiguating column names when doing joins. For example you
    129      * could map "name" to "people.name".  If a projection map is set
    130      * it must contain all column names the user may request, even if
    131      * the key and value are the same.
    132      *
    133      * @param columnMap maps from the user column names to the database column names
    134      */
    135     public void setProjectionMap(Map<String, String> columnMap) {
    136         mProjectionMap = columnMap;
    137     }
    138 
    139     /**
    140      * Sets the cursor factory to be used for the query.  You can use
    141      * one factory for all queries on a database but it is normally
    142      * easier to specify the factory when doing this query.
    143      *
    144      * @param factory the factory to use.
    145      */
    146     public void setCursorFactory(SQLiteDatabase.CursorFactory factory) {
    147         mFactory = factory;
    148     }
    149 
    150     /**
    151      * When set, the selection is verified against malicious arguments.
    152      * When using this class to create a statement using
    153      * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
    154      * non-numeric limits will raise an exception. If a projection map is specified, fields
    155      * not in that map will be ignored.
    156      * If this class is used to execute the statement directly using
    157      * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
    158      * or
    159      * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
    160      * additionally also parenthesis escaping selection are caught.
    161      *
    162      * To summarize: To get maximum protection against malicious third party apps (for example
    163      * content provider consumers), make sure to do the following:
    164      * <ul>
    165      * <li>Set this value to true</li>
    166      * <li>Use a projection map</li>
    167      * <li>Use one of the query overloads instead of getting the statement as a sql string</li>
    168      * </ul>
    169      * By default, this value is false.
    170      */
    171     public void setStrict(boolean flag) {
    172         mStrict = flag;
    173     }
    174 
    175     /**
    176      * Build an SQL query string from the given clauses.
    177      *
    178      * @param distinct true if you want each row to be unique, false otherwise.
    179      * @param tables The table names to compile the query against.
    180      * @param columns A list of which columns to return. Passing null will
    181      *            return all columns, which is discouraged to prevent reading
    182      *            data from storage that isn't going to be used.
    183      * @param where A filter declaring which rows to return, formatted as an SQL
    184      *            WHERE clause (excluding the WHERE itself). Passing null will
    185      *            return all rows for the given URL.
    186      * @param groupBy A filter declaring how to group rows, formatted as an SQL
    187      *            GROUP BY clause (excluding the GROUP BY itself). Passing null
    188      *            will cause the rows to not be grouped.
    189      * @param having A filter declare which row groups to include in the cursor,
    190      *            if row grouping is being used, formatted as an SQL HAVING
    191      *            clause (excluding the HAVING itself). Passing null will cause
    192      *            all row groups to be included, and is required when row
    193      *            grouping is not being used.
    194      * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
    195      *            (excluding the ORDER BY itself). Passing null will use the
    196      *            default sort order, which may be unordered.
    197      * @param limit Limits the number of rows returned by the query,
    198      *            formatted as LIMIT clause. Passing null denotes no LIMIT clause.
    199      * @return the SQL query string
    200      */
    201     public static String buildQueryString(
    202             boolean distinct, String tables, String[] columns, String where,
    203             String groupBy, String having, String orderBy, String limit) {
    204         if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
    205             throw new IllegalArgumentException(
    206                     "HAVING clauses are only permitted when using a groupBy clause");
    207         }
    208         if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) {
    209             throw new IllegalArgumentException("invalid LIMIT clauses:" + limit);
    210         }
    211 
    212         StringBuilder query = new StringBuilder(120);
    213 
    214         query.append("SELECT ");
    215         if (distinct) {
    216             query.append("DISTINCT ");
    217         }
    218         if (columns != null && columns.length != 0) {
    219             appendColumns(query, columns);
    220         } else {
    221             query.append("* ");
    222         }
    223         query.append("FROM ");
    224         query.append(tables);
    225         appendClause(query, " WHERE ", where);
    226         appendClause(query, " GROUP BY ", groupBy);
    227         appendClause(query, " HAVING ", having);
    228         appendClause(query, " ORDER BY ", orderBy);
    229         appendClause(query, " LIMIT ", limit);
    230 
    231         return query.toString();
    232     }
    233 
    234     private static void appendClause(StringBuilder s, String name, String clause) {
    235         if (!TextUtils.isEmpty(clause)) {
    236             s.append(name);
    237             s.append(clause);
    238         }
    239     }
    240 
    241     /**
    242      * Add the names that are non-null in columns to s, separating
    243      * them with commas.
    244      */
    245     public static void appendColumns(StringBuilder s, String[] columns) {
    246         int n = columns.length;
    247 
    248         for (int i = 0; i < n; i++) {
    249             String column = columns[i];
    250 
    251             if (column != null) {
    252                 if (i > 0) {
    253                     s.append(", ");
    254                 }
    255                 s.append(column);
    256             }
    257         }
    258         s.append(' ');
    259     }
    260 
    261     /**
    262      * Perform a query by combining all current settings and the
    263      * information passed into this method.
    264      *
    265      * @param db the database to query on
    266      * @param projectionIn A list of which columns to return. Passing
    267      *   null will return all columns, which is discouraged to prevent
    268      *   reading data from storage that isn't going to be used.
    269      * @param selection A filter declaring which rows to return,
    270      *   formatted as an SQL WHERE clause (excluding the WHERE
    271      *   itself). Passing null will return all rows for the given URL.
    272      * @param selectionArgs You may include ?s in selection, which
    273      *   will be replaced by the values from selectionArgs, in order
    274      *   that they appear in the selection. The values will be bound
    275      *   as Strings.
    276      * @param groupBy A filter declaring how to group rows, formatted
    277      *   as an SQL GROUP BY clause (excluding the GROUP BY
    278      *   itself). Passing null will cause the rows to not be grouped.
    279      * @param having A filter declare which row groups to include in
    280      *   the cursor, if row grouping is being used, formatted as an
    281      *   SQL HAVING clause (excluding the HAVING itself).  Passing
    282      *   null will cause all row groups to be included, and is
    283      *   required when row grouping is not being used.
    284      * @param sortOrder How to order the rows, formatted as an SQL
    285      *   ORDER BY clause (excluding the ORDER BY itself). Passing null
    286      *   will use the default sort order, which may be unordered.
    287      * @return a cursor over the result set
    288      * @see android.content.ContentResolver#query(android.net.Uri, String[],
    289      *      String, String[], String)
    290      */
    291     public Cursor query(SQLiteDatabase db, String[] projectionIn,
    292             String selection, String[] selectionArgs, String groupBy,
    293             String having, String sortOrder) {
    294         return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
    295                 null /* limit */, null /* cancellationSignal */);
    296     }
    297 
    298     /**
    299      * Perform a query by combining all current settings and the
    300      * information passed into this method.
    301      *
    302      * @param db the database to query on
    303      * @param projectionIn A list of which columns to return. Passing
    304      *   null will return all columns, which is discouraged to prevent
    305      *   reading data from storage that isn't going to be used.
    306      * @param selection A filter declaring which rows to return,
    307      *   formatted as an SQL WHERE clause (excluding the WHERE
    308      *   itself). Passing null will return all rows for the given URL.
    309      * @param selectionArgs You may include ?s in selection, which
    310      *   will be replaced by the values from selectionArgs, in order
    311      *   that they appear in the selection. The values will be bound
    312      *   as Strings.
    313      * @param groupBy A filter declaring how to group rows, formatted
    314      *   as an SQL GROUP BY clause (excluding the GROUP BY
    315      *   itself). Passing null will cause the rows to not be grouped.
    316      * @param having A filter declare which row groups to include in
    317      *   the cursor, if row grouping is being used, formatted as an
    318      *   SQL HAVING clause (excluding the HAVING itself).  Passing
    319      *   null will cause all row groups to be included, and is
    320      *   required when row grouping is not being used.
    321      * @param sortOrder How to order the rows, formatted as an SQL
    322      *   ORDER BY clause (excluding the ORDER BY itself). Passing null
    323      *   will use the default sort order, which may be unordered.
    324      * @param limit Limits the number of rows returned by the query,
    325      *   formatted as LIMIT clause. Passing null denotes no LIMIT clause.
    326      * @return a cursor over the result set
    327      * @see android.content.ContentResolver#query(android.net.Uri, String[],
    328      *      String, String[], String)
    329      */
    330     public Cursor query(SQLiteDatabase db, String[] projectionIn,
    331             String selection, String[] selectionArgs, String groupBy,
    332             String having, String sortOrder, String limit) {
    333         return query(db, projectionIn, selection, selectionArgs,
    334                 groupBy, having, sortOrder, limit, null);
    335     }
    336 
    337     /**
    338      * Perform a query by combining all current settings and the
    339      * information passed into this method.
    340      *
    341      * @param db the database to query on
    342      * @param projectionIn A list of which columns to return. Passing
    343      *   null will return all columns, which is discouraged to prevent
    344      *   reading data from storage that isn't going to be used.
    345      * @param selection A filter declaring which rows to return,
    346      *   formatted as an SQL WHERE clause (excluding the WHERE
    347      *   itself). Passing null will return all rows for the given URL.
    348      * @param selectionArgs You may include ?s in selection, which
    349      *   will be replaced by the values from selectionArgs, in order
    350      *   that they appear in the selection. The values will be bound
    351      *   as Strings.
    352      * @param groupBy A filter declaring how to group rows, formatted
    353      *   as an SQL GROUP BY clause (excluding the GROUP BY
    354      *   itself). Passing null will cause the rows to not be grouped.
    355      * @param having A filter declare which row groups to include in
    356      *   the cursor, if row grouping is being used, formatted as an
    357      *   SQL HAVING clause (excluding the HAVING itself).  Passing
    358      *   null will cause all row groups to be included, and is
    359      *   required when row grouping is not being used.
    360      * @param sortOrder How to order the rows, formatted as an SQL
    361      *   ORDER BY clause (excluding the ORDER BY itself). Passing null
    362      *   will use the default sort order, which may be unordered.
    363      * @param limit Limits the number of rows returned by the query,
    364      *   formatted as LIMIT clause. Passing null denotes no LIMIT clause.
    365      * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
    366      * If the operation is canceled, then {@link OperationCanceledException} will be thrown
    367      * when the query is executed.
    368      * @return a cursor over the result set
    369      * @see android.content.ContentResolver#query(android.net.Uri, String[],
    370      *      String, String[], String)
    371      */
    372     public Cursor query(SQLiteDatabase db, String[] projectionIn,
    373             String selection, String[] selectionArgs, String groupBy,
    374             String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
    375         if (mTables == null) {
    376             return null;
    377         }
    378 
    379         if (mStrict && selection != null && selection.length() > 0) {
    380             // Validate the user-supplied selection to detect syntactic anomalies
    381             // in the selection string that could indicate a SQL injection attempt.
    382             // The idea is to ensure that the selection clause is a valid SQL expression
    383             // by compiling it twice: once wrapped in parentheses and once as
    384             // originally specified. An attacker cannot create an expression that
    385             // would escape the SQL expression while maintaining balanced parentheses
    386             // in both the wrapped and original forms.
    387             String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy,
    388                     having, sortOrder, limit);
    389             db.validateSql(sqlForValidation, cancellationSignal); // will throw if query is invalid
    390         }
    391 
    392         String sql = buildQuery(
    393                 projectionIn, selection, groupBy, having,
    394                 sortOrder, limit);
    395 
    396         if (Log.isLoggable(TAG, Log.DEBUG)) {
    397             Log.d(TAG, "Performing query: " + sql);
    398         }
    399         return db.rawQueryWithFactory(
    400                 mFactory, sql, selectionArgs,
    401                 SQLiteDatabase.findEditTable(mTables),
    402                 cancellationSignal); // will throw if query is invalid
    403     }
    404 
    405     /**
    406      * Construct a SELECT statement suitable for use in a group of
    407      * SELECT statements that will be joined through UNION operators
    408      * in buildUnionQuery.
    409      *
    410      * @param projectionIn A list of which columns to return. Passing
    411      *    null will return all columns, which is discouraged to
    412      *    prevent reading data from storage that isn't going to be
    413      *    used.
    414      * @param selection A filter declaring which rows to return,
    415      *   formatted as an SQL WHERE clause (excluding the WHERE
    416      *   itself).  Passing null will return all rows for the given
    417      *   URL.
    418      * @param groupBy A filter declaring how to group rows, formatted
    419      *   as an SQL GROUP BY clause (excluding the GROUP BY itself).
    420      *   Passing null will cause the rows to not be grouped.
    421      * @param having A filter declare which row groups to include in
    422      *   the cursor, if row grouping is being used, formatted as an
    423      *   SQL HAVING clause (excluding the HAVING itself).  Passing
    424      *   null will cause all row groups to be included, and is
    425      *   required when row grouping is not being used.
    426      * @param sortOrder How to order the rows, formatted as an SQL
    427      *   ORDER BY clause (excluding the ORDER BY itself). Passing null
    428      *   will use the default sort order, which may be unordered.
    429      * @param limit Limits the number of rows returned by the query,
    430      *   formatted as LIMIT clause. Passing null denotes no LIMIT clause.
    431      * @return the resulting SQL SELECT statement
    432      */
    433     public String buildQuery(
    434             String[] projectionIn, String selection, String groupBy,
    435             String having, String sortOrder, String limit) {
    436         String[] projection = computeProjection(projectionIn);
    437 
    438         StringBuilder where = new StringBuilder();
    439         boolean hasBaseWhereClause = mWhereClause != null && mWhereClause.length() > 0;
    440 
    441         if (hasBaseWhereClause) {
    442             where.append(mWhereClause.toString());
    443             where.append(')');
    444         }
    445 
    446         // Tack on the user's selection, if present.
    447         if (selection != null && selection.length() > 0) {
    448             if (hasBaseWhereClause) {
    449                 where.append(" AND ");
    450             }
    451 
    452             where.append('(');
    453             where.append(selection);
    454             where.append(')');
    455         }
    456 
    457         return buildQueryString(
    458                 mDistinct, mTables, projection, where.toString(),
    459                 groupBy, having, sortOrder, limit);
    460     }
    461 
    462     /**
    463      * @deprecated This method's signature is misleading since no SQL parameter
    464      * substitution is carried out.  The selection arguments parameter does not get
    465      * used at all.  To avoid confusion, call
    466      * {@link #buildQuery(String[], String, String, String, String, String)} instead.
    467      */
    468     @Deprecated
    469     public String buildQuery(
    470             String[] projectionIn, String selection, String[] selectionArgs,
    471             String groupBy, String having, String sortOrder, String limit) {
    472         return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
    473     }
    474 
    475     /**
    476      * Construct a SELECT statement suitable for use in a group of
    477      * SELECT statements that will be joined through UNION operators
    478      * in buildUnionQuery.
    479      *
    480      * @param typeDiscriminatorColumn the name of the result column
    481      *   whose cells will contain the name of the table from which
    482      *   each row was drawn.
    483      * @param unionColumns the names of the columns to appear in the
    484      *   result.  This may include columns that do not appear in the
    485      *   table this SELECT is querying (i.e. mTables), but that do
    486      *   appear in one of the other tables in the UNION query that we
    487      *   are constructing.
    488      * @param columnsPresentInTable a Set of the names of the columns
    489      *   that appear in this table (i.e. in the table whose name is
    490      *   mTables).  Since columns in unionColumns include columns that
    491      *   appear only in other tables, we use this array to distinguish
    492      *   which ones actually are present.  Other columns will have
    493      *   NULL values for results from this subquery.
    494      * @param computedColumnsOffset all columns in unionColumns before
    495      *   this index are included under the assumption that they're
    496      *   computed and therefore won't appear in columnsPresentInTable,
    497      *   e.g. "date * 1000 as normalized_date"
    498      * @param typeDiscriminatorValue the value used for the
    499      *   type-discriminator column in this subquery
    500      * @param selection A filter declaring which rows to return,
    501      *   formatted as an SQL WHERE clause (excluding the WHERE
    502      *   itself).  Passing null will return all rows for the given
    503      *   URL.
    504      * @param groupBy A filter declaring how to group rows, formatted
    505      *   as an SQL GROUP BY clause (excluding the GROUP BY itself).
    506      *   Passing null will cause the rows to not be grouped.
    507      * @param having A filter declare which row groups to include in
    508      *   the cursor, if row grouping is being used, formatted as an
    509      *   SQL HAVING clause (excluding the HAVING itself).  Passing
    510      *   null will cause all row groups to be included, and is
    511      *   required when row grouping is not being used.
    512      * @return the resulting SQL SELECT statement
    513      */
    514     public String buildUnionSubQuery(
    515             String typeDiscriminatorColumn,
    516             String[] unionColumns,
    517             Set<String> columnsPresentInTable,
    518             int computedColumnsOffset,
    519             String typeDiscriminatorValue,
    520             String selection,
    521             String groupBy,
    522             String having) {
    523         int unionColumnsCount = unionColumns.length;
    524         String[] projectionIn = new String[unionColumnsCount];
    525 
    526         for (int i = 0; i < unionColumnsCount; i++) {
    527             String unionColumn = unionColumns[i];
    528 
    529             if (unionColumn.equals(typeDiscriminatorColumn)) {
    530                 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
    531                         + typeDiscriminatorColumn;
    532             } else if (i <= computedColumnsOffset
    533                        || columnsPresentInTable.contains(unionColumn)) {
    534                 projectionIn[i] = unionColumn;
    535             } else {
    536                 projectionIn[i] = "NULL AS " + unionColumn;
    537             }
    538         }
    539         return buildQuery(
    540                 projectionIn, selection, groupBy, having,
    541                 null /* sortOrder */,
    542                 null /* limit */);
    543     }
    544 
    545     /**
    546      * @deprecated This method's signature is misleading since no SQL parameter
    547      * substitution is carried out.  The selection arguments parameter does not get
    548      * used at all.  To avoid confusion, call
    549      * {@link #buildUnionSubQuery}
    550      * instead.
    551      */
    552     @Deprecated
    553     public String buildUnionSubQuery(
    554             String typeDiscriminatorColumn,
    555             String[] unionColumns,
    556             Set<String> columnsPresentInTable,
    557             int computedColumnsOffset,
    558             String typeDiscriminatorValue,
    559             String selection,
    560             String[] selectionArgs,
    561             String groupBy,
    562             String having) {
    563         return buildUnionSubQuery(
    564                 typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
    565                 computedColumnsOffset, typeDiscriminatorValue, selection,
    566                 groupBy, having);
    567     }
    568 
    569     /**
    570      * Given a set of subqueries, all of which are SELECT statements,
    571      * construct a query that returns the union of what those
    572      * subqueries return.
    573      * @param subQueries an array of SQL SELECT statements, all of
    574      *   which must have the same columns as the same positions in
    575      *   their results
    576      * @param sortOrder How to order the rows, formatted as an SQL
    577      *   ORDER BY clause (excluding the ORDER BY itself).  Passing
    578      *   null will use the default sort order, which may be unordered.
    579      * @param limit The limit clause, which applies to the entire union result set
    580      *
    581      * @return the resulting SQL SELECT statement
    582      */
    583     public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
    584         StringBuilder query = new StringBuilder(128);
    585         int subQueryCount = subQueries.length;
    586         String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
    587 
    588         for (int i = 0; i < subQueryCount; i++) {
    589             if (i > 0) {
    590                 query.append(unionOperator);
    591             }
    592             query.append(subQueries[i]);
    593         }
    594         appendClause(query, " ORDER BY ", sortOrder);
    595         appendClause(query, " LIMIT ", limit);
    596         return query.toString();
    597     }
    598 
    599     private String[] computeProjection(String[] projectionIn) {
    600         if (projectionIn != null && projectionIn.length > 0) {
    601             if (mProjectionMap != null) {
    602                 String[] projection = new String[projectionIn.length];
    603                 int length = projectionIn.length;
    604 
    605                 for (int i = 0; i < length; i++) {
    606                     String userColumn = projectionIn[i];
    607                     String column = mProjectionMap.get(userColumn);
    608 
    609                     if (column != null) {
    610                         projection[i] = column;
    611                         continue;
    612                     }
    613 
    614                     if (!mStrict &&
    615                             ( userColumn.contains(" AS ") || userColumn.contains(" as "))) {
    616                         /* A column alias already exist */
    617                         projection[i] = userColumn;
    618                         continue;
    619                     }
    620 
    621                     throw new IllegalArgumentException("Invalid column "
    622                             + projectionIn[i]);
    623                 }
    624                 return projection;
    625             } else {
    626                 return projectionIn;
    627             }
    628         } else if (mProjectionMap != null) {
    629             // Return all columns in projection map.
    630             Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
    631             String[] projection = new String[entrySet.size()];
    632             Iterator<Entry<String, String>> entryIter = entrySet.iterator();
    633             int i = 0;
    634 
    635             while (entryIter.hasNext()) {
    636                 Entry<String, String> entry = entryIter.next();
    637 
    638                 // Don't include the _count column when people ask for no projection.
    639                 if (entry.getKey().equals(BaseColumns._COUNT)) {
    640                     continue;
    641                 }
    642                 projection[i++] = entry.getValue();
    643             }
    644             return projection;
    645         }
    646         return null;
    647     }
    648 }
    649