blob: 24b62b89454dd0e177232e504ba848716555d9d0 [file] [log] [blame]
/*
* 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 android.compat.annotation.UnsupportedAppUsage;
import android.database.CursorWindow;
import android.database.DatabaseUtils;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.os.ParcelFileDescriptor;
/**
* Provides a single client the ability to use a database.
*
* <h2>About database sessions</h2>
* <p>
* Database access is always performed using a session. The session
* manages the lifecycle of transactions and database connections.
* </p><p>
* Sessions can be used to perform both read-only and read-write operations.
* There is some advantage to knowing when a session is being used for
* read-only purposes because the connection pool can optimize the use
* of the available connections to permit multiple read-only operations
* to execute in parallel whereas read-write operations may need to be serialized.
* </p><p>
* When <em>Write Ahead Logging (WAL)</em> is enabled, the database can
* execute simultaneous read-only and read-write transactions, provided that
* at most one read-write transaction is performed at a time. When WAL is not
* enabled, read-only transactions can execute in parallel but read-write
* transactions are mutually exclusive.
* </p>
*
* <h2>Ownership and concurrency guarantees</h2>
* <p>
* Session objects are not thread-safe. In fact, session objects are thread-bound.
* The {@link SQLiteDatabase} uses a thread-local variable to associate a session
* with each thread for the use of that thread alone. Consequently, each thread
* has its own session object and therefore its own transaction state independent
* of other threads.
* </p><p>
* A thread has at most one session per database. This constraint ensures that
* a thread can never use more than one database connection at a time for a
* given database. As the number of available database connections is limited,
* if a single thread tried to acquire multiple connections for the same database
* at the same time, it might deadlock. Therefore we allow there to be only
* one session (so, at most one connection) per thread per database.
* </p>
*
* <h2>Transactions</h2>
* <p>
* There are two kinds of transaction: implicit transactions and explicit
* transactions.
* </p><p>
* An implicit transaction is created whenever a database operation is requested
* and there is no explicit transaction currently in progress. An implicit transaction
* only lasts for the duration of the database operation in question and then it
* is ended. If the database operation was successful, then its changes are committed.
* </p><p>
* An explicit transaction is started by calling {@link #beginTransaction} and
* specifying the desired transaction mode. Once an explicit transaction has begun,
* all subsequent database operations will be performed as part of that transaction.
* To end an explicit transaction, first call {@link #setTransactionSuccessful} if the
* transaction was successful, then call {@link #end}. If the transaction was
* marked successful, its changes will be committed, otherwise they will be rolled back.
* </p><p>
* Explicit transactions can also be nested. A nested explicit transaction is
* started with {@link #beginTransaction}, marked successful with
* {@link #setTransactionSuccessful}and ended with {@link #endTransaction}.
* If any nested transaction is not marked successful, then the entire transaction
* including all of its nested transactions will be rolled back
* when the outermost transaction is ended.
* </p><p>
* To improve concurrency, an explicit transaction can be yielded by calling
* {@link #yieldTransaction}. If there is contention for use of the database,
* then yielding ends the current transaction, commits its changes, releases the
* database connection for use by another session for a little while, and starts a
* new transaction with the same properties as the original one.
* Changes committed by {@link #yieldTransaction} cannot be rolled back.
* </p><p>
* When a transaction is started, the client can provide a {@link SQLiteTransactionListener}
* to listen for notifications of transaction-related events.
* </p><p>
* Recommended usage:
* <code><pre>
* // First, begin the transaction.
* session.beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, 0);
* try {
* // Then do stuff...
* session.execute("INSERT INTO ...", null, 0);
*
* // As the very last step before ending the transaction, mark it successful.
* session.setTransactionSuccessful();
* } finally {
* // Finally, end the transaction.
* // This statement will commit the transaction if it was marked successful or
* // roll it back otherwise.
* session.endTransaction();
* }
* </pre></code>
* </p>
*
* <h2>Database connections</h2>
* <p>
* A {@link SQLiteDatabase} can have multiple active sessions at the same
* time. Each session acquires and releases connections to the database
* as needed to perform each requested database transaction. If all connections
* are in use, then database transactions on some sessions will block until a
* connection becomes available.
* </p><p>
* The session acquires a single database connection only for the duration
* of a single (implicit or explicit) database transaction, then releases it.
* This characteristic allows a small pool of database connections to be shared
* efficiently by multiple sessions as long as they are not all trying to perform
* database transactions at the same time.
* </p>
*
* <h2>Responsiveness</h2>
* <p>
* Because there are a limited number of database connections and the session holds
* a database connection for the entire duration of a database transaction,
* it is important to keep transactions short. This is especially important
* for read-write transactions since they may block other transactions
* from executing. Consider calling {@link #yieldTransaction} periodically
* during long-running transactions.
* </p><p>
* Another important consideration is that transactions that take too long to
* run may cause the application UI to become unresponsive. Even if the transaction
* is executed in a background thread, the user will get bored and
* frustrated if the application shows no data for several seconds while
* a transaction runs.
* </p><p>
* Guidelines:
* <ul>
* <li>Do not perform database transactions on the UI thread.</li>
* <li>Keep database transactions as short as possible.</li>
* <li>Simple queries often run faster than complex queries.</li>
* <li>Measure the performance of your database transactions.</li>
* <li>Consider what will happen when the size of the data set grows.
* A query that works well on 100 rows may struggle with 10,000.</li>
* </ul>
*
* <h2>Reentrance</h2>
* <p>
* This class must tolerate reentrant execution of SQLite operations because
* triggers may call custom SQLite functions that perform additional queries.
* </p>
*
* @hide
*/
public final class SQLiteSession {
private final SQLiteConnectionPool mConnectionPool;
private SQLiteConnection mConnection;
private int mConnectionFlags;
private int mConnectionUseCount;
private Transaction mTransactionPool;
private Transaction mTransactionStack;
/**
* Transaction mode: Deferred.
* <p>
* In a deferred transaction, no locks are acquired on the database
* until the first operation is performed. If the first operation is
* read-only, then a <code>SHARED</code> lock is acquired, otherwise
* a <code>RESERVED</code> lock is acquired.
* </p><p>
* While holding a <code>SHARED</code> lock, this session is only allowed to
* read but other sessions are allowed to read or write.
* While holding a <code>RESERVED</code> lock, this session is allowed to read
* or write but other sessions are only allowed to read.
* </p><p>
* Because the lock is only acquired when needed in a deferred transaction,
* it is possible for another session to write to the database first before
* this session has a chance to do anything.
* </p><p>
* Corresponds to the SQLite <code>BEGIN DEFERRED</code> transaction mode.
* </p>
*/
public static final int TRANSACTION_MODE_DEFERRED = 0;
/**
* Transaction mode: Immediate.
* <p>
* When an immediate transaction begins, the session acquires a
* <code>RESERVED</code> lock.
* </p><p>
* While holding a <code>RESERVED</code> lock, this session is allowed to read
* or write but other sessions are only allowed to read.
* </p><p>
* Corresponds to the SQLite <code>BEGIN IMMEDIATE</code> transaction mode.
* </p>
*/
public static final int TRANSACTION_MODE_IMMEDIATE = 1;
/**
* Transaction mode: Exclusive.
* <p>
* When an exclusive transaction begins, the session acquires an
* <code>EXCLUSIVE</code> lock.
* </p><p>
* While holding an <code>EXCLUSIVE</code> lock, this session is allowed to read
* or write but no other sessions are allowed to access the database.
* </p><p>
* Corresponds to the SQLite <code>BEGIN EXCLUSIVE</code> transaction mode.
* </p>
*/
public static final int TRANSACTION_MODE_EXCLUSIVE = 2;
/**
* Creates a session bound to the specified connection pool.
*
* @param connectionPool The connection pool.
*/
public SQLiteSession(SQLiteConnectionPool connectionPool) {
if (connectionPool == null) {
throw new IllegalArgumentException("connectionPool must not be null");
}
mConnectionPool = connectionPool;
}
/**
* Returns true if the session has a transaction in progress.
*
* @return True if the session has a transaction in progress.
*/
public boolean hasTransaction() {
return mTransactionStack != null;
}
/**
* Returns true if the session has a nested transaction in progress.
*
* @return True if the session has a nested transaction in progress.
*/
public boolean hasNestedTransaction() {
return mTransactionStack != null && mTransactionStack.mParent != null;
}
/**
* Returns true if the session has an active database connection.
*
* @return True if the session has an active database connection.
*/
public boolean hasConnection() {
return mConnection != null;
}
/**
* Begins a transaction.
* <p>
* Transactions may nest. If the transaction is not in progress,
* then a database connection is obtained and a new transaction is started.
* Otherwise, a nested transaction is started.
* </p><p>
* Each call to {@link #beginTransaction} must be matched exactly by a call
* to {@link #endTransaction}. To mark a transaction as successful,
* call {@link #setTransactionSuccessful} before calling {@link #endTransaction}.
* If the transaction is not successful, or if any of its nested
* transactions were not successful, then the entire transaction will
* be rolled back when the outermost transaction is ended.
* </p>
*
* @param transactionMode The transaction mode. One of: {@link #TRANSACTION_MODE_DEFERRED},
* {@link #TRANSACTION_MODE_IMMEDIATE}, or {@link #TRANSACTION_MODE_EXCLUSIVE}.
* Ignored when creating a nested transaction.
* @param transactionListener The transaction listener, or null if none.
* @param connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
*
* @throws IllegalStateException if {@link #setTransactionSuccessful} has already been
* called for the current transaction.
* @throws SQLiteException if an error occurs.
* @throws OperationCanceledException if the operation was canceled.
*
* @see #setTransactionSuccessful
* @see #yieldTransaction
* @see #endTransaction
*/
@UnsupportedAppUsage
public void beginTransaction(int transactionMode,
SQLiteTransactionListener transactionListener, int connectionFlags,
CancellationSignal cancellationSignal) {
throwIfTransactionMarkedSuccessful();
beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags,
cancellationSignal);
}
private void beginTransactionUnchecked(int transactionMode,
SQLiteTransactionListener transactionListener, int connectionFlags,
CancellationSignal cancellationSignal) {
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
}
if (mTransactionStack == null) {
acquireConnection(null, connectionFlags, cancellationSignal); // might throw
}
try {
// Set up the transaction such that we can back out safely
// in case we fail part way.
if (mTransactionStack == null) {
// Execute SQL might throw a runtime exception.
switch (transactionMode) {
case TRANSACTION_MODE_IMMEDIATE:
mConnection.execute("BEGIN IMMEDIATE;", null,
cancellationSignal); // might throw
break;
case TRANSACTION_MODE_EXCLUSIVE:
mConnection.execute("BEGIN EXCLUSIVE;", null,
cancellationSignal); // might throw
break;
default:
mConnection.execute("BEGIN;", null, cancellationSignal); // might throw
break;
}
}
// Listener might throw a runtime exception.
if (transactionListener != null) {
try {
transactionListener.onBegin(); // might throw
} catch (RuntimeException ex) {
if (mTransactionStack == null) {
mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw
}
throw ex;
}
}
// Bookkeeping can't throw, except an OOM, which is just too bad...
Transaction transaction = obtainTransaction(transactionMode, transactionListener);
transaction.mParent = mTransactionStack;
mTransactionStack = transaction;
} finally {
if (mTransactionStack == null) {
releaseConnection(); // might throw
}
}
}
/**
* Marks the current transaction as having completed successfully.
* <p>
* This method can be called at most once between {@link #beginTransaction} and
* {@link #endTransaction} to indicate that the changes made by the transaction should be
* committed. If this method is not called, the changes will be rolled back
* when the transaction is ended.
* </p>
*
* @throws IllegalStateException if there is no current transaction, or if
* {@link #setTransactionSuccessful} has already been called for the current transaction.
*
* @see #beginTransaction
* @see #endTransaction
*/
public void setTransactionSuccessful() {
throwIfNoTransaction();
throwIfTransactionMarkedSuccessful();
mTransactionStack.mMarkedSuccessful = true;
}
/**
* Ends the current transaction and commits or rolls back changes.
* <p>
* If this is the outermost transaction (not nested within any other
* transaction), then the changes are committed if {@link #setTransactionSuccessful}
* was called or rolled back otherwise.
* </p><p>
* This method must be called exactly once for each call to {@link #beginTransaction}.
* </p>
*
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
*
* @throws IllegalStateException if there is no current transaction.
* @throws SQLiteException if an error occurs.
* @throws OperationCanceledException if the operation was canceled.
*
* @see #beginTransaction
* @see #setTransactionSuccessful
* @see #yieldTransaction
*/
public void endTransaction(CancellationSignal cancellationSignal) {
throwIfNoTransaction();
assert mConnection != null;
endTransactionUnchecked(cancellationSignal, false);
}
private void endTransactionUnchecked(CancellationSignal cancellationSignal, boolean yielding) {
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
}
final Transaction top = mTransactionStack;
boolean successful = (top.mMarkedSuccessful || yielding) && !top.mChildFailed;
RuntimeException listenerException = null;
final SQLiteTransactionListener listener = top.mListener;
if (listener != null) {
try {
if (successful) {
listener.onCommit(); // might throw
} else {
listener.onRollback(); // might throw
}
} catch (RuntimeException ex) {
listenerException = ex;
successful = false;
}
}
mTransactionStack = top.mParent;
recycleTransaction(top);
if (mTransactionStack != null) {
if (!successful) {
mTransactionStack.mChildFailed = true;
}
} else {
try {
if (successful) {
mConnection.execute("COMMIT;", null, cancellationSignal); // might throw
} else {
mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw
}
} finally {
releaseConnection(); // might throw
}
}
if (listenerException != null) {
throw listenerException;
}
}
/**
* Temporarily ends a transaction to let other threads have use of
* the database. Begins a new transaction after a specified delay.
* <p>
* If there are other threads waiting to acquire connections,
* then the current transaction is committed and the database
* connection is released. After a short delay, a new transaction
* is started.
* </p><p>
* The transaction is assumed to be successful so far. Do not call
* {@link #setTransactionSuccessful()} before calling this method.
* This method will fail if the transaction has already been marked
* successful.
* </p><p>
* The changes that were committed by a yield cannot be rolled back later.
* </p><p>
* Before this method was called, there must already have been
* a transaction in progress. When this method returns, there will
* still be a transaction in progress, either the same one as before
* or a new one if the transaction was actually yielded.
* </p><p>
* This method should not be called when there is a nested transaction
* in progress because it is not possible to yield a nested transaction.
* If <code>throwIfNested</code> is true, then attempting to yield
* a nested transaction will throw {@link IllegalStateException}, otherwise
* the method will return <code>false</code> in that case.
* </p><p>
* If there is no nested transaction in progress but a previous nested
* transaction failed, then the transaction is not yielded (because it
* must be rolled back) and this method returns <code>false</code>.
* </p>
*
* @param sleepAfterYieldDelayMillis A delay time to wait after yielding
* the database connection to allow other threads some time to run.
* If the value is less than or equal to zero, there will be no additional
* delay beyond the time it will take to begin a new transaction.
* @param throwIfUnsafe If true, then instead of returning false when no
* transaction is in progress, a nested transaction is in progress, or when
* the transaction has already been marked successful, throws {@link IllegalStateException}.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return True if the transaction was actually yielded.
*
* @throws IllegalStateException if <code>throwIfNested</code> is true and
* there is no current transaction, there is a nested transaction in progress or
* if {@link #setTransactionSuccessful} has already been called for the current transaction.
* @throws SQLiteException if an error occurs.
* @throws OperationCanceledException if the operation was canceled.
*
* @see #beginTransaction
* @see #endTransaction
*/
public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe,
CancellationSignal cancellationSignal) {
if (throwIfUnsafe) {
throwIfNoTransaction();
throwIfTransactionMarkedSuccessful();
throwIfNestedTransaction();
} else {
if (mTransactionStack == null || mTransactionStack.mMarkedSuccessful
|| mTransactionStack.mParent != null) {
return false;
}
}
assert mConnection != null;
if (mTransactionStack.mChildFailed) {
return false;
}
return yieldTransactionUnchecked(sleepAfterYieldDelayMillis,
cancellationSignal); // might throw
}
private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis,
CancellationSignal cancellationSignal) {
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
}
if (!mConnectionPool.shouldYieldConnection(mConnection, mConnectionFlags)) {
return false;
}
final int transactionMode = mTransactionStack.mMode;
final SQLiteTransactionListener listener = mTransactionStack.mListener;
final int connectionFlags = mConnectionFlags;
endTransactionUnchecked(cancellationSignal, true); // might throw
if (sleepAfterYieldDelayMillis > 0) {
try {
Thread.sleep(sleepAfterYieldDelayMillis);
} catch (InterruptedException ex) {
// we have been interrupted, that's all we need to do
}
}
beginTransactionUnchecked(transactionMode, listener, connectionFlags,
cancellationSignal); // might throw
return true;
}
/**
* Prepares a statement for execution but does not bind its parameters or execute it.
* <p>
* 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.
* </p><p>
* 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 and reused if possible.
* </p>
*
* @param sql The SQL statement to prepare.
* @param connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @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.
* @throws OperationCanceledException if the operation was canceled.
*/
public void prepare(String sql, int connectionFlags, CancellationSignal cancellationSignal,
SQLiteStatementInfo outStatementInfo) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
mConnection.prepare(sql, outStatementInfo); // might throw
} finally {
releaseConnection(); // might throw
}
}
/**
* 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 connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @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, int connectionFlags,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
return;
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
mConnection.execute(sql, bindArgs, cancellationSignal); // might throw
} finally {
releaseConnection(); // might throw
}
}
/**
* Executes a statement that returns a single <code>long</code> result.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @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 <code>long</code>, 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, int connectionFlags,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
return 0;
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
return mConnection.executeForLong(sql, bindArgs, cancellationSignal); // might throw
} finally {
releaseConnection(); // might throw
}
}
/**
* 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 connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @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 <code>String</code>, 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, int connectionFlags,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
return null;
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
return mConnection.executeForString(sql, bindArgs, cancellationSignal); // might throw
} finally {
releaseConnection(); // might throw
}
}
/**
* 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 connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @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,
int connectionFlags, CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
return null;
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
return mConnection.executeForBlobFileDescriptor(sql, bindArgs,
cancellationSignal); // might throw
} finally {
releaseConnection(); // might throw
}
}
/**
* 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 connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @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, int connectionFlags,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
return 0;
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
return mConnection.executeForChangedRowCount(sql, bindArgs,
cancellationSignal); // might throw
} finally {
releaseConnection(); // might throw
}
}
/**
* 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 connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @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, int connectionFlags,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
return 0;
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
return mConnection.executeForLastInsertedRowId(sql, bindArgs,
cancellationSignal); // might throw
} finally {
releaseConnection(); // might throw
}
}
/**
* 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 <code>startPos</code>.
* @param countAllRows True to count all rows that the query would return
* regagless of whether they fit in the window.
* @param connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @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 <code>countAllRows</code> 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,
int connectionFlags, CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
if (window == null) {
throw new IllegalArgumentException("window must not be null.");
}
if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
window.clear();
return 0;
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
return mConnection.executeForCursorWindow(sql, bindArgs,
window, startPos, requiredPos, countAllRows,
cancellationSignal); // might throw
} finally {
releaseConnection(); // might throw
}
}
/**
* Performs special reinterpretation of certain SQL statements such as "BEGIN",
* "COMMIT" and "ROLLBACK" to ensure that transaction state invariants are
* maintained.
*
* This function is mainly used to support legacy apps that perform their
* own transactions by executing raw SQL rather than calling {@link #beginTransaction}
* and the like.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return True if the statement was of a special form that was handled here,
* false otherwise.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags,
CancellationSignal cancellationSignal) {
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
}
final int type = DatabaseUtils.getSqlStatementType(sql);
switch (type) {
case DatabaseUtils.STATEMENT_BEGIN:
beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags,
cancellationSignal);
return true;
case DatabaseUtils.STATEMENT_COMMIT:
setTransactionSuccessful();
endTransaction(cancellationSignal);
return true;
case DatabaseUtils.STATEMENT_ABORT:
endTransaction(cancellationSignal);
return true;
}
return false;
}
private void acquireConnection(String sql, int connectionFlags,
CancellationSignal cancellationSignal) {
if (mConnection == null) {
assert mConnectionUseCount == 0;
mConnection = mConnectionPool.acquireConnection(sql, connectionFlags,
cancellationSignal); // might throw
mConnectionFlags = connectionFlags;
}
mConnectionUseCount += 1;
}
private void releaseConnection() {
assert mConnection != null;
assert mConnectionUseCount > 0;
if (--mConnectionUseCount == 0) {
try {
mConnectionPool.releaseConnection(mConnection); // might throw
} finally {
mConnection = null;
}
}
}
private void throwIfNoTransaction() {
if (mTransactionStack == null) {
throw new IllegalStateException("Cannot perform this operation because "
+ "there is no current transaction.");
}
}
private void throwIfTransactionMarkedSuccessful() {
if (mTransactionStack != null && mTransactionStack.mMarkedSuccessful) {
throw new IllegalStateException("Cannot perform this operation because "
+ "the transaction has already been marked successful. The only "
+ "thing you can do now is call endTransaction().");
}
}
private void throwIfNestedTransaction() {
if (hasNestedTransaction()) {
throw new IllegalStateException("Cannot perform this operation because "
+ "a nested transaction is in progress.");
}
}
private Transaction obtainTransaction(int mode, SQLiteTransactionListener listener) {
Transaction transaction = mTransactionPool;
if (transaction != null) {
mTransactionPool = transaction.mParent;
transaction.mParent = null;
transaction.mMarkedSuccessful = false;
transaction.mChildFailed = false;
} else {
transaction = new Transaction();
}
transaction.mMode = mode;
transaction.mListener = listener;
return transaction;
}
private void recycleTransaction(Transaction transaction) {
transaction.mParent = mTransactionPool;
transaction.mListener = null;
mTransactionPool = transaction;
}
private static final class Transaction {
public Transaction mParent;
public int mMode;
public SQLiteTransactionListener mListener;
public boolean mMarkedSuccessful;
public boolean mChildFailed;
}
}