/*
* Copyright (C) 2011 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 dalvik.system.BlockGuard;
import dalvik.system.CloseGuard;
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDebug.DbStats;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.util.LruCache;
import android.util.Printer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Represents a SQLite database connection.
* Each connection wraps an instance of a native sqlite3
object.
*
* When database connection pooling is enabled, there can be multiple active * connections to the same database. Otherwise there is typically only one * connection per database. *
* When the SQLite WAL feature is enabled, multiple readers and one writer * can concurrently access the database. Without WAL, readers and writers * are mutually exclusive. *
* ** Connection objects are not thread-safe. They are acquired as needed to * perform a database operation and are then returned to the pool. At any * given time, a connection is either owned and used by a {@link SQLiteSession} * object or the {@link SQLiteConnectionPool}. Those classes are * responsible for serializing operations to guard against concurrent * use of a connection. *
* The guarantee of having a single owner allows this class to be implemented * without locks and greatly simplifies resource management. *
* ** The connection object object owns *all* of the SQLite related native * objects that are associated with the connection. What's more, there are * no other objects in the system that are capable of obtaining handles to * those native objects. Consequently, when the connection is closed, we do * not have to worry about what other components might have references to * its associated SQLite state -- there are none. *
* Encapsulation is what ensures that the connection object's * lifecycle does not become a tortured mess of finalizers and reference * queues. *
* ** This class must tolerate reentrant execution of SQLite operations because * triggers may call custom SQLite functions that perform additional queries. *
* * @hide */ public final class SQLiteConnection implements CancellationSignal.OnCancelListener { private static final String TAG = "SQLiteConnection"; private static final boolean DEBUG = false; private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private static final Pattern TRIM_SQL_PATTERN = Pattern.compile("[\\s]*\\n+[\\s]*"); private final CloseGuard mCloseGuard = CloseGuard.get(); private final SQLiteConnectionPool mPool; private final SQLiteDatabaseConfiguration mConfiguration; private final int mConnectionId; private final boolean mIsPrimaryConnection; private final boolean mIsReadOnlyConnection; private final PreparedStatementCache mPreparedStatementCache; private PreparedStatement mPreparedStatementPool; // The recent operations log. private final OperationLog mRecentOperations = new OperationLog(); // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY) private long mConnectionPtr; private boolean mOnlyAllowReadOnlyOperations; // The number of times attachCancellationSignal has been called. // Because SQLite statement execution can be reentrant, we keep track of how many // times we have attempted to attach a cancellation signal to the connection so that // we can ensure that we detach the signal at the right time. private int mCancellationSignalAttachCount; private static native long nativeOpen(String path, int openFlags, String label, boolean enableTrace, boolean enableProfile); private static native void nativeClose(long connectionPtr); private static native void nativeRegisterCustomFunction(long connectionPtr, SQLiteCustomFunction function); private static native void nativeRegisterLocalizedCollators(long connectionPtr, String locale); private static native long nativePrepareStatement(long connectionPtr, String sql); private static native void nativeFinalizeStatement(long connectionPtr, long statementPtr); private static native int nativeGetParameterCount(long connectionPtr, long statementPtr); private static native boolean nativeIsReadOnly(long connectionPtr, long statementPtr); private static native int nativeGetColumnCount(long connectionPtr, long statementPtr); private static native String nativeGetColumnName(long connectionPtr, long statementPtr, int index); private static native void nativeBindNull(long connectionPtr, long statementPtr, int index); private static native void nativeBindLong(long connectionPtr, long statementPtr, int index, long value); private static native void nativeBindDouble(long connectionPtr, long statementPtr, int index, double value); private static native void nativeBindString(long connectionPtr, long statementPtr, int index, String value); private static native void nativeBindBlob(long connectionPtr, long statementPtr, int index, byte[] value); private static native void nativeResetStatementAndClearBindings( long connectionPtr, long statementPtr); private static native void nativeExecute(long connectionPtr, long statementPtr); private static native long nativeExecuteForLong(long connectionPtr, long statementPtr); private static native String nativeExecuteForString(long connectionPtr, long statementPtr); private static native int nativeExecuteForBlobFileDescriptor( long connectionPtr, long statementPtr); private static native int nativeExecuteForChangedRowCount(long connectionPtr, long statementPtr); private static native long nativeExecuteForLastInsertedRowId( long connectionPtr, long statementPtr); private static native long nativeExecuteForCursorWindow( long connectionPtr, long statementPtr, long windowPtr, int startPos, int requiredPos, boolean countAllRows); private static native int nativeGetDbLookaside(long connectionPtr); private static native void nativeCancel(long connectionPtr); private static native void nativeResetCancel(long connectionPtr, boolean cancelable); private SQLiteConnection(SQLiteConnectionPool pool, SQLiteDatabaseConfiguration configuration, int connectionId, boolean primaryConnection) { mPool = pool; mConfiguration = new SQLiteDatabaseConfiguration(configuration); mConnectionId = connectionId; mIsPrimaryConnection = primaryConnection; mIsReadOnlyConnection = (configuration.openFlags & SQLiteDatabase.OPEN_READONLY) != 0; mPreparedStatementCache = new PreparedStatementCache( mConfiguration.maxSqlCacheSize); mCloseGuard.open("close"); } @Override protected void finalize() throws Throwable { try { if (mPool != null && mConnectionPtr != 0) { mPool.onConnectionLeaked(); } dispose(true); } finally { super.finalize(); } } // Called by SQLiteConnectionPool only. static SQLiteConnection open(SQLiteConnectionPool pool, SQLiteDatabaseConfiguration configuration, int connectionId, boolean primaryConnection) { SQLiteConnection connection = new SQLiteConnection(pool, configuration, connectionId, primaryConnection); try { connection.open(); return connection; } catch (SQLiteException ex) { connection.dispose(false); throw ex; } } // Called by SQLiteConnectionPool only. // Closes the database closes and releases all of its associated resources. // Do not call methods on the connection after it is closed. It will probably crash. void close() { dispose(false); } private void open() { mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags, mConfiguration.label, SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME); setPageSize(); setForeignKeyModeFromConfiguration(); setWalModeFromConfiguration(); setJournalSizeLimit(); setAutoCheckpointInterval(); setLocaleFromConfiguration(); // Register custom functions. final int functionCount = mConfiguration.customFunctions.size(); for (int i = 0; i < functionCount; i++) { SQLiteCustomFunction function = mConfiguration.customFunctions.get(i); nativeRegisterCustomFunction(mConnectionPtr, function); } } private void dispose(boolean finalized) { if (mCloseGuard != null) { if (finalized) { mCloseGuard.warnIfOpen(); } mCloseGuard.close(); } if (mConnectionPtr != 0) { final int cookie = mRecentOperations.beginOperation("close", null, null); try { mPreparedStatementCache.evictAll(); nativeClose(mConnectionPtr); mConnectionPtr = 0; } finally { mRecentOperations.endOperation(cookie); } } } private void setPageSize() { if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { final long newValue = SQLiteGlobal.getDefaultPageSize(); long value = executeForLong("PRAGMA page_size", null, null); if (value != newValue) { execute("PRAGMA page_size=" + newValue, null, null); } } } private void setAutoCheckpointInterval() { if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { final long newValue = SQLiteGlobal.getWALAutoCheckpoint(); long value = executeForLong("PRAGMA wal_autocheckpoint", null, null); if (value != newValue) { executeForLong("PRAGMA wal_autocheckpoint=" + newValue, null, null); } } } private void setJournalSizeLimit() { if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { final long newValue = SQLiteGlobal.getJournalSizeLimit(); long value = executeForLong("PRAGMA journal_size_limit", null, null); if (value != newValue) { executeForLong("PRAGMA journal_size_limit=" + newValue, null, null); } } } private void setForeignKeyModeFromConfiguration() { if (!mIsReadOnlyConnection) { final long newValue = mConfiguration.foreignKeyConstraintsEnabled ? 1 : 0; long value = executeForLong("PRAGMA foreign_keys", null, null); if (value != newValue) { execute("PRAGMA foreign_keys=" + newValue, null, null); } } } private void setWalModeFromConfiguration() { if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { if ((mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) { setJournalMode("WAL"); setSyncMode(SQLiteGlobal.getWALSyncMode()); } else { setJournalMode(SQLiteGlobal.getDefaultJournalMode()); setSyncMode(SQLiteGlobal.getDefaultSyncMode()); } } } private void setSyncMode(String newValue) { String value = executeForString("PRAGMA synchronous", null, null); if (!canonicalizeSyncMode(value).equalsIgnoreCase( canonicalizeSyncMode(newValue))) { execute("PRAGMA synchronous=" + newValue, null, null); } } private static String canonicalizeSyncMode(String value) { if (value.equals("0")) { return "OFF"; } else if (value.equals("1")) { return "NORMAL"; } else if (value.equals("2")) { return "FULL"; } return value; } private void setJournalMode(String newValue) { String value = executeForString("PRAGMA journal_mode", null, null); if (!value.equalsIgnoreCase(newValue)) { try { String result = executeForString("PRAGMA journal_mode=" + newValue, null, null); if (result.equalsIgnoreCase(newValue)) { return; } // PRAGMA journal_mode silently fails and returns the original journal // mode in some cases if the journal mode could not be changed. } catch (SQLiteDatabaseLockedException ex) { // This error (SQLITE_BUSY) occurs if one connection has the database // open in WAL mode and another tries to change it to non-WAL. } // Because we always disable WAL mode when a database is first opened // (even if we intend to re-enable it), we can encounter problems if // there is another open connection to the database somewhere. // This can happen for a variety of reasons such as an application opening // the same database in multiple processes at the same time or if there is a // crashing content provider service that the ActivityManager has // removed from its registry but whose process hasn't quite died yet // by the time it is restarted in a new process. // // If we don't change the journal mode, nothing really bad happens. // In the worst case, an application that enables WAL might not actually // get it, although it can still use connection pooling. Log.w(TAG, "Could not change the database journal mode of '" + mConfiguration.label + "' from '" + value + "' to '" + newValue + "' because the database is locked. This usually means that " + "there are other open connections to the database which prevents " + "the database from enabling or disabling write-ahead logging mode. " + "Proceeding without changing the journal mode."); } } private void setLocaleFromConfiguration() { if ((mConfiguration.openFlags & SQLiteDatabase.NO_LOCALIZED_COLLATORS) != 0) { return; } // Register the localized collators. final String newLocale = mConfiguration.locale.toString(); nativeRegisterLocalizedCollators(mConnectionPtr, newLocale); // If the database is read-only, we cannot modify the android metadata table // or existing indexes. if (mIsReadOnlyConnection) { return; } try { // Ensure the android metadata table exists. execute("CREATE TABLE IF NOT EXISTS android_metadata (locale TEXT)", null, null); // Check whether the locale was actually changed. final String oldLocale = executeForString("SELECT locale FROM android_metadata " + "UNION SELECT NULL ORDER BY locale DESC LIMIT 1", null, null); if (oldLocale != null && oldLocale.equals(newLocale)) { return; } // Go ahead and update the indexes using the new locale. execute("BEGIN", null, null); boolean success = false; try { execute("DELETE FROM android_metadata", null, null); execute("INSERT INTO android_metadata (locale) VALUES(?)", new Object[] { newLocale }, null); execute("REINDEX LOCALIZED", null, null); success = true; } finally { execute(success ? "COMMIT" : "ROLLBACK", null, null); } } catch (RuntimeException ex) { throw new SQLiteException("Failed to change locale for db '" + mConfiguration.label + "' to '" + newLocale + "'.", ex); } } // Called by SQLiteConnectionPool only. void reconfigure(SQLiteDatabaseConfiguration configuration) { mOnlyAllowReadOnlyOperations = false; // Register custom functions. final int functionCount = configuration.customFunctions.size(); for (int i = 0; i < functionCount; i++) { SQLiteCustomFunction function = configuration.customFunctions.get(i); if (!mConfiguration.customFunctions.contains(function)) { nativeRegisterCustomFunction(mConnectionPtr, function); } } // Remember what changed. boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled != mConfiguration.foreignKeyConstraintsEnabled; boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags) & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; boolean localeChanged = !configuration.locale.equals(mConfiguration.locale); // Update configuration parameters. mConfiguration.updateParametersFrom(configuration); // Update prepared statement cache size. mPreparedStatementCache.resize(configuration.maxSqlCacheSize); // Update foreign key mode. if (foreignKeyModeChanged) { setForeignKeyModeFromConfiguration(); } // Update WAL. if (walModeChanged) { setWalModeFromConfiguration(); } // Update locale. if (localeChanged) { setLocaleFromConfiguration(); } } // Called by SQLiteConnectionPool only. // When set to true, executing write operations will throw SQLiteException. // Preparing statements that might write is ok, just don't execute them. void setOnlyAllowReadOnlyOperations(boolean readOnly) { mOnlyAllowReadOnlyOperations = readOnly; } // Called by SQLiteConnectionPool only. // Returns true if the prepared statement cache contains the specified SQL. boolean isPreparedStatementInCache(String sql) { return mPreparedStatementCache.get(sql) != null; } /** * Gets the unique id of this connection. * @return The connection id. */ public int getConnectionId() { return mConnectionId; } /** * Returns true if this is the primary database connection. * @return True if this is the primary database connection. */ public boolean isPrimaryConnection() { return mIsPrimaryConnection; } /** * Prepares a statement for execution but does not bind its parameters or execute it. ** This method can be used to check for syntax errors during compilation * prior to execution of the statement. If the {@code outStatementInfo} argument * is not null, the provided {@link SQLiteStatementInfo} object is populated * with information about the statement. *
* A prepared statement makes no reference to the arguments that may eventually * be bound to it, consequently it it possible to cache certain prepared statements * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable, * then it will be stored in the cache for later. *
* To take advantage of this behavior as an optimization, the connection pool * provides a method to acquire a connection that already has a given SQL statement * in its prepared statement cache so that it is ready for execution. *
* * @param sql The SQL statement to prepare. * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate * with information about the statement, or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error. */ public void prepare(String sql, SQLiteStatementInfo outStatementInfo) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } final int cookie = mRecentOperations.beginOperation("prepare", sql, null); try { final PreparedStatement statement = acquirePreparedStatement(sql); try { if (outStatementInfo != null) { outStatementInfo.numParameters = statement.mNumParameters; outStatementInfo.readOnly = statement.mReadOnly; final int columnCount = nativeGetColumnCount( mConnectionPtr, statement.mStatementPtr); if (columnCount == 0) { outStatementInfo.columnNames = EMPTY_STRING_ARRAY; } else { outStatementInfo.columnNames = new String[columnCount]; for (int i = 0; i < columnCount; i++) { outStatementInfo.columnNames[i] = nativeGetColumnName( mConnectionPtr, statement.mStatementPtr, i); } } } } finally { releasePreparedStatement(statement); } } catch (RuntimeException ex) { mRecentOperations.failOperation(cookie, ex); throw ex; } finally { mRecentOperations.endOperation(cookie); } } /** * Executes a statement that does not return a result. * * @param sql The SQL statement to execute. * @param bindArgs The arguments to bind, or null if none. * @param cancellationSignal A signal to cancel the operation in progress, or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. * @throws OperationCanceledException if the operation was canceled. */ public void execute(String sql, Object[] bindArgs, CancellationSignal cancellationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } final int cookie = mRecentOperations.beginOperation("execute", sql, bindArgs); try { final PreparedStatement statement = acquirePreparedStatement(sql); try { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); applyBlockGuardPolicy(statement); attachCancellationSignal(cancellationSignal); try { nativeExecute(mConnectionPtr, statement.mStatementPtr); } finally { detachCancellationSignal(cancellationSignal); } } finally { releasePreparedStatement(statement); } } catch (RuntimeException ex) { mRecentOperations.failOperation(cookie, ex); throw ex; } finally { mRecentOperations.endOperation(cookie); } } /** * Executes a statement that returns a singlelong
result.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The value of the first column in the first row of the result set
* as a long
, or zero if none.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
public long executeForLong(String sql, Object[] bindArgs,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
final int cookie = mRecentOperations.beginOperation("executeForLong", sql, bindArgs);
try {
final PreparedStatement statement = acquirePreparedStatement(sql);
try {
throwIfStatementForbidden(statement);
bindArguments(statement, bindArgs);
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr);
} finally {
detachCancellationSignal(cancellationSignal);
}
} finally {
releasePreparedStatement(statement);
}
} catch (RuntimeException ex) {
mRecentOperations.failOperation(cookie, ex);
throw ex;
} finally {
mRecentOperations.endOperation(cookie);
}
}
/**
* Executes a statement that returns a single {@link String} result.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The value of the first column in the first row of the result set
* as a String
, or null if none.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
public String executeForString(String sql, Object[] bindArgs,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
final int cookie = mRecentOperations.beginOperation("executeForString", sql, bindArgs);
try {
final PreparedStatement statement = acquirePreparedStatement(sql);
try {
throwIfStatementForbidden(statement);
bindArguments(statement, bindArgs);
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr);
} finally {
detachCancellationSignal(cancellationSignal);
}
} finally {
releasePreparedStatement(statement);
}
} catch (RuntimeException ex) {
mRecentOperations.failOperation(cookie, ex);
throw ex;
} finally {
mRecentOperations.endOperation(cookie);
}
}
/**
* Executes a statement that returns a single BLOB result as a
* file descriptor to a shared memory region.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The file descriptor for a shared memory region that contains
* the value of the first column in the first row of the result set as a BLOB,
* or null if none.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
final int cookie = mRecentOperations.beginOperation("executeForBlobFileDescriptor",
sql, bindArgs);
try {
final PreparedStatement statement = acquirePreparedStatement(sql);
try {
throwIfStatementForbidden(statement);
bindArguments(statement, bindArgs);
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
int fd = nativeExecuteForBlobFileDescriptor(
mConnectionPtr, statement.mStatementPtr);
return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null;
} finally {
detachCancellationSignal(cancellationSignal);
}
} finally {
releasePreparedStatement(statement);
}
} catch (RuntimeException ex) {
mRecentOperations.failOperation(cookie, ex);
throw ex;
} finally {
mRecentOperations.endOperation(cookie);
}
}
/**
* Executes a statement that returns a count of the number of rows
* that were changed. Use for UPDATE or DELETE SQL statements.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The number of rows that were changed.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
public int executeForChangedRowCount(String sql, Object[] bindArgs,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
int changedRows = 0;
final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
sql, bindArgs);
try {
final PreparedStatement statement = acquirePreparedStatement(sql);
try {
throwIfStatementForbidden(statement);
bindArguments(statement, bindArgs);
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
changedRows = nativeExecuteForChangedRowCount(
mConnectionPtr, statement.mStatementPtr);
return changedRows;
} finally {
detachCancellationSignal(cancellationSignal);
}
} finally {
releasePreparedStatement(statement);
}
} catch (RuntimeException ex) {
mRecentOperations.failOperation(cookie, ex);
throw ex;
} finally {
if (mRecentOperations.endOperationDeferLog(cookie)) {
mRecentOperations.logOperation(cookie, "changedRows=" + changedRows);
}
}
}
/**
* Executes a statement that returns the row id of the last row inserted
* by the statement. Use for INSERT SQL statements.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The row id of the last row that was inserted, or 0 if none.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
public long executeForLastInsertedRowId(String sql, Object[] bindArgs,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
final int cookie = mRecentOperations.beginOperation("executeForLastInsertedRowId",
sql, bindArgs);
try {
final PreparedStatement statement = acquirePreparedStatement(sql);
try {
throwIfStatementForbidden(statement);
bindArguments(statement, bindArgs);
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
return nativeExecuteForLastInsertedRowId(
mConnectionPtr, statement.mStatementPtr);
} finally {
detachCancellationSignal(cancellationSignal);
}
} finally {
releasePreparedStatement(statement);
}
} catch (RuntimeException ex) {
mRecentOperations.failOperation(cookie, ex);
throw ex;
} finally {
mRecentOperations.endOperation(cookie);
}
}
/**
* Executes a statement and populates the specified {@link CursorWindow}
* with a range of results. Returns the number of rows that were counted
* during query execution.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param window The cursor window to clear and fill.
* @param startPos The start position for filling the window.
* @param requiredPos The position of a row that MUST be in the window.
* If it won't fit, then the query should discard part of what it filled
* so that it does. Must be greater than or equal to startPos
.
* @param countAllRows True to count all rows that the query would return
* regagless of whether they fit in the window.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The number of rows that were counted during query execution. Might
* not be all rows in the result set unless countAllRows
is true.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
public int executeForCursorWindow(String sql, Object[] bindArgs,
CursorWindow window, int startPos, int requiredPos, boolean countAllRows,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (window == null) {
throw new IllegalArgumentException("window must not be null.");
}
window.acquireReference();
try {
int actualPos = -1;
int countedRows = -1;
int filledRows = -1;
final int cookie = mRecentOperations.beginOperation("executeForCursorWindow",
sql, bindArgs);
try {
final PreparedStatement statement = acquirePreparedStatement(sql);
try {
throwIfStatementForbidden(statement);
bindArguments(statement, bindArgs);
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
final long result = nativeExecuteForCursorWindow(
mConnectionPtr, statement.mStatementPtr, window.mWindowPtr,
startPos, requiredPos, countAllRows);
actualPos = (int)(result >> 32);
countedRows = (int)result;
filledRows = window.getNumRows();
window.setStartPosition(actualPos);
return countedRows;
} finally {
detachCancellationSignal(cancellationSignal);
}
} finally {
releasePreparedStatement(statement);
}
} catch (RuntimeException ex) {
mRecentOperations.failOperation(cookie, ex);
throw ex;
} finally {
if (mRecentOperations.endOperationDeferLog(cookie)) {
mRecentOperations.logOperation(cookie, "window='" + window
+ "', startPos=" + startPos
+ ", actualPos=" + actualPos
+ ", filledRows=" + filledRows
+ ", countedRows=" + countedRows);
}
}
} finally {
window.releaseReference();
}
}
private PreparedStatement acquirePreparedStatement(String sql) {
PreparedStatement statement = mPreparedStatementCache.get(sql);
boolean skipCache = false;
if (statement != null) {
if (!statement.mInUse) {
return statement;
}
// The statement is already in the cache but is in use (this statement appears
// to be not only re-entrant but recursive!). So prepare a new copy of the
// statement but do not cache it.
skipCache = true;
}
final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
try {
final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
final int type = DatabaseUtils.getSqlStatementType(sql);
final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
if (!skipCache && isCacheable(type)) {
mPreparedStatementCache.put(sql, statement);
statement.mInCache = true;
}
} catch (RuntimeException ex) {
// Finalize the statement if an exception occurred and we did not add
// it to the cache. If it is already in the cache, then leave it there.
if (statement == null || !statement.mInCache) {
nativeFinalizeStatement(mConnectionPtr, statementPtr);
}
throw ex;
}
statement.mInUse = true;
return statement;
}
private void releasePreparedStatement(PreparedStatement statement) {
statement.mInUse = false;
if (statement.mInCache) {
try {
nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr);
} catch (SQLiteException ex) {
// The statement could not be reset due to an error. Remove it from the cache.
// When remove() is called, the cache will invoke its entryRemoved() callback,
// which will in turn call finalizePreparedStatement() to finalize and
// recycle the statement.
if (DEBUG) {
Log.d(TAG, "Could not reset prepared statement due to an exception. "
+ "Removing it from the cache. SQL: "
+ trimSqlForDisplay(statement.mSql), ex);
}
mPreparedStatementCache.remove(statement.mSql);
}
} else {
finalizePreparedStatement(statement);
}
}
private void finalizePreparedStatement(PreparedStatement statement) {
nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr);
recyclePreparedStatement(statement);
}
private void attachCancellationSignal(CancellationSignal cancellationSignal) {
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
mCancellationSignalAttachCount += 1;
if (mCancellationSignalAttachCount == 1) {
// Reset cancellation flag before executing the statement.
nativeResetCancel(mConnectionPtr, true /*cancelable*/);
// After this point, onCancel() may be called concurrently.
cancellationSignal.setOnCancelListener(this);
}
}
}
private void detachCancellationSignal(CancellationSignal cancellationSignal) {
if (cancellationSignal != null) {
assert mCancellationSignalAttachCount > 0;
mCancellationSignalAttachCount -= 1;
if (mCancellationSignalAttachCount == 0) {
// After this point, onCancel() cannot be called concurrently.
cancellationSignal.setOnCancelListener(null);
// Reset cancellation flag after executing the statement.
nativeResetCancel(mConnectionPtr, false /*cancelable*/);
}
}
}
// CancellationSignal.OnCancelListener callback.
// This method may be called on a different thread than the executing statement.
// However, it will only be called between calls to attachCancellationSignal and
// detachCancellationSignal, while a statement is executing. We can safely assume
// that the SQLite connection is still alive.
@Override
public void onCancel() {
nativeCancel(mConnectionPtr);
}
private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
final int count = bindArgs != null ? bindArgs.length : 0;
if (count != statement.mNumParameters) {
throw new SQLiteBindOrColumnIndexOutOfRangeException(
"Expected " + statement.mNumParameters + " bind arguments but "
+ count + " were provided.");
}
if (count == 0) {
return;
}
final long statementPtr = statement.mStatementPtr;
for (int i = 0; i < count; i++) {
final Object arg = bindArgs[i];
switch (DatabaseUtils.getTypeOfObject(arg)) {
case Cursor.FIELD_TYPE_NULL:
nativeBindNull(mConnectionPtr, statementPtr, i + 1);
break;
case Cursor.FIELD_TYPE_INTEGER:
nativeBindLong(mConnectionPtr, statementPtr, i + 1,
((Number)arg).longValue());
break;
case Cursor.FIELD_TYPE_FLOAT:
nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
((Number)arg).doubleValue());
break;
case Cursor.FIELD_TYPE_BLOB:
nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
break;
case Cursor.FIELD_TYPE_STRING:
default:
if (arg instanceof Boolean) {
// Provide compatibility with legacy applications which may pass
// Boolean values in bind args.
nativeBindLong(mConnectionPtr, statementPtr, i + 1,
((Boolean)arg).booleanValue() ? 1 : 0);
} else {
nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
}
break;
}
}
}
private void throwIfStatementForbidden(PreparedStatement statement) {
if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) {
throw new SQLiteException("Cannot execute this statement because it "
+ "might modify the database but the connection is read-only.");
}
}
private static boolean isCacheable(int statementType) {
if (statementType == DatabaseUtils.STATEMENT_UPDATE
|| statementType == DatabaseUtils.STATEMENT_SELECT) {
return true;
}
return false;
}
private void applyBlockGuardPolicy(PreparedStatement statement) {
if (!mConfiguration.isInMemoryDb()) {
if (statement.mReadOnly) {
BlockGuard.getThreadPolicy().onReadFromDisk();
} else {
BlockGuard.getThreadPolicy().onWriteToDisk();
}
}
}
/**
* Dumps debugging information about this connection.
*
* @param printer The printer to receive the dump, not null.
* @param verbose True to dump more verbose information.
*/
public void dump(Printer printer, boolean verbose) {
dumpUnsafe(printer, verbose);
}
/**
* Dumps debugging information about this connection, in the case where the
* caller might not actually own the connection.
*
* This function is written so that it may be called by a thread that does not
* own the connection. We need to be very careful because the connection state is
* not synchronized.
*
* At worst, the method may return stale or slightly wrong data, however
* it should not crash. This is ok as it is only used for diagnostic purposes.
*
* @param printer The printer to receive the dump, not null.
* @param verbose True to dump more verbose information.
*/
void dumpUnsafe(Printer printer, boolean verbose) {
printer.println("Connection #" + mConnectionId + ":");
if (verbose) {
printer.println(" connectionPtr: 0x" + Long.toHexString(mConnectionPtr));
}
printer.println(" isPrimaryConnection: " + mIsPrimaryConnection);
printer.println(" onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations);
mRecentOperations.dump(printer, verbose);
if (verbose) {
mPreparedStatementCache.dump(printer);
}
}
/**
* Describes the currently executing operation, in the case where the
* caller might not actually own the connection.
*
* This function is written so that it may be called by a thread that does not
* own the connection. We need to be very careful because the connection state is
* not synchronized.
*
* At worst, the method may return stale or slightly wrong data, however
* it should not crash. This is ok as it is only used for diagnostic purposes.
*
* @return A description of the current operation including how long it has been running,
* or null if none.
*/
String describeCurrentOperationUnsafe() {
return mRecentOperations.describeCurrentOperation();
}
/**
* Collects statistics about database connection memory usage.
*
* @param dbStatsList The list to populate.
*/
void collectDbStats(ArrayList