/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.database.sqlite; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; import android.os.Build; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import com.android.internal.util.ArrayUtils; import libcore.util.EmptyArray; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This is a convenience class that helps build SQL queries to be sent to * {@link SQLiteDatabase} objects. *
* This class is often used to compose a SQL query from client-supplied fragments. Best practice * to protect against invalid or illegal SQL is to set the following: *
* WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) * * @param inWhere the chunk of text to append to the {@code WHERE} clause. */ public void appendWhere(@NonNull CharSequence inWhere) { if (mWhereClause == null) { mWhereClause = new StringBuilder(inWhere.length() + 16); } mWhereClause.append(inWhere); } /** * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded * by parenthesis and ANDed with the selection passed to {@link #query}. The final * {@code WHERE} clause looks like: *
* WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) * * @param inWhere the chunk of text to append to the {@code WHERE} clause. it will be escaped * to avoid SQL injection attacks */ public void appendWhereEscapeString(@NonNull String inWhere) { if (mWhereClause == null) { mWhereClause = new StringBuilder(inWhere.length() + 16); } DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere); } /** * Add a standalone chunk to the {@code WHERE} clause of this query. *
* This method differs from {@link #appendWhere(CharSequence)} in that it
* automatically appends {@code AND} to any existing {@code WHERE} clause
* already under construction before appending the given standalone
* expression wrapped in parentheses.
*
* @param inWhere the standalone expression to append to the {@code WHERE}
* clause. It will be wrapped in parentheses when it's appended.
*/
public void appendWhereStandalone(@NonNull CharSequence inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
if (mWhereClause.length() > 0) {
mWhereClause.append(" AND ");
}
mWhereClause.append('(').append(inWhere).append(')');
}
/**
* Sets the projection map for the query. The projection map maps
* from column names that the caller passes into query to database
* column names. This is useful for renaming columns as well as
* disambiguating column names when doing joins. For example you
* could map "name" to "people.name". If a projection map is set
* it must contain all column names the user may request, even if
* the key and value are the same.
*
* @param columnMap maps from the user column names to the database column names
*/
public void setProjectionMap(@Nullable Map
* This feature is disabled by default on each newly constructed
* {@link SQLiteQueryBuilder} and needs to be manually enabled.
*/
public void setStrict(boolean strict) {
if (strict) {
mStrictFlags |= STRICT_PARENTHESES;
} else {
mStrictFlags &= ~STRICT_PARENTHESES;
}
}
/**
* Get if the query is marked as strict, as last configured by
* {@link #setStrict(boolean)}.
*/
public boolean isStrict() {
return (mStrictFlags & STRICT_PARENTHESES) != 0;
}
/**
* When enabled, verify that all projections and {@link ContentValues} only
* contain valid columns as defined by {@link #setProjectionMap(Map)}.
*
* This enforcement applies to {@link #insert}, {@link #query}, and
* {@link #update} operations. Any enforcement failures will throw an
* {@link IllegalArgumentException}.
*
* This feature is disabled by default on each newly constructed
* {@link SQLiteQueryBuilder} and needs to be manually enabled.
*/
public void setStrictColumns(boolean strictColumns) {
if (strictColumns) {
mStrictFlags |= STRICT_COLUMNS;
} else {
mStrictFlags &= ~STRICT_COLUMNS;
}
}
/**
* Get if the query is marked as strict, as last configured by
* {@link #setStrictColumns(boolean)}.
*/
public boolean isStrictColumns() {
return (mStrictFlags & STRICT_COLUMNS) != 0;
}
/**
* When enabled, verify that all untrusted SQL conforms to a restricted SQL
* grammar. Here are the restrictions applied:
*
* This enforcement applies to {@link #query}, {@link #update} and
* {@link #delete} operations. This enforcement does not apply to trusted
* inputs, such as those provided by {@link #appendWhere}. Any enforcement
* failures will throw an {@link IllegalArgumentException}.
*
* This feature is disabled by default on each newly constructed
* {@link SQLiteQueryBuilder} and needs to be manually enabled.
*/
public void setStrictGrammar(boolean strictGrammar) {
if (strictGrammar) {
mStrictFlags |= STRICT_GRAMMAR;
} else {
mStrictFlags &= ~STRICT_GRAMMAR;
}
}
/**
* Get if the query is marked as strict, as last configured by
* {@link #setStrictGrammar(boolean)}.
*/
public boolean isStrictGrammar() {
return (mStrictFlags & STRICT_GRAMMAR) != 0;
}
/**
* Build an SQL query string from the given clauses.
*
* @param distinct true if you want each row to be unique, false otherwise.
* @param tables The table names to compile the query against.
* @param columns A list of which columns to return. Passing null will
* return all columns, which is discouraged to prevent reading
* data from storage that isn't going to be used.
* @param where A filter declaring which rows to return, formatted as an SQL
* {@code WHERE} clause (excluding the {@code WHERE} itself). Passing {@code null} will
* return all rows for the given URL.
* @param groupBy A filter declaring how to group rows, formatted as an SQL
* {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). Passing {@code null}
* will cause the rows to not be grouped.
* @param having A filter declare which row groups to include in the cursor,
* if row grouping is being used, formatted as an SQL {@code HAVING}
* clause (excluding the {@code HAVING} itself). Passing null will cause
* all row groups to be included, and is required when row
* grouping is not being used.
* @param orderBy How to order the rows, formatted as an SQL {@code ORDER BY} clause
* (excluding the {@code ORDER BY} itself). Passing null will use the
* default sort order, which may be unordered.
* @param limit Limits the number of rows returned by the query,
* formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
* @return the SQL query string
*/
public static String buildQueryString(
boolean distinct, String tables, String[] columns, String where,
String groupBy, String having, String orderBy, String limit) {
if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
throw new IllegalArgumentException(
"HAVING clauses are only permitted when using a groupBy clause");
}
StringBuilder query = new StringBuilder(120);
query.append("SELECT ");
if (distinct) {
query.append("DISTINCT ");
}
if (columns != null && columns.length != 0) {
appendColumns(query, columns);
} else {
query.append("* ");
}
query.append("FROM ");
query.append(tables);
appendClause(query, " WHERE ", where);
appendClause(query, " GROUP BY ", groupBy);
appendClause(query, " HAVING ", having);
appendClause(query, " ORDER BY ", orderBy);
appendClause(query, " LIMIT ", limit);
return query.toString();
}
private static void appendClause(StringBuilder s, String name, String clause) {
if (!TextUtils.isEmpty(clause)) {
s.append(name);
s.append(clause);
}
}
/**
* Add the names that are non-null in columns to s, separating
* them with commas.
*/
public static void appendColumns(StringBuilder s, String[] columns) {
int n = columns.length;
for (int i = 0; i < n; i++) {
String column = columns[i];
if (column != null) {
if (i > 0) {
s.append(", ");
}
s.append(column);
}
}
s.append(' ');
}
/**
* Perform a query by combining all current settings and the
* information passed into this method.
*
* @param db the database to query on
* @param projectionIn A list of which columns to return. Passing
* null will return all columns, which is discouraged to prevent
* reading data from storage that isn't going to be used.
* @param selection A filter declaring which rows to return,
* formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
* itself). Passing null will return all rows for the given URL.
* @param selectionArgs You may include ?s in selection, which
* will be replaced by the values from selectionArgs, in order
* that they appear in the selection. The values will be bound
* as Strings.
* @param groupBy A filter declaring how to group rows, formatted
* as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
* itself). Passing null will cause the rows to not be grouped.
* @param having A filter declare which row groups to include in
* the cursor, if row grouping is being used, formatted as an
* SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
* null will cause all row groups to be included, and is
* required when row grouping is not being used.
* @param sortOrder How to order the rows, formatted as an SQL
* {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
* will use the default sort order, which may be unordered.
* @return a cursor over the result set
* @see android.content.ContentResolver#query(android.net.Uri, String[],
* String, String[], String)
*/
public Cursor query(SQLiteDatabase db, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder) {
return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
null /* limit */, null /* cancellationSignal */);
}
/**
* Perform a query by combining all current settings and the
* information passed into this method.
*
* @param db the database to query on
* @param projectionIn A list of which columns to return. Passing
* null will return all columns, which is discouraged to prevent
* reading data from storage that isn't going to be used.
* @param selection A filter declaring which rows to return,
* formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
* itself). Passing null will return all rows for the given URL.
* @param selectionArgs You may include ?s in selection, which
* will be replaced by the values from selectionArgs, in order
* that they appear in the selection. The values will be bound
* as Strings.
* @param groupBy A filter declaring how to group rows, formatted
* as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
* itself). Passing null will cause the rows to not be grouped.
* @param having A filter declare which row groups to include in
* the cursor, if row grouping is being used, formatted as an
* SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
* null will cause all row groups to be included, and is
* required when row grouping is not being used.
* @param sortOrder How to order the rows, formatted as an SQL
* {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
* will use the default sort order, which may be unordered.
* @param limit Limits the number of rows returned by the query,
* formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
* @return a cursor over the result set
* @see android.content.ContentResolver#query(android.net.Uri, String[],
* String, String[], String)
*/
public Cursor query(SQLiteDatabase db, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder, String limit) {
return query(db, projectionIn, selection, selectionArgs,
groupBy, having, sortOrder, limit, null);
}
/**
* Perform a query by combining all current settings and the
* information passed into this method.
*
* @param db the database to query on
* @param projectionIn A list of which columns to return. Passing
* null will return all columns, which is discouraged to prevent
* reading data from storage that isn't going to be used.
* @param selection A filter declaring which rows to return,
* formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
* itself). Passing null will return all rows for the given URL.
* @param selectionArgs You may include ?s in selection, which
* will be replaced by the values from selectionArgs, in order
* that they appear in the selection. The values will be bound
* as Strings.
* @param groupBy A filter declaring how to group rows, formatted
* as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
* itself). Passing null will cause the rows to not be grouped.
* @param having A filter declare which row groups to include in
* the cursor, if row grouping is being used, formatted as an
* SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
* null will cause all row groups to be included, and is
* required when row grouping is not being used.
* @param sortOrder How to order the rows, formatted as an SQL
* {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
* will use the default sort order, which may be unordered.
* @param limit Limits the number of rows returned by the query,
* formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* If the operation is canceled, then {@link OperationCanceledException} will be thrown
* when the query is executed.
* @return a cursor over the result set
* @see android.content.ContentResolver#query(android.net.Uri, String[],
* String, String[], String)
*/
public Cursor query(SQLiteDatabase db, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
if (mTables == null) {
return null;
}
final String sql;
final String unwrappedSql = buildQuery(
projectionIn, selection, groupBy, having,
sortOrder, limit);
if (isStrictColumns()) {
enforceStrictColumns(projectionIn);
}
if (isStrictGrammar()) {
enforceStrictGrammar(selection, groupBy, having, sortOrder, limit);
}
if (isStrict()) {
// Validate the user-supplied selection to detect syntactic anomalies
// in the selection string that could indicate a SQL injection attempt.
// The idea is to ensure that the selection clause is a valid SQL expression
// by compiling it twice: once wrapped in parentheses and once as
// originally specified. An attacker cannot create an expression that
// would escape the SQL expression while maintaining balanced parentheses
// in both the wrapped and original forms.
// NOTE: The ordering of the below operations is important; we must
// execute the wrapped query to ensure the untrusted clause has been
// fully isolated.
// Validate the unwrapped query
db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
// Execute wrapped query for extra protection
final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
wrap(having), sortOrder, limit);
sql = wrappedSql;
} else {
// Execute unwrapped query
sql = unwrappedSql;
}
final String[] sqlArgs = selectionArgs;
if (Log.isLoggable(TAG, Log.DEBUG)) {
if (Build.IS_DEBUGGABLE) {
Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
} else {
Log.d(TAG, sql);
}
}
return db.rawQueryWithFactory(
mFactory, sql, sqlArgs,
SQLiteDatabase.findEditTable(mTables),
cancellationSignal); // will throw if query is invalid
}
/**
* Perform an insert by combining all current settings and the
* information passed into this method.
*
* @param db the database to insert on
* @return the row ID of the newly inserted row, or -1 if an error occurred
*/
public long insert(@NonNull SQLiteDatabase db, @NonNull ContentValues values) {
Objects.requireNonNull(mTables, "No tables defined");
Objects.requireNonNull(db, "No database defined");
Objects.requireNonNull(values, "No values defined");
if (isStrictColumns()) {
enforceStrictColumns(values);
}
final String sql = buildInsert(values);
final ArrayMap
*
*
*
* All column references must be valid as defined by
* {@link #setProjectionMap(Map)}.
*