Merge "Return count of rows in a resultset only once (when startPos = 0)"
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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},
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user