diff options
4 files changed, 171 insertions, 23 deletions
diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index 43dd5d7..d58a689 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -56,7 +56,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { private final SQLiteCursorDriver mDriver; /** The number of rows in the cursor */ - private int mCount = NO_COUNT; + private volatile int mCount = NO_COUNT; /** A mapping of column names to column indices, to speed up lookups */ private Map<String, Integer> mColumnNameMap; @@ -138,13 +138,21 @@ public class SQLiteCursor extends AbstractWindowedCursor { } try { int count = getQuery().fillWindow(cw, mMaxRead, mCount); - // return -1 means not finished + // return -1 means there is still more data to be retrieved from the resultset if (count != 0) { if (count == NO_COUNT){ mCount += mMaxRead; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received -1 from native_fill_window. read " + + mCount + " rows so far"); + } sendMessage(); } else { - mCount = count; + mCount += count; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received all data from native_fill_window. read " + + mCount + " rows."); + } sendMessage(); break; } @@ -308,13 +316,23 @@ public class SQLiteCursor extends AbstractWindowedCursor { } } mWindow.setStartPosition(startPos); - mCount = getQuery().fillWindow(mWindow, mInitialRead, 0); - // return -1 means not finished - if (mCount == NO_COUNT){ + int count = getQuery().fillWindow(mWindow, mInitialRead, 0); + // return -1 means there is still more data to be retrieved from the resultset + if (count == NO_COUNT){ mCount = startPos + mInitialRead; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received -1 from native_fill_window. read " + mCount + " rows so far"); + } Thread t = new Thread(new QueryThread(mCursorState), "query thread"); t.start(); - } + } else if (startPos == 0) { // native_fill_window returns count(*) only for startPos = 0 + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received count(*) from native_fill_window: " + count); + } + mCount = count; + } else if (mCount <= 0) { + throw new IllegalStateException("count should never be non-zero negative number"); + } } private synchronized SQLiteQuery getQuery() { @@ -504,4 +522,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { super.finalize(); } } + + /** + * this is only for testing purposes. + */ + /* package */ int getMCount() { + return mCount; + } } diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index 63a8ce9..e9e0172 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -18,6 +18,7 @@ package android.database.sqlite; import android.database.CursorWindow; import android.os.SystemClock; +import android.util.Log; /** * A SQLite program that represents a query that reads the resulting rows into a CursorWindow. @@ -58,6 +59,7 @@ public class SQLiteQuery extends SQLiteProgram { /* package */ SQLiteQuery(SQLiteDatabase db, SQLiteQuery query) { super(db, query.mSql); this.mBindArgs = query.mBindArgs; + this.mOffsetIndex = query.mOffsetIndex; } /** @@ -78,8 +80,8 @@ public class SQLiteQuery extends SQLiteProgram { // if the start pos is not equal to 0, then most likely window is // too small for the data set, loading by another thread // is not safe in this situation. the native code will ignore maxRead - int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex, - maxRead, lastPos); + int numRows = native_fill_window(window, window.getStartPosition(), + mOffsetIndex, maxRead, lastPos); mDatabase.logTimeStat(mSql, timeStart); return numRows; } catch (IllegalStateException e){ @@ -88,6 +90,9 @@ public class SQLiteQuery extends SQLiteProgram { } catch (SQLiteDatabaseCorruptException e) { mDatabase.onCorruption(); throw e; + } catch (SQLiteException e) { + Log.e(TAG, "exception: " + e.getMessage() + "; query: " + mSql); + throw e; } finally { window.releaseReference(); } diff --git a/core/jni/android_database_SQLiteQuery.cpp b/core/jni/android_database_SQLiteQuery.cpp index e383123..d18cfd8 100644 --- a/core/jni/android_database_SQLiteQuery.cpp +++ b/core/jni/android_database_SQLiteQuery.cpp @@ -15,7 +15,7 @@ */ #undef LOG_TAG -#define LOG_TAG "Cursor" +#define LOG_TAG "SqliteCursor.cpp" #include <jni.h> #include <JNIHelp.h> @@ -116,6 +116,7 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, int retryCount; int boundParams; CursorWindow * window; + bool gotAllRows = true; if (statement == NULL) { LOGE("Invalid statement in fillWindow()"); @@ -131,8 +132,7 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, err = sqlite3_bind_int(statement, offsetParam, startPos); if (err != SQLITE_OK) { LOGE("Unable to bind offset position, offsetParam = %d", offsetParam); - jniThrowException(env, "java/lang/IllegalArgumentException", - sqlite3_errmsg(GET_HANDLE(env, object))); + throw_sqlite3_exception(env, GET_HANDLE(env, object)); return 0; } LOG_WINDOW("Bound to startPos %d", startPos); @@ -182,7 +182,8 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, field_slot_t * fieldDir = window->allocRow(); if (!fieldDir) { LOGE("Failed allocating fieldDir at startPos %d row %d", startPos, numRows); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + gotAllRows = false; + goto return_count; } } @@ -207,7 +208,8 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, window->freeLastRow(); LOGD("Failed allocating %u bytes for text/blob at %d,%d", size, startPos + numRows, i); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + gotAllRows = false; + goto return_count; } window->copyIn(offset, text, size); @@ -225,8 +227,9 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, int64_t value = sqlite3_column_int64(statement, i); if (!window->putLong(numRows, i, value)) { window->freeLastRow(); - LOGD("Failed allocating space for a long in column %d", i); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + LOGE("Failed allocating space for a long in column %d", i); + gotAllRows = false; + goto return_count; } LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + numRows, i, value); } else if (type == SQLITE_FLOAT) { @@ -234,8 +237,9 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, double value = sqlite3_column_double(statement, i); if (!window->putDouble(numRows, i, value)) { window->freeLastRow(); - LOGD("Failed allocating space for a double in column %d", i); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + LOGE("Failed allocating space for a double in column %d", i); + gotAllRows = false; + goto return_count; } LOG_WINDOW("%d,%d is FLOAT %lf", startPos + numRows, i, value); } else if (type == SQLITE_BLOB) { @@ -247,7 +251,8 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, window->freeLastRow(); LOGD("Failed allocating %u bytes for blob at %d,%d", size, startPos + numRows, i); - return startPos + numRows + finish_program_and_get_row_count(statement) + 1; + gotAllRows = false; + goto return_count; } window->copyIn(offset, blob, size); @@ -306,12 +311,24 @@ static jint native_fill_window(JNIEnv* env, jobject object, jobject javaWindow, LOG_WINDOW("Resetting statement %p after fetching %d rows in %d bytes\n\n\n\n", statement, numRows, window->size() - window->freeSpace()); -// LOGI("Filled window with %d rows in %d bytes", numRows, window->size() - window->freeSpace()); + LOG_WINDOW("Filled window with %d rows in %d bytes", numRows, + window->size() - window->freeSpace()); if (err == SQLITE_ROW) { + // there is more data to be returned. let the caller know by returning -1 return -1; - } else { + } + return_count: + if (startPos) { sqlite3_reset(statement); - return startPos + numRows; + LOG_WINDOW("Not doing count(*) because startPos %d is non-zero", startPos); + return 0; + } else if (gotAllRows) { + sqlite3_reset(statement); + LOG_WINDOW("Not doing count(*) because we already know the count(*)"); + return numRows; + } else { + // since startPos == 0, we need to get the count(*) of the result set + return numRows + 1 + finish_program_and_get_row_count(statement); } } @@ -336,7 +353,8 @@ static jstring native_column_name(JNIEnv* env, jobject object, jint columnIndex) static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ - {"native_fill_window", "(Landroid/database/CursorWindow;IIII)I", (void *)native_fill_window}, + {"native_fill_window", "(Landroid/database/CursorWindow;IIII)I", + (void *)native_fill_window}, {"native_column_count", "()I", (void*)native_column_count}, {"native_column_name", "(I)Ljava/lang/String;", (void *)native_column_name}, }; diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java index 3c3ff3f..bbffe70 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteCursorTest.java @@ -16,11 +16,16 @@ package android.database.sqlite; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; import java.io.File; +import java.util.HashSet; +import java.util.Set; public class SQLiteCursorTest extends AndroidTestCase { private SQLiteDatabase mDatabase; @@ -91,4 +96,99 @@ public class SQLiteCursorTest extends AndroidTestCase { assertTrue(mDatabase.mConnectionPool.getConnectionList().contains(db)); assertTrue(db.isOpen()); } + + @SmallTest + public void testFillWindow() { + // create schema + final String testTable = "testV"; + mDatabase.beginTransaction(); + mDatabase.execSQL("CREATE TABLE " + testTable + " (col1 int, desc text not null);"); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + // populate the table with data + // create a big string that will almost fit a page but not quite. + // since sqlite wants to make sure each row is in a page, this string will allocate + // a new database page for each row. + StringBuilder buff = new StringBuilder(); + for (int i = 0; i < 500; i++) { + buff.append(i % 10 + ""); + } + ContentValues values = new ContentValues(); + values.put("desc", buff.toString()); + + // insert more than 1MB of data in the table. this should ensure that the entire tabledata + // will need more than one CursorWindow + int N = 5000; + Set<Integer> rows = new HashSet<Integer>(); + mDatabase.beginTransaction(); + for (int j = 0; j < N; j++) { + values.put("col1", j); + mDatabase.insert(testTable, null, values); + rows.add(j); // store in a hashtable so we can verify the results from cursor later on + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + assertEquals(N, rows.size()); + Cursor c1 = mDatabase.rawQuery("select * from " + testTable, null); + assertEquals(N, c1.getCount()); + c1.close(); + + // scroll through ALL data in the table using a cursor. should cause multiple calls to + // native_fill_window (and re-fills of the CursorWindow object) + Cursor c = mDatabase.query(testTable, new String[]{"col1", "desc"}, + null, null, null, null, null); + int i = 0; + while (c.moveToNext()) { + int val = c.getInt(0); + assertTrue(rows.contains(val)); + assertTrue(rows.remove(val)); + } + // did I see all the rows in the table? + assertTrue(rows.isEmpty()); + + // change data and make sure the cursor picks up new data & count + rows = new HashSet<Integer>(); + mDatabase.beginTransaction(); + int M = N + 1000; + for (int j = 0; j < M; j++) { + rows.add(j); + if (j < N) { + continue; + } + values.put("col1", j); + mDatabase.insert(testTable, null, values); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + assertEquals(M, rows.size()); + c.requery(); + i = 0; + while (c.moveToNext()) { + int val = c.getInt(0); + assertTrue(rows.contains(val)); + assertTrue(rows.remove(val)); + } + // did I see all data from the modified table + assertTrue(rows.isEmpty()); + + // move cursor back to 1st row and scroll to about halfway in the result set + // and then delete 75% of data - and then do requery + c.moveToFirst(); + int K = N / 2; + for (int p = 0; p < K && c.moveToNext(); p++) { + // nothing to do - just scrolling to about half-point in the resultset + } + mDatabase.beginTransaction(); + mDatabase.delete(testTable, "col1 < ?", new String[]{ (3 * M / 4) + ""}); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + c.requery(); + assertEquals(M / 4, c.getCount()); + while (c.moveToNext()) { + // just move the cursor to next row - to make sure it can go through the entire + // resultset without any problems + } + c.close(); + } } |
