summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Brown <jeffbrown@google.com>2012-01-12 15:03:26 -0800
committerAndroid (Google) Code Review <android-gerrit@google.com>2012-01-12 15:03:26 -0800
commit986f00faf44b0d9ed5b1384746ca4254037fc180 (patch)
treeee51ba282c84cc22d2391ff307fdfda10af9b92e
parent156936975dec49022681f218b8221ef9e3d87011 (diff)
parente5360fbf3efe85427f7e7f59afe7bbeddb4949ac (diff)
downloadframeworks_base-986f00faf44b0d9ed5b1384746ca4254037fc180.zip
frameworks_base-986f00faf44b0d9ed5b1384746ca4254037fc180.tar.gz
frameworks_base-986f00faf44b0d9ed5b1384746ca4254037fc180.tar.bz2
Merge "Rewrite SQLite database wrappers."
-rw-r--r--api/current.txt4
-rw-r--r--core/java/android/database/sqlite/DatabaseConnectionPool.java348
-rw-r--r--core/java/android/database/sqlite/SQLiteClosable.java22
-rw-r--r--core/java/android/database/sqlite/SQLiteCompiledSql.java158
-rw-r--r--core/java/android/database/sqlite/SQLiteConnection.java1149
-rw-r--r--core/java/android/database/sqlite/SQLiteConnectionPool.java907
-rw-r--r--core/java/android/database/sqlite/SQLiteCursor.java118
-rw-r--r--core/java/android/database/sqlite/SQLiteCursorDriver.java2
-rw-r--r--core/java/android/database/sqlite/SQLiteCustomFunction.java53
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabase.java1759
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java167
-rw-r--r--core/java/android/database/sqlite/SQLiteDebug.java7
-rw-r--r--core/java/android/database/sqlite/SQLiteDirectCursorDriver.java37
-rw-r--r--core/java/android/database/sqlite/SQLiteGlobal.java89
-rw-r--r--core/java/android/database/sqlite/SQLiteOpenHelper.java14
-rw-r--r--core/java/android/database/sqlite/SQLiteProgram.java372
-rw-r--r--core/java/android/database/sqlite/SQLiteQuery.java154
-rw-r--r--core/java/android/database/sqlite/SQLiteQueryBuilder.java14
-rw-r--r--core/java/android/database/sqlite/SQLiteSession.java878
-rw-r--r--core/java/android/database/sqlite/SQLiteStatement.java248
-rw-r--r--core/java/android/database/sqlite/SQLiteStatementInfo.java39
-rw-r--r--core/java/android/util/LruCache.java17
-rw-r--r--core/jni/Android.mk8
-rw-r--r--core/jni/AndroidRuntime.cpp14
-rw-r--r--core/jni/android_database_CursorWindow.cpp2
-rw-r--r--core/jni/android_database_SQLiteCommon.cpp139
-rw-r--r--core/jni/android_database_SQLiteCommon.h51
-rw-r--r--core/jni/android_database_SQLiteCompiledSql.cpp123
-rw-r--r--core/jni/android_database_SQLiteConnection.cpp959
-rw-r--r--core/jni/android_database_SQLiteDatabase.cpp636
-rw-r--r--core/jni/android_database_SQLiteGlobal.cpp78
-rw-r--r--core/jni/android_database_SQLiteProgram.cpp195
-rw-r--r--core/jni/android_database_SQLiteQuery.cpp276
-rw-r--r--core/jni/android_database_SQLiteStatement.cpp286
-rw-r--r--core/jni/sqlite3_exception.h47
-rw-r--r--core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java368
-rw-r--r--core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java48
-rw-r--r--core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java971
-rw-r--r--core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java213
-rw-r--r--core/tests/coretests/src/android/database/sqlite/SQLiteUnfinalizedExceptionTest.java81
40 files changed, 5224 insertions, 5827 deletions
diff --git a/api/current.txt b/api/current.txt
index fa2a475..8e93499 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -7204,7 +7204,7 @@ package android.database.sqlite {
method public long insertWithOnConflict(java.lang.String, java.lang.String, android.content.ContentValues, int);
method public boolean isDatabaseIntegrityOk();
method public boolean isDbLockedByCurrentThread();
- method public boolean isDbLockedByOtherThreads();
+ method public deprecated boolean isDbLockedByOtherThreads();
method public boolean isOpen();
method public boolean isReadOnly();
method public deprecated void markTableSyncable(java.lang.String, java.lang.String);
@@ -7226,7 +7226,7 @@ package android.database.sqlite {
method public long replace(java.lang.String, java.lang.String, android.content.ContentValues);
method public long replaceOrThrow(java.lang.String, java.lang.String, android.content.ContentValues) throws android.database.SQLException;
method public void setLocale(java.util.Locale);
- method public void setLockingEnabled(boolean);
+ method public deprecated void setLockingEnabled(boolean);
method public void setMaxSqlCacheSize(int);
method public long setMaximumSize(long);
method public void setPageSize(long);
diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java
deleted file mode 100644
index 39a9d23..0000000
--- a/core/java/android/database/sqlite/DatabaseConnectionPool.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * Copyright (C) 20010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.database.sqlite;
-
-import android.content.res.Resources;
-import android.os.SystemClock;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Random;
-
-/**
- * A connection pool to be used by readers.
- * Note that each connection can be used by only one reader at a time.
- */
-/* package */ class DatabaseConnectionPool {
-
- private static final String TAG = "DatabaseConnectionPool";
-
- /** The default connection pool size. */
- private volatile int mMaxPoolSize =
- Resources.getSystem().getInteger(com.android.internal.R.integer.db_connection_pool_size);
-
- /** The connection pool objects are stored in this member.
- * TODO: revisit this data struct as the number of pooled connections increase beyond
- * single-digit values.
- */
- private final ArrayList<PoolObj> mPool = new ArrayList<PoolObj>(mMaxPoolSize);
-
- /** the main database connection to which this connection pool is attached */
- private final SQLiteDatabase mParentDbObj;
-
- /** Random number generator used to pick a free connection out of the pool */
- private Random rand; // lazily initialized
-
- /* package */ DatabaseConnectionPool(SQLiteDatabase db) {
- this.mParentDbObj = db;
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Max Pool Size: " + mMaxPoolSize);
- }
- }
-
- /**
- * close all database connections in the pool - even if they are in use!
- */
- /* package */ synchronized void close() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Closing the connection pool on " + mParentDbObj.getPath() + toString());
- }
- for (int i = mPool.size() - 1; i >= 0; i--) {
- mPool.get(i).mDb.close();
- }
- mPool.clear();
- }
-
- /**
- * get a free connection from the pool
- *
- * @param sql if not null, try to find a connection inthe pool which already has cached
- * the compiled statement for this sql.
- * @return the Database connection that the caller can use
- */
- /* package */ synchronized SQLiteDatabase get(String sql) {
- SQLiteDatabase db = null;
- PoolObj poolObj = null;
- int poolSize = mPool.size();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert sql != null;
- doAsserts();
- }
- if (getFreePoolSize() == 0) {
- // no free ( = available) connections
- if (mMaxPoolSize == poolSize) {
- // maxed out. can't open any more connections.
- // let the caller wait on one of the pooled connections
- // preferably a connection caching the pre-compiled statement of the given SQL
- if (mMaxPoolSize == 1) {
- poolObj = mPool.get(0);
- } else {
- for (int i = 0; i < mMaxPoolSize; i++) {
- if (mPool.get(i).mDb.isInStatementCache(sql)) {
- poolObj = mPool.get(i);
- break;
- }
- }
- if (poolObj == null) {
- // there are no database connections with the given SQL pre-compiled.
- // ok to return any of the connections.
- if (rand == null) {
- rand = new Random(SystemClock.elapsedRealtime());
- }
- poolObj = mPool.get(rand.nextInt(mMaxPoolSize));
- }
- }
- db = poolObj.mDb;
- } else {
- // create a new connection and add it to the pool, since we haven't reached
- // max pool size allowed
- db = mParentDbObj.createPoolConnection((short)(poolSize + 1));
- poolObj = new PoolObj(db);
- mPool.add(poolSize, poolObj);
- }
- } else {
- // there are free connections available. pick one
- // preferably a connection caching the pre-compiled statement of the given SQL
- for (int i = 0; i < poolSize; i++) {
- if (mPool.get(i).isFree() && mPool.get(i).mDb.isInStatementCache(sql)) {
- poolObj = mPool.get(i);
- break;
- }
- }
- if (poolObj == null) {
- // didn't find a free database connection with the given SQL already
- // pre-compiled. return a free connection (this means, the same SQL could be
- // pre-compiled on more than one database connection. potential wasted memory.)
- for (int i = 0; i < poolSize; i++) {
- if (mPool.get(i).isFree()) {
- poolObj = mPool.get(i);
- break;
- }
- }
- }
- db = poolObj.mDb;
- }
-
- assert poolObj != null;
- assert poolObj.mDb == db;
-
- poolObj.acquire();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "END get-connection: " + toString() + poolObj.toString());
- }
- return db;
- // TODO if a thread acquires a connection and dies without releasing the connection, then
- // there could be a connection leak.
- }
-
- /**
- * release the given database connection back to the pool.
- * @param db the connection to be released
- */
- /* package */ synchronized void release(SQLiteDatabase db) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert db.mConnectionNum > 0;
- doAsserts();
- assert mPool.get(db.mConnectionNum - 1).mDb == db;
- }
-
- PoolObj poolObj = mPool.get(db.mConnectionNum - 1);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "BEGIN release-conn: " + toString() + poolObj.toString());
- }
-
- if (poolObj.isFree()) {
- throw new IllegalStateException("Releasing object already freed: " +
- db.mConnectionNum);
- }
-
- poolObj.release();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "END release-conn: " + toString() + poolObj.toString());
- }
- }
-
- /**
- * Returns a list of all database connections in the pool (both free and busy connections).
- * This method is used when "adb bugreport" is done.
- */
- /* package */ synchronized ArrayList<SQLiteDatabase> getConnectionList() {
- ArrayList<SQLiteDatabase> list = new ArrayList<SQLiteDatabase>();
- for (int i = mPool.size() - 1; i >= 0; i--) {
- list.add(mPool.get(i).mDb);
- }
- return list;
- }
-
- /**
- * package level access for testing purposes only. otherwise, private should be sufficient.
- */
- /* package */ int getFreePoolSize() {
- int count = 0;
- for (int i = mPool.size() - 1; i >= 0; i--) {
- if (mPool.get(i).isFree()) {
- count++;
- }
- }
- return count++;
- }
-
- /**
- * only for testing purposes
- */
- /* package */ ArrayList<PoolObj> getPool() {
- return mPool;
- }
-
- @Override
- public String toString() {
- StringBuilder buff = new StringBuilder();
- buff.append("db: ");
- buff.append(mParentDbObj.getPath());
- buff.append(", totalsize = ");
- buff.append(mPool.size());
- buff.append(", #free = ");
- buff.append(getFreePoolSize());
- buff.append(", maxpoolsize = ");
- buff.append(mMaxPoolSize);
- for (PoolObj p : mPool) {
- buff.append("\n");
- buff.append(p.toString());
- }
- return buff.toString();
- }
-
- private void doAsserts() {
- for (int i = 0; i < mPool.size(); i++) {
- mPool.get(i).verify();
- assert mPool.get(i).mDb.mConnectionNum == (i + 1);
- }
- }
-
- /** only used for testing purposes. */
- /* package */ synchronized void setMaxPoolSize(int size) {
- mMaxPoolSize = size;
- }
-
- /** only used for testing purposes. */
- /* package */ synchronized int getMaxPoolSize() {
- return mMaxPoolSize;
- }
-
- /** only used for testing purposes. */
- /* package */ boolean isDatabaseObjFree(SQLiteDatabase db) {
- return mPool.get(db.mConnectionNum - 1).isFree();
- }
-
- /** only used for testing purposes. */
- /* package */ int getSize() {
- return mPool.size();
- }
-
- /**
- * represents objects in the connection pool.
- * package-level access for testing purposes only.
- */
- /* package */ static class PoolObj {
-
- private final SQLiteDatabase mDb;
- private boolean mFreeBusyFlag = FREE;
- private static final boolean FREE = true;
- private static final boolean BUSY = false;
-
- /** the number of threads holding this connection */
- // @GuardedBy("this")
- private int mNumHolders = 0;
-
- /** contains the threadIds of the threads holding this connection.
- * used for debugging purposes only.
- */
- // @GuardedBy("this")
- private HashSet<Long> mHolderIds = new HashSet<Long>();
-
- public PoolObj(SQLiteDatabase db) {
- mDb = db;
- }
-
- private synchronized void acquire() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert isFree();
- long id = Thread.currentThread().getId();
- assert !mHolderIds.contains(id);
- mHolderIds.add(id);
- }
-
- mNumHolders++;
- mFreeBusyFlag = BUSY;
- }
-
- private synchronized void release() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- long id = Thread.currentThread().getId();
- assert mHolderIds.size() == mNumHolders;
- assert mHolderIds.contains(id);
- mHolderIds.remove(id);
- }
-
- mNumHolders--;
- if (mNumHolders == 0) {
- mFreeBusyFlag = FREE;
- }
- }
-
- private synchronized boolean isFree() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- verify();
- }
- return (mFreeBusyFlag == FREE);
- }
-
- private synchronized void verify() {
- if (mFreeBusyFlag == FREE) {
- assert mNumHolders == 0;
- } else {
- assert mNumHolders > 0;
- }
- }
-
- /**
- * only for testing purposes
- */
- /* package */ synchronized int getNumHolders() {
- return mNumHolders;
- }
-
- @Override
- public String toString() {
- StringBuilder buff = new StringBuilder();
- buff.append(", conn # ");
- buff.append(mDb.mConnectionNum);
- buff.append(", mCountHolders = ");
- synchronized(this) {
- buff.append(mNumHolders);
- buff.append(", freeBusyFlag = ");
- buff.append(mFreeBusyFlag);
- for (Long l : mHolderIds) {
- buff.append(", id = " + l);
- }
- }
- return buff.toString();
- }
- }
-}
diff --git a/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java
index 01e9fb3..7e91a7b 100644
--- a/core/java/android/database/sqlite/SQLiteClosable.java
+++ b/core/java/android/database/sqlite/SQLiteClosable.java
@@ -16,8 +16,6 @@
package android.database.sqlite;
-import android.database.CursorWindow;
-
/**
* An object created from a SQLiteDatabase that can be closed.
*/
@@ -31,7 +29,7 @@ public abstract class SQLiteClosable {
synchronized(this) {
if (mReferenceCount <= 0) {
throw new IllegalStateException(
- "attempt to re-open an already-closed object: " + getObjInfo());
+ "attempt to re-open an already-closed object: " + this);
}
mReferenceCount++;
}
@@ -56,22 +54,4 @@ public abstract class SQLiteClosable {
onAllReferencesReleasedFromContainer();
}
}
-
- private String getObjInfo() {
- StringBuilder buff = new StringBuilder();
- buff.append(this.getClass().getName());
- buff.append(" (");
- if (this instanceof SQLiteDatabase) {
- buff.append("database = ");
- buff.append(((SQLiteDatabase)this).getPath());
- } else if (this instanceof SQLiteProgram) {
- buff.append("mSql = ");
- buff.append(((SQLiteProgram)this).mSql);
- } else if (this instanceof CursorWindow) {
- buff.append("mStartPos = ");
- buff.append(((CursorWindow)this).getStartPosition());
- }
- buff.append(") ");
- return buff.toString();
- }
}
diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java
deleted file mode 100644
index dafbc79..0000000
--- a/core/java/android/database/sqlite/SQLiteCompiledSql.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.database.sqlite;
-
-import android.os.StrictMode;
-import android.util.Log;
-
-/**
- * This class encapsulates compilation of sql statement and release of the compiled statement obj.
- * Once a sql statement is compiled, it is cached in {@link SQLiteDatabase}
- * and it is released in one of the 2 following ways
- * 1. when {@link SQLiteDatabase} object is closed.
- * 2. if this is not cached in {@link SQLiteDatabase}, {@link android.database.Cursor#close()}
- * releaases this obj.
- */
-/* package */ class SQLiteCompiledSql {
-
- private static final String TAG = "SQLiteCompiledSql";
-
- /** The database this program is compiled against. */
- /* package */ final SQLiteDatabase mDatabase;
-
- /**
- * Native linkage, do not modify. This comes from the database.
- */
- /* package */ final int nHandle;
-
- /**
- * Native linkage, do not modify. When non-0 this holds a reference to a valid
- * sqlite3_statement object. It is only updated by the native code, but may be
- * checked in this class when the database lock is held to determine if there
- * is a valid native-side program or not.
- */
- /* package */ int nStatement = 0;
-
- /** the following are for debugging purposes */
- private String mSqlStmt = null;
- private final Throwable mStackTrace;
-
- /** when in cache and is in use, this member is set */
- private boolean mInUse = false;
-
- /* package */ SQLiteCompiledSql(SQLiteDatabase db, String sql) {
- db.verifyDbIsOpen();
- db.verifyLockOwner();
- mDatabase = db;
- mSqlStmt = sql;
- if (StrictMode.vmSqliteObjectLeaksEnabled()) {
- mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
- } else {
- mStackTrace = null;
- }
- nHandle = db.mNativeHandle;
- native_compile(sql);
- }
-
- /* package */ void releaseSqlStatement() {
- // Note that native_finalize() checks to make sure that nStatement is
- // non-null before destroying it.
- if (nStatement != 0) {
- mDatabase.finalizeStatementLater(nStatement);
- nStatement = 0;
- }
- }
-
- /**
- * returns true if acquire() succeeds. false otherwise.
- */
- /* package */ synchronized boolean acquire() {
- if (mInUse) {
- // it is already in use.
- return false;
- }
- mInUse = true;
- return true;
- }
-
- /* package */ synchronized void release() {
- mInUse = false;
- }
-
- /* package */ synchronized void releaseIfNotInUse() {
- // if it is not in use, release its memory from the database
- if (!mInUse) {
- releaseSqlStatement();
- }
- }
-
- /**
- * Make sure that the native resource is cleaned up.
- */
- @Override
- protected void finalize() throws Throwable {
- try {
- if (nStatement == 0) return;
- // don't worry about finalizing this object if it is ALREADY in the
- // queue of statements to be finalized later
- if (mDatabase.isInQueueOfStatementsToBeFinalized(nStatement)) {
- return;
- }
- // finalizer should NEVER get called
- // but if the database itself is not closed and is GC'ed, then
- // all sub-objects attached to the database could end up getting GC'ed too.
- // in that case, don't print any warning.
- if (mInUse && mStackTrace != null) {
- int len = mSqlStmt.length();
- StrictMode.onSqliteObjectLeaked(
- "Releasing statement in a finalizer. Please ensure " +
- "that you explicitly call close() on your cursor: " +
- mSqlStmt.substring(0, (len > 1000) ? 1000 : len),
- mStackTrace);
- }
- releaseSqlStatement();
- } finally {
- super.finalize();
- }
- }
-
- @Override public String toString() {
- synchronized(this) {
- StringBuilder buff = new StringBuilder();
- buff.append(" nStatement=");
- buff.append(nStatement);
- buff.append(", mInUse=");
- buff.append(mInUse);
- buff.append(", db=");
- buff.append(mDatabase.getPath());
- buff.append(", db_connectionNum=");
- buff.append(mDatabase.mConnectionNum);
- buff.append(", sql=");
- int len = mSqlStmt.length();
- buff.append(mSqlStmt.substring(0, (len > 100) ? 100 : len));
- return buff.toString();
- }
- }
-
- /**
- * Compiles SQL into a SQLite program.
- *
- * <P>The database lock must be held when calling this method.
- * @param sql The SQL to compile.
- */
- private final native void native_compile(String sql);
-}
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
new file mode 100644
index 0000000..e45d66d
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -0,0 +1,1149 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import dalvik.system.BlockGuard;
+import dalvik.system.CloseGuard;
+
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDebug.DbStats;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.util.LruCache;
+import android.util.Printer;
+
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a SQLite database connection.
+ * Each connection wraps an instance of a native <code>sqlite3</code> object.
+ * <p>
+ * When database connection pooling is enabled, there can be multiple active
+ * connections to the same database. Otherwise there is typically only one
+ * connection per database.
+ * </p><p>
+ * When the SQLite WAL feature is enabled, multiple readers and one writer
+ * can concurrently access the database. Without WAL, readers and writers
+ * are mutually exclusive.
+ * </p>
+ *
+ * <h2>Ownership and concurrency guarantees</h2>
+ * <p>
+ * Connection objects are not thread-safe. They are acquired as needed to
+ * perform a database operation and are then returned to the pool. At any
+ * given time, a connection is either owned and used by a {@link SQLiteSession}
+ * object or the {@link SQLiteConnectionPool}. Those classes are
+ * responsible for serializing operations to guard against concurrent
+ * use of a connection.
+ * </p><p>
+ * The guarantee of having a single owner allows this class to be implemented
+ * without locks and greatly simplifies resource management.
+ * </p>
+ *
+ * <h2>Encapsulation guarantees</h2>
+ * <p>
+ * The connection object object owns *all* of the SQLite related native
+ * objects that are associated with the connection. What's more, there are
+ * no other objects in the system that are capable of obtaining handles to
+ * those native objects. Consequently, when the connection is closed, we do
+ * not have to worry about what other components might have references to
+ * its associated SQLite state -- there are none.
+ * </p><p>
+ * Encapsulation is what ensures that the connection object's
+ * lifecycle does not become a tortured mess of finalizers and reference
+ * queues.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteConnection {
+ private static final String TAG = "SQLiteConnection";
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ private static final Pattern TRIM_SQL_PATTERN = Pattern.compile("[\\s]*\\n+[\\s]*");
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final SQLiteConnectionPool mPool;
+ private final SQLiteDatabaseConfiguration mConfiguration;
+ private final int mConnectionId;
+ private final boolean mIsPrimaryConnection;
+ private final PreparedStatementCache mPreparedStatementCache;
+ private PreparedStatement mPreparedStatementPool;
+
+ // The recent operations log.
+ private final OperationLog mRecentOperations = new OperationLog();
+
+ // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY)
+ private int mConnectionPtr;
+
+ private boolean mOnlyAllowReadOnlyOperations;
+
+ private static native int nativeOpen(String path, int openFlags, String label,
+ boolean enableTrace, boolean enableProfile);
+ private static native void nativeClose(int connectionPtr);
+ private static native void nativeRegisterCustomFunction(int connectionPtr,
+ SQLiteCustomFunction function);
+ private static native void nativeSetLocale(int connectionPtr, String locale);
+ private static native int nativePrepareStatement(int connectionPtr, String sql);
+ private static native void nativeFinalizeStatement(int connectionPtr, int statementPtr);
+ private static native int nativeGetParameterCount(int connectionPtr, int statementPtr);
+ private static native boolean nativeIsReadOnly(int connectionPtr, int statementPtr);
+ private static native int nativeGetColumnCount(int connectionPtr, int statementPtr);
+ private static native String nativeGetColumnName(int connectionPtr, int statementPtr,
+ int index);
+ private static native void nativeBindNull(int connectionPtr, int statementPtr,
+ int index);
+ private static native void nativeBindLong(int connectionPtr, int statementPtr,
+ int index, long value);
+ private static native void nativeBindDouble(int connectionPtr, int statementPtr,
+ int index, double value);
+ private static native void nativeBindString(int connectionPtr, int statementPtr,
+ int index, String value);
+ private static native void nativeBindBlob(int connectionPtr, int statementPtr,
+ int index, byte[] value);
+ private static native void nativeResetStatementAndClearBindings(
+ int connectionPtr, int statementPtr);
+ private static native void nativeExecute(int connectionPtr, int statementPtr);
+ private static native long nativeExecuteForLong(int connectionPtr, int statementPtr);
+ private static native String nativeExecuteForString(int connectionPtr, int statementPtr);
+ private static native int nativeExecuteForBlobFileDescriptor(
+ int connectionPtr, int statementPtr);
+ private static native int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr);
+ private static native long nativeExecuteForLastInsertedRowId(
+ int connectionPtr, int statementPtr);
+ private static native long nativeExecuteForCursorWindow(
+ int connectionPtr, int statementPtr, int windowPtr,
+ int startPos, int requiredPos, boolean countAllRows);
+ private static native int nativeGetDbLookaside(int connectionPtr);
+
+ private SQLiteConnection(SQLiteConnectionPool pool,
+ SQLiteDatabaseConfiguration configuration,
+ int connectionId, boolean primaryConnection) {
+ mPool = pool;
+ mConfiguration = new SQLiteDatabaseConfiguration(configuration);
+ mConnectionId = connectionId;
+ mIsPrimaryConnection = primaryConnection;
+ mPreparedStatementCache = new PreparedStatementCache(
+ mConfiguration.maxSqlCacheSize);
+ mCloseGuard.open("close");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mPool != null && mConnectionPtr != 0) {
+ mPool.onConnectionLeaked();
+ }
+
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ // Called by SQLiteConnectionPool only.
+ static SQLiteConnection open(SQLiteConnectionPool pool,
+ SQLiteDatabaseConfiguration configuration,
+ int connectionId, boolean primaryConnection) {
+ SQLiteConnection connection = new SQLiteConnection(pool, configuration,
+ connectionId, primaryConnection);
+ try {
+ connection.open();
+ return connection;
+ } catch (SQLiteException ex) {
+ connection.dispose(false);
+ throw ex;
+ }
+ }
+
+ // Called by SQLiteConnectionPool only.
+ // Closes the database closes and releases all of its associated resources.
+ // Do not call methods on the connection after it is closed. It will probably crash.
+ void close() {
+ dispose(false);
+ }
+
+ private void open() {
+ SQLiteGlobal.initializeOnce();
+
+ mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags,
+ mConfiguration.label,
+ SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME);
+
+ setLocaleFromConfiguration();
+ }
+
+ private void dispose(boolean finalized) {
+ if (mCloseGuard != null) {
+ if (finalized) {
+ mCloseGuard.warnIfOpen();
+ }
+ mCloseGuard.close();
+ }
+
+ if (mConnectionPtr != 0) {
+ mRecentOperations.beginOperation("close", null, null);
+ try {
+ mPreparedStatementCache.evictAll();
+ nativeClose(mConnectionPtr);
+ mConnectionPtr = 0;
+ } finally {
+ mRecentOperations.endOperation();
+ }
+ }
+ }
+
+ private void setLocaleFromConfiguration() {
+ nativeSetLocale(mConnectionPtr, mConfiguration.locale.toString());
+ }
+
+ // Called by SQLiteConnectionPool only.
+ void reconfigure(SQLiteDatabaseConfiguration configuration) {
+ // Register custom functions.
+ final int functionCount = configuration.customFunctions.size();
+ for (int i = 0; i < functionCount; i++) {
+ SQLiteCustomFunction function = configuration.customFunctions.get(i);
+ if (!mConfiguration.customFunctions.contains(function)) {
+ nativeRegisterCustomFunction(mConnectionPtr, function);
+ }
+ }
+
+ // Remember whether locale has changed.
+ boolean localeChanged = !configuration.locale.equals(mConfiguration.locale);
+
+ // Update configuration parameters.
+ mConfiguration.updateParametersFrom(configuration);
+
+ // Update prepared statement cache size.
+ mPreparedStatementCache.resize(configuration.maxSqlCacheSize);
+
+ // Update locale.
+ if (localeChanged) {
+ setLocaleFromConfiguration();
+ }
+ }
+
+ // Called by SQLiteConnectionPool only.
+ // When set to true, executing write operations will throw SQLiteException.
+ // Preparing statements that might write is ok, just don't execute them.
+ void setOnlyAllowReadOnlyOperations(boolean readOnly) {
+ mOnlyAllowReadOnlyOperations = readOnly;
+ }
+
+ // Called by SQLiteConnectionPool only.
+ // Returns true if the prepared statement cache contains the specified SQL.
+ boolean isPreparedStatementInCache(String sql) {
+ return mPreparedStatementCache.get(sql) != null;
+ }
+
+ /**
+ * Gets the unique id of this connection.
+ * @return The connection id.
+ */
+ public int getConnectionId() {
+ return mConnectionId;
+ }
+
+ /**
+ * Returns true if this is the primary database connection.
+ * @return True if this is the primary database connection.
+ */
+ public boolean isPrimaryConnection() {
+ return mIsPrimaryConnection;
+ }
+
+ /**
+ * Prepares a statement for execution but does not bind its parameters or execute it.
+ * <p>
+ * This method can be used to check for syntax errors during compilation
+ * prior to execution of the statement. If the {@code outStatementInfo} argument
+ * is not null, the provided {@link SQLiteStatementInfo} object is populated
+ * with information about the statement.
+ * </p><p>
+ * A prepared statement makes no reference to the arguments that may eventually
+ * be bound to it, consequently it it possible to cache certain prepared statements
+ * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable,
+ * then it will be stored in the cache for later.
+ * </p><p>
+ * To take advantage of this behavior as an optimization, the connection pool
+ * provides a method to acquire a connection that already has a given SQL statement
+ * in its prepared statement cache so that it is ready for execution.
+ * </p>
+ *
+ * @param sql The SQL statement to prepare.
+ * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate
+ * with information about the statement, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error.
+ */
+ public void prepare(String sql, SQLiteStatementInfo outStatementInfo) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ mRecentOperations.beginOperation("prepare", sql, null);
+ try {
+ PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ if (outStatementInfo != null) {
+ outStatementInfo.numParameters = statement.mNumParameters;
+ outStatementInfo.readOnly = statement.mReadOnly;
+
+ final int columnCount = nativeGetColumnCount(
+ mConnectionPtr, statement.mStatementPtr);
+ if (columnCount == 0) {
+ outStatementInfo.columnNames = EMPTY_STRING_ARRAY;
+ } else {
+ outStatementInfo.columnNames = new String[columnCount];
+ for (int i = 0; i < columnCount; i++) {
+ outStatementInfo.columnNames[i] = nativeGetColumnName(
+ mConnectionPtr, statement.mStatementPtr, i);
+ }
+ }
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation();
+ }
+ }
+
+ /**
+ * Executes a statement that does not return a result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public void execute(String sql, Object[] bindArgs) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ mRecentOperations.beginOperation("execute", sql, bindArgs);
+ try {
+ PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ nativeExecute(mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation();
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single <code>long</code> result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @return The value of the first column in the first row of the result set
+ * as a <code>long</code>, or zero if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public long executeForLong(String sql, Object[] bindArgs) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ mRecentOperations.beginOperation("executeForLong", sql, bindArgs);
+ try {
+ PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation();
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single {@link String} result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @return The value of the first column in the first row of the result set
+ * as a <code>String</code>, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public String executeForString(String sql, Object[] bindArgs) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ mRecentOperations.beginOperation("executeForString", sql, bindArgs);
+ try {
+ PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation();
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single BLOB result as a
+ * file descriptor to a shared memory region.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @return The file descriptor for a shared memory region that contains
+ * the value of the first column in the first row of the result set as a BLOB,
+ * or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ mRecentOperations.beginOperation("executeForBlobFileDescriptor", sql, bindArgs);
+ try {
+ PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ int fd = nativeExecuteForBlobFileDescriptor(
+ mConnectionPtr, statement.mStatementPtr);
+ return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null;
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation();
+ }
+ }
+
+ /**
+ * Executes a statement that returns a count of the number of rows
+ * that were changed. Use for UPDATE or DELETE SQL statements.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @return The number of rows that were changed.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public int executeForChangedRowCount(String sql, Object[] bindArgs) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ mRecentOperations.beginOperation("executeForChangedRowCount", sql, bindArgs);
+ try {
+ PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ return nativeExecuteForChangedRowCount(
+ mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation();
+ }
+ }
+
+ /**
+ * Executes a statement that returns the row id of the last row inserted
+ * by the statement. Use for INSERT SQL statements.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @return The row id of the last row that was inserted, or 0 if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public long executeForLastInsertedRowId(String sql, Object[] bindArgs) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ mRecentOperations.beginOperation("executeForLastInsertedRowId", sql, bindArgs);
+ try {
+ PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ return nativeExecuteForLastInsertedRowId(
+ mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation();
+ }
+ }
+
+ /**
+ * Executes a statement and populates the specified {@link CursorWindow}
+ * with a range of results. Returns the number of rows that were counted
+ * during query execution.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param window The cursor window to clear and fill.
+ * @param startPos The start position for filling the window.
+ * @param requiredPos The position of a row that MUST be in the window.
+ * If it won't fit, then the query should discard part of what it filled
+ * so that it does. Must be greater than or equal to <code>startPos</code>.
+ * @param countAllRows True to count all rows that the query would return
+ * regagless of whether they fit in the window.
+ * @return The number of rows that were counted during query execution. Might
+ * not be all rows in the result set unless <code>countAllRows</code> is true.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public int executeForCursorWindow(String sql, Object[] bindArgs,
+ CursorWindow window, int startPos, int requiredPos, boolean countAllRows) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+ if (window == null) {
+ throw new IllegalArgumentException("window must not be null.");
+ }
+
+ int actualPos = -1;
+ int countedRows = -1;
+ int filledRows = -1;
+ mRecentOperations.beginOperation("executeForCursorWindow", sql, bindArgs);
+ try {
+ PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ final long result = nativeExecuteForCursorWindow(
+ mConnectionPtr, statement.mStatementPtr, window.mWindowPtr,
+ startPos, requiredPos, countAllRows);
+ actualPos = (int)(result >> 32);
+ countedRows = (int)result;
+ filledRows = window.getNumRows();
+ window.setStartPosition(actualPos);
+ return countedRows;
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(ex);
+ throw ex;
+ } finally {
+ if (mRecentOperations.endOperationDeferLog()) {
+ mRecentOperations.logOperation("window='" + window
+ + "', startPos=" + startPos
+ + ", actualPos=" + actualPos
+ + ", filledRows=" + filledRows
+ + ", countedRows=" + countedRows);
+ }
+ }
+ }
+
+ private PreparedStatement acquirePreparedStatement(String sql) {
+ PreparedStatement statement = mPreparedStatementCache.get(sql);
+ if (statement != null) {
+ return statement;
+ }
+
+ final int statementPtr = nativePrepareStatement(mConnectionPtr, sql);
+ try {
+ final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
+ final int type = DatabaseUtils.getSqlStatementType(sql);
+ final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
+ statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
+ if (isCacheable(type)) {
+ mPreparedStatementCache.put(sql, statement);
+ statement.mInCache = true;
+ }
+ } catch (RuntimeException ex) {
+ // Finalize the statement if an exception occurred and we did not add
+ // it to the cache. If it is already in the cache, then leave it there.
+ if (statement == null || !statement.mInCache) {
+ nativeFinalizeStatement(mConnectionPtr, statementPtr);
+ }
+ throw ex;
+ }
+ return statement;
+ }
+
+ private void releasePreparedStatement(PreparedStatement statement) {
+ if (statement.mInCache) {
+ try {
+ nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr);
+ } catch (SQLiteException ex) {
+ // The statement could not be reset due to an error.
+ // The entryRemoved() callback for the cache will recursively call
+ // releasePreparedStatement() again, but this time mInCache will be false
+ // so the statement will be finalized and recycled.
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ Log.v(TAG, "Could not reset prepared statement due to an exception. "
+ + "Removing it from the cache. SQL: "
+ + trimSqlForDisplay(statement.mSql), ex);
+ }
+ mPreparedStatementCache.remove(statement.mSql);
+ }
+ } else {
+ nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr);
+ recyclePreparedStatement(statement);
+ }
+ }
+
+ private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
+ final int count = bindArgs != null ? bindArgs.length : 0;
+ if (count != statement.mNumParameters) {
+ throw new SQLiteBindOrColumnIndexOutOfRangeException(
+ "Expected " + statement.mNumParameters + " bind arguments but "
+ + bindArgs.length + " were provided.");
+ }
+ if (count == 0) {
+ return;
+ }
+
+ final int statementPtr = statement.mStatementPtr;
+ for (int i = 0; i < count; i++) {
+ final Object arg = bindArgs[i];
+ switch (DatabaseUtils.getTypeOfObject(arg)) {
+ case Cursor.FIELD_TYPE_NULL:
+ nativeBindNull(mConnectionPtr, statementPtr, i + 1);
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ nativeBindLong(mConnectionPtr, statementPtr, i + 1,
+ ((Number)arg).longValue());
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
+ ((Number)arg).doubleValue());
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ default:
+ if (arg instanceof Boolean) {
+ // Provide compatibility with legacy applications which may pass
+ // Boolean values in bind args.
+ nativeBindLong(mConnectionPtr, statementPtr, i + 1,
+ ((Boolean)arg).booleanValue() ? 1 : 0);
+ } else {
+ nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
+ }
+ break;
+ }
+ }
+ }
+
+ private void throwIfStatementForbidden(PreparedStatement statement) {
+ if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) {
+ throw new SQLiteException("Cannot execute this statement because it "
+ + "might modify the database but the connection is read-only.");
+ }
+ }
+
+ private static boolean isCacheable(int statementType) {
+ if (statementType == DatabaseUtils.STATEMENT_UPDATE
+ || statementType == DatabaseUtils.STATEMENT_SELECT) {
+ return true;
+ }
+ return false;
+ }
+
+ private void applyBlockGuardPolicy(PreparedStatement statement) {
+ if (!mConfiguration.isInMemoryDb()) {
+ if (statement.mReadOnly) {
+ BlockGuard.getThreadPolicy().onReadFromDisk();
+ } else {
+ BlockGuard.getThreadPolicy().onWriteToDisk();
+ }
+ }
+ }
+
+ /**
+ * Dumps debugging information about this connection.
+ *
+ * @param printer The printer to receive the dump, not null.
+ */
+ public void dump(Printer printer) {
+ dumpUnsafe(printer);
+ }
+
+ /**
+ * Dumps debugging information about this connection, in the case where the
+ * caller might not actually own the connection.
+ *
+ * This function is written so that it may be called by a thread that does not
+ * own the connection. We need to be very careful because the connection state is
+ * not synchronized.
+ *
+ * At worst, the method may return stale or slightly wrong data, however
+ * it should not crash. This is ok as it is only used for diagnostic purposes.
+ *
+ * @param printer The printer to receive the dump, not null.
+ */
+ void dumpUnsafe(Printer printer) {
+ printer.println("Connection #" + mConnectionId + ":");
+ printer.println(" isPrimaryConnection: " + mIsPrimaryConnection);
+ printer.println(" connectionPtr: 0x" + Integer.toHexString(mConnectionPtr));
+ printer.println(" onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations);
+
+ mRecentOperations.dump(printer);
+ mPreparedStatementCache.dump(printer);
+ }
+
+ /**
+ * Describes the currently executing operation, in the case where the
+ * caller might not actually own the connection.
+ *
+ * This function is written so that it may be called by a thread that does not
+ * own the connection. We need to be very careful because the connection state is
+ * not synchronized.
+ *
+ * At worst, the method may return stale or slightly wrong data, however
+ * it should not crash. This is ok as it is only used for diagnostic purposes.
+ *
+ * @return A description of the current operation including how long it has been running,
+ * or null if none.
+ */
+ String describeCurrentOperationUnsafe() {
+ return mRecentOperations.describeCurrentOperation();
+ }
+
+ /**
+ * Collects statistics about database connection memory usage.
+ *
+ * @param dbStatsList The list to populate.
+ */
+ void collectDbStats(ArrayList<DbStats> dbStatsList) {
+ // Get information about the main database.
+ int lookaside = nativeGetDbLookaside(mConnectionPtr);
+ long pageCount = 0;
+ long pageSize = 0;
+ try {
+ pageCount = executeForLong("PRAGMA page_count;", null);
+ pageSize = executeForLong("PRAGMA page_size;", null);
+ } catch (SQLiteException ex) {
+ // Ignore.
+ }
+ dbStatsList.add(getMainDbStatsUnsafe(lookaside, pageCount, pageSize));
+
+ // Get information about attached databases.
+ // We ignore the first row in the database list because it corresponds to
+ // the main database which we have already described.
+ CursorWindow window = new CursorWindow("collectDbStats");
+ try {
+ executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false);
+ for (int i = 1; i < window.getNumRows(); i++) {
+ String name = window.getString(i, 1);
+ String path = window.getString(i, 2);
+ pageCount = 0;
+ pageSize = 0;
+ try {
+ pageCount = executeForLong("PRAGMA " + name + ".page_count;", null);
+ pageSize = executeForLong("PRAGMA " + name + ".page_size;", null);
+ } catch (SQLiteException ex) {
+ // Ignore.
+ }
+ String label = " (attached) " + name;
+ if (!path.isEmpty()) {
+ label += ": " + path;
+ }
+ dbStatsList.add(new DbStats(label, pageCount, pageSize, 0, 0, 0, 0));
+ }
+ } catch (SQLiteException ex) {
+ // Ignore.
+ } finally {
+ window.close();
+ }
+ }
+
+ /**
+ * Collects statistics about database connection memory usage, in the case where the
+ * caller might not actually own the connection.
+ *
+ * @return The statistics object, never null.
+ */
+ void collectDbStatsUnsafe(ArrayList<DbStats> dbStatsList) {
+ dbStatsList.add(getMainDbStatsUnsafe(0, 0, 0));
+ }
+
+ private DbStats getMainDbStatsUnsafe(int lookaside, long pageCount, long pageSize) {
+ // The prepared statement cache is thread-safe so we can access its statistics
+ // even if we do not own the database connection.
+ String label = mConfiguration.path;
+ if (!mIsPrimaryConnection) {
+ label += " (" + mConnectionId + ")";
+ }
+ return new DbStats(label, pageCount, pageSize, lookaside,
+ mPreparedStatementCache.hitCount(),
+ mPreparedStatementCache.missCount(),
+ mPreparedStatementCache.size());
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteConnection: " + mConfiguration.path + " (" + mConnectionId + ")";
+ }
+
+ private PreparedStatement obtainPreparedStatement(String sql, int statementPtr,
+ int numParameters, int type, boolean readOnly) {
+ PreparedStatement statement = mPreparedStatementPool;
+ if (statement != null) {
+ mPreparedStatementPool = statement.mPoolNext;
+ statement.mPoolNext = null;
+ statement.mInCache = false;
+ } else {
+ statement = new PreparedStatement();
+ }
+ statement.mSql = sql;
+ statement.mStatementPtr = statementPtr;
+ statement.mNumParameters = numParameters;
+ statement.mType = type;
+ statement.mReadOnly = readOnly;
+ return statement;
+ }
+
+ private void recyclePreparedStatement(PreparedStatement statement) {
+ statement.mSql = null;
+ statement.mPoolNext = mPreparedStatementPool;
+ mPreparedStatementPool = statement;
+ }
+
+ private static String trimSqlForDisplay(String sql) {
+ return TRIM_SQL_PATTERN.matcher(sql).replaceAll(" ");
+ }
+
+ /**
+ * Holder type for a prepared statement.
+ *
+ * Although this object holds a pointer to a native statement object, it
+ * does not have a finalizer. This is deliberate. The {@link SQLiteConnection}
+ * owns the statement object and will take care of freeing it when needed.
+ * In particular, closing the connection requires a guarantee of deterministic
+ * resource disposal because all native statement objects must be freed before
+ * the native database object can be closed. So no finalizers here.
+ */
+ private static final class PreparedStatement {
+ // Next item in pool.
+ public PreparedStatement mPoolNext;
+
+ // The SQL from which the statement was prepared.
+ public String mSql;
+
+ // The native sqlite3_stmt object pointer.
+ // Lifetime is managed explicitly by the connection.
+ public int mStatementPtr;
+
+ // The number of parameters that the prepared statement has.
+ public int mNumParameters;
+
+ // The statement type.
+ public int mType;
+
+ // True if the statement is read-only.
+ public boolean mReadOnly;
+
+ // True if the statement is in the cache.
+ public boolean mInCache;
+ }
+
+ private final class PreparedStatementCache
+ extends LruCache<String, PreparedStatement> {
+ public PreparedStatementCache(int size) {
+ super(size);
+ }
+
+ @Override
+ protected void entryRemoved(boolean evicted, String key,
+ PreparedStatement oldValue, PreparedStatement newValue) {
+ oldValue.mInCache = false;
+ releasePreparedStatement(oldValue);
+ }
+
+ public void dump(Printer printer) {
+ printer.println(" Prepared statement cache:");
+ Map<String, PreparedStatement> cache = snapshot();
+ if (!cache.isEmpty()) {
+ int i = 0;
+ for (Map.Entry<String, PreparedStatement> entry : cache.entrySet()) {
+ PreparedStatement statement = entry.getValue();
+ if (statement.mInCache) { // might be false due to a race with entryRemoved
+ String sql = entry.getKey();
+ printer.println(" " + i + ": statementPtr=0x"
+ + Integer.toHexString(statement.mStatementPtr)
+ + ", numParameters=" + statement.mNumParameters
+ + ", type=" + statement.mType
+ + ", readOnly=" + statement.mReadOnly
+ + ", sql=\"" + trimSqlForDisplay(sql) + "\"");
+ }
+ i += 1;
+ }
+ } else {
+ printer.println(" <none>");
+ }
+ }
+ }
+
+ private static final class OperationLog {
+ private static final int MAX_RECENT_OPERATIONS = 10;
+
+ private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS];
+ private int mIndex;
+
+ public void beginOperation(String kind, String sql, Object[] bindArgs) {
+ synchronized (mOperations) {
+ final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS;
+ Operation operation = mOperations[index];
+ if (operation == null) {
+ operation = new Operation();
+ mOperations[index] = operation;
+ } else {
+ operation.mFinished = false;
+ operation.mException = null;
+ if (operation.mBindArgs != null) {
+ operation.mBindArgs.clear();
+ }
+ }
+ operation.mStartTime = System.currentTimeMillis();
+ operation.mKind = kind;
+ operation.mSql = sql;
+ if (bindArgs != null) {
+ if (operation.mBindArgs == null) {
+ operation.mBindArgs = new ArrayList<Object>();
+ } else {
+ operation.mBindArgs.clear();
+ }
+ for (int i = 0; i < bindArgs.length; i++) {
+ final Object arg = bindArgs[i];
+ if (arg != null && arg instanceof byte[]) {
+ // Don't hold onto the real byte array longer than necessary.
+ operation.mBindArgs.add(EMPTY_BYTE_ARRAY);
+ } else {
+ operation.mBindArgs.add(arg);
+ }
+ }
+ }
+ mIndex = index;
+ }
+ }
+
+ public void failOperation(Exception ex) {
+ synchronized (mOperations) {
+ final Operation operation = mOperations[mIndex];
+ operation.mException = ex;
+ }
+ }
+
+ public boolean endOperationDeferLog() {
+ synchronized (mOperations) {
+ return endOperationDeferLogLocked();
+ }
+ }
+
+ private boolean endOperationDeferLogLocked() {
+ final Operation operation = mOperations[mIndex];
+ operation.mEndTime = System.currentTimeMillis();
+ operation.mFinished = true;
+ return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery(
+ operation.mEndTime - operation.mStartTime);
+ }
+
+ public void endOperation() {
+ synchronized (mOperations) {
+ if (endOperationDeferLogLocked()) {
+ logOperationLocked(null);
+ }
+ }
+ }
+
+ public void logOperation(String detail) {
+ synchronized (mOperations) {
+ logOperationLocked(detail);
+ }
+ }
+
+ private void logOperationLocked(String detail) {
+ final Operation operation = mOperations[mIndex];
+ StringBuilder msg = new StringBuilder();
+ operation.describe(msg);
+ if (detail != null) {
+ msg.append(", ").append(detail);
+ }
+ Log.d(TAG, msg.toString());
+ }
+
+ public String describeCurrentOperation() {
+ synchronized (mOperations) {
+ final Operation operation = mOperations[mIndex];
+ if (operation != null && !operation.mFinished) {
+ StringBuilder msg = new StringBuilder();
+ operation.describe(msg);
+ return msg.toString();
+ }
+ return null;
+ }
+ }
+
+ public void dump(Printer printer) {
+ synchronized (mOperations) {
+ printer.println(" Most recently executed operations:");
+ int index = mIndex;
+ Operation operation = mOperations[index];
+ if (operation != null) {
+ int n = 0;
+ do {
+ StringBuilder msg = new StringBuilder();
+ msg.append(" ").append(n).append(": [");
+ msg.append(operation.getFormattedStartTime());
+ msg.append("] ");
+ operation.describe(msg);
+ printer.println(msg.toString());
+
+ if (index > 0) {
+ index -= 1;
+ } else {
+ index = MAX_RECENT_OPERATIONS - 1;
+ }
+ n += 1;
+ operation = mOperations[index];
+ } while (operation != null && n < MAX_RECENT_OPERATIONS);
+ } else {
+ printer.println(" <none>");
+ }
+ }
+ }
+ }
+
+ private static final class Operation {
+ private static final SimpleDateFormat sDateFormat =
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+
+ public long mStartTime;
+ public long mEndTime;
+ public String mKind;
+ public String mSql;
+ public ArrayList<Object> mBindArgs;
+ public boolean mFinished;
+ public Exception mException;
+
+ public void describe(StringBuilder msg) {
+ msg.append(mKind);
+ if (mFinished) {
+ msg.append(" took ").append(mEndTime - mStartTime).append("ms");
+ } else {
+ msg.append(" started ").append(System.currentTimeMillis() - mStartTime)
+ .append("ms ago");
+ }
+ msg.append(" - ").append(getStatus());
+ if (mSql != null) {
+ msg.append(", sql=\"").append(trimSqlForDisplay(mSql)).append("\"");
+ }
+ if (mBindArgs != null && mBindArgs.size() != 0) {
+ msg.append(", bindArgs=[");
+ final int count = mBindArgs.size();
+ for (int i = 0; i < count; i++) {
+ final Object arg = mBindArgs.get(i);
+ if (i != 0) {
+ msg.append(", ");
+ }
+ if (arg == null) {
+ msg.append("null");
+ } else if (arg instanceof byte[]) {
+ msg.append("<byte[]>");
+ } else if (arg instanceof String) {
+ msg.append("\"").append((String)arg).append("\"");
+ } else {
+ msg.append(arg);
+ }
+ }
+ msg.append("]");
+ }
+ if (mException != null) {
+ msg.append(", exception=\"").append(mException.getMessage()).append("\"");
+ }
+ }
+
+ private String getStatus() {
+ if (!mFinished) {
+ return "running";
+ }
+ return mException != null ? "failed" : "succeeded";
+ }
+
+ private String getFormattedStartTime() {
+ return sDateFormat.format(new Date(mStartTime));
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
new file mode 100644
index 0000000..b88bfee
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -0,0 +1,907 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import dalvik.system.CloseGuard;
+
+import android.database.sqlite.SQLiteDebug.DbStats;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.PrefixPrinter;
+import android.util.Printer;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+
+/**
+ * Maintains a pool of active SQLite database connections.
+ * <p>
+ * At any given time, a connection is either owned by the pool, or it has been
+ * acquired by a {@link SQLiteSession}. When the {@link SQLiteSession} is
+ * finished with the connection it is using, it must return the connection
+ * back to the pool.
+ * </p><p>
+ * The pool holds strong references to the connections it owns. However,
+ * it only holds <em>weak references</em> to the connections that sessions
+ * have acquired from it. Using weak references in the latter case ensures
+ * that the connection pool can detect when connections have been improperly
+ * abandoned so that it can create new connections to replace them if needed.
+ * </p><p>
+ * The connection pool is thread-safe (but the connections themselves are not).
+ * </p>
+ *
+ * <h2>Exception safety</h2>
+ * <p>
+ * This code attempts to maintain the invariant that opened connections are
+ * always owned. Unfortunately that means it needs to handle exceptions
+ * all over to ensure that broken connections get cleaned up. Most
+ * operations invokving SQLite can throw {@link SQLiteException} or other
+ * runtime exceptions. This is a bit of a pain to deal with because the compiler
+ * cannot help us catch missing exception handling code.
+ * </p><p>
+ * The general rule for this file: If we are making calls out to
+ * {@link SQLiteConnection} then we must be prepared to handle any
+ * runtime exceptions it might throw at us. Note that out-of-memory
+ * is an {@link Error}, not a {@link RuntimeException}. We don't trouble ourselves
+ * handling out of memory because it is hard to do anything at all sensible then
+ * and most likely the VM is about to crash.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteConnectionPool implements Closeable {
+ private static final String TAG = "SQLiteConnectionPool";
+
+ // Amount of time to wait in milliseconds before unblocking acquireConnection
+ // and logging a message about the connection pool being busy.
+ private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final Object mLock = new Object();
+ private final AtomicBoolean mConnectionLeaked = new AtomicBoolean();
+ private final SQLiteDatabaseConfiguration mConfiguration;
+ private boolean mIsOpen;
+ private int mNextConnectionId;
+
+ private ConnectionWaiter mConnectionWaiterPool;
+ private ConnectionWaiter mConnectionWaiterQueue;
+
+ // Strong references to all available connections.
+ private final ArrayList<SQLiteConnection> mAvailableNonPrimaryConnections =
+ new ArrayList<SQLiteConnection>();
+ private SQLiteConnection mAvailablePrimaryConnection;
+
+ // Weak references to all acquired connections. The associated value
+ // is a boolean that indicates whether the connection must be reconfigured
+ // before being returned to the available connection list.
+ // For example, the prepared statement cache size may have changed and
+ // need to be updated.
+ private final WeakHashMap<SQLiteConnection, Boolean> mAcquiredConnections =
+ new WeakHashMap<SQLiteConnection, Boolean>();
+
+ /**
+ * Connection flag: Read-only.
+ * <p>
+ * This flag indicates that the connection will only be used to
+ * perform read-only operations.
+ * </p>
+ */
+ public static final int CONNECTION_FLAG_READ_ONLY = 1 << 0;
+
+ /**
+ * Connection flag: Primary connection affinity.
+ * <p>
+ * This flag indicates that the primary connection is required.
+ * This flag helps support legacy applications that expect most data modifying
+ * operations to be serialized by locking the primary database connection.
+ * Setting this flag essentially implements the old "db lock" concept by preventing
+ * an operation from being performed until it can obtain exclusive access to
+ * the primary connection.
+ * </p>
+ */
+ public static final int CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY = 1 << 1;
+
+ /**
+ * Connection flag: Connection is being used interactively.
+ * <p>
+ * This flag indicates that the connection is needed by the UI thread.
+ * The connection pool can use this flag to elevate the priority
+ * of the database connection request.
+ * </p>
+ */
+ public static final int CONNECTION_FLAG_INTERACTIVE = 1 << 2;
+
+ private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) {
+ mConfiguration = new SQLiteDatabaseConfiguration(configuration);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Opens a connection pool for the specified database.
+ *
+ * @param configuration The database configuration.
+ * @return The connection pool.
+ *
+ * @throws SQLiteException if a database error occurs.
+ */
+ public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) {
+ if (configuration == null) {
+ throw new IllegalArgumentException("configuration must not be null.");
+ }
+
+ // Create the pool.
+ SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration);
+ pool.open(); // might throw
+ return pool;
+ }
+
+ // Might throw
+ private void open() {
+ // Open the primary connection.
+ // This might throw if the database is corrupt.
+ mAvailablePrimaryConnection = openConnectionLocked(
+ true /*primaryConnection*/); // might throw
+
+ // Mark the pool as being open for business.
+ mIsOpen = true;
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Closes the connection pool.
+ * <p>
+ * When the connection pool is closed, it will refuse all further requests
+ * to acquire connections. All connections that are currently available in
+ * the pool are closed immediately. Any connections that are still in use
+ * will be closed as soon as they are returned to the pool.
+ * </p>
+ *
+ * @throws IllegalStateException if the pool has been closed.
+ */
+ public void close() {
+ dispose(false);
+ }
+
+ private void dispose(boolean finalized) {
+ if (mCloseGuard != null) {
+ if (finalized) {
+ mCloseGuard.warnIfOpen();
+ }
+ mCloseGuard.close();
+ }
+
+ if (!finalized) {
+ // Close all connections. We don't need (or want) to do this
+ // when finalized because we don't know what state the connections
+ // themselves will be in. The finalizer is really just here for CloseGuard.
+ // The connections will take care of themselves when their own finalizers run.
+ synchronized (mLock) {
+ throwIfClosedLocked();
+
+ mIsOpen = false;
+
+ final int count = mAvailableNonPrimaryConnections.size();
+ for (int i = 0; i < count; i++) {
+ closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i));
+ }
+ mAvailableNonPrimaryConnections.clear();
+
+ if (mAvailablePrimaryConnection != null) {
+ closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
+ mAvailablePrimaryConnection = null;
+ }
+
+ final int pendingCount = mAcquiredConnections.size();
+ if (pendingCount != 0) {
+ Log.i(TAG, "The connection pool for " + mConfiguration.label
+ + " has been closed but there are still "
+ + pendingCount + " connections in use. They will be closed "
+ + "as they are released back to the pool.");
+ }
+
+ wakeConnectionWaitersLocked();
+ }
+ }
+ }
+
+ /**
+ * Reconfigures the database configuration of the connection pool and all of its
+ * connections.
+ * <p>
+ * Configuration changes are propagated down to connections immediately if
+ * they are available or as soon as they are released. This includes changes
+ * that affect the size of the pool.
+ * </p>
+ *
+ * @param configuration The new configuration.
+ *
+ * @throws IllegalStateException if the pool has been closed.
+ */
+ public void reconfigure(SQLiteDatabaseConfiguration configuration) {
+ if (configuration == null) {
+ throw new IllegalArgumentException("configuration must not be null.");
+ }
+
+ synchronized (mLock) {
+ throwIfClosedLocked();
+
+ final boolean poolSizeChanged = mConfiguration.maxConnectionPoolSize
+ != configuration.maxConnectionPoolSize;
+ mConfiguration.updateParametersFrom(configuration);
+
+ if (poolSizeChanged) {
+ int availableCount = mAvailableNonPrimaryConnections.size();
+ while (availableCount-- > mConfiguration.maxConnectionPoolSize - 1) {
+ SQLiteConnection connection =
+ mAvailableNonPrimaryConnections.remove(availableCount);
+ closeConnectionAndLogExceptionsLocked(connection);
+ }
+ }
+
+ reconfigureAllConnectionsLocked();
+
+ wakeConnectionWaitersLocked();
+ }
+ }
+
+ /**
+ * Acquires a connection from the pool.
+ * <p>
+ * The caller must call {@link #releaseConnection} to release the connection
+ * back to the pool when it is finished. Failure to do so will result
+ * in much unpleasantness.
+ * </p>
+ *
+ * @param sql If not null, try to find a connection that already has
+ * the specified SQL statement in its prepared statement cache.
+ * @param connectionFlags The connection request flags.
+ * @return The connection that was acquired, never null.
+ *
+ * @throws IllegalStateException if the pool has been closed.
+ * @throws SQLiteException if a database error occurs.
+ */
+ public SQLiteConnection acquireConnection(String sql, int connectionFlags) {
+ return waitForConnection(sql, connectionFlags);
+ }
+
+ /**
+ * Releases a connection back to the pool.
+ * <p>
+ * It is ok to call this method after the pool has closed, to release
+ * connections that were still in use at the time of closure.
+ * </p>
+ *
+ * @param connection The connection to release. Must not be null.
+ *
+ * @throws IllegalStateException if the connection was not acquired
+ * from this pool or if it has already been released.
+ */
+ public void releaseConnection(SQLiteConnection connection) {
+ synchronized (mLock) {
+ Boolean mustReconfigure = mAcquiredConnections.remove(connection);
+ if (mustReconfigure == null) {
+ throw new IllegalStateException("Cannot perform this operation "
+ + "because the specified connection was not acquired "
+ + "from this pool or has already been released.");
+ }
+
+ if (!mIsOpen) {
+ closeConnectionAndLogExceptionsLocked(connection);
+ } else if (connection.isPrimaryConnection()) {
+ assert mAvailablePrimaryConnection == null;
+ try {
+ if (mustReconfigure == Boolean.TRUE) {
+ connection.reconfigure(mConfiguration); // might throw
+ }
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to reconfigure released primary connection, closing it: "
+ + connection, ex);
+ closeConnectionAndLogExceptionsLocked(connection);
+ connection = null;
+ }
+ if (connection != null) {
+ mAvailablePrimaryConnection = connection;
+ }
+ wakeConnectionWaitersLocked();
+ } else if (mAvailableNonPrimaryConnections.size() >=
+ mConfiguration.maxConnectionPoolSize - 1) {
+ closeConnectionAndLogExceptionsLocked(connection);
+ } else {
+ try {
+ if (mustReconfigure == Boolean.TRUE) {
+ connection.reconfigure(mConfiguration); // might throw
+ }
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to reconfigure released non-primary connection, "
+ + "closing it: " + connection, ex);
+ closeConnectionAndLogExceptionsLocked(connection);
+ connection = null;
+ }
+ if (connection != null) {
+ mAvailableNonPrimaryConnections.add(connection);
+ }
+ wakeConnectionWaitersLocked();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the session should yield the connection due to
+ * contention over available database connections.
+ *
+ * @param connection The connection owned by the session.
+ * @param connectionFlags The connection request flags.
+ * @return True if the session should yield its connection.
+ *
+ * @throws IllegalStateException if the connection was not acquired
+ * from this pool or if it has already been released.
+ */
+ public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) {
+ synchronized (mLock) {
+ if (!mAcquiredConnections.containsKey(connection)) {
+ throw new IllegalStateException("Cannot perform this operation "
+ + "because the specified connection was not acquired "
+ + "from this pool or has already been released.");
+ }
+
+ if (!mIsOpen) {
+ return false;
+ }
+
+ return isSessionBlockingImportantConnectionWaitersLocked(
+ connection.isPrimaryConnection(), connectionFlags);
+ }
+ }
+
+ /**
+ * Collects statistics about database connection memory usage.
+ *
+ * @param dbStatsList The list to populate.
+ */
+ public void collectDbStats(ArrayList<DbStats> dbStatsList) {
+ synchronized (mLock) {
+ if (mAvailablePrimaryConnection != null) {
+ mAvailablePrimaryConnection.collectDbStats(dbStatsList);
+ }
+
+ for (SQLiteConnection connection : mAvailableNonPrimaryConnections) {
+ connection.collectDbStats(dbStatsList);
+ }
+
+ for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
+ connection.collectDbStatsUnsafe(dbStatsList);
+ }
+ }
+ }
+
+ // Might throw.
+ private SQLiteConnection openConnectionLocked(boolean primaryConnection) {
+ final int connectionId = mNextConnectionId++;
+ return SQLiteConnection.open(this, mConfiguration,
+ connectionId, primaryConnection); // might throw
+ }
+
+ void onConnectionLeaked() {
+ // This code is running inside of the SQLiteConnection finalizer.
+ //
+ // We don't know whether it is just the connection that has been finalized (and leaked)
+ // or whether the connection pool has also been or is about to be finalized.
+ // Consequently, it would be a bad idea to try to grab any locks or to
+ // do any significant work here. So we do the simplest possible thing and
+ // set a flag. waitForConnection() periodically checks this flag (when it
+ // times out) so that it can recover from leaked connections and wake
+ // itself or other threads up if necessary.
+ //
+ // You might still wonder why we don't try to do more to wake up the waiters
+ // immediately. First, as explained above, it would be hard to do safely
+ // unless we started an extra Thread to function as a reference queue. Second,
+ // this is never supposed to happen in normal operation. Third, there is no
+ // guarantee that the GC will actually detect the leak in a timely manner so
+ // it's not all that important that we recover from the leak in a timely manner
+ // either. Fourth, if a badly behaved application finds itself hung waiting for
+ // several seconds while waiting for a leaked connection to be detected and recreated,
+ // then perhaps its authors will have added incentive to fix the problem!
+
+ Log.w(TAG, "A SQLiteConnection object for database '"
+ + mConfiguration.label + "' was leaked! Please fix your application "
+ + "to end transactions in progress properly and to close the database "
+ + "when it is no longer needed.");
+
+ mConnectionLeaked.set(true);
+ }
+
+ // Can't throw.
+ private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) {
+ try {
+ connection.close(); // might throw
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to close connection, its fate is now in the hands "
+ + "of the merciful GC: " + connection, ex);
+ }
+ }
+
+ // Can't throw.
+ private void reconfigureAllConnectionsLocked() {
+ boolean wake = false;
+ if (mAvailablePrimaryConnection != null) {
+ try {
+ mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to reconfigure available primary connection, closing it: "
+ + mAvailablePrimaryConnection, ex);
+ closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
+ mAvailablePrimaryConnection = null;
+ wake = true;
+ }
+ }
+
+ int count = mAvailableNonPrimaryConnections.size();
+ for (int i = 0; i < count; i++) {
+ final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i);
+ try {
+ connection.reconfigure(mConfiguration); // might throw
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: "
+ + connection, ex);
+ closeConnectionAndLogExceptionsLocked(connection);
+ mAvailableNonPrimaryConnections.remove(i--);
+ count -= 1;
+ wake = true;
+ }
+ }
+
+ if (!mAcquiredConnections.isEmpty()) {
+ ArrayList<SQLiteConnection> keysToUpdate = new ArrayList<SQLiteConnection>(
+ mAcquiredConnections.size());
+ for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) {
+ if (entry.getValue() != Boolean.TRUE) {
+ keysToUpdate.add(entry.getKey());
+ }
+ }
+ final int updateCount = keysToUpdate.size();
+ for (int i = 0; i < updateCount; i++) {
+ mAcquiredConnections.put(keysToUpdate.get(i), Boolean.TRUE);
+ }
+ }
+
+ if (wake) {
+ wakeConnectionWaitersLocked();
+ }
+ }
+
+ // Might throw.
+ private SQLiteConnection waitForConnection(String sql, int connectionFlags) {
+ final boolean wantPrimaryConnection =
+ (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0;
+
+ final ConnectionWaiter waiter;
+ synchronized (mLock) {
+ throwIfClosedLocked();
+
+ // Try to acquire a connection.
+ SQLiteConnection connection = null;
+ if (!wantPrimaryConnection) {
+ connection = tryAcquireNonPrimaryConnectionLocked(
+ sql, connectionFlags); // might throw
+ }
+ if (connection == null) {
+ connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw
+ }
+ if (connection != null) {
+ return connection;
+ }
+
+ // No connections available. Enqueue a waiter in priority order.
+ final int priority = getPriority(connectionFlags);
+ final long startTime = SystemClock.uptimeMillis();
+ waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime,
+ priority, wantPrimaryConnection, sql, connectionFlags);
+ ConnectionWaiter predecessor = null;
+ ConnectionWaiter successor = mConnectionWaiterQueue;
+ while (successor != null) {
+ if (priority > successor.mPriority) {
+ waiter.mNext = successor;
+ break;
+ }
+ predecessor = successor;
+ successor = successor.mNext;
+ }
+ if (predecessor != null) {
+ predecessor.mNext = waiter;
+ } else {
+ mConnectionWaiterQueue = waiter;
+ }
+ }
+
+ // Park the thread until a connection is assigned or the pool is closed.
+ // Rethrow an exception from the wait, if we got one.
+ long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
+ long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
+ for (;;) {
+ // Detect and recover from connection leaks.
+ if (mConnectionLeaked.compareAndSet(true, false)) {
+ wakeConnectionWaitersLocked();
+ }
+
+ // Wait to be unparked (may already have happened), a timeout, or interruption.
+ LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L);
+
+ // Clear the interrupted flag, just in case.
+ Thread.interrupted();
+
+ // Check whether we are done waiting yet.
+ synchronized (mLock) {
+ throwIfClosedLocked();
+
+ SQLiteConnection connection = waiter.mAssignedConnection;
+ if (connection != null) {
+ recycleConnectionWaiterLocked(waiter);
+ return connection;
+ }
+
+ RuntimeException ex = waiter.mException;
+ if (ex != null) {
+ recycleConnectionWaiterLocked(waiter);
+ throw ex; // rethrow!
+ }
+
+ final long now = SystemClock.uptimeMillis();
+ if (now < nextBusyTimeoutTime) {
+ busyTimeoutMillis = now - nextBusyTimeoutTime;
+ } else {
+ logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags);
+ busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
+ nextBusyTimeoutTime = now + busyTimeoutMillis;
+ }
+ }
+ }
+ }
+
+ // Can't throw.
+ private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) {
+ final Thread thread = Thread.currentThread();
+ StringBuilder msg = new StringBuilder();
+ msg.append("The connection pool for database '").append(mConfiguration.label);
+ msg.append("' has been unable to grant a connection to thread ");
+ msg.append(thread.getId()).append(" (").append(thread.getName()).append(") ");
+ msg.append("with flags 0x").append(Integer.toHexString(connectionFlags));
+ msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n");
+
+ ArrayList<String> requests = new ArrayList<String>();
+ int activeConnections = 0;
+ int idleConnections = 0;
+ if (!mAcquiredConnections.isEmpty()) {
+ for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) {
+ final SQLiteConnection connection = entry.getKey();
+ String description = connection.describeCurrentOperationUnsafe();
+ if (description != null) {
+ requests.add(description);
+ activeConnections += 1;
+ } else {
+ idleConnections += 1;
+ }
+ }
+ }
+ int availableConnections = mAvailableNonPrimaryConnections.size();
+ if (mAvailablePrimaryConnection != null) {
+ availableConnections += 1;
+ }
+
+ msg.append("Connections: ").append(activeConnections).append(" active, ");
+ msg.append(idleConnections).append(" idle, ");
+ msg.append(availableConnections).append(" available.\n");
+
+ if (!requests.isEmpty()) {
+ msg.append("\nRequests in progress:\n");
+ for (String request : requests) {
+ msg.append(" ").append(request).append("\n");
+ }
+ }
+
+ Log.w(TAG, msg.toString());
+ }
+
+ // Can't throw.
+ private void wakeConnectionWaitersLocked() {
+ // Unpark all waiters that have requests that we can fulfill.
+ // This method is designed to not throw runtime exceptions, although we might send
+ // a waiter an exception for it to rethrow.
+ ConnectionWaiter predecessor = null;
+ ConnectionWaiter waiter = mConnectionWaiterQueue;
+ boolean primaryConnectionNotAvailable = false;
+ boolean nonPrimaryConnectionNotAvailable = false;
+ while (waiter != null) {
+ boolean unpark = false;
+ if (!mIsOpen) {
+ unpark = true;
+ } else {
+ try {
+ SQLiteConnection connection = null;
+ if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) {
+ connection = tryAcquireNonPrimaryConnectionLocked(
+ waiter.mSql, waiter.mConnectionFlags); // might throw
+ if (connection == null) {
+ nonPrimaryConnectionNotAvailable = true;
+ }
+ }
+ if (connection == null && !primaryConnectionNotAvailable) {
+ connection = tryAcquirePrimaryConnectionLocked(
+ waiter.mConnectionFlags); // might throw
+ if (connection == null) {
+ primaryConnectionNotAvailable = true;
+ }
+ }
+ if (connection != null) {
+ waiter.mAssignedConnection = connection;
+ unpark = true;
+ } else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) {
+ // There are no connections available and the pool is still open.
+ // We cannot fulfill any more connection requests, so stop here.
+ break;
+ }
+ } catch (RuntimeException ex) {
+ // Let the waiter handle the exception from acquiring a connection.
+ waiter.mException = ex;
+ unpark = true;
+ }
+ }
+
+ final ConnectionWaiter successor = waiter.mNext;
+ if (unpark) {
+ if (predecessor != null) {
+ predecessor.mNext = successor;
+ } else {
+ mConnectionWaiterQueue = successor;
+ }
+ waiter.mNext = null;
+
+ LockSupport.unpark(waiter.mThread);
+ } else {
+ predecessor = waiter;
+ }
+ waiter = successor;
+ }
+ }
+
+ // Might throw.
+ private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) {
+ // If the primary connection is available, acquire it now.
+ SQLiteConnection connection = mAvailablePrimaryConnection;
+ if (connection != null) {
+ mAvailablePrimaryConnection = null;
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+
+ // Make sure that the primary connection actually exists and has just been acquired.
+ for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) {
+ if (acquiredConnection.isPrimaryConnection()) {
+ return null;
+ }
+ }
+
+ // Uhoh. No primary connection! Either this is the first time we asked
+ // for it, or maybe it leaked?
+ connection = openConnectionLocked(true /*primaryConnection*/); // might throw
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+
+ // Might throw.
+ private SQLiteConnection tryAcquireNonPrimaryConnectionLocked(
+ String sql, int connectionFlags) {
+ // Try to acquire the next connection in the queue.
+ SQLiteConnection connection;
+ final int availableCount = mAvailableNonPrimaryConnections.size();
+ if (availableCount > 1 && sql != null) {
+ // If we have a choice, then prefer a connection that has the
+ // prepared statement in its cache.
+ for (int i = 0; i < availableCount; i++) {
+ connection = mAvailableNonPrimaryConnections.get(i);
+ if (connection.isPreparedStatementInCache(sql)) {
+ mAvailableNonPrimaryConnections.remove(i);
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+ }
+ }
+ if (availableCount > 0) {
+ // Otherwise, just grab the next one.
+ connection = mAvailableNonPrimaryConnections.remove(availableCount - 1);
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+
+ // Expand the pool if needed.
+ int openConnections = mAcquiredConnections.size();
+ if (mAvailablePrimaryConnection != null) {
+ openConnections += 1;
+ }
+ if (openConnections >= mConfiguration.maxConnectionPoolSize) {
+ return null;
+ }
+ connection = openConnectionLocked(false /*primaryConnection*/); // might throw
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+
+ // Might throw.
+ private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) {
+ try {
+ final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0;
+ connection.setOnlyAllowReadOnlyOperations(readOnly);
+
+ mAcquiredConnections.put(connection, Boolean.FALSE);
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to prepare acquired connection for session, closing it: "
+ + connection +", connectionFlags=" + connectionFlags);
+ closeConnectionAndLogExceptionsLocked(connection);
+ throw ex; // rethrow!
+ }
+ }
+
+ private boolean isSessionBlockingImportantConnectionWaitersLocked(
+ boolean holdingPrimaryConnection, int connectionFlags) {
+ ConnectionWaiter waiter = mConnectionWaiterQueue;
+ if (waiter != null) {
+ final int priority = getPriority(connectionFlags);
+ do {
+ // Only worry about blocked connections that have same or lower priority.
+ if (priority > waiter.mPriority) {
+ break;
+ }
+
+ // If we are holding the primary connection then we are blocking the waiter.
+ // Likewise, if we are holding a non-primary connection and the waiter
+ // would accept a non-primary connection, then we are blocking the waier.
+ if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) {
+ return true;
+ }
+
+ waiter = waiter.mNext;
+ } while (waiter != null);
+ }
+ return false;
+ }
+
+ private static int getPriority(int connectionFlags) {
+ return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0;
+ }
+
+ private void throwIfClosedLocked() {
+ if (!mIsOpen) {
+ throw new IllegalStateException("Cannot perform this operation "
+ + "because the connection pool have been closed.");
+ }
+ }
+
+ private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime,
+ int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) {
+ ConnectionWaiter waiter = mConnectionWaiterPool;
+ if (waiter != null) {
+ mConnectionWaiterPool = waiter.mNext;
+ waiter.mNext = null;
+ } else {
+ waiter = new ConnectionWaiter();
+ }
+ waiter.mThread = thread;
+ waiter.mStartTime = startTime;
+ waiter.mPriority = priority;
+ waiter.mWantPrimaryConnection = wantPrimaryConnection;
+ waiter.mSql = sql;
+ waiter.mConnectionFlags = connectionFlags;
+ return waiter;
+ }
+
+ private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) {
+ waiter.mNext = mConnectionWaiterPool;
+ waiter.mThread = null;
+ waiter.mSql = null;
+ waiter.mAssignedConnection = null;
+ waiter.mException = null;
+ mConnectionWaiterPool = waiter;
+ }
+
+ /**
+ * Dumps debugging information about this connection pool.
+ *
+ * @param printer The printer to receive the dump, not null.
+ */
+ public void dump(Printer printer) {
+ Printer indentedPrinter = PrefixPrinter.create(printer, " ");
+ synchronized (mLock) {
+ printer.println("Connection pool for " + mConfiguration.path + ":");
+ printer.println(" Open: " + mIsOpen);
+ printer.println(" Max connections: " + mConfiguration.maxConnectionPoolSize);
+
+ printer.println(" Available primary connection:");
+ if (mAvailablePrimaryConnection != null) {
+ mAvailablePrimaryConnection.dump(indentedPrinter);
+ } else {
+ indentedPrinter.println("<none>");
+ }
+
+ printer.println(" Available non-primary connections:");
+ if (!mAvailableNonPrimaryConnections.isEmpty()) {
+ final int count = mAvailableNonPrimaryConnections.size();
+ for (int i = 0; i < count; i++) {
+ mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter);
+ }
+ } else {
+ indentedPrinter.println("<none>");
+ }
+
+ printer.println(" Acquired connections:");
+ if (!mAcquiredConnections.isEmpty()) {
+ for (Map.Entry<SQLiteConnection, Boolean> entry :
+ mAcquiredConnections.entrySet()) {
+ final SQLiteConnection connection = entry.getKey();
+ connection.dumpUnsafe(indentedPrinter);
+ indentedPrinter.println(" Pending reconfiguration: " + entry.getValue());
+ }
+ } else {
+ indentedPrinter.println("<none>");
+ }
+
+ printer.println(" Connection waiters:");
+ if (mConnectionWaiterQueue != null) {
+ int i = 0;
+ final long now = SystemClock.uptimeMillis();
+ for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null;
+ waiter = waiter.mNext, i++) {
+ indentedPrinter.println(i + ": waited for "
+ + ((now - waiter.mStartTime) * 0.001f)
+ + " ms - thread=" + waiter.mThread
+ + ", priority=" + waiter.mPriority
+ + ", sql='" + waiter.mSql + "'");
+ }
+ } else {
+ indentedPrinter.println("<none>");
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteConnectionPool: " + mConfiguration.path;
+ }
+
+ private static final class ConnectionWaiter {
+ public ConnectionWaiter mNext;
+ public Thread mThread;
+ public long mStartTime;
+ public int mPriority;
+ public boolean mWantPrimaryConnection;
+ public String mSql;
+ public int mConnectionFlags;
+ public SQLiteConnection mAssignedConnection;
+ public RuntimeException mException;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java
index 8dcedf2..9dcb498 100644
--- a/core/java/android/database/sqlite/SQLiteCursor.java
+++ b/core/java/android/database/sqlite/SQLiteCursor.java
@@ -43,7 +43,7 @@ public class SQLiteCursor extends AbstractWindowedCursor {
private final String[] mColumns;
/** The query object for the cursor */
- private SQLiteQuery mQuery;
+ private final SQLiteQuery mQuery;
/** The compiled query this cursor came from */
private final SQLiteCursorDriver mDriver;
@@ -96,9 +96,6 @@ public class SQLiteCursor extends AbstractWindowedCursor {
if (query == null) {
throw new IllegalArgumentException("query object cannot be null");
}
- if (query.mDatabase == null) {
- throw new IllegalArgumentException("query.mDatabase cannot be null");
- }
if (StrictMode.vmSqliteObjectLeaksEnabled()) {
mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
} else {
@@ -109,38 +106,21 @@ public class SQLiteCursor extends AbstractWindowedCursor {
mColumnNameMap = null;
mQuery = query;
- query.mDatabase.lock(query.mSql);
- try {
- // Setup the list of columns
- int columnCount = mQuery.columnCountLocked();
- mColumns = new String[columnCount];
-
- // Read in all column names
- for (int i = 0; i < columnCount; i++) {
- String columnName = mQuery.columnNameLocked(i);
- mColumns[i] = columnName;
- if (false) {
- Log.v("DatabaseWindow", "mColumns[" + i + "] is "
- + mColumns[i]);
- }
-
- // Make note of the row ID column index for quick access to it
- if ("_id".equals(columnName)) {
- mRowIdColumnIndex = i;
- }
+ mColumns = query.getColumnNames();
+ for (int i = 0; i < mColumns.length; i++) {
+ // Make note of the row ID column index for quick access to it
+ if ("_id".equals(mColumns[i])) {
+ mRowIdColumnIndex = i;
}
- } finally {
- query.mDatabase.unlock();
}
}
/**
+ * Get the database that this cursor is associated with.
* @return the SQLiteDatabase that this cursor is associated with.
*/
public SQLiteDatabase getDatabase() {
- synchronized (this) {
- return mQuery.mDatabase;
- }
+ return mQuery.getDatabase();
}
@Override
@@ -167,7 +147,7 @@ public class SQLiteCursor extends AbstractWindowedCursor {
if (mCount == NO_COUNT) {
int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
- mCount = getQuery().fillWindow(mWindow, startPos, requiredPos, true);
+ mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
mCursorWindowCapacity = mWindow.getNumRows();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
@@ -175,14 +155,10 @@ public class SQLiteCursor extends AbstractWindowedCursor {
} else {
int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
mCursorWindowCapacity);
- getQuery().fillWindow(mWindow, startPos, requiredPos, false);
+ mQuery.fillWindow(mWindow, startPos, requiredPos, false);
}
}
- private synchronized SQLiteQuery getQuery() {
- return mQuery;
- }
-
@Override
public int getColumnIndex(String columnName) {
// Create mColumnNameMap on demand
@@ -237,75 +213,28 @@ public class SQLiteCursor extends AbstractWindowedCursor {
if (isClosed()) {
return false;
}
- long timeStart = 0;
- if (false) {
- timeStart = System.currentTimeMillis();
- }
synchronized (this) {
+ if (!mQuery.getDatabase().isOpen()) {
+ return false;
+ }
+
if (mWindow != null) {
mWindow.clear();
}
mPos = -1;
- SQLiteDatabase db = null;
- try {
- db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql);
- } catch (IllegalStateException e) {
- // for backwards compatibility, just return false
- Log.w(TAG, "requery() failed " + e.getMessage(), e);
- return false;
- }
- if (!db.equals(mQuery.mDatabase)) {
- // since we need to use a different database connection handle,
- // re-compile the query
- try {
- db.lock(mQuery.mSql);
- } catch (IllegalStateException e) {
- // for backwards compatibility, just return false
- Log.w(TAG, "requery() failed " + e.getMessage(), e);
- return false;
- }
- try {
- // close the old mQuery object and open a new one
- mQuery.close();
- mQuery = new SQLiteQuery(db, mQuery);
- } catch (IllegalStateException e) {
- // for backwards compatibility, just return false
- Log.w(TAG, "requery() failed " + e.getMessage(), e);
- return false;
- } finally {
- db.unlock();
- }
- }
- // This one will recreate the temp table, and get its count
- mDriver.cursorRequeried(this);
mCount = NO_COUNT;
- try {
- mQuery.requery();
- } catch (IllegalStateException e) {
- // for backwards compatibility, just return false
- Log.w(TAG, "requery() failed " + e.getMessage(), e);
- return false;
- }
- }
- if (false) {
- Log.v("DatabaseWindow", "closing window in requery()");
- Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
+ mDriver.cursorRequeried(this);
}
- boolean result = false;
try {
- result = super.requery();
+ return super.requery();
} catch (IllegalStateException e) {
// for backwards compatibility, just return false
Log.w(TAG, "requery() failed " + e.getMessage(), e);
+ return false;
}
- if (false) {
- long timeEnd = System.currentTimeMillis();
- Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
- }
- return result;
}
@Override
@@ -330,20 +259,17 @@ public class SQLiteCursor extends AbstractWindowedCursor {
// if the cursor hasn't been closed yet, close it first
if (mWindow != null) {
if (mStackTrace != null) {
- int len = mQuery.mSql.length();
+ String sql = mQuery.getSql();
+ int len = sql.length();
StrictMode.onSqliteObjectLeaked(
"Finalizing a Cursor that has not been deactivated or closed. " +
- "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable +
- ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len),
+ "database = " + mQuery.getDatabase().getLabel() +
+ ", table = " + mEditTable +
+ ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
mStackTrace);
}
close();
SQLiteDebug.notifyActiveCursorFinalized();
- } else {
- if (false) {
- Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() +
- ", table = " + mEditTable + ", query = " + mQuery.mSql);
- }
}
} finally {
super.finalize();
diff --git a/core/java/android/database/sqlite/SQLiteCursorDriver.java b/core/java/android/database/sqlite/SQLiteCursorDriver.java
index b3963f9..ad2cdd2 100644
--- a/core/java/android/database/sqlite/SQLiteCursorDriver.java
+++ b/core/java/android/database/sqlite/SQLiteCursorDriver.java
@@ -39,7 +39,7 @@ public interface SQLiteCursorDriver {
void cursorDeactivated();
/**
- * Called by a SQLiteCursor when it is requeryed.
+ * Called by a SQLiteCursor when it is requeried.
*/
void cursorRequeried(Cursor cursor);
diff --git a/core/java/android/database/sqlite/SQLiteCustomFunction.java b/core/java/android/database/sqlite/SQLiteCustomFunction.java
new file mode 100644
index 0000000..02f3284
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteCustomFunction.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+/**
+ * Describes a custom SQL function.
+ *
+ * @hide
+ */
+public final class SQLiteCustomFunction {
+ public final String name;
+ public final int numArgs;
+ public final SQLiteDatabase.CustomFunction callback;
+
+ /**
+ * Create custom function.
+ *
+ * @param name The name of the sqlite3 function.
+ * @param numArgs The number of arguments for the function, or -1 to
+ * support any number of arguments.
+ * @param callback The callback to invoke when the function is executed.
+ */
+ public SQLiteCustomFunction(String name, int numArgs,
+ SQLiteDatabase.CustomFunction callback) {
+ if (name == null) {
+ throw new IllegalArgumentException("name must not be null.");
+ }
+
+ this.name = name;
+ this.numArgs = numArgs;
+ this.callback = callback;
+ }
+
+ // Called from native.
+ @SuppressWarnings("unused")
+ private void dispatchCallback(String[] args) {
+ callback.callback(args);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index f990be6..377a680 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -16,7 +16,6 @@
package android.database.sqlite;
-import android.app.AppGlobals;
import android.content.ContentValues;
import android.content.res.Resources;
import android.database.Cursor;
@@ -25,61 +24,117 @@ import android.database.DatabaseUtils;
import android.database.DefaultDatabaseErrorHandler;
import android.database.SQLException;
import android.database.sqlite.SQLiteDebug.DbStats;
-import android.os.Debug;
-import android.os.StatFs;
-import android.os.SystemClock;
-import android.os.SystemProperties;
+import android.os.Looper;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
-import android.util.LruCache;
import android.util.Pair;
-import dalvik.system.BlockGuard;
+import android.util.Printer;
+
+import dalvik.system.CloseGuard;
+
import java.io.File;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Random;
import java.util.WeakHashMap;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.regex.Pattern;
/**
* Exposes methods to manage a SQLite database.
- * <p>SQLiteDatabase has methods to create, delete, execute SQL commands, and
+ *
+ * <p>
+ * SQLiteDatabase has methods to create, delete, execute SQL commands, and
* perform other common database management tasks.
- * <p>See the Notepad sample application in the SDK for an example of creating
+ * </p><p>
+ * See the Notepad sample application in the SDK for an example of creating
* and managing a database.
- * <p> Database names must be unique within an application, not across all
- * applications.
+ * </p><p>
+ * Database names must be unique within an application, not across all applications.
+ * </p>
*
* <h3>Localized Collation - ORDER BY</h3>
- * <p>In addition to SQLite's default <code>BINARY</code> collator, Android supplies
- * two more, <code>LOCALIZED</code>, which changes with the system's current locale
- * if you wire it up correctly (XXX a link needed!), and <code>UNICODE</code>, which
- * is the Unicode Collation Algorithm and not tailored to the current locale.
+ * <p>
+ * In addition to SQLite's default <code>BINARY</code> collator, Android supplies
+ * two more, <code>LOCALIZED</code>, which changes with the system's current locale,
+ * and <code>UNICODE</code>, which is the Unicode Collation Algorithm and not tailored
+ * to the current locale.
+ * </p>
*/
public class SQLiteDatabase extends SQLiteClosable {
private static final String TAG = "SQLiteDatabase";
- private static final boolean ENABLE_DB_SAMPLE = false; // true to enable stats in event log
- private static final int EVENT_DB_OPERATION = 52000;
+
private static final int EVENT_DB_CORRUPT = 75004;
- /**
- * Algorithms used in ON CONFLICT clause
- * http://www.sqlite.org/lang_conflict.html
- */
- /**
- * When a constraint violation occurs, an immediate ROLLBACK occurs,
+ // Stores reference to all databases opened in the current process.
+ // (The referent Object is not used at this time.)
+ // INVARIANT: Guarded by sActiveDatabases.
+ private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases =
+ new WeakHashMap<SQLiteDatabase, Object>();
+
+ // Thread-local for database sessions that belong to this database.
+ // Each thread has its own database session.
+ // INVARIANT: Immutable.
+ private final ThreadLocal<SQLiteSession> mThreadSession = new ThreadLocal<SQLiteSession>() {
+ @Override
+ protected SQLiteSession initialValue() {
+ return createSession();
+ }
+ };
+
+ // The optional factory to use when creating new Cursors. May be null.
+ // INVARIANT: Immutable.
+ private final CursorFactory mCursorFactory;
+
+ // Error handler to be used when SQLite returns corruption errors.
+ // INVARIANT: Immutable.
+ private final DatabaseErrorHandler mErrorHandler;
+
+ // Shared database state lock.
+ // This lock guards all of the shared state of the database, such as its
+ // configuration, whether it is open or closed, and so on. This lock should
+ // be held for as little time as possible.
+ //
+ // The lock MUST NOT be held while attempting to acquire database connections or
+ // while executing SQL statements on behalf of the client as it can lead to deadlock.
+ //
+ // It is ok to hold the lock while reconfiguring the connection pool or dumping
+ // statistics because those operations are non-reentrant and do not try to acquire
+ // connections that might be held by other threads.
+ //
+ // Basic rule: grab the lock, access or modify global state, release the lock, then
+ // do the required SQL work.
+ private final Object mLock = new Object();
+
+ // Warns if the database is finalized without being closed properly.
+ // INVARIANT: Guarded by mLock.
+ private final CloseGuard mCloseGuardLocked = CloseGuard.get();
+
+ // The database configuration.
+ // INVARIANT: Guarded by mLock.
+ private final SQLiteDatabaseConfiguration mConfigurationLocked;
+
+ // The connection pool for the database, null when closed.
+ // The pool itself is thread-safe, but the reference to it can only be acquired
+ // when the lock is held.
+ // INVARIANT: Guarded by mLock.
+ private SQLiteConnectionPool mConnectionPoolLocked;
+
+ // True if the database has attached databases.
+ // INVARIANT: Guarded by mLock.
+ private boolean mHasAttachedDbsLocked;
+
+ // True if the database is in WAL mode.
+ // INVARIANT: Guarded by mLock.
+ private boolean mIsWALEnabledLocked;
+
+ /**
+ * When a constraint violation occurs, an immediate ROLLBACK occurs,
* thus ending the current transaction, and the command aborts with a
* return code of SQLITE_CONSTRAINT. If no transaction is active
* (other than the implied transaction that is created on every command)
- * then this algorithm works the same as ABORT.
+ * then this algorithm works the same as ABORT.
*/
public static final int CONFLICT_ROLLBACK = 1;
@@ -118,14 +173,15 @@ public class SQLiteDatabase extends SQLiteClosable {
* violation occurs then the IGNORE algorithm is used. When this conflict
* resolution strategy deletes rows in order to satisfy a constraint,
* it does not invoke delete triggers on those rows.
- * This behavior might change in a future release.
+ * This behavior might change in a future release.
*/
public static final int CONFLICT_REPLACE = 5;
/**
- * use the following when no conflict action is specified.
+ * Use the following when no conflict action is specified.
*/
public static final int CONFLICT_NONE = 0;
+
private static final String[] CONFLICT_VALUES = new String[]
{"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
@@ -146,7 +202,7 @@ public class SQLiteDatabase extends SQLiteClosable {
public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000;
/**
- * Flag for {@link #openDatabase} to open the database for reading and writing.
+ * Open flag: Flag for {@link #openDatabase} to open the database for reading and writing.
* If the disk is full, this may fail even before you actually write anything.
*
* {@more} Note that the value of this flag is 0, so it is the default.
@@ -154,7 +210,7 @@ public class SQLiteDatabase extends SQLiteClosable {
public static final int OPEN_READWRITE = 0x00000000; // update native code if changing
/**
- * Flag for {@link #openDatabase} to open the database for reading only.
+ * Open flag: Flag for {@link #openDatabase} to open the database for reading only.
* This is the only reliable way to open a database if the disk may be full.
*/
public static final int OPEN_READONLY = 0x00000001; // update native code if changing
@@ -162,7 +218,8 @@ public class SQLiteDatabase extends SQLiteClosable {
private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing
/**
- * Flag for {@link #openDatabase} to open the database without support for localized collators.
+ * Open flag: Flag for {@link #openDatabase} to open the database without support for
+ * localized collators.
*
* {@more} This causes the collator <code>LOCALIZED</code> not to be created.
* You must be consistent when using this flag to use the setting the database was
@@ -171,190 +228,62 @@ public class SQLiteDatabase extends SQLiteClosable {
public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing
/**
- * Flag for {@link #openDatabase} to create the database file if it does not already exist.
+ * Open flag: Flag for {@link #openDatabase} to create the database file if it does not
+ * already exist.
*/
public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing
/**
- * Indicates whether the most-recently started transaction has been marked as successful.
- */
- private boolean mInnerTransactionIsSuccessful;
-
- /**
- * Valid during the life of a transaction, and indicates whether the entire transaction (the
- * outer one and all of the inner ones) so far has been successful.
- */
- private boolean mTransactionIsSuccessful;
-
- /**
- * Valid during the life of a transaction.
- */
- private SQLiteTransactionListener mTransactionListener;
-
- /**
- * this member is set if {@link #execSQL(String)} is used to begin and end transactions.
- */
- private boolean mTransactionUsingExecSql;
-
- /** Synchronize on this when accessing the database */
- private final DatabaseReentrantLock mLock = new DatabaseReentrantLock(true);
-
- private long mLockAcquiredWallTime = 0L;
- private long mLockAcquiredThreadTime = 0L;
-
- // limit the frequency of complaints about each database to one within 20 sec
- // unless run command adb shell setprop log.tag.Database VERBOSE
- private static final int LOCK_WARNING_WINDOW_IN_MS = 20000;
- /** If the lock is held this long then a warning will be printed when it is released. */
- private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300;
- private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100;
- private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000;
-
- private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000;
-
- // The pattern we remove from database filenames before
- // potentially logging them.
- private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+");
-
- private long mLastLockMessageTime = 0L;
-
- // Things related to query logging/sampling for debugging
- // slow/frequent queries during development. Always log queries
- // which take (by default) 500ms+; shorter queries are sampled
- // accordingly. Commit statements, which are typically slow, are
- // logged together with the most recently executed SQL statement,
- // for disambiguation. The 500ms value is configurable via a
- // SystemProperty, but developers actively debugging database I/O
- // should probably use the regular log tunable,
- // LOG_SLOW_QUERIES_PROPERTY, defined below.
- private static int sQueryLogTimeInMillis = 0; // lazily initialized
- private static final int QUERY_LOG_SQL_LENGTH = 64;
- private static final String COMMIT_SQL = "COMMIT;";
- private static final String BEGIN_SQL = "BEGIN;";
- private final Random mRandom = new Random();
- /** the last non-commit/rollback sql statement in a transaction */
- // guarded by 'this'
- private String mLastSqlStatement = null;
-
- synchronized String getLastSqlStatement() {
- return mLastSqlStatement;
- }
-
- synchronized void setLastSqlStatement(String sql) {
- mLastSqlStatement = sql;
- }
-
- /** guarded by {@link #mLock} */
- private long mTransStartTime;
-
- // String prefix for slow database query EventLog records that show
- // lock acquistions of the database.
- /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:";
-
- /** Used by native code, do not rename. make it volatile, so it is thread-safe. */
- /* package */ volatile int mNativeHandle = 0;
-
- /**
- * The size, in bytes, of a block on "/data". This corresponds to the Unix
- * statfs.f_bsize field. note that this field is lazily initialized.
- */
- private static int sBlockSize = 0;
-
- /** The path for the database file */
- private final String mPath;
-
- /** The anonymized path for the database file for logging purposes */
- private String mPathForLogs = null; // lazily populated
-
- /** The flags passed to open/create */
- private final int mFlags;
-
- /** The optional factory to use when creating new Cursors */
- private final CursorFactory mFactory;
-
- private final WeakHashMap<SQLiteClosable, Object> mPrograms;
-
- /** Default statement-cache size per database connection ( = instance of this class) */
- private static final int DEFAULT_SQL_CACHE_SIZE = 25;
-
- /**
- * for each instance of this class, a LRU cache is maintained to store
- * the compiled query statement ids returned by sqlite database.
- * key = SQL statement with "?" for bind args
- * value = {@link SQLiteCompiledSql}
- * If an application opens the database and keeps it open during its entire life, then
- * there will not be an overhead of compilation of SQL statements by sqlite.
+ * Absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}.
*
- * why is this cache NOT static? because sqlite attaches compiledsql statements to the
- * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is
- * invoked.
- *
- * this cache's max size is settable by calling the method
- * (@link #setMaxSqlCacheSize(int)}.
- */
- // guarded by this
- private LruCache<String, SQLiteCompiledSql> mCompiledQueries;
-
- /**
- * absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}
- * size of each prepared-statement is between 1K - 6K, depending on the complexity of the
- * SQL statement & schema.
+ * Each prepared-statement is between 1K - 6K, depending on the complexity of the
+ * SQL statement & schema. A large SQL cache may use a significant amount of memory.
*/
public static final int MAX_SQL_CACHE_SIZE = 100;
- private boolean mCacheFullWarning;
-
- /** Used to find out where this object was created in case it never got closed. */
- private final Throwable mStackTrace;
-
- /** stores the list of statement ids that need to be finalized by sqlite */
- private final ArrayList<Integer> mClosedStatementIds = new ArrayList<Integer>();
-
- /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors
- * Corruption
- * */
- private final DatabaseErrorHandler mErrorHandler;
-
- /** The Database connection pool {@link DatabaseConnectionPool}.
- * Visibility is package-private for testing purposes. otherwise, private visibility is enough.
- */
- /* package */ volatile DatabaseConnectionPool mConnectionPool = null;
-
- /** Each database connection handle in the pool is assigned a number 1..N, where N is the
- * size of the connection pool.
- * The main connection handle to which the pool is attached is assigned a value of 0.
- */
- /* package */ final short mConnectionNum;
-
- /** on pooled database connections, this member points to the parent ( = main)
- * database connection handle.
- * package visibility only for testing purposes
- */
- /* package */ SQLiteDatabase mParentConnObj = null;
-
- private static final String MEMORY_DB_PATH = ":memory:";
- /** set to true if the database has attached databases */
- private volatile boolean mHasAttachedDbs = false;
-
- /** stores reference to all databases opened in the current process. */
- private static ArrayList<WeakReference<SQLiteDatabase>> mActiveDatabases =
- new ArrayList<WeakReference<SQLiteDatabase>>();
-
- synchronized void addSQLiteClosable(SQLiteClosable closable) {
- // mPrograms is per instance of SQLiteDatabase and it doesn't actually touch the database
- // itself. so, there is no need to lock().
- mPrograms.put(closable, null);
+ private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory,
+ DatabaseErrorHandler errorHandler) {
+ mCursorFactory = cursorFactory;
+ mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
+ mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);
}
- synchronized void removeSQLiteClosable(SQLiteClosable closable) {
- mPrograms.remove(closable);
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
}
@Override
protected void onAllReferencesReleased() {
- if (isOpen()) {
- // close the database which will close all pending statements to be finalized also
- close();
+ dispose(false);
+ }
+
+ private void dispose(boolean finalized) {
+ final SQLiteConnectionPool pool;
+ synchronized (mLock) {
+ if (mCloseGuardLocked != null) {
+ if (finalized) {
+ mCloseGuardLocked.warnIfOpen();
+ }
+ mCloseGuardLocked.close();
+ }
+
+ pool = mConnectionPoolLocked;
+ mConnectionPoolLocked = null;
+ }
+
+ if (!finalized) {
+ synchronized (sActiveDatabases) {
+ sActiveDatabases.remove(this);
+ }
+
+ if (pool != null) {
+ pool.close();
+ }
}
}
@@ -364,7 +293,9 @@ public class SQLiteDatabase extends SQLiteClosable {
*
* @return the number of bytes actually released
*/
- static public native int releaseMemory();
+ public static int releaseMemory() {
+ return SQLiteGlobal.releaseMemory();
+ }
/**
* Control whether or not the SQLiteDatabase is made thread-safe by using locks
@@ -372,159 +303,82 @@ public class SQLiteDatabase extends SQLiteClosable {
* DB will only be used by a single thread then you should set this to false.
* The default is true.
* @param lockingEnabled set to true to enable locks, false otherwise
+ *
+ * @deprecated This method now does nothing. Do not use.
*/
+ @Deprecated
public void setLockingEnabled(boolean lockingEnabled) {
- mLockingEnabled = lockingEnabled;
}
/**
- * If set then the SQLiteDatabase is made thread-safe by using locks
- * around critical sections
+ * Gets a label to use when describing the database in log messages.
+ * @return The label.
*/
- private boolean mLockingEnabled = true;
-
- /* package */ void onCorruption() {
- EventLog.writeEvent(EVENT_DB_CORRUPT, mPath);
- mErrorHandler.onCorruption(this);
+ String getLabel() {
+ synchronized (mLock) {
+ return mConfigurationLocked.label;
+ }
}
/**
- * Locks the database for exclusive access. The database lock must be held when
- * touch the native sqlite3* object since it is single threaded and uses
- * a polling lock contention algorithm. The lock is recursive, and may be acquired
- * multiple times by the same thread. This is a no-op if mLockingEnabled is false.
- *
- * @see #unlock()
+ * Sends a corruption message to the database error handler.
*/
- /* package */ void lock(String sql) {
- lock(sql, false);
- }
-
- /* pachage */ void lock() {
- lock(null, false);
- }
-
- private static final long LOCK_WAIT_PERIOD = 30L;
- private void lock(String sql, boolean forced) {
- // make sure this method is NOT being called from a 'synchronized' method
- if (Thread.holdsLock(this)) {
- Log.w(TAG, "don't lock() while in a synchronized method");
- }
- verifyDbIsOpen();
- if (!forced && !mLockingEnabled) return;
- boolean done = false;
- long timeStart = SystemClock.uptimeMillis();
- while (!done) {
- try {
- // wait for 30sec to acquire the lock
- done = mLock.tryLock(LOCK_WAIT_PERIOD, TimeUnit.SECONDS);
- if (!done) {
- // lock not acquired in NSec. print a message and stacktrace saying the lock
- // has not been available for 30sec.
- Log.w(TAG, "database lock has not been available for " + LOCK_WAIT_PERIOD +
- " sec. Current Owner of the lock is " + mLock.getOwnerDescription() +
- ". Continuing to wait in thread: " + Thread.currentThread().getId());
- }
- } catch (InterruptedException e) {
- // ignore the interruption
- }
- }
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
- if (mLock.getHoldCount() == 1) {
- // Use elapsed real-time since the CPU may sleep when waiting for IO
- mLockAcquiredWallTime = SystemClock.elapsedRealtime();
- mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
- }
- }
- if (sql != null) {
- if (ENABLE_DB_SAMPLE) {
- logTimeStat(sql, timeStart, GET_LOCK_LOG_PREFIX);
- }
- }
- }
- private static class DatabaseReentrantLock extends ReentrantLock {
- DatabaseReentrantLock(boolean fair) {
- super(fair);
- }
- @Override
- public Thread getOwner() {
- return super.getOwner();
- }
- public String getOwnerDescription() {
- Thread t = getOwner();
- return (t== null) ? "none" : String.valueOf(t.getId());
- }
+ void onCorruption() {
+ EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel());
+ mErrorHandler.onCorruption(this);
}
/**
- * Locks the database for exclusive access. The database lock must be held when
- * touch the native sqlite3* object since it is single threaded and uses
- * a polling lock contention algorithm. The lock is recursive, and may be acquired
- * multiple times by the same thread.
+ * Gets the {@link SQLiteSession} that belongs to this thread for this database.
+ * Once a thread has obtained a session, it will continue to obtain the same
+ * session even after the database has been closed (although the session will not
+ * be usable). However, a thread that does not already have a session cannot
+ * obtain one after the database has been closed.
*
- * @see #unlockForced()
+ * The idea is that threads that have active connections to the database may still
+ * have work to complete even after the call to {@link #close}. Active database
+ * connections are not actually disposed until they are released by the threads
+ * that own them.
+ *
+ * @return The session, never null.
+ *
+ * @throws IllegalStateException if the thread does not yet have a session and
+ * the database is not open.
*/
- private void lockForced() {
- lock(null, true);
- }
-
- private void lockForced(String sql) {
- lock(sql, true);
+ SQLiteSession getThreadSession() {
+ return mThreadSession.get(); // initialValue() throws if database closed
}
- /**
- * Releases the database lock. This is a no-op if mLockingEnabled is false.
- *
- * @see #unlock()
- */
- /* package */ void unlock() {
- if (!mLockingEnabled) return;
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
- if (mLock.getHoldCount() == 1) {
- checkLockHoldTime();
- }
+ SQLiteSession createSession() {
+ final SQLiteConnectionPool pool;
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ pool = mConnectionPoolLocked;
}
- mLock.unlock();
+ return new SQLiteSession(pool);
}
/**
- * Releases the database lock.
+ * Gets default connection flags that are appropriate for this thread, taking into
+ * account whether the thread is acting on behalf of the UI.
*
- * @see #unlockForced()
+ * @param readOnly True if the connection should be read-only.
+ * @return The connection flags.
*/
- private void unlockForced() {
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
- if (mLock.getHoldCount() == 1) {
- checkLockHoldTime();
- }
+ int getThreadDefaultConnectionFlags(boolean readOnly) {
+ int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY :
+ SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY;
+ if (isMainThread()) {
+ flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE;
}
- mLock.unlock();
+ return flags;
}
- private void checkLockHoldTime() {
- // Use elapsed real-time since the CPU may sleep when waiting for IO
- long elapsedTime = SystemClock.elapsedRealtime();
- long lockedTime = elapsedTime - mLockAcquiredWallTime;
- if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT &&
- !Log.isLoggable(TAG, Log.VERBOSE) &&
- (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) {
- return;
- }
- if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) {
- int threadTime = (int)
- ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000);
- if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS ||
- lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) {
- mLastLockMessageTime = elapsedTime;
- String msg = "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was "
- + threadTime + "ms";
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) {
- Log.d(TAG, msg, new Exception());
- } else {
- Log.d(TAG, msg);
- }
- }
- }
+ private static boolean isMainThread() {
+ // FIXME: There should be a better way to do this.
+ // Would also be nice to have something that would work across Binder calls.
+ Looper looper = Looper.myLooper();
+ return looper != null && looper == Looper.getMainLooper();
}
/**
@@ -636,50 +490,9 @@ public class SQLiteDatabase extends SQLiteClosable {
private void beginTransaction(SQLiteTransactionListener transactionListener,
boolean exclusive) {
- verifyDbIsOpen();
- lockForced(BEGIN_SQL);
- boolean ok = false;
- try {
- // If this thread already had the lock then get out
- if (mLock.getHoldCount() > 1) {
- if (mInnerTransactionIsSuccessful) {
- String msg = "Cannot call beginTransaction between "
- + "calling setTransactionSuccessful and endTransaction";
- IllegalStateException e = new IllegalStateException(msg);
- Log.e(TAG, "beginTransaction() failed", e);
- throw e;
- }
- ok = true;
- return;
- }
-
- // This thread didn't already have the lock, so begin a database
- // transaction now.
- if (exclusive && mConnectionPool == null) {
- execSQL("BEGIN EXCLUSIVE;");
- } else {
- execSQL("BEGIN IMMEDIATE;");
- }
- mTransStartTime = SystemClock.uptimeMillis();
- mTransactionListener = transactionListener;
- mTransactionIsSuccessful = true;
- mInnerTransactionIsSuccessful = false;
- if (transactionListener != null) {
- try {
- transactionListener.onBegin();
- } catch (RuntimeException e) {
- execSQL("ROLLBACK;");
- throw e;
- }
- }
- ok = true;
- } finally {
- if (!ok) {
- // beginTransaction is called before the try block so we must release the lock in
- // the case of failure.
- unlockForced();
- }
- }
+ getThreadSession().beginTransaction(exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE :
+ SQLiteSession.TRANSACTION_MODE_IMMEDIATE, transactionListener,
+ getThreadDefaultConnectionFlags(false /*readOnly*/));
}
/**
@@ -687,68 +500,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* are committed and rolled back.
*/
public void endTransaction() {
- verifyLockOwner();
- try {
- if (mInnerTransactionIsSuccessful) {
- mInnerTransactionIsSuccessful = false;
- } else {
- mTransactionIsSuccessful = false;
- }
- if (mLock.getHoldCount() != 1) {
- return;
- }
- RuntimeException savedException = null;
- if (mTransactionListener != null) {
- try {
- if (mTransactionIsSuccessful) {
- mTransactionListener.onCommit();
- } else {
- mTransactionListener.onRollback();
- }
- } catch (RuntimeException e) {
- savedException = e;
- mTransactionIsSuccessful = false;
- }
- }
- if (mTransactionIsSuccessful) {
- execSQL(COMMIT_SQL);
- // if write-ahead logging is used, we have to take care of checkpoint.
- // TODO: should applications be given the flexibility of choosing when to
- // trigger checkpoint?
- // for now, do checkpoint after every COMMIT because that is the fastest
- // way to guarantee that readers will see latest data.
- // but this is the slowest way to run sqlite with in write-ahead logging mode.
- if (this.mConnectionPool != null) {
- execSQL("PRAGMA wal_checkpoint;");
- if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
- Log.i(TAG, "PRAGMA wal_Checkpoint done");
- }
- }
- // log the transaction time to the Eventlog.
- if (ENABLE_DB_SAMPLE) {
- logTimeStat(getLastSqlStatement(), mTransStartTime, COMMIT_SQL);
- }
- } else {
- try {
- execSQL("ROLLBACK;");
- if (savedException != null) {
- throw savedException;
- }
- } catch (SQLException e) {
- if (false) {
- Log.d(TAG, "exception during rollback, maybe the DB previously "
- + "performed an auto-rollback");
- }
- }
- }
- } finally {
- mTransactionListener = null;
- unlockForced();
- if (false) {
- Log.v(TAG, "unlocked " + Thread.currentThread()
- + ", holdCount is " + mLock.getHoldCount());
- }
- }
+ getThreadSession().endTransaction();
}
/**
@@ -761,86 +513,46 @@ public class SQLiteDatabase extends SQLiteClosable {
* transaction is already marked as successful.
*/
public void setTransactionSuccessful() {
- verifyDbIsOpen();
- if (!mLock.isHeldByCurrentThread()) {
- throw new IllegalStateException("no transaction pending");
- }
- if (mInnerTransactionIsSuccessful) {
- throw new IllegalStateException(
- "setTransactionSuccessful may only be called once per call to beginTransaction");
- }
- mInnerTransactionIsSuccessful = true;
+ getThreadSession().setTransactionSuccessful();
}
/**
- * return true if there is a transaction pending
+ * Returns true if the current thread has a transaction pending.
+ *
+ * @return True if the current thread is in a transaction.
*/
public boolean inTransaction() {
- return mLock.getHoldCount() > 0 || mTransactionUsingExecSql;
- }
-
- /* package */ synchronized void setTransactionUsingExecSqlFlag() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.i(TAG, "found execSQL('begin transaction')");
- }
- mTransactionUsingExecSql = true;
- }
-
- /* package */ synchronized void resetTransactionUsingExecSqlFlag() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- if (mTransactionUsingExecSql) {
- Log.i(TAG, "found execSQL('commit or end or rollback')");
- }
- }
- mTransactionUsingExecSql = false;
+ return getThreadSession().hasTransaction();
}
/**
- * Returns true if the caller is considered part of the current transaction, if any.
+ * Returns true if the current thread is holding an active connection to the database.
* <p>
- * Caller is part of the current transaction if either of the following is true
- * <ol>
- * <li>If transaction is started by calling beginTransaction() methods AND if the caller is
- * in the same thread as the thread that started the transaction.
- * </li>
- * <li>If the transaction is started by calling {@link #execSQL(String)} like this:
- * execSQL("BEGIN transaction"). In this case, every thread in the process is considered
- * part of the current transaction.</li>
- * </ol>
- *
- * @return true if the caller is considered part of the current transaction, if any.
- */
- /* package */ synchronized boolean amIInTransaction() {
- // always do this test on the main database connection - NOT on pooled database connection
- // since transactions always occur on the main database connections only.
- SQLiteDatabase db = (isPooledConnection()) ? mParentConnObj : this;
- boolean b = (!db.inTransaction()) ? false :
- db.mTransactionUsingExecSql || db.mLock.isHeldByCurrentThread();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.i(TAG, "amIinTransaction: " + b);
- }
- return b;
- }
-
- /**
- * Checks if the database lock is held by this thread.
+ * The name of this method comes from a time when having an active connection
+ * to the database meant that the thread was holding an actual lock on the
+ * database. Nowadays, there is no longer a true "database lock" although threads
+ * may block if they cannot acquire a database connection to perform a
+ * particular operation.
+ * </p>
*
- * @return true, if this thread is holding the database lock.
+ * @return True if the current thread is holding an active connection to the database.
*/
public boolean isDbLockedByCurrentThread() {
- return mLock.isHeldByCurrentThread();
+ return getThreadSession().hasConnection();
}
/**
- * Checks if the database is locked by another thread. This is
- * just an estimate, since this status can change at any time,
- * including after the call is made but before the result has
- * been acted upon.
+ * Always returns false.
+ * <p>
+ * There is no longer the concept of a database lock, so this method always returns false.
+ * </p>
*
- * @return true, if the database is locked by another thread
+ * @return False.
+ * @deprecated Always returns false. Do not use this method.
*/
+ @Deprecated
public boolean isDbLockedByOtherThreads() {
- return !mLock.isHeldByCurrentThread() && mLock.isLocked();
+ return false;
}
/**
@@ -884,46 +596,12 @@ public class SQLiteDatabase extends SQLiteClosable {
return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay);
}
- private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) {
- if (mLock.getQueueLength() == 0) {
- // Reset the lock acquire time since we know that the thread was willing to yield
- // the lock at this time.
- mLockAcquiredWallTime = SystemClock.elapsedRealtime();
- mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
- return false;
- }
- setTransactionSuccessful();
- SQLiteTransactionListener transactionListener = mTransactionListener;
- endTransaction();
- if (checkFullyYielded) {
- if (this.isDbLockedByCurrentThread()) {
- throw new IllegalStateException(
- "Db locked more than once. yielfIfContended cannot yield");
- }
- }
- if (sleepAfterYieldDelay > 0) {
- // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to
- // check if anyone is using the database. If the database is not contended,
- // retake the lock and return.
- long remainingDelay = sleepAfterYieldDelay;
- while (remainingDelay > 0) {
- try {
- Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ?
- remainingDelay : SLEEP_AFTER_YIELD_QUANTUM);
- } catch (InterruptedException e) {
- Thread.interrupted();
- }
- remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM;
- if (mLock.getQueueLength() == 0) {
- break;
- }
- }
- }
- beginTransactionWithListener(transactionListener);
- return true;
+ private boolean yieldIfContendedHelper(boolean throwIfUnsafe, long sleepAfterYieldDelay) {
+ return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe);
}
/**
+ * Deprecated.
* @deprecated This method no longer serves any useful purpose and has been deprecated.
*/
@Deprecated
@@ -932,19 +610,6 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * Used to allow returning sub-classes of {@link Cursor} when calling query.
- */
- public interface CursorFactory {
- /**
- * See
- * {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}.
- */
- public Cursor newCursor(SQLiteDatabase db,
- SQLiteCursorDriver masterQuery, String editTable,
- SQLiteQuery query);
- }
-
- /**
* Open the database according to the flags {@link #OPEN_READWRITE}
* {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}.
*
@@ -983,50 +648,9 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
DatabaseErrorHandler errorHandler) {
- SQLiteDatabase sqliteDatabase = openDatabase(path, factory, flags, errorHandler,
- (short) 0 /* the main connection handle */);
-
- // set sqlite pagesize to mBlockSize
- if (sBlockSize == 0) {
- // TODO: "/data" should be a static final String constant somewhere. it is hardcoded
- // in several places right now.
- sBlockSize = new StatFs("/data").getBlockSize();
- }
- sqliteDatabase.setPageSize(sBlockSize);
- sqliteDatabase.setJournalMode(path, "TRUNCATE");
-
- // add this database to the list of databases opened in this process
- synchronized(mActiveDatabases) {
- mActiveDatabases.add(new WeakReference<SQLiteDatabase>(sqliteDatabase));
- }
- return sqliteDatabase;
- }
-
- private static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
- DatabaseErrorHandler errorHandler, short connectionNum) {
- SQLiteDatabase db = new SQLiteDatabase(path, factory, flags, errorHandler, connectionNum);
- try {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.i(TAG, "opening the db : " + path);
- }
- // Open the database.
- db.dbopen(path, flags);
- db.setLocale(Locale.getDefault());
- if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
- db.enableSqlTracing(path, connectionNum);
- }
- if (SQLiteDebug.DEBUG_SQL_TIME) {
- db.enableSqlProfiling(path, connectionNum);
- }
- return db;
- } catch (SQLiteDatabaseCorruptException e) {
- db.mErrorHandler.onCorruption(db);
- return SQLiteDatabase.openDatabase(path, factory, flags, errorHandler);
- } catch (SQLiteException e) {
- Log.e(TAG, "Failed to open the database. closing it.", e);
- db.close();
- throw e;
- }
+ SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler);
+ db.open();
+ return db;
}
/**
@@ -1051,16 +675,46 @@ public class SQLiteDatabase extends SQLiteClosable {
return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler);
}
- private void setJournalMode(final String dbPath, final String mode) {
- // journal mode can be set only for non-memory databases
+ private void open() {
+ try {
+ try {
+ openInner();
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ openInner();
+ }
+
+ // Disable WAL if it was previously enabled.
+ setJournalMode("TRUNCATE");
+ } catch (SQLiteException ex) {
+ Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex);
+ close();
+ throw ex;
+ }
+ }
+
+ private void openInner() {
+ synchronized (mLock) {
+ assert mConnectionPoolLocked == null;
+ mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked);
+ mCloseGuardLocked.open("close");
+ }
+
+ synchronized (sActiveDatabases) {
+ sActiveDatabases.put(this, null);
+ }
+ }
+
+ private void setJournalMode(String mode) {
+ // Journal mode can be set only for non-memory databases
// AND can't be set for readonly databases
- if (dbPath.equalsIgnoreCase(MEMORY_DB_PATH) || isReadOnly()) {
+ if (isInMemoryDatabase() || isReadOnly()) {
return;
}
String s = DatabaseUtils.stringForQuery(this, "PRAGMA journal_mode=" + mode, null);
if (!s.equalsIgnoreCase(mode)) {
- Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + dbPath +
- " (on pragma set journal_mode, sqlite returned:" + s);
+ Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + getLabel()
+ + " (on pragma set journal_mode, sqlite returned:" + s);
}
}
@@ -1077,159 +731,37 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public static SQLiteDatabase create(CursorFactory factory) {
// This is a magic string with special meaning for SQLite.
- return openDatabase(MEMORY_DB_PATH, factory, CREATE_IF_NECESSARY);
+ return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH,
+ factory, CREATE_IF_NECESSARY);
}
/**
* Close the database.
*/
public void close() {
- if (!isOpen()) {
- return;
- }
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.i(TAG, "closing db: " + mPath + " (connection # " + mConnectionNum);
- }
- lock();
- try {
- // some other thread could have closed this database while I was waiting for lock.
- // check the database state
- if (!isOpen()) {
- return;
- }
- closeClosable();
- // finalize ALL statements queued up so far
- closePendingStatements();
- releaseCustomFunctions();
- // close this database instance - regardless of its reference count value
- closeDatabase();
- if (mConnectionPool != null) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert mConnectionPool != null;
- Log.i(TAG, mConnectionPool.toString());
- }
- mConnectionPool.close();
- }
- } finally {
- unlock();
- }
- }
-
- private void closeClosable() {
- /* deallocate all compiled SQL statement objects from mCompiledQueries cache.
- * this should be done before de-referencing all {@link SQLiteClosable} objects
- * from this database object because calling
- * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database
- * to be closed. sqlite doesn't let a database close if there are
- * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries.
- */
- deallocCachedSqlStatements();
-
- Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator();
- while (iter.hasNext()) {
- Map.Entry<SQLiteClosable, Object> entry = iter.next();
- SQLiteClosable program = entry.getKey();
- if (program != null) {
- program.onAllReferencesReleasedFromContainer();
- }
- }
- }
-
- /**
- * package level access for testing purposes
- */
- /* package */ void closeDatabase() throws SQLiteException {
- try {
- dbclose();
- } catch (SQLiteUnfinalizedObjectsException e) {
- String msg = e.getMessage();
- String[] tokens = msg.split(",", 2);
- int stmtId = Integer.parseInt(tokens[0]);
- // get extra info about this statement, if it is still to be released by closeClosable()
- Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator();
- boolean found = false;
- while (iter.hasNext()) {
- Map.Entry<SQLiteClosable, Object> entry = iter.next();
- SQLiteClosable program = entry.getKey();
- if (program != null && program instanceof SQLiteProgram) {
- SQLiteCompiledSql compiledSql = ((SQLiteProgram)program).mCompiledSql;
- if (compiledSql.nStatement == stmtId) {
- msg = compiledSql.toString();
- found = true;
- }
- }
- }
- if (!found) {
- // the statement is already released by closeClosable(). is it waiting to be
- // finalized?
- if (mClosedStatementIds.contains(stmtId)) {
- Log.w(TAG, "this shouldn't happen. finalizing the statement now: ");
- closePendingStatements();
- // try to close the database again
- closeDatabase();
- }
- } else {
- // the statement is not yet closed. most probably programming error in the app.
- throw new SQLiteUnfinalizedObjectsException(
- "close() on database: " + getPath() +
- " failed due to un-close()d SQL statements: " + msg);
- }
- }
- }
-
- /**
- * Native call to close the database.
- */
- private native void dbclose();
-
- /**
- * A callback interface for a custom sqlite3 function.
- * This can be used to create a function that can be called from
- * sqlite3 database triggers.
- * @hide
- */
- public interface CustomFunction {
- public void callback(String[] args);
+ dispose(false);
}
/**
* Registers a CustomFunction callback as a function that can be called from
- * sqlite3 database triggers.
+ * SQLite database triggers.
+ *
* @param name the name of the sqlite3 function
* @param numArgs the number of arguments for the function
* @param function callback to call when the function is executed
* @hide
*/
public void addCustomFunction(String name, int numArgs, CustomFunction function) {
- verifyDbIsOpen();
- synchronized (mCustomFunctions) {
- int ref = native_addCustomFunction(name, numArgs, function);
- if (ref != 0) {
- // save a reference to the function for cleanup later
- mCustomFunctions.add(new Integer(ref));
- } else {
- throw new SQLiteException("failed to add custom function " + name);
- }
- }
- }
+ // Create wrapper (also validates arguments).
+ SQLiteCustomFunction wrapper = new SQLiteCustomFunction(name, numArgs, function);
- private void releaseCustomFunctions() {
- synchronized (mCustomFunctions) {
- for (int i = 0; i < mCustomFunctions.size(); i++) {
- Integer function = mCustomFunctions.get(i);
- native_releaseCustomFunction(function.intValue());
- }
- mCustomFunctions.clear();
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ mConfigurationLocked.customFunctions.add(wrapper);
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
}
- // list of CustomFunction references so we can clean up when the database closes
- private final ArrayList<Integer> mCustomFunctions =
- new ArrayList<Integer>();
-
- private native int native_addCustomFunction(String name, int numArgs, CustomFunction function);
- private native void native_releaseCustomFunction(int function);
-
/**
* Gets the database version.
*
@@ -1364,7 +896,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* {@link SQLiteStatement}s are not synchronized, see the documentation for more details.
*/
public SQLiteStatement compileStatement(String sql) throws SQLException {
- verifyDbIsOpen();
+ throwIfNotOpen(); // fail fast
return new SQLiteStatement(this, sql, null);
}
@@ -1442,7 +974,7 @@ public class SQLiteDatabase extends SQLiteClosable {
boolean distinct, String table, String[] columns,
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit) {
- verifyDbIsOpen();
+ throwIfNotOpen(); // fail fast
String sql = SQLiteQueryBuilder.buildQueryString(
distinct, table, columns, selection, groupBy, having, orderBy, limit);
@@ -1553,21 +1085,11 @@ public class SQLiteDatabase extends SQLiteClosable {
public Cursor rawQueryWithFactory(
CursorFactory cursorFactory, String sql, String[] selectionArgs,
String editTable) {
- verifyDbIsOpen();
- BlockGuard.getThreadPolicy().onReadFromDisk();
+ throwIfNotOpen(); // fail fast
- SQLiteDatabase db = getDbConnection(sql);
- SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable);
-
- Cursor cursor = null;
- try {
- cursor = driver.query(
- cursorFactory != null ? cursorFactory : mFactory,
- selectionArgs);
- } finally {
- releaseDbConnection(db);
- }
- return cursor;
+ SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable);
+ return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory,
+ selectionArgs);
}
/**
@@ -1716,9 +1238,6 @@ public class SQLiteDatabase extends SQLiteClosable {
SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeInsert();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
@@ -1739,9 +1258,6 @@ public class SQLiteDatabase extends SQLiteClosable {
(!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
try {
return statement.executeUpdateDelete();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
@@ -1808,9 +1324,6 @@ public class SQLiteDatabase extends SQLiteClosable {
SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeUpdateDelete();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
@@ -1891,264 +1404,105 @@ public class SQLiteDatabase extends SQLiteClosable {
private int executeSql(String sql, Object[] bindArgs) throws SQLException {
if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) {
- disableWriteAheadLogging();
- mHasAttachedDbs = true;
+ boolean disableWal = false;
+ synchronized (mLock) {
+ if (!mHasAttachedDbsLocked) {
+ mHasAttachedDbsLocked = true;
+ disableWal = true;
+ }
+ }
+ if (disableWal) {
+ disableWriteAheadLogging();
+ }
}
+
SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs);
try {
return statement.executeUpdateDelete();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
}
- @Override
- protected void finalize() throws Throwable {
- try {
- if (isOpen()) {
- Log.e(TAG, "close() was never explicitly called on database '" +
- mPath + "' ", mStackTrace);
- closeClosable();
- onAllReferencesReleased();
- releaseCustomFunctions();
- }
- } finally {
- super.finalize();
- }
- }
-
/**
- * Private constructor.
+ * Returns true if the database is opened as read only.
*
- * @param path The full path to the database
- * @param factory The factory to use when creating cursors, may be NULL.
- * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already
- * exists, mFlags will be updated appropriately.
- * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database
- * corruption. may be NULL.
- * @param connectionNum 0 for main database connection handle. 1..N for pooled database
- * connection handles.
+ * @return True if database is opened as read only.
*/
- private SQLiteDatabase(String path, CursorFactory factory, int flags,
- DatabaseErrorHandler errorHandler, short connectionNum) {
- if (path == null) {
- throw new IllegalArgumentException("path should not be null");
+ public boolean isReadOnly() {
+ synchronized (mLock) {
+ return isReadOnlyLocked();
}
- setMaxSqlCacheSize(DEFAULT_SQL_CACHE_SIZE);
- mFlags = flags;
- mPath = path;
- mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
- mFactory = factory;
- mPrograms = new WeakHashMap<SQLiteClosable,Object>();
- // Set the DatabaseErrorHandler to be used when SQLite reports corruption.
- // If the caller sets errorHandler = null, then use default errorhandler.
- mErrorHandler = (errorHandler == null) ? new DefaultDatabaseErrorHandler() : errorHandler;
- mConnectionNum = connectionNum;
- /* sqlite soft heap limit http://www.sqlite.org/c3ref/soft_heap_limit64.html
- * set it to 4 times the default cursor window size.
- * TODO what is an appropriate value, considering the WAL feature which could burn
- * a lot of memory with many connections to the database. needs testing to figure out
- * optimal value for this.
- */
- int limit = Resources.getSystem().getInteger(
- com.android.internal.R.integer.config_cursorWindowSize) * 1024 * 4;
- native_setSqliteSoftHeapLimit(limit);
+ }
+
+ private boolean isReadOnlyLocked() {
+ return (mConfigurationLocked.openFlags & OPEN_READ_MASK) == OPEN_READONLY;
}
/**
- * return whether the DB is opened as read only.
- * @return true if DB is opened as read only
+ * Returns true if the database is in-memory db.
+ *
+ * @return True if the database is in-memory.
+ * @hide
*/
- public boolean isReadOnly() {
- return (mFlags & OPEN_READ_MASK) == OPEN_READONLY;
+ public boolean isInMemoryDatabase() {
+ synchronized (mLock) {
+ return mConfigurationLocked.isInMemoryDb();
+ }
}
/**
- * @return true if the DB is currently open (has not been closed)
+ * Returns true if the database is currently open.
+ *
+ * @return True if the database is currently open (has not been closed).
*/
public boolean isOpen() {
- return mNativeHandle != 0;
+ synchronized (mLock) {
+ return mConnectionPoolLocked != null;
+ }
}
+ /**
+ * Returns true if the new version code is greater than the current database version.
+ *
+ * @param newVersion The new version code.
+ * @return True if the new version code is greater than the current database version.
+ */
public boolean needUpgrade(int newVersion) {
return newVersion > getVersion();
}
/**
- * Getter for the path to the database file.
+ * Gets the path to the database file.
*
- * @return the path to our database file.
+ * @return The path to the database file.
*/
public final String getPath() {
- return mPath;
- }
-
- /* package */ void logTimeStat(String sql, long beginMillis) {
- if (ENABLE_DB_SAMPLE) {
- logTimeStat(sql, beginMillis, null);
+ synchronized (mLock) {
+ return mConfigurationLocked.path;
}
}
- private void logTimeStat(String sql, long beginMillis, String prefix) {
- // Sample fast queries in proportion to the time taken.
- // Quantize the % first, so the logged sampling probability
- // exactly equals the actual sampling rate for this query.
-
- int samplePercent;
- long durationMillis = SystemClock.uptimeMillis() - beginMillis;
- if (durationMillis == 0 && prefix == GET_LOCK_LOG_PREFIX) {
- // The common case is locks being uncontended. Don't log those,
- // even at 1%, which is our default below.
- return;
- }
- if (sQueryLogTimeInMillis == 0) {
- sQueryLogTimeInMillis = SystemProperties.getInt("db.db_operation.threshold_ms", 500);
- }
- if (durationMillis >= sQueryLogTimeInMillis) {
- samplePercent = 100;
- } else {
- samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1;
- if (mRandom.nextInt(100) >= samplePercent) return;
- }
-
- // Note: the prefix will be "COMMIT;" or "GETLOCK:" when non-null. We wait to do
- // it here so we avoid allocating in the common case.
- if (prefix != null) {
- sql = prefix + sql;
- }
- if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH);
-
- // ActivityThread.currentPackageName() only returns non-null if the
- // current thread is an application main thread. This parameter tells
- // us whether an event loop is blocked, and if so, which app it is.
- //
- // Sadly, there's no fast way to determine app name if this is *not* a
- // main thread, or when we are invoked via Binder (e.g. ContentProvider).
- // Hopefully the full path to the database will be informative enough.
-
- String blockingPackage = AppGlobals.getInitialPackage();
- if (blockingPackage == null) blockingPackage = "";
-
- EventLog.writeEvent(
- EVENT_DB_OPERATION,
- getPathForLogs(),
- sql,
- durationMillis,
- blockingPackage,
- samplePercent);
- }
-
- /**
- * Removes email addresses from database filenames before they're
- * logged to the EventLog where otherwise apps could potentially
- * read them.
- */
- private String getPathForLogs() {
- if (mPathForLogs != null) {
- return mPathForLogs;
- }
- if (mPath == null) {
- return null;
- }
- if (mPath.indexOf('@') == -1) {
- mPathForLogs = mPath;
- } else {
- mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY");
- }
- return mPathForLogs;
- }
-
/**
* Sets the locale for this database. Does nothing if this database has
* the NO_LOCALIZED_COLLATORS flag set or was opened read only.
+ *
+ * @param locale The new locale.
+ *
* @throws SQLException if the locale could not be set. The most common reason
* for this is that there is no collator available for the locale you requested.
* In this case the database remains unchanged.
*/
public void setLocale(Locale locale) {
- lock();
- try {
- native_setLocale(locale.toString(), mFlags);
- } finally {
- unlock();
- }
- }
-
- /* package */ void verifyDbIsOpen() {
- if (!isOpen()) {
- throw new IllegalStateException("database " + getPath() + " (conn# " +
- mConnectionNum + ") already closed");
- }
- }
-
- /* package */ void verifyLockOwner() {
- verifyDbIsOpen();
- if (mLockingEnabled && !isDbLockedByCurrentThread()) {
- throw new IllegalStateException("Don't have database lock!");
- }
- }
-
- /**
- * Adds the given SQL and its compiled-statement-id-returned-by-sqlite to the
- * cache of compiledQueries attached to 'this'.
- * <p>
- * If there is already a {@link SQLiteCompiledSql} in compiledQueries for the given SQL,
- * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current
- * mapping is NOT replaced with the new mapping).
- */
- /* package */ synchronized void addToCompiledQueries(
- String sql, SQLiteCompiledSql compiledStatement) {
- // don't insert the new mapping if a mapping already exists
- if (mCompiledQueries.get(sql) != null) {
- return;
+ if (locale == null) {
+ throw new IllegalArgumentException("locale must not be null.");
}
- int maxCacheSz = (mConnectionNum == 0) ? mCompiledQueries.maxSize() :
- mParentConnObj.mCompiledQueries.maxSize();
-
- if (SQLiteDebug.DEBUG_SQL_CACHE) {
- boolean printWarning = (mConnectionNum == 0)
- ? (!mCacheFullWarning && mCompiledQueries.size() == maxCacheSz)
- : (!mParentConnObj.mCacheFullWarning &&
- mParentConnObj.mCompiledQueries.size() == maxCacheSz);
- if (printWarning) {
- /*
- * cache size is not enough for this app. log a warning.
- * chances are it is NOT using ? for bindargs - or cachesize is too small.
- */
- Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " +
- getPath() + ". Use setMaxSqlCacheSize() to increase cachesize. ");
- mCacheFullWarning = true;
- Log.d(TAG, "Here are the SQL statements in Cache of database: " + mPath);
- for (String s : mCompiledQueries.snapshot().keySet()) {
- Log.d(TAG, "Sql statement in Cache: " + s);
- }
- }
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ mConfigurationLocked.locale = locale;
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
- /* add the given SQLiteCompiledSql compiledStatement to cache.
- * no need to worry about the cache size - because {@link #mCompiledQueries}
- * self-limits its size.
- */
- mCompiledQueries.put(sql, compiledStatement);
- }
-
- /** package-level access for testing purposes */
- /* package */ synchronized void deallocCachedSqlStatements() {
- for (SQLiteCompiledSql compiledSql : mCompiledQueries.snapshot().values()) {
- compiledSql.releaseSqlStatement();
- }
- mCompiledQueries.evictAll();
- }
-
- /**
- * From the compiledQueries cache, returns the compiled-statement-id for the given SQL.
- * Returns null, if not found in the cache.
- */
- /* package */ synchronized SQLiteCompiledSql getCompiledStatementForSql(String sql) {
- return mCompiledQueries.get(sql);
}
/**
@@ -2162,116 +1516,22 @@ public class SQLiteDatabase extends SQLiteClosable {
* This method is thread-safe.
*
* @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE})
- * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE} or
- * the value set with previous setMaxSqlCacheSize() call.
+ * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE}.
*/
public void setMaxSqlCacheSize(int cacheSize) {
- synchronized (this) {
- LruCache<String, SQLiteCompiledSql> oldCompiledQueries = mCompiledQueries;
- if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
- throw new IllegalStateException(
- "expected value between 0 and " + MAX_SQL_CACHE_SIZE);
- } else if (oldCompiledQueries != null && cacheSize < oldCompiledQueries.maxSize()) {
- throw new IllegalStateException("cannot set cacheSize to a value less than the "
- + "value set with previous setMaxSqlCacheSize() call.");
- }
- mCompiledQueries = new LruCache<String, SQLiteCompiledSql>(cacheSize) {
- @Override
- protected void entryRemoved(boolean evicted, String key, SQLiteCompiledSql oldValue,
- SQLiteCompiledSql newValue) {
- verifyLockOwner();
- oldValue.releaseIfNotInUse();
- }
- };
- if (oldCompiledQueries != null) {
- for (Map.Entry<String, SQLiteCompiledSql> entry
- : oldCompiledQueries.snapshot().entrySet()) {
- mCompiledQueries.put(entry.getKey(), entry.getValue());
- }
- }
- }
- }
-
- /* package */ synchronized boolean isInStatementCache(String sql) {
- return mCompiledQueries.get(sql) != null;
- }
-
- /* package */ synchronized void releaseCompiledSqlObj(
- String sql, SQLiteCompiledSql compiledSql) {
- if (mCompiledQueries.get(sql) == compiledSql) {
- // it is in cache - reset its inUse flag
- compiledSql.release();
- } else {
- // it is NOT in cache. finalize it.
- compiledSql.releaseSqlStatement();
- }
- }
-
- private synchronized int getCacheHitNum() {
- return mCompiledQueries.hitCount();
- }
-
- private synchronized int getCacheMissNum() {
- return mCompiledQueries.missCount();
- }
-
- private synchronized int getCachesize() {
- return mCompiledQueries.size();
- }
-
- /* package */ void finalizeStatementLater(int id) {
- if (!isOpen()) {
- // database already closed. this statement will already have been finalized.
- return;
- }
- synchronized(mClosedStatementIds) {
- if (mClosedStatementIds.contains(id)) {
- // this statement id is already queued up for finalization.
- return;
- }
- mClosedStatementIds.add(id);
- }
- }
-
- /* package */ boolean isInQueueOfStatementsToBeFinalized(int id) {
- if (!isOpen()) {
- // database already closed. this statement will already have been finalized.
- // return true so that the caller doesn't have to worry about finalizing this statement.
- return true;
- }
- synchronized(mClosedStatementIds) {
- return mClosedStatementIds.contains(id);
+ if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
+ throw new IllegalStateException(
+ "expected value between 0 and " + MAX_SQL_CACHE_SIZE);
}
- }
- /* package */ void closePendingStatements() {
- if (!isOpen()) {
- // since this database is already closed, no need to finalize anything.
- mClosedStatementIds.clear();
- return;
- }
- verifyLockOwner();
- /* to minimize synchronization on mClosedStatementIds, make a copy of the list */
- ArrayList<Integer> list = new ArrayList<Integer>(mClosedStatementIds.size());
- synchronized(mClosedStatementIds) {
- list.addAll(mClosedStatementIds);
- mClosedStatementIds.clear();
- }
- // finalize all the statements from the copied list
- int size = list.size();
- for (int i = 0; i < size; i++) {
- native_finalize(list.get(i));
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ mConfigurationLocked.maxSqlCacheSize = cacheSize;
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
}
/**
- * for testing only
- */
- /* package */ ArrayList<Integer> getQueuedUpStmtList() {
- return mClosedStatementIds;
- }
-
- /**
* This method enables parallel execution of queries from multiple threads on the same database.
* It does this by opening multiple handles to the database and using a different
* database handle for each query.
@@ -2314,37 +1574,43 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return true if write-ahead-logging is set. false otherwise
*/
public boolean enableWriteAheadLogging() {
- // make sure the database is not READONLY. WAL doesn't make sense for readonly-databases.
- if (isReadOnly()) {
- return false;
- }
- // acquire lock - no that no other thread is enabling WAL at the same time
- lock();
- try {
- if (mConnectionPool != null) {
- // already enabled
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+
+ if (mIsWALEnabledLocked) {
return true;
}
- if (mPath.equalsIgnoreCase(MEMORY_DB_PATH)) {
+
+ if (isReadOnlyLocked()) {
+ // WAL doesn't make sense for readonly-databases.
+ // TODO: True, but connection pooling does still make sense...
+ return false;
+ }
+
+ if (mConfigurationLocked.isInMemoryDb()) {
Log.i(TAG, "can't enable WAL for memory databases.");
return false;
}
// make sure this database has NO attached databases because sqlite's write-ahead-logging
// doesn't work for databases with attached databases
- if (mHasAttachedDbs) {
+ if (mHasAttachedDbsLocked) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG,
- "this database: " + mPath + " has attached databases. can't enable WAL.");
+ Log.d(TAG, "this database: " + mConfigurationLocked.label
+ + " has attached databases. can't enable WAL.");
}
return false;
}
- mConnectionPool = new DatabaseConnectionPool(this);
- setJournalMode(mPath, "WAL");
- return true;
- } finally {
- unlock();
+
+ mIsWALEnabledLocked = true;
+ mConfigurationLocked.maxConnectionPoolSize = Math.max(2,
+ Resources.getSystem().getInteger(
+ com.android.internal.R.integer.db_connection_pool_size));
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
+
+ setJournalMode("WAL");
+ return true;
}
/**
@@ -2352,176 +1618,66 @@ public class SQLiteDatabase extends SQLiteClosable {
* @hide
*/
public void disableWriteAheadLogging() {
- // grab database lock so that writeAheadLogging is not disabled from 2 different threads
- // at the same time
- lock();
- try {
- if (mConnectionPool == null) {
- return; // already disabled
- }
- mConnectionPool.close();
- setJournalMode(mPath, "TRUNCATE");
- mConnectionPool = null;
- } finally {
- unlock();
- }
- }
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
- /* package */ SQLiteDatabase getDatabaseHandle(String sql) {
- if (isPooledConnection()) {
- // this is a pooled database connection
- // use it if it is open AND if I am not currently part of a transaction
- if (isOpen() && !amIInTransaction()) {
- // TODO: use another connection from the pool
- // if this connection is currently in use by some other thread
- // AND if there are free connections in the pool
- return this;
- } else {
- // the pooled connection is not open! could have been closed either due
- // to corruption on this or some other connection to the database
- // OR, maybe the connection pool is disabled after this connection has been
- // allocated to me. try to get some other pooled or main database connection
- return getParentDbConnObj().getDbConnection(sql);
+ if (!mIsWALEnabledLocked) {
+ return;
}
- } else {
- // this is NOT a pooled connection. can we get one?
- return getDbConnection(sql);
- }
- }
- /* package */ SQLiteDatabase createPoolConnection(short connectionNum) {
- SQLiteDatabase db = openDatabase(mPath, mFactory, mFlags, mErrorHandler, connectionNum);
- db.mParentConnObj = this;
- return db;
- }
-
- private synchronized SQLiteDatabase getParentDbConnObj() {
- return mParentConnObj;
- }
+ mIsWALEnabledLocked = false;
+ mConfigurationLocked.maxConnectionPoolSize = 1;
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+ }
- private boolean isPooledConnection() {
- return this.mConnectionNum > 0;
+ setJournalMode("TRUNCATE");
}
- /* package */ SQLiteDatabase getDbConnection(String sql) {
- verifyDbIsOpen();
- // this method should always be called with main database connection handle.
- // the only time when it is called with pooled database connection handle is
- // corruption occurs while trying to open a pooled database connection handle.
- // in that case, simply return 'this' handle
- if (isPooledConnection()) {
- return this;
+ /**
+ * Collect statistics about all open databases in the current process.
+ * Used by bug report.
+ */
+ static ArrayList<DbStats> getDbStats() {
+ ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>();
+ for (SQLiteDatabase db : getActiveDatabases()) {
+ db.collectDbStats(dbStatsList);
}
+ return dbStatsList;
+ }
- // use the current connection handle if
- // 1. if the caller is part of the ongoing transaction, if any
- // 2. OR, if there is NO connection handle pool setup
- if (amIInTransaction() || mConnectionPool == null) {
- return this;
- } else {
- // get a connection handle from the pool
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert mConnectionPool != null;
- Log.i(TAG, mConnectionPool.toString());
+ private void collectDbStats(ArrayList<DbStats> dbStatsList) {
+ synchronized (mLock) {
+ if (mConnectionPoolLocked != null) {
+ mConnectionPoolLocked.collectDbStats(dbStatsList);
}
- return mConnectionPool.get(sql);
}
}
- private void releaseDbConnection(SQLiteDatabase db) {
- // ignore this release call if
- // 1. the database is closed
- // 2. OR, if db is NOT a pooled connection handle
- // 3. OR, if the database being released is same as 'this' (this condition means
- // that we should always be releasing a pooled connection handle by calling this method
- // from the 'main' connection handle
- if (!isOpen() || !db.isPooledConnection() || (db == this)) {
- return;
+ private static ArrayList<SQLiteDatabase> getActiveDatabases() {
+ ArrayList<SQLiteDatabase> databases = new ArrayList<SQLiteDatabase>();
+ synchronized (sActiveDatabases) {
+ databases.addAll(sActiveDatabases.keySet());
}
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert isPooledConnection();
- assert mConnectionPool != null;
- Log.d(TAG, "releaseDbConnection threadid = " + Thread.currentThread().getId() +
- ", releasing # " + db.mConnectionNum + ", " + getPath());
- }
- mConnectionPool.release(db);
+ return databases;
}
/**
- * this method is used to collect data about ALL open databases in the current process.
- * bugreport is a user of this data.
+ * Dump detailed information about all open databases in the current process.
+ * Used by bug report.
*/
- /* package */ static ArrayList<DbStats> getDbStats() {
- ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>();
- // make a local copy of mActiveDatabases - so that this method is not competing
- // for synchronization lock on mActiveDatabases
- ArrayList<WeakReference<SQLiteDatabase>> tempList;
- synchronized(mActiveDatabases) {
- tempList = (ArrayList<WeakReference<SQLiteDatabase>>)mActiveDatabases.clone();
+ static void dumpAll(Printer printer) {
+ for (SQLiteDatabase db : getActiveDatabases()) {
+ db.dump(printer);
}
- for (WeakReference<SQLiteDatabase> w : tempList) {
- SQLiteDatabase db = w.get();
- if (db == null || !db.isOpen()) {
- continue;
- }
+ }
- try {
- // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db
- int lookasideUsed = db.native_getDbLookaside();
-
- // get the lastnode of the dbname
- String path = db.getPath();
- int indx = path.lastIndexOf("/");
- String lastnode = path.substring((indx != -1) ? ++indx : 0);
-
- // get list of attached dbs and for each db, get its size and pagesize
- List<Pair<String, String>> attachedDbs = db.getAttachedDbs();
- if (attachedDbs == null) {
- continue;
- }
- for (int i = 0; i < attachedDbs.size(); i++) {
- Pair<String, String> p = attachedDbs.get(i);
- long pageCount = DatabaseUtils.longForQuery(db, "PRAGMA " + p.first
- + ".page_count;", null);
-
- // first entry in the attached db list is always the main database
- // don't worry about prefixing the dbname with "main"
- String dbName;
- if (i == 0) {
- dbName = lastnode;
- } else {
- // lookaside is only relevant for the main db
- lookasideUsed = 0;
- dbName = " (attached) " + p.first;
- // if the attached db has a path, attach the lastnode from the path to above
- if (p.second.trim().length() > 0) {
- int idx = p.second.lastIndexOf("/");
- dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0);
- }
- }
- if (pageCount > 0) {
- dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(),
- lookasideUsed, db.getCacheHitNum(), db.getCacheMissNum(),
- db.getCachesize()));
- }
- }
- // if there are pooled connections, return the cache stats for them also.
- // while we are trying to query the pooled connections for stats, some other thread
- // could be disabling conneciton pool. so, grab a reference to the connection pool.
- DatabaseConnectionPool connPool = db.mConnectionPool;
- if (connPool != null) {
- for (SQLiteDatabase pDb : connPool.getConnectionList()) {
- dbStatsList.add(new DbStats("(pooled # " + pDb.mConnectionNum + ") "
- + lastnode, 0, 0, 0, pDb.getCacheHitNum(),
- pDb.getCacheMissNum(), pDb.getCachesize()));
- }
- }
- } catch (SQLiteException e) {
- // ignore. we don't care about exceptions when we are taking adb
- // bugreport!
+ private void dump(Printer printer) {
+ synchronized (mLock) {
+ if (mConnectionPoolLocked != null) {
+ printer.println("");
+ mConnectionPoolLocked.dump(printer);
}
}
- return dbStatsList;
}
/**
@@ -2532,23 +1688,27 @@ public class SQLiteDatabase extends SQLiteClosable {
* is not open.
*/
public List<Pair<String, String>> getAttachedDbs() {
- if (!isOpen()) {
- return null;
- }
ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>();
- if (!mHasAttachedDbs) {
- // No attached databases.
- // There is a small window where attached databases exist but this flag is not set yet.
- // This can occur when this thread is in a race condition with another thread
- // that is executing the SQL statement: "attach database <blah> as <foo>"
- // If this thread is NOT ok with such a race condition (and thus possibly not receive
- // the entire list of attached databases), then the caller should ensure that no thread
- // is executing any SQL statements while a thread is calling this method.
- // Typically, this method is called when 'adb bugreport' is done or the caller wants to
- // collect stats on the database and all its attached databases.
- attachedDbs.add(new Pair<String, String>("main", mPath));
- return attachedDbs;
+ synchronized (mLock) {
+ if (mConnectionPoolLocked == null) {
+ return null; // not open
+ }
+
+ if (!mHasAttachedDbsLocked) {
+ // No attached databases.
+ // There is a small window where attached databases exist but this flag is not
+ // set yet. This can occur when this thread is in a race condition with another
+ // thread that is executing the SQL statement: "attach database <blah> as <foo>"
+ // If this thread is NOT ok with such a race condition (and thus possibly not
+ // receivethe entire list of attached databases), then the caller should ensure
+ // that no thread is executing any SQL statements while a thread is calling this
+ // method. Typically, this method is called when 'adb bugreport' is done or the
+ // caller wants to collect stats on the database and all its attached databases.
+ attachedDbs.add(new Pair<String, String>("main", mConfigurationLocked.path));
+ return attachedDbs;
+ }
}
+
// has attached databases. query sqlite to get the list of attached databases.
Cursor c = null;
try {
@@ -2583,7 +1743,8 @@ public class SQLiteDatabase extends SQLiteClosable {
* false otherwise.
*/
public boolean isDatabaseIntegrityOk() {
- verifyDbIsOpen();
+ throwIfNotOpen(); // fail fast
+
List<Pair<String, String>> attachedDbs = null;
try {
attachedDbs = getAttachedDbs();
@@ -2594,8 +1755,9 @@ public class SQLiteDatabase extends SQLiteClosable {
} catch (SQLiteException e) {
// can't get attachedDb list. do integrity check on the main database
attachedDbs = new ArrayList<Pair<String, String>>();
- attachedDbs.add(new Pair<String, String>("main", this.mPath));
+ attachedDbs.add(new Pair<String, String>("main", getPath()));
}
+
for (int i = 0; i < attachedDbs.size(); i++) {
Pair<String, String> p = attachedDbs.get(i);
SQLiteStatement prog = null;
@@ -2615,59 +1777,64 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * Native call to open the database.
+ * Prevent other threads from using the database's primary connection.
*
- * @param path The full path to the database
- */
- private native void dbopen(String path, int flags);
-
- /**
- * Native call to setup tracing of all SQL statements
+ * This method is only used by {@link SQLiteOpenHelper} when transitioning from
+ * a readable to a writable database. It should not be used in any other way.
*
- * @param path the full path to the database
- * @param connectionNum connection number: 0 - N, where the main database
- * connection handle is numbered 0 and the connection handles in the connection
- * pool are numbered 1..N.
+ * @see #unlockPrimaryConnection()
*/
- private native void enableSqlTracing(String path, short connectionNum);
+ void lockPrimaryConnection() {
+ getThreadSession().beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED,
+ null, SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY);
+ }
/**
- * Native call to setup profiling of all SQL statements.
- * currently, sqlite's profiling = printing of execution-time
- * (wall-clock time) of each of the SQL statements, as they
- * are executed.
+ * Allow other threads to use the database's primary connection.
*
- * @param path the full path to the database
- * @param connectionNum connection number: 0 - N, where the main database
- * connection handle is numbered 0 and the connection handles in the connection
- * pool are numbered 1..N.
+ * @see #lockPrimaryConnection()
*/
- private native void enableSqlProfiling(String path, short connectionNum);
+ void unlockPrimaryConnection() {
+ getThreadSession().endTransaction();
+ }
- /**
- * Native call to set the locale. {@link #lock} must be held when calling
- * this method.
- * @throws SQLException
- */
- private native void native_setLocale(String loc, int flags);
+ @Override
+ public String toString() {
+ return "SQLiteDatabase: " + getPath();
+ }
- /**
- * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here
- * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html
- * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED
- */
- private native int native_getDbLookaside();
+ private void throwIfNotOpen() {
+ synchronized (mConnectionPoolLocked) {
+ throwIfNotOpenLocked();
+ }
+ }
+
+ private void throwIfNotOpenLocked() {
+ if (mConnectionPoolLocked == null) {
+ throw new IllegalStateException("The database '" + mConfigurationLocked.label
+ + "' is not open.");
+ }
+ }
/**
- * finalizes the given statement id.
- *
- * @param statementId statement to be finzlied by sqlite
+ * Used to allow returning sub-classes of {@link Cursor} when calling query.
*/
- private final native void native_finalize(int statementId);
+ public interface CursorFactory {
+ /**
+ * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}.
+ */
+ public Cursor newCursor(SQLiteDatabase db,
+ SQLiteCursorDriver masterQuery, String editTable,
+ SQLiteQuery query);
+ }
/**
- * set sqlite soft heap limit
- * http://www.sqlite.org/c3ref/soft_heap_limit64.html
+ * A callback interface for a custom sqlite3 function.
+ * This can be used to create a function that can be called from
+ * sqlite3 database triggers.
+ * @hide
*/
- private native void native_setSqliteSoftHeapLimit(int softHeapLimit);
+ public interface CustomFunction {
+ public void callback(String[] args);
+ }
}
diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
new file mode 100644
index 0000000..bc79ad3
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Describes how to configure a database.
+ * <p>
+ * The purpose of this object is to keep track of all of the little
+ * configuration settings that are applied to a database after it
+ * is opened so that they can be applied to all connections in the
+ * connection pool uniformly.
+ * </p><p>
+ * Each connection maintains its own copy of this object so it can
+ * keep track of which settings have already been applied.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteDatabaseConfiguration {
+ // The pattern we use to strip email addresses from database paths
+ // when constructing a label to use in log messages.
+ private static final Pattern EMAIL_IN_DB_PATTERN =
+ Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+");
+
+ /**
+ * Special path used by in-memory databases.
+ */
+ public static final String MEMORY_DB_PATH = ":memory:";
+
+ /**
+ * The database path.
+ */
+ public final String path;
+
+ /**
+ * The flags used to open the database.
+ */
+ public final int openFlags;
+
+ /**
+ * The label to use to describe the database when it appears in logs.
+ * This is derived from the path but is stripped to remove PII.
+ */
+ public final String label;
+
+ /**
+ * The maximum number of connections to retain in the connection pool.
+ * Must be at least 1.
+ *
+ * Default is 1.
+ */
+ public int maxConnectionPoolSize;
+
+ /**
+ * The maximum size of the prepared statement cache for each database connection.
+ * Must be non-negative.
+ *
+ * Default is 25.
+ */
+ public int maxSqlCacheSize;
+
+ /**
+ * The database locale.
+ *
+ * Default is the value returned by {@link Locale#getDefault()}.
+ */
+ public Locale locale;
+
+ /**
+ * The custom functions to register.
+ */
+ public final ArrayList<SQLiteCustomFunction> customFunctions =
+ new ArrayList<SQLiteCustomFunction>();
+
+ /**
+ * Creates a database configuration with the required parameters for opening a
+ * database and default values for all other parameters.
+ *
+ * @param path The database path.
+ * @param openFlags Open flags for the database, such as {@link SQLiteDatabase#OPEN_READWRITE}.
+ */
+ public SQLiteDatabaseConfiguration(String path, int openFlags) {
+ if (path == null) {
+ throw new IllegalArgumentException("path must not be null.");
+ }
+
+ this.path = path;
+ this.openFlags = openFlags;
+ label = stripPathForLogs(path);
+
+ // Set default values for optional parameters.
+ maxConnectionPoolSize = 1;
+ maxSqlCacheSize = 25;
+ locale = Locale.getDefault();
+ }
+
+ /**
+ * Creates a database configuration as a copy of another configuration.
+ *
+ * @param other The other configuration.
+ */
+ public SQLiteDatabaseConfiguration(SQLiteDatabaseConfiguration other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other must not be null.");
+ }
+
+ this.path = other.path;
+ this.openFlags = other.openFlags;
+ this.label = other.label;
+ updateParametersFrom(other);
+ }
+
+ /**
+ * Updates the non-immutable parameters of this configuration object
+ * from the other configuration object.
+ *
+ * @param other The object from which to copy the parameters.
+ */
+ public void updateParametersFrom(SQLiteDatabaseConfiguration other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other must not be null.");
+ }
+ if (!path.equals(other.path) || openFlags != other.openFlags) {
+ throw new IllegalArgumentException("other configuration must refer to "
+ + "the same database.");
+ }
+
+ maxConnectionPoolSize = other.maxConnectionPoolSize;
+ maxSqlCacheSize = other.maxSqlCacheSize;
+ locale = other.locale;
+ customFunctions.clear();
+ customFunctions.addAll(other.customFunctions);
+ }
+
+ /**
+ * Returns true if the database is in-memory.
+ * @return True if the database is in-memory.
+ */
+ public boolean isInMemoryDb() {
+ return path.equalsIgnoreCase(MEMORY_DB_PATH);
+ }
+
+ private static String stripPathForLogs(String path) {
+ if (path.indexOf('@') == -1) {
+ return path;
+ }
+ return EMAIL_IN_DB_PATTERN.matcher(path).replaceAll("XX@YY");
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
index 029bb4a..d87c3e4 100644
--- a/core/java/android/database/sqlite/SQLiteDebug.java
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -30,6 +30,12 @@ import android.util.Printer;
*/
public final class SQLiteDebug {
/**
+ * Controls the printing of informational SQL log messages.
+ */
+ public static final boolean DEBUG_SQL_LOG =
+ Log.isLoggable("SQLiteLog", Log.VERBOSE);
+
+ /**
* Controls the printing of SQL statements as they are executed.
*/
public static final boolean DEBUG_SQL_STATEMENTS =
@@ -186,6 +192,7 @@ public final class SQLiteDebug {
* @param printer The printer for dumping database state.
*/
public static void dump(Printer printer, String[] args) {
+ SQLiteDatabase.dumpAll(printer);
}
/**
diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
index a5e762e..52fd1d2 100644
--- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
+++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
@@ -25,10 +25,9 @@ import android.database.sqlite.SQLiteDatabase.CursorFactory;
* @hide
*/
public class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
- private String mEditTable;
- private SQLiteDatabase mDatabase;
- private Cursor mCursor;
- private String mSql;
+ private final SQLiteDatabase mDatabase;
+ private final String mEditTable;
+ private final String mSql;
private SQLiteQuery mQuery;
public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable) {
@@ -38,33 +37,27 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
}
public Cursor query(CursorFactory factory, String[] selectionArgs) {
- // Compile the query
- SQLiteQuery query = null;
-
+ final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql);
+ final Cursor cursor;
try {
- mDatabase.lock(mSql);
- mDatabase.closePendingStatements();
- query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs);
+ query.bindAllArgsAsStrings(selectionArgs);
- // Create the cursor
if (factory == null) {
- mCursor = new SQLiteCursor(this, mEditTable, query);
+ cursor = new SQLiteCursor(this, mEditTable, query);
} else {
- mCursor = factory.newCursor(mDatabase, this, mEditTable, query);
+ cursor = factory.newCursor(mDatabase, this, mEditTable, query);
}
-
- mQuery = query;
- query = null;
- return mCursor;
- } finally {
- // Make sure this object is cleaned up if something happens
- if (query != null) query.close();
- mDatabase.unlock();
+ } catch (RuntimeException ex) {
+ query.close();
+ throw ex;
}
+
+ mQuery = query;
+ return cursor;
}
public void cursorClosed() {
- mCursor = null;
+ // Do nothing
}
public void setBindArguments(String[] bindArgs) {
diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java
new file mode 100644
index 0000000..5e129be
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteGlobal.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.os.StatFs;
+
+/**
+ * Provides access to SQLite functions that affect all database connection,
+ * such as memory management.
+ *
+ * @hide
+ */
+public final class SQLiteGlobal {
+ private static final String TAG = "SQLiteGlobal";
+
+ private static final Object sLock = new Object();
+ private static boolean sInitialized;
+ private static int sSoftHeapLimit;
+ private static int sDefaultPageSize;
+
+ private static native void nativeConfig(boolean verboseLog, int softHeapLimit);
+ private static native int nativeReleaseMemory(int bytesToFree);
+
+ private SQLiteGlobal() {
+ }
+
+ /**
+ * Initializes global SQLite settings the first time it is called.
+ * Should be called before opening the first (or any) database.
+ * Does nothing on repeated subsequent calls.
+ */
+ public static void initializeOnce() {
+ synchronized (sLock) {
+ if (!sInitialized) {
+ sInitialized = true;
+
+ // Limit to 8MB for now. This is 4 times the maximum cursor window
+ // size, as has been used by the original code in SQLiteDatabase for
+ // a long time.
+ // TODO: We really do need to test whether this helps or hurts us.
+ sSoftHeapLimit = 8 * 1024 * 1024;
+
+ // Configure SQLite.
+ nativeConfig(SQLiteDebug.DEBUG_SQL_LOG, sSoftHeapLimit);
+ }
+ }
+ }
+
+ /**
+ * Attempts to release memory by pruning the SQLite page cache and other
+ * internal data structures.
+ *
+ * @return The number of bytes that were freed.
+ */
+ public static int releaseMemory() {
+ synchronized (sLock) {
+ if (!sInitialized) {
+ return 0;
+ }
+ return nativeReleaseMemory(sSoftHeapLimit);
+ }
+ }
+
+ /**
+ * Gets the default page size to use when creating a database.
+ */
+ public static int getDefaultPageSize() {
+ synchronized (sLock) {
+ if (sDefaultPageSize == 0) {
+ sDefaultPageSize = new StatFs("/data").getBlockSize();
+ }
+ return sDefaultPageSize;
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java
index 56cf948..31da7e4 100644
--- a/core/java/android/database/sqlite/SQLiteOpenHelper.java
+++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java
@@ -143,12 +143,14 @@ public abstract class SQLiteOpenHelper {
// If we have a read-only database open, someone could be using it
// (though they shouldn't), which would cause a lock to be held on
// the file, and our attempts to open the database read-write would
- // fail waiting for the file lock. To prevent that, we acquire the
- // lock on the read-only database, which shuts out other users.
+ // fail waiting for the file lock. To prevent that, we acquire a lock
+ // on the read-only database, which shuts out other users.
boolean success = false;
SQLiteDatabase db = null;
- if (mDatabase != null) mDatabase.lock();
+ if (mDatabase != null) {
+ mDatabase.lockPrimaryConnection();
+ }
try {
mIsInitializing = true;
if (mName == null) {
@@ -185,11 +187,13 @@ public abstract class SQLiteOpenHelper {
if (success) {
if (mDatabase != null) {
try { mDatabase.close(); } catch (Exception e) { }
- mDatabase.unlock();
+ mDatabase.unlockPrimaryConnection();
}
mDatabase = db;
} else {
- if (mDatabase != null) mDatabase.unlock();
+ if (mDatabase != null) {
+ mDatabase.unlockPrimaryConnection();
+ }
if (db != null) db.close();
}
}
diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java
index 2bbc6d7..8194458 100644
--- a/core/java/android/database/sqlite/SQLiteProgram.java
+++ b/core/java/android/database/sqlite/SQLiteProgram.java
@@ -17,225 +17,104 @@
package android.database.sqlite;
import android.database.DatabaseUtils;
-import android.database.Cursor;
-import java.util.HashMap;
+import java.util.Arrays;
/**
* A base class for compiled SQLite programs.
- *<p>
- * SQLiteProgram is NOT internally synchronized so code using a SQLiteProgram from multiple
- * threads should perform its own synchronization when using the SQLiteProgram.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
*/
public abstract class SQLiteProgram extends SQLiteClosable {
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
- private static final String TAG = "SQLiteProgram";
+ private final SQLiteDatabase mDatabase;
+ private final String mSql;
+ private final boolean mReadOnly;
+ private final String[] mColumnNames;
+ private final int mNumParameters;
+ private final Object[] mBindArgs;
- /** The database this program is compiled against.
- * @hide
- */
- protected SQLiteDatabase mDatabase;
-
- /** The SQL used to create this query */
- /* package */ final String mSql;
-
- /**
- * Native linkage, do not modify. This comes from the database and should not be modified
- * in here or in the native code.
- * @hide
- */
- protected int nHandle;
-
- /**
- * the SQLiteCompiledSql object for the given sql statement.
- */
- /* package */ SQLiteCompiledSql mCompiledSql;
-
- /**
- * SQLiteCompiledSql statement id is populated with the corresponding object from the above
- * member. This member is used by the native_bind_* methods
- * @hide
- */
- protected int nStatement;
-
- /**
- * In the case of {@link SQLiteStatement}, this member stores the bindargs passed
- * to the following methods, instead of actually doing the binding.
- * <ul>
- * <li>{@link #bindBlob(int, byte[])}</li>
- * <li>{@link #bindDouble(int, double)}</li>
- * <li>{@link #bindLong(int, long)}</li>
- * <li>{@link #bindNull(int)}</li>
- * <li>{@link #bindString(int, String)}</li>
- * </ul>
- * <p>
- * Each entry in the array is a Pair of
- * <ol>
- * <li>bind arg position number</li>
- * <li>the value to be bound to the bindarg</li>
- * </ol>
- * <p>
- * It is lazily initialized in the above bind methods
- * and it is cleared in {@link #clearBindings()} method.
- * <p>
- * It is protected (in multi-threaded environment) by {@link SQLiteProgram}.this
- */
- /* package */ HashMap<Integer, Object> mBindArgs = null;
- /* package */ final int mStatementType;
- /* package */ static final int STATEMENT_CACHEABLE = 16;
- /* package */ static final int STATEMENT_DONT_PREPARE = 32;
- /* package */ static final int STATEMENT_USE_POOLED_CONN = 64;
- /* package */ static final int STATEMENT_TYPE_MASK = 0x0f;
-
- /* package */ SQLiteProgram(SQLiteDatabase db, String sql) {
- this(db, sql, null, true);
- }
-
- /* package */ SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs,
- boolean compileFlag) {
+ SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs) {
+ mDatabase = db;
mSql = sql.trim();
+
int n = DatabaseUtils.getSqlStatementType(mSql);
switch (n) {
- case DatabaseUtils.STATEMENT_UPDATE:
- mStatementType = n | STATEMENT_CACHEABLE;
- break;
- case DatabaseUtils.STATEMENT_SELECT:
- mStatementType = n | STATEMENT_CACHEABLE | STATEMENT_USE_POOLED_CONN;
- break;
case DatabaseUtils.STATEMENT_BEGIN:
case DatabaseUtils.STATEMENT_COMMIT:
case DatabaseUtils.STATEMENT_ABORT:
- mStatementType = n | STATEMENT_DONT_PREPARE;
+ mReadOnly = false;
+ mColumnNames = EMPTY_STRING_ARRAY;
+ mNumParameters = 0;
break;
+
default:
- mStatementType = n;
- }
- db.acquireReference();
- db.addSQLiteClosable(this);
- mDatabase = db;
- nHandle = db.mNativeHandle;
- if (bindArgs != null) {
- int size = bindArgs.length;
- for (int i = 0; i < size; i++) {
- this.addToBindArgs(i + 1, bindArgs[i]);
- }
- }
- if (compileFlag) {
- compileAndbindAllArgs();
+ boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT);
+ SQLiteStatementInfo info = new SQLiteStatementInfo();
+ db.getThreadSession().prepare(mSql,
+ db.getThreadDefaultConnectionFlags(assumeReadOnly), info);
+ mReadOnly = info.readOnly;
+ mColumnNames = info.columnNames;
+ mNumParameters = info.numParameters;
+ break;
}
- }
- private void compileSql() {
- // only cache CRUD statements
- if ((mStatementType & STATEMENT_CACHEABLE) == 0) {
- mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
- nStatement = mCompiledSql.nStatement;
- // since it is not in the cache, no need to acquire() it.
- return;
+ if (mNumParameters != 0) {
+ mBindArgs = new Object[mNumParameters];
+ } else {
+ mBindArgs = null;
}
- mCompiledSql = mDatabase.getCompiledStatementForSql(mSql);
- if (mCompiledSql == null) {
- // create a new compiled-sql obj
- mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
-
- // add it to the cache of compiled-sqls
- // but before adding it and thus making it available for anyone else to use it,
- // make sure it is acquired by me.
- mCompiledSql.acquire();
- mDatabase.addToCompiledQueries(mSql, mCompiledSql);
- } else {
- // it is already in compiled-sql cache.
- // try to acquire the object.
- if (!mCompiledSql.acquire()) {
- int last = mCompiledSql.nStatement;
- // the SQLiteCompiledSql in cache is in use by some other SQLiteProgram object.
- // we can't have two different SQLiteProgam objects can't share the same
- // CompiledSql object. create a new one.
- // finalize it when I am done with it in "this" object.
- mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
- // since it is not in the cache, no need to acquire() it.
+ if (bindArgs != null) {
+ if (bindArgs.length > mNumParameters) {
+ throw new IllegalArgumentException("Too many bind arguments. "
+ + bindArgs.length + " arguments were provided but the statement needs "
+ + mNumParameters + " arguments.");
}
+ System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length);
}
- nStatement = mCompiledSql.nStatement;
}
- @Override
- protected void onAllReferencesReleased() {
- release();
- mDatabase.removeSQLiteClosable(this);
- mDatabase.releaseReference();
+ final SQLiteDatabase getDatabase() {
+ return mDatabase;
}
- @Override
- protected void onAllReferencesReleasedFromContainer() {
- release();
- mDatabase.releaseReference();
+ final String getSql() {
+ return mSql;
}
- /* package */ void release() {
- if (mCompiledSql == null) {
- return;
- }
- mDatabase.releaseCompiledSqlObj(mSql, mCompiledSql);
- mCompiledSql = null;
- nStatement = 0;
+ final Object[] getBindArgs() {
+ return mBindArgs;
}
- /**
- * Returns a unique identifier for this program.
- *
- * @return a unique identifier for this program
- * @deprecated do not use this method. it is not guaranteed to be the same across executions of
- * the SQL statement contained in this object.
- */
- @Deprecated
- public final int getUniqueId() {
- return -1;
+ final String[] getColumnNames() {
+ return mColumnNames;
}
- /**
- * used only for testing purposes
- */
- /* package */ int getSqlStatementId() {
- synchronized(this) {
- return (mCompiledSql == null) ? 0 : nStatement;
- }
+ /** @hide */
+ protected final SQLiteSession getSession() {
+ return mDatabase.getThreadSession();
}
- /* package */ String getSqlString() {
- return mSql;
+ /** @hide */
+ protected final int getConnectionFlags() {
+ return mDatabase.getThreadDefaultConnectionFlags(mReadOnly);
}
- private void bind(int type, int index, Object value) {
- mDatabase.verifyDbIsOpen();
- addToBindArgs(index, (type == Cursor.FIELD_TYPE_NULL) ? null : value);
- if (nStatement > 0) {
- // bind only if the SQL statement is compiled
- acquireReference();
- try {
- switch (type) {
- case Cursor.FIELD_TYPE_NULL:
- native_bind_null(index);
- break;
- case Cursor.FIELD_TYPE_BLOB:
- native_bind_blob(index, (byte[]) value);
- break;
- case Cursor.FIELD_TYPE_FLOAT:
- native_bind_double(index, (Double) value);
- break;
- case Cursor.FIELD_TYPE_INTEGER:
- native_bind_long(index, (Long) value);
- break;
- case Cursor.FIELD_TYPE_STRING:
- default:
- native_bind_string(index, (String) value);
- break;
- }
- } finally {
- releaseReference();
- }
- }
+ /** @hide */
+ protected final void onCorruption() {
+ mDatabase.onCorruption();
+ }
+
+ /**
+ * Unimplemented.
+ * @deprecated This method is deprecated and must not be used.
+ */
+ @Deprecated
+ public final int getUniqueId() {
+ return -1;
}
/**
@@ -245,7 +124,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param index The 1-based index to the parameter to bind null to
*/
public void bindNull(int index) {
- bind(Cursor.FIELD_TYPE_NULL, index, null);
+ bind(index, null);
}
/**
@@ -256,7 +135,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param value The value to bind
*/
public void bindLong(int index, long value) {
- bind(Cursor.FIELD_TYPE_INTEGER, index, value);
+ bind(index, value);
}
/**
@@ -267,7 +146,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param value The value to bind
*/
public void bindDouble(int index, double value) {
- bind(Cursor.FIELD_TYPE_FLOAT, index, value);
+ bind(index, value);
}
/**
@@ -275,13 +154,13 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* {@link #clearBindings} is called.
*
* @param index The 1-based index to the parameter to bind
- * @param value The value to bind
+ * @param value The value to bind, must not be null
*/
public void bindString(int index, String value) {
if (value == null) {
throw new IllegalArgumentException("the bind value at index " + index + " is null");
}
- bind(Cursor.FIELD_TYPE_STRING, index, value);
+ bind(index, value);
}
/**
@@ -289,29 +168,21 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* {@link #clearBindings} is called.
*
* @param index The 1-based index to the parameter to bind
- * @param value The value to bind
+ * @param value The value to bind, must not be null
*/
public void bindBlob(int index, byte[] value) {
if (value == null) {
throw new IllegalArgumentException("the bind value at index " + index + " is null");
}
- bind(Cursor.FIELD_TYPE_BLOB, index, value);
+ bind(index, value);
}
/**
* Clears all existing bindings. Unset bindings are treated as NULL.
*/
public void clearBindings() {
- mBindArgs = null;
- if (this.nStatement == 0) {
- return;
- }
- mDatabase.verifyDbIsOpen();
- acquireReference();
- try {
- native_clear_bindings();
- } finally {
- releaseReference();
+ if (mBindArgs != null) {
+ Arrays.fill(mBindArgs, null);
}
}
@@ -319,102 +190,33 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* Release this program's resources, making it invalid.
*/
public void close() {
- mBindArgs = null;
- if (nHandle == 0 || !mDatabase.isOpen()) {
- return;
- }
releaseReference();
}
- private void addToBindArgs(int index, Object value) {
- if (mBindArgs == null) {
- mBindArgs = new HashMap<Integer, Object>();
- }
- mBindArgs.put(index, value);
- }
-
- /* package */ void compileAndbindAllArgs() {
- if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) {
- if (mBindArgs != null) {
- throw new IllegalArgumentException("Can't pass bindargs for this sql :" + mSql);
- }
- // no need to prepare this SQL statement
- return;
- }
- if (nStatement == 0) {
- // SQL statement is not compiled yet. compile it now.
- compileSql();
- }
- if (mBindArgs == null) {
- return;
- }
- for (int index : mBindArgs.keySet()) {
- Object value = mBindArgs.get(index);
- if (value == null) {
- native_bind_null(index);
- } else if (value instanceof Double || value instanceof Float) {
- native_bind_double(index, ((Number) value).doubleValue());
- } else if (value instanceof Number) {
- native_bind_long(index, ((Number) value).longValue());
- } else if (value instanceof Boolean) {
- Boolean bool = (Boolean)value;
- native_bind_long(index, (bool) ? 1 : 0);
- if (bool) {
- native_bind_long(index, 1);
- } else {
- native_bind_long(index, 0);
- }
- } else if (value instanceof byte[]){
- native_bind_blob(index, (byte[]) value);
- } else {
- native_bind_string(index, value.toString());
- }
- }
- }
-
/**
* Given an array of String bindArgs, this method binds all of them in one single call.
*
- * @param bindArgs the String array of bind args.
+ * @param bindArgs the String array of bind args, none of which must be null.
*/
public void bindAllArgsAsStrings(String[] bindArgs) {
- if (bindArgs == null) {
- return;
- }
- int size = bindArgs.length;
- for (int i = 0; i < size; i++) {
- bindString(i + 1, bindArgs[i]);
+ if (bindArgs != null) {
+ for (int i = bindArgs.length; i != 0; i--) {
+ bindString(i, bindArgs[i - 1]);
+ }
}
}
- /* package */ synchronized final void setNativeHandle(int nHandle) {
- this.nHandle = nHandle;
+ @Override
+ protected void onAllReferencesReleased() {
+ clearBindings();
}
- /**
- * @hide
- * Compiles SQL into a SQLite program.
- *
- * <P>The database lock must be held when calling this method.
- * @param sql The SQL to compile.
- */
- protected final native void native_compile(String sql);
-
- /**
- * @hide
- */
- protected final native void native_finalize();
-
- /** @hide */
- protected final native void native_bind_null(int index);
- /** @hide */
- protected final native void native_bind_long(int index, long value);
- /** @hide */
- protected final native void native_bind_double(int index, double value);
- /** @hide */
- protected final native void native_bind_string(int index, String value);
- /** @hide */
- protected final native void native_bind_blob(int index, byte[] value);
- private final native void native_clear_bindings();
+ private void bind(int index, Object value) {
+ if (index < 1 || index > mNumParameters) {
+ throw new IllegalArgumentException("Cannot bind argument at index "
+ + index + " because the index is out of range. "
+ + "The statement has " + mNumParameters + " parameters.");
+ }
+ mBindArgs[index - 1] = value;
+ }
}
-
diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java
index 6dd2539..17aa886 100644
--- a/core/java/android/database/sqlite/SQLiteQuery.java
+++ b/core/java/android/database/sqlite/SQLiteQuery.java
@@ -17,60 +17,24 @@
package android.database.sqlite;
import android.database.CursorWindow;
-import android.os.SystemClock;
-import android.text.TextUtils;
import android.util.Log;
/**
- * A SQLite program that represents a query that reads the resulting rows into a CursorWindow.
- * This class is used by SQLiteCursor and isn't useful itself.
- *
- * SQLiteQuery is not internally synchronized so code using a SQLiteQuery from multiple
- * threads should perform its own synchronization when using the SQLiteQuery.
+ * Represents a query that reads the resulting rows into a {@link SQLiteQuery}.
+ * This class is used by {@link SQLiteCursor} and isn't useful itself.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
*/
public final class SQLiteQuery extends SQLiteProgram {
private static final String TAG = "SQLiteQuery";
- private static native long nativeFillWindow(int databasePtr, int statementPtr, int windowPtr,
- int offsetParam, int startPos, int requiredPos, boolean countAllRows);
-
- private static native int nativeColumnCount(int statementPtr);
- private static native String nativeColumnName(int statementPtr, int columnIndex);
-
- /** The index of the unbound OFFSET parameter */
- private int mOffsetIndex = 0;
-
- private boolean mClosed = false;
-
- /**
- * Create a persistent query object.
- *
- * @param db The database that this query object is associated with
- * @param query The SQL string for this query.
- * @param offsetIndex The 1-based index to the OFFSET parameter,
- */
- /* package */ SQLiteQuery(SQLiteDatabase db, String query, int offsetIndex, String[] bindArgs) {
- super(db, query);
- mOffsetIndex = offsetIndex;
- bindAllArgsAsStrings(bindArgs);
- }
-
- /**
- * Constructor used to create new instance to replace a given instance of this class.
- * This constructor is used when the current Query object is now associated with a different
- * {@link SQLiteDatabase} object.
- *
- * @param db The database that this query object is associated with
- * @param query the instance of {@link SQLiteQuery} to be replaced
- */
- /* package */ SQLiteQuery(SQLiteDatabase db, SQLiteQuery query) {
- super(db, query.mSql);
- this.mBindArgs = query.mBindArgs;
- this.mOffsetIndex = query.mOffsetIndex;
+ SQLiteQuery(SQLiteDatabase db, String query) {
+ super(db, query, null);
}
/**
- * Reads rows into a buffer. This method acquires the database lock.
+ * Reads rows into a buffer.
*
* @param window The window to fill into
* @param startPos The start position for filling the window.
@@ -81,106 +45,30 @@ public final class SQLiteQuery extends SQLiteProgram {
* @return Number of rows that were enumerated. Might not be all rows
* unless countAllRows is true.
*/
- /* package */ int fillWindow(CursorWindow window,
- int startPos, int requiredPos, boolean countAllRows) {
- mDatabase.lock(mSql);
- long timeStart = SystemClock.uptimeMillis();
+ int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) {
+ acquireReference();
try {
- acquireReference();
+ window.acquireReference();
try {
- window.acquireReference();
- long result = nativeFillWindow(nHandle, nStatement, window.mWindowPtr,
- mOffsetIndex, startPos, requiredPos, countAllRows);
- int actualPos = (int)(result >> 32);
- int countedRows = (int)result;
- window.setStartPosition(actualPos);
- if (SQLiteDebug.DEBUG_LOG_SLOW_QUERIES) {
- long elapsed = SystemClock.uptimeMillis() - timeStart;
- if (SQLiteDebug.shouldLogSlowQuery(elapsed)) {
- Log.d(TAG, "fillWindow took " + elapsed
- + " ms: window=\"" + window
- + "\", startPos=" + startPos
- + ", requiredPos=" + requiredPos
- + ", offset=" + mOffsetIndex
- + ", actualPos=" + actualPos
- + ", filledRows=" + window.getNumRows()
- + ", countedRows=" + countedRows
- + ", query=\"" + mSql + "\""
- + ", args=[" + (mBindArgs != null ?
- TextUtils.join(", ", mBindArgs.values()) : "")
- + "]");
- }
- }
- mDatabase.logTimeStat(mSql, timeStart);
- return countedRows;
- } catch (IllegalStateException e){
- // simply ignore it
- return 0;
- } catch (SQLiteDatabaseCorruptException e) {
- mDatabase.onCorruption();
- throw e;
- } catch (SQLiteException e) {
- Log.e(TAG, "exception: " + e.getMessage() + "; query: " + mSql);
- throw e;
+ int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(),
+ window, startPos, requiredPos, countAllRows, getConnectionFlags());
+ return numRows;
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
+ } catch (SQLiteException ex) {
+ Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql());
+ throw ex;
} finally {
window.releaseReference();
}
} finally {
releaseReference();
- mDatabase.unlock();
- }
- }
-
- /**
- * Get the column count for the statement. Only valid on query based
- * statements. The database must be locked
- * when calling this method.
- *
- * @return The number of column in the statement's result set.
- */
- /* package */ int columnCountLocked() {
- acquireReference();
- try {
- return nativeColumnCount(nStatement);
- } finally {
- releaseReference();
- }
- }
-
- /**
- * Retrieves the column name for the given column index. The database must be locked
- * when calling this method.
- *
- * @param columnIndex the index of the column to get the name for
- * @return The requested column's name
- */
- /* package */ String columnNameLocked(int columnIndex) {
- acquireReference();
- try {
- return nativeColumnName(nStatement, columnIndex);
- } finally {
- releaseReference();
}
}
@Override
public String toString() {
- return "SQLiteQuery: " + mSql;
- }
-
- @Override
- public void close() {
- super.close();
- mClosed = true;
- }
-
- /**
- * Called by SQLiteCursor when it is requeried.
- */
- /* package */ void requery() {
- if (mClosed) {
- throw new IllegalStateException("requerying a closed cursor");
- }
- compileAndbindAllArgs();
+ return "SQLiteQuery: " + getSql();
}
}
diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
index 8f8eb6e..1b7b398 100644
--- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java
+++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
@@ -341,7 +341,7 @@ public class SQLiteQueryBuilder
// in both the wrapped and original forms.
String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy,
having, sortOrder, limit);
- validateSql(db, sqlForValidation); // will throw if query is invalid
+ validateQuerySql(db, sqlForValidation); // will throw if query is invalid
}
String sql = buildQuery(
@@ -357,16 +357,12 @@ public class SQLiteQueryBuilder
}
/**
- * Verifies that a SQL statement is valid by compiling it.
+ * Verifies that a SQL SELECT statement is valid by compiling it.
* If the SQL statement is not valid, this method will throw a {@link SQLiteException}.
*/
- private void validateSql(SQLiteDatabase db, String sql) {
- db.lock(sql);
- try {
- new SQLiteCompiledSql(db, sql).releaseSqlStatement();
- } finally {
- db.unlock();
- }
+ private void validateQuerySql(SQLiteDatabase db, String sql) {
+ db.getThreadSession().prepare(sql,
+ db.getThreadDefaultConnectionFlags(true /*readOnly*/), null);
}
/**
diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java
new file mode 100644
index 0000000..61fe45a
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteSession.java
@@ -0,0 +1,878 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Provides a single client the ability to use a database.
+ *
+ * <h2>About database sessions</h2>
+ * <p>
+ * Database access is always performed using a session. The session
+ * manages the lifecycle of transactions and database connections.
+ * </p><p>
+ * Sessions can be used to perform both read-only and read-write operations.
+ * There is some advantage to knowing when a session is being used for
+ * read-only purposes because the connection pool can optimize the use
+ * of the available connections to permit multiple read-only operations
+ * to execute in parallel whereas read-write operations may need to be serialized.
+ * </p><p>
+ * When <em>Write Ahead Logging (WAL)</em> is enabled, the database can
+ * execute simultaneous read-only and read-write transactions, provided that
+ * at most one read-write transaction is performed at a time. When WAL is not
+ * enabled, read-only transactions can execute in parallel but read-write
+ * transactions are mutually exclusive.
+ * </p>
+ *
+ * <h2>Ownership and concurrency guarantees</h2>
+ * <p>
+ * Session objects are not thread-safe. In fact, session objects are thread-bound.
+ * The {@link SQLiteDatabase} uses a thread-local variable to associate a session
+ * with each thread for the use of that thread alone. Consequently, each thread
+ * has its own session object and therefore its own transaction state independent
+ * of other threads.
+ * </p><p>
+ * A thread has at most one session per database. This constraint ensures that
+ * a thread can never use more than one database connection at a time for a
+ * given database. As the number of available database connections is limited,
+ * if a single thread tried to acquire multiple connections for the same database
+ * at the same time, it might deadlock. Therefore we allow there to be only
+ * one session (so, at most one connection) per thread per database.
+ * </p>
+ *
+ * <h2>Transactions</h2>
+ * <p>
+ * There are two kinds of transaction: implicit transactions and explicit
+ * transactions.
+ * </p><p>
+ * An implicit transaction is created whenever a database operation is requested
+ * and there is no explicit transaction currently in progress. An implicit transaction
+ * only lasts for the duration of the database operation in question and then it
+ * is ended. If the database operation was successful, then its changes are committed.
+ * </p><p>
+ * An explicit transaction is started by calling {@link #beginTransaction} and
+ * specifying the desired transaction mode. Once an explicit transaction has begun,
+ * all subsequent database operations will be performed as part of that transaction.
+ * To end an explicit transaction, first call {@link #setTransactionSuccessful} if the
+ * transaction was successful, then call {@link #end}. If the transaction was
+ * marked successful, its changes will be committed, otherwise they will be rolled back.
+ * </p><p>
+ * Explicit transactions can also be nested. A nested explicit transaction is
+ * started with {@link #beginTransaction}, marked successful with
+ * {@link #setTransactionSuccessful}and ended with {@link #endTransaction}.
+ * If any nested transaction is not marked successful, then the entire transaction
+ * including all of its nested transactions will be rolled back
+ * when the outermost transaction is ended.
+ * </p><p>
+ * To improve concurrency, an explicit transaction can be yielded by calling
+ * {@link #yieldTransaction}. If there is contention for use of the database,
+ * then yielding ends the current transaction, commits its changes, releases the
+ * database connection for use by another session for a little while, and starts a
+ * new transaction with the same properties as the original one.
+ * Changes committed by {@link #yieldTransaction} cannot be rolled back.
+ * </p><p>
+ * When a transaction is started, the client can provide a {@link SQLiteTransactionListener}
+ * to listen for notifications of transaction-related events.
+ * </p><p>
+ * Recommended usage:
+ * <code><pre>
+ * // First, begin the transaction.
+ * session.beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, 0);
+ * try {
+ * // Then do stuff...
+ * session.execute("INSERT INTO ...", null, 0);
+ *
+ * // As the very last step before ending the transaction, mark it successful.
+ * session.setTransactionSuccessful();
+ * } finally {
+ * // Finally, end the transaction.
+ * // This statement will commit the transaction if it was marked successful or
+ * // roll it back otherwise.
+ * session.endTransaction();
+ * }
+ * </pre></code>
+ * </p>
+ *
+ * <h2>Database connections</h2>
+ * <p>
+ * A {@link SQLiteDatabase} can have multiple active sessions at the same
+ * time. Each session acquires and releases connections to the database
+ * as needed to perform each requested database transaction. If all connections
+ * are in use, then database transactions on some sessions will block until a
+ * connection becomes available.
+ * </p><p>
+ * The session acquires a single database connection only for the duration
+ * of a single (implicit or explicit) database transaction, then releases it.
+ * This characteristic allows a small pool of database connections to be shared
+ * efficiently by multiple sessions as long as they are not all trying to perform
+ * database transactions at the same time.
+ * </p>
+ *
+ * <h2>Responsiveness</h2>
+ * <p>
+ * Because there are a limited number of database connections and the session holds
+ * a database connection for the entire duration of a database transaction,
+ * it is important to keep transactions short. This is especially important
+ * for read-write transactions since they may block other transactions
+ * from executing. Consider calling {@link #yieldTransaction} periodically
+ * during long-running transactions.
+ * </p><p>
+ * Another important consideration is that transactions that take too long to
+ * run may cause the application UI to become unresponsive. Even if the transaction
+ * is executed in a background thread, the user will get bored and
+ * frustrated if the application shows no data for several seconds while
+ * a transaction runs.
+ * </p><p>
+ * Guidelines:
+ * <ul>
+ * <li>Do not perform database transactions on the UI thread.</li>
+ * <li>Keep database transactions as short as possible.</li>
+ * <li>Simple queries often run faster than complex queries.</li>
+ * <li>Measure the performance of your database transactions.</li>
+ * <li>Consider what will happen when the size of the data set grows.
+ * A query that works well on 100 rows may struggle with 10,000.</li>
+ * </ul>
+ *
+ * TODO: Support timeouts on all possibly blocking operations.
+ *
+ * @hide
+ */
+public final class SQLiteSession {
+ private final SQLiteConnectionPool mConnectionPool;
+
+ private SQLiteConnection mConnection;
+ private int mConnectionFlags;
+ private Transaction mTransactionPool;
+ private Transaction mTransactionStack;
+
+ /**
+ * Transaction mode: Deferred.
+ * <p>
+ * In a deferred transaction, no locks are acquired on the database
+ * until the first operation is performed. If the first operation is
+ * read-only, then a <code>SHARED</code> lock is acquired, otherwise
+ * a <code>RESERVED</code> lock is acquired.
+ * </p><p>
+ * While holding a <code>SHARED</code> lock, this session is only allowed to
+ * read but other sessions are allowed to read or write.
+ * While holding a <code>RESERVED</code> lock, this session is allowed to read
+ * or write but other sessions are only allowed to read.
+ * </p><p>
+ * Because the lock is only acquired when needed in a deferred transaction,
+ * it is possible for another session to write to the database first before
+ * this session has a chance to do anything.
+ * </p><p>
+ * Corresponds to the SQLite <code>BEGIN DEFERRED</code> transaction mode.
+ * </p>
+ */
+ public static final int TRANSACTION_MODE_DEFERRED = 0;
+
+ /**
+ * Transaction mode: Immediate.
+ * <p>
+ * When an immediate transaction begins, the session acquires a
+ * <code>RESERVED</code> lock.
+ * </p><p>
+ * While holding a <code>RESERVED</code> lock, this session is allowed to read
+ * or write but other sessions are only allowed to read.
+ * </p><p>
+ * Corresponds to the SQLite <code>BEGIN IMMEDIATE</code> transaction mode.
+ * </p>
+ */
+ public static final int TRANSACTION_MODE_IMMEDIATE = 1;
+
+ /**
+ * Transaction mode: Exclusive.
+ * <p>
+ * When an exclusive transaction begins, the session acquires an
+ * <code>EXCLUSIVE</code> lock.
+ * </p><p>
+ * While holding an <code>EXCLUSIVE</code> lock, this session is allowed to read
+ * or write but no other sessions are allowed to access the database.
+ * </p><p>
+ * Corresponds to the SQLite <code>BEGIN EXCLUSIVE</code> transaction mode.
+ * </p>
+ */
+ public static final int TRANSACTION_MODE_EXCLUSIVE = 2;
+
+ /**
+ * Creates a session bound to the specified connection pool.
+ *
+ * @param connectionPool The connection pool.
+ */
+ public SQLiteSession(SQLiteConnectionPool connectionPool) {
+ if (connectionPool == null) {
+ throw new IllegalArgumentException("connectionPool must not be null");
+ }
+
+ mConnectionPool = connectionPool;
+ }
+
+ /**
+ * Returns true if the session has a transaction in progress.
+ *
+ * @return True if the session has a transaction in progress.
+ */
+ public boolean hasTransaction() {
+ return mTransactionStack != null;
+ }
+
+ /**
+ * Returns true if the session has a nested transaction in progress.
+ *
+ * @return True if the session has a nested transaction in progress.
+ */
+ public boolean hasNestedTransaction() {
+ return mTransactionStack != null && mTransactionStack.mParent != null;
+ }
+
+ /**
+ * Returns true if the session has an active database connection.
+ *
+ * @return True if the session has an active database connection.
+ */
+ public boolean hasConnection() {
+ return mConnection != null;
+ }
+
+ /**
+ * Begins a transaction.
+ * <p>
+ * Transactions may nest. If the transaction is not in progress,
+ * then a database connection is obtained and a new transaction is started.
+ * Otherwise, a nested transaction is started.
+ * </p><p>
+ * Each call to {@link #beginTransaction} must be matched exactly by a call
+ * to {@link #endTransaction}. To mark a transaction as successful,
+ * call {@link #setTransactionSuccessful} before calling {@link #endTransaction}.
+ * If the transaction is not successful, or if any of its nested
+ * transactions were not successful, then the entire transaction will
+ * be rolled back when the outermost transaction is ended.
+ * </p>
+ *
+ * @param transactionMode The transaction mode. One of: {@link #TRANSACTION_MODE_DEFERRED},
+ * {@link #TRANSACTION_MODE_IMMEDIATE}, or {@link #TRANSACTION_MODE_EXCLUSIVE}.
+ * Ignored when creating a nested transaction.
+ * @param transactionListener The transaction listener, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ *
+ * @throws IllegalStateException if {@link #setTransactionSuccessful} has already been
+ * called for the current transaction.
+ *
+ * @see #setTransactionSuccessful
+ * @see #yieldTransaction
+ * @see #endTransaction
+ */
+ public void beginTransaction(int transactionMode,
+ SQLiteTransactionListener transactionListener, int connectionFlags) {
+ throwIfTransactionMarkedSuccessful();
+ beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags);
+ }
+
+ private void beginTransactionUnchecked(int transactionMode,
+ SQLiteTransactionListener transactionListener, int connectionFlags) {
+ acquireConnectionIfNoTransaction(null, connectionFlags); // might throw
+ try {
+ // Set up the transaction such that we can back out safely
+ // in case we fail part way.
+ if (mTransactionStack == null) {
+ // Execute SQL might throw a runtime exception.
+ switch (transactionMode) {
+ case TRANSACTION_MODE_IMMEDIATE:
+ mConnection.execute("BEGIN IMMEDIATE;", null); // might throw
+ break;
+ case TRANSACTION_MODE_EXCLUSIVE:
+ mConnection.execute("BEGIN EXCLUSIVE;", null); // might throw
+ break;
+ default:
+ mConnection.execute("BEGIN;", null); // might throw
+ break;
+ }
+ }
+
+ // Listener might throw a runtime exception.
+ if (transactionListener != null) {
+ try {
+ transactionListener.onBegin(); // might throw
+ } catch (RuntimeException ex) {
+ if (mTransactionStack == null) {
+ mConnection.execute("ROLLBACK;", null); // might throw
+ }
+ throw ex;
+ }
+ }
+
+ // Bookkeeping can't throw, except an OOM, which is just too bad...
+ Transaction transaction = obtainTransaction(transactionMode, transactionListener);
+ transaction.mParent = mTransactionStack;
+ mTransactionStack = transaction;
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Marks the current transaction as having completed successfully.
+ * <p>
+ * This method can be called at most once between {@link #beginTransaction} and
+ * {@link #endTransaction} to indicate that the changes made by the transaction should be
+ * committed. If this method is not called, the changes will be rolled back
+ * when the transaction is ended.
+ * </p>
+ *
+ * @throws IllegalStateException if there is no current transaction, or if
+ * {@link #setTransactionSuccessful} has already been called for the current transaction.
+ *
+ * @see #beginTransaction
+ * @see #endTransaction
+ */
+ public void setTransactionSuccessful() {
+ throwIfNoTransaction();
+ throwIfTransactionMarkedSuccessful();
+
+ mTransactionStack.mMarkedSuccessful = true;
+ }
+
+ /**
+ * Ends the current transaction and commits or rolls back changes.
+ * <p>
+ * If this is the outermost transaction (not nested within any other
+ * transaction), then the changes are committed if {@link #setTransactionSuccessful}
+ * was called or rolled back otherwise.
+ * </p><p>
+ * This method must be called exactly once for each call to {@link #beginTransaction}.
+ * </p>
+ *
+ * @throws IllegalStateException if there is no current transaction.
+ *
+ * @see #beginTransaction
+ * @see #setTransactionSuccessful
+ * @see #yieldTransaction
+ */
+ public void endTransaction() {
+ throwIfNoTransaction();
+ assert mConnection != null;
+
+ endTransactionUnchecked();
+ }
+
+ private void endTransactionUnchecked() {
+ final Transaction top = mTransactionStack;
+ boolean successful = top.mMarkedSuccessful && !top.mChildFailed;
+
+ RuntimeException listenerException = null;
+ final SQLiteTransactionListener listener = top.mListener;
+ if (listener != null) {
+ try {
+ if (successful) {
+ listener.onCommit(); // might throw
+ } else {
+ listener.onRollback(); // might throw
+ }
+ } catch (RuntimeException ex) {
+ listenerException = ex;
+ successful = false;
+ }
+ }
+
+ mTransactionStack = top.mParent;
+ recycleTransaction(top);
+
+ if (mTransactionStack != null) {
+ if (!successful) {
+ mTransactionStack.mChildFailed = true;
+ }
+ } else {
+ try {
+ if (successful) {
+ mConnection.execute("COMMIT;", null); // might throw
+ } else {
+ mConnection.execute("ROLLBACK;", null); // might throw
+ }
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ if (listenerException != null) {
+ throw listenerException;
+ }
+ }
+
+ /**
+ * Temporarily ends a transaction to let other threads have use of
+ * the database. Begins a new transaction after a specified delay.
+ * <p>
+ * If there are other threads waiting to acquire connections,
+ * then the current transaction is committed and the database
+ * connection is released. After a short delay, a new transaction
+ * is started.
+ * </p><p>
+ * The transaction is assumed to be successful so far. Do not call
+ * {@link #setTransactionSuccessful()} before calling this method.
+ * This method will fail if the transaction has already been marked
+ * successful.
+ * </p><p>
+ * The changes that were committed by a yield cannot be rolled back later.
+ * </p><p>
+ * Before this method was called, there must already have been
+ * a transaction in progress. When this method returns, there will
+ * still be a transaction in progress, either the same one as before
+ * or a new one if the transaction was actually yielded.
+ * </p><p>
+ * This method should not be called when there is a nested transaction
+ * in progress because it is not possible to yield a nested transaction.
+ * If <code>throwIfNested</code> is true, then attempting to yield
+ * a nested transaction will throw {@link IllegalStateException}, otherwise
+ * the method will return <code>false</code> in that case.
+ * </p><p>
+ * If there is no nested transaction in progress but a previous nested
+ * transaction failed, then the transaction is not yielded (because it
+ * must be rolled back) and this method returns <code>false</code>.
+ * </p>
+ *
+ * @param sleepAfterYieldDelayMillis A delay time to wait after yielding
+ * the database connection to allow other threads some time to run.
+ * If the value is less than or equal to zero, there will be no additional
+ * delay beyond the time it will take to begin a new transaction.
+ * @param throwIfUnsafe If true, then instead of returning false when no
+ * transaction is in progress, a nested transaction is in progress, or when
+ * the transaction has already been marked successful, throws {@link IllegalStateException}.
+ * @return True if the transaction was actually yielded.
+ *
+ * @throws IllegalStateException if <code>throwIfNested</code> is true and
+ * there is no current transaction, there is a nested transaction in progress or
+ * if {@link #setTransactionSuccessful} has already been called for the current transaction.
+ *
+ * @see #beginTransaction
+ * @see #endTransaction
+ */
+ public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe) {
+ if (throwIfUnsafe) {
+ throwIfNoTransaction();
+ throwIfTransactionMarkedSuccessful();
+ throwIfNestedTransaction();
+ } else {
+ if (mTransactionStack == null || mTransactionStack.mMarkedSuccessful
+ || mTransactionStack.mParent != null) {
+ return false;
+ }
+ }
+ assert mConnection != null;
+
+ if (mTransactionStack.mChildFailed) {
+ return false;
+ }
+
+ return yieldTransactionUnchecked(sleepAfterYieldDelayMillis); // might throw
+ }
+
+ private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis) {
+ if (!mConnectionPool.shouldYieldConnection(mConnection, mConnectionFlags)) {
+ return false;
+ }
+
+ final int transactionMode = mTransactionStack.mMode;
+ final SQLiteTransactionListener listener = mTransactionStack.mListener;
+ final int connectionFlags = mConnectionFlags;
+ endTransactionUnchecked(); // might throw
+
+ if (sleepAfterYieldDelayMillis > 0) {
+ try {
+ Thread.sleep(sleepAfterYieldDelayMillis);
+ } catch (InterruptedException ex) {
+ // we have been interrupted, that's all we need to do
+ }
+ }
+
+ beginTransactionUnchecked(transactionMode, listener, connectionFlags); // might throw
+ return true;
+ }
+
+ /**
+ * Prepares a statement for execution but does not bind its parameters or execute it.
+ * <p>
+ * This method can be used to check for syntax errors during compilation
+ * prior to execution of the statement. If the {@code outStatementInfo} argument
+ * is not null, the provided {@link SQLiteStatementInfo} object is populated
+ * with information about the statement.
+ * </p><p>
+ * A prepared statement makes no reference to the arguments that may eventually
+ * be bound to it, consequently it it possible to cache certain prepared statements
+ * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable,
+ * then it will be stored in the cache for later and reused if possible.
+ * </p>
+ *
+ * @param sql The SQL statement to prepare.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate
+ * with information about the statement, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error.
+ */
+ public void prepare(String sql, int connectionFlags, SQLiteStatementInfo outStatementInfo) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+ try {
+ mConnection.prepare(sql, outStatementInfo); // might throw
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that does not return a result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public void execute(String sql, Object[] bindArgs, int connectionFlags) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags)) {
+ return;
+ }
+
+ acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+ try {
+ mConnection.execute(sql, bindArgs); // might throw
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single <code>long</code> result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @return The value of the first column in the first row of the result set
+ * as a <code>long</code>, or zero if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public long executeForLong(String sql, Object[] bindArgs, int connectionFlags) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags)) {
+ return 0;
+ }
+
+ acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+ try {
+ return mConnection.executeForLong(sql, bindArgs); // might throw
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single {@link String} result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @return The value of the first column in the first row of the result set
+ * as a <code>String</code>, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public String executeForString(String sql, Object[] bindArgs, int connectionFlags) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags)) {
+ return null;
+ }
+
+ acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+ try {
+ return mConnection.executeForString(sql, bindArgs); // might throw
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single BLOB result as a
+ * file descriptor to a shared memory region.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @return The file descriptor for a shared memory region that contains
+ * the value of the first column in the first row of the result set as a BLOB,
+ * or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs,
+ int connectionFlags) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags)) {
+ return null;
+ }
+
+ acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+ try {
+ return mConnection.executeForBlobFileDescriptor(sql, bindArgs); // might throw
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns a count of the number of rows
+ * that were changed. Use for UPDATE or DELETE SQL statements.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @return The number of rows that were changed.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags)) {
+ return 0;
+ }
+
+ acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+ try {
+ return mConnection.executeForChangedRowCount(sql, bindArgs); // might throw
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns the row id of the last row inserted
+ * by the statement. Use for INSERT SQL statements.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @return The row id of the last row that was inserted, or 0 if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int connectionFlags) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags)) {
+ return 0;
+ }
+
+ acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+ try {
+ return mConnection.executeForLastInsertedRowId(sql, bindArgs); // might throw
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement and populates the specified {@link CursorWindow}
+ * with a range of results. Returns the number of rows that were counted
+ * during query execution.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param window The cursor window to clear and fill.
+ * @param startPos The start position for filling the window.
+ * @param requiredPos The position of a row that MUST be in the window.
+ * If it won't fit, then the query should discard part of what it filled
+ * so that it does. Must be greater than or equal to <code>startPos</code>.
+ * @param countAllRows True to count all rows that the query would return
+ * regagless of whether they fit in the window.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @return The number of rows that were counted during query execution. Might
+ * not be all rows in the result set unless <code>countAllRows</code> is true.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ public int executeForCursorWindow(String sql, Object[] bindArgs,
+ CursorWindow window, int startPos, int requiredPos, boolean countAllRows,
+ int connectionFlags) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+ if (window == null) {
+ throw new IllegalArgumentException("window must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags)) {
+ window.clear();
+ return 0;
+ }
+
+ acquireConnectionIfNoTransaction(sql, connectionFlags); // might throw
+ try {
+ return mConnection.executeForCursorWindow(sql, bindArgs,
+ window, startPos, requiredPos, countAllRows); // might throw
+ } finally {
+ releaseConnectionIfNoTransaction(); // might throw
+ }
+ }
+
+ /**
+ * Performs special reinterpretation of certain SQL statements such as "BEGIN",
+ * "COMMIT" and "ROLLBACK" to ensure that transaction state invariants are
+ * maintained.
+ *
+ * This function is mainly used to support legacy apps that perform their
+ * own transactions by executing raw SQL rather than calling {@link #beginTransaction}
+ * and the like.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @return True if the statement was of a special form that was handled here,
+ * false otherwise.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ */
+ private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags) {
+ final int type = DatabaseUtils.getSqlStatementType(sql);
+ switch (type) {
+ case DatabaseUtils.STATEMENT_BEGIN:
+ beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags);
+ return true;
+
+ case DatabaseUtils.STATEMENT_COMMIT:
+ setTransactionSuccessful();
+ endTransaction();
+ return true;
+
+ case DatabaseUtils.STATEMENT_ABORT:
+ endTransaction();
+ return true;
+ }
+ return false;
+ }
+
+ private void acquireConnectionIfNoTransaction(String sql, int connectionFlags) {
+ if (mTransactionStack == null) {
+ assert mConnection == null;
+ mConnection = mConnectionPool.acquireConnection(sql, connectionFlags); // might throw
+ mConnectionFlags = connectionFlags;
+ }
+ }
+
+ private void releaseConnectionIfNoTransaction() {
+ if (mTransactionStack == null && mConnection != null) {
+ try {
+ mConnectionPool.releaseConnection(mConnection); // might throw
+ } finally {
+ mConnection = null;
+ }
+ }
+ }
+
+ private void throwIfNoTransaction() {
+ if (mTransactionStack == null) {
+ throw new IllegalStateException("Cannot perform this operation because "
+ + "there is no current transaction.");
+ }
+ }
+
+ private void throwIfTransactionMarkedSuccessful() {
+ if (mTransactionStack != null && mTransactionStack.mMarkedSuccessful) {
+ throw new IllegalStateException("Cannot perform this operation because "
+ + "the transaction has already been marked successful. The only "
+ + "thing you can do now is call endTransaction().");
+ }
+ }
+
+ private void throwIfNestedTransaction() {
+ if (mTransactionStack == null && mTransactionStack.mParent != null) {
+ throw new IllegalStateException("Cannot perform this operation because "
+ + "a nested transaction is in progress.");
+ }
+ }
+
+ private Transaction obtainTransaction(int mode, SQLiteTransactionListener listener) {
+ Transaction transaction = mTransactionPool;
+ if (transaction != null) {
+ mTransactionPool = transaction.mParent;
+ transaction.mParent = null;
+ transaction.mMarkedSuccessful = false;
+ transaction.mChildFailed = false;
+ } else {
+ transaction = new Transaction();
+ }
+ transaction.mMode = mode;
+ transaction.mListener = listener;
+ return transaction;
+ }
+
+ private void recycleTransaction(Transaction transaction) {
+ transaction.mParent = mTransactionPool;
+ transaction.mListener = null;
+ mTransactionPool = transaction;
+ }
+
+ private static final class Transaction {
+ public Transaction mParent;
+ public int mMode;
+ public SQLiteTransactionListener mListener;
+ public boolean mMarkedSuccessful;
+ public boolean mChildFailed;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java
index c99a6fb..4e20da0 100644
--- a/core/java/android/database/sqlite/SQLiteStatement.java
+++ b/core/java/android/database/sqlite/SQLiteStatement.java
@@ -16,47 +16,19 @@
package android.database.sqlite;
-import android.database.DatabaseUtils;
import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
-import android.util.Log;
-
-import java.io.IOException;
-
-import dalvik.system.BlockGuard;
/**
- * A pre-compiled statement against a {@link SQLiteDatabase} that can be reused.
- * The statement cannot return multiple rows, but 1x1 result sets are allowed.
- * Don't use SQLiteStatement constructor directly, please use
- * {@link SQLiteDatabase#compileStatement(String)}
- *<p>
- * SQLiteStatement is NOT internally synchronized so code using a SQLiteStatement from multiple
- * threads should perform its own synchronization when using the SQLiteStatement.
+ * Represents a statement that can be executed against a database. The statement
+ * cannot return multiple rows or columns, but single value (1 x 1) result sets
+ * are supported.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
*/
-@SuppressWarnings("deprecation")
-public final class SQLiteStatement extends SQLiteProgram
-{
- private static final String TAG = "SQLiteStatement";
-
- private static final boolean READ = true;
- private static final boolean WRITE = false;
-
- private SQLiteDatabase mOrigDb;
- private int mState;
- /** possible value for {@link #mState}. indicates that a transaction is started. */
- private static final int TRANS_STARTED = 1;
- /** possible value for {@link #mState}. indicates that a lock is acquired. */
- private static final int LOCK_ACQUIRED = 2;
-
- /**
- * Don't use SQLiteStatement constructor directly, please use
- * {@link SQLiteDatabase#compileStatement(String)}
- * @param db
- * @param sql
- */
- /* package */ SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
- super(db, sql, bindArgs, false /* don't compile sql statement */);
+public final class SQLiteStatement extends SQLiteProgram {
+ SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
+ super(db, sql, bindArgs);
}
/**
@@ -67,7 +39,15 @@ public final class SQLiteStatement extends SQLiteProgram
* some reason
*/
public void execute() {
- executeUpdateDelete();
+ acquireReference();
+ try {
+ getSession().execute(getSql(), getBindArgs(), getConnectionFlags());
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
+ } finally {
+ releaseReference();
+ }
}
/**
@@ -79,21 +59,15 @@ public final class SQLiteStatement extends SQLiteProgram
* some reason
*/
public int executeUpdateDelete() {
+ acquireReference();
try {
- saveSqlAsLastSqlStatement();
- acquireAndLock(WRITE);
- int numChanges = 0;
- if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) {
- // since the statement doesn't have to be prepared,
- // call the following native method which will not prepare
- // the query plan
- native_executeSql(mSql);
- } else {
- numChanges = native_execute();
- }
- return numChanges;
+ return getSession().executeForChangedRowCount(
+ getSql(), getBindArgs(), getConnectionFlags());
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
+ releaseReference();
}
}
@@ -107,23 +81,18 @@ public final class SQLiteStatement extends SQLiteProgram
* some reason
*/
public long executeInsert() {
+ acquireReference();
try {
- saveSqlAsLastSqlStatement();
- acquireAndLock(WRITE);
- return native_executeInsert();
+ return getSession().executeForLastInsertedRowId(
+ getSql(), getBindArgs(), getConnectionFlags());
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
+ releaseReference();
}
}
- private void saveSqlAsLastSqlStatement() {
- if (((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_UPDATE) ||
- (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_BEGIN) {
- mDatabase.setLastSqlStatement(mSql);
- }
- }
/**
* Execute a statement that returns a 1 by 1 table with a numeric value.
* For example, SELECT COUNT(*) FROM table;
@@ -133,17 +102,15 @@ public final class SQLiteStatement extends SQLiteProgram
* @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
*/
public long simpleQueryForLong() {
+ acquireReference();
try {
- long timeStart = acquireAndLock(READ);
- long retValue = native_1x1_long();
- mDatabase.logTimeStat(mSql, timeStart);
- return retValue;
- } catch (SQLiteDoneException e) {
- throw new SQLiteDoneException(
- "expected 1 row from this query but query returned no data. check the query: " +
- mSql);
+ return getSession().executeForLong(
+ getSql(), getBindArgs(), getConnectionFlags());
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
+ releaseReference();
}
}
@@ -156,17 +123,15 @@ public final class SQLiteStatement extends SQLiteProgram
* @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
*/
public String simpleQueryForString() {
+ acquireReference();
try {
- long timeStart = acquireAndLock(READ);
- String retValue = native_1x1_string();
- mDatabase.logTimeStat(mSql, timeStart);
- return retValue;
- } catch (SQLiteDoneException e) {
- throw new SQLiteDoneException(
- "expected 1 row from this query but query returned no data. check the query: " +
- mSql);
+ return getSession().executeForString(
+ getSql(), getBindArgs(), getConnectionFlags());
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
+ releaseReference();
}
}
@@ -179,121 +144,20 @@ public final class SQLiteStatement extends SQLiteProgram
* @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
*/
public ParcelFileDescriptor simpleQueryForBlobFileDescriptor() {
+ acquireReference();
try {
- long timeStart = acquireAndLock(READ);
- ParcelFileDescriptor retValue = native_1x1_blob_ashmem();
- mDatabase.logTimeStat(mSql, timeStart);
- return retValue;
- } catch (IOException ex) {
- Log.e(TAG, "simpleQueryForBlobFileDescriptor() failed", ex);
- return null;
- } catch (SQLiteDoneException e) {
- throw new SQLiteDoneException(
- "expected 1 row from this query but query returned no data. check the query: " +
- mSql);
+ return getSession().executeForBlobFileDescriptor(
+ getSql(), getBindArgs(), getConnectionFlags());
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
- }
- }
-
- /**
- * Called before every method in this class before executing a SQL statement,
- * this method does the following:
- * <ul>
- * <li>make sure the database is open</li>
- * <li>get a database connection from the connection pool,if possible</li>
- * <li>notifies {@link BlockGuard} of read/write</li>
- * <li>if the SQL statement is an update, start transaction if not already in one.
- * otherwise, get lock on the database</li>
- * <li>acquire reference on this object</li>
- * <li>and then return the current time _after_ the database lock was acquired</li>
- * </ul>
- * <p>
- * This method removes the duplicate code from the other public
- * methods in this class.
- */
- private long acquireAndLock(boolean rwFlag) {
- mState = 0;
- // use pooled database connection handles for SELECT SQL statements
- mDatabase.verifyDbIsOpen();
- SQLiteDatabase db = ((mStatementType & SQLiteProgram.STATEMENT_USE_POOLED_CONN) > 0)
- ? mDatabase.getDbConnection(mSql) : mDatabase;
- // use the database connection obtained above
- mOrigDb = mDatabase;
- mDatabase = db;
- setNativeHandle(mDatabase.mNativeHandle);
- if (rwFlag == WRITE) {
- BlockGuard.getThreadPolicy().onWriteToDisk();
- } else {
- BlockGuard.getThreadPolicy().onReadFromDisk();
- }
-
- /*
- * Special case handling of SQLiteDatabase.execSQL("BEGIN transaction").
- * we know it is execSQL("BEGIN transaction") from the caller IF there is no lock held.
- * beginTransaction() methods in SQLiteDatabase call lockForced() before
- * calling execSQL("BEGIN transaction").
- */
- if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_BEGIN) {
- if (!mDatabase.isDbLockedByCurrentThread()) {
- // transaction is NOT started by calling beginTransaction() methods in
- // SQLiteDatabase
- mDatabase.setTransactionUsingExecSqlFlag();
- }
- } else if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_UPDATE) {
- // got update SQL statement. if there is NO pending transaction, start one
- if (!mDatabase.inTransaction()) {
- mDatabase.beginTransactionNonExclusive();
- mState = TRANS_STARTED;
- }
+ releaseReference();
}
- // do I have database lock? if not, grab it.
- if (!mDatabase.isDbLockedByCurrentThread()) {
- mDatabase.lock(mSql);
- mState = LOCK_ACQUIRED;
- }
-
- acquireReference();
- long startTime = SystemClock.uptimeMillis();
- mDatabase.closePendingStatements();
- compileAndbindAllArgs();
- return startTime;
}
- /**
- * this method releases locks and references acquired in {@link #acquireAndLock(boolean)}
- */
- private void releaseAndUnlock() {
- releaseReference();
- if (mState == TRANS_STARTED) {
- try {
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
- } else if (mState == LOCK_ACQUIRED) {
- mDatabase.unlock();
- }
- if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_COMMIT ||
- (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_ABORT) {
- mDatabase.resetTransactionUsingExecSqlFlag();
- }
- clearBindings();
- // release the compiled sql statement so that the caller's SQLiteStatement no longer
- // has a hard reference to a database object that may get deallocated at any point.
- release();
- // restore the database connection handle to the original value
- mDatabase = mOrigDb;
- setNativeHandle(mDatabase.mNativeHandle);
+ @Override
+ public String toString() {
+ return "SQLiteProgram: " + getSql();
}
-
- private final native int native_execute();
- private final native long native_executeInsert();
- private final native long native_1x1_long();
- private final native String native_1x1_string();
- private final native ParcelFileDescriptor native_1x1_blob_ashmem() throws IOException;
- private final native void native_executeSql(String sql);
}
diff --git a/core/java/android/database/sqlite/SQLiteStatementInfo.java b/core/java/android/database/sqlite/SQLiteStatementInfo.java
new file mode 100644
index 0000000..3edfdb0
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteStatementInfo.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+/**
+ * Describes a SQLite statement.
+ *
+ * @hide
+ */
+public final class SQLiteStatementInfo {
+ /**
+ * The number of parameters that the statement has.
+ */
+ public int numParameters;
+
+ /**
+ * The names of all columns in the result set of the statement.
+ */
+ public String[] columnNames;
+
+ /**
+ * True if the statement is read-only.
+ */
+ public boolean readOnly;
+}
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<K, V> {
}
/**
+ * 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
diff --git a/core/jni/Android.mk b/core/jni/Android.mk
index 8be1996..39b84bb 100644
--- a/core/jni/Android.mk
+++ b/core/jni/Android.mk
@@ -39,12 +39,10 @@ LOCAL_SRC_FILES:= \
android_opengl_GLES11Ext.cpp \
android_opengl_GLES20.cpp \
android_database_CursorWindow.cpp \
- android_database_SQLiteCompiledSql.cpp \
+ android_database_SQLiteCommon.cpp \
+ android_database_SQLiteConnection.cpp \
+ android_database_SQLiteGlobal.cpp \
android_database_SQLiteDebug.cpp \
- android_database_SQLiteDatabase.cpp \
- android_database_SQLiteProgram.cpp \
- android_database_SQLiteQuery.cpp \
- android_database_SQLiteStatement.cpp \
android_emoji_EmojiFactory.cpp \
android_view_Display.cpp \
android_view_DisplayEventReceiver.cpp \
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index c006615..8a3063f 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -121,12 +121,9 @@ extern int register_android_view_HardwareRenderer(JNIEnv* env);
extern int register_android_view_Surface(JNIEnv* env);
extern int register_android_view_TextureView(JNIEnv* env);
extern int register_android_database_CursorWindow(JNIEnv* env);
-extern int register_android_database_SQLiteCompiledSql(JNIEnv* env);
-extern int register_android_database_SQLiteDatabase(JNIEnv* env);
+extern int register_android_database_SQLiteConnection(JNIEnv* env);
+extern int register_android_database_SQLiteGlobal(JNIEnv* env);
extern int register_android_database_SQLiteDebug(JNIEnv* env);
-extern int register_android_database_SQLiteProgram(JNIEnv* env);
-extern int register_android_database_SQLiteQuery(JNIEnv* env);
-extern int register_android_database_SQLiteStatement(JNIEnv* env);
extern int register_android_debug_JNITest(JNIEnv* env);
extern int register_android_nio_utils(JNIEnv* env);
extern int register_android_text_format_Time(JNIEnv* env);
@@ -1141,12 +1138,9 @@ static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_graphics_YuvImage),
REG_JNI(register_android_database_CursorWindow),
- REG_JNI(register_android_database_SQLiteCompiledSql),
- REG_JNI(register_android_database_SQLiteDatabase),
+ REG_JNI(register_android_database_SQLiteConnection),
+ REG_JNI(register_android_database_SQLiteGlobal),
REG_JNI(register_android_database_SQLiteDebug),
- REG_JNI(register_android_database_SQLiteProgram),
- REG_JNI(register_android_database_SQLiteQuery),
- REG_JNI(register_android_database_SQLiteStatement),
REG_JNI(register_android_os_Debug),
REG_JNI(register_android_os_FileObserver),
REG_JNI(register_android_os_FileUtils),
diff --git a/core/jni/android_database_CursorWindow.cpp b/core/jni/android_database_CursorWindow.cpp
index 659fa35..d53644d 100644
--- a/core/jni/android_database_CursorWindow.cpp
+++ b/core/jni/android_database_CursorWindow.cpp
@@ -31,8 +31,8 @@
#include <unistd.h>
#include "binder/CursorWindow.h"
-#include "sqlite3_exception.h"
#include "android_util_Binder.h"
+#include "android_database_SQLiteCommon.h"
namespace android {
diff --git a/core/jni/android_database_SQLiteCommon.cpp b/core/jni/android_database_SQLiteCommon.cpp
new file mode 100644
index 0000000..d5fdb15
--- /dev/null
+++ b/core/jni/android_database_SQLiteCommon.cpp
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+
+#include "android_database_SQLiteCommon.h"
+
+namespace android {
+
+/* throw a SQLiteException with a message appropriate for the error in handle */
+void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle) {
+ throw_sqlite3_exception(env, handle, NULL);
+}
+
+/* throw a SQLiteException with the given message */
+void throw_sqlite3_exception(JNIEnv* env, const char* message) {
+ throw_sqlite3_exception(env, NULL, message);
+}
+
+/* throw a SQLiteException with a message appropriate for the error in handle
+ concatenated with the given message
+ */
+void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle, const char* message) {
+ if (handle) {
+ throw_sqlite3_exception(env, sqlite3_errcode(handle),
+ sqlite3_errmsg(handle), message);
+ } else {
+ // we use SQLITE_OK so that a generic SQLiteException is thrown;
+ // any code not specified in the switch statement below would do.
+ throw_sqlite3_exception(env, SQLITE_OK, "unknown error", message);
+ }
+}
+
+/* throw a SQLiteException for a given error code */
+void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* message) {
+ if (errcode == SQLITE_DONE) {
+ throw_sqlite3_exception(env, errcode, NULL, message);
+ } else {
+ char temp[21];
+ sprintf(temp, "error code %d", errcode);
+ throw_sqlite3_exception(env, errcode, temp, message);
+ }
+}
+
+/* throw a SQLiteException for a given error code, sqlite3message, and
+ user message
+ */
+void throw_sqlite3_exception(JNIEnv* env, int errcode,
+ const char* sqlite3Message, const char* message) {
+ const char* exceptionClass;
+ switch (errcode) {
+ case SQLITE_IOERR:
+ exceptionClass = "android/database/sqlite/SQLiteDiskIOException";
+ break;
+ case SQLITE_CORRUPT:
+ case SQLITE_NOTADB: // treat "unsupported file format" error as corruption also
+ exceptionClass = "android/database/sqlite/SQLiteDatabaseCorruptException";
+ break;
+ case SQLITE_CONSTRAINT:
+ exceptionClass = "android/database/sqlite/SQLiteConstraintException";
+ break;
+ case SQLITE_ABORT:
+ exceptionClass = "android/database/sqlite/SQLiteAbortException";
+ break;
+ case SQLITE_DONE:
+ exceptionClass = "android/database/sqlite/SQLiteDoneException";
+ break;
+ case SQLITE_FULL:
+ exceptionClass = "android/database/sqlite/SQLiteFullException";
+ break;
+ case SQLITE_MISUSE:
+ exceptionClass = "android/database/sqlite/SQLiteMisuseException";
+ break;
+ case SQLITE_PERM:
+ exceptionClass = "android/database/sqlite/SQLiteAccessPermException";
+ break;
+ case SQLITE_BUSY:
+ exceptionClass = "android/database/sqlite/SQLiteDatabaseLockedException";
+ break;
+ case SQLITE_LOCKED:
+ exceptionClass = "android/database/sqlite/SQLiteTableLockedException";
+ break;
+ case SQLITE_READONLY:
+ exceptionClass = "android/database/sqlite/SQLiteReadOnlyDatabaseException";
+ break;
+ case SQLITE_CANTOPEN:
+ exceptionClass = "android/database/sqlite/SQLiteCantOpenDatabaseException";
+ break;
+ case SQLITE_TOOBIG:
+ exceptionClass = "android/database/sqlite/SQLiteBlobTooBigException";
+ break;
+ case SQLITE_RANGE:
+ exceptionClass = "android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException";
+ break;
+ case SQLITE_NOMEM:
+ exceptionClass = "android/database/sqlite/SQLiteOutOfMemoryException";
+ break;
+ case SQLITE_MISMATCH:
+ exceptionClass = "android/database/sqlite/SQLiteDatatypeMismatchException";
+ break;
+ case SQLITE_UNCLOSED:
+ exceptionClass = "android/database/sqlite/SQLiteUnfinalizedObjectsException";
+ break;
+ default:
+ exceptionClass = "android/database/sqlite/SQLiteException";
+ break;
+ }
+
+ if (sqlite3Message != NULL && message != NULL) {
+ char* fullMessage = (char *)malloc(strlen(sqlite3Message) + strlen(message) + 3);
+ if (fullMessage != NULL) {
+ strcpy(fullMessage, sqlite3Message);
+ strcat(fullMessage, ": ");
+ strcat(fullMessage, message);
+ jniThrowException(env, exceptionClass, fullMessage);
+ free(fullMessage);
+ } else {
+ jniThrowException(env, exceptionClass, sqlite3Message);
+ }
+ } else if (sqlite3Message != NULL) {
+ jniThrowException(env, exceptionClass, sqlite3Message);
+ } else {
+ jniThrowException(env, exceptionClass, message);
+ }
+}
+
+
+} // namespace android
diff --git a/core/jni/android_database_SQLiteCommon.h b/core/jni/android_database_SQLiteCommon.h
new file mode 100644
index 0000000..0cac176
--- /dev/null
+++ b/core/jni/android_database_SQLiteCommon.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2007 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.
+ */
+
+#ifndef _ANDROID_DATABASE_SQLITE_COMMON_H
+#define _ANDROID_DATABASE_SQLITE_COMMON_H
+
+#include <jni.h>
+#include <JNIHelp.h>
+
+#include <sqlite3.h>
+
+// Special log tags defined in SQLiteDebug.java.
+#define SQLITE_LOG_TAG "SQLiteLog"
+#define SQLITE_TRACE_TAG "SQLiteStatements"
+#define SQLITE_PROFILE_TAG "SQLiteTime"
+
+namespace android {
+
+/* throw a SQLiteException with a message appropriate for the error in handle */
+void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle);
+
+/* throw a SQLiteException with the given message */
+void throw_sqlite3_exception(JNIEnv* env, const char* message);
+
+/* throw a SQLiteException with a message appropriate for the error in handle
+ concatenated with the given message
+ */
+void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle, const char* message);
+
+/* throw a SQLiteException for a given error code */
+void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* message);
+
+void throw_sqlite3_exception(JNIEnv* env, int errcode,
+ const char* sqlite3Message, const char* message);
+
+}
+
+#endif // _ANDROID_DATABASE_SQLITE_COMMON_H
diff --git a/core/jni/android_database_SQLiteCompiledSql.cpp b/core/jni/android_database_SQLiteCompiledSql.cpp
deleted file mode 100644
index 857267a..0000000
--- a/core/jni/android_database_SQLiteCompiledSql.cpp
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2006-2008 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.
- */
-
-#undef LOG_TAG
-#define LOG_TAG "Cursor"
-
-#include <jni.h>
-#include <JNIHelp.h>
-#include <android_runtime/AndroidRuntime.h>
-
-#include <sqlite3.h>
-
-#include <utils/Log.h>
-
-#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "sqlite3_exception.h"
-
-
-namespace android {
-
-static jfieldID gHandleField;
-static jfieldID gStatementField;
-
-
-#define GET_STATEMENT(env, object) \
- (sqlite3_stmt *)env->GetIntField(object, gStatementField)
-#define GET_HANDLE(env, object) \
- (sqlite3 *)env->GetIntField(object, gHandleField)
-
-
-sqlite3_stmt * compile(JNIEnv* env, jobject object,
- sqlite3 * handle, jstring sqlString)
-{
- int err;
- jchar const * sql;
- jsize sqlLen;
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
-
- // Make sure not to leak the statement if it already exists
- if (statement != NULL) {
- sqlite3_finalize(statement);
- env->SetIntField(object, gStatementField, 0);
- }
-
- // Compile the SQL
- sql = env->GetStringChars(sqlString, NULL);
- sqlLen = env->GetStringLength(sqlString);
- err = sqlite3_prepare16_v2(handle, sql, sqlLen * 2, &statement, NULL);
- env->ReleaseStringChars(sqlString, sql);
-
- if (err == SQLITE_OK) {
- // Store the statement in the Java object for future calls
- ALOGV("Prepared statement %p on %p", statement, handle);
- env->SetIntField(object, gStatementField, (int)statement);
- return statement;
- } else {
- // Error messages like 'near ")": syntax error' are not
- // always helpful enough, so construct an error string that
- // includes the query itself.
- const char *query = env->GetStringUTFChars(sqlString, NULL);
- char *message = (char*) malloc(strlen(query) + 50);
- if (message) {
- strcpy(message, ", while compiling: "); // less than 50 chars
- strcat(message, query);
- }
- env->ReleaseStringUTFChars(sqlString, query);
- throw_sqlite3_exception(env, handle, message);
- free(message);
- return NULL;
- }
-}
-
-static void native_compile(JNIEnv* env, jobject object, jstring sqlString)
-{
- compile(env, object, GET_HANDLE(env, object), sqlString);
-}
-
-
-static JNINativeMethod sMethods[] =
-{
- /* name, signature, funcPtr */
- {"native_compile", "(Ljava/lang/String;)V", (void *)native_compile},
-};
-
-int register_android_database_SQLiteCompiledSql(JNIEnv * env)
-{
- jclass clazz;
-
- clazz = env->FindClass("android/database/sqlite/SQLiteCompiledSql");
- if (clazz == NULL) {
- ALOGE("Can't find android/database/sqlite/SQLiteCompiledSql");
- return -1;
- }
-
- gHandleField = env->GetFieldID(clazz, "nHandle", "I");
- gStatementField = env->GetFieldID(clazz, "nStatement", "I");
-
- if (gHandleField == NULL || gStatementField == NULL) {
- ALOGE("Error locating fields");
- return -1;
- }
-
- return AndroidRuntime::registerNativeMethods(env,
- "android/database/sqlite/SQLiteCompiledSql", sMethods, NELEM(sMethods));
-}
-
-} // namespace android
diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp
new file mode 100644
index 0000000..d0d53f6
--- /dev/null
+++ b/core/jni/android_database_SQLiteConnection.cpp
@@ -0,0 +1,959 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "SQLiteConnection"
+
+#include <jni.h>
+#include <JNIHelp.h>
+#include <android_runtime/AndroidRuntime.h>
+
+#include <utils/Log.h>
+#include <utils/String8.h>
+#include <utils/String16.h>
+#include <cutils/ashmem.h>
+#include <sys/mman.h>
+
+#include <string.h>
+#include <unistd.h>
+
+#include "binder/CursorWindow.h"
+
+#include <sqlite3.h>
+#include <sqlite3_android.h>
+
+#include "android_database_SQLiteCommon.h"
+
+#define UTF16_STORAGE 0
+#define ANDROID_TABLE "android_metadata"
+
+namespace android {
+
+static struct {
+ jfieldID name;
+ jfieldID numArgs;
+ jmethodID dispatchCallback;
+} gSQLiteCustomFunctionClassInfo;
+
+static struct {
+ jclass clazz;
+} gStringClassInfo;
+
+struct SQLiteConnection {
+ // Open flags.
+ // Must be kept in sync with the constants defined in SQLiteDatabase.java.
+ enum {
+ OPEN_READWRITE = 0x00000000,
+ OPEN_READONLY = 0x00000001,
+ OPEN_READ_MASK = 0x00000001,
+ NO_LOCALIZED_COLLATORS = 0x00000010,
+ CREATE_IF_NECESSARY = 0x10000000,
+ };
+
+ sqlite3* const db;
+ const int openFlags;
+ const String8 path;
+ const String8 label;
+
+ SQLiteConnection(sqlite3* db, int openFlags, const String8& path, const String8& label) :
+ db(db), openFlags(openFlags), path(path), label(label) { }
+};
+
+// Called each time a statement begins execution, when tracing is enabled.
+static void sqliteTraceCallback(void *data, const char *sql) {
+ SQLiteConnection* connection = static_cast<SQLiteConnection*>(data);
+ ALOG(LOG_VERBOSE, SQLITE_TRACE_TAG, "%s: \"%s\"\n",
+ connection->label.string(), sql);
+}
+
+// Called each time a statement finishes execution, when profiling is enabled.
+static void sqliteProfileCallback(void *data, const char *sql, sqlite3_uint64 tm) {
+ SQLiteConnection* connection = static_cast<SQLiteConnection*>(data);
+ ALOG(LOG_VERBOSE, SQLITE_PROFILE_TAG, "%s: \"%s\" took %0.3f ms\n",
+ connection->label.string(), sql, tm * 0.000001f);
+}
+
+
+static jint nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFlags,
+ jstring labelStr, jboolean enableTrace, jboolean enableProfile) {
+ int sqliteFlags;
+ if (openFlags & SQLiteConnection::CREATE_IF_NECESSARY) {
+ sqliteFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
+ } else if (openFlags & SQLiteConnection::OPEN_READONLY) {
+ sqliteFlags = SQLITE_OPEN_READONLY;
+ } else {
+ sqliteFlags = SQLITE_OPEN_READWRITE;
+ }
+
+ const char* pathChars = env->GetStringUTFChars(pathStr, NULL);
+ String8 path(pathChars);
+ env->ReleaseStringUTFChars(pathStr, pathChars);
+
+ const char* labelChars = env->GetStringUTFChars(labelStr, NULL);
+ String8 label(labelChars);
+ env->ReleaseStringUTFChars(labelStr, labelChars);
+
+ sqlite3* db;
+ int err = sqlite3_open_v2(path.string(), &db, sqliteFlags, NULL);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception_errcode(env, err, "Could not open database");
+ return 0;
+ }
+
+ // Set the default busy handler to retry for 1000ms and then return SQLITE_BUSY
+ err = sqlite3_busy_timeout(db, 1000 /* ms */);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, db, "Could not set busy timeout");
+ sqlite3_close(db);
+ return 0;
+ }
+
+ // Enable WAL auto-checkpointing after a commit whenever at least one frame is in the log.
+ // This ensures that a checkpoint will occur after each transaction if needed.
+ err = sqlite3_wal_autocheckpoint(db, 1);
+ if (err) {
+ throw_sqlite3_exception(env, db, "Could not enable auto-checkpointing.");
+ sqlite3_close(db);
+ return 0;
+ }
+
+ // Register custom Android functions.
+ err = register_android_functions(db, UTF16_STORAGE);
+ if (err) {
+ throw_sqlite3_exception(env, db, "Could not register Android SQL functions.");
+ sqlite3_close(db);
+ return 0;
+ }
+
+ // Create wrapper object.
+ SQLiteConnection* connection = new SQLiteConnection(db, openFlags, path, label);
+
+ // Enable tracing and profiling if requested.
+ if (enableTrace) {
+ sqlite3_trace(db, &sqliteTraceCallback, connection);
+ }
+ if (enableProfile) {
+ sqlite3_profile(db, &sqliteProfileCallback, connection);
+ }
+
+ ALOGV("Opened connection %p with label '%s'", db, label.string());
+ return reinterpret_cast<jint>(connection);
+}
+
+static void nativeClose(JNIEnv* env, jclass clazz, jint connectionPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+
+ if (connection) {
+ ALOGV("Closing connection %p", connection->db);
+ int err = sqlite3_close(connection->db);
+ if (err != SQLITE_OK) {
+ // This can happen if sub-objects aren't closed first. Make sure the caller knows.
+ ALOGE("sqlite3_close(%p) failed: %d", connection->db, err);
+ throw_sqlite3_exception(env, connection->db, "Count not close db.");
+ return;
+ }
+
+ delete connection;
+ }
+}
+
+// Called each time a custom function is evaluated.
+static void sqliteCustomFunctionCallback(sqlite3_context *context,
+ int argc, sqlite3_value **argv) {
+ JNIEnv* env = AndroidRuntime::getJNIEnv();
+
+ // Get the callback function object.
+ // Create a new local reference to it in case the callback tries to do something
+ // dumb like unregister the function (thereby destroying the global ref) while it is running.
+ jobject functionObjGlobal = reinterpret_cast<jobject>(sqlite3_user_data(context));
+ jobject functionObj = env->NewLocalRef(functionObjGlobal);
+
+ jobjectArray argsArray = env->NewObjectArray(argc, gStringClassInfo.clazz, NULL);
+ if (argsArray) {
+ for (int i = 0; i < argc; i++) {
+ const jchar* arg = static_cast<const jchar*>(sqlite3_value_text16(argv[i]));
+ if (!arg) {
+ ALOGW("NULL argument in custom_function_callback. This should not happen.");
+ } else {
+ size_t argLen = sqlite3_value_bytes16(argv[i]) / sizeof(jchar);
+ jstring argStr = env->NewString(arg, argLen);
+ if (!argStr) {
+ goto error; // out of memory error
+ }
+ env->SetObjectArrayElement(argsArray, i, argStr);
+ env->DeleteLocalRef(argStr);
+ }
+ }
+
+ // TODO: Support functions that return values.
+ env->CallVoidMethod(functionObj,
+ gSQLiteCustomFunctionClassInfo.dispatchCallback, argsArray);
+
+error:
+ env->DeleteLocalRef(argsArray);
+ }
+
+ env->DeleteLocalRef(functionObj);
+
+ if (env->ExceptionCheck()) {
+ ALOGE("An exception was thrown by custom SQLite function.");
+ LOGE_EX(env);
+ env->ExceptionClear();
+ }
+}
+
+// Called when a custom function is destroyed.
+static void sqliteCustomFunctionDestructor(void* data) {
+ jobject functionObjGlobal = reinterpret_cast<jobject>(data);
+
+ JNIEnv* env = AndroidRuntime::getJNIEnv();
+ env->DeleteGlobalRef(functionObjGlobal);
+}
+
+static void nativeRegisterCustomFunction(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jobject functionObj) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+
+ jstring nameStr = jstring(env->GetObjectField(
+ functionObj, gSQLiteCustomFunctionClassInfo.name));
+ jint numArgs = env->GetIntField(functionObj, gSQLiteCustomFunctionClassInfo.numArgs);
+
+ jobject functionObjGlobal = env->NewGlobalRef(functionObj);
+
+ const char* name = env->GetStringUTFChars(nameStr, NULL);
+ int err = sqlite3_create_function_v2(connection->db, name, numArgs, SQLITE_UTF16,
+ reinterpret_cast<void*>(functionObjGlobal),
+ &sqliteCustomFunctionCallback, NULL, NULL, &sqliteCustomFunctionDestructor);
+ env->ReleaseStringUTFChars(nameStr, name);
+
+ if (err != SQLITE_OK) {
+ ALOGE("sqlite3_create_function returned %d", err);
+ env->DeleteGlobalRef(functionObjGlobal);
+ throw_sqlite3_exception(env, connection->db);
+ return;
+ }
+}
+
+// Set locale in the android_metadata table, install localized collators, and rebuild indexes
+static void nativeSetLocale(JNIEnv* env, jclass clazz, jint connectionPtr, jstring localeStr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+
+ if (connection->openFlags & SQLiteConnection::NO_LOCALIZED_COLLATORS) {
+ // We should probably throw IllegalStateException but the contract for
+ // setLocale says that we just do nothing. Oh well.
+ return;
+ }
+
+ int err;
+ char const* locale = env->GetStringUTFChars(localeStr, NULL);
+ sqlite3_stmt* stmt = NULL;
+ char** meta = NULL;
+ int rowCount, colCount;
+ char* dbLocale = NULL;
+
+ // create the table, if necessary and possible
+ if (!(connection->openFlags & SQLiteConnection::OPEN_READONLY)) {
+ err = sqlite3_exec(connection->db,
+ "CREATE TABLE IF NOT EXISTS " ANDROID_TABLE " (locale TEXT)",
+ NULL, NULL, NULL);
+ if (err != SQLITE_OK) {
+ ALOGE("CREATE TABLE " ANDROID_TABLE " failed");
+ throw_sqlite3_exception(env, connection->db);
+ goto done;
+ }
+ }
+
+ // try to read from the table
+ err = sqlite3_get_table(connection->db,
+ "SELECT locale FROM " ANDROID_TABLE " LIMIT 1",
+ &meta, &rowCount, &colCount, NULL);
+ if (err != SQLITE_OK) {
+ ALOGE("SELECT locale FROM " ANDROID_TABLE " failed");
+ throw_sqlite3_exception(env, connection->db);
+ goto done;
+ }
+
+ dbLocale = (rowCount >= 1) ? meta[colCount] : NULL;
+
+ if (dbLocale != NULL && !strcmp(dbLocale, locale)) {
+ // database locale is the same as the desired locale; set up the collators and go
+ err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db);
+ }
+ goto done; // no database changes needed
+ }
+
+ if (connection->openFlags & SQLiteConnection::OPEN_READONLY) {
+ // read-only database, so we're going to have to put up with whatever we got
+ // For registering new index. Not for modifing the read-only database.
+ err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db);
+ }
+ goto done;
+ }
+
+ // need to update android_metadata and indexes atomically, so use a transaction...
+ err = sqlite3_exec(connection->db, "BEGIN TRANSACTION", NULL, NULL, NULL);
+ if (err != SQLITE_OK) {
+ ALOGE("BEGIN TRANSACTION failed setting locale");
+ throw_sqlite3_exception(env, connection->db);
+ goto done;
+ }
+
+ err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
+ if (err != SQLITE_OK) {
+ ALOGE("register_localized_collators() failed setting locale");
+ throw_sqlite3_exception(env, connection->db);
+ goto rollback;
+ }
+
+ err = sqlite3_exec(connection->db, "DELETE FROM " ANDROID_TABLE, NULL, NULL, NULL);
+ if (err != SQLITE_OK) {
+ ALOGE("DELETE failed setting locale");
+ throw_sqlite3_exception(env, connection->db);
+ goto rollback;
+ }
+
+ static const char *sql = "INSERT INTO " ANDROID_TABLE " (locale) VALUES(?);";
+ err = sqlite3_prepare_v2(connection->db, sql, -1, &stmt, NULL);
+ if (err != SQLITE_OK) {
+ ALOGE("sqlite3_prepare_v2(\"%s\") failed", sql);
+ throw_sqlite3_exception(env, connection->db);
+ goto rollback;
+ }
+
+ err = sqlite3_bind_text(stmt, 1, locale, -1, SQLITE_TRANSIENT);
+ if (err != SQLITE_OK) {
+ ALOGE("sqlite3_bind_text() failed setting locale");
+ throw_sqlite3_exception(env, connection->db);
+ goto rollback;
+ }
+
+ err = sqlite3_step(stmt);
+ if (err != SQLITE_OK && err != SQLITE_DONE) {
+ ALOGE("sqlite3_step(\"%s\") failed setting locale", sql);
+ throw_sqlite3_exception(env, connection->db);
+ goto rollback;
+ }
+
+ err = sqlite3_exec(connection->db, "REINDEX LOCALIZED", NULL, NULL, NULL);
+ if (err != SQLITE_OK) {
+ ALOGE("REINDEX LOCALIZED failed");
+ throw_sqlite3_exception(env, connection->db);
+ goto rollback;
+ }
+
+ // all done, yay!
+ err = sqlite3_exec(connection->db, "COMMIT TRANSACTION", NULL, NULL, NULL);
+ if (err != SQLITE_OK) {
+ ALOGE("COMMIT TRANSACTION failed setting locale");
+ throw_sqlite3_exception(env, connection->db);
+ goto done;
+ }
+
+rollback:
+ if (err != SQLITE_OK) {
+ sqlite3_exec(connection->db, "ROLLBACK TRANSACTION", NULL, NULL, NULL);
+ }
+
+done:
+ if (stmt) {
+ sqlite3_finalize(stmt);
+ }
+ if (meta) {
+ sqlite3_free_table(meta);
+ }
+ if (locale) {
+ env->ReleaseStringUTFChars(localeStr, locale);
+ }
+}
+
+static jint nativePrepareStatement(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jstring sqlString) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+
+ jsize sqlLength = env->GetStringLength(sqlString);
+ const jchar* sql = env->GetStringCritical(sqlString, NULL);
+ sqlite3_stmt* statement;
+ int err = sqlite3_prepare16_v2(connection->db,
+ sql, sqlLength * sizeof(jchar), &statement, NULL);
+ env->ReleaseStringCritical(sqlString, sql);
+
+ if (err != SQLITE_OK) {
+ // Error messages like 'near ")": syntax error' are not
+ // always helpful enough, so construct an error string that
+ // includes the query itself.
+ const char *query = env->GetStringUTFChars(sqlString, NULL);
+ char *message = (char*) malloc(strlen(query) + 50);
+ if (message) {
+ strcpy(message, ", while compiling: "); // less than 50 chars
+ strcat(message, query);
+ }
+ env->ReleaseStringUTFChars(sqlString, query);
+ throw_sqlite3_exception(env, connection->db, message);
+ free(message);
+ return 0;
+ }
+
+ ALOGV("Prepared statement %p on connection %p", statement, connection->db);
+ return reinterpret_cast<jint>(statement);
+}
+
+static void nativeFinalizeStatement(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ ALOGV("Finalized statement %p on connection %p", statement, connection->db);
+ int err = sqlite3_finalize(statement);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static jint nativeGetParameterCount(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ return sqlite3_bind_parameter_count(statement);
+}
+
+static jboolean nativeIsReadOnly(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ return sqlite3_stmt_readonly(statement) != 0;
+}
+
+static jint nativeGetColumnCount(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ return sqlite3_column_count(statement);
+}
+
+static jstring nativeGetColumnName(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr, jint index) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ const jchar* name = static_cast<const jchar*>(sqlite3_column_name16(statement, index));
+ if (name) {
+ size_t length = 0;
+ while (name[length]) {
+ length += 1;
+ }
+ return env->NewString(name, length);
+ }
+ return NULL;
+}
+
+static void nativeBindNull(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr, jint index) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = sqlite3_bind_null(statement, index);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeBindLong(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr, jint index, jlong value) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = sqlite3_bind_int64(statement, index, value);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeBindDouble(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr, jint index, jdouble value) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = sqlite3_bind_double(statement, index, value);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeBindString(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr, jint index, jstring valueString) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ jsize valueLength = env->GetStringLength(valueString);
+ const jchar* value = env->GetStringCritical(valueString, NULL);
+ int err = sqlite3_bind_text16(statement, index, value, valueLength * sizeof(jchar),
+ SQLITE_TRANSIENT);
+ env->ReleaseStringCritical(valueString, value);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeBindBlob(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr, jint index, jbyteArray valueArray) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ jsize valueLength = env->GetArrayLength(valueArray);
+ jbyte* value = static_cast<jbyte*>(env->GetPrimitiveArrayCritical(valueArray, NULL));
+ int err = sqlite3_bind_blob(statement, index, value, valueLength, SQLITE_TRANSIENT);
+ env->ReleasePrimitiveArrayCritical(valueArray, value, JNI_ABORT);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeResetStatementAndClearBindings(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = sqlite3_reset(statement);
+ if (err == SQLITE_OK) {
+ err = sqlite3_clear_bindings(statement);
+ }
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static int executeNonQuery(JNIEnv* env, SQLiteConnection* connection, sqlite3_stmt* statement) {
+ int err = sqlite3_step(statement);
+ if (err == SQLITE_ROW) {
+ throw_sqlite3_exception(env,
+ "Queries can be performed using SQLiteDatabase query or rawQuery methods only.");
+ } else if (err != SQLITE_DONE) {
+ throw_sqlite3_exception_errcode(env, err, sqlite3_errmsg(connection->db));
+ }
+ return err;
+}
+
+static void nativeExecute(JNIEnv* env, jclass clazz, jint connectionPtr,
+ jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ executeNonQuery(env, connection, statement);
+}
+
+static jint nativeExecuteForChangedRowCount(JNIEnv* env, jclass clazz,
+ jint connectionPtr, jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = executeNonQuery(env, connection, statement);
+ return err == SQLITE_DONE ? sqlite3_changes(connection->db) : -1;
+}
+
+static jlong nativeExecuteForLastInsertedRowId(JNIEnv* env, jclass clazz,
+ jint connectionPtr, jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = executeNonQuery(env, connection, statement);
+ return err == SQLITE_DONE && sqlite3_changes(connection->db) > 0
+ ? sqlite3_last_insert_rowid(connection->db) : -1;
+}
+
+static int executeOneRowQuery(JNIEnv* env, SQLiteConnection* connection, sqlite3_stmt* statement) {
+ int err = sqlite3_step(statement);
+ if (err != SQLITE_ROW) {
+ throw_sqlite3_exception_errcode(env, err, sqlite3_errmsg(connection->db));
+ }
+ return err;
+}
+
+static jlong nativeExecuteForLong(JNIEnv* env, jclass clazz,
+ jint connectionPtr, jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = executeOneRowQuery(env, connection, statement);
+ if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
+ return sqlite3_column_int64(statement, 0);
+ }
+ return -1;
+}
+
+static jstring nativeExecuteForString(JNIEnv* env, jclass clazz,
+ jint connectionPtr, jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = executeOneRowQuery(env, connection, statement);
+ if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
+ const jchar* text = static_cast<const jchar*>(sqlite3_column_text16(statement, 0));
+ if (text) {
+ size_t length = sqlite3_column_bytes16(statement, 0) / sizeof(jchar);
+ return env->NewString(text, length);
+ }
+ }
+ return NULL;
+}
+
+static int createAshmemRegionWithData(JNIEnv* env, const void* data, size_t length) {
+ int error = 0;
+ int fd = ashmem_create_region(NULL, length);
+ if (fd < 0) {
+ error = errno;
+ ALOGE("ashmem_create_region failed: %s", strerror(error));
+ } else {
+ if (length > 0) {
+ void* ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (ptr == MAP_FAILED) {
+ error = errno;
+ ALOGE("mmap failed: %s", strerror(error));
+ } else {
+ memcpy(ptr, data, length);
+ munmap(ptr, length);
+ }
+ }
+
+ if (!error) {
+ if (ashmem_set_prot_region(fd, PROT_READ) < 0) {
+ error = errno;
+ ALOGE("ashmem_set_prot_region failed: %s", strerror(errno));
+ } else {
+ return fd;
+ }
+ }
+
+ close(fd);
+ }
+
+ jniThrowIOException(env, error);
+ return -1;
+}
+
+static jint nativeExecuteForBlobFileDescriptor(JNIEnv* env, jclass clazz,
+ jint connectionPtr, jint statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+
+ int err = executeOneRowQuery(env, connection, statement);
+ if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
+ const void* blob = sqlite3_column_blob(statement, 0);
+ if (blob) {
+ int length = sqlite3_column_bytes(statement, 0);
+ if (length >= 0) {
+ return createAshmemRegionWithData(env, blob, length);
+ }
+ }
+ }
+ return -1;
+}
+
+enum CopyRowResult {
+ CPR_OK,
+ CPR_FULL,
+ CPR_ERROR,
+};
+
+static CopyRowResult copyRow(JNIEnv* env, CursorWindow* window,
+ sqlite3_stmt* statement, int numColumns, int startPos, int addedRows) {
+ // Allocate a new field directory for the row.
+ status_t status = window->allocRow();
+ if (status) {
+ LOG_WINDOW("Failed allocating fieldDir at startPos %d row %d, error=%d",
+ startPos, addedRows, status);
+ return CPR_FULL;
+ }
+
+ // Pack the row into the window.
+ CopyRowResult result = CPR_OK;
+ for (int i = 0; i < numColumns; i++) {
+ int type = sqlite3_column_type(statement, i);
+ if (type == SQLITE_TEXT) {
+ // TEXT data
+ const char* text = reinterpret_cast<const char*>(
+ sqlite3_column_text(statement, i));
+ // SQLite does not include the NULL terminator in size, but does
+ // ensure all strings are NULL terminated, so increase size by
+ // one to make sure we store the terminator.
+ size_t sizeIncludingNull = sqlite3_column_bytes(statement, i) + 1;
+ status = window->putString(addedRows, i, text, sizeIncludingNull);
+ if (status) {
+ LOG_WINDOW("Failed allocating %u bytes for text at %d,%d, error=%d",
+ sizeIncludingNull, startPos + addedRows, i, status);
+ result = CPR_FULL;
+ break;
+ }
+ LOG_WINDOW("%d,%d is TEXT with %u bytes",
+ startPos + addedRows, i, sizeIncludingNull);
+ } else if (type == SQLITE_INTEGER) {
+ // INTEGER data
+ int64_t value = sqlite3_column_int64(statement, i);
+ status = window->putLong(addedRows, i, value);
+ if (status) {
+ LOG_WINDOW("Failed allocating space for a long in column %d, error=%d",
+ i, status);
+ result = CPR_FULL;
+ break;
+ }
+ LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + addedRows, i, value);
+ } else if (type == SQLITE_FLOAT) {
+ // FLOAT data
+ double value = sqlite3_column_double(statement, i);
+ status = window->putDouble(addedRows, i, value);
+ if (status) {
+ LOG_WINDOW("Failed allocating space for a double in column %d, error=%d",
+ i, status);
+ result = CPR_FULL;
+ break;
+ }
+ LOG_WINDOW("%d,%d is FLOAT %lf", startPos + addedRows, i, value);
+ } else if (type == SQLITE_BLOB) {
+ // BLOB data
+ const void* blob = sqlite3_column_blob(statement, i);
+ size_t size = sqlite3_column_bytes(statement, i);
+ status = window->putBlob(addedRows, i, blob, size);
+ if (status) {
+ LOG_WINDOW("Failed allocating %u bytes for blob at %d,%d, error=%d",
+ size, startPos + addedRows, i, status);
+ result = CPR_FULL;
+ break;
+ }
+ LOG_WINDOW("%d,%d is Blob with %u bytes",
+ startPos + addedRows, i, size);
+ } else if (type == SQLITE_NULL) {
+ // NULL field
+ status = window->putNull(addedRows, i);
+ if (status) {
+ LOG_WINDOW("Failed allocating space for a null in column %d, error=%d",
+ i, status);
+ result = CPR_FULL;
+ break;
+ }
+
+ LOG_WINDOW("%d,%d is NULL", startPos + addedRows, i);
+ } else {
+ // Unknown data
+ ALOGE("Unknown column type when filling database window");
+ throw_sqlite3_exception(env, "Unknown column type when filling window");
+ result = CPR_ERROR;
+ break;
+ }
+ }
+
+ // Free the last row if if was not successfully copied.
+ if (result != CPR_OK) {
+ window->freeLastRow();
+ }
+ return result;
+}
+
+static jlong nativeExecuteForCursorWindow(JNIEnv* env, jclass clazz,
+ jint connectionPtr, jint statementPtr, jint windowPtr,
+ jint startPos, jint requiredPos, jboolean countAllRows) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
+ CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
+
+ status_t status = window->clear();
+ if (status) {
+ String8 msg;
+ msg.appendFormat("Failed to clear the cursor window, status=%d", status);
+ throw_sqlite3_exception(env, connection->db, msg.string());
+ return 0;
+ }
+
+ int numColumns = sqlite3_column_count(statement);
+ status = window->setNumColumns(numColumns);
+ if (status) {
+ String8 msg;
+ msg.appendFormat("Failed to set the cursor window column count to %d, status=%d",
+ numColumns, status);
+ throw_sqlite3_exception(env, connection->db, msg.string());
+ return 0;
+ }
+
+ int retryCount = 0;
+ int totalRows = 0;
+ int addedRows = 0;
+ bool windowFull = false;
+ bool gotException = false;
+ while (!gotException && (!windowFull || countAllRows)) {
+ int err = sqlite3_step(statement);
+ if (err == SQLITE_ROW) {
+ LOG_WINDOW("Stepped statement %p to row %d", statement, totalRows);
+ retryCount = 0;
+ totalRows += 1;
+
+ // Skip the row if the window is full or we haven't reached the start position yet.
+ if (startPos >= totalRows || windowFull) {
+ continue;
+ }
+
+ CopyRowResult cpr = copyRow(env, window, statement, numColumns, startPos, addedRows);
+ if (cpr == CPR_FULL && addedRows && startPos + addedRows < requiredPos) {
+ // We filled the window before we got to the one row that we really wanted.
+ // Clear the window and start filling it again from here.
+ // TODO: Would be nicer if we could progressively replace earlier rows.
+ window->clear();
+ window->setNumColumns(numColumns);
+ startPos += addedRows;
+ addedRows = 0;
+ cpr = copyRow(env, window, statement, numColumns, startPos, addedRows);
+ }
+
+ if (cpr == CPR_OK) {
+ addedRows += 1;
+ } else if (cpr == CPR_FULL) {
+ windowFull = true;
+ } else {
+ gotException = true;
+ }
+ } else if (err == SQLITE_DONE) {
+ // All rows processed, bail
+ LOG_WINDOW("Processed all rows");
+ break;
+ } else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) {
+ // The table is locked, retry
+ LOG_WINDOW("Database locked, retrying");
+ if (retryCount > 50) {
+ ALOGE("Bailing on database busy retry");
+ throw_sqlite3_exception(env, connection->db, "retrycount exceeded");
+ gotException = true;
+ } else {
+ // Sleep to give the thread holding the lock a chance to finish
+ usleep(1000);
+ retryCount++;
+ }
+ } else {
+ throw_sqlite3_exception(env, connection->db);
+ gotException = true;
+ }
+ }
+
+ LOG_WINDOW("Resetting statement %p after fetching %d rows and adding %d rows"
+ "to the window in %d bytes",
+ statement, totalRows, addedRows, window->size() - window->freeSpace());
+ sqlite3_reset(statement);
+
+ // Report the total number of rows on request.
+ if (startPos > totalRows) {
+ ALOGE("startPos %d > actual rows %d", startPos, totalRows);
+ }
+ jlong result = jlong(startPos) << 32 | jlong(totalRows);
+ return result;
+}
+
+static jint nativeGetDbLookaside(JNIEnv* env, jobject clazz, jint connectionPtr) {
+ SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
+
+ int cur = -1;
+ int unused;
+ sqlite3_db_status(connection->db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur, &unused, 0);
+ return cur;
+}
+
+
+static JNINativeMethod sMethods[] =
+{
+ /* name, signature, funcPtr */
+ { "nativeOpen", "(Ljava/lang/String;ILjava/lang/String;ZZ)I",
+ (void*)nativeOpen },
+ { "nativeClose", "(I)V",
+ (void*)nativeClose },
+ { "nativeRegisterCustomFunction", "(ILandroid/database/sqlite/SQLiteCustomFunction;)V",
+ (void*)nativeRegisterCustomFunction },
+ { "nativeSetLocale", "(ILjava/lang/String;)V",
+ (void*)nativeSetLocale },
+ { "nativePrepareStatement", "(ILjava/lang/String;)I",
+ (void*)nativePrepareStatement },
+ { "nativeFinalizeStatement", "(II)V",
+ (void*)nativeFinalizeStatement },
+ { "nativeGetParameterCount", "(II)I",
+ (void*)nativeGetParameterCount },
+ { "nativeIsReadOnly", "(II)Z",
+ (void*)nativeIsReadOnly },
+ { "nativeGetColumnCount", "(II)I",
+ (void*)nativeGetColumnCount },
+ { "nativeGetColumnName", "(III)Ljava/lang/String;",
+ (void*)nativeGetColumnName },
+ { "nativeBindNull", "(III)V",
+ (void*)nativeBindNull },
+ { "nativeBindLong", "(IIIJ)V",
+ (void*)nativeBindLong },
+ { "nativeBindDouble", "(IIID)V",
+ (void*)nativeBindDouble },
+ { "nativeBindString", "(IIILjava/lang/String;)V",
+ (void*)nativeBindString },
+ { "nativeBindBlob", "(III[B)V",
+ (void*)nativeBindBlob },
+ { "nativeResetStatementAndClearBindings", "(II)V",
+ (void*)nativeResetStatementAndClearBindings },
+ { "nativeExecute", "(II)V",
+ (void*)nativeExecute },
+ { "nativeExecuteForLong", "(II)J",
+ (void*)nativeExecuteForLong },
+ { "nativeExecuteForString", "(II)Ljava/lang/String;",
+ (void*)nativeExecuteForString },
+ { "nativeExecuteForBlobFileDescriptor", "(II)I",
+ (void*)nativeExecuteForBlobFileDescriptor },
+ { "nativeExecuteForChangedRowCount", "(II)I",
+ (void*)nativeExecuteForChangedRowCount },
+ { "nativeExecuteForLastInsertedRowId", "(II)J",
+ (void*)nativeExecuteForLastInsertedRowId },
+ { "nativeExecuteForCursorWindow", "(IIIIIZ)J",
+ (void*)nativeExecuteForCursorWindow },
+ { "nativeGetDbLookaside", "(I)I",
+ (void*)nativeGetDbLookaside },
+};
+
+#define FIND_CLASS(var, className) \
+ var = env->FindClass(className); \
+ LOG_FATAL_IF(! var, "Unable to find class " className);
+
+#define GET_METHOD_ID(var, clazz, methodName, fieldDescriptor) \
+ var = env->GetMethodID(clazz, methodName, fieldDescriptor); \
+ LOG_FATAL_IF(! var, "Unable to find method" methodName);
+
+#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \
+ var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \
+ LOG_FATAL_IF(! var, "Unable to find field " fieldName);
+
+int register_android_database_SQLiteConnection(JNIEnv *env)
+{
+ jclass clazz;
+ FIND_CLASS(clazz, "android/database/sqlite/SQLiteCustomFunction");
+
+ GET_FIELD_ID(gSQLiteCustomFunctionClassInfo.name, clazz,
+ "name", "Ljava/lang/String;");
+ GET_FIELD_ID(gSQLiteCustomFunctionClassInfo.numArgs, clazz,
+ "numArgs", "I");
+ GET_METHOD_ID(gSQLiteCustomFunctionClassInfo.dispatchCallback,
+ clazz, "dispatchCallback", "([Ljava/lang/String;)V");
+
+ FIND_CLASS(clazz, "java/lang/String");
+ gStringClassInfo.clazz = jclass(env->NewGlobalRef(clazz));
+
+ return AndroidRuntime::registerNativeMethods(env, "android/database/sqlite/SQLiteConnection",
+ sMethods, NELEM(sMethods));
+}
+
+} // namespace android
diff --git a/core/jni/android_database_SQLiteDatabase.cpp b/core/jni/android_database_SQLiteDatabase.cpp
deleted file mode 100644
index 28c421d..0000000
--- a/core/jni/android_database_SQLiteDatabase.cpp
+++ /dev/null
@@ -1,636 +0,0 @@
-/*
- * Copyright (C) 2006-2007 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.
- */
-
-#undef LOG_TAG
-#define LOG_TAG "SqliteDatabaseCpp"
-
-#include <utils/Log.h>
-#include <utils/String8.h>
-#include <utils/String16.h>
-
-#include <jni.h>
-#include <JNIHelp.h>
-#include <android_runtime/AndroidRuntime.h>
-
-#include <sqlite3.h>
-#include <sqlite3_android.h>
-#include <string.h>
-#include <utils/Log.h>
-#include <utils/threads.h>
-#include <utils/List.h>
-#include <utils/Errors.h>
-#include <ctype.h>
-
-#include <stdio.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <netinet/in.h>
-#include <string.h>
-#include <netdb.h>
-#include <sys/ioctl.h>
-
-#include "sqlite3_exception.h"
-
-#define UTF16_STORAGE 0
-#define INVALID_VERSION -1
-#define ANDROID_TABLE "android_metadata"
-/* uncomment the next line to force-enable logging of all statements */
-// #define DB_LOG_STATEMENTS
-
-#define DEBUG_JNI 0
-
-namespace android {
-
-enum {
- OPEN_READWRITE = 0x00000000,
- OPEN_READONLY = 0x00000001,
- OPEN_READ_MASK = 0x00000001,
- NO_LOCALIZED_COLLATORS = 0x00000010,
- CREATE_IF_NECESSARY = 0x10000000
-};
-
-static jfieldID offset_db_handle;
-static jmethodID method_custom_function_callback;
-static jclass string_class;
-static jint sSqliteSoftHeapLimit = 0;
-
-static char *createStr(const char *path, short extra) {
- int len = strlen(path) + extra;
- char *str = (char *)malloc(len + 1);
- strncpy(str, path, len);
- str[len] = NULL;
- return str;
-}
-
-static void sqlLogger(void *databaseName, int iErrCode, const char *zMsg) {
- // skip printing this message if it is due to certain types of errors
- if (iErrCode == 0 || iErrCode == SQLITE_CONSTRAINT) return;
- // print databasename, errorcode and msg
- ALOGI("sqlite returned: error code = %d, msg = %s, db=%s\n", iErrCode, zMsg, databaseName);
-}
-
-// register the logging func on sqlite. needs to be done BEFORE any sqlite3 func is called.
-static void registerLoggingFunc(const char *path) {
- static bool loggingFuncSet = false;
- if (loggingFuncSet) {
- return;
- }
-
- ALOGV("Registering sqlite logging func \n");
- int err = sqlite3_config(SQLITE_CONFIG_LOG, &sqlLogger, (void *)createStr(path, 0));
- if (err != SQLITE_OK) {
- ALOGW("sqlite returned error = %d when trying to register logging func.\n", err);
- return;
- }
- loggingFuncSet = true;
-}
-
-/* public native void dbopen(String path, int flags, String locale); */
-static void dbopen(JNIEnv* env, jobject object, jstring pathString, jint flags)
-{
- int err;
- sqlite3 * handle = NULL;
- sqlite3_stmt * statement = NULL;
- char const * path8 = env->GetStringUTFChars(pathString, NULL);
- int sqliteFlags;
-
- // register the logging func on sqlite. needs to be done BEFORE any sqlite3 func is called.
- registerLoggingFunc(path8);
-
- // convert our flags into the sqlite flags
- if (flags & CREATE_IF_NECESSARY) {
- sqliteFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
- } else if (flags & OPEN_READONLY) {
- sqliteFlags = SQLITE_OPEN_READONLY;
- } else {
- sqliteFlags = SQLITE_OPEN_READWRITE;
- }
-
- err = sqlite3_open_v2(path8, &handle, sqliteFlags, NULL);
- if (err != SQLITE_OK) {
- ALOGE("sqlite3_open_v2(\"%s\", &handle, %d, NULL) failed\n", path8, sqliteFlags);
- throw_sqlite3_exception(env, handle);
- goto done;
- }
-
- // The soft heap limit prevents the page cache allocations from growing
- // beyond the given limit, no matter what the max page cache sizes are
- // set to. The limit does not, as of 3.5.0, affect any other allocations.
- sqlite3_soft_heap_limit(sSqliteSoftHeapLimit);
-
- // Set the default busy handler to retry for 1000ms and then return SQLITE_BUSY
- err = sqlite3_busy_timeout(handle, 1000 /* ms */);
- if (err != SQLITE_OK) {
- ALOGE("sqlite3_busy_timeout(handle, 1000) failed for \"%s\"\n", path8);
- throw_sqlite3_exception(env, handle);
- goto done;
- }
-
-#ifdef DB_INTEGRITY_CHECK
- static const char* integritySql = "pragma integrity_check(1);";
- err = sqlite3_prepare_v2(handle, integritySql, -1, &statement, NULL);
- if (err != SQLITE_OK) {
- ALOGE("sqlite_prepare_v2(handle, \"%s\") failed for \"%s\"\n", integritySql, path8);
- throw_sqlite3_exception(env, handle);
- goto done;
- }
-
- // first is OK or error message
- err = sqlite3_step(statement);
- if (err != SQLITE_ROW) {
- ALOGE("integrity check failed for \"%s\"\n", integritySql, path8);
- throw_sqlite3_exception(env, handle);
- goto done;
- } else {
- const char *text = (const char*)sqlite3_column_text(statement, 0);
- if (strcmp(text, "ok") != 0) {
- ALOGE("integrity check failed for \"%s\": %s\n", integritySql, path8, text);
- jniThrowException(env, "android/database/sqlite/SQLiteDatabaseCorruptException", text);
- goto done;
- }
- }
-#endif
-
- err = register_android_functions(handle, UTF16_STORAGE);
- if (err) {
- throw_sqlite3_exception(env, handle);
- goto done;
- }
-
- ALOGV("Opened '%s' - %p\n", path8, handle);
- env->SetIntField(object, offset_db_handle, (int) handle);
- handle = NULL; // The caller owns the handle now.
-
-done:
- // Release allocated resources
- if (path8 != NULL) env->ReleaseStringUTFChars(pathString, path8);
- if (statement != NULL) sqlite3_finalize(statement);
- if (handle != NULL) sqlite3_close(handle);
-}
-
-static char *getDatabaseName(JNIEnv* env, sqlite3 * handle, jstring databaseName, short connNum) {
- char const *path = env->GetStringUTFChars(databaseName, NULL);
- if (path == NULL) {
- ALOGE("Failure in getDatabaseName(). VM ran out of memory?\n");
- return NULL; // VM would have thrown OutOfMemoryError
- }
- char *dbNameStr = createStr(path, 4);
- if (connNum > 999) { // TODO: if number of pooled connections > 999, fix this line.
- connNum = -1;
- }
- sprintf(dbNameStr + strlen(path), "|%03d", connNum);
- env->ReleaseStringUTFChars(databaseName, path);
- return dbNameStr;
-}
-
-static void sqlTrace(void *databaseName, const char *sql) {
- ALOGI("sql_statement|%s|%s\n", (char *)databaseName, sql);
-}
-
-/* public native void enableSqlTracing(); */
-static void enableSqlTracing(JNIEnv* env, jobject object, jstring databaseName, jshort connType)
-{
- sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle);
- sqlite3_trace(handle, &sqlTrace, (void *)getDatabaseName(env, handle, databaseName, connType));
-}
-
-static void sqlProfile(void *databaseName, const char *sql, sqlite3_uint64 tm) {
- double d = tm/1000000.0;
- ALOGI("elapsedTime4Sql|%s|%.3f ms|%s\n", (char *)databaseName, d, sql);
-}
-
-/* public native void enableSqlProfiling(); */
-static void enableSqlProfiling(JNIEnv* env, jobject object, jstring databaseName, jshort connType)
-{
- sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle);
- sqlite3_profile(handle, &sqlProfile, (void *)getDatabaseName(env, handle, databaseName,
- connType));
-}
-
-/* public native void close(); */
-static void dbclose(JNIEnv* env, jobject object)
-{
- sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle);
-
- if (handle != NULL) {
- // release the memory associated with the traceFuncArg in enableSqlTracing function
- void *traceFuncArg = sqlite3_trace(handle, &sqlTrace, NULL);
- if (traceFuncArg != NULL) {
- free(traceFuncArg);
- }
- // release the memory associated with the traceFuncArg in enableSqlProfiling function
- traceFuncArg = sqlite3_profile(handle, &sqlProfile, NULL);
- if (traceFuncArg != NULL) {
- free(traceFuncArg);
- }
- ALOGV("Closing database: handle=%p\n", handle);
- int result = sqlite3_close(handle);
- if (result == SQLITE_OK) {
- ALOGV("Closed %p\n", handle);
- env->SetIntField(object, offset_db_handle, 0);
- } else {
- // This can happen if sub-objects aren't closed first. Make sure the caller knows.
- throw_sqlite3_exception(env, handle);
- ALOGE("sqlite3_close(%p) failed: %d\n", handle, result);
- }
- }
-}
-
-/* native int native_getDbLookaside(); */
-static jint native_getDbLookaside(JNIEnv* env, jobject object)
-{
- sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle);
- int pCur = -1;
- int unused;
- sqlite3_db_status(handle, SQLITE_DBSTATUS_LOOKASIDE_USED, &pCur, &unused, 0);
- return pCur;
-}
-
-/* set locale in the android_metadata table, install localized collators, and rebuild indexes */
-static void native_setLocale(JNIEnv* env, jobject object, jstring localeString, jint flags)
-{
- if ((flags & NO_LOCALIZED_COLLATORS)) return;
-
- int err;
- char const* locale8 = env->GetStringUTFChars(localeString, NULL);
- sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle);
- sqlite3_stmt* stmt = NULL;
- char** meta = NULL;
- int rowCount, colCount;
- char* dbLocale = NULL;
-
- // create the table, if necessary and possible
- if (!(flags & OPEN_READONLY)) {
- static const char *createSql ="CREATE TABLE IF NOT EXISTS " ANDROID_TABLE " (locale TEXT)";
- err = sqlite3_exec(handle, createSql, NULL, NULL, NULL);
- if (err != SQLITE_OK) {
- ALOGE("CREATE TABLE " ANDROID_TABLE " failed\n");
- throw_sqlite3_exception(env, handle);
- goto done;
- }
- }
-
- // try to read from the table
- static const char *selectSql = "SELECT locale FROM " ANDROID_TABLE " LIMIT 1";
- err = sqlite3_get_table(handle, selectSql, &meta, &rowCount, &colCount, NULL);
- if (err != SQLITE_OK) {
- ALOGE("SELECT locale FROM " ANDROID_TABLE " failed\n");
- throw_sqlite3_exception(env, handle);
- goto done;
- }
-
- dbLocale = (rowCount >= 1) ? meta[colCount] : NULL;
-
- if (dbLocale != NULL && !strcmp(dbLocale, locale8)) {
- // database locale is the same as the desired locale; set up the collators and go
- err = register_localized_collators(handle, locale8, UTF16_STORAGE);
- if (err != SQLITE_OK) throw_sqlite3_exception(env, handle);
- goto done; // no database changes needed
- }
-
- if ((flags & OPEN_READONLY)) {
- // read-only database, so we're going to have to put up with whatever we got
- // For registering new index. Not for modifing the read-only database.
- err = register_localized_collators(handle, locale8, UTF16_STORAGE);
- if (err != SQLITE_OK) throw_sqlite3_exception(env, handle);
- goto done;
- }
-
- // need to update android_metadata and indexes atomically, so use a transaction...
- err = sqlite3_exec(handle, "BEGIN TRANSACTION", NULL, NULL, NULL);
- if (err != SQLITE_OK) {
- ALOGE("BEGIN TRANSACTION failed setting locale\n");
- throw_sqlite3_exception(env, handle);
- goto done;
- }
-
- err = register_localized_collators(handle, locale8, UTF16_STORAGE);
- if (err != SQLITE_OK) {
- ALOGE("register_localized_collators() failed setting locale\n");
- throw_sqlite3_exception(env, handle);
- goto rollback;
- }
-
- err = sqlite3_exec(handle, "DELETE FROM " ANDROID_TABLE, NULL, NULL, NULL);
- if (err != SQLITE_OK) {
- ALOGE("DELETE failed setting locale\n");
- throw_sqlite3_exception(env, handle);
- goto rollback;
- }
-
- static const char *sql = "INSERT INTO " ANDROID_TABLE " (locale) VALUES(?);";
- err = sqlite3_prepare_v2(handle, sql, -1, &stmt, NULL);
- if (err != SQLITE_OK) {
- ALOGE("sqlite3_prepare_v2(\"%s\") failed\n", sql);
- throw_sqlite3_exception(env, handle);
- goto rollback;
- }
-
- err = sqlite3_bind_text(stmt, 1, locale8, -1, SQLITE_TRANSIENT);
- if (err != SQLITE_OK) {
- ALOGE("sqlite3_bind_text() failed setting locale\n");
- throw_sqlite3_exception(env, handle);
- goto rollback;
- }
-
- err = sqlite3_step(stmt);
- if (err != SQLITE_OK && err != SQLITE_DONE) {
- ALOGE("sqlite3_step(\"%s\") failed setting locale\n", sql);
- throw_sqlite3_exception(env, handle);
- goto rollback;
- }
-
- err = sqlite3_exec(handle, "REINDEX LOCALIZED", NULL, NULL, NULL);
- if (err != SQLITE_OK) {
- ALOGE("REINDEX LOCALIZED failed\n");
- throw_sqlite3_exception(env, handle);
- goto rollback;
- }
-
- // all done, yay!
- err = sqlite3_exec(handle, "COMMIT TRANSACTION", NULL, NULL, NULL);
- if (err != SQLITE_OK) {
- ALOGE("COMMIT TRANSACTION failed setting locale\n");
- throw_sqlite3_exception(env, handle);
- goto done;
- }
-
-rollback:
- if (err != SQLITE_OK) {
- sqlite3_exec(handle, "ROLLBACK TRANSACTION", NULL, NULL, NULL);
- }
-
-done:
- if (locale8 != NULL) env->ReleaseStringUTFChars(localeString, locale8);
- if (stmt != NULL) sqlite3_finalize(stmt);
- if (meta != NULL) sqlite3_free_table(meta);
-}
-
-static void native_setSqliteSoftHeapLimit(JNIEnv* env, jobject clazz, jint limit) {
- sSqliteSoftHeapLimit = limit;
-}
-
-static jint native_releaseMemory(JNIEnv *env, jobject clazz)
-{
- // Attempt to release as much memory from the
- return sqlite3_release_memory(sSqliteSoftHeapLimit);
-}
-
-static void native_finalize(JNIEnv* env, jobject object, jint statementId)
-{
- if (statementId > 0) {
- sqlite3_finalize((sqlite3_stmt *)statementId);
- }
-}
-
-static void custom_function_callback(sqlite3_context * context, int argc, sqlite3_value ** argv) {
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- if (!env) {
- ALOGE("custom_function_callback cannot call into Java on this thread");
- return;
- }
- // get global ref to CustomFunction object from our user data
- jobject function = (jobject)sqlite3_user_data(context);
-
- // pack up the arguments into a string array
- jobjectArray strArray = env->NewObjectArray(argc, string_class, NULL);
- if (!strArray)
- goto done;
- for (int i = 0; i < argc; i++) {
- char* arg = (char *)sqlite3_value_text(argv[i]);
- if (!arg) {
- ALOGE("NULL argument in custom_function_callback. This should not happen.");
- return;
- }
- jobject obj = env->NewStringUTF(arg);
- if (!obj)
- goto done;
- env->SetObjectArrayElement(strArray, i, obj);
- env->DeleteLocalRef(obj);
- }
-
- env->CallVoidMethod(function, method_custom_function_callback, strArray);
- env->DeleteLocalRef(strArray);
-
-done:
- if (env->ExceptionCheck()) {
- ALOGE("An exception was thrown by custom sqlite3 function.");
- LOGE_EX(env);
- env->ExceptionClear();
- }
-}
-
-static jint native_addCustomFunction(JNIEnv* env, jobject object,
- jstring name, jint numArgs, jobject function)
-{
- sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle);
- char const *nameStr = env->GetStringUTFChars(name, NULL);
- jobject ref = env->NewGlobalRef(function);
- ALOGD_IF(DEBUG_JNI, "native_addCustomFunction %s ref: %p", nameStr, ref);
- int err = sqlite3_create_function(handle, nameStr, numArgs, SQLITE_UTF8,
- (void *)ref, custom_function_callback, NULL, NULL);
- env->ReleaseStringUTFChars(name, nameStr);
-
- if (err == SQLITE_OK)
- return (int)ref;
- else {
- ALOGE("sqlite3_create_function returned %d", err);
- env->DeleteGlobalRef(ref);
- throw_sqlite3_exception(env, handle);
- return 0;
- }
-}
-
-static void native_releaseCustomFunction(JNIEnv* env, jobject object, jint ref)
-{
- ALOGD_IF(DEBUG_JNI, "native_releaseCustomFunction %d", ref);
- env->DeleteGlobalRef((jobject)ref);
-}
-
-static JNINativeMethod sMethods[] =
-{
- /* name, signature, funcPtr */
- {"dbopen", "(Ljava/lang/String;I)V", (void *)dbopen},
- {"dbclose", "()V", (void *)dbclose},
- {"enableSqlTracing", "(Ljava/lang/String;S)V", (void *)enableSqlTracing},
- {"enableSqlProfiling", "(Ljava/lang/String;S)V", (void *)enableSqlProfiling},
- {"native_setLocale", "(Ljava/lang/String;I)V", (void *)native_setLocale},
- {"native_getDbLookaside", "()I", (void *)native_getDbLookaside},
- {"native_setSqliteSoftHeapLimit", "(I)V", (void *)native_setSqliteSoftHeapLimit},
- {"releaseMemory", "()I", (void *)native_releaseMemory},
- {"native_finalize", "(I)V", (void *)native_finalize},
- {"native_addCustomFunction",
- "(Ljava/lang/String;ILandroid/database/sqlite/SQLiteDatabase$CustomFunction;)I",
- (void *)native_addCustomFunction},
- {"native_releaseCustomFunction", "(I)V", (void *)native_releaseCustomFunction},
-};
-
-int register_android_database_SQLiteDatabase(JNIEnv *env)
-{
- jclass clazz;
-
- clazz = env->FindClass("android/database/sqlite/SQLiteDatabase");
- if (clazz == NULL) {
- ALOGE("Can't find android/database/sqlite/SQLiteDatabase\n");
- return -1;
- }
-
- string_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/String"));
- if (string_class == NULL) {
- ALOGE("Can't find java/lang/String\n");
- return -1;
- }
-
- offset_db_handle = env->GetFieldID(clazz, "mNativeHandle", "I");
- if (offset_db_handle == NULL) {
- ALOGE("Can't find SQLiteDatabase.mNativeHandle\n");
- return -1;
- }
-
- clazz = env->FindClass("android/database/sqlite/SQLiteDatabase$CustomFunction");
- if (clazz == NULL) {
- ALOGE("Can't find android/database/sqlite/SQLiteDatabase$CustomFunction\n");
- return -1;
- }
- method_custom_function_callback = env->GetMethodID(clazz, "callback", "([Ljava/lang/String;)V");
- if (method_custom_function_callback == NULL) {
- ALOGE("Can't find method SQLiteDatabase.CustomFunction.callback\n");
- return -1;
- }
-
- return AndroidRuntime::registerNativeMethods(env, "android/database/sqlite/SQLiteDatabase",
- sMethods, NELEM(sMethods));
-}
-
-/* throw a SQLiteException with a message appropriate for the error in handle */
-void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle) {
- throw_sqlite3_exception(env, handle, NULL);
-}
-
-/* throw a SQLiteException with the given message */
-void throw_sqlite3_exception(JNIEnv* env, const char* message) {
- throw_sqlite3_exception(env, NULL, message);
-}
-
-/* throw a SQLiteException with a message appropriate for the error in handle
- concatenated with the given message
- */
-void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle, const char* message) {
- if (handle) {
- throw_sqlite3_exception(env, sqlite3_errcode(handle),
- sqlite3_errmsg(handle), message);
- } else {
- // we use SQLITE_OK so that a generic SQLiteException is thrown;
- // any code not specified in the switch statement below would do.
- throw_sqlite3_exception(env, SQLITE_OK, "unknown error", message);
- }
-}
-
-/* throw a SQLiteException for a given error code */
-void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* message) {
- if (errcode == SQLITE_DONE) {
- throw_sqlite3_exception(env, errcode, NULL, message);
- } else {
- char temp[21];
- sprintf(temp, "error code %d", errcode);
- throw_sqlite3_exception(env, errcode, temp, message);
- }
-}
-
-/* throw a SQLiteException for a given error code, sqlite3message, and
- user message
- */
-void throw_sqlite3_exception(JNIEnv* env, int errcode,
- const char* sqlite3Message, const char* message) {
- const char* exceptionClass;
- switch (errcode) {
- case SQLITE_IOERR:
- exceptionClass = "android/database/sqlite/SQLiteDiskIOException";
- break;
- case SQLITE_CORRUPT:
- case SQLITE_NOTADB: // treat "unsupported file format" error as corruption also
- exceptionClass = "android/database/sqlite/SQLiteDatabaseCorruptException";
- break;
- case SQLITE_CONSTRAINT:
- exceptionClass = "android/database/sqlite/SQLiteConstraintException";
- break;
- case SQLITE_ABORT:
- exceptionClass = "android/database/sqlite/SQLiteAbortException";
- break;
- case SQLITE_DONE:
- exceptionClass = "android/database/sqlite/SQLiteDoneException";
- break;
- case SQLITE_FULL:
- exceptionClass = "android/database/sqlite/SQLiteFullException";
- break;
- case SQLITE_MISUSE:
- exceptionClass = "android/database/sqlite/SQLiteMisuseException";
- break;
- case SQLITE_PERM:
- exceptionClass = "android/database/sqlite/SQLiteAccessPermException";
- break;
- case SQLITE_BUSY:
- exceptionClass = "android/database/sqlite/SQLiteDatabaseLockedException";
- break;
- case SQLITE_LOCKED:
- exceptionClass = "android/database/sqlite/SQLiteTableLockedException";
- break;
- case SQLITE_READONLY:
- exceptionClass = "android/database/sqlite/SQLiteReadOnlyDatabaseException";
- break;
- case SQLITE_CANTOPEN:
- exceptionClass = "android/database/sqlite/SQLiteCantOpenDatabaseException";
- break;
- case SQLITE_TOOBIG:
- exceptionClass = "android/database/sqlite/SQLiteBlobTooBigException";
- break;
- case SQLITE_RANGE:
- exceptionClass = "android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException";
- break;
- case SQLITE_NOMEM:
- exceptionClass = "android/database/sqlite/SQLiteOutOfMemoryException";
- break;
- case SQLITE_MISMATCH:
- exceptionClass = "android/database/sqlite/SQLiteDatatypeMismatchException";
- break;
- case SQLITE_UNCLOSED:
- exceptionClass = "android/database/sqlite/SQLiteUnfinalizedObjectsException";
- break;
- default:
- exceptionClass = "android/database/sqlite/SQLiteException";
- break;
- }
-
- if (sqlite3Message != NULL && message != NULL) {
- char* fullMessage = (char *)malloc(strlen(sqlite3Message) + strlen(message) + 3);
- if (fullMessage != NULL) {
- strcpy(fullMessage, sqlite3Message);
- strcat(fullMessage, ": ");
- strcat(fullMessage, message);
- jniThrowException(env, exceptionClass, fullMessage);
- free(fullMessage);
- } else {
- jniThrowException(env, exceptionClass, sqlite3Message);
- }
- } else if (sqlite3Message != NULL) {
- jniThrowException(env, exceptionClass, sqlite3Message);
- } else {
- jniThrowException(env, exceptionClass, message);
- }
-}
-
-
-} // namespace android
diff --git a/core/jni/android_database_SQLiteGlobal.cpp b/core/jni/android_database_SQLiteGlobal.cpp
new file mode 100644
index 0000000..82cae5a
--- /dev/null
+++ b/core/jni/android_database_SQLiteGlobal.cpp
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "SQLiteGlobal"
+
+#include <jni.h>
+#include <JNIHelp.h>
+#include <android_runtime/AndroidRuntime.h>
+
+#include <sqlite3.h>
+#include <sqlite3_android.h>
+
+#include "android_database_SQLiteCommon.h"
+
+namespace android {
+
+// Called each time a message is logged.
+static void sqliteLogCallback(void* data, int iErrCode, const char* zMsg) {
+ bool verboseLog = !!data;
+ if (iErrCode == 0 || iErrCode == SQLITE_CONSTRAINT) {
+ if (verboseLog) {
+ ALOGV(LOG_VERBOSE, SQLITE_LOG_TAG, "(%d) %s\n", iErrCode, zMsg);
+ }
+ } else {
+ ALOG(LOG_ERROR, SQLITE_LOG_TAG, "(%d) %s\n", iErrCode, zMsg);
+ }
+}
+
+// Sets the global SQLite configuration.
+// This must be called before any other SQLite functions are called. */
+static void nativeConfig(JNIEnv* env, jclass clazz, jboolean verboseLog, jint softHeapLimit) {
+ // Enable multi-threaded mode. In this mode, SQLite is safe to use by multiple
+ // threads as long as no two threads use the same database connection at the same
+ // time (which we guarantee in the SQLite database wrappers).
+ sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
+
+ // Redirect SQLite log messages to the Android log.
+ sqlite3_config(SQLITE_CONFIG_LOG, &sqliteLogCallback, verboseLog ? (void*)1 : NULL);
+
+ // The soft heap limit prevents the page cache allocations from growing
+ // beyond the given limit, no matter what the max page cache sizes are
+ // set to. The limit does not, as of 3.5.0, affect any other allocations.
+ sqlite3_soft_heap_limit(softHeapLimit);
+}
+
+static jint nativeReleaseMemory(JNIEnv* env, jclass clazz, jint bytesToFree) {
+ return sqlite3_release_memory(bytesToFree);
+}
+
+static JNINativeMethod sMethods[] =
+{
+ /* name, signature, funcPtr */
+ { "nativeConfig", "(ZI)V",
+ (void*)nativeConfig },
+ { "nativeReleaseMemory", "(I)I",
+ (void*)nativeReleaseMemory },
+};
+
+int register_android_database_SQLiteGlobal(JNIEnv *env)
+{
+ return AndroidRuntime::registerNativeMethods(env, "android/database/sqlite/SQLiteGlobal",
+ sMethods, NELEM(sMethods));
+}
+
+} // namespace android
diff --git a/core/jni/android_database_SQLiteProgram.cpp b/core/jni/android_database_SQLiteProgram.cpp
deleted file mode 100644
index 2e34c00..0000000
--- a/core/jni/android_database_SQLiteProgram.cpp
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2006-2008 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.
- */
-
-#undef LOG_TAG
-#define LOG_TAG "Cursor"
-
-#include <jni.h>
-#include <JNIHelp.h>
-#include <android_runtime/AndroidRuntime.h>
-
-#include <sqlite3.h>
-
-#include <utils/Log.h>
-
-#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "sqlite3_exception.h"
-
-
-namespace android {
-
-static jfieldID gHandleField;
-static jfieldID gStatementField;
-
-
-#define GET_STATEMENT(env, object) \
- (sqlite3_stmt *)env->GetIntField(object, gStatementField)
-#define GET_HANDLE(env, object) \
- (sqlite3 *)env->GetIntField(object, gHandleField)
-
-static void native_compile(JNIEnv* env, jobject object, jstring sqlString)
-{
- char buf[65];
- strcpy(buf, "android_database_SQLiteProgram->native_compile() not implemented");
- throw_sqlite3_exception(env, GET_HANDLE(env, object), buf);
- return;
-}
-
-static void native_bind_null(JNIEnv* env, jobject object,
- jint index)
-{
- int err;
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
-
- err = sqlite3_bind_null(statement, index);
- if (err != SQLITE_OK) {
- char buf[32];
- sprintf(buf, "handle %p", statement);
- throw_sqlite3_exception(env, GET_HANDLE(env, object), buf);
- return;
- }
-}
-
-static void native_bind_long(JNIEnv* env, jobject object,
- jint index, jlong value)
-{
- int err;
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
-
- err = sqlite3_bind_int64(statement, index, value);
- if (err != SQLITE_OK) {
- char buf[32];
- sprintf(buf, "handle %p", statement);
- throw_sqlite3_exception(env, GET_HANDLE(env, object), buf);
- return;
- }
-}
-
-static void native_bind_double(JNIEnv* env, jobject object,
- jint index, jdouble value)
-{
- int err;
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
-
- err = sqlite3_bind_double(statement, index, value);
- if (err != SQLITE_OK) {
- char buf[32];
- sprintf(buf, "handle %p", statement);
- throw_sqlite3_exception(env, GET_HANDLE(env, object), buf);
- return;
- }
-}
-
-static void native_bind_string(JNIEnv* env, jobject object,
- jint index, jstring sqlString)
-{
- int err;
- jchar const * sql;
- jsize sqlLen;
- sqlite3_stmt * statement= GET_STATEMENT(env, object);
-
- sql = env->GetStringChars(sqlString, NULL);
- sqlLen = env->GetStringLength(sqlString);
- err = sqlite3_bind_text16(statement, index, sql, sqlLen * 2, SQLITE_TRANSIENT);
- env->ReleaseStringChars(sqlString, sql);
- if (err != SQLITE_OK) {
- char buf[32];
- sprintf(buf, "handle %p", statement);
- throw_sqlite3_exception(env, GET_HANDLE(env, object), buf);
- return;
- }
-}
-
-static void native_bind_blob(JNIEnv* env, jobject object,
- jint index, jbyteArray value)
-{
- int err;
- jchar const * sql;
- jsize sqlLen;
- sqlite3_stmt * statement= GET_STATEMENT(env, object);
-
- jint len = env->GetArrayLength(value);
- jbyte * bytes = env->GetByteArrayElements(value, NULL);
-
- err = sqlite3_bind_blob(statement, index, bytes, len, SQLITE_TRANSIENT);
- env->ReleaseByteArrayElements(value, bytes, JNI_ABORT);
-
- if (err != SQLITE_OK) {
- char buf[32];
- sprintf(buf, "statement %p", statement);
- throw_sqlite3_exception(env, GET_HANDLE(env, object), buf);
- return;
- }
-}
-
-static void native_clear_bindings(JNIEnv* env, jobject object)
-{
- int err;
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
-
- err = sqlite3_clear_bindings(statement);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, GET_HANDLE(env, object));
- return;
- }
-}
-
-static void native_finalize(JNIEnv* env, jobject object)
-{
- char buf[66];
- strcpy(buf, "android_database_SQLiteProgram->native_finalize() not implemented");
- throw_sqlite3_exception(env, GET_HANDLE(env, object), buf);
- return;
-}
-
-
-static JNINativeMethod sMethods[] =
-{
- /* name, signature, funcPtr */
- {"native_bind_null", "(I)V", (void *)native_bind_null},
- {"native_bind_long", "(IJ)V", (void *)native_bind_long},
- {"native_bind_double", "(ID)V", (void *)native_bind_double},
- {"native_bind_string", "(ILjava/lang/String;)V", (void *)native_bind_string},
- {"native_bind_blob", "(I[B)V", (void *)native_bind_blob},
- {"native_clear_bindings", "()V", (void *)native_clear_bindings},
-};
-
-int register_android_database_SQLiteProgram(JNIEnv * env)
-{
- jclass clazz;
-
- clazz = env->FindClass("android/database/sqlite/SQLiteProgram");
- if (clazz == NULL) {
- ALOGE("Can't find android/database/sqlite/SQLiteProgram");
- return -1;
- }
-
- gHandleField = env->GetFieldID(clazz, "nHandle", "I");
- gStatementField = env->GetFieldID(clazz, "nStatement", "I");
-
- if (gHandleField == NULL || gStatementField == NULL) {
- ALOGE("Error locating fields");
- return -1;
- }
-
- return AndroidRuntime::registerNativeMethods(env,
- "android/database/sqlite/SQLiteProgram", sMethods, NELEM(sMethods));
-}
-
-} // namespace android
diff --git a/core/jni/android_database_SQLiteQuery.cpp b/core/jni/android_database_SQLiteQuery.cpp
deleted file mode 100644
index da7ccf0..0000000
--- a/core/jni/android_database_SQLiteQuery.cpp
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2006 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.
- */
-
-#undef LOG_TAG
-#define LOG_TAG "SqliteCursor.cpp"
-
-#include <jni.h>
-#include <JNIHelp.h>
-#include <android_runtime/AndroidRuntime.h>
-
-#include <sqlite3.h>
-
-#include <utils/Log.h>
-
-#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "binder/CursorWindow.h"
-#include "sqlite3_exception.h"
-
-
-namespace android {
-
-enum CopyRowResult {
- CPR_OK,
- CPR_FULL,
- CPR_ERROR,
-};
-
-static CopyRowResult copyRow(JNIEnv* env, CursorWindow* window,
- sqlite3_stmt* statement, int numColumns, int startPos, int addedRows) {
- // Allocate a new field directory for the row. This pointer is not reused
- // since it may be possible for it to be relocated on a call to alloc() when
- // the field data is being allocated.
- status_t status = window->allocRow();
- if (status) {
- LOG_WINDOW("Failed allocating fieldDir at startPos %d row %d, error=%d",
- startPos, addedRows, status);
- return CPR_FULL;
- }
-
- // Pack the row into the window.
- CopyRowResult result = CPR_OK;
- for (int i = 0; i < numColumns; i++) {
- int type = sqlite3_column_type(statement, i);
- if (type == SQLITE_TEXT) {
- // TEXT data
- const char* text = reinterpret_cast<const char*>(
- sqlite3_column_text(statement, i));
- // SQLite does not include the NULL terminator in size, but does
- // ensure all strings are NULL terminated, so increase size by
- // one to make sure we store the terminator.
- size_t sizeIncludingNull = sqlite3_column_bytes(statement, i) + 1;
- status = window->putString(addedRows, i, text, sizeIncludingNull);
- if (status) {
- LOG_WINDOW("Failed allocating %u bytes for text at %d,%d, error=%d",
- sizeIncludingNull, startPos + addedRows, i, status);
- result = CPR_FULL;
- break;
- }
- LOG_WINDOW("%d,%d is TEXT with %u bytes",
- startPos + addedRows, i, sizeIncludingNull);
- } else if (type == SQLITE_INTEGER) {
- // INTEGER data
- int64_t value = sqlite3_column_int64(statement, i);
- status = window->putLong(addedRows, i, value);
- if (status) {
- LOG_WINDOW("Failed allocating space for a long in column %d, error=%d",
- i, status);
- result = CPR_FULL;
- break;
- }
- LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + addedRows, i, value);
- } else if (type == SQLITE_FLOAT) {
- // FLOAT data
- double value = sqlite3_column_double(statement, i);
- status = window->putDouble(addedRows, i, value);
- if (status) {
- LOG_WINDOW("Failed allocating space for a double in column %d, error=%d",
- i, status);
- result = CPR_FULL;
- break;
- }
- LOG_WINDOW("%d,%d is FLOAT %lf", startPos + addedRows, i, value);
- } else if (type == SQLITE_BLOB) {
- // BLOB data
- const void* blob = sqlite3_column_blob(statement, i);
- size_t size = sqlite3_column_bytes(statement, i);
- status = window->putBlob(addedRows, i, blob, size);
- if (status) {
- LOG_WINDOW("Failed allocating %u bytes for blob at %d,%d, error=%d",
- size, startPos + addedRows, i, status);
- result = CPR_FULL;
- break;
- }
- LOG_WINDOW("%d,%d is Blob with %u bytes",
- startPos + addedRows, i, size);
- } else if (type == SQLITE_NULL) {
- // NULL field
- status = window->putNull(addedRows, i);
- if (status) {
- LOG_WINDOW("Failed allocating space for a null in column %d, error=%d",
- i, status);
- result = CPR_FULL;
- break;
- }
-
- LOG_WINDOW("%d,%d is NULL", startPos + addedRows, i);
- } else {
- // Unknown data
- ALOGE("Unknown column type when filling database window");
- throw_sqlite3_exception(env, "Unknown column type when filling window");
- result = CPR_ERROR;
- break;
- }
- }
-
- // Free the last row if if was not successfully copied.
- if (result != CPR_OK) {
- window->freeLastRow();
- }
- return result;
-}
-
-static jlong nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr,
- jint statementPtr, jint windowPtr, jint offsetParam,
- jint startPos, jint requiredPos, jboolean countAllRows) {
- sqlite3* database = reinterpret_cast<sqlite3*>(databasePtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
-
- // Only do the binding if there is a valid offsetParam. If no binding needs to be done
- // offsetParam will be set to 0, an invalid value.
- if (offsetParam > 0) {
- // Bind the offset parameter, telling the program which row to start with
- // If an offset parameter is used, we cannot simply clear the window if it
- // turns out that the requiredPos won't fit because the result set may
- // depend on startPos, so we set startPos to requiredPos.
- startPos = requiredPos;
- int err = sqlite3_bind_int(statement, offsetParam, startPos);
- if (err != SQLITE_OK) {
- ALOGE("Unable to bind offset position, offsetParam = %d", offsetParam);
- throw_sqlite3_exception(env, database);
- return 0;
- }
- LOG_WINDOW("Bound offset position to startPos %d", startPos);
- }
-
- // We assume numRows is initially 0.
- LOG_WINDOW("Window: numRows = %d, size = %d, freeSpace = %d",
- window->getNumRows(), window->size(), window->freeSpace());
-
- int numColumns = sqlite3_column_count(statement);
- status_t status = window->setNumColumns(numColumns);
- if (status) {
- ALOGE("Failed to change column count from %d to %d", window->getNumColumns(), numColumns);
- jniThrowException(env, "java/lang/IllegalStateException", "numColumns mismatch");
- return 0;
- }
-
- int retryCount = 0;
- int totalRows = 0;
- int addedRows = 0;
- bool windowFull = false;
- bool gotException = false;
- while (!gotException && (!windowFull || countAllRows)) {
- int err = sqlite3_step(statement);
- if (err == SQLITE_ROW) {
- LOG_WINDOW("Stepped statement %p to row %d", statement, totalRows);
- retryCount = 0;
- totalRows += 1;
-
- // Skip the row if the window is full or we haven't reached the start position yet.
- if (startPos >= totalRows || windowFull) {
- continue;
- }
-
- CopyRowResult cpr = copyRow(env, window, statement, numColumns, startPos, addedRows);
- if (cpr == CPR_FULL && addedRows && startPos + addedRows < requiredPos) {
- // We filled the window before we got to the one row that we really wanted.
- // Clear the window and start filling it again from here.
- // TODO: Would be nicer if we could progressively replace earlier rows.
- window->clear();
- window->setNumColumns(numColumns);
- startPos += addedRows;
- addedRows = 0;
- cpr = copyRow(env, window, statement, numColumns, startPos, addedRows);
- }
-
- if (cpr == CPR_OK) {
- addedRows += 1;
- } else if (cpr == CPR_FULL) {
- windowFull = true;
- } else {
- gotException = true;
- }
- } else if (err == SQLITE_DONE) {
- // All rows processed, bail
- LOG_WINDOW("Processed all rows");
- break;
- } else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) {
- // The table is locked, retry
- LOG_WINDOW("Database locked, retrying");
- if (retryCount > 50) {
- ALOGE("Bailing on database busy retry");
- throw_sqlite3_exception(env, database, "retrycount exceeded");
- gotException = true;
- } else {
- // Sleep to give the thread holding the lock a chance to finish
- usleep(1000);
- retryCount++;
- }
- } else {
- throw_sqlite3_exception(env, database);
- gotException = true;
- }
- }
-
- LOG_WINDOW("Resetting statement %p after fetching %d rows and adding %d rows"
- "to the window in %d bytes",
- statement, totalRows, addedRows, window->size() - window->freeSpace());
- sqlite3_reset(statement);
-
- // Report the total number of rows on request.
- if (startPos > totalRows) {
- ALOGE("startPos %d > actual rows %d", startPos, totalRows);
- }
- jlong result = jlong(startPos) << 32 | jlong(totalRows);
- return result;
-}
-
-static jint nativeColumnCount(JNIEnv* env, jclass clazz, jint statementPtr) {
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
- return sqlite3_column_count(statement);
-}
-
-static jstring nativeColumnName(JNIEnv* env, jclass clazz, jint statementPtr,
- jint columnIndex) {
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
- const char* name = sqlite3_column_name(statement, columnIndex);
- return env->NewStringUTF(name);
-}
-
-
-static JNINativeMethod sMethods[] =
-{
- /* name, signature, funcPtr */
- { "nativeFillWindow", "(IIIIIIZ)J",
- (void*)nativeFillWindow },
- { "nativeColumnCount", "(I)I",
- (void*)nativeColumnCount},
- { "nativeColumnName", "(II)Ljava/lang/String;",
- (void*)nativeColumnName},
-};
-
-int register_android_database_SQLiteQuery(JNIEnv * env)
-{
- return AndroidRuntime::registerNativeMethods(env,
- "android/database/sqlite/SQLiteQuery", sMethods, NELEM(sMethods));
-}
-
-} // namespace android
diff --git a/core/jni/android_database_SQLiteStatement.cpp b/core/jni/android_database_SQLiteStatement.cpp
deleted file mode 100644
index e376258..0000000
--- a/core/jni/android_database_SQLiteStatement.cpp
+++ /dev/null
@@ -1,286 +0,0 @@
-/* //device/libs/android_runtime/android_database_SQLiteCursor.cpp
-**
-** Copyright 2006, 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.
-*/
-
-#undef LOG_TAG
-#define LOG_TAG "SQLiteStatementCpp"
-
-#include "android_util_Binder.h"
-
-#include <jni.h>
-#include <JNIHelp.h>
-#include <android_runtime/AndroidRuntime.h>
-
-#include <sqlite3.h>
-
-#include <cutils/ashmem.h>
-#include <utils/Log.h>
-
-#include <fcntl.h>
-#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
-#include <sys/mman.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-
-#include "sqlite3_exception.h"
-
-namespace android {
-
-
-sqlite3_stmt * compile(JNIEnv* env, jobject object,
- sqlite3 * handle, jstring sqlString);
-
-static jfieldID gHandleField;
-static jfieldID gStatementField;
-
-
-#define GET_STATEMENT(env, object) \
- (sqlite3_stmt *)env->GetIntField(object, gStatementField)
-#define GET_HANDLE(env, object) \
- (sqlite3 *)env->GetIntField(object, gHandleField)
-
-
-static jint native_execute(JNIEnv* env, jobject object)
-{
- int err;
- sqlite3 * handle = GET_HANDLE(env, object);
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
- int numChanges = -1;
-
- // Execute the statement
- err = sqlite3_step(statement);
-
- // Throw an exception if an error occurred
- if (err == SQLITE_ROW) {
- throw_sqlite3_exception(env,
- "Queries can be performed using SQLiteDatabase query or rawQuery methods only.");
- } else if (err != SQLITE_DONE) {
- throw_sqlite3_exception_errcode(env, err, sqlite3_errmsg(handle));
- } else {
- numChanges = sqlite3_changes(handle);
- }
-
- // Reset the statement so it's ready to use again
- sqlite3_reset(statement);
- return numChanges;
-}
-
-static jlong native_executeInsert(JNIEnv* env, jobject object)
-{
- sqlite3 * handle = GET_HANDLE(env, object);
- jint numChanges = native_execute(env, object);
- if (numChanges > 0) {
- return sqlite3_last_insert_rowid(handle);
- } else {
- return -1;
- }
-}
-
-static jlong native_1x1_long(JNIEnv* env, jobject object)
-{
- int err;
- sqlite3 * handle = GET_HANDLE(env, object);
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
- jlong value = -1;
-
- // Execute the statement
- err = sqlite3_step(statement);
-
- // Handle the result
- if (err == SQLITE_ROW) {
- // No errors, read the data and return it
- value = sqlite3_column_int64(statement, 0);
- } else {
- throw_sqlite3_exception_errcode(env, err, sqlite3_errmsg(handle));
- }
-
- // Reset the statment so it's ready to use again
- sqlite3_reset(statement);
-
- return value;
-}
-
-static jstring native_1x1_string(JNIEnv* env, jobject object)
-{
- int err;
- sqlite3 * handle = GET_HANDLE(env, object);
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
- jstring value = NULL;
-
- // Execute the statement
- err = sqlite3_step(statement);
-
- // Handle the result
- if (err == SQLITE_ROW) {
- // No errors, read the data and return it
- char const * text = (char const *)sqlite3_column_text(statement, 0);
- value = env->NewStringUTF(text);
- } else {
- throw_sqlite3_exception_errcode(env, err, sqlite3_errmsg(handle));
- }
-
- // Reset the statment so it's ready to use again
- sqlite3_reset(statement);
-
- return value;
-}
-
-static jobject createParcelFileDescriptor(JNIEnv * env, int fd)
-{
- // Create FileDescriptor object
- jobject fileDesc = jniCreateFileDescriptor(env, fd);
- if (fileDesc == NULL) {
- // FileDescriptor constructor has thrown an exception
- close(fd);
- return NULL;
- }
-
- // Wrap it in a ParcelFileDescriptor
- jobject parcelFileDesc = newParcelFileDescriptor(env, fileDesc);
- if (parcelFileDesc == NULL) {
- // ParcelFileDescriptor constructor has thrown an exception
- close(fd);
- return NULL;
- }
-
- return parcelFileDesc;
-}
-
-// Creates an ashmem area, copies some data into it, and returns
-// a ParcelFileDescriptor for the ashmem area.
-static jobject create_ashmem_region_with_data(JNIEnv * env,
- const void * data, int length)
-{
- // Create ashmem area
- int fd = ashmem_create_region(NULL, length);
- if (fd < 0) {
- ALOGE("ashmem_create_region failed: %s", strerror(errno));
- jniThrowIOException(env, errno);
- return NULL;
- }
-
- if (length > 0) {
- // mmap the ashmem area
- void * ashmem_ptr =
- mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- if (ashmem_ptr == MAP_FAILED) {
- ALOGE("mmap failed: %s", strerror(errno));
- jniThrowIOException(env, errno);
- close(fd);
- return NULL;
- }
-
- // Copy data to ashmem area
- memcpy(ashmem_ptr, data, length);
-
- // munmap ashmem area
- if (munmap(ashmem_ptr, length) < 0) {
- ALOGE("munmap failed: %s", strerror(errno));
- jniThrowIOException(env, errno);
- close(fd);
- return NULL;
- }
- }
-
- // Make ashmem area read-only
- if (ashmem_set_prot_region(fd, PROT_READ) < 0) {
- ALOGE("ashmem_set_prot_region failed: %s", strerror(errno));
- jniThrowIOException(env, errno);
- close(fd);
- return NULL;
- }
-
- // Wrap it in a ParcelFileDescriptor
- return createParcelFileDescriptor(env, fd);
-}
-
-static jobject native_1x1_blob_ashmem(JNIEnv* env, jobject object)
-{
- int err;
- sqlite3 * handle = GET_HANDLE(env, object);
- sqlite3_stmt * statement = GET_STATEMENT(env, object);
- jobject value = NULL;
-
- // Execute the statement
- err = sqlite3_step(statement);
-
- // Handle the result
- if (err == SQLITE_ROW) {
- // No errors, read the data and return it
- const void * blob = sqlite3_column_blob(statement, 0);
- if (blob != NULL) {
- int len = sqlite3_column_bytes(statement, 0);
- if (len >= 0) {
- value = create_ashmem_region_with_data(env, blob, len);
- }
- }
- } else {
- throw_sqlite3_exception_errcode(env, err, sqlite3_errmsg(handle));
- }
-
- // Reset the statment so it's ready to use again
- sqlite3_reset(statement);
-
- return value;
-}
-
-static void native_executeSql(JNIEnv* env, jobject object, jstring sql)
-{
- char const* sqlString = env->GetStringUTFChars(sql, NULL);
- sqlite3 * handle = GET_HANDLE(env, object);
- int err = sqlite3_exec(handle, sqlString, NULL, NULL, NULL);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, handle);
- }
- env->ReleaseStringUTFChars(sql, sqlString);
-}
-
-static JNINativeMethod sMethods[] =
-{
- /* name, signature, funcPtr */
- {"native_execute", "()I", (void *)native_execute},
- {"native_executeInsert", "()J", (void *)native_executeInsert},
- {"native_1x1_long", "()J", (void *)native_1x1_long},
- {"native_1x1_string", "()Ljava/lang/String;", (void *)native_1x1_string},
- {"native_1x1_blob_ashmem", "()Landroid/os/ParcelFileDescriptor;", (void *)native_1x1_blob_ashmem},
- {"native_executeSql", "(Ljava/lang/String;)V", (void *)native_executeSql},
-};
-
-int register_android_database_SQLiteStatement(JNIEnv * env)
-{
- jclass clazz;
-
- clazz = env->FindClass("android/database/sqlite/SQLiteStatement");
- if (clazz == NULL) {
- ALOGE("Can't find android/database/sqlite/SQLiteStatement");
- return -1;
- }
-
- gHandleField = env->GetFieldID(clazz, "nHandle", "I");
- gStatementField = env->GetFieldID(clazz, "nStatement", "I");
-
- if (gHandleField == NULL || gStatementField == NULL) {
- ALOGE("Error locating fields");
- return -1;
- }
-
- return AndroidRuntime::registerNativeMethods(env,
- "android/database/sqlite/SQLiteStatement", sMethods, NELEM(sMethods));
-}
-
-} // namespace android
diff --git a/core/jni/sqlite3_exception.h b/core/jni/sqlite3_exception.h
deleted file mode 100644
index 13735a1..0000000
--- a/core/jni/sqlite3_exception.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/* //device/libs/include/android_runtime/sqlite3_exception.h
-**
-** Copyright 2007, 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.
-*/
-
-#ifndef _SQLITE3_EXCEPTION_H
-#define _SQLITE3_EXCEPTION_H 1
-
-#include <jni.h>
-#include <JNIHelp.h>
-//#include <android_runtime/AndroidRuntime.h>
-
-#include <sqlite3.h>
-
-namespace android {
-
-/* throw a SQLiteException with a message appropriate for the error in handle */
-void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle);
-
-/* throw a SQLiteException with the given message */
-void throw_sqlite3_exception(JNIEnv* env, const char* message);
-
-/* throw a SQLiteException with a message appropriate for the error in handle
- concatenated with the given message
- */
-void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle, const char* message);
-
-/* throw a SQLiteException for a given error code */
-void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* message);
-
-void throw_sqlite3_exception(JNIEnv* env, int errcode,
- const char* sqlite3Message, const char* message);
-}
-
-#endif // _SQLITE3_EXCEPTION_H
diff --git a/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java b/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java
deleted file mode 100644
index 525dd2d..0000000
--- a/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
- * Copyright (C) 2006 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.Context;
-import android.database.sqlite.SQLiteDatabaseTest.ClassToTestSqlCompilationAndCaching;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Log;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-public class DatabaseConnectionPoolTest extends AndroidTestCase {
- private static final String TAG = "DatabaseConnectionPoolTest";
-
- private static final int MAX_CONN = 5;
- private static final String TEST_SQL = "select * from test where i = ? AND j = 1";
- private static final String[] TEST_SQLS = new String[] {
- TEST_SQL, TEST_SQL + 1, TEST_SQL + 2, TEST_SQL + 3, TEST_SQL + 4
- };
-
- private SQLiteDatabase mDatabase;
- private File mDatabaseFile;
- private DatabaseConnectionPool mTestPool;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
-
- File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE);
- mDatabaseFile = new File(dbDir, "database_test.db");
- if (mDatabaseFile.exists()) {
- mDatabaseFile.delete();
- }
- mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null);
- assertNotNull(mDatabase);
- mDatabase.execSQL("create table test (i int, j int);");
- mTestPool = new DatabaseConnectionPool(mDatabase);
- assertNotNull(mTestPool);
- }
-
- @Override
- protected void tearDown() throws Exception {
- mTestPool.close();
- mDatabase.close();
- mDatabaseFile.delete();
- super.tearDown();
- }
-
- @SmallTest
- public void testGetAndRelease() {
- mTestPool.setMaxPoolSize(MAX_CONN);
- // connections should be lazily created.
- assertEquals(0, mTestPool.getSize());
- // MAX pool size should be set to MAX_CONN
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // get a connection
- SQLiteDatabase db = mTestPool.get(TEST_SQL);
- // pool size should be one - since only one should be allocated for the above get()
- assertEquals(1, mTestPool.getSize());
- assertEquals(mDatabase, db.mParentConnObj);
- // no free connections should be available
- assertEquals(0, mTestPool.getFreePoolSize());
- assertFalse(mTestPool.isDatabaseObjFree(db));
- // release the connection
- mTestPool.release(db);
- assertEquals(1, mTestPool.getFreePoolSize());
- assertEquals(1, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- assertTrue(mTestPool.isDatabaseObjFree(db));
- // release the same object again and expect IllegalStateException
- try {
- mTestPool.release(db);
- fail("illegalStateException expected");
- } catch (IllegalStateException e ) {
- // expected.
- }
- }
-
- /**
- * get all connections from the pool and ask for one more.
- * should get one of the connections already got so far.
- */
- @SmallTest
- public void testGetAllConnAndOneMore() {
- mTestPool.setMaxPoolSize(MAX_CONN);
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- ArrayList<SQLiteDatabase> dbObjs = new ArrayList<SQLiteDatabase>();
- for (int i = 0; i < MAX_CONN; i++) {
- SQLiteDatabase db = mTestPool.get(TEST_SQL);
- assertFalse(dbObjs.contains(db));
- dbObjs.add(db);
- assertEquals(mDatabase, db.mParentConnObj);
- }
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // pool is maxed out and no free connections. ask for one more connection
- SQLiteDatabase db1 = mTestPool.get(TEST_SQL);
- // make sure db1 is one of the existing ones
- assertTrue(dbObjs.contains(db1));
- // pool size should remain at MAX_CONN
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // release db1 but since it is allocated 2 times, it should still remain 'busy'
- mTestPool.release(db1);
- assertFalse(mTestPool.isDatabaseObjFree(db1));
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // release all connections
- for (int i = 0; i < MAX_CONN; i++) {
- mTestPool.release(dbObjs.get(i));
- }
- // all objects in the pool should be freed now
- assertEquals(MAX_CONN, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- }
-
- /**
- * same as above except that each connection has different SQL statement associated with it.
- */
- @SmallTest
- public void testConnRetrievalForPreviouslySeenSql() {
- mTestPool.setMaxPoolSize(MAX_CONN);
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- HashMap<String, SQLiteDatabase> dbObjs = new HashMap<String, SQLiteDatabase>();
- for (int i = 0; i < MAX_CONN; i++) {
- SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]);
- executeSqlOnDatabaseConn(db, TEST_SQLS[i]);
- assertFalse(dbObjs.values().contains(db));
- dbObjs.put(TEST_SQLS[i], db);
- }
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // pool is maxed out and no free connections. ask for one more connection
- // use a previously seen SQL statement
- String testSql = TEST_SQLS[MAX_CONN - 1];
- SQLiteDatabase db1 = mTestPool.get(testSql);
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // make sure db1 is one of the existing ones
- assertTrue(dbObjs.values().contains(db1));
- assertEquals(db1, dbObjs.get(testSql));
- // do the same again
- SQLiteDatabase db2 = mTestPool.get(testSql);
- // make sure db1 is one of the existing ones
- assertEquals(db2, dbObjs.get(testSql));
-
- // pool size should remain at MAX_CONN
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- // release db1 but since the same connection is allocated 3 times,
- // it should still remain 'busy'
- mTestPool.release(db1);
- assertFalse(mTestPool.isDatabaseObjFree(dbObjs.get(testSql)));
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- // release db2 but since the same connection is allocated 2 times,
- // it should still remain 'busy'
- mTestPool.release(db2);
- assertFalse(mTestPool.isDatabaseObjFree(dbObjs.get(testSql)));
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- // release all connections
- for (int i = 0; i < MAX_CONN; i++) {
- mTestPool.release(dbObjs.get(TEST_SQLS[i]));
- }
- // all objects in the pool should be freed now
- assertEquals(MAX_CONN, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- }
-
- private void executeSqlOnDatabaseConn(SQLiteDatabase db, String sql) {
- // get the given sql be compiled on the given database connection.
- // this will help DatabaseConenctionPool figure out if a given SQL statement
- // is already cached by a database connection.
- ClassToTestSqlCompilationAndCaching c =
- ClassToTestSqlCompilationAndCaching.create(db, sql);
- c.close();
- }
-
- /**
- * get a connection for a SQL statement 'blah'. (connection_s)
- * make sure the pool has at least one free connection even after this get().
- * and get a connection for the same SQL again.
- * this connection should be different from connection_s.
- * even though there is a connection with the given SQL pre-compiled, since is it not free
- * AND since the pool has free connections available, should get a new connection.
- */
- @SmallTest
- public void testGetConnForTheSameSql() {
- mTestPool.setMaxPoolSize(MAX_CONN);
-
- SQLiteDatabase db = mTestPool.get(TEST_SQL);
- executeSqlOnDatabaseConn(db, TEST_SQL);
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(1, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- assertFalse(mTestPool.isDatabaseObjFree(db));
-
- SQLiteDatabase db1 = mTestPool.get(TEST_SQL);
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(2, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- assertFalse(mTestPool.isDatabaseObjFree(db1));
- assertFalse(db1.equals(db));
-
- mTestPool.release(db);
- assertEquals(1, mTestPool.getFreePoolSize());
- assertEquals(2, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- mTestPool.release(db1);
- assertEquals(2, mTestPool.getFreePoolSize());
- assertEquals(2, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- }
-
- /**
- * get the same connection N times and release it N times.
- * this tests DatabaseConnectionPool.PoolObj.mNumHolders
- */
- @SmallTest
- public void testGetSameConnNtimesAndReleaseItNtimes() {
- mTestPool.setMaxPoolSize(MAX_CONN);
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- HashMap<String, SQLiteDatabase> dbObjs = new HashMap<String, SQLiteDatabase>();
- for (int i = 0; i < MAX_CONN; i++) {
- SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]);
- executeSqlOnDatabaseConn(db, TEST_SQLS[i]);
- assertFalse(dbObjs.values().contains(db));
- dbObjs.put(TEST_SQLS[i], db);
- }
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // every connection in the pool should have numHolders = 1
- for (int i = 0; i < MAX_CONN; i ++) {
- assertEquals(1, mTestPool.getPool().get(i).getNumHolders());
- }
- // pool is maxed out and no free connections. ask for one more connection
- // use a previously seen SQL statement
- String testSql = TEST_SQLS[MAX_CONN - 1];
- SQLiteDatabase db1 = mTestPool.get(testSql);
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // make sure db1 is one of the existing ones
- assertTrue(dbObjs.values().contains(db1));
- assertEquals(db1, dbObjs.get(testSql));
- assertFalse(mTestPool.isDatabaseObjFree(db1));
- DatabaseConnectionPool.PoolObj poolObj = mTestPool.getPool().get(db1.mConnectionNum - 1);
- int numHolders = poolObj.getNumHolders();
- assertEquals(2, numHolders);
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // get the same connection N times more
- int N = 100;
- for (int i = 0; i < N; i++) {
- SQLiteDatabase db2 = mTestPool.get(testSql);
- assertEquals(db1, db2);
- assertFalse(mTestPool.isDatabaseObjFree(db2));
- // numHolders for this object should be now up by 1
- int prev = numHolders;
- numHolders = poolObj.getNumHolders();
- assertEquals(prev + 1, numHolders);
- }
- // release it N times
- for (int i = 0; i < N; i++) {
- mTestPool.release(db1);
- int prev = numHolders;
- numHolders = poolObj.getNumHolders();
- assertEquals(prev - 1, numHolders);
- assertFalse(mTestPool.isDatabaseObjFree(db1));
- }
- // the connection should still have 2 more holders
- assertFalse(mTestPool.isDatabaseObjFree(db1));
- assertEquals(2, poolObj.getNumHolders());
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // release 2 more times
- mTestPool.release(db1);
- mTestPool.release(db1);
- assertEquals(0, poolObj.getNumHolders());
- assertEquals(1, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- assertTrue(mTestPool.isDatabaseObjFree(db1));
- }
-
- @SmallTest
- public void testStressTest() {
- mTestPool.setMaxPoolSize(MAX_CONN);
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
-
- HashMap<SQLiteDatabase, Integer> dbMap = new HashMap<SQLiteDatabase, Integer>();
- for (int i = 0; i < MAX_CONN; i++) {
- SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]);
- assertFalse(dbMap.containsKey(db));
- dbMap.put(db, 1);
- executeSqlOnDatabaseConn(db, TEST_SQLS[i]);
- }
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // ask for lot more connections but since the pool is maxed out, we should start receiving
- // connections that we already got so far
- for (int i = MAX_CONN; i < 1000; i++) {
- SQLiteDatabase db = mTestPool.get(TEST_SQL + i);
- assertTrue(dbMap.containsKey(db));
- int k = dbMap.get(db);
- dbMap.put(db, ++k);
- }
- assertEquals(0, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- // print the distribution of the database connection handles received, should be uniform.
- for (SQLiteDatabase d : dbMap.keySet()) {
- Log.i(TAG, "connection # " + d.mConnectionNum + ", numHolders: " + dbMap.get(d));
- }
- // print the pool info
- Log.i(TAG, mTestPool.toString());
- // release all
- for (SQLiteDatabase d : dbMap.keySet()) {
- int num = dbMap.get(d);
- for (int i = 0; i < num; i++) {
- mTestPool.release(d);
- }
- }
- assertEquals(MAX_CONN, mTestPool.getFreePoolSize());
- assertEquals(MAX_CONN, mTestPool.getSize());
- assertEquals(MAX_CONN, mTestPool.getMaxPoolSize());
- }
-}
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java
index f6b1d04..9ccc6e8 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java
@@ -21,8 +21,6 @@ import android.content.Context;
import android.database.Cursor;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Log;
import java.io.File;
import java.util.HashSet;
@@ -54,52 +52,8 @@ public class SQLiteCursorTest extends AndroidTestCase {
super.tearDown();
}
- @SmallTest
- public void testQueryObjReassignment() {
- mDatabase.enableWriteAheadLogging();
- // have a few connections in the database connection pool
- DatabaseConnectionPool pool = mDatabase.mConnectionPool;
- pool.setMaxPoolSize(5);
- SQLiteCursor cursor =
- (SQLiteCursor) mDatabase.rawQuery("select * from " + TABLE_NAME, null);
- assertNotNull(cursor);
- // it should use a pooled database connection
- SQLiteDatabase db = cursor.getDatabase();
- assertTrue(db.mConnectionNum > 0);
- assertFalse(mDatabase.equals(db));
- assertEquals(mDatabase, db.mParentConnObj);
- assertTrue(pool.getConnectionList().contains(db));
- assertTrue(db.isOpen());
- // do a requery. cursor should continue to use the above pooled connection
- cursor.requery();
- SQLiteDatabase dbAgain = cursor.getDatabase();
- assertEquals(db, dbAgain);
- // disable WAL so that the pooled connection held by the above cursor is closed
- mDatabase.disableWriteAheadLogging();
- assertFalse(db.isOpen());
- assertNull(mDatabase.mConnectionPool);
- // requery - which should make the cursor use mDatabase connection since the pooled
- // connection is no longer available
- cursor.requery();
- SQLiteDatabase db1 = cursor.getDatabase();
- assertTrue(db1.mConnectionNum == 0);
- assertEquals(mDatabase, db1);
- assertNull(mDatabase.mConnectionPool);
- assertTrue(db1.isOpen());
- assertFalse(mDatabase.equals(db));
- // enable WAL and requery - this time a pooled connection should be used
- mDatabase.enableWriteAheadLogging();
- cursor.requery();
- db = cursor.getDatabase();
- assertTrue(db.mConnectionNum > 0);
- assertFalse(mDatabase.equals(db));
- assertEquals(mDatabase, db.mParentConnObj);
- assertTrue(mDatabase.mConnectionPool.getConnectionList().contains(db));
- assertTrue(db.isOpen());
- }
-
/**
- * this test could take a while to execute. so, designate it as LargetTest
+ * this test could take a while to execute. so, designate it as LargeTest
*/
@LargeTest
public void testFillWindow() {
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
deleted file mode 100644
index 5ef8d11..0000000
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ /dev/null
@@ -1,971 +0,0 @@
-/*
- * Copyright (C) 2006 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.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.DatabaseErrorHandler;
-import android.database.DatabaseUtils;
-import android.database.DefaultDatabaseErrorHandler;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteDatabase.CursorFactory;
-import android.database.sqlite.SQLiteStatement;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.test.suitebuilder.annotation.Suppress;
-import android.util.Log;
-import android.util.Pair;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-public class SQLiteDatabaseTest extends AndroidTestCase {
- private static final String TAG = "DatabaseGeneralTest";
- private static final String TEST_TABLE = "test";
- private static final int CURRENT_DATABASE_VERSION = 42;
- private SQLiteDatabase mDatabase;
- private File mDatabaseFile;
- private static final int INSERT = 1;
- private static final int UPDATE = 2;
- private static final int DELETE = 3;
- private static final String DB_NAME = "database_test.db";
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- dbSetUp();
- }
-
- @Override
- protected void tearDown() throws Exception {
- dbTeardown();
- super.tearDown();
- }
-
- private void dbTeardown() throws Exception {
- mDatabase.close();
- mDatabaseFile.delete();
- }
-
- private void dbSetUp() throws Exception {
- File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE);
- mDatabaseFile = new File(dbDir, DB_NAME);
- if (mDatabaseFile.exists()) {
- mDatabaseFile.delete();
- }
- mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null, null);
- assertNotNull(mDatabase);
- mDatabase.setVersion(CURRENT_DATABASE_VERSION);
- }
-
- @SmallTest
- public void testEnableWriteAheadLogging() {
- mDatabase.disableWriteAheadLogging();
- assertNull(mDatabase.mConnectionPool);
- mDatabase.enableWriteAheadLogging();
- DatabaseConnectionPool pool = mDatabase.mConnectionPool;
- assertNotNull(pool);
- // make the same call again and make sure the pool already setup is not re-created
- mDatabase.enableWriteAheadLogging();
- assertEquals(pool, mDatabase.mConnectionPool);
- }
-
- @SmallTest
- public void testDisableWriteAheadLogging() {
- mDatabase.execSQL("create table test (i int);");
- mDatabase.enableWriteAheadLogging();
- assertNotNull(mDatabase.mConnectionPool);
- // get a pooled database connection
- SQLiteDatabase db = mDatabase.getDbConnection("select * from test");
- assertNotNull(db);
- assertFalse(mDatabase.equals(db));
- assertTrue(db.isOpen());
- // disable WAL - which should close connection pool and all pooled connections
- mDatabase.disableWriteAheadLogging();
- assertNull(mDatabase.mConnectionPool);
- assertFalse(db.isOpen());
- }
-
- @SmallTest
- public void testCursorsWithClosedDbConnAfterDisableWriteAheadLogging() {
- mDatabase.disableWriteAheadLogging();
- mDatabase.beginTransactionNonExclusive();
- mDatabase.execSQL("create table test (i int);");
- mDatabase.execSQL("insert into test values(1);");
- mDatabase.setTransactionSuccessful();
- mDatabase.endTransaction();
- mDatabase.enableWriteAheadLogging();
- assertNotNull(mDatabase.mConnectionPool);
- assertEquals(0, mDatabase.mConnectionPool.getSize());
- assertEquals(0, mDatabase.mConnectionPool.getFreePoolSize());
- // get a cursor which should use pooled database connection
- Cursor c = mDatabase.rawQuery("select * from test", null);
- assertEquals(1, c.getCount());
- assertEquals(1, mDatabase.mConnectionPool.getSize());
- assertEquals(1, mDatabase.mConnectionPool.getFreePoolSize());
- SQLiteDatabase db = mDatabase.mConnectionPool.getConnectionList().get(0);
- assertTrue(mDatabase.mConnectionPool.isDatabaseObjFree(db));
- // disable WAL - which should close connection pool and all pooled connections
- mDatabase.disableWriteAheadLogging();
- assertNull(mDatabase.mConnectionPool);
- assertFalse(db.isOpen());
- // cursor data should still be accessible because it is fetching data from CursorWindow
- c.moveToNext();
- assertEquals(1, c.getInt(0));
- c.requery();
- assertEquals(1, c.getCount());
- c.moveToNext();
- assertEquals(1, c.getInt(0));
- c.close();
- }
-
- /**
- * a transaction should be started before a standalone-update/insert/delete statement
- */
- @SmallTest
- public void testStartXactBeforeUpdateSql() throws InterruptedException {
- runTestForStartXactBeforeUpdateSql(INSERT);
- runTestForStartXactBeforeUpdateSql(UPDATE);
- runTestForStartXactBeforeUpdateSql(DELETE);
- }
- private void runTestForStartXactBeforeUpdateSql(int stmtType) throws InterruptedException {
- createTableAndClearCache();
-
- ContentValues values = new ContentValues();
- // make some changes to data in TEST_TABLE
- for (int i = 0; i < 5; i++) {
- values.put("i", i);
- values.put("j", "i" + System.currentTimeMillis());
- mDatabase.insert(TEST_TABLE, null, values);
- switch (stmtType) {
- case UPDATE:
- values.put("j", "u" + System.currentTimeMillis());
- mDatabase.update(TEST_TABLE, values, "i = " + i, null);
- break;
- case DELETE:
- mDatabase.delete(TEST_TABLE, "i = 1", null);
- break;
- }
- }
- // do a query. even though query uses a different database connection,
- // it should still see the above changes to data because the above standalone
- // insert/update/deletes are done in transactions automatically.
- String sql = "select count(*) from " + TEST_TABLE;
- SQLiteStatement stmt = mDatabase.compileStatement(sql);
- final int expectedValue = (stmtType == DELETE) ? 4 : 5;
- assertEquals(expectedValue, stmt.simpleQueryForLong());
- stmt.close();
- Cursor c = mDatabase.rawQuery(sql, null);
- assertEquals(1, c.getCount());
- c.moveToFirst();
- assertEquals(expectedValue, c.getLong(0));
- c.close();
-
- // do 5 more changes in a transaction but do a query before and after the commit
- mDatabase.beginTransaction();
- for (int i = 10; i < 15; i++) {
- values.put("i", i);
- values.put("j", "i" + System.currentTimeMillis());
- mDatabase.insert(TEST_TABLE, null, values);
- switch (stmtType) {
- case UPDATE:
- values.put("j", "u" + System.currentTimeMillis());
- mDatabase.update(TEST_TABLE, values, "i = " + i, null);
- break;
- case DELETE:
- mDatabase.delete(TEST_TABLE, "i = 1", null);
- break;
- }
- }
- mDatabase.setTransactionSuccessful();
- // do a query before commit - should still have 5 rows
- // this query should run in a different thread to force it to use a different database
- // connection
- Thread t = new Thread() {
- @Override public void run() {
- String sql = "select count(*) from " + TEST_TABLE;
- SQLiteStatement stmt = getDb().compileStatement(sql);
- assertEquals(expectedValue, stmt.simpleQueryForLong());
- stmt.close();
- Cursor c = getDb().rawQuery(sql, null);
- assertEquals(1, c.getCount());
- c.moveToFirst();
- assertEquals(expectedValue, c.getLong(0));
- c.close();
- }
- };
- t.start();
- // wait until the above thread is done
- t.join();
- // commit and then query. should see changes from the transaction
- mDatabase.endTransaction();
- stmt = mDatabase.compileStatement(sql);
- final int expectedValue2 = (stmtType == DELETE) ? 9 : 10;
- assertEquals(expectedValue2, stmt.simpleQueryForLong());
- stmt.close();
- c = mDatabase.rawQuery(sql, null);
- assertEquals(1, c.getCount());
- c.moveToFirst();
- assertEquals(expectedValue2, c.getLong(0));
- c.close();
- }
- private synchronized SQLiteDatabase getDb() {
- return mDatabase;
- }
-
- /**
- * Test to ensure that readers are able to read the database data (old versions)
- * EVEN WHEN the writer is in a transaction on the same database.
- *<p>
- * This test starts 1 Writer and 2 Readers and sets up connection pool for readers
- * by calling the method {@link SQLiteDatabase#enableWriteAheadLogging()}.
- * <p>
- * Writer does the following in a tight loop
- * <pre>
- * begin transaction
- * insert into table_1
- * insert into table_2
- * commit
- * </pre>
- * <p>
- * As long a the writer is alive, Readers do the following in a tight loop at the same time
- * <pre>
- * Reader_K does "select count(*) from table_K" where K = 1 or 2
- * </pre>
- * <p>
- * The test is run for TIME_TO_RUN_WAL_TEST_FOR sec.
- * <p>
- * The test is repeated for different connection-pool-sizes (1..3)
- * <p>
- * And at the end of of each test, the following statistics are printed
- * <ul>
- * <li>connection-pool-size</li>
- * <li>number-of-transactions by writer</li>
- * <li>number of reads by reader_K while the writer is IN or NOT-IN xaction</li>
- * </ul>
- */
- @LargeTest
- @Suppress // run this test only if you need to collect the numbers from this test
- public void testConcurrencyEffectsOfConnPool() throws Exception {
- // run the test with sqlite WAL enable
- runConnectionPoolTest(true);
-
- // run the same test WITHOUT sqlite WAL enabled
- runConnectionPoolTest(false);
- }
-
- private void runConnectionPoolTest(boolean useWal) throws Exception {
- int M = 3;
- StringBuilder[] buff = new StringBuilder[M];
- for (int i = 0; i < M; i++) {
- if (useWal) {
- // set up connection pool
- mDatabase.enableWriteAheadLogging();
- mDatabase.mConnectionPool.setMaxPoolSize(i + 1);
- } else {
- mDatabase.disableWriteAheadLogging();
- }
- mDatabase.execSQL("CREATE TABLE t1 (i int, j int);");
- mDatabase.execSQL("CREATE TABLE t2 (i int, j int);");
- mDatabase.beginTransaction();
- for (int k = 0; k < 5; k++) {
- mDatabase.execSQL("insert into t1 values(?,?);", new String[] {k+"", k+""});
- mDatabase.execSQL("insert into t2 values(?,?);", new String[] {k+"", k+""});
- }
- mDatabase.setTransactionSuccessful();
- mDatabase.endTransaction();
-
- // start a writer
- Writer w = new Writer(mDatabase);
-
- // initialize an array of counters to be passed to the readers
- Reader r1 = new Reader(mDatabase, "t1", w, 0);
- Reader r2 = new Reader(mDatabase, "t2", w, 1);
- w.start();
- r1.start();
- r2.start();
-
- // wait for all threads to die
- w.join();
- r1.join();
- r2.join();
-
- // print the stats
- int[][] counts = getCounts();
- buff[i] = new StringBuilder();
- buff[i].append("connpool-size = ");
- buff[i].append(i + 1);
- buff[i].append(", num xacts by writer = ");
- buff[i].append(getNumXacts());
- buff[i].append(", num-reads-in-xact/NOT-in-xact by reader1 = ");
- buff[i].append(counts[0][1] + "/" + counts[0][0]);
- buff[i].append(", by reader2 = ");
- buff[i].append(counts[1][1] + "/" + counts[1][0]);
-
- Log.i(TAG, "done testing for conn-pool-size of " + (i+1));
-
- dbTeardown();
- dbSetUp();
- }
- Log.i(TAG, "duration of test " + TIME_TO_RUN_WAL_TEST_FOR + " sec");
- for (int i = 0; i < M; i++) {
- Log.i(TAG, buff[i].toString());
- }
- }
-
- private boolean inXact = false;
- private int numXacts;
- private static final int TIME_TO_RUN_WAL_TEST_FOR = 15; // num sec this test should run
- private int[][] counts = new int[2][2];
-
- private synchronized boolean inXact() {
- return inXact;
- }
-
- private synchronized void setInXactFlag(boolean flag) {
- inXact = flag;
- }
-
- private synchronized void setCounts(int readerNum, int[] numReads) {
- counts[readerNum][0] = numReads[0];
- counts[readerNum][1] = numReads[1];
- }
-
- private synchronized int[][] getCounts() {
- return counts;
- }
-
- private synchronized void setNumXacts(int num) {
- numXacts = num;
- }
-
- private synchronized int getNumXacts() {
- return numXacts;
- }
-
- private class Writer extends Thread {
- private SQLiteDatabase db = null;
- public Writer(SQLiteDatabase db) {
- this.db = db;
- }
- @Override public void run() {
- // in a loop, for N sec, do the following
- // BEGIN transaction
- // insert into table t1, t2
- // Commit
- long now = System.currentTimeMillis();
- int k;
- for (k = 0;(System.currentTimeMillis() - now) / 1000 < TIME_TO_RUN_WAL_TEST_FOR; k++) {
- db.beginTransactionNonExclusive();
- setInXactFlag(true);
- for (int i = 0; i < 10; i++) {
- db.execSQL("insert into t1 values(?,?);", new String[] {i+"", i+""});
- db.execSQL("insert into t2 values(?,?);", new String[] {i+"", i+""});
- }
- db.setTransactionSuccessful();
- setInXactFlag(false);
- db.endTransaction();
- }
- setNumXacts(k);
- }
- }
-
- private class Reader extends Thread {
- private SQLiteDatabase db = null;
- private String table = null;
- private Writer w = null;
- private int readerNum;
- private int[] numReads = new int[2];
- public Reader(SQLiteDatabase db, String table, Writer w, int readerNum) {
- this.db = db;
- this.table = table;
- this.w = w;
- this.readerNum = readerNum;
- }
- @Override public void run() {
- // while the write is alive, in a loop do the query on a table
- while (w.isAlive()) {
- for (int i = 0; i < 10; i++) {
- DatabaseUtils.longForQuery(db, "select count(*) from " + this.table, null);
- // update count of reads
- numReads[inXact() ? 1 : 0] += 1;
- }
- }
- setCounts(readerNum, numReads);
- }
- }
-
- public static class ClassToTestSqlCompilationAndCaching extends SQLiteProgram {
- private ClassToTestSqlCompilationAndCaching(SQLiteDatabase db, String sql) {
- super(db, sql);
- }
- public static ClassToTestSqlCompilationAndCaching create(SQLiteDatabase db, String sql) {
- db.lock();
- try {
- return new ClassToTestSqlCompilationAndCaching(db, sql);
- } finally {
- db.unlock();
- }
- }
- }
-
- @SmallTest
- public void testLruCachingOfSqliteCompiledSqlObjs() {
- createTableAndClearCache();
- // set cache size
- int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE;
- mDatabase.setMaxSqlCacheSize(N);
-
- // do N+1 queries - and when the 0th entry is removed from LRU cache due to the
- // insertion of (N+1)th entry, make sure 0th entry is closed
- ArrayList<Integer> stmtObjs = new ArrayList<Integer>();
- ArrayList<String> sqlStrings = new ArrayList<String>();
- int stmt0 = 0;
- for (int i = 0; i < N+1; i++) {
- String s = "insert into test values(" + i + ",?);";
- sqlStrings.add(s);
- ClassToTestSqlCompilationAndCaching c =
- ClassToTestSqlCompilationAndCaching.create(mDatabase, s);
- int n = c.getSqlStatementId();
- stmtObjs.add(i, n);
- if (i == 0) {
- // save the statementId of this obj. we want to make sure it is thrown out of
- // the cache at the end of this test.
- stmt0 = n;
- }
- c.close();
- }
- // is 0'th entry out of the cache? it should be in the list of statementIds
- // corresponding to the pre-compiled sql statements to be finalized.
- assertTrue(mDatabase.getQueuedUpStmtList().contains(stmt0));
- for (int i = 1; i < N+1; i++) {
- SQLiteCompiledSql compSql = mDatabase.getCompiledStatementForSql(sqlStrings.get(i));
- assertNotNull(compSql);
- assertTrue(stmtObjs.contains(compSql.nStatement));
- }
- }
-
- @MediumTest
- public void testDbCloseReleasingAllCachedSql() {
- mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " +
- "num1 INTEGER, num2 INTEGER, image BLOB);");
- final String statement = "DELETE FROM test WHERE _id=?;";
- SQLiteStatement statementDoNotClose = mDatabase.compileStatement(statement);
- statementDoNotClose.bindLong(1, 1);
- /* do not close statementDoNotClose object.
- * That should leave it in SQLiteDatabase.mPrograms.
- * mDatabase.close() in tearDown() should release it.
- */
- }
-
- private void createTableAndClearCache() {
- mDatabase.disableWriteAheadLogging();
- mDatabase.execSQL("DROP TABLE IF EXISTS " + TEST_TABLE);
- mDatabase.execSQL("CREATE TABLE " + TEST_TABLE + " (i int, j int);");
- mDatabase.enableWriteAheadLogging();
- mDatabase.lock();
- // flush the above statement from cache and close all the pending statements to be released
- mDatabase.deallocCachedSqlStatements();
- mDatabase.closePendingStatements();
- mDatabase.unlock();
- assertEquals(0, mDatabase.getQueuedUpStmtList().size());
- }
-
- /**
- * test to make sure the statement finalizations are not done right away but
- * piggy-backed onto the next sql statement execution on the same database.
- */
- @SmallTest
- public void testStatementClose() {
- createTableAndClearCache();
- // fill up statement cache in mDatabase
- int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE;
- mDatabase.setMaxSqlCacheSize(N);
- SQLiteStatement stmt;
- int stmt0Id = 0;
- for (int i = 0; i < N; i ++) {
- ClassToTestSqlCompilationAndCaching c =
- ClassToTestSqlCompilationAndCaching.create(mDatabase,
- "insert into test values(" + i + ", ?);");
- // keep track of 0th entry
- if (i == 0) {
- stmt0Id = c.getSqlStatementId();
- }
- c.close();
- }
-
- // add one more to the cache - and the above 'stmt0Id' should fall out of cache
- ClassToTestSqlCompilationAndCaching stmt1 =
- ClassToTestSqlCompilationAndCaching.create(mDatabase,
- "insert into test values(100, ?);");
- stmt1.close();
-
- // the above close() should have queuedUp the statement for finalization
- ArrayList<Integer> statementIds = mDatabase.getQueuedUpStmtList();
- assertTrue(statementIds.contains(stmt0Id));
-
- // execute something to see if this statement gets finalized
- mDatabase.execSQL("delete from test where i = 10;");
- statementIds = mDatabase.getQueuedUpStmtList();
- assertFalse(statementIds.contains(stmt0Id));
- }
-
- /**
- * same as above - except that the statement to be finalized is from Thread # 1.
- * and it is eventually finalized in Thread # 2 when it executes a SQL statement.
- * @throws InterruptedException
- */
- @LargeTest
- public void testStatementCloseDiffThread() throws InterruptedException {
- createTableAndClearCache();
- final int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE;
- mDatabase.setMaxSqlCacheSize(N);
- // fill up statement cache in mDatabase in a thread
- Thread t1 = new Thread() {
- @Override public void run() {
- SQLiteStatement stmt;
- for (int i = 0; i < N; i++) {
- ClassToTestSqlCompilationAndCaching c =
- ClassToTestSqlCompilationAndCaching.create(getDb(),
- "insert into test values(" + i + ", ?);");
- // keep track of 0th entry
- if (i == 0) {
- stmt0Id = c.getSqlStatementId();
- }
- c.close();
- }
- }
- };
- t1.start();
- // wait for the thread to finish
- t1.join();
- // mDatabase shouldn't have any statements to be released
- assertEquals(0, mDatabase.getQueuedUpStmtList().size());
-
- // add one more to the cache - and the above 'stmt0Id' should fall out of cache
- // just for the heck of it, do it in a separate thread
- Thread t2 = new Thread() {
- @Override public void run() {
- ClassToTestSqlCompilationAndCaching stmt1 =
- ClassToTestSqlCompilationAndCaching.create(getDb(),
- "insert into test values(100, ?);");
- stmt1.bindLong(1, 1);
- stmt1.close();
- }
- };
- t2.start();
- t2.join();
-
- // close() in the above thread should have queuedUp the stmt0Id for finalization
- ArrayList<Integer> statementIds = getDb().getQueuedUpStmtList();
- assertTrue(statementIds.contains(getStmt0Id()));
- assertEquals(1, statementIds.size());
-
- // execute something to see if this statement gets finalized
- // again do it in a separate thread
- Thread t3 = new Thread() {
- @Override public void run() {
- getDb().execSQL("delete from test where i = 10;");
- }
- };
- t3.start();
- t3.join();
-
- // is the statement finalized?
- statementIds = getDb().getQueuedUpStmtList();
- assertFalse(statementIds.contains(getStmt0Id()));
- }
-
- private volatile int stmt0Id = 0;
- private synchronized int getStmt0Id() {
- return this.stmt0Id;
- }
-
- /**
- * same as above - except that the queue of statements to be finalized are finalized
- * by database close() operation.
- */
- @LargeTest
- public void testStatementCloseByDbClose() throws InterruptedException {
- createTableAndClearCache();
- // fill up statement cache in mDatabase in a thread
- Thread t1 = new Thread() {
- @Override public void run() {
- int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE;
- getDb().setMaxSqlCacheSize(N);
- SQLiteStatement stmt;
- for (int i = 0; i < N; i ++) {
- ClassToTestSqlCompilationAndCaching c =
- ClassToTestSqlCompilationAndCaching.create(getDb(),
- "insert into test values(" + i + ", ?);");
- // keep track of 0th entry
- if (i == 0) {
- stmt0Id = c.getSqlStatementId();
- }
- c.close();
- }
- }
- };
- t1.start();
- // wait for the thread to finish
- t1.join();
-
- // add one more to the cache - and the above 'stmt0Id' should fall out of cache
- // just for the heck of it, do it in a separate thread
- Thread t2 = new Thread() {
- @Override public void run() {
- ClassToTestSqlCompilationAndCaching stmt1 =
- ClassToTestSqlCompilationAndCaching.create(getDb(),
- "insert into test values(100, ?);");
- stmt1.bindLong(1, 1);
- stmt1.close();
- }
- };
- t2.start();
- t2.join();
-
- // close() in the above thread should have queuedUp the statement for finalization
- ArrayList<Integer> statementIds = getDb().getQueuedUpStmtList();
- assertTrue(getStmt0Id() > 0);
- assertTrue(statementIds.contains(stmt0Id));
- assertEquals(1, statementIds.size());
-
- // close the database. everything from mClosedStatementIds in mDatabase
- // should be finalized and cleared from the list
- // again do it in a separate thread
- Thread t3 = new Thread() {
- @Override public void run() {
- getDb().close();
- }
- };
- t3.start();
- t3.join();
-
- // check mClosedStatementIds in mDatabase. it should be empty
- statementIds = getDb().getQueuedUpStmtList();
- assertEquals(0, statementIds.size());
- }
-
- /**
- * This test tests usage execSQL() to begin transaction works in the following way
- * Thread #1 does
- * execSQL("begin transaction");
- * insert()
- * Thread # 2
- * query()
- * Thread#1 ("end transaction")
- * Thread # 2 query will execute - because java layer will not have locked the SQLiteDatabase
- * object and sqlite will consider this query to be part of the transaction.
- *
- * but if thread # 1 uses beginTransaction() instead of execSQL() to start transaction,
- * then Thread # 2's query will have been blocked by java layer
- * until Thread#1 ends transaction.
- *
- * @throws InterruptedException
- */
- @SmallTest
- public void testExecSqlToStartAndEndTransaction() throws InterruptedException {
- runExecSqlToStartAndEndTransaction("END");
- // same as above, instead now do "COMMIT" or "ROLLBACK" instead of "END" transaction
- runExecSqlToStartAndEndTransaction("COMMIT");
- runExecSqlToStartAndEndTransaction("ROLLBACK");
- }
- private void runExecSqlToStartAndEndTransaction(String str) throws InterruptedException {
- createTableAndClearCache();
- // disable WAL just so queries and updates use the same database connection
- mDatabase.disableWriteAheadLogging();
- mDatabase.execSQL("BEGIN transaction");
- // even though mDatabase.beginTransaction() is not called to start transaction,
- // mDatabase connection should now be in transaction as a result of
- // mDatabase.execSQL("BEGIN transaction")
- // but mDatabase.mLock should not be held by any thread
- assertTrue(mDatabase.inTransaction());
- assertFalse(mDatabase.isDbLockedByCurrentThread());
- assertFalse(mDatabase.isDbLockedByOtherThreads());
- assertTrue(mDatabase.amIInTransaction());
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(10, 999);");
- assertTrue(mDatabase.inTransaction());
- assertFalse(mDatabase.isDbLockedByCurrentThread());
- assertFalse(mDatabase.isDbLockedByOtherThreads());
- assertTrue(mDatabase.amIInTransaction());
- Thread t = new Thread() {
- @Override public void run() {
- assertTrue(mDatabase.amIInTransaction());
- assertEquals(999, DatabaseUtils.longForQuery(getDb(),
- "select j from " + TEST_TABLE + " WHERE i = 10", null));
- assertTrue(getDb().inTransaction());
- assertFalse(getDb().isDbLockedByCurrentThread());
- assertFalse(getDb().isDbLockedByOtherThreads());
- assertTrue(mDatabase.amIInTransaction());
- }
- };
- t.start();
- t.join();
- assertTrue(mDatabase.amIInTransaction());
- assertTrue(mDatabase.inTransaction());
- assertFalse(mDatabase.isDbLockedByCurrentThread());
- assertFalse(mDatabase.isDbLockedByOtherThreads());
- mDatabase.execSQL(str);
- assertFalse(mDatabase.amIInTransaction());
- assertFalse(mDatabase.inTransaction());
- assertFalse(mDatabase.isDbLockedByCurrentThread());
- assertFalse(mDatabase.isDbLockedByOtherThreads());
- }
-
- /**
- * test the following
- * http://b/issue?id=2871037
- * Cursor cursor = db.query(...);
- * // with WAL enabled, the above uses a pooled database connection
- * db.beginTransaction()
- * try {
- * db.insert(......);
- * cursor.requery();
- * // since the cursor uses pooled database connection, the above requery
- * // will not return the results that were inserted above since the insert is
- * // done using main database connection AND the transaction is not committed yet.
- * // fix is to make the above cursor use the main database connection - and NOT
- * // the pooled database connection
- * db.setTransactionSuccessful()
- * } finally {
- * db.endTransaction()
- * }
- *
- * @throws InterruptedException
- */
- @SmallTest
- public void testTransactionAndWalInterplay1() throws InterruptedException {
- createTableAndClearCache();
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(10, 999);");
- String sql = "select * from " + TEST_TABLE;
- Cursor c = mDatabase.rawQuery(sql, null);
- // should have 1 row in the table
- assertEquals(1, c.getCount());
- mDatabase.beginTransactionNonExclusive();
- try {
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(100, 9909);");
- assertEquals(2, DatabaseUtils.longForQuery(mDatabase,
- "select count(*) from " + TEST_TABLE, null));
- // requery on the previously opened cursor
- // cursor should now use the main database connection and see 2 rows
- c.requery();
- assertEquals(2, c.getCount());
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
- c.close();
-
- // do the same test but now do the requery in a separate thread.
- createTableAndClearCache();
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(10, 999);");
- final Cursor c1 = mDatabase.rawQuery("select count(*) from " + TEST_TABLE, null);
- // should have 1 row in the table
- assertEquals(1, c1.getCount());
- mDatabase.beginTransactionNonExclusive();
- try {
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(100, 9909);");
- assertEquals(2, DatabaseUtils.longForQuery(mDatabase,
- "select count(*) from " + TEST_TABLE, null));
- // query in a different thread. that causes the cursor to use a pooled connection
- // and since this thread hasn't committed its changes, the cursor should still see only
- // 1 row
- Thread t = new Thread() {
- @Override public void run() {
- c1.requery();
- assertEquals(1, c1.getCount());
- }
- };
- t.start();
- t.join();
- // should be 2 rows now - including the the row inserted above
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
- c1.close();
- }
-
- /**
- * This test is same as {@link #testTransactionAndWalInterplay1()} except the following:
- * instead of mDatabase.beginTransactionNonExclusive(), use execSQL("BEGIN transaction")
- * and instead of mDatabase.endTransaction(), use execSQL("END");
- */
- @SmallTest
- public void testTransactionAndWalInterplay2() throws InterruptedException {
- createTableAndClearCache();
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(10, 999);");
- String sql = "select * from " + TEST_TABLE;
- Cursor c = mDatabase.rawQuery(sql, null);
- // should have 1 row in the table
- assertEquals(1, c.getCount());
- mDatabase.execSQL("BEGIN transaction");
- try {
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(100, 9909);");
- assertEquals(2, DatabaseUtils.longForQuery(mDatabase,
- "select count(*) from " + TEST_TABLE, null));
- // requery on the previously opened cursor
- // cursor should now use the main database connection and see 2 rows
- c.requery();
- assertEquals(2, c.getCount());
- } finally {
- mDatabase.execSQL("commit;");
- }
- c.close();
-
- // do the same test but now do the requery in a separate thread.
- createTableAndClearCache();
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(10, 999);");
- final Cursor c1 = mDatabase.rawQuery("select count(*) from " + TEST_TABLE, null);
- // should have 1 row in the table
- assertEquals(1, c1.getCount());
- mDatabase.execSQL("BEGIN transaction");
- try {
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(100, 9909);");
- assertEquals(2, DatabaseUtils.longForQuery(mDatabase,
- "select count(*) from " + TEST_TABLE, null));
- // query in a different thread. but since the transaction is started using
- // execSQ() instead of beginTransaction(), cursor's query is considered part of
- // the same transaction - and hence it should see the above inserted row
- Thread t = new Thread() {
- @Override public void run() {
- c1.requery();
- assertEquals(1, c1.getCount());
- }
- };
- t.start();
- t.join();
- // should be 2 rows now - including the the row inserted above
- } finally {
- mDatabase.execSQL("commit");
- }
- c1.close();
- }
-
- /**
- * This test is same as {@link #testTransactionAndWalInterplay2()} except the following:
- * instead of committing the data, do rollback and make sure the data seen by the query
- * within the transaction is now gone.
- */
- @SmallTest
- public void testTransactionAndWalInterplay3() {
- createTableAndClearCache();
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(10, 999);");
- String sql = "select * from " + TEST_TABLE;
- Cursor c = mDatabase.rawQuery(sql, null);
- // should have 1 row in the table
- assertEquals(1, c.getCount());
- mDatabase.execSQL("BEGIN transaction");
- try {
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(100, 9909);");
- assertEquals(2, DatabaseUtils.longForQuery(mDatabase,
- "select count(*) from " + TEST_TABLE, null));
- // requery on the previously opened cursor
- // cursor should now use the main database connection and see 2 rows
- c.requery();
- assertEquals(2, c.getCount());
- } finally {
- // rollback the change
- mDatabase.execSQL("rollback;");
- }
- // since the change is rolled back, do the same query again and should now find only 1 row
- c.requery();
- assertEquals(1, c.getCount());
- assertEquals(1, DatabaseUtils.longForQuery(mDatabase,
- "select count(*) from " + TEST_TABLE, null));
- c.close();
- }
-
- @SmallTest
- public void testAttachDb() {
- String newDb = "/sdcard/mydata.db";
- File f = new File(newDb);
- if (f.exists()) {
- f.delete();
- }
- assertFalse(f.exists());
- SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(newDb, null);
- db.execSQL("create table test1 (i int);");
- db.execSQL("insert into test1 values(1);");
- db.execSQL("insert into test1 values(11);");
- Cursor c = null;
- try {
- c = db.rawQuery("select * from test1", null);
- int count = c.getCount();
- Log.i(TAG, "count: " + count);
- assertEquals(2, count);
- } finally {
- c.close();
- db.close();
- c = null;
- }
-
- mDatabase.execSQL("attach database ? as newDb" , new String[]{newDb});
- Cursor c1 = null;
- try {
- c1 = mDatabase.rawQuery("select * from newDb.test1", null);
- assertEquals(2, c1.getCount());
- } catch (Exception e) {
- fail("unexpected exception: " + e.getMessage());
- } finally {
- if (c1 != null) {
- c1.close();
- }
- }
- List<Pair<String, String>> dbs = mDatabase.getAttachedDbs();
- for (Pair<String, String> p: dbs) {
- Log.i(TAG, "attached dbs: " + p.first + " : " + p.second);
- }
- assertEquals(2, dbs.size());
- }
-
- /**
- * http://b/issue?id=2943028
- * SQLiteOpenHelper maintains a Singleton even if it is in bad state.
- */
- @SmallTest
- public void testCloseAndReopen() {
- mDatabase.close();
- TestOpenHelper helper = new TestOpenHelper(getContext(), DB_NAME, null,
- CURRENT_DATABASE_VERSION, new DefaultDatabaseErrorHandler());
- mDatabase = helper.getWritableDatabase();
- createTableAndClearCache();
- mDatabase.execSQL("INSERT into " + TEST_TABLE + " values(10, 999);");
- Cursor c = mDatabase.query(TEST_TABLE, new String[]{"i", "j"}, null, null, null, null, null);
- assertEquals(1, c.getCount());
- c.close();
- mDatabase.close();
- assertFalse(mDatabase.isOpen());
- mDatabase = helper.getReadableDatabase();
- assertTrue(mDatabase.isOpen());
- c = mDatabase.query(TEST_TABLE, new String[]{"i", "j"}, null, null, null, null, null);
- assertEquals(1, c.getCount());
- c.close();
- }
- private class TestOpenHelper extends SQLiteOpenHelper {
- public TestOpenHelper(Context context, String name, CursorFactory factory, int version,
- DatabaseErrorHandler errorHandler) {
- super(context, name, factory, version, errorHandler);
- }
- @Override public void onCreate(SQLiteDatabase db) {}
- @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
- }
-}
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java
deleted file mode 100644
index 955336a..0000000
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2006 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.Context;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import java.io.File;
-import java.util.Random;
-import java.util.concurrent.locks.ReentrantLock;
-
-public class SQLiteStatementTest extends AndroidTestCase {
- private SQLiteDatabase mDatabase;
- private File mDatabaseFile;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
-
- File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE);
- mDatabaseFile = new File(dbDir, "database_test.db");
- if (mDatabaseFile.exists()) {
- mDatabaseFile.delete();
- }
- mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null);
- assertNotNull(mDatabase);
- }
-
- @Override
- protected void tearDown() throws Exception {
- mDatabase.close();
- mDatabaseFile.delete();
- super.tearDown();
- }
-
- /**
- * Start 2 threads to repeatedly execute the above SQL statement.
- * Even though 2 threads are executing the same SQL, they each should get their own copy of
- * prepared SQL statement id and there SHOULD NOT be an error from sqlite or android.
- * @throws InterruptedException thrown if the test threads started by this test are interrupted
- */
- @LargeTest
- public void testUseOfSameSqlStatementBy2Threads() throws InterruptedException {
- mDatabase.execSQL("CREATE TABLE test_pstmt (i INTEGER PRIMARY KEY, j text);");
- final String stmt = "SELECT * FROM test_pstmt WHERE i = ?";
- class RunStmtThread extends Thread {
- @Override public void run() {
- // do it enough times to make sure there are no corner cases going untested
- for (int i = 0; i < 1000; i++) {
- SQLiteStatement s1 = mDatabase.compileStatement(stmt);
- s1.bindLong(1, i);
- s1.execute();
- s1.close();
- }
- }
- }
- RunStmtThread t1 = new RunStmtThread();
- t1.start();
- RunStmtThread t2 = new RunStmtThread();
- t2.start();
- while (t1.isAlive() || t2.isAlive()) {
- Thread.sleep(10);
- }
- }
-
- /**
- * A simple test: start 2 threads to repeatedly execute the same {@link SQLiteStatement}.
- * The 2 threads take turns to use the {@link SQLiteStatement}; i.e., it is NOT in use
- * by both the threads at the same time.
- *
- * @throws InterruptedException thrown if the test threads started by this test are interrupted
- */
- @LargeTest
- public void testUseOfSameSqliteStatementBy2Threads() throws InterruptedException {
- mDatabase.execSQL("CREATE TABLE test_pstmt (i INTEGER PRIMARY KEY, j text);");
- final String stmt = "SELECT * FROM test_pstmt WHERE i = ?";
- final SQLiteStatement s1 = mDatabase.compileStatement(stmt);
- class RunStmtThread extends Thread {
- @Override public void run() {
- // do it enough times to make sure there are no corner cases going untested
- for (int i = 0; i < 1000; i++) {
- lock();
- try {
- s1.bindLong(1, i);
- s1.execute();
- } finally {
- unlock();
- }
- Thread.yield();
- }
- }
- }
- RunStmtThread t1 = new RunStmtThread();
- t1.start();
- RunStmtThread t2 = new RunStmtThread();
- t2.start();
- while (t1.isAlive() || t2.isAlive()) {
- Thread.sleep(10);
- }
- }
- /** Synchronize on this when accessing the SqliteStatemet in the above */
- private final ReentrantLock mLock = new ReentrantLock(true);
- private void lock() {
- mLock.lock();
- }
- private void unlock() {
- mLock.unlock();
- }
-
- /**
- * Tests the following: a {@link SQLiteStatement} object should not refer to a
- * pre-compiled SQL statement id except in during the period of binding the arguments
- * and executing the SQL statement.
- */
- @LargeTest
- public void testReferenceToPrecompiledStatementId() {
- mDatabase.execSQL("create table t (i int, j text);");
- verifyReferenceToPrecompiledStatementId(false);
- verifyReferenceToPrecompiledStatementId(true);
-
- // a small stress test to make sure there are no side effects of
- // the acquire & release of pre-compiled statement id by SQLiteStatement object.
- for (int i = 0; i < 100; i++) {
- verifyReferenceToPrecompiledStatementId(false);
- verifyReferenceToPrecompiledStatementId(true);
- }
- }
-
- @SuppressWarnings("deprecation")
- private void verifyReferenceToPrecompiledStatementId(boolean wal) {
- if (wal) {
- mDatabase.enableWriteAheadLogging();
- } else {
- mDatabase.disableWriteAheadLogging();
- }
- // test with INSERT statement - doesn't use connection pool, if WAL is set
- SQLiteStatement stmt = mDatabase.compileStatement("insert into t values(?,?);");
- assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
- assertEquals(mDatabase, stmt.mDatabase);
- // sql statement should not be compiled yet
- assertEquals(0, stmt.nStatement);
- assertEquals(0, stmt.getSqlStatementId());
- int colValue = new Random().nextInt();
- stmt.bindLong(1, colValue);
- // verify that the sql statement is still not compiled
- assertEquals(0, stmt.getSqlStatementId());
- // should still be using the mDatabase connection - verify
- assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
- assertEquals(mDatabase, stmt.mDatabase);
- stmt.bindString(2, "blah" + colValue);
- // verify that the sql statement is still not compiled
- assertEquals(0, stmt.getSqlStatementId());
- assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
- assertEquals(mDatabase, stmt.mDatabase);
- stmt.executeInsert();
- // now that the statement is executed, pre-compiled statement should be released
- assertEquals(0, stmt.nStatement);
- assertEquals(0, stmt.getSqlStatementId());
- assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
- assertEquals(mDatabase, stmt.mDatabase);
- stmt.close();
- // pre-compiled SQL statement should still remain released from this object
- assertEquals(0, stmt.nStatement);
- assertEquals(0, stmt.getSqlStatementId());
- // but the database handle should still be the same
- assertEquals(mDatabase, stmt.mDatabase);
-
- // test with a SELECT statement - uses connection pool if WAL is set
- stmt = mDatabase.compileStatement("select i from t where j=?;");
- // sql statement should not be compiled yet
- assertEquals(0, stmt.nStatement);
- assertEquals(0, stmt.getSqlStatementId());
- assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
- assertEquals(mDatabase, stmt.mDatabase);
- stmt.bindString(1, "blah" + colValue);
- // verify that the sql statement is still not compiled
- assertEquals(0, stmt.nStatement);
- assertEquals(0, stmt.getSqlStatementId());
- assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
- assertEquals(mDatabase, stmt.mDatabase);
- // execute the statement
- Long l = stmt.simpleQueryForLong();
- assertEquals(colValue, l.intValue());
- // now that the statement is executed, pre-compiled statement should be released
- assertEquals(0, stmt.nStatement);
- assertEquals(0, stmt.getSqlStatementId());
- assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
- assertEquals(mDatabase, stmt.mDatabase);
- stmt.close();
- // pre-compiled SQL statement should still remain released from this object
- assertEquals(0, stmt.nStatement);
- assertEquals(0, stmt.getSqlStatementId());
- // but the database handle should still remain attached to the statement
- assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
- assertEquals(mDatabase, stmt.mDatabase);
- }
-}
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteUnfinalizedExceptionTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteUnfinalizedExceptionTest.java
deleted file mode 100644
index cd2005d..0000000
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteUnfinalizedExceptionTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2010 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.Context;
-import android.database.sqlite.SQLiteDatabaseTest.ClassToTestSqlCompilationAndCaching;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import java.io.File;
-
-public class SQLiteUnfinalizedExceptionTest extends AndroidTestCase {
- private SQLiteDatabase mDatabase;
- private File mDatabaseFile;
- private static final String TABLE_NAME = "testCursor";
- @Override
- protected void setUp() throws Exception {
- super.setUp();
-
- File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE);
- mDatabaseFile = new File(dbDir, "UnfinalizedExceptionTest.db");
- if (mDatabaseFile.exists()) {
- mDatabaseFile.delete();
- }
- mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null);
- assertNotNull(mDatabase);
- }
-
- @Override
- protected void tearDown() throws Exception {
- mDatabase.close();
- mDatabaseFile.delete();
- super.tearDown();
- }
-
- @SmallTest
- public void testUnfinalizedExceptionNotExcpected() {
- mDatabase.execSQL("CREATE TABLE " + TABLE_NAME + " (i int, j int);");
- // the above statement should be in SQLiteDatabase.mPrograms
- // and should automatically be finalized when database is closed
- mDatabase.lock();
- try {
- mDatabase.closeDatabase();
- } finally {
- mDatabase.unlock();
- }
- }
-
- @SmallTest
- public void testUnfinalizedException() {
- mDatabase.execSQL("CREATE TABLE " + TABLE_NAME + " (i int, j int);");
- mDatabase.lock();
- mDatabase.closePendingStatements(); // clears the above from finalizer queue in mdatabase
- mDatabase.unlock();
- ClassToTestSqlCompilationAndCaching.create(mDatabase, "select * from " + TABLE_NAME);
- // since the above is NOT closed, closing database should fail
- mDatabase.lock();
- try {
- mDatabase.closeDatabase();
- fail("exception expected");
- } catch (SQLiteUnfinalizedObjectsException e) {
- // expected
- } finally {
- mDatabase.unlock();
- }
- }
-}