diff options
author | Jeff Brown <jeffbrown@google.com> | 2011-10-31 17:48:13 -0700 |
---|---|---|
committer | Jeff Brown <jeffbrown@google.com> | 2012-01-12 14:56:18 -0800 |
commit | e5360fbf3efe85427f7e7f59afe7bbeddb4949ac (patch) | |
tree | 53d32881de72a9b4b018f01dade23373cb65cc88 /core/java/android/database/sqlite | |
parent | 3f11f302c7e8c4ba513f6c559f33db96319c3825 (diff) | |
download | frameworks_base-e5360fbf3efe85427f7e7f59afe7bbeddb4949ac.zip frameworks_base-e5360fbf3efe85427f7e7f59afe7bbeddb4949ac.tar.gz frameworks_base-e5360fbf3efe85427f7e7f59afe7bbeddb4949ac.tar.bz2 |
Rewrite SQLite database wrappers.
The main theme of this change is encapsulation. This change
preserves all existing functionality but the implementation
is now much cleaner.
Instead of a "database lock", access to the database is treated
as a resource acquisition problem. If a thread's owns a database
connection, then it can access the database; otherwise, it must
acquire a database connection first, and potentially wait for other
threads to give up theirs. The SQLiteConnectionPool encapsulates
the details of how connections are created, configured, acquired,
released and disposed.
One new feature is that SQLiteConnectionPool can make scheduling
decisions about which thread should next acquire a database
connection when there is contention among threads. The factors
considered include wait queue ordering (fairness among peers),
whether the connection is needed for an interactive operation
(unfairness on behalf of the UI), and whether the primary connection
is needed or if any old connection will do. Thus one goal of the
new SQLiteConnectionPool is to improve the utilization of
database connections.
To emulate some quirks of the old "database lock," we introduce
the concept of the primary database connection. The primary
database connection is the one that is typically used to perform
write operations to the database. When a thread holds the primary
database connection, it effectively prevents other threads from
modifying the database (although they can still read). What's
more, those threads will block when they try to acquire the primary
connection, which provides the same kind of mutual exclusion
features that the old "database lock" had. (In truth, we
probably don't need to be requiring use of the primary database
connection in as many places as we do now, but we can seek to refine
that behavior in future patches.)
Another significant change is that native sqlite3_stmt objects
(prepared statements) are fully encapsulated by the SQLiteConnection
object that owns them. This ensures that the connection can
finalize (destroy) all extant statements that belong to a database
connection when the connection is closed. (In the original code,
this was very complicated because the sqlite3_stmt objects were
managed by SQLiteCompiledSql objects which had different lifetime
from the original SQLiteDatabase that created them. Worse, the
SQLiteCompiledSql finalizer method couldn't actually destroy the
sqlite3_stmt objects because it ran on the finalizer thread and
therefore could not guarantee that it could acquire the database
lock in order to do the work. This resulted in some rather
tortured logic involving a list of pending finalizable statements
and a high change of deadlocks or leaks.)
Because sqlite3_stmt objects never escape the confines of the
SQLiteConnection that owns them, we can also greatly simplify
the design of the SQLiteProgram, SQLiteQuery and SQLiteStatement
objects. They no longer have to wrangle a native sqlite3_stmt
object pointer and manage its lifecycle. So now all they do
is hold bind arguments and provide a fancy API.
All of the JNI glue related to managing database connections
and performing transactions is now bound to SQLiteConnection
(rather than being scattered everywhere). This makes sense because
SQLiteConnection owns the native sqlite3 object, so it is the
only class in the system that can interact with the native
SQLite database directly. Encapsulation for the win.
One particularly tricky part of this change is managing the
ownership of SQLiteConnection objects. At any given time,
a SQLiteConnection is either owned by a SQLiteConnectionPool
or by a SQLiteSession. SQLiteConnections should never be leaked,
but we handle that case too (and yell about it with CloseGuard).
A SQLiteSession object is responsible for acquiring and releasing
a SQLiteConnection object on behalf of a single thread as needed.
For example, the session acquires a connection when a transaction
begins and releases it when finished. If the session cannot
acquire a connection immediately, then the requested operation
blocks until a connection becomes available.
SQLiteSessions are thread-local. A SQLiteDatabase assigns a
distinct session to each thread that performs database operations.
This is very very important. First, it prevents two threads
from trying to use the same SQLiteConnection at the same time
(because two threads can't share the same session).
Second, it prevents a single thread from trying to acquire two
SQLiteConnections simultaneously from the same database (because
a single thread can't have two sessions for the same database which,
in addition to being greedy, could result in a deadlock).
There is strict layering between the various database objects,
objects at lower layers are not aware of objects at higher layers.
Moreover, objects at higher layers generally own objects at lower
layers and are responsible for ensuring they are properly disposed
when no longer needed (good for the environment).
API layer: SQLiteDatabase, SQLiteProgram, SQLiteQuery, SQLiteStatement.
Session layer: SQLiteSession.
Connection layer: SQLiteConnectionPool, SQLiteConnection.
Native layer: JNI glue.
By avoiding cyclic dependencies between layers, we make the
architecture much more intelligible, maintainable and robust.
Finally, this change adds a great deal of new debugging information.
It is now possible to view a list of the most recent database
operations including how long they took to run using
"adb shell dumpsys dbinfo". (Because most of the interesting
work happens in SQLiteConnection, it is easy to add debugging
instrumentation to track all database operations in one place.)
Change-Id: Iffb4ce72d8bcf20b4e087d911da6aa84d2f15297
Diffstat (limited to 'core/java/android/database/sqlite')
20 files changed, 3969 insertions, 2566 deletions
diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java deleted file mode 100644 index 39a9d23..0000000 --- a/core/java/android/database/sqlite/DatabaseConnectionPool.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright (C) 20010 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.content.res.Resources; -import android.os.SystemClock; -import android.util.Log; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Random; - -/** - * A connection pool to be used by readers. - * Note that each connection can be used by only one reader at a time. - */ -/* package */ class DatabaseConnectionPool { - - private static final String TAG = "DatabaseConnectionPool"; - - /** The default connection pool size. */ - private volatile int mMaxPoolSize = - Resources.getSystem().getInteger(com.android.internal.R.integer.db_connection_pool_size); - - /** The connection pool objects are stored in this member. - * TODO: revisit this data struct as the number of pooled connections increase beyond - * single-digit values. - */ - private final ArrayList<PoolObj> mPool = new ArrayList<PoolObj>(mMaxPoolSize); - - /** the main database connection to which this connection pool is attached */ - private final SQLiteDatabase mParentDbObj; - - /** Random number generator used to pick a free connection out of the pool */ - private Random rand; // lazily initialized - - /* package */ DatabaseConnectionPool(SQLiteDatabase db) { - this.mParentDbObj = db; - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Max Pool Size: " + mMaxPoolSize); - } - } - - /** - * close all database connections in the pool - even if they are in use! - */ - /* package */ synchronized void close() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Closing the connection pool on " + mParentDbObj.getPath() + toString()); - } - for (int i = mPool.size() - 1; i >= 0; i--) { - mPool.get(i).mDb.close(); - } - mPool.clear(); - } - - /** - * get a free connection from the pool - * - * @param sql if not null, try to find a connection inthe pool which already has cached - * the compiled statement for this sql. - * @return the Database connection that the caller can use - */ - /* package */ synchronized SQLiteDatabase get(String sql) { - SQLiteDatabase db = null; - PoolObj poolObj = null; - int poolSize = mPool.size(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert sql != null; - doAsserts(); - } - if (getFreePoolSize() == 0) { - // no free ( = available) connections - if (mMaxPoolSize == poolSize) { - // maxed out. can't open any more connections. - // let the caller wait on one of the pooled connections - // preferably a connection caching the pre-compiled statement of the given SQL - if (mMaxPoolSize == 1) { - poolObj = mPool.get(0); - } else { - for (int i = 0; i < mMaxPoolSize; i++) { - if (mPool.get(i).mDb.isInStatementCache(sql)) { - poolObj = mPool.get(i); - break; - } - } - if (poolObj == null) { - // there are no database connections with the given SQL pre-compiled. - // ok to return any of the connections. - if (rand == null) { - rand = new Random(SystemClock.elapsedRealtime()); - } - poolObj = mPool.get(rand.nextInt(mMaxPoolSize)); - } - } - db = poolObj.mDb; - } else { - // create a new connection and add it to the pool, since we haven't reached - // max pool size allowed - db = mParentDbObj.createPoolConnection((short)(poolSize + 1)); - poolObj = new PoolObj(db); - mPool.add(poolSize, poolObj); - } - } else { - // there are free connections available. pick one - // preferably a connection caching the pre-compiled statement of the given SQL - for (int i = 0; i < poolSize; i++) { - if (mPool.get(i).isFree() && mPool.get(i).mDb.isInStatementCache(sql)) { - poolObj = mPool.get(i); - break; - } - } - if (poolObj == null) { - // didn't find a free database connection with the given SQL already - // pre-compiled. return a free connection (this means, the same SQL could be - // pre-compiled on more than one database connection. potential wasted memory.) - for (int i = 0; i < poolSize; i++) { - if (mPool.get(i).isFree()) { - poolObj = mPool.get(i); - break; - } - } - } - db = poolObj.mDb; - } - - assert poolObj != null; - assert poolObj.mDb == db; - - poolObj.acquire(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "END get-connection: " + toString() + poolObj.toString()); - } - return db; - // TODO if a thread acquires a connection and dies without releasing the connection, then - // there could be a connection leak. - } - - /** - * release the given database connection back to the pool. - * @param db the connection to be released - */ - /* package */ synchronized void release(SQLiteDatabase db) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert db.mConnectionNum > 0; - doAsserts(); - assert mPool.get(db.mConnectionNum - 1).mDb == db; - } - - PoolObj poolObj = mPool.get(db.mConnectionNum - 1); - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "BEGIN release-conn: " + toString() + poolObj.toString()); - } - - if (poolObj.isFree()) { - throw new IllegalStateException("Releasing object already freed: " + - db.mConnectionNum); - } - - poolObj.release(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "END release-conn: " + toString() + poolObj.toString()); - } - } - - /** - * Returns a list of all database connections in the pool (both free and busy connections). - * This method is used when "adb bugreport" is done. - */ - /* package */ synchronized ArrayList<SQLiteDatabase> getConnectionList() { - ArrayList<SQLiteDatabase> list = new ArrayList<SQLiteDatabase>(); - for (int i = mPool.size() - 1; i >= 0; i--) { - list.add(mPool.get(i).mDb); - } - return list; - } - - /** - * package level access for testing purposes only. otherwise, private should be sufficient. - */ - /* package */ int getFreePoolSize() { - int count = 0; - for (int i = mPool.size() - 1; i >= 0; i--) { - if (mPool.get(i).isFree()) { - count++; - } - } - return count++; - } - - /** - * only for testing purposes - */ - /* package */ ArrayList<PoolObj> getPool() { - return mPool; - } - - @Override - public String toString() { - StringBuilder buff = new StringBuilder(); - buff.append("db: "); - buff.append(mParentDbObj.getPath()); - buff.append(", totalsize = "); - buff.append(mPool.size()); - buff.append(", #free = "); - buff.append(getFreePoolSize()); - buff.append(", maxpoolsize = "); - buff.append(mMaxPoolSize); - for (PoolObj p : mPool) { - buff.append("\n"); - buff.append(p.toString()); - } - return buff.toString(); - } - - private void doAsserts() { - for (int i = 0; i < mPool.size(); i++) { - mPool.get(i).verify(); - assert mPool.get(i).mDb.mConnectionNum == (i + 1); - } - } - - /** only used for testing purposes. */ - /* package */ synchronized void setMaxPoolSize(int size) { - mMaxPoolSize = size; - } - - /** only used for testing purposes. */ - /* package */ synchronized int getMaxPoolSize() { - return mMaxPoolSize; - } - - /** only used for testing purposes. */ - /* package */ boolean isDatabaseObjFree(SQLiteDatabase db) { - return mPool.get(db.mConnectionNum - 1).isFree(); - } - - /** only used for testing purposes. */ - /* package */ int getSize() { - return mPool.size(); - } - - /** - * represents objects in the connection pool. - * package-level access for testing purposes only. - */ - /* package */ static class PoolObj { - - private final SQLiteDatabase mDb; - private boolean mFreeBusyFlag = FREE; - private static final boolean FREE = true; - private static final boolean BUSY = false; - - /** the number of threads holding this connection */ - // @GuardedBy("this") - private int mNumHolders = 0; - - /** contains the threadIds of the threads holding this connection. - * used for debugging purposes only. - */ - // @GuardedBy("this") - private HashSet<Long> mHolderIds = new HashSet<Long>(); - - public PoolObj(SQLiteDatabase db) { - mDb = db; - } - - private synchronized void acquire() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert isFree(); - long id = Thread.currentThread().getId(); - assert !mHolderIds.contains(id); - mHolderIds.add(id); - } - - mNumHolders++; - mFreeBusyFlag = BUSY; - } - - private synchronized void release() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - long id = Thread.currentThread().getId(); - assert mHolderIds.size() == mNumHolders; - assert mHolderIds.contains(id); - mHolderIds.remove(id); - } - - mNumHolders--; - if (mNumHolders == 0) { - mFreeBusyFlag = FREE; - } - } - - private synchronized boolean isFree() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - verify(); - } - return (mFreeBusyFlag == FREE); - } - - private synchronized void verify() { - if (mFreeBusyFlag == FREE) { - assert mNumHolders == 0; - } else { - assert mNumHolders > 0; - } - } - - /** - * only for testing purposes - */ - /* package */ synchronized int getNumHolders() { - return mNumHolders; - } - - @Override - public String toString() { - StringBuilder buff = new StringBuilder(); - buff.append(", conn # "); - buff.append(mDb.mConnectionNum); - buff.append(", mCountHolders = "); - synchronized(this) { - buff.append(mNumHolders); - buff.append(", freeBusyFlag = "); - buff.append(mFreeBusyFlag); - for (Long l : mHolderIds) { - buff.append(", id = " + l); - } - } - return buff.toString(); - } - } -} diff --git a/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java index 01e9fb3..7e91a7b 100644 --- a/core/java/android/database/sqlite/SQLiteClosable.java +++ b/core/java/android/database/sqlite/SQLiteClosable.java @@ -16,8 +16,6 @@ package android.database.sqlite; -import android.database.CursorWindow; - /** * An object created from a SQLiteDatabase that can be closed. */ @@ -31,7 +29,7 @@ public abstract class SQLiteClosable { synchronized(this) { if (mReferenceCount <= 0) { throw new IllegalStateException( - "attempt to re-open an already-closed object: " + getObjInfo()); + "attempt to re-open an already-closed object: " + this); } mReferenceCount++; } @@ -56,22 +54,4 @@ public abstract class SQLiteClosable { onAllReferencesReleasedFromContainer(); } } - - private String getObjInfo() { - StringBuilder buff = new StringBuilder(); - buff.append(this.getClass().getName()); - buff.append(" ("); - if (this instanceof SQLiteDatabase) { - buff.append("database = "); - buff.append(((SQLiteDatabase)this).getPath()); - } else if (this instanceof SQLiteProgram) { - buff.append("mSql = "); - buff.append(((SQLiteProgram)this).mSql); - } else if (this instanceof CursorWindow) { - buff.append("mStartPos = "); - buff.append(((CursorWindow)this).getStartPosition()); - } - buff.append(") "); - return buff.toString(); - } } diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java deleted file mode 100644 index dafbc79..0000000 --- a/core/java/android/database/sqlite/SQLiteCompiledSql.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2009 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.os.StrictMode; -import android.util.Log; - -/** - * This class encapsulates compilation of sql statement and release of the compiled statement obj. - * Once a sql statement is compiled, it is cached in {@link SQLiteDatabase} - * and it is released in one of the 2 following ways - * 1. when {@link SQLiteDatabase} object is closed. - * 2. if this is not cached in {@link SQLiteDatabase}, {@link android.database.Cursor#close()} - * releaases this obj. - */ -/* package */ class SQLiteCompiledSql { - - private static final String TAG = "SQLiteCompiledSql"; - - /** The database this program is compiled against. */ - /* package */ final SQLiteDatabase mDatabase; - - /** - * Native linkage, do not modify. This comes from the database. - */ - /* package */ final int nHandle; - - /** - * Native linkage, do not modify. When non-0 this holds a reference to a valid - * sqlite3_statement object. It is only updated by the native code, but may be - * checked in this class when the database lock is held to determine if there - * is a valid native-side program or not. - */ - /* package */ int nStatement = 0; - - /** the following are for debugging purposes */ - private String mSqlStmt = null; - private final Throwable mStackTrace; - - /** when in cache and is in use, this member is set */ - private boolean mInUse = false; - - /* package */ SQLiteCompiledSql(SQLiteDatabase db, String sql) { - db.verifyDbIsOpen(); - db.verifyLockOwner(); - mDatabase = db; - mSqlStmt = sql; - if (StrictMode.vmSqliteObjectLeaksEnabled()) { - mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - } else { - mStackTrace = null; - } - nHandle = db.mNativeHandle; - native_compile(sql); - } - - /* package */ void releaseSqlStatement() { - // Note that native_finalize() checks to make sure that nStatement is - // non-null before destroying it. - if (nStatement != 0) { - mDatabase.finalizeStatementLater(nStatement); - nStatement = 0; - } - } - - /** - * returns true if acquire() succeeds. false otherwise. - */ - /* package */ synchronized boolean acquire() { - if (mInUse) { - // it is already in use. - return false; - } - mInUse = true; - return true; - } - - /* package */ synchronized void release() { - mInUse = false; - } - - /* package */ synchronized void releaseIfNotInUse() { - // if it is not in use, release its memory from the database - if (!mInUse) { - releaseSqlStatement(); - } - } - - /** - * Make sure that the native resource is cleaned up. - */ - @Override - protected void finalize() throws Throwable { - try { - if (nStatement == 0) return; - // don't worry about finalizing this object if it is ALREADY in the - // queue of statements to be finalized later - if (mDatabase.isInQueueOfStatementsToBeFinalized(nStatement)) { - return; - } - // finalizer should NEVER get called - // but if the database itself is not closed and is GC'ed, then - // all sub-objects attached to the database could end up getting GC'ed too. - // in that case, don't print any warning. - if (mInUse && mStackTrace != null) { - int len = mSqlStmt.length(); - StrictMode.onSqliteObjectLeaked( - "Releasing statement in a finalizer. Please ensure " + - "that you explicitly call close() on your cursor: " + - mSqlStmt.substring(0, (len > 1000) ? 1000 : len), - mStackTrace); - } - releaseSqlStatement(); - } finally { - super.finalize(); - } - } - - @Override public String toString() { - synchronized(this) { - StringBuilder buff = new StringBuilder(); - buff.append(" nStatement="); - buff.append(nStatement); - buff.append(", mInUse="); - buff.append(mInUse); - buff.append(", db="); - buff.append(mDatabase.getPath()); - buff.append(", db_connectionNum="); - buff.append(mDatabase.mConnectionNum); - buff.append(", sql="); - int len = mSqlStmt.length(); - buff.append(mSqlStmt.substring(0, (len > 100) ? 100 : len)); - return buff.toString(); - } - } - - /** - * Compiles SQL into a SQLite program. - * - * <P>The database lock must be held when calling this method. - * @param sql The SQL to compile. - */ - private final native void native_compile(String sql); -} diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java new file mode 100644 index 0000000..e45d66d --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -0,0 +1,1149 @@ +/* + * 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.ParcelFileDescriptor; +import android.util.Log; +import android.util.LruCache; +import android.util.Printer; + +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Represents a SQLite database connection. + * Each connection wraps an instance of a native <code>sqlite3</code> object. + * <p> + * 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. + * </p><p> + * 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. + * </p> + * + * <h2>Ownership and concurrency guarantees</h2> + * <p> + * 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. + * </p><p> + * The guarantee of having a single owner allows this class to be implemented + * without locks and greatly simplifies resource management. + * </p> + * + * <h2>Encapsulation guarantees</h2> + * <p> + * 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. + * </p><p> + * Encapsulation is what ensures that the connection object's + * lifecycle does not become a tortured mess of finalizers and reference + * queues. + * </p> + * + * @hide + */ +public final class SQLiteConnection { + private static final String TAG = "SQLiteConnection"; + + 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 PreparedStatementCache mPreparedStatementCache; + private PreparedStatement mPreparedStatementPool; + + // The recent operations log. + private final OperationLog mRecentOperations = new OperationLog(); + + // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY) + private int mConnectionPtr; + + private boolean mOnlyAllowReadOnlyOperations; + + private static native int nativeOpen(String path, int openFlags, String label, + boolean enableTrace, boolean enableProfile); + private static native void nativeClose(int connectionPtr); + private static native void nativeRegisterCustomFunction(int connectionPtr, + SQLiteCustomFunction function); + private static native void nativeSetLocale(int connectionPtr, String locale); + private static native int nativePrepareStatement(int connectionPtr, String sql); + private static native void nativeFinalizeStatement(int connectionPtr, int statementPtr); + private static native int nativeGetParameterCount(int connectionPtr, int statementPtr); + private static native boolean nativeIsReadOnly(int connectionPtr, int statementPtr); + private static native int nativeGetColumnCount(int connectionPtr, int statementPtr); + private static native String nativeGetColumnName(int connectionPtr, int statementPtr, + int index); + private static native void nativeBindNull(int connectionPtr, int statementPtr, + int index); + private static native void nativeBindLong(int connectionPtr, int statementPtr, + int index, long value); + private static native void nativeBindDouble(int connectionPtr, int statementPtr, + int index, double value); + private static native void nativeBindString(int connectionPtr, int statementPtr, + int index, String value); + private static native void nativeBindBlob(int connectionPtr, int statementPtr, + int index, byte[] value); + private static native void nativeResetStatementAndClearBindings( + int connectionPtr, int statementPtr); + private static native void nativeExecute(int connectionPtr, int statementPtr); + private static native long nativeExecuteForLong(int connectionPtr, int statementPtr); + private static native String nativeExecuteForString(int connectionPtr, int statementPtr); + private static native int nativeExecuteForBlobFileDescriptor( + int connectionPtr, int statementPtr); + private static native int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr); + private static native long nativeExecuteForLastInsertedRowId( + int connectionPtr, int statementPtr); + private static native long nativeExecuteForCursorWindow( + int connectionPtr, int statementPtr, int windowPtr, + int startPos, int requiredPos, boolean countAllRows); + private static native int nativeGetDbLookaside(int connectionPtr); + + private SQLiteConnection(SQLiteConnectionPool pool, + SQLiteDatabaseConfiguration configuration, + int connectionId, boolean primaryConnection) { + mPool = pool; + mConfiguration = new SQLiteDatabaseConfiguration(configuration); + mConnectionId = connectionId; + mIsPrimaryConnection = primaryConnection; + 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() { + SQLiteGlobal.initializeOnce(); + + mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags, + mConfiguration.label, + SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME); + + setLocaleFromConfiguration(); + } + + private void dispose(boolean finalized) { + if (mCloseGuard != null) { + if (finalized) { + mCloseGuard.warnIfOpen(); + } + mCloseGuard.close(); + } + + if (mConnectionPtr != 0) { + mRecentOperations.beginOperation("close", null, null); + try { + mPreparedStatementCache.evictAll(); + nativeClose(mConnectionPtr); + mConnectionPtr = 0; + } finally { + mRecentOperations.endOperation(); + } + } + } + + private void setLocaleFromConfiguration() { + nativeSetLocale(mConnectionPtr, mConfiguration.locale.toString()); + } + + // Called by SQLiteConnectionPool only. + void reconfigure(SQLiteDatabaseConfiguration configuration) { + // 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 whether locale has changed. + boolean localeChanged = !configuration.locale.equals(mConfiguration.locale); + + // Update configuration parameters. + mConfiguration.updateParametersFrom(configuration); + + // Update prepared statement cache size. + mPreparedStatementCache.resize(configuration.maxSqlCacheSize); + + // 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. + * <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. + * </p><p> + * 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. + * </p> + * + * @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."); + } + + mRecentOperations.beginOperation("prepare", sql, null); + try { + 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(ex); + throw ex; + } finally { + mRecentOperations.endOperation(); + } + } + + /** + * 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. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + */ + public void execute(String sql, Object[] bindArgs) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + mRecentOperations.beginOperation("execute", sql, bindArgs); + try { + PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + nativeExecute(mConnectionPtr, statement.mStatementPtr); + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(ex); + throw ex; + } finally { + mRecentOperations.endOperation(); + } + } + + /** + * 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. + * @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. + */ + public long executeForLong(String sql, Object[] bindArgs) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + mRecentOperations.beginOperation("executeForLong", sql, bindArgs); + try { + PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr); + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(ex); + throw ex; + } finally { + mRecentOperations.endOperation(); + } + } + + /** + * 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. + * @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. + */ + public String executeForString(String sql, Object[] bindArgs) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + mRecentOperations.beginOperation("executeForString", sql, bindArgs); + try { + PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr); + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(ex); + throw ex; + } finally { + mRecentOperations.endOperation(); + } + } + + /** + * 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. + * @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. + */ + public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + mRecentOperations.beginOperation("executeForBlobFileDescriptor", sql, bindArgs); + try { + PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + int fd = nativeExecuteForBlobFileDescriptor( + mConnectionPtr, statement.mStatementPtr); + return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null; + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(ex); + throw ex; + } finally { + mRecentOperations.endOperation(); + } + } + + /** + * 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. + * @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. + */ + public int executeForChangedRowCount(String sql, Object[] bindArgs) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + mRecentOperations.beginOperation("executeForChangedRowCount", sql, bindArgs); + try { + PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + return nativeExecuteForChangedRowCount( + mConnectionPtr, statement.mStatementPtr); + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(ex); + throw ex; + } finally { + mRecentOperations.endOperation(); + } + } + + /** + * 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. + * @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. + */ + public long executeForLastInsertedRowId(String sql, Object[] bindArgs) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + mRecentOperations.beginOperation("executeForLastInsertedRowId", sql, bindArgs); + try { + PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + return nativeExecuteForLastInsertedRowId( + mConnectionPtr, statement.mStatementPtr); + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(ex); + throw ex; + } finally { + mRecentOperations.endOperation(); + } + } + + /** + * 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. + * @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. + */ + public int executeForCursorWindow(String sql, Object[] bindArgs, + CursorWindow window, int startPos, int requiredPos, boolean countAllRows) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + if (window == null) { + throw new IllegalArgumentException("window must not be null."); + } + + int actualPos = -1; + int countedRows = -1; + int filledRows = -1; + mRecentOperations.beginOperation("executeForCursorWindow", sql, bindArgs); + try { + PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + 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 { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(ex); + throw ex; + } finally { + if (mRecentOperations.endOperationDeferLog()) { + mRecentOperations.logOperation("window='" + window + + "', startPos=" + startPos + + ", actualPos=" + actualPos + + ", filledRows=" + filledRows + + ", countedRows=" + countedRows); + } + } + } + + private PreparedStatement acquirePreparedStatement(String sql) { + PreparedStatement statement = mPreparedStatementCache.get(sql); + if (statement != null) { + return statement; + } + + final int 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 (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; + } + return statement; + } + + private void releasePreparedStatement(PreparedStatement statement) { + if (statement.mInCache) { + try { + nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr); + } catch (SQLiteException ex) { + // The statement could not be reset due to an error. + // The entryRemoved() callback for the cache will recursively call + // releasePreparedStatement() again, but this time mInCache will be false + // so the statement will be finalized and recycled. + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(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 { + nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr); + recyclePreparedStatement(statement); + } + } + + 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 " + + bindArgs.length + " were provided."); + } + if (count == 0) { + return; + } + + final int 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. + */ + public void dump(Printer printer) { + dumpUnsafe(printer); + } + + /** + * 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. + */ + void dumpUnsafe(Printer printer) { + printer.println("Connection #" + mConnectionId + ":"); + printer.println(" isPrimaryConnection: " + mIsPrimaryConnection); + printer.println(" connectionPtr: 0x" + Integer.toHexString(mConnectionPtr)); + printer.println(" onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations); + + mRecentOperations.dump(printer); + 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<DbStats> dbStatsList) { + // Get information about the main database. + int lookaside = nativeGetDbLookaside(mConnectionPtr); + long pageCount = 0; + long pageSize = 0; + try { + pageCount = executeForLong("PRAGMA page_count;", null); + pageSize = executeForLong("PRAGMA page_size;", null); + } catch (SQLiteException ex) { + // Ignore. + } + dbStatsList.add(getMainDbStatsUnsafe(lookaside, pageCount, pageSize)); + + // Get information about attached databases. + // We ignore the first row in the database list because it corresponds to + // the main database which we have already described. + CursorWindow window = new CursorWindow("collectDbStats"); + try { + executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false); + for (int i = 1; i < window.getNumRows(); i++) { + String name = window.getString(i, 1); + String path = window.getString(i, 2); + pageCount = 0; + pageSize = 0; + try { + pageCount = executeForLong("PRAGMA " + name + ".page_count;", null); + pageSize = executeForLong("PRAGMA " + name + ".page_size;", null); + } catch (SQLiteException ex) { + // Ignore. + } + String label = " (attached) " + name; + if (!path.isEmpty()) { + label += ": " + path; + } + dbStatsList.add(new DbStats(label, pageCount, pageSize, 0, 0, 0, 0)); + } + } catch (SQLiteException ex) { + // Ignore. + } finally { + window.close(); + } + } + + /** + * Collects statistics about database connection memory usage, in the case where the + * caller might not actually own the connection. + * + * @return The statistics object, never null. + */ + void collectDbStatsUnsafe(ArrayList<DbStats> dbStatsList) { + dbStatsList.add(getMainDbStatsUnsafe(0, 0, 0)); + } + + private DbStats getMainDbStatsUnsafe(int lookaside, long pageCount, long pageSize) { + // The prepared statement cache is thread-safe so we can access its statistics + // even if we do not own the database connection. + String label = mConfiguration.path; + if (!mIsPrimaryConnection) { + label += " (" + mConnectionId + ")"; + } + return new DbStats(label, pageCount, pageSize, lookaside, + mPreparedStatementCache.hitCount(), + mPreparedStatementCache.missCount(), + mPreparedStatementCache.size()); + } + + @Override + public String toString() { + return "SQLiteConnection: " + mConfiguration.path + " (" + mConnectionId + ")"; + } + + private PreparedStatement obtainPreparedStatement(String sql, int statementPtr, + int numParameters, int type, boolean readOnly) { + PreparedStatement statement = mPreparedStatementPool; + if (statement != null) { + mPreparedStatementPool = statement.mPoolNext; + statement.mPoolNext = null; + statement.mInCache = false; + } else { + statement = new PreparedStatement(); + } + statement.mSql = sql; + statement.mStatementPtr = statementPtr; + statement.mNumParameters = numParameters; + statement.mType = type; + statement.mReadOnly = readOnly; + return statement; + } + + private void recyclePreparedStatement(PreparedStatement statement) { + statement.mSql = null; + statement.mPoolNext = mPreparedStatementPool; + mPreparedStatementPool = statement; + } + + private static String trimSqlForDisplay(String sql) { + return TRIM_SQL_PATTERN.matcher(sql).replaceAll(" "); + } + + /** + * Holder type for a prepared statement. + * + * Although this object holds a pointer to a native statement object, it + * does not have a finalizer. This is deliberate. The {@link SQLiteConnection} + * owns the statement object and will take care of freeing it when needed. + * In particular, closing the connection requires a guarantee of deterministic + * resource disposal because all native statement objects must be freed before + * the native database object can be closed. So no finalizers here. + */ + private static final class PreparedStatement { + // Next item in pool. + public PreparedStatement mPoolNext; + + // The SQL from which the statement was prepared. + public String mSql; + + // The native sqlite3_stmt object pointer. + // Lifetime is managed explicitly by the connection. + public int mStatementPtr; + + // The number of parameters that the prepared statement has. + public int mNumParameters; + + // The statement type. + public int mType; + + // True if the statement is read-only. + public boolean mReadOnly; + + // True if the statement is in the cache. + public boolean mInCache; + } + + private final class PreparedStatementCache + extends LruCache<String, PreparedStatement> { + public PreparedStatementCache(int size) { + super(size); + } + + @Override + protected void entryRemoved(boolean evicted, String key, + PreparedStatement oldValue, PreparedStatement newValue) { + oldValue.mInCache = false; + releasePreparedStatement(oldValue); + } + + public void dump(Printer printer) { + printer.println(" Prepared statement cache:"); + Map<String, PreparedStatement> cache = snapshot(); + if (!cache.isEmpty()) { + int i = 0; + for (Map.Entry<String, PreparedStatement> entry : cache.entrySet()) { + PreparedStatement statement = entry.getValue(); + if (statement.mInCache) { // might be false due to a race with entryRemoved + String sql = entry.getKey(); + printer.println(" " + i + ": statementPtr=0x" + + Integer.toHexString(statement.mStatementPtr) + + ", numParameters=" + statement.mNumParameters + + ", type=" + statement.mType + + ", readOnly=" + statement.mReadOnly + + ", sql=\"" + trimSqlForDisplay(sql) + "\""); + } + i += 1; + } + } else { + printer.println(" <none>"); + } + } + } + + private static final class OperationLog { + private static final int MAX_RECENT_OPERATIONS = 10; + + private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS]; + private int mIndex; + + public void beginOperation(String kind, String sql, Object[] bindArgs) { + synchronized (mOperations) { + final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS; + Operation operation = mOperations[index]; + if (operation == null) { + operation = new Operation(); + mOperations[index] = operation; + } else { + operation.mFinished = false; + operation.mException = null; + if (operation.mBindArgs != null) { + operation.mBindArgs.clear(); + } + } + operation.mStartTime = System.currentTimeMillis(); + operation.mKind = kind; + operation.mSql = sql; + if (bindArgs != null) { + if (operation.mBindArgs == null) { + operation.mBindArgs = new ArrayList<Object>(); + } else { + operation.mBindArgs.clear(); + } + for (int i = 0; i < bindArgs.length; i++) { + final Object arg = bindArgs[i]; + if (arg != null && arg instanceof byte[]) { + // Don't hold onto the real byte array longer than necessary. + operation.mBindArgs.add(EMPTY_BYTE_ARRAY); + } else { + operation.mBindArgs.add(arg); + } + } + } + mIndex = index; + } + } + + public void failOperation(Exception ex) { + synchronized (mOperations) { + final Operation operation = mOperations[mIndex]; + operation.mException = ex; + } + } + + public boolean endOperationDeferLog() { + synchronized (mOperations) { + return endOperationDeferLogLocked(); + } + } + + private boolean endOperationDeferLogLocked() { + final Operation operation = mOperations[mIndex]; + operation.mEndTime = System.currentTimeMillis(); + operation.mFinished = true; + return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery( + operation.mEndTime - operation.mStartTime); + } + + public void endOperation() { + synchronized (mOperations) { + if (endOperationDeferLogLocked()) { + logOperationLocked(null); + } + } + } + + public void logOperation(String detail) { + synchronized (mOperations) { + logOperationLocked(detail); + } + } + + private void logOperationLocked(String detail) { + final Operation operation = mOperations[mIndex]; + StringBuilder msg = new StringBuilder(); + operation.describe(msg); + if (detail != null) { + msg.append(", ").append(detail); + } + Log.d(TAG, msg.toString()); + } + + public String describeCurrentOperation() { + synchronized (mOperations) { + final Operation operation = mOperations[mIndex]; + if (operation != null && !operation.mFinished) { + StringBuilder msg = new StringBuilder(); + operation.describe(msg); + return msg.toString(); + } + return null; + } + } + + public void dump(Printer printer) { + synchronized (mOperations) { + printer.println(" Most recently executed operations:"); + int index = mIndex; + Operation operation = mOperations[index]; + if (operation != null) { + int n = 0; + do { + StringBuilder msg = new StringBuilder(); + msg.append(" ").append(n).append(": ["); + msg.append(operation.getFormattedStartTime()); + msg.append("] "); + operation.describe(msg); + printer.println(msg.toString()); + + if (index > 0) { + index -= 1; + } else { + index = MAX_RECENT_OPERATIONS - 1; + } + n += 1; + operation = mOperations[index]; + } while (operation != null && n < MAX_RECENT_OPERATIONS); + } else { + printer.println(" <none>"); + } + } + } + } + + private static final class Operation { + private static final SimpleDateFormat sDateFormat = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + + public long mStartTime; + public long mEndTime; + public String mKind; + public String mSql; + public ArrayList<Object> mBindArgs; + public boolean mFinished; + public Exception mException; + + public void describe(StringBuilder msg) { + msg.append(mKind); + if (mFinished) { + msg.append(" took ").append(mEndTime - mStartTime).append("ms"); + } else { + msg.append(" started ").append(System.currentTimeMillis() - mStartTime) + .append("ms ago"); + } + msg.append(" - ").append(getStatus()); + if (mSql != null) { + msg.append(", sql=\"").append(trimSqlForDisplay(mSql)).append("\""); + } + if (mBindArgs != null && mBindArgs.size() != 0) { + msg.append(", bindArgs=["); + final int count = mBindArgs.size(); + for (int i = 0; i < count; i++) { + final Object arg = mBindArgs.get(i); + if (i != 0) { + msg.append(", "); + } + if (arg == null) { + msg.append("null"); + } else if (arg instanceof byte[]) { + msg.append("<byte[]>"); + } else if (arg instanceof String) { + msg.append("\"").append((String)arg).append("\""); + } else { + msg.append(arg); + } + } + msg.append("]"); + } + if (mException != null) { + msg.append(", exception=\"").append(mException.getMessage()).append("\""); + } + } + + private String getStatus() { + if (!mFinished) { + return "running"; + } + return mException != null ? "failed" : "succeeded"; + } + + private String getFormattedStartTime() { + return sDateFormat.format(new Date(mStartTime)); + } + } +} diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java new file mode 100644 index 0000000..b88bfee --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -0,0 +1,907 @@ +/* + * 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.CloseGuard; + +import android.database.sqlite.SQLiteDebug.DbStats; +import android.os.SystemClock; +import android.util.Log; +import android.util.PrefixPrinter; +import android.util.Printer; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; + +/** + * Maintains a pool of active SQLite database connections. + * <p> + * At any given time, a connection is either owned by the pool, or it has been + * acquired by a {@link SQLiteSession}. When the {@link SQLiteSession} is + * finished with the connection it is using, it must return the connection + * back to the pool. + * </p><p> + * The pool holds strong references to the connections it owns. However, + * it only holds <em>weak references</em> to the connections that sessions + * have acquired from it. Using weak references in the latter case ensures + * that the connection pool can detect when connections have been improperly + * abandoned so that it can create new connections to replace them if needed. + * </p><p> + * The connection pool is thread-safe (but the connections themselves are not). + * </p> + * + * <h2>Exception safety</h2> + * <p> + * This code attempts to maintain the invariant that opened connections are + * always owned. Unfortunately that means it needs to handle exceptions + * all over to ensure that broken connections get cleaned up. Most + * operations invokving SQLite can throw {@link SQLiteException} or other + * runtime exceptions. This is a bit of a pain to deal with because the compiler + * cannot help us catch missing exception handling code. + * </p><p> + * The general rule for this file: If we are making calls out to + * {@link SQLiteConnection} then we must be prepared to handle any + * runtime exceptions it might throw at us. Note that out-of-memory + * is an {@link Error}, not a {@link RuntimeException}. We don't trouble ourselves + * handling out of memory because it is hard to do anything at all sensible then + * and most likely the VM is about to crash. + * </p> + * + * @hide + */ +public final class SQLiteConnectionPool implements Closeable { + private static final String TAG = "SQLiteConnectionPool"; + + // Amount of time to wait in milliseconds before unblocking acquireConnection + // and logging a message about the connection pool being busy. + private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final Object mLock = new Object(); + private final AtomicBoolean mConnectionLeaked = new AtomicBoolean(); + private final SQLiteDatabaseConfiguration mConfiguration; + private boolean mIsOpen; + private int mNextConnectionId; + + private ConnectionWaiter mConnectionWaiterPool; + private ConnectionWaiter mConnectionWaiterQueue; + + // Strong references to all available connections. + private final ArrayList<SQLiteConnection> mAvailableNonPrimaryConnections = + new ArrayList<SQLiteConnection>(); + private SQLiteConnection mAvailablePrimaryConnection; + + // Weak references to all acquired connections. The associated value + // is a boolean that indicates whether the connection must be reconfigured + // before being returned to the available connection list. + // For example, the prepared statement cache size may have changed and + // need to be updated. + private final WeakHashMap<SQLiteConnection, Boolean> mAcquiredConnections = + new WeakHashMap<SQLiteConnection, Boolean>(); + + /** + * Connection flag: Read-only. + * <p> + * This flag indicates that the connection will only be used to + * perform read-only operations. + * </p> + */ + public static final int CONNECTION_FLAG_READ_ONLY = 1 << 0; + + /** + * Connection flag: Primary connection affinity. + * <p> + * This flag indicates that the primary connection is required. + * This flag helps support legacy applications that expect most data modifying + * operations to be serialized by locking the primary database connection. + * Setting this flag essentially implements the old "db lock" concept by preventing + * an operation from being performed until it can obtain exclusive access to + * the primary connection. + * </p> + */ + public static final int CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY = 1 << 1; + + /** + * Connection flag: Connection is being used interactively. + * <p> + * This flag indicates that the connection is needed by the UI thread. + * The connection pool can use this flag to elevate the priority + * of the database connection request. + * </p> + */ + public static final int CONNECTION_FLAG_INTERACTIVE = 1 << 2; + + private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) { + mConfiguration = new SQLiteDatabaseConfiguration(configuration); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(true); + } finally { + super.finalize(); + } + } + + /** + * Opens a connection pool for the specified database. + * + * @param configuration The database configuration. + * @return The connection pool. + * + * @throws SQLiteException if a database error occurs. + */ + public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) { + if (configuration == null) { + throw new IllegalArgumentException("configuration must not be null."); + } + + // Create the pool. + SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration); + pool.open(); // might throw + return pool; + } + + // Might throw + private void open() { + // Open the primary connection. + // This might throw if the database is corrupt. + mAvailablePrimaryConnection = openConnectionLocked( + true /*primaryConnection*/); // might throw + + // Mark the pool as being open for business. + mIsOpen = true; + mCloseGuard.open("close"); + } + + /** + * Closes the connection pool. + * <p> + * When the connection pool is closed, it will refuse all further requests + * to acquire connections. All connections that are currently available in + * the pool are closed immediately. Any connections that are still in use + * will be closed as soon as they are returned to the pool. + * </p> + * + * @throws IllegalStateException if the pool has been closed. + */ + public void close() { + dispose(false); + } + + private void dispose(boolean finalized) { + if (mCloseGuard != null) { + if (finalized) { + mCloseGuard.warnIfOpen(); + } + mCloseGuard.close(); + } + + if (!finalized) { + // Close all connections. We don't need (or want) to do this + // when finalized because we don't know what state the connections + // themselves will be in. The finalizer is really just here for CloseGuard. + // The connections will take care of themselves when their own finalizers run. + synchronized (mLock) { + throwIfClosedLocked(); + + mIsOpen = false; + + final int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i)); + } + mAvailableNonPrimaryConnections.clear(); + + if (mAvailablePrimaryConnection != null) { + closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); + mAvailablePrimaryConnection = null; + } + + final int pendingCount = mAcquiredConnections.size(); + if (pendingCount != 0) { + Log.i(TAG, "The connection pool for " + mConfiguration.label + + " has been closed but there are still " + + pendingCount + " connections in use. They will be closed " + + "as they are released back to the pool."); + } + + wakeConnectionWaitersLocked(); + } + } + } + + /** + * Reconfigures the database configuration of the connection pool and all of its + * connections. + * <p> + * Configuration changes are propagated down to connections immediately if + * they are available or as soon as they are released. This includes changes + * that affect the size of the pool. + * </p> + * + * @param configuration The new configuration. + * + * @throws IllegalStateException if the pool has been closed. + */ + public void reconfigure(SQLiteDatabaseConfiguration configuration) { + if (configuration == null) { + throw new IllegalArgumentException("configuration must not be null."); + } + + synchronized (mLock) { + throwIfClosedLocked(); + + final boolean poolSizeChanged = mConfiguration.maxConnectionPoolSize + != configuration.maxConnectionPoolSize; + mConfiguration.updateParametersFrom(configuration); + + if (poolSizeChanged) { + int availableCount = mAvailableNonPrimaryConnections.size(); + while (availableCount-- > mConfiguration.maxConnectionPoolSize - 1) { + SQLiteConnection connection = + mAvailableNonPrimaryConnections.remove(availableCount); + closeConnectionAndLogExceptionsLocked(connection); + } + } + + reconfigureAllConnectionsLocked(); + + wakeConnectionWaitersLocked(); + } + } + + /** + * Acquires a connection from the pool. + * <p> + * The caller must call {@link #releaseConnection} to release the connection + * back to the pool when it is finished. Failure to do so will result + * in much unpleasantness. + * </p> + * + * @param sql If not null, try to find a connection that already has + * the specified SQL statement in its prepared statement cache. + * @param connectionFlags The connection request flags. + * @return The connection that was acquired, never null. + * + * @throws IllegalStateException if the pool has been closed. + * @throws SQLiteException if a database error occurs. + */ + public SQLiteConnection acquireConnection(String sql, int connectionFlags) { + return waitForConnection(sql, connectionFlags); + } + + /** + * Releases a connection back to the pool. + * <p> + * It is ok to call this method after the pool has closed, to release + * connections that were still in use at the time of closure. + * </p> + * + * @param connection The connection to release. Must not be null. + * + * @throws IllegalStateException if the connection was not acquired + * from this pool or if it has already been released. + */ + public void releaseConnection(SQLiteConnection connection) { + synchronized (mLock) { + Boolean mustReconfigure = mAcquiredConnections.remove(connection); + if (mustReconfigure == null) { + throw new IllegalStateException("Cannot perform this operation " + + "because the specified connection was not acquired " + + "from this pool or has already been released."); + } + + if (!mIsOpen) { + closeConnectionAndLogExceptionsLocked(connection); + } else if (connection.isPrimaryConnection()) { + assert mAvailablePrimaryConnection == null; + try { + if (mustReconfigure == Boolean.TRUE) { + connection.reconfigure(mConfiguration); // might throw + } + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure released primary connection, closing it: " + + connection, ex); + closeConnectionAndLogExceptionsLocked(connection); + connection = null; + } + if (connection != null) { + mAvailablePrimaryConnection = connection; + } + wakeConnectionWaitersLocked(); + } else if (mAvailableNonPrimaryConnections.size() >= + mConfiguration.maxConnectionPoolSize - 1) { + closeConnectionAndLogExceptionsLocked(connection); + } else { + try { + if (mustReconfigure == Boolean.TRUE) { + connection.reconfigure(mConfiguration); // might throw + } + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure released non-primary connection, " + + "closing it: " + connection, ex); + closeConnectionAndLogExceptionsLocked(connection); + connection = null; + } + if (connection != null) { + mAvailableNonPrimaryConnections.add(connection); + } + wakeConnectionWaitersLocked(); + } + } + } + + /** + * Returns true if the session should yield the connection due to + * contention over available database connections. + * + * @param connection The connection owned by the session. + * @param connectionFlags The connection request flags. + * @return True if the session should yield its connection. + * + * @throws IllegalStateException if the connection was not acquired + * from this pool or if it has already been released. + */ + public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) { + synchronized (mLock) { + if (!mAcquiredConnections.containsKey(connection)) { + throw new IllegalStateException("Cannot perform this operation " + + "because the specified connection was not acquired " + + "from this pool or has already been released."); + } + + if (!mIsOpen) { + return false; + } + + return isSessionBlockingImportantConnectionWaitersLocked( + connection.isPrimaryConnection(), connectionFlags); + } + } + + /** + * Collects statistics about database connection memory usage. + * + * @param dbStatsList The list to populate. + */ + public void collectDbStats(ArrayList<DbStats> dbStatsList) { + synchronized (mLock) { + if (mAvailablePrimaryConnection != null) { + mAvailablePrimaryConnection.collectDbStats(dbStatsList); + } + + for (SQLiteConnection connection : mAvailableNonPrimaryConnections) { + connection.collectDbStats(dbStatsList); + } + + for (SQLiteConnection connection : mAcquiredConnections.keySet()) { + connection.collectDbStatsUnsafe(dbStatsList); + } + } + } + + // Might throw. + private SQLiteConnection openConnectionLocked(boolean primaryConnection) { + final int connectionId = mNextConnectionId++; + return SQLiteConnection.open(this, mConfiguration, + connectionId, primaryConnection); // might throw + } + + void onConnectionLeaked() { + // This code is running inside of the SQLiteConnection finalizer. + // + // We don't know whether it is just the connection that has been finalized (and leaked) + // or whether the connection pool has also been or is about to be finalized. + // Consequently, it would be a bad idea to try to grab any locks or to + // do any significant work here. So we do the simplest possible thing and + // set a flag. waitForConnection() periodically checks this flag (when it + // times out) so that it can recover from leaked connections and wake + // itself or other threads up if necessary. + // + // You might still wonder why we don't try to do more to wake up the waiters + // immediately. First, as explained above, it would be hard to do safely + // unless we started an extra Thread to function as a reference queue. Second, + // this is never supposed to happen in normal operation. Third, there is no + // guarantee that the GC will actually detect the leak in a timely manner so + // it's not all that important that we recover from the leak in a timely manner + // either. Fourth, if a badly behaved application finds itself hung waiting for + // several seconds while waiting for a leaked connection to be detected and recreated, + // then perhaps its authors will have added incentive to fix the problem! + + Log.w(TAG, "A SQLiteConnection object for database '" + + mConfiguration.label + "' was leaked! Please fix your application " + + "to end transactions in progress properly and to close the database " + + "when it is no longer needed."); + + mConnectionLeaked.set(true); + } + + // Can't throw. + private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) { + try { + connection.close(); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to close connection, its fate is now in the hands " + + "of the merciful GC: " + connection, ex); + } + } + + // Can't throw. + private void reconfigureAllConnectionsLocked() { + boolean wake = false; + if (mAvailablePrimaryConnection != null) { + try { + mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure available primary connection, closing it: " + + mAvailablePrimaryConnection, ex); + closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); + mAvailablePrimaryConnection = null; + wake = true; + } + } + + int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i); + try { + connection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: " + + connection, ex); + closeConnectionAndLogExceptionsLocked(connection); + mAvailableNonPrimaryConnections.remove(i--); + count -= 1; + wake = true; + } + } + + if (!mAcquiredConnections.isEmpty()) { + ArrayList<SQLiteConnection> keysToUpdate = new ArrayList<SQLiteConnection>( + mAcquiredConnections.size()); + for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) { + if (entry.getValue() != Boolean.TRUE) { + keysToUpdate.add(entry.getKey()); + } + } + final int updateCount = keysToUpdate.size(); + for (int i = 0; i < updateCount; i++) { + mAcquiredConnections.put(keysToUpdate.get(i), Boolean.TRUE); + } + } + + if (wake) { + wakeConnectionWaitersLocked(); + } + } + + // Might throw. + private SQLiteConnection waitForConnection(String sql, int connectionFlags) { + final boolean wantPrimaryConnection = + (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0; + + final ConnectionWaiter waiter; + synchronized (mLock) { + throwIfClosedLocked(); + + // Try to acquire a connection. + SQLiteConnection connection = null; + if (!wantPrimaryConnection) { + connection = tryAcquireNonPrimaryConnectionLocked( + sql, connectionFlags); // might throw + } + if (connection == null) { + connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw + } + if (connection != null) { + return connection; + } + + // No connections available. Enqueue a waiter in priority order. + final int priority = getPriority(connectionFlags); + final long startTime = SystemClock.uptimeMillis(); + waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime, + priority, wantPrimaryConnection, sql, connectionFlags); + ConnectionWaiter predecessor = null; + ConnectionWaiter successor = mConnectionWaiterQueue; + while (successor != null) { + if (priority > successor.mPriority) { + waiter.mNext = successor; + break; + } + predecessor = successor; + successor = successor.mNext; + } + if (predecessor != null) { + predecessor.mNext = waiter; + } else { + mConnectionWaiterQueue = waiter; + } + } + + // Park the thread until a connection is assigned or the pool is closed. + // Rethrow an exception from the wait, if we got one. + long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; + long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis; + for (;;) { + // Detect and recover from connection leaks. + if (mConnectionLeaked.compareAndSet(true, false)) { + wakeConnectionWaitersLocked(); + } + + // Wait to be unparked (may already have happened), a timeout, or interruption. + LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L); + + // Clear the interrupted flag, just in case. + Thread.interrupted(); + + // Check whether we are done waiting yet. + synchronized (mLock) { + throwIfClosedLocked(); + + SQLiteConnection connection = waiter.mAssignedConnection; + if (connection != null) { + recycleConnectionWaiterLocked(waiter); + return connection; + } + + RuntimeException ex = waiter.mException; + if (ex != null) { + recycleConnectionWaiterLocked(waiter); + throw ex; // rethrow! + } + + final long now = SystemClock.uptimeMillis(); + if (now < nextBusyTimeoutTime) { + busyTimeoutMillis = now - nextBusyTimeoutTime; + } else { + logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags); + busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; + nextBusyTimeoutTime = now + busyTimeoutMillis; + } + } + } + } + + // Can't throw. + private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) { + final Thread thread = Thread.currentThread(); + StringBuilder msg = new StringBuilder(); + msg.append("The connection pool for database '").append(mConfiguration.label); + msg.append("' has been unable to grant a connection to thread "); + msg.append(thread.getId()).append(" (").append(thread.getName()).append(") "); + msg.append("with flags 0x").append(Integer.toHexString(connectionFlags)); + msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n"); + + ArrayList<String> requests = new ArrayList<String>(); + int activeConnections = 0; + int idleConnections = 0; + if (!mAcquiredConnections.isEmpty()) { + for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) { + final SQLiteConnection connection = entry.getKey(); + String description = connection.describeCurrentOperationUnsafe(); + if (description != null) { + requests.add(description); + activeConnections += 1; + } else { + idleConnections += 1; + } + } + } + int availableConnections = mAvailableNonPrimaryConnections.size(); + if (mAvailablePrimaryConnection != null) { + availableConnections += 1; + } + + msg.append("Connections: ").append(activeConnections).append(" active, "); + msg.append(idleConnections).append(" idle, "); + msg.append(availableConnections).append(" available.\n"); + + if (!requests.isEmpty()) { + msg.append("\nRequests in progress:\n"); + for (String request : requests) { + msg.append(" ").append(request).append("\n"); + } + } + + Log.w(TAG, msg.toString()); + } + + // Can't throw. + private void wakeConnectionWaitersLocked() { + // Unpark all waiters that have requests that we can fulfill. + // This method is designed to not throw runtime exceptions, although we might send + // a waiter an exception for it to rethrow. + ConnectionWaiter predecessor = null; + ConnectionWaiter waiter = mConnectionWaiterQueue; + boolean primaryConnectionNotAvailable = false; + boolean nonPrimaryConnectionNotAvailable = false; + while (waiter != null) { + boolean unpark = false; + if (!mIsOpen) { + unpark = true; + } else { + try { + SQLiteConnection connection = null; + if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) { + connection = tryAcquireNonPrimaryConnectionLocked( + waiter.mSql, waiter.mConnectionFlags); // might throw + if (connection == null) { + nonPrimaryConnectionNotAvailable = true; + } + } + if (connection == null && !primaryConnectionNotAvailable) { + connection = tryAcquirePrimaryConnectionLocked( + waiter.mConnectionFlags); // might throw + if (connection == null) { + primaryConnectionNotAvailable = true; + } + } + if (connection != null) { + waiter.mAssignedConnection = connection; + unpark = true; + } else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) { + // There are no connections available and the pool is still open. + // We cannot fulfill any more connection requests, so stop here. + break; + } + } catch (RuntimeException ex) { + // Let the waiter handle the exception from acquiring a connection. + waiter.mException = ex; + unpark = true; + } + } + + final ConnectionWaiter successor = waiter.mNext; + if (unpark) { + if (predecessor != null) { + predecessor.mNext = successor; + } else { + mConnectionWaiterQueue = successor; + } + waiter.mNext = null; + + LockSupport.unpark(waiter.mThread); + } else { + predecessor = waiter; + } + waiter = successor; + } + } + + // Might throw. + private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) { + // If the primary connection is available, acquire it now. + SQLiteConnection connection = mAvailablePrimaryConnection; + if (connection != null) { + mAvailablePrimaryConnection = null; + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Make sure that the primary connection actually exists and has just been acquired. + for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) { + if (acquiredConnection.isPrimaryConnection()) { + return null; + } + } + + // Uhoh. No primary connection! Either this is the first time we asked + // for it, or maybe it leaked? + connection = openConnectionLocked(true /*primaryConnection*/); // might throw + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Might throw. + private SQLiteConnection tryAcquireNonPrimaryConnectionLocked( + String sql, int connectionFlags) { + // Try to acquire the next connection in the queue. + SQLiteConnection connection; + final int availableCount = mAvailableNonPrimaryConnections.size(); + if (availableCount > 1 && sql != null) { + // If we have a choice, then prefer a connection that has the + // prepared statement in its cache. + for (int i = 0; i < availableCount; i++) { + connection = mAvailableNonPrimaryConnections.get(i); + if (connection.isPreparedStatementInCache(sql)) { + mAvailableNonPrimaryConnections.remove(i); + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + } + } + if (availableCount > 0) { + // Otherwise, just grab the next one. + connection = mAvailableNonPrimaryConnections.remove(availableCount - 1); + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Expand the pool if needed. + int openConnections = mAcquiredConnections.size(); + if (mAvailablePrimaryConnection != null) { + openConnections += 1; + } + if (openConnections >= mConfiguration.maxConnectionPoolSize) { + return null; + } + connection = openConnectionLocked(false /*primaryConnection*/); // might throw + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Might throw. + private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) { + try { + final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0; + connection.setOnlyAllowReadOnlyOperations(readOnly); + + mAcquiredConnections.put(connection, Boolean.FALSE); + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to prepare acquired connection for session, closing it: " + + connection +", connectionFlags=" + connectionFlags); + closeConnectionAndLogExceptionsLocked(connection); + throw ex; // rethrow! + } + } + + private boolean isSessionBlockingImportantConnectionWaitersLocked( + boolean holdingPrimaryConnection, int connectionFlags) { + ConnectionWaiter waiter = mConnectionWaiterQueue; + if (waiter != null) { + final int priority = getPriority(connectionFlags); + do { + // Only worry about blocked connections that have same or lower priority. + if (priority > waiter.mPriority) { + break; + } + + // If we are holding the primary connection then we are blocking the waiter. + // Likewise, if we are holding a non-primary connection and the waiter + // would accept a non-primary connection, then we are blocking the waier. + if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) { + return true; + } + + waiter = waiter.mNext; + } while (waiter != null); + } + return false; + } + + private static int getPriority(int connectionFlags) { + return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0; + } + + private void throwIfClosedLocked() { + if (!mIsOpen) { + throw new IllegalStateException("Cannot perform this operation " + + "because the connection pool have been closed."); + } + } + + private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime, + int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) { + ConnectionWaiter waiter = mConnectionWaiterPool; + if (waiter != null) { + mConnectionWaiterPool = waiter.mNext; + waiter.mNext = null; + } else { + waiter = new ConnectionWaiter(); + } + waiter.mThread = thread; + waiter.mStartTime = startTime; + waiter.mPriority = priority; + waiter.mWantPrimaryConnection = wantPrimaryConnection; + waiter.mSql = sql; + waiter.mConnectionFlags = connectionFlags; + return waiter; + } + + private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) { + waiter.mNext = mConnectionWaiterPool; + waiter.mThread = null; + waiter.mSql = null; + waiter.mAssignedConnection = null; + waiter.mException = null; + mConnectionWaiterPool = waiter; + } + + /** + * Dumps debugging information about this connection pool. + * + * @param printer The printer to receive the dump, not null. + */ + public void dump(Printer printer) { + Printer indentedPrinter = PrefixPrinter.create(printer, " "); + synchronized (mLock) { + printer.println("Connection pool for " + mConfiguration.path + ":"); + printer.println(" Open: " + mIsOpen); + printer.println(" Max connections: " + mConfiguration.maxConnectionPoolSize); + + printer.println(" Available primary connection:"); + if (mAvailablePrimaryConnection != null) { + mAvailablePrimaryConnection.dump(indentedPrinter); + } else { + indentedPrinter.println("<none>"); + } + + printer.println(" Available non-primary connections:"); + if (!mAvailableNonPrimaryConnections.isEmpty()) { + final int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter); + } + } else { + indentedPrinter.println("<none>"); + } + + printer.println(" Acquired connections:"); + if (!mAcquiredConnections.isEmpty()) { + for (Map.Entry<SQLiteConnection, Boolean> entry : + mAcquiredConnections.entrySet()) { + final SQLiteConnection connection = entry.getKey(); + connection.dumpUnsafe(indentedPrinter); + indentedPrinter.println(" Pending reconfiguration: " + entry.getValue()); + } + } else { + indentedPrinter.println("<none>"); + } + + printer.println(" Connection waiters:"); + if (mConnectionWaiterQueue != null) { + int i = 0; + final long now = SystemClock.uptimeMillis(); + for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null; + waiter = waiter.mNext, i++) { + indentedPrinter.println(i + ": waited for " + + ((now - waiter.mStartTime) * 0.001f) + + " ms - thread=" + waiter.mThread + + ", priority=" + waiter.mPriority + + ", sql='" + waiter.mSql + "'"); + } + } else { + indentedPrinter.println("<none>"); + } + } + } + + @Override + public String toString() { + return "SQLiteConnectionPool: " + mConfiguration.path; + } + + private static final class ConnectionWaiter { + public ConnectionWaiter mNext; + public Thread mThread; + public long mStartTime; + public int mPriority; + public boolean mWantPrimaryConnection; + public String mSql; + public int mConnectionFlags; + public SQLiteConnection mAssignedConnection; + public RuntimeException mException; + } +} diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index 8dcedf2..9dcb498 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -43,7 +43,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { private final String[] mColumns; /** The query object for the cursor */ - private SQLiteQuery mQuery; + private final SQLiteQuery mQuery; /** The compiled query this cursor came from */ private final SQLiteCursorDriver mDriver; @@ -96,9 +96,6 @@ public class SQLiteCursor extends AbstractWindowedCursor { if (query == null) { throw new IllegalArgumentException("query object cannot be null"); } - if (query.mDatabase == null) { - throw new IllegalArgumentException("query.mDatabase cannot be null"); - } if (StrictMode.vmSqliteObjectLeaksEnabled()) { mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); } else { @@ -109,38 +106,21 @@ public class SQLiteCursor extends AbstractWindowedCursor { mColumnNameMap = null; mQuery = query; - query.mDatabase.lock(query.mSql); - try { - // Setup the list of columns - int columnCount = mQuery.columnCountLocked(); - mColumns = new String[columnCount]; - - // Read in all column names - for (int i = 0; i < columnCount; i++) { - String columnName = mQuery.columnNameLocked(i); - mColumns[i] = columnName; - if (false) { - Log.v("DatabaseWindow", "mColumns[" + i + "] is " - + mColumns[i]); - } - - // Make note of the row ID column index for quick access to it - if ("_id".equals(columnName)) { - mRowIdColumnIndex = i; - } + mColumns = query.getColumnNames(); + for (int i = 0; i < mColumns.length; i++) { + // Make note of the row ID column index for quick access to it + if ("_id".equals(mColumns[i])) { + mRowIdColumnIndex = i; } - } finally { - query.mDatabase.unlock(); } } /** + * Get the database that this cursor is associated with. * @return the SQLiteDatabase that this cursor is associated with. */ public SQLiteDatabase getDatabase() { - synchronized (this) { - return mQuery.mDatabase; - } + return mQuery.getDatabase(); } @Override @@ -167,7 +147,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { if (mCount == NO_COUNT) { int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); - mCount = getQuery().fillWindow(mWindow, startPos, requiredPos, true); + mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); mCursorWindowCapacity = mWindow.getNumRows(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "received count(*) from native_fill_window: " + mCount); @@ -175,14 +155,10 @@ public class SQLiteCursor extends AbstractWindowedCursor { } else { int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, mCursorWindowCapacity); - getQuery().fillWindow(mWindow, startPos, requiredPos, false); + mQuery.fillWindow(mWindow, startPos, requiredPos, false); } } - private synchronized SQLiteQuery getQuery() { - return mQuery; - } - @Override public int getColumnIndex(String columnName) { // Create mColumnNameMap on demand @@ -237,75 +213,28 @@ public class SQLiteCursor extends AbstractWindowedCursor { if (isClosed()) { return false; } - long timeStart = 0; - if (false) { - timeStart = System.currentTimeMillis(); - } synchronized (this) { + if (!mQuery.getDatabase().isOpen()) { + return false; + } + if (mWindow != null) { mWindow.clear(); } mPos = -1; - SQLiteDatabase db = null; - try { - db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } - if (!db.equals(mQuery.mDatabase)) { - // since we need to use a different database connection handle, - // re-compile the query - try { - db.lock(mQuery.mSql); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } - try { - // close the old mQuery object and open a new one - mQuery.close(); - mQuery = new SQLiteQuery(db, mQuery); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } finally { - db.unlock(); - } - } - // This one will recreate the temp table, and get its count - mDriver.cursorRequeried(this); mCount = NO_COUNT; - try { - mQuery.requery(); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } - } - if (false) { - Log.v("DatabaseWindow", "closing window in requery()"); - Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); + mDriver.cursorRequeried(this); } - boolean result = false; try { - result = super.requery(); + return super.requery(); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); + return false; } - if (false) { - long timeEnd = System.currentTimeMillis(); - Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); - } - return result; } @Override @@ -330,20 +259,17 @@ public class SQLiteCursor extends AbstractWindowedCursor { // if the cursor hasn't been closed yet, close it first if (mWindow != null) { if (mStackTrace != null) { - int len = mQuery.mSql.length(); + String sql = mQuery.getSql(); + int len = sql.length(); StrictMode.onSqliteObjectLeaked( "Finalizing a Cursor that has not been deactivated or closed. " + - "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + - ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len), + "database = " + mQuery.getDatabase().getLabel() + + ", table = " + mEditTable + + ", query = " + sql.substring(0, (len > 1000) ? 1000 : len), mStackTrace); } close(); SQLiteDebug.notifyActiveCursorFinalized(); - } else { - if (false) { - Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + - ", table = " + mEditTable + ", query = " + mQuery.mSql); - } } } finally { super.finalize(); diff --git a/core/java/android/database/sqlite/SQLiteCursorDriver.java b/core/java/android/database/sqlite/SQLiteCursorDriver.java index b3963f9..ad2cdd2 100644 --- a/core/java/android/database/sqlite/SQLiteCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteCursorDriver.java @@ -39,7 +39,7 @@ public interface SQLiteCursorDriver { void cursorDeactivated(); /** - * Called by a SQLiteCursor when it is requeryed. + * Called by a SQLiteCursor when it is requeried. */ void cursorRequeried(Cursor cursor); diff --git a/core/java/android/database/sqlite/SQLiteCustomFunction.java b/core/java/android/database/sqlite/SQLiteCustomFunction.java new file mode 100644 index 0000000..02f3284 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteCustomFunction.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Describes a custom SQL function. + * + * @hide + */ +public final class SQLiteCustomFunction { + public final String name; + public final int numArgs; + public final SQLiteDatabase.CustomFunction callback; + + /** + * Create custom function. + * + * @param name The name of the sqlite3 function. + * @param numArgs The number of arguments for the function, or -1 to + * support any number of arguments. + * @param callback The callback to invoke when the function is executed. + */ + public SQLiteCustomFunction(String name, int numArgs, + SQLiteDatabase.CustomFunction callback) { + if (name == null) { + throw new IllegalArgumentException("name must not be null."); + } + + this.name = name; + this.numArgs = numArgs; + this.callback = callback; + } + + // Called from native. + @SuppressWarnings("unused") + private void dispatchCallback(String[] args) { + callback.callback(args); + } +} diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index f990be6..377a680 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,7 +16,6 @@ package android.database.sqlite; -import android.app.AppGlobals; import android.content.ContentValues; import android.content.res.Resources; import android.database.Cursor; @@ -25,61 +24,117 @@ import android.database.DatabaseUtils; import android.database.DefaultDatabaseErrorHandler; import android.database.SQLException; import android.database.sqlite.SQLiteDebug.DbStats; -import android.os.Debug; -import android.os.StatFs; -import android.os.SystemClock; -import android.os.SystemProperties; +import android.os.Looper; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; -import android.util.LruCache; import android.util.Pair; -import dalvik.system.BlockGuard; +import android.util.Printer; + +import dalvik.system.CloseGuard; + import java.io.File; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Random; import java.util.WeakHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; -import java.util.regex.Pattern; /** * Exposes methods to manage a SQLite database. - * <p>SQLiteDatabase has methods to create, delete, execute SQL commands, and + * + * <p> + * SQLiteDatabase has methods to create, delete, execute SQL commands, and * perform other common database management tasks. - * <p>See the Notepad sample application in the SDK for an example of creating + * </p><p> + * See the Notepad sample application in the SDK for an example of creating * and managing a database. - * <p> Database names must be unique within an application, not across all - * applications. + * </p><p> + * Database names must be unique within an application, not across all applications. + * </p> * * <h3>Localized Collation - ORDER BY</h3> - * <p>In addition to SQLite's default <code>BINARY</code> collator, Android supplies - * two more, <code>LOCALIZED</code>, which changes with the system's current locale - * if you wire it up correctly (XXX a link needed!), and <code>UNICODE</code>, which - * is the Unicode Collation Algorithm and not tailored to the current locale. + * <p> + * In addition to SQLite's default <code>BINARY</code> collator, Android supplies + * two more, <code>LOCALIZED</code>, which changes with the system's current locale, + * and <code>UNICODE</code>, which is the Unicode Collation Algorithm and not tailored + * to the current locale. + * </p> */ public class SQLiteDatabase extends SQLiteClosable { private static final String TAG = "SQLiteDatabase"; - private static final boolean ENABLE_DB_SAMPLE = false; // true to enable stats in event log - private static final int EVENT_DB_OPERATION = 52000; + private static final int EVENT_DB_CORRUPT = 75004; - /** - * Algorithms used in ON CONFLICT clause - * http://www.sqlite.org/lang_conflict.html - */ - /** - * When a constraint violation occurs, an immediate ROLLBACK occurs, + // Stores reference to all databases opened in the current process. + // (The referent Object is not used at this time.) + // INVARIANT: Guarded by sActiveDatabases. + private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases = + new WeakHashMap<SQLiteDatabase, Object>(); + + // Thread-local for database sessions that belong to this database. + // Each thread has its own database session. + // INVARIANT: Immutable. + private final ThreadLocal<SQLiteSession> mThreadSession = new ThreadLocal<SQLiteSession>() { + @Override + protected SQLiteSession initialValue() { + return createSession(); + } + }; + + // The optional factory to use when creating new Cursors. May be null. + // INVARIANT: Immutable. + private final CursorFactory mCursorFactory; + + // Error handler to be used when SQLite returns corruption errors. + // INVARIANT: Immutable. + private final DatabaseErrorHandler mErrorHandler; + + // Shared database state lock. + // This lock guards all of the shared state of the database, such as its + // configuration, whether it is open or closed, and so on. This lock should + // be held for as little time as possible. + // + // The lock MUST NOT be held while attempting to acquire database connections or + // while executing SQL statements on behalf of the client as it can lead to deadlock. + // + // It is ok to hold the lock while reconfiguring the connection pool or dumping + // statistics because those operations are non-reentrant and do not try to acquire + // connections that might be held by other threads. + // + // Basic rule: grab the lock, access or modify global state, release the lock, then + // do the required SQL work. + private final Object mLock = new Object(); + + // Warns if the database is finalized without being closed properly. + // INVARIANT: Guarded by mLock. + private final CloseGuard mCloseGuardLocked = CloseGuard.get(); + + // The database configuration. + // INVARIANT: Guarded by mLock. + private final SQLiteDatabaseConfiguration mConfigurationLocked; + + // The connection pool for the database, null when closed. + // The pool itself is thread-safe, but the reference to it can only be acquired + // when the lock is held. + // INVARIANT: Guarded by mLock. + private SQLiteConnectionPool mConnectionPoolLocked; + + // True if the database has attached databases. + // INVARIANT: Guarded by mLock. + private boolean mHasAttachedDbsLocked; + + // True if the database is in WAL mode. + // INVARIANT: Guarded by mLock. + private boolean mIsWALEnabledLocked; + + /** + * When a constraint violation occurs, an immediate ROLLBACK occurs, * thus ending the current transaction, and the command aborts with a * return code of SQLITE_CONSTRAINT. If no transaction is active * (other than the implied transaction that is created on every command) - * then this algorithm works the same as ABORT. + * then this algorithm works the same as ABORT. */ public static final int CONFLICT_ROLLBACK = 1; @@ -118,14 +173,15 @@ public class SQLiteDatabase extends SQLiteClosable { * violation occurs then the IGNORE algorithm is used. When this conflict * resolution strategy deletes rows in order to satisfy a constraint, * it does not invoke delete triggers on those rows. - * This behavior might change in a future release. + * This behavior might change in a future release. */ public static final int CONFLICT_REPLACE = 5; /** - * use the following when no conflict action is specified. + * Use the following when no conflict action is specified. */ public static final int CONFLICT_NONE = 0; + private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; @@ -146,7 +202,7 @@ public class SQLiteDatabase extends SQLiteClosable { public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000; /** - * Flag for {@link #openDatabase} to open the database for reading and writing. + * Open flag: Flag for {@link #openDatabase} to open the database for reading and writing. * If the disk is full, this may fail even before you actually write anything. * * {@more} Note that the value of this flag is 0, so it is the default. @@ -154,7 +210,7 @@ public class SQLiteDatabase extends SQLiteClosable { public static final int OPEN_READWRITE = 0x00000000; // update native code if changing /** - * Flag for {@link #openDatabase} to open the database for reading only. + * Open flag: Flag for {@link #openDatabase} to open the database for reading only. * This is the only reliable way to open a database if the disk may be full. */ public static final int OPEN_READONLY = 0x00000001; // update native code if changing @@ -162,7 +218,8 @@ public class SQLiteDatabase extends SQLiteClosable { private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing /** - * Flag for {@link #openDatabase} to open the database without support for localized collators. + * Open flag: Flag for {@link #openDatabase} to open the database without support for + * localized collators. * * {@more} This causes the collator <code>LOCALIZED</code> not to be created. * You must be consistent when using this flag to use the setting the database was @@ -171,190 +228,62 @@ public class SQLiteDatabase extends SQLiteClosable { public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing /** - * Flag for {@link #openDatabase} to create the database file if it does not already exist. + * Open flag: Flag for {@link #openDatabase} to create the database file if it does not + * already exist. */ public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing /** - * Indicates whether the most-recently started transaction has been marked as successful. - */ - private boolean mInnerTransactionIsSuccessful; - - /** - * Valid during the life of a transaction, and indicates whether the entire transaction (the - * outer one and all of the inner ones) so far has been successful. - */ - private boolean mTransactionIsSuccessful; - - /** - * Valid during the life of a transaction. - */ - private SQLiteTransactionListener mTransactionListener; - - /** - * this member is set if {@link #execSQL(String)} is used to begin and end transactions. - */ - private boolean mTransactionUsingExecSql; - - /** Synchronize on this when accessing the database */ - private final DatabaseReentrantLock mLock = new DatabaseReentrantLock(true); - - private long mLockAcquiredWallTime = 0L; - private long mLockAcquiredThreadTime = 0L; - - // limit the frequency of complaints about each database to one within 20 sec - // unless run command adb shell setprop log.tag.Database VERBOSE - private static final int LOCK_WARNING_WINDOW_IN_MS = 20000; - /** If the lock is held this long then a warning will be printed when it is released. */ - private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300; - private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100; - private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000; - - private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000; - - // The pattern we remove from database filenames before - // potentially logging them. - private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); - - private long mLastLockMessageTime = 0L; - - // Things related to query logging/sampling for debugging - // slow/frequent queries during development. Always log queries - // which take (by default) 500ms+; shorter queries are sampled - // accordingly. Commit statements, which are typically slow, are - // logged together with the most recently executed SQL statement, - // for disambiguation. The 500ms value is configurable via a - // SystemProperty, but developers actively debugging database I/O - // should probably use the regular log tunable, - // LOG_SLOW_QUERIES_PROPERTY, defined below. - private static int sQueryLogTimeInMillis = 0; // lazily initialized - private static final int QUERY_LOG_SQL_LENGTH = 64; - private static final String COMMIT_SQL = "COMMIT;"; - private static final String BEGIN_SQL = "BEGIN;"; - private final Random mRandom = new Random(); - /** the last non-commit/rollback sql statement in a transaction */ - // guarded by 'this' - private String mLastSqlStatement = null; - - synchronized String getLastSqlStatement() { - return mLastSqlStatement; - } - - synchronized void setLastSqlStatement(String sql) { - mLastSqlStatement = sql; - } - - /** guarded by {@link #mLock} */ - private long mTransStartTime; - - // String prefix for slow database query EventLog records that show - // lock acquistions of the database. - /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; - - /** Used by native code, do not rename. make it volatile, so it is thread-safe. */ - /* package */ volatile int mNativeHandle = 0; - - /** - * The size, in bytes, of a block on "/data". This corresponds to the Unix - * statfs.f_bsize field. note that this field is lazily initialized. - */ - private static int sBlockSize = 0; - - /** The path for the database file */ - private final String mPath; - - /** The anonymized path for the database file for logging purposes */ - private String mPathForLogs = null; // lazily populated - - /** The flags passed to open/create */ - private final int mFlags; - - /** The optional factory to use when creating new Cursors */ - private final CursorFactory mFactory; - - private final WeakHashMap<SQLiteClosable, Object> mPrograms; - - /** Default statement-cache size per database connection ( = instance of this class) */ - private static final int DEFAULT_SQL_CACHE_SIZE = 25; - - /** - * for each instance of this class, a LRU cache is maintained to store - * the compiled query statement ids returned by sqlite database. - * key = SQL statement with "?" for bind args - * value = {@link SQLiteCompiledSql} - * If an application opens the database and keeps it open during its entire life, then - * there will not be an overhead of compilation of SQL statements by sqlite. + * Absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}. * - * why is this cache NOT static? because sqlite attaches compiledsql statements to the - * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is - * invoked. - * - * this cache's max size is settable by calling the method - * (@link #setMaxSqlCacheSize(int)}. - */ - // guarded by this - private LruCache<String, SQLiteCompiledSql> mCompiledQueries; - - /** - * absolute max value that can be set by {@link #setMaxSqlCacheSize(int)} - * size of each prepared-statement is between 1K - 6K, depending on the complexity of the - * SQL statement & schema. + * Each prepared-statement is between 1K - 6K, depending on the complexity of the + * SQL statement & schema. A large SQL cache may use a significant amount of memory. */ public static final int MAX_SQL_CACHE_SIZE = 100; - private boolean mCacheFullWarning; - - /** Used to find out where this object was created in case it never got closed. */ - private final Throwable mStackTrace; - - /** stores the list of statement ids that need to be finalized by sqlite */ - private final ArrayList<Integer> mClosedStatementIds = new ArrayList<Integer>(); - - /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors - * Corruption - * */ - private final DatabaseErrorHandler mErrorHandler; - - /** The Database connection pool {@link DatabaseConnectionPool}. - * Visibility is package-private for testing purposes. otherwise, private visibility is enough. - */ - /* package */ volatile DatabaseConnectionPool mConnectionPool = null; - - /** Each database connection handle in the pool is assigned a number 1..N, where N is the - * size of the connection pool. - * The main connection handle to which the pool is attached is assigned a value of 0. - */ - /* package */ final short mConnectionNum; - - /** on pooled database connections, this member points to the parent ( = main) - * database connection handle. - * package visibility only for testing purposes - */ - /* package */ SQLiteDatabase mParentConnObj = null; - - private static final String MEMORY_DB_PATH = ":memory:"; - /** set to true if the database has attached databases */ - private volatile boolean mHasAttachedDbs = false; - - /** stores reference to all databases opened in the current process. */ - private static ArrayList<WeakReference<SQLiteDatabase>> mActiveDatabases = - new ArrayList<WeakReference<SQLiteDatabase>>(); - - synchronized void addSQLiteClosable(SQLiteClosable closable) { - // mPrograms is per instance of SQLiteDatabase and it doesn't actually touch the database - // itself. so, there is no need to lock(). - mPrograms.put(closable, null); + private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory, + DatabaseErrorHandler errorHandler) { + mCursorFactory = cursorFactory; + mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler(); + mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags); } - synchronized void removeSQLiteClosable(SQLiteClosable closable) { - mPrograms.remove(closable); + @Override + protected void finalize() throws Throwable { + try { + dispose(true); + } finally { + super.finalize(); + } } @Override protected void onAllReferencesReleased() { - if (isOpen()) { - // close the database which will close all pending statements to be finalized also - close(); + dispose(false); + } + + private void dispose(boolean finalized) { + final SQLiteConnectionPool pool; + synchronized (mLock) { + if (mCloseGuardLocked != null) { + if (finalized) { + mCloseGuardLocked.warnIfOpen(); + } + mCloseGuardLocked.close(); + } + + pool = mConnectionPoolLocked; + mConnectionPoolLocked = null; + } + + if (!finalized) { + synchronized (sActiveDatabases) { + sActiveDatabases.remove(this); + } + + if (pool != null) { + pool.close(); + } } } @@ -364,7 +293,9 @@ public class SQLiteDatabase extends SQLiteClosable { * * @return the number of bytes actually released */ - static public native int releaseMemory(); + public static int releaseMemory() { + return SQLiteGlobal.releaseMemory(); + } /** * Control whether or not the SQLiteDatabase is made thread-safe by using locks @@ -372,159 +303,82 @@ public class SQLiteDatabase extends SQLiteClosable { * DB will only be used by a single thread then you should set this to false. * The default is true. * @param lockingEnabled set to true to enable locks, false otherwise + * + * @deprecated This method now does nothing. Do not use. */ + @Deprecated public void setLockingEnabled(boolean lockingEnabled) { - mLockingEnabled = lockingEnabled; } /** - * If set then the SQLiteDatabase is made thread-safe by using locks - * around critical sections + * Gets a label to use when describing the database in log messages. + * @return The label. */ - private boolean mLockingEnabled = true; - - /* package */ void onCorruption() { - EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); - mErrorHandler.onCorruption(this); + String getLabel() { + synchronized (mLock) { + return mConfigurationLocked.label; + } } /** - * Locks the database for exclusive access. The database lock must be held when - * touch the native sqlite3* object since it is single threaded and uses - * a polling lock contention algorithm. The lock is recursive, and may be acquired - * multiple times by the same thread. This is a no-op if mLockingEnabled is false. - * - * @see #unlock() + * Sends a corruption message to the database error handler. */ - /* package */ void lock(String sql) { - lock(sql, false); - } - - /* pachage */ void lock() { - lock(null, false); - } - - private static final long LOCK_WAIT_PERIOD = 30L; - private void lock(String sql, boolean forced) { - // make sure this method is NOT being called from a 'synchronized' method - if (Thread.holdsLock(this)) { - Log.w(TAG, "don't lock() while in a synchronized method"); - } - verifyDbIsOpen(); - if (!forced && !mLockingEnabled) return; - boolean done = false; - long timeStart = SystemClock.uptimeMillis(); - while (!done) { - try { - // wait for 30sec to acquire the lock - done = mLock.tryLock(LOCK_WAIT_PERIOD, TimeUnit.SECONDS); - if (!done) { - // lock not acquired in NSec. print a message and stacktrace saying the lock - // has not been available for 30sec. - Log.w(TAG, "database lock has not been available for " + LOCK_WAIT_PERIOD + - " sec. Current Owner of the lock is " + mLock.getOwnerDescription() + - ". Continuing to wait in thread: " + Thread.currentThread().getId()); - } - } catch (InterruptedException e) { - // ignore the interruption - } - } - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - // Use elapsed real-time since the CPU may sleep when waiting for IO - mLockAcquiredWallTime = SystemClock.elapsedRealtime(); - mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); - } - } - if (sql != null) { - if (ENABLE_DB_SAMPLE) { - logTimeStat(sql, timeStart, GET_LOCK_LOG_PREFIX); - } - } - } - private static class DatabaseReentrantLock extends ReentrantLock { - DatabaseReentrantLock(boolean fair) { - super(fair); - } - @Override - public Thread getOwner() { - return super.getOwner(); - } - public String getOwnerDescription() { - Thread t = getOwner(); - return (t== null) ? "none" : String.valueOf(t.getId()); - } + void onCorruption() { + EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel()); + mErrorHandler.onCorruption(this); } /** - * Locks the database for exclusive access. The database lock must be held when - * touch the native sqlite3* object since it is single threaded and uses - * a polling lock contention algorithm. The lock is recursive, and may be acquired - * multiple times by the same thread. + * Gets the {@link SQLiteSession} that belongs to this thread for this database. + * Once a thread has obtained a session, it will continue to obtain the same + * session even after the database has been closed (although the session will not + * be usable). However, a thread that does not already have a session cannot + * obtain one after the database has been closed. * - * @see #unlockForced() + * The idea is that threads that have active connections to the database may still + * have work to complete even after the call to {@link #close}. Active database + * connections are not actually disposed until they are released by the threads + * that own them. + * + * @return The session, never null. + * + * @throws IllegalStateException if the thread does not yet have a session and + * the database is not open. */ - private void lockForced() { - lock(null, true); - } - - private void lockForced(String sql) { - lock(sql, true); + SQLiteSession getThreadSession() { + return mThreadSession.get(); // initialValue() throws if database closed } - /** - * Releases the database lock. This is a no-op if mLockingEnabled is false. - * - * @see #unlock() - */ - /* package */ void unlock() { - if (!mLockingEnabled) return; - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - checkLockHoldTime(); - } + SQLiteSession createSession() { + final SQLiteConnectionPool pool; + synchronized (mLock) { + throwIfNotOpenLocked(); + pool = mConnectionPoolLocked; } - mLock.unlock(); + return new SQLiteSession(pool); } /** - * Releases the database lock. + * Gets default connection flags that are appropriate for this thread, taking into + * account whether the thread is acting on behalf of the UI. * - * @see #unlockForced() + * @param readOnly True if the connection should be read-only. + * @return The connection flags. */ - private void unlockForced() { - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) { - if (mLock.getHoldCount() == 1) { - checkLockHoldTime(); - } + int getThreadDefaultConnectionFlags(boolean readOnly) { + int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY : + SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY; + if (isMainThread()) { + flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE; } - mLock.unlock(); + return flags; } - private void checkLockHoldTime() { - // Use elapsed real-time since the CPU may sleep when waiting for IO - long elapsedTime = SystemClock.elapsedRealtime(); - long lockedTime = elapsedTime - mLockAcquiredWallTime; - if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT && - !Log.isLoggable(TAG, Log.VERBOSE) && - (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) { - return; - } - if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) { - int threadTime = (int) - ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000); - if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS || - lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) { - mLastLockMessageTime = elapsedTime; - String msg = "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was " - + threadTime + "ms"; - if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) { - Log.d(TAG, msg, new Exception()); - } else { - Log.d(TAG, msg); - } - } - } + private static boolean isMainThread() { + // FIXME: There should be a better way to do this. + // Would also be nice to have something that would work across Binder calls. + Looper looper = Looper.myLooper(); + return looper != null && looper == Looper.getMainLooper(); } /** @@ -636,50 +490,9 @@ public class SQLiteDatabase extends SQLiteClosable { private void beginTransaction(SQLiteTransactionListener transactionListener, boolean exclusive) { - verifyDbIsOpen(); - lockForced(BEGIN_SQL); - boolean ok = false; - try { - // If this thread already had the lock then get out - if (mLock.getHoldCount() > 1) { - if (mInnerTransactionIsSuccessful) { - String msg = "Cannot call beginTransaction between " - + "calling setTransactionSuccessful and endTransaction"; - IllegalStateException e = new IllegalStateException(msg); - Log.e(TAG, "beginTransaction() failed", e); - throw e; - } - ok = true; - return; - } - - // This thread didn't already have the lock, so begin a database - // transaction now. - if (exclusive && mConnectionPool == null) { - execSQL("BEGIN EXCLUSIVE;"); - } else { - execSQL("BEGIN IMMEDIATE;"); - } - mTransStartTime = SystemClock.uptimeMillis(); - mTransactionListener = transactionListener; - mTransactionIsSuccessful = true; - mInnerTransactionIsSuccessful = false; - if (transactionListener != null) { - try { - transactionListener.onBegin(); - } catch (RuntimeException e) { - execSQL("ROLLBACK;"); - throw e; - } - } - ok = true; - } finally { - if (!ok) { - // beginTransaction is called before the try block so we must release the lock in - // the case of failure. - unlockForced(); - } - } + getThreadSession().beginTransaction(exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE : + SQLiteSession.TRANSACTION_MODE_IMMEDIATE, transactionListener, + getThreadDefaultConnectionFlags(false /*readOnly*/)); } /** @@ -687,68 +500,7 @@ public class SQLiteDatabase extends SQLiteClosable { * are committed and rolled back. */ public void endTransaction() { - verifyLockOwner(); - try { - if (mInnerTransactionIsSuccessful) { - mInnerTransactionIsSuccessful = false; - } else { - mTransactionIsSuccessful = false; - } - if (mLock.getHoldCount() != 1) { - return; - } - RuntimeException savedException = null; - if (mTransactionListener != null) { - try { - if (mTransactionIsSuccessful) { - mTransactionListener.onCommit(); - } else { - mTransactionListener.onRollback(); - } - } catch (RuntimeException e) { - savedException = e; - mTransactionIsSuccessful = false; - } - } - if (mTransactionIsSuccessful) { - execSQL(COMMIT_SQL); - // if write-ahead logging is used, we have to take care of checkpoint. - // TODO: should applications be given the flexibility of choosing when to - // trigger checkpoint? - // for now, do checkpoint after every COMMIT because that is the fastest - // way to guarantee that readers will see latest data. - // but this is the slowest way to run sqlite with in write-ahead logging mode. - if (this.mConnectionPool != null) { - execSQL("PRAGMA wal_checkpoint;"); - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.i(TAG, "PRAGMA wal_Checkpoint done"); - } - } - // log the transaction time to the Eventlog. - if (ENABLE_DB_SAMPLE) { - logTimeStat(getLastSqlStatement(), mTransStartTime, COMMIT_SQL); - } - } else { - try { - execSQL("ROLLBACK;"); - if (savedException != null) { - throw savedException; - } - } catch (SQLException e) { - if (false) { - Log.d(TAG, "exception during rollback, maybe the DB previously " - + "performed an auto-rollback"); - } - } - } - } finally { - mTransactionListener = null; - unlockForced(); - if (false) { - Log.v(TAG, "unlocked " + Thread.currentThread() - + ", holdCount is " + mLock.getHoldCount()); - } - } + getThreadSession().endTransaction(); } /** @@ -761,86 +513,46 @@ public class SQLiteDatabase extends SQLiteClosable { * transaction is already marked as successful. */ public void setTransactionSuccessful() { - verifyDbIsOpen(); - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("no transaction pending"); - } - if (mInnerTransactionIsSuccessful) { - throw new IllegalStateException( - "setTransactionSuccessful may only be called once per call to beginTransaction"); - } - mInnerTransactionIsSuccessful = true; + getThreadSession().setTransactionSuccessful(); } /** - * return true if there is a transaction pending + * Returns true if the current thread has a transaction pending. + * + * @return True if the current thread is in a transaction. */ public boolean inTransaction() { - return mLock.getHoldCount() > 0 || mTransactionUsingExecSql; - } - - /* package */ synchronized void setTransactionUsingExecSqlFlag() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.i(TAG, "found execSQL('begin transaction')"); - } - mTransactionUsingExecSql = true; - } - - /* package */ synchronized void resetTransactionUsingExecSqlFlag() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - if (mTransactionUsingExecSql) { - Log.i(TAG, "found execSQL('commit or end or rollback')"); - } - } - mTransactionUsingExecSql = false; + return getThreadSession().hasTransaction(); } /** - * Returns true if the caller is considered part of the current transaction, if any. + * Returns true if the current thread is holding an active connection to the database. * <p> - * Caller is part of the current transaction if either of the following is true - * <ol> - * <li>If transaction is started by calling beginTransaction() methods AND if the caller is - * in the same thread as the thread that started the transaction. - * </li> - * <li>If the transaction is started by calling {@link #execSQL(String)} like this: - * execSQL("BEGIN transaction"). In this case, every thread in the process is considered - * part of the current transaction.</li> - * </ol> - * - * @return true if the caller is considered part of the current transaction, if any. - */ - /* package */ synchronized boolean amIInTransaction() { - // always do this test on the main database connection - NOT on pooled database connection - // since transactions always occur on the main database connections only. - SQLiteDatabase db = (isPooledConnection()) ? mParentConnObj : this; - boolean b = (!db.inTransaction()) ? false : - db.mTransactionUsingExecSql || db.mLock.isHeldByCurrentThread(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.i(TAG, "amIinTransaction: " + b); - } - return b; - } - - /** - * Checks if the database lock is held by this thread. + * The name of this method comes from a time when having an active connection + * to the database meant that the thread was holding an actual lock on the + * database. Nowadays, there is no longer a true "database lock" although threads + * may block if they cannot acquire a database connection to perform a + * particular operation. + * </p> * - * @return true, if this thread is holding the database lock. + * @return True if the current thread is holding an active connection to the database. */ public boolean isDbLockedByCurrentThread() { - return mLock.isHeldByCurrentThread(); + return getThreadSession().hasConnection(); } /** - * Checks if the database is locked by another thread. This is - * just an estimate, since this status can change at any time, - * including after the call is made but before the result has - * been acted upon. + * Always returns false. + * <p> + * There is no longer the concept of a database lock, so this method always returns false. + * </p> * - * @return true, if the database is locked by another thread + * @return False. + * @deprecated Always returns false. Do not use this method. */ + @Deprecated public boolean isDbLockedByOtherThreads() { - return !mLock.isHeldByCurrentThread() && mLock.isLocked(); + return false; } /** @@ -884,46 +596,12 @@ public class SQLiteDatabase extends SQLiteClosable { return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay); } - private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) { - if (mLock.getQueueLength() == 0) { - // Reset the lock acquire time since we know that the thread was willing to yield - // the lock at this time. - mLockAcquiredWallTime = SystemClock.elapsedRealtime(); - mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); - return false; - } - setTransactionSuccessful(); - SQLiteTransactionListener transactionListener = mTransactionListener; - endTransaction(); - if (checkFullyYielded) { - if (this.isDbLockedByCurrentThread()) { - throw new IllegalStateException( - "Db locked more than once. yielfIfContended cannot yield"); - } - } - if (sleepAfterYieldDelay > 0) { - // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to - // check if anyone is using the database. If the database is not contended, - // retake the lock and return. - long remainingDelay = sleepAfterYieldDelay; - while (remainingDelay > 0) { - try { - Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ? - remainingDelay : SLEEP_AFTER_YIELD_QUANTUM); - } catch (InterruptedException e) { - Thread.interrupted(); - } - remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM; - if (mLock.getQueueLength() == 0) { - break; - } - } - } - beginTransactionWithListener(transactionListener); - return true; + private boolean yieldIfContendedHelper(boolean throwIfUnsafe, long sleepAfterYieldDelay) { + return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe); } /** + * Deprecated. * @deprecated This method no longer serves any useful purpose and has been deprecated. */ @Deprecated @@ -932,19 +610,6 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Used to allow returning sub-classes of {@link Cursor} when calling query. - */ - public interface CursorFactory { - /** - * See - * {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}. - */ - public Cursor newCursor(SQLiteDatabase db, - SQLiteCursorDriver masterQuery, String editTable, - SQLiteQuery query); - } - - /** * Open the database according to the flags {@link #OPEN_READWRITE} * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. * @@ -983,50 +648,9 @@ public class SQLiteDatabase extends SQLiteClosable { */ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) { - SQLiteDatabase sqliteDatabase = openDatabase(path, factory, flags, errorHandler, - (short) 0 /* the main connection handle */); - - // set sqlite pagesize to mBlockSize - if (sBlockSize == 0) { - // TODO: "/data" should be a static final String constant somewhere. it is hardcoded - // in several places right now. - sBlockSize = new StatFs("/data").getBlockSize(); - } - sqliteDatabase.setPageSize(sBlockSize); - sqliteDatabase.setJournalMode(path, "TRUNCATE"); - - // add this database to the list of databases opened in this process - synchronized(mActiveDatabases) { - mActiveDatabases.add(new WeakReference<SQLiteDatabase>(sqliteDatabase)); - } - return sqliteDatabase; - } - - private static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, - DatabaseErrorHandler errorHandler, short connectionNum) { - SQLiteDatabase db = new SQLiteDatabase(path, factory, flags, errorHandler, connectionNum); - try { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.i(TAG, "opening the db : " + path); - } - // Open the database. - db.dbopen(path, flags); - db.setLocale(Locale.getDefault()); - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - db.enableSqlTracing(path, connectionNum); - } - if (SQLiteDebug.DEBUG_SQL_TIME) { - db.enableSqlProfiling(path, connectionNum); - } - return db; - } catch (SQLiteDatabaseCorruptException e) { - db.mErrorHandler.onCorruption(db); - return SQLiteDatabase.openDatabase(path, factory, flags, errorHandler); - } catch (SQLiteException e) { - Log.e(TAG, "Failed to open the database. closing it.", e); - db.close(); - throw e; - } + SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler); + db.open(); + return db; } /** @@ -1051,16 +675,46 @@ public class SQLiteDatabase extends SQLiteClosable { return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler); } - private void setJournalMode(final String dbPath, final String mode) { - // journal mode can be set only for non-memory databases + private void open() { + try { + try { + openInner(); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + openInner(); + } + + // Disable WAL if it was previously enabled. + setJournalMode("TRUNCATE"); + } catch (SQLiteException ex) { + Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex); + close(); + throw ex; + } + } + + private void openInner() { + synchronized (mLock) { + assert mConnectionPoolLocked == null; + mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked); + mCloseGuardLocked.open("close"); + } + + synchronized (sActiveDatabases) { + sActiveDatabases.put(this, null); + } + } + + private void setJournalMode(String mode) { + // Journal mode can be set only for non-memory databases // AND can't be set for readonly databases - if (dbPath.equalsIgnoreCase(MEMORY_DB_PATH) || isReadOnly()) { + if (isInMemoryDatabase() || isReadOnly()) { return; } String s = DatabaseUtils.stringForQuery(this, "PRAGMA journal_mode=" + mode, null); if (!s.equalsIgnoreCase(mode)) { - Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + dbPath + - " (on pragma set journal_mode, sqlite returned:" + s); + Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + getLabel() + + " (on pragma set journal_mode, sqlite returned:" + s); } } @@ -1077,159 +731,37 @@ public class SQLiteDatabase extends SQLiteClosable { */ public static SQLiteDatabase create(CursorFactory factory) { // This is a magic string with special meaning for SQLite. - return openDatabase(MEMORY_DB_PATH, factory, CREATE_IF_NECESSARY); + return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH, + factory, CREATE_IF_NECESSARY); } /** * Close the database. */ public void close() { - if (!isOpen()) { - return; - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.i(TAG, "closing db: " + mPath + " (connection # " + mConnectionNum); - } - lock(); - try { - // some other thread could have closed this database while I was waiting for lock. - // check the database state - if (!isOpen()) { - return; - } - closeClosable(); - // finalize ALL statements queued up so far - closePendingStatements(); - releaseCustomFunctions(); - // close this database instance - regardless of its reference count value - closeDatabase(); - if (mConnectionPool != null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert mConnectionPool != null; - Log.i(TAG, mConnectionPool.toString()); - } - mConnectionPool.close(); - } - } finally { - unlock(); - } - } - - private void closeClosable() { - /* deallocate all compiled SQL statement objects from mCompiledQueries cache. - * this should be done before de-referencing all {@link SQLiteClosable} objects - * from this database object because calling - * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database - * to be closed. sqlite doesn't let a database close if there are - * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries. - */ - deallocCachedSqlStatements(); - - Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry<SQLiteClosable, Object> entry = iter.next(); - SQLiteClosable program = entry.getKey(); - if (program != null) { - program.onAllReferencesReleasedFromContainer(); - } - } - } - - /** - * package level access for testing purposes - */ - /* package */ void closeDatabase() throws SQLiteException { - try { - dbclose(); - } catch (SQLiteUnfinalizedObjectsException e) { - String msg = e.getMessage(); - String[] tokens = msg.split(",", 2); - int stmtId = Integer.parseInt(tokens[0]); - // get extra info about this statement, if it is still to be released by closeClosable() - Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator(); - boolean found = false; - while (iter.hasNext()) { - Map.Entry<SQLiteClosable, Object> entry = iter.next(); - SQLiteClosable program = entry.getKey(); - if (program != null && program instanceof SQLiteProgram) { - SQLiteCompiledSql compiledSql = ((SQLiteProgram)program).mCompiledSql; - if (compiledSql.nStatement == stmtId) { - msg = compiledSql.toString(); - found = true; - } - } - } - if (!found) { - // the statement is already released by closeClosable(). is it waiting to be - // finalized? - if (mClosedStatementIds.contains(stmtId)) { - Log.w(TAG, "this shouldn't happen. finalizing the statement now: "); - closePendingStatements(); - // try to close the database again - closeDatabase(); - } - } else { - // the statement is not yet closed. most probably programming error in the app. - throw new SQLiteUnfinalizedObjectsException( - "close() on database: " + getPath() + - " failed due to un-close()d SQL statements: " + msg); - } - } - } - - /** - * Native call to close the database. - */ - private native void dbclose(); - - /** - * A callback interface for a custom sqlite3 function. - * This can be used to create a function that can be called from - * sqlite3 database triggers. - * @hide - */ - public interface CustomFunction { - public void callback(String[] args); + dispose(false); } /** * Registers a CustomFunction callback as a function that can be called from - * sqlite3 database triggers. + * SQLite database triggers. + * * @param name the name of the sqlite3 function * @param numArgs the number of arguments for the function * @param function callback to call when the function is executed * @hide */ public void addCustomFunction(String name, int numArgs, CustomFunction function) { - verifyDbIsOpen(); - synchronized (mCustomFunctions) { - int ref = native_addCustomFunction(name, numArgs, function); - if (ref != 0) { - // save a reference to the function for cleanup later - mCustomFunctions.add(new Integer(ref)); - } else { - throw new SQLiteException("failed to add custom function " + name); - } - } - } + // Create wrapper (also validates arguments). + SQLiteCustomFunction wrapper = new SQLiteCustomFunction(name, numArgs, function); - private void releaseCustomFunctions() { - synchronized (mCustomFunctions) { - for (int i = 0; i < mCustomFunctions.size(); i++) { - Integer function = mCustomFunctions.get(i); - native_releaseCustomFunction(function.intValue()); - } - mCustomFunctions.clear(); + synchronized (mLock) { + throwIfNotOpenLocked(); + mConfigurationLocked.customFunctions.add(wrapper); + mConnectionPoolLocked.reconfigure(mConfigurationLocked); } } - // list of CustomFunction references so we can clean up when the database closes - private final ArrayList<Integer> mCustomFunctions = - new ArrayList<Integer>(); - - private native int native_addCustomFunction(String name, int numArgs, CustomFunction function); - private native void native_releaseCustomFunction(int function); - /** * Gets the database version. * @@ -1364,7 +896,7 @@ public class SQLiteDatabase extends SQLiteClosable { * {@link SQLiteStatement}s are not synchronized, see the documentation for more details. */ public SQLiteStatement compileStatement(String sql) throws SQLException { - verifyDbIsOpen(); + throwIfNotOpen(); // fail fast return new SQLiteStatement(this, sql, null); } @@ -1442,7 +974,7 @@ public class SQLiteDatabase extends SQLiteClosable { boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { - verifyDbIsOpen(); + throwIfNotOpen(); // fail fast String sql = SQLiteQueryBuilder.buildQueryString( distinct, table, columns, selection, groupBy, having, orderBy, limit); @@ -1553,21 +1085,11 @@ public class SQLiteDatabase extends SQLiteClosable { public Cursor rawQueryWithFactory( CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { - verifyDbIsOpen(); - BlockGuard.getThreadPolicy().onReadFromDisk(); + throwIfNotOpen(); // fail fast - SQLiteDatabase db = getDbConnection(sql); - SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable); - - Cursor cursor = null; - try { - cursor = driver.query( - cursorFactory != null ? cursorFactory : mFactory, - selectionArgs); - } finally { - releaseDbConnection(db); - } - return cursor; + SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable); + return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory, + selectionArgs); } /** @@ -1716,9 +1238,6 @@ public class SQLiteDatabase extends SQLiteClosable { SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs); try { return statement.executeInsert(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; } finally { statement.close(); } @@ -1739,9 +1258,6 @@ public class SQLiteDatabase extends SQLiteClosable { (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs); try { return statement.executeUpdateDelete(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; } finally { statement.close(); } @@ -1808,9 +1324,6 @@ public class SQLiteDatabase extends SQLiteClosable { SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs); try { return statement.executeUpdateDelete(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; } finally { statement.close(); } @@ -1891,264 +1404,105 @@ public class SQLiteDatabase extends SQLiteClosable { private int executeSql(String sql, Object[] bindArgs) throws SQLException { if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { - disableWriteAheadLogging(); - mHasAttachedDbs = true; + boolean disableWal = false; + synchronized (mLock) { + if (!mHasAttachedDbsLocked) { + mHasAttachedDbsLocked = true; + disableWal = true; + } + } + if (disableWal) { + disableWriteAheadLogging(); + } } + SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); try { return statement.executeUpdateDelete(); - } catch (SQLiteDatabaseCorruptException e) { - onCorruption(); - throw e; } finally { statement.close(); } } - @Override - protected void finalize() throws Throwable { - try { - if (isOpen()) { - Log.e(TAG, "close() was never explicitly called on database '" + - mPath + "' ", mStackTrace); - closeClosable(); - onAllReferencesReleased(); - releaseCustomFunctions(); - } - } finally { - super.finalize(); - } - } - /** - * Private constructor. + * Returns true if the database is opened as read only. * - * @param path The full path to the database - * @param factory The factory to use when creating cursors, may be NULL. - * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already - * exists, mFlags will be updated appropriately. - * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database - * corruption. may be NULL. - * @param connectionNum 0 for main database connection handle. 1..N for pooled database - * connection handles. + * @return True if database is opened as read only. */ - private SQLiteDatabase(String path, CursorFactory factory, int flags, - DatabaseErrorHandler errorHandler, short connectionNum) { - if (path == null) { - throw new IllegalArgumentException("path should not be null"); + public boolean isReadOnly() { + synchronized (mLock) { + return isReadOnlyLocked(); } - setMaxSqlCacheSize(DEFAULT_SQL_CACHE_SIZE); - mFlags = flags; - mPath = path; - mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - mFactory = factory; - mPrograms = new WeakHashMap<SQLiteClosable,Object>(); - // Set the DatabaseErrorHandler to be used when SQLite reports corruption. - // If the caller sets errorHandler = null, then use default errorhandler. - mErrorHandler = (errorHandler == null) ? new DefaultDatabaseErrorHandler() : errorHandler; - mConnectionNum = connectionNum; - /* sqlite soft heap limit http://www.sqlite.org/c3ref/soft_heap_limit64.html - * set it to 4 times the default cursor window size. - * TODO what is an appropriate value, considering the WAL feature which could burn - * a lot of memory with many connections to the database. needs testing to figure out - * optimal value for this. - */ - int limit = Resources.getSystem().getInteger( - com.android.internal.R.integer.config_cursorWindowSize) * 1024 * 4; - native_setSqliteSoftHeapLimit(limit); + } + + private boolean isReadOnlyLocked() { + return (mConfigurationLocked.openFlags & OPEN_READ_MASK) == OPEN_READONLY; } /** - * return whether the DB is opened as read only. - * @return true if DB is opened as read only + * Returns true if the database is in-memory db. + * + * @return True if the database is in-memory. + * @hide */ - public boolean isReadOnly() { - return (mFlags & OPEN_READ_MASK) == OPEN_READONLY; + public boolean isInMemoryDatabase() { + synchronized (mLock) { + return mConfigurationLocked.isInMemoryDb(); + } } /** - * @return true if the DB is currently open (has not been closed) + * Returns true if the database is currently open. + * + * @return True if the database is currently open (has not been closed). */ public boolean isOpen() { - return mNativeHandle != 0; + synchronized (mLock) { + return mConnectionPoolLocked != null; + } } + /** + * Returns true if the new version code is greater than the current database version. + * + * @param newVersion The new version code. + * @return True if the new version code is greater than the current database version. + */ public boolean needUpgrade(int newVersion) { return newVersion > getVersion(); } /** - * Getter for the path to the database file. + * Gets the path to the database file. * - * @return the path to our database file. + * @return The path to the database file. */ public final String getPath() { - return mPath; - } - - /* package */ void logTimeStat(String sql, long beginMillis) { - if (ENABLE_DB_SAMPLE) { - logTimeStat(sql, beginMillis, null); + synchronized (mLock) { + return mConfigurationLocked.path; } } - private void logTimeStat(String sql, long beginMillis, String prefix) { - // Sample fast queries in proportion to the time taken. - // Quantize the % first, so the logged sampling probability - // exactly equals the actual sampling rate for this query. - - int samplePercent; - long durationMillis = SystemClock.uptimeMillis() - beginMillis; - if (durationMillis == 0 && prefix == GET_LOCK_LOG_PREFIX) { - // The common case is locks being uncontended. Don't log those, - // even at 1%, which is our default below. - return; - } - if (sQueryLogTimeInMillis == 0) { - sQueryLogTimeInMillis = SystemProperties.getInt("db.db_operation.threshold_ms", 500); - } - if (durationMillis >= sQueryLogTimeInMillis) { - samplePercent = 100; - } else { - samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1; - if (mRandom.nextInt(100) >= samplePercent) return; - } - - // Note: the prefix will be "COMMIT;" or "GETLOCK:" when non-null. We wait to do - // it here so we avoid allocating in the common case. - if (prefix != null) { - sql = prefix + sql; - } - if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH); - - // ActivityThread.currentPackageName() only returns non-null if the - // current thread is an application main thread. This parameter tells - // us whether an event loop is blocked, and if so, which app it is. - // - // Sadly, there's no fast way to determine app name if this is *not* a - // main thread, or when we are invoked via Binder (e.g. ContentProvider). - // Hopefully the full path to the database will be informative enough. - - String blockingPackage = AppGlobals.getInitialPackage(); - if (blockingPackage == null) blockingPackage = ""; - - EventLog.writeEvent( - EVENT_DB_OPERATION, - getPathForLogs(), - sql, - durationMillis, - blockingPackage, - samplePercent); - } - - /** - * Removes email addresses from database filenames before they're - * logged to the EventLog where otherwise apps could potentially - * read them. - */ - private String getPathForLogs() { - if (mPathForLogs != null) { - return mPathForLogs; - } - if (mPath == null) { - return null; - } - if (mPath.indexOf('@') == -1) { - mPathForLogs = mPath; - } else { - mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY"); - } - return mPathForLogs; - } - /** * Sets the locale for this database. Does nothing if this database has * the NO_LOCALIZED_COLLATORS flag set or was opened read only. + * + * @param locale The new locale. + * * @throws SQLException if the locale could not be set. The most common reason * for this is that there is no collator available for the locale you requested. * In this case the database remains unchanged. */ public void setLocale(Locale locale) { - lock(); - try { - native_setLocale(locale.toString(), mFlags); - } finally { - unlock(); - } - } - - /* package */ void verifyDbIsOpen() { - if (!isOpen()) { - throw new IllegalStateException("database " + getPath() + " (conn# " + - mConnectionNum + ") already closed"); - } - } - - /* package */ void verifyLockOwner() { - verifyDbIsOpen(); - if (mLockingEnabled && !isDbLockedByCurrentThread()) { - throw new IllegalStateException("Don't have database lock!"); - } - } - - /** - * Adds the given SQL and its compiled-statement-id-returned-by-sqlite to the - * cache of compiledQueries attached to 'this'. - * <p> - * If there is already a {@link SQLiteCompiledSql} in compiledQueries for the given SQL, - * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current - * mapping is NOT replaced with the new mapping). - */ - /* package */ synchronized void addToCompiledQueries( - String sql, SQLiteCompiledSql compiledStatement) { - // don't insert the new mapping if a mapping already exists - if (mCompiledQueries.get(sql) != null) { - return; + if (locale == null) { + throw new IllegalArgumentException("locale must not be null."); } - int maxCacheSz = (mConnectionNum == 0) ? mCompiledQueries.maxSize() : - mParentConnObj.mCompiledQueries.maxSize(); - - if (SQLiteDebug.DEBUG_SQL_CACHE) { - boolean printWarning = (mConnectionNum == 0) - ? (!mCacheFullWarning && mCompiledQueries.size() == maxCacheSz) - : (!mParentConnObj.mCacheFullWarning && - mParentConnObj.mCompiledQueries.size() == maxCacheSz); - if (printWarning) { - /* - * cache size is not enough for this app. log a warning. - * chances are it is NOT using ? for bindargs - or cachesize is too small. - */ - Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + - getPath() + ". Use setMaxSqlCacheSize() to increase cachesize. "); - mCacheFullWarning = true; - Log.d(TAG, "Here are the SQL statements in Cache of database: " + mPath); - for (String s : mCompiledQueries.snapshot().keySet()) { - Log.d(TAG, "Sql statement in Cache: " + s); - } - } + synchronized (mLock) { + throwIfNotOpenLocked(); + mConfigurationLocked.locale = locale; + mConnectionPoolLocked.reconfigure(mConfigurationLocked); } - /* add the given SQLiteCompiledSql compiledStatement to cache. - * no need to worry about the cache size - because {@link #mCompiledQueries} - * self-limits its size. - */ - mCompiledQueries.put(sql, compiledStatement); - } - - /** package-level access for testing purposes */ - /* package */ synchronized void deallocCachedSqlStatements() { - for (SQLiteCompiledSql compiledSql : mCompiledQueries.snapshot().values()) { - compiledSql.releaseSqlStatement(); - } - mCompiledQueries.evictAll(); - } - - /** - * From the compiledQueries cache, returns the compiled-statement-id for the given SQL. - * Returns null, if not found in the cache. - */ - /* package */ synchronized SQLiteCompiledSql getCompiledStatementForSql(String sql) { - return mCompiledQueries.get(sql); } /** @@ -2162,116 +1516,22 @@ public class SQLiteDatabase extends SQLiteClosable { * This method is thread-safe. * * @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE}) - * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE} or - * the value set with previous setMaxSqlCacheSize() call. + * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE}. */ public void setMaxSqlCacheSize(int cacheSize) { - synchronized (this) { - LruCache<String, SQLiteCompiledSql> oldCompiledQueries = mCompiledQueries; - if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { - throw new IllegalStateException( - "expected value between 0 and " + MAX_SQL_CACHE_SIZE); - } else if (oldCompiledQueries != null && cacheSize < oldCompiledQueries.maxSize()) { - throw new IllegalStateException("cannot set cacheSize to a value less than the " - + "value set with previous setMaxSqlCacheSize() call."); - } - mCompiledQueries = new LruCache<String, SQLiteCompiledSql>(cacheSize) { - @Override - protected void entryRemoved(boolean evicted, String key, SQLiteCompiledSql oldValue, - SQLiteCompiledSql newValue) { - verifyLockOwner(); - oldValue.releaseIfNotInUse(); - } - }; - if (oldCompiledQueries != null) { - for (Map.Entry<String, SQLiteCompiledSql> entry - : oldCompiledQueries.snapshot().entrySet()) { - mCompiledQueries.put(entry.getKey(), entry.getValue()); - } - } - } - } - - /* package */ synchronized boolean isInStatementCache(String sql) { - return mCompiledQueries.get(sql) != null; - } - - /* package */ synchronized void releaseCompiledSqlObj( - String sql, SQLiteCompiledSql compiledSql) { - if (mCompiledQueries.get(sql) == compiledSql) { - // it is in cache - reset its inUse flag - compiledSql.release(); - } else { - // it is NOT in cache. finalize it. - compiledSql.releaseSqlStatement(); - } - } - - private synchronized int getCacheHitNum() { - return mCompiledQueries.hitCount(); - } - - private synchronized int getCacheMissNum() { - return mCompiledQueries.missCount(); - } - - private synchronized int getCachesize() { - return mCompiledQueries.size(); - } - - /* package */ void finalizeStatementLater(int id) { - if (!isOpen()) { - // database already closed. this statement will already have been finalized. - return; - } - synchronized(mClosedStatementIds) { - if (mClosedStatementIds.contains(id)) { - // this statement id is already queued up for finalization. - return; - } - mClosedStatementIds.add(id); - } - } - - /* package */ boolean isInQueueOfStatementsToBeFinalized(int id) { - if (!isOpen()) { - // database already closed. this statement will already have been finalized. - // return true so that the caller doesn't have to worry about finalizing this statement. - return true; - } - synchronized(mClosedStatementIds) { - return mClosedStatementIds.contains(id); + if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { + throw new IllegalStateException( + "expected value between 0 and " + MAX_SQL_CACHE_SIZE); } - } - /* package */ void closePendingStatements() { - if (!isOpen()) { - // since this database is already closed, no need to finalize anything. - mClosedStatementIds.clear(); - return; - } - verifyLockOwner(); - /* to minimize synchronization on mClosedStatementIds, make a copy of the list */ - ArrayList<Integer> list = new ArrayList<Integer>(mClosedStatementIds.size()); - synchronized(mClosedStatementIds) { - list.addAll(mClosedStatementIds); - mClosedStatementIds.clear(); - } - // finalize all the statements from the copied list - int size = list.size(); - for (int i = 0; i < size; i++) { - native_finalize(list.get(i)); + synchronized (mLock) { + throwIfNotOpenLocked(); + mConfigurationLocked.maxSqlCacheSize = cacheSize; + mConnectionPoolLocked.reconfigure(mConfigurationLocked); } } /** - * for testing only - */ - /* package */ ArrayList<Integer> getQueuedUpStmtList() { - return mClosedStatementIds; - } - - /** * This method enables parallel execution of queries from multiple threads on the same database. * It does this by opening multiple handles to the database and using a different * database handle for each query. @@ -2314,37 +1574,43 @@ public class SQLiteDatabase extends SQLiteClosable { * @return true if write-ahead-logging is set. false otherwise */ public boolean enableWriteAheadLogging() { - // make sure the database is not READONLY. WAL doesn't make sense for readonly-databases. - if (isReadOnly()) { - return false; - } - // acquire lock - no that no other thread is enabling WAL at the same time - lock(); - try { - if (mConnectionPool != null) { - // already enabled + synchronized (mLock) { + throwIfNotOpenLocked(); + + if (mIsWALEnabledLocked) { return true; } - if (mPath.equalsIgnoreCase(MEMORY_DB_PATH)) { + + if (isReadOnlyLocked()) { + // WAL doesn't make sense for readonly-databases. + // TODO: True, but connection pooling does still make sense... + return false; + } + + if (mConfigurationLocked.isInMemoryDb()) { Log.i(TAG, "can't enable WAL for memory databases."); return false; } // make sure this database has NO attached databases because sqlite's write-ahead-logging // doesn't work for databases with attached databases - if (mHasAttachedDbs) { + if (mHasAttachedDbsLocked) { if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, - "this database: " + mPath + " has attached databases. can't enable WAL."); + Log.d(TAG, "this database: " + mConfigurationLocked.label + + " has attached databases. can't enable WAL."); } return false; } - mConnectionPool = new DatabaseConnectionPool(this); - setJournalMode(mPath, "WAL"); - return true; - } finally { - unlock(); + + mIsWALEnabledLocked = true; + mConfigurationLocked.maxConnectionPoolSize = Math.max(2, + Resources.getSystem().getInteger( + com.android.internal.R.integer.db_connection_pool_size)); + mConnectionPoolLocked.reconfigure(mConfigurationLocked); } + + setJournalMode("WAL"); + return true; } /** @@ -2352,176 +1618,66 @@ public class SQLiteDatabase extends SQLiteClosable { * @hide */ public void disableWriteAheadLogging() { - // grab database lock so that writeAheadLogging is not disabled from 2 different threads - // at the same time - lock(); - try { - if (mConnectionPool == null) { - return; // already disabled - } - mConnectionPool.close(); - setJournalMode(mPath, "TRUNCATE"); - mConnectionPool = null; - } finally { - unlock(); - } - } + synchronized (mLock) { + throwIfNotOpenLocked(); - /* package */ SQLiteDatabase getDatabaseHandle(String sql) { - if (isPooledConnection()) { - // this is a pooled database connection - // use it if it is open AND if I am not currently part of a transaction - if (isOpen() && !amIInTransaction()) { - // TODO: use another connection from the pool - // if this connection is currently in use by some other thread - // AND if there are free connections in the pool - return this; - } else { - // the pooled connection is not open! could have been closed either due - // to corruption on this or some other connection to the database - // OR, maybe the connection pool is disabled after this connection has been - // allocated to me. try to get some other pooled or main database connection - return getParentDbConnObj().getDbConnection(sql); + if (!mIsWALEnabledLocked) { + return; } - } else { - // this is NOT a pooled connection. can we get one? - return getDbConnection(sql); - } - } - /* package */ SQLiteDatabase createPoolConnection(short connectionNum) { - SQLiteDatabase db = openDatabase(mPath, mFactory, mFlags, mErrorHandler, connectionNum); - db.mParentConnObj = this; - return db; - } - - private synchronized SQLiteDatabase getParentDbConnObj() { - return mParentConnObj; - } + mIsWALEnabledLocked = false; + mConfigurationLocked.maxConnectionPoolSize = 1; + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } - private boolean isPooledConnection() { - return this.mConnectionNum > 0; + setJournalMode("TRUNCATE"); } - /* package */ SQLiteDatabase getDbConnection(String sql) { - verifyDbIsOpen(); - // this method should always be called with main database connection handle. - // the only time when it is called with pooled database connection handle is - // corruption occurs while trying to open a pooled database connection handle. - // in that case, simply return 'this' handle - if (isPooledConnection()) { - return this; + /** + * Collect statistics about all open databases in the current process. + * Used by bug report. + */ + static ArrayList<DbStats> getDbStats() { + ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>(); + for (SQLiteDatabase db : getActiveDatabases()) { + db.collectDbStats(dbStatsList); } + return dbStatsList; + } - // use the current connection handle if - // 1. if the caller is part of the ongoing transaction, if any - // 2. OR, if there is NO connection handle pool setup - if (amIInTransaction() || mConnectionPool == null) { - return this; - } else { - // get a connection handle from the pool - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert mConnectionPool != null; - Log.i(TAG, mConnectionPool.toString()); + private void collectDbStats(ArrayList<DbStats> dbStatsList) { + synchronized (mLock) { + if (mConnectionPoolLocked != null) { + mConnectionPoolLocked.collectDbStats(dbStatsList); } - return mConnectionPool.get(sql); } } - private void releaseDbConnection(SQLiteDatabase db) { - // ignore this release call if - // 1. the database is closed - // 2. OR, if db is NOT a pooled connection handle - // 3. OR, if the database being released is same as 'this' (this condition means - // that we should always be releasing a pooled connection handle by calling this method - // from the 'main' connection handle - if (!isOpen() || !db.isPooledConnection() || (db == this)) { - return; + private static ArrayList<SQLiteDatabase> getActiveDatabases() { + ArrayList<SQLiteDatabase> databases = new ArrayList<SQLiteDatabase>(); + synchronized (sActiveDatabases) { + databases.addAll(sActiveDatabases.keySet()); } - if (Log.isLoggable(TAG, Log.DEBUG)) { - assert isPooledConnection(); - assert mConnectionPool != null; - Log.d(TAG, "releaseDbConnection threadid = " + Thread.currentThread().getId() + - ", releasing # " + db.mConnectionNum + ", " + getPath()); - } - mConnectionPool.release(db); + return databases; } /** - * this method is used to collect data about ALL open databases in the current process. - * bugreport is a user of this data. + * Dump detailed information about all open databases in the current process. + * Used by bug report. */ - /* package */ static ArrayList<DbStats> getDbStats() { - ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>(); - // make a local copy of mActiveDatabases - so that this method is not competing - // for synchronization lock on mActiveDatabases - ArrayList<WeakReference<SQLiteDatabase>> tempList; - synchronized(mActiveDatabases) { - tempList = (ArrayList<WeakReference<SQLiteDatabase>>)mActiveDatabases.clone(); + static void dumpAll(Printer printer) { + for (SQLiteDatabase db : getActiveDatabases()) { + db.dump(printer); } - for (WeakReference<SQLiteDatabase> w : tempList) { - SQLiteDatabase db = w.get(); - if (db == null || !db.isOpen()) { - continue; - } + } - try { - // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db - int lookasideUsed = db.native_getDbLookaside(); - - // get the lastnode of the dbname - String path = db.getPath(); - int indx = path.lastIndexOf("/"); - String lastnode = path.substring((indx != -1) ? ++indx : 0); - - // get list of attached dbs and for each db, get its size and pagesize - List<Pair<String, String>> attachedDbs = db.getAttachedDbs(); - if (attachedDbs == null) { - continue; - } - for (int i = 0; i < attachedDbs.size(); i++) { - Pair<String, String> p = attachedDbs.get(i); - long pageCount = DatabaseUtils.longForQuery(db, "PRAGMA " + p.first - + ".page_count;", null); - - // first entry in the attached db list is always the main database - // don't worry about prefixing the dbname with "main" - String dbName; - if (i == 0) { - dbName = lastnode; - } else { - // lookaside is only relevant for the main db - lookasideUsed = 0; - dbName = " (attached) " + p.first; - // if the attached db has a path, attach the lastnode from the path to above - if (p.second.trim().length() > 0) { - int idx = p.second.lastIndexOf("/"); - dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0); - } - } - if (pageCount > 0) { - dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), - lookasideUsed, db.getCacheHitNum(), db.getCacheMissNum(), - db.getCachesize())); - } - } - // if there are pooled connections, return the cache stats for them also. - // while we are trying to query the pooled connections for stats, some other thread - // could be disabling conneciton pool. so, grab a reference to the connection pool. - DatabaseConnectionPool connPool = db.mConnectionPool; - if (connPool != null) { - for (SQLiteDatabase pDb : connPool.getConnectionList()) { - dbStatsList.add(new DbStats("(pooled # " + pDb.mConnectionNum + ") " - + lastnode, 0, 0, 0, pDb.getCacheHitNum(), - pDb.getCacheMissNum(), pDb.getCachesize())); - } - } - } catch (SQLiteException e) { - // ignore. we don't care about exceptions when we are taking adb - // bugreport! + private void dump(Printer printer) { + synchronized (mLock) { + if (mConnectionPoolLocked != null) { + printer.println(""); + mConnectionPoolLocked.dump(printer); } } - return dbStatsList; } /** @@ -2532,23 +1688,27 @@ public class SQLiteDatabase extends SQLiteClosable { * is not open. */ public List<Pair<String, String>> getAttachedDbs() { - if (!isOpen()) { - return null; - } ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>(); - if (!mHasAttachedDbs) { - // No attached databases. - // There is a small window where attached databases exist but this flag is not set yet. - // This can occur when this thread is in a race condition with another thread - // that is executing the SQL statement: "attach database <blah> as <foo>" - // If this thread is NOT ok with such a race condition (and thus possibly not receive - // the entire list of attached databases), then the caller should ensure that no thread - // is executing any SQL statements while a thread is calling this method. - // Typically, this method is called when 'adb bugreport' is done or the caller wants to - // collect stats on the database and all its attached databases. - attachedDbs.add(new Pair<String, String>("main", mPath)); - return attachedDbs; + synchronized (mLock) { + if (mConnectionPoolLocked == null) { + return null; // not open + } + + if (!mHasAttachedDbsLocked) { + // No attached databases. + // There is a small window where attached databases exist but this flag is not + // set yet. This can occur when this thread is in a race condition with another + // thread that is executing the SQL statement: "attach database <blah> as <foo>" + // If this thread is NOT ok with such a race condition (and thus possibly not + // receivethe entire list of attached databases), then the caller should ensure + // that no thread is executing any SQL statements while a thread is calling this + // method. Typically, this method is called when 'adb bugreport' is done or the + // caller wants to collect stats on the database and all its attached databases. + attachedDbs.add(new Pair<String, String>("main", mConfigurationLocked.path)); + return attachedDbs; + } } + // has attached databases. query sqlite to get the list of attached databases. Cursor c = null; try { @@ -2583,7 +1743,8 @@ public class SQLiteDatabase extends SQLiteClosable { * false otherwise. */ public boolean isDatabaseIntegrityOk() { - verifyDbIsOpen(); + throwIfNotOpen(); // fail fast + List<Pair<String, String>> attachedDbs = null; try { attachedDbs = getAttachedDbs(); @@ -2594,8 +1755,9 @@ public class SQLiteDatabase extends SQLiteClosable { } catch (SQLiteException e) { // can't get attachedDb list. do integrity check on the main database attachedDbs = new ArrayList<Pair<String, String>>(); - attachedDbs.add(new Pair<String, String>("main", this.mPath)); + attachedDbs.add(new Pair<String, String>("main", getPath())); } + for (int i = 0; i < attachedDbs.size(); i++) { Pair<String, String> p = attachedDbs.get(i); SQLiteStatement prog = null; @@ -2615,59 +1777,64 @@ public class SQLiteDatabase extends SQLiteClosable { } /** - * Native call to open the database. + * Prevent other threads from using the database's primary connection. * - * @param path The full path to the database - */ - private native void dbopen(String path, int flags); - - /** - * Native call to setup tracing of all SQL statements + * This method is only used by {@link SQLiteOpenHelper} when transitioning from + * a readable to a writable database. It should not be used in any other way. * - * @param path the full path to the database - * @param connectionNum connection number: 0 - N, where the main database - * connection handle is numbered 0 and the connection handles in the connection - * pool are numbered 1..N. + * @see #unlockPrimaryConnection() */ - private native void enableSqlTracing(String path, short connectionNum); + void lockPrimaryConnection() { + getThreadSession().beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, + null, SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY); + } /** - * Native call to setup profiling of all SQL statements. - * currently, sqlite's profiling = printing of execution-time - * (wall-clock time) of each of the SQL statements, as they - * are executed. + * Allow other threads to use the database's primary connection. * - * @param path the full path to the database - * @param connectionNum connection number: 0 - N, where the main database - * connection handle is numbered 0 and the connection handles in the connection - * pool are numbered 1..N. + * @see #lockPrimaryConnection() */ - private native void enableSqlProfiling(String path, short connectionNum); + void unlockPrimaryConnection() { + getThreadSession().endTransaction(); + } - /** - * Native call to set the locale. {@link #lock} must be held when calling - * this method. - * @throws SQLException - */ - private native void native_setLocale(String loc, int flags); + @Override + public String toString() { + return "SQLiteDatabase: " + getPath(); + } - /** - * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here - * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html - * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED - */ - private native int native_getDbLookaside(); + private void throwIfNotOpen() { + synchronized (mConnectionPoolLocked) { + throwIfNotOpenLocked(); + } + } + + private void throwIfNotOpenLocked() { + if (mConnectionPoolLocked == null) { + throw new IllegalStateException("The database '" + mConfigurationLocked.label + + "' is not open."); + } + } /** - * finalizes the given statement id. - * - * @param statementId statement to be finzlied by sqlite + * Used to allow returning sub-classes of {@link Cursor} when calling query. */ - private final native void native_finalize(int statementId); + public interface CursorFactory { + /** + * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}. + */ + public Cursor newCursor(SQLiteDatabase db, + SQLiteCursorDriver masterQuery, String editTable, + SQLiteQuery query); + } /** - * set sqlite soft heap limit - * http://www.sqlite.org/c3ref/soft_heap_limit64.html + * A callback interface for a custom sqlite3 function. + * This can be used to create a function that can be called from + * sqlite3 database triggers. + * @hide */ - private native void native_setSqliteSoftHeapLimit(int softHeapLimit); + public interface CustomFunction { + public void callback(String[] args); + } } diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java new file mode 100644 index 0000000..bc79ad3 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java @@ -0,0 +1,167 @@ +/* + * 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 java.util.ArrayList; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Describes how to configure a database. + * <p> + * The purpose of this object is to keep track of all of the little + * configuration settings that are applied to a database after it + * is opened so that they can be applied to all connections in the + * connection pool uniformly. + * </p><p> + * Each connection maintains its own copy of this object so it can + * keep track of which settings have already been applied. + * </p> + * + * @hide + */ +public final class SQLiteDatabaseConfiguration { + // The pattern we use to strip email addresses from database paths + // when constructing a label to use in log messages. + private static final Pattern EMAIL_IN_DB_PATTERN = + Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); + + /** + * Special path used by in-memory databases. + */ + public static final String MEMORY_DB_PATH = ":memory:"; + + /** + * The database path. + */ + public final String path; + + /** + * The flags used to open the database. + */ + public final int openFlags; + + /** + * The label to use to describe the database when it appears in logs. + * This is derived from the path but is stripped to remove PII. + */ + public final String label; + + /** + * The maximum number of connections to retain in the connection pool. + * Must be at least 1. + * + * Default is 1. + */ + public int maxConnectionPoolSize; + + /** + * The maximum size of the prepared statement cache for each database connection. + * Must be non-negative. + * + * Default is 25. + */ + public int maxSqlCacheSize; + + /** + * The database locale. + * + * Default is the value returned by {@link Locale#getDefault()}. + */ + public Locale locale; + + /** + * The custom functions to register. + */ + public final ArrayList<SQLiteCustomFunction> customFunctions = + new ArrayList<SQLiteCustomFunction>(); + + /** + * Creates a database configuration with the required parameters for opening a + * database and default values for all other parameters. + * + * @param path The database path. + * @param openFlags Open flags for the database, such as {@link SQLiteDatabase#OPEN_READWRITE}. + */ + public SQLiteDatabaseConfiguration(String path, int openFlags) { + if (path == null) { + throw new IllegalArgumentException("path must not be null."); + } + + this.path = path; + this.openFlags = openFlags; + label = stripPathForLogs(path); + + // Set default values for optional parameters. + maxConnectionPoolSize = 1; + maxSqlCacheSize = 25; + locale = Locale.getDefault(); + } + + /** + * Creates a database configuration as a copy of another configuration. + * + * @param other The other configuration. + */ + public SQLiteDatabaseConfiguration(SQLiteDatabaseConfiguration other) { + if (other == null) { + throw new IllegalArgumentException("other must not be null."); + } + + this.path = other.path; + this.openFlags = other.openFlags; + this.label = other.label; + updateParametersFrom(other); + } + + /** + * Updates the non-immutable parameters of this configuration object + * from the other configuration object. + * + * @param other The object from which to copy the parameters. + */ + public void updateParametersFrom(SQLiteDatabaseConfiguration other) { + if (other == null) { + throw new IllegalArgumentException("other must not be null."); + } + if (!path.equals(other.path) || openFlags != other.openFlags) { + throw new IllegalArgumentException("other configuration must refer to " + + "the same database."); + } + + maxConnectionPoolSize = other.maxConnectionPoolSize; + maxSqlCacheSize = other.maxSqlCacheSize; + locale = other.locale; + customFunctions.clear(); + customFunctions.addAll(other.customFunctions); + } + + /** + * Returns true if the database is in-memory. + * @return True if the database is in-memory. + */ + public boolean isInMemoryDb() { + return path.equalsIgnoreCase(MEMORY_DB_PATH); + } + + private static String stripPathForLogs(String path) { + if (path.indexOf('@') == -1) { + return path; + } + return EMAIL_IN_DB_PATTERN.matcher(path).replaceAll("XX@YY"); + } +} diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index 029bb4a..d87c3e4 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -30,6 +30,12 @@ import android.util.Printer; */ public final class SQLiteDebug { /** + * Controls the printing of informational SQL log messages. + */ + public static final boolean DEBUG_SQL_LOG = + Log.isLoggable("SQLiteLog", Log.VERBOSE); + + /** * Controls the printing of SQL statements as they are executed. */ public static final boolean DEBUG_SQL_STATEMENTS = @@ -186,6 +192,7 @@ public final class SQLiteDebug { * @param printer The printer for dumping database state. */ public static void dump(Printer printer, String[] args) { + SQLiteDatabase.dumpAll(printer); } /** diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java index a5e762e..52fd1d2 100644 --- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java @@ -25,10 +25,9 @@ import android.database.sqlite.SQLiteDatabase.CursorFactory; * @hide */ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { - private String mEditTable; - private SQLiteDatabase mDatabase; - private Cursor mCursor; - private String mSql; + private final SQLiteDatabase mDatabase; + private final String mEditTable; + private final String mSql; private SQLiteQuery mQuery; public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable) { @@ -38,33 +37,27 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { } public Cursor query(CursorFactory factory, String[] selectionArgs) { - // Compile the query - SQLiteQuery query = null; - + final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql); + final Cursor cursor; try { - mDatabase.lock(mSql); - mDatabase.closePendingStatements(); - query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); + query.bindAllArgsAsStrings(selectionArgs); - // Create the cursor if (factory == null) { - mCursor = new SQLiteCursor(this, mEditTable, query); + cursor = new SQLiteCursor(this, mEditTable, query); } else { - mCursor = factory.newCursor(mDatabase, this, mEditTable, query); + cursor = factory.newCursor(mDatabase, this, mEditTable, query); } - - mQuery = query; - query = null; - return mCursor; - } finally { - // Make sure this object is cleaned up if something happens - if (query != null) query.close(); - mDatabase.unlock(); + } catch (RuntimeException ex) { + query.close(); + throw ex; } + + mQuery = query; + return cursor; } public void cursorClosed() { - mCursor = null; + // Do nothing } public void setBindArguments(String[] bindArgs) { diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java new file mode 100644 index 0000000..5e129be --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteGlobal.java @@ -0,0 +1,89 @@ +/* + * 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.os.StatFs; + +/** + * Provides access to SQLite functions that affect all database connection, + * such as memory management. + * + * @hide + */ +public final class SQLiteGlobal { + private static final String TAG = "SQLiteGlobal"; + + private static final Object sLock = new Object(); + private static boolean sInitialized; + private static int sSoftHeapLimit; + private static int sDefaultPageSize; + + private static native void nativeConfig(boolean verboseLog, int softHeapLimit); + private static native int nativeReleaseMemory(int bytesToFree); + + private SQLiteGlobal() { + } + + /** + * Initializes global SQLite settings the first time it is called. + * Should be called before opening the first (or any) database. + * Does nothing on repeated subsequent calls. + */ + public static void initializeOnce() { + synchronized (sLock) { + if (!sInitialized) { + sInitialized = true; + + // Limit to 8MB for now. This is 4 times the maximum cursor window + // size, as has been used by the original code in SQLiteDatabase for + // a long time. + // TODO: We really do need to test whether this helps or hurts us. + sSoftHeapLimit = 8 * 1024 * 1024; + + // Configure SQLite. + nativeConfig(SQLiteDebug.DEBUG_SQL_LOG, sSoftHeapLimit); + } + } + } + + /** + * Attempts to release memory by pruning the SQLite page cache and other + * internal data structures. + * + * @return The number of bytes that were freed. + */ + public static int releaseMemory() { + synchronized (sLock) { + if (!sInitialized) { + return 0; + } + return nativeReleaseMemory(sSoftHeapLimit); + } + } + + /** + * Gets the default page size to use when creating a database. + */ + public static int getDefaultPageSize() { + synchronized (sLock) { + if (sDefaultPageSize == 0) { + sDefaultPageSize = new StatFs("/data").getBlockSize(); + } + return sDefaultPageSize; + } + } +} diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 56cf948..31da7e4 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -143,12 +143,14 @@ public abstract class SQLiteOpenHelper { // If we have a read-only database open, someone could be using it // (though they shouldn't), which would cause a lock to be held on // the file, and our attempts to open the database read-write would - // fail waiting for the file lock. To prevent that, we acquire the - // lock on the read-only database, which shuts out other users. + // fail waiting for the file lock. To prevent that, we acquire a lock + // on the read-only database, which shuts out other users. boolean success = false; SQLiteDatabase db = null; - if (mDatabase != null) mDatabase.lock(); + if (mDatabase != null) { + mDatabase.lockPrimaryConnection(); + } try { mIsInitializing = true; if (mName == null) { @@ -185,11 +187,13 @@ public abstract class SQLiteOpenHelper { if (success) { if (mDatabase != null) { try { mDatabase.close(); } catch (Exception e) { } - mDatabase.unlock(); + mDatabase.unlockPrimaryConnection(); } mDatabase = db; } else { - if (mDatabase != null) mDatabase.unlock(); + if (mDatabase != null) { + mDatabase.unlockPrimaryConnection(); + } if (db != null) db.close(); } } diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 2bbc6d7..8194458 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -17,225 +17,104 @@ package android.database.sqlite; import android.database.DatabaseUtils; -import android.database.Cursor; -import java.util.HashMap; +import java.util.Arrays; /** * A base class for compiled SQLite programs. - *<p> - * SQLiteProgram is NOT internally synchronized so code using a SQLiteProgram from multiple - * threads should perform its own synchronization when using the SQLiteProgram. + * <p> + * This class is not thread-safe. + * </p> */ public abstract class SQLiteProgram extends SQLiteClosable { + private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private static final String TAG = "SQLiteProgram"; + private final SQLiteDatabase mDatabase; + private final String mSql; + private final boolean mReadOnly; + private final String[] mColumnNames; + private final int mNumParameters; + private final Object[] mBindArgs; - /** The database this program is compiled against. - * @hide - */ - protected SQLiteDatabase mDatabase; - - /** The SQL used to create this query */ - /* package */ final String mSql; - - /** - * Native linkage, do not modify. This comes from the database and should not be modified - * in here or in the native code. - * @hide - */ - protected int nHandle; - - /** - * the SQLiteCompiledSql object for the given sql statement. - */ - /* package */ SQLiteCompiledSql mCompiledSql; - - /** - * SQLiteCompiledSql statement id is populated with the corresponding object from the above - * member. This member is used by the native_bind_* methods - * @hide - */ - protected int nStatement; - - /** - * In the case of {@link SQLiteStatement}, this member stores the bindargs passed - * to the following methods, instead of actually doing the binding. - * <ul> - * <li>{@link #bindBlob(int, byte[])}</li> - * <li>{@link #bindDouble(int, double)}</li> - * <li>{@link #bindLong(int, long)}</li> - * <li>{@link #bindNull(int)}</li> - * <li>{@link #bindString(int, String)}</li> - * </ul> - * <p> - * Each entry in the array is a Pair of - * <ol> - * <li>bind arg position number</li> - * <li>the value to be bound to the bindarg</li> - * </ol> - * <p> - * It is lazily initialized in the above bind methods - * and it is cleared in {@link #clearBindings()} method. - * <p> - * It is protected (in multi-threaded environment) by {@link SQLiteProgram}.this - */ - /* package */ HashMap<Integer, Object> mBindArgs = null; - /* package */ final int mStatementType; - /* package */ static final int STATEMENT_CACHEABLE = 16; - /* package */ static final int STATEMENT_DONT_PREPARE = 32; - /* package */ static final int STATEMENT_USE_POOLED_CONN = 64; - /* package */ static final int STATEMENT_TYPE_MASK = 0x0f; - - /* package */ SQLiteProgram(SQLiteDatabase db, String sql) { - this(db, sql, null, true); - } - - /* package */ SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs, - boolean compileFlag) { + SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs) { + mDatabase = db; mSql = sql.trim(); + int n = DatabaseUtils.getSqlStatementType(mSql); switch (n) { - case DatabaseUtils.STATEMENT_UPDATE: - mStatementType = n | STATEMENT_CACHEABLE; - break; - case DatabaseUtils.STATEMENT_SELECT: - mStatementType = n | STATEMENT_CACHEABLE | STATEMENT_USE_POOLED_CONN; - break; case DatabaseUtils.STATEMENT_BEGIN: case DatabaseUtils.STATEMENT_COMMIT: case DatabaseUtils.STATEMENT_ABORT: - mStatementType = n | STATEMENT_DONT_PREPARE; + mReadOnly = false; + mColumnNames = EMPTY_STRING_ARRAY; + mNumParameters = 0; break; + default: - mStatementType = n; - } - db.acquireReference(); - db.addSQLiteClosable(this); - mDatabase = db; - nHandle = db.mNativeHandle; - if (bindArgs != null) { - int size = bindArgs.length; - for (int i = 0; i < size; i++) { - this.addToBindArgs(i + 1, bindArgs[i]); - } - } - if (compileFlag) { - compileAndbindAllArgs(); + boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT); + SQLiteStatementInfo info = new SQLiteStatementInfo(); + db.getThreadSession().prepare(mSql, + db.getThreadDefaultConnectionFlags(assumeReadOnly), info); + mReadOnly = info.readOnly; + mColumnNames = info.columnNames; + mNumParameters = info.numParameters; + break; } - } - private void compileSql() { - // only cache CRUD statements - if ((mStatementType & STATEMENT_CACHEABLE) == 0) { - mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); - nStatement = mCompiledSql.nStatement; - // since it is not in the cache, no need to acquire() it. - return; + if (mNumParameters != 0) { + mBindArgs = new Object[mNumParameters]; + } else { + mBindArgs = null; } - mCompiledSql = mDatabase.getCompiledStatementForSql(mSql); - if (mCompiledSql == null) { - // create a new compiled-sql obj - mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); - - // add it to the cache of compiled-sqls - // but before adding it and thus making it available for anyone else to use it, - // make sure it is acquired by me. - mCompiledSql.acquire(); - mDatabase.addToCompiledQueries(mSql, mCompiledSql); - } else { - // it is already in compiled-sql cache. - // try to acquire the object. - if (!mCompiledSql.acquire()) { - int last = mCompiledSql.nStatement; - // the SQLiteCompiledSql in cache is in use by some other SQLiteProgram object. - // we can't have two different SQLiteProgam objects can't share the same - // CompiledSql object. create a new one. - // finalize it when I am done with it in "this" object. - mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); - // since it is not in the cache, no need to acquire() it. + if (bindArgs != null) { + if (bindArgs.length > mNumParameters) { + throw new IllegalArgumentException("Too many bind arguments. " + + bindArgs.length + " arguments were provided but the statement needs " + + mNumParameters + " arguments."); } + System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length); } - nStatement = mCompiledSql.nStatement; } - @Override - protected void onAllReferencesReleased() { - release(); - mDatabase.removeSQLiteClosable(this); - mDatabase.releaseReference(); + final SQLiteDatabase getDatabase() { + return mDatabase; } - @Override - protected void onAllReferencesReleasedFromContainer() { - release(); - mDatabase.releaseReference(); + final String getSql() { + return mSql; } - /* package */ void release() { - if (mCompiledSql == null) { - return; - } - mDatabase.releaseCompiledSqlObj(mSql, mCompiledSql); - mCompiledSql = null; - nStatement = 0; + final Object[] getBindArgs() { + return mBindArgs; } - /** - * Returns a unique identifier for this program. - * - * @return a unique identifier for this program - * @deprecated do not use this method. it is not guaranteed to be the same across executions of - * the SQL statement contained in this object. - */ - @Deprecated - public final int getUniqueId() { - return -1; + final String[] getColumnNames() { + return mColumnNames; } - /** - * used only for testing purposes - */ - /* package */ int getSqlStatementId() { - synchronized(this) { - return (mCompiledSql == null) ? 0 : nStatement; - } + /** @hide */ + protected final SQLiteSession getSession() { + return mDatabase.getThreadSession(); } - /* package */ String getSqlString() { - return mSql; + /** @hide */ + protected final int getConnectionFlags() { + return mDatabase.getThreadDefaultConnectionFlags(mReadOnly); } - private void bind(int type, int index, Object value) { - mDatabase.verifyDbIsOpen(); - addToBindArgs(index, (type == Cursor.FIELD_TYPE_NULL) ? null : value); - if (nStatement > 0) { - // bind only if the SQL statement is compiled - acquireReference(); - try { - switch (type) { - case Cursor.FIELD_TYPE_NULL: - native_bind_null(index); - break; - case Cursor.FIELD_TYPE_BLOB: - native_bind_blob(index, (byte[]) value); - break; - case Cursor.FIELD_TYPE_FLOAT: - native_bind_double(index, (Double) value); - break; - case Cursor.FIELD_TYPE_INTEGER: - native_bind_long(index, (Long) value); - break; - case Cursor.FIELD_TYPE_STRING: - default: - native_bind_string(index, (String) value); - break; - } - } finally { - releaseReference(); - } - } + /** @hide */ + protected final void onCorruption() { + mDatabase.onCorruption(); + } + + /** + * Unimplemented. + * @deprecated This method is deprecated and must not be used. + */ + @Deprecated + public final int getUniqueId() { + return -1; } /** @@ -245,7 +124,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param index The 1-based index to the parameter to bind null to */ public void bindNull(int index) { - bind(Cursor.FIELD_TYPE_NULL, index, null); + bind(index, null); } /** @@ -256,7 +135,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindLong(int index, long value) { - bind(Cursor.FIELD_TYPE_INTEGER, index, value); + bind(index, value); } /** @@ -267,7 +146,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindDouble(int index, double value) { - bind(Cursor.FIELD_TYPE_FLOAT, index, value); + bind(index, value); } /** @@ -275,13 +154,13 @@ public abstract class SQLiteProgram extends SQLiteClosable { * {@link #clearBindings} is called. * * @param index The 1-based index to the parameter to bind - * @param value The value to bind + * @param value The value to bind, must not be null */ public void bindString(int index, String value) { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } - bind(Cursor.FIELD_TYPE_STRING, index, value); + bind(index, value); } /** @@ -289,29 +168,21 @@ public abstract class SQLiteProgram extends SQLiteClosable { * {@link #clearBindings} is called. * * @param index The 1-based index to the parameter to bind - * @param value The value to bind + * @param value The value to bind, must not be null */ public void bindBlob(int index, byte[] value) { if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } - bind(Cursor.FIELD_TYPE_BLOB, index, value); + bind(index, value); } /** * Clears all existing bindings. Unset bindings are treated as NULL. */ public void clearBindings() { - mBindArgs = null; - if (this.nStatement == 0) { - return; - } - mDatabase.verifyDbIsOpen(); - acquireReference(); - try { - native_clear_bindings(); - } finally { - releaseReference(); + if (mBindArgs != null) { + Arrays.fill(mBindArgs, null); } } @@ -319,102 +190,33 @@ public abstract class SQLiteProgram extends SQLiteClosable { * Release this program's resources, making it invalid. */ public void close() { - mBindArgs = null; - if (nHandle == 0 || !mDatabase.isOpen()) { - return; - } releaseReference(); } - private void addToBindArgs(int index, Object value) { - if (mBindArgs == null) { - mBindArgs = new HashMap<Integer, Object>(); - } - mBindArgs.put(index, value); - } - - /* package */ void compileAndbindAllArgs() { - if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { - if (mBindArgs != null) { - throw new IllegalArgumentException("Can't pass bindargs for this sql :" + mSql); - } - // no need to prepare this SQL statement - return; - } - if (nStatement == 0) { - // SQL statement is not compiled yet. compile it now. - compileSql(); - } - if (mBindArgs == null) { - return; - } - for (int index : mBindArgs.keySet()) { - Object value = mBindArgs.get(index); - if (value == null) { - native_bind_null(index); - } else if (value instanceof Double || value instanceof Float) { - native_bind_double(index, ((Number) value).doubleValue()); - } else if (value instanceof Number) { - native_bind_long(index, ((Number) value).longValue()); - } else if (value instanceof Boolean) { - Boolean bool = (Boolean)value; - native_bind_long(index, (bool) ? 1 : 0); - if (bool) { - native_bind_long(index, 1); - } else { - native_bind_long(index, 0); - } - } else if (value instanceof byte[]){ - native_bind_blob(index, (byte[]) value); - } else { - native_bind_string(index, value.toString()); - } - } - } - /** * Given an array of String bindArgs, this method binds all of them in one single call. * - * @param bindArgs the String array of bind args. + * @param bindArgs the String array of bind args, none of which must be null. */ public void bindAllArgsAsStrings(String[] bindArgs) { - if (bindArgs == null) { - return; - } - int size = bindArgs.length; - for (int i = 0; i < size; i++) { - bindString(i + 1, bindArgs[i]); + if (bindArgs != null) { + for (int i = bindArgs.length; i != 0; i--) { + bindString(i, bindArgs[i - 1]); + } } } - /* package */ synchronized final void setNativeHandle(int nHandle) { - this.nHandle = nHandle; + @Override + protected void onAllReferencesReleased() { + clearBindings(); } - /** - * @hide - * Compiles SQL into a SQLite program. - * - * <P>The database lock must be held when calling this method. - * @param sql The SQL to compile. - */ - protected final native void native_compile(String sql); - - /** - * @hide - */ - protected final native void native_finalize(); - - /** @hide */ - protected final native void native_bind_null(int index); - /** @hide */ - protected final native void native_bind_long(int index, long value); - /** @hide */ - protected final native void native_bind_double(int index, double value); - /** @hide */ - protected final native void native_bind_string(int index, String value); - /** @hide */ - protected final native void native_bind_blob(int index, byte[] value); - private final native void native_clear_bindings(); + private void bind(int index, Object value) { + if (index < 1 || index > mNumParameters) { + throw new IllegalArgumentException("Cannot bind argument at index " + + index + " because the index is out of range. " + + "The statement has " + mNumParameters + " parameters."); + } + mBindArgs[index - 1] = value; + } } - diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index 6dd2539..17aa886 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -17,60 +17,24 @@ package android.database.sqlite; import android.database.CursorWindow; -import android.os.SystemClock; -import android.text.TextUtils; import android.util.Log; /** - * A SQLite program that represents a query that reads the resulting rows into a CursorWindow. - * This class is used by SQLiteCursor and isn't useful itself. - * - * SQLiteQuery is not internally synchronized so code using a SQLiteQuery from multiple - * threads should perform its own synchronization when using the SQLiteQuery. + * Represents a query that reads the resulting rows into a {@link SQLiteQuery}. + * This class is used by {@link SQLiteCursor} and isn't useful itself. + * <p> + * This class is not thread-safe. + * </p> */ public final class SQLiteQuery extends SQLiteProgram { private static final String TAG = "SQLiteQuery"; - private static native long nativeFillWindow(int databasePtr, int statementPtr, int windowPtr, - int offsetParam, int startPos, int requiredPos, boolean countAllRows); - - private static native int nativeColumnCount(int statementPtr); - private static native String nativeColumnName(int statementPtr, int columnIndex); - - /** The index of the unbound OFFSET parameter */ - private int mOffsetIndex = 0; - - private boolean mClosed = false; - - /** - * Create a persistent query object. - * - * @param db The database that this query object is associated with - * @param query The SQL string for this query. - * @param offsetIndex The 1-based index to the OFFSET parameter, - */ - /* package */ SQLiteQuery(SQLiteDatabase db, String query, int offsetIndex, String[] bindArgs) { - super(db, query); - mOffsetIndex = offsetIndex; - bindAllArgsAsStrings(bindArgs); - } - - /** - * Constructor used to create new instance to replace a given instance of this class. - * This constructor is used when the current Query object is now associated with a different - * {@link SQLiteDatabase} object. - * - * @param db The database that this query object is associated with - * @param query the instance of {@link SQLiteQuery} to be replaced - */ - /* package */ SQLiteQuery(SQLiteDatabase db, SQLiteQuery query) { - super(db, query.mSql); - this.mBindArgs = query.mBindArgs; - this.mOffsetIndex = query.mOffsetIndex; + SQLiteQuery(SQLiteDatabase db, String query) { + super(db, query, null); } /** - * Reads rows into a buffer. This method acquires the database lock. + * Reads rows into a buffer. * * @param window The window to fill into * @param startPos The start position for filling the window. @@ -81,106 +45,30 @@ public final class SQLiteQuery extends SQLiteProgram { * @return Number of rows that were enumerated. Might not be all rows * unless countAllRows is true. */ - /* package */ int fillWindow(CursorWindow window, - int startPos, int requiredPos, boolean countAllRows) { - mDatabase.lock(mSql); - long timeStart = SystemClock.uptimeMillis(); + int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) { + acquireReference(); try { - acquireReference(); + window.acquireReference(); try { - window.acquireReference(); - long result = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, - mOffsetIndex, startPos, requiredPos, countAllRows); - int actualPos = (int)(result >> 32); - int countedRows = (int)result; - window.setStartPosition(actualPos); - if (SQLiteDebug.DEBUG_LOG_SLOW_QUERIES) { - long elapsed = SystemClock.uptimeMillis() - timeStart; - if (SQLiteDebug.shouldLogSlowQuery(elapsed)) { - Log.d(TAG, "fillWindow took " + elapsed - + " ms: window=\"" + window - + "\", startPos=" + startPos - + ", requiredPos=" + requiredPos - + ", offset=" + mOffsetIndex - + ", actualPos=" + actualPos - + ", filledRows=" + window.getNumRows() - + ", countedRows=" + countedRows - + ", query=\"" + mSql + "\"" - + ", args=[" + (mBindArgs != null ? - TextUtils.join(", ", mBindArgs.values()) : "") - + "]"); - } - } - mDatabase.logTimeStat(mSql, timeStart); - return countedRows; - } catch (IllegalStateException e){ - // simply ignore it - return 0; - } catch (SQLiteDatabaseCorruptException e) { - mDatabase.onCorruption(); - throw e; - } catch (SQLiteException e) { - Log.e(TAG, "exception: " + e.getMessage() + "; query: " + mSql); - throw e; + int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(), + window, startPos, requiredPos, countAllRows, getConnectionFlags()); + return numRows; + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } catch (SQLiteException ex) { + Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql()); + throw ex; } finally { window.releaseReference(); } } finally { releaseReference(); - mDatabase.unlock(); - } - } - - /** - * Get the column count for the statement. Only valid on query based - * statements. The database must be locked - * when calling this method. - * - * @return The number of column in the statement's result set. - */ - /* package */ int columnCountLocked() { - acquireReference(); - try { - return nativeColumnCount(nStatement); - } finally { - releaseReference(); - } - } - - /** - * Retrieves the column name for the given column index. The database must be locked - * when calling this method. - * - * @param columnIndex the index of the column to get the name for - * @return The requested column's name - */ - /* package */ String columnNameLocked(int columnIndex) { - acquireReference(); - try { - return nativeColumnName(nStatement, columnIndex); - } finally { - releaseReference(); } } @Override public String toString() { - return "SQLiteQuery: " + mSql; - } - - @Override - public void close() { - super.close(); - mClosed = true; - } - - /** - * Called by SQLiteCursor when it is requeried. - */ - /* package */ void requery() { - if (mClosed) { - throw new IllegalStateException("requerying a closed cursor"); - } - compileAndbindAllArgs(); + return "SQLiteQuery: " + getSql(); } } diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index 8f8eb6e..1b7b398 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -341,7 +341,7 @@ public class SQLiteQueryBuilder // in both the wrapped and original forms. String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy, having, sortOrder, limit); - validateSql(db, sqlForValidation); // will throw if query is invalid + validateQuerySql(db, sqlForValidation); // will throw if query is invalid } String sql = buildQuery( @@ -357,16 +357,12 @@ public class SQLiteQueryBuilder } /** - * Verifies that a SQL statement is valid by compiling it. + * Verifies that a SQL SELECT statement is valid by compiling it. * If the SQL statement is not valid, this method will throw a {@link SQLiteException}. */ - private void validateSql(SQLiteDatabase db, String sql) { - db.lock(sql); - try { - new SQLiteCompiledSql(db, sql).releaseSqlStatement(); - } finally { - db.unlock(); - } + private void validateQuerySql(SQLiteDatabase db, String sql) { + db.getThreadSession().prepare(sql, + db.getThreadDefaultConnectionFlags(true /*readOnly*/), null); } /** diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java new file mode 100644 index 0000000..61fe45a --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteSession.java @@ -0,0 +1,878 @@ +/* + * 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.database.CursorWindow; +import android.database.DatabaseUtils; +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> + * + * TODO: Support timeouts on all possibly blocking operations. + * + * @hide + */ +public final class SQLiteSession { + private final SQLiteConnectionPool mConnectionPool; + + private SQLiteConnection mConnection; + private int mConnectionFlags; + 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}. + * + * @throws IllegalStateException if {@link #setTransactionSuccessful} has already been + * called for the current transaction. + * + * @see #setTransactionSuccessful + * @see #yieldTransaction + * @see #endTransaction + */ + public void beginTransaction(int transactionMode, + SQLiteTransactionListener transactionListener, int connectionFlags) { + throwIfTransactionMarkedSuccessful(); + beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags); + } + + private void beginTransactionUnchecked(int transactionMode, + SQLiteTransactionListener transactionListener, int connectionFlags) { + acquireConnectionIfNoTransaction(null, connectionFlags); // 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); // might throw + break; + case TRANSACTION_MODE_EXCLUSIVE: + mConnection.execute("BEGIN EXCLUSIVE;", null); // might throw + break; + default: + mConnection.execute("BEGIN;", null); // 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); // 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 { + releaseConnectionIfNoTransaction(); // 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> + * + * @throws IllegalStateException if there is no current transaction. + * + * @see #beginTransaction + * @see #setTransactionSuccessful + * @see #yieldTransaction + */ + public void endTransaction() { + throwIfNoTransaction(); + assert mConnection != null; + + endTransactionUnchecked(); + } + + private void endTransactionUnchecked() { + final Transaction top = mTransactionStack; + boolean successful = top.mMarkedSuccessful && !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); // might throw + } else { + mConnection.execute("ROLLBACK;", null); // might throw + } + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * @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. + * + * @see #beginTransaction + * @see #endTransaction + */ + public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe) { + 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); // might throw + } + + private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis) { + if (!mConnectionPool.shouldYieldConnection(mConnection, mConnectionFlags)) { + return false; + } + + final int transactionMode = mTransactionStack.mMode; + final SQLiteTransactionListener listener = mTransactionStack.mListener; + final int connectionFlags = mConnectionFlags; + endTransactionUnchecked(); // 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); // 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 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, int connectionFlags, SQLiteStatementInfo outStatementInfo) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + try { + mConnection.prepare(sql, outStatementInfo); // might throw + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + */ + public void execute(String sql, Object[] bindArgs, int connectionFlags) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags)) { + return; + } + + acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + try { + mConnection.execute(sql, bindArgs); // might throw + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * @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. + */ + public long executeForLong(String sql, Object[] bindArgs, int connectionFlags) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags)) { + return 0; + } + + acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + try { + return mConnection.executeForLong(sql, bindArgs); // might throw + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * @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. + */ + public String executeForString(String sql, Object[] bindArgs, int connectionFlags) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags)) { + return null; + } + + acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + try { + return mConnection.executeForString(sql, bindArgs); // might throw + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * @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. + */ + public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs, + int connectionFlags) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags)) { + return null; + } + + acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + try { + return mConnection.executeForBlobFileDescriptor(sql, bindArgs); // might throw + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * @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. + */ + public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags)) { + return 0; + } + + acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + try { + return mConnection.executeForChangedRowCount(sql, bindArgs); // might throw + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * @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. + */ + public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int connectionFlags) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags)) { + return 0; + } + + acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + try { + return mConnection.executeForLastInsertedRowId(sql, bindArgs); // might throw + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * @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. + */ + public int executeForCursorWindow(String sql, Object[] bindArgs, + CursorWindow window, int startPos, int requiredPos, boolean countAllRows, + int connectionFlags) { + 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)) { + window.clear(); + return 0; + } + + acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw + try { + return mConnection.executeForCursorWindow(sql, bindArgs, + window, startPos, requiredPos, countAllRows); // might throw + } finally { + releaseConnectionIfNoTransaction(); // 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}. + * @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. + */ + private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags) { + final int type = DatabaseUtils.getSqlStatementType(sql); + switch (type) { + case DatabaseUtils.STATEMENT_BEGIN: + beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags); + return true; + + case DatabaseUtils.STATEMENT_COMMIT: + setTransactionSuccessful(); + endTransaction(); + return true; + + case DatabaseUtils.STATEMENT_ABORT: + endTransaction(); + return true; + } + return false; + } + + private void acquireConnectionIfNoTransaction(String sql, int connectionFlags) { + if (mTransactionStack == null) { + assert mConnection == null; + mConnection = mConnectionPool.acquireConnection(sql, connectionFlags); // might throw + mConnectionFlags = connectionFlags; + } + } + + private void releaseConnectionIfNoTransaction() { + if (mTransactionStack == null && mConnection != null) { + 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 (mTransactionStack == null && mTransactionStack.mParent != null) { + 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; + } +} diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index c99a6fb..4e20da0 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -16,47 +16,19 @@ package android.database.sqlite; -import android.database.DatabaseUtils; import android.os.ParcelFileDescriptor; -import android.os.SystemClock; -import android.util.Log; - -import java.io.IOException; - -import dalvik.system.BlockGuard; /** - * A pre-compiled statement against a {@link SQLiteDatabase} that can be reused. - * The statement cannot return multiple rows, but 1x1 result sets are allowed. - * Don't use SQLiteStatement constructor directly, please use - * {@link SQLiteDatabase#compileStatement(String)} - *<p> - * SQLiteStatement is NOT internally synchronized so code using a SQLiteStatement from multiple - * threads should perform its own synchronization when using the SQLiteStatement. + * Represents a statement that can be executed against a database. The statement + * cannot return multiple rows or columns, but single value (1 x 1) result sets + * are supported. + * <p> + * This class is not thread-safe. + * </p> */ -@SuppressWarnings("deprecation") -public final class SQLiteStatement extends SQLiteProgram -{ - private static final String TAG = "SQLiteStatement"; - - private static final boolean READ = true; - private static final boolean WRITE = false; - - private SQLiteDatabase mOrigDb; - private int mState; - /** possible value for {@link #mState}. indicates that a transaction is started. */ - private static final int TRANS_STARTED = 1; - /** possible value for {@link #mState}. indicates that a lock is acquired. */ - private static final int LOCK_ACQUIRED = 2; - - /** - * Don't use SQLiteStatement constructor directly, please use - * {@link SQLiteDatabase#compileStatement(String)} - * @param db - * @param sql - */ - /* package */ SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) { - super(db, sql, bindArgs, false /* don't compile sql statement */); +public final class SQLiteStatement extends SQLiteProgram { + SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) { + super(db, sql, bindArgs); } /** @@ -67,7 +39,15 @@ public final class SQLiteStatement extends SQLiteProgram * some reason */ public void execute() { - executeUpdateDelete(); + acquireReference(); + try { + getSession().execute(getSql(), getBindArgs(), getConnectionFlags()); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } } /** @@ -79,21 +59,15 @@ public final class SQLiteStatement extends SQLiteProgram * some reason */ public int executeUpdateDelete() { + acquireReference(); try { - saveSqlAsLastSqlStatement(); - acquireAndLock(WRITE); - int numChanges = 0; - if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { - // since the statement doesn't have to be prepared, - // call the following native method which will not prepare - // the query plan - native_executeSql(mSql); - } else { - numChanges = native_execute(); - } - return numChanges; + return getSession().executeForChangedRowCount( + getSql(), getBindArgs(), getConnectionFlags()); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); + releaseReference(); } } @@ -107,23 +81,18 @@ public final class SQLiteStatement extends SQLiteProgram * some reason */ public long executeInsert() { + acquireReference(); try { - saveSqlAsLastSqlStatement(); - acquireAndLock(WRITE); - return native_executeInsert(); + return getSession().executeForLastInsertedRowId( + getSql(), getBindArgs(), getConnectionFlags()); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); + releaseReference(); } } - private void saveSqlAsLastSqlStatement() { - if (((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_UPDATE) || - (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_BEGIN) { - mDatabase.setLastSqlStatement(mSql); - } - } /** * Execute a statement that returns a 1 by 1 table with a numeric value. * For example, SELECT COUNT(*) FROM table; @@ -133,17 +102,15 @@ public final class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public long simpleQueryForLong() { + acquireReference(); try { - long timeStart = acquireAndLock(READ); - long retValue = native_1x1_long(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } catch (SQLiteDoneException e) { - throw new SQLiteDoneException( - "expected 1 row from this query but query returned no data. check the query: " + - mSql); + return getSession().executeForLong( + getSql(), getBindArgs(), getConnectionFlags()); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); + releaseReference(); } } @@ -156,17 +123,15 @@ public final class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public String simpleQueryForString() { + acquireReference(); try { - long timeStart = acquireAndLock(READ); - String retValue = native_1x1_string(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } catch (SQLiteDoneException e) { - throw new SQLiteDoneException( - "expected 1 row from this query but query returned no data. check the query: " + - mSql); + return getSession().executeForString( + getSql(), getBindArgs(), getConnectionFlags()); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); + releaseReference(); } } @@ -179,121 +144,20 @@ public final class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public ParcelFileDescriptor simpleQueryForBlobFileDescriptor() { + acquireReference(); try { - long timeStart = acquireAndLock(READ); - ParcelFileDescriptor retValue = native_1x1_blob_ashmem(); - mDatabase.logTimeStat(mSql, timeStart); - return retValue; - } catch (IOException ex) { - Log.e(TAG, "simpleQueryForBlobFileDescriptor() failed", ex); - return null; - } catch (SQLiteDoneException e) { - throw new SQLiteDoneException( - "expected 1 row from this query but query returned no data. check the query: " + - mSql); + return getSession().executeForBlobFileDescriptor( + getSql(), getBindArgs(), getConnectionFlags()); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; } finally { - releaseAndUnlock(); - } - } - - /** - * Called before every method in this class before executing a SQL statement, - * this method does the following: - * <ul> - * <li>make sure the database is open</li> - * <li>get a database connection from the connection pool,if possible</li> - * <li>notifies {@link BlockGuard} of read/write</li> - * <li>if the SQL statement is an update, start transaction if not already in one. - * otherwise, get lock on the database</li> - * <li>acquire reference on this object</li> - * <li>and then return the current time _after_ the database lock was acquired</li> - * </ul> - * <p> - * This method removes the duplicate code from the other public - * methods in this class. - */ - private long acquireAndLock(boolean rwFlag) { - mState = 0; - // use pooled database connection handles for SELECT SQL statements - mDatabase.verifyDbIsOpen(); - SQLiteDatabase db = ((mStatementType & SQLiteProgram.STATEMENT_USE_POOLED_CONN) > 0) - ? mDatabase.getDbConnection(mSql) : mDatabase; - // use the database connection obtained above - mOrigDb = mDatabase; - mDatabase = db; - setNativeHandle(mDatabase.mNativeHandle); - if (rwFlag == WRITE) { - BlockGuard.getThreadPolicy().onWriteToDisk(); - } else { - BlockGuard.getThreadPolicy().onReadFromDisk(); - } - - /* - * Special case handling of SQLiteDatabase.execSQL("BEGIN transaction"). - * we know it is execSQL("BEGIN transaction") from the caller IF there is no lock held. - * beginTransaction() methods in SQLiteDatabase call lockForced() before - * calling execSQL("BEGIN transaction"). - */ - if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_BEGIN) { - if (!mDatabase.isDbLockedByCurrentThread()) { - // transaction is NOT started by calling beginTransaction() methods in - // SQLiteDatabase - mDatabase.setTransactionUsingExecSqlFlag(); - } - } else if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_UPDATE) { - // got update SQL statement. if there is NO pending transaction, start one - if (!mDatabase.inTransaction()) { - mDatabase.beginTransactionNonExclusive(); - mState = TRANS_STARTED; - } + releaseReference(); } - // do I have database lock? if not, grab it. - if (!mDatabase.isDbLockedByCurrentThread()) { - mDatabase.lock(mSql); - mState = LOCK_ACQUIRED; - } - - acquireReference(); - long startTime = SystemClock.uptimeMillis(); - mDatabase.closePendingStatements(); - compileAndbindAllArgs(); - return startTime; } - /** - * this method releases locks and references acquired in {@link #acquireAndLock(boolean)} - */ - private void releaseAndUnlock() { - releaseReference(); - if (mState == TRANS_STARTED) { - try { - mDatabase.setTransactionSuccessful(); - } finally { - mDatabase.endTransaction(); - } - } else if (mState == LOCK_ACQUIRED) { - mDatabase.unlock(); - } - if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_COMMIT || - (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == - DatabaseUtils.STATEMENT_ABORT) { - mDatabase.resetTransactionUsingExecSqlFlag(); - } - clearBindings(); - // release the compiled sql statement so that the caller's SQLiteStatement no longer - // has a hard reference to a database object that may get deallocated at any point. - release(); - // restore the database connection handle to the original value - mDatabase = mOrigDb; - setNativeHandle(mDatabase.mNativeHandle); + @Override + public String toString() { + return "SQLiteProgram: " + getSql(); } - - private final native int native_execute(); - private final native long native_executeInsert(); - private final native long native_1x1_long(); - private final native String native_1x1_string(); - private final native ParcelFileDescriptor native_1x1_blob_ashmem() throws IOException; - private final native void native_executeSql(String sql); } diff --git a/core/java/android/database/sqlite/SQLiteStatementInfo.java b/core/java/android/database/sqlite/SQLiteStatementInfo.java new file mode 100644 index 0000000..3edfdb0 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteStatementInfo.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * Describes a SQLite statement. + * + * @hide + */ +public final class SQLiteStatementInfo { + /** + * The number of parameters that the statement has. + */ + public int numParameters; + + /** + * The names of all columns in the result set of the statement. + */ + public String[] columnNames; + + /** + * True if the statement is read-only. + */ + public boolean readOnly; +} |