Rewrite SQLite database wrappers.
The main theme of this change is encapsulation. This change preserves all existing functionality but the implementation is now much cleaner. Instead of a "database lock", access to the database is treated as a resource acquisition problem. If a thread's owns a database connection, then it can access the database; otherwise, it must acquire a database connection first, and potentially wait for other threads to give up theirs. The SQLiteConnectionPool encapsulates the details of how connections are created, configured, acquired, released and disposed. One new feature is that SQLiteConnectionPool can make scheduling decisions about which thread should next acquire a database connection when there is contention among threads. The factors considered include wait queue ordering (fairness among peers), whether the connection is needed for an interactive operation (unfairness on behalf of the UI), and whether the primary connection is needed or if any old connection will do. Thus one goal of the new SQLiteConnectionPool is to improve the utilization of database connections. To emulate some quirks of the old "database lock," we introduce the concept of the primary database connection. The primary database connection is the one that is typically used to perform write operations to the database. When a thread holds the primary database connection, it effectively prevents other threads from modifying the database (although they can still read). What's more, those threads will block when they try to acquire the primary connection, which provides the same kind of mutual exclusion features that the old "database lock" had. (In truth, we probably don't need to be requiring use of the primary database connection in as many places as we do now, but we can seek to refine that behavior in future patches.) Another significant change is that native sqlite3_stmt objects (prepared statements) are fully encapsulated by the SQLiteConnection object that owns them. This ensures that the connection can finalize (destroy) all extant statements that belong to a database connection when the connection is closed. (In the original code, this was very complicated because the sqlite3_stmt objects were managed by SQLiteCompiledSql objects which had different lifetime from the original SQLiteDatabase that created them. Worse, the SQLiteCompiledSql finalizer method couldn't actually destroy the sqlite3_stmt objects because it ran on the finalizer thread and therefore could not guarantee that it could acquire the database lock in order to do the work. This resulted in some rather tortured logic involving a list of pending finalizable statements and a high change of deadlocks or leaks.) Because sqlite3_stmt objects never escape the confines of the SQLiteConnection that owns them, we can also greatly simplify the design of the SQLiteProgram, SQLiteQuery and SQLiteStatement objects. They no longer have to wrangle a native sqlite3_stmt object pointer and manage its lifecycle. So now all they do is hold bind arguments and provide a fancy API. All of the JNI glue related to managing database connections and performing transactions is now bound to SQLiteConnection (rather than being scattered everywhere). This makes sense because SQLiteConnection owns the native sqlite3 object, so it is the only class in the system that can interact with the native SQLite database directly. Encapsulation for the win. One particularly tricky part of this change is managing the ownership of SQLiteConnection objects. At any given time, a SQLiteConnection is either owned by a SQLiteConnectionPool or by a SQLiteSession. SQLiteConnections should never be leaked, but we handle that case too (and yell about it with CloseGuard). A SQLiteSession object is responsible for acquiring and releasing a SQLiteConnection object on behalf of a single thread as needed. For example, the session acquires a connection when a transaction begins and releases it when finished. If the session cannot acquire a connection immediately, then the requested operation blocks until a connection becomes available. SQLiteSessions are thread-local. A SQLiteDatabase assigns a distinct session to each thread that performs database operations. This is very very important. First, it prevents two threads from trying to use the same SQLiteConnection at the same time (because two threads can't share the same session). Second, it prevents a single thread from trying to acquire two SQLiteConnections simultaneously from the same database (because a single thread can't have two sessions for the same database which, in addition to being greedy, could result in a deadlock). There is strict layering between the various database objects, objects at lower layers are not aware of objects at higher layers. Moreover, objects at higher layers generally own objects at lower layers and are responsible for ensuring they are properly disposed when no longer needed (good for the environment). API layer: SQLiteDatabase, SQLiteProgram, SQLiteQuery, SQLiteStatement. Session layer: SQLiteSession. Connection layer: SQLiteConnectionPool, SQLiteConnection. Native layer: JNI glue. By avoiding cyclic dependencies between layers, we make the architecture much more intelligible, maintainable and robust. Finally, this change adds a great deal of new debugging information. It is now possible to view a list of the most recent database operations including how long they took to run using "adb shell dumpsys dbinfo". (Because most of the interesting work happens in SQLiteConnection, it is easy to add debugging instrumentation to track all database operations in one place.) Change-Id: Iffb4ce72d8bcf20b4e087d911da6aa84d2f15297
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
1149
core/java/android/database/sqlite/SQLiteConnection.java
Normal file
1149
core/java/android/database/sqlite/SQLiteConnection.java
Normal file
File diff suppressed because it is too large
Load Diff
907
core/java/android/database/sqlite/SQLiteConnectionPool.java
Normal file
907
core/java/android/database/sqlite/SQLiteConnectionPool.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
mDriver.cursorRequeried(this);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
Log.v("DatabaseWindow", "closing window in requery()");
|
||||
Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
53
core/java/android/database/sqlite/SQLiteCustomFunction.java
Normal file
53
core/java/android/database/sqlite/SQLiteCustomFunction.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,12 @@ import android.util.Printer;
|
||||
* {@hide}
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
89
core/java/android/database/sqlite/SQLiteGlobal.java
Normal file
89
core/java/android/database/sqlite/SQLiteGlobal.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
if (mNumParameters != 0) {
|
||||
mBindArgs = new Object[mNumParameters];
|
||||
} 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.
|
||||
mBindArgs = null;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/* package */ void release() {
|
||||
if (mCompiledSql == null) {
|
||||
return;
|
||||
}
|
||||
mDatabase.releaseCompiledSqlObj(mSql, mCompiledSql);
|
||||
mCompiledSql = null;
|
||||
nStatement = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* used only for testing purposes
|
||||
*/
|
||||
/* package */ int getSqlStatementId() {
|
||||
synchronized(this) {
|
||||
return (mCompiledSql == null) ? 0 : nStatement;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ String getSqlString() {
|
||||
final String getSql() {
|
||||
return mSql;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
final Object[] getBindArgs() {
|
||||
return mBindArgs;
|
||||
}
|
||||
|
||||
final String[] getColumnNames() {
|
||||
return mColumnNames;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
protected final SQLiteSession getSession() {
|
||||
return mDatabase.getThreadSession();
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
protected final int getConnectionFlags() {
|
||||
return mDatabase.getThreadDefaultConnectionFlags(mReadOnly);
|
||||
}
|
||||
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
SQLiteQuery(SQLiteDatabase db, String query) {
|
||||
super(db, query, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,83 +45,23 @@ 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();
|
||||
}
|
||||
@@ -165,22 +69,6 @@ public final class SQLiteQuery extends SQLiteProgram {
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
878
core/java/android/database/sqlite/SQLiteSession.java
Normal file
878
core/java/android/database/sqlite/SQLiteSession.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
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);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
try {
|
||||
return getSession().executeForBlobFileDescriptor(
|
||||
getSql(), getBindArgs(), getConnectionFlags());
|
||||
} catch (SQLiteDatabaseCorruptException ex) {
|
||||
onCorruption();
|
||||
throw ex;
|
||||
} finally {
|
||||
releaseReference();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
39
core/java/android/database/sqlite/SQLiteStatementInfo.java
Normal file
39
core/java/android/database/sqlite/SQLiteStatementInfo.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -85,6 +85,23 @@ public class LruCache<K, V> {
|
||||
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
139
core/jni/android_database_SQLiteCommon.cpp
Normal file
139
core/jni/android_database_SQLiteCommon.cpp
Normal file
@@ -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
|
||||
51
core/jni/android_database_SQLiteCommon.h
Normal file
51
core/jni/android_database_SQLiteCommon.h
Normal file
@@ -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
|
||||
@@ -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
|
||||
959
core/jni/android_database_SQLiteConnection.cpp
Normal file
959
core/jni/android_database_SQLiteConnection.cpp
Normal file
@@ -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
|
||||
@@ -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
|
||||
78
core/jni/android_database_SQLiteGlobal.cpp
Normal file
78
core/jni/android_database_SQLiteGlobal.cpp
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user