I'm writing an Android app that needs to write to the SQLite database. Currently it's using rawQueryWithString to build the update query, and I'm using ? placeholders in my query combined with the selectionArgs argument to pass in the actual values.
However, sometimes I actually want to update my column (of type Date) to NULL, but if I pass in null in my selectionArgs then I get this error:
IllegalArgumentException: the bind value at index 1 is null
I can't really see how I'm supposed to make it work when the value is actually null. I guess I could pass in an empty string, which in the case of a Date column like this might just work, but suppose it was a string column and I actually did want to mean NULL in contrast to the empty string (or are they considered equivalent in SQLite?)
Here's the code:
String timestampStr = null; // Obviously not really set like this
SQLiteDatabase d = getWritableDatabase();
DBCursor c = (DBCursor) d.rawQueryWithFactory(
new DBCursor.Factory(),
"Update subject set schedulingTimestamp = ? where identifier = ?",
new String[] { timestampStr, subjId.toString() },
null);
d.close();
The column was added with the following query, so I presume it's a nullable column since I didn't specify otherwise:
ALTER TABLE subject ADD schedulingTimestamp DATE;
Wildcards are not meant to be used for inserting/updating values in SQL, AFAIK. In Android, you can use ContentValues instead in conjunction with the update() method, instead of trying to shoehorn it in the raw query method.
Related
Conclusion: Android's database APIs work but the documentation is horribly incomplete.
I have recently run into a brain wrecking situation due to the flexibility Sqlite provides by not forcing you to specify the data type when creating the table. The problem was my mindset that assumed that every data type would be a general character sequence if not specified and therefore the way to talk to database is through java.lang.String.
But you can't blame me either when you see methods like the below:
int delete (String table,
String whereClause,
String[] whereArgs)
in the SqlDatabase class from Android docs.
I have a table consisting of Phone No(that I stored as java.lang.String) and Timestamp as a long field. When I tried deleting a record using this method, it just never got deleted despite countless debugging.
I checked everything and query was alright and table is existent and all the checklist until by chance, I discovered that removing the '' around the timestamp while querying in a raw manner instead of using the above method yields a successful deletion, something like this:
DELETE FROM messages_records_table WHERE messageTimestamp = 1508494606000;
instead of the following:
DELETE FROM messages_records_table WHERE messageTimestamp = '1508494606000';
or,
DELETE FROM messages_records_table WHERE messageTimestamp = "1508494606000";
Phone No isn't a problem; it's the timestamp that was creating the problem in INSERTION/DELETION
So, I tried running a raw deletion query with quotes removed(that are required with a string/varchar type) and it yielded successful deletion. I used the following method for this:
db.execSQL(String sql, Object[] whereArgs)
The key thing to notice here is that Object[] is different from String[] when compared to delete(). I passed a Long to Object to make it work but passing a Long.toString() in delete() seems to be useless.
So my question is, Is my analysis correct and delete() API is basically useless or have I missed some bigger picture..after all, it's provided by Android team carefully?
SQLite supports multiple data types; and while column types are not strictly enforced, values might be automatically converted in some cases (this is called affinity).
When your values are stored as numbers, you should access them as numbers, not as strings.
The Android database API does not allow you to use parameter types other than strings in most functions. This is a horrible design bug.
To search for a number, either use execSQL(), which allows you to use number parameters, or convert the string value back into a number:
db.delete(..., "timestamp = CAST(? AS NUMBER)",
new String[]{ String.valueOf(ts) });
The problem was my mindset that assumed that every data type would be
a general character sequence if not specified and therefore the way to
talk to database is through java.lang.String.
I think that's the real issue.
If you specify no type e.g.
CREATE TABLE mytable (col1,col2,col3)
Then according to Determination of Column Affinity(3.1) rule 3:-
3) If the declared type for a column contains the string "BLOB" or if no
type is specified then the column has affinity BLOB.
And then according to Section 3
A column with affinity BLOB does not prefer one storage class over
another and no attempt is made to coerce data from one storage class
into another.
I've personally never had an issue with delete. However I do have a tendency to always delete according to rowid.
Here's a working example usage that shows that delete isn't useless and is deleting according to a long. However the columns are all of type INTEGER :-
int pudeletes;
int sldeletes;
int rdeletes;
int pdeletes;
if(doesProductExist(productid)) {
// if not in a transaction then begin a transaction
if(!intransaction) {
db.beginTransaction();
}
String whereargs[] = { Long.toString(productid)};
// Delete ProductUsage rows that use this product
pudeletes = db.delete(
DBProductusageTableConstants.PRODUCTUSAGE_TABLE,
DBProductusageTableConstants.PRODUCTUSAGE_PRODUCTREF_COL +
" = ?",
whereargs
);
// Delete ShopList rows that use this product
sldeletes = db.delete(
DBShopListTableConstants.SHOPLIST_TABLE,
DBShopListTableConstants.SHOPLIST_PRODUCTREF_COL +
" = ?",
whereargs
);
// Delete Rules rows that use this product
rdeletes = db.delete(
DBRulesTableConstants.RULES_TABLE,
DBRulesTableConstants.RULES_PRODUCTREF_COL +
" = ?",
whereargs
);
// Delete the Product
pdeletes = db.delete(
DBProductsTableConstants.PRODUCTS_TABLE,
DBProductsTableConstants.PRODUCTS_ID_COL +
" = ?",
whereargs
);
// if originally not in a transaction then as one was started
// complete and end the transaction
if(!intransaction) {
db.setTransactionSuccessful();
db.endTransaction();
}
}
I have an SQLite query that does not return a record by _id on Android Nougat but does return a record on Android pre-Nougat.
When I attach a debugger to the Nougat emulator and start poking around, I observe the following:
Original Code
readableDatabase.query("report_view", null, "_id = ?", new String[]{"2016309001"}, null, null, null).getCount()
Result: 0
Poking around / Evaluate expression
readableDatabase.rawQuery("SELECT * FROM report_view WHERE _id = 2016309001", null).getCount()
Result: 1
Enabling the SQLite log results in (note the single quotes):
V/SQLiteStatements: /data/user/0/xyz/bla.db: "SELECT * FROM report_view WHERE _id = '2016309001'"
For the non-raw query on Nougat. I cannot seem to enable the SQLite log on the pre-Nougat (Marshmallow) device.
So it seems the issue is caused by Android surrounding the parameter with single quotes. The _id column is of type integer. And a value surrounded by quotes is of type string. Why is this suddenly an issue on Nougat?
Unless I am mistaken your original code passes the date parameter explicitly as a string (new String[]), and the Android documentation for the query method, selectioArgs parameter states
selectionArgs String: 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 if you want something else than strings, you should probably perform a cast, or better, do not use the query and rawQuery methods, but prepared statements (SQLiteStatement), which will allow you to specify the type of parameters.
Also the '?' being substituted in the log should be a "fake" substitution for logging purposes (at least I hope it is a fake substitution for logging purposes, as Android should be using a bound parameter there, and not replace it in the query).
Finally the field types in Android are used only for type affinity, so it is entirely possible that even if you declared your field as "integer" in the table, if you used the Query method and its String-only parameters, you actually ended up with strings in your records.
In theory, SQLite should not behave differently. It is possible that you are using a different table definition that results in a different affinity.
Anyway, the parameter you give to the query function is a string, so it is not surprising that the database handles it as a string.
If you want to give a number to the database, you have to make the parameter a number.
And the Android framework doesn't allow this for most database functions, so you have to put it directly into the string:
db.query("report_view", null, "_id = " + "2016309001", null, null, null, null).getCount();
Please note that there is a helper function for counting rows:
DatabaseUtils.queryNumEntries(db, "report_view", "_id = " + "2016309001");
I have an Android SQLite table definition like this:
create table MyTable(..., myDate integer, ...);
Later in my code I query this table to retrieve the value for myDate via a cursor:
long dateInstant = cursor.getLong(cursor.getColumnIndexOrThrow("myDate"));
However, I'm wondering if this is the best approach. If the value is not set (i.e. null on the database) the above method returns 0, which happens to also be a valid value. I need to differentiate valid values from null values. Also, from the javadoc for getLong:
The result and whether this method throws an exception when the column value is null, the column type is not an integral type, or the integer value is outside the range [Long.MIN_VALUE, Long.MAX_VALUE] is implementation-defined.
which seems to suggest, if I read this correctly, that at some point in the future the SQLite implementation shipped in Android may choose to throw an exception rather than return 0. I'm looking for a consistent solution across Android versions.
In short, how can I robustly handle the retrieval of integer data from a column in a way which allows me to differentiate between valid values and null?
You can do
long dateInstant = 0; // some value to represent null
if (!cursor.isNull (colIndex))
dateInstance = cursor.getLong (colIndex);
I'm passing the below statement as a rawQuery in Android:
SELECT DISTINCT ltUsers._id,ltUsers.NAME,ltUsers.GLOBAL_ID, ltGroups.GROUP_NAME
FROM ltUsers
JOIN ltGroups ON (ltUsers.GROUP_ID = ltGroups.GLOBAL_ID)
WHERE ltgroups.GLOBAL_ID = ? " +
ORDER BY ltUsers.NAME ASC,ltgroups.GLOBAL_ID ASC;
With the rawQuery as follows:
Cursor c = db.rawQuery(sql,args)
It works just fine if I pass a value to the parameter, e.g.
String[] args = new String[]{"2"}
However, I also want to be able to show all rows, unlimited by the GLOBAL_ID in the WHERE clause. Testing on a dump of my SQLite database outside of Android - as well as in Android by just writing the parameter directly into the statement - shows the following clause to be a valid way to do this:
WHERE ltGroups.GLOBAL_ID = ltGroups.GLOBAL_ID
Yet when I pass the field reference ltGroups.GLOBAL_ID or [ltGroups].[GLOBAL_ID] as a parameter it fails to return any rows in the rawQuery. Any ideas on why this might be happening? Happy to produce any extra information.
Parameters always replace specific values, not anything else.
When you put the string "ltGroups.GLOBAL_ID" into the parameters array, it is interpreted as exactly that, a string.
(To show all records, just omit the WHERE clause.)
I have to execute the following query in Android SQLite
UPDATE player SET rank=rank+1 WHERE rank >= 3;
Here rank is the column name of INTEGER type.
I know about update method
update(tablename, values, whereClause, whereArgs);
I will pass player as first argument, rank >= 3 as 3rd argument and null as last argument but I am unable to find what to pass as 2nd argument. According to the docs it should be an object of ContentValues.
so I created its object as
ContentValues values = new ContentValues();
values.put("rank",?????);
I am confused about what to put the value for rank key which is a column in my table. Please suggest me what to pass as value to put() method of values
alternatively you can use exec , and execute your sql command
db.execSQL("UPDATE player SET rank=rank+1 WHERE rank >= 3");
to do it using the update method, you will need to get the current rank in a variable using Cursor, and then do the + thing manually "current+1"
I believe update method is not One-Size-Fit-All method. So I think you need to use rawQuery() to achieve this kind of sql execution. for example:
db.rawQuery("UPDATE player SET rank=rank+1 WHERE rank >= 3;", null);
or perhaps use execSQL as said by el7r