Skip to content

fix: Emitted ACTIVATE_VERSION now honor the stream aliases configured with stream maps#3654

Merged
edgarrmondragon merged 1 commit into
v0.54from
fix/streams-maps-activate-version
May 28, 2026
Merged

fix: Emitted ACTIVATE_VERSION now honor the stream aliases configured with stream maps#3654
edgarrmondragon merged 1 commit into
v0.54from
fix/streams-maps-activate-version

Conversation

@edgarrmondragon
Copy link
Copy Markdown
Collaborator

@edgarrmondragon edgarrmondragon commented May 28, 2026

SSIA

Summary by Sourcery

Ensure ACTIVATE_VERSION messages emitted by mapped streams respect configured stream aliases instead of always using the original stream name.

Bug Fixes:

  • Fix ACTIVATE_VERSION emission to use each stream map alias and skip mappings that remove records.

Tests:

  • Enable ACTIVATE_VERSION message emission in mapped stream tests and update JSONL snapshots to validate correct behavior with stream aliases.

@edgarrmondragon edgarrmondragon self-assigned this May 28, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 28, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Fix ACTIVATE_VERSION emission to respect stream aliases defined by stream maps and extend mapper tests/snapshots to assert consistent aliased stream names across SCHEMA, ACTIVATE_VERSION, and RECORD messages.

Sequence diagram for ACTIVATE_VERSION emission with stream aliasing

sequenceDiagram
    participant Tap as Tap
    participant Stream as MappedStream
    participant StreamMap as StreamMap
    participant Singer as singer

    Tap->>Stream: _write_activate_version_message(full_table_version)
    loop for each stream_map in stream_maps
        Stream->>StreamMap: [check isinstance RemoveRecordTransform]
        alt non RemoveRecordTransform
            Stream->>Tap: write_message(ActivateVersionMessage)
            Note over Stream,Singer: ActivateVersionMessage(stream=stream_map.stream_alias,
            Note over Stream,Singer: version=full_table_version)
        else RemoveRecordTransform
            Stream-->>Stream: [continue]
        end
    end
Loading

File-Level Changes

Change Details Files
Emit ACTIVATE_VERSION messages using stream map aliases instead of the raw stream name.
  • Replace direct singer.write_message call with iteration over self.stream_maps when emitting ACTIVATE_VERSION.
  • Skip RemoveRecordTransform maps when emitting ACTIVATE_VERSION messages.
  • Use the tap instance’s write_message to emit ActivateVersionMessage objects with stream set to stream_map.stream_alias and version set to full_table_version.
singer_sdk/streams/core.py
Update mapped stream tests to enable ACTIVATE_VERSION emission and refresh snapshots accordingly.
  • Initialize MappedTap in mapped stream test with emit_activate_version_messages set to True in config.
  • Re-record snapshot files for various mapped stream scenarios so that expected outputs now include ACTIVATE_VERSION lines using the aliased stream name.
tests/core/test_mapper.py
tests/snapshots/mapped_stream/aliased_stream.jsonl
tests/snapshots/mapped_stream/aliased_stream_batch.jsonl
tests/snapshots/mapped_stream/aliased_stream_not_expr.jsonl
tests/snapshots/mapped_stream/aliased_stream_quoted.jsonl
tests/snapshots/mapped_stream/builtin_variable_original_stream_name_alias.jsonl
tests/snapshots/mapped_stream/builtin_variable_original_stream_name_no_alias.jsonl
tests/snapshots/mapped_stream/builtin_variable_self.jsonl
tests/snapshots/mapped_stream/builtin_variable_stream_name.jsonl
tests/snapshots/mapped_stream/builtin_variable_stream_name_alias.jsonl
tests/snapshots/mapped_stream/builtin_variable_stream_name_alias_expr.jsonl
tests/snapshots/mapped_stream/builtin_variable_underscore.jsonl
tests/snapshots/mapped_stream/changed_key_properties.jsonl
tests/snapshots/mapped_stream/dates.jsonl
tests/snapshots/mapped_stream/drop_property.jsonl
tests/snapshots/mapped_stream/drop_property_null_string.jsonl
tests/snapshots/mapped_stream/fake_credit_card_number.jsonl
tests/snapshots/mapped_stream/fake_email_seed_instance.jsonl
tests/snapshots/mapped_stream/filter_records.jsonl
tests/snapshots/mapped_stream/filter_records_cast_to_bool.jsonl
tests/snapshots/mapped_stream/flatten_all.jsonl
tests/snapshots/mapped_stream/flatten_all_with_dot_separator.jsonl
tests/snapshots/mapped_stream/flatten_depth_0.jsonl
tests/snapshots/mapped_stream/flatten_depth_1.jsonl
tests/snapshots/mapped_stream/json_dumps.jsonl
tests/snapshots/mapped_stream/keep_all_fields.jsonl
tests/snapshots/mapped_stream/map_and_flatten.jsonl
tests/snapshots/mapped_stream/no_map.jsonl
tests/snapshots/mapped_stream/non_pk_passthrough.jsonl
tests/snapshots/mapped_stream/only_mapped_fields.jsonl
tests/snapshots/mapped_stream/only_mapped_fields_null_string.jsonl
tests/snapshots/mapped_stream/record_to_column.jsonl
tests/snapshots/mapped_stream/sourced_stream_1.jsonl
tests/snapshots/mapped_stream/sourced_stream_1_null_string.jsonl
tests/snapshots/mapped_stream/sourced_stream_2.jsonl

Tips and commands

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!

Customizing Your Experience

Access your dashboard 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

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In _write_activate_version_message, the iteration and filtering over self.stream_maps duplicates logic used elsewhere for emitting aliased messages; consider refactoring to share a common helper so that SCHEMA/RECORD/ACTIVATE_VERSION stay consistent if stream-map behavior changes.
  • The special-case isinstance(stream_map, RemoveRecordTransform) check leaks transform-specific knowledge into the core stream logic; moving this decision behind a method/flag on the stream map (e.g., should_emit_messages) would keep concerns better separated.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_write_activate_version_message`, the iteration and filtering over `self.stream_maps` duplicates logic used elsewhere for emitting aliased messages; consider refactoring to share a common helper so that SCHEMA/RECORD/ACTIVATE_VERSION stay consistent if stream-map behavior changes.
- The special-case `isinstance(stream_map, RemoveRecordTransform)` check leaks transform-specific knowledge into the core stream logic; moving this decision behind a method/flag on the stream map (e.g., `should_emit_messages`) would keep concerns better separated.

## Individual Comments

### Comment 1
<location path="tests/core/test_parent_child.py" line_range="508-509" />
<code_context>
     assert tally["sibling"] == 4, msg
+
+
+@time_machine.travel(DATETIME, tick=False)
+def test_activate_version_uses_stream_alias():
+    """ACTIVATE_VERSION must use the mapped stream alias, not the raw stream name.
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a complementary test for non-aliased streams to ensure ACTIVATE_VERSION still uses the original stream name when no `__alias__` is configured.

To fully verify the behavior, please add a second test where `stream_maps` is empty or omits `__alias__` (e.g. `{"raw_name": {}}`) and assert that `SCHEMA`, `RECORD`, and `ACTIVATE_VERSION` all use `"raw_name"`. This will help prevent regressions in the non-aliased path.

Suggested implementation:

```python
    msg = "Sibling records should also be emitted"
    assert tally["sibling"] == 4, msg


def test_activate_version_uses_raw_stream_name_when_no_alias():
    """ACTIVATE_VERSION must use the raw stream name when no alias is configured.

    When stream_maps is empty or does not define __alias__ for a stream, the
    ACTIVATE_VERSION message must carry the original raw stream name so the target
    can still correctly correlate it with the SCHEMA and RECORD messages.
    """
    # Use a stream map that keeps the stream unaliased.
    stream_maps = {"raw_name": {}}

    # This helper should mirror the setup used in `test_activate_version_uses_stream_alias`,
    # but assert that SCHEMA, RECORD, and ACTIVATE_VERSION all use "raw_name".
    _assert_activate_version_stream_name(
        stream_maps=stream_maps,
        expected_stream_name="raw_name",
    )


@time_machine.travel(DATETIME, tick=False)
def test_activate_version_uses_stream_alias():

```

To fully implement this, you should:

1. Introduce a shared helper `_assert_activate_version_stream_name(stream_maps, expected_stream_name)` in this test module (or reuse an existing helper if one already exists in the file) that:
   - Configures/overrides `stream_maps` for the parent/child tap or target.
   - Executes the scenario that produces SCHEMA, RECORD, and ACTIVATE_VERSION messages for the relevant stream.
   - Collects those messages (e.g. via an in-memory target, captured stdout, or existing test utilities).
   - Asserts that:
     * The SCHEMA message's `stream` (or equivalent field) equals `expected_stream_name`.
     * At least one RECORD message for that logical stream uses `expected_stream_name`.
     * The ACTIVATE_VERSION message's `stream` (or equivalent field) equals `expected_stream_name`.

2. Update `test_activate_version_uses_stream_alias` to call `_assert_activate_version_stream_name` with a `stream_maps` that includes `{"raw_name": {"__alias__": "aliased_name"}}` and `expected_stream_name="aliased_name"`, so both tests share the same verification logic.

3. Ensure any fixtures or utilities required by `_assert_activate_version_stream_name` (e.g. tap/target runners, message collectors) are imported or injected into the helper according to the conventions already used elsewhere in `tests/core/test_parent_child.py`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tests/core/test_parent_child.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.81%. Comparing base (7a0a2e5) to head (bfe5079).

Additional details and impacted files
@@           Coverage Diff           @@
##            v0.54    #3654   +/-   ##
=======================================
  Coverage   93.81%   93.81%           
=======================================
  Files          73       73           
  Lines        5948     5951    +3     
  Branches      729      731    +2     
=======================================
+ Hits         5580     5583    +3     
  Misses        273      273           
  Partials       95       95           
Flag Coverage Δ
core 82.40% <100.00%> (+0.07%) ⬆️
end-to-end 75.19% <50.00%> (-0.03%) ⬇️
optional-components 42.73% <0.00%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 28, 2026

Merging this PR will not alter performance

✅ 8 untouched benchmarks


Comparing fix/streams-maps-activate-version (bfe5079) with v0.54 (7a0a2e5)

Open in CodSpeed

@read-the-docs-community
Copy link
Copy Markdown

read-the-docs-community Bot commented May 28, 2026

@edgarrmondragon edgarrmondragon force-pushed the fix/streams-maps-activate-version branch from 0bb0bc0 to 020cc39 Compare May 28, 2026 19:08
…ed with stream maps

Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
@edgarrmondragon edgarrmondragon force-pushed the fix/streams-maps-activate-version branch from 020cc39 to bfe5079 Compare May 28, 2026 19:13
@edgarrmondragon
Copy link
Copy Markdown
Collaborator Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new implementation skips emitting ACTIVATE_VERSION entirely when self.stream_maps is empty, which changes prior behavior; consider falling back to self.name in that case so unmapped streams still emit an activation message.
  • Looping over all stream_maps and emitting an ACTIVATE_VERSION for each non-RemoveRecordTransform may produce duplicate activation messages when multiple maps share the same stream_alias; consider deduplicating on alias or clearly constraining expected configurations.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new implementation skips emitting ACTIVATE_VERSION entirely when `self.stream_maps` is empty, which changes prior behavior; consider falling back to `self.name` in that case so unmapped streams still emit an activation message.
- Looping over all `stream_maps` and emitting an ACTIVATE_VERSION for each non-`RemoveRecordTransform` may produce duplicate activation messages when multiple maps share the same `stream_alias`; consider deduplicating on alias or clearly constraining expected configurations.

## Individual Comments

### Comment 1
<location path="singer_sdk/streams/core.py" line_range="858-869" />
<code_context>
+            if isinstance(stream_map, RemoveRecordTransform):
+                continue
+
+            self._tap.write_message(
+                singer.ActivateVersionMessage(
+                    stream=stream_map.stream_alias,
+                    version=full_table_version,
+                )
             )
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid emitting duplicate ACTIVATE_VERSION messages for the same mapped alias.

If `self.stream_maps` can contain multiple entries with the same `stream_alias`, this will emit duplicate `ACTIVATE_VERSION` messages for that alias. Even if the protocol tolerates this, it adds noise and may confuse some targets. Consider de‑duplicating by alias (e.g., keep a `seen_aliases` set and emit once per alias), unless there’s a requirement to send one message per mapping.

```suggestion
    def _write_activate_version_message(self, full_table_version: int) -> None:
        """Write out an ACTIVATE_VERSION message."""
        seen_aliases = set()

        for stream_map in self.stream_maps:
            if isinstance(stream_map, RemoveRecordTransform):
                continue

            stream_alias = stream_map.stream_alias
            if stream_alias in seen_aliases:
                continue

            seen_aliases.add(stream_alias)

            self._tap.write_message(
                singer.ActivateVersionMessage(
                    stream=stream_alias,
                    version=full_table_version,
                )
            )
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread singer_sdk/streams/core.py
@edgarrmondragon edgarrmondragon merged commit 85f64b4 into v0.54 May 28, 2026
59 of 61 checks passed
@edgarrmondragon edgarrmondragon deleted the fix/streams-maps-activate-version branch May 28, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant