/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.database.sqlite; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import dalvik.annotation.optimization.FastNative; import java.io.Closeable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.Reference; import java.util.Objects; /** * A {@link SQLiteRawStatement} represents a SQLite prepared statement. The methods correspond very * closely to SQLite APIs that operate on a sqlite_stmt object. In general, each API in this class * corresponds to a single SQLite API. *

* A {@link SQLiteRawStatement} must be created through a database, and there must be a * transaction open at the time. Statements are implicitly closed when the outermost transaction * ends, or if the current transaction is marked successful. Statements may be explicitly * closed at any time with {@link #close}. The {@link #close} operation is idempotent and may be * called multiple times without harm. *

* Multiple {@link SQLiteRawStatement}s may be open simultaneously. They are independent of each * other. Closing one statement does not affect any other statement nor does it have any effect * on the enclosing transaction. *

* Once a {@link SQLiteRawStatement} has been closed, no further database operations are * permitted on that statement. An {@link IllegalStateException} will be thrown if a database * operation is attempted on a closed statement. *

* All operations on a {@link SQLiteRawStatement} must be invoked from the thread that created * it. A {@link IllegalStateException} will be thrown if cross-thread use is detected. *

* A common pattern for statements is try-with-resources. *

 * // Begin a transaction.
 * database.beginTransaction();
 * try (SQLiteRawStatement statement = database.createRawStatement("SELECT * FROM ...")) {
 *     while (statement.step()) {
 *         // Fetch columns from the result rows.
 *     }
 *     database.setTransactionSuccessful();
 * } finally {
 *     database.endTransaction();
 * }
 * 
* Note that {@link SQLiteRawStatement} is unrelated to {@link SQLiteStatement}. * * @see sqlite3_stmt */ @FlaggedApi(Flags.FLAG_SQLITE_APIS_35) public final class SQLiteRawStatement implements Closeable { private static final String TAG = "SQLiteRawStatement"; /** * The database for this object. */ private final SQLiteDatabase mDatabase; /** * The session for this object. */ private final SQLiteSession mSession; /** * The PreparedStatement associated with this object. This is returned to * {@link SQLiteSession} when the object is closed. This also retains immutable attributes of * the statement, like the parameter count. */ private SQLiteConnection.PreparedStatement mPreparedStatement; /** * The native statement associated with this object. This is pulled from the * PreparedStatement for faster access. */ private final long mStatement; /** * The SQL string, for logging. */ private final String mSql; /** * The thread that created this object. The object is tied to a connection, which is tied to * its session, which is tied to the thread. (The lifetime of this object is bounded by the * lifetime of the enclosing transaction, so there are more rules than just the relationships * in the second sentence.) This variable is set to null when the statement is closed. */ private Thread mThread; /** * The field types for SQLite columns. * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = { SQLITE_DATA_TYPE_INTEGER, SQLITE_DATA_TYPE_FLOAT, SQLITE_DATA_TYPE_TEXT, SQLITE_DATA_TYPE_BLOB, SQLITE_DATA_TYPE_NULL}) public @interface SQLiteDataType {} /** * The constant returned by {@link #getColumnType} when the column value is SQLITE_INTEGER. */ public static final int SQLITE_DATA_TYPE_INTEGER = 1; /** * The constant returned by {@link #getColumnType} when the column value is SQLITE_FLOAT. */ public static final int SQLITE_DATA_TYPE_FLOAT = 2; /** * The constant returned by {@link #getColumnType} when the column value is SQLITE_TEXT. */ public static final int SQLITE_DATA_TYPE_TEXT = 3; /** * The constant returned by {@link #getColumnType} when the column value is SQLITE_BLOB. */ public static final int SQLITE_DATA_TYPE_BLOB = 4; /** * The constant returned by {@link #getColumnType} when the column value is SQLITE_NULL. */ public static final int SQLITE_DATA_TYPE_NULL = 5; /** * SQLite error codes that are used by this class. */ private static final int SQLITE_BUSY = 5; private static final int SQLITE_LOCKED = 6; private static final int SQLITE_ROW = 100; private static final int SQLITE_DONE = 101; /** * Create the statement with empty bindings. The construtor will throw * {@link IllegalStateException} if a transaction is not in progress. Clients should call * {@link SQLiteDatabase.createRawStatement} to create a new instance. */ SQLiteRawStatement(@NonNull SQLiteDatabase db, @NonNull String sql) { mThread = Thread.currentThread(); mDatabase = db; mSession = mDatabase.getThreadSession(); mSession.throwIfNoTransaction(); mSql = sql; // Acquire a connection and prepare the statement. mPreparedStatement = mSession.acquirePersistentStatement(mSql, this); mStatement = mPreparedStatement.mStatementPtr; } /** * Throw if the current session is not the session under which the object was created. Throw * if the object has been closed. The actual check is that the current thread is not equal to * the creation thread. */ private void throwIfInvalid() { if (mThread != Thread.currentThread()) { // Disambiguate the reasons for a mismatch. if (mThread == null) { throw new IllegalStateException("method called on a closed statement"); } else { throw new IllegalStateException("method called on a foreign thread: " + mThread); } } } /** * Throw {@link IllegalArgumentException} if the length + offset are invalid with respect to * the array length. */ private void throwIfInvalidBounds(int arrayLength, int offset, int length) { if (arrayLength < 0) { throw new IllegalArgumentException("invalid array length " + arrayLength); } if (offset < 0 || offset >= arrayLength) { throw new IllegalArgumentException("invalid offset " + offset + " for array length " + arrayLength); } if (length <= 0 || ((arrayLength - offset) < length)) { throw new IllegalArgumentException("invalid offset " + offset + " and length " + length + " for array length " + arrayLength); } } /** * Close the object and release any native resources. It is not an error to call this on an * already-closed object. */ @Override public void close() { if (mThread != null) { // The object is known not to be closed, so this only throws if the caller is not in // the creation thread. throwIfInvalid(); mSession.releasePersistentStatement(mPreparedStatement, this); mThread = null; } } /** * Return true if the statement is still open and false otherwise. * * @return True if the statement is open. */ public boolean isOpen() { return mThread != null; } /** * Step to the next result row. This returns true if the statement stepped to a new row, and * false if the statement is done. The method throws on any other result, including a busy or * locked database. If WAL is enabled then the database should never be locked or busy. * * @see sqlite3_step * * @return True if a row is available and false otherwise. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteDatabaseLockedException if the database is locked or busy. * @throws SQLiteException if a native error occurs. */ public boolean step() { throwIfInvalid(); try { int err = nativeStep(mStatement, true); switch (err) { case SQLITE_ROW: return true; case SQLITE_DONE: return false; case SQLITE_BUSY: throw new SQLiteDatabaseLockedException("database " + mDatabase + " busy"); case SQLITE_LOCKED: throw new SQLiteDatabaseLockedException("database " + mDatabase + " locked"); } // This line of code should never be reached, because the native method should already // have thrown an exception. throw new SQLiteException("unknown error " + err); } finally { Reference.reachabilityFence(this); } } /** * Step to the next result. This returns the raw result code code from the native method. The * expected values are SQLITE_ROW and SQLITE_DONE. For other return values, clients must * decode the error and handle it themselves. http://sqlite.org/rescode.html for the current * list of result codes. * * @return The native result code from the sqlite3_step() operation. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @hide */ public int stepNoThrow() { throwIfInvalid(); try { return nativeStep(mStatement, false); } finally { Reference.reachabilityFence(this); } } /** * Reset the statement. * * @see sqlite3_reset * * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteException if a native error occurs. */ public void reset() { throwIfInvalid(); try { nativeReset(mStatement, false); } finally { Reference.reachabilityFence(this); } } /** * Clear all parameter bindings. * * @see sqlite3_clear_bindings * * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteException if a native error occurs. */ public void clearBindings() { throwIfInvalid(); try { nativeClearBindings(mStatement); } finally { Reference.reachabilityFence(this); } } /** * Return the number of parameters in the statement. * * @see * sqlite3_bind_parameter_count * * @return The number of parameters in the statement. * @throws IllegalStateException if the statement is closed or this is a foreign thread. */ public int getParameterCount() { throwIfInvalid(); try { return nativeBindParameterCount(mStatement); } finally { Reference.reachabilityFence(this); } } /** * Return the index of the parameter with specified name. If the name does not match any * parameter, 0 is returned. * * @see * sqlite3_bind_parameter_index * * @param name The name of a parameter. * @return The index of the parameter or 0 if the name does not identify a parameter. * @throws IllegalStateException if the statement is closed or this is a foreign thread. */ public int getParameterIndex(@NonNull String name) { Objects.requireNonNull(name); throwIfInvalid(); try { return nativeBindParameterIndex(mStatement, name); } finally { Reference.reachabilityFence(this); } } /** * Return the name of the parameter at the specified index. Null is returned if there is no * such parameter or if the parameter does not have a name. * * @see * sqlite3_bind_parameter_name * * @param parameterIndex The index of the parameter. * @return The name of the parameter. * @throws IllegalStateException if the statement is closed or this is a foreign thread. */ @Nullable public String getParameterName(int parameterIndex) { throwIfInvalid(); try { return nativeBindParameterName(mStatement, parameterIndex); } finally { Reference.reachabilityFence(this); } } /** * Bind a blob to a parameter. Parameter indices start at 1. The function throws if the * parameter index is out of bounds. * * @see sqlite3_bind_blob * * @param parameterIndex The index of the parameter in the query. It is one-based. * @param value The value to be bound to the parameter. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range. * @throws SQLiteException if a native error occurs. */ public void bindBlob(int parameterIndex, @NonNull byte[] value) { Objects.requireNonNull(value); throwIfInvalid(); try { nativeBindBlob(mStatement, parameterIndex, value, 0, value.length); } finally { Reference.reachabilityFence(this); } } /** * Bind a blob to a parameter. Parameter indices start at 1. The function throws if the * parameter index is out of bounds. The sub-array value[offset] to value[offset+length-1] is * bound. * * @see sqlite3_bind_blob * * @param parameterIndex The index of the parameter in the query. It is one-based. * @param value The value to be bound to the parameter. * @param offset An offset into the value array * @param length The number of bytes to bind from the value array. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws IllegalArgumentException if the sub-array exceeds the bounds of the value array. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range. * @throws SQLiteException if a native error occurs. */ public void bindBlob(int parameterIndex, @NonNull byte[] value, int offset, int length) { Objects.requireNonNull(value); throwIfInvalid(); throwIfInvalidBounds(value.length, offset, length); try { nativeBindBlob(mStatement, parameterIndex, value, offset, length); } finally { Reference.reachabilityFence(this); } } /** * Bind a double to a parameter. Parameter indices start at 1. The function throws if the * parameter index is out of bounds. * * @see sqlite3_bind_double * * @param parameterIndex The index of the parameter in the query. It is one-based. * @param value The value to be bound to the parameter. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range. * @throws SQLiteException if a native error occurs. */ public void bindDouble(int parameterIndex, double value) { throwIfInvalid(); try { nativeBindDouble(mStatement, parameterIndex, value); } finally { Reference.reachabilityFence(this); } } /** * Bind an int to a parameter. Parameter indices start at 1. The function throws if the * parameter index is out of bounds. * * @see sqlite3_bind_int * * @param parameterIndex The index of the parameter in the query. It is one-based. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range. * @throws SQLiteException if a native error occurs. */ public void bindInt(int parameterIndex, int value) { throwIfInvalid(); try { nativeBindInt(mStatement, parameterIndex, value); } finally { Reference.reachabilityFence(this); } } /** * Bind a long to the parameter. Parameter indices start at 1. The function throws if the * parameter index is out of bounds. * * @see sqlite3_bind_int64 * * @param value The value to be bound to the parameter. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range. * @throws SQLiteException if a native error occurs. */ public void bindLong(int parameterIndex, long value) { throwIfInvalid(); try { nativeBindLong(mStatement, parameterIndex, value); } finally { Reference.reachabilityFence(this); } } /** * Bind a null to the parameter. Parameter indices start at 1. The function throws if the * parameter index is out of bounds. * * @see sqlite3_bind_null * * @param parameterIndex The index of the parameter in the query. It is one-based. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range. * @throws SQLiteException if a native error occurs. */ public void bindNull(int parameterIndex) { throwIfInvalid(); try { nativeBindNull(mStatement, parameterIndex); } finally { Reference.reachabilityFence(this); } } /** * Bind a string to the parameter. Parameter indices start at 1. The function throws if the * parameter index is out of bounds. The string may not be null. * * @see sqlite3_bind_text16 * * @param parameterIndex The index of the parameter in the query. It is one-based. * @param value The value to be bound to the parameter. * @throws IllegalStateException if the statement is closed or this is a foreign thread. * @throws SQLiteBindOrColumnIndexOutOfRangeException if the parameter is out of range. * @throws SQLiteException if a native error occurs. */ public void bindText(int parameterIndex, @NonNull String value) { Objects.requireNonNull(value); throwIfInvalid(); try { nativeBindText(mStatement, parameterIndex, value); } finally { Reference.reachabilityFence(this); } } /** * Return the number of columns in the current result row. * * @see sqlite3_column_count * * @return The number of columns in the result row. * @throws IllegalStateException if the statement is closed or this is a foreign thread. */ public int getResultColumnCount() { throwIfInvalid(); try { return nativeColumnCount(mStatement); } finally { Reference.reachabilityFence(this); } } /** * Return the type of the column in the result row. Column indices start at 0. * * @see sqlite3_column_type * * @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 SQLiteException if a native error occurs. */ @SQLiteDataType public int getColumnType(int columnIndex) { throwIfInvalid(); try { return nativeColumnType(mStatement, columnIndex); } finally { Reference.reachabilityFence(this); } } /** * Return the name of the column in the result row. Column indices start at 0. This throws * an exception if column is not in the result. * * @see sqlite3_column_name * * @param columnIndex The index of a column in the result row. It is zero-based. * @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 SQLiteOutOfMemoryException if the database cannot allocate memory for the name. */ @NonNull public String getColumnName(int columnIndex) { throwIfInvalid(); try { return nativeColumnName(mStatement, columnIndex); } finally { Reference.reachabilityFence(this); } } /** * Return the length of the column value in the result row. Column indices start at 0. This * returns 0 for a null and number of bytes for text or blob. Numeric values are converted to a * string and the length of the string is returned. See the sqlite documentation for * details. Note that this cannot be used to distinguish a null value from an empty text or * blob. Note that this returns the number of bytes in the text value, not the number of * characters. * * @see sqlite3_column_bytes * * @param columnIndex The index of a column in the result row. It is zero-based. * @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 SQLiteException if a native error occurs. */ public int getColumnLength(int columnIndex) { throwIfInvalid(); try { return nativeColumnBytes(mStatement, columnIndex); } finally { Reference.reachabilityFence(this); } } /** * Return the column value of the result row as a blob. Column indices start at 0. This * throws an exception if column is not in the result. This returns null if the column value * is null. * * The column value will be converted if it is not of type {@link #SQLITE_DATA_TYPE_BLOB}; see * the sqlite documentation for details. * * @see sqlite3_column_blob * * @param columnIndex The index of a column in the result row. It is zero-based. * @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 SQLiteException if a native error occurs. */ @Nullable public byte[] getColumnBlob(int columnIndex) { throwIfInvalid(); try { return nativeColumnBlob(mStatement, columnIndex); } finally { Reference.reachabilityFence(this); } } /** * Copy the column value of the result row, interpreted as a blob, into the buffer. Column * indices start at 0. This throws an exception if column is not in the result row. Bytes are * copied into the buffer starting at the offset. Bytes are copied from the blob starting at * srcOffset. Length bytes are copied unless the column value has fewer bytes available. The * function returns the number of bytes copied. * * The column value will be converted if it is not of type {@link #SQLITE_DATA_TYPE_BLOB}; see * the sqlite documentation for details. * * @see sqlite3_column_blob * * @param columnIndex The index of a column in the result row. It is zero-based. * @param buffer A pre-allocated array to be filled with the value of the column. * @param offset An offset into the buffer: copying starts here. * @param length The number of bytes to copy. * @param srcOffset The offset into the blob from which to start copying. * @return the number of bytes that were copied. * @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 SQLiteException if a native error occurs. */ public int readColumnBlob(int columnIndex, @NonNull byte[] buffer, int offset, int length, int srcOffset) { Objects.requireNonNull(buffer); throwIfInvalid(); throwIfInvalidBounds(buffer.length, offset, length); try { return nativeColumnBuffer(mStatement, columnIndex, buffer, offset, length, srcOffset); } finally { Reference.reachabilityFence(this); } } /** * Return the column value as a double. Column indices start at 0. This throws an exception * if column is not in the result. * * The column value will be converted if it is not of type {@link #SQLITE_DATA_TYPE_FLOAT}; see * the sqlite documentation for details. * * @see sqlite3_column_double * * @param columnIndex The index of a column in the result row. It is zero-based. * @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 SQLiteException if a native error occurs. */ public double getColumnDouble(int columnIndex) { throwIfInvalid(); try { return nativeColumnDouble(mStatement, columnIndex); } finally { Reference.reachabilityFence(this); } } /** * Return the column value as a int. Column indices start at 0. This throws an exception if * column is not in the result. * * The column value will be converted if it is not of type {@link #SQLITE_DATA_TYPE_INTEGER}; * see the sqlite documentation for details. * * @see sqlite3_column_int * * @param columnIndex The index of a column in the result row. It is zero-based. * @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 SQLiteException if a native error occurs. */ public int getColumnInt(int columnIndex) { throwIfInvalid(); try { return nativeColumnInt(mStatement, columnIndex); } finally { Reference.reachabilityFence(this); } } /** * Return the column value as a long. Column indices start at 0. This throws an exception if * column is not in the result. * * The column value will be converted if it is not of type {@link #SQLITE_DATA_TYPE_INTEGER}; * see the sqlite documentation for details. * * @see sqlite3_column_long * * @param columnIndex The index of a column in the result row. It is zero-based. * @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 SQLiteException if a native error occurs. */ public long getColumnLong(int columnIndex) { throwIfInvalid(); try { return nativeColumnLong(mStatement, columnIndex); } finally { Reference.reachabilityFence(this); } } /** * Return the column value as a text. Column indices start at 0. This throws an exception if * column is not in the result. * * The column value will be converted if it is not of type {@link #SQLITE_DATA_TYPE_TEXT}; see * the sqlite documentation for details. * * @see sqlite3_column_text16 * * @param columnIndex The index of a column in the result row. It is zero-based. * @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 SQLiteException if a native error occurs. */ @NonNull public String getColumnText(int columnIndex) { throwIfInvalid(); try { return nativeColumnText(mStatement, columnIndex); } finally { Reference.reachabilityFence(this); } } @Override public String toString() { if (isOpen()) { return "SQLiteRawStatement: " + mSql; } else { return "SQLiteRawStatement: (closed) " + mSql; } } /** * Native methods that only require a statement. */ /** * Metadata about the prepared statement. The results are a property of the statement itself * and not of any data in the database. */ @FastNative private static native int nativeBindParameterCount(long stmt); @FastNative private static native int nativeBindParameterIndex(long stmt, String name); @FastNative private static native String nativeBindParameterName(long stmt, int param); @FastNative private static native int nativeColumnCount(long stmt); /** * Operations on the statement */ private static native int nativeStep(long stmt, boolean throwOnError); private static native void nativeReset(long stmt, boolean clear); @FastNative private static native void nativeClearBindings(long stmt); /** * Methods that bind values to parameters. */ @FastNative private static native void nativeBindBlob(long stmt, int param, byte[] val, int off, int len); @FastNative private static native void nativeBindDouble(long stmt, int param, double val); @FastNative private static native void nativeBindInt(long stmt, int param, int val); @FastNative private static native void nativeBindLong(long stmt, int param, long val); @FastNative private static native void nativeBindNull(long stmt, int param); @FastNative private static native void nativeBindText(long stmt, int param, String val); /** * Methods that return information about the columns int the current result row. */ @FastNative private static native int nativeColumnType(long stmt, int col); @FastNative private static native String nativeColumnName(long stmt, int col); /** * Methods that return information about the value columns in the current result row. */ @FastNative private static native int nativeColumnBytes(long stmt, int col); @FastNative private static native byte[] nativeColumnBlob(long stmt, int col); @FastNative private static native int nativeColumnBuffer(long stmt, int col, byte[] val, int off, int len, int srcOffset); @FastNative private static native double nativeColumnDouble(long stmt, int col); @FastNative private static native int nativeColumnInt(long stmt, int col); @FastNative private static native long nativeColumnLong(long stmt, int col); @FastNative private static native String nativeColumnText(long stmt, int col); }