From e5360fbf3efe85427f7e7f59afe7bbeddb4949ac Mon Sep 17 00:00:00 2001
From: Jeff Brown
Date: Mon, 31 Oct 2011 17:48:13 -0700
Subject: 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
---
.../database/sqlite/DatabaseConnectionPool.java | 348 ----
.../android/database/sqlite/SQLiteClosable.java | 22 +-
.../android/database/sqlite/SQLiteCompiledSql.java | 158 --
.../android/database/sqlite/SQLiteConnection.java | 1149 +++++++++++++
.../database/sqlite/SQLiteConnectionPool.java | 907 ++++++++++
.../java/android/database/sqlite/SQLiteCursor.java | 118 +-
.../database/sqlite/SQLiteCursorDriver.java | 2 +-
.../database/sqlite/SQLiteCustomFunction.java | 53 +
.../android/database/sqlite/SQLiteDatabase.java | 1759 ++++++--------------
.../sqlite/SQLiteDatabaseConfiguration.java | 167 ++
core/java/android/database/sqlite/SQLiteDebug.java | 7 +
.../database/sqlite/SQLiteDirectCursorDriver.java | 37 +-
.../java/android/database/sqlite/SQLiteGlobal.java | 89 +
.../android/database/sqlite/SQLiteOpenHelper.java | 14 +-
.../android/database/sqlite/SQLiteProgram.java | 372 +----
core/java/android/database/sqlite/SQLiteQuery.java | 154 +-
.../database/sqlite/SQLiteQueryBuilder.java | 14 +-
.../android/database/sqlite/SQLiteSession.java | 878 ++++++++++
.../android/database/sqlite/SQLiteStatement.java | 248 +--
.../database/sqlite/SQLiteStatementInfo.java | 39 +
core/java/android/util/LruCache.java | 17 +
21 files changed, 3986 insertions(+), 2566 deletions(-)
delete mode 100644 core/java/android/database/sqlite/DatabaseConnectionPool.java
delete mode 100644 core/java/android/database/sqlite/SQLiteCompiledSql.java
create mode 100644 core/java/android/database/sqlite/SQLiteConnection.java
create mode 100644 core/java/android/database/sqlite/SQLiteConnectionPool.java
create mode 100644 core/java/android/database/sqlite/SQLiteCustomFunction.java
create mode 100644 core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
create mode 100644 core/java/android/database/sqlite/SQLiteGlobal.java
create mode 100644 core/java/android/database/sqlite/SQLiteSession.java
create mode 100644 core/java/android/database/sqlite/SQLiteStatementInfo.java
(limited to 'core/java')
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 mPool = new ArrayList(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 getConnectionList() {
- ArrayList list = new ArrayList();
- 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 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 mHolderIds = new HashSet();
-
- 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.
- *
- *
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 sqlite3 object.
+ *
+ * When database connection pooling is enabled, there can be multiple active
+ * connections to the same database. Otherwise there is typically only one
+ * connection per database.
+ *
+ * When the SQLite WAL feature is enabled, multiple readers and one writer
+ * can concurrently access the database. Without WAL, readers and writers
+ * are mutually exclusive.
+ *
+ *
+ *
Ownership and concurrency guarantees
+ *
+ * Connection objects are not thread-safe. They are acquired as needed to
+ * perform a database operation and are then returned to the pool. At any
+ * given time, a connection is either owned and used by a {@link SQLiteSession}
+ * object or the {@link SQLiteConnectionPool}. Those classes are
+ * responsible for serializing operations to guard against concurrent
+ * use of a connection.
+ *
+ * The guarantee of having a single owner allows this class to be implemented
+ * without locks and greatly simplifies resource management.
+ *
+ *
+ *
Encapsulation guarantees
+ *
+ * The connection object object owns *all* of the SQLite related native
+ * objects that are associated with the connection. What's more, there are
+ * no other objects in the system that are capable of obtaining handles to
+ * those native objects. Consequently, when the connection is closed, we do
+ * not have to worry about what other components might have references to
+ * its associated SQLite state -- there are none.
+ *
+ * Encapsulation is what ensures that the connection object's
+ * lifecycle does not become a tortured mess of finalizers and reference
+ * queues.
+ *
+ *
+ * @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.
+ *
+ * This method can be used to check for syntax errors during compilation
+ * prior to execution of the statement. If the {@code outStatementInfo} argument
+ * is not null, the provided {@link SQLiteStatementInfo} object is populated
+ * with information about the statement.
+ *
+ * A prepared statement makes no reference to the arguments that may eventually
+ * be bound to it, consequently it it possible to cache certain prepared statements
+ * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable,
+ * then it will be stored in the cache for later.
+ *
+ * To take advantage of this behavior as an optimization, the connection pool
+ * provides a method to acquire a connection that already has a given SQL statement
+ * in its prepared statement cache so that it is ready for execution.
+ *
+ *
+ * @param sql The SQL statement to prepare.
+ * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate
+ * with information about the statement, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error.
+ */
+ public void prepare(String sql, SQLiteStatementInfo outStatementInfo) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ 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 long 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 long, 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 String, 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 startPos.
+ * @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 countAllRows 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 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 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 {
+ 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 cache = snapshot();
+ if (!cache.isEmpty()) {
+ int i = 0;
+ for (Map.Entry 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(" ");
+ }
+ }
+ }
+
+ 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
*
- * @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.
+ *
+ * There is no longer the concept of a database lock, so this method always returns false.
+ *
*
- * @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));
- }
- 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> iter = mPrograms.entrySet().iterator();
- while (iter.hasNext()) {
- Map.Entry 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> iter = mPrograms.entrySet().iterator();
- boolean found = false;
- while (iter.hasNext()) {
- Map.Entry 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 mCustomFunctions =
- new ArrayList();
-
- 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();
- // 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'.
- *
- * 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 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(cacheSize) {
- @Override
- protected void entryRemoved(boolean evicted, String key, SQLiteCompiledSql oldValue,
- SQLiteCompiledSql newValue) {
- verifyLockOwner();
- oldValue.releaseIfNotInUse();
- }
- };
- if (oldCompiledQueries != null) {
- for (Map.Entry 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 list = new ArrayList(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 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 getDbStats() {
+ ArrayList dbStatsList = new ArrayList();
+ 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 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 getActiveDatabases() {
+ ArrayList databases = new ArrayList();
+ 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 getDbStats() {
- ArrayList dbStatsList = new ArrayList();
- // make a local copy of mActiveDatabases - so that this method is not competing
- // for synchronization lock on mActiveDatabases
- ArrayList> tempList;
- synchronized(mActiveDatabases) {
- tempList = (ArrayList>)mActiveDatabases.clone();
+ static void dumpAll(Printer printer) {
+ for (SQLiteDatabase db : getActiveDatabases()) {
+ db.dump(printer);
}
- for (WeakReference 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> attachedDbs = db.getAttachedDbs();
- if (attachedDbs == null) {
- continue;
- }
- for (int i = 0; i < attachedDbs.size(); i++) {
- Pair 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> getAttachedDbs() {
- if (!isOpen()) {
- return null;
- }
ArrayList> attachedDbs = new ArrayList>();
- 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 as "
- // 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("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 as "
+ // 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("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> 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>();
- attachedDbs.add(new Pair("main", this.mPath));
+ attachedDbs.add(new Pair("main", getPath()));
}
+
for (int i = 0; i < attachedDbs.size(); i++) {
Pair 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.
+ *
+ * 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.
+ *
+ * Each connection maintains its own copy of this object so it can
+ * keep track of which settings have already been applied.
+ *
+ *
+ * @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 customFunctions =
+ new ArrayList();
+
+ /**
+ * 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.
- *
- * SQLiteProgram is NOT internally synchronized so code using a SQLiteProgram from multiple
- * threads should perform its own synchronization when using the SQLiteProgram.
+ *
+ * This class is not thread-safe.
+ *
*/
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.
- *
- *
{@link #bindBlob(int, byte[])}
- *
{@link #bindDouble(int, double)}
- *
{@link #bindLong(int, long)}
- *
{@link #bindNull(int)}
- *
{@link #bindString(int, String)}
- *
- *
- * Each entry in the array is a Pair of
- *
- *
bind arg position number
- *
the value to be bound to the bindarg
- *
- *
- * It is lazily initialized in the above bind methods
- * and it is cleared in {@link #clearBindings()} method.
- *
- * It is protected (in multi-threaded environment) by {@link SQLiteProgram}.this
- */
- /* package */ HashMap 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();
- }
- 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.
- *
- *
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.
+ *
+ * This class is not thread-safe.
+ *
*/
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.
+ *
+ *
About database sessions
+ *
+ * Database access is always performed using a session. The session
+ * manages the lifecycle of transactions and database connections.
+ *
+ * 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.
+ *
+ * When Write Ahead Logging (WAL) 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.
+ *
+ *
+ *
Ownership and concurrency guarantees
+ *
+ * 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.
+ *
+ * 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.
+ *
+ *
+ *
Transactions
+ *
+ * There are two kinds of transaction: implicit transactions and explicit
+ * transactions.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * When a transaction is started, the client can provide a {@link SQLiteTransactionListener}
+ * to listen for notifications of transaction-related events.
+ *
+ * Recommended usage:
+ *
+ * // 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();
+ * }
+ *
+ *
+ *
+ *
Database connections
+ *
+ * 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.
+ *
+ * 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.
+ *
+ *
+ *
Responsiveness
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * Guidelines:
+ *
+ *
Do not perform database transactions on the UI thread.
+ *
Keep database transactions as short as possible.
+ *
Simple queries often run faster than complex queries.
+ *
Measure the performance of your database transactions.
+ *
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.
+ *
+ *
+ * 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.
+ *
+ * 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 SHARED lock is acquired, otherwise
+ * a RESERVED lock is acquired.
+ *
+ * While holding a SHARED lock, this session is only allowed to
+ * read but other sessions are allowed to read or write.
+ * While holding a RESERVED lock, this session is allowed to read
+ * or write but other sessions are only allowed to read.
+ *
+ * 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.
+ *
+ * Corresponds to the SQLite BEGIN DEFERRED transaction mode.
+ *
+ */
+ public static final int TRANSACTION_MODE_DEFERRED = 0;
+
+ /**
+ * Transaction mode: Immediate.
+ *
+ * When an immediate transaction begins, the session acquires a
+ * RESERVED lock.
+ *
+ * While holding a RESERVED lock, this session is allowed to read
+ * or write but other sessions are only allowed to read.
+ *
+ * Corresponds to the SQLite BEGIN IMMEDIATE transaction mode.
+ *
+ */
+ public static final int TRANSACTION_MODE_IMMEDIATE = 1;
+
+ /**
+ * Transaction mode: Exclusive.
+ *
+ * When an exclusive transaction begins, the session acquires an
+ * EXCLUSIVE lock.
+ *
+ * While holding an EXCLUSIVE lock, this session is allowed to read
+ * or write but no other sessions are allowed to access the database.
+ *
+ * Corresponds to the SQLite BEGIN EXCLUSIVE transaction mode.
+ *
+ */
+ 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ *
+ * @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.
+ *
+ * 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.
+ *
+ *
+ * @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.
+ *
+ * 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.
+ *
+ * This method must be called exactly once for each call to {@link #beginTransaction}.
+ *
+ *
+ * @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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * The changes that were committed by a yield cannot be rolled back later.
+ *
+ * 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.
+ *
+ * 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 throwIfNested is true, then attempting to yield
+ * a nested transaction will throw {@link IllegalStateException}, otherwise
+ * the method will return false in that case.
+ *
+ * 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 false.
+ *
+ *
+ * @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 throwIfNested 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.
+ *
+ * This method can be used to check for syntax errors during compilation
+ * prior to execution of the statement. If the {@code outStatementInfo} argument
+ * is not null, the provided {@link SQLiteStatementInfo} object is populated
+ * with information about the statement.
+ *
+ * A prepared statement makes no reference to the arguments that may eventually
+ * be bound to it, consequently it it possible to cache certain prepared statements
+ * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable,
+ * then it will be stored in the cache for later and reused if possible.
+ *
+ *
+ * @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 long 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 long, 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 String, 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 startPos.
+ * @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 countAllRows 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)}
- *
- * 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.
+ *
+ * This class is not thread-safe.
+ *
*/
-@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:
- *
- *
make sure the database is open
- *
get a database connection from the connection pool,if possible
- *
notifies {@link BlockGuard} of read/write
- *
if the SQL statement is an update, start transaction if not already in one.
- * otherwise, get lock on the database
- *
acquire reference on this object
- *
and then return the current time _after_ the database lock was acquired
- *
- *
- * 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;
+}
diff --git a/core/java/android/util/LruCache.java b/core/java/android/util/LruCache.java
index f1014a7..51e373c 100644
--- a/core/java/android/util/LruCache.java
+++ b/core/java/android/util/LruCache.java
@@ -86,6 +86,23 @@ public class LruCache {
}
/**
+ * Sets the size of the cache.
+ * @param maxSize The new maximum size.
+ *
+ * @hide
+ */
+ public void resize(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+
+ synchronized (this) {
+ this.maxSize = maxSize;
+ }
+ trimToSize(maxSize);
+ }
+
+ /**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
--
cgit v1.1