GitHub
10/08/2026, 11:39 AMmain)
Is this a regression?
• Yes
Python Version
3.10
Bug scope
Taps (catalog, state, etc.)
Operating System
Linux
Description
When discover_streams() returns two streams with the same name, a top-level stream is replaced by the last one, but a child stream is not: every child stream of that name syncs, and the stream gets one set of records from each.
Tap.streams keeps the last stream of a name, so the catalog and the top-level sync loop see one stream. Tap.load_streams() adds every discovered stream to parent.child_streams before that happens, and Stream._sync_children() syncs each child in that list that is selected. The catalog entry for the name selects all of them.
A tap hits this when it lets a user-defined stream replace a built-in stream of the same name, for example a custom query stream.
from singer_sdk import Stream, Tap
from singer_sdk import typing as th
class Parent(Stream):
name = "parent"
schema = th.PropertiesList(th.Property("id", th.IntegerType)).to_dict()
def get_records(self, context):
yield {"id": 1}
def get_child_context(self, record, context):
return {"parent_id": record["id"]}
class Child(Stream):
name = "child"
parent_stream_type = Parent
schema = th.PropertiesList(th.Property("source", th.StringType)).to_dict()
def get_records(self, context):
yield {"source": type(self).name}
class ChildOverride(Child):
pass
class MyTap(Tap):
name = "tap-example"
config_jsonschema = th.PropertiesList().to_dict()
def discover_streams(self):
return [Parent(self), Child(self), ChildOverride(self)]
MyTap(config={}).sync_all()
The child stream gets two records:
{"stream": "child", "record": {"source": "Child"}}
{"stream": "child", "record": {"source": "ChildOverride"}}
Without parent_stream_type = Parent on Child, it gets one record, from ChildOverride.
The two streams do not need to be different classes. Two instances of one class with the same name also both sync as children.
Possible fixes, in `load_streams()`:
• Keep only the last stream of each name before the parent-child links are built, which matches Tap.streams.
• Raise an error when two discovered streams have the same name.
Link to Slack/Linen
No response
---
🤖 Generated with Claude Code
meltano/sdk