fix(jira): bronze promotion gaps, comment bodies nulled on ingest, staging dedup by the natural key - #2613
Merged
Merged
Conversation
Airbyte creates bronze tables as plain MergeTree; jira__bronze_promoted migrates each one to ReplacingMergeTree(_airbyte_extracted_at) keyed by unique_key. Boards and the four Jira catalogues were absent from that list, so full-refresh syncs kept appending a copy of the whole set with nothing to collapse them, and FINAL on those tables raises "Storage MergeTree doesn't support FINAL" (#1886). All five already carry unique_key as their primary key, so they take the same order_by as the rest. Also drop bronze_jira._boards. jira_sprints needs a parent stream to enumerate board ids, and reconcile auto-selects every stream discover reports (ADR-0015), so an earlier manifest that exposed the parent materialised it as a bronze table. The current manifest declares the parent inline under parent_stream_configs[].stream, which keeps it out of the catalog: nothing writes to the table and no model reads it. Refs #2609, #742 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…the natural key
The comments stream declared `body` as an object while the AddFields
transformation writes the ADF document into that same path as a JSON string.
The destination's typing step cannot serialize a string into a declared object,
so it nulls the field and records
`{"field":"body","change":"NULLED","reason":"DESTINATION_SERIALIZATION_ERROR"}`
in `_airbyte_meta`. The bodies stay available at the source; they are lost on
the way in, on every sync.
The three sibling projections that pass a `tojson` string through the same
mechanism — `jira_issue.custom_fields_json`, `jira_issue_history.items` and
`jira_worklogs.comment` — all declare `string` and carry no serialization
errors, so `body` matches them now rather than declaring a shape the connector
does not produce.
Dedup by `unique_key` instead of `_airbyte_raw_id` in the two staging models
that read append-only bronze. `_airbyte_raw_id` is unique per physical row, so
`LIMIT 1 BY` on it removed nothing and every re-appended copy of a comment or
worklog reached staging. It also blocks recovery: with several copies of one
comment in flight, `_version` is evaluated once per query, so a
ReplacingMergeTree tie-break could keep the row whose body was nulled.
Refs #2609
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
…e-projection Two rules distilled from the jira_comments.body loss: a Jinja AddFields value is a string, and a target declared as an object gets NULLED by the destination's typing step with DESTINATION_SERIALIZATION_ERROR in _airbyte_meta — on every sync, while the source still has the data. And re-projecting fields the payload already carries is what creates that mismatch surface in the first place; renames and typing belong in dbt, injection is only for extraction-time values. Refs #2609 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
…its promotion CI caught a third instance of the AddFields-versus-schema mismatch class: the boards stream computes tenant_id/source_id/unique_key/collected_at but its inline schema never declared them, so the destination drops the values and the bronze table has no unique_key column at all. Declare the four fields, and add them to the DDL snapshot. Promoting jira_boards has to wait for a sync with the fixed schema: every existing row carries a NULL key, ReplacingMergeTree treats NULL keys as equal, and promoting now would collapse the table into a single row. The four catalogues stay promoted, and their snapshot engines flip to ReplacingMergeTree ORDER BY unique_key because the snapshot captures post-dbt state. Refs #2609 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
hello1101n
approved these changes
Aug 18, 2026
…the copy The macro gains an optional CTAS filter, and boards is promoted with `where='unique_key IS NOT NULL'`. Rows synced before the schema declared the AddFields columns carry a NULL key, and ReplacingMergeTree treats NULL keys as equal — unfiltered, promotion would collapse them into one phantom row. The stream is full refresh, so the first sync after this deploy rewrites the complete keyed set and the filtered rows lose nothing; promotion runs in the workflow after the sync step, by which point the key column exists and is populated. Refs #2609 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
mitasovr
enabled auto-merge
August 19, 2026 07:01
github-merge-queue
Bot
removed this pull request from the merge queue due to no response for status checks
Aug 19, 2026
…he test stops sleeping the window out Since the api_budget landed, a 403 counts as a rate-limit hit: with no X-RateLimit-Remaining header the budget layer reads the calls left as zero, and with no reset header it cannot shorten the fixed one-hour window - so the retry that used to be instant (Retry-After: 0) blocks in acquire_call until the window expires. In CI that reads as the job hanging right after the previous test's "Finished syncing". A real secondary-limit 403 from GitHub still reports the hourly budget in X-RateLimit-Remaining - secondary limits meter concurrency, not the hourly quota - so stamping the header makes the mock more faithful, and the remaining-header path takes precedence over the status-code fallback in the CDK. The proxy 429 test is unaffected: the budget policies match only the GitHub API host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
mitasovr
enabled auto-merge
August 19, 2026 10:17
mitasovr
disabled auto-merge
August 19, 2026 10:42
mitasovr
enabled auto-merge
August 19, 2026 10:43
mitasovr
pushed a commit
that referenced
this pull request
Aug 20, 2026
Three staging models conflicted with #2613, which promoted the remaining bronze tables to ReplacingMergeTree and moved staging dedup to the natural key: - jira__bronze_promoted: union of both sides — main's five catalogue promotions plus this branch's four census tables. - jira__task_comments, jira__task_worklogs: keep this branch's projections, which read jira__comment_state / jira__worklog_state. Those already dedup bronze with LIMIT 1 BY unique_key, so main's dedup fix is preserved. The jira descriptor stays at 3.0.0: main did not bump past 2.8.0, so the major carrying the silver contract change is still unreleased. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
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.
Closes part of #2609. Also removes the artifact tracked in #742.
Bronze promotion gap
Airbyte writes bronze tables as plain
MergeTreewithdestinationSyncMode='append', andjira__bronze_promotedmigrates each one toReplacingMergeTree(_airbyte_extracted_at)ordered byunique_key. Five tables were never in that list:bronze_jira.jira_boards— promoted with a NULL-key filter, see the boards section belowbronze_jira.jira_statusesbronze_jira.jira_issuetypesbronze_jira.jira_prioritiesbronze_jira.jira_resolutionsAll five are full-refresh streams, so every sync re-appends the whole set. On a plain
MergeTreenothing ever collapses those copies: background merges do not deduplicate, andFINALraisesStorage MergeTree doesn't support FINAL(#1886). The row count therefore grows without bound relative to the number of distinct entities, and any reader that does not deduplicate by hand multiplies its result by the number of syncs that have run.The four catalogues already carry
unique_keyend to end and take the sameorder_by='unique_key'. The committed DDL snapshot captures post-dbt state, so their engines flip toReplacingMergeTreethere as well.Boards: undeclared AddFields columns
CI caught a third instance of the AddFields-versus-schema mismatch class (the first two:
bodydeclared as an object, and benign number/string coercions). The boards stream computestenant_id/source_id/unique_key/collected_at, but its inline schema never declared them — and the destination materialises columns from the catalog schema, so the values were silently dropped and the bronze table has nounique_keycolumn at all.Seeing the defect and the fix on an instance:
This PR declares the four fields in the schema, and promotes
jira_boardswith a guard: the promotion macro gains an optional CTAS filter, and boards passeswhere='unique_key IS NOT NULL'. Rows synced before the schema fix carry a NULL key, andReplacingMergeTreetreats NULL keys as equal — unfiltered, they would collapse into one phantom row. Filtering them away loses nothing: the stream is full refresh, so the first post-deploy sync rewrites the complete set with keys populated, and promotion runs in the workflow after the sync step, by which point the column exists. No state reset is needed for boards — unlikejira_commentsbelow, every sync is already a full run.Dropping the substream-parent artifact
jira_sprintsis a substream and needs a parent stream to enumerate board ids. Reconcile auto-selects every stream that discover reports (ADR-0015), so a parent visible in the catalog materialises as a real bronze table — the leading underscore in the name is a convention and does not prevent it.The current manifest declares that parent inline under
parent_stream_configs[].stream, which keeps it out of the catalog entirely.bronze_jira._boardsis what an earlier shape left behind: nothing writes to it, no model or script in the repository references it, and board data lives inbronze_jira.jira_boards. The migration drops it.Comment bodies nulled by the destination
The comments stream declares
bodyas an object, while the AddFields transformation writes the ADF document into that same path as a JSON string. The destination's typing step cannot serialize a string into a declared object, so it nulls the field and records this in_airbyte_meta:{"changes":[{"field":"body","change":"NULLED","reason":"DESTINATION_SERIALIZATION_ERROR"}]}Every row whose
bodyis null carries that marker and no other row does. The bodies are still served by the API, so this is loss on the way in, and it recurs on every sync rather than being a one-off.The three sibling projections that hand a
tojsonstring to the same mechanism —jira_issue.custom_fields_json,jira_issue_history.items,jira_worklogs.comment— all declarestringand carry no serialization errors. Declaringbodythe same way matches what the connector actually produces.Dedup by the natural key, not the row id
jira__task_commentsandjira__task_worklogsread append-only bronze throughORDER BY _airbyte_extracted_at DESC LIMIT 1 BY _airbyte_raw_id._airbyte_raw_idis unique per physical row, so thatLIMIT 1 BYremoved nothing and every re-appended copy reached staging. Both now key onunique_key, which is what the ordering was written for.This also gates recovery of the nulled bodies. A re-sync appends a fresh copy of each comment; with the old dedup both copies reach staging,
_versionisnow64(3)and is evaluated once per query, so the ReplacingMergeTree tie-break between them is arbitrary and can keep the row whose body was nulled. With dedup onunique_keyonly the newest bronze row per comment reaches staging.Rolling out on a warm instance
Deploying the fix stops the loss for new and edited comments, but does not bring back the bodies already nulled: the comments stream is incremental on
updatedand its parent carriesincremental_dependency: true, so a comment nobody touches is never re-fetched. Recovery needs a state reset, in this order:jira_comments(its parent's cursor goes with it —incremental_dependency), then run a sync.The order matters because of the dedup fix above: resetting state before the new dedup lands would put both copies of every comment — the nulled one and the re-fetched one — into staging, where the
now64(3)version gives them an identical tie-break and the engine may keep the nulled copy.Bronze needs no cleanup: it is append-only ReplacingMergeTree keyed by
unique_keywith_airbyte_extracted_atas the version, so the re-fetched row wins any correct read and the stale one is swept by a background merge.Verification after the re-sync — this must return zero:
Notes for review
This does not make the data-quality register's
count()-versus-unique_keyrow disappear, and it is not meant to. That check reads withoutFINAL, and a ReplacingMergeTree collapses duplicates only during background merges, so the row reflects merge lag once the engine is right. What the promotion fixes is the case underneath it: on plainMergeTreenothing collapses at all andFINALerrors outright. See Jira data-quality register: checks that cannot reach zero, and one that does not test what it reports #2609 for the register side.The promotion macro documents a race: rows inserted between its
CREATEandEXCHANGEland in the copy it drops. The first promotion of these five tables should land when no Jira sync is active.The migration channel has no ledger and re-runs on every deploy, so it uses
DROP TABLE IF EXISTS.dbt parsewas not usable as a check here: the dbt available locally is dbt-fusion 2.0 preview, which fails on ~900 pre-existing test-argument deprecations across the project. No error referenced the changed model.🤖 Generated with Claude Code