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

Support RealmValue (aka mixed) #1051

Merged
merged 25 commits into from
Dec 19, 2022
Merged

Support RealmValue (aka mixed) #1051

merged 25 commits into from
Dec 19, 2022

Conversation

nielsenko
Copy link
Contributor

@nielsenko nielsenko commented Nov 30, 2022

Support RealmValue that allows users to store different types in the same field

@RealmModel()
class _AnythingGoes {
  @Indexed()
  late RealmValue any;
  late List<RealmValue> manyAny;
}

void main() {
  final realm = Realm(Configuration.local([AnythingGoes.schema]));

  final something = realm.write(() => realm.add(AnythingGoes(any: RealmValue.string('text'))));

  realm.write(() {
    something.manyAny.addAll([null, true, 'text', 42, 3.14].map(RealmValue.from));
  });
}

Resolves: #684

@coveralls
Copy link

coveralls commented Dec 12, 2022

Pull Request Test Coverage Report for Build 3713731875

  • 58 of 58 (100.0%) changed or added relevant lines in 8 files are covered.
  • 3 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+0.4%) to 89.407%

Files with Coverage Reduction New Missed Lines %
lib/src/realm_class.dart 1 93.73%
lib/src/list.dart 2 85.93%
Totals Coverage Status
Change from base Build 3693854074: 0.4%
Covered Lines: 2819
Relevant Lines: 3153

💛 - Coveralls

@nielsenko nielsenko force-pushed the kn/mixed branch 3 times, most recently from c077705 to 538e841 Compare December 13, 2022 10:27
@nielsenko nielsenko marked this pull request as ready for review December 13, 2022 10:28
@nielsenko nielsenko requested review from blagoev, nirinchev and desistefanova and removed request for blagoev December 13, 2022 10:29
Copy link
Contributor

@blagoev blagoev left a comment

Choose a reason for hiding this comment

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

Looks good as initial version,
Added some comments.
Also it would be good to check what additional tests .NET has and implement here. I think they have more elaborate tests like using embedded objects etc.

CHANGELOG.md Outdated Show resolved Hide resolved
common/lib/src/realm_types.dart Show resolved Hide resolved
common/lib/src/realm_types.dart Outdated Show resolved Hide resolved
common/lib/src/realm_types.dart Show resolved Hide resolved
Copy link
Member

@nirinchev nirinchev left a comment

Choose a reason for hiding this comment

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

We probably need to expose ObjectType similarly to what we're doing in C#.

6 │ class _Bad {
│ ━━━━ in realm model for 'Bad'
7 │ RealmValue? wrong;
│ ^^^^^^^^^^^ RealmValue? is nullable
Copy link
Member

Choose a reason for hiding this comment

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

Should we be more descriptive with these error messages? In my experience people don't understand short errors like these and reach out to support to ask them what they mean. We could rephrase that to something like: 'wrong' is declared as 'RealmValue?' which is not allowed. 'RealmValue' can already represent null with 'RealmValue.nullValue()', so you don't need to declare the property itself as nullable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In general I agree that we should have long descriptive error messages, but we need to agree on the team. Previously I was asked to simplify them.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would vote for short and clear messages. We could reword this to be more clear, but long error messages stand in the way after a while. So imo, this could be

`RealmValue?` is declared nullable. Use `RealmValue` or `RealmValue myfield = RealmValue.nullValue()` to assign a default null value.

Copy link
Contributor Author

@nielsenko nielsenko Dec 15, 2022

Choose a reason for hiding this comment

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

I don't think it gets in the way. The short version is at the top, the rest you can read if you are confused.

But I don't think this is specific to this PR. If we want more verbose error messages, I think we should change a lot of them.

generator/lib/src/realm_model_info.dart Outdated Show resolved Hide resolved
common/lib/src/realm_types.dart Show resolved Hide resolved
Comment on lines +119 to +121
final tuple = realm.metadata.getByClassKey(realmCore.getClassKey(value));
type = tuple.item1;
targetMetadata = tuple.item2;
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this handles the case where you open a Realm with an incomplete schema that contains RealmValue properties linking to tables not in the user schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will add a test for this case.

What should be the expected outcome? Do we error out, or use dynamic realm objects?

Copy link
Member

Choose a reason for hiding this comment

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

We should return dynamic objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm .. added a test, but it crashes in

realmCore.getProperty(object, propertyMeta.key);

At least the c-api don't support this

common/lib/src/realm_types.dart Show resolved Hide resolved
common/lib/src/realm_types.dart Outdated Show resolved Hide resolved
Comment on lines +31 to +32
late RealmValue oneAny;
late List<RealmValue> manyAny;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
late RealmValue oneAny;
late List<RealmValue> manyAny;
late RealmValue oneValue;
late List<RealmValue> manyValues;

Maybe will be better if you rename all the places where you have members any to value. 'Any' is confusing to me.

Uuid.v4(),
];

final config = Configuration.inMemory([AnythingGoes.schema]);
Copy link
Contributor

Choose a reason for hiding this comment

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

We are testing only with inMemory Realm. Is that enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I don't see a reason test with other configurations.

Copy link
Contributor Author

@nielsenko nielsenko Dec 15, 2022

Choose a reason for hiding this comment

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

Okay, I had forgotten some things in dart, so I believe there's more work needed to support RealmValue, but that can probably be split into a different PR...

I will just comment that even trying to read a mixed property from core (via the c-api) containing a realm object with unknown schema will crash, and since the property is mixed we really have no way to detect this, so the crash is inevitable 😢

test/realm_value_test.dart Outdated Show resolved Hide resolved
late List<RealmValue> manyAny;
}

void main() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Will be good to make more tests for example:

  • Query by RealmValue;
  • Frozen objects with RealmValue;
  • Embedded objects with RealmValue;
  • Flexible sync with RealmValue;
  • Subscriptions by RealmValue;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would test realm-core more than realm-dart.

Copy link
Member

Choose a reason for hiding this comment

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

Some of these have the potential to surface symmetric bugs and may be valuable. Let's say you make a mistake and you store double values as negative the original value (e.g. 4.56 -> RealmValue(-4.56)). This kind of bug will always pass a test like:

final value = 4.56;
realm.write(() {
  myObj.any = value;
});

expect(myObj.any, value);

Ideally, that should be tested by serializing the object to json and comparing the stringified representation as seen by Core, but those other tests may also be valuable. While I agree it's testing Core, historically, SDKs have been the primary source of bug reports for the Core team and it doesn't hurt to have a few extra tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As the 6th SDK I see less value in tests that essentially test realm-core. Adding them now will delay this PR, and require maintenance down the line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.. an argument can be made that we are only the 2nd SDK to use the c-api though

generator/lib/src/field_element_ex.dart Show resolved Hide resolved
@nielsenko
Copy link
Contributor Author

nielsenko commented Dec 15, 2022

We probably need to expose ObjectType similarly to what we're doing in C#.

I assume this is to catch the error case that you cannot store embedded objects in a RealmValue (or asymmetric objects for that matter, when we eventually support those).

I have reworked the code to catch this misuse in the type system.

common/lib/src/realm_types.dart Outdated Show resolved Hide resolved
@blagoev
Copy link
Contributor

blagoev commented Dec 15, 2022

@nirinchev I couldn't get your meaning for this one

We probably need to expose ObjectType similarly to what we're doing in C#.

Could you clarify this. Especially important if it is a reason to block a PR.

@nirinchev
Copy link
Member

Okay, I had forgotten some things in dart, so I believe there's more work needed to support RealmValue, but that can probably be split into a different PR. Here's an example of an issue that we can't handle today. Let's say you have a synchronized Realm with the following schema (this is v1 of your app).

class MyDynamic {
  RealmValue value;
}

class Person {
  String name;
}

In v2, you add a new type - Dog. Now, a new client comes in and adds a Dog instance as a value for MyDynamic.value. A client in v1 of your app will have no way to do something meaningful with that object as it won't have the schema for Dog and there's no way to inspect what properties exist. In C# and other SDKs we expose the object schema on RealmObject instances, so you could do something like:

final obj = myDynamic.value.as<RealmObjectBase>();
switch (obj.schema.name) {
  case "Person": // do something
  default:
    final nameProp = obj.schema.properties.firstOrNull(p => p.name == "name");
    if (nameProp != null && nameProp.propertyType == RealmPropertyType.string) {
      // do something with the name
    }
}

My thinking with exposing ObjectType on RealmValue was to support use cases like:

public String getDisplayName(RealmValue value) {
  switch (value.objectType) {
    case null:
      // value is not an object or is null - do nothing
	  return "unknown";
    case "Person":
      final person = value.as<Person>();
      return '${person.firstName} ${person.lastName}';
    case "Company":
      final company = value.as<Company>();
      return company.legalEntity.name;
    default:
      final obj = value.as<RealmObjectBase>();
      final nameProp = obj.schema.properties.firstOrNull(p => p.name == "name");
      if (nameProp != null || nameProp.propertyType == 'string') {
        return obj.dynamic.get<string>('name');
      }

      return "unknown";
  }
}

But again, this depends on the schema being available on the realm object.

Copy link
Member

@nirinchev nirinchev left a comment

Choose a reason for hiding this comment

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

As discussed, I think it's fine to ship the feature as is while the C API limitation is addressed. I'd still recommend adding a json verification for the roundtrip test to protect us from symmetric bugs.

@nielsenko nielsenko merged commit 391e4ca into main Dec 19, 2022
@nielsenko nielsenko deleted the kn/mixed branch December 19, 2022 08:07
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support AnyRealmValue
5 participants