diff --git a/CHANGELOG.md b/CHANGELOG.md index 771001e515..ebb49d0592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ## Enhancements +* Added support for primitive lists in migrations using `RealmObjectSchema.addRealmListField(String name, Class type)` (#5329). * Now users can use `String`, `byte[]`, `Boolean`, `Long`, `Integer`, `Short`, `Byte`, `Double`, `Float` and `Date` as a type parameter of `RealmList`. ## Bug Fixes diff --git a/realm/realm-library/src/androidTest/java/io/realm/DynamicRealmObjectTests.java b/realm/realm-library/src/androidTest/java/io/realm/DynamicRealmObjectTests.java index c87b63b0ce..3ac4e327db 100644 --- a/realm/realm-library/src/androidTest/java/io/realm/DynamicRealmObjectTests.java +++ b/realm/realm-library/src/androidTest/java/io/realm/DynamicRealmObjectTests.java @@ -95,6 +95,13 @@ public void setUp() { typedObj.setFieldDate(new Date(1000)); typedObj.setFieldObject(typedObj); typedObj.getFieldList().add(typedObj); + typedObj.getFieldIntegerList().add(1); + typedObj.getFieldStringList().add("str"); + typedObj.getFieldBooleanList().add(true); + typedObj.getFieldFloatList().add(1.23F); + typedObj.getFieldDoubleList().add(1.234D); + typedObj.getFieldBinaryList().add(new byte[] {1, 2, 3}); + typedObj.getFieldDateList().add(new Date(1000)); dObjTyped = new DynamicRealmObject(typedObj); realm.commitTransaction(); @@ -114,15 +121,16 @@ public void tearDown() { // Types supported by the DynamicRealmObject. private enum SupportedType { - BOOLEAN, SHORT, INT, LONG, BYTE, FLOAT, DOUBLE, STRING, BINARY, DATE, OBJECT, LIST + BOOLEAN, SHORT, INT, LONG, BYTE, FLOAT, DOUBLE, STRING, BINARY, DATE, OBJECT, LIST, + LIST_INTEGER, LIST_STRING, LIST_BOOLEAN, LIST_FLOAT, LIST_DOUBLE, LIST_BINARY, LIST_DATE } private enum ThreadConfinedMethods { GET_BOOLEAN, GET_BYTE, GET_SHORT, GET_INT, GET_LONG, GET_FLOAT, GET_DOUBLE, - GET_BLOB, GET_STRING, GET_DATE, GET_OBJECT, GET_LIST, GET, + GET_BLOB, GET_STRING, GET_DATE, GET_OBJECT, GET_LIST, GET_PRIMITIVE_LIST, GET, SET_BOOLEAN, SET_BYTE, SET_SHORT, SET_INT, SET_LONG, SET_FLOAT, SET_DOUBLE, - SET_BLOB, SET_STRING, SET_DATE, SET_OBJECT, SET_LIST, SET, + SET_BLOB, SET_STRING, SET_DATE, SET_OBJECT, SET_LIST, SET_PRIMITIVE_LIST, SET, IS_NULL, SET_NULL, @@ -134,33 +142,35 @@ private enum ThreadConfinedMethods { @SuppressWarnings({"ResultOfMethodCallIgnored", "EqualsWithItself", "SelfEquals"}) private static void callThreadConfinedMethod(DynamicRealmObject obj, ThreadConfinedMethods method) { switch (method) { - case GET_BOOLEAN: obj.getBoolean(AllJavaTypes.FIELD_BOOLEAN); break; - case GET_BYTE: obj.getByte(AllJavaTypes.FIELD_BYTE); break; - case GET_SHORT: obj.getShort(AllJavaTypes.FIELD_SHORT); break; - case GET_INT: obj.getInt(AllJavaTypes.FIELD_INT); break; - case GET_LONG: obj.getLong(AllJavaTypes.FIELD_LONG); break; - case GET_FLOAT: obj.getFloat(AllJavaTypes.FIELD_FLOAT); break; - case GET_DOUBLE: obj.getDouble(AllJavaTypes.FIELD_DOUBLE); break; - case GET_BLOB: obj.getBlob(AllJavaTypes.FIELD_BINARY); break; - case GET_STRING: obj.getString(AllJavaTypes.FIELD_STRING); break; - case GET_DATE: obj.getDate(AllJavaTypes.FIELD_DATE); break; - case GET_OBJECT: obj.getObject(AllJavaTypes.FIELD_OBJECT); break; - case GET_LIST: obj.getList(AllJavaTypes.FIELD_LIST); break; - case GET: obj.get(AllJavaTypes.FIELD_LONG); break; - - case SET_BOOLEAN: obj.setBoolean(AllJavaTypes.FIELD_BOOLEAN, true); break; - case SET_BYTE: obj.setByte(AllJavaTypes.FIELD_BYTE, (byte) 1); break; - case SET_SHORT: obj.setShort(AllJavaTypes.FIELD_SHORT, (short) 1); break; - case SET_INT: obj.setInt(AllJavaTypes.FIELD_INT, 1); break; - case SET_LONG: obj.setLong(AllJavaTypes.FIELD_LONG, 1L); break; - case SET_FLOAT: obj.setFloat(AllJavaTypes.FIELD_FLOAT, 1F); break; - case SET_DOUBLE: obj.setDouble(AllJavaTypes.FIELD_DOUBLE, 1D); break; - case SET_BLOB: obj.setBlob(AllJavaTypes.FIELD_BINARY, new byte[] {1, 2, 3}); break; - case SET_STRING: obj.setString(AllJavaTypes.FIELD_STRING, "12345"); break; - case SET_DATE: obj.setDate(AllJavaTypes.FIELD_DATE, new Date(1L)); break; - case SET_OBJECT: obj.setObject(AllJavaTypes.FIELD_OBJECT, obj); break; - case SET_LIST: obj.setList(AllJavaTypes.FIELD_LIST, new RealmList<>(obj)); break; - case SET: obj.set(AllJavaTypes.FIELD_LONG, 1L); break; + case GET_BOOLEAN: obj.getBoolean(AllJavaTypes.FIELD_BOOLEAN); break; + case GET_BYTE: obj.getByte(AllJavaTypes.FIELD_BYTE); break; + case GET_SHORT: obj.getShort(AllJavaTypes.FIELD_SHORT); break; + case GET_INT: obj.getInt(AllJavaTypes.FIELD_INT); break; + case GET_LONG: obj.getLong(AllJavaTypes.FIELD_LONG); break; + case GET_FLOAT: obj.getFloat(AllJavaTypes.FIELD_FLOAT); break; + case GET_DOUBLE: obj.getDouble(AllJavaTypes.FIELD_DOUBLE); break; + case GET_BLOB: obj.getBlob(AllJavaTypes.FIELD_BINARY); break; + case GET_STRING: obj.getString(AllJavaTypes.FIELD_STRING); break; + case GET_DATE: obj.getDate(AllJavaTypes.FIELD_DATE); break; + case GET_OBJECT: obj.getObject(AllJavaTypes.FIELD_OBJECT); break; + case GET_LIST: obj.getList(AllJavaTypes.FIELD_LIST); break; + case GET_PRIMITIVE_LIST: obj.getList(AllJavaTypes.FIELD_STRING_LIST, String.class); break; + case GET: obj.get(AllJavaTypes.FIELD_LONG); break; + + case SET_BOOLEAN: obj.setBoolean(AllJavaTypes.FIELD_BOOLEAN, true); break; + case SET_BYTE: obj.setByte(AllJavaTypes.FIELD_BYTE, (byte) 1); break; + case SET_SHORT: obj.setShort(AllJavaTypes.FIELD_SHORT, (short) 1); break; + case SET_INT: obj.setInt(AllJavaTypes.FIELD_INT, 1); break; + case SET_LONG: obj.setLong(AllJavaTypes.FIELD_LONG, 1L); break; + case SET_FLOAT: obj.setFloat(AllJavaTypes.FIELD_FLOAT, 1F); break; + case SET_DOUBLE: obj.setDouble(AllJavaTypes.FIELD_DOUBLE, 1D); break; + case SET_BLOB: obj.setBlob(AllJavaTypes.FIELD_BINARY, new byte[] {1, 2, 3}); break; + case SET_STRING: obj.setString(AllJavaTypes.FIELD_STRING, "12345"); break; + case SET_DATE: obj.setDate(AllJavaTypes.FIELD_DATE, new Date(1L)); break; + case SET_OBJECT: obj.setObject(AllJavaTypes.FIELD_OBJECT, obj); break; + case SET_LIST: obj.setList(AllJavaTypes.FIELD_LIST, new RealmList<>(obj)); break; + case SET_PRIMITIVE_LIST: obj.setList(AllJavaTypes.FIELD_STRING_LIST,new RealmList("foo")); break; + case SET: obj.set(AllJavaTypes.FIELD_LONG, 1L); break; case IS_NULL: obj.isNull(AllJavaTypes.FIELD_OBJECT); break; case SET_NULL: obj.setNull(AllJavaTypes.FIELD_OBJECT); break; @@ -326,7 +336,16 @@ private static void callGetter(DynamicRealmObject target, SupportedType type, Li case BINARY: target.getBlob(fieldName); break; case DATE: target.getDate(fieldName); break; case OBJECT: target.getObject(fieldName); break; - case LIST: target.getList(fieldName); break; + case LIST: + case LIST_INTEGER: + case LIST_STRING: + case LIST_BOOLEAN: + case LIST_FLOAT: + case LIST_DOUBLE: + case LIST_BINARY: + case LIST_DATE: + target.getList(fieldName); + break; default: fail(); } @@ -450,6 +469,13 @@ private static void callSetter(DynamicRealmObject target, SupportedType type, Li case DATE: target.getDate(fieldName); break; case OBJECT: target.setObject(fieldName, null); target.setObject(fieldName, target); break; case LIST: target.setList(fieldName, new RealmList()); break; + case LIST_INTEGER: target.setList(fieldName, new RealmList(1)); break; + case LIST_STRING: target.setList(fieldName, new RealmList("foo")); break; + case LIST_BOOLEAN: target.setList(fieldName, new RealmList(true)); break; + case LIST_FLOAT: target.setList(fieldName, new RealmList(1.23F)); break; + case LIST_DOUBLE: target.setList(fieldName, new RealmList(1.234D)); break; + case LIST_BINARY: target.setList(fieldName, new RealmList(new byte[]{})); break; + case LIST_DATE: target.setList(fieldName, new RealmList(new Date())); break; default: fail(); } @@ -509,6 +535,27 @@ public void typedGettersAndSetters() { dObj.setObject(AllJavaTypes.FIELD_OBJECT, dObj); assertEquals(dObj, dObj.getObject(AllJavaTypes.FIELD_OBJECT)); break; + case LIST_INTEGER: + checkSetGetValueList(dObj, AllJavaTypes.FIELD_INTEGER_LIST, Integer.class, new RealmList<>(null, 1)); + break; + case LIST_STRING: + checkSetGetValueList(dObj, AllJavaTypes.FIELD_STRING_LIST, String.class, new RealmList<>(null, "foo")); + break; + case LIST_BOOLEAN: + checkSetGetValueList(dObj, AllJavaTypes.FIELD_BOOLEAN_LIST, Boolean.class, new RealmList<>(null, true)); + break; + case LIST_FLOAT: + checkSetGetValueList(dObj, AllJavaTypes.FIELD_FLOAT_LIST, Float.class, new RealmList<>(null, 1.23F)); + break; + case LIST_DOUBLE: + checkSetGetValueList(dObj, AllJavaTypes.FIELD_DOUBLE_LIST, Double.class, new RealmList<>(null, 1.234D)); + break; + case LIST_BINARY: + checkSetGetValueList(dObj, AllJavaTypes.FIELD_BINARY_LIST, byte[].class, new RealmList<>(null, new byte[] {1, 2, 3})); + break; + case LIST_DATE: + checkSetGetValueList(dObj, AllJavaTypes.FIELD_DATE_LIST, Date.class, new RealmList<>(null, new Date(1000))); + break; case LIST: // Ignores. See testGetList/testSetList. break; @@ -521,6 +568,11 @@ public void typedGettersAndSetters() { } } + private void checkSetGetValueList(DynamicRealmObject obj, String fieldName, Class primitiveType, RealmList list) { + obj.set(fieldName, list); + assertArrayEquals(list.toArray(), obj.getList(fieldName, primitiveType).toArray()); + } + @Test public void setter_null() { realm.beginTransaction(); @@ -545,6 +597,55 @@ public void setter_null() { } catch (IllegalArgumentException ignored) { } break; + case LIST_INTEGER: + try { + dObj.setNull(NullTypes.FIELD_INTEGER_LIST_NULL); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_STRING: + try { + dObj.setNull(NullTypes.FIELD_STRING_LIST_NULL); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_BOOLEAN: + try { + dObj.setNull(NullTypes.FIELD_BOOLEAN_LIST_NULL); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_FLOAT: + try { + dObj.setNull(NullTypes.FIELD_FLOAT_LIST_NULL); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_DOUBLE: + try { + dObj.setNull(NullTypes.FIELD_DOUBLE_LIST_NULL); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_BINARY: + try { + dObj.setNull(NullTypes.FIELD_BINARY_LIST_NULL); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_DATE: + try { + dObj.setNull(NullTypes.FIELD_DATE_LIST_NULL); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; case BOOLEAN: dObj.setNull(NullTypes.FIELD_BOOLEAN_NULL); assertTrue(dObj.isNull(NullTypes.FIELD_BOOLEAN_NULL)); @@ -606,6 +707,13 @@ public void setter_nullOnRequiredFieldsThrows() { switch (type) { case OBJECT: continue; // Ignore case LIST: fieldName = NullTypes.FIELD_LIST_NULL; break; + case LIST_INTEGER: fieldName = NullTypes.FIELD_INTEGER_LIST_NULL; break; + case LIST_STRING: fieldName = NullTypes.FIELD_STRING_LIST_NULL; break; + case LIST_BOOLEAN: fieldName = NullTypes.FIELD_BOOLEAN_LIST_NULL; break; + case LIST_FLOAT: fieldName = NullTypes.FIELD_FLOAT_LIST_NULL; break; + case LIST_DOUBLE: fieldName = NullTypes.FIELD_DATE_LIST_NULL; break; + case LIST_BINARY: fieldName = NullTypes.FIELD_BINARY_LIST_NULL; break; + case LIST_DATE: fieldName = NullTypes.FIELD_DATE_LIST_NULL; break; case BOOLEAN: fieldName = NullTypes.FIELD_BOOLEAN_NOT_NULL; break; case BYTE: fieldName = NullTypes.FIELD_BYTE_NOT_NULL; break; case SHORT: fieldName = NullTypes.FIELD_SHORT_NOT_NULL; break; @@ -651,6 +759,55 @@ public void typedSetter_null() { } catch (IllegalArgumentException ignored) { } break; + case LIST_INTEGER: + try { + dObj.setList(NullTypes.FIELD_INTEGER_LIST_NULL, null); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_STRING: + try { + dObj.setList(NullTypes.FIELD_STRING_LIST_NULL, null); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_BOOLEAN: + try { + dObj.setList(NullTypes.FIELD_BOOLEAN_LIST_NULL, null); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_FLOAT: + try { + dObj.setList(NullTypes.FIELD_FLOAT_LIST_NULL, null); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_DOUBLE: + try { + dObj.setList(NullTypes.FIELD_DOUBLE_LIST_NULL, null); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_BINARY: + try { + dObj.setList(NullTypes.FIELD_BINARY_LIST_NULL, null); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; + case LIST_DATE: + try { + dObj.setList(NullTypes.FIELD_DATE_LIST_NULL, null); + fail(); + } catch (IllegalArgumentException ignored) { + } + break; case DATE: dObj.setDate(NullTypes.FIELD_DATE_NULL, null); assertNull(dObj.getDate(NullTypes.FIELD_DATE_NULL)); @@ -885,6 +1042,32 @@ public void setList_wrongTypeThrows() { dObjTyped.setList(AllJavaTypes.FIELD_LIST, wrongDynamicList); } + @Test + public void setList_javaModelClassesThrowProperErrorMessage() { + dynamicRealm.beginTransaction(); + try { + dObjDynamic.setList(AllJavaTypes.FIELD_LIST, new RealmList<>(typedObj)); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("RealmList must contain `DynamicRealmObject's, not Java model classes.")); + } + } + + @Test + public void setList_objectsOwnList() { + dynamicRealm.beginTransaction(); + + // Test model classes + int originalSize = dObjDynamic.getList(AllJavaTypes.FIELD_LIST).size(); + dObjDynamic.setList(AllJavaTypes.FIELD_LIST, dObjDynamic.getList(AllJavaTypes.FIELD_LIST)); + assertEquals(originalSize, dObjDynamic.getList(AllJavaTypes.FIELD_LIST).size()); + + // Smoke test value lists + originalSize = dObjDynamic.getList(AllJavaTypes.FIELD_STRING_LIST, String.class).size(); + dObjDynamic.setList(AllJavaTypes.FIELD_STRING_LIST, dObjDynamic.getList(AllJavaTypes.FIELD_STRING_LIST, String.class)); + assertEquals(originalSize, dObjDynamic.getList(AllJavaTypes.FIELD_STRING_LIST, String.class).size()); + } + @Test public void untypedSetter_listWrongTypeThrows() { realm.beginTransaction(); @@ -925,6 +1108,7 @@ public void getList() { assertEquals("fido", listObject.getString(Dog.FIELD_NAME)); } + @Test public void untypedGetterSetter() { realm.beginTransaction(); @@ -977,7 +1161,7 @@ public void untypedGetterSetter() { dObj.set(AllJavaTypes.FIELD_OBJECT, dObj); assertEquals(dObj, dObj.get(AllJavaTypes.FIELD_OBJECT)); break; - case LIST: + case LIST: { RealmList newList = new RealmList(); newList.add(dObj); dObj.set(AllJavaTypes.FIELD_LIST, newList); @@ -985,6 +1169,63 @@ public void untypedGetterSetter() { assertEquals(1, list.size()); assertEquals(dObj, list.get(0)); break; + } + case LIST_INTEGER: { + RealmList newList = new RealmList<>(null, 1); + dObj.set(AllJavaTypes.FIELD_INTEGER_LIST, newList); + RealmList list = dObj.getList(AllJavaTypes.FIELD_INTEGER_LIST, Integer.class); + assertEquals(2, list.size()); + assertArrayEquals(newList.toArray(), list.toArray()); + break; + } + case LIST_STRING: { + RealmList newList = new RealmList<>(null, "Foo"); + dObj.set(AllJavaTypes.FIELD_STRING_LIST, newList); + RealmList list = dObj.getList(AllJavaTypes.FIELD_STRING_LIST, String.class); + assertEquals(2, list.size()); + assertArrayEquals(newList.toArray(), list.toArray()); + break; + } + case LIST_BOOLEAN: { + RealmList newList = new RealmList<>(null, true); + dObj.set(AllJavaTypes.FIELD_BOOLEAN_LIST, newList); + RealmList list = dObj.getList(AllJavaTypes.FIELD_BOOLEAN_LIST, Boolean.class); + assertEquals(2, list.size()); + assertArrayEquals(newList.toArray(), list.toArray()); + break; + } + case LIST_FLOAT: { + RealmList newList = new RealmList<>(null, 1.23F); + dObj.set(AllJavaTypes.FIELD_FLOAT_LIST, newList); + RealmList list = dObj.getList(AllJavaTypes.FIELD_FLOAT_LIST, Float.class); + assertEquals(2, list.size()); + assertArrayEquals(newList.toArray(), list.toArray()); + break; + } + case LIST_DOUBLE: { + RealmList newList = new RealmList<>(null, 1.24D); + dObj.set(AllJavaTypes.FIELD_DOUBLE_LIST, newList); + RealmList list = dObj.getList(AllJavaTypes.FIELD_DOUBLE_LIST, Double.class); + assertEquals(2, list.size()); + assertArrayEquals(newList.toArray(), list.toArray()); + break; + } + case LIST_BINARY: { + RealmList newList = new RealmList<>(null, new byte[] {1, 2, 3}); + dObj.set(AllJavaTypes.FIELD_BINARY_LIST, newList); + RealmList list = dObj.getList(AllJavaTypes.FIELD_BINARY_LIST, byte[].class); + assertEquals(2, list.size()); + assertArrayEquals(newList.toArray(), list.toArray()); + break; + } + case LIST_DATE: { + RealmList newList = new RealmList<>(null, new Date(1000)); + dObj.set(AllJavaTypes.FIELD_DATE_LIST, newList); + RealmList list = dObj.getList(AllJavaTypes.FIELD_DATE_LIST, Date.class); + assertEquals(2, list.size()); + assertArrayEquals(newList.toArray(), list.toArray()); + break; + } default: fail(); } @@ -1033,11 +1274,17 @@ public void untypedSetter_usingStringConversion() { // These types don't have a string representation that can be parsed. case OBJECT: case LIST: + case LIST_INTEGER: + case LIST_STRING: + case LIST_BOOLEAN: + case LIST_FLOAT: + case LIST_DOUBLE: + case LIST_BINARY: + case LIST_DATE: case STRING: case BINARY: case BYTE: break; - default: fail("Unknown type: " + type); break; @@ -1081,6 +1328,13 @@ public void untypedSetter_illegalImplicitConversionThrows() { case BYTE: case OBJECT: case LIST: + case LIST_INTEGER: + case LIST_STRING: + case LIST_BOOLEAN: + case LIST_FLOAT: + case LIST_DOUBLE: + case LIST_BINARY: + case LIST_DATE: case STRING: case BINARY: continue; @@ -1200,6 +1454,13 @@ public void getFieldType() { assertEquals(RealmFieldType.INTEGER, dObjTyped.getFieldType(AllJavaTypes.FIELD_SHORT)); assertEquals(RealmFieldType.INTEGER, dObjTyped.getFieldType(AllJavaTypes.FIELD_INT)); assertEquals(RealmFieldType.INTEGER, dObjTyped.getFieldType(AllJavaTypes.FIELD_LONG)); + assertEquals(RealmFieldType.INTEGER_LIST, dObjTyped.getFieldType(AllJavaTypes.FIELD_INTEGER_LIST)); + assertEquals(RealmFieldType.STRING_LIST, dObjTyped.getFieldType(AllJavaTypes.FIELD_STRING_LIST)); + assertEquals(RealmFieldType.BOOLEAN_LIST, dObjTyped.getFieldType(AllJavaTypes.FIELD_BOOLEAN_LIST)); + assertEquals(RealmFieldType.FLOAT_LIST, dObjTyped.getFieldType(AllJavaTypes.FIELD_FLOAT_LIST)); + assertEquals(RealmFieldType.DOUBLE_LIST, dObjTyped.getFieldType(AllJavaTypes.FIELD_DOUBLE_LIST)); + assertEquals(RealmFieldType.BINARY_LIST, dObjTyped.getFieldType(AllJavaTypes.FIELD_BINARY_LIST)); + assertEquals(RealmFieldType.DATE_LIST, dObjTyped.getFieldType(AllJavaTypes.FIELD_DATE_LIST)); } @Test @@ -1253,6 +1514,13 @@ public void toString_nullValues() { assertTrue(str.contains(NullTypes.FIELD_DATE_NULL + ":null")); assertTrue(str.contains(NullTypes.FIELD_OBJECT_NULL + ":null")); assertTrue(str.contains(NullTypes.FIELD_LIST_NULL + ":RealmList[0]")); + assertTrue(str.contains(NullTypes.FIELD_INTEGER_LIST_NULL + ":RealmList[0]")); + assertTrue(str.contains(NullTypes.FIELD_STRING_LIST_NULL + ":RealmList[0]")); + assertTrue(str.contains(NullTypes.FIELD_BOOLEAN_LIST_NULL + ":RealmList[0]")); + assertTrue(str.contains(NullTypes.FIELD_FLOAT_LIST_NULL + ":RealmList[0]")); + assertTrue(str.contains(NullTypes.FIELD_DOUBLE_LIST_NULL + ":RealmList[0]")); + assertTrue(str.contains(NullTypes.FIELD_BINARY_LIST_NULL + ":RealmList[0]")); + assertTrue(str.contains(NullTypes.FIELD_DATE_LIST_NULL + ":RealmList[0]")); } @Test diff --git a/realm/realm-library/src/androidTest/java/io/realm/RealmObjectSchemaTests.java b/realm/realm-library/src/androidTest/java/io/realm/RealmObjectSchemaTests.java index e77c0543db..6615bffa9b 100644 --- a/realm/realm-library/src/androidTest/java/io/realm/RealmObjectSchemaTests.java +++ b/realm/realm-library/src/androidTest/java/io/realm/RealmObjectSchemaTests.java @@ -19,7 +19,6 @@ import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -37,8 +36,10 @@ import io.realm.internal.Table; import io.realm.rule.TestRealmConfigurationFactory; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -107,6 +108,7 @@ public enum SchemaFieldType { SIMPLE, OBJECT, LIST } + // Enumerate all standard field types public enum FieldType { STRING(String.class, true), SHORT(Short.class, true), PRIMITIVE_SHORT(short.class, false), @@ -118,8 +120,7 @@ public enum FieldType { DOUBLE(Double.class, true), PRIMITIVE_DOUBLE(double.class, false), BLOB(byte[].class, true), DATE(Date.class, true), - OBJECT(RealmObject.class, false), - LIST(RealmList.class, false); + OBJECT(RealmObject.class, false); final Class clazz; final boolean defaultNullable; @@ -138,6 +139,37 @@ public boolean isNullable() { } } + // Enumerate all list types + public enum FieldListType { + STRING_LIST(String.class, true), + SHORT_LIST(Short.class, true), PRIMITIVE_SHORT_LIST(short.class, false), + INT_LIST(Integer.class, true), PRIMITIVE_INT_LIST(int.class, false), + LONG_LIST(Long.class, true), PRIMITIVE_LONG_LIST(long.class, false), + BYTE_LIST(Byte.class, true), PRIMITIVE_BYTE_LIST(byte.class, false), + BOOLEAN_LIST(Boolean.class, true), PRIMITIVE_BOOLEAN_LIST(boolean.class, false), + FLOAT_LIST(Float.class, true), PRIMITIVE_FLOAT_LIST(float.class, false), + DOUBLE_LIST(Double.class, true), PRIMITIVE_DOUBLE_LIST(double.class, false), + BLOB_LIST(byte[].class, true), + DATE_LIST(Date.class, true), + LIST(RealmList.class, false); // List of Realm Objects + + final Class clazz; + final boolean defaultNullable; + + FieldListType(Class clazz, boolean defaultNullable) { + this.clazz = clazz; + this.defaultNullable = defaultNullable; + } + + public Class getType() { + return clazz; + } + + public boolean isNullable() { + return defaultNullable; + } + } + public enum IndexFieldType { STRING(String.class, true), SHORT(Short.class, true), PRIMITIVE_SHORT(short.class, false), @@ -253,20 +285,28 @@ public void addRemoveField() { } return; } + String fieldName = "foo"; for (FieldType fieldType : FieldType.values()) { - String fieldName = "foo"; switch (fieldType) { case OBJECT: schema.addRealmObjectField(fieldName, DOG_SCHEMA); checkAddedAndRemovable(fieldName); break; + default: + // All simple fields + schema.addField(fieldName, fieldType.getType()); + checkAddedAndRemovable(fieldName); + } + } + for (FieldListType fieldType : FieldListType.values()) { + switch (fieldType) { case LIST: schema.addRealmListField(fieldName, DOG_SCHEMA); checkAddedAndRemovable(fieldName); break; default: - // All simple fields - schema.addField(fieldName, fieldType.getType()); + // All primitive lists + schema.addRealmListField(fieldName, fieldType.getType()); checkAddedAndRemovable(fieldName); } } @@ -354,11 +394,10 @@ public void requiredFieldAttribute() { if (type == ObjectSchemaType.IMMUTABLE) { return; } + String fieldName = "foo"; for (FieldType fieldType : FieldType.values()) { - String fieldName = "foo"; switch (fieldType) { case OBJECT: continue; // Not possible. - case LIST: continue; // Not possible. default: // All simple types schema.addField(fieldName, fieldType.getType(), FieldAttribute.REQUIRED); @@ -366,6 +405,20 @@ public void requiredFieldAttribute() { schema.removeField(fieldName); } } + for (FieldListType fieldType : FieldListType.values()) { + switch(fieldType) { + case LIST: + continue; // Not possible. + default: + // All simple list types + schema.addRealmListField(fieldName, fieldType.getType()); + if (fieldType.isNullable()) { + schema.setRequired(fieldName, true); + } + assertTrue(fieldName + " should be required", schema.isRequired(fieldName)); + schema.removeField(fieldName); + } + } } @Test @@ -397,6 +450,12 @@ public void invalidIndexedFieldAttributeThrows() { } catch (IllegalArgumentException ignored) { } } + + // Probe for all variants of primitive lists + try { + schema.addRealmListField("foo", String.class); + } catch (IllegalArgumentException ignored) { + } } @Test @@ -438,6 +497,17 @@ public void invalidPrimaryKeyFieldAttributeThrows() { } catch (IllegalArgumentException ignored) { } } + + try { + schema.addRealmListField("foo", schema); + } catch (IllegalArgumentException ignored) { + } + + // Probe for all variants of primitive lists + try { + schema.addRealmListField("foo", String.class); + } catch (IllegalArgumentException ignored) { + } } @Test @@ -544,14 +614,14 @@ public void addIndexFieldModifier_alreadyIndexedThrows() { } @Test - public void setRemoveNullable() { + public void setNullable_trueAndFalse() { if (type == ObjectSchemaType.IMMUTABLE) { thrown.expect(UnsupportedOperationException.class); schema.setNullable("test", true); return; } + String fieldName = "foo"; for (FieldType fieldType : FieldType.values()) { - String fieldName = "foo"; switch (fieldType) { case OBJECT: // Objects are always nullable and cannot be changed. @@ -563,6 +633,17 @@ public void setRemoveNullable() { } catch (IllegalArgumentException ignored) { } break; + default: + // All simple types. + schema.addField(fieldName, fieldType.getType()); + assertEquals(fieldType.isNullable(), schema.isNullable(fieldName)); + schema.setNullable(fieldName, !fieldType.isNullable()); + assertEquals(!fieldType.isNullable(), schema.isNullable(fieldName)); + } + schema.removeField(fieldName); + } + for (FieldListType fieldType : FieldListType.values()) { + switch (fieldType) { case LIST: // Lists are not nullable and cannot be configured to be so. schema.addRealmListField(fieldName, schema); @@ -574,25 +655,25 @@ public void setRemoveNullable() { } break; default: - // All simple types. - schema.addField(fieldName, fieldType.getType()); - assertEquals(fieldType.isNullable(), schema.isNullable(fieldName)); + // All simple list types. + schema.addRealmListField(fieldName, fieldType.getType()); + assertEquals("Type: " + fieldType, fieldType.isNullable(), schema.isNullable(fieldName)); schema.setNullable(fieldName, !fieldType.isNullable()); - assertEquals(!fieldType.isNullable(), schema.isNullable(fieldName)); + assertEquals("Type: " + fieldType, !fieldType.isNullable(), schema.isNullable(fieldName)); } schema.removeField(fieldName); } } @Test - public void setRemoveRequired() { + public void setRequired_trueAndFalse() { if (type == ObjectSchemaType.IMMUTABLE) { thrown.expect(UnsupportedOperationException.class); schema.setRequired("test", true); return; } + String fieldName = "foo"; for (FieldType fieldType : FieldType.values()) { - String fieldName = "foo"; switch (fieldType) { case OBJECT: // Objects are always nullable and cannot be configured otherwise. @@ -604,6 +685,17 @@ public void setRemoveRequired() { } catch (IllegalArgumentException ignored) { } break; + default: + // All simple types. + schema.addField(fieldName, fieldType.getType()); + assertEquals(!fieldType.isNullable(), schema.isRequired(fieldName)); + schema.setRequired(fieldName, fieldType.isNullable()); + assertEquals(fieldType.isNullable(), schema.isRequired(fieldName)); + } + schema.removeField(fieldName); + } + for (FieldListType fieldType : FieldListType.values()) { + switch (fieldType) { case LIST: // Lists are always non-nullable and cannot be configured otherwise. schema.addRealmListField(fieldName, schema); @@ -615,8 +707,8 @@ public void setRemoveRequired() { } break; default: - // All simple types. - schema.addField(fieldName, fieldType.getType()); + // All simple list types. + schema.addRealmListField(fieldName, fieldType.getType()); assertEquals(!fieldType.isNullable(), schema.isRequired(fieldName)); schema.setRequired(fieldName, fieldType.isNullable()); assertEquals(fieldType.isNullable(), schema.isRequired(fieldName)); @@ -636,8 +728,7 @@ public void setRequired_nullValueBecomesDefaultValue() { String fieldName = fieldType.name(); switch (fieldType) { case OBJECT: - case LIST: - // Skip always nullable fields. + // Skip always nullable fields break; default: // Skip not-nullable fields . @@ -667,8 +758,119 @@ public void setRequired_nullValueBecomesDefaultValue() { break; } } + for (FieldListType fieldType : FieldListType.values()) { + switch(fieldType) { + case LIST: + // Skip always non-nullable fields. + break; + case STRING_LIST: + checkListValueConversionToDefaultValue(String.class, ""); + break; + case SHORT_LIST: + checkListValueConversionToDefaultValue(Short.class, (short) 0); + break; + case INT_LIST: + checkListValueConversionToDefaultValue(Integer.class, 0); + break; + case LONG_LIST: + checkListValueConversionToDefaultValue(Long.class, 0L); + break; + case BYTE_LIST: + checkListValueConversionToDefaultValue(Byte.class, (byte) 0); + break; + case BOOLEAN_LIST: + checkListValueConversionToDefaultValue(Boolean.class, false); + break; + case FLOAT_LIST: + checkListValueConversionToDefaultValue(Float.class, 0.0F); + break; + case DOUBLE_LIST: + checkListValueConversionToDefaultValue(Double.class, 0.0D); + break; + case BLOB_LIST: + checkListValueConversionToDefaultValue(byte[].class, new byte[0]); + break; + case DATE_LIST: + checkListValueConversionToDefaultValue(Date.class, new Date(0)); + break; + case PRIMITIVE_INT_LIST: + case PRIMITIVE_LONG_LIST: + case PRIMITIVE_BYTE_LIST: + case PRIMITIVE_BOOLEAN_LIST: + case PRIMITIVE_FLOAT_LIST: + case PRIMITIVE_DOUBLE_LIST: + case PRIMITIVE_SHORT_LIST: + // Skip not-nullable fields + break; + default: + throw new IllegalArgumentException("Unknown type: " + fieldType); + } + } } + // Checks that null values in a value list are correctly converted to default values + // when field is set to required. + private void checkListValueConversionToDefaultValue(Class type, Object defaultValue) { + schema.addRealmListField("foo", type); + DynamicRealmObject obj = ((DynamicRealm) realm).createObject(schema.getClassName()); + RealmList list = new RealmList<>(); + list.add(null); + obj.setList("foo", list); + assertNull(obj.getList("foo", type).first()); + + // Convert from nullable to required + schema.setRequired("foo", true); + if (defaultValue instanceof byte[]) { + assertArrayEquals((byte[]) defaultValue, (byte[]) obj.getList("foo", type).first()); + } else { + assertEquals(defaultValue, obj.getList("foo", type).first()); + } + + // Convert back again + schema.setRequired("foo", false); + if (defaultValue instanceof byte[]) { + //noinspection ConstantConditions + assertArrayEquals((byte[]) defaultValue, (byte[]) obj.getList("foo", type).first()); + } else { + assertEquals(defaultValue, obj.getList("foo", type).first()); + } + + // Cleanup + schema.removeField("foo"); + } + + // Special test for making sure that binary data in all forms are transformed correctly + // when moving between nullable and required states. + @Test + public void binaryData_nullabilityConversions() { + if (type == ObjectSchemaType.IMMUTABLE) { + return; + } + schema.addRealmListField("foo", byte[].class); + + DynamicRealmObject obj = ((DynamicRealm) realm).createObject(schema.getClassName()); + RealmList list = obj.getList("foo", byte[].class); + assertTrue(list.size() == 0); + + // Initial content (nullable) + list.add(null); + list.add(new byte[] {1, 2, 3}); + assertNull(list.get(0)); + assertArrayEquals(new byte[] {1, 2, 3}, list.get(1)); + + // Transform to required + schema.setRequired("foo", true); + list = obj.getList("foo", byte[].class); + assertEquals(0, list.get(0).length); + assertArrayEquals(new byte[] {1, 2, 3}, list.get(1)); + + // Transform back to nullable + schema.setRequired("foo", false); + list = obj.getList("foo", byte[].class); + assertEquals(0, list.get(0).length); + assertArrayEquals(new byte[] {1, 2, 3}, list.get(1)); + } + @Test public void setRequired_true_onPrimaryKeyField_containsNullValues_shouldThrow() { if (type == ObjectSchemaType.IMMUTABLE) { @@ -781,7 +983,7 @@ public void setRequired_false_onIndexedField() { } @Test - public void setRemovePrimaryKey() { + public void setPrimaryKey_trueAndFalse() { if (type == ObjectSchemaType.IMMUTABLE) { try { schema.addPrimaryKey("test"); @@ -824,7 +1026,7 @@ public void removeNonExistingPrimaryKeyThrows() { } @Test - public void setRemoveIndex() { + public void setIndex_trueAndFalse() { if (type == ObjectSchemaType.IMMUTABLE) { try { schema.addIndex("test"); @@ -1115,6 +1317,21 @@ public void getFieldType_nonLatinName() { assertEquals(RealmFieldType.INTEGER, objSchema.getFieldType(NonLatinFieldNames.FIELD_LONG_GREEK_CHAR)); } + @Test + public void addList_modelClassThrowsWithProperError() { + if (type == ObjectSchemaType.IMMUTABLE) { + return; + } + + try { + schema.addRealmListField("field", AllJavaTypes.class); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Use 'addRealmListField(String name, RealmObjectSchema schema)' instead")); + } + } + + private interface FieldRunnable { void run(String fieldName); } diff --git a/realm/realm-library/src/main/cpp/io_realm_internal_Table.cpp b/realm/realm-library/src/main/cpp/io_realm_internal_Table.cpp index ced1e5d440..1618c48ad9 100644 --- a/realm/realm-library/src/main/cpp/io_realm_internal_Table.cpp +++ b/realm/realm-library/src/main/cpp/io_realm_internal_Table.cpp @@ -74,7 +74,6 @@ JNIEXPORT jlong JNICALL Java_io_realm_internal_Table_nativeAddColumn(JNIEnv* env try { JStringAccessor name2(env, name); // throws bool is_column_nullable = to_bool(isNullable); - DataType dataType = DataType(colType); if (is_column_nullable && dataType == type_LinkList) { ThrowException(env, IllegalArgument, "List fields cannot be nullable."); @@ -85,6 +84,25 @@ JNIEXPORT jlong JNICALL Java_io_realm_internal_Table_nativeAddColumn(JNIEnv* env return 0; } +JNIEXPORT jlong JNICALL Java_io_realm_internal_Table_nativeAddPrimitiveListColumn(JNIEnv* env, jobject, + jlong native_table_ptr, jint j_col_type, + jstring j_name, jboolean j_is_nullable) +{ + if (!TABLE_VALID(env, TBL(native_table_ptr))) { + return 0; + } + try { + JStringAccessor name(env, j_name); // throws + bool is_column_nullable = to_bool(j_is_nullable); + DataType data_type = DataType(j_col_type); + Table* table = TBL(native_table_ptr); + size_t col = table->add_column(type_Table, name); + return table->get_subdescriptor(col)->add_column(data_type, ObjectStore::ArrayColumnName, nullptr, is_column_nullable); + } + CATCH_STD() + return reinterpret_cast(nullptr); +} + JNIEXPORT jlong JNICALL Java_io_realm_internal_Table_nativeAddColumnLink(JNIEnv* env, jobject, jlong nativeTablePtr, jint colType, jstring name, jlong targetTablePtr) @@ -179,7 +197,6 @@ JNIEXPORT jboolean JNICALL Java_io_realm_internal_Table_nativeIsColumnNullable(J return to_jbool(table->is_nullable(S(columnIndex))); // noexcept } // For primitive list - // FIXME: Add test in https://github.com/realm/realm-java/pull/5221 before merging to master return to_jbool(table->get_descriptor()->get_subdescriptor(S(columnIndex))->is_nullable(S(0))); // noexcept } @@ -199,7 +216,101 @@ JNIEXPORT jboolean JNICALL Java_io_realm_internal_Table_nativeIsColumnNullable(J // 5. search indexing must be preserved // 6. removing the original column and renaming the temporary column will make it look like original is being modified -JNIEXPORT void JNICALL Java_io_realm_internal_Table_nativeConvertColumnToNullable(JNIEnv* env, jobject, +// Converts a table to allow for nullable values +// Works on both normal table columns and sub tables +static void convert_column_to_nullable(JNIEnv* env, Table* old_table, size_t old_col_ndx, Table* new_table, size_t new_col_ndx, bool is_primary_key) +{ + DataType column_type = old_table->get_column_type(old_col_ndx); + if (old_table != new_table) { + new_table->add_empty_row(old_table->size()); + } + for (size_t i = 0; i < old_table->size(); ++i) { + switch (column_type) { + case type_String: { + // Payload copy is needed + StringData sd(old_table->get_string(old_col_ndx, i)); + if (is_primary_key) { + new_table->set_string_unique(new_col_ndx, i, sd); + } + else { + new_table->set_string(new_col_ndx, i, sd); + } + break; + } + case type_Binary: { + BinaryData bd = old_table->get_binary(old_col_ndx, i); + new_table->set_binary(new_col_ndx, i, BinaryData(bd.data(), bd.size())); + break; + } + case type_Int: + if (is_primary_key) { + new_table->set_int_unique(new_col_ndx, i, old_table->get_int(old_col_ndx, i)); + } + else { + new_table->set_int(new_col_ndx, i, old_table->get_int(old_col_ndx, i)); + } + break; + case type_Bool: + new_table->set_bool(new_col_ndx, i, old_table->get_bool(old_col_ndx, i)); + break; + case type_Timestamp: + new_table->set_timestamp(new_col_ndx, i, old_table->get_timestamp(old_col_ndx, i)); + break; + case type_Float: + new_table->set_float(new_col_ndx, i, old_table->get_float(old_col_ndx, i)); + break; + case type_Double: + new_table->set_double(new_col_ndx, i, old_table->get_double(old_col_ndx, i)); + break; + case type_Link: + case type_LinkList: + case type_Mixed: + case type_Table: + // checked previously + break; + case type_OldDateTime: + ThrowException(env, UnsupportedOperation, "The old DateTime type is not supported."); + return; + } + } +} + +// Creates the new column into which all old data is copied when switching between nullable and non-nullable. +static void create_new_column(Table* table, size_t column_index, bool nullable) +{ + std::string column_name = table->get_column_name(column_index); + DataType column_type = table->get_column_type(column_index); + bool is_subtable = table->get_column_type(column_index) == DataType::type_Table; + size_t j = 0; + while (true) { + std::ostringstream ss; + ss << std::string("__TMP__") << j; + std::string str = ss.str(); + StringData tmp_column_name(str); + if (table->get_column_index(tmp_column_name) == realm::not_found) { + if (is_subtable) { + DataType original_type = table->get_subdescriptor(column_index)->get_column_type(0); + table->insert_column(column_index, type_Table, tmp_column_name, true); + table->get_subdescriptor(column_index)->add_column(original_type, ObjectStore::ArrayColumnName, nullptr, nullable); + } + else { + table->insert_column(column_index, column_type, tmp_column_name, nullable); + } + break; + } + j++; + } + + // Search index has too be added first since if it is a PK field, add_xxx_unique will check it. + if (!is_subtable) { + // TODO indexes on sub tables not supported yet? + if (table->has_search_index(column_index + 1)) { + table->add_search_index(column_index); + } + } +} + +JNIEXPORT void JNICALL Java_io_realm_internal_Table_nativeConvertColumnToNullable(JNIEnv* env, jobject obj, jlong native_table_ptr, jlong j_column_index, jboolean is_primary_key) @@ -208,248 +319,212 @@ JNIEXPORT void JNICALL Java_io_realm_internal_Table_nativeConvertColumnToNullabl if (!TBL_AND_COL_INDEX_VALID(env, table, j_column_index)) { return; } - if (table->has_shared_type()) { - ThrowException(env, UnsupportedOperation, "Not allowed to convert field in subtable."); - return; - } try { - size_t column_index = S(j_column_index); - if (table->is_nullable(column_index)) { - return; // column is already nullable + Table* table = TBL(native_table_ptr); + if (!TBL_AND_COL_INDEX_VALID(env, table, j_column_index)) { + return; + } + if (table->has_shared_type()) { + ThrowException(env, UnsupportedOperation, "Not allowed to convert field in subtable."); + return; } - std::string column_name = table->get_column_name(column_index); + size_t column_index = S(j_column_index); DataType column_type = table->get_column_type(column_index); - if (column_type == type_Link || column_type == type_LinkList || column_type == type_Mixed || - column_type == type_Table) { + std::string column_name = table->get_column_name(column_index); + bool is_subtable = (column_type == DataType::type_Table); + + // Cannot convert Object links or lists of objects + if (column_type == type_Link || column_type == type_LinkList || column_type == type_Mixed) { ThrowException(env, IllegalArgument, "Wrong type - cannot be converted to nullable."); } - std::string tmp_column_name; + // Exit quickly if column is already nullable + if (Java_io_realm_internal_Table_nativeIsColumnNullable(env, obj, native_table_ptr, j_column_index)) { + return; + } - size_t j = 0; - while (true) { - std::ostringstream ss; - ss << std::string("__TMP__") << j; - std::string str = ss.str(); - StringData sd(str); - if (table->get_column_index(sd) == realm::not_found) { - table->insert_column(column_index, column_type, sd, true); - tmp_column_name = ss.str(); - break; + // 1. Create temporary table + create_new_column(table, column_index, true); + + // Move all values + if (is_subtable) { + for (size_t i = 0; i < table->size(); ++i) { + TableRef new_subtable = table->get_subtable(column_index, i); + TableRef old_subtable = table->get_subtable(column_index + 1, i); + convert_column_to_nullable(env, old_subtable.get(), 0, new_subtable.get(), 0, is_primary_key); } - j++; } - - // Search index has too be added first since if it is a PK field, add_xxx_unique will check it. - if (table->has_search_index(column_index + 1)) { - table->add_search_index(column_index); + else { + convert_column_to_nullable(env, table, column_index + 1, table, column_index, is_primary_key); } - for (size_t i = 0; i < table->size(); ++i) { - switch (column_type) { - case type_String: { + // Cleanup + table->remove_column(column_index + 1); + table->rename_column(column_index, column_name); + + } + CATCH_STD() +} + +// Convert a tables values to not nullable, but converting all null values to the defaul value for the type +// Works on both normal table columns and sub tables +static void convert_column_to_not_nullable(JNIEnv* env, Table* old_table, size_t old_col_ndx, Table* new_table, size_t new_col_ndx, bool is_primary_key) +{ + DataType column_type = old_table->get_column_type(old_col_ndx); + std::string column_name = old_table->get_column_name(old_col_ndx); + if (old_table != new_table) { + new_table->add_empty_row(old_table->size()); + } + for (size_t i = 0; i < old_table->size(); ++i) { + switch (column_type) { // FIXME: respect user-specified default values + case type_String: { + StringData sd = old_table->get_string(old_col_ndx, i); + if (sd == realm::null()) { + if (is_primary_key) { + THROW_JAVA_EXCEPTION(env, JavaExceptionDef::IllegalState, + format(c_null_values_cannot_set_required_msg, column_name)); + } + else { + new_table->set_string(new_col_ndx, i, ""); + } + } + else { // Payload copy is needed - StringData sd(table->get_string(column_index + 1, i)); if (is_primary_key) { - table->set_string_unique(column_index, i, sd); + new_table->set_string_unique(new_col_ndx, i, sd); } else { - table->set_string(column_index, i, sd); + new_table->set_string(new_col_ndx, i, sd); } - break; } - case type_Binary: { + break; + } + case type_Binary: { + BinaryData bd = old_table->get_binary(old_col_ndx, i); + if (bd.is_null()) { + new_table->set_binary(new_col_ndx, i, BinaryData("", 0)); + } + else { // Payload copy is needed - BinaryData bd = table->get_binary(column_index + 1, i); - std::vector binary_copy(bd.data(), bd.data() + bd.size()); - table->set_binary(column_index, i, BinaryData(binary_copy.data(), binary_copy.size())); - break; + std::vector bd_copy(bd.data(), bd.data() + bd.size()); + new_table->set_binary(new_col_ndx, i, BinaryData(bd_copy.data(), bd_copy.size())); } - case type_Int: + break; + } + case type_Int: + if (old_table->is_null(old_col_ndx, i)) { if (is_primary_key) { - table->set_int_unique(column_index, i, table->get_int(column_index + 1, i)); + THROW_JAVA_EXCEPTION(env, JavaExceptionDef::IllegalState, + format(c_null_values_cannot_set_required_msg, column_name)); } else { - table->set_int(column_index, i, table->get_int(column_index + 1, i)); + new_table->set_int(new_col_ndx, i, 0); } - break; - case type_Bool: - table->set_bool(column_index, i, table->get_bool(column_index + 1, i)); - break; - case type_Timestamp: - table->set_timestamp(column_index, i, table->get_timestamp(column_index + 1, i)); - break; - case type_Float: - table->set_float(column_index, i, table->get_float(column_index + 1, i)); - break; - case type_Double: - table->set_double(column_index, i, table->get_double(column_index + 1, i)); - break; - case type_Link: - case type_LinkList: - case type_Mixed: - case type_Table: - // checked previously - break; - case type_OldDateTime: - ThrowException(env, UnsupportedOperation, "The old DateTime type is not supported."); - return; - } + } + else { + if (is_primary_key) { + new_table->set_int_unique(new_col_ndx, i, old_table->get_int(old_col_ndx, i)); + } + else { + new_table->set_int(new_col_ndx, i, old_table->get_int(old_col_ndx, i)); + } + } + break; + case type_Bool: + if (old_table->is_null(old_col_ndx, i)) { + new_table->set_bool(new_col_ndx, i, false); + } + else { + new_table->set_bool(new_col_ndx, i, old_table->get_bool(old_col_ndx, i)); + } + break; + case type_Timestamp: + if (old_table->is_null(old_col_ndx, i)) { + new_table->set_timestamp(new_col_ndx, i, Timestamp(0, 0)); + } + else { + new_table->set_timestamp(new_col_ndx, i, old_table->get_timestamp(old_col_ndx, i)); + } + break; + case type_Float: + if (old_table->is_null(old_col_ndx, i)) { + new_table->set_float(new_col_ndx, i, 0.0); + } + else { + new_table->set_float(new_col_ndx, i, old_table->get_float(old_col_ndx, i)); + } + break; + case type_Double: + if (old_table->is_null(old_col_ndx, i)) { + new_table->set_double(new_col_ndx, i, 0.0); + } + else { + new_table->set_double(new_col_ndx, i, old_table->get_double(old_col_ndx, i)); + } + break; + case type_Link: + case type_LinkList: + case type_Mixed: + case type_Table: + // checked previously + break; + case type_OldDateTime: + // not used + ThrowException(env, UnsupportedOperation, "The old DateTime type is not supported."); + return; } - table->remove_column(column_index + 1); - table->rename_column(table->get_column_index(tmp_column_name), column_name); } - CATCH_STD() } -JNIEXPORT void JNICALL Java_io_realm_internal_Table_nativeConvertColumnToNotNullable(JNIEnv* env, jobject, + +JNIEXPORT void JNICALL Java_io_realm_internal_Table_nativeConvertColumnToNotNullable(JNIEnv* env, jobject obj, jlong native_table_ptr, jlong j_column_index, jboolean is_primary_key) { - Table* table = TBL(native_table_ptr); - if (!TBL_AND_COL_INDEX_VALID(env, table, j_column_index)) { - return; - } - if (table->has_shared_type()) { - ThrowException(env, UnsupportedOperation, "Not allowed to convert field in subtable."); - return; - } try { - size_t column_index = S(j_column_index); - if (!table->is_nullable(column_index)) { - return; // column is already not nullable + Table* table = TBL(native_table_ptr); + if (!TBL_AND_COL_INDEX_VALID(env, table, j_column_index)) { + return; + } + if (table->has_shared_type()) { + ThrowException(env, UnsupportedOperation, "Not allowed to convert field in subtable."); + return; } + // Exit quickly if column is already non-nullable + if (!Java_io_realm_internal_Table_nativeIsColumnNullable(env, obj, native_table_ptr, j_column_index)) { + return; + } + + size_t column_index = S(j_column_index); std::string column_name = table->get_column_name(column_index); DataType column_type = table->get_column_type(column_index); - if (column_type == type_Link || column_type == type_LinkList || column_type == type_Mixed || - column_type == type_Table) { + bool is_subtable = (column_type == DataType::type_Table); + + if (column_type == type_Link || column_type == type_LinkList || column_type == type_Mixed) { ThrowException(env, IllegalArgument, "Wrong type - cannot be converted to nullable."); } - std::string tmp_column_name; - size_t j = 0; - while (true) { - std::ostringstream ss; - ss << std::string("__TMP__") << j; - std::string str = ss.str(); - StringData sd(str); - if (table->get_column_index(sd) == realm::not_found) { - table->insert_column(column_index, column_type, sd, false); - tmp_column_name = ss.str(); - break; + // 1. Create temporary table + create_new_column(table, column_index, false); + + // 2. Move all values + if (is_subtable) { + for (size_t i = 0; i < table->size(); ++i) { + TableRef new_subtable = table->get_subtable(column_index, i); + TableRef old_subtable = table->get_subtable(column_index + 1, i); + convert_column_to_not_nullable(env, old_subtable.get(), 0, new_subtable.get(), 0, is_primary_key); } - j++; } - - // Search index has too be added first since if it is a PK field, add_xxx_unique will check it. - if (table->has_search_index(column_index + 1)) { - table->add_search_index(column_index); + else { + convert_column_to_not_nullable(env, table, column_index + 1, table, column_index, is_primary_key); } - for (size_t i = 0; i < table->size(); ++i) { - switch (column_type) { // FIXME: respect user-specified default values - case type_String: { - StringData sd = table->get_string(column_index + 1, i); - if (sd == realm::null()) { - if (is_primary_key) { - THROW_JAVA_EXCEPTION(env, JavaExceptionDef::IllegalState, - format(c_null_values_cannot_set_required_msg, column_name)); - } - else { - table->set_string(column_index, i, ""); - } - } - else { - // Payload copy is needed - if (is_primary_key) { - table->set_string_unique(column_index, i, sd); - } - else { - table->set_string(column_index, i, sd); - } - } - break; - } - case type_Binary: { - BinaryData bd = table->get_binary(column_index + 1, i); - if (bd.is_null()) { - table->set_binary(column_index, i, BinaryData("", 0)); - } - else { - // Payload copy is needed - std::vector bd_copy(bd.data(), bd.data() + bd.size()); - table->set_binary(column_index, i, BinaryData(bd_copy.data(), bd_copy.size())); - } - break; - } - case type_Int: - if (table->is_null(column_index + 1, i)) { - if (is_primary_key) { - THROW_JAVA_EXCEPTION(env, JavaExceptionDef::IllegalState, - format(c_null_values_cannot_set_required_msg, column_name)); - } - else { - table->set_int(column_index, i, 0); - } - } - else { - if (is_primary_key) { - table->set_int_unique(column_index, i, table->get_int(column_index + 1, i)); - } - else { - table->set_int(column_index, i, table->get_int(column_index + 1, i)); - } - } - break; - case type_Bool: - if (table->is_null(column_index + 1, i)) { - table->set_bool(column_index, i, false); - } - else { - table->set_bool(column_index, i, table->get_bool(column_index + 1, i)); - } - break; - case type_Timestamp: - if (table->is_null(column_index + 1, i)) { - table->set_timestamp(column_index, i, Timestamp(0, 0)); - } - else { - table->set_timestamp(column_index, i, table->get_timestamp(column_index + 1, i)); - } - break; - case type_Float: - if (table->is_null(column_index + 1, i)) { - table->set_float(column_index, i, 0.0); - } - else { - table->set_float(column_index, i, table->get_float(column_index + 1, i)); - } - break; - case type_Double: - if (table->is_null(column_index + 1, i)) { - table->set_double(column_index, i, 0.0); - } - else { - table->set_double(column_index, i, table->get_double(column_index + 1, i)); - } - break; - case type_Link: - case type_LinkList: - case type_Mixed: - case type_Table: - // checked previously - break; - case type_OldDateTime: - // not used - ThrowException(env, UnsupportedOperation, "The old DateTime type is not supported."); - return; - } - } + // 3. Delete old values table->remove_column(column_index + 1); - table->rename_column(table->get_column_index(tmp_column_name), column_name); + table->rename_column(column_index, column_name); } CATCH_STD() } diff --git a/realm/realm-library/src/main/java/io/realm/DynamicRealmObject.java b/realm/realm-library/src/main/java/io/realm/DynamicRealmObject.java index 012085f1f8..85fbbaecdf 100644 --- a/realm/realm-library/src/main/java/io/realm/DynamicRealmObject.java +++ b/realm/realm-library/src/main/java/io/realm/DynamicRealmObject.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Date; +import java.util.Iterator; import java.util.Locale; import javax.annotation.Nonnull; @@ -336,6 +337,8 @@ public DynamicRealmObject getObject(String fieldName) { /** * Returns the {@link RealmList} of {@link DynamicRealmObject}s being linked from the given field. + *

+ * If the list contains primitive types, use {@link #getList(String, Class)} instead. * * @param fieldName the name of the field. * @return the {@link RealmList} data for this field. @@ -346,7 +349,7 @@ public RealmList getList(String fieldName) { long columnIndex = proxyState.getRow$realm().getColumnIndex(fieldName); try { - OsList osList = proxyState.getRow$realm().getList(columnIndex); + OsList osList = proxyState.getRow$realm().getModelList(columnIndex); //noinspection ConstantConditions @Nonnull String className = osList.getTargetTable().getClassName(); @@ -358,15 +361,54 @@ public RealmList getList(String fieldName) { } /** - * Returns the {@link RealmList} of values being linked from the given field. + * Returns the {@link RealmList} containing only primitive values. + * + *

+ * If the list contains references to other Realm objects, use {@link #getList(String)} instead. * * @param fieldName the name of the field. + * @param primitiveType the type of elements in the list. Only primitive types are supported. * @return the {@link RealmList} data for this field. - * @throws IllegalArgumentException if field name doesn't exist or it doesn't contain a list of values. + * @throws IllegalArgumentException if field name doesn't exist or it doesn't contain a list of primitive objects. */ - public RealmList getValueList(String fieldName, Class valueClass) { - // TODO implement this - return null; + public RealmList getList(String fieldName, Class primitiveType) { + proxyState.getRealm$realm().checkIfValid(); + + if (primitiveType == null) { + throw new IllegalArgumentException("Non-null 'primitiveType' required."); + } + long columnIndex = proxyState.getRow$realm().getColumnIndex(fieldName); + RealmFieldType realmType = classToRealmType(primitiveType); + try { + OsList osList = proxyState.getRow$realm().getValueList(columnIndex, realmType); + return new RealmList<>(primitiveType, osList, proxyState.getRealm$realm()); + } catch (IllegalArgumentException e) { + checkFieldType(fieldName, columnIndex, realmType); + throw e; + } + } + + private RealmFieldType classToRealmType(Class primitiveType) { + if (primitiveType.equals(Integer.class) + || primitiveType.equals(Long.class) + || primitiveType.equals(Short.class) + || primitiveType.equals(Byte.class)) { + return RealmFieldType.INTEGER_LIST; + } else if (primitiveType.equals(Boolean.class)) { + return RealmFieldType.BOOLEAN_LIST; + } else if (primitiveType.equals(String.class)) { + return RealmFieldType.STRING_LIST; + } else if (primitiveType.equals(byte[].class)) { + return RealmFieldType.BINARY_LIST; + } else if (primitiveType.equals(Date.class)) { + return RealmFieldType.DATE_LIST; + } else if (primitiveType.equals(Float.class)) { + return RealmFieldType.FLOAT_LIST; + } else if (primitiveType.equals(Double.class)) { + return RealmFieldType.DOUBLE_LIST; + } else { + throw new IllegalArgumentException("Unsupported element type. Only primitive types supported. Yours was: " + primitiveType); + } } /** @@ -393,6 +435,15 @@ public boolean isNull(String fieldName) { case DATE: return proxyState.getRow$realm().isNull(columnIndex); case LIST: + case LINKING_OBJECTS: + case INTEGER_LIST: + case BOOLEAN_LIST: + case STRING_LIST: + case BINARY_LIST: + case DATE_LIST: + case FLOAT_LIST: + case DOUBLE_LIST: + // fall through default: return false; } @@ -510,28 +561,7 @@ private void setValue(String fieldName, Object value) { setObject(fieldName, (DynamicRealmObject) value); } else if (valueClass == RealmList.class) { RealmList list = (RealmList) value; - if (list.className == null && list.clazz == null) { - // unmanaged RealmList - long columnIndex = proxyState.getRow$realm().getColumnIndex(fieldName); - final RealmFieldType columnType = proxyState.getRow$realm().getColumnType(columnIndex); - if (columnType == RealmFieldType.LIST) { - //noinspection unchecked - for (Object element : list) { - if (!(element instanceof RealmModel)) { - throw new IllegalArgumentException("All elements in the list must be an instance of RealmModel."); - } - } - //noinspection unchecked - setList(fieldName, (RealmList) list); - } else { - setValueList(fieldName, list); - } - } else if (list.className != null || RealmModel.class.isAssignableFrom(list.clazz)) { - //noinspection unchecked - setList(fieldName, (RealmList) list); - } else { - setValueList(fieldName, list); - } + setList(fieldName, list); } else { throw new IllegalArgumentException("Value is of an type not supported: " + value.getClass()); } @@ -727,21 +757,54 @@ public void setObject(String fieldName, @Nullable DynamicRealmObject value) { * Sets the reference to a {@link RealmList} on the given field. * * @param fieldName field name. - * @param list list of references. - * @throws IllegalArgumentException if field name doesn't exist, it is not a list field, the type - * of the object represented by the DynamicRealmObject doesn't match or any element in the list belongs to a - * different Realm. + * @param list list of objects. Must either be primitive types or {@link DynamicRealmObject}s. + * @throws IllegalArgumentException if field name doesn't exist, it is not a list field, the objects in the + * list doesn't match the expected type or any Realm object in the list belongs to a different Realm. */ - public void setList(String fieldName, RealmList list) { + public void setList(String fieldName, RealmList list) { proxyState.getRealm$realm().checkIfValid(); //noinspection ConstantConditions if (list == null) { - throw new IllegalArgumentException("Null values not allowed for lists"); + throw new IllegalArgumentException("Non-null 'list' required"); + } + + // Find type of list in Realm + long columnIndex = proxyState.getRow$realm().getColumnIndex(fieldName); + final RealmFieldType columnType = proxyState.getRow$realm().getColumnType(columnIndex); + + switch (columnType) { + case LIST: + // Due to type erasure it is not possible to check the generic parameter, + // instead we try to see if the first element is of the wrong type in order + // to throw a better error message. + // Primitive types are checked inside `setModelList` + if (!list.isEmpty()) { + E element = list.first(); + if (!(element instanceof DynamicRealmObject) && RealmModel.class.isAssignableFrom(element.getClass())) { + throw new IllegalArgumentException("RealmList must contain `DynamicRealmObject's, not Java model classes."); + } + } + //noinspection unchecked + setModelList(fieldName, (RealmList) list); + break; + case INTEGER_LIST: + case BOOLEAN_LIST: + case STRING_LIST: + case BINARY_LIST: + case DATE_LIST: + case FLOAT_LIST: + case DOUBLE_LIST: + setValueList(fieldName, list, columnType); + break; + default: + throw new IllegalArgumentException(String.format("Field '%s' is not a list but a %s", fieldName, columnType)); } + } + private void setModelList(String fieldName, RealmList list) { long columnIndex = proxyState.getRow$realm().getColumnIndex(fieldName); - OsList osList = proxyState.getRow$realm().getList(columnIndex); + OsList osList = proxyState.getRow$realm().getModelList(columnIndex); Table linkTargetTable = osList.getTargetTable(); //noinspection ConstantConditions @Nonnull @@ -788,17 +851,72 @@ public void setList(String fieldName, RealmList list) { } } - /** - * Sets the reference to a {@link RealmList} on the given field. - * - * @param fieldName field name. - * @param list list of references. - * @throws IllegalArgumentException if field name doesn't exist, it is not a list field, the type - * of the object represented by the DynamicRealmObject doesn't match or any element in the list belongs to a - * different Realm. - */ - public void setValueList(String fieldName, RealmList list) { - // TODO implement this + @SuppressWarnings("unchecked") + private void setValueList(String fieldName, RealmList list, RealmFieldType primitiveType) { + long columnIndex = proxyState.getRow$realm().getColumnIndex(fieldName); + OsList osList = proxyState.getRow$realm().getValueList(columnIndex, primitiveType); + + Class elementClass; + switch(primitiveType) { + case INTEGER_LIST: elementClass = (Class) Long.class; break; + case BOOLEAN_LIST: elementClass = (Class) Boolean.class; break; + case STRING_LIST: elementClass = (Class) String.class; break; + case BINARY_LIST: elementClass = (Class) byte[].class; break; + case DATE_LIST: elementClass = (Class) Date.class; break; + case FLOAT_LIST: elementClass = (Class) Float.class; break; + case DOUBLE_LIST: elementClass = (Class) Double.class; break; + default: + throw new IllegalArgumentException("Unsupported type: " + primitiveType); + } + final ManagedListOperator operator = getOperator(proxyState.getRealm$realm(), osList, primitiveType, elementClass); + + if (list.isManaged() && osList.size() == list.size()) { + // There is a chance that the source list and the target list are the same list in the same object. + // In this case, we can't use removeAll(). + final int size = list.size(); + final Iterator iterator = list.iterator(); + for (int i = 0; i < size; i++) { + @Nullable + final Object value = iterator.next(); + operator.set(i, value); + } + } else { + osList.removeAll(); + for (Object value : list) { + operator.append(value); + } + } + } + + private ManagedListOperator getOperator(BaseRealm realm, OsList osList, RealmFieldType valueListType, Class valueClass) { + if (valueListType == RealmFieldType.STRING_LIST) { + //noinspection unchecked + return (ManagedListOperator) new StringListOperator(realm, osList, (Class) valueClass); + } + if (valueListType == RealmFieldType.INTEGER_LIST) { + return new LongListOperator<>(realm, osList, valueClass); + } + if (valueListType == RealmFieldType.BOOLEAN_LIST) { + //noinspection unchecked + return (ManagedListOperator) new BooleanListOperator(realm, osList, (Class) valueClass); + } + if (valueListType == RealmFieldType.BINARY_LIST) { + //noinspection unchecked + return (ManagedListOperator) new BinaryListOperator(realm, osList, (Class) valueClass); + } + if (valueListType == RealmFieldType.DOUBLE_LIST) { + //noinspection unchecked + return (ManagedListOperator) new DoubleListOperator(realm, osList, (Class) valueClass); + } + if (valueListType == RealmFieldType.FLOAT_LIST) { + //noinspection unchecked + return (ManagedListOperator) new FloatListOperator(realm, osList, (Class) valueClass); + } + if (valueListType == RealmFieldType.DATE_LIST) { + //noinspection unchecked + return (ManagedListOperator) new DateListOperator(realm, osList, (Class) valueClass); + } + throw new IllegalArgumentException("Unexpected list type: " + valueListType.name()); } /** @@ -964,7 +1082,28 @@ public String toString() { break; case LIST: String targetClassName = proxyState.getRow$realm().getTable().getLinkTarget(columnIndex).getClassName(); - sb.append(String.format(Locale.US, "RealmList<%s>[%s]", targetClassName, proxyState.getRow$realm().getList(columnIndex).size())); + sb.append(String.format(Locale.US, "RealmList<%s>[%s]", targetClassName, proxyState.getRow$realm().getModelList(columnIndex).size())); + break; + case INTEGER_LIST: + sb.append(String.format(Locale.US, "RealmList[%s]", proxyState.getRow$realm().getValueList(columnIndex, type).size())); + break; + case BOOLEAN_LIST: + sb.append(String.format(Locale.US, "RealmList[%s]", proxyState.getRow$realm().getValueList(columnIndex, type).size())); + break; + case STRING_LIST: + sb.append(String.format(Locale.US, "RealmList[%s]", proxyState.getRow$realm().getValueList(columnIndex, type).size())); + break; + case BINARY_LIST: + sb.append(String.format(Locale.US, "RealmList[%s]", proxyState.getRow$realm().getValueList(columnIndex, type).size())); + break; + case DATE_LIST: + sb.append(String.format(Locale.US, "RealmList[%s]", proxyState.getRow$realm().getValueList(columnIndex, type).size())); + break; + case FLOAT_LIST: + sb.append(String.format(Locale.US, "RealmList[%s]", proxyState.getRow$realm().getValueList(columnIndex, type).size())); + break; + case DOUBLE_LIST: + sb.append(String.format(Locale.US, "RealmList[%s]", proxyState.getRow$realm().getValueList(columnIndex, type).size())); break; default: sb.append("?"); diff --git a/realm/realm-library/src/main/java/io/realm/ImmutableRealmObjectSchema.java b/realm/realm-library/src/main/java/io/realm/ImmutableRealmObjectSchema.java index 477f08758a..48c3e67d4e 100644 --- a/realm/realm-library/src/main/java/io/realm/ImmutableRealmObjectSchema.java +++ b/realm/realm-library/src/main/java/io/realm/ImmutableRealmObjectSchema.java @@ -55,6 +55,11 @@ public RealmObjectSchema addRealmListField(String fieldName, RealmObjectSchema o throw new UnsupportedOperationException(SCHEMA_IMMUTABLE_EXCEPTION_MSG); } + @Override + public RealmObjectSchema addRealmListField(String fieldName, Class primitiveType) { + throw new UnsupportedOperationException(SCHEMA_IMMUTABLE_EXCEPTION_MSG); + } + @Override public RealmObjectSchema removeField(String fieldName) { throw new UnsupportedOperationException(SCHEMA_IMMUTABLE_EXCEPTION_MSG); diff --git a/realm/realm-library/src/main/java/io/realm/MutableRealmObjectSchema.java b/realm/realm-library/src/main/java/io/realm/MutableRealmObjectSchema.java index 2281c0ff8a..51e269bb93 100644 --- a/realm/realm-library/src/main/java/io/realm/MutableRealmObjectSchema.java +++ b/realm/realm-library/src/main/java/io/realm/MutableRealmObjectSchema.java @@ -19,7 +19,6 @@ import java.util.Locale; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import io.realm.internal.OsObjectStore; import io.realm.internal.Table; @@ -105,7 +104,7 @@ public RealmObjectSchema addField(String fieldName, Class fieldType, FieldAtt nullable = false; } - long columnIndex = table.addColumn(metadata.realmType, fieldName, nullable); + long columnIndex = table.addColumn(metadata.fieldType, fieldName, nullable); try { addModifiers(fieldName, attributes); } catch (Exception e) { @@ -132,6 +131,25 @@ public RealmObjectSchema addRealmListField(String fieldName, RealmObjectSchema o return this; } + @Override + public RealmObjectSchema addRealmListField(String fieldName, Class primitiveType) { + checkLegalName(fieldName); + checkFieldNameIsAvailable(fieldName); + + FieldMetaData metadata = SUPPORTED_SIMPLE_FIELDS.get(primitiveType); + if (metadata == null) { + if (primitiveType.equals(RealmObjectSchema.class) || RealmModel.class.isAssignableFrom(primitiveType)) { + throw new IllegalArgumentException("Use 'addRealmListField(String name, RealmObjectSchema schema)' instead to add lists that link to other RealmObjects: " + fieldName); + } else { + throw new IllegalArgumentException(String.format(Locale.US, + "RealmList does not support lists with this type: %s(%s)", + fieldName, primitiveType)); + } + } + table.addColumn(metadata.listType, fieldName, metadata.defaultNullable); + return this; + } + @Override public RealmObjectSchema removeField(String fieldName) { realm.checkNotInSync(); // destructive modification of a schema is not permitted diff --git a/realm/realm-library/src/main/java/io/realm/MutableRealmSchema.java b/realm/realm-library/src/main/java/io/realm/MutableRealmSchema.java index 8d36769f95..10f060b24e 100644 --- a/realm/realm-library/src/main/java/io/realm/MutableRealmSchema.java +++ b/realm/realm-library/src/main/java/io/realm/MutableRealmSchema.java @@ -64,12 +64,12 @@ public RealmObjectSchema createWithPrimaryKeyField(String className, String prim String internalTableName = checkAndGetTableNameFromClassName(className); RealmObjectSchema.FieldMetaData metadata = RealmObjectSchema.getSupportedSimpleFields().get(fieldType); - if (metadata == null || (metadata.realmType != RealmFieldType.STRING && - metadata.realmType != RealmFieldType.INTEGER)) { + if (metadata == null || (metadata.fieldType != RealmFieldType.STRING && + metadata.fieldType != RealmFieldType.INTEGER)) { throw new IllegalArgumentException(String.format("Realm doesn't support primary key field type '%s'.", fieldType)); } - boolean isStringField = (metadata.realmType == RealmFieldType.STRING); + boolean isStringField = (metadata.fieldType == RealmFieldType.STRING); boolean nullable = metadata.defaultNullable; if (MutableRealmObjectSchema.containsAttribute(attributes, FieldAttribute.REQUIRED)) { diff --git a/realm/realm-library/src/main/java/io/realm/RealmList.java b/realm/realm-library/src/main/java/io/realm/RealmList.java index 7bcbe78c35..b13841e276 100644 --- a/realm/realm-library/src/main/java/io/realm/RealmList.java +++ b/realm/realm-library/src/main/java/io/realm/RealmList.java @@ -70,7 +70,7 @@ public class RealmList extends AbstractList implements OrderedRealmCollect protected String className; // Always null if RealmList is unmanaged, always non-null if managed. - private final ManagedListOperator osListOperator; + final ManagedListOperator osListOperator; final protected BaseRealm realm; private List unmanagedList; // Used for listeners on RealmList diff --git a/realm/realm-library/src/main/java/io/realm/RealmObjectSchema.java b/realm/realm-library/src/main/java/io/realm/RealmObjectSchema.java index 27f0129173..3442f1074e 100644 --- a/realm/realm-library/src/main/java/io/realm/RealmObjectSchema.java +++ b/realm/realm-library/src/main/java/io/realm/RealmObjectSchema.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; + import io.realm.annotations.Required; import io.realm.internal.ColumnInfo; import io.realm.internal.OsObject; @@ -47,23 +49,23 @@ public abstract class RealmObjectSchema { static { Map, FieldMetaData> m = new HashMap<>(); - m.put(String.class, new FieldMetaData(RealmFieldType.STRING, true)); - m.put(short.class, new FieldMetaData(RealmFieldType.INTEGER, false)); - m.put(Short.class, new FieldMetaData(RealmFieldType.INTEGER, true)); - m.put(int.class, new FieldMetaData(RealmFieldType.INTEGER, false)); - m.put(Integer.class, new FieldMetaData(RealmFieldType.INTEGER, true)); - m.put(long.class, new FieldMetaData(RealmFieldType.INTEGER, false)); - m.put(Long.class, new FieldMetaData(RealmFieldType.INTEGER, true)); - m.put(float.class, new FieldMetaData(RealmFieldType.FLOAT, false)); - m.put(Float.class, new FieldMetaData(RealmFieldType.FLOAT, true)); - m.put(double.class, new FieldMetaData(RealmFieldType.DOUBLE, false)); - m.put(Double.class, new FieldMetaData(RealmFieldType.DOUBLE, true)); - m.put(boolean.class, new FieldMetaData(RealmFieldType.BOOLEAN, false)); - m.put(Boolean.class, new FieldMetaData(RealmFieldType.BOOLEAN, true)); - m.put(byte.class, new FieldMetaData(RealmFieldType.INTEGER, false)); - m.put(Byte.class, new FieldMetaData(RealmFieldType.INTEGER, true)); - m.put(byte[].class, new FieldMetaData(RealmFieldType.BINARY, true)); - m.put(Date.class, new FieldMetaData(RealmFieldType.DATE, true)); + m.put(String.class, new FieldMetaData(RealmFieldType.STRING, RealmFieldType.STRING_LIST, true)); + m.put(short.class, new FieldMetaData(RealmFieldType.INTEGER, RealmFieldType.INTEGER_LIST, false)); + m.put(Short.class, new FieldMetaData(RealmFieldType.INTEGER, RealmFieldType.INTEGER_LIST, true)); + m.put(int.class, new FieldMetaData(RealmFieldType.INTEGER, RealmFieldType.INTEGER_LIST, false)); + m.put(Integer.class, new FieldMetaData(RealmFieldType.INTEGER, RealmFieldType.INTEGER_LIST, true)); + m.put(long.class, new FieldMetaData(RealmFieldType.INTEGER, RealmFieldType.INTEGER_LIST, false)); + m.put(Long.class, new FieldMetaData(RealmFieldType.INTEGER, RealmFieldType.INTEGER_LIST, true)); + m.put(float.class, new FieldMetaData(RealmFieldType.FLOAT, RealmFieldType.FLOAT_LIST, false)); + m.put(Float.class, new FieldMetaData(RealmFieldType.FLOAT, RealmFieldType.FLOAT_LIST, true)); + m.put(double.class, new FieldMetaData(RealmFieldType.DOUBLE, RealmFieldType.DOUBLE_LIST, false)); + m.put(Double.class, new FieldMetaData(RealmFieldType.DOUBLE, RealmFieldType.DOUBLE_LIST, true)); + m.put(boolean.class, new FieldMetaData(RealmFieldType.BOOLEAN, RealmFieldType.BOOLEAN_LIST, false)); + m.put(Boolean.class, new FieldMetaData(RealmFieldType.BOOLEAN, RealmFieldType.BOOLEAN_LIST, true)); + m.put(byte.class, new FieldMetaData(RealmFieldType.INTEGER, RealmFieldType.INTEGER_LIST, false)); + m.put(Byte.class, new FieldMetaData(RealmFieldType.INTEGER, RealmFieldType.INTEGER_LIST, true)); + m.put(byte[].class, new FieldMetaData(RealmFieldType.BINARY, RealmFieldType.BINARY_LIST, true)); + m.put(Date.class, new FieldMetaData(RealmFieldType.DATE, RealmFieldType.DATE_LIST, true)); SUPPORTED_SIMPLE_FIELDS = Collections.unmodifiableMap(m); } @@ -71,8 +73,8 @@ public abstract class RealmObjectSchema { static { Map, FieldMetaData> m = new HashMap<>(); - m.put(RealmObject.class, new FieldMetaData(RealmFieldType.OBJECT, false)); - m.put(RealmList.class, new FieldMetaData(RealmFieldType.LIST, false)); + m.put(RealmObject.class, new FieldMetaData(RealmFieldType.OBJECT, null, false)); + m.put(RealmList.class, new FieldMetaData(RealmFieldType.LIST, null, false)); SUPPORTED_LINKED_FIELDS = Collections.unmodifiableMap(m); } @@ -151,7 +153,9 @@ public String getClassName() { public abstract RealmObjectSchema addRealmObjectField(String fieldName, RealmObjectSchema objectSchema); /** - * Adds a new field that references a {@link RealmList}. + * Adds a new field that contains a {@link RealmList} with references to other Realm model classes. + *

+ * If the list contains primitive types, use {@link #addRealmListField(String, Class)} instead. * * @param fieldName name of the field to add. * @param objectSchema schema for the Realm type being referenced. @@ -161,6 +165,34 @@ public String getClassName() { */ public abstract RealmObjectSchema addRealmListField(String fieldName, RealmObjectSchema objectSchema); + /** + * Adds a new field that references a {@link RealmList} with primitive values. See {@link RealmObject} for the + * list of supported types. + *

+ * Nullability of elements are defined by using the correct class e.g., {@code Integer.class} instead of + * {@code int.class}. Alternatively {@link #setRequired(String, boolean)} can be used. + *

+ * Example: + *

+     * {@code
+     * // Defines the list of Strings as being non null.
+     * RealmObjectSchema schema = schema.create("Person")
+     *     .addRealmListField("children", String.class)
+     *     .setRequired("children", true)
+     * }
+     * 
+ * If the list contains references to other Realm classes, use + * {@link #addRealmListField(String, RealmObjectSchema)} instead. + * + * @param fieldName name of the field to add. + * @param primitiveType simple type of elements in the array. + * @return the updated schema. + * @throws IllegalArgumentException if the field name is illegal, a field with that name already exists or + * the element type isn't supported. + * @throws UnsupportedOperationException if this {@link RealmObjectSchema} is immutable. + */ + public abstract RealmObjectSchema addRealmListField(String fieldName, Class primitiveType); + /** * Removes a field from the class. * @@ -525,11 +557,13 @@ protected void copy(ColumnInfo src, ColumnInfo dst) { // Tuple containing data about each supported Java type. static final class FieldMetaData { - final RealmFieldType realmType; + final RealmFieldType fieldType; // Underlying Realm type for fields with this type + final RealmFieldType listType; // Underlying Realm type for RealmLists containing this type final boolean defaultNullable; - FieldMetaData(RealmFieldType realmType, boolean defaultNullable) { - this.realmType = realmType; + FieldMetaData(RealmFieldType fieldType, @Nullable RealmFieldType listType, boolean defaultNullable) { + this.fieldType = fieldType; + this.listType = listType; this.defaultNullable = defaultNullable; } } diff --git a/realm/realm-library/src/main/java/io/realm/internal/CheckedRow.java b/realm/realm-library/src/main/java/io/realm/internal/CheckedRow.java index f3a3e1f97b..115130670b 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/CheckedRow.java +++ b/realm/realm-library/src/main/java/io/realm/internal/CheckedRow.java @@ -97,17 +97,6 @@ public void setNull(long columnIndex) { } } - @Override - public OsList getList(long columnIndex) { - RealmFieldType fieldType = getTable().getColumnType(columnIndex); - if (fieldType != RealmFieldType.LIST) { - throw new IllegalArgumentException( - String.format(Locale.US, "Field '%s' is not a 'RealmList'.", - getTable().getColumnName(columnIndex))); - } - return super.getList(columnIndex); - } - @Override public OsList getModelList(long columnIndex) { RealmFieldType fieldType = getTable().getColumnType(columnIndex); diff --git a/realm/realm-library/src/main/java/io/realm/internal/InvalidRow.java b/realm/realm-library/src/main/java/io/realm/internal/InvalidRow.java index 79510c7e08..42f160d1d9 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/InvalidRow.java +++ b/realm/realm-library/src/main/java/io/realm/internal/InvalidRow.java @@ -104,11 +104,6 @@ public boolean isNullLink(long columnIndex) { throw getStubException(); } - @Override - public OsList getList(long columnIndex) { - throw getStubException(); - } - @Override public OsList getModelList(long columnIndex) { throw getStubException(); diff --git a/realm/realm-library/src/main/java/io/realm/internal/PendingRow.java b/realm/realm-library/src/main/java/io/realm/internal/PendingRow.java index 88ace3a27b..638fc3d5f8 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/PendingRow.java +++ b/realm/realm-library/src/main/java/io/realm/internal/PendingRow.java @@ -133,11 +133,6 @@ public boolean isNullLink(long columnIndex) { throw new IllegalStateException(QUERY_NOT_RETURNED_MESSAGE); } - @Override - public OsList getList(long columnIndex) { - throw new IllegalStateException(QUERY_NOT_RETURNED_MESSAGE); - } - @Override public OsList getModelList(long columnIndex) { throw new IllegalStateException(QUERY_NOT_RETURNED_MESSAGE); diff --git a/realm/realm-library/src/main/java/io/realm/internal/Row.java b/realm/realm-library/src/main/java/io/realm/internal/Row.java index f1a556f057..681d818999 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/Row.java +++ b/realm/realm-library/src/main/java/io/realm/internal/Row.java @@ -81,9 +81,6 @@ public interface Row { boolean isNullLink(long columnIndex); - // FIXME remove this in DynamicRealm PR - OsList getList(long columnIndex); - OsList getModelList(long columnIndex); OsList getValueList(long columnIndex, RealmFieldType fieldType); diff --git a/realm/realm-library/src/main/java/io/realm/internal/Table.java b/realm/realm-library/src/main/java/io/realm/internal/Table.java index 31c004a77d..b68bab8ab0 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/Table.java +++ b/realm/realm-library/src/main/java/io/realm/internal/Table.java @@ -100,7 +100,28 @@ private void verifyColumnName(String name) { */ public long addColumn(RealmFieldType type, String name, boolean isNullable) { verifyColumnName(name); - return nativeAddColumn(nativePtr, type.getNativeValue(), name, isNullable); + switch (type) { + case INTEGER: + case BOOLEAN: + case STRING: + case BINARY: + case DATE: + case FLOAT: + case DOUBLE: + return nativeAddColumn(nativePtr, type.getNativeValue(), name, isNullable); + + case INTEGER_LIST: + case BOOLEAN_LIST: + case STRING_LIST: + case BINARY_LIST: + case DATE_LIST: + case FLOAT_LIST: + case DOUBLE_LIST: + return nativeAddPrimitiveListColumn(nativePtr, type.getNativeValue() - 128, name, isNullable); + + default: + throw new IllegalArgumentException("Unsupported type: " + type); + } } /** @@ -688,6 +709,8 @@ public static String getTableNameForClass(String name) { private native long nativeAddColumn(long nativeTablePtr, int type, String name, boolean isNullable); + private native long nativeAddPrimitiveListColumn(long nativeTablePtr, int type, String name, boolean isNullable); + private native long nativeAddColumnLink(long nativeTablePtr, int type, String name, long targetTablePtr); private native void nativeRenameColumn(long nativeTablePtr, long columnIndex, String name); diff --git a/realm/realm-library/src/main/java/io/realm/internal/UncheckedRow.java b/realm/realm-library/src/main/java/io/realm/internal/UncheckedRow.java index 5c98952c7d..c7e035b89c 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/UncheckedRow.java +++ b/realm/realm-library/src/main/java/io/realm/internal/UncheckedRow.java @@ -172,11 +172,6 @@ public boolean isNullLink(long columnIndex) { return nativeIsNullLink(nativePtr, columnIndex); } - @Override - public OsList getList(long columnIndex) { - return new OsList(this, columnIndex); - } - @Override public OsList getModelList(long columnIndex) { return new OsList(this, columnIndex);