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

RDART-156: Fix RealmObject.hashCode #1420

Merged
merged 2 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

### Fixed
* Fixed iteration after `skip` bug ([#1409](https://github.com/realm/realm-dart/issues/1409))
* Fixed RealmObject not overriding `hashCode`, which would lead to sets of RealmObjects potentially containing duplicates. ([#1418](https://github.com/realm/realm-dart/issues/1418))

### Compatibility
* Realm Studio: 13.0.0 or later.
Expand Down
9 changes: 9 additions & 0 deletions lib/src/native/realm_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,15 @@ class _RealmCore {
bool userEquals(User first, User second) => _equals(first.handle, second.handle);
bool subscriptionEquals(Subscription first, Subscription second) => _equals(first.handle, second.handle);

int objectGetHashCode(RealmObjectBase value) {
final link = realmCore._getObjectAsLink(value);

var hashCode = -986587137;
hashCode = (hashCode * -1521134295) + link.classKey;
hashCode = (hashCode * -1521134295) + link.targetKey;
return hashCode;
}

RealmResultsHandle resultsSnapshot(RealmResults results) {
final resultsPointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_snapshot(results.handle._pointer));
return RealmResultsHandle._(resultsPointer, results.realm.handle);
Expand Down
12 changes: 12 additions & 0 deletions lib/src/realm_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,21 @@ mixin RealmObjectBase on RealmEntity implements RealmObjectBaseMarker, Finalizab
if (identical(this, other)) return true;
if (other is! RealmObjectBase) return false;
if (!isManaged || !other.isManaged) return false;

return realmCore.objectEquals(this, other);
}

late final int _managedHashCode = realmCore.objectGetHashCode(this);

@override
int get hashCode {
if (!isManaged) {
return super.hashCode;
}

return _managedHashCode;
}

/// Gets a value indicating whether this object is managed and represents a row in the database.
///
/// If a managed object has been removed from the [Realm], it is no longer valid and accessing properties on it
Expand Down
99 changes: 98 additions & 1 deletion test/realm_object_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

// ignore_for_file: unused_local_variable, avoid_relative_lib_imports

import 'dart:io';
import 'dart:typed_data';
import 'package:test/test.dart' hide test, throws;
import '../lib/realm.dart';
Expand Down Expand Up @@ -723,4 +722,102 @@ Future<void> main([List<String>? args]) async {
if (count > 1) fail('Should only receive one event');
}
});

test('RealmObject read deleted object properties', () {
var config = Configuration.local([Team.schema, Person.schema]);
var realm = getRealm(config);

var team = Team("TeamOne");
realm.write(() => realm.add(team));
var teams = realm.all<Team>();
var teamBeforeDelete = teams[0];
realm.write(() => realm.delete(team));
expect(team.isValid, false);
expect(teamBeforeDelete.isValid, false);
expect(team, teamBeforeDelete);
expect(() => team.name, throws<RealmException>("Accessing object of type Team which has been invalidated or deleted"));
expect(() => teamBeforeDelete.name, throws<RealmException>("Accessing object of type Team which has been invalidated or deleted"));
});

test('RealmObject.hashCode changes after adding to Realm', () {
final config = Configuration.local([Team.schema, Person.schema]);
final realm = getRealm(config);

final team = Team("TeamOne");

final unmanagedHash = team.hashCode;

realm.write(() => realm.add(team));

final managedHash = team.hashCode;

expect(managedHash, isNot(unmanagedHash));
expect(managedHash, equals(team.hashCode));
});

test('RealmObject.hashCode is different for different objects', () {
final config = Configuration.local([Team.schema, Person.schema]);
final realm = getRealm(config);

final a = Team("a");
final b = Team("b");

expect(a.hashCode, isNot(b.hashCode));

realm.write(() {
realm.add(a);
realm.add(b);
});

expect(a.hashCode, isNot(b.hashCode));
});

test('RealmObject.hashCode is same for equal objects', () {
final config = Configuration.local([Team.schema, Person.schema]);
final realm = getRealm(config);

final team = Team("TeamOne");

realm.write(() {
realm.add(team);
});

final teamAgain = realm.all<Team>().first;

expect(team.hashCode, equals(teamAgain.hashCode));
});

test('RealmObject.hashCode remains stable after deletion', () {
final config = Configuration.local([Team.schema, Person.schema]);
final realm = getRealm(config);

final team = Team("TeamOne");

realm.write(() {
realm.add(team);
});

final teamAgain = realm.all<Team>().first;

final managedHash = team.hashCode;

realm.write(() => realm.delete(team));

expect(team.hashCode, equals(managedHash)); // Object that was just deleted shouldn't change its hash code
expect(teamAgain.hashCode, equals(managedHash)); // Object that didn't hash its hash code and its row got deleted should still have the same hash code
});

test("RealmObject when added to set doesn't have duplicates", () {
final config = Configuration.local([Team.schema, Person.schema]);
final realm = getRealm(config);

realm.write(() {
realm.add(Team("TeamOne"));
});

final setOne = realm.all<Team>().toSet();
final setTwo = realm.all<Team>().toSet();

expect(setOne.difference(setTwo).length, 0);
});
}
2 changes: 1 addition & 1 deletion test/subscription_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ Future<void> main([List<String>? args]) async {
final writeReason = sessionError.compensatingWrites!.first;
expect(writeReason, isNotNull);
expect(writeReason.objectType, "Product");
expect(writeReason.reason, 'write to "$productId" in table "${writeReason.objectType}" not allowed; object is outside of the current query view');
expect(writeReason.reason, 'write to ObjectID("$productId") in table "${writeReason.objectType}" not allowed; object is outside of the current query view');
expect(writeReason.primaryKey.value, productId);
});
}