diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 1a096971e4f2d..fa79a9e56440c 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -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. + *

+ * 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 diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index ba39283c65dfd..fb3f42865d9b0 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -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(); } diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index f6ca1d128d9f3..966459389623a 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -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(); diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java index 2a9a427faa37f..8006b738b1cae 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java @@ -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 sqlStrings = new ArrayList(); 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(); } }; diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java index 93dbab56bee1f..eb27551b0add9 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java @@ -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();