diff options
| author | 2024-12-06 13:18:37 -0800 | |
|---|---|---|
| committer | 2024-12-07 12:10:31 -0800 | |
| commit | 4cc641f58e77b38f0b628cbc973f56b8e99ce800 (patch) | |
| tree | 201ff706823cff4bc6884aed29ecdd297d6043ad | |
| parent | 9176f4a731df973ec1a276d04d0bfe8800d189e4 (diff) | |
Fine-tune sqlite misuse exceptions
The baseline code throws a "column-out-of-bounds" exception for some
APIs, where the bounds are determined by the sqlite statement.
However, this exception is misleading if the real problem is that the
statement is not properly prepared. The code change executes the
desired sqlite api and throws an error if one is detected. If no
error is detected but the column is out of bounds, the
column-out-of-bounds exception is thrown.
Note that this change does not throw an exception when one would not
have been thrown before. It only changes the exception that is
thrown.
Flag: EXEMPT bug-fix
Bug: 359676342
Test: atest
* FrameworksCoreTests:android.database
* CtsDatabaseTestCases
Change-Id: Ic85982aeff680b19dedd14460cb26ba5b5ce18ed
3 files changed, 63 insertions, 17 deletions
diff --git a/core/java/android/database/sqlite/SQLiteRawStatement.java b/core/java/android/database/sqlite/SQLiteRawStatement.java index c59d3cea0414..3f3e46b4334c 100644 --- a/core/java/android/database/sqlite/SQLiteRawStatement.java +++ b/core/java/android/database/sqlite/SQLiteRawStatement.java @@ -554,10 +554,16 @@ public final class SQLiteRawStatement implements Closeable { * * @see <a href="http://sqlite.org/c3ref/column_blob.html">sqlite3_column_type</a> * + * If the row has no data then a {@link SQLiteMisuseException} is thrown. This condition can + * occur the last call to {@link #step()} returned false or if {@link #step()} was not called + * before the statement was created or after the last call to {@link #reset()}. Note that + * {@link SQLiteMisuseException} may be thrown for other reasons. + * * @param columnIndex The index of a column in the result row. It is zero-based. * @return The type of the value in the column of the result row. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. * @throws SQLiteException if a native error occurs. */ @SQLiteDataType @@ -580,6 +586,7 @@ public final class SQLiteRawStatement implements Closeable { * @return The name of the column in the result row. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. See {@link #getColumnType()}. * @throws SQLiteOutOfMemoryException if the database cannot allocate memory for the name. */ @NonNull @@ -606,6 +613,7 @@ public final class SQLiteRawStatement implements Closeable { * @return The length, in bytes, of the value in the column. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. See {@link #getColumnType()}. * @throws SQLiteException if a native error occurs. */ public int getColumnLength(int columnIndex) { @@ -631,6 +639,7 @@ public final class SQLiteRawStatement implements Closeable { * @return The value of the column as a blob, or null if the column is NULL. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. See {@link #getColumnType()}. * @throws SQLiteException if a native error occurs. */ @Nullable @@ -664,6 +673,7 @@ public final class SQLiteRawStatement implements Closeable { * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws IllegalArgumentException if the buffer is too small for offset+length. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. See {@link #getColumnType()}. * @throws SQLiteException if a native error occurs. */ public int readColumnBlob(int columnIndex, @NonNull byte[] buffer, int offset, @@ -691,6 +701,7 @@ public final class SQLiteRawStatement implements Closeable { * @return The value of a column as a double. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. See {@link #getColumnType()}. * @throws SQLiteException if a native error occurs. */ public double getColumnDouble(int columnIndex) { @@ -715,6 +726,7 @@ public final class SQLiteRawStatement implements Closeable { * @return The value of the column as an int. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. See {@link #getColumnType()}. * @throws SQLiteException if a native error occurs. */ public int getColumnInt(int columnIndex) { @@ -739,6 +751,7 @@ public final class SQLiteRawStatement implements Closeable { * @return The value of the column as an long. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. See {@link #getColumnType()}. * @throws SQLiteException if a native error occurs. */ public long getColumnLong(int columnIndex) { @@ -763,6 +776,7 @@ public final class SQLiteRawStatement implements Closeable { * @return The value of the column as a string. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the column is out of range. + * @throws SQLiteMisuseException if the row has no data. See {@link #getColumnType()}. * @throws SQLiteException if a native error occurs. */ @NonNull diff --git a/core/jni/android_database_SQLiteRawStatement.cpp b/core/jni/android_database_SQLiteRawStatement.cpp index 85a6bdf95928..32c2ef73a5b1 100644 --- a/core/jni/android_database_SQLiteRawStatement.cpp +++ b/core/jni/android_database_SQLiteRawStatement.cpp @@ -70,12 +70,32 @@ static void throwInvalidParameter(JNIEnv *env, jlong stmtPtr, jint index) { } } +// If the last operation failed, throw an exception and return true. Otherwise return false. +static bool throwIfError(JNIEnv *env, jlong stmtPtr) { + switch (sqlite3_errcode(db(stmtPtr))) { + case SQLITE_OK: + case SQLITE_DONE: + case SQLITE_ROW: return false; + } + throw_sqlite3_exception(env, db(stmtPtr), nullptr); + return true; +} -// This throws a SQLiteBindOrColumnIndexOutOfRangeException if the column index is out -// of bounds. It returns true if an exception was thrown. +// This throws a SQLiteBindOrColumnIndexOutOfRangeException if the column index is out of +// bounds. It throws SQLiteMisuseException if the statement's column count is zero; that +// generally occurs because the client has forgotten to call step() or the client has stepped +// past the end of the query. The function returns true if an exception was thrown. static bool throwIfInvalidColumn(JNIEnv *env, jlong stmtPtr, jint col) { - if (col < 0 || col >= sqlite3_data_count(stmt(stmtPtr))) { - int count = sqlite3_data_count(stmt(stmtPtr)); + int count = sqlite3_data_count(stmt(stmtPtr)); + if (throwIfError(env, stmtPtr)) { + return true; + } else if (count == 0) { + // A count of zero indicates a misuse: the statement has never been step()'ed. + const char* message = "row has no data"; + const char* errmsg = sqlite3_errstr(SQLITE_MISUSE); + throw_sqlite3_exception(env, SQLITE_MISUSE, errmsg, message); + return true; + } else if (col < 0 || col >= count) { std::string message = android::base::StringPrintf( "column index %d out of bounds [0,%d]", col, count - 1); char const * errmsg = sqlite3_errstr(SQLITE_RANGE); @@ -86,17 +106,6 @@ static bool throwIfInvalidColumn(JNIEnv *env, jlong stmtPtr, jint col) { } } -// If the last operation failed, throw an exception and return true. Otherwise return false. -static bool throwIfError(JNIEnv *env, jlong stmtPtr) { - switch (sqlite3_errcode(db(stmtPtr))) { - case SQLITE_OK: - case SQLITE_DONE: - case SQLITE_ROW: return false; - } - throw_sqlite3_exception(env, db(stmtPtr), nullptr); - return true; -} - static jint bindParameterCount(JNIEnv* env, jclass, jlong stmtPtr) { return sqlite3_bind_parameter_count(stmt(stmtPtr)); } diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java index 6dad3b7b2ac4..13b12fcf300a 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java @@ -1010,6 +1010,7 @@ public class SQLiteRawStatementTest { mDatabase.beginTransaction(); try { mDatabase.execSQL("CREATE TABLE t1 (i int, j int);"); + mDatabase.execSQL("INSERT INTO t1 (i, j) VALUES (2, 20)"); mDatabase.setTransactionSuccessful(); } finally { mDatabase.endTransaction(); @@ -1017,13 +1018,35 @@ public class SQLiteRawStatementTest { mDatabase.beginTransactionReadOnly(); try (SQLiteRawStatement s = mDatabase.createRawStatement("SELECT * from t1")) { - s.step(); - s.getColumnText(5); // out-of-range column + assertTrue(s.step()); + s.getColumnText(5); // out-of-range column: the range is [0,2). fail("JNI exception not thrown"); } catch (SQLiteBindOrColumnIndexOutOfRangeException e) { // Passing case. } finally { mDatabase.endTransaction(); } + + mDatabase.beginTransactionReadOnly(); + try (SQLiteRawStatement s = mDatabase.createRawStatement("SELECT * from t1")) { + // Do not step the statement. The column count will be zero. + s.getColumnText(5); // out-of-range column: never stepped. + fail("JNI exception not thrown"); + } catch (SQLiteMisuseException e) { + // Passing case. + } finally { + mDatabase.endTransaction(); + } + + mDatabase.beginTransactionReadOnly(); + try (SQLiteRawStatement s = mDatabase.createRawStatement("SELECT * from t1")) { + // Do not step the statement. The column count will be zero. + s.getColumnText(0); // out-of-range column: never stepped. + fail("JNI exception not thrown"); + } catch (SQLiteMisuseException e) { + // Passing case. + } finally { + mDatabase.endTransaction(); + } } } |