/*
 * 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 java.io.File;
import java.util.ArrayList;

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();
    }

    /**
     * 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) {}
    }
}
