From 615d40a79a725b6199d83855189137e0bf09c59c Mon Sep 17 00:00:00 2001 From: Tom Hollingworth Date: Mon, 8 Jun 2026 18:46:06 -0400 Subject: [PATCH 1/2] fix(query): handle pagination and offset for ordered cascade queries Replace hardcoded result limit of 1000 with a named constant. Implement missing first/offset pagination propagation from cascade directives to the parent query when ordering is applied and resolve an existing TODO comment. Add test cases to prove out solution. --- query/query.go | 16 ++- query/query4_test.go | 226 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 3 deletions(-) diff --git a/query/query.go b/query/query.go index 6926e2ac6ed..33de24c358f 100644 --- a/query/query.go +++ b/query/query.go @@ -32,6 +32,9 @@ import ( "github.com/dgraph-io/dgraph/v25/x" ) +// defaultRetrieveCount is the default number of results to retrieve for a query when pagination arguments are not specified. +const defaultRetrieveCount = 1000 + /* * QUERY: * Let's take this query from GraphQL as example: @@ -663,6 +666,10 @@ func treeCopy(gq *dql.GraphQuery, sg *SubGraph) error { func (args *params) addCascadePaginationArguments(gq *dql.GraphQuery) { args.Cascade.First, _ = strconv.Atoi(gq.Args["first"]) delete(gq.Args, "first") + if args.Cascade.First == 0 { + // Default to a only retrieve up to a set number of results. + args.Cascade.First = defaultRetrieveCount + } args.Cascade.Offset, _ = strconv.Atoi(gq.Args["offset"]) delete(gq.Args, "offset") } @@ -2529,10 +2536,13 @@ func (sg *SubGraph) applyOrderAndPagination(ctx context.Context) error { } } - // Todo: fix offset for cascade queries. if sg.Params.Count == 0 { - // Only retrieve up to 1000 results by default. - sg.Params.Count = 1000 + // Default to a only retrieve up to a set number of results + sg.Params.Count = defaultRetrieveCount + if sg.Params.Cascade != nil && len(sg.Params.Cascade.Fields) > 0 { + sg.Params.Count = sg.Params.Cascade.First + sg.Params.Offset = sg.Params.Cascade.Offset + } } x.AssertTrue(len(sg.Params.Order) > 0) diff --git a/query/query4_test.go b/query/query4_test.go index b2b963d9fdb..c09024a67ff 100644 --- a/query/query4_test.go +++ b/query/query4_test.go @@ -12,6 +12,7 @@ import ( "encoding/json" "fmt" "math/big" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -1900,3 +1901,228 @@ func TestMultiplesSortingOrderWithVarAndPredicate(t *testing.T) { _, err := processQuery(context.Background(), t, query) require.ErrorContains(t, err, "Val() is not allowed in multiple sorting. Got: [SECTIONS_COUNT]") } + +// generateCascadeTestTriples generates triples for N nodes with item names in a +// pseudo-random order to force the sort algorithm to actually sort rather than +// returning entries in insertion order. +func generateCascadeTestTriples(numNodes int, uidBase uint64) string { + var sb strings.Builder + // Generate indices and shuffle them using a simple deterministic shuffle + indices := make([]int, numNodes) + for i := 0; i < numNodes; i++ { + indices[i] = i + } + // Deterministic "shuffle" to ensure unordered insertion + for i := numNodes - 1; i > 0; i-- { + j := (i * 2654435761) % (i + 1) + indices[i], indices[j] = indices[j], indices[i] + } + + for _, idx := range indices { + uid := uidBase + uint64(idx) + // Format name with zero-padding so alphabetical order is deterministic + name := fmt.Sprintf("item_%04d", idx) + detail := fmt.Sprintf("detail_%04d", idx) + fmt.Fprintf(&sb, "<0x%X> \"%s\" .\n", uid, name) + fmt.Fprintf(&sb, "<0x%X> \"%s\" .\n", uid, detail) + } + return sb.String() +} + +// TestCascadeWithOrderAndLargeDataSet verifies cascade behavior with order +// when there are large numbers of nodes (> 1000). It tests: +// - Default limit behavior (no explicit first): 900, 1000, 1100, 3000 nodes +// expecting 900, 1000, 1000, 1000 respectively +// - Explicit first greater than available nodes: ensures all nodes are returned +func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { + // Schema for cascade test type + schema := ` + type CascadeTestItem { + item_name: string + item_detail: string + } + cascade_test_name: string @index(exact) . + cascade_test_detail: string @index(exact) . + ` + + t.Run("DefaultLimit", func(t *testing.T) { + tests := []struct { + name string + numNodes int + expectedCount int + uidBase uint64 + deleteBase uint64 + deleteCount int + }{ + {"900Nodes_DefaultLimit", 900, 900, 0x10000, 0x10000, 900}, + {"1000Nodes_DefaultLimit", 1000, 1000, 0x20000, 0x20000, 1000}, + {"1100Nodes_DefaultLimit", 1100, 1000, 0x30000, 0x30000, 1100}, + {"3000Nodes_DefaultLimit", 3000, 1000, 0x40000, 0x40000, 3000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clean up from previous runs + setSchema(testSchema) + + // Set up schema + setSchema(schema) + t.Cleanup(func() { + dropPredicate("cascade_test_name") + dropPredicate("cascade_test_detail") + setSchema(testSchema) + }) + + // Insert data in unordered fashion + triples := generateCascadeTestTriples(tt.numNodes, tt.uidBase) + require.NoError(t, addTriplesToCluster(triples)) + + // Query with orderasc and @cascade, no explicit first + query := `{ + me(func: type(CascadeTestItem), orderasc: cascade_test_name) @cascade { + item_name: cascade_test_name + item_detail: cascade_test_detail + } + }` + + js := processQueryNoErr(t, query) + var response struct { + Data struct { + Me []struct { + ItemName string `json:"item_name"` + ItemDetail string `json:"item_detail"` + } `json:"me"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal([]byte(js), &response)) + require.Len(t, response.Data.Me, tt.expectedCount, + "Expected %d results for %d nodes with default limit", tt.expectedCount, tt.numNodes) + + // Verify results are in ascending order + for i := 1; i < len(response.Data.Me); i++ { + require.GreaterOrEqual(t, response.Data.Me[i].ItemName, response.Data.Me[i-1].ItemName, + "Results should be in ascending order at index %d", i) + } + }) + } + }) + + t.Run("ExplicitFirstGreaterThanAvailable", func(t *testing.T) { + tests := []struct { + name string + numNodes int + first int + expectedCount int + uidBase uint64 + }{ + {"1100Nodes_First10000", 1100, 10000, 1100, 0x50000}, + {"3000Nodes_First10000", 3000, 10000, 3000, 0x60000}, + {"3000Nodes_First1500", 3000, 1500, 1500, 0x70000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clean up from previous runs + setSchema(testSchema) + + // Set up schema + setSchema(schema) + t.Cleanup(func() { + dropPredicate("cascade_test_name") + dropPredicate("cascade_test_detail") + setSchema(testSchema) + }) + + // Insert data in unordered fashion + triples := generateCascadeTestTriples(tt.numNodes, tt.uidBase) + require.NoError(t, addTriplesToCluster(triples)) + + // Query with explicit first > available, orderasc and @cascade + query := fmt.Sprintf(`{ + me(func: type(CascadeTestItem), first: %d, orderasc: cascade_test_name) @cascade { + item_name: cascade_test_name + item_detail: cascade_test_detail + } + }`, tt.first) + + js := processQueryNoErr(t, query) + var response struct { + Data struct { + Me []struct { + ItemName string `json:"item_name"` + ItemDetail string `json:"item_detail"` + } `json:"me"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal([]byte(js), &response)) + require.Len(t, response.Data.Me, tt.expectedCount, + "Expected %d results for %d nodes with first:%d", tt.expectedCount, tt.numNodes, tt.first) + + // Verify results are in ascending order + for i := 1; i < len(response.Data.Me); i++ { + require.GreaterOrEqual(t, response.Data.Me[i].ItemName, response.Data.Me[i-1].ItemName, + "Results should be in ascending order at index %d", i) + } + }) + } + }) + + t.Run("OrderDescWithLargeDataSet", func(t *testing.T) { + tests := []struct { + name string + numNodes int + first int + expectedCount int + uidBase uint64 + }{ + {"1100Nodes_OrderDesc_First10000", 1100, 10000, 1100, 0x80000}, + {"3000Nodes_OrderDesc_First10000", 3000, 10000, 3000, 0x90000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clean up from previous runs + setSchema(testSchema) + + // Set up schema + setSchema(schema) + t.Cleanup(func() { + dropPredicate("cascade_test_name") + dropPredicate("cascade_test_detail") + setSchema(testSchema) + }) + + // Insert data in unordered fashion + triples := generateCascadeTestTriples(tt.numNodes, tt.uidBase) + require.NoError(t, addTriplesToCluster(triples)) + + // Query with orderdesc, explicit first, and @cascade + query := fmt.Sprintf(`{ + me(func: type(CascadeTestItem), first: %d, orderdesc: cascade_test_name) @cascade { + item_name: cascade_test_name + item_detail: cascade_test_detail + } + }`, tt.first) + + js := processQueryNoErr(t, query) + var response struct { + Data struct { + Me []struct { + ItemName string `json:"item_name"` + ItemDetail string `json:"item_detail"` + } `json:"me"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal([]byte(js), &response)) + require.Len(t, response.Data.Me, tt.expectedCount, + "Expected %d results for %d nodes with orderdesc first:%d", tt.expectedCount, tt.numNodes, tt.first) + + // Verify results are in descending order + for i := 1; i < len(response.Data.Me); i++ { + require.LessOrEqual(t, response.Data.Me[i].ItemName, response.Data.Me[i-1].ItemName, + "Results should be in descending order at index %d", i) + } + }) + } + }) +} From cae4f52a6102c0f8f7ee04e314e724278d1c3a0a Mon Sep 17 00:00:00 2001 From: Tom Hollingworth Date: Mon, 8 Jun 2026 19:29:33 -0400 Subject: [PATCH 2/2] test(query): refactor cascade sort tests with parent-child relationships for better test scenarios - Change shuffle-based string sorting data with deterministic pseudo-random integers for numeric sorting - Introduce OrderedCascadeParent and OrderedCascadeChild types to validate order desc with cascading on parent-child edges - Update schema definitions and test cases, removing unused deletion parameters --- query/query4_test.go | 172 ++++++++++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 69 deletions(-) diff --git a/query/query4_test.go b/query/query4_test.go index c09024a67ff..9a9800795c9 100644 --- a/query/query4_test.go +++ b/query/query4_test.go @@ -1902,47 +1902,68 @@ func TestMultiplesSortingOrderWithVarAndPredicate(t *testing.T) { require.ErrorContains(t, err, "Val() is not allowed in multiple sorting. Got: [SECTIONS_COUNT]") } -// generateCascadeTestTriples generates triples for N nodes with item names in a -// pseudo-random order to force the sort algorithm to actually sort rather than -// returning entries in insertion order. +// generateCascadeTestTriples generates triples for N OrderedCascadeParent nodes with +// pseudo-random parent_number values (1-10000) to force the sort algorithm to actually +// sort rather than returning entries in insertion order. +// Only even-indexed parents receive 3-5 OrderedCascadeChild nodes each. func generateCascadeTestTriples(numNodes int, uidBase uint64) string { var sb strings.Builder - // Generate indices and shuffle them using a simple deterministic shuffle - indices := make([]int, numNodes) + childBase := uint64(0xF0000) + childIdx := uint64(0) + + // Generate deterministic pseudo-random numbers in range 1-10000 + // Using a simple LCG-based approach for reproducibility + numbers := make([]int, numNodes) + seed := uint64(12345) for i := 0; i < numNodes; i++ { - indices[i] = i - } - // Deterministic "shuffle" to ensure unordered insertion - for i := numNodes - 1; i > 0; i-- { - j := (i * 2654435761) % (i + 1) - indices[i], indices[j] = indices[j], indices[i] + seed = seed*6364136223846793005 + 1442695040888963407 // LCG + numbers[i] = int((seed % 10000) + 1) } - for _, idx := range indices { - uid := uidBase + uint64(idx) - // Format name with zero-padding so alphabetical order is deterministic - name := fmt.Sprintf("item_%04d", idx) - detail := fmt.Sprintf("detail_%04d", idx) - fmt.Fprintf(&sb, "<0x%X> \"%s\" .\n", uid, name) - fmt.Fprintf(&sb, "<0x%X> \"%s\" .\n", uid, detail) + for i := 0; i < numNodes; i++ { + uid := uidBase + uint64(i) + fmt.Fprintf(&sb, "<0x%X> \"%d\" .\n", uid, numbers[i]) + + // Only even-indexed parents get children (3-5 children deterministically) + if i%2 == 0 { + numChildren := 3 + (i % 2 * 0) + (i/2)%3 // produces 3, 4, or 5 cyclically + for j := 0; j < numChildren; j++ { + childUID := childBase + childIdx + attrValue := (childIdx%2 == 0) // alternating true/false + fmt.Fprintf(&sb, "<0x%X> <0x%X> .\n", uid, childUID) + fmt.Fprintf(&sb, "<0x%X> \"%t\" .\n", childUID, attrValue) + childIdx++ + } + } } return sb.String() } // TestCascadeWithOrderAndLargeDataSet verifies cascade behavior with order // when there are large numbers of nodes (> 1000). It tests: -// - Default limit behavior (no explicit first): 900, 1000, 1100, 3000 nodes +// - Default limit behavior (no explicit first): 900, 1000, 1100, 3000 parents // expecting 900, 1000, 1000, 1000 respectively // - Explicit first greater than available nodes: ensures all nodes are returned +// - Order desc with cascade on parent-child relationships +// +// Schema: OrderedCascadeParent has a pseudo-random parent_number (1-10000) and +// an array of OrderedCascadeChild nodes (only even-indexed parents have children). +// Each child has a boolean child_attr attribute. func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { - // Schema for cascade test type + // Schema for cascade test with parent-child relationship schema := ` - type CascadeTestItem { - item_name: string - item_detail: string + type OrderedCascadeParent { + parent_number: int + children: [uid] } - cascade_test_name: string @index(exact) . - cascade_test_detail: string @index(exact) . + + type OrderedCascadeChild { + child_attr: bool + } + + cascade_parent_number: int @index(int) . + cascade_child_attr: bool . + cascade_children: [uid] . ` t.Run("DefaultLimit", func(t *testing.T) { @@ -1951,13 +1972,11 @@ func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { numNodes int expectedCount int uidBase uint64 - deleteBase uint64 - deleteCount int }{ - {"900Nodes_DefaultLimit", 900, 900, 0x10000, 0x10000, 900}, - {"1000Nodes_DefaultLimit", 1000, 1000, 0x20000, 0x20000, 1000}, - {"1100Nodes_DefaultLimit", 1100, 1000, 0x30000, 0x30000, 1100}, - {"3000Nodes_DefaultLimit", 3000, 1000, 0x40000, 0x40000, 3000}, + {"900Nodes_DefaultLimit", 900, 900, 0x10000}, + {"1000Nodes_DefaultLimit", 1000, 1000, 0x20000}, + {"1100Nodes_DefaultLimit", 1100, 1000, 0x30000}, + {"3000Nodes_DefaultLimit", 3000, 1000, 0x40000}, } for _, tt := range tests { @@ -1968,20 +1987,23 @@ func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { // Set up schema setSchema(schema) t.Cleanup(func() { - dropPredicate("cascade_test_name") - dropPredicate("cascade_test_detail") + dropPredicate("cascade_parent_number") + dropPredicate("cascade_child_attr") + dropPredicate("cascade_children") setSchema(testSchema) }) - // Insert data in unordered fashion + // Insert data with pseudo-random numbers and parent-child relationships triples := generateCascadeTestTriples(tt.numNodes, tt.uidBase) require.NoError(t, addTriplesToCluster(triples)) - // Query with orderasc and @cascade, no explicit first + // Query with orderasc on parent_number and @cascade on children query := `{ - me(func: type(CascadeTestItem), orderasc: cascade_test_name) @cascade { - item_name: cascade_test_name - item_detail: cascade_test_detail + me(func: type(OrderedCascadeParent), orderasc: cascade_parent_number) @cascade { + parent_number + children { + child_attr + } } }` @@ -1989,19 +2011,21 @@ func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { var response struct { Data struct { Me []struct { - ItemName string `json:"item_name"` - ItemDetail string `json:"item_detail"` + ParentNumber int `json:"parent_number"` + Children []struct { + ChildAttr bool `json:"child_attr"` + } `json:"children"` } `json:"me"` } `json:"data"` } require.NoError(t, json.Unmarshal([]byte(js), &response)) require.Len(t, response.Data.Me, tt.expectedCount, - "Expected %d results for %d nodes with default limit", tt.expectedCount, tt.numNodes) + "Expected %d results for %d parent nodes with default limit", tt.expectedCount, tt.numNodes) - // Verify results are in ascending order + // Verify results are in ascending order by parent_number for i := 1; i < len(response.Data.Me); i++ { - require.GreaterOrEqual(t, response.Data.Me[i].ItemName, response.Data.Me[i-1].ItemName, - "Results should be in ascending order at index %d", i) + require.GreaterOrEqual(t, response.Data.Me[i].ParentNumber, response.Data.Me[i-1].ParentNumber, + "Results should be in ascending order by parent_number at index %d", i) } }) } @@ -2028,20 +2052,23 @@ func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { // Set up schema setSchema(schema) t.Cleanup(func() { - dropPredicate("cascade_test_name") - dropPredicate("cascade_test_detail") + dropPredicate("cascade_parent_number") + dropPredicate("cascade_child_attr") + dropPredicate("cascade_children") setSchema(testSchema) }) - // Insert data in unordered fashion + // Insert data with pseudo-random numbers and parent-child relationships triples := generateCascadeTestTriples(tt.numNodes, tt.uidBase) require.NoError(t, addTriplesToCluster(triples)) // Query with explicit first > available, orderasc and @cascade query := fmt.Sprintf(`{ - me(func: type(CascadeTestItem), first: %d, orderasc: cascade_test_name) @cascade { - item_name: cascade_test_name - item_detail: cascade_test_detail + me(func: type(OrderedCascadeParent), first: %d, orderasc: cascade_parent_number) @cascade { + parent_number + children { + child_attr + } } }`, tt.first) @@ -2049,19 +2076,21 @@ func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { var response struct { Data struct { Me []struct { - ItemName string `json:"item_name"` - ItemDetail string `json:"item_detail"` + ParentNumber int `json:"parent_number"` + Children []struct { + ChildAttr bool `json:"child_attr"` + } `json:"children"` } `json:"me"` } `json:"data"` } require.NoError(t, json.Unmarshal([]byte(js), &response)) require.Len(t, response.Data.Me, tt.expectedCount, - "Expected %d results for %d nodes with first:%d", tt.expectedCount, tt.numNodes, tt.first) + "Expected %d results for %d parent nodes with first:%d", tt.expectedCount, tt.numNodes, tt.first) - // Verify results are in ascending order + // Verify results are in ascending order by parent_number for i := 1; i < len(response.Data.Me); i++ { - require.GreaterOrEqual(t, response.Data.Me[i].ItemName, response.Data.Me[i-1].ItemName, - "Results should be in ascending order at index %d", i) + require.GreaterOrEqual(t, response.Data.Me[i].ParentNumber, response.Data.Me[i-1].ParentNumber, + "Results should be in ascending order by parent_number at index %d", i) } }) } @@ -2087,20 +2116,23 @@ func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { // Set up schema setSchema(schema) t.Cleanup(func() { - dropPredicate("cascade_test_name") - dropPredicate("cascade_test_detail") + dropPredicate("cascade_parent_number") + dropPredicate("cascade_child_attr") + dropPredicate("cascade_children") setSchema(testSchema) }) - // Insert data in unordered fashion + // Insert data with pseudo-random numbers and parent-child relationships triples := generateCascadeTestTriples(tt.numNodes, tt.uidBase) require.NoError(t, addTriplesToCluster(triples)) - // Query with orderdesc, explicit first, and @cascade + // Query with orderdesc on parent_number, explicit first, and @cascade query := fmt.Sprintf(`{ - me(func: type(CascadeTestItem), first: %d, orderdesc: cascade_test_name) @cascade { - item_name: cascade_test_name - item_detail: cascade_test_detail + me(func: type(OrderedCascadeParent), first: %d, orderdesc: cascade_parent_number) @cascade { + parent_number + children { + child_attr + } } }`, tt.first) @@ -2108,19 +2140,21 @@ func TestCascadeWithOrderAndLargeDataSet(t *testing.T) { var response struct { Data struct { Me []struct { - ItemName string `json:"item_name"` - ItemDetail string `json:"item_detail"` + ParentNumber int `json:"parent_number"` + Children []struct { + ChildAttr bool `json:"child_attr"` + } `json:"children"` } `json:"me"` } `json:"data"` } require.NoError(t, json.Unmarshal([]byte(js), &response)) require.Len(t, response.Data.Me, tt.expectedCount, - "Expected %d results for %d nodes with orderdesc first:%d", tt.expectedCount, tt.numNodes, tt.first) + "Expected %d results for %d parent nodes with orderdesc first:%d", tt.expectedCount, tt.numNodes, tt.first) - // Verify results are in descending order + // Verify results are in descending order by parent_number for i := 1; i < len(response.Data.Me); i++ { - require.LessOrEqual(t, response.Data.Me[i].ItemName, response.Data.Me[i-1].ItemName, - "Results should be in descending order at index %d", i) + require.LessOrEqual(t, response.Data.Me[i].ParentNumber, response.Data.Me[i-1].ParentNumber, + "Results should be in descending order by parent_number at index %d", i) } }) }