Repository navigation
fix(low-code): skip null parent keys in SubstreamPartitionRouter - #1207
Merged
Merged
Conversation
A parent record whose `parent_key` path exists but holds null produced a slice with a None partition value, so child requests rendered `None` into the path or, with `request_option`, went out without the filter. Treat a null value like a missing path: skip the record and log it at debug level. The parent cursor still observes the record and closes the partition, so parent state is unchanged. 0, False and "" are still emitted.
👋 Greetings, Airbyte Team Member!Here are some helpful tips and reminders for your convenience. 💡 Show Tips and TricksTesting This CDK VersionYou can test this version of the CDK using the following: # Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@bazarnov/fix-substream-null-parent-key#egg=airbyte-python-cdk[dev]' --help
# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch bazarnov/fix-substream-null-parent-keyPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
PyTest Results (Fast)4 985 tests +3 4 973 ✅ +3 7m 15s ⏱️ - 2m 18s Results for commit 338d092. ± Comparison against base commit d214b0c. This pull request removes 1 and adds 4 tests. Note that renamed tests count towards both. |
PyTest Results (Full)4 988 tests +3 4 976 ✅ +3 15m 55s ⏱️ -9s Results for commit 338d092. ± Comparison against base commit d214b0c. This pull request removes 1 and adds 4 tests. Note that renamed tests count towards both. |
Anatolii Yatsuk (tolik0)
approved these changes
Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
👉 TL;DR: Child streams now skip parent records whose linking field is null, the same way they already skip records where that field is missing. Before, those records produced child requests with no usable ID, which usually failed the sync or fetched data without the parent filter.
Specifically,
SubstreamPartitionRouter.stream_slicesnow readsparent_keywithdpath.get(..., default=None)and yields no slice when the value isNone, logging the skip at debug level.0,Falseand""are still emitted.Sacha Brouck (@SBrouck) proposed the same fix in the issue thread.
Changes
substream_partition_router.py: replace thetry/except KeyErroraround theparent_keylookup withdpath.get(..., default=None)and skip the record when the value isNone(missing path, null leaf or null intermediate object). The skip is logged at debug level, which resolves theFIXMEasking for that log.test_substream_partition_router.py: parametrize the missing-key test intotest_substream_partition_router_skips_parent_record_without_partition_valuewithmissing_key,null_value,nested_null_valueandnull_intermediatecases. Thenull_valuecase uses0as the valid key, and the test now also asserts that the parent cursor observes the skipped record.Review Spotlight
Reviewers with limited time, please review first:
substream_partition_router.py#L232-L241: the skip.observe(L224) runs before it andclose_partition(L259) after it, neither gated byemit_slice, so parent state is unaffected.test_substream_partition_router.py#L1245-L1263: the cases, including0as a valid key.Risks
Parent records with a null
parent_keyvalue no longer produce child slices. Manifest-only connectors get this with the next SDM base image bump, Python connectors with their CDK bump.Blast radius on airbyte monorepo
master(fc800755):git grep -l SubstreamPartitionRouterover connectormanifest.yamlfiles finds 241 manifests, and resolving$refs withManifestReferenceResolvergives 1325ParentStreamConfigs under a stream in 238 connectors. 1002 key on the parent's primary key, which is never null. A review of the other 323 (54 connectors) finds realistic null keys in:source-circlecijobs(job_number, nullable in the schema, e.g. for approval jobs): the path isjob/{{ config['job_number'] or stream_partition['job_parent_number'] }}with spec default"2", so by default only duplicate/job/2requests go away. With the override cleared,/job/None(404, sync fails) is no longer requested.source-lookercontent_metadata(content_metadata_id):/content_metadata/None(404, sync fails) is no longer requested. The stream crosses four parents on the same field, so combinations from a null-keyed later parent, which repeated the first parent's request, also drop out. It is a Python connector onairbyte-cdk ^4.0.1, so this only reaches it with a CDK major upgrade.source-survicaterespondents_attributesandrespondents_responses(respondent):respondents//attributesandrespondents//responsesare no longer requested.source-linkedin-adsvideos_creative_posts(content/reference): already filtered on the parent side. The filter is still needed for empty strings.Other effects:
request_option(37 configs, 16 connectors):_get_request_optiondrops aNonevalue (if value:), so the child request went out without the parent filter and its records landed in aNonepartition. That request is no longer made.board_id/pulse_idonly when present, google-ads filterschange_statusby resource type, free-agentperiodis a period index whose0still emits).incremental_dependency(125 configs, 34 connectors), checked with a local end-to-end read where the null-keyed record holds the max parent cursor. No new{partition_field: null}per-partition state is written; existing entries stay unused.Test plan
python -m pytest unit_tests/sources/declarative/partition_routers/test_substream_partition_router.py::test_substream_partition_router_skips_parent_record_without_partition_value -q -p no:cacheprovidersubstream_partition_router.pyfrommain: 2 failed, 2 passed.null_valueandnested_null_valuefail withLeft contains one more item: {'partition_field': None, 'parent_slice': {'slice': 'second'}}.python -m pytest unit_tests/sources/declarative/partition_routers unit_tests/sources/declarative/incremental unit_tests/sources/declarative/retrievers unit_tests/sources/declarative/parsers unit_tests/sources/declarative/test_concurrent_declarative_source.py -q -p no:cacheprovider: 773 passed.ConcurrentDeclarativeSourcewithrequests_mock, parents with a valid, a null and a missingcontent/reference): onmainthe child requests include/posts/Noneand an unscoped/comments; with the fix only/posts/urn1and/comments?post=urn1are requested.ruff format,ruff checkandmypy --config-file mypy.inion the changed files: clean.Fixes #1127