Handle boxing in field vs its getter/setter
This CL fixes a bug in room where we were not using the right type
adapters when a field's type does not exactly match its getter/setter
type.
For instance, if field is Integer and getter returns `int`, the code
we generate would do a null check on that getter, causing a compilation
failure.
The core issue was that `isAssignable` always returns true between
boxed types and we were always inferring the type from field (because
of the assumption that getter/setter has the same type).
Now the PojoProcessor updates the reader/writer adapter if the
getter/setter type does not match the field's type. We also put a
warning in this case as it might cause unexpected values in the
database.
Bug:142780009
Test: BoxedPrimitivesTest, TableEntityProcessorTest
Change-Id: Ic1592d66a01000b32696d0c69b60536f8f5550ef
diff --git a/room/common/api/2.3.0-alpha01.txt b/room/common/api/2.3.0-alpha01.txt
index 238967d..4e032bd 100644
--- a/room/common/api/2.3.0-alpha01.txt
+++ b/room/common/api/2.3.0-alpha01.txt
@@ -167,6 +167,8 @@
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_IS_DROPPED = "ROOM_PARENT_INDEX_IS_DROPPED";
+ field public static final String MISMATCHED_GETTER = "ROOM_MISMATCHED_GETTER_TYPE";
+ field public static final String MISMATCHED_SETTER = "ROOM_MISMATCHED_SETTER_TYPE";
field public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD = "ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX";
field public static final String MISSING_JAVA_TMP_DIR = "ROOM_MISSING_JAVA_TMP_DIR";
field public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
diff --git a/room/common/api/current.txt b/room/common/api/current.txt
index 238967d..4e032bd 100644
--- a/room/common/api/current.txt
+++ b/room/common/api/current.txt
@@ -167,6 +167,8 @@
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_IS_DROPPED = "ROOM_PARENT_INDEX_IS_DROPPED";
+ field public static final String MISMATCHED_GETTER = "ROOM_MISMATCHED_GETTER_TYPE";
+ field public static final String MISMATCHED_SETTER = "ROOM_MISMATCHED_SETTER_TYPE";
field public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD = "ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX";
field public static final String MISSING_JAVA_TMP_DIR = "ROOM_MISSING_JAVA_TMP_DIR";
field public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
diff --git a/room/common/api/public_plus_experimental_2.3.0-alpha01.txt b/room/common/api/public_plus_experimental_2.3.0-alpha01.txt
index 238967d..4e032bd 100644
--- a/room/common/api/public_plus_experimental_2.3.0-alpha01.txt
+++ b/room/common/api/public_plus_experimental_2.3.0-alpha01.txt
@@ -167,6 +167,8 @@
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_IS_DROPPED = "ROOM_PARENT_INDEX_IS_DROPPED";
+ field public static final String MISMATCHED_GETTER = "ROOM_MISMATCHED_GETTER_TYPE";
+ field public static final String MISMATCHED_SETTER = "ROOM_MISMATCHED_SETTER_TYPE";
field public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD = "ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX";
field public static final String MISSING_JAVA_TMP_DIR = "ROOM_MISSING_JAVA_TMP_DIR";
field public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
diff --git a/room/common/api/public_plus_experimental_current.txt b/room/common/api/public_plus_experimental_current.txt
index 238967d..4e032bd 100644
--- a/room/common/api/public_plus_experimental_current.txt
+++ b/room/common/api/public_plus_experimental_current.txt
@@ -167,6 +167,8 @@
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_IS_DROPPED = "ROOM_PARENT_INDEX_IS_DROPPED";
+ field public static final String MISMATCHED_GETTER = "ROOM_MISMATCHED_GETTER_TYPE";
+ field public static final String MISMATCHED_SETTER = "ROOM_MISMATCHED_SETTER_TYPE";
field public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD = "ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX";
field public static final String MISSING_JAVA_TMP_DIR = "ROOM_MISSING_JAVA_TMP_DIR";
field public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
diff --git a/room/common/api/restricted_2.3.0-alpha01.txt b/room/common/api/restricted_2.3.0-alpha01.txt
index ed6c4ee..a2a0510 100644
--- a/room/common/api/restricted_2.3.0-alpha01.txt
+++ b/room/common/api/restricted_2.3.0-alpha01.txt
@@ -176,6 +176,8 @@
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_IS_DROPPED = "ROOM_PARENT_INDEX_IS_DROPPED";
+ field public static final String MISMATCHED_GETTER = "ROOM_MISMATCHED_GETTER_TYPE";
+ field public static final String MISMATCHED_SETTER = "ROOM_MISMATCHED_SETTER_TYPE";
field public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD = "ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX";
field public static final String MISSING_JAVA_TMP_DIR = "ROOM_MISSING_JAVA_TMP_DIR";
field public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
diff --git a/room/common/api/restricted_current.txt b/room/common/api/restricted_current.txt
index ed6c4ee..a2a0510 100644
--- a/room/common/api/restricted_current.txt
+++ b/room/common/api/restricted_current.txt
@@ -176,6 +176,8 @@
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_IS_DROPPED = "ROOM_PARENT_INDEX_IS_DROPPED";
+ field public static final String MISMATCHED_GETTER = "ROOM_MISMATCHED_GETTER_TYPE";
+ field public static final String MISMATCHED_SETTER = "ROOM_MISMATCHED_SETTER_TYPE";
field public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD = "ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX";
field public static final String MISSING_JAVA_TMP_DIR = "ROOM_MISSING_JAVA_TMP_DIR";
field public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
diff --git a/room/common/src/main/java/androidx/room/RoomWarnings.java b/room/common/src/main/java/androidx/room/RoomWarnings.java
index e8ad213..66cc1b8 100644
--- a/room/common/src/main/java/androidx/room/RoomWarnings.java
+++ b/room/common/src/main/java/androidx/room/RoomWarnings.java
@@ -134,6 +134,47 @@
public static final String RELATION_QUERY_WITHOUT_TRANSACTION =
"ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
+ /**
+ * Reported when an `@Entity` field's type do not exactly match the getter type.
+ * For instance, in the following class:
+ * <pre>
+ * {@code @}Entity
+ * class Foo {
+ * ...
+ * private Boolean value;
+ * public boolean getValue() {
+ * return value == null ? false : value;
+ * }
+ * }
+ * </pre>
+ *
+ * Trying to insert this entity into database will always set {@code value} column to
+ * {@code false} when {@code Foo.value} is {@code null} since Room will use the {@code getValue}
+ * method to read the value. So even thought the database column is nullable, it will never
+ * be inserted as {@code null} if inserted as a {@code Foo} instance.
+ */
+ public static final String MISMATCHED_GETTER = "ROOM_MISMATCHED_GETTER_TYPE";
+
+ /**
+ * Reported when an `@Entity` field's type do not exactly match the setter type.
+ * For instance, in the following class:
+ * <pre>
+ * {@code @}Entity
+ * class Foo {
+ * ...
+ * private Boolean value;
+ * public void setValue(boolean value) {
+ * this.value = value;
+ * }
+ * }
+ * </pre>
+ *
+ * If Room reads this entity from the database, it will always set {@code Foo.value} to
+ * {@code false} when the column value is {@code null} since Room will use the {@code setValue}
+ * method to write the value.
+ */
+ public static final String MISMATCHED_SETTER = "ROOM_MISMATCHED_SETTER_TYPE";
+
/** @deprecated This type should not be instantiated as it contains only static methods. */
@Deprecated
@SuppressWarnings("PrivateConstructorForUtilityClass")
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
index 235a457..745c40f 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
@@ -718,6 +718,23 @@
context.checker.check(
success || bindingScope == FieldProcessor.BindingScope.READ_FROM_CURSOR,
field.element, CANNOT_FIND_GETTER_FOR_FIELD)
+ if (success && !context.processingEnv.typeUtils.isSameType(field.getter.type, field.type)) {
+ // getter's parameter type is not exactly the same as the field type.
+ // put a warning and update the value statement binder.
+ context.logger.w(
+ warning = Warning.MISMATCHED_GETTER_TYPE,
+ element = field.element,
+ msg = ProcessorErrors.mismatchedGetter(
+ fieldName = field.name,
+ ownerType = element.asType().typeName(),
+ getterType = field.getter.type.typeName(),
+ fieldType = field.typeName
+ ))
+ field.statementBinder = context.typeAdapterStore.findStatementValueBinder(
+ input = field.getter.type,
+ affinity = field.affinity
+ )
+ }
}
private fun assignSetters(
@@ -769,6 +786,23 @@
context.checker.check(
success || bindingScope == FieldProcessor.BindingScope.BIND_TO_STMT,
field.element, CANNOT_FIND_SETTER_FOR_FIELD)
+ if (success && !context.processingEnv.typeUtils.isSameType(field.setter.type, field.type)) {
+ // setter's parameter type is not exactly the same as the field type.
+ // put a warning and update the value reader adapter.
+ context.logger.w(
+ warning = Warning.MISMATCHED_SETTER_TYPE,
+ element = field.element,
+ msg = ProcessorErrors.mismatchedSetter(
+ fieldName = field.name,
+ ownerType = element.asType().typeName(),
+ setterType = field.setter.type.typeName(),
+ fieldType = field.typeName
+ ))
+ field.cursorValueReader = context.typeAdapterStore.findCursorValueReader(
+ output = field.setter.type,
+ affinity = field.affinity
+ )
+ }
}
/**
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index d331aa1..d5b85f4 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -730,4 +730,26 @@
fun invalidChannelType(typeName: String) = "'$typeName' is not supported as a return type. " +
"Instead declare return type as ${KotlinTypeNames.FLOW} and use Flow transforming " +
"functions that converts the Flow into a Channel."
+
+ fun mismatchedGetter(
+ fieldName: String,
+ ownerType: TypeName,
+ getterType: TypeName,
+ fieldType: TypeName
+ ) = """
+ $ownerType's $fieldName field has type $fieldType but its getter returns $getterType.
+ This mismatch might cause unexpected $fieldName values in the database when $ownerType
+ is inserted into database.
+ """.trim()
+
+ fun mismatchedSetter(
+ fieldName: String,
+ ownerType: TypeName,
+ setterType: TypeName,
+ fieldType: TypeName
+ ) = """
+ $ownerType's $fieldName field has type $fieldType but its setter accepts $setterType.
+ This mismatch might cause unexpected $fieldName values when $ownerType is read from the
+ database.
+ """.trim()
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
index 0abd15e9..f959979 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
@@ -36,7 +36,9 @@
DEFAULT_CONSTRUCTOR("ROOM_DEFAULT_CONSTRUCTOR"),
MISSING_COPY_ANNOTATIONS("MISSING_COPY_ANNOTATIONS"),
MISSING_INDEX_ON_JUNCTION("MISSING_INDEX_ON_JUNCTION"),
- JDK_VERSION_HAS_BUG("JDK_VERSION_HAS_BUG");
+ JDK_VERSION_HAS_BUG("JDK_VERSION_HAS_BUG"),
+ MISMATCHED_GETTER_TYPE("ROOM_MISMATCHED_GETTER_TYPE"),
+ MISMATCHED_SETTER_TYPE("ROOM_MISMATCHED_SETTER_TYPE");
companion object {
val PUBLIC_KEY_MAP = Warning.values().associateBy { it.publicKey }
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
index 448ab8f..4ffb54a 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
@@ -189,7 +189,10 @@
val constructorFields = node.directFields.filter {
it.field.setter.callType == CallType.CONSTRUCTOR
}.associateBy { fwi ->
- FieldReadWriteWriter(fwi).readIntoTmpVar(cursorVar, scope)
+ FieldReadWriteWriter(fwi).readIntoTmpVar(
+ cursorVar,
+ fwi.field.setter.type.typeName(),
+ scope)
}
// read decomposed fields (e.g. embedded)
node.subNodes.forEach(::visitNode)
@@ -349,9 +352,12 @@
/**
* Reads the value into a temporary local variable.
*/
- fun readIntoTmpVar(cursorVar: String, scope: CodeGenScope): String {
+ fun readIntoTmpVar(
+ cursorVar: String,
+ typeName: TypeName,
+ scope: CodeGenScope
+ ): String {
val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
- val typeName = field.getter.type.typeName()
scope.builder().apply {
addStatement("final $T $L", typeName, tmpField)
if (alwaysExists) {
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
index b9ec119..f9b4038 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
@@ -17,6 +17,7 @@
package androidx.room.processor
import COMMON
+import androidx.room.ext.typeName
import androidx.room.parser.SQLTypeAffinity
import androidx.room.processor.ProcessorErrors.RELATION_IN_ENTITY
import androidx.room.vo.CallType
@@ -28,6 +29,8 @@
import androidx.room.vo.Pojo
import androidx.room.vo.columnNames
import com.google.testing.compile.JavaFileObjects
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
import compileLibrarySource
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.hasItems
@@ -35,6 +38,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import java.lang.AssertionError
import javax.lang.model.type.TypeKind.INT
@RunWith(JUnit4::class)
@@ -141,6 +145,46 @@
}
@Test
+ fun setterWithAssignableType_2() {
+ singleEntity("""
+ @PrimaryKey
+ private Integer id;
+ public Integer getId() {return id;}
+ public void setId(int id) {}
+ """) { entity, invocation ->
+ val idField = entity.fields.first()
+ val cursorValueReader = idField.cursorValueReader
+ ?: throw AssertionError("must have a cursor value reader")
+ assertThat(cursorValueReader.typeMirror().typeName(),
+ `is`(invocation.typeUtils.getPrimitiveType(INT).typeName()))
+ }.compilesWithoutError()
+ .withWarningContaining(
+ ProcessorErrors.mismatchedSetter(
+ fieldName = "id",
+ ownerType = ClassName.bestGuess("foo.bar.MyEntity"),
+ setterType = TypeName.INT,
+ fieldType = TypeName.INT.box()
+ )
+ )
+ }
+
+ @Test
+ fun getterWithAssignableType_2() {
+ singleEntity("""
+ @PrimaryKey
+ private Integer id;
+ public int getId() {return id == null ? 0 : id;}
+ public void setId(Integer id) {}
+ """) { entity, invocation ->
+ val idField = entity.fields.first()
+ val statementBinder = idField.statementBinder
+ ?: throw AssertionError("must have a statement binder")
+ assertThat(statementBinder.typeMirror().typeName(),
+ `is`(invocation.typeUtils.getPrimitiveType(INT).typeName()))
+ }.compilesWithoutError()
+ }
+
+ @Test
fun noSetter() {
singleEntity("""
@PrimaryKey
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index 7e0f759..5f24684 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -75,6 +75,7 @@
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(ESPRESSO_CORE)
+ androidTestImplementation(TRUTH)
androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/BoxedPrimitivesTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/BoxedPrimitivesTest.java
new file mode 100644
index 0000000..fd2afd2
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/BoxedPrimitivesTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2019 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 androidx.room.integration.testapp.test;
+
+import static androidx.room.integration.testapp.test.BoxedPrimitivesTest.BaseBoxed.DEFAULT_BOOLEAN_VALUE;
+import static androidx.room.integration.testapp.test.BoxedPrimitivesTest.BaseBoxed.DEFAULT_NUMBER_VALUE;
+
+import android.content.Context;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class BoxedPrimitivesTest {
+ private BoxingTestDatabase mDb;
+
+ @Before
+ public void initDb() {
+ Context context = ApplicationProvider.getApplicationContext();
+ mDb = Room.inMemoryDatabaseBuilder(
+ context,
+ BoxingTestDatabase.class)
+ .build();
+ }
+
+ @After
+ public void closeDb() {
+ mDb.close();
+ }
+
+ @Test
+ public void unboxedConstructor() {
+ test(mDb.constructor());
+ }
+
+ @Test
+ public void unboxedGetter() {
+ test(mDb.field());
+ }
+
+ /**
+ * Assert that if a nullable pojo is provided, we can actually insert/read null
+ */
+ private void test(BaseDao<? extends BaseBoxed> dao) {
+ long rowId = dao.insert(new BoxedBooleanHolder(null, null));
+ BaseBoxed read = dao.find(rowId);
+ Truth.assertThat(read.mFlag).isNull();
+ Truth.assertThat(read.mNumber).isNull();
+ }
+
+ @Test
+ public void testInsert_constructor() {
+ long rowId = mDb.constructor().insertTyped(new ConstructorEntity(null, null));
+ testInsertedAsEntity(mDb.constructor(), rowId);
+ }
+
+ @Test
+ public void testInsert_field() {
+ long rowId = mDb.field().insertTyped(new FieldEntity());
+ testInsertedAsEntity(mDb.field(), rowId);
+ }
+
+ /**
+ * assert the case where row was inserted via entity hence we should've read the default values
+ * already.
+ **/
+ private void testInsertedAsEntity(BaseDao<? extends BaseBoxed> dao, long rowId) {
+ BaseBoxed read = dao.find(rowId);
+ // default getter value
+ Truth.assertThat(read.mFlag).isEqualTo(DEFAULT_BOOLEAN_VALUE);
+ Truth.assertThat(read.mNumber).isEqualTo(DEFAULT_NUMBER_VALUE);
+ }
+
+ static class BaseBoxed {
+ @ColumnInfo(name = "boxed_bool")
+ Boolean mFlag;
+ @ColumnInfo(name = "boxed_int")
+ Integer mNumber;
+
+ static final boolean DEFAULT_BOOLEAN_VALUE = true;
+ static final int DEFAULT_NUMBER_VALUE = 41;
+
+ @SuppressWarnings("unused")
+ boolean getFlag() {
+ return mFlag == null ? DEFAULT_BOOLEAN_VALUE : mFlag;
+ }
+
+ @SuppressWarnings("unused")
+ int getNumber() {
+ return mNumber == null ? DEFAULT_NUMBER_VALUE : mNumber;
+ }
+ }
+
+ @Entity
+ static class ConstructorEntity extends BaseBoxed {
+ @PrimaryKey(autoGenerate = true)
+ public long rowId = 0;
+
+ ConstructorEntity(Boolean flag, Integer number) {
+ this.mFlag = flag;
+ this.mNumber = number;
+ }
+ }
+
+ @Entity
+ static class FieldEntity extends BaseBoxed {
+ @PrimaryKey(autoGenerate = true)
+ public long rowId = 0;
+
+ void setBoxed(Boolean boxed) {
+ this.mFlag = boxed;
+ }
+
+ void setNumber(Integer number) {
+ this.mNumber = number;
+ }
+ }
+
+ static class BoxedBooleanHolder {
+ @ColumnInfo(name = "boxed_bool")
+ final Boolean mFlag;
+ @ColumnInfo(name = "boxed_int")
+ final Integer mNumber;
+
+ BoxedBooleanHolder(Boolean flag, Integer number) {
+ mFlag = flag;
+ mNumber = number;
+ }
+ }
+
+ interface BaseDao<T> {
+ long insert(BoxedBooleanHolder t);
+
+ @Insert
+ long insertTyped(T t);
+
+ T find(long rowId);
+ }
+
+ @Dao
+ interface BoxedConstructorDao extends BaseDao<ConstructorEntity> {
+ @Override
+ @Insert(entity = ConstructorEntity.class)
+ long insert(BoxedBooleanHolder item);
+
+ @Override
+ @Query("SELECT * FROM ConstructorEntity WHERE rowId = :rowId")
+ ConstructorEntity find(long rowId);
+ }
+
+ @Dao
+ interface BoxedFieldDao extends BaseDao<FieldEntity> {
+
+ @Insert(entity = FieldEntity.class)
+ long insert(BoxedBooleanHolder item);
+
+ @Override
+ @Query("SELECT * FROM FieldEntity WHERE rowId = :rowId")
+ FieldEntity find(long rowId);
+ }
+
+ @Database(
+ entities = {FieldEntity.class, ConstructorEntity.class},
+ version = 1,
+ exportSchema = false
+ )
+ abstract static class BoxingTestDatabase extends RoomDatabase {
+ abstract BoxedConstructorDao constructor();
+
+ abstract BoxedFieldDao field();
+ }
+}