Skip to content

✨ feat(manager): Added support query with nested relations - #23

Open
KiddoV wants to merge 4 commits into
Shreemanarjun:mainfrom
KiddoV:22-support-query-nested-relation
Open

✨ feat(manager): Added support query with nested relations#23
KiddoV wants to merge 4 commits into
Shreemanarjun:mainfrom
KiddoV:22-support-query-nested-relation

Conversation

@KiddoV

@KiddoV KiddoV commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

This PR add support for query with nested relations, that currently missing for Datum.
Related issue #22

Summary by CodeRabbit

  • Refactor
    • Improved relation-loading to build hierarchical relation paths and eagerly load nested relations with safer early-exit handling for missing or empty relations.
  • Tests
    • Added tests validating nested related-data queries and adjusted test models to reflect the new relational-loading behavior.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the Datum framework by enabling the querying and loading of nested relations for entities. This change expands the data retrieval capabilities, allowing the system to traverse complex relation paths, which was previously not possible. The refactoring improves the maintainability and extensibility of the relation loading mechanism.

Highlights

  • Nested Relation Queries: Implemented support for querying nested relations (e.g., user.address.city) within the Datum framework, allowing for deeper data retrieval.
  • Refactored Relation Loading: The existing _loadRelations method was refactored to utilize a new recursive helper method, _loadRelationPath, which efficiently handles loading relations level by level.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • packages/datum/lib/source/core/manager/datum_manager.dart
    • Refactored the _loadRelations method to support nested relation paths.
    • Introduced a new private method _loadRelationPath for recursively loading relations.
    • Adjusted the logic for BelongsTo and HasMany relations within the new recursive structure to collect related entities for subsequent levels.
Activity
  • No specific activity has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for querying nested relations by refactoring _fetchAndStitchRelations to recursively load relation paths, with a new _loadRelationPath helper method. However, a potential runtime exception has been identified in the HasMany relation processing logic due to an unsafe cast. This flaw could lead to a Denial of Service if query parameters are exposed to untrusted input. My review includes a suggestion to fix this to make the implementation more robust.

Comment thread packages/datum/lib/source/core/manager/datum_manager.dart
@KiddoV KiddoV changed the title Added support query with nested relations ✨ feat(manager): Added support query with nested relations Mar 27, 2026
@Shreemanarjun

Copy link
Copy Markdown
Owner

Please add tests which bug you are fixing . @KiddoV .

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Refactored eager-loading to build dot-separated relation paths and recursively load nested relations via a new async helper, handling BelongsTo and HasMany with early-return branches for missing relations, empty keys, or no related entities. Tests adjusted to use non-const constructors and cached relation maps; added a nested related-data query test.

Changes

Cohort / File(s) Summary
Relation Loading Refactor
packages/datum/lib/source/core/manager/datum_manager.dart
Rewrote _fetchAndStitchRelations to construct a dot-separated relation tree and added async recursive _loadRelationTree. Resolves relations on the first related entity, handles BelongsTo (query by localKey → setRaw) and HasMany (query by foreignKey → group by parent id → setRaw), and includes multiple early-return branches; recursion proceeds only when children exist and related entities were found.
Test models & query updates
packages/datum/test/core/relational_data_test.dart
Changed User, Post, Profile, Tag constructors from const to non-const; moved relations to a cached late final Map<String, Relation> _relations returned by the getter. Added a Relational Data: query test that seeds data with a MockLocalAdapter and asserts nested relations using DatumQuery(withRelated: ['posts', 'posts.author']).

Sequence Diagram(s)

sequenceDiagram
    participant Manager
    participant RelationTree
    participant Adapter
    participant Entities

    Manager->>RelationTree: build dot-separated relation tree from withRelated
    Manager->>Manager: call _loadRelationTree(rootNode, entities)
    alt entities empty
        Manager-->>Manager: return
    else
        Manager->>RelationTree: resolve relation on first entity
        RelationTree-->>Manager: relation descriptor (BelongsTo/HasMany) or null
        alt relation missing
            Manager-->>Manager: log and return
        else
            Manager->>Adapter: query related records (by localKey or foreignKey)
            Adapter-->>Entities: return related records
            Manager->>Entities: setRaw on parent relations / group for HasMany
            alt children exist and relatedEntities not empty
                Manager->>Manager: recurse for each child node with relatedEntities
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop through dot-paths, soft and light,

stitching kin in the quiet night,
Belong and Many join the dance,
nested hops of eager chance,
a rabbit cheers this recursive sight 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding support for querying with nested relations in the Datum manager. It directly corresponds to the PR objectives and code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@KiddoV
KiddoV force-pushed the 22-support-query-nested-relation branch from fb242c4 to b147039 Compare March 31, 2026 13:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/datum/lib/source/core/manager/datum_manager.dart`:
- Around line 1021-1078: This code only handles BelongsTo and HasMany, so HasOne
and ManyToMany paths never call setRaw or recurse; add branches for relation is
HasOne and relation is ManyToMany in the same conditional where
BelongsTo/HasMany are handled (use the same variables: relationName,
relatedManager, relatedEntities, source, userId) so they mirror
_getRelatedEntities() behavior: for HasOne query relatedManager with
foreignKey/localKey similar to HasMany but set a single related entity via
entity.relations[relationName]?.setRaw(related) and add that related to
relatedEntities; for ManyToMany perform the intermediate/pivot join query the
same way _getRelatedEntities() does (fetch related ids via pivot, then load
related entities) and setRaw the resulting list on each parent and add to
relatedEntities so eager loading covers HasOne and ManyToMany as well.
- Around line 1021-1076: The code assumes the primary key is always "id" when
stitching relations; update both BelongsTo and HasMany branches to use
relation.localKey consistently: in the BelongsTo branch build the lookup map
from fetched rows using (fetchedItem as
RelationalDatumEntity).toDatumMap()[relation.localKey] instead of e.id so keys
match the DatumQuery filter, and when reading the parent key use
entity.toDatumMap()[foreignKeyName] as now; in the HasMany branch compute
localKeyValues from entities.map((e) => e.toDatumMap()[relation.localKey]) and
when assigning related children use
grouped[entity.toDatumMap()[relation.localKey]] (ensure grouping keys come from
fetched.toDatumMap()[foreignKeyName] and remain strings) so all joins use
relation.localKey rather than hard-coded id when calling relatedManager.query,
building grouped maps, and calling entity.relations[relationName]?.setRaw.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23eafc04-f1bd-435b-9050-f88747179e7c

📥 Commits

Reviewing files that changed from the base of the PR and between 21c7246 and b147039.

📒 Files selected for processing (1)
  • packages/datum/lib/source/core/manager/datum_manager.dart

Comment on lines +1021 to +1078
if (relation is BelongsTo) {
final foreignKeyName = relation.foreignKey;
final foreignKeyValues = entities.map((e) => e.toDatumMap()[foreignKeyName]).nonNulls.toSet().toList();

for (final entity in entities) {
final related = relatedEntitiesByParentId[entity.id] ?? [];
(entity as RelationalDatumEntity).relations[relationName]?.setRaw(related);
}
if (foreignKeyValues.isEmpty) return;

final fetched = await relatedManager.query(
DatumQuery(
filters: [
Filter(relation.localKey, FilterOperator.isIn, foreignKeyValues)
],
),
source: source,
userId: userId,
);

final byId = {for (var e in fetched) e.id: e};

for (final entity in entities) {
final fk = entity.toDatumMap()[foreignKeyName];
final related = byId[fk];

entity.relations[relationName]?.setRaw(related);

if (related is RelationalDatumEntity) {
relatedEntities.add(related);
}
}
} else if (relation is HasMany) {
final foreignKeyName = relation.foreignKey;
final localKeyValues = entities.map((e) => e.id).toSet().toList();

if (localKeyValues.isEmpty) return;

final fetched = await relatedManager.query(
DatumQuery(
filters: [
Filter(foreignKeyName, FilterOperator.isIn, localKeyValues)
],
),
source: source,
userId: userId,
);

final grouped = <String, List<RelationalDatumEntity>>{};

for (final entity in fetched) {
final parentId = (entity as RelationalDatumEntity).toDatumMap()[foreignKeyName];

(grouped[parentId] ??= []).add(entity);
relatedEntities.add(entity);
}

for (final entity in entities) {
final related = grouped[entity.id] ?? [];
entity.relations[relationName]?.setRaw(related);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

HasOne and ManyToMany relations currently fall through without loading.

This helper only handles BelongsTo and HasMany. If withRelated includes a HasOne or ManyToMany path, the method neither calls setRaw nor recurses into its children, so eager loading silently returns partial data. _getRelatedEntities() later in this file already supports both relation types, so this loader should match that coverage before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/datum/lib/source/core/manager/datum_manager.dart` around lines 1021
- 1078, This code only handles BelongsTo and HasMany, so HasOne and ManyToMany
paths never call setRaw or recurse; add branches for relation is HasOne and
relation is ManyToMany in the same conditional where BelongsTo/HasMany are
handled (use the same variables: relationName, relatedManager, relatedEntities,
source, userId) so they mirror _getRelatedEntities() behavior: for HasOne query
relatedManager with foreignKey/localKey similar to HasMany but set a single
related entity via entity.relations[relationName]?.setRaw(related) and add that
related to relatedEntities; for ManyToMany perform the intermediate/pivot join
query the same way _getRelatedEntities() does (fetch related ids via pivot, then
load related entities) and setRaw the resulting list on each parent and add to
relatedEntities so eager loading covers HasOne and ManyToMany as well.

Comment on lines +1021 to +1076
if (relation is BelongsTo) {
final foreignKeyName = relation.foreignKey;
final foreignKeyValues = entities.map((e) => e.toDatumMap()[foreignKeyName]).nonNulls.toSet().toList();

for (final entity in entities) {
final related = relatedEntitiesByParentId[entity.id] ?? [];
(entity as RelationalDatumEntity).relations[relationName]?.setRaw(related);
}
if (foreignKeyValues.isEmpty) return;

final fetched = await relatedManager.query(
DatumQuery(
filters: [
Filter(relation.localKey, FilterOperator.isIn, foreignKeyValues)
],
),
source: source,
userId: userId,
);

final byId = {for (var e in fetched) e.id: e};

for (final entity in entities) {
final fk = entity.toDatumMap()[foreignKeyName];
final related = byId[fk];

entity.relations[relationName]?.setRaw(related);

if (related is RelationalDatumEntity) {
relatedEntities.add(related);
}
}
} else if (relation is HasMany) {
final foreignKeyName = relation.foreignKey;
final localKeyValues = entities.map((e) => e.id).toSet().toList();

if (localKeyValues.isEmpty) return;

final fetched = await relatedManager.query(
DatumQuery(
filters: [
Filter(foreignKeyName, FilterOperator.isIn, localKeyValues)
],
),
source: source,
userId: userId,
);

final grouped = <String, List<RelationalDatumEntity>>{};

for (final entity in fetched) {
final parentId = (entity as RelationalDatumEntity).toDatumMap()[foreignKeyName];

(grouped[parentId] ??= []).add(entity);
relatedEntities.add(entity);
}

for (final entity in entities) {
final related = grouped[entity.id] ?? [];
entity.relations[relationName]?.setRaw(related);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Honor relation.localKey when batching and stitching.

BelongsTo filters on relation.localKey but then looks up fetched rows by e.id, and HasMany hard-codes entity.id on both sides of the join. Any relation using a non-id local key will attach the wrong children/parent here.

Suggested fix
     if (relation is BelongsTo) {
       final foreignKeyName = relation.foreignKey;
       final foreignKeyValues = entities.map((e) => e.toDatumMap()[foreignKeyName]).nonNulls.toSet().toList();

       if (foreignKeyValues.isEmpty) return;

       final fetched = await relatedManager.query(
         DatumQuery(
           filters: [
             Filter(relation.localKey, FilterOperator.isIn, foreignKeyValues)
           ],
         ),
         source: source,
         userId: userId,
       );

-      final byId = {for (var e in fetched) e.id: e};
+      final byLocalKey = {
+        for (final e in fetched) e.toDatumMap()[relation.localKey]: e,
+      };

       for (final entity in entities) {
         final fk = entity.toDatumMap()[foreignKeyName];
-        final related = byId[fk];
+        final related = byLocalKey[fk];

         entity.relations[relationName]?.setRaw(related);

         if (related is RelationalDatumEntity) {
           relatedEntities.add(related);
         }
       }
     } else if (relation is HasMany) {
       final foreignKeyName = relation.foreignKey;
-      final localKeyValues = entities.map((e) => e.id).toSet().toList();
+      final localKeyValues = entities
+          .map((e) => e.toDatumMap()[relation.localKey])
+          .nonNulls
+          .toSet()
+          .toList();

       if (localKeyValues.isEmpty) return;

       final fetched = await relatedManager.query(
         DatumQuery(
           filters: [
             Filter(foreignKeyName, FilterOperator.isIn, localKeyValues)
           ],
         ),
         source: source,
         userId: userId,
       );

-      final grouped = <String, List<RelationalDatumEntity>>{};
+      final grouped = <Object?, List<RelationalDatumEntity>>{};

       for (final entity in fetched) {
         final parentId = (entity as RelationalDatumEntity).toDatumMap()[foreignKeyName];

         (grouped[parentId] ??= []).add(entity);
         relatedEntities.add(entity);
       }

       for (final entity in entities) {
-        final related = grouped[entity.id] ?? [];
+        final parentKey = entity.toDatumMap()[relation.localKey];
+        final related = grouped[parentKey] ?? const <RelationalDatumEntity>[];
         entity.relations[relationName]?.setRaw(related);
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/datum/lib/source/core/manager/datum_manager.dart` around lines 1021
- 1076, The code assumes the primary key is always "id" when stitching
relations; update both BelongsTo and HasMany branches to use relation.localKey
consistently: in the BelongsTo branch build the lookup map from fetched rows
using (fetchedItem as RelationalDatumEntity).toDatumMap()[relation.localKey]
instead of e.id so keys match the DatumQuery filter, and when reading the parent
key use entity.toDatumMap()[foreignKeyName] as now; in the HasMany branch
compute localKeyValues from entities.map((e) =>
e.toDatumMap()[relation.localKey]) and when assigning related children use
grouped[entity.toDatumMap()[relation.localKey]] (ensure grouping keys come from
fetched.toDatumMap()[foreignKeyName] and remain strings) so all joins use
relation.localKey rather than hard-coded id when calling relatedManager.query,
building grouped maps, and calling entity.relations[relationName]?.setRaw.

@KiddoV

KiddoV commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

@Shreemanarjun I added test. Please check!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/datum/test/core/relational_data_test.dart (2)

886-886: Test name is misleading.

The test is named 'query supports nested relation field (author.name)' which implies filtering or selecting by a nested field, but the test actually verifies eager loading of nested relations via withRelated. Consider renaming to accurately describe what's being tested.

✏️ Suggested rename
-    test('query supports nested relation field (author.name)', () async {
+    test('query eager-loads nested relations via withRelated', () async {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/datum/test/core/relational_data_test.dart` at line 886, Rename the
test description to accurately reflect that it's verifying eager loading of
nested relations via withRelated rather than querying/filtering by a nested
field; update the test named 'query supports nested relation field
(author.name)' to something like 'eager loads nested relation via withRelated
(author.name)' or 'withRelated eagerly loads nested relation author.name' so it
clearly references the use of withRelated in the test body.

920-924: Consider adding assertions on the nested author's data.

The test verifies that author is not null but doesn't assert on the actual author data. Adding assertions on author.id and author.name would strengthen the test by confirming the correct entity was loaded.

💪 Proposed enhancement
       final author = postsWithAuthors?.first.relations['author']?.value;
       expect(postsWithAuthors?.length, 1, reason: 'Should find 1 post');
       expect(author, isNotNull, reason: 'Post author should exist');
+      expect((author as User).id, testUser.id, reason: 'Author ID should match testUser');
+      expect(author.name, 'Kiddo V', reason: 'Author name should match');
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/datum/test/core/relational_data_test.dart` around lines 920 - 924,
The test currently checks postsWithAuthors and that author is not null but
doesn't assert the author's fields; update the assertions to verify the nested
author's identity by asserting expected values for author.id and author.name
(for example compare to the source user in usersWithPosts.first or to the
specific expected literals used when creating the fixture). Locate the variables
postsWithAuthors and author in the test (and the usersWithPosts fixture) and add
assertEquals-like expectations for author.id and author.name to confirm the
correct User entity was loaded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/datum/test/core/relational_data_test.dart`:
- Around line 870-885: The setUp block creating testUser and testPost is missing
a call to Datum.resetForTesting(), which risks test-state leakage; update the
setUp for this test group to call Datum.resetForTesting() before creating
testUser/testPost (i.e., invoke Datum.resetForTesting() at the start of the
setUp for this group so the singleton Datum is cleared before the fixtures are
created).

---

Nitpick comments:
In `@packages/datum/test/core/relational_data_test.dart`:
- Line 886: Rename the test description to accurately reflect that it's
verifying eager loading of nested relations via withRelated rather than
querying/filtering by a nested field; update the test named 'query supports
nested relation field (author.name)' to something like 'eager loads nested
relation via withRelated (author.name)' or 'withRelated eagerly loads nested
relation author.name' so it clearly references the use of withRelated in the
test body.
- Around line 920-924: The test currently checks postsWithAuthors and that
author is not null but doesn't assert the author's fields; update the assertions
to verify the nested author's identity by asserting expected values for
author.id and author.name (for example compare to the source user in
usersWithPosts.first or to the specific expected literals used when creating the
fixture). Locate the variables postsWithAuthors and author in the test (and the
usersWithPosts fixture) and add assertEquals-like expectations for author.id and
author.name to confirm the correct User entity was loaded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5814a0f-2340-4d3c-ae70-1ba239a748cb

📥 Commits

Reviewing files that changed from the base of the PR and between b147039 and 7de7c4c.

📒 Files selected for processing (1)
  • packages/datum/test/core/relational_data_test.dart

Comment thread packages/datum/test/core/relational_data_test.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/datum/test/core/relational_data_test.dart`:
- Around line 919-927: The test accesses usersWithPosts.first without checking
the query result, which can throw a RangeError; update the test around
userManager.query / DatumQuery to assert usersWithPosts is not empty (e.g.,
expect or an explicit check) before using usersWithPosts.first, and only then
derive postsWithAuthors and author from that first element so the failure
message is clear and avoids an obscure RangeError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a66dce85-6204-4e02-b298-53a917605435

📥 Commits

Reviewing files that changed from the base of the PR and between 7de7c4c and f365f9e.

📒 Files selected for processing (1)
  • packages/datum/test/core/relational_data_test.dart

Comment thread packages/datum/test/core/relational_data_test.dart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants