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
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.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;
+ // 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;
+
/**
- * Algorithms used in ON CONFLICT clause
- * http://www.sqlite.org/lang_conflict.html
- */
- /**
- * When a constraint violation occurs, an immediate ROLLBACK occurs,
+ * 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 @@
* 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 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 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 @@
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 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 @@
*
* @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 @@
* 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;
+ String getLabel() {
+ synchronized (mLock) {
+ return mConfigurationLocked.label;
+ }
+ }
- /* package */ void onCorruption() {
- EventLog.writeEvent(EVENT_DB_CORRUPT, mPath);
+ /**
+ * Sends a corruption message to the database error handler.
+ */
+ 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. This is a no-op if mLockingEnabled is false.
+ * 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 #unlock()
+ * 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.
*/
- /* package */ void lock(String sql) {
- lock(sql, false);
+ SQLiteSession getThreadSession() {
+ return mThreadSession.get(); // initialValue() throws if database closed
}
- /* 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");
+ SQLiteSession createSession() {
+ final SQLiteConnectionPool pool;
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ pool = mConnectionPoolLocked;
}
- 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());
- }
+ return new SQLiteSession(pool);
}
/**
- * 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 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 lockForced() {
- lock(null, true);
+ 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;
+ }
+ return flags;
}
- private void lockForced(String sql) {
- lock(sql, true);
- }
-
- /**
- * 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();
- }
- }
- mLock.unlock();
- }
-
- /**
- * Releases the database lock.
- *
- * @see #unlockForced()
- */
- private void unlockForced() {
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
- if (mLock.getHoldCount() == 1) {
- checkLockHoldTime();
- }
- }
- mLock.unlock();
- }
-
- 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 @@
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 @@
* 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 @@
* 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>
+ * 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 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.
- *
- * @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 @@
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 @@
}
/**
- * 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 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 @@
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 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);
+
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ mConfigurationLocked.customFunctions.add(wrapper);
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
}
- private void releaseCustomFunctions() {
- synchronized (mCustomFunctions) {
- for (int i = 0; i < mCustomFunctions.size(); i++) {
- Integer function = mCustomFunctions.get(i);
- native_releaseCustomFunction(function.intValue());
- }
- mCustomFunctions.clear();
- }
- }
-
- // 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 @@
* {@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 @@
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 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 @@
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 @@
(!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
try {
return statement.executeUpdateDelete();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
@@ -1808,9 +1324,6 @@
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 @@
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.
- */
- private SQLiteDatabase(String path, CursorFactory factory, int flags,
- DatabaseErrorHandler errorHandler, short connectionNum) {
- if (path == null) {
- throw new IllegalArgumentException("path should not be null");
- }
- 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);
- }
-
- /**
- * return whether the DB is opened as read only.
- * @return true if DB is opened as read only
+ * @return True if database is opened as read only.
*/
public boolean isReadOnly() {
- return (mFlags & OPEN_READ_MASK) == OPEN_READONLY;
+ synchronized (mLock) {
+ return isReadOnlyLocked();
+ }
+ }
+
+ private boolean isReadOnlyLocked() {
+ return (mConfigurationLocked.openFlags & OPEN_READ_MASK) == OPEN_READONLY;
}
/**
- * @return true if the DB is currently open (has not been closed)
+ * Returns true if the database is in-memory db.
+ *
+ * @return True if the database is in-memory.
+ * @hide
*/
- public boolean isOpen() {
- return mNativeHandle != 0;
+ public boolean isInMemoryDatabase() {
+ synchronized (mLock) {
+ return mConfigurationLocked.isInMemoryDb();
+ }
}
+ /**
+ * Returns true if the database is currently open.
+ *
+ * @return True if the database is currently open (has not been closed).
+ */
+ public boolean isOpen() {
+ 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,115 +1516,21 @@
* 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());
- }
- }
+ if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
+ throw new IllegalStateException(
+ "expected value between 0 and " + MAX_SQL_CACHE_SIZE);
}
- }
- /* 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();
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ mConfigurationLocked.maxSqlCacheSize = cacheSize;
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
}
- 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);
- }
- }
-
- /* 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));
- }
- }
-
- /**
- * 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
@@ -2314,37 +1574,43 @@
* @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,178 +1618,68 @@
* @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
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+
+ if (!mIsWALEnabledLocked) {
+ return;
}
- mConnectionPool.close();
- setJournalMode(mPath, "TRUNCATE");
- mConnectionPool = null;
- } finally {
- unlock();
- }
- }
- /* 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);
- }
- } 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;
- }
-
- private boolean isPooledConnection() {
- return this.mConnectionNum > 0;
- }
-
- /* 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;
+ mIsWALEnabledLocked = false;
+ mConfigurationLocked.maxConnectionPoolSize = 1;
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
- // 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());
- }
- 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;
- }
- 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);
+ setJournalMode("TRUNCATE");
}
/**
- * this method is used to collect data about ALL open databases in the current process.
- * bugreport is a user of this data.
+ * Collect statistics about all open databases in the current process.
+ * Used by bug report.
*/
- /* package */ static ArrayList<DbStats> getDbStats() {
+ 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();
- }
- 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!
- }
+ for (SQLiteDatabase db : getActiveDatabases()) {
+ db.collectDbStats(dbStatsList);
}
return dbStatsList;
}
+ private void collectDbStats(ArrayList<DbStats> dbStatsList) {
+ synchronized (mLock) {
+ if (mConnectionPoolLocked != null) {
+ mConnectionPoolLocked.collectDbStats(dbStatsList);
+ }
+ }
+ }
+
+ private static ArrayList<SQLiteDatabase> getActiveDatabases() {
+ ArrayList<SQLiteDatabase> databases = new ArrayList<SQLiteDatabase>();
+ synchronized (sActiveDatabases) {
+ databases.addAll(sActiveDatabases.keySet());
+ }
+ return databases;
+ }
+
+ /**
+ * Dump detailed information about all open databases in the current process.
+ * Used by bug report.
+ */
+ static void dumpAll(Printer printer) {
+ for (SQLiteDatabase db : getActiveDatabases()) {
+ db.dump(printer);
+ }
+ }
+
+ private void dump(Printer printer) {
+ synchronized (mLock) {
+ if (mConnectionPoolLocked != null) {
+ printer.println("");
+ mConnectionPoolLocked.dump(printer);
+ }
+ }
+ }
+
/**
* Returns list of full pathnames of all attached databases including the main database
* by executing 'pragma database_list' on the database.
@@ -2532,23 +1688,27 @@
* 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 @@
* false otherwise.
*/
public boolean isDatabaseIntegrityOk() {
- verifyDbIsOpen();
+ throwIfNotOpen(); // fail fast
+
List<Pair<String, String>> attachedDbs = null;
try {
attachedDbs = getAttachedDbs();
@@ -2594,8 +1755,9 @@
} 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 @@
}
/**
- * 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();
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteDatabase: " + getPath();
+ }
+
+ private void throwIfNotOpen() {
+ synchronized (mConnectionPoolLocked) {
+ throwIfNotOpenLocked();
+ }
+ }
+
+ private void throwIfNotOpenLocked() {
+ if (mConnectionPoolLocked == null) {
+ throw new IllegalStateException("The database '" + mConfigurationLocked.label
+ + "' is not open.");
+ }
+ }
/**
- * Native call to set the locale. {@link #lock} must be held when calling
- * this method.
- * @throws SQLException
+ * Used to allow returning sub-classes of {@link Cursor} when calling query.
*/
- private native void native_setLocale(String loc, int flags);
+ public interface CursorFactory {
+ /**
+ * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}.
+ */
+ public Cursor newCursor(SQLiteDatabase db,
+ SQLiteCursorDriver masterQuery, String editTable,
+ SQLiteQuery query);
+ }
/**
- * 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
+ * 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 int native_getDbLookaside();
-
- /**
- * finalizes the given statement id.
- *
- * @param statementId statement to be finzlied by sqlite
- */
- private final native void native_finalize(int statementId);
-
- /**
- * set sqlite soft heap limit
- * http://www.sqlite.org/c3ref/soft_heap_limit64.html
- */
- private native void native_setSqliteSoftHeapLimit(int softHeapLimit);
+ public interface CustomFunction {
+ public void callback(String[] args);
+ }
}