From 91e7e79a4a827f0c915246f1943e28bfef2c7f3c Mon Sep 17 00:00:00 2001 From: Thanh Tong Date: Fri, 7 Aug 2026 18:28:43 +0000 Subject: [PATCH] Add $rank and $denseRank window operator compatibility tests Signed-off-by: Thanh Tong --- ...st_window_denseRank_argument_validation.py | 227 ++++++++++++++++++ .../test_window_denseRank_order_dependence.py | 166 +++++++++++++ .../test_window_denseRank_tie_handling.py | 196 +++++++++++++++ .../test_window_rank_argument_validation.py | 227 ++++++++++++++++++ .../rank/test_window_rank_order_dependence.py | 166 +++++++++++++ .../rank/test_window_rank_tie_handling.py | 201 ++++++++++++++++ 6 files changed, 1183 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_argument_validation.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_order_dependence.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_tie_handling.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_argument_validation.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_order_dependence.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_tie_handling.py diff --git a/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_argument_validation.py b/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_argument_validation.py new file mode 100644 index 000000000..1b38b8fe8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_argument_validation.py @@ -0,0 +1,227 @@ +""" +Tests for $denseRank argument validation in window context. + +$denseRank is a frameless rank operator with a fixed accepted shape: +- Its value must be exactly the empty object `{}` — any other value is rejected. +- It takes no other arguments, so a `window` key (or any extra key) is rejected. +- It requires a top-level `sortBy` with exactly one element — omitted, empty, + and multi-field sortBy are all rejected. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.window.utils.window_test_case import ( + run_window_operator, +) +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess +from documentdb_tests.framework.error_codes import ( + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + RANK_STYLE_WINDOW_NON_EMPTY_ARG_ERROR, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, +) +from documentdb_tests.framework.executor import execute_command + +SINGLE_DOC = [{"_id": 1, "partition": "A", "value": 10}] + + +# Property [Accepted Shape]: $denseRank takes exactly `{}` and no other arguments. + + +def test_denseRank_empty_object_accepted(collection): + """$denseRank with `{}` as its value is the valid form.""" + result = run_window_operator(collection, "$denseRank", SINGLE_DOC, expression={}) + expected = [{"_id": 1, "partition": "A", "value": 10, "result": 1}] + assertSuccess(result, expected, msg="$denseRank accepts empty object") + + +# Property [Non-Empty Value Rejected]: any value other than `{}` errors with 5371603. + +NON_EMPTY_ARGS = [ + ("non_empty_object", {"a": 1}), + ("field_path_string", "$value"), + ("empty_string", ""), + ("integer", 1), + ("zero", 0), + ("double", 1.5), + ("bool_true", True), + ("bool_false", False), + ("null", None), + ("empty_array", []), + ("non_empty_array", [1, 2]), +] + + +@pytest.mark.parametrize("case_id,arg", NON_EMPTY_ARGS, ids=[c[0] for c in NON_EMPTY_ARGS]) +def test_denseRank_non_empty_value_errors(collection, case_id, arg): + """$denseRank rejects any value that is not the empty object.""" + result = run_window_operator(collection, "$denseRank", SINGLE_DOC, expression=arg) + assertFailureCode( + result, + RANK_STYLE_WINDOW_NON_EMPTY_ARG_ERROR, + msg=f"$denseRank rejects {case_id} value — only '{{}}' is valid", + ) + + +# Property [Frameless]: $denseRank takes no other arguments, including `window`. + + +@pytest.mark.parametrize( + "case_id,window", + [ + ("documents_cumulative", {"documents": ["unbounded", "current"]}), + ("documents_whole_partition", {"documents": ["unbounded", "unbounded"]}), + ("documents_sliding", {"documents": [-1, 1]}), + ("range_bounds", {"range": ["unbounded", "current"]}), + ], + ids=["documents_cumulative", "documents_whole_partition", "documents_sliding", "range_bounds"], +) +def test_denseRank_window_key_errors(collection, case_id, window): + """$denseRank is frameless — specifying a `window` key is rejected.""" + result = run_window_operator(collection, "$denseRank", SINGLE_DOC, window=window, expression={}) + assertFailureCode( + result, + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + msg=f"$denseRank rejects a {case_id} window — it is frameless", + ) + + +def test_denseRank_unknown_key_in_output_field_errors(collection): + """An unknown key alongside $denseRank in the output field is rejected.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": {"_id": 1}, + "output": {"result": {"$denseRank": {}, "unknownKey": 1}}, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + msg="unknown key alongside $denseRank rejected", + ) + + +def test_denseRank_second_operator_in_output_field_errors(collection): + """Another window operator in the same output field as $denseRank is rejected.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": {"_id": 1}, + "output": {"result": {"$denseRank": {}, "$rank": {}}}, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + msg="a second window operator alongside $denseRank rejected", + ) + + +def test_denseRank_window_key_errors_on_empty_collection(collection): + """The `window` rejection is a parse-time error — it fires with no documents.""" + result = run_window_operator( + collection, + "$denseRank", + [], + window={"documents": ["unbounded", "current"]}, + expression={}, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + msg="parse-time window rejection fires on an empty collection", + ) + + +# Property [sortBy Requirement]: $denseRank needs a top-level sortBy with one element. + + +def test_denseRank_sortBy_omitted_errors(collection): + """$denseRank requires sortBy — omitting it is rejected.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "output": {"result": {"$denseRank": {}}}, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, + msg="$denseRank requires a sortBy expression", + ) + + +def test_denseRank_sortBy_empty_object_errors(collection): + """An empty sortBy object has no elements, so $denseRank rejects it.""" + result = run_window_operator(collection, "$denseRank", SINGLE_DOC, sort_by={}, expression={}) + assertFailureCode( + result, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, + msg="$denseRank rejects an empty sortBy object", + ) + + +def test_denseRank_multi_field_sortBy_errors(collection): + """$denseRank requires exactly one sortBy element — two fields are rejected.""" + result = run_window_operator( + collection, "$denseRank", SINGLE_DOC, sort_by={"value": -1, "_id": 1}, expression={} + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, + msg="$denseRank rejects a multi-field sortBy", + ) + + +def test_denseRank_sortBy_error_on_empty_collection(collection): + """The sortBy requirement is validated at parse time — it fires with no documents.""" + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "output": {"result": {"$denseRank": {}}}, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, + msg="parse-time sortBy validation fires on an empty collection", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_order_dependence.py b/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_order_dependence.py new file mode 100644 index 000000000..01afc250e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_order_dependence.py @@ -0,0 +1,166 @@ +""" +Tests for $denseRank order dependence and partition semantics. + +$denseRank is order-dependent: ranks are assigned in sortBy order, so +reversing the sort direction or sorting on a different field produces different +ranks for the same documents. Ranking restarts at 1 in every partition. +Documents with the same sort value share the same rank, and the next distinct +value gets the immediately following rank (no gaps). +""" + +from documentdb_tests.compatibility.tests.core.operator.window.utils.window_test_case import ( + BASIC_DOCS, + run_window_operator, +) +from documentdb_tests.framework.assertions import assertSuccess + +# Property [Order Dependence]: changing sortBy changes the assigned ranks. + + +def test_denseRank_ascending_sort(collection): + """Ascending sort assigns ranks in ascending order of the sort field.""" + result = run_window_operator( + collection, + "$denseRank", + BASIC_DOCS, + sort_by={"_id": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "value": 10, "result": 1}, + {"_id": 2, "partition": "A", "value": 20, "result": 2}, + {"_id": 3, "partition": "A", "value": 30, "result": 3}, + {"_id": 4, "partition": "A", "value": 40, "result": 4}, + {"_id": 5, "partition": "A", "value": 50, "result": 5}, + ] + assertSuccess(result, expected, msg="ascending sort ranks documents 1..5 in _id order") + + +def test_denseRank_descending_sort(collection): + """Descending sort reverses the assigned ranks — order-dependent operator.""" + result = run_window_operator( + collection, + "$denseRank", + BASIC_DOCS, + sort_by={"_id": -1}, + expression={}, + ) + expected = [ + {"_id": 5, "partition": "A", "value": 50, "result": 1}, + {"_id": 4, "partition": "A", "value": 40, "result": 2}, + {"_id": 3, "partition": "A", "value": 30, "result": 3}, + {"_id": 2, "partition": "A", "value": 20, "result": 4}, + {"_id": 1, "partition": "A", "value": 10, "result": 5}, + ] + assertSuccess( + result, + expected, + msg="descending sort reverses ranks — different result than ascending", + ) + + +def test_denseRank_sort_on_different_field(collection): + """Sorting on a different field assigns ranks by that field's order.""" + docs = [ + {"_id": 1, "partition": "A", "value": 50}, + {"_id": 2, "partition": "A", "value": 10}, + {"_id": 3, "partition": "A", "value": 30}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"value": 1}, + expression={}, + ) + expected = [ + {"_id": 2, "partition": "A", "value": 10, "result": 1}, + {"_id": 3, "partition": "A", "value": 30, "result": 2}, + {"_id": 1, "partition": "A", "value": 50, "result": 3}, + ] + assertSuccess(result, expected, msg="output follows the value sort order; ranks 1..3 by value") + + +# Property [Partition Isolation]: ranking restarts at 1 in each partition. + + +def test_denseRank_restarts_per_partition(collection): + """Each partition is ranked independently starting from 1.""" + docs = [ + {"_id": 1, "partition": "A", "value": 10}, + {"_id": 2, "partition": "A", "value": 20}, + {"_id": 3, "partition": "B", "value": 30}, + {"_id": 4, "partition": "B", "value": 40}, + {"_id": 5, "partition": "B", "value": 50}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"_id": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "value": 10, "result": 1}, + {"_id": 2, "partition": "A", "value": 20, "result": 2}, + {"_id": 3, "partition": "B", "value": 30, "result": 1}, + {"_id": 4, "partition": "B", "value": 40, "result": 2}, + {"_id": 5, "partition": "B", "value": 50, "result": 3}, + ] + assertSuccess(result, expected, msg="ranking restarts at 1 in each partition") + + +def test_denseRank_without_partitionBy(collection): + """Omitting partitionBy treats the whole collection as a single partition.""" + docs = [ + {"_id": 1, "partition": "A", "value": 10}, + {"_id": 2, "partition": "B", "value": 20}, + {"_id": 3, "partition": "C", "value": 30}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"_id": 1}, + partition_by=None, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "value": 10, "result": 1}, + {"_id": 2, "partition": "B", "value": 20, "result": 2}, + {"_id": 3, "partition": "C", "value": 30, "result": 3}, + ] + assertSuccess( + result, expected, msg="omitted partitionBy ranks the whole collection continuously" + ) + + +# Property [Empty and Single-Document Input]: smallest partition sizes rank correctly. + + +def test_denseRank_single_document_partition(collection): + """A single-document partition gets rank 1.""" + result = run_window_operator( + collection, + "$denseRank", + [{"_id": 1, "partition": "A", "value": 10}], + sort_by={"_id": 1}, + expression={}, + ) + assertSuccess( + result, + [{"_id": 1, "partition": "A", "value": 10, "result": 1}], + msg="single-document partition gets rank 1", + ) + + +def test_denseRank_empty_collection(collection): + """$denseRank on an empty collection returns no documents without error.""" + result = run_window_operator( + collection, + "$denseRank", + [], + sort_by={"_id": 1}, + expression={}, + ) + assertSuccess(result, [], msg="empty collection produces no documents") diff --git a/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_tie_handling.py b/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_tie_handling.py new file mode 100644 index 000000000..32a377e16 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/window/denseRank/test_window_denseRank_tie_handling.py @@ -0,0 +1,196 @@ +""" +Tests for $denseRank tie handling in window context. + +$denseRank is a rank operator that assigns the same rank to documents with +equal sort values and does NOT skip rank positions after a tie. This is the +"dense" behavior: if two documents tie at rank 1, the next distinct sort value +gets rank 2 (not 3). This distinguishes $denseRank from $rank (which skips: +1, 1, 3) and from $documentNumber (which never shares: 1, 2, 3). These tests +cover no ties, all ties, and partial ties at the beginning, middle, and end +of a partition, as well as multiple tie groups and cross-partition isolation. +""" + +from documentdb_tests.compatibility.tests.core.operator.window.utils.window_test_case import ( + run_window_operator, +) +from documentdb_tests.framework.assertions import assertSuccess + + +def test_denseRank_no_ties(collection): + """With all-distinct sort values, $denseRank assigns sequential 1, 2, 3, 4, 5.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 20}, + {"_id": 3, "partition": "A", "score": 30}, + {"_id": 4, "partition": "A", "score": 40}, + {"_id": 5, "partition": "A", "score": 50}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 20, "result": 2}, + {"_id": 3, "partition": "A", "score": 30, "result": 3}, + {"_id": 4, "partition": "A", "score": 40, "result": 4}, + {"_id": 5, "partition": "A", "score": 50, "result": 5}, + ] + assertSuccess(result, expected, msg="distinct sort values get sequential ranks") + + +def test_denseRank_all_ties(collection): + """When every sort value ties, $denseRank assigns rank 1 to all documents.""" + docs = [ + {"_id": 1, "partition": "A", "score": 50}, + {"_id": 2, "partition": "A", "score": 50}, + {"_id": 3, "partition": "A", "score": 50}, + {"_id": 4, "partition": "A", "score": 50}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 50, "result": 1}, + {"_id": 2, "partition": "A", "score": 50, "result": 1}, + {"_id": 3, "partition": "A", "score": 50, "result": 1}, + {"_id": 4, "partition": "A", "score": 50, "result": 1}, + ] + assertSuccess(result, expected, msg="all-tie partition gets rank 1 for every document") + + +def test_denseRank_partial_tie_at_beginning(collection): + """A tie at the start shares rank 1, next distinct value gets rank 2 (dense).""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 10}, + {"_id": 3, "partition": "A", "score": 20}, + {"_id": 4, "partition": "A", "score": 30}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 10, "result": 1}, + {"_id": 3, "partition": "A", "score": 20, "result": 2}, + {"_id": 4, "partition": "A", "score": 30, "result": 3}, + ] + assertSuccess(result, expected, msg="tie at beginning yields 1,1,2,3 — no gap after tie") + + +def test_denseRank_partial_tie_in_middle(collection): + """A tie in the middle shares the same rank, next value is rank+1 (dense).""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 20}, + {"_id": 3, "partition": "A", "score": 20}, + {"_id": 4, "partition": "A", "score": 30}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 20, "result": 2}, + {"_id": 3, "partition": "A", "score": 20, "result": 2}, + {"_id": 4, "partition": "A", "score": 30, "result": 3}, + ] + assertSuccess(result, expected, msg="tie in middle yields 1,2,2,3 — no gap after tie") + + +def test_denseRank_partial_tie_at_end(collection): + """A tie at the end shares the same rank — no gap before the tie.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 20}, + {"_id": 3, "partition": "A", "score": 30}, + {"_id": 4, "partition": "A", "score": 30}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 20, "result": 2}, + {"_id": 3, "partition": "A", "score": 30, "result": 3}, + {"_id": 4, "partition": "A", "score": 30, "result": 3}, + ] + assertSuccess(result, expected, msg="tie at end yields 1,2,3,3 — tied docs share rank") + + +def test_denseRank_multiple_tie_groups(collection): + """Multiple tie groups: each group shares a rank, no gaps between groups.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 10}, + {"_id": 3, "partition": "A", "score": 20}, + {"_id": 4, "partition": "A", "score": 20}, + {"_id": 5, "partition": "A", "score": 30}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 10, "result": 1}, + {"_id": 3, "partition": "A", "score": 20, "result": 2}, + {"_id": 4, "partition": "A", "score": 20, "result": 2}, + {"_id": 5, "partition": "A", "score": 30, "result": 3}, + ] + assertSuccess( + result, expected, msg="two tie groups yield 1,1,2,2,3 — dense ranking with no gaps" + ) + + +def test_denseRank_tie_across_partitions(collection): + """Ties reset independently per partition — each partition ranks from 1.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 10}, + {"_id": 3, "partition": "A", "score": 20}, + {"_id": 4, "partition": "B", "score": 50}, + {"_id": 5, "partition": "B", "score": 50}, + {"_id": 6, "partition": "B", "score": 60}, + ] + result = run_window_operator( + collection, + "$denseRank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 10, "result": 1}, + {"_id": 3, "partition": "A", "score": 20, "result": 2}, + {"_id": 4, "partition": "B", "score": 50, "result": 1}, + {"_id": 5, "partition": "B", "score": 50, "result": 1}, + {"_id": 6, "partition": "B", "score": 60, "result": 2}, + ] + assertSuccess( + result, expected, msg="ties reset per partition — each partition starts at rank 1" + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_argument_validation.py b/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_argument_validation.py new file mode 100644 index 000000000..aacb5aea0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_argument_validation.py @@ -0,0 +1,227 @@ +""" +Tests for $rank argument validation in window context. + +$rank is a frameless rank operator with a fixed accepted shape: +- Its value must be exactly the empty object `{}` — any other value is rejected. +- It takes no other arguments, so a `window` key (or any extra key) is rejected. +- It requires a top-level `sortBy` with exactly one element — omitted, empty, + and multi-field sortBy are all rejected. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.window.utils.window_test_case import ( + run_window_operator, +) +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess +from documentdb_tests.framework.error_codes import ( + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + RANK_STYLE_WINDOW_NON_EMPTY_ARG_ERROR, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, +) +from documentdb_tests.framework.executor import execute_command + +SINGLE_DOC = [{"_id": 1, "partition": "A", "value": 10}] + + +# Property [Accepted Shape]: $rank takes exactly `{}` and no other arguments. + + +def test_rank_empty_object_accepted(collection): + """$rank with `{}` as its value is the valid form.""" + result = run_window_operator(collection, "$rank", SINGLE_DOC, expression={}) + expected = [{"_id": 1, "partition": "A", "value": 10, "result": 1}] + assertSuccess(result, expected, msg="$rank accepts empty object") + + +# Property [Non-Empty Value Rejected]: any value other than `{}` errors with 5371603. + +NON_EMPTY_ARGS = [ + ("non_empty_object", {"a": 1}), + ("field_path_string", "$value"), + ("empty_string", ""), + ("integer", 1), + ("zero", 0), + ("double", 1.5), + ("bool_true", True), + ("bool_false", False), + ("null", None), + ("empty_array", []), + ("non_empty_array", [1, 2]), +] + + +@pytest.mark.parametrize("case_id,arg", NON_EMPTY_ARGS, ids=[c[0] for c in NON_EMPTY_ARGS]) +def test_rank_non_empty_value_errors(collection, case_id, arg): + """$rank rejects any value that is not the empty object.""" + result = run_window_operator(collection, "$rank", SINGLE_DOC, expression=arg) + assertFailureCode( + result, + RANK_STYLE_WINDOW_NON_EMPTY_ARG_ERROR, + msg=f"$rank rejects {case_id} value — only '{{}}' is valid", + ) + + +# Property [Frameless]: $rank takes no other arguments, including `window`. + + +@pytest.mark.parametrize( + "case_id,window", + [ + ("documents_cumulative", {"documents": ["unbounded", "current"]}), + ("documents_whole_partition", {"documents": ["unbounded", "unbounded"]}), + ("documents_sliding", {"documents": [-1, 1]}), + ("range_bounds", {"range": ["unbounded", "current"]}), + ], + ids=["documents_cumulative", "documents_whole_partition", "documents_sliding", "range_bounds"], +) +def test_rank_window_key_errors(collection, case_id, window): + """$rank is frameless — specifying a `window` key is rejected.""" + result = run_window_operator(collection, "$rank", SINGLE_DOC, window=window, expression={}) + assertFailureCode( + result, + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + msg=f"$rank rejects a {case_id} window — it is frameless", + ) + + +def test_rank_unknown_key_in_output_field_errors(collection): + """An unknown key alongside $rank in the output field is rejected.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": {"_id": 1}, + "output": {"result": {"$rank": {}, "unknownKey": 1}}, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + msg="unknown key alongside $rank rejected", + ) + + +def test_rank_second_operator_in_output_field_errors(collection): + """Another window operator in the same output field as $rank is rejected.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": {"_id": 1}, + "output": {"result": {"$rank": {}, "$documentNumber": {}}}, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + msg="a second window operator alongside $rank rejected", + ) + + +def test_rank_window_key_errors_on_empty_collection(collection): + """The `window` rejection is a parse-time error — it fires with no documents.""" + result = run_window_operator( + collection, + "$rank", + [], + window={"documents": ["unbounded", "current"]}, + expression={}, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_EXTRA_ARGS_ERROR, + msg="parse-time window rejection fires on an empty collection", + ) + + +# Property [sortBy Requirement]: $rank needs a top-level sortBy with one element. + + +def test_rank_sortBy_omitted_errors(collection): + """$rank requires sortBy — omitting it is rejected.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "output": {"result": {"$rank": {}}}, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, + msg="$rank requires a sortBy expression", + ) + + +def test_rank_sortBy_empty_object_errors(collection): + """An empty sortBy object has no elements, so $rank rejects it.""" + result = run_window_operator(collection, "$rank", SINGLE_DOC, sort_by={}, expression={}) + assertFailureCode( + result, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, + msg="$rank rejects an empty sortBy object", + ) + + +def test_rank_multi_field_sortBy_errors(collection): + """$rank requires exactly one sortBy element — two fields are rejected.""" + result = run_window_operator( + collection, "$rank", SINGLE_DOC, sort_by={"value": -1, "_id": 1}, expression={} + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, + msg="$rank rejects a multi-field sortBy", + ) + + +def test_rank_sortBy_error_on_empty_collection(collection): + """The sortBy requirement is validated at parse time — it fires with no documents.""" + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "output": {"result": {"$rank": {}}}, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, + RANK_STYLE_WINDOW_SORTBY_ONE_ELEMENT_ERROR, + msg="parse-time sortBy validation fires on an empty collection", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_order_dependence.py b/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_order_dependence.py new file mode 100644 index 000000000..49a728aa3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_order_dependence.py @@ -0,0 +1,166 @@ +""" +Tests for $rank order dependence and partition semantics. + +$rank is order-dependent: ranks are assigned in sortBy order, so reversing the +sort direction or sorting on a different field produces different ranks for the +same documents. Ranking restarts at 1 in every partition. Unlike +$documentNumber, tied documents share the same rank and the next rank skips +positions accordingly. +""" + +from documentdb_tests.compatibility.tests.core.operator.window.utils.window_test_case import ( + BASIC_DOCS, + run_window_operator, +) +from documentdb_tests.framework.assertions import assertSuccess + +# Property [Order Dependence]: changing sortBy changes the assigned ranks. + + +def test_rank_ascending_sort(collection): + """Ascending sort assigns sequential ranks when all values are distinct.""" + result = run_window_operator( + collection, + "$rank", + BASIC_DOCS, + sort_by={"_id": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "value": 10, "result": 1}, + {"_id": 2, "partition": "A", "value": 20, "result": 2}, + {"_id": 3, "partition": "A", "value": 30, "result": 3}, + {"_id": 4, "partition": "A", "value": 40, "result": 4}, + {"_id": 5, "partition": "A", "value": 50, "result": 5}, + ] + assertSuccess(result, expected, msg="ascending sort ranks documents 1..5 in _id order") + + +def test_rank_descending_sort(collection): + """Descending sort reverses the assigned ranks — order-dependent operator.""" + result = run_window_operator( + collection, + "$rank", + BASIC_DOCS, + sort_by={"_id": -1}, + expression={}, + ) + expected = [ + {"_id": 5, "partition": "A", "value": 50, "result": 1}, + {"_id": 4, "partition": "A", "value": 40, "result": 2}, + {"_id": 3, "partition": "A", "value": 30, "result": 3}, + {"_id": 2, "partition": "A", "value": 20, "result": 4}, + {"_id": 1, "partition": "A", "value": 10, "result": 5}, + ] + assertSuccess( + result, + expected, + msg="descending sort reverses ranks — different result than ascending", + ) + + +def test_rank_sort_on_different_field(collection): + """Sorting on a different field assigns ranks by that field's order.""" + docs = [ + {"_id": 1, "partition": "A", "value": 50}, + {"_id": 2, "partition": "A", "value": 10}, + {"_id": 3, "partition": "A", "value": 30}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"value": 1}, + expression={}, + ) + expected = [ + {"_id": 2, "partition": "A", "value": 10, "result": 1}, + {"_id": 3, "partition": "A", "value": 30, "result": 2}, + {"_id": 1, "partition": "A", "value": 50, "result": 3}, + ] + assertSuccess(result, expected, msg="output follows the value sort order; ranks 1..3 by value") + + +# Property [Partition Isolation]: ranking restarts at 1 in each partition. + + +def test_rank_restarts_per_partition(collection): + """Each partition is ranked independently starting from 1.""" + docs = [ + {"_id": 1, "partition": "A", "value": 10}, + {"_id": 2, "partition": "A", "value": 20}, + {"_id": 3, "partition": "B", "value": 30}, + {"_id": 4, "partition": "B", "value": 40}, + {"_id": 5, "partition": "B", "value": 50}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"_id": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "value": 10, "result": 1}, + {"_id": 2, "partition": "A", "value": 20, "result": 2}, + {"_id": 3, "partition": "B", "value": 30, "result": 1}, + {"_id": 4, "partition": "B", "value": 40, "result": 2}, + {"_id": 5, "partition": "B", "value": 50, "result": 3}, + ] + assertSuccess(result, expected, msg="ranking restarts at 1 in each partition") + + +def test_rank_without_partitionBy(collection): + """Omitting partitionBy treats the whole collection as a single partition.""" + docs = [ + {"_id": 1, "partition": "A", "value": 10}, + {"_id": 2, "partition": "B", "value": 20}, + {"_id": 3, "partition": "C", "value": 30}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"_id": 1}, + partition_by=None, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "value": 10, "result": 1}, + {"_id": 2, "partition": "B", "value": 20, "result": 2}, + {"_id": 3, "partition": "C", "value": 30, "result": 3}, + ] + assertSuccess( + result, expected, msg="omitted partitionBy ranks the whole collection continuously" + ) + + +# Property [Empty and Single-Document Input]: smallest partition sizes rank correctly. + + +def test_rank_single_document_partition(collection): + """A single-document partition gets rank 1.""" + result = run_window_operator( + collection, + "$rank", + [{"_id": 1, "partition": "A", "value": 10}], + sort_by={"_id": 1}, + expression={}, + ) + assertSuccess( + result, + [{"_id": 1, "partition": "A", "value": 10, "result": 1}], + msg="single-document partition gets rank 1", + ) + + +def test_rank_empty_collection(collection): + """$rank on an empty collection returns no documents without error.""" + result = run_window_operator( + collection, + "$rank", + [], + sort_by={"_id": 1}, + expression={}, + ) + assertSuccess(result, [], msg="empty collection produces no documents") diff --git a/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_tie_handling.py b/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_tie_handling.py new file mode 100644 index 000000000..2c9d5e9d7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/window/rank/test_window_rank_tie_handling.py @@ -0,0 +1,201 @@ +""" +Tests for $rank tie handling in window context. + +$rank is a rank operator that assigns positions where tied documents (same sort +value) share the same rank, and the next distinct value skips positions. For +example, two documents tied at rank 1 produce ranks 1, 1, 3 — position 2 is +skipped. This is the distinguishing behavior from $documentNumber (which always +assigns unique sequential positions: 1, 2, 3) and $denseRank (which shares +rank but does not skip: 1, 1, 2). These tests cover no ties, all ties, and +partial ties at the beginning, middle, and end of a partition, as well as +multiple tie groups and cross-partition independence. +""" + +from documentdb_tests.compatibility.tests.core.operator.window.utils.window_test_case import ( + run_window_operator, +) +from documentdb_tests.framework.assertions import assertSuccess + + +def test_rank_no_ties(collection): + """With all-distinct sort values, $rank assigns sequential 1, 2, 3, 4, 5.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 20}, + {"_id": 3, "partition": "A", "score": 30}, + {"_id": 4, "partition": "A", "score": 40}, + {"_id": 5, "partition": "A", "score": 50}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 20, "result": 2}, + {"_id": 3, "partition": "A", "score": 30, "result": 3}, + {"_id": 4, "partition": "A", "score": 40, "result": 4}, + {"_id": 5, "partition": "A", "score": 50, "result": 5}, + ] + assertSuccess(result, expected, msg="distinct sort values get sequential ranks 1..5") + + +def test_rank_all_ties(collection): + """When every sort value ties, $rank assigns rank 1 to all documents.""" + docs = [ + {"_id": 1, "partition": "A", "score": 50}, + {"_id": 2, "partition": "A", "score": 50}, + {"_id": 3, "partition": "A", "score": 50}, + {"_id": 4, "partition": "A", "score": 50}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 50, "result": 1}, + {"_id": 2, "partition": "A", "score": 50, "result": 1}, + {"_id": 3, "partition": "A", "score": 50, "result": 1}, + {"_id": 4, "partition": "A", "score": 50, "result": 1}, + ] + assertSuccess(result, expected, msg="all-tie partition assigns rank 1 to every document") + + +def test_rank_partial_tie_at_beginning(collection): + """A tie at the start shares rank 1 and skips rank 2 → 1, 1, 3, 4.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 10}, + {"_id": 3, "partition": "A", "score": 20}, + {"_id": 4, "partition": "A", "score": 30}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 10, "result": 1}, + {"_id": 3, "partition": "A", "score": 20, "result": 3}, + {"_id": 4, "partition": "A", "score": 30, "result": 4}, + ] + assertSuccess(result, expected, msg="tie at beginning yields 1, 1, 3, 4 — rank 2 skipped") + + +def test_rank_partial_tie_in_middle(collection): + """A tie in the middle shares rank 2 and skips rank 3 → 1, 2, 2, 4.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 20}, + {"_id": 3, "partition": "A", "score": 20}, + {"_id": 4, "partition": "A", "score": 30}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 20, "result": 2}, + {"_id": 3, "partition": "A", "score": 20, "result": 2}, + {"_id": 4, "partition": "A", "score": 30, "result": 4}, + ] + assertSuccess(result, expected, msg="tie in middle yields 1, 2, 2, 4 — rank 3 skipped") + + +def test_rank_partial_tie_at_end(collection): + """A tie at the end shares rank 3 → 1, 2, 3, 3.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 20}, + {"_id": 3, "partition": "A", "score": 30}, + {"_id": 4, "partition": "A", "score": 30}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 20, "result": 2}, + {"_id": 3, "partition": "A", "score": 30, "result": 3}, + {"_id": 4, "partition": "A", "score": 30, "result": 3}, + ] + assertSuccess(result, expected, msg="tie at end yields 1, 2, 3, 3 — no skip needed at tail") + + +def test_rank_multiple_tie_groups(collection): + """Two separate tie groups → 1, 1, 3, 3, 5 — each group skips the next.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 10}, + {"_id": 3, "partition": "A", "score": 20}, + {"_id": 4, "partition": "A", "score": 20}, + {"_id": 5, "partition": "A", "score": 30}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 10, "result": 1}, + {"_id": 3, "partition": "A", "score": 20, "result": 3}, + {"_id": 4, "partition": "A", "score": 20, "result": 3}, + {"_id": 5, "partition": "A", "score": 30, "result": 5}, + ] + assertSuccess( + result, + expected, + msg="two tie groups yield 1, 1, 3, 3, 5 — positions 2 and 4 skipped", + ) + + +def test_rank_tie_across_partitions(collection): + """Ties in separate partitions are ranked independently — each restarts at 1.""" + docs = [ + {"_id": 1, "partition": "A", "score": 10}, + {"_id": 2, "partition": "A", "score": 10}, + {"_id": 3, "partition": "A", "score": 20}, + {"_id": 4, "partition": "B", "score": 10}, + {"_id": 5, "partition": "B", "score": 10}, + {"_id": 6, "partition": "B", "score": 20}, + ] + result = run_window_operator( + collection, + "$rank", + docs, + sort_by={"score": 1}, + expression={}, + ) + expected = [ + {"_id": 1, "partition": "A", "score": 10, "result": 1}, + {"_id": 2, "partition": "A", "score": 10, "result": 1}, + {"_id": 3, "partition": "A", "score": 20, "result": 3}, + {"_id": 4, "partition": "B", "score": 10, "result": 1}, + {"_id": 5, "partition": "B", "score": 10, "result": 1}, + {"_id": 6, "partition": "B", "score": 20, "result": 3}, + ] + assertSuccess( + result, + expected, + msg="tie handling resets independently per partition — both partitions get 1, 1, 3", + )