diff options
author | Jeff Brown <jeffbrown@google.com> | 2012-01-12 15:03:26 -0800 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2012-01-12 15:03:26 -0800 |
commit | 986f00faf44b0d9ed5b1384746ca4254037fc180 (patch) | |
tree | ee51ba282c84cc22d2391ff307fdfda10af9b92e | |
parent | 156936975dec49022681f218b8221ef9e3d87011 (diff) | |
parent | e5360fbf3efe85427f7e7f59afe7bbeddb4949ac (diff) | |
download | frameworks_base-986f00faf44b0d9ed5b1384746ca4254037fc180.zip frameworks_base-986f00faf44b0d9ed5b1384746ca4254037fc180.tar.gz frameworks_base-986f00faf44b0d9ed5b1384746ca4254037fc180.tar.bz2 |
Merge "Rewrite SQLite database wrappers."
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(); - } - } -} |