diff --git a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
index 5a4fa2d9ccb..df6592e0010 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
@@ -283,4 +283,26 @@ public void fts4() {
assertThat(results.getCount()).isEqualTo(0);
results.close();
}
+
+ @Config(minSdk = LOLLIPOP) // The SQLite error messages were updated significantly in Lollipop.
+ @SdkSuppress(minSdkVersion = LOLLIPOP)
+ @Test
+ public void uniqueConstraintViolation_errorMessage() {
+ database.execSQL(
+ "CREATE TABLE my_table(\n"
+ + " _id INTEGER PRIMARY KEY AUTOINCREMENT, \n"
+ + " unique_column TEXT UNIQUE\n"
+ + ");\n");
+ ContentValues values = new ContentValues();
+ values.put("unique_column", "test");
+ database.insertOrThrow("my_table", null, values);
+ SQLiteConstraintException exception =
+ assertThrows(
+ SQLiteConstraintException.class,
+ () -> database.insertOrThrow("my_table", null, values));
+ assertThat(exception)
+ .hasMessageThat()
+ .startsWith("UNIQUE constraint failed: my_table.unique_column");
+ assertThat(exception).hasMessageThat().contains("code 2067");
+ }
}
diff --git a/integration_tests/room/build.gradle b/integration_tests/room/build.gradle
new file mode 100644
index 00000000000..1798d40ba7e
--- /dev/null
+++ b/integration_tests/room/build.gradle
@@ -0,0 +1,38 @@
+import org.robolectric.gradle.AndroidProjectConfigPlugin
+
+apply plugin: 'com.android.library'
+apply plugin: AndroidProjectConfigPlugin
+
+android {
+ compileSdk 34
+ namespace 'org.robolectric.integrationtests.room'
+
+ defaultConfig {
+ minSdk 19
+ targetSdk 34
+ }
+
+ compileOptions {
+ sourceCompatibility = '1.8'
+ targetCompatibility = '1.8'
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+}
+
+dependencies {
+ // Testing dependencies
+ testImplementation project(path: ':testapp')
+ testImplementation project(":robolectric")
+ testImplementation libs.junit4
+ testImplementation libs.guava.testlib
+ testImplementation libs.guava.testlib
+ testImplementation libs.truth
+ implementation 'androidx.room:room-runtime:2.6.0'
+ annotationProcessor 'androidx.room:room-compiler:2.6.0'
+}
diff --git a/integration_tests/room/src/main/AndroidManifest.xml b/integration_tests/room/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..3707a8dcbd3
--- /dev/null
+++ b/integration_tests/room/src/main/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/User.java b/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/User.java
new file mode 100644
index 00000000000..a55d242f33f
--- /dev/null
+++ b/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/User.java
@@ -0,0 +1,15 @@
+package org.robolectric.integrationtests.room;
+
+import androidx.room.Entity;
+
+/** A simple User entity */
+@Entity(
+ tableName = "users",
+ primaryKeys = {"id"})
+public class User {
+ public long id;
+
+ public String username;
+
+ public String email;
+}
diff --git a/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/UserDao.java b/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/UserDao.java
new file mode 100644
index 00000000000..d7a4debce19
--- /dev/null
+++ b/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/UserDao.java
@@ -0,0 +1,20 @@
+package org.robolectric.integrationtests.room;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.Upsert;
+import java.util.List;
+
+/** Dao for {@link User} */
+@Dao
+public interface UserDao {
+ @Upsert
+ void upsert(User user);
+
+ @Insert
+ void insert(User user);
+
+ @Query("SELECT * FROM users")
+ List getAllUsers();
+}
diff --git a/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/UserDatabase.java b/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/UserDatabase.java
new file mode 100644
index 00000000000..f2b853882cf
--- /dev/null
+++ b/integration_tests/room/src/main/java/org/robolectric/integrationtests/room/UserDatabase.java
@@ -0,0 +1,24 @@
+package org.robolectric.integrationtests.room;
+
+import android.content.Context;
+import androidx.room.Database;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+/** Room database for {@link User} */
+@Database(
+ entities = {User.class},
+ version = 1,
+ exportSchema = false)
+public abstract class UserDatabase extends RoomDatabase {
+
+ public abstract UserDao userDao();
+
+ public static synchronized UserDatabase getInstance(Context context) {
+ return Room.databaseBuilder(
+ context.getApplicationContext(), UserDatabase.class, "user_database")
+ .fallbackToDestructiveMigration()
+ .allowMainThreadQueries()
+ .build();
+ }
+}
diff --git a/integration_tests/room/src/test/java/org/robolectric/integrationtests/room/UserDatabaseTest.java b/integration_tests/room/src/test/java/org/robolectric/integrationtests/room/UserDatabaseTest.java
new file mode 100644
index 00000000000..273568c4a93
--- /dev/null
+++ b/integration_tests/room/src/test/java/org/robolectric/integrationtests/room/UserDatabaseTest.java
@@ -0,0 +1,35 @@
+package org.robolectric.integrationtests.room;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.SQLiteMode;
+
+@RunWith(RobolectricTestRunner.class)
+public final class UserDatabaseTest {
+
+ /**
+ * There was an issue using Room with {@link SQLiteMode.Mode.LEGACY}. The {@link
+ * android.database.sqlite.SQLiteException} exceptions were wrapped in a way that was not
+ * compatible with Room.
+ */
+ @Test
+ @SQLiteMode(SQLiteMode.Mode.LEGACY)
+ public void upsert_conflict_usingLegacySQLite() {
+ UserDatabase db = UserDatabase.getInstance(RuntimeEnvironment.getApplication());
+
+ User user = new User();
+ user.id = 12;
+ user.username = "username";
+ user.email = "username@example.com";
+
+ UserDao dao = db.userDao();
+ dao.upsert(user);
+ dao.upsert(user); // Should succeed
+
+ assertThat(dao.getAllUsers()).hasSize(1);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
index 878777c10fd..5e8151ceb16 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
@@ -4,12 +4,14 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.robolectric.annotation.SQLiteMode.Mode.LEGACY;
import static org.robolectric.shadows.ShadowLegacySQLiteConnection.convertSQLWithLocalizedUnicodeCollator;
import android.content.ContentValues;
import android.database.Cursor;
+import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatatypeMismatchException;
import android.database.sqlite.SQLiteStatement;
@@ -188,6 +190,23 @@ public void interruption_doesNotConcurrentlyModifyDatabase() {
ShadowLegacySQLiteConnection.reset();
}
+ @Test
+ public void uniqueConstraintViolation_errorMessage() {
+ database.execSQL(
+ "CREATE TABLE my_table(\n"
+ + " _id INTEGER PRIMARY KEY AUTOINCREMENT, \n"
+ + " unique_column TEXT UNIQUE\n"
+ + ");\n");
+ ContentValues values = new ContentValues();
+ values.put("unique_column", "test");
+ database.insertOrThrow("my_table", null, values);
+ SQLiteConstraintException exception =
+ assertThrows(
+ SQLiteConstraintException.class,
+ () -> database.insertOrThrow("my_table", null, values));
+ assertThat(exception).hasMessageThat().endsWith("(code 2067 SQLITE_CONSTRAINT_UNIQUE)");
+ }
+
@Test
public void test_setUseInMemoryDatabase() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
diff --git a/settings.gradle b/settings.gradle
index 1894b8c9825..37692707e62 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -41,4 +41,5 @@ include ":integration_tests:multidex"
include ":integration_tests:play_services"
include ":integration_tests:sparsearray"
include ":integration_tests:nativegraphics"
+include ":integration_tests:room"
include ':testapp'
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacySQLiteConnection.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacySQLiteConnection.java
index fa141603800..2159878ebf0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacySQLiteConnection.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacySQLiteConnection.java
@@ -30,6 +30,8 @@
import com.almworks.sqlite4java.SQLiteConstants;
import com.almworks.sqlite4java.SQLiteException;
import com.almworks.sqlite4java.SQLiteStatement;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Uninterruptibles;
import java.io.File;
import java.util.ArrayList;
@@ -446,7 +448,6 @@ long open(final String path) {
synchronized (lock) {
final SQLiteConnection dbConnection =
execute(
- "open SQLite connection",
new Callable() {
@Override
public SQLiteConnection call() throws Exception {
@@ -477,7 +478,6 @@ long prepareStatement(final long connectionPtr, final String sql) {
final SQLiteConnection connection = getConnection(connectionPtr);
final SQLiteStatement statement =
execute(
- "prepare statement",
new Callable() {
@Override
public SQLiteStatement call() throws Exception {
@@ -495,13 +495,11 @@ public SQLiteStatement call() throws Exception {
void close(final long connectionPtr) {
synchronized (lock) {
final SQLiteConnection connection = getConnection(connectionPtr);
- execute("close connection", new Callable() {
- @Override
- public Void call() throws Exception {
- connection.dispose();
- return null;
- }
- });
+ execute(
+ () -> {
+ connection.dispose();
+ return null;
+ });
connectionsMap.remove(connectionPtr);
statementPtrsForConnection.remove(connectionPtr);
}
@@ -526,13 +524,12 @@ void reset() {
private static void shutdownDbExecutor(ExecutorService executorService, Collection connections) {
for (final SQLiteConnection connection : connections) {
- getFuture("close connection on reset", executorService.submit(new Callable() {
- @Override
- public Void call() throws Exception {
- connection.dispose();
- return null;
- }
- }));
+ getFuture(
+ executorService.submit(
+ () -> {
+ connection.dispose();
+ return null;
+ }));
}
executorService.shutdown();
@@ -552,13 +549,11 @@ void finalizeStmt(final long connectionPtr, final long statementPtr) {
final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
statementsMap.remove(statementPtr);
- execute("finalize statement", new Callable() {
- @Override
- public Void call() throws Exception {
- statement.dispose();
- return null;
- }
- });
+ execute(
+ () -> {
+ statement.dispose();
+ return null;
+ });
}
}
@@ -569,13 +564,14 @@ void cancel(final long connectionPtr) {
for (Long statementPtr : statementPtrsForConnection.get(connectionPtr)) {
final SQLiteStatement statement = statementsMap.get(statementPtr);
if (statement != null) {
- execute("cancel", new Callable() {
- @Override
- public Void call() throws Exception {
- statement.cancel();
- return null;
- }
- });
+ execute(
+ new Callable() {
+ @Override
+ public Void call() throws Exception {
+ statement.cancel();
+ return null;
+ }
+ });
}
}
}
@@ -589,7 +585,6 @@ int getParameterCount(final long connectionPtr, final long statementPtr) {
return executeStatementOperation(
connectionPtr,
statementPtr,
- "get parameters count in prepared statement",
new StatementOperation() {
@Override
public Integer call(final SQLiteStatement statement) throws Exception {
@@ -606,7 +601,6 @@ boolean isReadOnly(final long connectionPtr, final long statementPtr) {
return executeStatementOperation(
connectionPtr,
statementPtr,
- "call isReadOnly",
new StatementOperation() {
@Override
public Boolean call(final SQLiteStatement statement) throws Exception {
@@ -619,7 +613,6 @@ long executeForLong(final long connectionPtr, final long statementPtr) {
return executeStatementOperation(
connectionPtr,
statementPtr,
- "execute for long",
new StatementOperation() {
@Override
public Long call(final SQLiteStatement statement) throws Exception {
@@ -640,7 +633,6 @@ void executeStatement(final long connectionPtr, final long statementPtr) {
executeStatementOperation(
connectionPtr,
statementPtr,
- "execute",
new StatementOperation() {
@Override
public Void call(final SQLiteStatement statement) throws Exception {
@@ -654,7 +646,6 @@ String executeForString(final long connectionPtr, final long statementPtr) {
return executeStatementOperation(
connectionPtr,
statementPtr,
- "execute for string",
new StatementOperation() {
@Override
public String call(final SQLiteStatement statement) throws Exception {
@@ -671,7 +662,6 @@ int getColumnCount(final long connectionPtr, final long statementPtr) {
return executeStatementOperation(
connectionPtr,
statementPtr,
- "get columns count",
new StatementOperation() {
@Override
public Integer call(final SQLiteStatement statement) throws Exception {
@@ -684,7 +674,6 @@ String getColumnName(final long connectionPtr, final long statementPtr, final in
return executeStatementOperation(
connectionPtr,
statementPtr,
- "get column name at index " + index,
new StatementOperation() {
@Override
public String call(final SQLiteStatement statement) throws Exception {
@@ -697,7 +686,6 @@ void bindNull(final long connectionPtr, final long statementPtr, final int index
executeStatementOperation(
connectionPtr,
statementPtr,
- "bind null at index " + index,
new StatementOperation() {
@Override
public Void call(final SQLiteStatement statement) throws Exception {
@@ -711,7 +699,6 @@ void bindLong(final long connectionPtr, final long statementPtr, final int index
executeStatementOperation(
connectionPtr,
statementPtr,
- "bind long at index " + index + " with value " + value,
new StatementOperation() {
@Override
public Void call(final SQLiteStatement statement) throws Exception {
@@ -725,7 +712,6 @@ void bindDouble(final long connectionPtr, final long statementPtr, final int ind
executeStatementOperation(
connectionPtr,
statementPtr,
- "bind double at index " + index + " with value " + value,
new StatementOperation() {
@Override
public Void call(final SQLiteStatement statement) throws Exception {
@@ -739,7 +725,6 @@ void bindString(final long connectionPtr, final long statementPtr, final int ind
executeStatementOperation(
connectionPtr,
statementPtr,
- "bind string at index " + index,
new StatementOperation() {
@Override
public Void call(final SQLiteStatement statement) throws Exception {
@@ -753,7 +738,6 @@ void bindBlob(final long connectionPtr, final long statementPtr, final int index
executeStatementOperation(
connectionPtr,
statementPtr,
- "bind blob at index " + index,
new StatementOperation() {
@Override
public Void call(final SQLiteStatement statement) throws Exception {
@@ -769,7 +753,6 @@ int executeForChangedRowCount(final long connectionPtr, final long statementPtr)
final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
return execute(
- "execute for changed row count",
new Callable() {
@Override
public Integer call() throws Exception {
@@ -790,7 +773,6 @@ long executeForLastInsertedRowId(final long connectionPtr, final long statementP
final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
return execute(
- "execute for last inserted row ID",
new Callable() {
@Override
public Long call() throws Exception {
@@ -805,7 +787,6 @@ long executeForCursorWindow(final long connectionPtr, final long statementPtr, f
return executeStatementOperation(
connectionPtr,
statementPtr,
- "execute for cursor window",
new StatementOperation() {
@Override
public Integer call(final SQLiteStatement statement) throws Exception {
@@ -818,7 +799,6 @@ void resetStatementAndClearBindings(final long connectionPtr, final long stateme
executeStatementOperation(
connectionPtr,
statementPtr,
- "reset statement",
new StatementOperation() {
@Override
public Void call(final SQLiteStatement statement) throws Exception {
@@ -832,53 +812,157 @@ interface StatementOperation {
T call(final SQLiteStatement statement) throws Exception;
}
- private T executeStatementOperation(final long connectionPtr,
- final long statementPtr,
- final String comment,
- final StatementOperation statementOperation) {
+ private T executeStatementOperation(
+ final long connectionPtr,
+ final long statementPtr,
+ final StatementOperation statementOperation) {
synchronized (lock) {
final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
- return execute(comment, new Callable() {
- @Override
- public T call() throws Exception {
- return statementOperation.call(statement);
- }
- });
+ return execute(
+ () -> {
+ return statementOperation.call(statement);
+ });
+ }
}
- }
- /**
- * Any Callable passed in to execute must not synchronize on lock, as this will result in a deadlock
- */
- private T execute(final String comment, final Callable work) {
+ /**
+ * Any Callable passed in to execute must not synchronize on lock, as this will result in a
+ * deadlock
+ */
+ private T execute(final Callable work) {
synchronized (lock) {
return PerfStatsCollector.getInstance()
- .measure("sqlite", () -> getFuture(comment, dbExecutor.submit(work)));
+ .measure("sqlite", () -> getFuture(dbExecutor.submit(work)));
+ }
}
- }
- private static T getFuture(final String comment, final Future future) {
- try {
- return Uninterruptibles.getUninterruptibly(future);
- // No need to catch cancellationexception - we never cancel these futures
- } catch (ExecutionException e) {
- Throwable t = e.getCause();
- if (t instanceof SQLiteException) {
+ private static T getFuture(final Future future) {
+ try {
+ return Uninterruptibles.getUninterruptibly(future);
+ // No need to catch cancellationexception - we never cancel these futures
+ } catch (ExecutionException e) {
+ Throwable t = e.getCause();
+ if (t instanceof SQLiteException) {
+ SQLiteException sqliteException = (SQLiteException) t;
final RuntimeException sqlException =
- getSqliteException("Cannot " + comment, ((SQLiteException) t).getBaseErrorCode());
- sqlException.initCause(e);
- throw sqlException;
+ getSqliteException(sqliteException.getMessage(), sqliteException.getErrorCode());
+ sqlException.initCause(e);
+ throw sqlException;
} else if (t instanceof android.database.sqlite.SQLiteException) {
throw (android.database.sqlite.SQLiteException) t;
- } else {
- throw new RuntimeException(e);
+ } else {
+ throw new RuntimeException(e);
+ }
}
}
- }
- private static RuntimeException getSqliteException(final String message, final int baseErrorCode) {
- // Mapping is from throw_sqlite3_exception in android_database_SQLiteCommon.cpp
- switch (baseErrorCode) {
+ // These are from android_database_SQLiteCommon.cpp
+ private static final ImmutableMap ERROR_CODE_MAP =
+ new ImmutableMap.Builder()
+ .put(4, "SQLITE_ABORT")
+ .put(23, "SQLITE_AUTH")
+ .put(5, "SQLITE_BUSY")
+ .put(14, "SQLITE_CANTOPEN")
+ .put(19, "SQLITE_CONSTRAINT")
+ .put(11, "SQLITE_CORRUPT")
+ .put(101, "SQLITE_DONE")
+ .put(16, "SQLITE_EMPTY")
+ .put(1, "SQLITE_ERROR")
+ .put(24, "SQLITE_FORMAT")
+ .put(13, "SQLITE_FULL")
+ .put(2, "SQLITE_INTERNAL")
+ .put(9, "SQLITE_INTERRUPT")
+ .put(10, "SQLITE_IOERR")
+ .put(6, "SQLITE_LOCKED")
+ .put(20, "SQLITE_MISMATCH")
+ .put(21, "SQLITE_MISUSE")
+ .put(22, "SQLITE_NOLFS")
+ .put(7, "SQLITE_NOMEM")
+ .put(26, "SQLITE_NOTADB")
+ .put(12, "SQLITE_NOTFOUND")
+ .put(27, "SQLITE_NOTICE")
+ .put(0, "SQLITE_OK")
+ .put(3, "SQLITE_PERM")
+ .put(15, "SQLITE_PROTOCOL")
+ .put(25, "SQLITE_RANGE")
+ .put(8, "SQLITE_READONLY")
+ .put(100, "SQLITE_ROW")
+ .put(17, "SQLITE_SCHEMA")
+ .put(18, "SQLITE_TOOBIG")
+ .put(28, "SQLITE_WARNING")
+ // Extended Result Code List
+ .put(516, "SQLITE_ABORT_ROLLBACK")
+ .put(261, "SQLITE_BUSY_RECOVERY")
+ .put(517, "SQLITE_BUSY_SNAPSHOT")
+ .put(1038, "SQLITE_CANTOPEN_CONVPATH")
+ .put(782, "SQLITE_CANTOPEN_FULLPATH")
+ .put(526, "SQLITE_CANTOPEN_ISDIR")
+ .put(270, "SQLITE_CANTOPEN_NOTEMPDIR")
+ .put(275, "SQLITE_CONSTRAINT_CHECK")
+ .put(531, "SQLITE_CONSTRAINT_COMMITHOOK")
+ .put(787, "SQLITE_CONSTRAINT_FOREIGNKEY")
+ .put(1043, "SQLITE_CONSTRAINT_FUNCTION")
+ .put(1299, "SQLITE_CONSTRAINT_NOTNULL")
+ .put(1555, "SQLITE_CONSTRAINT_PRIMARYKEY")
+ .put(2579, "SQLITE_CONSTRAINT_ROWID")
+ .put(1811, "SQLITE_CONSTRAINT_TRIGGER")
+ .put(2067, "SQLITE_CONSTRAINT_UNIQUE")
+ .put(2323, "SQLITE_CONSTRAINT_VTAB")
+ .put(267, "SQLITE_CORRUPT_VTAB")
+ .put(3338, "SQLITE_IOERR_ACCESS")
+ .put(2826, "SQLITE_IOERR_BLOCKED")
+ .put(3594, "SQLITE_IOERR_CHECKRESERVEDLOCK")
+ .put(4106, "SQLITE_IOERR_CLOSE")
+ .put(6666, "SQLITE_IOERR_CONVPATH")
+ .put(2570, "SQLITE_IOERR_DELETE")
+ .put(5898, "SQLITE_IOERR_DELETE_NOENT")
+ .put(4362, "SQLITE_IOERR_DIR_CLOSE")
+ .put(1290, "SQLITE_IOERR_DIR_FSYNC")
+ .put(1802, "SQLITE_IOERR_FSTAT")
+ .put(1034, "SQLITE_IOERR_FSYNC")
+ .put(6410, "SQLITE_IOERR_GETTEMPPATH")
+ .put(3850, "SQLITE_IOERR_LOCK")
+ .put(6154, "SQLITE_IOERR_MMAP")
+ .put(3082, "SQLITE_IOERR_NOMEM")
+ .put(2314, "SQLITE_IOERR_RDLOCK")
+ .put(266, "SQLITE_IOERR_READ")
+ .put(5642, "SQLITE_IOERR_SEEK")
+ .put(5130, "SQLITE_IOERR_SHMLOCK")
+ .put(5386, "SQLITE_IOERR_SHMMAP")
+ .put(4618, "SQLITE_IOERR_SHMOPEN")
+ .put(4874, "SQLITE_IOERR_SHMSIZE")
+ .put(522, "SQLITE_IOERR_SHORT_READ")
+ .put(1546, "SQLITE_IOERR_TRUNCATE")
+ .put(2058, "SQLITE_IOERR_UNLOCK")
+ .put(778, "SQLITE_IOERR_WRITE")
+ .put(262, "SQLITE_LOCKED_SHAREDCACHE")
+ .put(539, "SQLITE_NOTICE_RECOVER_ROLLBACK")
+ .put(283, "SQLITE_NOTICE_RECOVER_WAL")
+ .put(256, "SQLITE_OK_LOAD_PERMANENTLY")
+ .put(520, "SQLITE_READONLY_CANTLOCK")
+ .put(1032, "SQLITE_READONLY_DBMOVED")
+ .put(264, "SQLITE_READONLY_RECOVERY")
+ .put(776, "SQLITE_READONLY_ROLLBACK")
+ .put(284, "SQLITE_WARNING_AUTOINDEX")
+ .build();
+
+ private static RuntimeException getSqliteException(
+ final String sqliteErrorMessage, final int errorCode) {
+ final int baseErrorCode = errorCode & 0xff;
+ // Remove redundant error code prefix from sqlite4java. The error code is added
+ // as a suffix below.
+ String errorMessageWithoutCode = sqliteErrorMessage.replaceAll("^\\[\\d+\\] ?", "");
+ StringBuilder fullMessage = new StringBuilder(errorMessageWithoutCode);
+ fullMessage.append(" (code ");
+ fullMessage.append(errorCode);
+ String errorCodeMessage = ERROR_CODE_MAP.getOrDefault(errorCode, "");
+ if (MoreObjects.firstNonNull(errorCodeMessage, "").length() > 0) {
+ fullMessage.append(" ").append(errorCodeMessage);
+ }
+ fullMessage.append(")");
+ String message = fullMessage.toString();
+ // Mapping is from throw_sqlite3_exception in android_database_SQLiteCommon.cpp
+ switch (baseErrorCode) {
case SQLiteConstants.SQLITE_ABORT: return new SQLiteAbortException(message);
case SQLiteConstants.SQLITE_PERM: return new SQLiteAccessPermException(message);
case SQLiteConstants.SQLITE_RANGE: return new SQLiteBindOrColumnIndexOutOfRangeException(message);