Skip to content

Commit d4856f4

Browse files
h3n4lclaude
andauthored
feat: add WithMaxRows option to cap find() and countDocuments() results (#17)
Add functional options pattern to Execute() with WithMaxRows(n) that caps find() and countDocuments() results at the driver level. - Add ExecuteOption type and WithMaxRows() function - Apply min(queryLimit, maxRows) in executeFind and executeCountDocuments - Aggregate operations intentionally not affected - Add comprehensive tests for all limit scenarios Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 88ce61b commit d4856f4

6 files changed

Lines changed: 205 additions & 13 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ func main() {
5353
}
5454
```
5555

56+
## Execute Options
57+
58+
The `Execute` method accepts optional configuration:
59+
60+
### WithMaxRows
61+
62+
Limit the maximum number of rows returned by `find()` and `countDocuments()` operations. This is useful to prevent excessive memory usage or network traffic from unbounded queries.
63+
64+
```go
65+
// Cap results at 1000 rows
66+
result, err := gc.Execute(ctx, "mydb", `db.users.find()`, gomongo.WithMaxRows(1000))
67+
```
68+
69+
**Behavior:**
70+
- If the query includes `.limit(N)`, the effective limit is `min(N, maxRows)`
71+
- Query limit 50 + MaxRows 1000 → returns up to 50 rows
72+
- Query limit 5000 + MaxRows 1000 → returns up to 1000 rows
73+
- `aggregate()` operations are not affected (use `$limit` stage instead)
74+
5675
## Output Format
5776

5877
Results are returned in Extended JSON (Relaxed) format:

client.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,29 @@ type Result struct {
2323
Statement string
2424
}
2525

26+
// executeConfig holds configuration for Execute.
27+
type executeConfig struct {
28+
maxRows *int64
29+
}
30+
31+
// ExecuteOption configures Execute behavior.
32+
type ExecuteOption func(*executeConfig)
33+
34+
// WithMaxRows limits the maximum number of rows returned by find() and
35+
// countDocuments() operations. If the query includes .limit(N), the effective
36+
// limit is min(N, maxRows). Aggregate operations are not affected.
37+
func WithMaxRows(n int64) ExecuteOption {
38+
return func(c *executeConfig) {
39+
c.maxRows = &n
40+
}
41+
}
42+
2643
// Execute parses and executes a MongoDB shell statement.
2744
// Returns results as Extended JSON (Relaxed) strings.
28-
func (c *Client) Execute(ctx context.Context, database, statement string) (*Result, error) {
29-
return execute(ctx, c.client, database, statement)
45+
func (c *Client) Execute(ctx context.Context, database, statement string, opts ...ExecuteOption) (*Result, error) {
46+
cfg := &executeConfig{}
47+
for _, opt := range opts {
48+
opt(cfg)
49+
}
50+
return execute(ctx, c.client, database, statement, cfg.maxRows)
3051
}

collection_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2153,3 +2153,132 @@ func TestCursorMinMaxCombined(t *testing.T) {
21532153
require.NoError(t, err)
21542154
require.Equal(t, 2, result.RowCount)
21552155
}
2156+
2157+
func TestWithMaxRowsCapsResults(t *testing.T) {
2158+
client := testutil.GetClient(t)
2159+
dbName := "testdb_maxrows_cap"
2160+
defer testutil.CleanupDatabase(t, client, dbName)
2161+
2162+
ctx := context.Background()
2163+
2164+
// Insert 20 documents
2165+
collection := client.Database(dbName).Collection("items")
2166+
docs := make([]any, 20)
2167+
for i := 0; i < 20; i++ {
2168+
docs[i] = bson.M{"index": i}
2169+
}
2170+
_, err := collection.InsertMany(ctx, docs)
2171+
require.NoError(t, err)
2172+
2173+
gc := gomongo.NewClient(client)
2174+
2175+
// Without MaxRows - returns all 20
2176+
result, err := gc.Execute(ctx, dbName, "db.items.find()")
2177+
require.NoError(t, err)
2178+
require.Equal(t, 20, result.RowCount)
2179+
2180+
// With MaxRows(10) - caps at 10
2181+
result, err = gc.Execute(ctx, dbName, "db.items.find()", gomongo.WithMaxRows(10))
2182+
require.NoError(t, err)
2183+
require.Equal(t, 10, result.RowCount)
2184+
}
2185+
2186+
func TestWithMaxRowsQueryLimitTakesPrecedence(t *testing.T) {
2187+
client := testutil.GetClient(t)
2188+
dbName := "testdb_maxrows_query_limit"
2189+
defer testutil.CleanupDatabase(t, client, dbName)
2190+
2191+
ctx := context.Background()
2192+
2193+
// Insert 20 documents
2194+
collection := client.Database(dbName).Collection("items")
2195+
docs := make([]any, 20)
2196+
for i := 0; i < 20; i++ {
2197+
docs[i] = bson.M{"index": i}
2198+
}
2199+
_, err := collection.InsertMany(ctx, docs)
2200+
require.NoError(t, err)
2201+
2202+
gc := gomongo.NewClient(client)
2203+
2204+
// Query limit(5) is smaller than MaxRows(100) - should return 5
2205+
result, err := gc.Execute(ctx, dbName, "db.items.find().limit(5)", gomongo.WithMaxRows(100))
2206+
require.NoError(t, err)
2207+
require.Equal(t, 5, result.RowCount)
2208+
}
2209+
2210+
func TestWithMaxRowsTakesPrecedenceOverLargerLimit(t *testing.T) {
2211+
client := testutil.GetClient(t)
2212+
dbName := "testdb_maxrows_precedence"
2213+
defer testutil.CleanupDatabase(t, client, dbName)
2214+
2215+
ctx := context.Background()
2216+
2217+
// Insert 20 documents
2218+
collection := client.Database(dbName).Collection("items")
2219+
docs := make([]any, 20)
2220+
for i := 0; i < 20; i++ {
2221+
docs[i] = bson.M{"index": i}
2222+
}
2223+
_, err := collection.InsertMany(ctx, docs)
2224+
require.NoError(t, err)
2225+
2226+
gc := gomongo.NewClient(client)
2227+
2228+
// Query limit(100) is larger than MaxRows(5) - should return 5
2229+
result, err := gc.Execute(ctx, dbName, "db.items.find().limit(100)", gomongo.WithMaxRows(5))
2230+
require.NoError(t, err)
2231+
require.Equal(t, 5, result.RowCount)
2232+
}
2233+
2234+
func TestExecuteBackwardCompatibility(t *testing.T) {
2235+
client := testutil.GetClient(t)
2236+
dbName := "testdb_backward_compat"
2237+
defer testutil.CleanupDatabase(t, client, dbName)
2238+
2239+
ctx := context.Background()
2240+
2241+
collection := client.Database(dbName).Collection("items")
2242+
_, err := collection.InsertMany(ctx, []any{
2243+
bson.M{"name": "a"},
2244+
bson.M{"name": "b"},
2245+
bson.M{"name": "c"},
2246+
})
2247+
require.NoError(t, err)
2248+
2249+
gc := gomongo.NewClient(client)
2250+
2251+
// Execute without options should work (backward compatible)
2252+
result, err := gc.Execute(ctx, dbName, "db.items.find()")
2253+
require.NoError(t, err)
2254+
require.Equal(t, 3, result.RowCount)
2255+
}
2256+
2257+
func TestCountDocumentsWithMaxRows(t *testing.T) {
2258+
client := testutil.GetClient(t)
2259+
dbName := "testdb_count_maxrows"
2260+
defer testutil.CleanupDatabase(t, client, dbName)
2261+
2262+
ctx := context.Background()
2263+
2264+
// Insert 100 documents
2265+
collection := client.Database(dbName).Collection("items")
2266+
docs := make([]any, 100)
2267+
for i := 0; i < 100; i++ {
2268+
docs[i] = bson.M{"index": i}
2269+
}
2270+
_, err := collection.InsertMany(ctx, docs)
2271+
require.NoError(t, err)
2272+
2273+
gc := gomongo.NewClient(client)
2274+
2275+
// Without MaxRows - counts all 100
2276+
result, err := gc.Execute(ctx, dbName, "db.items.countDocuments()")
2277+
require.NoError(t, err)
2278+
require.Equal(t, "100", result.Rows[0])
2279+
2280+
// With MaxRows(50) - counts up to 50
2281+
result, err = gc.Execute(ctx, dbName, "db.items.countDocuments()", gomongo.WithMaxRows(50))
2282+
require.NoError(t, err)
2283+
require.Equal(t, "50", result.Rows[0])
2284+
}

executor.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
// execute parses and executes a MongoDB shell statement.
12-
func execute(ctx context.Context, client *mongo.Client, database, statement string) (*Result, error) {
12+
func execute(ctx context.Context, client *mongo.Client, database, statement string, maxRows *int64) (*Result, error) {
1313
op, err := translator.Parse(statement)
1414
if err != nil {
1515
// Convert internal errors to public errors
@@ -33,7 +33,7 @@ func execute(ctx context.Context, client *mongo.Client, database, statement stri
3333
}
3434
}
3535

36-
result, err := executor.Execute(ctx, client, database, op, statement)
36+
result, err := executor.Execute(ctx, client, database, op, statement, maxRows)
3737
if err != nil {
3838
return nil, err
3939
}

internal/executor/collection.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,27 @@ import (
1212
"go.mongodb.org/mongo-driver/v2/mongo/options"
1313
)
1414

15+
// computeEffectiveLimit returns the minimum of opLimit and maxRows.
16+
// Returns nil if both are nil.
17+
func computeEffectiveLimit(opLimit, maxRows *int64) *int64 {
18+
if opLimit == nil && maxRows == nil {
19+
return nil
20+
}
21+
if opLimit == nil {
22+
return maxRows
23+
}
24+
if maxRows == nil {
25+
return opLimit
26+
}
27+
// Both are non-nil, return the minimum
28+
if *opLimit < *maxRows {
29+
return opLimit
30+
}
31+
return maxRows
32+
}
33+
1534
// executeFind executes a find operation.
16-
func executeFind(ctx context.Context, client *mongo.Client, database string, op *translator.Operation) (*Result, error) {
35+
func executeFind(ctx context.Context, client *mongo.Client, database string, op *translator.Operation, maxRows *int64) (*Result, error) {
1736
collection := client.Database(database).Collection(op.Collection)
1837

1938
filter := op.Filter
@@ -25,8 +44,10 @@ func executeFind(ctx context.Context, client *mongo.Client, database string, op
2544
if op.Sort != nil {
2645
opts.SetSort(op.Sort)
2746
}
28-
if op.Limit != nil {
29-
opts.SetLimit(*op.Limit)
47+
// Compute effective limit: min(op.Limit, maxRows)
48+
effectiveLimit := computeEffectiveLimit(op.Limit, maxRows)
49+
if effectiveLimit != nil {
50+
opts.SetLimit(*effectiveLimit)
3051
}
3152
if op.Skip != nil {
3253
opts.SetSkip(*op.Skip)
@@ -230,7 +251,7 @@ func executeGetIndexes(ctx context.Context, client *mongo.Client, database strin
230251
}
231252

232253
// executeCountDocuments executes a db.collection.countDocuments() command.
233-
func executeCountDocuments(ctx context.Context, client *mongo.Client, database string, op *translator.Operation) (*Result, error) {
254+
func executeCountDocuments(ctx context.Context, client *mongo.Client, database string, op *translator.Operation, maxRows *int64) (*Result, error) {
234255
collection := client.Database(database).Collection(op.Collection)
235256

236257
filter := op.Filter
@@ -242,8 +263,10 @@ func executeCountDocuments(ctx context.Context, client *mongo.Client, database s
242263
if op.Hint != nil {
243264
opts.SetHint(op.Hint)
244265
}
245-
if op.Limit != nil {
246-
opts.SetLimit(*op.Limit)
266+
// Compute effective limit: min(op.Limit, maxRows)
267+
effectiveLimit := computeEffectiveLimit(op.Limit, maxRows)
268+
if effectiveLimit != nil {
269+
opts.SetLimit(*effectiveLimit)
247270
}
248271
if op.Skip != nil {
249272
opts.SetSkip(*op.Skip)

internal/executor/executor.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ type Result struct {
1616
}
1717

1818
// Execute executes a parsed operation against MongoDB.
19-
func Execute(ctx context.Context, client *mongo.Client, database string, op *translator.Operation, statement string) (*Result, error) {
19+
func Execute(ctx context.Context, client *mongo.Client, database string, op *translator.Operation, statement string, maxRows *int64) (*Result, error) {
2020
switch op.OpType {
2121
case translator.OpFind:
22-
return executeFind(ctx, client, database, op)
22+
return executeFind(ctx, client, database, op, maxRows)
2323
case translator.OpFindOne:
2424
return executeFindOne(ctx, client, database, op)
2525
case translator.OpAggregate:
@@ -35,7 +35,7 @@ func Execute(ctx context.Context, client *mongo.Client, database string, op *tra
3535
case translator.OpGetIndexes:
3636
return executeGetIndexes(ctx, client, database, op)
3737
case translator.OpCountDocuments:
38-
return executeCountDocuments(ctx, client, database, op)
38+
return executeCountDocuments(ctx, client, database, op, maxRows)
3939
case translator.OpEstimatedDocumentCount:
4040
return executeEstimatedDocumentCount(ctx, client, database, op)
4141
case translator.OpDistinct:

0 commit comments

Comments
 (0)