diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5ece9862..ca3cc59469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ ### Enhancements +* Support for primary key properties (for int and string columns). Declaring a property + to be the primary key ensures uniqueness for that property for all objects of a given type. + At the moment indexes on primary keys are not yet supported but this will be added in a future + release. +* Added methods to update or insert (upsert) for objects with primary keys defined. * `[RLMObject initWithObject:]` and `[RLMObject createInRealmWithObject:]` now support any object type with kvc properties. * Improve performance when getting the count of items matching a query but not @@ -21,11 +26,15 @@ * Add an automatic check for updates when using Realm in a simulator (the checker code is not compiled into device builds). This can be disabled by setting the REALM_DISABLE_UPDATE_CHECKER environment variable to any value. +* Add support for Int16 and Int64 properties in Swift classes. ### Bugfixes * Realm change notifications when beginning a write transaction are now sent after updating rather than before, to match refresh. +* `-isEqual:` now uses the default `NSObject` implementation unless a primary key + is specified for an RLMObject. When a primary key is specified, `-isEqual:` calls + `-isEqualToObject:` and a corresponding implementation for `-hash` is also implemented. 0.84.0 Release notes (2014-08-28) ============================================================= diff --git a/Realm-Xcode6.xcodeproj/project.pbxproj b/Realm-Xcode6.xcodeproj/project.pbxproj index 5fb898ab56..7d878889d0 100644 --- a/Realm-Xcode6.xcodeproj/project.pbxproj +++ b/Realm-Xcode6.xcodeproj/project.pbxproj @@ -61,6 +61,8 @@ 3F8DCA7D19930FCB0008BD7F /* SwiftRealmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81A1FD01955FE0100FDED82 /* SwiftRealmTests.swift */; }; 3F8DCA7E19930FCB0008BD7F /* SwiftUnicodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E891759A197A1B600068ACC6 /* SwiftUnicodeTests.swift */; }; 3F8DCA81199310D40008BD7F /* RLMTestObjects.m in Sources */ = {isa = PBXBuildFile; fileRef = E81A1FC71955FE0100FDED82 /* RLMTestObjects.m */; }; + 3FE79FF819BA6A5900780C9A /* RLMSwiftSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 3FE79FF719BA6A5900780C9A /* RLMSwiftSupport.h */; }; + 3FE79FF919BA6A5900780C9A /* RLMSwiftSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 3FE79FF719BA6A5900780C9A /* RLMSwiftSupport.h */; }; E81A1F851955FC9300FDED82 /* RLMAccessor.h in Headers */ = {isa = PBXBuildFile; fileRef = E81A1F631955FC9300FDED82 /* RLMAccessor.h */; }; E81A1F861955FC9300FDED82 /* RLMAccessor.mm in Sources */ = {isa = PBXBuildFile; fileRef = E81A1F641955FC9300FDED82 /* RLMAccessor.mm */; }; E81A1F881955FC9300FDED82 /* RLMArray_Private.hpp in Headers */ = {isa = PBXBuildFile; fileRef = E81A1F651955FC9300FDED82 /* RLMArray_Private.hpp */; }; @@ -311,6 +313,7 @@ 3F8DCA5019930EED0008BD7F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Realm/Tests/TestHost/Info.plist; sourceTree = SOURCE_ROOT; }; 3F8DCA5119930EED0008BD7F /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Realm/Tests/TestHost/main.m; sourceTree = SOURCE_ROOT; }; 3F8DCA5719930F550008BD7F /* iOS Device Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "iOS Device Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3FE79FF719BA6A5900780C9A /* RLMSwiftSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RLMSwiftSupport.h; sourceTree = ""; }; E81A1F621955FC9300FDED82 /* Realm-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Realm-Info.plist"; sourceTree = ""; }; E81A1F631955FC9300FDED82 /* RLMAccessor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RLMAccessor.h; sourceTree = ""; }; E81A1F641955FC9300FDED82 /* RLMAccessor.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RLMAccessor.mm; sourceTree = ""; }; @@ -505,19 +508,6 @@ name = "Objective-C"; sourceTree = ""; }; - E81A20071955FE2200FDED82 /* Helpers */ = { - isa = PBXGroup; - children = ( - E8A543AD195C9C9E00990A20 /* RLMPredicateUtil.h */, - E8A543AE195C9C9E00990A20 /* RLMPredicateUtil.m */, - E81A1FC41955FE0100FDED82 /* RLMTestCase.h */, - E81A1FC51955FE0100FDED82 /* RLMTestCase.m */, - E8F8D90A196CB8DD00475368 /* SwiftTestCase.swift */, - E8F8D90B196CB8DD00475368 /* SwiftTestObjects.swift */, - ); - name = Helpers; - sourceTree = ""; - }; E88C36FE19745E3200C9963D /* Swift */ = { isa = PBXGroup; children = ( @@ -596,6 +586,7 @@ E81A1F7D1955FC9300FDED82 /* RLMSchema_Private.h */, 3F20DA2019BE1EA6007DE308 /* RLMUpdateChecker.hpp */, 3F20DA2119BE1EA6007DE308 /* RLMUpdateChecker.mm */, + 3FE79FF719BA6A5900780C9A /* RLMSwiftSupport.h */, E81A1F811955FC9300FDED82 /* RLMUtil.hpp */, E81A1F821955FC9300FDED82 /* RLMUtil.mm */, ); @@ -614,7 +605,6 @@ E8D89BA71955FC6D00CF2B9A /* Tests */ = { isa = PBXGroup; children = ( - E81A20071955FE2200FDED82 /* Helpers */, E81A20061955FE1600FDED82 /* Objective-C */, E8D89BA81955FC6D00CF2B9A /* Supporting Files */, E81A1FC81955FE0100FDED82 /* Swift */, @@ -638,6 +628,12 @@ children = ( 3F44109E19953F5900223146 /* RLMTestObjects.h */, E81A1FC71955FE0100FDED82 /* RLMTestObjects.m */, + E8A543AD195C9C9E00990A20 /* RLMPredicateUtil.h */, + E8A543AE195C9C9E00990A20 /* RLMPredicateUtil.m */, + E81A1FC41955FE0100FDED82 /* RLMTestCase.h */, + E81A1FC51955FE0100FDED82 /* RLMTestCase.m */, + E8F8D90A196CB8DD00475368 /* SwiftTestCase.swift */, + E8F8D90B196CB8DD00475368 /* SwiftTestObjects.swift */, E8FE4159196DBA7C00073FBB /* TestFramework.h */, ); name = "Test Framework"; @@ -662,6 +658,7 @@ E856D1FC1956154C00FB2FCF /* RLMObject_Private.h in Headers */, E856D2001956154C00FB2FCF /* RLMObjectSchema.h in Headers */, E856D1FF1956154C00FB2FCF /* RLMObjectSchema_Private.hpp in Headers */, + 3FE79FF919BA6A5900780C9A /* RLMSwiftSupport.h in Headers */, E856D2021956154C00FB2FCF /* RLMObjectStore.hpp in Headers */, 024E6094198B2D51002FA042 /* RLMPlatform.h in Headers */, E856D2051956154C00FB2FCF /* RLMProperty.h in Headers */, @@ -703,6 +700,7 @@ E81A1F951955FC9300FDED82 /* RLMObject_Private.h in Headers */, E81A1F9A1955FC9300FDED82 /* RLMObjectSchema.h in Headers */, E81A1F991955FC9300FDED82 /* RLMObjectSchema_Private.hpp in Headers */, + 3FE79FF819BA6A5900780C9A /* RLMSwiftSupport.h in Headers */, E81A1F9D1955FC9300FDED82 /* RLMObjectStore.hpp in Headers */, 024E6097198B2D59002FA042 /* RLMPlatform.h in Headers */, E81A1FA11955FC9300FDED82 /* RLMProperty.h in Headers */, diff --git a/Realm.xcodeproj/project.pbxproj b/Realm.xcodeproj/project.pbxproj index 47b27aee4c..06459537aa 100644 --- a/Realm.xcodeproj/project.pbxproj +++ b/Realm.xcodeproj/project.pbxproj @@ -116,6 +116,9 @@ 02E4D6ED192E58320082808D /* RLMTestObjects.m in Sources */ = {isa = PBXBuildFile; fileRef = 02E4D6EB192E58320082808D /* RLMTestObjects.m */; }; 02F4EAF6195DF0A6008743D9 /* RLMMigration.h in Headers */ = {isa = PBXBuildFile; fileRef = 02026CA119354DDF00E4EEF8 /* RLMMigration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 02F4EAF7195DF0B0008743D9 /* RLMMigration_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 02026CA7193562B400E4EEF8 /* RLMMigration_Private.h */; }; + 3FF51A2819BA25A100F2E9B7 /* RLMSwiftSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 3FF51A2619BA25A100F2E9B7 /* RLMSwiftSupport.h */; }; + 3FF51A2919BA25A100F2E9B7 /* RLMSwiftSupportFallback.m in Sources */ = {isa = PBXBuildFile; fileRef = 3FF51A2719BA25A100F2E9B7 /* RLMSwiftSupportFallback.m */; }; + 3FF51A2A19BA277800F2E9B7 /* RLMSwiftSupportFallback.m in Sources */ = {isa = PBXBuildFile; fileRef = 3FF51A2719BA25A100F2E9B7 /* RLMSwiftSupportFallback.m */; }; 3F80E10119BE2A9400907B21 /* RLMUpdateChecker.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F80E10019BE2A9400907B21 /* RLMUpdateChecker.mm */; }; 3F80E10219BE2A9400907B21 /* RLMUpdateChecker.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F80E10019BE2A9400907B21 /* RLMUpdateChecker.mm */; }; 3F80E10419BE2AAA00907B21 /* RLMUpdateChecker.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3F80E10319BE2AAA00907B21 /* RLMUpdateChecker.hpp */; }; @@ -225,6 +228,8 @@ 02E4D6E8192E58250082808D /* MixedTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MixedTests.m; sourceTree = ""; }; 02E4D6EB192E58320082808D /* RLMTestObjects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RLMTestObjects.m; sourceTree = ""; }; 02E4D6EE192E583A0082808D /* RLMTestObjects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RLMTestObjects.h; sourceTree = ""; }; + 3FF51A2619BA25A100F2E9B7 /* RLMSwiftSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RLMSwiftSupport.h; sourceTree = ""; }; + 3FF51A2719BA25A100F2E9B7 /* RLMSwiftSupportFallback.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RLMSwiftSupportFallback.m; sourceTree = ""; }; 3F80E10019BE2A9400907B21 /* RLMUpdateChecker.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RLMUpdateChecker.mm; sourceTree = ""; }; 3F80E10319BE2AAA00907B21 /* RLMUpdateChecker.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RLMUpdateChecker.hpp; sourceTree = ""; }; 429E395319409432001DC9C1 /* libc++.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libc++.dylib"; path = "usr/lib/libc++.dylib"; sourceTree = SDKROOT; }; @@ -337,6 +342,8 @@ 02E4D6A6192E3DC40082808D /* RLMSchema.h */, 02E4D6A7192E3DC40082808D /* RLMSchema.mm */, 02E4D6A5192E3DC40082808D /* RLMSchema_Private.h */, + 3FF51A2619BA25A100F2E9B7 /* RLMSwiftSupport.h */, + 3FF51A2719BA25A100F2E9B7 /* RLMSwiftSupportFallback.m */, 3F80E10319BE2AAA00907B21 /* RLMUpdateChecker.hpp */, 3F80E10019BE2A9400907B21 /* RLMUpdateChecker.mm */, 02E4D6A8192E3DC40082808D /* RLMUtil.hpp */, @@ -454,6 +461,7 @@ 02E4D6B2192E3DC40082808D /* RLMArray.h in Headers */, 02E4D6B0192E3DC40082808D /* RLMArray_Private.hpp in Headers */, 02E4D6BA192E3DC40082808D /* RLMConstants.h in Headers */, + 3FF51A2819BA25A100F2E9B7 /* RLMSwiftSupport.h in Headers */, 02F4EAF6195DF0A6008743D9 /* RLMMigration.h in Headers */, 02F4EAF7195DF0B0008743D9 /* RLMMigration_Private.h in Headers */, 02E4D6C0192E3DC40082808D /* RLMObject.h in Headers */, @@ -710,6 +718,7 @@ 02E4D6AE192E3DC40082808D /* RLMAccessor.mm in Sources */, 022C8BE9194237BE00BCDB9D /* RLMArray.mm in Sources */, 022C8BEA194237BE00BCDB9D /* RLMArrayLinkView.mm in Sources */, + 3FF51A2919BA25A100F2E9B7 /* RLMSwiftSupportFallback.m in Sources */, 02E4D6B4192E3DC40082808D /* RLMArrayTableView.mm in Sources */, 02E4D6BC192E3DC40082808D /* RLMConstants.m in Sources */, 02026CA519354DDF00E4EEF8 /* RLMMigration.mm in Sources */, @@ -757,6 +766,7 @@ 02E4D6AF192E3DC40082808D /* RLMAccessor.mm in Sources */, 022C8BEB194237C400BCDB9D /* RLMArray.mm in Sources */, 022C8BEC194237C400BCDB9D /* RLMArrayLinkView.mm in Sources */, + 3FF51A2A19BA277800F2E9B7 /* RLMSwiftSupportFallback.m in Sources */, 02E4D6B5192E3DC40082808D /* RLMArrayTableView.mm in Sources */, 02E4D6BD192E3DC40082808D /* RLMConstants.m in Sources */, 02026CA619354DDF00E4EEF8 /* RLMMigration.mm in Sources */, diff --git a/Realm/RLMAccessor.h b/Realm/RLMAccessor.h index 4b5afdcca0..07fcbea4b6 100644 --- a/Realm/RLMAccessor.h +++ b/Realm/RLMAccessor.h @@ -35,7 +35,7 @@ void RLMDynamicValidatedSet(RLMObject *obj, NSString *propName, id val); id RLMDynamicGet(RLMObject *obj, NSString *propName); // by property/column -void RLMDynamicSet(RLMObject *obj, RLMProperty *prop, id val); +void RLMDynamicSet(RLMObject *obj, RLMProperty *prop, id val, bool enforceUnique, bool tryUpdate); // // Class modification diff --git a/Realm/RLMAccessor.mm b/Realm/RLMAccessor.mm index 8b00890158..b427214027 100644 --- a/Realm/RLMAccessor.mm +++ b/Realm/RLMAccessor.mm @@ -20,7 +20,7 @@ #import "RLMProperty_Private.h" #import "RLMArray_Private.hpp" #import "RLMUtil.hpp" -#import "RLMObjectSchema.h" +#import "RLMObjectSchema_Private.hpp" #import "RLMObjectStore.hpp" #import @@ -52,17 +52,30 @@ static inline long long RLMGetLong(__unsafe_unretained RLMObject *obj, NSUIntege RLMVerifyAttached(obj); return obj->_row.get_int(colIndex); } -static inline void RLMSetLong(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, long long val) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, long long val) { RLMVerifyInWriteTransaction(obj); obj->_row.set_int(colIndex, val); } +static inline void RLMSetValueUnique(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, NSString *propName, long long val) { + RLMVerifyInWriteTransaction(obj); + size_t row = obj->_row.get_table()->find_first_int(colIndex, val); + if (row == obj->_row.get_index()) { + return; + } + if (row != tightdb::not_found) { + NSString *reason = [NSString stringWithFormat:@"Setting primary key with existing value '%lld' for property '%@'", + val, propName]; + @throw [NSException exceptionWithName:@"RLMException" reason:reason userInfo:nil]; + } + obj->_row.set_int(colIndex, val); +} // float getter/setter static inline float RLMGetFloat(__unsafe_unretained RLMObject *obj, NSUInteger colIndex) { RLMVerifyAttached(obj); return obj->_row.get_float(colIndex); } -static inline void RLMSetFloat(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, float val) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, float val) { RLMVerifyInWriteTransaction(obj); obj->_row.set_float(colIndex, val); } @@ -72,7 +85,7 @@ static inline double RLMGetDouble(__unsafe_unretained RLMObject *obj, NSUInteger RLMVerifyAttached(obj); return obj->_row.get_double(colIndex); } -static inline void RLMSetDouble(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, double val) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, double val) { RLMVerifyInWriteTransaction(obj); obj->_row.set_double(colIndex, val); } @@ -82,7 +95,7 @@ static inline bool RLMGetBool(__unsafe_unretained RLMObject *obj, NSUInteger col RLMVerifyAttached(obj); return obj->_row.get_bool(colIndex); } -static inline void RLMSetBool(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, bool val) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, bool val) { RLMVerifyInWriteTransaction(obj); obj->_row.set_bool(colIndex, val); } @@ -92,10 +105,25 @@ static inline void RLMSetBool(__unsafe_unretained RLMObject *obj, NSUInteger col RLMVerifyAttached(obj); return RLMStringDataToNSString(obj->_row.get_string(colIndex)); } -static inline void RLMSetString(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained NSString *val) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained NSString *val) { RLMVerifyInWriteTransaction(obj); obj->_row.set_string(colIndex, RLMStringDataWithNSString(val)); } +static inline void RLMSetValueUnique(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, NSString *propName, + __unsafe_unretained NSString *val) { + RLMVerifyInWriteTransaction(obj); + tightdb::StringData str = RLMStringDataWithNSString(val); + size_t row = obj->_row.get_table()->find_first_string(colIndex, str); + if (row == obj->_row.get_index()) { + return; + } + if (row != tightdb::not_found) { + NSString *reason = [NSString stringWithFormat:@"Setting unique property '%@' with existing value '%@'", + val, propName]; + @throw [NSException exceptionWithName:@"RLMException" reason:reason userInfo:nil]; + } + obj->_row.set_string(colIndex, str); +} // date getter/setter static inline NSDate *RLMGetDate(__unsafe_unretained RLMObject *obj, NSUInteger colIndex) { @@ -103,7 +131,7 @@ static inline void RLMSetString(__unsafe_unretained RLMObject *obj, NSUInteger c tightdb::DateTime dt = obj->_row.get_datetime(colIndex); return [NSDate dateWithTimeIntervalSince1970:dt.get_datetime()]; } -static inline void RLMSetDate(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained NSDate *date) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained NSDate *date) { RLMVerifyInWriteTransaction(obj); std::time_t time = date.timeIntervalSince1970; obj->_row.set_datetime(colIndex, tightdb::DateTime(time)); @@ -115,7 +143,7 @@ static inline void RLMSetDate(__unsafe_unretained RLMObject *obj, NSUInteger col tightdb::BinaryData data = obj->_row.get_binary(colIndex); return [NSData dataWithBytes:data.data() length:data.size()]; } -static inline void RLMSetData(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained NSData *data) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained NSData *data) { RLMVerifyInWriteTransaction(obj); obj->_row.set_binary(colIndex, RLMBinaryDataForNSData(data)); } @@ -130,10 +158,10 @@ static inline void RLMSetData(__unsafe_unretained RLMObject *obj, NSUInteger col NSUInteger index = obj->_row.get_link(colIndex); return RLMCreateObjectAccessor(obj.realm, objectClassName, index); } -static inline void RLMSetLink(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained id val) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained RLMObject *val, bool tryUpdate = false) { RLMVerifyInWriteTransaction(obj); - if (!val || val == NSNull.null) { + if (!val || (id)val == NSNull.null) { // if null obj->_row.nullify_link(colIndex); } @@ -141,7 +169,13 @@ static inline void RLMSetLink(__unsafe_unretained RLMObject *obj, NSUInteger col // add to Realm if not in it. RLMObject *link = val; if (link.realm != obj.realm) { - [obj.realm addObject:link]; + // only try to update if link object has primary key + if (tryUpdate && link.objectSchema.primaryKeyProperty) { + [obj.realm addOrUpdateObject:link]; + } + else { + [obj.realm addObject:link]; + } } // set link obj->_row.set_link(colIndex, link->_row.get_index()); @@ -158,7 +192,7 @@ static inline void RLMSetLink(__unsafe_unretained RLMObject *obj, NSUInteger col realm:obj.realm]; return ar; } -static inline void RLMSetArray(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained id val) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger colIndex, __unsafe_unretained id val, bool tryUpdate = false) { RLMVerifyInWriteTransaction(obj); tightdb::LinkViewRef linkView = obj->_row.get_linklist(colIndex); @@ -168,7 +202,13 @@ static inline void RLMSetArray(__unsafe_unretained RLMObject *obj, NSUInteger co for (RLMObject *link in val) { // add to realm if needed if (link.realm != obj.realm) { - [obj.realm addObject:link]; + // only try to update if link object has primary key + if (tryUpdate && link.objectSchema.primaryKeyProperty) { + [obj.realm addOrUpdateObject:link]; + } + else { + [obj.realm addObject:link]; + } } // set in link view linkView->add(link->_row.get_index()); @@ -209,7 +249,7 @@ static inline id RLMGetAnyProperty(__unsafe_unretained RLMObject *obj, NSUIntege } } } -static inline void RLMSetAnyProperty(__unsafe_unretained RLMObject *obj, NSUInteger col_ndx, __unsafe_unretained id val) { +static inline void RLMSetValue(__unsafe_unretained RLMObject *obj, NSUInteger col_ndx, __unsafe_unretained id val) { RLMVerifyInWriteTransaction(obj); // FIXME - enable when Any supports links @@ -256,14 +296,22 @@ static inline void RLMSetAnyProperty(__unsafe_unretained RLMObject *obj, NSUInte static IMP RLMAccessorGetter(RLMProperty *prop, char accessorCode, NSString *objectClassName) { NSUInteger colIndex = prop.column; switch (accessorCode) { + case 's': + return imp_implementationWithBlock(^(RLMObject *obj) { + return (short)RLMGetLong(obj, colIndex); + }); case 'i': return imp_implementationWithBlock(^(RLMObject *obj) { return (int)RLMGetLong(obj, colIndex); }); - case 'l': + case 'q': return imp_implementationWithBlock(^(RLMObject *obj) { return RLMGetLong(obj, colIndex); }); + case 'l': + return imp_implementationWithBlock(^(RLMObject *obj) { + return (long)RLMGetLong(obj, colIndex); + }); case 'f': return imp_implementationWithBlock(^(RLMObject *obj) { return RLMGetFloat(obj, colIndex); @@ -277,7 +325,7 @@ static IMP RLMAccessorGetter(RLMProperty *prop, char accessorCode, NSString *obj return imp_implementationWithBlock(^(RLMObject *obj) { return RLMGetBool(obj, colIndex); }); - case 's': + case 'S': return imp_implementationWithBlock(^(RLMObject *obj) { return RLMGetString(obj, colIndex); }); @@ -306,58 +354,38 @@ static IMP RLMAccessorGetter(RLMProperty *prop, char accessorCode, NSString *obj } } +template +static IMP RLMMakeSetter(NSUInteger colIndex, bool isPrimary) { + if (isPrimary) { + return imp_implementationWithBlock(^(__unused RLMObject *obj, __unused ArgType val) { + @throw [NSException exceptionWithName:@"RLMException" + reason:@"Primary key can't be changed after an object is inserted." + userInfo:nil]; + }); + } + return imp_implementationWithBlock(^(RLMObject *obj, ArgType val) { + RLMSetValue(obj, colIndex, static_cast(val)); + }); +} + // dynamic setter with column closure static IMP RLMAccessorSetter(RLMProperty *prop, char accessorCode) { NSUInteger colIndex = prop.column; switch (accessorCode) { - case 'i': - return imp_implementationWithBlock(^(RLMObject *obj, int val) { - RLMSetLong(obj, colIndex, val); - }); - case 'l': - return imp_implementationWithBlock(^(RLMObject *obj, long val) { - RLMSetLong(obj, colIndex, val); - }); - case 'f': - return imp_implementationWithBlock(^(RLMObject *obj, float val) { - RLMSetFloat(obj, colIndex, val); - }); - case 'd': - return imp_implementationWithBlock(^(RLMObject *obj, double val) { - RLMSetDouble(obj, colIndex, val); - }); - case 'B': - return imp_implementationWithBlock(^(RLMObject *obj, bool val) { - RLMSetBool(obj, colIndex, val); - }); - case 'c': - return imp_implementationWithBlock(^(RLMObject *obj, BOOL val) { - RLMSetBool(obj, colIndex, val); - }); - case 's': - return imp_implementationWithBlock(^(RLMObject *obj, NSString *val) { - RLMSetString(obj, colIndex, val); - }); - case 'a': - return imp_implementationWithBlock(^(RLMObject *obj, NSDate *date) { - RLMSetDate(obj, colIndex, date); - }); - case 'e': - return imp_implementationWithBlock(^(RLMObject *obj, NSData *data) { - RLMSetData(obj, colIndex, data); - }); - case 'k': - return imp_implementationWithBlock(^(RLMObject *obj, RLMObject *link) { - RLMSetLink(obj, colIndex, link); - }); - case 't': - return imp_implementationWithBlock(^(RLMObject *obj, RLMArray *val) { - RLMSetArray(obj, colIndex, val); - }); - case '@': - return imp_implementationWithBlock(^(RLMObject *obj, id val) { - RLMSetAnyProperty(obj, colIndex, val); - }); + case 's': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'i': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'l': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'q': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'f': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'd': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'B': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'c': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'S': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'a': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'e': return RLMMakeSetter(colIndex, prop.isPrimary); + case 'k': return RLMMakeSetter(colIndex, prop.isPrimary); + case 't': return RLMMakeSetter(colIndex, prop.isPrimary); + case '@': return RLMMakeSetter(colIndex, prop.isPrimary); default: @throw [NSException exceptionWithName:@"RLMException" reason:@"Invalid accessor code" @@ -425,8 +453,10 @@ static IMP RLMAccessorStandaloneSetter(RLMProperty *prop, char accessorCode) { // the @ type maps to multiple tightdb types (string, date, array, mixed, any which are id in objc) static const char *getterTypeStringForObjcCode(char code) { switch (code) { + case 's': return GETTER_TYPES("s"); case 'i': return GETTER_TYPES("i"); case 'l': return GETTER_TYPES("l"); + case 'q': return GETTER_TYPES("q"); case 'f': return GETTER_TYPES("f"); case 'd': return GETTER_TYPES("d"); case 'B': return GETTER_TYPES("B"); @@ -441,8 +471,10 @@ static IMP RLMAccessorStandaloneSetter(RLMProperty *prop, char accessorCode) { // the @ type maps to multiple tightdb types (string, date, array, mixed, any which are id in objc) static const char *setterTypeStringForObjcCode(char code) { switch (code) { + case 's': return SETTER_TYPES("s"); case 'i': return SETTER_TYPES("i"); case 'l': return SETTER_TYPES("l"); + case 'q': return SETTER_TYPES("q"); case 'f': return SETTER_TYPES("f"); case 'd': return SETTER_TYPES("d"); case 'B': return SETTER_TYPES("B"); @@ -455,11 +487,10 @@ static IMP RLMAccessorStandaloneSetter(RLMProperty *prop, char accessorCode) { // get accessor lookup code based on objc type and rlm type static char accessorCodeForType(char objcTypeCode, RLMPropertyType rlmType) { switch (objcTypeCode) { - case 'q': return 'l'; // long long same as long case '@': // custom accessors for strings and subtables switch (rlmType) { // custom accessor codes for types that map to objc objects case RLMPropertyTypeObject: return 'k'; - case RLMPropertyTypeString: return 's'; + case RLMPropertyTypeString: return 'S'; case RLMPropertyTypeArray: return 't'; case RLMPropertyTypeDate: return 'a'; case RLMPropertyTypeData: return 'e'; @@ -559,7 +590,8 @@ Class RLMStandaloneAccessorClassForObjectClass(Class objectClass, RLMObjectSchem } void RLMDynamicValidatedSet(RLMObject *obj, NSString *propName, id val) { - RLMProperty *prop = obj.objectSchema[propName]; + RLMObjectSchema *schema = obj.objectSchema; + RLMProperty *prop = schema[propName]; if (!prop) { @throw [NSException exceptionWithName:@"RLMException" reason:@"Invalid property name" @@ -572,43 +604,56 @@ void RLMDynamicValidatedSet(RLMObject *obj, NSString *propName, id val) { userInfo:@{@"Property name:" : propName ?: @"nil", @"Value": val ? [val description] : @"nil"}]; } - RLMDynamicSet(obj, (RLMProperty *)prop, val); + RLMDynamicSet(obj, prop, val, prop.isPrimary, false); } -void RLMDynamicSet(__unsafe_unretained RLMObject *obj, __unsafe_unretained RLMProperty *prop, __unsafe_unretained id val) { +void RLMDynamicSet(__unsafe_unretained RLMObject *obj, __unsafe_unretained RLMProperty *prop, __unsafe_unretained id val, + bool enforceUnique, bool tryUpdate) { NSUInteger col = prop.column; switch (accessorCodeForType(prop.objcType, prop.type)) { + case 's': case 'i': case 'l': - RLMSetLong(obj, col, [val longLongValue]); + case 'q': + if (enforceUnique) { + RLMSetValueUnique(obj, col, prop.name, [val longLongValue]); + } + else { + RLMSetValue(obj, col, [val longLongValue]); + } break; case 'f': - RLMSetFloat(obj, col, [val floatValue]); + RLMSetValue(obj, col, [val floatValue]); break; case 'd': - RLMSetDouble(obj, col, [val doubleValue]); + RLMSetValue(obj, col, [val doubleValue]); break; case 'B': case 'c': - RLMSetBool(obj, col, (bool)[val boolValue]); + RLMSetValue(obj, col, (bool)[val boolValue]); break; - case 's': - RLMSetString(obj, col, val); + case 'S': + if (enforceUnique) { + RLMSetValueUnique(obj, col, prop.name, (NSString *)val); + } + else { + RLMSetValue(obj, col, (NSString *)val); + } break; case 'a': - RLMSetDate(obj, col, val); + RLMSetValue(obj, col, (NSDate *)val); break; case 'e': - RLMSetData(obj, col, val); + RLMSetValue(obj, col, (NSData *)val); break; case 'k': - RLMSetLink(obj, col, val); + RLMSetValue(obj, col, (RLMObject *)val, tryUpdate); break; case 't': - RLMSetArray(obj, col, val); + RLMSetValue(obj, col, (RLMArray *)val, tryUpdate); break; case '@': - RLMSetAnyProperty(obj, col, val); + RLMSetValue(obj, col, val); break; default: @throw [NSException exceptionWithName:@"RLMException" @@ -627,13 +672,15 @@ id RLMDynamicGet(__unsafe_unretained RLMObject *obj, __unsafe_unretained NSStrin } NSUInteger col = prop.column; switch (accessorCodeForType(prop.objcType, prop.type)) { + case 's': return @((short)RLMGetLong(obj, col)); case 'i': return @((int)RLMGetLong(obj, col)); - case 'l': return @(RLMGetLong(obj, col)); + case 'l': return @((long)RLMGetLong(obj, col)); + case 'q': return @(RLMGetLong(obj, col)); case 'f': return @(RLMGetFloat(obj, col)); case 'd': return @(RLMGetDouble(obj, col)); case 'B': return @(RLMGetBool(obj, col)); case 'c': return @(RLMGetBool(obj, col)); - case 's': return RLMGetString(obj, col); + case 'S': return RLMGetString(obj, col); case 'a': return RLMGetDate(obj, col); case 'e': return RLMGetData(obj, col); case 'k': return RLMGetLink(obj, col, prop.objectClassName); diff --git a/Realm/RLMArray.mm b/Realm/RLMArray.mm index ddc908e853..15d6f9948a 100644 --- a/Realm/RLMArray.mm +++ b/Realm/RLMArray.mm @@ -146,7 +146,14 @@ - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject { - (NSUInteger)indexOfObject:(RLMObject *)object { RLMValidateMatchingObjectType(self, object); - return [_backingArray indexOfObject:object]; + NSUInteger index = 0; + for (RLMObject *cmp in _backingArray) { + if ([object isEqualToObject:cmp]) { + return index; + } + index++; + } + return NSNotFound; } - (void)deleteObjectsFromRealm { diff --git a/Realm/RLMConstants.h b/Realm/RLMConstants.h index dec2d5723d..c5304b5fd2 100644 --- a/Realm/RLMConstants.h +++ b/Realm/RLMConstants.h @@ -24,7 +24,7 @@ Attributes which can be returned when implementing attributesForProperty: */ -typedef NS_ENUM(NSUInteger, RLMPropertyAttributes) { +typedef NS_OPTIONS(NSUInteger, RLMPropertyAttributes) { /** Create an index for this property for improved search performance. */ diff --git a/Realm/RLMMigration.mm b/Realm/RLMMigration.mm index e4917c7c48..2eda6cecbe 100644 --- a/Realm/RLMMigration.mm +++ b/Realm/RLMMigration.mm @@ -18,7 +18,9 @@ #import "RLMMigration_Private.h" #import "RLMRealm_Private.hpp" +#import "RLMProperty_Private.h" #import "RLMSchema_Private.h" +#import "RLMObjectSchema_Private.hpp" #import "RLMObject_Private.h" #import "RLMObjectStore.hpp" #import "RLMArray.h" @@ -93,27 +95,69 @@ - (void)enumerateObjects:(NSString *)className block:(RLMObjectMigrationBlock)bl } } +- (void)verifyPrimaryKeyUniqueness { + for (RLMObjectSchema *objectSchema in _realm.schema.objectSchema) { + // if we have a new primary key not equal to our old one, verify uniqueness + RLMProperty *primaryProperty = objectSchema.primaryKeyProperty; + RLMProperty *oldPrimaryProperty = [[_oldRealm.schema schemaForClassName:objectSchema.className] primaryKeyProperty]; + if (primaryProperty && primaryProperty != oldPrimaryProperty) { + // FIXME: replace with count of distinct once we support indexing + + // FIXME: support other types + tightdb::TableRef &table = objectSchema->_table; + NSUInteger count = table->size(); + if (primaryProperty.type == RLMPropertyTypeString) { + for (NSUInteger i = 0; i < count; i++) { + if (table->count_string(primaryProperty.column, table->get_string(primaryProperty.column, i)) > 1) { + NSString *reason = [NSString stringWithFormat:@"Primary key property '%@' has duplicate values after migration.", primaryProperty.name]; + @throw [NSException exceptionWithName:@"RLMException" reason:reason userInfo:nil]; + } + } + } + else { + for (NSUInteger i = 0; i < count; i++) { + if (table->count_int(primaryProperty.column, table->get_int(primaryProperty.column, i)) > 1) { + NSString *reason = [NSString stringWithFormat:@"Primary key property '%@' has duplicate values after migration.", primaryProperty.name]; + @throw [NSException exceptionWithName:@"RLMException" reason:reason userInfo:nil]; + } + } + } + } + } +} + - (void)migrateWithBlock:(RLMMigrationBlock)block { // start write transaction [_realm beginWriteTransaction]; - // add new tables/columns for the current shared schema - bool changed = RLMRealmSetSchema(_realm, [RLMSchema sharedSchema], true); + @try { + // add new tables/columns for the current shared schema + bool changed = RLMRealmSetSchema(_realm, [RLMSchema sharedSchema], true); - // apply block and set new schema version - NSUInteger oldVersion = RLMRealmSchemaVersion(_realm); - NSUInteger newVersion = block(self, oldVersion); - RLMRealmSetSchemaVersion(_realm, newVersion); + // disable all primary keys for migration + for (RLMObjectSchema *objectSchema in _realm.schema.objectSchema) { + objectSchema.primaryKeyProperty.isPrimary = NO; + } - // make sure a new version was provided if changes were made - if (changed && oldVersion >= newVersion) { - @throw [NSException exceptionWithName:@"RLMException" - reason:@"Migration block should return a higher version after a schema update" - userInfo:@{@"path" : _realm.path}]; - } + // apply block and set new schema version + NSUInteger oldVersion = RLMRealmSchemaVersion(_realm); + NSUInteger newVersion = block(self, oldVersion); + RLMRealmSetSchemaVersion(_realm, newVersion); + + // make sure a new version was provided if changes were made + if (changed && oldVersion >= newVersion) { + @throw [NSException exceptionWithName:@"RLMException" + reason:@"Migration block should return a higher version after a schema update" + userInfo:@{@"path" : _realm.path}]; + } - // end transaction - [_realm commitWriteTransaction]; + // verify uniqueness for any new unique columns before committing + [self verifyPrimaryKeyUniqueness]; + } + @finally { + // end transaction + [_realm commitWriteTransaction]; + } } @end diff --git a/Realm/RLMObject.h b/Realm/RLMObject.h index 0518177442..43a90c0e4f 100644 --- a/Realm/RLMObject.h +++ b/Realm/RLMObject.h @@ -141,6 +141,43 @@ */ +(instancetype)createInRealm:(RLMRealm *)realm withObject:(id)object; +/** + Create or update an RLMObject in the default Realm with a a given object. + + This method can only be called on object types with a primary key defined. If there is already + an object with the same primary key value in the default RLMRealm its values are updated and the object + is returned. Otherwise this creates and populates a new instance of this object in the default Realm. + + @param object The object used to populate the object. This can be any key/value compliant + object, or a JSON object such as those returned from the methods in NSJSONSerialization, or + an NSArray with one object for each persisted property. An exception will be + thrown if all required properties are not present or no default is provided. + + When passing in an NSArray, all properties must be present, valid and in the same order as the properties defined in the model. + + @see defaultPropertyValues, primaryKey + */ ++(instancetype)createOrUpdateInDefaultRealmWithObject:(id)object; + +/** + Create or update an RLMObject with a a given object. + + This method can only be called on object types with a primary key defined. If there is already + an object with the same primary key value in the provided RLMRealm its values are updated and the object + is returned. Otherwise this creates and populates a new instance of this object in the provided Realm. + + @param realm The Realm in which this object is persisted. + @param object The object used to populate the object. This can be any key/value compliant + object, or a JSON object such as those returned from the methods in NSJSONSerialization, or + an NSArray with one object for each persisted property. An exception will be + thrown if all required properties are not present or no default is provided. + + When passing in an NSArray, all properties must be present, valid and in the same order as the properties defined in the model. + + @see defaultPropertyValues, primaryKey + */ ++(instancetype)createOrUpdateInRealm:(RLMRealm *)realm withObject:(id)object; + /** The Realm in which this object is persisted. Returns nil for standalone objects. */ @@ -177,6 +214,15 @@ */ + (NSDictionary *)defaultPropertyValues; +/** + Implement to designate a property as the primary key for a RLMObject subclass. Only properties of + type RLMPropertyTypeString and RLMPropertyTypeInt can be designated as the primary key. Primary key + properties enforce uniqueness for each value whenever the property is set which incurs some overhead. + + @return Name of the property designated as the primary key. + */ ++ (NSString *)primaryKey; + /** Implement to return an array of property names to ignore. These properties will not be persisted and are treated as transient. @@ -252,6 +298,16 @@ */ + (RLMArray *)objectsInRealm:(RLMRealm *)realm withPredicate:(NSPredicate *)predicate; +/** + Returns YES if another RLMObject points to the same object in a RLMRealm. For RLMObject types + with a primary, key, `isEqual:` is overridden to use this method (along with a corresponding + implementation for `hash`. + + @param object The object to compare to. + + @return YES if the object represents the same object in the same RLMRealm. + */ +- (BOOL)isEqualToObject:(RLMObject *)object; #pragma mark - @@ -264,8 +320,8 @@ // id object = rlmObject[@"propertyName"]; // --(id)objectForKeyedSubscript:(NSString *)key; --(void)setObject:(id)obj forKeyedSubscript:(NSString *)key; +- (id)objectForKeyedSubscript:(NSString *)key; +- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key; #pragma mark - diff --git a/Realm/RLMObject.mm b/Realm/RLMObject.mm index 4ca9e8a750..b072614195 100644 --- a/Realm/RLMObject.mm +++ b/Realm/RLMObject.mm @@ -22,10 +22,7 @@ #import "RLMObjectStore.hpp" #import "RLMQueryUtil.hpp" #import "RLMUtil.hpp" - -#if REALM_SWIFT -#import -#endif +#import "RLMSwiftSupport.h" #import @@ -97,6 +94,14 @@ +(instancetype)createInRealm:(RLMRealm *)realm withObject:(id)value { return RLMCreateObjectInRealmWithValue(realm, [self className], value); } ++(instancetype)createOrUpdateInDefaultRealmWithObject:(id)object { + return RLMCreateObjectInRealmWithValue([RLMRealm defaultRealm], [self className], object, true); +} + ++(instancetype)createOrUpdateInRealm:(RLMRealm *)realm withObject:(id)value { + return RLMCreateObjectInRealmWithValue(realm, [self className], value, true); +} + // default attributes for property implementation #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-parameter" @@ -116,6 +121,11 @@ + (NSArray *)ignoredProperties { return nil; } +// default primaryKey implementation ++ (NSString *)primaryKey { + return nil; +} + -(id)objectForKeyedSubscript:(NSString *)key { if (_realm) { return RLMDynamicGet(self, key); @@ -178,11 +188,9 @@ - (NSString *)JSONString { // overridden at runtime per-class for performance + (NSString *)className { NSString *className = NSStringFromClass(self); -#if REALM_SWIFT if ([RLMSwiftSupport isSwiftClassName:className]) { className = [RLMSwiftSupport demangleClassName:className]; } -#endif return className; } @@ -244,7 +252,24 @@ - (BOOL)isEqualToObject:(RLMObject *)object { } - (BOOL)isEqual:(id)object { - return [self isEqualToObject:object]; + if (_objectSchema.primaryKeyProperty) { + return [self isEqualToObject:object]; + } + else { + return [super isEqual:object]; + } +} + +- (NSUInteger)hash { + if (_objectSchema.primaryKeyProperty) { + id primaryProperty = [self valueForKey:_objectSchema.primaryKeyProperty.name]; + + // modify the hash of our primary key value to avoid potential (although unlikely) collisions + return [primaryProperty hash] ^ 1; + } + else { + return [super hash]; + } } @end diff --git a/Realm/RLMObjectSchema.h b/Realm/RLMObjectSchema.h index 63b572824e..fd610bfa95 100644 --- a/Realm/RLMObjectSchema.h +++ b/Realm/RLMObjectSchema.h @@ -41,6 +41,11 @@ */ @property (nonatomic, readonly) NSString *className; +/** + The property which is the primary key for this object (if any). + */ +@property (nonatomic, readonly) RLMProperty *primaryKeyProperty; + /** Retrieve an RLMProperty object by name. diff --git a/Realm/RLMObjectSchema.mm b/Realm/RLMObjectSchema.mm index 6646e34ea5..5eae5bf170 100644 --- a/Realm/RLMObjectSchema.mm +++ b/Realm/RLMObjectSchema.mm @@ -17,9 +17,10 @@ //////////////////////////////////////////////////////////////////////////// #import "RLMObjectSchema_Private.hpp" +#import "RLMObject_Private.h" #import "RLMProperty_Private.h" #import "RLMSchema_Private.h" -#import "RLMObject_Private.h" +#import "RLMSwiftSupport.h" #import "RLMUtil.hpp" #import @@ -30,7 +31,6 @@ @interface RLMObjectSchema () @property (nonatomic, readwrite, assign) NSString *className; @end - @implementation RLMObjectSchema - (instancetype)initWithClassName:(NSString *)objectClassName objectClass:(Class)objectClass properties:(NSArray *)properties { @@ -56,42 +56,97 @@ -(void)setProperties:(NSArray *)properties { _properties = properties; } -+(instancetype)schemaForObjectClass:(Class)objectClass { - // get object properties +- (void)setPrimaryKeyProperty:(RLMProperty *)primaryKeyProperty { + _primaryKeyProperty.isPrimary = NO; + primaryKeyProperty.isPrimary = YES; + _primaryKeyProperty = primaryKeyProperty; +} + ++ (instancetype)schemaForObjectClass:(Class)objectClass { + return [self schemaForObjectClass:objectClass createAccessors:NO]; +} + ++ (instancetype)schemaForObjectClass:(Class)objectClass createAccessors:(BOOL)create { + RLMObjectSchema *schema = [RLMObjectSchema new]; + schema.className = [objectClass className]; + schema.objectClass = objectClass; + + // create array of RLMProperties + if ([RLMSwiftSupport isSwiftClassName:NSStringFromClass(objectClass)]) { + schema.properties = [RLMSwiftSupport propertiesForClass:objectClass]; + } + else { + schema.properties = [self propertiesForClass:objectClass]; + } + + if (NSString *primaryKey = [objectClass primaryKey]) { + for (RLMProperty *prop in schema.properties) { + if ([primaryKey isEqualToString:prop.name]) { + // FIXME - re-enable when we have core suppport + //attr = attr | RLMPropertyAttributeIndexed; + schema.primaryKeyProperty = prop; + break; + } + } + + if (!schema.primaryKeyProperty) { + NSString *message = [NSString stringWithFormat:@"Primary key property '%@' does not exist on object '%@'", + primaryKey, schema.className]; + @throw [NSException exceptionWithName:@"RLMException" reason:message userInfo:nil]; + } + if (schema.primaryKeyProperty.type != RLMPropertyTypeInt && schema.primaryKeyProperty.type != RLMPropertyTypeString) { + @throw [NSException exceptionWithName:@"RLMException" + reason:@"Only 'string' and 'int' properties can be designated the primary key" + userInfo:nil]; + } + } + + if (create) { + schema.standaloneClass = RLMStandaloneAccessorClassForObjectClass(objectClass, schema); + + RLMReplaceSharedSchemaMethod(objectClass, schema); + RLMReplaceClassNameMethod(objectClass, schema.className); + } + + return schema; +} + ++ (NSArray *)propertiesForClass:(Class)objectClass { + NSArray *ignoredProperties = [objectClass ignoredProperties]; + unsigned int count; objc_property_t *props = class_copyPropertyList(objectClass, &count); - - // create array of RLMProperties NSMutableArray *propArray = [NSMutableArray arrayWithCapacity:count]; for (unsigned int i = 0; i < count; i++) { - NSString *propertyName = [NSString stringWithUTF8String:property_getName(props[i])]; - BOOL ignored = [[objectClass ignoredProperties] containsObject:propertyName]; - - if (!ignored) { // Don't process ignored properties - RLMPropertyAttributes attr = [objectClass attributesForProperty:propertyName]; - RLMProperty *prop = [RLMProperty propertyForObjectProperty:props[i] attributes:attr]; - if (prop) { - [propArray addObject:prop]; - } + NSString *propertyName = @(property_getName(props[i])); + if ([ignoredProperties containsObject:propertyName]) { + continue; } + + unsigned int attCount; + objc_property_attribute_t *atts = property_copyAttributeList(props[i], &attCount); + RLMProperty *prop = [[RLMProperty alloc] initWithName:propertyName + attributes:[objectClass attributesForProperty:propertyName] + attributeList:atts + attributeCount:attCount]; + free(atts); + [propArray addObject:prop]; } - + free(props); - - // create schema object and set properties - RLMObjectSchema *schema = [RLMObjectSchema new]; - schema.properties = propArray; - schema.className = [objectClass className]; - schema.objectClass = objectClass; - schema.standaloneClass = RLMStandaloneAccessorClassForObjectClass(objectClass, schema); - return schema; + return propArray; } // generate a schema from a table - specify the custom class name for the dynamic // class and the name to be used in the schema - used for migrations and dynamic interface -+(instancetype)schemaForTable:(tightdb::Table *)table className:(NSString *)className { ++(instancetype)schemaFromTableForClassName:(NSString *)className realm:(RLMRealm *)realm { + tightdb::TableRef table = RLMTableForObjectClass(realm, className); + if (!table) { + return nil; + } + // create array of RLMProperties size_t count = table->get_column_count(); NSMutableArray *propArray = [NSMutableArray arrayWithCapacity:count]; @@ -111,13 +166,23 @@ +(instancetype)schemaForTable:(tightdb::Table *)table className:(NSString *)clas [propArray addObject:prop]; } - + // create schema object and set properties RLMObjectSchema *schema = [RLMObjectSchema new]; schema.properties = propArray; schema.className = className; - // for dynamic interface use vanilla RLMObject + // get primary key from realm metadata + NSString *primaryKey = RLMRealmPrimaryKeyForObjectClass(realm, className); + if (primaryKey) { + schema.primaryKeyProperty = schema[primaryKey]; + if (!schema.primaryKeyProperty) { + NSString *reason = [NSString stringWithFormat:@"No property matching primary key '%@'", primaryKey]; + @throw [NSException exceptionWithName:@"RLMException" reason:reason userInfo:nil]; + } + } + + // for dynamic schema use vanilla RLMObject accessor classes schema.objectClass = RLMObject.class; schema.standaloneClass = RLMObject.class; @@ -129,8 +194,8 @@ - (id)copyWithZone:(NSZone *)zone { schema.properties = self.properties; schema.objectClass = self.objectClass; schema.className = self.className; + schema.primaryKeyProperty = schema[_primaryKeyProperty.name]; return schema; } @end - diff --git a/Realm/RLMObjectSchema_Private.hpp b/Realm/RLMObjectSchema_Private.hpp index c61cca81bb..bbe55f4bed 100644 --- a/Realm/RLMObjectSchema_Private.hpp +++ b/Realm/RLMObjectSchema_Private.hpp @@ -35,10 +35,14 @@ @property (nonatomic, readwrite, assign) Class accessorClass; @property (nonatomic, readwrite, assign) Class standaloneClass; +@property (nonatomic, readwrite) RLMProperty *primaryKeyProperty; + // returns a cached or new schema for a given object class +// creates standalone accessor classes for the object schema if create is YES +(instancetype)schemaForObjectClass:(Class)objectClass; ++(instancetype)schemaForObjectClass:(Class)objectClass createAccessors:(BOOL)create; // generate a schema from a table -+(instancetype)schemaForTable:(tightdb::Table *)table className:(NSString *)className; ++(instancetype)schemaFromTableForClassName:(NSString *)className realm:(RLMRealm *)realm; @end diff --git a/Realm/RLMObjectStore.hpp b/Realm/RLMObjectStore.hpp index d860ff0932..cd84c57dcc 100644 --- a/Realm/RLMObjectStore.hpp +++ b/Realm/RLMObjectStore.hpp @@ -24,8 +24,8 @@ // update tables in realm to the targetSchema and set schema on realm // returns true if modifications were made -// NOTE: must be called from within write transaction if allowMutation is true -bool RLMRealmSetSchema(RLMRealm *realm, RLMSchema *targetSchema, bool allowMutation = false); +// NOTE: must be called from within write transaction if initializeSchema is true +bool RLMRealmSetSchema(RLMRealm *realm, RLMSchema *targetSchema, bool initializeSchema = false); // initialize a realm if needed with the given schema // for uninitialized dbs, the initial version is set and tables are created for the target schema @@ -40,7 +40,8 @@ void RLMRealmInitializeReadOnlyWithSchema(RLMRealm *realm, RLMSchema *targetSche // // add an object to the given realm -void RLMAddObjectToRealm(RLMObject *object, RLMRealm *realm); +// if tryUpdate is 'true', update an existing object with the same primary key value +void RLMAddObjectToRealm(RLMObject *object, RLMRealm *realm, bool tryUpdate = false); // delete an object from its realm void RLMDeleteObjectFromRealm(RLMObject *object); @@ -49,7 +50,9 @@ void RLMDeleteObjectFromRealm(RLMObject *object); RLMArray *RLMGetObjects(RLMRealm *realm, NSString *objectClassName, NSPredicate *predicate); // create object from array or dictionary -RLMObject *RLMCreateObjectInRealmWithValue(RLMRealm *realm, NSString *className, id value); +// if tryUpdate is 'true', update an existing object with the same primary key value +RLMObject *RLMCreateObjectInRealmWithValue(RLMRealm *realm, NSString *className, id value, bool tryUpdate = false); + // // Accessor Creation diff --git a/Realm/RLMObjectStore.mm b/Realm/RLMObjectStore.mm index 70d24227fc..e7bfd2d672 100644 --- a/Realm/RLMObjectStore.mm +++ b/Realm/RLMObjectStore.mm @@ -19,34 +19,15 @@ #import "RLMObjectStore.hpp" #import "RLMRealm_Private.hpp" #import "RLMArray_Private.hpp" -#import "RLMSchema_Private.h" #import "RLMObjectSchema_Private.hpp" #import "RLMObject_Private.h" #import "RLMProperty_Private.h" -#import "RLMAccessor.h" #import "RLMQueryUtil.hpp" #import "RLMUtil.hpp" #import -// get the table used to store object of objectClass -static inline tightdb::TableRef RLMTableForObjectClass(RLMRealm *realm, - NSString *className, - bool &created) { - NSString *tableName = realm.schema.tableNamesForClass[className]; - return realm.group->get_or_add_table(tableName.UTF8String, &created); -} -static inline tightdb::TableRef RLMTableForObjectClass(RLMRealm *realm, - NSString *className) { - NSString *tableName = realm.schema.tableNamesForClass[className]; - return realm.group->get_table(tableName.UTF8String); -} - - -static void RLMVerifyAndAlignColumns(RLMObjectSchema *objectSchema) { - RLMObjectSchema *tableSchema = [RLMObjectSchema schemaForTable:objectSchema->_table.get() - className:objectSchema.className]; - +static void RLMVerifyAndAlignColumns(RLMObjectSchema *tableSchema, RLMObjectSchema *objectSchema) { // FIXME - this method should calculate all mismatched columns, and missing/extra columns, and include // all of this information in a single exception // FIXME - verify property attributes @@ -84,27 +65,17 @@ static void RLMVerifyAndAlignColumns(RLMObjectSchema *objectSchema) { @"new property name": schemaProp.objectClassName}]; } } + if (tableProp.isPrimary != schemaProp.isPrimary) { + @throw [NSException exceptionWithName:@"RLMException" + reason:@"Property primary key designation does not match - migration required" + userInfo:@{@"property name": tableProp.name}]; + } // align schemaProp.column = tableProp.column; } } - -// verify and align all tables in schema -static void RLMVerifyAndAlignSchema(RLMSchema *schema) { - for (RLMObjectSchema *objectSchema in schema.objectSchema) { - RLMVerifyAndAlignColumns(objectSchema); - - // create accessors - // FIXME - we need to generate different accessors keyed by the hash of the objectSchema (to preserve column ordering) - // it's possible to have multiple realms with different on-disk layouts, which requires - // us to have multiple accessors for each type/instance combination - objectSchema.accessorClass = RLMAccessorClassForObjectClass(objectSchema.objectClass, objectSchema); - } -} - - // create a column for a property in a table // NOTE: must be called from within write transaction static void RLMCreateColumn(RLMRealm *realm, tightdb::Table &table, RLMProperty *prop) { @@ -119,61 +90,16 @@ static void RLMCreateColumn(RLMRealm *realm, tightdb::Table &table, RLMProperty default: { prop.column = table.add_column(tightdb::DataType(prop.type), prop.name.UTF8String); if (prop.attributes & RLMPropertyAttributeIndexed) { - table.set_index(prop.column); - } - } - } -} - -// NOTE: must be called from within write transaction -static bool RLMCreateMissingTables(RLMRealm *realm) { - bool changed = false; - for (RLMObjectSchema *objectSchema in realm.schema.objectSchema) { - bool created = false; - tightdb::TableRef table = RLMTableForObjectClass(realm, objectSchema.className, created); - changed |= created; - - // store the table in this object schema - objectSchema->_table = move(table); - } - return changed; -} - -// add missing columns to objects described in targetSchema -// NOTE: must be called from within write transaction -static bool RLMAddMissingColumns(RLMRealm *realm) { - bool added = false; - for (RLMObjectSchema *objectSchema in realm.schema.objectSchema) { - // add columns - RLMObjectSchema *tableSchema = [RLMObjectSchema schemaForTable:objectSchema->_table.get() className:objectSchema.className]; - for (RLMProperty *prop in objectSchema.properties) { - // add any new properties (new name or different type) - if (!tableSchema[prop.name] || ![prop isEqualToProperty:tableSchema[prop.name]]) { - RLMCreateColumn(realm, *objectSchema->_table, prop); - added = true; - } - } - } - return added; -} - -// remove old columns in the realm not in targetSchema -// NOTE: must be called from within write transaction -static bool RLMRemoveExtraColumns(RLMRealm *realm) { - bool removed = false; - for (RLMObjectSchema *objectSchema in realm.schema.objectSchema) { - RLMObjectSchema *tableSchema = [RLMObjectSchema schemaForTable:objectSchema->_table.get() className:objectSchema.className]; - - // remove any columns from tableSchema not in final schema - for (int i = (int)tableSchema.properties.count - 1; i >= 0; i--) { - RLMProperty *prop = tableSchema.properties[i]; - if (!objectSchema[prop.name] || ![prop isEqualToProperty:objectSchema[prop.name]]) { - objectSchema->_table->remove_column(prop.column); - removed = true; + // FIXME - support other types + if (prop.type != RLMPropertyTypeString) { + NSLog(@"RLMPropertyAttributeIndexed only supported for 'NSString' properties"); + } + else { + table.set_index(prop.column); + } } } } - return removed; } void RLMRealmInitializeReadOnlyWithSchema(RLMRealm *realm, RLMSchema *targetSchema) { @@ -186,10 +112,11 @@ void RLMRealmInitializeReadOnlyWithSchema(RLMRealm *realm, RLMSchema *targetSche realm.schema = [targetSchema copy]; for (RLMObjectSchema *objectSchema in realm.schema.objectSchema) { - objectSchema->_table = RLMTableForObjectClass(realm, objectSchema.className); // read-only realms may be missing tables entirely + objectSchema->_table = RLMTableForObjectClass(realm, objectSchema.className); if (objectSchema->_table) { - RLMVerifyAndAlignColumns(objectSchema); + RLMObjectSchema *tableSchema = [RLMObjectSchema schemaFromTableForClassName:objectSchema.className realm:realm]; + RLMVerifyAndAlignColumns(tableSchema, objectSchema); } objectSchema.accessorClass = RLMAccessorClassForObjectClass(objectSchema.objectClass, objectSchema); } @@ -200,14 +127,14 @@ void RLMRealmInitializeWithSchema(RLMRealm *realm, RLMSchema *targetSchema) { @try { // check to see if this is the first time loading this realm - bool initializing = (RLMRealmSchemaVersion(realm) == RLMNotVersioned); - if (initializing) { + bool firstInitialization = RLMRealmSchemaVersion(realm) == RLMNotVersioned; + if (firstInitialization) { // set initial version RLMRealmSetSchemaVersion(realm, 0); } // set the schema, mutating if we are initializing the db for the first time - RLMRealmSetSchema(realm, targetSchema, initializing); + RLMRealmSetSchema(realm, targetSchema, firstInitialization); } @finally { // FIXME: should rollback on exceptions rather than commit once that's implemented @@ -215,19 +142,71 @@ void RLMRealmInitializeWithSchema(RLMRealm *realm, RLMSchema *targetSchema) { } } -bool RLMRealmSetSchema(RLMRealm *realm, RLMSchema *targetSchema, bool allowMutation) { +bool RLMRealmSetSchema(RLMRealm *realm, RLMSchema *targetSchema, bool initializeSchema) { realm.schema = [targetSchema copy]; - // create missing tables - bool changed = RLMCreateMissingTables(realm); + bool changed = false; + if (initializeSchema) { + // first pass to create missing tables + for (RLMObjectSchema *objectSchema in realm.schema.objectSchema) { + bool created = false; + objectSchema->_table = RLMTableForObjectClass(realm, objectSchema.className, created); + changed |= created; + } + + // second pass adds/removes columns appropriately + for (RLMObjectSchema *objectSchema in realm.schema.objectSchema) { + RLMObjectSchema *tableSchema = [RLMObjectSchema schemaFromTableForClassName:objectSchema.className realm:realm]; + + // add missing columns + for (RLMProperty *prop in objectSchema.properties) { + // add any new properties (new name or different type) + if (!tableSchema[prop.name] || ![prop isEqualToProperty:tableSchema[prop.name]]) { + RLMCreateColumn(realm, *objectSchema->_table, prop); + changed = true; + } + } + + // remove extra columns + for (int i = (int)tableSchema.properties.count - 1; i >= 0; i--) { + RLMProperty *prop = tableSchema.properties[i]; + if (!objectSchema[prop.name] || ![prop isEqualToProperty:objectSchema[prop.name]]) { + objectSchema->_table->remove_column(prop.column); + changed = true; + } + } + + // update table metadata + if (objectSchema.primaryKeyProperty != nil) { + // if there is a primary key set, check if it is the same as the old key + if (tableSchema.primaryKeyProperty == nil || ![tableSchema.primaryKeyProperty isEqual:objectSchema.primaryKeyProperty]) { + RLMRealmSetPrimaryKeyForObjectClass(realm, objectSchema.className, objectSchema.primaryKeyProperty.name); + changed = true; + } + } + else if (tableSchema.primaryKeyProperty) { + // there is no primary key, so if thre was one nil out + RLMRealmSetPrimaryKeyForObjectClass(realm, objectSchema.objectClass, nil); + changed = true; + } + } - if (allowMutation) { - changed = RLMAddMissingColumns(realm) || changed; - changed = RLMRemoveExtraColumns(realm) || changed; // FIXME - remove deleted tables } - RLMVerifyAndAlignSchema(realm.schema); + for (RLMObjectSchema *objectSchema in realm.schema.objectSchema) { + // cache table instances on objectSchema + objectSchema->_table = RLMTableForObjectClass(realm, objectSchema.className); + + RLMObjectSchema *tableSchema = [RLMObjectSchema schemaFromTableForClassName:objectSchema.className realm:realm]; + RLMVerifyAndAlignColumns(tableSchema, objectSchema); + + // create accessors + // FIXME - we need to generate different accessors keyed by the hash of the objectSchema (to preserve column ordering) + // it's possible to have multiple realms with different on-disk layouts, which requires + // us to have multiple accessors for each type/instance combination + objectSchema.accessorClass = RLMAccessorClassForObjectClass(objectSchema.objectClass, objectSchema); + } return changed; } @@ -242,7 +221,43 @@ static inline void RLMVerifyInWriteTransaction(RLMRealm *realm) { RLMCheckThread(realm); } -void RLMAddObjectToRealm(RLMObject *object, RLMRealm *realm) { +template +static inline NSUInteger RLMCreateOrGetRowForObject(RLMObjectSchema *schema, F primaryValueGetter, bool tryUpdate, bool &created) { + // try to get existing row if updating + size_t rowIndex = tightdb::not_found; + tightdb::Table &table = *schema->_table; + if (tryUpdate) { + // verify primary key + RLMProperty *primaryProperty = schema.primaryKeyProperty; + if (!primaryProperty) { + NSString *reason = [NSString stringWithFormat:@"'%@' does not have a primary key and can not be updated", schema.className]; + @throw [NSException exceptionWithName:@"RLMExecption" reason:reason userInfo:nil]; + } + + // get primary value + id primaryValue = primaryValueGetter(primaryProperty); + + // search for existing object based on primary key type + if (primaryProperty.type == RLMPropertyTypeString) { + rowIndex = table.find_first_string(primaryProperty.column, RLMStringDataWithNSString(primaryValue)); + } + else { + rowIndex = table.find_first_int(primaryProperty.column, [primaryValue longLongValue]); + } + } + + // if no existing, create row + created = NO; + if (rowIndex == tightdb::not_found) { + rowIndex = table.add_empty_row(); + created = YES; + } + + // get accessor + return rowIndex; +} + +void RLMAddObjectToRealm(RLMObject *object, RLMRealm *realm, bool update) { // if already in the right realm then no-op if (object.realm == realm) { return; @@ -269,10 +284,10 @@ void RLMAddObjectToRealm(RLMObject *object, RLMRealm *realm) { // automatically detach it object->_row.detach(); - // create row in table - tightdb::Table &table = *schema->_table; - size_t rowIndex = table.add_empty_row(); - object->_row = table[rowIndex]; + // get or create row + bool created; + auto primaryGetter = [=](RLMProperty *p) { return [object valueForKey:p.getterName]; }; + object->_row = (*schema->_table)[RLMCreateOrGetRowForObject(schema, primaryGetter, update, created)]; // populate all properties for (RLMProperty *prop in schema.properties) { @@ -286,12 +301,15 @@ void RLMAddObjectToRealm(RLMObject *object, RLMRealm *realm) { if (!value && prop.type != RLMPropertyTypeObject) { @throw [NSException exceptionWithName:@"RLMException" reason:[NSString stringWithFormat:@"No value or default value specified for property '%@' in '%@'", - prop.name, objectClassName] + prop.name, schema.className] userInfo:nil]; } // set in table with out validation - RLMDynamicSet(object, prop, value); + // skip primary key when updating since it doesn't change + if (created || !prop.isPrimary) { + RLMDynamicSet(object, prop, value, prop.isPrimary, update); + } } // switch class to use table backed accessor @@ -299,7 +317,7 @@ void RLMAddObjectToRealm(RLMObject *object, RLMRealm *realm) { } -RLMObject *RLMCreateObjectInRealmWithValue(RLMRealm *realm, NSString *className, id value) { +RLMObject *RLMCreateObjectInRealmWithValue(RLMRealm *realm, NSString *className, id value, bool update) { // verify writable RLMVerifyInWriteTransaction(realm); @@ -312,30 +330,36 @@ void RLMAddObjectToRealm(RLMObject *object, RLMRealm *realm) { if (NSArray *array = RLMDynamicCast(value)) { array = RLMValidatedArrayForObjectSchema(value, objectSchema, schema); - // create row - tightdb::Table &table = *objectSchema->_table; - size_t rowIndex = table.add_empty_row(); - object->_row = table[rowIndex]; + // get or create our accessor + bool created; + auto primaryGetter = [=](RLMProperty *p) { return array[p.column]; }; + object->_row = (*objectSchema->_table)[RLMCreateOrGetRowForObject(objectSchema, primaryGetter, update, created)]; // populate NSArray *props = objectSchema.properties; for (NSUInteger i = 0; i < array.count; i++) { - RLMDynamicSet(object, (RLMProperty *)props[i], array[i]); + RLMProperty *prop = props[i]; + // skip primary key when updating since it doesn't change + if (created || !prop.isPrimary) { + RLMDynamicSet(object, (RLMProperty *)prop, array[i], prop.isPrimary, update); + } } } else { // assume dictionary or object with kvc properties NSDictionary *dict = RLMValidatedDictionaryForObjectSchema(value, objectSchema, schema); - // create row - tightdb::Table &table = *objectSchema->_table; - size_t rowIndex = table.add_empty_row(); - object->_row = table[rowIndex]; - + // get or create our accessor + bool created; + auto primaryGetter = [=](RLMProperty *p) { return dict[p.name]; }; + object->_row = (*objectSchema->_table)[RLMCreateOrGetRowForObject(objectSchema, primaryGetter, update, created)]; + // populate - NSArray *props = objectSchema.properties; - for (RLMProperty *prop in props) { - RLMDynamicSet(object, prop, dict[prop.name]); + for (RLMProperty *prop in objectSchema.properties) { + // skip primary key when updating since it doesn't change + if (created || !prop.isPrimary) { + RLMDynamicSet(object, prop, dict[prop.name], prop.isPrimary, update); + } } } @@ -386,4 +410,3 @@ void RLMDeleteObjectFromRealm(RLMObject *object) { return accessor; } - diff --git a/Realm/RLMProperty.m b/Realm/RLMProperty.m index 602c2991de..73a708616f 100644 --- a/Realm/RLMProperty.m +++ b/Realm/RLMProperty.m @@ -21,14 +21,6 @@ #import "RLMObject.h" #import "RLMSchema_Private.h" -// private properties -@interface RLMProperty () - -@property (nonatomic, assign) BOOL dynamic; -@property (nonatomic, assign) BOOL nonatomic; - -@end - @implementation RLMProperty - (instancetype)initWithName:(NSString *)name @@ -41,21 +33,11 @@ - (instancetype)initWithName:(NSString *)name _type = type; _objectClassName = objectClassName; _attributes = attributes; - [self updateAccessorNames]; [self setObjcCodeFromType]; + [self updateAccessorNames]; } - - return self; -} -+ (instancetype)propertyWithName:(NSString *)name type:(RLMPropertyType)type objectClassName:(NSString *)objectClassName { - RLMProperty *prop = [[RLMProperty alloc] init]; - prop->_name = name; - prop->_type = type; - prop->_objectClassName = objectClassName; - [prop updateAccessorNames]; - [prop setObjcCodeFromType]; - return prop; + return self; } -(void)updateAccessorNames { @@ -76,11 +58,7 @@ -(void)updateAccessorNames { -(void)setObjcCodeFromType { switch (_type) { case RLMPropertyTypeInt: -#if defined(__LP64__) && __LP64__ - _objcType = 'l'; -#else - _objcType = 'i'; -#endif + _objcType = 'q'; break; case RLMPropertyTypeBool: _objcType = 'c'; @@ -104,15 +82,14 @@ -(void)setObjcCodeFromType { // determine RLMPropertyType from objc code - returns true if valid type was found/set -(BOOL)parsePropertyTypeString:(const char *)code { - _objcType = *(code); // first char of type attr - if (self.objcType == 'q') { - _objcType = 'l'; // collapse these as they are the same - } - + _objcType = *code; // first char of type attr + // map to RLMPropertyType switch (self.objcType) { + case 's': // short case 'i': // int case 'l': // long + case 'q': // long long _type = RLMPropertyTypeInt; return YES; case 'f': @@ -128,7 +105,7 @@ -(BOOL)parsePropertyTypeString:(const char *)code { case '@': { NSString * const arrayPrefix = @"@\"RLMArray<"; - NSString *type = [NSString stringWithUTF8String:code]; + NSString *type = @(code); // if one charachter, this is an untyped id, ie [type isEqualToString:@"@"] if (type.length == 1) { _type = RLMPropertyTypeAny; @@ -144,16 +121,18 @@ -(BOOL)parsePropertyTypeString:(const char *)code { } else if ([type hasPrefix:arrayPrefix]) { // get object class from type string - @"RLMArray" - _objectClassName = [type substringWithRange:NSMakeRange(arrayPrefix.length, type.length-arrayPrefix.length-2)]; - _type = RLMPropertyTypeArray; - + NSString *className = [type substringWithRange:NSMakeRange(arrayPrefix.length, type.length-arrayPrefix.length-2)]; + // verify type - Class cls = [RLMSchema classForString:self.objectClassName]; + Class cls = [RLMSchema classForString:className]; if (class_getSuperclass(cls) != RLMObject.class) { @throw [NSException exceptionWithName:@"RLMException" reason:[NSString stringWithFormat:@"RLMArray sub-type '%@' must descend from RLMObject", self.objectClassName] userInfo:nil]; } + + _type = RLMPropertyTypeArray; + _objectClassName = [cls className]; } else if ([type isEqualToString:@"@\"NSNumber\""]) { @throw [NSException exceptionWithName:@"RLMException" @@ -161,12 +140,10 @@ -(BOOL)parsePropertyTypeString:(const char *)code { userInfo:nil]; } else { - // get object class and set type - _objectClassName = [type substringWithRange:NSMakeRange(2, type.length-3)]; - _type = RLMPropertyTypeObject; - + NSString *className = [type substringWithRange:NSMakeRange(2, type.length-3)]; + // verify type - Class cls = [RLMSchema classForString:self.objectClassName]; + Class cls = [RLMSchema classForString:className]; if (class_getSuperclass(cls) != RLMObject.class) { if ([_objectClassName isEqualToString:@"RLMArray"]) { @throw [NSException exceptionWithName:@"RLMException" @@ -177,6 +154,9 @@ -(BOOL)parsePropertyTypeString:(const char *)code { reason:[NSString stringWithFormat:@"'%@' is not supported as an RLMObject property. All properties must be primitives, NSString, NSDate, NSData, RLMArray, or subclasses of RLMObject. See http://realm.io/docs/cocoa/latest/api/Classes/RLMObject.html for more information.", self.objectClassName] userInfo:nil]; } + + _type = RLMPropertyTypeObject; + _objectClassName = [cls className]; } return YES; } @@ -185,58 +165,59 @@ -(BOOL)parsePropertyTypeString:(const char *)code { } } -+(instancetype)propertyForObjectProperty:(objc_property_t)runtimeProp - attributes:(RLMPropertyAttributes)attributes +- (instancetype)initWithName:(NSString *)name + attributes:(RLMPropertyAttributes)attributes + attributeList:(objc_property_attribute_t *)attrs + attributeCount:(unsigned int)attrCount { - // create new property - NSString *name = [NSString stringWithUTF8String:property_getName(runtimeProp)]; - RLMProperty *prop = [RLMProperty new]; - prop->_name = name; - prop->_attributes = attributes; - + self = [super init]; + if (!self) { + return self; + } + + _name = name; + _attributes = attributes; + // parse attributes - unsigned int attCount; - objc_property_attribute_t *atts = property_copyAttributeList(runtimeProp, &attCount); BOOL validType = NO; - for (unsigned int a = 0; a < attCount; a++) { - switch (*(atts[a].name)) { + for (unsigned int a = 0; a < attrCount; a++) { + switch (*attrs[a].name) { case 'T': - validType = [prop parsePropertyTypeString:atts[a].value]; + validType = [self parsePropertyTypeString:attrs[a].value]; break; case 'N': - prop.nonatomic = YES; + // nonatomic break; case 'D': - prop.dynamic = YES; + // dynamic break; case 'G': - prop.getterName = [NSString stringWithUTF8String:atts[a].value]; + self.getterName = @(attrs[a].value); break; case 'S': - prop.setterName = [NSString stringWithUTF8String:atts[a].value]; + self.setterName = @(attrs[a].value); break; default: break; } } - free(atts); - + // throw if there was no type if (!validType) { NSString *reason = [NSString stringWithFormat:@"Can't persist property '%@' with incompatible type. " - "Add to ignoredPropertyNames: method to ignore.", prop.name]; + "Add to ignoredPropertyNames: method to ignore.", self.name]; @throw [NSException exceptionWithName:@"RLMException" reason:reason userInfo:nil]; } - + // update getter/setter names - [prop updateAccessorNames]; - - return prop; + [self updateAccessorNames]; + + return self; } -(BOOL)isEqualToProperty:(RLMProperty *)prop { - return [_name isEqualToString:prop.name] && _type == prop.type && + return [_name isEqualToString:prop.name] && _type == prop.type && prop.isPrimary == _isPrimary && (_objectClassName == nil || [_objectClassName isEqualToString:prop.objectClassName]); } diff --git a/Realm/RLMProperty_Private.h b/Realm/RLMProperty_Private.h index ca2200c5bf..7ec402a8d7 100644 --- a/Realm/RLMProperty_Private.h +++ b/Realm/RLMProperty_Private.h @@ -22,14 +22,10 @@ // private property interface @interface RLMProperty () -// creates an RLMProperty object from a runtime property -+(instancetype)propertyForObjectProperty:(objc_property_t)runtimeProp - attributes:(RLMPropertyAttributes)attributes; - - (instancetype)initWithName:(NSString *)name - type:(RLMPropertyType)type - objectClassName:(NSString *)objectClassName - attributes:(RLMPropertyAttributes)attributes; + attributes:(RLMPropertyAttributes)attributes + attributeList:(objc_property_attribute_t *)attrs + attributeCount:(unsigned int)attrCount; // private setters @property (nonatomic, assign) NSUInteger column; @@ -42,6 +38,7 @@ @property (nonatomic, copy) NSString *getterName; @property (nonatomic, copy) NSString *setterName; @property (nonatomic, copy) NSString *objectClassName; +@property (nonatomic, assign) BOOL isPrimary; @end diff --git a/Realm/RLMRealm.h b/Realm/RLMRealm.h index c4e3e98131..85088f57f6 100644 --- a/Realm/RLMRealm.h +++ b/Realm/RLMRealm.h @@ -291,6 +291,26 @@ typedef void(^RLMNotificationBlock)(NSString *notification, RLMRealm *realm); */ - (void)addObjectsFromArray:(id)array; +/** + Adds or updates an object to be persisted it in this Realm. The object provided must have a designated + primary key. If no objects exist in the RLMRealm instance with the same primary key value, the object is + inserted. Otherwise, the existing object is updated with any changed values. + + @param object Object to be added or updated. + */ +- (void)addOrUpdateObject:(RLMObject *)object; + +/** + Adds or updates objects in the given array to be persisted it in this Realm. + + This is the equivalent of `addOrUpdateObject:` except for an array of objects. + + @param array `NSArray` or `RLMArray` of `RLMObject`s (or subclasses) to be added to this Realm. + + @see addOrUpdateObject: + */ +- (void)addOrUpdateObjectsFromArray:(id)array; + /** Delete an object from this Realm. diff --git a/Realm/RLMRealm.mm b/Realm/RLMRealm.mm index 7065102036..a03b2d5e35 100644 --- a/Realm/RLMRealm.mm +++ b/Realm/RLMRealm.mm @@ -540,7 +540,7 @@ - (BOOL)refresh { } - (void)addObject:(RLMObject *)object { - RLMAddObjectToRealm(object, self); + RLMAddObjectToRealm(object, self, false); } - (void)addObjectsFromArray:(id)array { @@ -549,6 +549,16 @@ - (void)addObjectsFromArray:(id)array { } } +- (void)addOrUpdateObject:(RLMObject *)object { + RLMAddObjectToRealm(object, self, true); +} + +- (void)addOrUpdateObjectsFromArray:(id)array { + for (RLMObject *obj in array) { + [self addOrUpdateObject:obj]; + } +} + - (void)deleteObject:(RLMObject *)object { RLMDeleteObjectFromRealm(object); } diff --git a/Realm/RLMRealm_Private.hpp b/Realm/RLMRealm_Private.hpp index b833dbeb36..97e6b64118 100644 --- a/Realm/RLMRealm_Private.hpp +++ b/Realm/RLMRealm_Private.hpp @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// #import "RLMRealm_Dynamic.h" -#import "RLMSchema.h" +#import "RLMSchema_Private.h" #import "RLMAccessor.h" #import @@ -44,3 +44,17 @@ inline void RLMCheckThread(RLMRealm *realm) { userInfo:nil]; } } + +// get the table used to store object of objectClass +static inline tightdb::TableRef RLMTableForObjectClass(RLMRealm *realm, + NSString *className, + bool &created) { + NSString *tableName = RLMTableNameForClass(className); + return realm.group->get_or_add_table(tableName.UTF8String, &created); +} +static inline tightdb::TableRef RLMTableForObjectClass(RLMRealm *realm, + NSString *className) { + NSString *tableName = RLMTableNameForClass(className); + return realm.group->get_table(tableName.UTF8String); +} + diff --git a/Realm/RLMSchema.mm b/Realm/RLMSchema.mm index e39c3b5b46..d40889fc45 100644 --- a/Realm/RLMSchema.mm +++ b/Realm/RLMSchema.mm @@ -16,22 +16,26 @@ // //////////////////////////////////////////////////////////////////////////// -#import "RLMRealm_Private.hpp" +#import "RLMObject.h" #import "RLMObjectSchema_Private.hpp" +#import "RLMRealm_Private.hpp" #import "RLMSchema_Private.h" -#import "RLMObject.h" +#import "RLMSwiftSupport.h" #import "RLMUtil.hpp" -#if REALM_SWIFT -#import -#endif - #import NSString * const c_objectTableNamePrefix = @"class_"; const char *c_metadataTableName = "metadata"; const char *c_versionColumnName = "version"; const size_t c_versionColumnIndex = 0; + +const char *c_primaryKeyTableName = "pk"; +const char *c_primaryKeyObjectClassColumnName = "pk_table"; +const size_t c_primaryKeyObjectClassColumnIndex = 0; +const char *c_primaryKeyPropertyNameColumnName = "pk_property"; +const size_t c_primaryKeyPropertyNameColumnIndex = 1; + const NSUInteger RLMNotVersioned = (NSUInteger)-1; @@ -41,7 +45,7 @@ @interface RLMSchema () @end static RLMSchema *s_sharedSchema; -static NSMutableDictionary *s_classNameToMangledName; +static NSMutableDictionary *s_localNameToClass; @implementation RLMSchema @@ -61,8 +65,6 @@ - (RLMObjectSchema *)objectForKeyedSubscript:(id )className { - (id)init { self = [super init]; if (self) { - // setup name mapping for object tables - _tableNamesForClass = [NSMutableDictionary dictionary]; _objectSchemaByName = [NSMutableDictionary dictionary]; } return self; @@ -70,11 +72,7 @@ - (id)init { - (void)setObjectSchema:(NSArray *)objectSchema { _objectSchema = objectSchema; - - // update mappings for (RLMObjectSchema *object in objectSchema) { - // set table name and mappings - _tableNamesForClass[object.className] = RLMTableNameForClassName(object.className); [(NSMutableDictionary *)_objectSchemaByName setObject:object forKey:object.className]; } } @@ -96,8 +94,7 @@ static inline bool IsRLMObjectSubclass(Class cls) { + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - // initialize mangled name mapping - s_classNameToMangledName = [NSMutableDictionary dictionary]; + s_localNameToClass = [NSMutableDictionary dictionary]; NSMutableArray *schemaArray = [NSMutableArray array]; RLMSchema *schema = [[RLMSchema alloc] init]; @@ -110,28 +107,22 @@ + (void)initialize { continue; } - RLMObjectSchema *objectSchema = nil; -#if REALM_SWIFT + // Delay init of Swift classes until after we know the names of all + // of them so that we can validate array types NSString *className = NSStringFromClass(cls); if ([RLMSwiftSupport isSwiftClassName:className]) { - objectSchema = [RLMSwiftSupport schemaForObjectClass:cls]; - objectSchema.standaloneClass = RLMStandaloneAccessorClassForObjectClass(cls, objectSchema); - s_classNameToMangledName[objectSchema.className] = objectSchema.objectClass; + s_localNameToClass[[RLMSwiftSupport demangleClassName:className]] = cls; } else { - objectSchema = [RLMObjectSchema schemaForObjectClass:cls]; + [schemaArray addObject:[RLMObjectSchema schemaForObjectClass:cls createAccessors:YES]]; } -#else - objectSchema = [RLMObjectSchema schemaForObjectClass:cls]; -#endif - // add to list - [schemaArray addObject:objectSchema]; - // implement sharedSchema and className for this class - RLMReplaceSharedSchemaMethod(cls, objectSchema); - RLMReplaceClassNameMethod(cls, objectSchema.className); } free(classes); + for (Class cls in s_localNameToClass.allValues) { + [schemaArray addObject:[RLMObjectSchema schemaForObjectClass:cls createAccessors:YES]]; + } + // set class array schema.objectSchema = schemaArray; @@ -156,9 +147,8 @@ + (instancetype)dynamicSchemaFromRealm:(RLMRealm *)realm { for (unsigned long i = 0; i < numTables; i++) { NSString *className = RLMClassForTableName(@(realm.group->get_table_name(i).data())); if (className) { - tightdb::TableRef table = realm.group->get_table(i); - RLMObjectSchema *object = [RLMObjectSchema schemaForTable:table.get() className:className]; - object->_table = move(table); + RLMObjectSchema *object = [RLMObjectSchema schemaFromTableForClassName:className realm:realm]; + object->_table = realm.group->get_table(i); [schemaArray addObject:object]; } } @@ -168,38 +158,69 @@ + (instancetype)dynamicSchemaFromRealm:(RLMRealm *)realm { return schema; } +NSUInteger RLMRealmSchemaVersion(RLMRealm *realm) { + tightdb::TableRef table = realm.group->get_table(c_metadataTableName); + if (!table || table->get_column_count() == 0) { + return RLMNotVersioned; + } + return NSUInteger(table->get_int(c_versionColumnIndex, 0)); +} -static inline tightdb::TableRef RLMVersionTable(RLMRealm *realm) { +void RLMRealmSetSchemaVersion(RLMRealm *realm, NSUInteger version) { tightdb::TableRef table = realm.group->get_or_add_table(c_metadataTableName); if (table->get_column_count() == 0) { // create columns table->add_column(tightdb::type_Int, c_versionColumnName); - + // set initial version table->add_empty_row(); - table->get(0).set_int(c_versionColumnIndex, RLMNotVersioned); + table->set_int(c_versionColumnIndex, 0, RLMNotVersioned); } - return move(table); + + table->set_int(c_versionColumnIndex, 0, version); } -NSUInteger RLMRealmSchemaVersion(RLMRealm *realm) { - tightdb::TableRef table = realm.group->get_table(c_metadataTableName); - if (!table || table->get_column_count() == 0) { - return RLMNotVersioned; +NSString *RLMRealmPrimaryKeyForObjectClass(RLMRealm *realm, NSString *objectClass) { + tightdb::TableRef table = realm.group->get_table(c_primaryKeyTableName); + if (!table) { + return nil; + } + size_t row = table->find_first_string(c_primaryKeyObjectClassColumnIndex, RLMStringDataWithNSString(objectClass)); + if (row == tightdb::not_found) { + return nil; } - return NSUInteger(table->get(0).get_int(c_versionColumnIndex)); + return RLMStringDataToNSString(table->get_string(c_primaryKeyPropertyNameColumnIndex, row)); } -void RLMRealmSetSchemaVersion(RLMRealm *realm, NSUInteger version) { - RLMVersionTable(realm)->get(0).set_int(c_versionColumnIndex, version); +void RLMRealmSetPrimaryKeyForObjectClass(RLMRealm *realm, NSString *objectClass, NSString *primaryKey) { + tightdb::TableRef table = realm.group->get_or_add_table(c_primaryKeyTableName); + if (table->get_column_count() == 0) { + // create columns + table->add_column(tightdb::type_String, c_primaryKeyObjectClassColumnName); + table->add_column(tightdb::type_String, c_primaryKeyPropertyNameColumnName); + } + + // get row or create if new object and populate + size_t row = table->find_first_string(c_primaryKeyObjectClassColumnIndex, RLMStringDataWithNSString(objectClass)); + if (row == tightdb::not_found && primaryKey != nil) { + row = table->add_empty_row(); + table->set_string(c_primaryKeyObjectClassColumnIndex, row, RLMStringDataWithNSString(objectClass)); + } + + // set if changing, or remove if setting to nil + if (primaryKey == nil && row != tightdb::not_found) { + table->remove(row); + } + else { + table->set_string(c_primaryKeyPropertyNameColumnIndex, row, RLMStringDataWithNSString(primaryKey)); + } } + + (Class)classForString:(NSString *)className { -#if REALM_SWIFT - if (s_classNameToMangledName[className]) { - className = s_classNameToMangledName[className]; + if (Class cls = s_localNameToClass[className]) { + return cls; } -#endif return NSClassFromString(className); } diff --git a/Realm/RLMSchema_Private.h b/Realm/RLMSchema_Private.h index f0a6b8c36d..a3cc3fd79f 100644 --- a/Realm/RLMSchema_Private.h +++ b/Realm/RLMSchema_Private.h @@ -33,10 +33,6 @@ extern const char *c_versionColumnName; extern const size_t c_versionColumnIndex; extern const NSUInteger RLMNotVersioned; -inline NSString *RLMTableNameForClassName(NSString *className) { - return [c_objectTableNamePrefix stringByAppendingString:className]; -} - inline NSString *RLMClassForTableName(NSString *tableName) { if ([tableName hasPrefix:c_objectTableNamePrefix]) { return [tableName substringFromIndex:6]; @@ -44,15 +40,26 @@ inline NSString *RLMClassForTableName(NSString *tableName) { return nil; } +inline NSString *RLMTableNameForClass(NSString *className) { + return [c_objectTableNamePrefix stringByAppendingString:className]; +} + // -// Realm schema version +// Realm schema metadata // NSUInteger RLMRealmSchemaVersion(RLMRealm *realm); // must be in write transaction to set void RLMRealmSetSchemaVersion(RLMRealm *realm, NSUInteger version); +// get primary key property name for object class +NSString *RLMRealmPrimaryKeyForObjectClass(RLMRealm *realm, NSString *objectClass); + +// sets primary key property for object class +// must be in write transaction to set +void RLMRealmSetPrimaryKeyForObjectClass(RLMRealm *realm, NSString *objectClass, NSString *primaryKey); + // // RLMSchema private interface @@ -61,9 +68,6 @@ void RLMRealmSetSchemaVersion(RLMRealm *realm, NSUInteger version); @interface RLMSchema () @property (nonatomic, readwrite, copy) NSArray *objectSchema; -// mapping of className to tableName -@property (nonatomic, readonly) NSMutableDictionary *tableNamesForClass; - // schema based on runtime objects +(instancetype)sharedSchema; diff --git a/Realm/RLMSwiftSupport.h b/Realm/RLMSwiftSupport.h new file mode 100644 index 0000000000..cf6f181e26 --- /dev/null +++ b/Realm/RLMSwiftSupport.h @@ -0,0 +1,34 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2014 Realm Inc. +// +// 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. +// +//////////////////////////////////////////////////////////////////////////// + +#import + +#if REALM_SWIFT +#import +#else + +// A dummy implementation of RLMSwiftSupport for Xcode 5 to avoid ifdef sea +@interface RLMSwiftSupport : NSObject + ++ (BOOL)isSwiftClassName:(NSString *)className; ++ (NSArray *)propertiesForClass:(Class)cls; ++ (NSString *)demangleClassName:(NSString *)className; + +@end + +#endif diff --git a/Realm/RLMSwiftSupportFallback.m b/Realm/RLMSwiftSupportFallback.m new file mode 100644 index 0000000000..e006749a3d --- /dev/null +++ b/Realm/RLMSwiftSupportFallback.m @@ -0,0 +1,35 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2014 Realm Inc. +// +// 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. +// +//////////////////////////////////////////////////////////////////////////// + +#import "RLMSwiftSupport.h" + +@implementation RLMSwiftSupport + ++ (BOOL)isSwiftClassName:(__unused NSString *)className { + return NO; +} + ++ (NSArray *)propertiesForClass:(__unused Class)cls { + return nil; +} + ++ (NSString *)demangleClassName:(NSString *)className { + return className; +} + +@end diff --git a/Realm/RLMUtil.mm b/Realm/RLMUtil.mm index d4dff56710..a322834c64 100644 --- a/Realm/RLMUtil.mm +++ b/Realm/RLMUtil.mm @@ -29,9 +29,11 @@ static inline bool nsnumber_is_like_integer(NSNumber *obj) { const char *data_type = [obj objCType]; // FIXME: Performance optimization - don't use strcmp, use first char in data_type. - return (strcmp(data_type, @encode(int)) == 0 || + return (strcmp(data_type, @encode(short)) == 0 || + strcmp(data_type, @encode(int)) == 0 || strcmp(data_type, @encode(long)) == 0 || strcmp(data_type, @encode(long long)) == 0 || + strcmp(data_type, @encode(unsigned short)) == 0 || strcmp(data_type, @encode(unsigned int)) == 0 || strcmp(data_type, @encode(unsigned long)) == 0 || strcmp(data_type, @encode(unsigned long long)) == 0); @@ -58,9 +60,11 @@ static inline bool nsnumber_is_like_float(NSNumber *obj) const char *data_type = [obj objCType]; // FIXME: Performance optimization - don't use strcmp, use first char in data_type. return (strcmp(data_type, @encode(float)) == 0 || + strcmp(data_type, @encode(short)) == 0 || strcmp(data_type, @encode(int)) == 0 || strcmp(data_type, @encode(long)) == 0 || strcmp(data_type, @encode(long long)) == 0 || + strcmp(data_type, @encode(unsigned short)) == 0 || strcmp(data_type, @encode(unsigned int)) == 0 || strcmp(data_type, @encode(unsigned long)) == 0 || strcmp(data_type, @encode(unsigned long long)) == 0 || @@ -74,9 +78,11 @@ static inline bool nsnumber_is_like_double(NSNumber *obj) // FIXME: Performance optimization - don't use strcmp, use first char in data_type. return (strcmp(data_type, @encode(double)) == 0 || strcmp(data_type, @encode(float)) == 0 || + strcmp(data_type, @encode(short)) == 0 || strcmp(data_type, @encode(int)) == 0 || strcmp(data_type, @encode(long)) == 0 || strcmp(data_type, @encode(long long)) == 0 || + strcmp(data_type, @encode(unsigned short)) == 0 || strcmp(data_type, @encode(unsigned int)) == 0 || strcmp(data_type, @encode(unsigned long)) == 0 || strcmp(data_type, @encode(unsigned long long)) == 0); diff --git a/Realm/Realm-Bridging-Header.h b/Realm/Realm-Bridging-Header.h index e039fd9a50..74f23dc8d5 100644 --- a/Realm/Realm-Bridging-Header.h +++ b/Realm/Realm-Bridging-Header.h @@ -20,6 +20,7 @@ #import "RLMObjectSchema.h" #import "RLMProperty.h" #import "RLMObject.h" +#import @interface RLMRealm () + (void)clearRealmCache; @@ -28,9 +29,9 @@ @interface RLMProperty () - (instancetype)initWithName:(NSString *)name - type:(RLMPropertyType)type - objectClassName:(NSString *)objectClassName - attributes:(RLMPropertyAttributes)attributes; + attributes:(RLMPropertyAttributes)attributes + attributeList:(objc_property_attribute_t *)attrs + attributeCount:(unsigned int)attrCount; @end diff --git a/Realm/Swift/RLMSwiftSupport.swift b/Realm/Swift/RLMSwiftSupport.swift index 93d5f7f10b..38248b5bb9 100644 --- a/Realm/Swift/RLMSwiftSupport.swift +++ b/Realm/Swift/RLMSwiftSupport.swift @@ -28,12 +28,12 @@ import Foundation return className.substringFromIndex(className.rangeOfString(".").location + 1) } - public class func schemaForObjectClass(aClass: AnyClass) -> RLMObjectSchema { + public class func propertiesForClass(aClass: AnyClass) -> [RLMProperty] { let className = demangleClassName(NSStringFromClass(aClass)) let swiftObject = (aClass as RLMObject.Type)() let reflection = reflect(swiftObject) - let ignoredPropertiesForClass = aClass.ignoredProperties() as NSArray? + let ignoredPropertiesForClass = (aClass.ignoredProperties() ?? []) as NSArray var properties = [RLMProperty]() @@ -41,63 +41,56 @@ import Foundation // super is an implicit property on Swift objects for i in 1.. RLMProperty { - let valueType = mirror.valueType - let (p, t) = { () -> (RLMProperty, String) in - switch valueType { - // Detect basic types (including optional versions) - case is Bool.Type, is Bool?.Type: - return (RLMProperty(name: name, type: .Bool, objectClassName: nil, attributes: attr), "c") - case is Int.Type, is Int?.Type: -#if arch(x86_64) || arch(arm64) - let t = "l" -#else - let t = "i" -#endif - return (RLMProperty(name: name, type: .Int, objectClassName: nil, attributes: attr), t) - case is Float.Type, is Float?.Type: - return (RLMProperty(name: name, type: .Float, objectClassName: nil, attributes: attr), "f") - case is Double.Type, is Double?.Type: - return (RLMProperty(name: name, type: .Double, objectClassName: nil, attributes: attr), "d") - case is String.Type, is String?.Type: - return (RLMProperty(name: name, type: .String, objectClassName: nil, attributes: attr), "S") - case is NSData.Type, is NSData?.Type: - return (RLMProperty(name: name, type: .Data, objectClassName: nil, attributes: attr), "@\"NSData\"") - case is NSDate.Type, is NSDate?.Type: - return (RLMProperty(name: name, type: .Date, objectClassName: nil, attributes: attr), "@\"NSDate\"") - case let objectType as RLMObject.Type: - let mangledClassName = NSStringFromClass(objectType.self) - let objectClassName = self.demangleClassName(mangledClassName) - let typeEncoding = "@\"\(mangledClassName))\"" - return (RLMProperty(name: name, type: .Object, objectClassName: objectClassName, attributes: attr), typeEncoding) - case let c as RLMArray.Type: - let objectClassName = (mirror.value as RLMArray).objectClassName - return (RLMProperty(name: name, type: .Array, objectClassName: objectClassName, attributes: attr), "@\"RLMArray\"") - default: - println("Can't persist property '\(name)' with incompatible type.\nAdd to ignoredPropertyNames: method to ignore.") - abort() - } - }() - - // create objc property - let attr = objc_property_attribute_t(name: "T", value: t) - class_addProperty(aClass, p.name, [attr], 1) - return p + class func objcTypeForSwiftType(name: String, mirror: MirrorType) -> String { + let valueType = mirror.valueType + switch valueType { + // Detect basic types (including optional versions) + // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html + case is Bool.Type, is Bool?.Type: + return "c" + case is Int.Type, is Int?.Type: + return "l" + case is Int16.Type, is Int16?.Type: + return "s" + case is Int32.Type, is Int32?.Type: + return "i" + case is Int64.Type, is Int64?.Type: + return "q" + case is Float.Type, is Float?.Type: + return "f" + case is Double.Type, is Double?.Type: + return "d" + case is String.Type, is String?.Type: + return "@\"NSString\"" + case is NSData.Type, is NSData?.Type: + return "@\"NSData\"" + case is NSDate.Type, is NSDate?.Type: + return "@\"NSDate\"" + case let objectType as RLMObject.Type: + return "@\"\(NSStringFromClass(objectType.self))\"" + case is RLMArray.Type: + return "@\"RLMArray<\((mirror.value as RLMArray).objectClassName)>\"" + default: + println("Can't persist property '\(name)' with incompatible type.\nAdd to ignoredPropertyNames: method to ignore.") + abort() + } } } diff --git a/Realm/Tests/ArrayPropertyTests.m b/Realm/Tests/ArrayPropertyTests.m index 058fc7456a..cddc5c4d95 100644 --- a/Realm/Tests/ArrayPropertyTests.m +++ b/Realm/Tests/ArrayPropertyTests.m @@ -130,9 +130,9 @@ -(void)testStandalone { [realm beginWriteTransaction]; [array.array replaceObjectAtIndex:0 withObject:obj3]; - XCTAssertEqualObjects([array.array objectAtIndex:0], obj3, @"Objects should be replaced"); + XCTAssertTrue([[array.array objectAtIndex:0] isEqualToObject:obj3], @"Objects should be replaced"); array.array[0] = obj1; - XCTAssertEqualObjects([array.array objectAtIndex:0], obj1, @"Objects should be replaced"); + XCTAssertTrue([obj1 isEqualToObject:[array.array objectAtIndex:0]], @"Objects should be replaced"); [array.array removeLastObject]; XCTAssertEqual(array.array.count, (NSUInteger)2, @"2 objects left"); [array.array addObject:obj1]; diff --git a/Realm/Tests/MigrationTests.mm b/Realm/Tests/MigrationTests.mm index 5dbb902bf5..74330dd466 100644 --- a/Realm/Tests/MigrationTests.mm +++ b/Realm/Tests/MigrationTests.mm @@ -20,10 +20,11 @@ #import "RLMTestCase.h" } #import "RLMMigration.h" +#import "RLMObjectSchema_Private.hpp" +#import "RLMObjectStore.hpp" #import "RLMProperty_Private.h" +#import "RLMRealm_Dynamic.h" #import "RLMSchema_Private.h" -#import "RLMObjectStore.hpp" -#import "RLMObjectSchema_Private.hpp" @interface MigrationObject : RLMObject @property int intCol; @@ -33,6 +34,16 @@ @interface MigrationObject : RLMObject @implementation MigrationObject @end +@interface MigrationPrimaryKeyObject : RLMObject +@property int intCol; +@end + +@implementation MigrationPrimaryKeyObject ++ (NSString *)primaryKey { + return @"intCol"; +} +@end + @interface MigrationTests : RLMTestCase @end @@ -184,6 +195,36 @@ - (void)testChangePropertyType { XCTAssertEqualObjects(mig1[@"stringCol"], @"2", @"stringCol should be string after migration."); } +- (void)testPrimaryKeyMigration { + // make string an int + RLMObjectSchema *objectSchema = [RLMObjectSchema schemaForObjectClass:MigrationPrimaryKeyObject.class]; + objectSchema.primaryKeyProperty.isPrimary = NO; + objectSchema.primaryKeyProperty = nil; + + // create realm with old schema and populate + RLMRealm *realm = [self realmWithSingleObject:objectSchema]; + [realm beginWriteTransaction]; + RLMCreateObjectInRealmWithValue(realm, MigrationPrimaryKeyObject.className, @[@1]); + RLMCreateObjectInRealmWithValue(realm, MigrationPrimaryKeyObject.className, @[@1]); + [realm commitWriteTransaction]; + + // apply migration + XCTAssertThrows([RLMRealm migrateRealmAtPath:RLMTestRealmPath() + withBlock:^NSUInteger(__unused RLMMigration *migration, + __unused NSUInteger oldSchemaVersion) { return 1; }], + @"Migration should throw due to duplicate primary keys)"); + + + [RLMRealm migrateRealmAtPath:RLMTestRealmPath() + withBlock:^NSUInteger(RLMMigration *migration, __unused NSUInteger oldSchemaVersion) { + __block int objectID = 0; + [migration enumerateObjects:@"MigrationPrimaryKeyObject" block:^(__unused RLMObject *oldObject, RLMObject *newObject) { + newObject[@"intCol"] = @(objectID++); + }]; + return 1; + }]; +} + - (void)testVersionNumberCanStaySameWithNoSchemaChanges { @autoreleasepool { [self dynamicRealmWithTestPathAndSchema:[RLMSchema sharedSchema]]; } diff --git a/Realm/Tests/ObjectTests.m b/Realm/Tests/ObjectTests.m index ae2b242b53..ac16cf0f72 100644 --- a/Realm/Tests/ObjectTests.m +++ b/Realm/Tests/ObjectTests.m @@ -23,8 +23,6 @@ #pragma mark - Test Objects -#pragma mark DefaultObject - @interface DefaultObject : RLMObject @property int intCol; @property float floatCol; @@ -53,7 +51,6 @@ + (NSDictionary *)defaultPropertyValues } @end -#pragma mark IgnoredURLObject @interface IgnoredURLObject : RLMObject @property NSString *name; @@ -67,7 +64,6 @@ + (NSArray *)ignoredProperties } @end -#pragma mark IndexedObject @interface IndexedObject : RLMObject @property NSString *name; @@ -85,7 +81,6 @@ + (RLMPropertyAttributes)attributesForProperty:(NSString *)propertyName } @end -#pragma mark CycleObject @class CycleObject; RLM_ARRAY_TYPE(CycleObject) @interface CycleObject :RLMObject @@ -95,7 +90,6 @@ @interface CycleObject :RLMObject @implementation CycleObject @end -#pragma mark DogExtraObject @interface DogExtraObject : RLMObject @property NSString *dogName; @property int age; @@ -105,6 +99,40 @@ @interface DogExtraObject : RLMObject @implementation DogExtraObject @end +@interface PrimaryIntObject : RLMObject +@property int intCol; +@end +RLM_ARRAY_TYPE(PrimaryIntObject); + +@implementation PrimaryIntObject ++ (NSString *)primaryKey { + return @"intCol"; +} +@end + +@interface PrimaryInt64Object : RLMObject +@property int64_t int64Col; +@end + +@implementation PrimaryInt64Object ++ (NSString *)primaryKey { + return @"int64Col"; +} +@end + +@interface PrimaryNestedObject : RLMObject +@property int primaryCol; +@property PrimaryStringObject *primaryStringObject; +@property StringObject *stringObject; +@property RLMArray *primaryIntArray; +@end + +@implementation PrimaryNestedObject ++ (NSString *)primaryKey { + return @"primaryCol"; +} +@end + #pragma mark - Private @interface RLMRealm () @@ -371,8 +399,8 @@ - (void)testDataTypes XCTAssertTrue([row2.binaryCol isEqual:bin2], @"row2.BinaryCol"); XCTAssertTrue(([row1.dateCol isEqual:timeZero]), @"row1.DateCol"); XCTAssertTrue(([row2.dateCol isEqual:timeNow]), @"row2.DateCol"); - XCTAssertEqual(row1.cBoolCol, (bool)false, @"row1.cBoolCol"); - XCTAssertEqual(row2.cBoolCol, (bool)true, @"row2.cBoolCol"); + XCTAssertEqual(row1.cBoolCol, false, @"row1.cBoolCol"); + XCTAssertEqual(row2.cBoolCol, true, @"row2.cBoolCol"); XCTAssertEqual(row1.longCol, 99L, @"row1.IntCol"); XCTAssertEqual(row2.longCol, -20L, @"row2.IntCol"); XCTAssertTrue([row1.objectCol.stringCol isEqual:@"c"], @"row1.objectCol"); @@ -776,7 +804,7 @@ - (void)testEquality [realm commitWriteTransaction]; XCTAssertFalse([obj isEqual:otherObj], @"One in realm, the other is not."); - XCTAssertTrue([obj isEqual:[IntObject allObjects][0]], @"Same table and index."); + XCTAssertTrue([obj isEqualToObject:[IntObject allObjects][0]], @"Same table and index."); [otherRealm beginWriteTransaction]; [otherRealm addObject: otherObj]; @@ -849,4 +877,109 @@ - (void)testIsDeleted { XCTAssertNil(obj1.realm, @"Realm should be nil after deletion"); } +- (void)testPrimaryKey { + [[RLMRealm defaultRealm] beginWriteTransaction]; + + [PrimaryStringObject createInDefaultRealmWithObject:(@[@"string", @1])]; + PrimaryStringObject *obj = [PrimaryStringObject createInDefaultRealmWithObject:(@[@"string2", @1])]; + XCTAssertThrows([PrimaryStringObject createInDefaultRealmWithObject:(@[@"string", @1])], @"Duplicate primary key should throw"); + XCTAssertThrows(obj.stringCol = @"string2", @"Setting primary key should throw"); + + [PrimaryIntObject createInDefaultRealmWithObject:(@[@1])]; + PrimaryIntObject *obj1 = [PrimaryIntObject createInDefaultRealmWithObject:(@{@"intCol": @2})]; + XCTAssertThrows([PrimaryIntObject createInDefaultRealmWithObject:(@[@1])], @"Duplicate primary key should throw"); + XCTAssertThrows(obj1.intCol = 2, @"Setting primary key should throw"); + + [PrimaryInt64Object createInDefaultRealmWithObject:(@[@(1LL << 40)])]; + PrimaryInt64Object *obj2 = [PrimaryInt64Object createInDefaultRealmWithObject:(@[@(1LL << 41)])]; + XCTAssertThrows([PrimaryInt64Object createInDefaultRealmWithObject:(@[@(1LL << 40)])], @"Duplicate primary key should throw"); + XCTAssertThrows(obj2.int64Col = 1LL << 41, @"Setting primary key should throw"); + + [[RLMRealm defaultRealm] commitWriteTransaction]; +} + +- (void)testCreateOrUpdate { + RLMRealm *realm = [RLMRealm defaultRealm]; + [realm beginWriteTransaction]; + + [PrimaryStringObject createOrUpdateInDefaultRealmWithObject:@[@"string", @1]]; + RLMArray *objects = [PrimaryStringObject allObjects]; + XCTAssertEqual([objects count], 1U, @"Should have 1 object"); + XCTAssertEqual([(PrimaryStringObject *)objects[0] intCol], 1, @"Value should be 1"); + + [PrimaryStringObject createOrUpdateInRealm:realm withObject:@{@"stringCol": @"string2", @"intCol": @2}]; + XCTAssertEqual([objects count], 2U, @"Should have 2 objects"); + + // upsert with new secondary property + [PrimaryStringObject createOrUpdateInDefaultRealmWithObject:@[@"string", @3]]; + XCTAssertEqual([objects count], 2U, @"Should have 2 objects"); + XCTAssertEqual([(PrimaryStringObject *)objects[0] intCol], 3, @"Value should be 3"); + + // upsert on non-primary key object shoudld throw + XCTAssertThrows([StringObject createOrUpdateInDefaultRealmWithObject:@[@"string"]]); + + [realm commitWriteTransaction]; +} + +- (void)testCreateOrUpdateNestedObjects { + RLMRealm *realm = [RLMRealm defaultRealm]; + [realm beginWriteTransaction]; + + [PrimaryNestedObject createOrUpdateInDefaultRealmWithObject:@[@0, @[@"string", @1], @[@"string"], @[@[@1]]]]; + XCTAssertEqual([[PrimaryNestedObject allObjects] count], 1U, @"Should have 1 object"); + XCTAssertEqual([[PrimaryStringObject allObjects] count], 1U, @"Should have 1 object"); + XCTAssertEqual([[PrimaryIntObject allObjects] count], 1U, @"Should have 1 object"); + + // update parent and nested object + [PrimaryNestedObject createOrUpdateInDefaultRealmWithObject:@{@"primaryCol": @0, + @"primaryStringObject": @[@"string", @2], + @"stringObject": @[@"string2"]}]; + XCTAssertEqual([[PrimaryNestedObject allObjects] count], 1U, @"Should have 1 object"); + XCTAssertEqual([[PrimaryStringObject allObjects] count], 1U, @"Should have 1 object"); + XCTAssertEqual([(PrimaryStringObject *)[[PrimaryStringObject allObjects] lastObject] intCol], 2, @"intCol should be 2"); + XCTAssertEqual([[StringObject allObjects] count], 2U, @"Should have 2 objects"); + + // inserting new object should update nested + PrimaryNestedObject *obj = [PrimaryNestedObject createOrUpdateInDefaultRealmWithObject:@[@1, @[@"string", @3], @[@"string"], @[]]]; + XCTAssertEqual([[PrimaryNestedObject allObjects] count], 2U, @"Should have 2 objects"); + XCTAssertEqual([[PrimaryStringObject allObjects] count], 1U, @"Should have 1 object"); + XCTAssertEqual([(PrimaryStringObject *)[[PrimaryStringObject allObjects] lastObject] intCol], 3, @"intCol should be 3"); + + // set addOrUpdate + obj.primaryStringObject = [PrimaryStringObject createInDefaultRealmWithObject:@[@"string2", @1]]; + PrimaryNestedObject *obj1 = [[PrimaryNestedObject alloc] initWithObject:@[@1, @[@"string2", @4], @[@"string"], @[@[@1], @[@2]]]]; + [realm addOrUpdateObject:obj1]; + XCTAssertEqual([[PrimaryNestedObject allObjects] count], 2U, @"Should have 2 objects"); + XCTAssertEqual([[PrimaryStringObject allObjects] count], 2U, @"Should have 2 objects"); + XCTAssertEqual([[PrimaryIntObject allObjects] count], 2U, @"Should have 2 objects"); + XCTAssertEqual([(PrimaryStringObject *)[[PrimaryStringObject allObjects] lastObject] intCol], 4, @"intCol should be 4"); + + // creating new object with same primary key should throw + XCTAssertThrows([PrimaryStringObject createInDefaultRealmWithObject:(@[@"string", @1])]); + + [realm commitWriteTransaction]; +} + +- (void)testObjectInSet { + [[RLMRealm defaultRealm] beginWriteTransaction]; + + // set object with primary and non primary keys as they both override isEqual and hash + PrimaryStringObject *obj = [PrimaryStringObject createInDefaultRealmWithObject:(@[@"string2", @1])]; + StringObject *strObj = [StringObject createInDefaultRealmWithObject:@[@"string"]]; + NSMutableSet *dict = [NSMutableSet set]; + [dict addObject:obj]; + [dict addObject:strObj]; + + // primary key objects should match even with duplicate instances of the same object + XCTAssertTrue([dict containsObject:obj]); + XCTAssertTrue([dict containsObject:[[PrimaryStringObject allObjects] firstObject]]); + + // non-primary key objects should only match when comparing identical instances + XCTAssertTrue([dict containsObject:strObj]); + XCTAssertFalse([dict containsObject:[[StringObject allObjects] firstObject]]); + + [[RLMRealm defaultRealm] commitWriteTransaction]; +} + + @end diff --git a/Realm/Tests/PropertyTypeTest.mm b/Realm/Tests/PropertyTypeTest.mm index a7f582d990..e2d12766d7 100644 --- a/Realm/Tests/PropertyTypeTest.mm +++ b/Realm/Tests/PropertyTypeTest.mm @@ -57,4 +57,78 @@ - (void)testPropertyTypes XCTAssertEqualObjects(RLMTypeToString(RLMPropertyType(-1)), @"Unknown", @"Unknown type"); } +- (void)testIntSizes +{ + RLMRealm *realm = [self realmWithTestPath]; + + int16_t v16 = 1 << 12; + int32_t v32 = 1 << 30; + int64_t v64 = 1LL << 40; + + AllIntSizesObject *obj = [AllIntSizesObject new]; + + // Test standalone + obj[@"int16"] = @(v16); + XCTAssertEqual([obj[@"int16"] shortValue], v16); + obj[@"int16"] = @(v32); + XCTAssertNotEqual([obj[@"int16"] intValue], v32, @"should truncate"); + + obj.int16 = 0; + obj.int16 = v16; + XCTAssertEqual(obj.int16, v16); + + obj[@"int32"] = @(v32); + XCTAssertEqual([obj[@"int32"] intValue], v32); + obj[@"int32"] = @(v64); + XCTAssertNotEqual([obj[@"int32"] longLongValue], v64, @"should truncate"); + + obj.int32 = 0; + obj.int32 = v32; + XCTAssertEqual(obj.int32, v32); + + obj[@"int64"] = @(v64); + XCTAssertEqual([obj[@"int64"] longLongValue], v64); + obj.int64 = 0; + obj.int64 = v64; + XCTAssertEqual(obj.int64, v64); + + // Test in realm + [realm beginWriteTransaction]; + [realm addObject:obj]; + + XCTAssertEqual(obj.int16, v16); + XCTAssertEqual(obj.int32, v32); + XCTAssertEqual(obj.int64, v64); + + obj.int16 = 0; + obj.int32 = 0; + obj.int64 = 0; + + obj[@"int16"] = @(v16); + XCTAssertEqual([obj[@"int16"] shortValue], v16); + obj[@"int16"] = @(v32); + XCTAssertNotEqual([obj[@"int16"] intValue], v32, @"should truncate"); + + obj.int16 = 0; + obj.int16 = v16; + XCTAssertEqual(obj.int16, v16); + + obj[@"int32"] = @(v32); + XCTAssertEqual([obj[@"int32"] intValue], v32); + obj[@"int32"] = @(v64); + XCTAssertNotEqual([obj[@"int32"] longLongValue], v64, @"should truncate"); + + obj.int32 = 0; + obj.int32 = v32; + XCTAssertEqual(obj.int32, v32); + + obj[@"int64"] = @(v64); + XCTAssertEqual([obj[@"int64"] longLongValue], v64); + obj.int64 = 0; + obj.int64 = v64; + XCTAssertEqual(obj.int64, v64); + + [realm commitWriteTransaction]; +} + @end diff --git a/Realm/Tests/QueryTests.m b/Realm/Tests/QueryTests.m index 6a5777f36b..b28903b415 100644 --- a/Realm/Tests/QueryTests.m +++ b/Realm/Tests/QueryTests.m @@ -1212,17 +1212,17 @@ - (void)testMultiLevelLinkQuery } [realm commitWriteTransaction]; - XCTAssertEqualObjects(circle, [CircleObject objectsInRealm:realm where:@"data = '4'"].firstObject); - XCTAssertEqualObjects(circle, [CircleObject objectsInRealm:realm where:@"next.data = '3'"].firstObject); - XCTAssertEqualObjects(circle, [CircleObject objectsInRealm:realm where:@"next.next.data = '2'"].firstObject); - XCTAssertEqualObjects(circle, [CircleObject objectsInRealm:realm where:@"next.next.next.data = '1'"].firstObject); - XCTAssertEqualObjects(circle, [CircleObject objectsInRealm:realm where:@"next.next.next.next.data = '0'"].firstObject); - XCTAssertEqualObjects(circle.next, [CircleObject objectsInRealm:realm where:@"next.next.next.data = '0'"].firstObject); - XCTAssertEqualObjects(circle.next.next, [CircleObject objectsInRealm:realm where:@"next.next.data = '0'"].firstObject); + XCTAssertTrue([circle isEqualToObject:[CircleObject objectsInRealm:realm where:@"data = '4'"].firstObject]); + XCTAssertTrue([circle isEqualToObject:[CircleObject objectsInRealm:realm where:@"next.data = '3'"].firstObject]); + XCTAssertTrue([circle isEqualToObject:[CircleObject objectsInRealm:realm where:@"next.next.data = '2'"].firstObject]); + XCTAssertTrue([circle isEqualToObject:[CircleObject objectsInRealm:realm where:@"next.next.next.data = '1'"].firstObject]); + XCTAssertTrue([circle isEqualToObject:[CircleObject objectsInRealm:realm where:@"next.next.next.next.data = '0'"].firstObject]); + XCTAssertTrue([circle.next isEqualToObject:[CircleObject objectsInRealm:realm where:@"next.next.next.data = '0'"].firstObject]); + XCTAssertTrue([circle.next.next isEqualToObject:[CircleObject objectsInRealm:realm where:@"next.next.data = '0'"].firstObject]); XCTAssertNoThrow(([CircleObject objectsInRealm:realm where:@"next = %@", circle])); XCTAssertThrows(([CircleObject objectsInRealm:realm where:@"next.next = %@", circle])); - XCTAssertEqualObjects(circle.next.next.next.next, [CircleObject objectsInRealm:realm where:@"next = nil"].firstObject); + XCTAssertTrue([circle.next.next.next.next isEqualToObject:[CircleObject objectsInRealm:realm where:@"next = nil"].firstObject]); } - (void)testArrayMultiLevelLinkQuery diff --git a/Realm/Tests/RLMTestObjects.h b/Realm/Tests/RLMTestObjects.h index 0b7694aaca..ed335c2508 100644 --- a/Realm/Tests/RLMTestObjects.h +++ b/Realm/Tests/RLMTestObjects.h @@ -36,6 +36,15 @@ @end +@interface AllIntSizesObject : RLMObject +// int8_t not supported due to being ambiguous with BOOL + +@property int16_t int16; +@property int32_t int32; +@property int64_t int64; + +@end + @interface FloatObject : RLMObject @property float floatCol; @@ -235,3 +244,11 @@ RLM_ARRAY_TYPE(CircleObject); @property NSDate *dateCol; @end + + +@interface PrimaryStringObject : RLMObject +@property NSString *stringCol; +@property int intCol; +@end + + diff --git a/Realm/Tests/RLMTestObjects.m b/Realm/Tests/RLMTestObjects.m index cf7851afbe..b806e4fed3 100644 --- a/Realm/Tests/RLMTestObjects.m +++ b/Realm/Tests/RLMTestObjects.m @@ -29,6 +29,9 @@ @implementation StringObject @implementation IntObject @end +@implementation AllIntSizesObject +@end + @implementation FloatObject @end @@ -123,3 +126,10 @@ @implementation DynamicObject @implementation AggregateObject @end + + +@implementation PrimaryStringObject ++ (NSString *)primaryKey { + return @"stringCol"; +} +@end diff --git a/Realm/Tests/RealmTests.mm b/Realm/Tests/RealmTests.mm index 55099d24df..a9e8e0a9d3 100644 --- a/Realm/Tests/RealmTests.mm +++ b/Realm/Tests/RealmTests.mm @@ -444,4 +444,27 @@ - (void)testReadOnlyRealmWithMissingColumns @"should reject table missing column"); } +- (void)testAddOrUpdate { + RLMRealm *realm = [RLMRealm defaultRealm]; + [realm beginWriteTransaction]; + + PrimaryStringObject *obj = [[PrimaryStringObject alloc] initWithObject:@[@"string", @1]]; + [realm addOrUpdateObject:obj]; + RLMArray *objects = [PrimaryStringObject allObjects]; + XCTAssertEqual([objects count], 1U, @"Should have 1 object"); + XCTAssertEqual([(PrimaryStringObject *)objects[0] intCol], 1, @"Value should be 1"); + + PrimaryStringObject *obj2 = [[PrimaryStringObject alloc] initWithObject:@[@"string2", @2]]; + [realm addOrUpdateObject:obj2]; + XCTAssertEqual([objects count], 2U, @"Should have 2 objects"); + + // upsert with new secondary property + PrimaryStringObject *obj3 = [[PrimaryStringObject alloc] initWithObject:@[@"string", @3]]; + [realm addOrUpdateObject:obj3]; + XCTAssertEqual([objects count], 2U, @"Should have 2 objects"); + XCTAssertEqual([(PrimaryStringObject *)objects[0] intCol], 3, @"Value should be 3"); + + [realm commitWriteTransaction]; +} + @end diff --git a/Realm/Tests/Swift/SwiftArrayPropertyTests.swift b/Realm/Tests/Swift/SwiftArrayPropertyTests.swift index 1d53136876..8cedc529e2 100644 --- a/Realm/Tests/Swift/SwiftArrayPropertyTests.swift +++ b/Realm/Tests/Swift/SwiftArrayPropertyTests.swift @@ -49,7 +49,8 @@ class SwiftArrayPropertyTests: SwiftTestCase { let arrayObjects = SwiftArrayPropertyObject.allObjectsInRealm(realm) XCTAssertEqual(arrayObjects.count, 1, "There should be a single SwiftStringObject in the realm") - XCTAssertEqual((arrayObjects.firstObject() as SwiftArrayPropertyObject).array.firstObject() as SwiftStringObject, string, "First array object should be the string object we added") + var cmp = (arrayObjects.firstObject() as SwiftArrayPropertyObject).array.firstObject() as SwiftStringObject + XCTAssertTrue(string.isEqualToObject(cmp), "First array object should be the string object we added") } func testPopulateEmptyArray() { @@ -159,7 +160,8 @@ class SwiftArrayPropertyTests: SwiftTestCase { let arrayObjects = ArrayPropertyObject.allObjectsInRealm(realm) XCTAssertEqual(arrayObjects.count, 1, "There should be a single StringObject in the realm") - XCTAssertEqual((arrayObjects.firstObject() as ArrayPropertyObject).array.firstObject() as StringObject, string, "First array object should be the string object we added") + var cmp = (arrayObjects.firstObject() as ArrayPropertyObject).array.firstObject() as StringObject + XCTAssertTrue(string.isEqualToObject(cmp), "First array object should be the string object we added") } func testPopulateEmptyArray_objc() { diff --git a/Realm/Tests/Swift/SwiftObjectInterfaceTests.swift b/Realm/Tests/Swift/SwiftObjectInterfaceTests.swift index e4bd3c4b92..e31d20945f 100644 --- a/Realm/Tests/Swift/SwiftObjectInterfaceTests.swift +++ b/Realm/Tests/Swift/SwiftObjectInterfaceTests.swift @@ -137,4 +137,22 @@ class SwiftObjectInterfaceTests: SwiftTestCase { XCTAssertEqual(objectFromRealm.intCol, 1, "Should be 1") XCTAssertEqual(objectFromRealm.stringCol!, "stringVal", "Should be stringVal") } + + func testCreateOrUpdate() { + let realm = RLMRealm.defaultRealm() + realm.beginWriteTransaction() + SwiftPrimaryStringObject.createOrUpdateInDefaultRealmWithObject(["string", 1]) + let objects = SwiftPrimaryStringObject.allObjects(); + XCTAssertEqual(objects.count, 1, "Should have 1 object"); + XCTAssertEqual((objects[0] as SwiftPrimaryStringObject).intCol, 1, "Value should be 1"); + + SwiftPrimaryStringObject.createOrUpdateInDefaultRealmWithObject(["stringCol": "string2", "intCol": 2]) + XCTAssertEqual(objects.count, 2, "Should have 2 objects") + + SwiftPrimaryStringObject.createOrUpdateInDefaultRealmWithObject(["string", 3]) + XCTAssertEqual(objects.count, 2, "Should have 2 objects") + XCTAssertEqual((objects[0] as SwiftPrimaryStringObject).intCol, 3, "Value should be 3"); + + realm.commitWriteTransaction() + } } diff --git a/Realm/Tests/Swift/SwiftPropertyTypeTest.swift b/Realm/Tests/Swift/SwiftPropertyTypeTest.swift index 6ae2836b20..7d10519927 100644 --- a/Realm/Tests/Swift/SwiftPropertyTypeTest.swift +++ b/Realm/Tests/Swift/SwiftPropertyTypeTest.swift @@ -48,4 +48,56 @@ class SwiftPropertyTypeTest: SwiftTestCase { XCTAssertEqual((objects[0] as SwiftIntObject).intCol, updatedLongNumber, "After update: 2 ^ 33 expected") } + + func testIntSizes() { + let realm = realmWithTestPath() + + let v16 = Int16(1) << 12 + let v32 = Int32(1) << 30 + // 1 << 40 doesn't auto-promote to Int64 on 32-bit platforms + let v64 = Int64(1) << 40 + realm.transactionWithBlock() { + let obj = SwiftAllIntSizesObject() + + obj.int16 = v16 + XCTAssertEqual(obj.int16, v16) + obj.int32 = v32 + XCTAssertEqual(obj.int32, v32) + obj.int64 = v64 + XCTAssertEqual(obj.int64, v64) + + realm.addObject(obj) + } + + let obj = SwiftAllIntSizesObject.allObjectsInRealm(realm)[0]! as SwiftAllIntSizesObject + XCTAssertEqual(obj.int16, v16) + XCTAssertEqual(obj.int32, v32) + XCTAssertEqual(obj.int64, v64) + } + + func testIntSizes_objc() { + let realm = realmWithTestPath() + + let v16 = Int16(1) << 12 + let v32 = Int32(1) << 30 + // 1 << 40 doesn't auto-promote to Int64 on 32-bit platforms + let v64 = Int64(1) << 40 + realm.transactionWithBlock() { + let obj = AllIntSizesObject() + + obj.int16 = v16 + XCTAssertEqual(obj.int16, v16) + obj.int32 = v32 + XCTAssertEqual(obj.int32, v32) + obj.int64 = v64 + XCTAssertEqual(obj.int64, v64) + + realm.addObject(obj) + } + + let obj = AllIntSizesObject.allObjectsInRealm(realm)[0]! as AllIntSizesObject + XCTAssertEqual(obj.int16, v16) + XCTAssertEqual(obj.int32, v32) + XCTAssertEqual(obj.int64, v64) + } } diff --git a/Realm/Tests/Swift/SwiftTestObjects.swift b/Realm/Tests/Swift/SwiftTestObjects.swift index 82d5e8b078..dba589dad9 100644 --- a/Realm/Tests/Swift/SwiftTestObjects.swift +++ b/Realm/Tests/Swift/SwiftTestObjects.swift @@ -72,6 +72,12 @@ class SwiftAggregateObject: RLMObject { dynamic var dateCol = NSDate() } +class SwiftAllIntSizesObject: RLMObject { + dynamic var int16 : Int16 = 0 + dynamic var int32 : Int32 = 0 + dynamic var int64 : Int64 = 0 +} + class SwiftEmployeeObject: RLMObject { dynamic var name = "" dynamic var age = 0 @@ -107,3 +113,12 @@ class SwiftIgnoredPropertiesObject: RLMObject { } } + +class SwiftPrimaryStringObject: RLMObject { + dynamic var stringCol = "" + dynamic var intCol = 0 + + override class func primaryKey() -> String { + return "stringCol" + } +} diff --git a/Realm/Tests/Swift/SwiftUnicodeTests.swift b/Realm/Tests/Swift/SwiftUnicodeTests.swift index 2fb35ee11b..364a201c57 100644 --- a/Realm/Tests/Swift/SwiftUnicodeTests.swift +++ b/Realm/Tests/Swift/SwiftUnicodeTests.swift @@ -36,7 +36,7 @@ class SwiftUnicodeTests: SwiftTestCase { XCTAssertEqual(obj1.stringCol, utf8TestString, "Storing and retrieving a string with UTF8 content should work") let obj2 = SwiftStringObject.objectsInRealm(realm, "stringCol == %@", utf8TestString).firstObject() as SwiftStringObject - XCTAssertEqual(obj1, obj2, "Querying a realm searching for a string with UTF8 content should work") + XCTAssertTrue(obj1.isEqualToObject(obj2), "Querying a realm searching for a string with UTF8 content should work") } func testUTF8PropertyWithUTF8StringContents() { @@ -65,7 +65,7 @@ class SwiftUnicodeTests: SwiftTestCase { XCTAssertEqual(obj1.stringCol, utf8TestString, "Storing and retrieving a string with UTF8 content should work") let obj2 = StringObject.objectsInRealm(realm, "stringCol == %@", utf8TestString).firstObject() as StringObject - XCTAssertEqual(obj1, obj2, "Querying a realm searching for a string with UTF8 content should work") + XCTAssertTrue(obj1.isEqualToObject(obj2), "Querying a realm searching for a string with UTF8 content should work") } func testUTF8PropertyWithUTF8StringContents_objc() { diff --git a/Realm/Tests/UnicodeTests.m b/Realm/Tests/UnicodeTests.m index 1038ad73da..1ed486a94e 100644 --- a/Realm/Tests/UnicodeTests.m +++ b/Realm/Tests/UnicodeTests.m @@ -37,7 +37,7 @@ - (void)testUTF8StringContents XCTAssertEqualObjects(obj1.stringCol, kUTF8TestString, @"Storing and retrieving a string with UTF8 content should work"); StringObject *obj2 = [[StringObject objectsInRealm:realm where:@"stringCol == %@", kUTF8TestString] firstObject]; - XCTAssertEqualObjects(obj1, obj2, @"Querying a realm searching for a string with UTF8 content should work"); + XCTAssertTrue([obj1 isEqualToObject:obj2], @"Querying a realm searching for a string with UTF8 content should work"); } - (void)testUTF8PropertyWithUTF8StringContents