Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ gomongo/
├── client.go # Public API: Client, NewClient, Execute
├── executor.go # Parse → Translate → Execute pipeline
├── translator.go # Walk ANTLR parse tree, build driver operations
├── method_registry.go # Registry of MongoDB methods with status and hints
├── helper_functions.go # Convert ObjectId(), ISODate(), etc. to BSON
├── errors.go # Error types (ParseError, UnsupportedOperationError)
├── errors.go # Error types (ParseError, UnsupportedOperationError, DeprecatedOperationError)
├── executor_test.go # Integration tests with testcontainers
└── go.mod
```
Expand Down Expand Up @@ -129,3 +130,27 @@ All query results are returned as Extended JSON (Relaxed) format using `bson.Mar
- **Description** — Clearly describe what the PR changes and why
- **Testing** — Include information about how the changes were tested
- **Breaking Changes** — Clearly mark any breaking API changes

## Adding New Method Support

When adding support for a new MongoDB method:

1. **Update `method_registry.go`** — Remove the method entry or change status to `statusSupported`
2. **Update `translator.go`** — Add handler in `visitMethodCall()` for the new method
3. **Add tests** — Create integration tests in `executor_test.go`
4. **Update README** — Add the method to the supported methods list

### Method Registry

The `method_registry.go` file maintains metadata about MongoDB methods:

- **statusSupported** — Method is implemented and working
- **statusDeprecated** — Method is deprecated; error includes alternative suggestion
- **statusUnsupported** — Method is recognized but not yet implemented

When implementing a previously unsupported method, remove its entry from the registry (supported methods don't need registry entries).

### Error Types

- **UnsupportedOperationError** — For methods not yet implemented (includes hint)
- **DeprecatedOperationError** — For deprecated methods (includes alternative)
207 changes: 135 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,6 @@ Go library for parsing and executing MongoDB shell syntax using the native Mongo

gomongo parses MongoDB shell commands (e.g., `db.users.find()`) and executes them using the Go MongoDB driver, eliminating the need for external mongosh CLI.

## Status

**MVP v0.1.0** - Basic functionality implemented:

| Feature | Status |
|---------|--------|
| `find()` with filter | Supported |
| `findOne()` | Not yet supported |
| Cursor modifiers (sort, limit, skip, projection) | Parsed but ignored |
| Helper functions (ObjectId, ISODate, UUID, etc.) | Supported |
| Shell commands (show dbs, show collections) | Not yet supported |
| Collection access (dot, bracket, getCollection) | Supported |

## Installation

```bash
Expand All @@ -33,28 +20,30 @@ package main
import (
"context"
"fmt"
"log"

"github.com/bytebase/gomongo"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func main() {
ctx := context.Background()

// Connect to MongoDB
client, err := mongo.Connect(options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
panic(err)
log.Fatal(err)
}
defer client.Disconnect(context.Background())
defer client.Disconnect(ctx)

// Create gomongo client
gc := gomongo.NewClient(client)

// Execute MongoDB shell command
ctx := context.Background()
result, err := gc.Execute(ctx, "mydb", `db.users.find()`)
// Execute MongoDB shell commands
result, err := gc.Execute(ctx, "mydb", `db.users.find({ age: { $gt: 25 } })`)
if err != nil {
panic(err)
log.Fatal(err)
}

// Print results (Extended JSON format)
Expand All @@ -64,49 +53,6 @@ func main() {
}
```

## Supported Operations (MVP)

| Category | Operation | Status |
|----------|-----------|--------|
| **Read** | `find()` | Supported (with filter) |
| | `findOne()` | Not yet supported |
| **Collection Access** | dot notation | Supported (`db.users`) |
| | bracket notation | Supported (`db["user-logs"]`) |
| | getCollection | Supported (`db.getCollection("users")`) |

## Supported Filter Syntax

```javascript
// Simple equality
db.users.find({ name: "alice" })

// Comparison operators
db.users.find({ age: { $gt: 25 } })
db.users.find({ age: { $lte: 30 } })

// Multiple conditions
db.users.find({ active: true, age: { $gte: 18 } })

// Array operators
db.users.find({ tags: { $in: ["admin", "user"] } })
```

## Supported Helper Functions

| Helper | Example | BSON Type |
|--------|---------|-----------|
| `ObjectId()` | `ObjectId("507f1f77bcf86cd799439011")` | ObjectID |
| `ISODate()` | `ISODate("2024-01-01T00:00:00Z")` | DateTime |
| `new Date()` | `new Date("2024-01-01")` | DateTime |
| `UUID()` | `UUID("550e8400-e29b-41d4-a716-446655440000")` | Binary (subtype 4) |
| `Long()` / `NumberLong()` | `Long(123)` | int64 |
| `Int32()` / `NumberInt()` | `Int32(123)` | int32 |
| `Double()` | `Double(1.5)` | float64 |
| `Decimal128()` | `Decimal128("123.45")` | Decimal128 |
| `Timestamp()` | `Timestamp(1627811580, 1)` | Timestamp |
| `/pattern/flags` | `/^test/i` | Regex |
| `RegExp()` | `RegExp("pattern", "i")` | Regex |

## Output Format

Results are returned in Extended JSON (Relaxed) format:
Expand All @@ -120,13 +66,130 @@ Results are returned in Extended JSON (Relaxed) format:
}
```

## Roadmap

Future versions will add:
- `findOne()` support
- Cursor modifiers (sort, limit, skip, projection)
- Shell commands (show dbs, show collections)

## License

Apache License 2.0
## Command Reference

### Milestone 1: Read Operations + Utility + Aggregation (Current)

#### Utility Commands

| Command | Syntax | Status |
|---------|--------|--------|
| show dbs | `show dbs` | Supported |
| show databases | `show databases` | Supported |
| show collections | `show collections` | Supported |
| db.getCollectionNames() | `db.getCollectionNames()` | Supported |
| db.getCollectionInfos() | `db.getCollectionInfos()` | Supported |

#### Read Commands

| Command | Syntax | Status | Notes |
|---------|--------|--------|-------|
| db.collection.find() | `find(query, projection)` | Supported | options deferred |
| db.collection.findOne() | `findOne(query, projection)` | Supported | |
| db.collection.countDocuments() | `countDocuments(filter)` | Supported | options deferred |
| db.collection.estimatedDocumentCount() | `estimatedDocumentCount()` | Supported | options deferred |
| db.collection.distinct() | `distinct(field, query)` | Supported | options deferred |
| db.collection.getIndexes() | `getIndexes()` | Supported | |

#### Cursor Modifiers

| Method | Syntax | Status |
|--------|--------|--------|
| cursor.limit() | `limit(number)` | Supported |
| cursor.skip() | `skip(number)` | Supported |
| cursor.sort() | `sort(document)` | Supported |
| cursor.count() | `count()` | Deprecated - use countDocuments() |

#### Aggregation

| Command | Syntax | Status | Notes |
|---------|--------|--------|-------|
| db.collection.aggregate() | `aggregate(pipeline)` | Supported | options deferred |

#### Object Constructors

| Constructor | Supported Syntax | Unsupported Syntax |
|-------------|------------------|-------------------|
| ObjectId() | `ObjectId()`, `ObjectId("hex")` | `new ObjectId()` |
| ISODate() | `ISODate()`, `ISODate("string")` | `new ISODate()` |
| Date() | `Date()`, `Date("string")`, `Date(timestamp)` | `new Date()` |
| UUID() | `UUID("hex")` | `new UUID()` |
| NumberInt() | `NumberInt(value)` | `new NumberInt()` |
| NumberLong() | `NumberLong(value)` | `new NumberLong()` |
| NumberDecimal() | `NumberDecimal("value")` | `new NumberDecimal()` |
| Timestamp() | `Timestamp(t, i)` | `new Timestamp()` |
| BinData() | `BinData(subtype, base64)` | |
| RegExp() | `RegExp("pattern", "flags")`, `/pattern/flags` | |

### Milestone 2: Write Operations (Planned)

| Command | Syntax | Status |
|---------|--------|--------|
| db.collection.insertOne() | `insertOne(document)` | Not yet supported |
| db.collection.insertMany() | `insertMany(documents)` | Not yet supported |
| db.collection.updateOne() | `updateOne(filter, update)` | Not yet supported |
| db.collection.updateMany() | `updateMany(filter, update)` | Not yet supported |
| db.collection.deleteOne() | `deleteOne(filter)` | Not yet supported |
| db.collection.deleteMany() | `deleteMany(filter)` | Not yet supported |
| db.collection.replaceOne() | `replaceOne(filter, replacement)` | Not yet supported |
| db.collection.findOneAndUpdate() | `findOneAndUpdate(filter, update)` | Not yet supported |
| db.collection.findOneAndReplace() | `findOneAndReplace(filter, replacement)` | Not yet supported |
| db.collection.findOneAndDelete() | `findOneAndDelete(filter)` | Not yet supported |

### Milestone 3: Administrative Operations (Planned)

#### Index Management

| Command | Syntax | Status |
|---------|--------|--------|
| db.collection.createIndex() | `createIndex(keys)` | Not yet supported |
| db.collection.createIndexes() | `createIndexes(indexSpecs)` | Not yet supported |
| db.collection.dropIndex() | `dropIndex(index)` | Not yet supported |
| db.collection.dropIndexes() | `dropIndexes()` | Not yet supported |

#### Collection Management

| Command | Syntax | Status |
|---------|--------|--------|
| db.createCollection() | `db.createCollection(name)` | Not yet supported |
| db.collection.drop() | `drop()` | Not yet supported |
| db.collection.renameCollection() | `renameCollection(newName)` | Not yet supported |
| db.dropDatabase() | `db.dropDatabase()` | Not yet supported |

#### Database Information

| Command | Syntax | Status |
|---------|--------|--------|
| db.stats() | `db.stats()` | Not yet supported |
| db.collection.stats() | `stats()` | Not yet supported |
| db.serverStatus() | `db.serverStatus()` | Not yet supported |
| db.serverBuildInfo() | `db.serverBuildInfo()` | Not yet supported |
| db.version() | `db.version()` | Not yet supported |
| db.hostInfo() | `db.hostInfo()` | Not yet supported |
| db.listCommands() | `db.listCommands()` | Not yet supported |

### Not Planned

The following categories are recognized but not planned for support:

| Category | Reason |
|----------|--------|
| Database switching (`use <db>`, `db.getSiblingDB()`) | Database is set at connection time |
| Interactive cursor methods (`hasNext()`, `next()`, `toArray()`) | Not an interactive shell |
| JavaScript execution (`forEach()`, `map()`) | No JavaScript engine |
| Replication (`rs.*`) | Cluster administration |
| Sharding (`sh.*`) | Cluster administration |
| User/Role management | Security administration |
| Client-side encryption | Security feature |
| Atlas Stream Processing (`sp.*`) | Atlas-specific |
| Native shell functions (`cat()`, `load()`, `quit()`) | Shell-specific |

For deprecated methods (e.g., `db.collection.insert()`, `db.collection.update()`), gomongo returns actionable error messages directing users to modern alternatives.

## Design Principles

1. **No database switching** - Database is set at connection time only
2. **Not an interactive shell** - No cursor iteration, REPL-style commands, or stateful operations
3. **Syntax translator, not validator** - Arguments pass directly to the Go driver; the server validates
4. **Single syntax for constructors** - Use `ObjectId()`, not `new ObjectId()`
5. **Clear error messages** - Actionable guidance for unsupported or deprecated syntax
10 changes: 10 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ func (e *UnsupportedOperationError) Error() string {
}
return fmt.Sprintf("unsupported operation %q", e.Operation)
}

// DeprecatedOperationError represents a deprecated operation with alternatives.
type DeprecatedOperationError struct {
Operation string
Alternative string
}

func (e *DeprecatedOperationError) Error() string {
return fmt.Sprintf("%s is deprecated. Use %s instead", e.Operation, e.Alternative)
}
41 changes: 32 additions & 9 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gomongo_test

import (
"context"
"slices"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -295,7 +296,36 @@ func TestUnsupportedOperation(t *testing.T) {

var unsupportedErr *gomongo.UnsupportedOperationError
require.ErrorAs(t, err, &unsupportedErr)
require.Equal(t, "insertOne", unsupportedErr.Operation)
require.Equal(t, "insertOne()", unsupportedErr.Operation)
}

func TestUnsupportedAtlasSearchIndex(t *testing.T) {
client, cleanup := setupTestContainer(t)
defer cleanup()

gc := gomongo.NewClient(client)
ctx := context.Background()

// Test createSearchIndex
_, err := gc.Execute(ctx, "testdb", `db.movies.createSearchIndex({ name: "default", definition: { mappings: { dynamic: true } } })`)
require.Error(t, err)

var unsupportedErr *gomongo.UnsupportedOperationError
require.ErrorAs(t, err, &unsupportedErr)
require.Equal(t, "createSearchIndex()", unsupportedErr.Operation)
require.Contains(t, unsupportedErr.Hint, "Atlas Search Index")
}

func TestMethodRegistryStats(t *testing.T) {
total, deprecated, unsupported := gomongo.MethodRegistryStats()

// Verify we have a reasonable number of methods registered
require.GreaterOrEqual(t, total, 100, "expected at least 100 methods in registry")
require.GreaterOrEqual(t, deprecated, 20, "expected at least 20 deprecated methods")
require.GreaterOrEqual(t, unsupported, 80, "expected at least 80 unsupported methods")

// Log stats for visibility
t.Logf("Method Registry Stats: total=%d, deprecated=%d, unsupported=%d", total, deprecated, unsupported)
}

func TestFindWithFilter(t *testing.T) {
Expand Down Expand Up @@ -582,14 +612,7 @@ func TestShowDatabases(t *testing.T) {
require.GreaterOrEqual(t, result.RowCount, 1)

// Check that mydb is in the result
found := false
for _, row := range result.Rows {
if row == "mydb" {
found = true
break
}
}
require.True(t, found, "expected 'mydb' in database list, got: %v", result.Rows)
require.True(t, slices.Contains(result.Rows, "mydb"), "expected 'mydb' in database list, got: %v", result.Rows)
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.24.5

require (
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/bytebase/parser v0.0.0-20260119035746-76308b5d11fd
github.com/bytebase/parser v0.0.0-20260120080341-a57d4b68030c
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go/modules/mongodb v0.40.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/bytebase/antlr/v4 v4.0.0-20240827034948-8c385f108920 h1:IfmPt5o5R70NKtOrs+QHOoCgViYZelZysGxVBvV4ybA=
github.com/bytebase/antlr/v4 v4.0.0-20240827034948-8c385f108920/go.mod h1:ykhjIPiv0IWpu3OGXCHdz2eUSe8UNGGD6baqjs8jSuU=
github.com/bytebase/parser v0.0.0-20260119035746-76308b5d11fd h1:JCEEza5T4CTNWZuwHe6/7mqG7Qg+q2ZiHP00UtW+NtQ=
github.com/bytebase/parser v0.0.0-20260119035746-76308b5d11fd/go.mod h1:jeak/EfutSOAuWKvrFIT2IZunhWprM7oTFBRgZ9RCxo=
github.com/bytebase/parser v0.0.0-20260120080341-a57d4b68030c h1:owIVaPTU4DrzzajK0BsuyRtScB/8JNQco7bzg4ZduLA=
github.com/bytebase/parser v0.0.0-20260120080341-a57d4b68030c/go.mod h1:jeak/EfutSOAuWKvrFIT2IZunhWprM7oTFBRgZ9RCxo=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
Expand Down
Loading
Loading