Handle equality and hashes for blobs

This commit is contained in:
Simon Binder 2022-09-01 18:11:41 +02:00
parent 04c3dbf1b5
commit 009056dc37
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
11 changed files with 134 additions and 75 deletions

View File

@ -1,5 +1,7 @@
library drift; library drift;
import 'package:collection/collection.dart';
// needed for the generated code that generates data classes with an Uint8List // needed for the generated code that generates data classes with an Uint8List
// field. // field.
export 'dart:typed_data' show Uint8List; export 'dart:typed_data' show Uint8List;
@ -17,3 +19,7 @@ export 'src/runtime/query_builder/query_builder.dart';
export 'src/runtime/types/converters.dart'; export 'src/runtime/types/converters.dart';
export 'src/runtime/types/mapping.dart'; export 'src/runtime/types/mapping.dart';
export 'src/utils/lazy_database.dart'; export 'src/utils/lazy_database.dart';
/// A [ListEquality] instance used by generated drift code for the `==` and
/// [Object.hashCode] implementation of generated classes if they contain lists.
const ListEquality $driftBlobEquality = ListEquality();

View File

@ -650,8 +650,8 @@ class User extends DataClass implements Insertable<User> {
} }
@override @override
int get hashCode => int get hashCode => Object.hash(id, name, isAwesome,
Object.hash(id, name, isAwesome, profilePicture, creationTime); $driftBlobEquality.hash(profilePicture), creationTime);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -659,7 +659,8 @@ class User extends DataClass implements Insertable<User> {
other.id == this.id && other.id == this.id &&
other.name == this.name && other.name == this.name &&
other.isAwesome == this.isAwesome && other.isAwesome == this.isAwesome &&
other.profilePicture == this.profilePicture && $driftBlobEquality.equals(
other.profilePicture, this.profilePicture) &&
other.creationTime == this.creationTime); other.creationTime == this.creationTime);
} }

View File

@ -63,6 +63,9 @@ class DriftDartType {
} }
extension OperationOnTypes on HasType { extension OperationOnTypes on HasType {
bool get isUint8ListInDart =>
type == DriftSqlType.blob && typeConverter == null;
/// Whether this type is nullable in Dart /// Whether this type is nullable in Dart
bool get nullableInDart { bool get nullableInDart {
if (isArray) return false; // Is a List<Something> in Dart, not nullable if (isArray) return false; // Is a List<Something> in Dart, not nullable

View File

@ -12,7 +12,7 @@ class ResultSetWriter {
void write() { void write() {
final className = query.resultClassName; final className = query.resultClassName;
final fieldNames = <String>[]; final fields = <EqualityField>[];
final nonNullableFields = <String>{}; final nonNullableFields = <String>{};
final into = scope.leaf(); final into = scope.leaf();
@ -34,7 +34,7 @@ class ResultSetWriter {
into.write('$modifier $runtimeType $name\n;'); into.write('$modifier $runtimeType $name\n;');
fieldNames.add(name); fields.add(EqualityField(name, isList: column.isUint8ListInDart));
if (!column.nullable) nonNullableFields.add(name); if (!column.nullable) nonNullableFields.add(name);
} }
@ -49,7 +49,7 @@ class ResultSetWriter {
into.write('$modifier $typeName $fieldName;\n'); into.write('$modifier $typeName $fieldName;\n');
fieldNames.add(fieldName); fields.add(EqualityField(fieldName));
if (!nested.isNullable) nonNullableFields.add(fieldName); if (!nested.isNullable) nonNullableFields.add(fieldName);
} else if (nested is NestedResultQuery) { } else if (nested is NestedResultQuery) {
final fieldName = nested.filedName(); final fieldName = nested.filedName();
@ -61,7 +61,7 @@ class ResultSetWriter {
into.write('$modifier List<$typeName> $fieldName;\n'); into.write('$modifier List<$typeName> $fieldName;\n');
fieldNames.add(fieldName); fields.add(EqualityField(fieldName));
nonNullableFields.add(fieldName); nonNullableFields.add(fieldName);
} }
} }
@ -73,11 +73,11 @@ class ResultSetWriter {
into.write('$className({'); into.write('$className({');
} }
for (final column in fieldNames) { for (final column in fields) {
if (nonNullableFields.contains(column)) { if (nonNullableFields.contains(column.lexeme)) {
into.write('required '); into.write('required ');
} }
into.write('this.$column,'); into.write('this.${column.lexeme},');
} }
if (scope.options.rawResultSetData) { if (scope.options.rawResultSetData) {
@ -89,11 +89,11 @@ class ResultSetWriter {
// if requested, override hashCode and equals // if requested, override hashCode and equals
if (scope.writer.options.overrideHashAndEqualsInResultSets) { if (scope.writer.options.overrideHashAndEqualsInResultSets) {
into.write('@override int get hashCode => '); into.write('@override int get hashCode => ');
const HashCodeWriter().writeHashCode(fieldNames, into); writeHashCode(fields, into);
into.write(';\n'); into.write(';\n');
overrideEquals(fieldNames, className, into); overrideEquals(fields, className, into);
overrideToString(className, fieldNames, into); overrideToString(className, fields.map((f) => f.lexeme).toList(), into);
} }
into.write('}\n'); into.write('}\n');

View File

@ -87,7 +87,11 @@ class DataClassWriter {
_writeHashCode(); _writeHashCode();
overrideEquals( overrideEquals(
columns.map((c) => c.dartGetterName), table.dartTypeCode(), _buffer); columns.map(
(c) => EqualityField(c.dartGetterName, isList: c.isUint8ListInDart)),
table.dartTypeCode(),
_buffer,
);
// finish class declaration // finish class declaration
_buffer.write('}'); _buffer.write('}');
@ -312,8 +316,11 @@ class DataClassWriter {
void _writeHashCode() { void _writeHashCode() {
_buffer.write('@override\n int get hashCode => '); _buffer.write('@override\n int get hashCode => ');
final fields = columns.map((c) => c.dartGetterName).toList(); final fields = columns
const HashCodeWriter().writeHashCode(fields, _buffer); .map(
(c) => EqualityField(c.dartGetterName, isList: c.isUint8ListInDart))
.toList();
writeHashCode(fields, _buffer);
_buffer.write(';'); _buffer.write(';');
} }
} }

View File

@ -0,0 +1,72 @@
const int _maxArgsToObjectHash = 20;
class EqualityField {
/// The Dart expression evaluating the field to include in the hash / equals
/// check.
final String lexeme;
/// Whether the field is a list that can't be compared with `==` directly.
final bool isList;
EqualityField(this.lexeme, {this.isList = false});
}
/// Writes an expression to calculate a hash code of an object that consists
/// of the [fields].
void writeHashCode(List<EqualityField> fields, StringBuffer into) {
if (fields.isEmpty) {
into.write('identityHashCode(this)');
} else if (fields.length == 1) {
final field = fields[0];
if (field.isList) {
into.write('\$driftBlobEquality.hash(${field.lexeme})');
} else {
into.write('${field.lexeme}.hashCode');
}
} else {
final needsHashAll = fields.length > _maxArgsToObjectHash;
into.write(needsHashAll ? 'Object.hashAll([' : 'Object.hash(');
var first = true;
for (final field in fields) {
if (!first) into.write(', ');
if (field.isList) {
into.write('\$driftBlobEquality.hash(${field.lexeme})');
} else {
into.write(field.lexeme);
}
first = false;
}
into.write(needsHashAll ? '])' : ')');
}
}
/// Writes a operator == override for a class consisting of the [fields] into
/// the buffer provided by [into].
void overrideEquals(
Iterable<EqualityField> fields, String className, StringBuffer into) {
into
..writeln('@override')
..write('bool operator ==(Object other) => ')
..write('identical(this, other) || (other is $className');
if (fields.isNotEmpty) {
into
..write(' && ')
..write(fields.map((field) {
final lexeme = field.lexeme;
if (field.isList) {
return '\$driftBlobEquality.equals(other.$lexeme, this.$lexeme)';
} else {
return 'other.$lexeme == this.$lexeme';
}
}).join(' && '));
}
into.writeln(');');
}

View File

@ -1,28 +0,0 @@
class HashCodeWriter {
static const int _maxArgsToObjectHash = 20;
const HashCodeWriter();
/// Writes an expression to calculate a hash code of an object that consists
/// of the [fields].
void writeHashCode(List<String> fields, StringBuffer into) {
if (fields.isEmpty) {
into.write('identityHashCode(this)');
} else if (fields.length == 1) {
into.write('${fields[0]}.hashCode');
} else {
final needsHashAll = fields.length > _maxArgsToObjectHash;
into.write(needsHashAll ? 'Object.hashAll([' : 'Object.hash(');
var first = true;
for (final field in fields) {
if (!first) into.write(', ');
into.write(field);
first = false;
}
into.write(needsHashAll ? '])' : ')');
}
}
}

View File

@ -1,18 +0,0 @@
/// Writes a operator == override for a class consisting of the [fields] into
/// the buffer provided by [into].
void overrideEquals(
Iterable<String> fields, String className, StringBuffer into) {
into
..write('@override\nbool operator ==(Object other) => ')
..write('identical(this, other) || (other is $className');
if (fields.isNotEmpty) {
into
..write(' && ')
..write(fields.map((field) {
return 'other.$field == this.$field';
}).join(' && '));
}
into.write(');\n');
}

View File

@ -10,7 +10,6 @@ export 'src/writer/queries/result_set_writer.dart';
export 'src/writer/tables/data_class_writer.dart'; export 'src/writer/tables/data_class_writer.dart';
export 'src/writer/tables/table_writer.dart'; export 'src/writer/tables/table_writer.dart';
export 'src/writer/tables/update_companion_writer.dart'; export 'src/writer/tables/update_companion_writer.dart';
export 'src/writer/utils/hash_code.dart';
export 'src/writer/utils/memoized_getter.dart'; export 'src/writer/utils/memoized_getter.dart';
export 'src/writer/utils/override_equals.dart'; export 'src/writer/utils/hash_and_equals.dart';
export 'src/writer/writer.dart'; export 'src/writer/writer.dart';

View File

@ -1,30 +1,42 @@
import 'package:charcode/ascii.dart'; import 'package:charcode/ascii.dart';
import 'package:drift_dev/src/writer/utils/hash_code.dart'; import 'package:drift_dev/src/writer/utils/hash_and_equals.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
test('hash code for no fields', () { test('hash code for no fields', () {
final buffer = StringBuffer(); final buffer = StringBuffer();
const HashCodeWriter().writeHashCode([], buffer); writeHashCode([], buffer);
expect(buffer.toString(), r'identityHashCode(this)'); expect(buffer.toString(), r'identityHashCode(this)');
}); });
test('hash code for a single field', () { test('hash code for a single field - not a list', () {
final buffer = StringBuffer(); final buffer = StringBuffer();
const HashCodeWriter().writeHashCode(['a'], buffer); writeHashCode([EqualityField('a')], buffer);
expect(buffer.toString(), r'a.hashCode'); expect(buffer.toString(), r'a.hashCode');
}); });
test('hash code for a single field - list', () {
final buffer = StringBuffer();
writeHashCode([EqualityField('a', isList: true)], buffer);
expect(buffer.toString(), r'$driftBlobEquality.hash(a)');
});
test('hash code for multiple fields', () { test('hash code for multiple fields', () {
final buffer = StringBuffer(); final buffer = StringBuffer();
const HashCodeWriter().writeHashCode(['a', 'b', 'c'], buffer); writeHashCode([
expect(buffer.toString(), r'Object.hash(a, b, c)'); EqualityField('a'),
EqualityField('b', isList: true),
EqualityField('c'),
], buffer);
expect(buffer.toString(), r'Object.hash(a, $driftBlobEquality.hash(b), c)');
}); });
test('hash code for lots of fields', () { test('hash code for lots of fields', () {
final buffer = StringBuffer(); final buffer = StringBuffer();
const HashCodeWriter().writeHashCode( writeHashCode(
List.generate(26, (index) => String.fromCharCode($a + index)), buffer); List.generate(
26, (index) => EqualityField(String.fromCharCode($a + index))),
buffer);
expect( expect(
buffer.toString(), buffer.toString(),
r'Object.hashAll([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, ' r'Object.hashAll([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, '

View File

@ -1,4 +1,4 @@
import 'package:drift_dev/src/writer/utils/override_equals.dart'; import 'package:drift_dev/src/writer/utils/hash_and_equals.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
@ -14,12 +14,17 @@ void main() {
test('overrides equals on class with fields', () { test('overrides equals on class with fields', () {
final buffer = StringBuffer(); final buffer = StringBuffer();
overrideEquals(['a', 'b', 'c'], 'Foo', buffer); overrideEquals([
EqualityField('a'),
EqualityField('b', isList: true),
EqualityField('c'),
], 'Foo', buffer);
expect( expect(
buffer.toString(), buffer.toString(),
'@override\nbool operator ==(Object other) => ' '@override\nbool operator ==(Object other) => '
'identical(this, other) || (other is Foo && ' 'identical(this, other) || (other is Foo && '
'other.a == this.a && other.b == this.b && other.c == this.c);\n'); r'other.a == this.a && $driftBlobEquality.equals(other.b, this.b) && '
'other.c == this.c);\n');
}); });
} }