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
70 changes: 35 additions & 35 deletions ANALYSIS.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public CreateCollectionOperator(MongoDatabase mongoDatabase, MongoOperation oper

@Override
public void applyInternal(ClientSession clientSession) {
if (DatabaseInspector.collectionExists(mongoDatabase, op.getCollection())) {
logger.info("Collection '{}' already exists, skipping createCollection", op.getCollection());
return;
}
mongoDatabase.createCollection(op.getCollection());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public CreateViewOperator(MongoDatabase mongoDatabase, MongoOperation operation)

@Override
protected void applyInternal(ClientSession clientSession) {
if (DatabaseInspector.collectionExists(mongoDatabase, op.getCollection())) {
logger.info("View '{}' already exists, skipping createView", op.getCollection());
return;
}
CreateViewOptions options = CreateViewOptionsMapper.map(op.getOptions());
mongoDatabase.createView(op.getCollection(), getViewOn(), getPipeline(), options);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2025 Flamingock (https://www.flamingock.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.flamingock.template.mongodb.model.operator;

import com.mongodb.client.MongoDatabase;
import org.bson.Document;

import java.util.ArrayList;
import java.util.List;

/**
* Package-private utility for inspecting MongoDB state before operator execution.
* Used by DDL operators to implement idempotent pre-checks.
*/
final class DatabaseInspector {

private DatabaseInspector() {
}

static boolean collectionExists(MongoDatabase database, String collectionName) {
return database.listCollectionNames()
.into(new ArrayList<>())
.contains(collectionName);
}

static boolean indexExistsByName(MongoDatabase database, String collection, String indexName) {
List<Document> indexes = database.getCollection(collection)
.listIndexes()
.into(new ArrayList<>());
return indexes.stream().anyMatch(idx -> indexName.equals(idx.getString("name")));
}

static boolean indexExistsByKeys(MongoDatabase database, String collection, Document keys) {
List<Document> indexes = database.getCollection(collection)
.listIndexes()
.into(new ArrayList<>());
return indexes.stream().anyMatch(idx -> {
Document idxKeys = idx.get("key", Document.class);
return idxKeys != null && idxKeys.equals(keys);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@ public DropIndexOperator(MongoDatabase mongoDatabase, MongoOperation operation)
protected void applyInternal(ClientSession clientSession) {
String indexName = getIndexName();
if (indexName != null) {
if (!DatabaseInspector.indexExistsByName(mongoDatabase, op.getCollection(), indexName)) {
logger.info("Index '{}' does not exist on collection '{}', skipping dropIndex", indexName, op.getCollection());
return;
}
mongoDatabase.getCollection(op.getCollection()).dropIndex(indexName);
} else {
if (!DatabaseInspector.indexExistsByKeys(mongoDatabase, op.getCollection(), op.getKeys())) {
logger.info("Index with specified keys does not exist on collection '{}', skipping dropIndex", op.getCollection());
return;
}
mongoDatabase.getCollection(op.getCollection()).dropIndex(op.getKeys());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@ public RenameCollectionOperator(MongoDatabase mongoDatabase, MongoOperation oper

@Override
protected void applyInternal(ClientSession clientSession) {
MongoNamespace target = new MongoNamespace(mongoDatabase.getName(), getTarget());
String targetName = getTarget();
boolean sourceExists = DatabaseInspector.collectionExists(mongoDatabase, op.getCollection());
boolean targetExists = DatabaseInspector.collectionExists(mongoDatabase, targetName);

if (!sourceExists && targetExists) {
logger.info("Collection '{}' already renamed to '{}', skipping renameCollection", op.getCollection(), targetName);
return;
}

MongoNamespace target = new MongoNamespace(mongoDatabase.getName(), targetName);
RenameCollectionOptions options = RenameCollectionOptionsMapper.map(op.getOptions());
mongoDatabase.getCollection(op.getCollection()).renameCollection(target, options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,41 @@ void createCollectionTest() {
assertTrue(collectionExists(COLLECTION_NAME), "Collection should exist after creation");
}

@Test
@DisplayName("WHEN createCollection operator is applied twice THEN second call succeeds silently")
void createCollectionIdempotentTest() {
assertFalse(collectionExists(COLLECTION_NAME), "Collection should not exist before creation");

MongoOperation operation = new MongoOperation();
operation.setType("createCollection");
operation.setCollection(COLLECTION_NAME);
operation.setParameters(new HashMap<>());

CreateCollectionOperator operator = new CreateCollectionOperator(mongoDatabase, operation);
operator.apply(null);
assertTrue(collectionExists(COLLECTION_NAME), "Collection should exist after first creation");

// Second apply should not throw
operator.apply(null);
assertTrue(collectionExists(COLLECTION_NAME), "Collection should still exist after second creation");
}

@Test
@DisplayName("WHEN createCollection operator is applied and collection already exists THEN operation is skipped")
void createCollectionAlreadyExistsTest() {
mongoDatabase.createCollection(COLLECTION_NAME);
assertTrue(collectionExists(COLLECTION_NAME), "Collection should exist before operator runs");

MongoOperation operation = new MongoOperation();
operation.setType("createCollection");
operation.setCollection(COLLECTION_NAME);
operation.setParameters(new HashMap<>());

CreateCollectionOperator operator = new CreateCollectionOperator(mongoDatabase, operation);
operator.apply(null);
assertTrue(collectionExists(COLLECTION_NAME), "Collection should still exist after skipped creation");
}

private boolean collectionExists(String collectionName) {
return mongoDatabase.listCollectionNames()
.into(new ArrayList<>())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -80,4 +81,69 @@ void createViewTest() {
assertEquals(1, viewResults.size(), "View should return only active users");
assertEquals("Active User", viewResults.get(0).getString("name"));
}

@Test
@DisplayName("WHEN createView operator is applied twice THEN second call succeeds silently and view still works")
void createViewIdempotentTest() {
mongoDatabase.createCollection(SOURCE_COLLECTION);
mongoDatabase.getCollection(SOURCE_COLLECTION).insertMany(Arrays.asList(
new Document("name", "Active User").append("status", "active"),
new Document("name", "Inactive User").append("status", "inactive")
));

MongoOperation operation = buildCreateViewOperation();
CreateViewOperator operator = new CreateViewOperator(mongoDatabase, operation);
operator.apply(null);

List<String> collections = mongoDatabase.listCollectionNames().into(new ArrayList<>());
assertTrue(collections.contains(VIEW_NAME), "View should exist after first creation");

// Second apply should not throw
assertDoesNotThrow(() -> operator.apply(null));

List<Document> viewResults = mongoDatabase.getCollection(VIEW_NAME)
.find()
.into(new ArrayList<>());
assertEquals(1, viewResults.size(), "View should still return only active users");
assertEquals("Active User", viewResults.get(0).getString("name"));
}

@Test
@DisplayName("WHEN createView operator is applied and view already exists THEN operation is skipped")
void createViewAlreadyExistsTest() {
mongoDatabase.createCollection(SOURCE_COLLECTION);

List<Document> pipeline = new ArrayList<>();
pipeline.add(new Document("$match", new Document("status", "active")));
mongoDatabase.createView(VIEW_NAME, SOURCE_COLLECTION, pipeline);
assertTrue(mongoDatabase.listCollectionNames().into(new ArrayList<>()).contains(VIEW_NAME),
"View should exist before operator runs");

MongoOperation operation = buildCreateViewOperation();
CreateViewOperator operator = new CreateViewOperator(mongoDatabase, operation);
assertDoesNotThrow(() -> operator.apply(null));

assertTrue(mongoDatabase.listCollectionNames().into(new ArrayList<>()).contains(VIEW_NAME),
"View should still exist after skipped creation");
}

private MongoOperation buildCreateViewOperation() {
MongoOperation operation = new MongoOperation();
operation.setType("createView");
operation.setCollection(VIEW_NAME);

Map<String, Object> params = new HashMap<>();
params.put("viewOn", SOURCE_COLLECTION);

List<Map<String, Object>> pipeline = new ArrayList<>();
Map<String, Object> matchStage = new HashMap<>();
Map<String, Object> matchQuery = new HashMap<>();
matchQuery.put("status", "active");
matchStage.put("$match", matchQuery);
pipeline.add(matchStage);
params.put("pipeline", pipeline);

operation.setParameters(params);
return operation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,91 @@ void dropIndexByKeysTest() {
assertFalse(indexExistsByKeys("name"), "Index should have been dropped");
}

@Test
@DisplayName("WHEN dropIndex by name is applied twice THEN second call succeeds silently")
void dropIndexByNameIdempotentTest() {
mongoDatabase.createCollection(COLLECTION_NAME);
mongoDatabase.getCollection(COLLECTION_NAME).createIndex(new Document("email", 1),
new com.mongodb.client.model.IndexOptions().name(INDEX_NAME));
assertTrue(indexExists(INDEX_NAME), "Index should exist before drop");

MongoOperation operation = new MongoOperation();
operation.setType("dropIndex");
operation.setCollection(COLLECTION_NAME);
Map<String, Object> params = new HashMap<>();
params.put("indexName", INDEX_NAME);
operation.setParameters(params);

DropIndexOperator operator = new DropIndexOperator(mongoDatabase, operation);
operator.apply(null);
assertFalse(indexExists(INDEX_NAME), "Index should have been dropped");

// Second apply should not throw
operator.apply(null);
assertFalse(indexExists(INDEX_NAME), "Index should still not exist after second drop");
}

@Test
@DisplayName("WHEN dropIndex by keys is applied twice THEN second call succeeds silently")
void dropIndexByKeysIdempotentTest() {
mongoDatabase.createCollection(COLLECTION_NAME);
mongoDatabase.getCollection(COLLECTION_NAME).createIndex(new Document("name", 1));
assertTrue(indexExistsByKeys("name"), "Index should exist before drop");

MongoOperation operation = new MongoOperation();
operation.setType("dropIndex");
operation.setCollection(COLLECTION_NAME);
Map<String, Object> params = new HashMap<>();
Map<String, Object> keys = new HashMap<>();
keys.put("name", 1);
params.put("keys", keys);
operation.setParameters(params);

DropIndexOperator operator = new DropIndexOperator(mongoDatabase, operation);
operator.apply(null);
assertFalse(indexExistsByKeys("name"), "Index should have been dropped");

// Second apply should not throw
operator.apply(null);
assertFalse(indexExistsByKeys("name"), "Index should still not exist after second drop");
}

@Test
@DisplayName("WHEN dropIndex by name targets non-existent index THEN operation is skipped without error")
void dropIndexByNameNonExistentTest() {
mongoDatabase.createCollection(COLLECTION_NAME);

MongoOperation operation = new MongoOperation();
operation.setType("dropIndex");
operation.setCollection(COLLECTION_NAME);
Map<String, Object> params = new HashMap<>();
params.put("indexName", "nonExistentIndex");
operation.setParameters(params);

DropIndexOperator operator = new DropIndexOperator(mongoDatabase, operation);
operator.apply(null);
// Should complete without throwing
}

@Test
@DisplayName("WHEN dropIndex by keys targets non-existent index THEN operation is skipped without error")
void dropIndexByKeysNonExistentTest() {
mongoDatabase.createCollection(COLLECTION_NAME);

MongoOperation operation = new MongoOperation();
operation.setType("dropIndex");
operation.setCollection(COLLECTION_NAME);
Map<String, Object> params = new HashMap<>();
Map<String, Object> keys = new HashMap<>();
keys.put("nonExistentField", 1);
params.put("keys", keys);
operation.setParameters(params);

DropIndexOperator operator = new DropIndexOperator(mongoDatabase, operation);
operator.apply(null);
// Should complete without throwing
}

private boolean indexExists(String indexName) {
List<Document> indexes = mongoDatabase.getCollection(COLLECTION_NAME)
.listIndexes()
Expand Down
Loading
Loading