-
Notifications
You must be signed in to change notification settings - Fork 299
Bug fix for pagination nested entities resulting key not found error. #3029
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
Open
anushakolan
wants to merge
8
commits into
main
Choose a base branch
from
dev/anushakolan/nested-entities-bug
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+331
−10
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
4511911
Bug fix for pagination nested entities resulting key not found error.
anushakolan 8b6ee7b
Update src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQuery…
anushakolan 5cb80b9
Addressed comments
anushakolan e8b1c2e
Update src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQuery…
anushakolan 242c806
Added relationship path suffix to keys
anushakolan 838c059
Addressed comments
anushakolan 9e8d793
Changes in test case to compare entire tree not sub trees
anushakolan 2a86277
Updated sql command
anushakolan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| using System.Diagnostics; | ||
| using System.Globalization; | ||
| using System.Net; | ||
| using System.Text; | ||
| using System.Text.Json; | ||
| using Azure.DataApiBuilder.Config.ObjectModel; | ||
| using Azure.DataApiBuilder.Core.Configurations; | ||
|
|
@@ -534,7 +535,24 @@ public static InputObjectType InputObjectTypeFromIInputField(IInputValueDefiniti | |
| // /books/items/items[idx]/authors -> Depth: 3 (0-indexed) which maps to the | ||
| // pagination metadata for the "authors/items" subquery. | ||
| string paginationObjectParentName = GetMetadataKey(context.Path) + "::" + context.Path.Parent.Depth(); | ||
| return (IMetadata?)context.ContextData[paginationObjectParentName]; | ||
|
|
||
| // For nested list fields under relationships (e.g. reviews.items, authors.items), | ||
| // include the relationship path suffix so we look up the same key that | ||
| // SetNewMetadataChildren stored ("::depth::relationshipPath"). | ||
| string relationshipPath = GetRelationshipPathSuffix(context.Path.Parent); | ||
| if (!string.IsNullOrEmpty(relationshipPath)) | ||
| { | ||
| paginationObjectParentName = paginationObjectParentName + "::" + relationshipPath; | ||
| } | ||
|
|
||
| if (context.ContextData.TryGetValue(key: paginationObjectParentName, out object? itemsPaginationMetadata) && itemsPaginationMetadata is not null) | ||
| { | ||
| return (IMetadata)itemsPaginationMetadata; | ||
| } | ||
|
|
||
| // If metadata is missing (e.g. Cosmos DB or pruned relationship), return an empty | ||
| // pagination metadata object to avoid KeyNotFoundException. | ||
| return PaginationMetadata.MakeEmptyPaginationMetadata(); | ||
| } | ||
|
|
||
| // This section would be reached when processing a Cosmos query of the form: | ||
|
|
@@ -582,7 +600,24 @@ private static IMetadata GetMetadataObjectField(IResolverContext context) | |
| // pagination metadata from context.ContextData | ||
| // The PaginationMetadata fetched has subquery metadata for "authors" from path "/books/items/authors" | ||
| string objectParentName = GetMetadataKey(context.Path) + "::" + context.Path.Parent.Parent.Depth(); | ||
| return (IMetadata)context.ContextData[objectParentName]!; | ||
|
|
||
| // Include relationship path suffix (for example, "addresses" or "phoneNumbers") so | ||
| // we look up the same key that SetNewMetadataChildren stored | ||
| // ("::depth::relationshipPath"). | ||
| string relationshipPath = GetRelationshipPathSuffix(context.Path.Parent.Parent); | ||
| if (!string.IsNullOrEmpty(relationshipPath)) | ||
| { | ||
| objectParentName = objectParentName + "::" + relationshipPath; | ||
| } | ||
|
|
||
| if (context.ContextData.TryGetValue(objectParentName, out object? indexerMetadata) && indexerMetadata is not null) | ||
| { | ||
| return (IMetadata)indexerMetadata; | ||
| } | ||
|
|
||
| // If no metadata is present (for example, for non-paginated relationships or when | ||
| // RBAC prunes a branch), return an empty pagination metadata object. | ||
| return PaginationMetadata.MakeEmptyPaginationMetadata(); | ||
| } | ||
|
|
||
| if (!context.Path.IsRootField() && ((NamePathSegment)context.Path.Parent).Name != PURE_RESOLVER_CONTEXT_SUFFIX) | ||
|
|
@@ -592,12 +627,35 @@ private static IMetadata GetMetadataObjectField(IResolverContext context) | |
| // e.g. metadata for index 4 will not exist. only 3. | ||
| // Depth: / 0 / 1 / 2 / 3 / 4 | ||
| // Path: /books/items/items[0]/publishers/books | ||
| // | ||
| // To handle arbitrary nesting depths with sibling relationships, we need to include | ||
| // the relationship field path in the key. For example: | ||
| // - /entity/items[0]/rel1/nested uses key ::3::rel1 | ||
| // - /entity/items[0]/rel2/nested uses key ::3::rel2 | ||
| // - /entity/items[0]/rel1/nested/deeper uses key ::4::rel1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why wouldn't this example use the key |
||
| // - /entity/items[0]/rel1/nested2/deeper uses key ::4::rel1::nested2 | ||
| string objectParentName = GetMetadataKey(context.Path.Parent) + "::" + context.Path.Parent.Depth(); | ||
| return (IMetadata)context.ContextData[objectParentName]!; | ||
| string relationshipPath = GetRelationshipPathSuffix(context.Path.Parent); | ||
| if (!string.IsNullOrEmpty(relationshipPath)) | ||
| { | ||
| objectParentName = objectParentName + "::" + relationshipPath; | ||
| } | ||
|
|
||
| if (context.ContextData.TryGetValue(objectParentName, out object? nestedMetadata) && nestedMetadata is not null) | ||
| { | ||
| return (IMetadata)nestedMetadata; | ||
| } | ||
|
|
||
| return PaginationMetadata.MakeEmptyPaginationMetadata(); | ||
| } | ||
|
|
||
| string metadataKey = GetMetadataKey(context.Path) + "::" + context.Path.Depth(); | ||
| return (IMetadata)context.ContextData[metadataKey]!; | ||
| if (context.ContextData.TryGetValue(metadataKey, out object? rootMetadata) && rootMetadata is not null) | ||
| { | ||
| return (IMetadata)rootMetadata; | ||
| } | ||
|
|
||
| return PaginationMetadata.MakeEmptyPaginationMetadata(); | ||
| } | ||
|
|
||
| private static string GetMetadataKey(HotChocolate.Path path) | ||
|
|
@@ -614,6 +672,50 @@ private static string GetMetadataKey(HotChocolate.Path path) | |
| return GetMetadataKey(path: path.Parent); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Builds a suffix representing the relationship path from the IndexerPathSegment (items[n]) | ||
| /// up to (but not including) the current path segment. This is used to create unique metadata | ||
| /// keys for sibling relationships at any nesting depth. | ||
| /// </summary> | ||
| /// <param name="path">The path to build the suffix for</param> | ||
| /// <returns> | ||
| /// A string like "rel1" for /entity/items[0]/rel1, | ||
| /// or "rel1::nested" for /entity/items[0]/rel1/nested, | ||
| /// or empty string if no IndexerPathSegment is found in the path ancestry. | ||
| /// </returns> | ||
| private static string GetRelationshipPathSuffix(HotChocolate.Path path) | ||
| { | ||
| List<string> pathParts = new(); | ||
| HotChocolate.Path? current = path; | ||
|
|
||
| // Walk up the path collecting relationship field names until we hit an IndexerPathSegment | ||
| while (current is not null && !current.IsRoot) | ||
| { | ||
| if (current is IndexerPathSegment) | ||
| { | ||
| // We've reached items[n], stop here | ||
| break; | ||
| } | ||
|
|
||
| if (current is NamePathSegment nameSegment) | ||
| { | ||
| pathParts.Add(nameSegment.Name); | ||
| } | ||
|
|
||
| current = current.Parent; | ||
| } | ||
|
|
||
| // If we didn't find an IndexerPathSegment, return empty (this handles root-level queries) | ||
| if (current is not IndexerPathSegment) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| // Reverse because we walked up the tree, but we want the path from root to leaf | ||
| pathParts.Reverse(); | ||
| return string.Join("::", pathParts); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resolves the name of the root object of a selection set to | ||
| /// use as the beginning of a key used to index pagination metadata in the | ||
|
|
@@ -655,7 +757,25 @@ private static void SetNewMetadataChildren(IResolverContext context, IMetadata? | |
| // When context.Path takes the form: "/entity/items[index]/nestedEntity" HC counts the depth as | ||
| // if the path took the form: "/entity/items/items[index]/nestedEntity" -> Depth of "nestedEntity" | ||
| // is 3 because depth is 0-indexed. | ||
| string contextKey = GetMetadataKey(context.Path) + "::" + context.Path.Depth(); | ||
| StringBuilder contextKeyBuilder = new(); | ||
| contextKeyBuilder | ||
| .Append(GetMetadataKey(context.Path)) | ||
| .Append("::") | ||
| .Append(context.Path.Depth()); | ||
|
|
||
| // For relationship fields at any depth, include the relationship path suffix to distinguish | ||
| // between sibling relationships. This handles arbitrary nesting depths. | ||
| // e.g., "/entity/items[0]/rel1" gets key ::3::rel1 | ||
| // e.g., "/entity/items[0]/rel1/nested" gets key ::4::rel1::nested | ||
| string relationshipPath = GetRelationshipPathSuffix(context.Path); | ||
| if (!string.IsNullOrEmpty(relationshipPath)) | ||
| { | ||
| contextKeyBuilder | ||
| .Append("::") | ||
| .Append(relationshipPath); | ||
| } | ||
|
|
||
| string contextKey = contextKeyBuilder.ToString(); | ||
|
|
||
| // It's okay to overwrite the context when we are visiting a different item in items e.g. books/items/items[1]/publishers since | ||
| // context for books/items/items[0]/publishers processing is done and that context isn't needed anymore. | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.