From 2827d6d974beabb12344040a002dcb52dd7106b5 Mon Sep 17 00:00:00 2001 From: Vasu Nori Date: Sun, 4 Jul 2010 00:26:18 -0700 Subject: [PATCH] for WAL to work, can't keep prepared SQL stmt_id in SQLiteStatement Some (including the Contacts app) do the following: 1. Open database 2. As part of database_connection.onCreate(), Create some SQLiteStatement objects to cache them in the process 3. attach databases WAL doesn't work with attached databases. so, apps doing the above should enable WAL only if there are no attached databases. But we would like to enable WAL automatically for all apps after step #1 above and disable WAL if the app subsequently does 'attach database' SQL. this works only if there are no SQLiteStatement objects created in step # 2, because SQLiteStatements cwmaintain a hard-reference to the database connection for life and also to the prepared SQL statement id. It is quite difficult to disable WAL in step # 3 if it is enabled in step # 1 and then a connection pool gets used by step # 2 would make WAL disabling easier if SQLiteStatement refers to prepared SQL statement id only when it is needed (during binding and execute calls) and thus NOT tied to a spacific database conenction. also, from the standpoint of not blocking readers, it helps NOT to have SQLiteStatement be married to a database connection and prepared SQL statement id for life. Change-Id: I464d57042965a28d2bde88e0f44b66ec119b40dc --- .../database/sqlite/SQLiteDatabase.java | 46 ++-- .../database/sqlite/SQLiteProgram.java | 53 +++-- .../database/sqlite/SQLiteStatement.java | 9 +- .../database/sqlite/SQLiteDatabaseTest.java | 40 ++-- .../database/sqlite/SQLiteStatementTest.java | 220 +++++++++++++----- 5 files changed, 244 insertions(+), 124 deletions(-) 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();