Skip to content

fix(data-imports): force nullable on delta columns added via schema evolution - #74674

Merged
trunk-io[bot] merged 1 commit into
masterfrom
posthog-code/fix-delta-schema-evolution-nullability
Jul 29, 2026
Merged

fix(data-imports): force nullable on delta columns added via schema evolution#74674
trunk-io[bot] merged 1 commit into
masterfrom
posthog-code/fix-delta-schema-evolution-nullability

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

Problem

An error tracking issue surfaced a DeltaError:

Failed to parse parquet: Parquet error: Optimize selected-file scan failed while scanning data:
Execution error: Non-nullable column 'real_model' is missing from the physical schema

raised from compact_table()table.optimize.compact() in delta_table_helper.py, called from run_post_load_operations in common/load.py at the end of a sync. The failure lives in shared pipeline code, not a specific source connector.

Root cause: _evolve_delta_schema adds newly-seen columns to a Delta table via delta_table.alter.add_columns(...), converting them straight from the incoming Arrow batch's field with deltalake.Field.from_arrow(field). That preserves whatever nullable value the incoming field happens to carry — which is False whenever the batch that first introduces the column contains no nulls. Once the column is added as NOT NULL, every file the table already holds (written before the column existed, so it's entirely absent from their physical schema) can never satisfy that constraint. A later optimize.compact() scans those old files and fails hard trying to reconcile them.

I reproduced this locally against the real deltalake library: seed a table, add a column via alter.add_columns with a non-nullable field, write a batch with it, then call optimize.compact() — it raises the exact same DeltaError message. Forcing the newly-added field to nullable=True before conversion makes compaction succeed and correctly backfills the pre-existing rows with null for the new column.

Changes

  • _evolve_delta_schema (delta_table_helper.py) now forces every newly-added column to nullable=True before calling deltalake.Field.from_arrow, regardless of the incoming batch's own field nullability.

No retry policy, NonRetryableErrors, or write-path behavior changed for existing columns — this only affects the nullability assigned to a column the very first time it's added to an existing table.

How did you test this code?

  • Added TestSchemaEvolutionNullability::test_compact_survives_a_column_added_by_an_all_non_null_batch to test_delta_table_helper.py: writes an initial batch without a column, appends a second batch that introduces the column with a non-nullable Arrow field (all values present, mirroring how upstream Arrow construction infers nullability), then runs compact_table() against the real local deltalake library. Verified this test fails with the exact production error on the pre-fix code and passes with the fix — the regression this PR closes.
  • Ran the full test_delta_table_helper.py suite: 67 passed (4 pre-existing failures in TestGetDeltaTableUnrecoverableErrors need a live object-storage service unavailable in this sandbox; identical on unmodified master).
  • ruff check / ruff format --check clean on both changed files.
  • Full-repo uv run mypy --cache-fine-grained . clean (17074 source files).

Docs update

N/A — internal pipeline implementation detail, no user-facing or API surface change.

🤖 Agent context

Autonomy: Fully autonomous

Triaged from a live PostHog error-tracking issue (webhook-delivered) using Claude Code. Pulled the stack trace and event properties via the PostHog MCP tools, traced the call path from run_post_load_operations into compact_table()/optimize.compact(), and reproduced the exact DeltaError locally against the real deltalake library to confirm the root cause before writing the fix. Searched open PRs for duplicates across several delta_table_helper.py/compaction-related fixes already in flight (#74635, #74630, #74541, #74658) — all address distinct root causes (transient S3/object-store races, a redundant re-fetch, a stale file list), none touch schema-evolution nullability. Invoked /writing-tests before adding the regression test.

@trunk-io

trunk-io Bot commented Jul 29, 2026

Copy link
Copy Markdown

😎 Merged successfully - details.

@github-actions

Copy link
Copy Markdown
Contributor

Hey @Gilbert09! 👋

It looks like your git author email on this PR isn't your @posthog.com address (owerstom@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 29, 2026 12:42
@Gilbert09 Gilbert09 added the stamphog Request AI approval (no full review) label Jul 29, 2026 — with PostHog
…volution

Columns added to a Delta table mid-lifetime always predate their own addition, so every file the table already holds was written without them. `_evolve_delta_schema` was adding new columns with whatever nullability the incoming Arrow batch happened to have, which is non-nullable whenever the introducing batch contains no nulls. A later `optimize.compact()` then fails trying to reconcile older files against a NOT NULL column they don't have at all.

Force newly-added columns to nullable=True regardless of the incoming field's own nullability, since older files can never retroactively gain the column's values.

Generated-By: PostHog Code
Task-Id: d3c0f944-c36f-4a90-9ebe-e1c05b7a5c99
@Gilbert09
Gilbert09 force-pushed the posthog-code/fix-delta-schema-evolution-nullability branch from 9e0dcfb to 7a134b2 Compare July 29, 2026 15:00

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small, well-tested bug fix to internal data-warehouse pipeline code (forces nullable on newly added Delta columns to prevent a real compaction crash), by an author on the owning team, with a regression test proving the fix; no risky territory (no API/schema-migration/auth/billing/CI surface) and no unresolved concerns.

  • 👍 on the PR from hex-security-app[bot].
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 8L, 1F substantive, 45L/2F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1b-small (45L, 2F, single-area, fix)
stamphog 2.0.0b3 .stamphog/policy.yml @ 7c4151e · reviewed head 7a134b2

@talyn-app

talyn-app Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

/trunk merge

@trunk-io
trunk-io Bot merged commit a53d348 into master Jul 29, 2026
236 checks passed
@trunk-io
trunk-io Bot deleted the posthog-code/fix-delta-schema-evolution-nullability branch July 29, 2026 17:27
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-29 18:18 UTC Run
prod-us ✅ Deployed 2026-07-29 18:41 UTC Run
prod-eu ✅ Deployed 2026-07-29 18:44 UTC Run

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

Labels

stamphog Request AI approval (no full review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants