Skip to content

feat(warehouse): deltalite real-write path (phase 2, flag-gated) - #75884

Merged
Gilbert09 merged 7 commits into
masterfrom
tom/deltalite-write-canary
Jul 31, 2026
Merged

feat(warehouse): deltalite real-write path (phase 2, flag-gated)#75884
Gilbert09 merged 7 commits into
masterfrom
tom/deltalite-write-canary

Conversation

@Gilbert09

@Gilbert09 Gilbert09 commented Jul 31, 2026

Copy link
Copy Markdown
Member

Problem

Phase 1 (the deltalite shadow) ran across the whole warehouse fleet on real prod S3 and proved deltalite is byte-for-byte equivalent to the delta-rs MERGE — the only divergences were deltalite being more correct (collapsing the null-composite-key duplicates that MERGE accumulates, since fixed by #75601). deltalite's real upsert + conditional-put commit path is therefore already well-exercised.

This PR does two things: (1) lets deltalite perform the real incremental merge behind a flag (phase 2 canary), and (2) removes the now-redundant phase-1 shadow.

Changes

Phase 2 — real writes. When the per-schema data-warehouse-deltalite-write flag matches, write_to_deltalake routes the incremental merge to deltalite.DeltaLiteTable.upsert instead of the delta-rs MERGE.

  • Flag-only — no env switch. Controlled entirely by the data-warehouse-deltalite-write flag, so it can be ramped or killed from the flag UI without a deploy. (Per-merge flag eval is a couple of indexed lookups; negligible fleet-wide.)
  • metrics: warehouse_load_deltalite_write_total{outcome=written|fallback}.

Important

Fallback by construction. _write_via_deltalite returns False — and the delta-rs MERGE runs as today — on anything: flag off, import failure, deltalite error, commit conflict, unsupported table. Enabling a schema can only change which engine writes, never whether the sync succeeds. Nothing changes until the flag is on.

Removes the phase-1 shadow. It did its job (proved equivalence + found the MERGE null-key bug), and running it alongside phase 2 is redundant. Deletes deltalite_shadow.py (compare engine, seed/upsert/cleanup, sampling + caps), the DATA_WAREHOUSE_DELTALITE_SHADOW_* settings, the shadow metrics, and the write_to_deltalake shadow hook. The per-schema flag helper the write path needs is kept in a focused new module, deltalite_write.py. Net −712 lines.

Rollout (after merge — no deploy needed to enable)

  1. Turn data-warehouse-deltalite-write on for team 2 only (dogfood), excluding Sentry.
  2. Watch warehouse_load_deltalite_write_total{outcome} + the deltalite write: logs, confirm team-2 table contents, ramp.

Note

On an already-bloated table, deltalite's first write collapses the accumulated duplicates — correct, but the row count drops (customer-visible). Team-2 dogfood first is deliberate.

Follow-ups (separate, out of scope here): remove the now-dead DATA_WAREHOUSE_DELTALITE_SHADOW_ENABLED env var from charts, and archive the data-warehouse-deltalite-shadow flag.

How did you test this code?

I'm an agent. 12 passed locally — TestDeltaliteWritePath (mocked deltalite: skips w/o PKs, falls back when flag off, routes to upsert when enabled, falls back when deltalite raises), the new test_deltalite_write.py flag tests (fail-closed, write-flag key + person properties, source_type resolution), and TestNullSafeMergePredicate. ruff clean; the two suites' only other failures are the pre-existing environmental objectstorage ones.

Rollback

Set the data-warehouse-deltalite-write flag off — merges return to the delta-rs MERGE. No deploy required.

Adds an optional deltalite write path for the incremental merge: when the
DATA_WAREHOUSE_DELTALITE_WRITE_ENABLED env switch is on and the per-schema
data-warehouse-deltalite-write flag matches, deltalite performs the real merge
(DeltaLiteTable.upsert) instead of the delta-rs MERGE.

Safety by construction: any failure — env/flag off, import error, deltalite
error/conflict/refusal — falls back to the delta-rs MERGE, so switching a schema
to deltalite can only change which engine writes, never whether the sync
succeeds. Off by default; nothing changes until the env + flag are set.

- settings: DATA_WAREHOUSE_DELTALITE_WRITE_ENABLED (master switch, default False)
- deltalite_shadow: extract _is_deltalite_flag_enabled; add is_deltalite_write_enabled
- metric: warehouse_load_deltalite_write_total{outcome=written|fallback}
- the forward shadow is skipped when deltalite did the real write (redundant)

Follows the phase-1 shadow, which validated deltalite is byte-for-byte
equivalent to MERGE across the fleet on real prod S3.
@Gilbert09 Gilbert09 self-assigned this Jul 31, 2026
@trunk-io

trunk-io Bot commented Jul 31, 2026

Copy link
Copy Markdown

😎 This pull request was merged.

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 31, 2026 09:01
@pr-assigner-resolver-posthog

Copy link
Copy Markdown

👀 Auto-assigned reviewers

These soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:

  • @PostHog/team-data-stack (owners.yaml)

Soft owners come from each directory's owners.yaml and each product's product.yaml (resolved nearest-file-wins). The locator after each owner is the file that decided it. Generated files and lockfiles are ignored when deciding ownership.

@veria-ai

veria-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Playwright — all passed

All tests passed.

View test results →

⚠️ Backend coverage — 98.0% of changed backend lines covered — 2 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ████████████████████ 98.0% (151 / 153)

File Patch Uncovered changed lines
products/warehouse_sources/backend/temporal/data_imports/pipelines/core/delta_table_helper.py 94.7% 613–614

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 30625569483 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.4% 8,782 / 22,290
demo ███████████░░░░░░░░░ 56.3% 1,497 / 2,661
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
data_tools ██████████████░░░░░░ 70.0% 63 / 90
tasks ██████████████░░░░░░ 70.1% 32,784 / 46,772
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 81.2% 24,750 / 30,477
cdp ████████████████░░░░ 82.1% 3,285 / 3,999
data_modeling █████████████████░░░ 85.6% 7,688 / 8,979
notebooks █████████████████░░░ 86.0% 7,794 / 9,060
wizard █████████████████░░░ 86.4% 1,060 / 1,227
actions █████████████████░░░ 86.6% 717 / 828
cohorts ██████████████████░░ 87.8% 6,181 / 7,040
product_tours ██████████████████░░ 87.9% 1,303 / 1,482
exports ██████████████████░░ 88.2% 7,046 / 7,986
data_warehouse ██████████████████░░ 88.4% 12,192 / 13,798
business_knowledge ██████████████████░░ 89.0% 4,391 / 4,936
engineering_analytics ██████████████████░░ 89.3% 6,529 / 7,309
conversations ██████████████████░░ 89.3% 17,600 / 19,701
dashboards ██████████████████░░ 89.4% 5,983 / 6,693
visual_review ██████████████████░░ 89.5% 5,870 / 6,558
alerts ██████████████████░░ 90.2% 4,458 / 4,942
links ██████████████████░░ 90.6% 183 / 202
streamlit_apps ██████████████████░░ 90.7% 2,630 / 2,901
mcp_analytics ██████████████████░░ 90.9% 3,191 / 3,511
error_tracking ██████████████████░░ 91.0% 10,926 / 12,008
slack_app ██████████████████░░ 91.1% 9,664 / 10,610
marketing_analytics ██████████████████░░ 91.2% 12,092 / 13,265
stamphog ██████████████████░░ 91.3% 4,505 / 4,936
mcp_store ██████████████████░░ 91.9% 4,257 / 4,634
product_analytics ███████████████████░ 92.5% 5,849 / 6,321
managed_migrations ███████████████████░ 92.6% 1,556 / 1,681
early_access_features ███████████████████░ 92.6% 1,287 / 1,390
notifications ███████████████████░ 92.6% 1,017 / 1,098
ai_observability ███████████████████░ 92.8% 15,618 / 16,821
surveys ███████████████████░ 93.1% 5,771 / 6,197
posthog_ai ███████████████████░ 93.2% 1,326 / 1,422
web_analytics ███████████████████░ 93.3% 14,911 / 15,976
approvals ███████████████████░ 93.3% 3,437 / 3,682
reminders ███████████████████░ 93.4% 468 / 501
legal_documents ███████████████████░ 93.8% 1,628 / 1,736
workflows ███████████████████░ 94.1% 7,241 / 7,698
endpoints ███████████████████░ 94.2% 8,655 / 9,192
tracing ███████████████████░ 94.5% 2,671 / 2,827
review_hog ███████████████████░ 94.6% 8,101 / 8,563
skills ███████████████████░ 94.6% 3,158 / 3,337
messaging ███████████████████░ 94.7% 2,885 / 3,048
experiments ███████████████████░ 95.4% 25,938 / 27,177
logs ███████████████████░ 95.5% 10,440 / 10,937
growth ███████████████████░ 96.1% 3,245 / 3,376
annotations ███████████████████░ 96.2% 732 / 761
revenue_analytics ███████████████████░ 96.3% 1,887 / 1,960
feature_flags ███████████████████░ 96.4% 17,488 / 18,144
replay_vision ███████████████████░ 96.4% 16,081 / 16,679
user_interviews ███████████████████░ 96.5% 2,638 / 2,734
access_control ███████████████████░ 96.9% 870 / 898
customer_analytics ███████████████████░ 97.1% 10,444 / 10,758
warehouse_sources ███████████████████░ 97.3% 359,265 / 369,255
data_catalog ████████████████████ 97.7% 2,588 / 2,648
analytics_platform ████████████████████ 98.0% 2,153 / 2,197
metrics ████████████████████ 98.2% 2,491 / 2,536
pulse ████████████████████ 98.4% 2,017 / 2,049
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

Phase 2 is now gated solely by the data-warehouse-deltalite-write flag — no
DATA_WAREHOUSE_DELTALITE_WRITE_ENABLED env var — so it can be ramped or killed
from the flag UI without a deploy. The per-merge flag eval is a couple of indexed
lookups; negligible fleet-wide.
The shadow verification did its job: it proved deltalite is byte-for-byte
equivalent to the delta-rs MERGE across the fleet on real prod S3, and surfaced
the null-composite-key duplicate bug in MERGE (fixed in #75601). With phase 2
(deltalite doing the real write) in this PR, running the shadow too is redundant.

Removes deltalite_shadow.py (compare engine, seed/upsert/cleanup, sampling +
caps), the DATA_WAREHOUSE_DELTALITE_SHADOW_* settings, the shadow metrics, and
the write_to_deltalake shadow hook. The per-schema flag helper the write path
needs is kept in a focused new module, deltalite_write.py.
… tests

The write-path fallback warning + metric were inside the same try/except as the
flag eval, so a flag miss or a flags-service blip logged 'deltalite write
failed' — which broke test_incremental_merge_dedupes_duplicate_source_rows
(it saw a second awarning). Split the flag gate out of the write attempt: a
flag off / eval error now falls back to the delta-rs MERGE silently, and the
warning + 'fallback' metric are reserved for a genuine deltalite write failure.

Also add an autouse fixture so the write-path tests' patch.dict(sys.modules)
doesn't evict the lazily-imported metrics module (which re-registered its
Prometheus metrics on the next test).
Addresses review: (1) move the lazy imports inside their try/except so an import
failure falls back to the MERGE as the docstring promises, and (2) scope the
fallback to the upsert *commit* only. A failure after deltalite commits (e.g.
update_incremental) no longer returns False — that would re-run the delta-rs
MERGE on top of deltalite's already-committed write. Post-commit bookkeeping is
now best-effort. Adds a test for the post-commit path.
Addresses review: after the upsert commits, the log + metric increment were
still outside a try, so if either raised the exception would propagate,
deltalite_wrote would stay unset, and the sync would fail/retry (or re-run the
MERGE) on top of an already-committed write. Wrap the whole post-commit block
(handle refresh, log, metric) best-effort and always return True. Parametrise
the post-commit test over refresh- and log-failure.
helper._logger is typed FilteringBoundLogger, so mypy rejected
._logger.ainfo.side_effect. Set the side effect on the mock logger before
constructing the helper (same MagicMock-logger pattern the other tests use).
@Gilbert09
Gilbert09 merged commit be94dce into master Jul 31, 2026
242 checks passed
@Gilbert09
Gilbert09 deleted the tom/deltalite-write-canary branch July 31, 2026 11:22
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-31 11:59 UTC Run
prod-us ✅ Deployed 2026-07-31 12:22 UTC Run
prod-eu ✅ Deployed 2026-07-31 12:22 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants