Distinguish null blobs in SQLiteRawStatement
The sqlite value API sqlite3_column_blob() returns a null if the
column type is SQLITE_NULL or a zero-length SQLITE_BLOB.
SQLiteRawStatement.getColumnBlob() now distinguishes between these two
cases: if the column type is SQLITE_NULL, return null, otherwise
return a byte array of length zero.
New unit tests have been added. Unit tests have also been added for
SQLiteRawStatement.getColumnText()'s handling of empty strings and
null values.
Test: atest
* FrameworksCoreTests:android.database
* CtsDatabaseTestCases
Flag: EXEMPT bugfix
Bug: 342687891
Change-Id: Ia041eeb761267de0a4b59af3ddcba6433b231bbb
diff --git a/core/jni/android_database_SQLiteRawStatement.cpp b/core/jni/android_database_SQLiteRawStatement.cpp
index b6b78811..8fc13a8 100644
--- a/core/jni/android_database_SQLiteRawStatement.cpp
+++ b/core/jni/android_database_SQLiteRawStatement.cpp
@@ -41,6 +41,11 @@
*/
namespace android {
+// A zero-length byte array that can be returned by getColumnBlob(). The theory is that
+// zero-length blobs are common enough that it is worth having a single, global instance. The
+// object is created in the jni registration function. It is never destroyed.
+static jbyteArray emptyArray = nullptr;
+
// Helper functions.
static sqlite3 *db(long statementPtr) {
return sqlite3_db_handle(reinterpret_cast<sqlite3_stmt*>(statementPtr));
@@ -226,7 +231,7 @@
throwIfInvalidColumn(env, stmtPtr, col);
const void* blob = sqlite3_column_blob(stmt(stmtPtr), col);
if (blob == nullptr) {
- return NULL;
+ return (sqlite3_column_type(stmt(stmtPtr), col) == SQLITE_NULL) ? NULL : emptyArray;
}
size_t size = sqlite3_column_bytes(stmt(stmtPtr), col);
jbyteArray result = env->NewByteArray(size);
@@ -316,8 +321,10 @@
int register_android_database_SQLiteRawStatement(JNIEnv *env)
{
- return RegisterMethodsOrDie(env, "android/database/sqlite/SQLiteRawStatement",
- sStatementMethods, NELEM(sStatementMethods));
+ RegisterMethodsOrDie(env, "android/database/sqlite/SQLiteRawStatement",
+ sStatementMethods, NELEM(sStatementMethods));
+ emptyArray = MakeGlobalRefOrDie(env, env->NewByteArray(0));
+ return 0;
}
} // namespace android
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java
index 548b8ec..8071d3d 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -507,6 +508,12 @@
s.bindInt(1, 3);
s.step();
s.reset();
+ // Bind a zero-length blob
+ s.clearBindings();
+ s.bindInt(1, 4);
+ s.bindBlob(2, new byte[0]);
+ s.step();
+ s.reset();
}
mDatabase.setTransactionSuccessful();
} finally {
@@ -545,6 +552,17 @@
for (int i = 0; i < c.length; i++) c[i] = 0;
s.bindInt(1, 3);
assertTrue(s.step());
+ assertNull(s.getColumnBlob(0));
+ assertEquals(0, s.readColumnBlob(0, c, 0, c.length, 0));
+ for (int i = 0; i < c.length; i++) assertEquals(0, c[i]);
+ s.reset();
+
+ // Fetch the zero-length blob
+ s.bindInt(1, 4);
+ assertTrue(s.step());
+ byte[] r = s.getColumnBlob(0);
+ assertNotNull(r);
+ assertEquals(0, r.length);
assertEquals(0, s.readColumnBlob(0, c, 0, c.length, 0));
for (int i = 0; i < c.length; i++) assertEquals(0, c[i]);
s.reset();
@@ -572,6 +590,83 @@
}
@Test
+ public void testText() {
+ mDatabase.beginTransaction();
+ try {
+ final String query = "CREATE TABLE t1 (i int, b text)";
+ try (SQLiteRawStatement s = mDatabase.createRawStatement(query)) {
+ assertFalse(s.step());
+ }
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+
+ // Insert data into the table.
+ mDatabase.beginTransaction();
+ try {
+ final String query = "INSERT INTO t1 (i, b) VALUES (?1, ?2)";
+ try (SQLiteRawStatement s = mDatabase.createRawStatement(query)) {
+ // Bind a string
+ s.bindInt(1, 1);
+ s.bindText(2, "text");
+ s.step();
+ s.reset();
+ s.clearBindings();
+
+ // Bind a zero-length string
+ s.bindInt(1, 2);
+ s.bindText(2, "");
+ s.step();
+ s.reset();
+ s.clearBindings();
+
+ // Bind a null string
+ s.clearBindings();
+ s.bindInt(1, 3);
+ s.step();
+ s.reset();
+ s.clearBindings();
+ }
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+
+ // Read back data and verify it against the reference copy.
+ mDatabase.beginTransactionReadOnly();
+ try {
+ final String query = "SELECT (b) FROM t1 WHERE i = ?1";
+ try (SQLiteRawStatement s = mDatabase.createRawStatement(query)) {
+ // Fetch the entire reference array.
+ s.bindInt(1, 1);
+ assertTrue(s.step());
+ assertEquals(SQLiteRawStatement.SQLITE_DATA_TYPE_TEXT, s.getColumnType(0));
+
+ String a = s.getColumnText(0);
+ assertNotNull(a);
+ assertEquals(a, "text");
+ s.reset();
+
+ s.bindInt(1, 2);
+ assertTrue(s.step());
+ String b = s.getColumnText(0);
+ assertNotNull(b);
+ assertEquals(b, "");
+ s.reset();
+
+ s.bindInt(1, 3);
+ assertTrue(s.step());
+ String c = s.getColumnText(0);
+ assertNull(c);
+ s.reset();
+ }
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ @Test
public void testParameterMetadata() {
createComplexDatabase();