Skip to content

Commit

Permalink
Improve the fidelity of legacy SQLite error messages
Browse files Browse the repository at this point in the history
Previously, in the legacy SQLite mode, all exceptions thrown by SQLite were
wrapped in an arbitrary RuntimeExceptions. This is not consistent with real
Android, which always throws exceptions of type
android.database.sqlite.SQLiteException (including subclasses). This has impact
on data layer libraries such as Room that explicitly catch Android
SQLiteExceptions in order to execute error callback logic.

For #8469

PiperOrigin-RevId: 582331582
  • Loading branch information
hoisie authored and Copybara-Service committed Nov 15, 2023
1 parent 3ff3de4 commit 4de9408
Show file tree
Hide file tree
Showing 10 changed files with 347 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
38 changes: 38 additions & 0 deletions integration_tests/room/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
10 changes: 10 additions & 0 deletions integration_tests/room/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Manifest for androidx memoryleaks test module
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.robolectric.integrationtests.room">

<application>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<User> getAllUsers();
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Loading

0 comments on commit 4de9408

Please sign in to comment.