fix(dbt): align staging contributor types with their silver union targets - #2101
Conversation
…gets The staging -> silver field-parity audit (#2080) fails on 13 type divergences across 5 root causes. A silver class table is a positional UNION ALL of its staging contributors, so each divergence either widens the published silver type depending on which connectors are enabled, or coerces values at the insert boundary. All fixes cast the outlier branch toward the type the data-carrying branch already publishes; silver schemas on warm clusters do not change (every write stays compatible), so no full-refresh is required for correctness — only for the deployed staging tables' declared types to converge, which can happen in any convenient window: dbt run --full-refresh --select github__pull_requests_commits \ gitlab__pull_requests_commits salesforce__crm_accounts \ salesforce__crm_activities salesforce__crm_contacts \ salesforce__crm_deals salesforce__crm_users \ m365__collab_document_activity_sharepoint * commit_order (github, gitlab): the models emit a literal `0` (the APIs provide no ordering), which ClickHouse types as UInt8; bitbucket emits Int64 from real data. toInt64(0) pins the branch to the contract type. * custom_fields (5 salesforce models, 6 branches): passed through from bronze as Nullable(String) while heal_crm_table ALTERs the silver tables to `String DEFAULT '{}'` — NULLs were being coerced to the default at insert time via insert_null_as_default. coalesce makes the '{}' fallback explicit in the model, matching hubspot's literal. * visited_page_count (m365 sharepoint): bronze carries the JSON `number` as Nullable(Decimal(38, 9)) and the model passed it through; the onedrive branch emits Nullable(Int64). A page count is integral — cast to Int64. Nothing downstream reads the column (checked migrations, gold, silver), so the silver type change on fresh clusters is safe. * close_date (hubspot deals): toDate() narrowed to Date while salesforce bronze is natively Nullable(Date32). toDate32 matches the wider type. * hire_date/termination_date (ms-entra, active-directory): constant-NULL placeholders typed Nullable(Date) against the Nullable(DateTime) the data-carrying branches (bamboohr, workday) emit. Retype the NULLs; zero rows are affected by construction. The regenerated snapshot carries the visited_page_count type change and also normalizes silver.contract_version to dump-ddl.sh's statement format — that entry was appended by hand with the semicolon on the statement line, a shape the dumper never produces, so the next convergence check would have flagged it as drift. Verified on a from-scratch bootstrap (all connectors green, dbt PASS=218/ERROR=0): the field-parity audit reports 0 failures, down from 13, across 39 union targets and 273 relations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
📝 WalkthroughWalkthroughConnector dbt models now use aligned nullable numeric, string, and datetime types. Salesforce models default null ChangesConnector schema normalization
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 field-parity audit reports 61 nullable-widening warnings: a branch declares `T` where its silver union target publishes `Nullable(T)`, because some other branch of the same target is nullable. The published silver type is correct — NULL there means "this source cannot measure this" (bitbucket carries no diff stats, zoom cannot split meeting kinds) and must stay distinct from a measured zero — but it was a supertype accident, dependent on which connectors happen to be enabled. Pin it: every such branch now emits Nullable explicitly via toNullable(), so the silver type no longer depends on the connector set. 47 casts across 17 models (ai counters, m365 meeting counters, git files_changed/lines_added/lines_removed, chat/meeting user_name + email, crm and task boolean flags). Values are untouched — same numbers, same zeros; only the declared type widens. Two of these were previously masked in CI: heal_collab_chat_table ALTERs slack/zulip/m365 staging to the contract types after dbt runs, so slack.direct_and_group_messages only surfaced once the table was rebuilt from the model. The model now owns the type instead of relying on the heal. NOT aligned — left as warnings on purpose: data_source, day, insight_source_id/field_* (jira metadata), status_id/status_name, collected_at, timestamp (15 findings). Those are mandatory-by-meaning columns where the nullable branch is the defect and the contract should narrow to NOT NULL — the opposite direction, tracked for the silver→gold contract work rather than blanket-widened here. Verified against the same from-scratch warehouse: 0 failures, warnings 61 -> 15, and the re-dumped connectors-ddl snapshot is byte-identical — silver already published these Nullable types, so nothing downstream moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
Why
The staging -> silver field-parity audit (#2080) fails on 13 type divergences. A silver
class_*table is a positionalUNION ALLof its staging contributors, so each divergence either silently changes the published silver type depending on which connectors are enabled, or coerces values at the insert boundary.What
13 failures = 5 root causes; every fix is a one-line cast in the outlier staging branch, toward the type the data-carrying branch already publishes:
commit_orderis a literal0(the APIs provide no ordering) which ClickHouse types asUInt8; bitbucket emitsInt64from real datatoInt64(0)custom_fieldspassed through asNullable(String)whileheal_crm_tableALTERs silver toString DEFAULT '{}'— NULLs were coerced to the default at insert time viainsert_null_as_defaultcoalesce(custom_fields, '{}'), matching hubspot's literalvisited_page_count: bronze carries the JSONnumberasNullable(Decimal(38, 9)), passed through; onedrive branch emitsNullable(Int64). A page count is integralCAST(... AS Nullable(Int64))close_datenarrowed toDatebytoDate()while salesforce bronze is nativelyNullable(Date32)toDate32(...)hire_date/termination_date: constant-NULL placeholders typedNullable(Date)vs theNullable(DateTime)the data-carrying branches (bamboohr, workday) emitThe regenerated snapshot carries the
visited_page_counttype change and also normalizessilver.contract_versiontodump-ddl.sh's statement format — that entry was appended by hand (semicolon on the statement line, a shape the dumper never produces), so the next convergence run would have flagged it as drift.Rollout
No full-refresh is required for correctness: silver schemas on warm clusters do not change, and every new write is compatible with the old staging column types (
Int64value0intoUInt8,StringintoNullable(String),Int64intoDecimal,Date32intoDate, NULL into NULL). Gold is untouched in all modes — the one silver type that changes on fresh builds (visited_page_count) has no readers in migrations, gold, or silver (checked), andapply-ch-migrations.shre-CREATEs gold views on every deploy anyway.To converge the declared types of already-deployed staging tables, run in any convenient window:
(Scoped exactly to these models — no
+. The github/gitlab rebuilds stamp fresh_versions, so the next silver increment after the refresh is one-time heavier.)Second commit: nullable-widening warnings (61 -> 15)
The audit also reports 61 warnings — branches declaring
Twhere the target publishesNullable(T)because another branch is nullable. The published type is correct (NULL = "this source cannot measure this", distinct from a measured zero), but it was a supertype accident dependent on the connector set. The second commit pins it: 47 explicittoNullable()casts across 17 models. Values untouched, and the re-dumped snapshot is byte-identical — silver already published these types.Two of those were masked in CI until now:
heal_collab_chat_tableALTERs slack/zulip/m365 staging after dbt, soslack.direct_and_group_messagesonly surfaced when the table was rebuilt from the model. The model now owns its type instead of relying on the heal.The 15 remaining warnings are deliberate:
data_source,day, jira field-metadata keys,status_id/status_name,collected_at,timestamp— mandatory-by-meaning columns where the nullable branch is the defect and the contract should narrow to NOT NULL. That is the opposite direction (silver narrowing + not_null tests), tracked for the silver→gold contract work.Verification
From-scratch bootstrap on this branch (all connectors green, dbt
PASS=218 / ERROR=0), then the field-parity audit from #2080:— down from 13 failures and 61 warnings. The touched staging models were rebuilt with
--full-refreshlocally before re-auditing, and the snapshot was re-dumped after both commits.🤖 Generated with Claude Code
Summary by CodeRabbit