Skip to content

Commit 454724d

Browse files
Merge branch '125-tomany-backlink-not-found' into 'main'
Generator: fix finding backlink source if target entity processed first, if ToOne target ID property renamed See merge request objectbox/objectbox-dart!119
2 parents 678be41 + 433712e commit 454724d

File tree

4 files changed

+263
-17
lines changed

4 files changed

+263
-17
lines changed

generator/lib/src/code_builder.dart

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,10 @@ class CodeBuilder extends Builder {
369369
}
370370
rel.targetId = targetEntity.id;
371371
}
372-
372+
}
373+
// Note: finding the backlink source requires that all ToMany relations have
374+
// a targetId set, so find backlink sources in a separate loop.
375+
for (var entity in model.entities) {
373376
for (var backlink in entity.backlinks) {
374377
backlink.source = _findBacklinkSource(model, entity, backlink);
375378
}
@@ -396,14 +399,25 @@ class CodeBuilder extends Builder {
396399
ModelRelation? srcRel;
397400
ModelProperty? srcProp;
398401

399-
throwAmbiguousError(String prop, String rel) =>
400-
throw InvalidGenerationSourceError(
401-
"Ambiguous relation backlink source for '${entity.name}.${bl.name}':"
402-
" Found matching property '$prop' and to-many relation '$rel'."
403-
" Maybe specify source name in @Backlink() annotation.",
404-
);
402+
throwAmbiguousError(
403+
Iterable<ModelProperty> toOneProps,
404+
Iterable<ModelRelation> toManyRels,
405+
) {
406+
final toOneList = toOneProps.map((p) => p.relationField);
407+
final toManyList = toManyRels.map((r) => r.name);
408+
final fieldList = [...toOneList, ...toManyList].join(', ');
409+
throw InvalidGenerationSourceError(
410+
"Can't determine backlink source for \"${entity.name}.${bl.name}\" "
411+
"as there is more than one matching ToOne or ToMany relation in \"${srcEntity.name}\": $fieldList."
412+
" Add the name of the source relation to the annotation, like @Backlink('<source>'), or modify your entity classes.",
413+
);
414+
}
405415

416+
// Note that in the model only the target ID (or relation) property of a
417+
// ToOne exists, so search for that.
406418
if (bl.srcField.isEmpty) {
419+
// No source field name given, try to find the source relation by type of
420+
// its target entity.
407421
final matchingProps = srcEntity.properties.where(
408422
(p) => p.isRelation && p.relationTarget == entity.name,
409423
);
@@ -412,20 +426,27 @@ class CodeBuilder extends Builder {
412426
);
413427
final candidatesCount = matchingProps.length + matchingRels.length;
414428
if (candidatesCount > 1) {
415-
throwAmbiguousError(matchingProps.toString(), matchingRels.toString());
429+
throwAmbiguousError(matchingProps, matchingRels);
416430
} else if (matchingProps.isNotEmpty) {
417431
srcProp = matchingProps.first;
418432
} else if (matchingRels.isNotEmpty) {
419433
srcRel = matchingRels.first;
420434
}
421435
} else {
422-
srcProp = srcEntity.findPropertyByName('${bl.srcField}Id');
436+
// Source field name given
437+
// For ToOne, expect the name of the ToOne field
438+
srcProp = srcEntity.properties.firstWhereOrNull(
439+
(p) => p.isRelation && p.relationField == bl.srcField,
440+
);
441+
// For ToMany, expect the name of the ToMany field
423442
srcRel = srcEntity.relations.firstWhereOrNull(
424443
(r) => r.name == bl.srcField,
425444
);
426445

446+
// This should be impossible as it would mean a ToOne and a ToMany field
447+
// share the same name, but check just in case.
427448
if (srcProp != null && srcRel != null) {
428-
throwAmbiguousError(srcProp.toString(), srcRel.toString());
449+
throwAmbiguousError([srcProp], [srcRel]);
429450
}
430451
}
431452

@@ -435,7 +456,8 @@ class CodeBuilder extends Builder {
435456
return BacklinkSourceProperty(srcProp);
436457
} else {
437458
throw InvalidGenerationSourceError(
438-
"Unknown relation backlink source for '${entity.name}.${bl.name}'",
459+
'Failed to find backlink source for "${entity.name}.${bl.name}" in "${srcEntity.name}", '
460+
'make sure a matching ToOne or ToMany relation exists.',
439461
);
440462
}
441463
}

generator/test/code_builder_test.dart

Lines changed: 225 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ void main() {
203203
}
204204
''';
205205

206-
final result = await testEnv.run(source, expectNoOutput: true);
206+
final result = await testEnv.run(source, ignoreOutput: true);
207207

208208
expect(result.builderResult.succeeded, false);
209209
expect(
@@ -271,6 +271,190 @@ void main() {
271271
''');
272272
});
273273

274+
test('Finds backlink source if type is unique', () async {
275+
final source = r'''
276+
library example;
277+
import 'package:objectbox/objectbox.dart';
278+
279+
@Entity()
280+
class Example {
281+
@Id()
282+
int id = 0;
283+
284+
final relA = ToMany<A>();
285+
final relB = ToOne<B>();
286+
}
287+
288+
// Name related classes to be lexically before Example so they are
289+
// processed first.
290+
291+
@Entity()
292+
class A {
293+
@Id()
294+
int id = 0;
295+
296+
@Backlink()
297+
final backRel = ToMany<Example>();
298+
}
299+
300+
@Entity()
301+
class B {
302+
@Id()
303+
int id = 0;
304+
305+
@Backlink()
306+
final backRel = ToMany<Example>();
307+
}
308+
''';
309+
310+
final testEnv = GeneratorTestEnv();
311+
await testEnv.run(source);
312+
313+
final entityA = testEnv.model.entities.firstWhere((e) => e.name == 'A');
314+
var backlinkSourceA = entityA.backlinks.first.source;
315+
expect(backlinkSourceA, isA<BacklinkSourceRelation>());
316+
expect((backlinkSourceA as BacklinkSourceRelation).srcRel.name, 'relA');
317+
318+
final entityB = testEnv.model.entities.firstWhere((e) => e.name == 'B');
319+
var backlinkSourceB = entityB.backlinks.first.source;
320+
expect(backlinkSourceB, isA<BacklinkSourceProperty>());
321+
expect(
322+
(backlinkSourceB as BacklinkSourceProperty).srcProp.relationField,
323+
'relB',
324+
);
325+
});
326+
327+
test('Errors if backlink source is not unique', () async {
328+
final source = r'''
329+
library example;
330+
import 'package:objectbox/objectbox.dart';
331+
332+
@Entity()
333+
class Example {
334+
@Id()
335+
int id = 0;
336+
337+
final relA1 = ToOne<A>();
338+
final relA2 = ToOne<A>();
339+
final relA3 = ToMany<A>();
340+
}
341+
342+
@Entity()
343+
class A {
344+
@Id()
345+
int id = 0;
346+
347+
@Backlink()
348+
final backRel = ToMany<Example>();
349+
}
350+
''';
351+
352+
final testEnv = GeneratorTestEnv();
353+
final result = await testEnv.run(source, ignoreOutput: true);
354+
355+
expect(result.builderResult.succeeded, false);
356+
expect(
357+
result.logs,
358+
contains(
359+
isA<LogRecord>()
360+
.having((r) => r.level, 'level', Level.SEVERE)
361+
.having(
362+
(r) => r.message,
363+
'message',
364+
contains('Can\'t determine backlink source for "A.backRel"'),
365+
),
366+
),
367+
);
368+
});
369+
370+
test('Errors if backlink source does not exist', () async {
371+
final source = r'''
372+
library example;
373+
import 'package:objectbox/objectbox.dart';
374+
375+
@Entity()
376+
class Example {
377+
@Id()
378+
int id = 0;
379+
}
380+
381+
@Entity()
382+
class A {
383+
@Id()
384+
int id = 0;
385+
386+
@Backlink()
387+
final backRel = ToMany<Example>();
388+
}
389+
''';
390+
391+
final testEnv = GeneratorTestEnv();
392+
final result = await testEnv.run(source, ignoreOutput: true);
393+
394+
expect(result.builderResult.succeeded, false);
395+
expect(
396+
result.logs,
397+
contains(
398+
isA<LogRecord>()
399+
.having((r) => r.level, 'level', Level.SEVERE)
400+
.having(
401+
(r) => r.message,
402+
'message',
403+
contains(
404+
'Failed to find backlink source for "A.backRel" in "Example"',
405+
),
406+
),
407+
),
408+
);
409+
});
410+
411+
test(
412+
'Does not pick implicit backlink source if explicit one does not exist',
413+
() async {
414+
final source = r'''
415+
library example;
416+
import 'package:objectbox/objectbox.dart';
417+
418+
@Entity()
419+
class Example {
420+
@Id()
421+
int id = 0;
422+
423+
final relA1 = ToOne<A>();
424+
final relA2 = ToMany<A>();
425+
}
426+
427+
@Entity()
428+
class A {
429+
@Id()
430+
int id = 0;
431+
432+
@Backlink('doesnotexist')
433+
final backRel = ToMany<Example>();
434+
}
435+
''';
436+
437+
final testEnv = GeneratorTestEnv();
438+
final result = await testEnv.run(source, ignoreOutput: true);
439+
440+
expect(result.builderResult.succeeded, false);
441+
expect(
442+
result.logs,
443+
contains(
444+
isA<LogRecord>()
445+
.having((r) => r.level, 'level', Level.SEVERE)
446+
.having(
447+
(r) => r.message,
448+
'message',
449+
contains(
450+
'Failed to find backlink source for "A.backRel" in "Example"',
451+
),
452+
),
453+
),
454+
);
455+
},
456+
);
457+
274458
test('@TargetIdProperty ToOne annotation', () async {
275459
final source = r'''
276460
library example;
@@ -298,6 +482,44 @@ void main() {
298482
expect(renamedRelationProperty!.type, OBXPropertyType.Relation);
299483
});
300484

485+
test('Explicit backlink to renamed ToOne target ID property', () async {
486+
final source = r'''
487+
library example;
488+
import 'package:objectbox/objectbox.dart';
489+
490+
@Entity()
491+
class Example {
492+
@Id()
493+
int id = 0;
494+
495+
@TargetIdProperty('customerRef')
496+
final customer = ToOne<Customer>();
497+
}
498+
499+
@Entity()
500+
class Customer {
501+
@Id()
502+
int id = 0;
503+
504+
@Backlink('customer')
505+
final backRel = ToMany<Example>();
506+
}
507+
''';
508+
509+
final testEnv = GeneratorTestEnv();
510+
await testEnv.run(source);
511+
512+
final customerEntity = testEnv.model.entities.firstWhere(
513+
(e) => e.name == 'Customer',
514+
);
515+
var backlinkSource = customerEntity.backlinks.first.source;
516+
expect(backlinkSource, isA<BacklinkSourceProperty>());
517+
expect(
518+
(backlinkSource as BacklinkSourceProperty).srcProp.relationField,
519+
'customer',
520+
);
521+
});
522+
301523
test('ToOne target ID property name conflict', () async {
302524
// Note: unlike in Java, for Dart it's also not supported to "expose" the
303525
// target ID (relation) property.
@@ -315,7 +537,7 @@ void main() {
315537
''';
316538

317539
final testEnv = GeneratorTestEnv();
318-
final result = await testEnv.run(source, expectNoOutput: true);
540+
final result = await testEnv.run(source, ignoreOutput: true);
319541

320542
expect(result.builderResult.succeeded, false);
321543
expect(
@@ -350,7 +572,7 @@ void main() {
350572
''';
351573

352574
final testEnv = GeneratorTestEnv();
353-
final result = await testEnv.run(source, expectNoOutput: true);
575+
final result = await testEnv.run(source, ignoreOutput: true);
354576

355577
expect(result.builderResult.succeeded, false);
356578
expect(

generator/test/generator_test_env.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class GeneratorTestEnv {
3737

3838
Future<GeneratorTestResult> run(
3939
String source, {
40-
bool expectNoOutput = false,
40+
bool ignoreOutput = false,
4141
}) async {
4242
final library = "example";
4343
// Enable resolving imports (imported packages must be a dependency of this package)
@@ -66,7 +66,7 @@ class GeneratorTestEnv {
6666
[resolver, codeBuilder],
6767
sourceAssets,
6868
readerWriter: readerWriter,
69-
outputs: expectNoOutput ? {} : expectedOutputs,
69+
outputs: ignoreOutput ? null : expectedOutputs,
7070
onLog: (record) {
7171
// Setting onLog overwrites the useful default logger set by
7272
// testBuilders, so reimplement it
@@ -76,7 +76,7 @@ class GeneratorTestEnv {
7676
},
7777
);
7878

79-
if (!expectNoOutput) {
79+
if (!ignoreOutput) {
8080
// Assert generator model
8181
final modelFile = File(path.join("lib", config.jsonFile));
8282
final jsonModel = await _readModelFile(modelFile);

objectbox/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
MongoDB to match the name used in the MongoDB database. [#713](https://github.com/objectbox/objectbox-dart/issues/713)
1616
* Provide a helpful error message if the name of a property conflicts with a target ID property
1717
created for a `ToOne` relation. [#713](https://github.com/objectbox/objectbox-dart/issues/713)
18+
* Generator: find `@Backlink()` source relation also in case target entity class is processed first.
19+
[#687](https://github.com/objectbox/objectbox-dart/issues/687)
1820

1921
### Sync
2022

0 commit comments

Comments
 (0)