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 convience 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             validateQuerySql(db, sqlForValidation,
    390                     cancellationSignal); // will throw if query is invalid
    391         }
    392 
    393         String sql = buildQuery(
    394                 projectionIn, selection, groupBy, having,
    395                 sortOrder, limit);
    396 
    397         if (Log.isLoggable(TAG, Log.DEBUG)) {
    398             Log.d(TAG, "Performing query: " + sql);
    399         }
    400         return db.rawQueryWithFactory(
    401                 mFactory, sql, selectionArgs,
    402                 SQLiteDatabase.findEditTable(mTables),
    403                 cancellationSignal); // will throw if query is invalid
    404     }
    405 
    406     /**
    407      * Verifies that a SQL SELECT statement is valid by compiling it.
    408      * If the SQL statement is not valid, this method will throw a {@link SQLiteException}.
    409      */
    410     private void validateQuerySql(SQLiteDatabase db, String sql,
    411             CancellationSignal cancellationSignal) {
    412         db.getThreadSession().prepare(sql,
    413                 db.getThreadDefaultConnectionFlags(true /*readOnly*/), cancellationSignal, null);
    414     }
    415 
    416     /**
    417      * Construct a SELECT statement suitable for use in a group of
    418      * SELECT statements that will be joined through UNION operators
    419      * in buildUnionQuery.
    420      *
    421      * @param projectionIn A list of which columns to return. Passing
    422      *    null will return all columns, which is discouraged to
    423      *    prevent reading data from storage that isn't going to be
    424      *    used.
    425      * @param selection A filter declaring which rows to return,
    426      *   formatted as an SQL WHERE clause (excluding the WHERE
    427      *   itself).  Passing null will return all rows for the given
    428      *   URL.
    429      * @param groupBy A filter declaring how to group rows, formatted
    430      *   as an SQL GROUP BY clause (excluding the GROUP BY itself).
    431      *   Passing null will cause the rows to not be grouped.
    432      * @param having A filter declare which row groups to include in
    433      *   the cursor, if row grouping is being used, formatted as an
    434      *   SQL HAVING clause (excluding the HAVING itself).  Passing
    435      *   null will cause all row groups to be included, and is
    436      *   required when row grouping is not being used.
    437      * @param sortOrder How to order the rows, formatted as an SQL
    438      *   ORDER BY clause (excluding the ORDER BY itself). Passing null
    439      *   will use the default sort order, which may be unordered.
    440      * @param limit Limits the number of rows returned by the query,
    441      *   formatted as LIMIT clause. Passing null denotes no LIMIT clause.
    442      * @return the resulting SQL SELECT statement
    443      */
    444     public String buildQuery(
    445             String[] projectionIn, String selection, String groupBy,
    446             String having, String sortOrder, String limit) {
    447         String[] projection = computeProjection(projectionIn);
    448 
    449         StringBuilder where = new StringBuilder();
    450         boolean hasBaseWhereClause = mWhereClause != null && mWhereClause.length() > 0;
    451 
    452         if (hasBaseWhereClause) {
    453             where.append(mWhereClause.toString());
    454             where.append(')');
    455         }
    456 
    457         // Tack on the user's selection, if present.
    458         if (selection != null && selection.length() > 0) {
    459             if (hasBaseWhereClause) {
    460                 where.append(" AND ");
    461             }
    462 
    463             where.append('(');
    464             where.append(selection);
    465             where.append(')');
    466         }
    467 
    468         return buildQueryString(
    469                 mDistinct, mTables, projection, where.toString(),
    470                 groupBy, having, sortOrder, limit);
    471     }
    472 
    473     /**
    474      * @deprecated This method's signature is misleading since no SQL parameter
    475      * substitution is carried out.  The selection arguments parameter does not get
    476      * used at all.  To avoid confusion, call
    477      * {@link #buildQuery(String[], String, String, String, String, String)} instead.
    478      */
    479     @Deprecated
    480     public String buildQuery(
    481             String[] projectionIn, String selection, String[] selectionArgs,
    482             String groupBy, String having, String sortOrder, String limit) {
    483         return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
    484     }
    485 
    486     /**
    487      * Construct a SELECT statement suitable for use in a group of
    488      * SELECT statements that will be joined through UNION operators
    489      * in buildUnionQuery.
    490      *
    491      * @param typeDiscriminatorColumn the name of the result column
    492      *   whose cells will contain the name of the table from which
    493      *   each row was drawn.
    494      * @param unionColumns the names of the columns to appear in the
    495      *   result.  This may include columns that do not appear in the
    496      *   table this SELECT is querying (i.e. mTables), but that do
    497      *   appear in one of the other tables in the UNION query that we
    498      *   are constructing.
    499      * @param columnsPresentInTable a Set of the names of the columns
    500      *   that appear in this table (i.e. in the table whose name is
    501      *   mTables).  Since columns in unionColumns include columns that
    502      *   appear only in other tables, we use this array to distinguish
    503      *   which ones actually are present.  Other columns will have
    504      *   NULL values for results from this subquery.
    505      * @param computedColumnsOffset all columns in unionColumns before
    506      *   this index are included under the assumption that they're
    507      *   computed and therefore won't appear in columnsPresentInTable,
    508      *   e.g. "date * 1000 as normalized_date"
    509      * @param typeDiscriminatorValue the value used for the
    510      *   type-discriminator column in this subquery
    511      * @param selection A filter declaring which rows to return,
    512      *   formatted as an SQL WHERE clause (excluding the WHERE
    513      *   itself).  Passing null will return all rows for the given
    514      *   URL.
    515      * @param groupBy A filter declaring how to group rows, formatted
    516      *   as an SQL GROUP BY clause (excluding the GROUP BY itself).
    517      *   Passing null will cause the rows to not be grouped.
    518      * @param having A filter declare which row groups to include in
    519      *   the cursor, if row grouping is being used, formatted as an
    520      *   SQL HAVING clause (excluding the HAVING itself).  Passing
    521      *   null will cause all row groups to be included, and is
    522      *   required when row grouping is not being used.
    523      * @return the resulting SQL SELECT statement
    524      */
    525     public String buildUnionSubQuery(
    526             String typeDiscriminatorColumn,
    527             String[] unionColumns,
    528             Set<String> columnsPresentInTable,
    529             int computedColumnsOffset,
    530             String typeDiscriminatorValue,
    531             String selection,
    532             String groupBy,
    533             String having) {
    534         int unionColumnsCount = unionColumns.length;
    535         String[] projectionIn = new String[unionColumnsCount];
    536 
    537         for (int i = 0; i < unionColumnsCount; i++) {
    538             String unionColumn = unionColumns[i];
    539 
    540             if (unionColumn.equals(typeDiscriminatorColumn)) {
    541                 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
    542                         + typeDiscriminatorColumn;
    543             } else if (i <= computedColumnsOffset
    544                        || columnsPresentInTable.contains(unionColumn)) {
    545                 projectionIn[i] = unionColumn;
    546             } else {
    547                 projectionIn[i] = "NULL AS " + unionColumn;
    548             }
    549         }
    550         return buildQuery(
    551                 projectionIn, selection, groupBy, having,
    552                 null /* sortOrder */,
    553                 null /* limit */);
    554     }
    555 
    556     /**
    557      * @deprecated This method's signature is misleading since no SQL parameter
    558      * substitution is carried out.  The selection arguments parameter does not get
    559      * used at all.  To avoid confusion, call
    560      * {@link #buildUnionSubQuery}
    561      * instead.
    562      */
    563     @Deprecated
    564     public String buildUnionSubQuery(
    565             String typeDiscriminatorColumn,
    566             String[] unionColumns,
    567             Set<String> columnsPresentInTable,
    568             int computedColumnsOffset,
    569             String typeDiscriminatorValue,
    570             String selection,
    571             String[] selectionArgs,
    572             String groupBy,
    573             String having) {
    574         return buildUnionSubQuery(
    575                 typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
    576                 computedColumnsOffset, typeDiscriminatorValue, selection,
    577                 groupBy, having);
    578     }
    579 
    580     /**
    581      * Given a set of subqueries, all of which are SELECT statements,
    582      * construct a query that returns the union of what those
    583      * subqueries return.
    584      * @param subQueries an array of SQL SELECT statements, all of
    585      *   which must have the same columns as the same positions in
    586      *   their results
    587      * @param sortOrder How to order the rows, formatted as an SQL
    588      *   ORDER BY clause (excluding the ORDER BY itself).  Passing
    589      *   null will use the default sort order, which may be unordered.
    590      * @param limit The limit clause, which applies to the entire union result set
    591      *
    592      * @return the resulting SQL SELECT statement
    593      */
    594     public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
    595         StringBuilder query = new StringBuilder(128);
    596         int subQueryCount = subQueries.length;
    597         String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
    598 
    599         for (int i = 0; i < subQueryCount; i++) {
    600             if (i > 0) {
    601                 query.append(unionOperator);
    602             }
    603             query.append(subQueries[i]);
    604         }
    605         appendClause(query, " ORDER BY ", sortOrder);
    606         appendClause(query, " LIMIT ", limit);
    607         return query.toString();
    608     }
    609 
    610     private String[] computeProjection(String[] projectionIn) {
    611         if (projectionIn != null && projectionIn.length > 0) {
    612             if (mProjectionMap != null) {
    613                 String[] projection = new String[projectionIn.length];
    614                 int length = projectionIn.length;
    615 
    616                 for (int i = 0; i < length; i++) {
    617                     String userColumn = projectionIn[i];
    618                     String column = mProjectionMap.get(userColumn);
    619 
    620                     if (column != null) {
    621                         projection[i] = column;
    622                         continue;
    623                     }
    624 
    625                     if (!mStrict &&
    626                             ( userColumn.contains(" AS ") || userColumn.contains(" as "))) {
    627                         /* A column alias already exist */
    628                         projection[i] = userColumn;
    629                         continue;
    630                     }
    631 
    632                     throw new IllegalArgumentException("Invalid column "
    633                             + projectionIn[i]);
    634                 }
    635                 return projection;
    636             } else {
    637                 return projectionIn;
    638             }
    639         } else if (mProjectionMap != null) {
    640             // Return all columns in projection map.
    641             Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
    642             String[] projection = new String[entrySet.size()];
    643             Iterator<Entry<String, String>> entryIter = entrySet.iterator();
    644             int i = 0;
    645 
    646             while (entryIter.hasNext()) {
    647                 Entry<String, String> entry = entryIter.next();
    648 
    649                 // Don't include the _count column when people ask for no projection.
    650                 if (entry.getKey().equals(BaseColumns._COUNT)) {
    651                     continue;
    652                 }
    653                 projection[i++] = entry.getValue();
    654             }
    655             return projection;
    656         }
    657         return null;
    658     }
    659 }
    660