Repository navigation
fix(low-code): close ConcurrentCursor end-window and clamping gaps - #1204
Merged
Baz (bazarnov) merged 1 commit intoOct 6, 2026
Merged
Conversation
- Read the final one-unit window when lower == upper and the last window includes its end (`cursor_granularity` set), as the legacy `DatetimeBasedCursor` does. The check runs after the start date and the clamp move the lower bound. `is_compare_strictly` skips the window: the factory passes it to `ConcurrentCursor` (new kwarg, default False) and `copy_without_state` keeps it. A gap that the start date or the clamp closes is skipped. - `DayClampingStrategy` ceiling keeps midnight values instead of adding a day, and the `WeekClampingStrategy` floor keeps the target weekday (at midnight). - `_split_per_slice_range` clamps the lower bound before stepping and stops when it passes the end, ends single windows at the end when there is a granularity, and treats a boundary clamped onto the end as the last window.
👋 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-concurrent-cursor-end-window-and-clamping#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-concurrent-cursor-end-window-and-clampingPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
PyTest Results (Fast)4 998 tests +16 4 986 ✅ +16 11m 1s ⏱️ + 1m 28s Results for commit b43e16b. ± Comparison against base commit d214b0c. This pull request removes 1 and adds 17 tests. Note that renamed tests count towards both. |
PyTest Results (Full)5 001 tests +16 4 989 ✅ +16 12m 44s ⏱️ - 3m 20s Results for commit b43e16b. ± Comparison against base commit d214b0c. This pull request removes 1 and adds 17 tests. Note that renamed tests count towards both. |
Daryna Ishchenko (darynaishchenko)
approved these changes
Oct 6, 2026
Baz (bazarnov)
deleted the
bazarnov/fix-concurrent-cursor-end-window-and-clamping
branch
October 6, 2026 09:30
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: Date-windowed incremental syncs stop silently skipping data in two cases. A date range that starts and ends on the same day (or other time unit) now reads that unit instead of nothing. Clamped windows (
clampingDAY, WEEK or MONTH) with a step of at least one day no longer skip days or get stuck, and with acursor_granularitythey no longer read into a period that is not over yet.Specifically,
ConcurrentCursor._split_per_slice_rangenow emits the final[end, end]window whencursor_granularityis set (skipped whenis_compare_strictlyis set, which the factory now passes),DayClampingStrategyand theWeekClampingStrategyfloor keep already-aligned values, and clamped slicing steps from the clamped lower bound.Changes
_split_per_slice_rangeno longer returns early whenlower == upperand acursor_granularityis set. Once the start date and the clamp are applied,lower == upperon the last window emits the one-unit[upper, upper]window, as the legacyDatetimeBasedCursordoes. Onmainthat window only appeared when a lookback moved the lower bound below the end.ConcurrentCursortakesis_compare_strictly(defaultFalse) and keeps it incopy_without_state. When it is set, no window starts at the end, also when a lookback, the start date or the clamp moved the lower bound there.create_concurrent_cursor_from_datetime_based_cursorpasses the manifest value, which also covers per-partition cursors.mainemitted an inverted or overlapping window there.DayClampingStrategyceiling returns midnight values unchanged instead of adding a day. This guard follows the patch Varun Billuri (@varunbiluri) proposed inDayClampingStrategyceiling skips a full day for already-midnight values (clamping: DAYloses one day per window) #1110.WeekClampingStrategyfloor keeps a value on the target weekday on that day (at midnight) instead of moving it a week back._split_per_slice_rangeclamps the lower bound before stepping and returns when it passes the end. A step shorter than the clamping period no longer collapses the first window (for WEEK and MONTH the step still has to be at least one day, see Risks), and when no complete period is left nothing is requested past the end.test_cursor.py,test_clamping.pyandtest_model_to_component_factory.py. Two existing assertions encoded the bugs and are updated: the gap test with a granularity now also expects the[50, 50]end unit (its name now says so), and the DAY "last day of month" test uses a non-midnight input.Review Spotlight
Reviewers with limited time, please review first:
cursor.py#L489-L501: early returns. With a granularity,lower == upperis decided after the start date and the clamp: the end unit is read on the last window only, and never underis_compare_strictlycursor.py#L502-L513: single-window endcursor.py#L537-L540: last-window detection in the stepping loopclamping.py#L38-L42: DAY ceiling guardclamping.py#L94-L98: WEEK floor guardRisks
cursor_granularity, no lookback, and an end that does not move between syncs (a fixedend_date, or a day-precision end such astoday_utc()with several syncs a day) re-reads the end unit on every later sync once a record on that unit has moved the state to the end: one request per partition per sync, and that unit's records are emitted again.mainmakes no request there. The last window always starts at the state value, so every other sync already re-reads the state unit, and the legacyDatetimeBasedCursorre-reads it in this case too. The state value does not change, and append+dedup absorbs the duplicates.manifest.yamlfiles on airbytemasteratfc800755: 271 connectors define an inlineDatetimeBasedCursor, 73 of them with a non-zerocursor_granularity, and 69 of those setend_datetime(47 with no static lookback, 13 with a static lookback, 13 with a lookback from config; the groups overlap).source-amplitude,source-app-store-connect,source-claude,source-coingecko-coins,source-dust,source-google-search-console,source-shiftbase,source-tempo,source-tvmaze-schedule,source-twilio,source-wikipedia-pageviews,source-spotify-ads, andsource-tiktok-marketingdaily reports only withattribution_window: 0. Streams with any lookback, or a now-based end at second precision, are unaffected in practice.source-tiktok-marketingdaily reports withattribution_window: 0).is_compare_strictly. The concurrent cursor ignored it onmain, so start == end with a lookback sent a[start, start]request. It now skips every window that would start at the end, as the legacy cursor does. Its monorepo users only see a difference when start and end resolve to the same value, which now makes no request:source-snapchat-marketing(cursor_granularity: PT0S) andsource-reprise(no granularity). PythonConcurrentCursorusers (source-salesforce,ConcurrentSourceAdapter) do not set it.manifest.yamlfiles on airbytemasteratfc800755setclamping(git grep -E '^[[:space:]]*clamping:'), and no connector code references the clamping classes, so only Builder and custom connectors that use it see these changes: days skipped at window boundaries are read; MONTH/WEEK streams with a step of at least one day that were stuck onmainmove again, and their first sync after upgrading can backfill several periods; with a granularity no window reaches past the end (without one, windows still end at the next period boundary, as onmain); WEEK syncs on the target weekday include the last complete week; a midnight-aligned DAY state re-reads its own day, as non-clamped cursors do; a gap or a strict end window that the clamp closes makes no request.main. On the boundary day their ceiling returns that day's midnight for a non-midnight value, so the first window collapses.ConcurrentCursorgets one keyword argument with a default.DayClampingStrategy.clampreturns midnight inputs unchanged, andWeekClampingStrategy(is_ceiling=False).clampkeeps a value on the target weekday on that day (at midnight).Test plan
python -m pytest unit_tests/sources/streams/concurrent/test_cursor.py unit_tests/sources/streams/concurrent/test_clamping.py unit_tests/sources/declarative/parsers/test_model_to_component_factory.py -qmain(git checkout origin/main -- airbyte_cdk/sources/streams/concurrent/cursor.py airbyte_cdk/sources/streams/concurrent/clamping.py airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py) with this branch's tests: 15 failed, 392 passed. 10 fail on slice or clamp assertions, 5 raiseTypeErrorfor the newis_compare_strictlykwarg.clamp(upper) == uppercondition changes no result: the lower-bound clamp already returns when clamping would move the end unit past the end.python -m pytest unit_tests/sources/streams unit_tests/sources/declarative/incremental unit_tests/sources/declarative/parsers unit_tests/sources/declarative/retrievers unit_tests/sources/declarative/partition_routers unit_tests/sources/declarative/stream_slicers unit_tests/legacy unit_tests/connector_builder -q: 1773 passed.unit_tests/sources/declarativefolders plusunit_tests/source_declarative_manifest: 1649 passed.test_read_with_concurrent_and_synchronous_streams*was deselected because it can crash locally on macOS (Cached parent stream with more than one partition corrupts the shared sqlite request cache under concurrent reads (no such table: responses,file is not a database,no more rows available) #1150).DatetimeBasedCursorvs the factory-builtConcurrentCursorfor start == end, with and withoutis_compare_strictlyand a lookback: the same slices in all 4 cases.maindiffers in 2.[end, end]window wheremainemits nothing, all with a granularity and no lookback; every other slice is identical.mainhas a gap in 368 DAY, 184 WEEK and 92 MONTH configs and a window past the end in 232, 168 and 190. This branch has no gap, inverted or misaligned window; the 136 remaining past-the-end cases are configs without a granularity, which keep ending at the next period boundary as onmain.ruff format,ruff checkandmypy --config-file mypy.inion the changed files: clean.Fixes #1198
Fixes #1110