Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Aggregation $facet stage tests - valid argument edge cases.

Covers the positive TEST_COVERAGE.md §4 (Argument Handling) cases for $facet:
valid edge cases such as empty sub-pipelines, many sub-pipelines, and
unusual-but-valid output field names.
"""

from __future__ import annotations

from typing import Any

import pytest

from documentdb_tests.compatibility.tests.core.operator.stages.utils.stage_test_case import (
StageTestCase,
populate_collection,
)
from documentdb_tests.framework.assertions import assertResult
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.parametrize import pytest_params

DOCS = [{"_id": 1, "cat": "A"}, {"_id": 2, "cat": "B"}]

LONG_NAME = "f" * 300

# Property [Valid Edge Cases]: empty sub-pipelines, many sub-pipelines, and
# unusual-but-valid output field names are accepted.
FACET_ARGUMENT_SUCCESS_TESTS: list[StageTestCase] = [
Comment thread
PatersonProjects marked this conversation as resolved.
StageTestCase(
id="many_subpipelines",
docs=DOCS,
pipeline=[{"$facet": {f"f{i}": [{"$count": "n"}] for i in range(50)}}],
expected=[{f"f{i}": [{"n": 2}] for i in range(50)}],
msg="A $facet with many valid sub-pipelines should complete without error",
),
StageTestCase(
id="very_long_field_name",
docs=DOCS,
pipeline=[{"$facet": {LONG_NAME: [{"$count": "n"}]}}],
expected=[{LONG_NAME: [{"n": 2}]}],
msg="$facet should accept and preserve a very long output field name",
),
StageTestCase(
id="unicode_field_name",
docs=DOCS,
pipeline=[{"$facet": {"日本語": [{"$count": "n"}]}}],
expected=[{"日本語": [{"n": 2}]}],
msg="$facet should accept and correctly retrieve a unicode output field name",
),
StageTestCase(
id="space_in_field_name",
docs=DOCS,
pipeline=[{"$facet": {"my field": [{"$count": "n"}]}}],
expected=[{"my field": [{"n": 2}]}],
msg="$facet should accept an output field name containing a space",
),
StageTestCase(
id="hyphen_in_field_name",
docs=DOCS,
pipeline=[{"$facet": {"my-field": [{"$count": "n"}]}}],
expected=[{"my-field": [{"n": 2}]}],
msg="$facet should accept an output field name containing a hyphen",
),
StageTestCase(
id="underscore_in_field_name",
docs=DOCS,
pipeline=[{"$facet": {"my_field": [{"$count": "n"}]}}],
expected=[{"my_field": [{"n": 2}]}],
msg="$facet should accept an output field name containing an underscore",
),
StageTestCase(
id="numeric_only_field_name",
docs=DOCS,
pipeline=[{"$facet": {"123": [{"$count": "n"}]}}],
expected=[{"123": [{"n": 2}]}],
msg="$facet should accept a numeric-only output field name",
),
StageTestCase(
id="at_sign_field_name",
docs=DOCS,
pipeline=[{"$facet": {"@field": [{"$count": "n"}]}}],
expected=[{"@field": [{"n": 2}]}],
msg="$facet should accept an output field name beginning with an at-sign",
),
]

# Combined list for parametrization.
FACET_ARGUMENT_TESTS = FACET_ARGUMENT_SUCCESS_TESTS


@pytest.mark.aggregate
@pytest.mark.parametrize("test_case", pytest_params(FACET_ARGUMENT_TESTS))
def test_facet_argument_validation(collection, test_case: StageTestCase):
"""Test $facet valid argument edge cases."""
coll = populate_collection(collection, test_case)
command: dict[str, Any] = {
"aggregate": coll.name,
"pipeline": test_case.pipeline,
"cursor": {},
}
command.update(test_case.extra_command_fields)
result = execute_command(coll, command)
assertResult(
result,
expected=test_case.expected,
error_code=test_case.error_code,
msg=test_case.msg,
ignore_doc_order=test_case.ignore_doc_order,
ignore_order_in=test_case.ignore_order_in,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""Aggregation $facet stage tests - BSON type pass-through.

Verifies that $facet passes documents of every standard BSON type through to
sub-pipelines without altering their values (TEST_COVERAGE.md §1 Data Type
Coverage, applied to the pass-through behaviour of the stage).
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any

import pytest
from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp

from documentdb_tests.compatibility.tests.core.operator.stages.utils.stage_test_case import (
StageTestCase,
populate_collection,
)
from documentdb_tests.framework.assertions import assertResult
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.parametrize import pytest_params

# Property [BSON Pass-Through]: $facet preserves one document of each standard
# BSON type inside its sub-pipelines.
FACET_BSON_PASSTHROUGH_TESTS: list[StageTestCase] = [
StageTestCase(
id="int32",
docs=[{"_id": 1, "val": 42}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": 42}]}],
msg="$facet should pass an int32 value through to sub-pipelines unchanged",
),
StageTestCase(
id="int64",
docs=[{"_id": 1, "val": Int64(42)}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": Int64(42)}]}],
msg="$facet should pass an int64 value through to sub-pipelines unchanged",
),
StageTestCase(
id="double",
docs=[{"_id": 1, "val": 1.5}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": 1.5}]}],
msg="$facet should pass a double value through to sub-pipelines unchanged",
),
StageTestCase(
id="decimal128",
docs=[{"_id": 1, "val": Decimal128("1.5")}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": Decimal128("1.5")}]}],
msg="$facet should pass a decimal128 value through to sub-pipelines unchanged",
),
StageTestCase(
id="string",
docs=[{"_id": 1, "val": "hello"}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": "hello"}]}],
msg="$facet should pass a string value through to sub-pipelines unchanged",
),
StageTestCase(
id="bool",
docs=[{"_id": 1, "val": True}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": True}]}],
msg="$facet should pass a boolean value through to sub-pipelines unchanged",
),
StageTestCase(
id="date",
docs=[{"_id": 1, "val": datetime(2024, 1, 1, tzinfo=timezone.utc)}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": datetime(2024, 1, 1, tzinfo=timezone.utc)}]}],
msg="$facet should pass a date value through to sub-pipelines unchanged",
),
StageTestCase(
id="null",
docs=[{"_id": 1, "val": None}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": None}]}],
msg="$facet should pass a null value through to sub-pipelines unchanged",
),
StageTestCase(
id="object",
docs=[{"_id": 1, "val": {"a": 1, "b": {"c": 2}}}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": {"a": 1, "b": {"c": 2}}}]}],
msg="$facet should pass an object value through to sub-pipelines unchanged",
),
StageTestCase(
id="array",
docs=[{"_id": 1, "val": [1, "two", 3.0]}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": [1, "two", 3.0]}]}],
msg="$facet should pass an array value through to sub-pipelines unchanged",
),
StageTestCase(
id="objectId",
docs=[{"_id": 1, "val": ObjectId("507f1f77bcf86cd799439011")}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": ObjectId("507f1f77bcf86cd799439011")}]}],
msg="$facet should pass an objectId value through to sub-pipelines unchanged",
),
StageTestCase(
id="binData",
docs=[{"_id": 1, "val": Binary(b"payload", 128)}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": Binary(b"payload", 128)}]}],
msg="$facet should pass a binary value through to sub-pipelines unchanged",
),
StageTestCase(
id="timestamp",
docs=[{"_id": 1, "val": Timestamp(123, 4)}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": Timestamp(123, 4)}]}],
msg="$facet should pass a timestamp value through to sub-pipelines unchanged",
),
StageTestCase(
id="minKey",
docs=[{"_id": 1, "val": MinKey()}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": MinKey()}]}],
msg="$facet should pass a minKey value through to sub-pipelines unchanged",
),
StageTestCase(
id="maxKey",
docs=[{"_id": 1, "val": MaxKey()}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": MaxKey()}]}],
msg="$facet should pass a maxKey value through to sub-pipelines unchanged",
),
StageTestCase(
id="regex",
docs=[{"_id": 1, "val": Regex(r"^hello", "i")}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": Regex(r"^hello", "i")}]}],
msg="$facet should pass a regex value through to sub-pipelines unchanged",
),
StageTestCase(
id="javascript",
docs=[{"_id": 1, "val": Code("function() {}")}],
pipeline=[{"$facet": {"docs": [{"$match": {"_id": 1}}]}}],
expected=[{"docs": [{"_id": 1, "val": Code("function() {}")}]}],
msg="$facet should pass a javascript value through to sub-pipelines unchanged",
),
]


@pytest.mark.aggregate
@pytest.mark.parametrize("test_case", pytest_params(FACET_BSON_PASSTHROUGH_TESTS))
def test_facet_bson_passthrough(collection, test_case: StageTestCase):
"""$facet passes a document field of each BSON type through unchanged."""
coll = populate_collection(collection, test_case)
command: dict[str, Any] = {
"aggregate": coll.name,
"pipeline": test_case.pipeline,
"cursor": {},
}
command.update(test_case.extra_command_fields)
result = execute_command(coll, command)
assertResult(
result,
expected=test_case.expected,
error_code=test_case.error_code,
msg=test_case.msg,
ignore_doc_order=test_case.ignore_doc_order,
ignore_order_in=test_case.ignore_order_in,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Aggregation $facet stage tests - collation wiring.

Per TEST_COVERAGE.md §19 (Foundational Spec Behaviors), collation semantics are
tested comprehensively under tests/core/collation/. These tests only verify
that $facet correctly wires the command-level collation into its sub-pipelines
(e.g. a case-insensitive $match and $sortByCount respect the collation).
"""

from __future__ import annotations

from typing import Any

import pytest

from documentdb_tests.compatibility.tests.core.operator.stages.utils.stage_test_case import (
StageTestCase,
populate_collection,
)
from documentdb_tests.framework.assertions import assertResult
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.parametrize import pytest_params

CI = {"locale": "en", "strength": 2} # case-insensitive collation
DOCS = [{"_id": 1, "cat": "a"}, {"_id": 2, "cat": "A"}, {"_id": 3, "cat": "b"}]

FACET_COLLATION_TESTS: list[StageTestCase] = [
StageTestCase(
id="applies_to_subpipeline_match",
docs=DOCS,
pipeline=[
{
"$facet": {
"ci": [{"$match": {"cat": "a"}}, {"$sort": {"_id": 1}}],
"total": [{"$count": "n"}],
}
}
],
expected=[{"ci": [{"_id": 1, "cat": "a"}, {"_id": 2, "cat": "A"}], "total": [{"n": 3}]}],
extra_command_fields={"collation": CI},
msg="Case-insensitive collation should apply to a $match in a sub-pipeline",
),
StageTestCase(
id="applies_to_subpipeline_sortByCount",
docs=DOCS,
pipeline=[
{
"$facet": {
"byCat": [
{"$sortByCount": "$cat"},
{"$project": {"_id": 0, "count": 1}},
{"$sort": {"count": -1}},
],
"total": [{"$count": "n"}],
}
}
],
expected=[{"byCat": [{"count": 2}, {"count": 1}], "total": [{"n": 3}]}],
extra_command_fields={"collation": CI},
msg="Case-insensitive collation should merge 'a'/'A' in $sortByCount grouping",
),
]


@pytest.mark.aggregate
@pytest.mark.collation
@pytest.mark.parametrize("test_case", pytest_params(FACET_COLLATION_TESTS))
def test_facet_collation(collection, test_case: StageTestCase):
"""A command-level collation applies inside $facet sub-pipelines."""
coll = populate_collection(collection, test_case)
command: dict[str, Any] = {
"aggregate": coll.name,
"pipeline": test_case.pipeline,
"cursor": {},
}
command.update(test_case.extra_command_fields)
result = execute_command(coll, command)
assertResult(
result,
expected=test_case.expected,
error_code=test_case.error_code,
msg=test_case.msg,
ignore_doc_order=test_case.ignore_doc_order,
ignore_order_in=test_case.ignore_order_in,
)
Loading
Loading