Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RCOCOA-2271: Collections in Mixed #8546

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

dianaafanador3
Copy link
Collaborator

@dianaafanador3 dianaafanador3 commented Apr 12, 2024

  • Added support for storing nested collections (List and Map not ManagedSet) in a AnyRealmValue.

    class MixedObject: Object {
      @Persisted var anyValue: AnyRealmValue
    }
    
    // You can build a AnyRealmValue from a Swift's Dictionary.
    let dictionary: Dictionary<String, AnyRealmValue> = ["key1": .string("hello"), "key2": .bool(false)]
    
    // You can build a AnyRealmValue from a Swift's Map
    // and nest a collection within another collection.
    let list: Array<AnyRealmValue> = [.int(12), .double(14.17), AnyRealmValue.fromDictionary(dictionary)]
    
    let realm = realmWithTestPath()
    try realm.write {
      let obj = MixedObject()
      obj.anyValue = AnyRealmValue.fromArray(list)
      realm.add(o)
    }
  • Added new operators to Swift's Query API for supporting querying nested collections.

    realm.objects(MixedObject.self).where { $0.anyValue[0][0][1] == .double(76.54) }

    The .all operator allows looking up in all keys or indexes.

    realm.objects(MixedObject.self).where { $0.anyValue["key"].all == .bool(false) }

@cla-bot cla-bot bot added the cla: yes label Apr 12, 2024
@dianaafanador3 dianaafanador3 marked this pull request as draft April 12, 2024 16:16
@dianaafanador3 dianaafanador3 force-pushed the dp/collections_in_mixed branch 8 times, most recently from b60f46f to 3386d7a Compare April 22, 2024 07:36
@dianaafanador3 dianaafanador3 marked this pull request as ready for review April 22, 2024 11:15
@dianaafanador3
Copy link
Collaborator Author

This is waiting for this PR to be merged to core. Also, there is one failing test which is working locally, taking a look at it, but putting this on review on the meantime.

@dianaafanador3 dianaafanador3 changed the title Collections in Mixed RCOCOA-2271: Collections in Mixed Apr 22, 2024
@dianaafanador3 dianaafanador3 force-pushed the dp/collections_in_mixed branch 9 times, most recently from ed91d6b to 45a17bd Compare April 23, 2024 14:44
Copy link
Member

@tgoyne tgoyne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A first pass over things.

Realm/RLMDictionary.h Outdated Show resolved Hide resolved
Realm/RLMAccessor.mm Outdated Show resolved Hide resolved
Realm/RLMAccessor.mm Outdated Show resolved Hide resolved
Realm/RLMArray_Private.hpp Show resolved Hide resolved
Realm/RLMConstants.h Outdated Show resolved Hide resolved
Realm/RLMAccessor.mm Outdated Show resolved Hide resolved
RealmSwift/AnyRealmValue.swift Outdated Show resolved Hide resolved
@@ -361,6 +361,14 @@ extension List: MutableCollection {
}
}

// MARK: - Hashable

extension List: Hashable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a valid definition of Hashable for List, and such a thing is probably not possible. Using a List as a dictionary key does not really make any sense too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as Map

@@ -779,6 +779,14 @@ public final class Map<Key: _MapKey, Value: RealmCollectionValue>: RLMSwiftColle
}
}

// MARK: - Hashable

extension Map: Hashable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this is not at all a valid Hashable implementation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ObjectIdentifier given that we already conform to Equatable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectIdentifier is not a valid definition either. Two managed dictionary objects which correspond to the same field are equal to each other but have different ObjectIdentifiers. All of our accessor types are fundamentally incompatible with Hashable due to that their identity changes when you promote an unmanaged object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgoyne we do need to conform to Hashable to be able to include Map or List as a an associated value in AnyRealmValue if we want to go that approach.
I can think of a few approaches for this:

  • Use ObjectIdentifer and sacrifice equality for managed and unmanaged object
  • Have associated values as Swift's native types Array/Dictionary, this will clearly will go bad with nested collections which will need to be instantiated completely to convert it (Discarded)
  • Store some reference to the collection as the associated value and then and have some static function that returns the collection (Not need to conform to hashable, but worse developer experience).

I'm open to discuss different solutions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can override the hash function on AnyRealmValue and return a constant value for the Map and List cases.

RealmSwift/Tests/ObjectCreationTests.swift Show resolved Hide resolved
Realm/RLMAccessor.mm Outdated Show resolved Hide resolved
Realm/RLMAccessor.mm Show resolved Hide resolved
@@ -890,15 +914,17 @@ void RLMSetSwiftPropertyAny(__unsafe_unretained RLMObjectBase *const obj, uint16
}

id RLMAccessorContext::box(realm::Mixed v) {
return RLMMixedToObjc(v, _realm, &_info);
auto property = (currentProperty) ? currentProperty : _info.propertyForTableColumn(_colKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would currentProperty be nil here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For any other mixed wrapped types besides list and dictionary the accessor is constructed without the current property because it is not needed to box the native value

Realm/RLMCollection.mm Outdated Show resolved Hide resolved
Realm/RLMCollection.mm Outdated Show resolved Hide resolved
Realm/RLMValue.h Outdated Show resolved Hide resolved
Realm/Tests/KVOTests.mm Outdated Show resolved Hide resolved
Realm/Tests/NotificationTests.m Show resolved Hide resolved
Realm/Tests/ObjectTests.m Outdated Show resolved Hide resolved
RealmSwift/AnyRealmValue.swift Outdated Show resolved Hide resolved
@dianaafanador3
Copy link
Collaborator Author

@tgoyne and @leemaguire solved pr comments and there are some comments as well

Copy link
Member

@elle-j elle-j left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see the Swift implementation of this as well! 🙂

CHANGELOG.md Outdated Show resolved Hide resolved
Package.swift Outdated Show resolved Hide resolved
Realm/RLMUtil.mm Show resolved Hide resolved
Realm/RLMValue.h Outdated Show resolved Hide resolved
@tgoyne
Copy link
Member

tgoyne commented Apr 29, 2024

There are a bunch of new warnings here that need to be fixed.

Realm/ObjectServerTests/RealmServer.swift Outdated Show resolved Hide resolved
Realm/RLMAccessor.mm Outdated Show resolved Hide resolved
Realm/RLMAccessor.mm Outdated Show resolved Hide resolved
Comment on lines 202 to 212
realm::Mixed value = _backingList.get_any(index);
RLMAccessorContext context(*_ownerInfo, *_objectInfo, _property);
if (value.is_type(realm::type_Dictionary)) {
return context.box(_backingList.get_dictionary(index));
}
else if (value.is_type(realm::type_List)) {
return context.box(_backingList.get_list(index));
}
else {
return context.box(value);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all done by _backingList.get().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think get is getting nested levels, this are done by using get_dictionary and get_list, which is going an extra level within the collection https://github.com/realm/realm-core/blob/970e558fbe1b3439a5c6d8fbdecfee35815d04ba/src/realm/list.cpp#L585

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I'm missing something

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List::get() calls get_dictionary() and get_list() as needed. As I have said a few times, all of the tests pass with this being just _backingList.get(). If you believe that there is some scenario where this doesn't work, you need to write a test for that scenario.

Realm/RLMQueryUtil.mm Show resolved Hide resolved

// For nested subscripts in NSPredicate like `anyCol[0]['key'] we need to iterate through the nested NSExpression to get the paths of the query.
void QueryBuilder::get_path_elements(std::vector<PathElement> &paths, NSExpression *expression) {
for (int i = 0; i < expression.arguments.count; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is still very aggressively confusing and adding a short description of what it's doing does not help. It's looping over the arguments, but then always recurring on the first argument regardless of where it finds a function expression? There's apparently non-constant expressions in the array, but they're just ignored?

Realm/RLMQueryUtil.mm Outdated Show resolved Hide resolved
}
} else if ([value isKindOfClass:[NSString class]]) {
NSString *key = (NSString *)value;
if ([key isEqual:@"all"]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

???

If you have a field named "all" it just completely ignores the field name and does a wildcard search instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this using "all" as the wildcard search key?

RealmSwift/Object.swift Outdated Show resolved Hide resolved
@dianaafanador3 dianaafanador3 force-pushed the dp/collections_in_mixed branch 3 times, most recently from 2e8b630 to cede8e2 Compare May 7, 2024 12:22

// MARK: - Hashable

public func hash(into hasher: inout Hasher) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to actually hash all of the types.

Comment on lines 202 to 212
realm::Mixed value = _backingList.get_any(index);
RLMAccessorContext context(*_ownerInfo, *_objectInfo, _property);
if (value.is_type(realm::type_Dictionary)) {
return context.box(_backingList.get_dictionary(index));
}
else if (value.is_type(realm::type_List)) {
return context.box(_backingList.get_list(index));
}
else {
return context.box(value);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List::get() calls get_dictionary() and get_list() as needed. As I have said a few times, all of the tests pass with this being just _backingList.get(). If you believe that there is some scenario where this doesn't work, you need to write a test for that scenario.

Comment on lines +160 to +171
RLMAccessorContext ctx = RLMAccessorContext(*_info);
for (NSUInteger index = state->state; index < count && batchCount < len; ++index) {
_strongBuffer[batchCount] = _results->get(ctx, index);
batchCount++;
}
} else {
// This is used by Dicitonary and List for nested collections.
RLMAccessorContext ctx = RLMAccessorContext(*_parentInfo, *_info, _property);
for (NSUInteger index = state->state; index < count && batchCount < len; ++index) {
_strongBuffer[batchCount] = _results->get(ctx, index);
batchCount++;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RLMAccessorContext ctx = RLMAccessorContext(*_info);
for (NSUInteger index = state->state; index < count && batchCount < len; ++index) {
_strongBuffer[batchCount] = _results->get(ctx, index);
batchCount++;
}
} else {
// This is used by Dicitonary and List for nested collections.
RLMAccessorContext ctx = RLMAccessorContext(*_parentInfo, *_info, _property);
for (NSUInteger index = state->state; index < count && batchCount < len; ++index) {
_strongBuffer[batchCount] = _results->get(ctx, index);
batchCount++;
}
auto ctx = _parentInfo ? RLMAccessorContext(*_parentInfo, *_info, _property) :
RLMAccessorContext(*_info);
for (NSUInteger index = state->state; index < count && batchCount < len; ++index) {
_strongBuffer[batchCount] = _results->get(ctx, index);
batchCount++;
}

@@ -454,9 +483,9 @@ - (BOOL)isFrozen {
- (instancetype)resolveInRealm:(RLMRealm *)realm {
auto& parentInfo = _ownerInfo->resolve(realm);
return translateErrors([&] {
return [[self.class alloc] initWithBackingCollection:_backingCollection.freeze(realm->_realm)
return [[RLMManagedDictionary alloc] initWithBackingCollection:_backingCollection.freeze(realm->_realm)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Things which produce a copy of the current object use self.class rather than explicitly spelling out the class name so that they work properly when called on subclasses. That's not directly relevant here, but it is the more generally correct pattern.

// We consider only `NSKeyPathExpressionType` values to build the keypath.
for (unsigned long i = 0; i < pathElements.size(); i++) {
if (keyPathExpression.arguments[0].expressionType == NSKeyPathExpressionType) {
keyPath = [NSString stringWithFormat:@"%@", keyPathExpression.arguments[0]];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a very roundabout way of writing keyPathExpression.arguments[0].description. I think the actually correct thing here is keyPathExpression.arguments[0].keyPath

Comment on lines +792 to +793
default:
fatalError()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not needed and will only lead to future bugs.

/// :nodoc:
extension RLMDictionary: RLMValue {
/// :nodoc:
public var rlm_anyValueType: RLMAnyValueType { .dictionary }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the obj-c API and it doesn't make any sense to be redefining it here.

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (RLMPropertyType)rlm_valueType {
REALM_UNREACHABLE();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not unreachable. rlm_valueType is part of the public API.

@@ -163,6 +163,27 @@ extension AnyRealmValue: BuiltInObjcBridgeable {
}
}


extension RLMManagedDictionary: BuiltInObjcBridgeable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not valid implementations of BuiltInObjcBridgeable, and declaring these types as BuiltInObjcBridgeable doesn't really make sense at all.

}
/// :nodoc:
public subscript(key: String) -> Query<AnyRealmValue> {
.init(appendKeyPath("['\(key)']", options: [.isPath]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to handle key containing quotes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants