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

Const schema refactoring #903

Draft
wants to merge 41 commits into
base: main
Choose a base branch
from
Draft

Const schema refactoring #903

wants to merge 41 commits into from

Conversation

nielsenko
Copy link
Contributor

@nielsenko nielsenko commented Sep 14, 2022

Const construct schema compile time

Dart has the ability to construct complex const objects compile time, which avoids any runtime overhead with regards to construction, and are never considered for garbage collection.

This is a very powerful feature, and it is used in Flutter to construct static widgets.

This PR explores the possibility of doing the same for our schema.

It introduces fly-weight property classes that are constructed compile time per realm model, and combined in a const constructed schema object.

These fly-weights are specialized by property type (ie. value, object, or list - set and map will come later), and help us deconstruct the static type of the collection, to get the static type of its elements.

This is useful to push type info and safety much deeper into the realm SDK, and allows us to avoid a fair amount runtime branching when reading from a realm. (The current PR doesn't do the same for writing yet)

It also refactor how we handle dynamic properties, which are now all accessed by get<T> including for lists, ie. get<List<T>>. The error messages are also improved.

The original idea came about, when implementing support for collections of nullable primitives, but was spun out into its own PR, as it is a significant change on its own.

The class hierarchy for realm objects changes slightly with this PR. It is now:

classDiagram

class RealmObject
<<abstract>> RealmObject
RealmObjectMarker <|.. RealmObject

class TypedRealmObject
<<abstract>> TypedRealmObject
RealmObject <|.. TypedRealmObject

class AGeneratedClass
RealmEntityMixin <|-- AGeneratedClass
RealmObjectMixin <|-- AGeneratedClass
TypedRealmObject <|.. AGeneratedClass
Loading

Here is an example of how the generated code looks like after this refactoring.

Given:

@RealmModel()
class _Player {
  @PrimaryKey()
  late String name;
  _Game? game;
  final scoresByRound = <int?>[]; // null means player didn't finish
}

@RealmModel()
class _Game {
  final winnerByRound = <_Player>[]; // I intended to support <_Player?> here as well, but alas core doesn't support it
  int get rounds => winnerByRound.length;
}

the generator produces:

class Player extends _Player
    with RealmEntityMixin, RealmObjectMixin<Player> {
  Player(
    String name, {
    Game? game,
    Iterable<int?> scoresByRound = const [],
  }) {
    _nameProperty.setValue(this, name);
    _gameProperty.setValue(this, game);
    _scoresByRoundProperty.setValue(this, RealmList<int?>(scoresByRound));
  }

  Player._();

  static const _nameProperty = ValueProperty<String>(
    'name',
    RealmPropertyType.string,
    primaryKey: true,
  );
  @override
  String get name => _nameProperty.getValue(this);
  @override
  set name(String value) => _nameProperty.setValue(this, value);

  static const _gameProperty = ObjectProperty<Game>('game', 'Game');
  @override
  Game? get game => _gameProperty.getValue(this);
  @override
  set game(covariant Game? value) => _gameProperty.setValue(this, value);

  static const _scoresByRoundProperty =
      ListProperty<int?>('scoresByRound', RealmPropertyType.int);
  @override
  RealmList<int?> get scoresByRound => _scoresByRoundProperty.getValue(this);
  @override
  set scoresByRound(covariant RealmList<int?> value) =>
      throw RealmUnsupportedSetError();

  static const schema = SchemaObject<Player>(
    Player._,
    'Player',
    {
      'name': _nameProperty,
      'game': _gameProperty,
      'scoresByRound': _scoresByRoundProperty,
    },
    _nameProperty,
  );
  @override
  SchemaObject get instanceSchema => schema;
}

class Game extends _Game
    with RealmEntityMixin, RealmObjectMixin<Game> {
  Game({
    Iterable<Player> winnerByRound = const [],
  }) {
    _winnerByRoundProperty.setValue(this, RealmList<Player>(winnerByRound));
  }

  Game._();

  static const _winnerByRoundProperty = ListProperty<Player>(
      'winnerByRound', RealmPropertyType.object,
      linkTarget: 'Player');
  @override
  RealmList<Player> get winnerByRound => _winnerByRoundProperty.getValue(this);
  @override
  set winnerByRound(covariant RealmList<Player> value) =>
      throw RealmUnsupportedSetError();

  static const schema = SchemaObject<Game>(
    Game._,
    'Game',
    {
      'winnerByRound': _winnerByRoundProperty,
    },
  );
  @override
  SchemaObject get instanceSchema => schema;
}

Notice how the schema is now static const, as it is assembled from the likewise static const property fly-weights.

@cla-bot cla-bot bot added the cla: yes label Sep 14, 2022
@nielsenko nielsenko requested review from nirinchev, blagoev and desistefanova and removed request for blagoev and nirinchev September 14, 2022 11:15
@nielsenko nielsenko self-assigned this Sep 14, 2022
@nielsenko nielsenko changed the title Kn/const schema refactoring Const schema refactoring Sep 14, 2022
@nielsenko nielsenko force-pushed the kn/const-schema-refactoring branch 2 times, most recently from 45bd665 to 039bac7 Compare September 14, 2022 14:55
@nielsenko nielsenko force-pushed the kn/nullable-in-collections-no-refactoring branch 3 times, most recently from 958b123 to 20d975d Compare September 20, 2022 11:44
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.

Looks good, I have mostly clarification comments and minor suggestions

generator/lib/src/realm_field_info.dart Outdated Show resolved Hide resolved
generator/lib/src/realm_object_generator.dart Outdated Show resolved Hide resolved
lib/src/configuration.dart Outdated Show resolved Hide resolved
lib/src/list.dart Outdated Show resolved Hide resolved
lib/src/native/realm_core.dart Show resolved Hide resolved
lib/src/realm_class.dart Outdated Show resolved Hide resolved
Comment on lines +268 to +262
var object = RealmObjectInternal.create<T>(this, handle, metadata);
return object;
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
var object = RealmObjectInternal.create<T>(this, handle, metadata);
return object;
return RealmObjectInternal.create<T>(this, handle, metadata);

lib/src/realm_class.dart Outdated Show resolved Hide resolved
lib/src/realm_object.dart Show resolved Hide resolved

return _values[name];
}
T getValue<T>(RealmObject object, String propertyName) => _values[propertyName] as T;
Copy link
Member

Choose a reason for hiding this comment

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

How does that work with default values? Like if I call getValue<int>(obj, age) and age doesn't exist in _values, wouldn't that throw?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Default values are handled at the property layer

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but that's happening after calling this method. Here's an example of what I'm thinking about: https://dartpad.dev/?id=4d103b52e90ef328e32c12665a38a337

@nielsenko nielsenko force-pushed the kn/nullable-in-collections-no-refactoring branch 3 times, most recently from 7a430df to f94c072 Compare September 21, 2022 13:25
Copy link
Contributor

@desistefanova desistefanova left a comment

Choose a reason for hiding this comment

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

Added some questions?

Comment on lines +52 to +54
const _intMapping = Mapping<int>();
const _boolMapping = Mapping<bool>();
const _doubleMapping = Mapping<double>();
Copy link
Contributor

@desistefanova desistefanova Sep 21, 2022

Choose a reason for hiding this comment

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

Why this types are special and we have constants only for them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because int, bool, and double are both members of the enum and primitive types in dart.. Hence this little trick

generator/lib/src/realm_object_generator.dart Outdated Show resolved Hide resolved
test/realm_test.dart Outdated Show resolved Hide resolved
@desistefanova
Copy link
Contributor

I suppose even though this is a refactoring we will still have a line in the changelog about it.

@nielsenko nielsenko force-pushed the kn/const-schema-refactoring branch 3 times, most recently from 9857287 to 36370c9 Compare September 23, 2022 08:56
@nielsenko nielsenko force-pushed the kn/nullable-in-collections-no-refactoring branch from f94c072 to b027b2c Compare September 23, 2022 11:40
@nielsenko nielsenko force-pushed the kn/nullable-in-collections-no-refactoring branch 2 times, most recently from 043695c to a6ef9fc Compare September 23, 2022 20:00
Caveat to be aware of:

```dart
@RealmModel()
class _Person {
   @PrimaryKey()
   late String name;
   int age = 42;
}

final p = Person('Bob', age: 42);

final p2 = Person('Bob')..age = 42;

// behaves slightly differently wrt. sync
```
…allows frozen and changes to be implemented without generator support, but doesn't introduce nearly as many changes. Based on feedback from @nirinchev
Comment on lines +28 to +31
Future<dynamic> generatorTestBuilder(String directoryName, String inputFileName) async {
final inputPath = _path.join(directoryName, inputFileName);
final expectedPath = _path.setExtension(inputPath, '.expected');
return testBuilder(
Copy link
Contributor

Choose a reason for hiding this comment

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

This change breaks the idea that not all the input files could have a relevant expected result. It is possible the .expected file to be missing, because of some other logic for expecting exception or expecting no output. Why is it changed?

Comment on lines +119 to +125
if (d.operation < 0) {
pen.red(bg: true); // delete
} else if (d.operation == 0) {
pen.reset(); // no-edit
} else if (d.operation > 0) {
pen.green(bg: true); // insert
}
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
if (d.operation < 0) {
pen.red(bg: true); // delete
} else if (d.operation == 0) {
pen.reset(); // no-edit
} else if (d.operation > 0) {
pen.green(bg: true); // insert
}
switch (d.operation) {
case DIFF_DELETE:
pen.red(bg: true);
break;
case DIFF_EQUAL:
pen.reset();
break;
case DIFF_INSERT:
pen.green(bg: true);
break;
default:
}

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.

Another set of small suggestions

@@ -2,6 +2,9 @@

**This project is in the Beta stage. The API should be quite stable, but occasional breaking changes may be made.**

### Breaking Changes
* Use const constructed schema objects. ([#903](https://github.com/realm/realm-dart/pull/903))
Copy link
Member

Choose a reason for hiding this comment

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

Is this a breaking change? I guess people just need to re-run the generator?

final objectType = property.ref.link_target.cast<Utf8>().toRealmDartString(treatEmptyAsNull: true);
result &= schemaProperty.linkTarget == objectType;
final isNullable = property.ref.flags & realm_property_flags.RLM_PROPERTY_NULLABLE != 0;
result &= !(isNullable ^ schemaProperty.optional); // isNullable <=> schemaProperty.optional
Copy link
Member

Choose a reason for hiding this comment

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

That seems like an overly complicated way to express isNullable == schemaProperty.optional.

@@ -2529,11 +2543,14 @@ extension on List<UserState> {
extension on realm_property_info {
SchemaProperty toSchemaProperty() {
final linkTarget = link_target == nullptr ? null : link_target.cast<Utf8>().toDartString();
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
final linkTarget = link_target == nullptr ? null : link_target.cast<Utf8>().toDartString();
final linkTarget = link_target.cast<Utf8>().toRealmDartString(treatEmptyAsNull: true);

propertyType: RealmPropertyType.values[type],
optional: flags & realm_property_flags.RLM_PROPERTY_NULLABLE == realm_property_flags.RLM_PROPERTY_NULLABLE,
primaryKey: flags & realm_property_flags.RLM_PROPERTY_PRIMARY_KEY == realm_property_flags.RLM_PROPERTY_PRIMARY_KEY,
linkTarget: linkTarget == null || linkTarget.isEmpty ? null : linkTarget,
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
linkTarget: linkTarget == null || linkTarget.isEmpty ? null : linkTarget,
linkTarget: linkTarget,

@nielsenko nielsenko marked this pull request as draft October 24, 2022 11:41
@nielsenko
Copy link
Contributor Author

Postponed once more..

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.

4 participants