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
6 changes: 3 additions & 3 deletions docs/testing/TEST_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading