feat(sql-plan): port SQLPlanGenerator consumers of the production model fields - #19
Draft
dmautz1 wants to merge 6 commits into
Draft
feat(sql-plan): port SQLPlanGenerator consumers of the production model fields#19dmautz1 wants to merge 6 commits into
dmautz1 wants to merge 6 commits into
Conversation
Ports the model-level deltas that accumulated in the pulseflow fork of
tablespec back upstream, adapted to this repo's structure. These fields
power upsert merge semantics, SQL plan generation, and cross-pipeline
references in production healthcare pipelines.
New capabilities:
- MergeCondition + UMFColumn.merge_strategy/merge_source/merge_condition:
column-level upsert merge semantics (LEAST/GREATEST/COALESCE, gated by
a source-row condition)
- UMFColumn.internal: helper columns that participate in derivation but
are excluded from final output/DDL/schemas
- UMFColumn.nullable now accepts plain booleans alongside Nullable
contexts (is_nullable_for_all_contexts already handled bool)
- TableReference (models/pipeline.py) + parse_table_reference() on
ForeignKey, OutgoingRelationship, IncomingRelationship, and
DerivationCandidate for pipeline-qualified references
- ForeignKey.join_filter and OutgoingRelationship.alternative_joins
(OR-join paths) for SQL plan generation
- DerivationCandidate.union_value: per-UNION-branch literal values
- UMFMetadata: base_table_filter, base_join_column, final_filter,
union_base_tables, union_type, union_exclude_base, union_coalesce_base,
final_dedup — base-view and final-assembly controls for generated tables
- IngestionConfig.update_mode ('upsert' | 'update_only') for _U file
merge behavior; order_by docs extended to cover data columns and
meta_source_offset tie-breaking
- UMF.effective_primary_key / DEFAULT_PRIMARY_KEY (meta_checksum) as the
default merge key
- ValidationRules warns when ingested-stage expectation types are placed
in raw validation_rules (shift-left guidance)
- Domain type registry is now cached at module level instead of being
re-constructed on every column validation
Deliberately not ported (upstream direction wins): PySpark-style
data_type validation, bool→LOB nullable normalization, lowercase
table_name constraint, and advisory no-primary-key warnings (would
break filterwarnings=error test policy).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Workbook tab assignment for multi-sheet report authoring: columns can declare which Excel worksheet tab they belong to in a generated multi-sheet workbook report. Authoring-time provenance consumed by report-configuration tooling; bounded to Excel's 31-character tab-name limit. The Columns-sheet exporter writes a trailing "Report Sheet" header and the importer locates it by header name (consistent with the existing header-mapped reader), so older workbooks without the column import unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions Source-layer specs (plain ingested tables) have no survivorship or derivation metadata, so their exported workbooks no longer carry empty Survivorship/Derivations tabs — matching the existing conditional emission of Validation Rules, Relationships, and File Format sheets. The importer already tolerates absent sheets, so round-trip behavior is unchanged for both spec shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to end The ported UMFColumn.internal flag and bool-widened nullable field were model-only; their consumers didn't honor them: - Schema generators (SQL DDL, PySpark, JSON): skip internal helper columns — they participate in derivation but are excluded from the output table. - GX baseline: skip internal columns in column-level expectations, column-count/ordered-list structural checks, and cross-column date pairs; a plain `nullable: false` now emits the global not-null expectation (it was silently skipped as falsy). - Excel export: a boolean nullable writes its value into every context cell instead of defaulting all contexts to False. - Compatibility checker: boolean nullable maps to the synthetic "*" context instead of crashing on model_fields_set, so True -> False is still reported as a breaking tightening. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…el fields Wire every SQL-generation field PR DocumentDrivenDX#18 landed as a declarative stub through its generator/resolver consumers: the new base_table_strategy 'union_branches' (per-source UNION branches projecting the target column set, with per-branch row_filter, union_value literals, CAST(NULL) alignment, and dedup-latest windows), base_table_filter / base_join_column / final_filter / final_dedup, ForeignKey.join_filter (candidate-level wins), and OutgoingRelationship.alternative_joins as a portable UNION-of-joins. Also: base views now project join-source columns (latent bug - emitted joins referenced columns disposition_base never selected), Excel metadata sheet JSON-encodes list values, umf.schema.json synced for the consumed fields, gold_union_branches conformance case executed on DuckDB + Spark, and a new docs/guide/sql-plans.md. Co-Authored-By: Claude Fable 5 <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.
Summary
Second PR in the series porting production-hardened capabilities from the pulseflow fork of tablespec back upstream. Stacked on #18 (
feat/port-umf-model-deltas) — the diff includes #18's commits until it merges; review this PR's own change asfeat/port-umf-model-deltas...feat/port-sql-plan-consumers. Draft until #18 lands.#18 landed the model fields as declarative stubs; this PR wires every SQL-generation field through its
SQLPlanGenerator/RelationshipResolverconsumers, per the series convention: nothing stays a model-only no-op. As with #18, everything is adapted to this repo's structure (single-statement views, CTE-mode contract, renderer seam, engine-agnostic SQL executed verbatim on DuckDB and Spark) rather than copied — and several capabilities the fork never actually had (per-branchrow_filteron union branches, per-branch window dedup, target-schema branch projection) are new design pinned by a real downstream acceptance shape.Per-field consumer wiring
base_table_strategy: 'union_branches'(new enum value — see model note below)The base table plus each
union_base_tablesentry (falling back tosource_tables) becomes one UNION branch inside a singledisposition_baseview. Unlike the fork'sunion_base_tableshandling (which projects the base table's schema from every branch), each branch projects the target column set through that source's own derivation candidates — required for cutover shapes where sources have different columns:DerivationCandidate.row_filter→ the branch WHERE clause (the single distinct value among a branch's candidates; conflicts raiseValueError). This is how generation cutovers are expressed.DerivationCandidate.union_value→CAST(<literal> AS <type>)per branch (source discriminators); native str/int/float/bool typing preserved end-to-end.CAST(NULL AS <type>), keeping the UNION column-aligned.dedup_strategy: latest+ candidateorder_by→ per-branchROW_NUMBER() OVER (PARTITION BY <target primary_key> ORDER BY <order_by> DESC NULLS LAST) ... WHERE __rn = 1(NULLS LAST pinned — DuckDB/Spark default NULL placement diverges).union_type→UNION ALL(default) /UNION.union_exclude_base→ per-union-branchNOT EXISTSanti-join on the target primary key against the base branch's post-filter, post-dedup rows (deliberate improvement over the fork's raw-base anti-join). No primary key →ValueError(the fork skipped silently — a correctness foot-gun).union_coalesce_base→ 3-part union (base-only /COALESCE(b.c, u.c)overlap with base winning pk, meta, and union_value columns / union-only). Restricted to exactly one union table: the fork's per-table 3-part emission would double-count base rows with several union tables →ValueError.base.<col>(the branch already applied the candidate mapping under the target name).base_table_filter→ WHERE on the plain base view and on the union base branch (closing a fork gap — it never applied the filter on union paths). Warns and no-ops underunpivot/union_sources, which don't consume it.base_join_column→ overrides the inferred base join key in both resolver and base view and overwritessource_columnon relationships declared outgoing from the base table (fork parity; the field exists precisely when the auto key is wrong and declared rels carry that same wrong key — documented contract).final_filter/final_dedup→ final assembly wraps asSELECT [DISTINCT *] FROM (<assembly>) _final WHERE <filter>so the filter can reference derived aliases; applies to the synthetic (no-base) path too; still one statement → CTE-mode safe.ForeignKey.join_filter→ new resolver pass fillsJoinInfo.join_filterfrom FK metadata only where no candidate-level filter exists (candidate filters are(table, table_instance)-keyed and can disambiguate multi-instance joins, so they win). Emission machinery already existed and was conformance-tested.OutgoingRelationship.alternative_joins→ emitted as a UNION-of-joins, notON (a = b OR c = d): Spark plans OR-joins as BroadcastNestedLoopJoin. One inner-join branch per path over the distinct base keys (primary = priority 1, alternatives in declared order),UNION+ROW_NUMBERby__branch_prioritykeeps one match per key, joined back null-safely via the portable(a = b OR (a IS NULL AND b IS NULL))expansion (no<=>, no* EXCEPT, no engine hints).base_keysscansdisposition_basewhen every key is base-sourced (the fork's documented lazy-view fan-out guard), else the previous step view with a warning. Resolver validates each entry's columns exist (ValueErrorotherwise). Non-direct strategies warn and use the primary path only.Model change
UMFMetadata.base_table_strategynarrows fromstr | NonetoLiteral["union_sources", "unpivot", "union_branches"] | None. Repo-wide only the two existing values were in use.union_base_tablespresent without the strategy logs a warning and no-ops (notwarnings.warn— the repo'sfilterwarnings = errorwould hard-fail legacy-shaped fixtures; not a validation error — fork-authored UMFs must still load). Migration for fork specs: addbase_table_strategy: union_branches.Also in this PR
ON base.<key> = ...referenced columnsdisposition_basenever selected.Union Valuecolumn on the Derivations sheet (native typing, appended header so older workbooks import unchanged); Metadata sheet now JSON-encodes list/dict values (str(list)previously brokeunion_base_tables— andsource_tables— on re-import). F009-DERIV-02 losslessness test extended: a union_branches spec's CTE plan is byte-identical across an Excel round-trip.umf.schema.jsonsynced for exactly the fields this PR consumes (the 8 UMFMetadata fields incl. the new enum,union_value,ForeignKey.join_filter,alternative_joins); the full drift regeneration remains AR-2026-03-16's follow-up.docs/guide/sql-plans.md(strategies, filters, dedup, join controls, error philosophy), linked from happy-path §5;excel.mdUnion Value row;docs/api/generators.mdgainsgenerate_sql_plan/SQLPlanGenerator.Deliberately NOT ported
_rewrite_join_filterhardcoded client-specific rewrites (client_mbr_id → ClientMemberId)ValueErrors)merge_strategy,update_mode, pre/post-upsert rules,effective_primary_keyAcceptance shape
The unit suite pins the real downstream cutover this port unblocks (synaptiq-northstar-idr's
silver_fact_inventory_line): two feeds unioned at aDATE '2026-07-20'cutover via complementaryrow_filters, asource_generationdiscriminator viaunion_value, one-sided columns NULL-cast, per-branchROW_NUMBER PARTITION BY arbit_id, cpt, dos, snapshot_date ORDER BY meta_load_dt DESC— compared via sqlglot normalization, not exact text. The conformance twin (gold_union_branches) executes the same shape end-to-end with a portable DATE filter and matches the committed Spark-oracle golden on both DuckDB and Spark (+ pairwise agreement).Testing
tests/unit/test_sql_plan_consumers.py) covering every field's consumer wiring, everyValueErrorpath, CTE-mode single-statement + both-dialect parseability, and the acceptance fixture; model Literal tests; 2 new Excel round-trip testsgold_union_branches(corpus + Spark-generated golden), green on both engine legsfilterwarnings = error; conformance 226 passed; zero existing goldens changed (everything is opt-in); ruff clean; pyright 0 errors on touched modules🤖 Generated with Claude Code