fix(bigquery): run the partition-field check on all three destinations (v0.5.4) - #91
Open
anaselmhamdi wants to merge 1 commit into
Open
fix(bigquery): run the partition-field check on all three destinations (v0.5.4)#91anaselmhamdi wants to merge 1 commit into
anaselmhamdi wants to merge 1 commit into
Conversation
…s (v0.5.4) 0.5.3 added a config-time check that `time_partitioning.field` exists in the schema the destination writes, and wired it to the batch destination only. `BigQueryStreamingConfigDetails` and `BigQueryStreamingV2ConfigDetails` had no model_validator at all, so `unnest: true` streaming pipelines were never covered by the guardrail the release note described. Extending it naively would have been worse than the gap. In STREAM sync mode bigquery_streaming_v2.finalize() returns immediately, so _partition_clause() -- the only thing that validates the field on v2 -- is never reached; v1 has no finalize() at all. On both, the field is only ever touched by create_table, which raises Conflict on an existing table and silently discards the spec. A streaming pipeline can therefore carry an arbitrarily wrong field and run correctly forever, and after the first run every table exists. A hard config-time check would have stopped all of them on upgrade. So the check now runs on all three destinations, but raises only where the spec actually reaches a table -- where BigQuery itself would reject it: - batch `bigquery`: every load job stamps time_partitioning onto the temp table it creates, so a bad field fails every run. Raises at config validation, as in 0.5.3. - streaming, table absent: about to be created with that spec. Raises, in place of BigQuery's opaque error. - streaming, table present: BigQuery keeps the table's own spec and the run succeeds exactly as before. Warns, naming the field and the columns, and omits the partitioning it would have discarded anyway. - v2 publish DDL: validates the spec being emitted, not the config. _publish_spec() returns the table's current spec on a mismatch with enforce_partitioning off, so a bad configured field never reaches that DDL and must not block the publish. Two related fixes: - `unnest: true` with no `time_partitioning` at all resolved to `_bizon_loaded_at`, a column unnest can never produce, so a config that never mentioned partitioning was rejected outright by 0.5.3 on the batch destination and failed at table creation on the streaming ones. An unwritten field now falls back to ingestion-time partitioning; a field the user wrote is still checked. Nobody has to touch a config to upgrade. - The check now covers the column's type, not just its presence: a STRING partition column (`_bizon_id` passed in 0.5.3) or HOUR on a DATE column is caught rather than left to BigQuery. Also: bigquery_streaming's Conflict handler reconciled the schema while ignoring partitioning entirely, so a config that had drifted from its live table produced no signal run after run; it now compares both specs and warns. And both streaming configs declared time_partitioning with `default=TimePartitioning(...)`, which pydantic v2 does not copy -- every config in the process shared one instance, latent until the new validator mutated it. Both now use default_factory. Verified: the exact CI pytest command and `tests/connectors/destinations` produce identical failure sets before and after (18 and 14, all pre-existing -- no local Postgres, live BigQuery), and all 26 shipped example configs validate to the same status as before. The new test file sits outside the bigquery* directories that CI ignores, so unlike 0.5.3's partitioning tests it actually runs there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The gap
0.5.3 added a config-time check that
time_partitioning.fieldexists in the schema the destination writes — and wired it to one of the three BigQuery destinations.BigQueryStreamingConfigDetailsandBigQueryStreamingV2ConfigDetailshave nomodel_validatorat all, sounnest: truestreaming pipelines were never covered by the guardrail the release note described.Why the obvious fix would have been worse
STREAMsync modebigquery_streaming_v2.finalize()returns immediately (destination.py:560), so_partition_clause()— the only thing that validates the field on v2 — is never reached.bigquery_streamingv1 has nofinalize()at all.create_table, which raisesConflicton an existing table and silently discards the spec.So a v1 or v2 stream pipeline can carry an arbitrarily wrong
time_partitioning.fieldand run correctly, forever. After the first run every table exists — that's the normal state of every streaming pipeline, not a corner case. Extending 0.5.3's hard config-time check to the streaming destinations would have stopped all of them on the bump.The rule
Raise only where BigQuery itself would raise; warn everywhere else — equivalently, check the spec that is actually about to be applied to a table, not the config in the abstract.
bigquery, bad field — every load job stamps the spec onto the temp table it createsSTRING/JSON(_bizon_id,_source_data)Conflict)create_table_publish_spec()emits the table's spec; worksunnest: true, notime_partitioningwrittencreate_tabletime_partitioning: DAYDAY/_bizon_loaded_atNo currently-working pipeline changes behaviour. Every new raise replaces an existing opaque failure.
Changes
describe_partition_problem()(bigquery/src/config.py) returns a message instead of raising, so each of the six call sites picks its own severity. It now covers the column's type as well as its presence — aSTRINGpartition column orHOURon aDATEcolumn is caught rather than left to BigQuery.resolve_partition_field()settles the implicit default:fielddefaults to_bizon_loaded_at, whichunnest: truecan never produce. An unwritten field falls back to ingestion-time partitioning and logs it; a field the user actually wrote is still checked. Nobody has to touch a config to upgrade.should_apply_partitioning()(partitioning.py) is the absent-vs-exists adjudicator shared by both streaming destinations. Theget_tableprobe only runs when there is a problem, and only once per table per process.bigquery_streaming'sConflicthandler reconciled the schema while ignoring partitioning entirely, so a config that had drifted from its live table produced no signal, run after run. It now compares both specs and warns. (BigQuery still cannot repartition in place; rebuilding remains the only fix.)time_partitioningwithdefault=TimePartitioning(...)— pydantic v2 does not copy aBaseModelpassed asdefault, so every config in the process shared one instance. Latent until the new validator mutated it. Both now usedefault_factory, which also makesmodel_fields_settruthful — that's how "did the user writefield?" is answered.CLAUDE.mdgains a BigQuery partitioning semantics section, since the severity split is exactly the kind of thing a later change "unifies" and breaks.Verification
tests/connectors/destinations/test_partition_field_validation.py— 44 tests parametrized over all three destinations, covering both the config layer and the runtime absent/present split. It sits outside thebigquery*/directories that CI ignores, so unlike 0.5.3's partitioning tests it actually runs in CI.kafka_streams.example.yml(the production shape:bigquery_streaming_v2,unnest: true,field: __inserted_at,TIMESTAMPin both streams) validates untouched and is now a regression test, along with a case that drops the column from one stream and asserts the warning names it.tests/connectors/destinations: identical 14, all live-BigQuery. No new failures.Not in scope
kafka_debezium.example.ymlsetstable_id:andrecord_schema:(singular) directly on the destination config — both unknown keys underextra="forbid", so that example cannot parse. Pre-existing, fails identically before and after this change. Worth its own fix.🤖 Generated with Claude Code