Skip to content

Commit 008cbba

Browse files
Merge pull request #3 from beekeeper-studio/feat/return-cursor
add ability to return a cursor instead of the results
2 parents 70855f9 + 83aab04 commit 008cbba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+853
-7615
lines changed

docs/usage/core-concepts.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,16 @@ The main class that ties everything together. It provides a simple API to execut
5050

5151
```typescript
5252
const queryLeaf = new QueryLeaf(mongoClient, 'mydatabase');
53+
54+
// Execute a query and get all results as an array
5355
const results = await queryLeaf.execute('SELECT * FROM users');
56+
57+
// Or use a cursor for more control and memory efficiency
58+
const cursor = await queryLeaf.executeCursor('SELECT * FROM users');
59+
await cursor.forEach(user => {
60+
console.log(`Processing user: ${user.name}`);
61+
});
62+
await cursor.close();
5463
```
5564

5665
### DummyQueryLeaf
@@ -61,6 +70,13 @@ A special implementation of QueryLeaf that doesn't execute real MongoDB operatio
6170
const dummyLeaf = new DummyQueryLeaf('mydatabase');
6271
await dummyLeaf.execute('SELECT * FROM users');
6372
// [DUMMY MongoDB] FIND in mydatabase.users with filter: {}
73+
74+
// Cursor support works with DummyQueryLeaf too
75+
const cursor = await dummyLeaf.executeCursor('SELECT * FROM users');
76+
await cursor.forEach(user => {
77+
// Process mock data
78+
});
79+
await cursor.close();
6480
```
6581

6682
## Relationship Between SQL and MongoDB Concepts
@@ -101,14 +117,31 @@ QueryLeaf uses specific naming conventions for mapping SQL to MongoDB:
101117

102118
## Execution Flow
103119

120+
QueryLeaf supports two main execution methods:
121+
122+
### Standard Execution
123+
104124
When you call `queryLeaf.execute(sqlQuery)`, the following happens:
105125

106126
1. The SQL query is parsed into an AST
107127
2. The AST is compiled into MongoDB commands
108128
3. The commands are executed against the MongoDB database
109-
4. The results are returned
129+
4. All results are loaded into memory and returned as an array
130+
131+
This is simple to use but can be memory-intensive for large result sets.
132+
133+
### Cursor Execution
134+
135+
When you call `queryLeaf.executeCursor(sqlQuery)`, the following happens:
136+
137+
1. The SQL query is parsed into an AST
138+
2. The AST is compiled into MongoDB commands
139+
3. For SELECT queries, a MongoDB cursor is returned instead of loading all results
140+
4. You control how and when results are processed (streaming/batching)
141+
142+
This approach is more memory-efficient for large datasets and gives you more control.
110143

111-
If any step fails, an error is thrown with details about what went wrong.
144+
If any step fails in either approach, an error is thrown with details about what went wrong.
112145

113146
## Extending QueryLeaf
114147

docs/usage/examples.md

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,37 @@ const usersInNY = await queryLeaf.execute(`
7373
`);
7474
```
7575

76+
### Using Cursors for Large Result Sets
77+
78+
```typescript
79+
// Use cursor for memory-efficient processing of large result sets
80+
const cursor = await queryLeaf.executeCursor(`
81+
SELECT _id, customer, total, items
82+
FROM orders
83+
WHERE status = 'completed'
84+
`);
85+
86+
try {
87+
// Process one document at a time without loading everything in memory
88+
let totalRevenue = 0;
89+
await cursor.forEach(order => {
90+
// Process each order individually
91+
totalRevenue += order.total;
92+
93+
// Access and process nested data
94+
order.items.forEach(item => {
95+
// Process each item in the order
96+
console.log(`Order ${order._id}: ${item.quantity}x ${item.name}`);
97+
});
98+
});
99+
100+
console.log(`Total revenue: $${totalRevenue}`);
101+
} finally {
102+
// Always close the cursor when done
103+
await cursor.close();
104+
}
105+
```
106+
76107
### Array Element Access
77108

78109
```typescript
@@ -169,11 +200,11 @@ async function getUserDashboardData(userId) {
169200
}
170201
```
171202

172-
### Product Catalog with Filtering
203+
### Product Catalog with Filtering and Cursor-Based Pagination
173204

174205
```typescript
175-
// Product catalog with filtering
176-
async function getProductCatalog(filters = {}) {
206+
// Product catalog with filtering and cursor-based pagination
207+
async function getProductCatalog(filters = {}, useCursor = false) {
177208
const client = new MongoClient('mongodb://localhost:27017');
178209
await client.connect();
179210

@@ -217,9 +248,52 @@ async function getProductCatalog(filters = {}) {
217248
query += ` LIMIT ${filters.limit}`;
218249
}
219250

220-
// Execute query
221-
return await queryLeaf.execute(query);
251+
if (filters.offset) {
252+
query += ` OFFSET ${filters.offset}`;
253+
}
254+
255+
// Execute query with or without cursor based on preference
256+
if (useCursor) {
257+
// Return a cursor for client-side pagination or streaming
258+
return await queryLeaf.executeCursor(query);
259+
} else {
260+
// Return all results at once (traditional approach)
261+
return await queryLeaf.execute(query);
262+
}
263+
} catch (error) {
264+
console.error('Error fetching product catalog:', error);
265+
throw error;
266+
} finally {
267+
if (!useCursor) {
268+
// If we returned a cursor, the caller is responsible for closing the client
269+
// after they are done with the cursor
270+
await client.close();
271+
}
272+
}
273+
}
274+
275+
// Example of using the product catalog with cursor
276+
async function streamProductCatalog() {
277+
const client = new MongoClient('mongodb://localhost:27017');
278+
await client.connect();
279+
280+
let cursor = null;
281+
try {
282+
const queryLeaf = new QueryLeaf(client, 'store');
283+
// Get a cursor for large result set
284+
cursor = await getProductCatalog({ category: 'Electronics', inStock: true }, true);
285+
286+
// Stream products to client one by one
287+
console.log('Streaming products:');
288+
await cursor.forEach(product => {
289+
console.log(`- ${product.name}: $${product.price} (${product.stock} in stock)`);
290+
});
291+
} catch (error) {
292+
console.error('Error:', error);
222293
} finally {
294+
// Close cursor if we have one
295+
if (cursor) await cursor.close();
296+
// Close client connection
223297
await client.close();
224298
}
225299
}

docs/usage/mongodb-client.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,25 @@ await mongoClient.connect();
4646
Once you have a MongoDB client, you can create a QueryLeaf instance:
4747

4848
```typescript
49-
import { QueryLeaf } from 'queryleaf';
49+
import { QueryLeaf } from '@queryleaf/lib';
5050

5151
// Create a QueryLeaf instance with your MongoDB client
5252
const queryLeaf = new QueryLeaf(mongoClient, 'mydatabase');
5353

54-
// Now you can execute SQL queries
54+
// Execute SQL queries and get all results at once
5555
const results = await queryLeaf.execute('SELECT * FROM users');
56+
57+
// For large result sets, use cursor execution for better memory efficiency
58+
const cursor = await queryLeaf.executeCursor('SELECT * FROM users');
59+
try {
60+
// Process results one at a time
61+
await cursor.forEach(user => {
62+
console.log(`User: ${user.name}`);
63+
});
64+
} finally {
65+
// Always close the cursor when done
66+
await cursor.close();
67+
}
5668
```
5769

5870
## Connection Management
@@ -80,7 +92,7 @@ async function main() {
8092
// Create QueryLeaf instance
8193
const queryLeaf = new QueryLeaf(client, 'mydatabase');
8294

83-
// Execute queries
95+
// Execute queries - get all results at once
8496
const users = await queryLeaf.execute('SELECT * FROM users LIMIT 10');
8597
console.log(`Found ${users.length} users`);
8698

@@ -90,6 +102,23 @@ async function main() {
90102
);
91103
console.log(`Found ${products.length} electronic products`);
92104

105+
// For large result sets, use cursor execution
106+
const ordersCursor = await queryLeaf.executeCursor(
107+
'SELECT * FROM orders WHERE total > 1000'
108+
);
109+
try {
110+
// Process results in a memory-efficient way
111+
let count = 0;
112+
await ordersCursor.forEach(order => {
113+
console.log(`Processing order #${order.orderId}`);
114+
count++;
115+
});
116+
console.log(`Processed ${count} high-value orders`);
117+
} finally {
118+
// Always close the cursor when done
119+
await ordersCursor.close();
120+
}
121+
93122
} catch (error) {
94123
console.error('Error:', error);
95124
} finally {

packages/lib/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Array element access (e.g., `items[0].name`)
2222
- GROUP BY with aggregation functions (COUNT, SUM, AVG, MIN, MAX)
2323
- JOINs between collections
24+
- Direct MongoDB cursor access for fine-grained result processing and memory efficiency
2425

2526
## Installation
2627

@@ -47,6 +48,13 @@ const queryLeaf = new QueryLeaf(mongoClient, 'mydatabase');
4748
const results = await queryLeaf.execute('SELECT * FROM users WHERE age > 21');
4849
console.log(results);
4950

51+
// Get a MongoDB cursor for more control over result processing and memory efficiency
52+
const cursor = await queryLeaf.executeCursor('SELECT * FROM users WHERE age > 30');
53+
await cursor.forEach((doc) => {
54+
console.log(`User: ${doc.name}`);
55+
});
56+
await cursor.close();
57+
5058
// When you're done, close your MongoDB client
5159
await mongoClient.close();
5260
```
@@ -64,6 +72,13 @@ const queryLeaf = new DummyQueryLeaf('mydatabase');
6472
// Operations will be logged to console but not executed
6573
await queryLeaf.execute('SELECT * FROM users WHERE age > 21');
6674
// [DUMMY MongoDB] FIND in mydatabase.users with filter: { "age": { "$gt": 21 } }
75+
76+
// You can also use cursor functionality with DummyQueryLeaf
77+
const cursor = await queryLeaf.executeCursor('SELECT * FROM users LIMIT 10');
78+
await cursor.forEach((doc) => {
79+
// Process each document
80+
});
81+
await cursor.close();
6782
```
6883

6984
## SQL Query Examples
@@ -85,6 +100,42 @@ SELECT status, COUNT(*) as count FROM orders GROUP BY status
85100
SELECT u.name, o.total FROM users u JOIN orders o ON u._id = o.userId
86101
```
87102

103+
## Working with Cursors
104+
105+
When working with large result sets, using MongoDB cursors directly can be more memory-efficient and gives you more control over result processing:
106+
107+
```typescript
108+
// Get a cursor for a SELECT query
109+
const cursor = await queryLeaf.executeCursor('SELECT * FROM products WHERE price > 100');
110+
111+
// Option 1: Convert to array (loads all results into memory)
112+
const results = await cursor.toArray();
113+
console.log(`Found ${results.length} products`);
114+
115+
// Option 2: Iterate with forEach (memory efficient)
116+
await cursor.forEach(product => {
117+
console.log(`Processing ${product.name}...`);
118+
});
119+
120+
// Option 3: Manual iteration with next/hasNext (most control)
121+
while (await cursor.hasNext()) {
122+
const product = await cursor.next();
123+
// Process each product individually
124+
console.log(`Product: ${product.name}, $${product.price}`);
125+
}
126+
127+
// Always close the cursor when done
128+
await cursor.close();
129+
```
130+
131+
Features:
132+
- Returns MongoDB `FindCursor` for normal queries and `AggregationCursor` for aggregations
133+
- Supports all cursor methods like `forEach()`, `toArray()`, `next()`, `hasNext()`
134+
- Efficiently handles large result sets with MongoDB's batching system
135+
- Works with all advanced QueryLeaf features (filtering, sorting, aggregations, etc.)
136+
- Only available for read operations (SELECT queries)
137+
```
138+
88139
## Links
89140
90141
- [Website](https://queryleaf.com)

0 commit comments

Comments
 (0)