From a4a0bac76139304ef094c461c5d146113ecd801f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:50:21 +0000 Subject: [PATCH] fix: treat empty string cursor field as no cursor field When cursor_field is [''] (list with empty string) instead of [] (empty list), treat it as no cursor field. This can occur when the platform serializes 'no cursor configured' streams incorrectly. Fixes airbytehq/oncall#10700 Co-Authored-By: unknown <> --- .../declarative/parsers/model_to_component_factory.py | 10 ++++++++-- airbyte_cdk/sources/streams/concurrent/helpers.py | 3 ++- .../parsers/test_model_to_component_factory.py | 6 ++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py b/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py index 3a772b6912..88ff142c7a 100644 --- a/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +++ b/airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py @@ -4339,8 +4339,14 @@ def _get_catalog_defined_cursor_field( configured_stream = self._stream_name_to_configured_stream.get(stream_name) # Depending on the operation is being performed, there may not be a configured stream yet. In this - # case we return None which will then use the default cursor field defined on the cursor model - if not configured_stream or not configured_stream.cursor_field: + # case we return None which will then use the default cursor field defined on the cursor model. + # We also treat cursor_field: [""] (list with empty string) as no cursor field, since this can + # occur when the platform serializes "no cursor configured" streams incorrectly. + if ( + not configured_stream + or not configured_stream.cursor_field + or not configured_stream.cursor_field[0] + ): return None elif len(configured_stream.cursor_field) > 1: raise ValueError( diff --git a/airbyte_cdk/sources/streams/concurrent/helpers.py b/airbyte_cdk/sources/streams/concurrent/helpers.py index 5e2edf055e..acc4cf7a21 100644 --- a/airbyte_cdk/sources/streams/concurrent/helpers.py +++ b/airbyte_cdk/sources/streams/concurrent/helpers.py @@ -34,7 +34,8 @@ def get_cursor_field_from_stream(stream: Stream) -> Optional[str]: raise ValueError( f"Nested cursor fields are not supported. Got {stream.cursor_field} for {stream.name}" ) - elif len(stream.cursor_field) == 0: + elif len(stream.cursor_field) == 0 or not stream.cursor_field[0]: + # Treat cursor_field: [""] (list with empty string) as no cursor field return None else: return stream.cursor_field[0] diff --git a/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py b/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py index dcdc2bcffb..0c3ea0a782 100644 --- a/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py +++ b/unit_tests/sources/declarative/parsers/test_model_to_component_factory.py @@ -5051,6 +5051,12 @@ def test_create_stream_with_multiple_schema_loaders(): "updated_at", id="test_allow_catalog_defined_cursor_field_false_defaults_to_stream_defined_cursor_field", ), + pytest.param( + True, + "", + "updated_at", + id="test_empty_string_catalog_cursor_field_defaults_to_stream_defined_cursor_field", + ), ], ) def test_catalog_defined_cursor_field(