From 3f3712a48ffc75eb8c81a13ccd800afbaf0c8019 Mon Sep 17 00:00:00 2001 From: jmarkerink Date: Tue, 27 Jan 2026 20:46:13 +0100 Subject: [PATCH 1/2] feat: implemented switch expression operator --- .../mongo/backend/aggregation/Expression.java | 79 ++++++++++++ .../backend/AbstractAggregationTest.java | 122 ++++++++++++++++++ 2 files changed, 201 insertions(+) diff --git a/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java b/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java index 1bef87fa..1378d608 100644 --- a/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java +++ b/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java @@ -1424,6 +1424,85 @@ Object apply(List expressionValue, Document document) { } }, + $switch { + @Override + Object apply(Object expressionValue, Document document) { + Document switchDocument = requireDocument(expressionValue, 40060); + + // Validate that 'branches' field exists + if (!switchDocument.containsKey("branches")) { + throw new MongoServerError(40061, "Missing 'branches' parameter to " + name()); + } + + // Validate unsupported parameters + List supportedKeys = asList("branches", "default"); + for (String key : switchDocument.keySet()) { + if (!supportedKeys.contains(key)) { + throw new MongoServerError(40067, "Unrecognized parameter to " + name() + ": " + key); + } + } + + // Get and validate branches + Object branchesValue = switchDocument.get("branches"); + if (!(branchesValue instanceof Collection)) { + throw new MongoServerError(40061, name() + " expected an array for 'branches', found: " + describeType(branchesValue)); + } + + Collection branches = (Collection) branchesValue; + if (branches.isEmpty()) { + throw new MongoServerError(40060, name() + " requires at least one branch"); + } + + // Evaluate each branch + for (Object branchValue : branches) { + if (!(branchValue instanceof Document)) { + throw new MongoServerError(40062, name() + " expected each branch to be an object, found: " + describeType(branchValue)); + } + + Document branch = (Document) branchValue; + + // Validate branch has required fields + if (!branch.containsKey("case")) { + throw new MongoServerError(40064, name() + " requires each branch have a 'case' expression"); + } + if (!branch.containsKey("then")) { + throw new MongoServerError(40065, name() + " requires each branch have a 'then' expression"); + } + + // Validate branch has no extra fields + for (String key : branch.keySet()) { + if (!asList("case", "then").contains(key)) { + throw new MongoServerError(40063, name() + " found an unknown argument to a branch: " + key); + } + } + + // Evaluate the case expression + Object caseExpression = branch.get("case"); + Object caseResult = evaluate(caseExpression, document); + + // If case is true, evaluate and return the then expression + if (Utils.isTrue(caseResult)) { + Object thenExpression = branch.get("then"); + return evaluate(thenExpression, document); + } + } + + // No case matched, check for default + if (switchDocument.containsKey("default")) { + Object defaultExpression = switchDocument.get("default"); + return evaluate(defaultExpression, document); + } + + // No case matched and no default provided + throw new MongoServerError(40066, name() + " could not find a matching branch for an input, and no default was specified."); + } + + @Override + Object apply(List expressionValue, Document document) { + throw new UnsupportedOperationException("must not be invoked"); + } + }, + $sqrt { @Override Object apply(List expressionValue, Document document) { diff --git a/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractAggregationTest.java b/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractAggregationTest.java index ab6f9e5a..10e2a861 100644 --- a/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractAggregationTest.java +++ b/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractAggregationTest.java @@ -2733,6 +2733,128 @@ void testProjectWithCondition() throws Exception { ); } + @Test + void testAggregateWithSwitch() throws Exception { + collection.insertOne(json("_id: 1, name: 'Dave', qty: 1")); + collection.insertOne(json("_id: 2, name: 'Carol', qty: 5")); + collection.insertOne(json("_id: 3, name: 'Bob', qty: 10")); + collection.insertOne(json("_id: 4, name: 'Alice', qty: 20")); + + List pipeline = jsonList(""" + $project: { + name: 1, + qtyDiscount: { + $switch: { + branches: [ + { case: { $gte: ['$qty', 10] }, then: 0.15 }, + { case: { $gte: ['$qty', 5] }, then: 0.10 }, + { case: { $gte: ['$qty', 1] }, then: 0.05 } + ], + default: 0 + } + } + } + """); + + assertThat(collection.aggregate(pipeline)) + .containsExactlyInAnyOrder( + json("_id: 1, name: 'Dave', qtyDiscount: 0.05"), + json("_id: 2, name: 'Carol', qtyDiscount: 0.10"), + json("_id: 3, name: 'Bob', qtyDiscount: 0.15"), + json("_id: 4, name: 'Alice', qtyDiscount: 0.15") + ); + } + + @Test + void testAggregateWithSwitchDefault() throws Exception { + collection.insertOne(json("_id: 1, status: 'active'")); + collection.insertOne(json("_id: 2, status: 'inactive'")); + collection.insertOne(json("_id: 3, status: 'unknown'")); + + List pipeline = jsonList(""" + $project: { + statusCode: { + $switch: { + branches: [ + { case: { $eq: ['$status', 'active'] }, then: 1 }, + { case: { $eq: ['$status', 'inactive'] }, then: 0 } + ], + default: -1 + } + } + } + """); + + assertThat(collection.aggregate(pipeline)) + .containsExactlyInAnyOrder( + json("_id: 1, statusCode: 1"), + json("_id: 2, statusCode: 0"), + json("_id: 3, statusCode: -1") + ); + } + + @Test + void testAggregateWithSwitchMissingDefault() throws Exception { + collection.insertOne(json("_id: 1, value: 100")); + + List pipeline = jsonList(""" + $project: { + result: { + $switch: { + branches: [ + { case: { $eq: ['$value', 50] }, then: 'fifty' } + ] + } + } + } + """); + + assertThatExceptionOfType(MongoCommandException.class) + .isThrownBy(() -> collection.aggregate(pipeline).first()) + .withMessageContaining("$switch could not find a matching branch for an input, and no default was specified"); + } + + @Test + void testAggregateWithSwitchMissingBranches() throws Exception { + collection.insertOne(json("_id: 1, value: 100")); + + List pipeline = jsonList(""" + $project: { + result: { + $switch: { + default: 'none' + } + } + } + """); + + assertThatExceptionOfType(MongoCommandException.class) + .isThrownBy(() -> collection.aggregate(pipeline).first()) + .withMessageContaining("Missing 'branches' parameter to $switch"); + } + + @Test + void testAggregateWithSwitchInvalidBranch() throws Exception { + collection.insertOne(json("_id: 1, value: 100")); + + List pipeline = jsonList(""" + $project: { + result: { + $switch: { + branches: [ + { case: { $eq: ['$value', 100] } } + ], + default: 'none' + } + } + } + """); + + assertThatExceptionOfType(MongoCommandException.class) + .isThrownBy(() -> collection.aggregate(pipeline).first()) + .withMessageContaining("$switch requires each branch have a 'then' expression"); + } + // https://github.com/bwaldvogel/mongo-java-server/issues/138 @Test public void testAggregateWithGeoNear() throws Exception { From 1f403b27a6874148567d7be19312b90b1359e073 Mon Sep 17 00:00:00 2001 From: jmarkerink Date: Tue, 27 Jan 2026 23:05:08 +0100 Subject: [PATCH 2/2] fix: server errors and related unit tests --- .../mongo/backend/aggregation/Expression.java | 4 +- .../backend/AbstractAggregationTest.java | 45 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java b/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java index 1378d608..ef9bb33f 100644 --- a/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java +++ b/core/src/main/java/de/bwaldvogel/mongo/backend/aggregation/Expression.java @@ -1431,14 +1431,14 @@ Object apply(Object expressionValue, Document document) { // Validate that 'branches' field exists if (!switchDocument.containsKey("branches")) { - throw new MongoServerError(40061, "Missing 'branches' parameter to " + name()); + throw new MongoServerError(40068, name() + " requires at least one branch"); } // Validate unsupported parameters List supportedKeys = asList("branches", "default"); for (String key : switchDocument.keySet()) { if (!supportedKeys.contains(key)) { - throw new MongoServerError(40067, "Unrecognized parameter to " + name() + ": " + key); + throw new MongoServerError(40067, name() + " found an unknown argument: " + key); } } diff --git a/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractAggregationTest.java b/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractAggregationTest.java index 10e2a861..81b349cf 100644 --- a/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractAggregationTest.java +++ b/test-common/src/main/java/de/bwaldvogel/mongo/backend/AbstractAggregationTest.java @@ -2830,7 +2830,28 @@ void testAggregateWithSwitchMissingBranches() throws Exception { assertThatExceptionOfType(MongoCommandException.class) .isThrownBy(() -> collection.aggregate(pipeline).first()) - .withMessageContaining("Missing 'branches' parameter to $switch"); + .withMessageContaining("$switch requires at least one branch"); + } + + @Test + void testAggregateWithSwitchEmptyBranches() throws Exception { + collection.insertOne(json("_id: 1, value: 100")); + + List pipeline = jsonList(""" + $project: { + result: { + $switch: { + branches: [ + ], + default: 'none' + } + } + } + """); + + assertThatExceptionOfType(MongoCommandException.class) + .isThrownBy(() -> collection.aggregate(pipeline).first()) + .withMessageContaining("$switch requires at least one branch"); } @Test @@ -2855,6 +2876,28 @@ void testAggregateWithSwitchInvalidBranch() throws Exception { .withMessageContaining("$switch requires each branch have a 'then' expression"); } + @Test + void testAggregateWithSwitchInvalidArgument() throws Exception { + collection.insertOne(json("_id: 1, value: 100")); + + List pipeline = jsonList(""" + $project: { + result: { + $switch: { + branches: [ + { case: { $eq: ['$value', 100] }, then: 'one hundred' } + ], + default_value: 'none' + } + } + } + """); + + assertThatExceptionOfType(MongoCommandException.class) + .isThrownBy(() -> collection.aggregate(pipeline).first()) + .withMessageContaining("$switch found an unknown argument: default_value"); + } + // https://github.com/bwaldvogel/mongo-java-server/issues/138 @Test public void testAggregateWithGeoNear() throws Exception {