Are android SQLite queries executed lazily? - android

I just encountered something strange. Say I have a table
_id | caption
1 | foo
and I execute the following query on it directly in SQLite:
select _id, (caption != "bar") as caption from the_table;
I get the (expected) output:
_id | caption
1 | 1
Executing this code however:
// This will execute the EXACT SAME query (checked via debugger)
Cursor affected = db.query(table, (String[]) projection.toArray(new String[0]), selection, selectionArgs, null, null, null);
// ############ UPDATE ROW 1 TO CAPTION=bar #######
int result = db.update(table, values, selection, selectionArgs);
affected.moveToPosition(-1);
while (affected.moveToNext()) {
long id = affected.getLong(0); // We made the _id column the first in the projection
for (int i = 1; i < affected.getColumnCount(); i++) {
Log.d("xyz", affected.getInt(i) + "");
}
}
gets me: "xyz 0" in LogCat.
Does the cursor execute the query lazily on the first cursor access?
It seems to work if I insert affected.getCount() right after the query.

Yes, there are leaky abstractions.
Query alone translates to a prepare call that compiles the SQL to a sqlite program (think bytecode). The program is not run.
To run a compiled sqlite program, one would use the step which either returns a result row, finihsed status code, or an error. In Android, you effectively run the program by calling one of the moveTo...() methods on the returned cursor.
Getting cursor count is not directly supported by the sqlite C API. Android implements it by running the query and storing the results in a "cursor window" buffer. The same buffer is used for accessing the cursor's data later.

Related

Initialize SQLite Cursor before accessing data from it

I am trying to insert data into a SQLite DB once a notification is received via FCM. For debugging purpose I am also inserting a dummy data into my DB when SHow Token is clicked on the HomeScreen activity.
However am getting
"I am getting "Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it."
Link to my code: - GitHub
Can someone please go through my code and let me know where I am going wrong.
Note - I added below in HomeScreen.java,MyFirebaseMessagingService.java and NotificationDetails.java
private SQLiteDB dbHelper = new SQLiteDB(this);
since the suggested
private SQLiteDB dbHelper;
did not work for me
When I used above I kept on getting Nullpointer exception, so I figured since the SQLiteDB class constructor is accepting a context, so let me pass one, post which I did not get NullPointer Exception.
Now I did this without being fully aware of the concept on context which I have been trying to wrap my head around, but since am an extreme noob to android I am not able to grasp it just yet. I suspect it might have something to do with the context I am passing.
Can someone please help me here with detailed instructions on how to fix this issue, I have been through many other threads on this but was not able to fix hence after 5 hrs of going through multiple SO questions, I am posting this one.
Thanks in advance to everyone in the community for the help. :)
Edit
Upon suggestion by admins, I am including below snippet of my code.
Where I am calling the cursor
dbHelper.insertNotification("This is a notification");
//Check if the message contains data
Cursor rs = dbHelper.getAllNotifications();
rs.moveToFirst();
token_text.setText("Token: " +rs.getString((rs.getColumnIndex("NOTIFICATION_DETAILS"))));
Insert Notification Function in SQLiteDB.java
public boolean insertNotification(String notification){
SQLiteDatabase db = this.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(NOTIFICATION_DETAILS,notification);
db.insert(NOTIFICATION_TABLE_NAME,null,contentValues);
return true;
}
getAllNotifications function
public Cursor getAllNotifications() {
SQLiteDatabase db = this.getWritableDatabase();
Cursor res = db.rawQuery( "SELECT * FROM " + NOTIFICATION_TABLE_NAME, null );
return res;
}
Couldn't read row 0, col -1 from CursorWindow.
Is saying that you are attempting to get the column at offset -1 from row 0 (the first row). So you have provided an invalid offset (it cannot be an offset of -1, the offset must be 0 or greater and the maximum value will be 1 less than the number of columns in the Cursor).
The most likely cause, is that Cursor method getColumnIndex(the_column_name_as_a_string) will return -1 when the column passed to the method cannot be found in the Cursor. Noting that due to a bug column name is case sensitive.
As such your issue is that the Cursor does not contain a column name NOTIFICATION_DETAILS and as you have used * (all columns) then that column does not exist in the table.
By the looks of it you should be using the String variable NOTIFICATION_DETAILS so you probably need to use :-
token_text.setText("Token: " +rs.getString((rs.getColumnIndex(NOTIFICATION_DETAILS)))); //<<<<<<<<<< double quotation marks removed.
Additional
You should NEVER assume that moveToFirst (or any Cursor move???? method) actually does the move. You should ALWAYS check the returned value. It will be true if the move was successful otherwise it would be false.
Again note that the column name passed to the getColumnIndex method is case dependant.
As such you should use something like
:-
dbHelper.insertNotification("This is a notification");
//Check if the message contains data
Cursor rs = dbHelper.getAllNotifications();
if (rs.moveToFirst()) {
token_text.setText("Token: " +rs.getString((rs.getColumnIndex(NOTIFICATION_DETAILS))));
} else {
........ code here if anything need to be done if there are no rows extracted
}
Addition re comment :-
Cursor rs = dbHelper.getAllNotifications(); rs.moveToFirst(); do{ for
(int i = 0; i < rs.getColumnCount(); i++) {
notification_array.add(rs.getString((rs.getColumnIndex(NOTIFICATION_DETAILS))));
} }while (rs.moveToNext());
using the following is much simpler :-
Cursor rs = dbHelper.getAllNotifications();
while (rs.moveToNext()) {
notification_array.add(rs.getString((rs.getColumnIndex(NOTIFICATION_DETAILS))));
}

Huge performance difference on SELECT queries using compileStatement() vs query() in SQLite / Android

In short:
Performing 23770 SELECT queries using query() and retrieving result using a Cursor takes 7 sec. I was able to reduce the time to 1 sec for the same by compiling the statement using compileStatement() and calling simpleQueryForString().
Is there a way to get similar performance without using compileStatement() since compileStatement() is limited to retrieving result only if output is 1x1 table?
More info:
I have an Android app which uses an SQLite database with a table having the following schema:
CREATE TABLE testtable(
id number primary key,
sentence text not null
);
The table is indexed on id.
What a part of my app does is to get an array of id's as input and retrieve the corresponding sentences from the table testtable.
I started by using the query() method which took around 7 sec to retrieve sentences for an array of 23770 ids. (23770 queries in 7 seconds)
I was trying to improve performance and I came to know that SQLiteStatement compileStatement(String sql) can improve performance by compiling the statements beforehand. And since SQLiteStatement has a method String simpleQueryForString() to retrieve results if the output is 1 x 1 table(which satisfies my usecase currently), I used it.
The improvement was massive. It could complete the same 23770 queries in 1 sec.
Although I can use this for now, the query may get complicated in future and the output may conatin more rows and columns which will make me use query() method.
So my question is: Is there a way to optimize queries without using compileStatement() and get similar performance?
This is the code I am testing with (The code using compileStatement() is commented):
public class DBMan extends SQLiteAssetHelper{
SQLiteDatabase db;
public DBMan(Context context){
super(context, "my.db", null, 1);
db = this.getReadableDatabase();
}
public String[] getSentences(Integer[] idList){
String[] result = new String[idList.length];
Cursor cur = null;
long timeStart = System.nanoTime();
try {
db.beginTransaction();
/* SQLiteStatement selStmt = db.compileStatement("SELECT sentence FROM testtable WHERE id=?"); */
for (int i = 0; i < idList.length; i++) {
// Querying using compileStatement() and simpleQueryForString()
/*
selStmt.clearBindings();
selStmt.bindLong(1, idList[i]);
result[i] = selStmt.simpleQueryForString();
*/
// Querying using query() and Cursor
cur = db.query(
"testtable",
new String[]{"sentence"},
"id = ?",
new String[]{String.valueOf(idList[i])},
null, null, null
);
if (cur.moveToFirst()) {
result[i] = cur.getString(0);
}
if (cur != null) {
cur.close();
}
}
db.setTransactionSuccessful();
}
finally {
db.endTransaction();
}
long totalTime = System.nanoTime() - timeStart;
Log.i("MYAPP", "DB total query time: "+totaltime/1000000000.0+" sec");
return result;
}
}
I'm using SQLiteAssetHelper which is an extension of SQLiteOpenHelper. I'm using it to copy my database file from assets folder on first run instead of creating it.
I'm used transactions although I'm doing only select queries as it reduces the number of shared locks that are obtained and dropped(see here).

Understanding SQLite Cursor Behavior

I'm writing a method to update default settings in a table. The table is very simple: two columns, the first containing labels to indicate the type of setting, the second to store the value of the setting.
At this point in the execution, the table is empty. I'm just setting up the initial value. So, I expect that this cursor will come back empty. But instead, I'm getting an error (shown below). The setting that I am working with is called "lastPlayer" and is supposed to get stored in the "SETTING_COLUMN" in the "SETTINGS_TABLE". Here's the code:
public static void updateSetting(String setting, String newVal) {
String table = "SETTINGS_TABLE";
String[] resultColumn = new String[] {VALUE_COLUMN};
String where = SETTING_COLUMN + "=" + setting;
System.err.println(where);
SQLiteDatabase db = godSimDBOpenHelper.getWritableDatabase();
Cursor cursor = db.query(table, resultColumn, where, null, null, null, null);
System.err.println("cursor returned"); //I never see this ouput
\\more
}
sqlite returned: error code = 1, msg = no such column: lastPlayer
Why is it saying that there is no such column lastPlayer? I thought that I was telling the query to look at the column "SETTING_COLUMN" and return the record where that column has a value "lastPlayer". I'm confused. Can somebody straighten me out? I've been looking a this for an hour and I just don't see what I am doing wrong.
Thanks!
You're not properly building/escaping your query. Since the value lastPlayer is not in quotes, your statement is checking for equality of two columns, which is what that error message is saying.
To properly build your query, it's best to not do this manually with String concatenation. Instead, the parameter selectionArgs of SQLiteDatabase.query() is meant to do this.
The parameters in your query should be defined as ? and then filled in based on the selectionArgs. From the docs:
You may include ?s in selection, which will be replaced by the values
from selectionArgs, in order that they appear in the selection. The
values will be bound as Strings.
So, your code would look like this:
String where = SETTING_COLUMN + " = ?";
Cursor cursor = db.query(table, resultColumn, where, new String[] { setting }, null, null, null);

Does cursor copy the result set?

For example, if i use following code to query some data from database.
Uri uri = Uri.parse("content://com.android.contacts/contacts?address_book_index_extras=true");
String selection = "LEFT OUTER JOIN (select raw_contact_id, data1 from data where mimetype_id = 5) AS phone_data ON(_id = phone_data.raw_contact_id)";
Cursor c = getContentResolver().query(Contacts.CONTENT_URI, null, selection, null, null);
What i want to ask is after the query method, does database copy its result set to cursor or just make cursor something like a pointer and point to the first line of result set and query for more data when we call `moveToNext'
thanks
Yes. It is a readonly copy of the DB.
From Android Developers:
This interface provides random read-write access to the result set returned by a database query. Cursor implementations are not required to be synchronized so code using a Cursor from multiple threads should perform its own synchronization when using the Cursor.

Pointing second cursor to same database in sqlite on android, this a no no?

I have this function that is filling out a class based on data from several tables. I got the first cursor:
String query="SELECT * FROM SESSION where _id =" + mSessionID + ";";
Cursor c = dbAdapter.selectRecordsFromDB(query, null);
Session session=null;
c.moveToFirst();
This works great. Then a little lower I do this:
long galleryId = c.getInt(4);
long packageId = c.getInt(5);
long contractId = c.getInt(6);
String query2="SELECT * FROM PHOTOPACKAGES WHERE _id =" + packageId + ";";
Cursor p = dbAdapter.selectRecordsFromDB(query2, null);
and the p cursor always returns -1 for its count. I can go right into the sqlite in the adb and run the same query where packageId = 1 and it works great...so not sure why this is not working, i don't see any other errors...can you just not use two cursors on the same database? p.s. selectRecordsFromDB is a helper function:
public Cursor selectRecordsFromDB(String query, String[] selectionArgs) {
Cursor c = myDataBase.rawQuery(query, selectionArgs);
return myDataBase.rawQuery(query, selectionArgs);
}
To answer your actual question: Yes you can target the same DB with multiple cursors. I believe there is something else wrong with your code.
Also as Philip pointed out, creating Cursors is very costly and you do not want to make extras just because, and always close them when finished with them.
Your selectRecordsFromDB function looks pretty darned weird, but it will probably work after a fashion, because the first cursor that you create goes out of focus straight away. Leaking open cursors like that is not a good idea though.

Categories

Resources