Garv Pushkarna data-assigment submission - #9
Open
Garvp wants to merge 2 commits into
Open
Conversation
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.
Summary
End-to-end CDC pipeline that mirrors a 6-table wallet/payments/transfers source system into a lake (immutable change history) and a warehouse (current-state + SCD2 history). It detects schema drift, stops the line on incompatible changes, supports time-travel reconstruction and operational restore, and enforces source-to-warehouse validation parity. Implemented in Python + DuckDB and runnable in a single process.
Layered into six modules with narrow contracts:
~1,400 LOC of pipeline + ~1,300 LOC of tests across 9 test modules covering modeling, CDC correctness, schema-change safety, warehouse correctness, time travel, catalog metadata, data quality, and full end-to-end scenarios.
See
submission/wallet-payment-transfer/ARCHITECTURE.mdfor the layered design walk-through andsubmission/wallet-payment-transfer/ASSUMPTIONS.mdfor tradeoffs and production analogues.Source Schema Design
customers— registered platform userswallets— balance accounts; one customer holds many (per-currency)transfers— money movements between wallets; have business identitypayment_methods— saved card / bank / upi tied to a customertransfer_events— state transitions for a transferledger_entries— append-only credit/debit on a walletVARCHARnatural identifierspayment_methods → customers,wallets → customers,transfers → wallets ×2 + payment_methods,transfer_events → transfers,ledger_entries → wallets + transfersupdated_at), referential lookups (customer_id,transfer_id), filtered scans (status), and audit-by-wallet ((wallet_id, occurred_at))CHECKandNOT NULL):wallet.balance >= 0transfer.amount > 0transfer.from_wallet_id != transfer.to_wallet_idtransfer.settled_at IS NULL OR settled_at >= created_atledger_entries.signed_amountconsistent withdirectionandamountcustomers.status,payment_methods.method_type/.status,wallets.currency/.status,transfers.status/.currency,transfer_events.to_status,ledger_entries.directionSchema is mirrored in versioned, frozen contracts at
source/contracts.py, which are the single source of truth for the schema detector and the data-quality rules.CDC Strategy
CDCCapturewraps everyinsert,update, anddeletecall into aCDCRecordwith operation, table, primary_key, before-image, after-image, monotonicsequence, andcaptured_at. The sequence is the LSN/Kafka-offset analogue and the primary correctness lever._cdc_seq=record.sequenceand opens an SCD2 history row with_is_current=true,_valid_from=captured_at,_op='insert'._valid_to=captured_at,_is_current=false) and opens a new one with the post-image; updates the current-state row in place._op='delete'row (with the pre-image); flips current-state's_deleted=true(soft delete preserves analytics over deleted rows).pipeline_checkpoints): one row each forlakeandwarehouse.run_tickreads frommin(checkpoint_lake, checkpoint_warehouse), so a tick that wrote the lake but failed before the warehouse merge replays cleanly.sequencePK silently drops duplicate appends.record.sequence > existing._cdc_seq, making it idempotent and out-of-order safe.set_checkpointrefuses to move backwards — bugs surface loudly.test_replay_does_not_double_apply,test_orchestrator_run_tick_is_idempotent, andtest_duplicate_sequence_is_deduped.Lake and Warehouse Modeling
lake_cdc_events(sequence PK, operation, table_name, primary_key, before_image JSON, after_image JSON, captured_at). One row per CDC event, no rewrite, no overwrite, no aggregation.replay_records()returns events in sequence order and is the warehouse's recovery feed.T:wh_T— current-state with_cdc_seq(last applied sequence) and_deleted(soft delete). Same column shape as the source +_cdc_seq+_deleted.wh_T_history— SCD2 with(business_key, _valid_from)PK and_valid_to,_is_current,_opcolumns. Every revision lands here;_is_current=trueinvariant: at most one row per PK.state_at(conn, table, ts)reconstructsTat any timestamp fromwh_T_historyusing_valid_from <= ts < _valid_to. Excludes deleted rows automatically. Tested intest_time_travel.pyagainst multiple inserted revisions.restore_current_state_to(conn, table, ts)overwriteswh_Twith the snapshot fromstate_at(table, ts)and does not modify history — restore is reversible, history is the system of record._cdc_seqresets to 0 so subsequent CDC events apply cleanly. Verified bytest_restored_state_accepts_new_cdc_records.Schema Change Safety
SchemaDetectorintrospects the live source viainformation_schema.columnsandduckdb_constraints()and compares againstsource/contracts.py. Eight rules:missing_table,column_dropped,type_changed,nullability_tightened,non_nullable_column_added,primary_key_changedcolumn_added(additive),enum_value_added(drift surface, not necessarily breaking)run_tick(..., fail_on_drift=True)runsdetector.check_all()before reading any new events. On a breaking drift it returns{aborted: true, reason: "schema_contract_violation", drift_summary: ...}and writes nothing to the lake or warehouse. Tested intest_run_tick_aborts_on_breaking_drift.CDCCapture(detector, require_compatible_schema=True)raisesSchemaContractViolationon every write while a breaking drift is present. The capture log stays empty. Tested intest_capture_with_strict_schema_refuses_writes_on_dropandtest_capture_refuses_writes_when_strict.scripts/check_schema_contracts.pyis the CI entry; it exits non-zero with a printed drift summary on any breaking finding, suitable for theschema-contract-checkGitHub status check.run_tick's aborted dict includes adrift_summaryfor log aggregation / Slack alerts.report.warningsand never abort — they are operational signals, not failures.Validation Parity
contracts.py:sys.pk_unique.<table>for every PKsys.not_null.<table>.<col>for every NOT NULL columnsys.enum.<table>.<col>for every enum-domained columnsys.fk.<table>.<col>for every FK (ignoring_deleted=truerows)biz.wallet.balance_non_negativebiz.transfer.amount_positivebiz.transfer.no_self_transferbiz.transfer.settled_at_after_createdbiz.transfer.settled_implies_settled_atbiz.transfer.failed_implies_reasonbiz.ledger.signed_amount_consistentbiz.ledger.balance_after_non_negativebiz.ledger.reconciles_to_wallet_balance(monetary integrity:SUM(signed_amount) per wallet == wh_wallets.balance)biz.transfer_event.allowed_transition(state machine validity)scripts/run_data_quality_checks.py:run_checks(conn)returns[(rule, count_of_failing_rows), ...]. The CI script exits non-zero on any failure and prints the rule_id + offending count.tests/test_data_quality.pyproves both directions: the seeded warehouse passes all rules and every individual rule fires when its invariant is violated (tested for PK uniqueness, enum, FK, negative balance, settled_at backward, ledger consistency, ledger reconciliation, status transition).test_every_contract_not_null_has_a_warehouse_rule) blocks a contract change without a corresponding rule.Catalog Exposure
13 datasets in
catalog/catalog.jsoncovering every output the pipeline produces:lake_cdc_eventswh_customers,wh_payment_methods,wh_wallets,wh_transfers,wh_transfer_events,wh_ledger_entries_historyEach dataset publishes:
name,layer(lake/warehouse),type(append-only-change-log/current-state/scd2-history)description,owner(data-platform),consumers(per-dataset list: finance / treasury / fraud / audit / analytics / product / support)update_cadence,sla_class(money-critical datasets aretier-1)tagsfor governance (monetary-integrity,pii,time-travel, etc.)primary_keyandschema/schema_extensionDiscovery / access in this scope: consumers
cat catalog/catalog.jsonorpython -m scripts.validate_catalogto verify it matches the pipeline outputs. Production analogue is DataHub / Atlan / Unity Catalog — the JSON shape is intentionally close to those tools' dataset envelopes so the migration is a serializer swap.tests/test_catalog.pyenforces:layer,type,sla_classappend-only-change-log, warehouse non-history →current-state, warehouse_history→scd2-historyValidation
Local validation commands run in order:
All five CI gates exit non-zero on failure and are mapped 1-1 to the checks listed in
github_pr_checks_data.md.Known Limitations / Next Steps
CDCCapturewith a Debezium stub — the current shape is Debezium-faithful (envelope columns, before/after, sequence == LSN) but does not exercise transactional grouping or snapshot-vs-streaming modes. With a stub we could test "snapshot then switch to streaming" explicitly.dq.freshness.<table>rule comparingMAX(_cdc_seq)against expected cadence is a one-day add.Dockerfile+docker-compose.ymlrunning Postgres + Kafka + this orchestrator would make the production analogue concrete. The contracts inpipeline/cdc.pyandpipeline/schema_detector.pyare designed to swap targets.column_added(additive) drift we currently emit a WARN; we could instead auto-extend the warehouse table inside the same tick. Out of scope by assignment.None of these change the architecture; they are additive.
Responsible AI Usage
_cdc_seqguard handles both duplicate replay AND out-of-order arrival;restore_current_state_toneeds to reset_cdc_seq=0so subsequent CDC applies cleanly).