From 75ac6f812e904625923bbb58cdc0045e157f00a1 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Mon, 13 Jul 2026 13:48:26 -0700 Subject: [PATCH 1/4] Add $isoDayOfWeek expression tests This change adds tests for the $isoDayOfWeek expression. It was originally authored by @mitchell-elholm. Closes #316 Co-authored-by: Mitchell Elholm Signed-off-by: Daniel Frankcom --- .../expressions/date/isoDayOfWeek/__init__.py | 0 .../test_isoDayOfWeek_calendar.py | 148 +++++++++++ .../test_isoDayOfWeek_date_types.py | 208 ++++++++++++++++ .../test_isoDayOfWeek_expressions.py | 230 ++++++++++++++++++ .../test_isoDayOfWeek_null_and_type_errors.py | 197 +++++++++++++++ .../test_isoDayOfWeek_timezone_input_types.py | 71 ++++++ .../test_isoDayOfWeek_timezone_names.py | 94 +++++++ .../test_isoDayOfWeek_timezone_offsets.py | 194 +++++++++++++++ .../test_isoDayOfWeek_timezone_validation.py | 129 ++++++++++ 9 files changed, 1271 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_calendar.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_date_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_null_and_type_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_input_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_names.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_offsets.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_validation.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_calendar.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_calendar.py new file mode 100644 index 000000000..90ccfaefb --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_calendar.py @@ -0,0 +1,148 @@ +"""Tests for $isoDayOfWeek ISO day-of-week extraction across the calendar and year range.""" + +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DATE_EPOCH, DATE_YEAR_1900 + +# Property [ISO Day Extraction]: $isoDayOfWeek returns the ISO 8601 day of the week +# (1=Monday to 7=Sunday) of a UTC date. +ISODAYOFWEEK_EXTRACTION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "monday", + doc={"date": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=1, + msg="$isoDayOfWeek should return 1 for a Monday", + ), + ExpressionTestCase( + "tuesday", + doc={"date": datetime(2024, 1, 16, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=2, + msg="$isoDayOfWeek should return 2 for a Tuesday", + ), + ExpressionTestCase( + "wednesday", + doc={"date": datetime(2024, 1, 17, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=3, + msg="$isoDayOfWeek should return 3 for a Wednesday", + ), + ExpressionTestCase( + "thursday", + doc={"date": datetime(2024, 1, 18, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=4, + msg="$isoDayOfWeek should return 4 for a Thursday", + ), + ExpressionTestCase( + "friday", + doc={"date": datetime(2024, 1, 19, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=5, + msg="$isoDayOfWeek should return 5 for a Friday", + ), + ExpressionTestCase( + "saturday", + doc={"date": datetime(2024, 1, 20, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=6, + msg="$isoDayOfWeek should return 6 for a Saturday", + ), + ExpressionTestCase( + "sunday", + doc={"date": datetime(2024, 1, 21, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=7, + msg="$isoDayOfWeek should return 7 for a Sunday", + ), + ExpressionTestCase( + "saturday_1998", + doc={"date": datetime(1998, 11, 7, 0, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=6, + msg="$isoDayOfWeek should return 6 for a Saturday in 1998", + ), +] + +# Property [Year Range]: the ISO day is correct at the epoch and at distant past and future +# dates, including the day immediately before the epoch. +ISODAYOFWEEK_YEAR_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "epoch", + doc={"date": DATE_EPOCH}, + expression={"$isoDayOfWeek": "$date"}, + expected=4, + msg="$isoDayOfWeek should return 4 for the epoch, a Thursday", + ), + ExpressionTestCase( + "pre_epoch", + doc={"date": datetime(1969, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=3, + msg="$isoDayOfWeek should return 3 for the day before the epoch, a Wednesday", + ), + ExpressionTestCase( + "distant_past", + doc={"date": DATE_YEAR_1900}, + expression={"$isoDayOfWeek": "$date"}, + expected=1, + msg="$isoDayOfWeek should return 1 for a distant past date, a Monday", + ), + ExpressionTestCase( + "distant_future", + doc={"date": datetime(2100, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=5, + msg="$isoDayOfWeek should return 5 for a distant future date, a Friday", + ), +] + +# Property [Leap Years]: the ISO day is correct on and around leap day, including century +# and non-century leap-rule boundaries. +ISODAYOFWEEK_LEAP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "leap_day_2020", + doc={"date": datetime(2020, 2, 29, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=6, + msg="$isoDayOfWeek should return 6 for leap day 2020, a Saturday", + ), + ExpressionTestCase( + "leap_day_2000", + doc={"date": datetime(2000, 2, 29, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=2, + msg="$isoDayOfWeek should return 2 for leap day of the year-2000 century leap, a Tuesday", + ), + ExpressionTestCase( + "non_leap_century_1900", + doc={"date": datetime(1900, 2, 28, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$date"}, + expected=3, + msg="$isoDayOfWeek should return 3 for Feb 28 of the non-leap century 1900, a Wednesday", + ), +] + +ISODAYOFWEEK_CALENDAR_TESTS: list[ExpressionTestCase] = ( + ISODAYOFWEEK_EXTRACTION_TESTS + ISODAYOFWEEK_YEAR_RANGE_TESTS + ISODAYOFWEEK_LEAP_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISODAYOFWEEK_CALENDAR_TESTS)) +def test_isoDayOfWeek_calendar(collection, test_case: ExpressionTestCase): + """Test $isoDayOfWeek ISO day extraction across the calendar, year range, and leap years.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_date_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_date_types.py new file mode 100644 index 000000000..6908a6262 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_date_types.py @@ -0,0 +1,208 @@ +"""Tests for $isoDayOfWeek with Timestamp, ObjectId, and extended-range date inputs.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DATE_MS_BEFORE_EPOCH, + DATE_MS_EPOCH, + DATE_MS_MAX, + DATE_MS_MIN, + DATE_MS_YEAR_10000, + OID_MAX_SIGNED32, + OID_MAX_UNSIGNED32, + OID_MIN_SIGNED32, + TS_MAX_SIGNED32, + TS_MAX_UNSIGNED32, +) + +# Property [Timestamp Input]: a BSON Timestamp is accepted as a date and yields its ISO day. +ISODAYOFWEEK_TIMESTAMP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "timestamp_monday", + doc={"date": ts_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=1, + msg="$isoDayOfWeek should return 1 for a Timestamp on a Monday", + ), + ExpressionTestCase( + "timestamp_saturday", + doc={"date": ts_from_args(2024, 1, 20, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=6, + msg="$isoDayOfWeek should return 6 for a Timestamp on a Saturday", + ), + ExpressionTestCase( + "timestamp_sunday", + doc={"date": ts_from_args(2024, 1, 21, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=7, + msg="$isoDayOfWeek should return 7 for a Timestamp on a Sunday", + ), + ExpressionTestCase( + "timestamp_summer", + doc={"date": ts_from_args(2024, 6, 15, 12, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=6, + msg="$isoDayOfWeek should return 6 for a Timestamp on a summer Saturday", + ), + ExpressionTestCase( + "timestamp_epoch", + doc={"date": ts_from_args(1970, 1, 1, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=4, + msg="$isoDayOfWeek should return 4 for a Timestamp at the epoch", + ), + ExpressionTestCase( + "timestamp_distant_future", + doc={"date": ts_from_args(2100, 1, 1, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=5, + msg="$isoDayOfWeek should return 5 for a Timestamp at a distant future date", + ), +] + +# Property [ObjectId Input]: an ObjectId is accepted as a date via its embedded timestamp. +ISODAYOFWEEK_OBJECTID_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "objectid_monday", + doc={"date": oid_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=1, + msg="$isoDayOfWeek should return 1 for an ObjectId on a Monday", + ), + ExpressionTestCase( + "objectid_saturday", + doc={"date": oid_from_args(2024, 1, 20, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=6, + msg="$isoDayOfWeek should return 6 for an ObjectId on a Saturday", + ), + ExpressionTestCase( + "objectid_sunday", + doc={"date": oid_from_args(2024, 1, 21, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=7, + msg="$isoDayOfWeek should return 7 for an ObjectId on a Sunday", + ), + ExpressionTestCase( + "objectid_summer", + doc={"date": oid_from_args(2024, 6, 15, 12, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=6, + msg="$isoDayOfWeek should return 6 for an ObjectId on a summer Saturday", + ), + ExpressionTestCase( + "objectid_epoch", + doc={"date": oid_from_args(1970, 1, 1, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=4, + msg="$isoDayOfWeek should return 4 for an ObjectId at the epoch", + ), + ExpressionTestCase( + "objectid_1980", + doc={"date": oid_from_args(1980, 1, 1, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$date"}, + expected=2, + msg="$isoDayOfWeek should return 2 for an ObjectId in 1980, a Tuesday", + ), +] + +# Property [Extended Range]: DatetimeMS, Timestamp, and ObjectId boundary instants +# beyond the native datetime range resolve to the correct ISO day of the week. +ISODAYOFWEEK_EXTENDED_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_ms_epoch", + doc={"date": DATE_MS_EPOCH}, + expression={"$isoDayOfWeek": "$date"}, + expected=4, + msg="$isoDayOfWeek should return 4 for the epoch as a DatetimeMS", + ), + ExpressionTestCase( + "date_ms_before_epoch", + doc={"date": DATE_MS_BEFORE_EPOCH}, + expression={"$isoDayOfWeek": "$date"}, + expected=3, + msg="$isoDayOfWeek should return 3 for a DatetimeMS one millisecond before the epoch", + ), + ExpressionTestCase( + "date_ms_year_10000", + doc={"date": DATE_MS_YEAR_10000}, + expression={"$isoDayOfWeek": "$date"}, + expected=6, + msg="$isoDayOfWeek should return 6 for a DatetimeMS at the year-10000 boundary", + ), + ExpressionTestCase( + "date_ms_max", + doc={"date": DATE_MS_MAX}, + expression={"$isoDayOfWeek": "$date"}, + expected=7, + msg="$isoDayOfWeek should return 7 for the maximum 64-bit DatetimeMS", + ), + ExpressionTestCase( + "date_ms_min", + doc={"date": DATE_MS_MIN}, + expression={"$isoDayOfWeek": "$date"}, + expected=7, + msg="$isoDayOfWeek should return 7 for the minimum 64-bit DatetimeMS", + ), + ExpressionTestCase( + "ts_boundary_max_s32", + doc={"date": TS_MAX_SIGNED32}, + expression={"$isoDayOfWeek": "$date"}, + expected=2, + msg="$isoDayOfWeek should return 2 for the max signed 32-bit Timestamp", + ), + ExpressionTestCase( + "ts_boundary_max_u32", + doc={"date": TS_MAX_UNSIGNED32}, + expression={"$isoDayOfWeek": "$date"}, + expected=7, + msg="$isoDayOfWeek should return 7 for the max unsigned 32-bit Timestamp", + ), + ExpressionTestCase( + "oid_boundary_max_s32", + doc={"date": OID_MAX_SIGNED32}, + expression={"$isoDayOfWeek": "$date"}, + expected=2, + msg="$isoDayOfWeek should return 2 for the max signed 32-bit ObjectId", + ), + ExpressionTestCase( + "oid_boundary_min_s32", + doc={"date": OID_MIN_SIGNED32}, + expression={"$isoDayOfWeek": "$date"}, + expected=5, + msg="$isoDayOfWeek should return 5 for the min signed 32-bit ObjectId", + ), + ExpressionTestCase( + "oid_boundary_max_u32", + doc={"date": OID_MAX_UNSIGNED32}, + expression={"$isoDayOfWeek": "$date"}, + expected=3, + msg="$isoDayOfWeek should return 3 for the max unsigned 32-bit ObjectId", + ), +] + +ISODAYOFWEEK_DATE_TYPES_TESTS: list[ExpressionTestCase] = ( + ISODAYOFWEEK_TIMESTAMP_TESTS + ISODAYOFWEEK_OBJECTID_TESTS + ISODAYOFWEEK_EXTENDED_RANGE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISODAYOFWEEK_DATE_TYPES_TESTS)) +def test_isoDayOfWeek_date_types(collection, test_case: ExpressionTestCase): + """Test $isoDayOfWeek with Timestamp, ObjectId, and extended-range date inputs.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_expressions.py new file mode 100644 index 000000000..236f70ca3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_expressions.py @@ -0,0 +1,230 @@ +"""Tests for $isoDayOfWeek argument forms, field-path resolution, and expression input.""" + +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + ISO_DATE_INVALID_ARRAY_INPUT_ERROR, + ISO_DATE_MISSING_DATE_ERROR, + ISO_DATE_UNKNOWN_FIELD_ERROR, + TYPE_MISMATCH_DATE_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Argument Forms]: the document form requires exactly a date field, and the +# operand-array form accepts only a single element. +ISODAYOFWEEK_ARGUMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "missing_date", + expression={"$isoDayOfWeek": {"timezone": "UTC"}}, + error_code=ISO_DATE_MISSING_DATE_ERROR, + msg="$isoDayOfWeek should error when the document form omits the date field", + ), + ExpressionTestCase( + "extra_field", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, tzinfo=timezone.utc), + "extra": 1, + } + }, + error_code=ISO_DATE_UNKNOWN_FIELD_ERROR, + msg="$isoDayOfWeek should error for an unknown field in the document form", + ), + ExpressionTestCase( + "object_expression_input", + doc={"date": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": {"a": "$date"}}, + error_code=ISO_DATE_UNKNOWN_FIELD_ERROR, + msg="$isoDayOfWeek should treat an object with an unknown key as an invalid document form", + ), + ExpressionTestCase( + "invalid_array", + expression={"$isoDayOfWeek": [1, 2]}, + error_code=ISO_DATE_INVALID_ARRAY_INPUT_ERROR, + msg="$isoDayOfWeek should error for a multi-element operand array", + ), + ExpressionTestCase( + "single_element_array", + expression={"$isoDayOfWeek": [datetime(2024, 1, 15, tzinfo=timezone.utc)]}, + expected=1, + msg="$isoDayOfWeek should accept a single-element operand array as the date", + ), + ExpressionTestCase( + "invalid_string", + expression={"$isoDayOfWeek": "string"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a literal string operand", + ), + ExpressionTestCase( + "invalid_number", + expression={"$isoDayOfWeek": 123}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a literal number operand", + ), + ExpressionTestCase( + "invalid_boolean", + expression={"$isoDayOfWeek": True}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a literal boolean operand", + ), +] + +# Property [Input Expression Types]: the operator accepts each expression type it can receive +# as the date argument (literal, field reference in shorthand and document form, an array +# expression holding a field reference, and a nested expression); a path or array expression +# that resolves to an array feeds the type contract and errors. +ISODAYOFWEEK_FIELD_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_date", + expression={"$isoDayOfWeek": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expected=1, + msg="$isoDayOfWeek should return the ISO day for a literal date operand", + ), + ExpressionTestCase( + "field_ref_simple", + doc={"d": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": "$d"}, + expected=1, + msg="$isoDayOfWeek should accept a date from a shorthand field reference", + ), + ExpressionTestCase( + "field_ref_object", + doc={"d": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": {"date": "$d"}}, + expected=1, + msg="$isoDayOfWeek should accept a date field reference in the document form", + ), + ExpressionTestCase( + "array_expression_field_ref", + doc={"d": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": ["$d"]}, + expected=1, + msg="$isoDayOfWeek should unwrap a single-element array expression holding a field " + "reference", + ), + ExpressionTestCase( + "nested_field_path", + doc={"a": {"b": datetime(2024, 1, 15, tzinfo=timezone.utc)}}, + expression={"$isoDayOfWeek": "$a.b"}, + expected=1, + msg="$isoDayOfWeek should accept a date resolved from a nested field path", + ), + ExpressionTestCase( + "nested_field_document_form", + doc={"doc": {"date": datetime(2024, 1, 15, tzinfo=timezone.utc)}}, + expression={"$isoDayOfWeek": "$doc.date"}, + expected=1, + msg="$isoDayOfWeek should accept a date resolved from a dotted document path", + ), + ExpressionTestCase( + "missing_nested_field", + doc={"a": {"x": 1}}, + expression={"$isoDayOfWeek": "$a.missing"}, + expected=None, + msg="$isoDayOfWeek should return null when a nested field path is missing", + ), + ExpressionTestCase( + "oid_field_ref", + doc={"oid": oid_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$oid"}, + expected=1, + msg="$isoDayOfWeek should accept an ObjectId from a shorthand field reference", + ), + ExpressionTestCase( + "oid_field_ref_object", + doc={"oid": oid_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoDayOfWeek": {"date": "$oid"}}, + expected=1, + msg="$isoDayOfWeek should accept an ObjectId field reference in the document form", + ), + ExpressionTestCase( + "ts_field_ref", + doc={"ts": ts_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoDayOfWeek": "$ts"}, + expected=1, + msg="$isoDayOfWeek should accept a Timestamp from a shorthand field reference", + ), + ExpressionTestCase( + "ts_field_ref_object", + doc={"ts": ts_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoDayOfWeek": {"date": "$ts"}}, + expected=1, + msg="$isoDayOfWeek should accept a Timestamp field reference in the document form", + ), + ExpressionTestCase( + "array_index_path", + doc={"a": [{"b": datetime(2024, 1, 15, tzinfo=timezone.utc)}]}, + expression={"$isoDayOfWeek": "$a.0.b"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should error when an array-index path resolves to an array", + ), + ExpressionTestCase( + "composite_array_path", + doc={ + "a": [ + {"b": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + {"b": datetime(2024, 1, 16, tzinfo=timezone.utc)}, + ] + }, + expression={"$isoDayOfWeek": "$a.b"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should error when a path over an array of objects resolves to an array", + ), + ExpressionTestCase( + "timezone_from_field", + doc={"date": datetime(2024, 1, 15, tzinfo=timezone.utc), "tz": "America/New_York"}, + expression={"$isoDayOfWeek": {"date": "$date", "timezone": "$tz"}}, + expected=7, + msg="$isoDayOfWeek should apply a timezone resolved from a field reference", + ), + ExpressionTestCase( + "missing_tz_field_ref", + doc={"date": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoDayOfWeek": {"date": "$date", "timezone": "$tz"}}, + expected=None, + msg="$isoDayOfWeek should return null when the timezone field reference is missing", + ), + ExpressionTestCase( + "expression_as_input", + expression={"$isoDayOfWeek": {"$dateFromString": {"dateString": "2024-01-15T00:00:00Z"}}}, + expected=1, + msg="$isoDayOfWeek should accept the result of a nested expression as the date", + ), +] + +# Property [Return Type]: $isoDayOfWeek returns a value of BSON type int. +ISODAYOFWEEK_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type", + doc={"date": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$type": {"$isoDayOfWeek": "$date"}}, + expected="int", + msg="$isoDayOfWeek should return an int", + ), +] + +ISODAYOFWEEK_EXPRESSION_TESTS: list[ExpressionTestCase] = ( + ISODAYOFWEEK_ARGUMENT_TESTS + ISODAYOFWEEK_FIELD_PATH_TESTS + ISODAYOFWEEK_RETURN_TYPE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISODAYOFWEEK_EXPRESSION_TESTS)) +def test_isoDayOfWeek_expressions(collection, test_case: ExpressionTestCase): + """Test $isoDayOfWeek argument forms, field paths, expression input, and return type.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_null_and_type_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_null_and_type_errors.py new file mode 100644 index 000000000..0bf0d3fd3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_null_and_type_errors.py @@ -0,0 +1,197 @@ +"""Tests for $isoDayOfWeek null propagation and non-date type rejection.""" + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, Regex + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_DATE_ERROR +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + MISSING, +) + +# Property [Null Propagation]: a null or missing date resolves to null rather than an error. +ISODAYOFWEEK_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_date", + doc={"date": None}, + expression={"$isoDayOfWeek": "$date"}, + expected=None, + msg="$isoDayOfWeek should return null for a null date", + ), + ExpressionTestCase( + "missing_date", + expression={"$isoDayOfWeek": MISSING}, + expected=None, + msg="$isoDayOfWeek should return null when the date references a missing field", + ), +] + +# Property [Type Rejection]: any non-date input type is rejected with a type-mismatch error. +ISODAYOFWEEK_TYPE_REJECTION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "string_date", + doc={"date": "not-a-date"}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a string as the date input", + ), + ExpressionTestCase( + "integer_date", + doc={"date": 123}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject an int as the date input", + ), + ExpressionTestCase( + "int64_date", + doc={"date": Int64(123)}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject an int64 as the date input", + ), + ExpressionTestCase( + "double_date", + doc={"date": 1.0}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a double as the date input", + ), + ExpressionTestCase( + "decimal128_date", + doc={"date": Decimal128("1")}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a decimal128 as the date input", + ), + ExpressionTestCase( + "boolean_true_date", + doc={"date": True}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a true boolean as the date input", + ), + ExpressionTestCase( + "boolean_false_date", + doc={"date": False}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a false boolean as the date input", + ), + ExpressionTestCase( + "array_date", + doc={"date": [1, 2, 3]}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject an array as the date input", + ), + ExpressionTestCase( + "object_date", + doc={"date": {"a": 1}}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject an object as the date input", + ), + ExpressionTestCase( + "regex_date", + doc={"date": Regex(".*")}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a regex as the date input", + ), + ExpressionTestCase( + "minkey_date", + doc={"date": MinKey()}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject MinKey as the date input", + ), + ExpressionTestCase( + "maxkey_date", + doc={"date": MaxKey()}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject MaxKey as the date input", + ), + ExpressionTestCase( + "bindata_date", + doc={"date": Binary(b"")}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject binary data as the date input", + ), + ExpressionTestCase( + "javascript_date", + doc={"date": Code("function(){}")}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject JavaScript code as the date input", + ), + ExpressionTestCase( + "float_nan_date", + doc={"date": FLOAT_NAN}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a float NaN as the date input", + ), + ExpressionTestCase( + "decimal128_nan_date", + doc={"date": DECIMAL128_NAN}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a decimal128 NaN as the date input", + ), + ExpressionTestCase( + "float_inf_date", + doc={"date": FLOAT_INFINITY}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a float infinity as the date input", + ), + ExpressionTestCase( + "float_neg_inf_date", + doc={"date": FLOAT_NEGATIVE_INFINITY}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a float negative infinity as the date input", + ), + ExpressionTestCase( + "decimal128_inf_date", + doc={"date": DECIMAL128_INFINITY}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a decimal128 infinity as the date input", + ), + ExpressionTestCase( + "decimal128_neg_inf_date", + doc={"date": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$isoDayOfWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoDayOfWeek should reject a decimal128 negative infinity as the date input", + ), +] + +ISODAYOFWEEK_NULL_AND_TYPE_ERROR_TESTS: list[ExpressionTestCase] = ( + ISODAYOFWEEK_NULL_TESTS + ISODAYOFWEEK_TYPE_REJECTION_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISODAYOFWEEK_NULL_AND_TYPE_ERROR_TESTS)) +def test_isoDayOfWeek_null_and_type_errors(collection, test_case: ExpressionTestCase): + """Test $isoDayOfWeek null propagation and non-date type rejection.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_input_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_input_types.py new file mode 100644 index 000000000..e8eb75b75 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_input_types.py @@ -0,0 +1,71 @@ +"""Tests for $isoDayOfWeek timezone application when the date is a Timestamp or ObjectId.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Timestamp Input with Zones]: a Timestamp input honours the zone offset before the +# ISO day is taken. +ISODAYOFWEEK_TIMESTAMP_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "ts_utc_no_cross", + expression={ + "$isoDayOfWeek": {"date": ts_from_args(2024, 1, 15, 0, 0, 0), "timezone": "UTC"} + }, + expected=1, + msg="$isoDayOfWeek should return 1 for a Monday Timestamp in UTC with no crossing", + ), + ExpressionTestCase( + "ts_minus5_bwd", + expression={ + "$isoDayOfWeek": {"date": ts_from_args(2024, 1, 15, 0, 0, 0), "timezone": "-05:00"} + }, + expected=7, + msg="$isoDayOfWeek should cross backward to Sunday for a Timestamp at a -05:00 offset", + ), +] + +# Property [ObjectId Input with Zones]: an ObjectId input honours the zone offset before the +# ISO day is taken. +ISODAYOFWEEK_OBJECTID_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "oid_utc_no_cross", + expression={ + "$isoDayOfWeek": {"date": oid_from_args(2024, 1, 15, 0, 0, 0), "timezone": "UTC"} + }, + expected=1, + msg="$isoDayOfWeek should return 1 for a Monday ObjectId in UTC with no crossing", + ), + ExpressionTestCase( + "oid_minus5_bwd", + expression={ + "$isoDayOfWeek": {"date": oid_from_args(2024, 1, 15, 0, 0, 0), "timezone": "-05:00"} + }, + expected=7, + msg="$isoDayOfWeek should cross backward to Sunday for an ObjectId at a -05:00 offset", + ), +] + +ISODAYOFWEEK_TIMEZONE_INPUT_TYPES_TESTS: list[ExpressionTestCase] = ( + ISODAYOFWEEK_TIMESTAMP_ZONE_TESTS + ISODAYOFWEEK_OBJECTID_ZONE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISODAYOFWEEK_TIMEZONE_INPUT_TYPES_TESTS)) +def test_isoDayOfWeek_timezone_input_types(collection, test_case: ExpressionTestCase): + """Test $isoDayOfWeek timezone application for Timestamp and ObjectId date inputs.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_names.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_names.py new file mode 100644 index 000000000..80df35d2f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_names.py @@ -0,0 +1,94 @@ +"""Tests for $isoDayOfWeek named-timezone and abbreviation application.""" + +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Named Zones]: a named zone or abbreviation shifts the instant before the ISO day +# is taken, which may cross a day boundary depending on the offset. +ISODAYOFWEEK_NAMED_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "tz_ny_bwd", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "America/New_York", + } + }, + expected=5, + msg="$isoDayOfWeek should cross backward to Friday for America/New_York", + ), + ExpressionTestCase( + "tz_gmt_no_cross", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "GMT", + } + }, + expected=6, + msg="$isoDayOfWeek should return 6 for GMT with no day crossing", + ), + ExpressionTestCase( + "tz_utc_no_cross", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "UTC", + } + }, + expected=6, + msg="$isoDayOfWeek should return 6 for UTC with no day crossing", + ), + ExpressionTestCase( + "tz_london_winter_no_cross", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": "Europe/London", + } + }, + expected=1, + msg="$isoDayOfWeek should return 1 for Europe/London in winter with no day crossing", + ), + ExpressionTestCase( + "tz_apia_fwd", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 14, 12, 0, 0, tzinfo=timezone.utc), + "timezone": "Pacific/Apia", + } + }, + expected=1, + msg="$isoDayOfWeek should cross forward to Monday for Pacific/Apia (+13:00)", + ), + ExpressionTestCase( + "tz_est_abbreviation", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, 3, 0, 0, tzinfo=timezone.utc), + "timezone": "EST", + } + }, + expected=7, + msg="$isoDayOfWeek should accept the EST three-letter abbreviation and cross to Sunday", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISODAYOFWEEK_NAMED_ZONE_TESTS)) +def test_isoDayOfWeek_timezone_names(collection, test_case: ExpressionTestCase): + """Test $isoDayOfWeek named-timezone and abbreviation application.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_offsets.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_offsets.py new file mode 100644 index 000000000..61486e798 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_offsets.py @@ -0,0 +1,194 @@ +"""Tests for $isoDayOfWeek UTC-offset timezone application, including edge and unusual offsets.""" + +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [UTC Offsets]: an explicit +HH:MM/-HH:MM offset shifts the instant before the ISO +# day is taken, including compact, half-hour, extreme, and out-of-range offsets the server +# still accepts. +ISODAYOFWEEK_OFFSET_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "tz_offset_plus530_no_cross", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "+05:30", + } + }, + expected=6, + msg="$isoDayOfWeek should return 6 for a +05:30 offset with no day crossing", + ), + ExpressionTestCase( + "tz_offset_minus5_bwd", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "-05:00", + } + }, + expected=5, + msg="$isoDayOfWeek should cross backward to Friday for a -05:00 offset", + ), + ExpressionTestCase( + "tz_offset_zero_no_cross", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "+00:00", + } + }, + expected=6, + msg="$isoDayOfWeek should return 6 for a +00:00 offset with no day crossing", + ), + ExpressionTestCase( + "tz_offset_minus4_to_friday", + expression={ + "$isoDayOfWeek": { + "date": datetime(1998, 11, 7, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "-0400", + } + }, + expected=5, + msg="$isoDayOfWeek should cross backward from Saturday to Friday for a -0400 offset", + ), + ExpressionTestCase( + "tz_offset_minus5_monday_to_sunday", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "-05:00", + } + }, + expected=7, + msg="$isoDayOfWeek should cross backward from Monday to Sunday for a -05:00 offset", + ), + ExpressionTestCase( + "tz_offset_plus2_sunday_to_monday", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 21, 23, 0, 0, tzinfo=timezone.utc), + "timezone": "+02:00", + } + }, + expected=1, + msg="$isoDayOfWeek should cross forward from Sunday to Monday for a +02:00 offset", + ), + ExpressionTestCase( + "tz_offset_no_colon", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "-0500", + } + }, + expected=7, + msg="$isoDayOfWeek should accept a compact -0500 offset without a colon", + ), + ExpressionTestCase( + "tz_offset_hour_only", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "-08", + } + }, + expected=7, + msg="$isoDayOfWeek should accept an hour-only -08 offset", + ), + ExpressionTestCase( + "tz_offset_minus13", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": "-13:00", + } + }, + expected=7, + msg="$isoDayOfWeek should accept a -13:00 offset", + ), + ExpressionTestCase( + "tz_offset_plus15", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 14, 10, 0, 0, tzinfo=timezone.utc), + "timezone": "+15:00", + } + }, + expected=1, + msg="$isoDayOfWeek should accept a +15:00 offset", + ), + ExpressionTestCase( + "tz_offset_minus0330", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, 2, 0, 0, tzinfo=timezone.utc), + "timezone": "-03:30", + } + }, + expected=7, + msg="$isoDayOfWeek should accept a -03:30 half-hour west offset", + ), + ExpressionTestCase( + "tz_offset_plus0570", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 14, 17, 0, 0, tzinfo=timezone.utc), + "timezone": "+05:70", + } + }, + expected=7, + msg="$isoDayOfWeek should accept a +05:70 (70-minute) offset", + ), + ExpressionTestCase( + "tz_offset_minus0570", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 15, 5, 0, 0, tzinfo=timezone.utc), + "timezone": "-05:70", + } + }, + expected=7, + msg="$isoDayOfWeek should accept a -05:70 (70-minute) offset", + ), + ExpressionTestCase( + "tz_offset_plus2500", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 13, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "+25:00", + } + }, + expected=7, + msg="$isoDayOfWeek should accept a +25:00 (25-hour) offset", + ), + ExpressionTestCase( + "tz_offset_minus2500", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 1, 16, 0, 0, 0, tzinfo=timezone.utc), + "timezone": "-25:00", + } + }, + expected=7, + msg="$isoDayOfWeek should accept a -25:00 (25-hour) offset", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISODAYOFWEEK_OFFSET_TESTS)) +def test_isoDayOfWeek_timezone_offsets(collection, test_case: ExpressionTestCase): + """Test $isoDayOfWeek UTC-offset timezone application, including edge and unusual offsets.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_validation.py new file mode 100644 index 000000000..3335e9779 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoDayOfWeek/test_isoDayOfWeek_timezone_validation.py @@ -0,0 +1,129 @@ +"""Tests for $isoDayOfWeek timezone validation and null-timezone propagation.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + INVALID_TIMEZONE_ERROR, + INVALID_TIMEZONE_TYPE_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Timezone Validation]: unparseable zone strings, wrong-typed timezones, and null +# timezones are rejected or propagate null. +ISODAYOFWEEK_TIMEZONE_VALIDATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "invalid_tz_olson", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": "NotATimezone", + } + }, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoDayOfWeek should reject an unparseable timezone string", + ), + ExpressionTestCase( + "invalid_tz_empty_string", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": "", + } + }, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoDayOfWeek should reject an empty-string timezone", + ), + ExpressionTestCase( + "invalid_tz_nonexistent_olson", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": "America/Nowhere", + } + }, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoDayOfWeek should reject a non-existent Olson timezone", + ), + ExpressionTestCase( + "invalid_tz_olson_lowercase", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": "america/new_york", + } + }, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoDayOfWeek should reject an all-lowercase Olson name", + ), + ExpressionTestCase( + "invalid_tz_olson_uppercase", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": "AMERICA/NEW_YORK", + } + }, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoDayOfWeek should reject an all-uppercase Olson name", + ), + *[ + ExpressionTestCase( + f"invalid_tz_{tid}", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": val, + } + }, + error_code=INVALID_TIMEZONE_TYPE_ERROR, + msg=f"$isoDayOfWeek should reject a {tid} timezone", + ) + for tid, val in [ + ("int", 5), + ("int64", Int64(5)), + ("double", 3.14), + ("decimal128", Decimal128("5")), + ("bool", True), + ("object", {"tz": "UTC"}), + ("array", ["UTC"]), + ("datetime", datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] + ], + ExpressionTestCase( + "null_tz", + expression={ + "$isoDayOfWeek": { + "date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), + "timezone": None, + } + }, + expected=None, + msg="$isoDayOfWeek should return null for a null timezone", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISODAYOFWEEK_TIMEZONE_VALIDATION_TESTS)) +def test_isoDayOfWeek_timezone_validation(collection, test_case: ExpressionTestCase): + """Test $isoDayOfWeek timezone validation and null-timezone propagation.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) From aac99c8a2400c0f2655529a455c81644e744c94f Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Mon, 13 Jul 2026 15:35:29 -0700 Subject: [PATCH 2/4] Add $isoWeek expression tests This change adds tests for the $isoWeek expression. It was originally authored by @mitchell-elholm. Closes #317 Co-authored-by: Mitchell Elholm Signed-off-by: Daniel Frankcom --- .../expressions/date/isoWeek/__init__.py | 0 .../date/isoWeek/test_isoWeek_calendar.py | 211 +++++++++++++++++ .../date/isoWeek/test_isoWeek_date_types.py | 208 +++++++++++++++++ .../date/isoWeek/test_isoWeek_expressions.py | 219 ++++++++++++++++++ .../test_isoWeek_null_and_type_errors.py | 214 +++++++++++++++++ .../test_isoWeek_timezone_input_types.py | 67 ++++++ .../isoWeek/test_isoWeek_timezone_names.py | 91 ++++++++ .../isoWeek/test_isoWeek_timezone_offsets.py | 134 +++++++++++ .../test_isoWeek_timezone_validation.py | 101 ++++++++ 9 files changed, 1245 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_calendar.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_date_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_null_and_type_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_input_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_names.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_offsets.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_validation.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_calendar.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_calendar.py new file mode 100644 index 000000000..2b28907a9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_calendar.py @@ -0,0 +1,211 @@ +"""Tests for $isoWeek ISO week-of-year extraction across the calendar and year range.""" + +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DATE_EPOCH, DATE_LEAP_FEB29 + +# Property [ISO Week Extraction]: $isoWeek returns the ISO 8601 week number (1 to 53) of a +# UTC date, where week 1 is the week containing the first Thursday of the year. +ISOWEEK_EXTRACTION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "jan1_2018_monday", + doc={"date": datetime(2018, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Jan 1 that falls on a Monday", + ), + ExpressionTestCase( + "jan1_2019_tuesday", + doc={"date": datetime(2019, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Jan 1 that falls on a Tuesday", + ), + ExpressionTestCase( + "jan1_2020_wednesday", + doc={"date": datetime(2020, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Jan 1 that falls on a Wednesday", + ), + ExpressionTestCase( + "jan1_2015_thursday", + doc={"date": datetime(2015, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Jan 1 that falls on a Thursday", + ), + ExpressionTestCase( + "jan1_2010_friday", + doc={"date": datetime(2010, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=53, + msg="$isoWeek should return 53 for a Jan 1 Friday that belongs to the prior ISO year", + ), + ExpressionTestCase( + "jan1_2011_saturday", + doc={"date": datetime(2011, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=52, + msg="$isoWeek should return 52 for a Jan 1 Saturday that belongs to the prior ISO year", + ), + ExpressionTestCase( + "jan1_2012_sunday", + doc={"date": datetime(2012, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=52, + msg="$isoWeek should return 52 for a Jan 1 Sunday that belongs to the prior ISO year", + ), + ExpressionTestCase( + "jan4_always_week1", + doc={"date": datetime(2016, 1, 4, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for Jan 4, which is always in ISO week 1", + ), + ExpressionTestCase( + "dec31_2004_week53", + doc={"date": datetime(2004, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=53, + msg="$isoWeek should return 53 for a Dec 31 in a 53-week year", + ), + ExpressionTestCase( + "dec31_2009_week53", + doc={"date": datetime(2009, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=53, + msg="$isoWeek should return 53 for a Dec 31 in a 53-week year", + ), + ExpressionTestCase( + "dec31_2015_week53", + doc={"date": datetime(2015, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=53, + msg="$isoWeek should return 53 for a Dec 31 in a 53-week year", + ), + ExpressionTestCase( + "dec31_2020_week53", + doc={"date": datetime(2020, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=53, + msg="$isoWeek should return 53 for a Dec 31 in a 53-week year", + ), + ExpressionTestCase( + "dec31_2018_week1", + doc={"date": datetime(2018, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Dec 31 that belongs to the next ISO year's week 1", + ), + ExpressionTestCase( + "dec29_2014_week1", + doc={"date": datetime(2014, 12, 29, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Dec 29 that belongs to the next ISO year's week 1", + ), + ExpressionTestCase( + "monday_week2_2024", + doc={"date": datetime(2024, 1, 8, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=2, + msg="$isoWeek should return 2 for the Monday that starts the second ISO week of 2024", + ), + ExpressionTestCase( + "sunday_late_week1_2024", + doc={"date": datetime(2024, 1, 7, 23, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for the last hour of the Sunday closing ISO week 1 of 2024", + ), + ExpressionTestCase( + "nov2_1998_week45", + doc={"date": datetime(1998, 11, 2, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=45, + msg="$isoWeek should return 45 for a Monday in November 1998", + ), +] + +# Property [Year Range]: the ISO week is correct at the epoch and at distant past and future +# dates, including the day immediately before the epoch. +ISOWEEK_YEAR_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "epoch", + doc={"date": DATE_EPOCH}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for the epoch", + ), + ExpressionTestCase( + "pre_epoch", + doc={"date": datetime(1969, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for the day before the epoch", + ), + ExpressionTestCase( + "distant_past", + doc={"date": datetime(1900, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=24, + msg="$isoWeek should return 24 for a distant past date in June 1900", + ), + ExpressionTestCase( + "distant_future", + doc={"date": datetime(2100, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=24, + msg="$isoWeek should return 24 for a distant future date in June 2100", + ), +] + +# Property [Leap Years]: the ISO week is correct on and around leap day, including century +# and non-century leap-rule boundaries. +ISOWEEK_LEAP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "leap_day_2000", + doc={"date": DATE_LEAP_FEB29}, + expression={"$isoWeek": "$date"}, + expected=9, + msg="$isoWeek should return 9 for leap day of the year-2000 century leap", + ), + ExpressionTestCase( + "leap_day_2020", + doc={"date": datetime(2020, 2, 29, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=9, + msg="$isoWeek should return 9 for leap day 2020", + ), + ExpressionTestCase( + "non_leap_century_1900", + doc={"date": datetime(1900, 2, 28, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$date"}, + expected=9, + msg="$isoWeek should return 9 for Feb 28 of the non-leap century 1900", + ), +] + +ISOWEEK_CALENDAR_TESTS: list[ExpressionTestCase] = ( + ISOWEEK_EXTRACTION_TESTS + ISOWEEK_YEAR_RANGE_TESTS + ISOWEEK_LEAP_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEK_CALENDAR_TESTS)) +def test_isoWeek_calendar(collection, test_case: ExpressionTestCase): + """Test $isoWeek ISO week extraction across the calendar, year range, and leap years.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_date_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_date_types.py new file mode 100644 index 000000000..11869485c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_date_types.py @@ -0,0 +1,208 @@ +"""Tests for $isoWeek with Timestamp, ObjectId, and extended-range date inputs.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DATE_MS_BEFORE_EPOCH, + DATE_MS_EPOCH, + DATE_MS_MAX, + DATE_MS_MIN, + DATE_MS_YEAR_10000, + OID_MAX_SIGNED32, + OID_MAX_UNSIGNED32, + OID_MIN_SIGNED32, + TS_MAX_SIGNED32, + TS_MAX_UNSIGNED32, +) + +# Property [Timestamp Input]: a BSON Timestamp is accepted as a date and yields its ISO week. +ISOWEEK_TIMESTAMP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "timestamp_jan1", + doc={"date": ts_from_args(2024, 1, 1, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Timestamp on Jan 1", + ), + ExpressionTestCase( + "timestamp_jan15", + doc={"date": ts_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=3, + msg="$isoWeek should return 3 for a Timestamp in mid-January", + ), + ExpressionTestCase( + "timestamp_jun15", + doc={"date": ts_from_args(2024, 6, 15, 12, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=24, + msg="$isoWeek should return 24 for a Timestamp in mid-June", + ), + ExpressionTestCase( + "timestamp_dec31", + doc={"date": ts_from_args(2024, 12, 31, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Timestamp on a Dec 31 in next year's week 1", + ), + ExpressionTestCase( + "timestamp_epoch", + doc={"date": ts_from_args(1970, 1, 1, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a Timestamp at the epoch", + ), + ExpressionTestCase( + "timestamp_distant_future", + doc={"date": ts_from_args(2100, 6, 15, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=24, + msg="$isoWeek should return 24 for a Timestamp at a distant future date", + ), +] + +# Property [ObjectId Input]: an ObjectId is accepted as a date via its embedded timestamp. +ISOWEEK_OBJECTID_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "objectid_jan1", + doc={"date": oid_from_args(2024, 1, 1, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for an ObjectId on Jan 1", + ), + ExpressionTestCase( + "objectid_jan15", + doc={"date": oid_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=3, + msg="$isoWeek should return 3 for an ObjectId in mid-January", + ), + ExpressionTestCase( + "objectid_jun15", + doc={"date": oid_from_args(2024, 6, 15, 12, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=24, + msg="$isoWeek should return 24 for an ObjectId in mid-June", + ), + ExpressionTestCase( + "objectid_dec31", + doc={"date": oid_from_args(2024, 12, 31, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for an ObjectId on a Dec 31 in next year's week 1", + ), + ExpressionTestCase( + "objectid_epoch", + doc={"date": oid_from_args(1970, 1, 1, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for an ObjectId at the epoch", + ), + ExpressionTestCase( + "objectid_1980", + doc={"date": oid_from_args(1980, 6, 15, 0, 0, 0)}, + expression={"$isoWeek": "$date"}, + expected=24, + msg="$isoWeek should return 24 for an ObjectId in mid-June 1980", + ), +] + +# Property [Extended Range]: DatetimeMS, Timestamp, and ObjectId boundary instants +# beyond the native datetime range resolve to the correct ISO week. +ISOWEEK_EXTENDED_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_ms_epoch", + doc={"date": DATE_MS_EPOCH}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for the epoch as a DatetimeMS", + ), + ExpressionTestCase( + "date_ms_before_epoch", + doc={"date": DATE_MS_BEFORE_EPOCH}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for a DatetimeMS one millisecond before the epoch", + ), + ExpressionTestCase( + "date_ms_year_10000", + doc={"date": DATE_MS_YEAR_10000}, + expression={"$isoWeek": "$date"}, + expected=52, + msg="$isoWeek should return 52 for a DatetimeMS at the year-10000 boundary", + ), + ExpressionTestCase( + "date_ms_max", + doc={"date": DATE_MS_MAX}, + expression={"$isoWeek": "$date"}, + expected=33, + msg="$isoWeek should return 33 for the maximum 64-bit DatetimeMS", + ), + ExpressionTestCase( + "date_ms_min", + doc={"date": DATE_MS_MIN}, + expression={"$isoWeek": "$date"}, + expected=19, + msg="$isoWeek should return 19 for the minimum 64-bit DatetimeMS", + ), + ExpressionTestCase( + "ts_boundary_max_s32", + doc={"date": TS_MAX_SIGNED32}, + expression={"$isoWeek": "$date"}, + expected=3, + msg="$isoWeek should return 3 for the max signed 32-bit Timestamp", + ), + ExpressionTestCase( + "ts_boundary_max_u32", + doc={"date": TS_MAX_UNSIGNED32}, + expression={"$isoWeek": "$date"}, + expected=5, + msg="$isoWeek should return 5 for the max unsigned 32-bit Timestamp", + ), + ExpressionTestCase( + "oid_boundary_max_s32", + doc={"date": OID_MAX_SIGNED32}, + expression={"$isoWeek": "$date"}, + expected=3, + msg="$isoWeek should return 3 for the max signed 32-bit ObjectId", + ), + ExpressionTestCase( + "oid_boundary_min_s32", + doc={"date": OID_MIN_SIGNED32}, + expression={"$isoWeek": "$date"}, + expected=50, + msg="$isoWeek should return 50 for the min signed 32-bit ObjectId", + ), + ExpressionTestCase( + "oid_boundary_max_u32", + doc={"date": OID_MAX_UNSIGNED32}, + expression={"$isoWeek": "$date"}, + expected=1, + msg="$isoWeek should return 1 for the max unsigned 32-bit ObjectId", + ), +] + +ISOWEEK_DATE_TYPES_TESTS: list[ExpressionTestCase] = ( + ISOWEEK_TIMESTAMP_TESTS + ISOWEEK_OBJECTID_TESTS + ISOWEEK_EXTENDED_RANGE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEK_DATE_TYPES_TESTS)) +def test_isoWeek_date_types(collection, test_case: ExpressionTestCase): + """Test $isoWeek with Timestamp, ObjectId, and extended-range date inputs.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_expressions.py new file mode 100644 index 000000000..efb0c1bc9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_expressions.py @@ -0,0 +1,219 @@ +"""Tests for $isoWeek argument forms, field-path resolution, and expression input.""" + +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + ISO_DATE_INVALID_ARRAY_INPUT_ERROR, + ISO_DATE_MISSING_DATE_ERROR, + ISO_DATE_UNKNOWN_FIELD_ERROR, + TYPE_MISMATCH_DATE_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Argument Forms]: the operand may be a date, a single-element operand array, or a +# document with exactly a date field; other operand shapes and scalar non-date operands are +# rejected. +ISOWEEK_ARGUMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "document_form_missing_date", + expression={"$isoWeek": {"timezone": "UTC"}}, + error_code=ISO_DATE_MISSING_DATE_ERROR, + msg="$isoWeek should error when the document form omits the date field", + ), + ExpressionTestCase( + "extra_field", + doc={"date": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "extra": 1}}, + error_code=ISO_DATE_UNKNOWN_FIELD_ERROR, + msg="$isoWeek should error for an unknown field in the document form", + ), + ExpressionTestCase( + "object_expression_input", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"a": "$date"}}, + error_code=ISO_DATE_UNKNOWN_FIELD_ERROR, + msg="$isoWeek should treat an object with an unknown key as an invalid document form", + ), + ExpressionTestCase( + "invalid_array", + expression={"$isoWeek": [1, 2]}, + error_code=ISO_DATE_INVALID_ARRAY_INPUT_ERROR, + msg="$isoWeek should error for a multi-element operand array", + ), + ExpressionTestCase( + "single_element_array", + expression={"$isoWeek": [datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)]}, + expected=24, + msg="$isoWeek should accept a single-element operand array as the date", + ), + ExpressionTestCase( + "invalid_string", + expression={"$isoWeek": "string"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a literal string operand", + ), + ExpressionTestCase( + "invalid_number", + expression={"$isoWeek": 123}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a literal number operand", + ), + ExpressionTestCase( + "invalid_boolean", + expression={"$isoWeek": True}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a literal boolean operand", + ), +] + +# Property [Input Expression Types]: the operator accepts each expression type it can receive +# as the date argument (literal, field reference in shorthand and document form, an array +# expression holding a field reference, and a nested expression); a path or array expression +# that resolves to an array feeds the type contract and errors. +ISOWEEK_FIELD_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_date", + expression={"$isoWeek": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expected=24, + msg="$isoWeek should return the ISO week for a literal date operand", + ), + ExpressionTestCase( + "field_ref_simple", + doc={"d": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": "$d"}, + expected=3, + msg="$isoWeek should accept a date from a shorthand field reference", + ), + ExpressionTestCase( + "field_ref_object", + doc={"d": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$d"}}, + expected=3, + msg="$isoWeek should accept a date field reference in the document form", + ), + ExpressionTestCase( + "array_expression_field_ref", + doc={"d": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": ["$d"]}, + expected=24, + msg="$isoWeek should unwrap a single-element array expression holding a field reference", + ), + ExpressionTestCase( + "nested_field_path", + doc={"a": {"b": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}}, + expression={"$isoWeek": "$a.b"}, + expected=24, + msg="$isoWeek should accept a date resolved from a nested field path", + ), + ExpressionTestCase( + "missing_nested_field", + doc={"doc": {"x": 1}}, + expression={"$isoWeek": "$doc.missing"}, + expected=None, + msg="$isoWeek should return null when a nested field path is missing", + ), + ExpressionTestCase( + "oid_field_ref", + doc={"oid": oid_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": "$oid"}, + expected=3, + msg="$isoWeek should accept an ObjectId from a shorthand field reference", + ), + ExpressionTestCase( + "oid_field_ref_object", + doc={"oid": oid_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": {"date": "$oid"}}, + expected=3, + msg="$isoWeek should accept an ObjectId field reference in the document form", + ), + ExpressionTestCase( + "ts_field_ref", + doc={"ts": ts_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": "$ts"}, + expected=3, + msg="$isoWeek should accept a Timestamp from a shorthand field reference", + ), + ExpressionTestCase( + "ts_field_ref_object", + doc={"ts": ts_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": {"date": "$ts"}}, + expected=3, + msg="$isoWeek should accept a Timestamp field reference in the document form", + ), + ExpressionTestCase( + "array_index_path", + doc={"a": [{"b": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}]}, + expression={"$isoWeek": "$a.0.b"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should error when an array-index path resolves to an array", + ), + ExpressionTestCase( + "composite_array_path", + doc={ + "a": [ + {"b": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + {"b": datetime(2024, 7, 15, tzinfo=timezone.utc)}, + ] + }, + expression={"$isoWeek": "$a.b"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should error when a path over an array of objects resolves to an array", + ), + ExpressionTestCase( + "timezone_from_field", + doc={"date": datetime(2024, 1, 1, 3, 0, 0, tzinfo=timezone.utc), "tz": "America/New_York"}, + expression={"$isoWeek": {"date": "$date", "timezone": "$tz"}}, + expected=52, + msg="$isoWeek should apply a timezone resolved from a field reference", + ), + ExpressionTestCase( + "missing_tz_field_ref", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "$tz"}}, + expected=None, + msg="$isoWeek should return null when the timezone field reference is missing", + ), + ExpressionTestCase( + "expression_as_input", + expression={"$isoWeek": {"$dateFromString": {"dateString": "2024-06-15T00:00:00Z"}}}, + expected=24, + msg="$isoWeek should accept the result of a nested expression as the date", + ), +] + +# Property [Return Type]: $isoWeek returns a value of BSON type int. +ISOWEEK_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$type": {"$isoWeek": "$date"}}, + expected="int", + msg="$isoWeek should return an int", + ), +] + +ISOWEEK_EXPRESSION_TESTS: list[ExpressionTestCase] = ( + ISOWEEK_ARGUMENT_TESTS + ISOWEEK_FIELD_PATH_TESTS + ISOWEEK_RETURN_TYPE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEK_EXPRESSION_TESTS)) +def test_isoWeek_expressions(collection, test_case: ExpressionTestCase): + """Test $isoWeek argument forms, field paths, expression input, and return type.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_null_and_type_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_null_and_type_errors.py new file mode 100644 index 000000000..168309ac8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_null_and_type_errors.py @@ -0,0 +1,214 @@ +"""Tests for $isoWeek null propagation and non-date type rejection.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, Regex + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_DATE_ERROR +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + MISSING, +) + +# Property [Null Propagation]: a null or missing date resolves to null rather than an error, +# including when an explicit timezone is supplied. +ISOWEEK_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_date", + doc={"date": None}, + expression={"$isoWeek": "$date"}, + expected=None, + msg="$isoWeek should return null for a null date", + ), + ExpressionTestCase( + "missing_date", + expression={"$isoWeek": MISSING}, + expected=None, + msg="$isoWeek should return null when the date references a missing field", + ), + ExpressionTestCase( + "null_date_with_timezone", + doc={"date": None}, + expression={"$isoWeek": {"date": "$date", "timezone": "UTC"}}, + expected=None, + msg="$isoWeek should return null for a null date even when a timezone is supplied", + ), +] + +# Property [Type Rejection]: any non-date input type is rejected with a type-mismatch error. +ISOWEEK_TYPE_REJECTION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "string_date", + doc={"date": "2024-01-15"}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a string as the date input", + ), + ExpressionTestCase( + "integer_date", + doc={"date": 123}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject an int as the date input", + ), + ExpressionTestCase( + "int64_date", + doc={"date": Int64(123)}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject an int64 as the date input", + ), + ExpressionTestCase( + "double_date", + doc={"date": 1.0}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a double as the date input", + ), + ExpressionTestCase( + "decimal128_date", + doc={"date": Decimal128("1")}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a decimal128 as the date input", + ), + ExpressionTestCase( + "boolean_true_date", + doc={"date": True}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a true boolean as the date input", + ), + ExpressionTestCase( + "boolean_false_date", + doc={"date": False}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a false boolean as the date input", + ), + ExpressionTestCase( + "array_date", + doc={"date": [1, 2, 3]}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject an array as the date input", + ), + ExpressionTestCase( + "array_containing_date", + doc={"date": [datetime(2024, 1, 15, tzinfo=timezone.utc)]}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject an array holding a date as the date input", + ), + ExpressionTestCase( + "object_date", + doc={"date": {"a": 1}}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject an object as the date input", + ), + ExpressionTestCase( + "regex_date", + doc={"date": Regex(".*")}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a regex as the date input", + ), + ExpressionTestCase( + "minkey_date", + doc={"date": MinKey()}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject MinKey as the date input", + ), + ExpressionTestCase( + "maxkey_date", + doc={"date": MaxKey()}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject MaxKey as the date input", + ), + ExpressionTestCase( + "bindata_date", + doc={"date": Binary(b"")}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject binary data as the date input", + ), + ExpressionTestCase( + "javascript_date", + doc={"date": Code("function(){}")}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject JavaScript code as the date input", + ), + ExpressionTestCase( + "float_nan_date", + doc={"date": FLOAT_NAN}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a float NaN as the date input", + ), + ExpressionTestCase( + "decimal128_nan_date", + doc={"date": DECIMAL128_NAN}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a decimal128 NaN as the date input", + ), + ExpressionTestCase( + "float_inf_date", + doc={"date": FLOAT_INFINITY}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a float infinity as the date input", + ), + ExpressionTestCase( + "float_neg_inf_date", + doc={"date": FLOAT_NEGATIVE_INFINITY}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a float negative infinity as the date input", + ), + ExpressionTestCase( + "decimal128_inf_date", + doc={"date": DECIMAL128_INFINITY}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a decimal128 infinity as the date input", + ), + ExpressionTestCase( + "decimal128_neg_inf_date", + doc={"date": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$isoWeek": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeek should reject a decimal128 negative infinity as the date input", + ), +] + +ISOWEEK_NULL_AND_TYPE_ERROR_TESTS: list[ExpressionTestCase] = ( + ISOWEEK_NULL_TESTS + ISOWEEK_TYPE_REJECTION_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEK_NULL_AND_TYPE_ERROR_TESTS)) +def test_isoWeek_null_and_type_errors(collection, test_case: ExpressionTestCase): + """Test $isoWeek null propagation and non-date type rejection.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_input_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_input_types.py new file mode 100644 index 000000000..ffde1b20c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_input_types.py @@ -0,0 +1,67 @@ +"""Tests for $isoWeek timezone application when the date is a Timestamp or ObjectId.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Timestamp Input with Zones]: a Timestamp input honours the zone offset before the +# ISO week is taken. +ISOWEEK_TIMESTAMP_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "ts_utc", + doc={"date": ts_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": {"date": "$date", "timezone": "UTC"}}, + expected=3, + msg="$isoWeek should return 3 for a mid-January Timestamp in UTC", + ), + ExpressionTestCase( + "ts_minus5_bwd", + doc={"date": ts_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-05:00"}}, + expected=2, + msg="$isoWeek should cross back to week 2 for a Timestamp at a -05:00 offset", + ), +] + +# Property [ObjectId Input with Zones]: an ObjectId input honours the zone offset before the +# ISO week is taken. +ISOWEEK_OBJECTID_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "oid_utc", + doc={"date": oid_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": {"date": "$date", "timezone": "UTC"}}, + expected=3, + msg="$isoWeek should return 3 for a mid-January ObjectId in UTC", + ), + ExpressionTestCase( + "oid_minus5_bwd", + doc={"date": oid_from_args(2024, 1, 15, 0, 0, 0)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-05:00"}}, + expected=2, + msg="$isoWeek should cross back to week 2 for an ObjectId at a -05:00 offset", + ), +] + +ISOWEEK_TIMEZONE_INPUT_TYPES_TESTS: list[ExpressionTestCase] = ( + ISOWEEK_TIMESTAMP_ZONE_TESTS + ISOWEEK_OBJECTID_ZONE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEK_TIMEZONE_INPUT_TYPES_TESTS)) +def test_isoWeek_timezone_input_types(collection, test_case: ExpressionTestCase): + """Test $isoWeek timezone application for Timestamp and ObjectId date inputs.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_names.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_names.py new file mode 100644 index 000000000..c3d70072a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_names.py @@ -0,0 +1,91 @@ +"""Tests for $isoWeek named-timezone and abbreviation application.""" + +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Named Zones]: a named zone or abbreviation shifts the instant before the ISO week +# is taken, which may cross a week boundary depending on the offset. +ISOWEEK_NAMED_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "tz_ny_no_cross", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "America/New_York"}}, + expected=24, + msg="$isoWeek should return 24 for America/New_York with no week crossing", + ), + ExpressionTestCase( + "tz_london_no_cross", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "Europe/London"}}, + expected=24, + msg="$isoWeek should return 24 for Europe/London with no week crossing", + ), + ExpressionTestCase( + "tz_gmt_no_cross", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "GMT"}}, + expected=24, + msg="$isoWeek should return 24 for GMT with no week crossing", + ), + ExpressionTestCase( + "tz_utc_no_cross", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "UTC"}}, + expected=24, + msg="$isoWeek should return 24 for UTC with no week crossing", + ), + ExpressionTestCase( + "tz_asia_tokyo", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "Asia/Tokyo"}}, + expected=24, + msg="$isoWeek should return 24 for Asia/Tokyo", + ), + ExpressionTestCase( + "tz_asia_kolkata", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "Asia/Kolkata"}}, + expected=24, + msg="$isoWeek should return 24 for Asia/Kolkata", + ), + ExpressionTestCase( + "tz_pacific_apia_fwd", + doc={"date": datetime(2024, 1, 7, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "Pacific/Apia"}}, + expected=2, + msg="$isoWeek should cross forward into week 2 for Pacific/Apia", + ), + ExpressionTestCase( + "tz_est_abbreviation", + doc={"date": datetime(2024, 1, 8, 3, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "EST"}}, + expected=1, + msg="$isoWeek should accept the EST abbreviation and cross back into week 1", + ), + ExpressionTestCase( + "tz_utc_datetime_input", + doc={"date": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "UTC"}}, + expected=3, + msg="$isoWeek should return 3 for a mid-January date in UTC", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEK_NAMED_ZONE_TESTS)) +def test_isoWeek_timezone_names(collection, test_case: ExpressionTestCase): + """Test $isoWeek named-timezone and abbreviation application.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_offsets.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_offsets.py new file mode 100644 index 000000000..0c8da8b02 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_offsets.py @@ -0,0 +1,134 @@ +"""Tests for $isoWeek UTC-offset timezone application, including compact and unusual offsets.""" + +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [UTC Offsets]: an explicit +HH:MM/-HH:MM offset shifts the instant before the ISO +# week is taken, including compact, hour-only, half-hour, extreme, and out-of-range offsets +# the server still accepts, and offsets that cross a week boundary. +ISOWEEK_OFFSET_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "tz_offset_plus530", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "+05:30"}}, + expected=24, + msg="$isoWeek should accept a +05:30 offset with no week crossing", + ), + ExpressionTestCase( + "tz_offset_minus5", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-05:00"}}, + expected=24, + msg="$isoWeek should accept a -05:00 offset with no week crossing", + ), + ExpressionTestCase( + "tz_offset_plus530_no_colon", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "+0530"}}, + expected=24, + msg="$isoWeek should accept a compact +0530 offset without a colon", + ), + ExpressionTestCase( + "tz_offset_plus3_hour_only", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "+03"}}, + expected=24, + msg="$isoWeek should accept an hour-only +03 offset", + ), + ExpressionTestCase( + "tz_offset_zero", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "+00:00"}}, + expected=24, + msg="$isoWeek should accept a +00:00 offset", + ), + ExpressionTestCase( + "tz_offset_minus5_no_colon", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-0500"}}, + expected=24, + msg="$isoWeek should accept a compact -0500 offset without a colon", + ), + ExpressionTestCase( + "tz_offset_minus8_hour_only", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-08"}}, + expected=24, + msg="$isoWeek should accept an hour-only -08 offset", + ), + ExpressionTestCase( + "tz_offset_minus13", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-13:00"}}, + expected=24, + msg="$isoWeek should accept a -13:00 offset", + ), + ExpressionTestCase( + "tz_offset_plus15", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "+15:00"}}, + expected=24, + msg="$isoWeek should accept a +15:00 offset", + ), + ExpressionTestCase( + "tz_offset_minus0330", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-03:30"}}, + expected=24, + msg="$isoWeek should accept a -03:30 half-hour west offset", + ), + ExpressionTestCase( + "tz_offset_plus0570", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "+05:70"}}, + expected=24, + msg="$isoWeek should accept a +05:70 (70-minute) offset", + ), + ExpressionTestCase( + "tz_offset_minus0570", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-05:70"}}, + expected=24, + msg="$isoWeek should accept a -05:70 (70-minute) offset", + ), + ExpressionTestCase( + "tz_offset_plus2500", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "+25:00"}}, + expected=24, + msg="$isoWeek should accept a +25:00 (25-hour) offset", + ), + ExpressionTestCase( + "tz_offset_minus2500", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-25:00"}}, + expected=24, + msg="$isoWeek should accept a -25:00 (25-hour) offset", + ), + ExpressionTestCase( + "tz_offset_minus5_week_cross", + doc={"date": datetime(1998, 11, 2, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "-0500"}}, + expected=44, + msg="$isoWeek should cross back to week 44 for a -0500 offset at a Monday midnight", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEK_OFFSET_TESTS)) +def test_isoWeek_timezone_offsets(collection, test_case: ExpressionTestCase): + """Test $isoWeek UTC-offset timezone application, including edge and unusual offsets.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_validation.py new file mode 100644 index 000000000..80b22e39b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeek/test_isoWeek_timezone_validation.py @@ -0,0 +1,101 @@ +"""Tests for $isoWeek timezone validation and null-timezone propagation.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + INVALID_TIMEZONE_ERROR, + INVALID_TIMEZONE_TYPE_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Timezone Validation]: unparseable zone strings, wrong-typed timezones, and null +# timezones are rejected or propagate null. +ISOWEEK_TIMEZONE_VALIDATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "invalid_tz_olson", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "NotATimezone"}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeek should reject an unparseable timezone string", + ), + ExpressionTestCase( + "invalid_tz_empty_string", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": ""}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeek should reject an empty-string timezone", + ), + ExpressionTestCase( + "invalid_tz_nonexistent_olson", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "America/Nowhere"}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeek should reject a non-existent Olson timezone", + ), + ExpressionTestCase( + "invalid_tz_olson_lowercase", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "america/new_york"}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeek should reject an all-lowercase Olson name", + ), + ExpressionTestCase( + "invalid_tz_olson_uppercase", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": "AMERICA/NEW_YORK"}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeek should reject an all-uppercase Olson name", + ), + *[ + ExpressionTestCase( + f"invalid_tz_{tid}", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": val}}, + error_code=INVALID_TIMEZONE_TYPE_ERROR, + msg=f"$isoWeek should reject a {tid} timezone", + ) + for tid, val in [ + ("int", 5), + ("int64", Int64(5)), + ("double", 3.14), + ("decimal128", Decimal128("5")), + ("bool", True), + ("object", {"tz": "UTC"}), + ("array", ["UTC"]), + ("datetime", datetime(2024, 6, 15, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] + ], + ExpressionTestCase( + "null_tz", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeek": {"date": "$date", "timezone": None}}, + expected=None, + msg="$isoWeek should return null for a null timezone", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEK_TIMEZONE_VALIDATION_TESTS)) +def test_isoWeek_timezone_validation(collection, test_case: ExpressionTestCase): + """Test $isoWeek timezone validation and null-timezone propagation.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) From 5a2d361fda60f53aa9b3cdd31461d2ad033eccc0 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Mon, 13 Jul 2026 16:44:50 -0700 Subject: [PATCH 3/4] Add $isoWeekYear expression tests This change adds tests for the $isoWeekYear expression. It was originally authored by @mitchell-elholm. Closes #318 Co-authored-by: Mitchell Elholm Signed-off-by: Daniel Frankcom --- .../expressions/date/isoWeekYear/__init__.py | 0 .../isoWeekYear/test_isoWeekYear_calendar.py | 198 +++++++++++++++ .../test_isoWeekYear_date_types.py | 210 ++++++++++++++++ .../test_isoWeekYear_expressions.py | 227 ++++++++++++++++++ .../test_isoWeekYear_null_and_type_errors.py | 214 +++++++++++++++++ .../test_isoWeekYear_timezone_input_types.py | 68 ++++++ .../test_isoWeekYear_timezone_names.py | 85 +++++++ .../test_isoWeekYear_timezone_offsets.py | 142 +++++++++++ .../test_isoWeekYear_timezone_validation.py | 101 ++++++++ 9 files changed, 1245 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_calendar.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_date_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_null_and_type_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_input_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_names.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_offsets.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_validation.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_calendar.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_calendar.py new file mode 100644 index 000000000..6f19439b4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_calendar.py @@ -0,0 +1,198 @@ +"""Tests for $isoWeekYear ISO week-numbering year extraction across the calendar and year range.""" + +from datetime import datetime, timezone + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DATE_EPOCH, DATE_LEAP_FEB29 + +# Property [ISO Week-Year Extraction]: $isoWeekYear returns the ISO 8601 week-numbering year, +# which differs from the calendar year for dates in a week that belongs to the adjacent year. +ISOWEEKYEAR_EXTRACTION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "jan1_2018_monday", + doc={"date": datetime(2018, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2018), + msg="$isoWeekYear should return 2018 for a Jan 1 that falls on a Monday", + ), + ExpressionTestCase( + "jan1_2019_tuesday", + doc={"date": datetime(2019, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2019), + msg="$isoWeekYear should return 2019 for a Jan 1 that falls on a Tuesday", + ), + ExpressionTestCase( + "jan1_2020_wednesday", + doc={"date": datetime(2020, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2020), + msg="$isoWeekYear should return 2020 for a Jan 1 that falls on a Wednesday", + ), + ExpressionTestCase( + "jan1_2015_thursday", + doc={"date": datetime(2015, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2015), + msg="$isoWeekYear should return 2015 for a Jan 1 that falls on a Thursday", + ), + ExpressionTestCase( + "jan1_2010_friday", + doc={"date": datetime(2010, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2009), + msg="$isoWeekYear should return the prior ISO year for a Jan 1 Friday", + ), + ExpressionTestCase( + "jan1_2011_saturday", + doc={"date": datetime(2011, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2010), + msg="$isoWeekYear should return the prior ISO year for a Jan 1 Saturday", + ), + ExpressionTestCase( + "jan1_2012_sunday", + doc={"date": datetime(2012, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2011), + msg="$isoWeekYear should return the prior ISO year for a Jan 1 Sunday", + ), + ExpressionTestCase( + "jan1_2016_prior_year", + doc={"date": datetime(2016, 1, 1, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2015), + msg="$isoWeekYear should return 2015 for a Jan 1 that belongs to the prior ISO year", + ), + ExpressionTestCase( + "jan4_2016_current_year", + doc={"date": datetime(2016, 1, 4, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2016), + msg="$isoWeekYear should return 2016 for Jan 4, which is always in the current ISO year", + ), + ExpressionTestCase( + "dec31_2018_next_year", + doc={"date": datetime(2018, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2019), + msg="$isoWeekYear should return 2019 for a Dec 31 that belongs to the next ISO year", + ), + ExpressionTestCase( + "dec29_2014_next_year", + doc={"date": datetime(2014, 12, 29, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2015), + msg="$isoWeekYear should return 2015 for a Dec 29 that belongs to the next ISO year", + ), + ExpressionTestCase( + "dec31_2015_same_year", + doc={"date": datetime(2015, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2015), + msg="$isoWeekYear should return 2015 for a Dec 31 that stays in the same ISO year", + ), + ExpressionTestCase( + "dec31_2020_same_year", + doc={"date": datetime(2020, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2020), + msg="$isoWeekYear should return 2020 for a Dec 31 that stays in the same ISO year", + ), + ExpressionTestCase( + "dec31_2000_same_year", + doc={"date": datetime(2000, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2000), + msg="$isoWeekYear should return 2000 for a Dec 31 that stays in the same ISO year", + ), + ExpressionTestCase( + "mid_year_2024", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for a mid-year date", + ), +] + +# Property [Year Range]: the ISO week-numbering year is correct at the epoch and at distant +# past and future dates, including the day immediately before the epoch. +ISOWEEKYEAR_YEAR_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "epoch", + doc={"date": DATE_EPOCH}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1970), + msg="$isoWeekYear should return 1970 for the epoch", + ), + ExpressionTestCase( + "pre_epoch", + doc={"date": datetime(1969, 12, 31, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1970), + msg="$isoWeekYear should return 1970 for the day before the epoch", + ), + ExpressionTestCase( + "distant_past", + doc={"date": datetime(1900, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1900), + msg="$isoWeekYear should return 1900 for a distant past date in June 1900", + ), + ExpressionTestCase( + "distant_future", + doc={"date": datetime(2100, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2100), + msg="$isoWeekYear should return 2100 for a distant future date in June 2100", + ), +] + +# Property [Leap Years]: the ISO week-numbering year is correct on and around leap day, +# including century and non-century leap-rule boundaries. +ISOWEEKYEAR_LEAP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "leap_day_2000", + doc={"date": DATE_LEAP_FEB29}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2000), + msg="$isoWeekYear should return 2000 for leap day of the year-2000 century leap", + ), + ExpressionTestCase( + "leap_day_2020", + doc={"date": datetime(2020, 2, 29, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2020), + msg="$isoWeekYear should return 2020 for leap day 2020", + ), + ExpressionTestCase( + "non_leap_century_1900", + doc={"date": datetime(1900, 2, 28, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1900), + msg="$isoWeekYear should return 1900 for Feb 28 of the non-leap century 1900", + ), +] + +ISOWEEKYEAR_CALENDAR_TESTS: list[ExpressionTestCase] = ( + ISOWEEKYEAR_EXTRACTION_TESTS + ISOWEEKYEAR_YEAR_RANGE_TESTS + ISOWEEKYEAR_LEAP_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEKYEAR_CALENDAR_TESTS)) +def test_isoWeekYear_calendar(collection, test_case: ExpressionTestCase): + """Test $isoWeekYear extraction across the calendar, year range, and leap years.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_date_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_date_types.py new file mode 100644 index 000000000..b49ce1887 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_date_types.py @@ -0,0 +1,210 @@ +"""Tests for $isoWeekYear with Timestamp, ObjectId, and extended-range date inputs.""" + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DATE_MS_BEFORE_EPOCH, + DATE_MS_EPOCH, + DATE_MS_MAX, + DATE_MS_MIN, + DATE_MS_YEAR_10000, + OID_MAX_SIGNED32, + OID_MAX_UNSIGNED32, + OID_MIN_SIGNED32, + TS_MAX_SIGNED32, + TS_MAX_UNSIGNED32, +) + +# Property [Timestamp Input]: a BSON Timestamp is accepted as a date and yields its ISO +# week-numbering year. +ISOWEEKYEAR_TIMESTAMP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "timestamp_jan1", + doc={"date": ts_from_args(2024, 1, 1, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for a Timestamp on Jan 1", + ), + ExpressionTestCase( + "timestamp_jun15", + doc={"date": ts_from_args(2024, 6, 15, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for a Timestamp in mid-June", + ), + ExpressionTestCase( + "timestamp_dec31_next_year", + doc={"date": ts_from_args(2024, 12, 31, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2025), + msg="$isoWeekYear should return 2025 for a Timestamp on a Dec 31 in next ISO year's week 1", + ), + ExpressionTestCase( + "timestamp_2016_jan1_prior_year", + doc={"date": ts_from_args(2016, 1, 1, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2015), + msg="$isoWeekYear should return 2015 for a Timestamp on a Jan 1 in the prior ISO year", + ), + ExpressionTestCase( + "timestamp_epoch", + doc={"date": ts_from_args(1970, 1, 1, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1970), + msg="$isoWeekYear should return 1970 for a Timestamp at the epoch", + ), + ExpressionTestCase( + "timestamp_distant_future", + doc={"date": ts_from_args(2100, 6, 15, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2100), + msg="$isoWeekYear should return 2100 for a Timestamp at a distant future date", + ), +] + +# Property [ObjectId Input]: an ObjectId is accepted as a date via its embedded timestamp. +ISOWEEKYEAR_OBJECTID_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "objectid_jan1", + doc={"date": oid_from_args(2024, 1, 1, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for an ObjectId on Jan 1", + ), + ExpressionTestCase( + "objectid_jun15", + doc={"date": oid_from_args(2024, 6, 15, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for an ObjectId in mid-June", + ), + ExpressionTestCase( + "objectid_dec31_next_year", + doc={"date": oid_from_args(2024, 12, 31, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2025), + msg="$isoWeekYear should return 2025 for an ObjectId on a Dec 31 in next ISO year's week 1", + ), + ExpressionTestCase( + "objectid_2016_jan1_prior_year", + doc={"date": oid_from_args(2016, 1, 1, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2015), + msg="$isoWeekYear should return 2015 for an ObjectId on a Jan 1 in the prior ISO year", + ), + ExpressionTestCase( + "objectid_epoch", + doc={"date": oid_from_args(1970, 1, 1, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1970), + msg="$isoWeekYear should return 1970 for an ObjectId at the epoch", + ), + ExpressionTestCase( + "objectid_1980", + doc={"date": oid_from_args(1980, 6, 15, 0, 0, 0)}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1980), + msg="$isoWeekYear should return 1980 for an ObjectId in mid-June 1980", + ), +] + +# Property [Extended Range]: DatetimeMS, Timestamp, and ObjectId boundary instants beyond the +# native datetime range resolve to the correct ISO week-numbering year. +ISOWEEKYEAR_EXTENDED_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_ms_epoch", + doc={"date": DATE_MS_EPOCH}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1970), + msg="$isoWeekYear should return 1970 for the epoch as a DatetimeMS", + ), + ExpressionTestCase( + "date_ms_before_epoch", + doc={"date": DATE_MS_BEFORE_EPOCH}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1970), + msg="$isoWeekYear should return 1970 for a DatetimeMS one millisecond before the epoch", + ), + ExpressionTestCase( + "date_ms_year_10000", + doc={"date": DATE_MS_YEAR_10000}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(9999), + msg="$isoWeekYear should return 9999 for a DatetimeMS at the year-10000 boundary", + ), + ExpressionTestCase( + "date_ms_max", + doc={"date": DATE_MS_MAX}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(292278994), + msg="$isoWeekYear should return the far-future ISO year for the maximum 64-bit DatetimeMS", + ), + ExpressionTestCase( + "date_ms_min", + doc={"date": DATE_MS_MIN}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(-292275055), + msg="$isoWeekYear should return the far-past ISO year for the minimum 64-bit DatetimeMS", + ), + ExpressionTestCase( + "ts_boundary_max_s32", + doc={"date": TS_MAX_SIGNED32}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2038), + msg="$isoWeekYear should return 2038 for the max signed 32-bit Timestamp", + ), + ExpressionTestCase( + "ts_boundary_max_u32", + doc={"date": TS_MAX_UNSIGNED32}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2106), + msg="$isoWeekYear should return 2106 for the max unsigned 32-bit Timestamp", + ), + ExpressionTestCase( + "oid_boundary_max_s32", + doc={"date": OID_MAX_SIGNED32}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(2038), + msg="$isoWeekYear should return 2038 for the max signed 32-bit ObjectId", + ), + ExpressionTestCase( + "oid_boundary_min_s32", + doc={"date": OID_MIN_SIGNED32}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1901), + msg="$isoWeekYear should return 1901 for the min signed 32-bit ObjectId", + ), + ExpressionTestCase( + "oid_boundary_max_u32", + doc={"date": OID_MAX_UNSIGNED32}, + expression={"$isoWeekYear": "$date"}, + expected=Int64(1970), + msg="$isoWeekYear should return 1970 for the max unsigned 32-bit ObjectId read as signed", + ), +] + +ISOWEEKYEAR_DATE_TYPES_TESTS: list[ExpressionTestCase] = ( + ISOWEEKYEAR_TIMESTAMP_TESTS + ISOWEEKYEAR_OBJECTID_TESTS + ISOWEEKYEAR_EXTENDED_RANGE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEKYEAR_DATE_TYPES_TESTS)) +def test_isoWeekYear_date_types(collection, test_case: ExpressionTestCase): + """Test $isoWeekYear with Timestamp, ObjectId, and extended-range date inputs.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_expressions.py new file mode 100644 index 000000000..83086f639 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_expressions.py @@ -0,0 +1,227 @@ +"""Tests for $isoWeekYear argument forms, field-path resolution, and expression input.""" + +from datetime import datetime, timezone + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + ISO_DATE_INVALID_ARRAY_INPUT_ERROR, + ISO_DATE_MISSING_DATE_ERROR, + ISO_DATE_UNKNOWN_FIELD_ERROR, + TYPE_MISMATCH_DATE_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Argument Forms]: the operand may be a date, a single-element operand array, or a +# document with exactly a date field; other operand shapes and scalar non-date operands are +# rejected. +ISOWEEKYEAR_ARGUMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "document_form_missing_date", + expression={"$isoWeekYear": {"timezone": "UTC"}}, + error_code=ISO_DATE_MISSING_DATE_ERROR, + msg="$isoWeekYear should error when the document form omits the date field", + ), + ExpressionTestCase( + "extra_field", + doc={"date": datetime(2024, 1, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "extra": 1}}, + error_code=ISO_DATE_UNKNOWN_FIELD_ERROR, + msg="$isoWeekYear should error for an unknown field in the document form", + ), + ExpressionTestCase( + "object_expression_input", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"a": "$date"}}, + error_code=ISO_DATE_UNKNOWN_FIELD_ERROR, + msg="$isoWeekYear should treat an object with an unknown key as an invalid document form", + ), + ExpressionTestCase( + "invalid_array", + expression={"$isoWeekYear": [1, 2]}, + error_code=ISO_DATE_INVALID_ARRAY_INPUT_ERROR, + msg="$isoWeekYear should error for a multi-element operand array", + ), + ExpressionTestCase( + "single_element_array", + expression={"$isoWeekYear": [datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)]}, + expected=Int64(2024), + msg="$isoWeekYear should accept a single-element operand array as the date", + ), + ExpressionTestCase( + "invalid_string", + expression={"$isoWeekYear": "string"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a literal string operand", + ), + ExpressionTestCase( + "invalid_number", + expression={"$isoWeekYear": 123}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a literal number operand", + ), + ExpressionTestCase( + "invalid_boolean", + expression={"$isoWeekYear": True}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a literal boolean operand", + ), +] + +# Property [Input Expression Types]: the operator accepts each expression type it can receive +# as the date argument (literal, field reference in shorthand and document form, an array +# expression holding a field reference, and a nested expression); a path or array expression +# that resolves to an array feeds the type contract and errors. +ISOWEEKYEAR_FIELD_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_date", + expression={"$isoWeekYear": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expected=Int64(2024), + msg="$isoWeekYear should return the ISO week-numbering year for a literal date operand", + ), + ExpressionTestCase( + "field_ref_simple", + doc={"d": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": "$d"}, + expected=Int64(2024), + msg="$isoWeekYear should accept a date from a shorthand field reference", + ), + ExpressionTestCase( + "field_ref_object", + doc={"d": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$d"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept a date field reference in the document form", + ), + ExpressionTestCase( + "array_expression_field_ref", + doc={"d": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": ["$d"]}, + expected=Int64(2024), + msg="$isoWeekYear should unwrap a single-element array expression with a field reference", + ), + ExpressionTestCase( + "nested_field_path", + doc={"a": {"b": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}}, + expression={"$isoWeekYear": "$a.b"}, + expected=Int64(2024), + msg="$isoWeekYear should accept a date resolved from a nested field path", + ), + ExpressionTestCase( + "nested_field_path_doc", + doc={"doc": {"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}}, + expression={"$isoWeekYear": "$doc.date"}, + expected=Int64(2024), + msg="$isoWeekYear should accept a date resolved from a nested document field path", + ), + ExpressionTestCase( + "missing_nested_field", + doc={"doc": {"x": 1}}, + expression={"$isoWeekYear": "$doc.missing"}, + expected=None, + msg="$isoWeekYear should return null when a nested field path is missing", + ), + ExpressionTestCase( + "oid_field_ref", + doc={"oid": oid_from_args(2024, 6, 15, 0, 0, 0)}, + expression={"$isoWeekYear": "$oid"}, + expected=Int64(2024), + msg="$isoWeekYear should accept an ObjectId from a shorthand field reference", + ), + ExpressionTestCase( + "oid_field_ref_object", + doc={"oid": oid_from_args(2024, 6, 15, 0, 0, 0)}, + expression={"$isoWeekYear": {"date": "$oid"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept an ObjectId field reference in the document form", + ), + ExpressionTestCase( + "ts_field_ref", + doc={"ts": ts_from_args(2024, 6, 15, 0, 0, 0)}, + expression={"$isoWeekYear": "$ts"}, + expected=Int64(2024), + msg="$isoWeekYear should accept a Timestamp from a shorthand field reference", + ), + ExpressionTestCase( + "ts_field_ref_object", + doc={"ts": ts_from_args(2024, 6, 15, 0, 0, 0)}, + expression={"$isoWeekYear": {"date": "$ts"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept a Timestamp field reference in the document form", + ), + ExpressionTestCase( + "array_index_path", + doc={"a": [{"b": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}]}, + expression={"$isoWeekYear": "$a.0.b"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should error when an array-index path resolves to an array", + ), + ExpressionTestCase( + "composite_array_path", + doc={ + "a": [ + {"b": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + {"b": datetime(2024, 7, 15, tzinfo=timezone.utc)}, + ] + }, + expression={"$isoWeekYear": "$a.b"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should error when a path over an array of objects resolves to an array", + ), + ExpressionTestCase( + "timezone_from_field", + doc={"date": datetime(2024, 1, 1, 3, 0, 0, tzinfo=timezone.utc), "tz": "America/New_York"}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "$tz"}}, + expected=Int64(2023), + msg="$isoWeekYear should apply a timezone resolved from a field reference", + ), + ExpressionTestCase( + "missing_tz_field_ref", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "$tz"}}, + expected=None, + msg="$isoWeekYear should return null when the timezone field reference is missing", + ), + ExpressionTestCase( + "expression_as_input", + expression={"$isoWeekYear": {"$dateFromString": {"dateString": "2024-06-15T00:00:00Z"}}}, + expected=Int64(2024), + msg="$isoWeekYear should accept the result of a nested expression as the date", + ), +] + +# Property [Return Type]: $isoWeekYear returns a value of BSON type long. +ISOWEEKYEAR_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$type": {"$isoWeekYear": "$date"}}, + expected="long", + msg="$isoWeekYear should return a long", + ), +] + +ISOWEEKYEAR_EXPRESSION_TESTS: list[ExpressionTestCase] = ( + ISOWEEKYEAR_ARGUMENT_TESTS + ISOWEEKYEAR_FIELD_PATH_TESTS + ISOWEEKYEAR_RETURN_TYPE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEKYEAR_EXPRESSION_TESTS)) +def test_isoWeekYear_expressions(collection, test_case: ExpressionTestCase): + """Test $isoWeekYear argument forms, field paths, expression input, and return type.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_null_and_type_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_null_and_type_errors.py new file mode 100644 index 000000000..6e58006b4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_null_and_type_errors.py @@ -0,0 +1,214 @@ +"""Tests for $isoWeekYear null propagation and non-date type rejection.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, Regex + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_DATE_ERROR +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + MISSING, +) + +# Property [Null Propagation]: a null or missing date resolves to null rather than an error, +# including when an explicit timezone is supplied. +ISOWEEKYEAR_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_date", + doc={"date": None}, + expression={"$isoWeekYear": "$date"}, + expected=None, + msg="$isoWeekYear should return null for a null date", + ), + ExpressionTestCase( + "missing_date", + expression={"$isoWeekYear": MISSING}, + expected=None, + msg="$isoWeekYear should return null when the date references a missing field", + ), + ExpressionTestCase( + "null_date_with_timezone", + doc={"date": None}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "UTC"}}, + expected=None, + msg="$isoWeekYear should return null for a null date even when a timezone is supplied", + ), +] + +# Property [Type Rejection]: any non-date input type is rejected with a type-mismatch error. +ISOWEEKYEAR_TYPE_REJECTION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "string_date", + doc={"date": "2024-01-15"}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a string as the date input", + ), + ExpressionTestCase( + "integer_date", + doc={"date": 123}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject an int as the date input", + ), + ExpressionTestCase( + "int64_date", + doc={"date": Int64(123)}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject an int64 as the date input", + ), + ExpressionTestCase( + "double_date", + doc={"date": 1.0}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a double as the date input", + ), + ExpressionTestCase( + "decimal128_date", + doc={"date": Decimal128("1")}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a decimal128 as the date input", + ), + ExpressionTestCase( + "boolean_true_date", + doc={"date": True}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a true boolean as the date input", + ), + ExpressionTestCase( + "boolean_false_date", + doc={"date": False}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a false boolean as the date input", + ), + ExpressionTestCase( + "array_date", + doc={"date": [1, 2, 3]}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject an array as the date input", + ), + ExpressionTestCase( + "array_containing_date", + doc={"date": [datetime(2024, 1, 15, tzinfo=timezone.utc)]}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject an array holding a date as the date input", + ), + ExpressionTestCase( + "object_date", + doc={"date": {"a": 1}}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject an object as the date input", + ), + ExpressionTestCase( + "regex_date", + doc={"date": Regex(".*")}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a regex as the date input", + ), + ExpressionTestCase( + "minkey_date", + doc={"date": MinKey()}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject MinKey as the date input", + ), + ExpressionTestCase( + "maxkey_date", + doc={"date": MaxKey()}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject MaxKey as the date input", + ), + ExpressionTestCase( + "bindata_date", + doc={"date": Binary(b"")}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject binary data as the date input", + ), + ExpressionTestCase( + "javascript_date", + doc={"date": Code("function(){}")}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject JavaScript code as the date input", + ), + ExpressionTestCase( + "float_nan_date", + doc={"date": FLOAT_NAN}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a float NaN as the date input", + ), + ExpressionTestCase( + "decimal128_nan_date", + doc={"date": DECIMAL128_NAN}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a decimal128 NaN as the date input", + ), + ExpressionTestCase( + "float_inf_date", + doc={"date": FLOAT_INFINITY}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a float infinity as the date input", + ), + ExpressionTestCase( + "float_neg_inf_date", + doc={"date": FLOAT_NEGATIVE_INFINITY}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a float negative infinity as the date input", + ), + ExpressionTestCase( + "decimal128_inf_date", + doc={"date": DECIMAL128_INFINITY}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a decimal128 infinity as the date input", + ), + ExpressionTestCase( + "decimal128_neg_inf_date", + doc={"date": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$isoWeekYear": "$date"}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$isoWeekYear should reject a decimal128 negative infinity as the date input", + ), +] + +ISOWEEKYEAR_NULL_AND_TYPE_ERROR_TESTS: list[ExpressionTestCase] = ( + ISOWEEKYEAR_NULL_TESTS + ISOWEEKYEAR_TYPE_REJECTION_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEKYEAR_NULL_AND_TYPE_ERROR_TESTS)) +def test_isoWeekYear_null_and_type_errors(collection, test_case: ExpressionTestCase): + """Test $isoWeekYear null propagation and non-date type rejection.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_input_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_input_types.py new file mode 100644 index 000000000..20bf4a277 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_input_types.py @@ -0,0 +1,68 @@ +"""Tests for $isoWeekYear timezone application when the date is a Timestamp or ObjectId.""" + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.date_utils import ( + oid_from_args, + ts_from_args, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Timestamp Input with Zones]: a Timestamp input honours the zone offset before the +# ISO week-numbering year is taken. +ISOWEEKYEAR_TIMESTAMP_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "ts_utc", + doc={"date": ts_from_args(2024, 1, 1, 2, 0, 0)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "UTC"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for an early-Jan-1 Timestamp in UTC", + ), + ExpressionTestCase( + "ts_minus5_cross", + doc={"date": ts_from_args(2024, 1, 1, 2, 0, 0)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-05:00"}}, + expected=Int64(2023), + msg="$isoWeekYear should cross back to 2023 for a Timestamp at a -05:00 offset", + ), +] + +# Property [ObjectId Input with Zones]: an ObjectId input honours the zone offset before the +# ISO week-numbering year is taken. +ISOWEEKYEAR_OBJECTID_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "oid_utc", + doc={"date": oid_from_args(2024, 1, 1, 2, 0, 0)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "UTC"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for an early-Jan-1 ObjectId in UTC", + ), + ExpressionTestCase( + "oid_minus5_cross", + doc={"date": oid_from_args(2024, 1, 1, 2, 0, 0)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-05:00"}}, + expected=Int64(2023), + msg="$isoWeekYear should cross back to 2023 for an ObjectId at a -05:00 offset", + ), +] + +ISOWEEKYEAR_TIMEZONE_INPUT_TYPES_TESTS: list[ExpressionTestCase] = ( + ISOWEEKYEAR_TIMESTAMP_ZONE_TESTS + ISOWEEKYEAR_OBJECTID_ZONE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEKYEAR_TIMEZONE_INPUT_TYPES_TESTS)) +def test_isoWeekYear_timezone_input_types(collection, test_case: ExpressionTestCase): + """Test $isoWeekYear timezone application for Timestamp and ObjectId date inputs.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_names.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_names.py new file mode 100644 index 000000000..6ee352fbe --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_names.py @@ -0,0 +1,85 @@ +"""Tests for $isoWeekYear named-timezone and abbreviation application.""" + +from datetime import datetime, timezone + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Named Zones]: a named zone or abbreviation shifts the instant before the ISO +# week-numbering year is taken, which may cross a year boundary depending on the offset. +ISOWEEKYEAR_NAMED_ZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "tz_ny_no_cross", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "America/New_York"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for America/New_York with no year crossing", + ), + ExpressionTestCase( + "tz_london_no_cross", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "Europe/London"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for Europe/London with no year crossing", + ), + ExpressionTestCase( + "tz_gmt_no_cross", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "GMT"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for GMT with no year crossing", + ), + ExpressionTestCase( + "tz_utc_no_cross", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "UTC"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for UTC with no year crossing", + ), + ExpressionTestCase( + "tz_asia_tokyo", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "Asia/Tokyo"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for Asia/Tokyo", + ), + ExpressionTestCase( + "tz_asia_kolkata", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "Asia/Kolkata"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for Asia/Kolkata", + ), + ExpressionTestCase( + "tz_pacific_apia", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "Pacific/Apia"}}, + expected=Int64(2024), + msg="$isoWeekYear should return 2024 for Pacific/Apia", + ), + ExpressionTestCase( + "tz_est_abbreviation_cross", + doc={"date": datetime(2024, 1, 1, 3, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "EST"}}, + expected=Int64(2023), + msg="$isoWeekYear should accept the EST abbreviation and cross back a year", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEKYEAR_NAMED_ZONE_TESTS)) +def test_isoWeekYear_timezone_names(collection, test_case: ExpressionTestCase): + """Test $isoWeekYear named-timezone and abbreviation application.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_offsets.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_offsets.py new file mode 100644 index 000000000..56ee34688 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_offsets.py @@ -0,0 +1,142 @@ +"""Tests for $isoWeekYear UTC-offset timezone application, including compact and unusual offsets.""" + +from datetime import datetime, timezone + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [UTC Offsets]: an explicit +HH:MM/-HH:MM offset shifts the instant before the ISO +# week-numbering year is taken, including compact, hour-only, half-hour, extreme, and +# out-of-range offsets the server still accepts, and offsets that cross a year boundary. +ISOWEEKYEAR_OFFSET_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "tz_offset_plus530", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "+05:30"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept a +05:30 offset with no year crossing", + ), + ExpressionTestCase( + "tz_offset_minus5", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-05:00"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept a -05:00 offset with no year crossing", + ), + ExpressionTestCase( + "tz_offset_zero", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "+00:00"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept a +00:00 offset", + ), + ExpressionTestCase( + "tz_offset_minus5_no_colon", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-0500"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept a compact -0500 offset without a colon", + ), + ExpressionTestCase( + "tz_offset_minus8_hour_only", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-08"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept an hour-only -08 offset", + ), + ExpressionTestCase( + "tz_offset_minus13", + doc={"date": datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-13:00"}}, + expected=Int64(2023), + msg="$isoWeekYear should accept a -13:00 offset that crosses back into the prior ISO year", + ), + ExpressionTestCase( + "tz_offset_plus15", + doc={"date": datetime(2024, 12, 31, 10, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "+15:00"}}, + expected=Int64(2025), + msg="$isoWeekYear should accept a +15:00 offset that crosses into the next ISO year", + ), + ExpressionTestCase( + "tz_offset_minus0330", + doc={"date": datetime(2024, 1, 1, 2, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-03:30"}}, + expected=Int64(2023), + msg="$isoWeekYear should accept a -03:30 half-hour west offset that crosses back a year", + ), + ExpressionTestCase( + "tz_offset_plus0570", + doc={"date": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "+05:70"}}, + expected=Int64(2024), + msg="$isoWeekYear should accept a +05:70 (70-minute) offset", + ), + ExpressionTestCase( + "tz_offset_minus0570", + doc={"date": datetime(2024, 1, 1, 5, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-05:70"}}, + expected=Int64(2023), + msg="$isoWeekYear should accept a -05:70 (70-minute) offset that crosses back a year", + ), + ExpressionTestCase( + "tz_offset_plus2500", + doc={"date": datetime(2024, 12, 30, 0, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "+25:00"}}, + expected=Int64(2025), + msg="$isoWeekYear should accept a +25:00 (25-hour) offset that crosses forward a year", + ), + ExpressionTestCase( + "tz_offset_minus2500", + doc={"date": datetime(2024, 1, 2, 0, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-25:00"}}, + expected=Int64(2023), + msg="$isoWeekYear should accept a -25:00 (25-hour) offset that crosses back a year", + ), + ExpressionTestCase( + "tz_offset_utc_2017_baseline", + doc={"date": datetime(2017, 1, 2, 0, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "+00:00"}}, + expected=Int64(2017), + msg="$isoWeekYear should return 2017 for a Jan 2 Monday at a +00:00 offset baseline", + ), + ExpressionTestCase( + "tz_offset_minus5_2017_cross", + doc={"date": datetime(2017, 1, 2, 0, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-0500"}}, + expected=Int64(2016), + msg="$isoWeekYear should cross back to 2016 for a -0500 offset at a Jan 2 midnight", + ), + ExpressionTestCase( + "tz_offset_minus5_shift_prev", + doc={"date": datetime(2018, 1, 1, 0, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "-05:00"}}, + expected=Int64(2017), + msg="$isoWeekYear should cross back to 2017 for a -05:00 offset at a Jan 1 midnight", + ), + ExpressionTestCase( + "tz_offset_plus2_shift_next", + doc={"date": datetime(2018, 12, 31, 23, 0, 0, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "+02:00"}}, + expected=Int64(2019), + msg="$isoWeekYear should cross forward to 2019 for a +02:00 offset late on Dec 31", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEKYEAR_OFFSET_TESTS)) +def test_isoWeekYear_timezone_offsets(collection, test_case: ExpressionTestCase): + """Test $isoWeekYear UTC-offset timezone application, including edge and unusual offsets.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_validation.py new file mode 100644 index 000000000..4cbe1dee0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/date/isoWeekYear/test_isoWeekYear_timezone_validation.py @@ -0,0 +1,101 @@ +"""Tests for $isoWeekYear timezone validation and null-timezone propagation.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils import ( + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + INVALID_TIMEZONE_ERROR, + INVALID_TIMEZONE_TYPE_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Timezone Validation]: unparseable zone strings, wrong-typed timezones, and null +# timezones are rejected or propagate null. +ISOWEEKYEAR_TIMEZONE_VALIDATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "invalid_tz_olson", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "NotATimezone"}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeekYear should reject an unparseable timezone string", + ), + ExpressionTestCase( + "invalid_tz_empty_string", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": ""}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeekYear should reject an empty-string timezone", + ), + ExpressionTestCase( + "invalid_tz_nonexistent_olson", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "America/Nowhere"}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeekYear should reject a non-existent Olson timezone", + ), + ExpressionTestCase( + "invalid_tz_olson_lowercase", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "america/new_york"}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeekYear should reject an all-lowercase Olson name", + ), + ExpressionTestCase( + "invalid_tz_olson_uppercase", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": "AMERICA/NEW_YORK"}}, + error_code=INVALID_TIMEZONE_ERROR, + msg="$isoWeekYear should reject an all-uppercase Olson name", + ), + *[ + ExpressionTestCase( + f"invalid_tz_{tid}", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": val}}, + error_code=INVALID_TIMEZONE_TYPE_ERROR, + msg=f"$isoWeekYear should reject a {tid} timezone", + ) + for tid, val in [ + ("int", 5), + ("int64", Int64(5)), + ("double", 3.14), + ("decimal128", Decimal128("5")), + ("bool", True), + ("object", {"tz": "UTC"}), + ("array", ["UTC"]), + ("datetime", datetime(2024, 6, 15, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] + ], + ExpressionTestCase( + "null_tz", + doc={"date": datetime(2024, 6, 15, tzinfo=timezone.utc)}, + expression={"$isoWeekYear": {"date": "$date", "timezone": None}}, + expected=None, + msg="$isoWeekYear should return null for a null timezone", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ISOWEEKYEAR_TIMEZONE_VALIDATION_TESTS)) +def test_isoWeekYear_timezone_validation(collection, test_case: ExpressionTestCase): + """Test $isoWeekYear timezone validation and null-timezone propagation.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, expected=test_case.expected, error_code=test_case.error_code, msg=test_case.msg + ) From 099493a6dcc8f0dca82c36557a48191cdf6c91d1 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Mon, 13 Jul 2026 17:17:49 -0700 Subject: [PATCH 4/4] Add retries for dockerhub pulls Signed-off-by: Daniel Frankcom --- .github/workflows/pr-tests.yml | 6 +++--- dev/compose-up.sh | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100755 dev/compose-up.sh diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index e36b9bb94..4ff2246b4 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -49,7 +49,7 @@ jobs: run: pip install -r requirements.txt - name: Start ${{ matrix.target.name }} target - run: docker compose -f dev/compose.yaml --profile ${{ matrix.target.profile }} up -d --wait + run: dev/compose-up.sh "${{ matrix.target.profile }}" - name: Run compatibility tests against ${{ matrix.target.name }} run: | @@ -161,7 +161,7 @@ jobs: PAIRS='[]' for row in $(echo "$TARGETS" | jq -c '.[]'); do PROFILE=$(echo "$row" | jq -r .profile) - docker compose -f dev/compose.yaml --profile "$PROFILE" up -d --wait + dev/compose-up.sh "$PROFILE" # Bring a replica-set target up to a writable primary so topology # detection classifies it correctly (collection does not initiate). python -m documentdb_tests.framework.engine_registry @@ -212,7 +212,7 @@ jobs: run: pip install -r requirements.txt - name: Start ${{ matrix.pair.target.name }} target - run: docker compose -f dev/compose.yaml --profile ${{ matrix.pair.target.profile }} up -d --wait + run: dev/compose-up.sh "${{ matrix.pair.target.profile }}" - name: "Run: ${{ matrix.pair.test }} against ${{ matrix.pair.target.name }}" run: | diff --git a/dev/compose-up.sh b/dev/compose-up.sh new file mode 100755 index 000000000..e5865bfe1 --- /dev/null +++ b/dev/compose-up.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Pull the images for a dev/compose.yaml profile (with retry) then bring it up. +# +# Docker Hub pulls occasionally time out fetching an auth token or manifest +# (context deadline exceeded), and a brief rate-limit shows up the same way. +# Retrying the network-bound pull before starting containers keeps a transient +# registry hiccup from failing CI. `up --wait` then starts from images that are +# already local. +# +# Single source of truth for both callers: the compose-up composite action and +# the crash-test discovery loop (see .github/workflows/pr-tests.yml). +set -euo pipefail + +profile="$1" + +for attempt in 1 2 3; do + docker compose -f dev/compose.yaml --profile "$profile" pull && break + echo "pull attempt $attempt failed; retrying in $((attempt * 10))s" + sleep $((attempt * 10)) +done + +docker compose -f dev/compose.yaml --profile "$profile" up -d --wait