Repository: meltano/sdk
Pull request #2970 (2025-04-14 16:34:14)

fix: Consider `oneOf` schemas when checking for `date` and `datetime` types
## Summary by Sourcery

Enhance type checking for date and datetime schemas to support `oneOf` schema compositions

Bug Fixes:
- Improve type detection for complex JSON schemas that use `oneOf` composition, ensuring correct identification of date and datetime types

Enhancements:
- Extend type checking functions to handle `oneOf` schema compositions
- Improve type detection robustness for complex schema definitions

Tests:
- Refactor tests for `is_datetime_type` and `is_date_or_datetime_type` to use parametrized testing with comprehensive test cases
- Add test cases for `oneOf` schema compositions to verify type detection

<!-- readthedocs-preview meltano-sdk start -->
----
📚 Documentation preview 📚: https://meltano-sdk--2970.org.readthedocs.build/en/2970/

<!-- readthedocs-preview meltano-sdk end -->

sourcery-ai[bot]: <!-- Generated by sourcery-ai[bot]: start review_guide -->

🧙 Sourcery is reviewing your pull request!

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
codspeed-hq[bot]: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/meltano/sdk/branches/edgarrmondragon%2Ffix%2Fcheck-date-datetime-type-oneof)

### Merging #2970 will **not alter performance**

<sub>Comparing <code>edgarrmondragon/fix/check-date-datetime-type-oneof</code> (9b71385) with <code>main</code> (38076a8)</sub>



### Summary

`✅ 7` untouched benchmarks  





Patch:
diff --git a/singer_sdk/helpers/_typing.py b/singer_sdk/helpers/_typing.py
index d39ef980..246a48f1 100644
--- a/singer_sdk/helpers/_typing.py
+++ b/singer_sdk/helpers/_typing.py
@@ -148,6 +148,8 @@ def is_datetime_type(type_dict: dict) -> bool:
         return any(is_datetime_type(type_dict) for type_dict in type_dict["anyOf"])
     if "allOf" in type_dict:
         return all(is_datetime_type(type_dict) for type_dict in type_dict["allOf"])
+    if "oneOf" in type_dict:
+        return any(is_datetime_type(option) for option in type_dict["oneOf"])
     if "type" in type_dict:
         return type_dict.get("format") == "date-time"
     msg = f"Could not detect type of replication key using schema '{type_dict}'"
@@ -175,6 +177,8 @@ def is_date_or_datetime_type(type_dict: dict) -> bool:
         return all(
             is_date_or_datetime_type(type_dict) for type_dict in type_dict["allOf"]
         )
+    if "oneOf" in type_dict:
+        return any(is_date_or_datetime_type(option) for option in type_dict["oneOf"])
 
     if "type" in type_dict:
         return type_dict.get("format") in {"date", "date-time"}


Test patch:
diff --git a/tests/core/test_jsonschema_helpers.py b/tests/core/test_jsonschema_helpers.py
index cae9f843..cb960826 100644
--- a/tests/core/test_jsonschema_helpers.py
+++ b/tests/core/test_jsonschema_helpers.py
@@ -1023,33 +1023,112 @@ def test_schema_dependencies():
     )
 
 
-def test_is_datetime_type():
-    assert is_datetime_type({"type": "string", "format": "date-time"})
-    assert not is_datetime_type({"type": "string"})
-
-    assert is_datetime_type({"anyOf": [{"type": "string", "format": "date-time"}]})
-    assert not is_datetime_type({"anyOf": [{"type": "string"}]})
-
-    assert is_datetime_type({"allOf": [{"type": "string", "format": "date-time"}]})
-    assert not is_datetime_type({"allOf": [{"type": "string"}]})
-
-
-def test_is_date_or_datetime_type():
-    assert is_date_or_datetime_type({"type": "string", "format": "date"})
-    assert is_date_or_datetime_type({"type": "string", "format": "date-time"})
-    assert not is_date_or_datetime_type({"type": "string"})
+@pytest.mark.parametrize(
+    "schema,expected",
+    [
+        pytest.param(
+            {"type": "string", "format": "date-time"},
+            True,
+            id="datetime_type",
+        ),
+        pytest.param(
+            {"type": "string"},
+            False,
+            id="plain_string_type",
+        ),
+        pytest.param(
+            {"anyOf": [{"type": "string", "format": "date-time"}]},
+            True,
+            id="anyof_datetime",
+        ),
+        pytest.param(
+            {"anyOf": [{"type": "string"}]},
+            False,
+            id="anyof_string",
+        ),
+        pytest.param(
+            {"allOf": [{"type": "string", "format": "date-time"}]},
+            True,
+            id="allof_datetime",
+        ),
+        pytest.param(
+            {"allOf": [{"type": "string"}]},
+            False,
+            id="allof_string",
+        ),
+        pytest.param(
+            {"oneOf": [{"type": "string", "format": "date-time"}, {"type": "null"}]},
+            True,
+            id="oneof_datetime",
+        ),
+        pytest.param(
+            {"oneOf": [{"type": "string"}, {"type": "null"}]},
+            False,
+            id="oneof_string_or_null",
+        ),
+    ],
+)
+def test_is_datetime_type(schema, expected):
+    assert is_datetime_type(schema) == expected
 
-    assert is_date_or_datetime_type(
-        {"anyOf": [{"type": "string", "format": "date-time"}]},
-    )
-    assert is_date_or_datetime_type({"anyOf": [{"type": "string", "format": "date"}]})
-    assert not is_date_or_datetime_type({"anyOf": [{"type": "string"}]})
 
-    assert is_date_or_datetime_type(
-        {"allOf": [{"type": "string", "format": "date-time"}]},
-    )
-    assert is_date_or_datetime_type({"allOf": [{"type": "string", "format": "date"}]})
-    assert not is_date_or_datetime_type({"allOf": [{"type": "string"}]})
+@pytest.mark.parametrize(
+    "schema,expected",
+    [
+        pytest.param(
+            {"type": "string", "format": "date"},
+            True,
+            id="date_type",
+        ),
+        pytest.param(
+            {"type": "string", "format": "date-time"},
+            True,
+            id="datetime_type",
+        ),
+        pytest.param(
+            {"type": "string"},
+            False,
+            id="plain_string_type",
+        ),
+        pytest.param(
+            {"anyOf": [{"type": "string", "format": "date-time"}]},
+            True,
+            id="anyof_datetime",
+        ),
+        pytest.param(
+            {"anyOf": [{"type": "string", "format": "date"}]},
+            True,
+            id="anyof_date",
+        ),
+        pytest.param(
+            {"anyOf": [{"type": "string"}]},
+            False,
+            id="anyof_string",
+        ),
+        pytest.param(
+            {"allOf": [{"type": "string", "format": "date-time"}]},
+            True,
+            id="allof_datetime",
+        ),
+        pytest.param(
+            {"allOf": [{"type": "string", "format": "date"}]},
+            True,
+            id="allof_date",
+        ),
+        pytest.param(
+            {"allOf": [{"type": "string"}]},
+            False,
+            id="allof_string",
+        ),
+        pytest.param(
+            {"oneOf": [{"type": "string", "format": "date-time"}, {"type": "null"}]},
+            True,
+            id="oneof_datetime",
+        ),
+    ],
+)
+def test_is_date_or_datetime_type(schema, expected):
+    assert is_date_or_datetime_type(schema) == expected
 
 
 def test_is_string_array_type():