How to handle nullable integer columns in SQLite database - android

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

Related

Can you set default value of a column based on the calculations of previous two columns in SQLite?

I have a table in SQLite where I would like to set the value of 3rd column based on the input value of first two columns.
Does SQLite even support an expression in the default value of a column?
create table contract(
id primary key autoincrement,
insurance_pct integer not null default 0,
historical_yield integer not null default 0,
guaranteed_yield integer not null default (insurance_pct/100 * historical_yield)
)
When I run the above statement I see following error.
Query execution failed
Reason:
SQL Error [1]: [SQLITE_ERROR] SQL error or missing database (default value of column [GUARANTEED_YIELD] is not constant)
As the error message "default value of column [GUARANTEED_YIELD] is not constant" clearly points out, you cannot use variables in the default expression.
One way to achieve what you want is to make an after insert trigger, that updates the column, when it was inserted as null. That however requires, that the column is not declared not null as otherwise the INSERT will fail. So you'd have to check that too in a before update trigger.
CREATE TABLE contract
(id integer PRIMARY KEY
AUTOINCREMENT,
insurance_pct integer
NOT NULL
DEFAULT 0,
historical_yield integer
NOT NULL
DEFAULT 0,
guaranteed_yield integer
NULL
DEFAULT NULL);
CREATE TRIGGER contract_ai
AFTER INSERT
ON contract
FOR EACH ROW
WHEN (new.guaranteed_yield IS NULL)
BEGIN
UPDATE contract
SET guaranteed_yield = insurance_pct / 100 * historical_yield
WHERE id = new.id;
END;
CREATE TRIGGER contract_bu
BEFORE UPDATE
ON contract
FOR EACH ROW
WHEN (new.guaranteed_yield IS NULL)
BEGIN
SELECT raise(FAIL, 'NOT NULL constraint failed: contract.guaranteed_yield');
END;
One other thing I noticed: guaranteed_yield is an integer but your default expression pretty likely produces non integer values. You might lose something due to the required rounding. I'm not sure whether this is intentional.
Addendum:
Looking at the comments to your question I'm not sure whether you merely want a default -- i.e. the value of guaranteed_yield should have the value of the expression, if no other value is explicitly given at INSERT but it is possible for it to have other (non null) values either from an INSERT or or a subsequent UPDATE -- or if you intend this to be a calculated column, that always has the value the expression gives. In the latter case: I second the other commenters. This is a potentially dangerous thing regarding inconsistencies. Preferably use the expression in your queries or create a view.
This is not supported in SQLite.
The CREATE TABLE documentation states :
An explicit DEFAULT clause may specify that the default value is NULL, a string constant, a blob constant, a signed-number, or any constant expression enclosed in parentheses.
Further, the document defines the concept of constant expression :
For the purposes of the DEFAULT clause, an expression is considered constant if it contains no sub-queries, column or table references, bound parameters, or string literals enclosed in double-quotes instead of single-quotes.
This namely excludes references to other columns.
Other possible approaches in your use case :
compute the value dynamically in your DML queries (UPDATE and INSERT)
use a trigger to provide a dynamic default on DML operations

Cursor.getType() returns FIELD_TYPE_NULL if all the rows are null for a particular column

I am creating a table using the following query:
private static final String SQL_CREATE_ENTRIES = "CREATE TABLE person_info ( uniqueId INTEGER,first_name TEXT,last_name TEXT,
address TEXT)";
sqLiteDatabase.execSQL(SQL_CREATE_ENTRIES);
I am inserting the values as follows:
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put("first_name", "Anshul");
values.put("last_name", "Jain");
values.put("address", "Bangalore");
return db.insert(TABLE_NAME, null, values);
Note that I am not giving any value for uniqueId column. Thus, uniqueId column values are null.
When I query the database and try to the type of each column using cursor.getType(i), it returns Cursor.FIELD_TYPE_NULL for uniqueId column. According to the documentation, if all the column values are null, then it will return this value. But ideally it should return Cursor.FIELD_TYPE_INTEGER because that's what I declared while creating the database.
Is there any other way of retrieving the correct column type when all the values of a column are null.
Most SQL database engines (every SQL database engine other than
SQLite, as far as we know) uses static, rigid typing. With static
typing, the datatype of a value is determined by its container - the
particular column in which the value is stored.
SQLite uses a more general dynamic type system. In SQLite, the
datatype of a value is associated with the value itself, not with its
container.
SQLite Docs
This behavior of returning Cursor.FIELD_TYPE_NULL(which according to you is not ideal) is absolutely ideal because SQLite is designed in that way only.
Querying the database to get the type of a Container using cursor.getType(i) will only work if the Container is not NULL otherwise it returns Cursor.FIELD_TYPE_NULL (as in your case).
You can use PRAGMA table_info(table_name) for retrieving the datatype.
Check this SO answer -- Getting the type of a column in SQLite

Binding NULL arguments with queryWithFactory

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.

Does value get converted when inserted into database?

I was curious if androids SQLiteDatabase insert method automatically handles type conversion.
Here is my example:
I have a csv file with a column name of age. Its type will be an INTEGER.
Lets say I have already created the database and table.
Now I am parsing the csv file with CSVReader, which parses each line and inserts each value into an index of a String[].
In order to insert each line of data into the database, I have to use a ContentValue object, which allows me to store values in it.
//Parse each line and store in line...
ContentValue values = new ContentValue();
values.put(KEY_AGE, line[1]); // Assume line[1] is the age
database.insert(table, null, values);
If I store the age value as a string (as seen above), and then insert it into the table, does Android handle the conversion to INTEGER before inserting it into the database?
I am asking this because I am trying to insert a bunch of tables into a database, and it looks much cleaner when I can just iterate through an array then explicitly state each put call, i.e:
Also if anyone has any design suggestions feel free to tell me.
CLEAN
int i = 0;
for(String s : TransitContract.Routes.COLUMN_ARRAY) {
values.put(s, line[i]);
i++;
}
UGLY
values.put(TransitContract.Routes.KEY_ROUTE_ID, line[0]);
values.put(TransitContract.Routes.KEY_AGENCY_ID, line[1]);
values.put(TransitContract.Routes.KEY_SHORT_NAME, line[2]);
values.put(TransitContract.Routes.KEY_LONG_NAME, line[3]);
values.put(TransitContract.Routes.KEY_DESCRIPTION, line[4]);
values.put(TransitContract.Routes.KEY_ROUTE_TYPE, Integer.parseInt(line[5]));
values.put(TransitContract.Routes.KEY_URL, line[6]);
values.put(TransitContract.Routes.KEY_COLOR, line[7]);
values.put(TransitContract.Routes.KEY_TEXT_COLOR, line[8]);
return mDatabase.insert(TransitContract.Routes.TABLE_NAME, null, values);
When you declare a column as INTEGER, SQLite will automatically convert strings to numbers, if possible.
See the documentation for Type Affinity.
If your ContentProvider doesn't restrict it (i.e. pass it directly to the SQLiteDatabase.insert() method), it should work. SQLite is not that picky about the types used in queries/inserts and the actual column type.
However, it would be best practice to parse and check the values before inserting. Otherwise you might actually insert a string which can't be parsed as integer and therefore retrieving the value might fail.
References:
Boolean datatype accepting string value and integer value
SQLite table with integer column stores string

inserting null as Integer value into a database

I am trying to store a null value into the database but everytime I load the value I get 0.
I declare the field just like "intVal integer"
This is how I retrieve it:
Integer x;
x = cursor.getInt(cursor.getColumnIndexOrThrow(MyDBAdapter.KEY_X));
Is that reliable? or is it undefined?
So it seems to me one cannot save null as an undefined integervalue
Many thanks
From the getInt() documentation:
Returns the value of the requested column as an int.
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 [Integer.MIN_VALUE, Integer.MAX_VALUE] is implementation-defined.
So, it's an implementation detail which you shouldn't rely on.
You can still get the effect you want, though: you simply do the null check before you read the value.
int index = cursor.getColumnIndexOrThrow(MyDBAdapter.KEY_X);
Integer x = null;
if (!cursor.isNull(index)
x = cursor.getInt(index);
// now x is either null or contains a valid value
SQLite is dynamically typed. Since you defined column of type Integer and it didn't find a value it returned 0 based on the hint.
Try using String as the type of column
As Rajdeep Dua mentioned, SQLite uses a dynamic typing. Here's what the documentation says:
Most SQL database engines (every SQL database engine other than
SQLite, as far as we know) uses static, rigid typing. With static
typing, the datatype of a value is determined by its container - the
particular column in which the value is stored.
SQLite uses a more general dynamic type system. In SQLite, the
datatype of a value is associated with the value itself, not with its
container.
The Android documentation says that the result of getInt() when the column value is null is implementation defined. For me, it returns 0 as well.
So I used getString() instead, although the result of this method is also implementation defined, for me, it returns null when the column value is null, or a string containing the number otherwise.
So the code looks like this:
Strint strVal = cursor.getInt(cursor.getColumnIndexOrThrow(MyDBAdapter.KEY_X));
if(strVal != null){
int intVal = Integer.parseInt(strVal);
}

Categories

Resources