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