Skip to content

Commit 7f65a06

Browse files
h3n4lclaude
andauthored
feat(mongodb): implement milestone 1 read operations (#52)
Add support for Milestone 1 read operations, utility commands, and aggregation: Collection methods: - countDocuments(filter?, options?) - estimatedDocumentCount(options?) - distinct(field, query?, options?) - aggregate(pipeline, options?) - getIndexes() Cursor modifiers: - count() Improvements: - Add 'new' keyword error handling with helpful message via NotifyErrorListeners - Reorganize test files by collection method (collection-*.js) - Remove redundant test files covered by new structure Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 76308b5 commit 7f65a06

19 files changed

Lines changed: 3196 additions & 1538 deletions

mongodb/MongoShellLexer.g4

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,22 @@ NUMBER_DECIMAL: 'NumberDecimal';
3434
TIMESTAMP: 'Timestamp';
3535
REG_EXP: 'RegExp';
3636

37-
// Cursor modifiers (methods)
37+
// Collection methods
3838
FIND: 'find';
3939
FIND_ONE: 'findOne';
40+
COUNT_DOCUMENTS: 'countDocuments';
41+
ESTIMATED_DOCUMENT_COUNT: 'estimatedDocumentCount';
42+
DISTINCT: 'distinct';
43+
AGGREGATE: 'aggregate';
44+
GET_INDEXES: 'getIndexes';
45+
46+
// Cursor modifiers (methods)
4047
SORT: 'sort';
4148
LIMIT: 'limit';
4249
SKIP_: 'skip';
4350
PROJECTION: 'projection';
4451
PROJECT: 'project';
52+
COUNT: 'count';
4553

4654
// Punctuation
4755
LPAREN: '(';

mongodb/MongoShellParser.g4

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
* MongoDB Shell (mongosh) Parser Grammar
33
* For use with ANTLR 4
44
*
5-
* Supports MVP read operations:
5+
* Milestone 1: Read Operations + Utility + Aggregation
66
* - Shell commands: show dbs, show databases, show collections
7-
* - Database statements: db.collection.method(...)
8-
* - Read methods: find(), findOne()
9-
* - Cursor modifiers: sort(), limit(), skip(), projection(), project()
10-
* - Helper functions: ObjectId(), ISODate(), UUID(), Long(), etc.
7+
* - Utility: db.getCollectionNames(), db.getCollectionInfos()
8+
* - Collection info: db.collection.getIndexes()
9+
* - Read methods: find(), findOne(), countDocuments(), estimatedDocumentCount(), distinct()
10+
* - Aggregation: db.collection.aggregate()
11+
* - Cursor modifiers: sort(), limit(), skip(), count(), projection(), project()
12+
* - Object constructors: ObjectId(), ISODate(), UUID(), NumberInt(), NumberLong(), NumberDecimal()
1113
* - Document syntax with unquoted keys and trailing commas
1214
*/
1315

@@ -55,9 +57,15 @@ methodChain
5557
methodCall
5658
: findMethod
5759
| findOneMethod
60+
| countDocumentsMethod
61+
| estimatedDocumentCountMethod
62+
| distinctMethod
63+
| aggregateMethod
64+
| getIndexesMethod
5865
| sortMethod
5966
| limitMethod
6067
| skipMethod
68+
| countMethod
6169
| projectionMethod
6270
| genericMethod
6371
;
@@ -71,6 +79,31 @@ findOneMethod
7179
: FIND_ONE LPAREN argument? RPAREN
7280
;
7381

82+
// countDocuments(filter?, options?)
83+
countDocumentsMethod
84+
: COUNT_DOCUMENTS LPAREN arguments? RPAREN
85+
;
86+
87+
// estimatedDocumentCount(options?)
88+
estimatedDocumentCountMethod
89+
: ESTIMATED_DOCUMENT_COUNT LPAREN argument? RPAREN
90+
;
91+
92+
// distinct(field, query?, options?)
93+
distinctMethod
94+
: DISTINCT LPAREN arguments RPAREN
95+
;
96+
97+
// aggregate(pipeline, options?)
98+
aggregateMethod
99+
: AGGREGATE LPAREN arguments RPAREN
100+
;
101+
102+
// getIndexes()
103+
getIndexesMethod
104+
: GET_INDEXES LPAREN RPAREN
105+
;
106+
74107
sortMethod
75108
: SORT LPAREN document RPAREN
76109
;
@@ -83,6 +116,11 @@ skipMethod
83116
: SKIP_ LPAREN NUMBER RPAREN
84117
;
85118

119+
// cursor.count() - returns count of documents matching the query
120+
countMethod
121+
: COUNT LPAREN RPAREN
122+
;
123+
86124
projectionMethod
87125
: (PROJECTION | PROJECT) LPAREN document RPAREN
88126
;
@@ -125,6 +163,14 @@ value
125163
| REGEX_LITERAL # regexLiteralValue
126164
| regExpConstructor # regexpConstructorValue
127165
| literal # literalValue
166+
| newKeywordError # newKeywordValue
167+
;
168+
169+
// Catch 'new' keyword usage and provide helpful error message
170+
newKeywordError
171+
: NEW (OBJECT_ID | ISO_DATE | DATE | UUID | LONG | NUMBER_LONG | INT32 | NUMBER_INT | DOUBLE | DECIMAL128 | NUMBER_DECIMAL | TIMESTAMP | REG_EXP)
172+
{ p.NotifyErrorListeners("'new' keyword is not supported. Use ObjectId(), ISODate(), UUID(), etc. directly without 'new'", nil, nil) }
173+
LPAREN arguments? RPAREN
128174
;
129175

130176
// Array: [ value, ... ] with optional trailing comma
@@ -149,19 +195,16 @@ helperFunction
149195
// ObjectId("hex") or ObjectId()
150196
objectIdHelper
151197
: OBJECT_ID LPAREN stringLiteral? RPAREN
152-
| NEW OBJECT_ID { p.NotifyErrorListeners("'new' keyword is not supported. Use ObjectId() directly", nil, nil) }
153198
;
154199

155200
// ISODate("iso-string") or ISODate()
156201
isoDateHelper
157202
: ISO_DATE LPAREN stringLiteral? RPAREN
158-
| NEW ISO_DATE { p.NotifyErrorListeners("'new' keyword is not supported. Use ISODate() directly", nil, nil) }
159203
;
160204

161205
// Date() or Date("string") or Date(timestamp)
162206
dateHelper
163207
: DATE LPAREN (stringLiteral | NUMBER)? RPAREN
164-
| NEW DATE { p.NotifyErrorListeners("'new' keyword is not supported. Use Date() directly", nil, nil) }
165208
;
166209

167210
// UUID("uuid-string")
@@ -198,7 +241,6 @@ timestampHelper
198241
// RegExp("pattern", "flags") constructor
199242
regExpConstructor
200243
: REG_EXP LPAREN stringLiteral (COMMA stringLiteral)? RPAREN
201-
| NEW REG_EXP { p.NotifyErrorListeners("'new' keyword is not supported. Use RegExp() directly", nil, nil) }
202244
;
203245

204246
// Literals
@@ -233,9 +275,15 @@ identifier
233275
| NULL
234276
| FIND
235277
| FIND_ONE
278+
| COUNT_DOCUMENTS
279+
| ESTIMATED_DOCUMENT_COUNT
280+
| DISTINCT
281+
| AGGREGATE
282+
| GET_INDEXES
236283
| SORT
237284
| LIMIT
238285
| SKIP_
286+
| COUNT
239287
| PROJECTION
240288
| PROJECT
241289
| GET_COLLECTION
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// db.collection.aggregate() - Aggregation pipeline
2+
3+
// Empty pipeline
4+
db.orders.aggregate([])
5+
6+
// Single stage pipelines
7+
db.orders.aggregate([{ $match: { status: "completed" } }])
8+
db.orders.aggregate([{ $group: { _id: "$category", count: { $sum: 1 } } }])
9+
db.orders.aggregate([{ $sort: { createdAt: -1 } }])
10+
db.orders.aggregate([{ $limit: 10 }])
11+
db.orders.aggregate([{ $skip: 20 }])
12+
db.orders.aggregate([{ $project: { name: 1, total: 1, _id: 0 } }])
13+
db.orders.aggregate([{ $unwind: "$items" }])
14+
db.orders.aggregate([{ $count: "totalOrders" }])
15+
16+
// $match stage variations
17+
db.users.aggregate([{ $match: { age: { $gt: 18 } } }])
18+
db.users.aggregate([{ $match: { status: { $in: ["active", "pending"] } } }])
19+
db.users.aggregate([{ $match: { $or: [{ role: "admin" }, { role: "moderator" }] } }])
20+
db.users.aggregate([{ $match: { "address.country": "USA" } }])
21+
22+
// $group stage variations
23+
db.orders.aggregate([{ $group: { _id: "$customerId", total: { $sum: "$amount" } } }])
24+
db.orders.aggregate([{ $group: { _id: "$category", avgPrice: { $avg: "$price" } } }])
25+
db.orders.aggregate([{ $group: { _id: "$status", count: { $sum: 1 }, items: { $push: "$name" } } }])
26+
db.orders.aggregate([{ $group: { _id: null, totalRevenue: { $sum: "$amount" } } }])
27+
db.sales.aggregate([{ $group: { _id: { year: { $year: "$date" }, month: { $month: "$date" } }, total: { $sum: "$amount" } } }])
28+
29+
// $project stage variations
30+
db.users.aggregate([{ $project: { name: 1, email: 1 } }])
31+
db.users.aggregate([{ $project: { password: 0, ssn: 0 } }])
32+
db.users.aggregate([{ $project: { fullName: { $concat: ["$firstName", " ", "$lastName"] } } }])
33+
db.orders.aggregate([{ $project: { total: { $multiply: ["$price", "$quantity"] } } }])
34+
35+
// $sort stage variations
36+
db.users.aggregate([{ $sort: { name: 1 } }])
37+
db.users.aggregate([{ $sort: { createdAt: -1 } }])
38+
db.users.aggregate([{ $sort: { lastName: 1, firstName: 1 } }])
39+
40+
// $lookup stage (join)
41+
db.orders.aggregate([{ $lookup: { from: "users", localField: "customerId", foreignField: "_id", as: "customer" } }])
42+
db.orders.aggregate([{ $lookup: { from: "products", localField: "productIds", foreignField: "_id", as: "products" } }])
43+
44+
// Multi-stage pipelines
45+
db.orders.aggregate([
46+
{ $match: { status: "completed" } },
47+
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
48+
{ $sort: { total: -1 } },
49+
{ $limit: 10 }
50+
])
51+
52+
db.sales.aggregate([
53+
{ $match: { date: { $gte: ISODate("2024-01-01"), $lt: ISODate("2025-01-01") } } },
54+
{ $group: {
55+
_id: { year: { $year: "$date" }, month: { $month: "$date" } },
56+
totalRevenue: { $sum: "$amount" },
57+
avgOrderValue: { $avg: "$amount" },
58+
orderCount: { $sum: 1 }
59+
} },
60+
{ $sort: { "_id.year": 1, "_id.month": 1 } }
61+
])
62+
63+
db.users.aggregate([
64+
{ $match: { status: "active" } },
65+
{ $lookup: { from: "orders", localField: "_id", foreignField: "customerId", as: "orders" } },
66+
{ $project: { name: 1, email: 1, orderCount: { $size: "$orders" } } },
67+
{ $sort: { orderCount: -1 } },
68+
{ $limit: 100 }
69+
])
70+
71+
// Pipeline with $addFields
72+
db.orders.aggregate([
73+
{ $addFields: { totalWithTax: { $multiply: ["$total", 1.1] } } }
74+
])
75+
76+
// Pipeline with $set (alias for $addFields)
77+
db.orders.aggregate([
78+
{ $set: { processed: true, processedAt: ISODate() } }
79+
])
80+
81+
// Pipeline with $unset
82+
db.users.aggregate([
83+
{ $unset: ["password", "ssn", "internalNotes"] }
84+
])
85+
86+
// Pipeline with $replaceRoot
87+
db.orders.aggregate([
88+
{ $replaceRoot: { newRoot: "$shipping" } }
89+
])
90+
91+
// Pipeline with $facet (multiple pipelines)
92+
db.products.aggregate([
93+
{ $facet: {
94+
categoryCounts: [{ $group: { _id: "$category", count: { $sum: 1 } } }],
95+
priceStats: [{ $group: { _id: null, avgPrice: { $avg: "$price" }, maxPrice: { $max: "$price" } } }]
96+
} }
97+
])
98+
99+
// Aggregate with options (options passed to driver)
100+
db.orders.aggregate([{ $match: { status: "completed" } }], { allowDiskUse: true })
101+
db.orders.aggregate([{ $group: { _id: "$category" } }], { maxTimeMS: 60000 })
102+
db.orders.aggregate([{ $sort: { total: -1 } }], { collation: { locale: "en" } })
103+
104+
// Aggregate with collection access patterns
105+
db["orders"].aggregate([{ $match: { status: "pending" } }])
106+
db['audit-logs'].aggregate([{ $group: { _id: "$action", count: { $sum: 1 } } }])
107+
db.getCollection("sales").aggregate([{ $match: { year: 2024 } }])
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// db.collection.countDocuments() - Count documents matching a filter
2+
3+
// Count all documents
4+
db.users.countDocuments()
5+
db.users.countDocuments({})
6+
7+
// Count with simple filter
8+
db.users.countDocuments({ status: "active" })
9+
db.users.countDocuments({ verified: true })
10+
db.users.countDocuments({ role: "admin" })
11+
12+
// Count with comparison operators
13+
db.users.countDocuments({ age: { $gt: 18 } })
14+
db.users.countDocuments({ age: { $gte: 21, $lt: 65 } })
15+
db.users.countDocuments({ loginCount: { $gte: 10 } })
16+
17+
// Count with logical operators
18+
db.users.countDocuments({ $or: [{ status: "active" }, { status: "pending" }] })
19+
db.users.countDocuments({ $and: [{ verified: true }, { active: true }] })
20+
21+
// Count with array operators
22+
db.users.countDocuments({ tags: { $in: ["premium", "enterprise"] } })
23+
db.users.countDocuments({ roles: { $all: ["read", "write"] } })
24+
25+
// Count with existence check
26+
db.users.countDocuments({ email: { $exists: true } })
27+
db.users.countDocuments({ deletedAt: { $exists: false } })
28+
29+
// Count with nested documents
30+
db.users.countDocuments({ "address.country": "USA" })
31+
db.users.countDocuments({ "profile.verified": true })
32+
33+
// Count with helper functions
34+
db.users.countDocuments({ createdAt: { $gt: ISODate("2024-01-01") } })
35+
db.users.countDocuments({ lastLogin: { $lt: ISODate("2024-06-01") } })
36+
37+
// Count with options (options passed to driver)
38+
db.users.countDocuments({ status: "active" }, { skip: 10, limit: 100 })
39+
db.users.countDocuments({ verified: true }, { maxTimeMS: 5000 })
40+
db.users.countDocuments({}, { hint: { status: 1 } })
41+
42+
// Count with collection access patterns
43+
db["users"].countDocuments({ active: true })
44+
db['audit-logs'].countDocuments({ level: "error" })
45+
db.getCollection("orders").countDocuments({ status: "completed" })
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// db.collection.distinct() - Find distinct values for a field
2+
3+
// Distinct with field only (required)
4+
db.users.distinct("status")
5+
db.users.distinct("country")
6+
db.users.distinct("role")
7+
db.orders.distinct("category")
8+
db.products.distinct("brand")
9+
10+
// Distinct with nested field
11+
db.users.distinct("address.city")
12+
db.users.distinct("address.country")
13+
db.users.distinct("profile.department")
14+
15+
// Distinct with field and empty query
16+
db.users.distinct("status", {})
17+
db.users.distinct("city", {})
18+
19+
// Distinct with field and filter query
20+
db.users.distinct("city", { country: "USA" })
21+
db.users.distinct("status", { active: true })
22+
db.users.distinct("role", { department: "engineering" })
23+
db.orders.distinct("productId", { status: "completed" })
24+
25+
// Distinct with comparison operators in query
26+
db.users.distinct("city", { age: { $gt: 18 } })
27+
db.users.distinct("status", { createdAt: { $gt: ISODate("2024-01-01") } })
28+
29+
// Distinct with logical operators in query
30+
db.users.distinct("role", { $or: [{ active: true }, { verified: true }] })
31+
db.users.distinct("department", { $and: [{ status: "active" }, { role: "employee" }] })
32+
33+
// Distinct with array operators in query
34+
db.users.distinct("city", { tags: { $in: ["premium", "enterprise"] } })
35+
36+
// Distinct with field, query, and options (options passed to driver)
37+
db.users.distinct("email", { status: "active" }, { collation: { locale: "en" } })
38+
db.users.distinct("name", {}, { maxTimeMS: 5000 })
39+
40+
// Distinct with collection access patterns
41+
db["users"].distinct("status")
42+
db['audit-logs'].distinct("action")
43+
db.getCollection("orders").distinct("status", { year: 2024 })
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// db.collection.estimatedDocumentCount() - Fast estimated count using collection metadata
2+
3+
// Basic estimated count (no filter - uses metadata)
4+
db.users.estimatedDocumentCount()
5+
db.orders.estimatedDocumentCount()
6+
db.products.estimatedDocumentCount()
7+
8+
// Estimated count with options (options passed to driver)
9+
db.users.estimatedDocumentCount({})
10+
db.users.estimatedDocumentCount({ maxTimeMS: 1000 })
11+
db.users.estimatedDocumentCount({ maxTimeMS: 5000 })
12+
13+
// Estimated count with collection access patterns
14+
db["users"].estimatedDocumentCount()
15+
db['audit-logs'].estimatedDocumentCount()
16+
db.getCollection("orders").estimatedDocumentCount()
17+
db.getCollection("large-collection").estimatedDocumentCount({ maxTimeMS: 10000 })

0 commit comments

Comments
 (0)