From 5c294a44e8d65d312c08edc7dfa0b53c2ef687c6 Mon Sep 17 00:00:00 2001 From: Cassie-2025 Date: Mon, 10 Aug 2026 21:21:02 +0000 Subject: [PATCH] Add $setWindowFields sortBy validation corner case tests Add tests for sortBy-related edge cases in $setWindowFields: - Range window with sortBy: null (treated as omitted, rejected) - Range window with sortBy: {} (no sort fields, rejected) - Time-range window with fractional bounds (must be integers) - Time-range window with single fractional bound - Bounded documents window with sortBy: null (rejected) Update TEST_COVERAGE.md to document these requirements. Signed-off-by: Cassie-2025 --- docs/testing/TEST_COVERAGE.md | 6 +- ..._setWindowFields_documents_frame_errors.py | 30 ++++++++++ ...test_setWindowFields_range_frame_errors.py | 57 ++++++++++++++++++ ...setWindowFields_time_range_frame_errors.py | 59 +++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/docs/testing/TEST_COVERAGE.md b/docs/testing/TEST_COVERAGE.md index 1f5ffd44c..95991bfa7 100644 --- a/docs/testing/TEST_COVERAGE.md +++ b/docs/testing/TEST_COVERAGE.md @@ -585,12 +585,12 @@ Same shape as §11, applied to a different axis: §11 covers expression operator - Frame validation errors (documents, range, time-range): invalid bound types, malformed arrays, conflicting modes — these are operator-agnostic - Stage-level parameter validation: verify the `$setWindowFields` stage document structure per the spec. For each parameter below, test both valid inputs (succeeds) and invalid inputs (errors with correct code): - `partitionBy`: optional. Valid: expression (`"$field"`), literal value, omitted (entire collection = one partition). Invalid: test is not needed — any expression is accepted. - - `sortBy`: optional in some cases, required in others. Valid: object with field-direction pairs (`{"field": 1}`, `{"a": 1, "b": -1}`), omitted when using unbounded `["unbounded", "unbounded"]` documents-mode window with a non-rank operator. Invalid/errors: omitted when using (1) rank/order operators (`$rank`, `$denseRank`, `$documentNumber`), (2) bounded documents-mode windows (e.g., `[-1, 1]`, `["unbounded", "current"]`, `["current", "unbounded"]`), (3) `$linearFill` operator, (4) range-mode or time-range-mode windows. Also invalid: non-object type, empty object, direction values other than 1/-1. + - `sortBy`: optional in some cases, required in others. Valid: object with field-direction pairs (`{"field": 1}`, `{"a": 1, "b": -1}`), omitted when using unbounded `["unbounded", "unbounded"]` documents-mode window with a non-rank operator. Invalid/errors: omitted when using (1) rank/order operators (`$rank`, `$denseRank`, `$documentNumber`), (2) bounded documents-mode windows (e.g., `[-1, 1]`, `["unbounded", "current"]`, `["current", "unbounded"]`), (3) `$linearFill` operator, (4) range-mode or time-range-mode windows. Also invalid: non-object type, empty object, direction values other than 1/-1. For range/time-range windows specifically: `null` (treated as omitted) and `{}` (no sort fields) are also rejected since range requires exactly one ascending sort field. - `output`: required. Valid: document with one or more output fields, each containing a window operator and optional `window` key; dotted field names create embedded documents (same semantics as `$addFields`/`$set`). Invalid: omitted, non-document type, empty document, output field with no recognized window operator. - `window`: optional per output field (omitting defaults to unbounded whole-partition). Valid: document with exactly one of `documents` or `range`. Invalid: empty document, both `documents` and `range` specified, unknown keys inside `window`, non-document type. - `window.documents`: valid: two-element array where each element is `"current"`, `"unbounded"`, or an integer; lower bound ≤ upper bound. Invalid: non-array, array with fewer or more than 2 elements, non-integer numeric bounds (e.g., 1.5), string values other than `"current"`/`"unbounded"`, lower bound > upper bound. - - `window.range`: valid: two-element array where each element is `"current"`, `"unbounded"`, or a number; lower bound ≤ upper bound; requires `sortBy` on a single field. Invalid: non-array, array with fewer or more than 2 elements, string values other than `"current"`/`"unbounded"`, lower bound > upper bound, `sortBy` with multiple fields. - - `window.unit`: optional; when specified, converts `range` window to time-based. Valid: `"year"`, `"quarter"`, `"month"`, `"week"`, `"day"`, `"hour"`, `"minute"`, `"second"`, `"millisecond"`. Invalid: unrecognized string, non-string type, used with `documents` mode (should be rejected or ignored — verify behavior). + - `window.range`: valid: two-element array where each element is `"current"`, `"unbounded"`, or a number; lower bound ≤ upper bound; requires `sortBy` on a single ascending field. Invalid: non-array, array with fewer or more than 2 elements, string values other than `"current"`/`"unbounded"`, lower bound > upper bound, `sortBy` with multiple fields, descending sort direction. + - `window.unit`: optional; when specified, converts `range` window to time-based. Valid: `"year"`, `"quarter"`, `"month"`, `"week"`, `"day"`, `"hour"`, `"minute"`, `"second"`, `"millisecond"`. Invalid: unrecognized string, non-string type, used with `documents` mode (should be rejected or ignored — verify behavior), fractional numeric bounds (e.g., 1.5 hours — time-range bounds must be integers). - Unknown top-level keys in the `$setWindowFields` stage document are rejected with an error. - Documents-mode frame boundaries: using `$sum` as a sample operator, verify that document-based frame boundary specifications (centered, trailing, leading, non-overlapping) correctly control which documents are selected into the window. Edge cases: empty frame, single-element frame, and frame wider than the partition. - Range-mode frame boundaries: using `$sum` as a sample operator, verify that numeric range-based frame bounds correctly define the window of documents selected for computation. diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_documents_frame_errors.py b/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_documents_frame_errors.py index 949511ee8..8cc53d374 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_documents_frame_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_documents_frame_errors.py @@ -573,3 +573,33 @@ def test_cumulative_documents_window_without_sortby(collection): }, ) assertFailureCode(result, 5339901, msg="cumulative documents window without sortBy rejected") + + +# sortBy: null is treated as omitted — same error as missing sortBy + + +def test_bounded_documents_window_with_null_sortby(collection): + """Bounded documents window [-1, 0] with sortBy: null produces error.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": None, + "output": { + "result": { + "$sum": "$value", + "window": {"documents": [-1, 0]}, + } + }, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode(result, 5339901, msg="bounded documents window with sortBy: null rejected") diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_range_frame_errors.py b/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_range_frame_errors.py index f79536ef1..af9a5c36c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_range_frame_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_range_frame_errors.py @@ -281,6 +281,63 @@ def test_range_no_sortby(collection): assertFailureCode(result, 5339902, msg="range window without sortBy rejected") +# sortBy: null and {} are treated as absent — same error as omitting sortBy entirely + + +def test_range_null_sortby(collection): + """Range window with sortBy: null (treated as omitted) produces error 5339902.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": None, + "output": { + "result": { + "$sum": "$value", + "window": {"range": [-10, 10]}, + } + }, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode(result, 5339902, msg="range window with sortBy: null rejected") + + +def test_range_empty_sortby(collection): + """Range window with sortBy: {} (empty object, no sort fields) produces error 5339902.""" + collection.insert_many(SINGLE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": {}, + "output": { + "result": { + "$sum": "$value", + "window": {"range": [-10, 10]}, + } + }, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode(result, 5339902, msg="range window with sortBy: {} rejected") + + def test_range_descending_sort(collection): """Range mode with descending sort produces error 8947401.""" collection.insert_many(SINGLE_DOC) diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_time_range_frame_errors.py b/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_time_range_frame_errors.py index 5703e0832..7bddd518d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_time_range_frame_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/setWindowFields/test_setWindowFields_time_range_frame_errors.py @@ -191,6 +191,65 @@ def test_time_range_unit_without_date_sortby(collection): assertFailureCode(result, 5429513, msg="time unit with non-date sortBy rejected") +# Property [Fractional Bound with Unit]: time-range bounds must be integers + + +def test_time_range_fractional_bound(collection): + """Range with fractional bound and time unit produces error.""" + collection.insert_many(SINGLE_DATE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": {"date": 1}, + "output": { + "result": { + "$sum": "$value", + "window": {"range": [-1.5, 1.5], "unit": "hour"}, + } + }, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode(result, FAILED_TO_PARSE_ERROR, msg="fractional bound with time unit rejected") + + +def test_time_range_fractional_lower_bound_only(collection): + """Range with fractional lower bound and integer upper bound with time unit produces error.""" + collection.insert_many(SINGLE_DATE_DOC) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$setWindowFields": { + "partitionBy": "$partition", + "sortBy": {"date": 1}, + "output": { + "result": { + "$sum": "$value", + "window": {"range": [-1.5, 2], "unit": "hour"}, + } + }, + } + } + ], + "cursor": {}, + }, + ) + assertFailureCode( + result, FAILED_TO_PARSE_ERROR, msg="fractional lower bound with time unit rejected" + ) + + # Property [Bound Type Validation with Unit]: bounds must be numeric when unit is specified