Repository: meltano/sdk
Pull request #1853 (2023-07-18 19:47:11)

fix(targets): Check against the unconformed key properties when validating record keys
Related:

* Closes: https://github.com/meltano/sdk/issues/1819

cc @pnadolny13


<!-- readthedocs-preview meltano-sdk start -->
----
:books: Documentation preview :books:: https://meltano-sdk--1853.org.readthedocs.build/en/1853/

<!-- readthedocs-preview meltano-sdk end -->

BuzzCutNorman: @edgarrmondragon, wow fixed with a single `_` bravo 👏.
edgarrmondragon: This seems to work in target snowflake (https://github.com/MeltanoLabs/target-snowflake/pull/94)
edgarrmondragon: I think we have enough validation that the fix works, so I'll go ahead and merge it

Patch:
diff --git a/singer_sdk/sinks/core.py b/singer_sdk/sinks/core.py
index ec43c406..9928aa6f 100644
--- a/singer_sdk/sinks/core.py
+++ b/singer_sdk/sinks/core.py
@@ -215,6 +215,9 @@ class Sink(metaclass=abc.ABCMeta):
     def key_properties(self) -> list[str]:
         """Return key properties.
 
+        Override this method to return a list of key properties in a format that is
+        compatible with the target.
+
         Returns:
             A list of stream key properties.
         """
@@ -331,10 +334,10 @@ class Sink(metaclass=abc.ABCMeta):
         Raises:
             MissingKeyPropertiesError: If record is missing one or more key properties.
         """
-        if not all(key_property in record for key_property in self.key_properties):
+        if any(key_property not in record for key_property in self._key_properties):
             msg = (
                 f"Record is missing one or more key_properties. \n"
-                f"Key Properties: {self.key_properties}, "
+                f"Key Properties: {self._key_properties}, "
                 f"Record Keys: {list(record.keys())}"
             )
             raise MissingKeyPropertiesError(


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 142e76fe..cb201c9a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -86,6 +86,10 @@ class BatchSinkMock(BatchSink):
         self.target.records_written.extend(context["records"])
         self.target.num_batches_processed += 1
 
+    @property
+    def key_properties(self) -> list[str]:
+        return [key.upper() for key in super().key_properties]
+
 
 class TargetMock(Target):
     """A mock Target class."""
diff --git a/tests/core/test_target_base.py b/tests/core/test_target_base.py
index 778fab72..1fd6b9a9 100644
--- a/tests/core/test_target_base.py
+++ b/tests/core/test_target_base.py
@@ -2,6 +2,9 @@ from __future__ import annotations
 
 import copy
 
+import pytest
+
+from singer_sdk.exceptions import MissingKeyPropertiesError
 from tests.conftest import BatchSinkMock, TargetMock
 
 
@@ -28,3 +31,25 @@ def test_get_sink():
         key_properties=key_properties,
     )
     assert sink_returned == sink
+
+
+def test_validate_record():
+    target = TargetMock()
+    sink = BatchSinkMock(
+        target=target,
+        stream_name="test",
+        schema={
+            "properties": {
+                "id": {"type": ["integer"]},
+                "name": {"type": ["string"]},
+            },
+        },
+        key_properties=["id"],
+    )
+
+    # Test valid record
+    sink._singer_validate_message({"id": 1, "name": "test"})
+
+    # Test invalid record
+    with pytest.raises(MissingKeyPropertiesError):
+        sink._singer_validate_message({"name": "test"})