Skip to content

Commit 9555f7d

Browse files
committed
Add MongoDB scripts support with updateMany/updateOne and getSiblingDB
- Add updateMany and updateOne operations for MongoDB - Add getSiblingDB() support for cross-database operations - Add script examples (ref-scripts, tenants-scripts) with test coverage - Handle new Date() conversion to BSON date format in update operations - Update README with supported operations and cross-database usage notes
1 parent 4c1666e commit 9555f7d

File tree

6 files changed

+198
-2
lines changed

6 files changed

+198
-2
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,14 @@ MongoDB uses database-based multi-tenancy (similar to MySQL). Migrations are Jav
577577
**Supported operations:**
578578
- `db.collection.insertOne({...})` - Insert a document
579579
- `db.collection.createIndex({...}, {...})` - Create an index
580+
- `db.collection.updateOne({filter}, {update})` - Update a single document
581+
- `db.collection.updateMany({filter}, {update})` - Update multiple documents
582+
- `db.getSiblingDB('dbname').collection.operation(...)` - Access different database
583+
584+
**Supported JavaScript features:**
585+
- `new Date()` - Automatically converted to current timestamp
586+
587+
**Note:** Single scripts execute in their source directory database (e.g., `ref-scripts`). To access collections in other databases, use `db.getSiblingDB('dbname')`.
580588

581589
Sample MongoDB configuration:
582590

@@ -585,9 +593,14 @@ baseLocation: migrations
585593
driver: mongodb
586594
dataSource: "mongodb://localhost:27017"
587595
singleMigrations:
588-
- admin
596+
- ref
597+
- configuration
598+
singleScripts:
599+
- ref-scripts
589600
tenantMigrations:
590601
- tenants
602+
tenantScripts:
603+
- tenants-scripts
591604
```
592605

593606
Sample migration file (`001_create_users.js`):

db/db_mongodb.go

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,11 +530,41 @@ func (mc *mongoDBConnector) executeMigration(migration types.Migration, dbName s
530530
func (mc *mongoDBConnector) executeMongoDBCommand(targetDB *mongo.Database, command string) error {
531531
command = strings.TrimSpace(command)
532532

533-
// Match pattern: db.collectionName.operation(...)
533+
// Match pattern: db.collectionName.operation(...) or db.getSiblingDB('dbname').collectionName.operation(...)
534534
if !strings.HasPrefix(command, "db.") {
535535
return nil // Skip non-db commands
536536
}
537537

538+
// Check for getSiblingDB pattern
539+
if strings.HasPrefix(command, "db.getSiblingDB(") {
540+
// Extract database name from getSiblingDB('dbname')
541+
start := strings.Index(command, "'")
542+
if start == -1 {
543+
start = strings.Index(command, "\"")
544+
}
545+
if start == -1 {
546+
return fmt.Errorf("invalid getSiblingDB syntax: %s", command)
547+
}
548+
549+
end := strings.Index(command[start+1:], "'")
550+
if end == -1 {
551+
end = strings.Index(command[start+1:], "\"")
552+
}
553+
if end == -1 {
554+
return fmt.Errorf("invalid getSiblingDB syntax: %s", command)
555+
}
556+
557+
dbName := command[start+1 : start+1+end]
558+
targetDB = mc.client.Database(dbName)
559+
560+
// Find the collection part after getSiblingDB('dbname').
561+
dotAfterDB := strings.Index(command[start+1+end:], ".")
562+
if dotAfterDB == -1 {
563+
return fmt.Errorf("invalid getSiblingDB syntax: %s", command)
564+
}
565+
command = "db." + command[start+1+end+dotAfterDB+1:]
566+
}
567+
538568
// Extract collection name and operation
539569
parts := strings.SplitN(command[3:], ".", 2) // Remove "db." prefix
540570
if len(parts) < 2 {
@@ -559,6 +589,10 @@ func (mc *mongoDBConnector) executeMongoDBCommand(targetDB *mongo.Database, comm
559589
return mc.handleInsertOne(col, rest[opEnd:])
560590
case "createIndex":
561591
return mc.handleCreateIndex(col, rest[opEnd:])
592+
case "updateMany":
593+
return mc.handleUpdateMany(col, rest[opEnd:])
594+
case "updateOne":
595+
return mc.handleUpdateOne(col, rest[opEnd:])
562596
default:
563597
common.LogWarn(mc.ctx, "Unsupported operation: %s", operation)
564598
return nil
@@ -653,6 +687,104 @@ func (mc *mongoDBConnector) handleCreateIndex(col *mongo.Collection, args string
653687
return err
654688
}
655689

690+
// handleUpdateMany executes updateMany operation
691+
func (mc *mongoDBConnector) handleUpdateMany(col *mongo.Collection, args string) error {
692+
return mc.handleUpdate(col, args, true)
693+
}
694+
695+
// handleUpdateOne executes updateOne operation
696+
func (mc *mongoDBConnector) handleUpdateOne(col *mongo.Collection, args string) error {
697+
return mc.handleUpdate(col, args, false)
698+
}
699+
700+
// handleUpdate executes update operations (updateOne or updateMany)
701+
func (mc *mongoDBConnector) handleUpdate(col *mongo.Collection, args string, many bool) error {
702+
// Extract filter and update documents from updateMany({filter}, {update}, {options})
703+
start := strings.Index(args, "{")
704+
if start == -1 {
705+
return fmt.Errorf("invalid update syntax")
706+
}
707+
708+
// Find the matching closing brace for the first argument (filter)
709+
braceCount := 0
710+
firstArgEnd := -1
711+
for i := start; i < len(args); i++ {
712+
if args[i] == '{' {
713+
braceCount++
714+
} else if args[i] == '}' {
715+
braceCount--
716+
if braceCount == 0 {
717+
firstArgEnd = i
718+
break
719+
}
720+
}
721+
}
722+
723+
if firstArgEnd == -1 {
724+
return fmt.Errorf("invalid update syntax")
725+
}
726+
727+
filterJSON := args[start : firstArgEnd+1]
728+
filterJSON = mc.jsToJSON(filterJSON)
729+
730+
// Parse filter
731+
var filter bson.M
732+
if err := bson.UnmarshalExtJSON([]byte(filterJSON), false, &filter); err != nil {
733+
return fmt.Errorf("failed to parse filter: %v", err)
734+
}
735+
736+
// Find second argument (update document)
737+
remaining := strings.TrimSpace(args[firstArgEnd+1:])
738+
if !strings.HasPrefix(remaining, ",") {
739+
return fmt.Errorf("missing update document")
740+
}
741+
remaining = strings.TrimSpace(remaining[1:])
742+
743+
updateStart := strings.Index(remaining, "{")
744+
if updateStart == -1 {
745+
return fmt.Errorf("invalid update document")
746+
}
747+
748+
// Find the matching closing brace for the update document
749+
braceCount = 0
750+
updateEnd := -1
751+
for i := updateStart; i < len(remaining); i++ {
752+
if remaining[i] == '{' {
753+
braceCount++
754+
} else if remaining[i] == '}' {
755+
braceCount--
756+
if braceCount == 0 {
757+
updateEnd = i
758+
break
759+
}
760+
}
761+
}
762+
763+
if updateEnd == -1 {
764+
return fmt.Errorf("invalid update document")
765+
}
766+
767+
updateJSON := remaining[updateStart : updateEnd+1]
768+
updateJSON = mc.jsToJSON(updateJSON)
769+
770+
// Handle new Date() - replace with current time in extended JSON format
771+
updateJSON = strings.ReplaceAll(updateJSON, "new Date()", fmt.Sprintf("{\"$date\":\"%s\"}", time.Now().Format(time.RFC3339Nano)))
772+
773+
// Parse update document
774+
var update bson.M
775+
if err := bson.UnmarshalExtJSON([]byte(updateJSON), false, &update); err != nil {
776+
return fmt.Errorf("failed to parse update document: %v", err)
777+
}
778+
779+
// Execute update
780+
if many {
781+
_, err := col.UpdateMany(mc.ctx, filter, update)
782+
return err
783+
}
784+
_, err := col.UpdateOne(mc.ctx, filter, update)
785+
return err
786+
}
787+
656788
// jsToJSON converts JavaScript object notation to proper JSON
657789
// Handles: {key: value} -> {"key": value}, {key: 'value'} -> {"key": "value"}
658790
func (mc *mongoDBConnector) jsToJSON(js string) string {

db/db_mongodb_integration_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,43 @@ func TestMongoDBCreateVersion(t *testing.T) {
8686
assert.Equal(t, lenBefore+int(results.MigrationsGrandTotal), lenAfter)
8787
}
8888

89+
func TestMongoDBScripts(t *testing.T) {
90+
configFile := "../test/migrator-mongodb.yaml"
91+
config, err := config.FromFile(configFile)
92+
assert.Nil(t, err)
93+
94+
connector := New(newTestContext(), config)
95+
defer connector.Dispose()
96+
97+
// Create test tenant with some initial data
98+
testTenant := fmt.Sprintf("scripttenant%d", time.Now().UnixNano())
99+
m1 := time.Now().UnixNano()
100+
tenantMigration := types.Migration{Name: fmt.Sprintf("%v.js", m1), SourceDir: "tenants", File: fmt.Sprintf("tenants/%v.js", m1), MigrationType: types.MigrationTypeTenantMigration, Contents: "db.settings.insertOne({k: 999, v: '999'})"}
101+
connector.CreateTenant(testTenant, "test-tenant-scripts", types.ActionApply, []types.Migration{tenantMigration}, false)
102+
103+
tenants := connector.GetTenants()
104+
noOfTenants := len(tenants)
105+
106+
s1 := time.Now().UnixNano()
107+
ts1 := time.Now().UnixNano()
108+
109+
// Single script - runs every time, uses getSiblingDB to access ref database
110+
singleScript := types.Migration{Name: fmt.Sprintf("%v.js", s1), SourceDir: "ref-scripts", File: fmt.Sprintf("ref-scripts/%v.js", s1), MigrationType: types.MigrationTypeSingleScript, Contents: "db.getSiblingDB('ref').modules.updateMany({}, {$set: {script_updated: new Date()}})"}
111+
112+
// Tenant script - runs every time for each tenant, uses updateMany
113+
tenantScript := types.Migration{Name: fmt.Sprintf("%v.js", ts1), SourceDir: "tenants-scripts", File: fmt.Sprintf("tenants-scripts/%v.js", ts1), MigrationType: types.MigrationTypeTenantScript, Contents: "db.settings.updateMany({}, {$set: {script_run_at: new Date()}})"}
114+
115+
scriptsToApply := []types.Migration{singleScript, tenantScript}
116+
117+
results, version := connector.CreateVersion("test-scripts", types.ActionApply, scriptsToApply, false)
118+
119+
assert.NotNil(t, version)
120+
assert.Equal(t, int32(1), results.SingleScripts)
121+
assert.Equal(t, int32(1), results.TenantScripts)
122+
assert.Equal(t, int32(noOfTenants), results.TenantScriptsTotal)
123+
assert.Equal(t, int32(1+noOfTenants), results.ScriptsGrandTotal)
124+
}
125+
89126
func TestMongoDBCreateTenant(t *testing.T) {
90127
configFile := "../test/migrator-mongodb.yaml"
91128
config, err := config.FromFile(configFile)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// This script runs every time to update statistics in ref database
2+
db.getSiblingDB('ref').modules.updateMany(
3+
{},
4+
{ $set: { last_updated: new Date() } }
5+
);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// This script runs every time for each tenant to refresh cache
2+
db.settings.updateMany(
3+
{},
4+
{ $set: { cached_at: new Date() } }
5+
);

test/migrator-mongodb.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ dataSource: "mongodb://localhost:27017"
44
singleMigrations:
55
- ref
66
- configuration
7+
singleScripts:
8+
- ref-scripts
79
tenantMigrations:
810
- tenants
11+
tenantScripts:
12+
- tenants-scripts

0 commit comments

Comments
 (0)