Merge "for WAL to work, can't keep prepared SQL stmt_id in SQLiteStatement"
This commit is contained in:
@@ -343,25 +343,14 @@ public class SQLiteDatabase extends SQLiteClosable {
|
||||
|
||||
private static final String MEMORY_DB_PATH = ":memory:";
|
||||
|
||||
/**
|
||||
* @param closable
|
||||
*/
|
||||
void addSQLiteClosable(SQLiteClosable closable) {
|
||||
lock();
|
||||
try {
|
||||
mPrograms.put(closable, null);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
synchronized void addSQLiteClosable(SQLiteClosable closable) {
|
||||
// mPrograms is per instance of SQLiteDatabase and it doesn't actually touch the database
|
||||
// itself. so, there is no need to lock().
|
||||
mPrograms.put(closable, null);
|
||||
}
|
||||
|
||||
void removeSQLiteClosable(SQLiteClosable closable) {
|
||||
lock();
|
||||
try {
|
||||
mPrograms.remove(closable);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
synchronized void removeSQLiteClosable(SQLiteClosable closable) {
|
||||
mPrograms.remove(closable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1261,6 +1250,8 @@ public class SQLiteDatabase extends SQLiteClosable {
|
||||
* statement and fill in those values with {@link SQLiteProgram#bindString}
|
||||
* and {@link SQLiteProgram#bindLong} each time you want to run the
|
||||
* statement. Statements may not return result sets larger than 1x1.
|
||||
*<p>
|
||||
* No two threads should be using the same {@link SQLiteStatement} at the same time.
|
||||
*
|
||||
* @param sql The raw SQL statement, may contain ? for unknown values to be
|
||||
* bound later.
|
||||
@@ -1269,19 +1260,7 @@ public class SQLiteDatabase extends SQLiteClosable {
|
||||
*/
|
||||
public SQLiteStatement compileStatement(String sql) throws SQLException {
|
||||
verifyDbIsOpen();
|
||||
String prefixSql = sql.trim().substring(0, 6);
|
||||
SQLiteDatabase db = this;
|
||||
// get a pooled database connection handle to use, if this is a query
|
||||
if (prefixSql.equalsIgnoreCase("SELECT")) {
|
||||
db = getDbConnection(sql);
|
||||
}
|
||||
db.lock();
|
||||
try {
|
||||
return new SQLiteStatement(db, sql);
|
||||
} finally {
|
||||
releaseDbConnection(db);
|
||||
db.unlock();
|
||||
}
|
||||
return new SQLiteStatement(this, sql);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2354,7 +2333,10 @@ public class SQLiteDatabase extends SQLiteClosable {
|
||||
return true;
|
||||
}
|
||||
|
||||
private synchronized void disableWriteAheadLogging() {
|
||||
/**
|
||||
* package visibility only for testing purposes
|
||||
*/
|
||||
/* package */ synchronized void disableWriteAheadLogging() {
|
||||
if (mConnectionPool == null) {
|
||||
return;
|
||||
}
|
||||
@@ -2394,7 +2376,7 @@ public class SQLiteDatabase extends SQLiteClosable {
|
||||
return this.mConnectionNum > 0;
|
||||
}
|
||||
|
||||
private SQLiteDatabase getDbConnection(String sql) {
|
||||
/* package */ SQLiteDatabase getDbConnection(String sql) {
|
||||
verifyDbIsOpen();
|
||||
|
||||
// use the current connection handle if
|
||||
|
||||
@@ -64,10 +64,15 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
protected int nStatement = 0;
|
||||
|
||||
/* package */ SQLiteProgram(SQLiteDatabase db, String sql) {
|
||||
mDatabase = db;
|
||||
this(db, sql, true);
|
||||
}
|
||||
|
||||
/* package */ SQLiteProgram(SQLiteDatabase db, String sql, boolean compileFlag) {
|
||||
mSql = sql.trim();
|
||||
attachObjectToDatabase(db);
|
||||
compileSql();
|
||||
if (compileFlag) {
|
||||
compileSql();
|
||||
}
|
||||
}
|
||||
|
||||
private void compileSql() {
|
||||
@@ -139,6 +144,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
private synchronized void attachObjectToDatabase(SQLiteDatabase db) {
|
||||
db.acquireReference();
|
||||
db.addSQLiteClosable(this);
|
||||
mDatabase = db;
|
||||
nHandle = db.mNativeHandle;
|
||||
}
|
||||
|
||||
@@ -147,6 +153,26 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
mDatabase.releaseReference();
|
||||
}
|
||||
|
||||
/* package */ synchronized void verifyDbAndCompileSql() {
|
||||
mDatabase.verifyDbIsOpen();
|
||||
// use pooled database connection handles for SELECT SQL statements
|
||||
SQLiteDatabase db = (getSqlStatementType(mSql) != SELECT_STMT) ? mDatabase
|
||||
: mDatabase.getDbConnection(mSql);
|
||||
if (!db.equals(mDatabase)) {
|
||||
// the database connection handle to be used is not the same as the one supplied
|
||||
// in the constructor. do some housekeeping.
|
||||
detachObjectFromDatabase();
|
||||
attachObjectToDatabase(db);
|
||||
}
|
||||
// compile the sql statement
|
||||
mDatabase.lock();
|
||||
try {
|
||||
compileSql();
|
||||
} finally {
|
||||
mDatabase.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAllReferencesReleased() {
|
||||
releaseCompiledSqlIfNotInCache();
|
||||
@@ -159,7 +185,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
mDatabase.releaseReference();
|
||||
}
|
||||
|
||||
private void releaseCompiledSqlIfNotInCache() {
|
||||
/* package */ synchronized void releaseCompiledSqlIfNotInCache() {
|
||||
if (mCompiledSql == null) {
|
||||
return;
|
||||
}
|
||||
@@ -222,7 +248,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
*/
|
||||
public void bindNull(int index) {
|
||||
synchronized (this) {
|
||||
mDatabase.verifyDbIsOpen();
|
||||
verifyDbAndCompileSql();
|
||||
acquireReference();
|
||||
try {
|
||||
native_bind_null(index);
|
||||
@@ -241,7 +267,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
*/
|
||||
public void bindLong(int index, long value) {
|
||||
synchronized (this) {
|
||||
mDatabase.verifyDbIsOpen();
|
||||
verifyDbAndCompileSql();
|
||||
acquireReference();
|
||||
try {
|
||||
native_bind_long(index, value);
|
||||
@@ -260,7 +286,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
*/
|
||||
public void bindDouble(int index, double value) {
|
||||
synchronized (this) {
|
||||
mDatabase.verifyDbIsOpen();
|
||||
verifyDbAndCompileSql();
|
||||
acquireReference();
|
||||
try {
|
||||
native_bind_double(index, value);
|
||||
@@ -282,7 +308,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
throw new IllegalArgumentException("the bind value at index " + index + " is null");
|
||||
}
|
||||
synchronized (this) {
|
||||
mDatabase.verifyDbIsOpen();
|
||||
verifyDbAndCompileSql();
|
||||
acquireReference();
|
||||
try {
|
||||
native_bind_string(index, value);
|
||||
@@ -304,7 +330,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
throw new IllegalArgumentException("the bind value at index " + index + " is null");
|
||||
}
|
||||
synchronized (this) {
|
||||
mDatabase.verifyDbIsOpen();
|
||||
verifyDbAndCompileSql();
|
||||
acquireReference();
|
||||
try {
|
||||
native_bind_blob(index, value);
|
||||
@@ -319,6 +345,9 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
*/
|
||||
public void clearBindings() {
|
||||
synchronized (this) {
|
||||
if (this.nStatement == 0) {
|
||||
return;
|
||||
}
|
||||
mDatabase.verifyDbIsOpen();
|
||||
acquireReference();
|
||||
try {
|
||||
@@ -334,14 +363,10 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
*/
|
||||
public void close() {
|
||||
synchronized (this) {
|
||||
if (nStatement == 0 || nHandle == 0 || !mDatabase.isOpen()) {
|
||||
if (nHandle == 0 || !mDatabase.isOpen()) {
|
||||
return;
|
||||
}
|
||||
releaseReference();
|
||||
// set all database objects to null/0, so that the user can't use a closed Object.
|
||||
mCompiledSql = null;
|
||||
nStatement = 0;
|
||||
nHandle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +391,6 @@ public abstract class SQLiteProgram extends SQLiteClosable {
|
||||
protected final native void native_bind_double(int index, double value);
|
||||
protected final native void native_bind_string(int index, String value);
|
||||
protected final native void native_bind_blob(int index, byte[] value);
|
||||
private final native void native_clear_bindings();
|
||||
/* package */ final native void native_clear_bindings();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import dalvik.system.BlockGuard;
|
||||
* SQLiteStatement is not internally synchronized so code using a SQLiteStatement from multiple
|
||||
* threads should perform its own synchronization when using the SQLiteStatement.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class SQLiteStatement extends SQLiteProgram
|
||||
{
|
||||
private static final boolean READ = true;
|
||||
@@ -41,7 +42,7 @@ public class SQLiteStatement extends SQLiteProgram
|
||||
* @param sql
|
||||
*/
|
||||
/* package */ SQLiteStatement(SQLiteDatabase db, String sql) {
|
||||
super(db, sql);
|
||||
super(db, sql, false /* don't compile sql statement */);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +135,7 @@ public class SQLiteStatement extends SQLiteProgram
|
||||
* methods in this class.
|
||||
*/
|
||||
private long acquireAndLock(boolean rwFlag) {
|
||||
mDatabase.verifyDbIsOpen();
|
||||
verifyDbAndCompileSql();
|
||||
if (rwFlag == WRITE) {
|
||||
BlockGuard.getThreadPolicy().onWriteToDisk();
|
||||
} else {
|
||||
@@ -153,6 +154,10 @@ public class SQLiteStatement extends SQLiteProgram
|
||||
private void releaseAndUnlock() {
|
||||
releaseReference();
|
||||
mDatabase.unlock();
|
||||
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.
|
||||
releaseCompiledSqlIfNotInCache();
|
||||
}
|
||||
|
||||
private final native void native_execute();
|
||||
|
||||
@@ -67,6 +67,7 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
|
||||
@SmallTest
|
||||
public void testEnableWriteAheadLogging() {
|
||||
mDatabase.disableWriteAheadLogging();
|
||||
assertNull(mDatabase.mConnectionPool);
|
||||
mDatabase.enableWriteAheadLogging();
|
||||
DatabaseConnectionPool pool = mDatabase.mConnectionPool;
|
||||
@@ -280,6 +281,7 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
|
||||
@SmallTest
|
||||
public void testLruCachingOfSqliteCompiledSqlObjs() {
|
||||
mDatabase.disableWriteAheadLogging();
|
||||
mDatabase.execSQL("CREATE TABLE test (i int, j int);");
|
||||
mDatabase.execSQL("insert into test values(1,1);");
|
||||
// set cache size
|
||||
@@ -292,9 +294,10 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
ArrayList<String> sqlStrings = new ArrayList<String>();
|
||||
SQLiteStatement stmt0 = null;
|
||||
for (int i = 0; i < N+1; i++) {
|
||||
String s = "select * from test where i = " + i;
|
||||
String s = "select * from test where i = " + i + " and j = ?";
|
||||
sqlStrings.add(s);
|
||||
SQLiteStatement c = mDatabase.compileStatement(s);
|
||||
c.bindLong(1, 1);
|
||||
stmtObjs.add(i, c.getSqlStatementId());
|
||||
if (i == 0) {
|
||||
// save thie SQLiteStatement obj. we want to make sure it is thrown out of
|
||||
@@ -307,6 +310,7 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
assertEquals(0, stmt0.getSqlStatementId());
|
||||
for (int i = 1; i < N+1; i++) {
|
||||
SQLiteCompiledSql compSql = mDatabase.getCompiledStatementForSql(sqlStrings.get(i));
|
||||
assertNotNull(compSql);
|
||||
assertTrue(stmtObjs.contains(compSql.nStatement));
|
||||
}
|
||||
}
|
||||
@@ -317,9 +321,11 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
"num1 INTEGER, num2 INTEGER, image BLOB);");
|
||||
final String statement = "DELETE FROM test WHERE _id=?;";
|
||||
SQLiteStatement statementDoNotClose = mDatabase.compileStatement(statement);
|
||||
// SQl statement is compiled only at find bind or execute call
|
||||
assertTrue(statementDoNotClose.getSqlStatementId() == 0);
|
||||
statementDoNotClose.bindLong(1, 1);
|
||||
assertTrue(statementDoNotClose.getSqlStatementId() > 0);
|
||||
int nStatement = statementDoNotClose.getSqlStatementId();
|
||||
assertTrue(statementDoNotClose.getSqlStatementId() == nStatement);
|
||||
/* do not close statementDoNotClose object.
|
||||
* That should leave it in SQLiteDatabase.mPrograms.
|
||||
* mDatabase.close() in tearDown() should release it.
|
||||
@@ -332,24 +338,26 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
*/
|
||||
@SmallTest
|
||||
public void testStatementClose() {
|
||||
mDatabase.execSQL("CREATE TABLE test (i int);");
|
||||
mDatabase.execSQL("CREATE TABLE test (i int, j int);");
|
||||
// fill up statement cache in mDatabase\
|
||||
int N = 26;
|
||||
mDatabase.setMaxSqlCacheSize(N);
|
||||
SQLiteStatement stmt;
|
||||
int stmt0Id = 0;
|
||||
for (int i = 0; i < N; i ++) {
|
||||
stmt = mDatabase.compileStatement("insert into test values(" + i + ");");
|
||||
stmt.executeInsert();
|
||||
stmt = mDatabase.compileStatement("insert into test values(" + i + ", ?);");
|
||||
stmt.bindLong(1, 1);
|
||||
// keep track of 0th entry
|
||||
if (i == 0) {
|
||||
stmt0Id = stmt.getSqlStatementId();
|
||||
}
|
||||
stmt.executeInsert();
|
||||
stmt.close();
|
||||
}
|
||||
|
||||
// add one more to the cache - and the above 'stmt0Id' should fall out of cache
|
||||
SQLiteStatement stmt1 = mDatabase.compileStatement("select * from test where i = 1;");
|
||||
SQLiteStatement stmt1 = mDatabase.compileStatement("select * from test where i = ?;");
|
||||
stmt1.bindLong(1, 1);
|
||||
stmt1.close();
|
||||
|
||||
// the above close() should have queuedUp the statement for finalization
|
||||
@@ -369,7 +377,7 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
*/
|
||||
@LargeTest
|
||||
public void testStatementCloseDiffThread() throws InterruptedException {
|
||||
mDatabase.execSQL("CREATE TABLE test (i int);");
|
||||
mDatabase.execSQL("CREATE TABLE test (i int, j int);");
|
||||
// fill up statement cache in mDatabase in a thread
|
||||
Thread t1 = new Thread() {
|
||||
@Override public void run() {
|
||||
@@ -377,12 +385,13 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
mDatabase.setMaxSqlCacheSize(N);
|
||||
SQLiteStatement stmt;
|
||||
for (int i = 0; i < N; i ++) {
|
||||
stmt = mDatabase.compileStatement("insert into test values(" + i + ");");
|
||||
stmt.executeInsert();
|
||||
stmt = mDatabase.compileStatement("insert into test values(" + i + ", ?);");
|
||||
stmt.bindLong(1,1);
|
||||
// keep track of 0th entry
|
||||
if (i == 0) {
|
||||
setStmt0Id(stmt.getSqlStatementId());
|
||||
}
|
||||
stmt.executeInsert();
|
||||
stmt.close();
|
||||
}
|
||||
}
|
||||
@@ -396,7 +405,8 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
Thread t2 = new Thread() {
|
||||
@Override public void run() {
|
||||
SQLiteStatement stmt1 = mDatabase.compileStatement(
|
||||
"select * from test where i = 1;");
|
||||
"select * from test where i = ?;");
|
||||
stmt1.bindLong(1, 1);
|
||||
stmt1.close();
|
||||
}
|
||||
};
|
||||
@@ -438,7 +448,7 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
*/
|
||||
@LargeTest
|
||||
public void testStatementCloseByDbClose() throws InterruptedException {
|
||||
mDatabase.execSQL("CREATE TABLE test (i int);");
|
||||
mDatabase.execSQL("CREATE TABLE test (i int, j int);");
|
||||
// fill up statement cache in mDatabase in a thread
|
||||
Thread t1 = new Thread() {
|
||||
@Override public void run() {
|
||||
@@ -446,12 +456,13 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
mDatabase.setMaxSqlCacheSize(N);
|
||||
SQLiteStatement stmt;
|
||||
for (int i = 0; i < N; i ++) {
|
||||
stmt = mDatabase.compileStatement("insert into test values(" + i + ");");
|
||||
stmt.executeInsert();
|
||||
stmt = mDatabase.compileStatement("insert into test values(" + i + ", ?);");
|
||||
stmt.bindLong(1, 1);
|
||||
// keep track of 0th entry
|
||||
if (i == 0) {
|
||||
setStmt0Id(stmt.getSqlStatementId());
|
||||
}
|
||||
stmt.executeInsert();
|
||||
stmt.close();
|
||||
}
|
||||
}
|
||||
@@ -465,7 +476,8 @@ public class SQLiteDatabaseTest extends AndroidTestCase {
|
||||
Thread t2 = new Thread() {
|
||||
@Override public void run() {
|
||||
SQLiteStatement stmt1 = mDatabase.compileStatement(
|
||||
"select * from test where i = 1;");
|
||||
"select * from test where i = ?;");
|
||||
stmt1.bindLong(1, 1);
|
||||
stmt1.close();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,21 +18,21 @@ package android.database.sqlite;
|
||||
|
||||
import android.content.Context;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.FlakyTest;
|
||||
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;
|
||||
Boolean exceptionRecvd = false;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
exceptionRecvd = false;
|
||||
|
||||
File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE);
|
||||
mDatabaseFile = new File(dbDir, "database_test.db");
|
||||
if (mDatabaseFile.exists()) {
|
||||
@@ -49,78 +49,60 @@ public class SQLiteStatementTest extends AndroidTestCase {
|
||||
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 Exception {
|
||||
public void testUseOfSameSqlStatementBy2Threads() throws InterruptedException {
|
||||
mDatabase.execSQL("CREATE TABLE test_pstmt (i INTEGER PRIMARY KEY, j text);");
|
||||
|
||||
// thread 1 creates a prepared statement
|
||||
final String stmt = "SELECT * FROM test_pstmt WHERE i = ?";
|
||||
|
||||
// start 2 threads to do repeatedly execute "stmt"
|
||||
// since these 2 threads are executing the same sql, they each should get
|
||||
// their own copy and
|
||||
// there SHOULD NOT be an error from sqlite: "prepared statement is busy"
|
||||
class RunStmtThread extends Thread {
|
||||
private static final int N = 1000;
|
||||
@Override public void run() {
|
||||
int i = 0;
|
||||
try {
|
||||
// execute many times
|
||||
for (i = 0; i < N; i++) {
|
||||
SQLiteStatement s1 = mDatabase.compileStatement(stmt);
|
||||
s1.bindLong(1, i);
|
||||
s1.execute();
|
||||
s1.close();
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
fail("SQLiteException: " + e.getMessage());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
fail("random unexpected exception: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
// 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(1000);
|
||||
}
|
||||
}
|
||||
while (t1.isAlive() || t2.isAlive()) {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
@FlakyTest
|
||||
public void testUseOfSamePreparedStatementBy2Threads() throws Exception {
|
||||
/**
|
||||
* 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);");
|
||||
|
||||
// thread 1 creates a prepared statement
|
||||
final String stmt = "SELECT * FROM test_pstmt WHERE i = ?";
|
||||
final SQLiteStatement s1 = mDatabase.compileStatement(stmt);
|
||||
|
||||
// start 2 threads to do repeatedly execute "stmt"
|
||||
// since these 2 threads are executing the same prepared statement,
|
||||
// should see an error from sqlite: "prepared statement is busy"
|
||||
class RunStmtThread extends Thread {
|
||||
private static final int N = 1000;
|
||||
@Override public void run() {
|
||||
int i = 0;
|
||||
try {
|
||||
// execute many times
|
||||
for (i = 0; i < N; i++) {
|
||||
// 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();
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
// expect it
|
||||
assertTrue(e.getMessage().contains("library routine called out of sequence:"));
|
||||
exceptionRecvd = true;
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
fail("random unexpected exception: " + e.getMessage());
|
||||
return;
|
||||
Thread.yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,33 +111,147 @@ public class SQLiteStatementTest extends AndroidTestCase {
|
||||
RunStmtThread t2 = new RunStmtThread();
|
||||
t2.start();
|
||||
while (t1.isAlive() || t2.isAlive()) {
|
||||
Thread.sleep(1000);
|
||||
Thread.sleep(10);
|
||||
}
|
||||
assertTrue(exceptionRecvd);
|
||||
}
|
||||
/** 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.
|
||||
*/
|
||||
@SmallTest
|
||||
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 now compiled
|
||||
int n = stmt.nStatement;
|
||||
assertTrue(n > 0);
|
||||
assertEquals(n, stmt.getSqlStatementId());
|
||||
// should still be using the mDatabase connection - verify
|
||||
assertEquals(mDatabase.mNativeHandle, stmt.nHandle);
|
||||
stmt.bindString(2, "blah" + colValue);
|
||||
assertEquals(n, stmt.nStatement);
|
||||
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 now compiled
|
||||
n = stmt.nStatement;
|
||||
assertTrue(n > 0);
|
||||
assertEquals(n, stmt.getSqlStatementId());
|
||||
SQLiteDatabase dbUsed = mDatabase;
|
||||
if (wal) {
|
||||
// if wal is set, should be using a pooled connection handle
|
||||
dbUsed = mDatabase.mConnectionPool.getConnectionList().get(0);
|
||||
assertTrue(mDatabase.mNativeHandle != dbUsed.mNativeHandle);
|
||||
}
|
||||
assertEquals(dbUsed.mNativeHandle, stmt.nHandle);
|
||||
assertEquals(dbUsed, 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());
|
||||
// but the database handle should still remain attached to the statement
|
||||
assertEquals(dbUsed.mNativeHandle, stmt.nHandle);
|
||||
assertEquals(dbUsed, 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(dbUsed, stmt.mDatabase);
|
||||
}
|
||||
|
||||
/**
|
||||
* test to make sure SqliteStatement.nStatement is populated only during bind and execute calls.
|
||||
*/
|
||||
public void testGetSqlStatementId() {
|
||||
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 statementOne = mDatabase.compileStatement(statement);
|
||||
// sql statement is NOT compiled until the bind or execute call.
|
||||
statementOne.bindLong(1, 1);
|
||||
SQLiteStatement statementTwo = mDatabase.compileStatement(statement);
|
||||
statementTwo.bindLong(1, 1);
|
||||
// since the same compiled statement is being accessed at the same time by 2 different
|
||||
// objects, they each get their own statement id
|
||||
assertTrue(statementOne.getSqlStatementId() != statementTwo.getSqlStatementId());
|
||||
statementOne.close();
|
||||
statementTwo.close();
|
||||
|
||||
// two SQLiteStatements referring to the same SQL statement should refer to the same
|
||||
// pre-compiled SQl statement id if the SQLiteStatement objects are NOT in use at the same
|
||||
// time
|
||||
statementOne = mDatabase.compileStatement(statement);
|
||||
statementOne.bindLong(1, 1);
|
||||
// now that the SQL statement is compiled, get its pre-compiled SQL statement id
|
||||
int n = statementOne.getSqlStatementId();
|
||||
statementOne.close();
|
||||
statementTwo = mDatabase.compileStatement(statement);
|
||||
statementTwo.bindLong(1, 2); // use different value for bindarg, just for the heck of it
|
||||
assertEquals(n, statementTwo.getSqlStatementId());
|
||||
statementTwo.close();
|
||||
|
||||
// now try to compile 2 different statements and they should have different uniquerIds.
|
||||
SQLiteStatement statement1 = mDatabase.compileStatement("DELETE FROM test WHERE _id=1;");
|
||||
SQLiteStatement statement2 = mDatabase.compileStatement("DELETE FROM test WHERE _id=2;");
|
||||
SQLiteStatement statement1 = mDatabase.compileStatement("DELETE FROM test WHERE _id > ?;");
|
||||
statement1.bindLong(1, 1);
|
||||
SQLiteStatement statement2 = mDatabase.compileStatement("DELETE FROM test WHERE _id < ?;");
|
||||
statement2.bindLong(1, 11);
|
||||
assertTrue(statement1.getSqlStatementId() != statement2.getSqlStatementId());
|
||||
statement1.close();
|
||||
statement2.close();
|
||||
@@ -166,8 +262,8 @@ public class SQLiteStatementTest extends AndroidTestCase {
|
||||
"num1 INTEGER, num2 INTEGER, image BLOB);");
|
||||
final String statement = "DELETE FROM test WHERE _id=?;";
|
||||
SQLiteStatement statementOne = mDatabase.compileStatement(statement);
|
||||
statementOne.bindLong(1, 1);
|
||||
assertTrue(statementOne.getSqlStatementId() > 0);
|
||||
int nStatement = statementOne.getSqlStatementId();
|
||||
statementOne.releaseReference();
|
||||
assertEquals(0, statementOne.getSqlStatementId());
|
||||
statementOne.close();
|
||||
@@ -178,8 +274,8 @@ public class SQLiteStatementTest extends AndroidTestCase {
|
||||
"num1 INTEGER, num2 INTEGER, image BLOB);");
|
||||
final String statement = "DELETE FROM test WHERE _id=?;";
|
||||
SQLiteStatement statementOne = mDatabase.compileStatement(statement);
|
||||
statementOne.bindLong(1, 1);
|
||||
assertTrue(statementOne.getSqlStatementId() > 0);
|
||||
int nStatement = statementOne.getSqlStatementId();
|
||||
statementOne.releaseReferenceFromContainer();
|
||||
assertEquals(0, statementOne.getSqlStatementId());
|
||||
statementOne.close();
|
||||
|
||||
Reference in New Issue
Block a user