Fix column lineage transformation details always appearing as null - #3114
Open
kalra-mohit wants to merge 2 commits into
Open
Fix column lineage transformation details always appearing as null#3114kalra-mohit wants to merge 2 commits into
kalra-mohit wants to merge 2 commits into
Conversation
…arquezProject#3100) Marquez's internal LineageEvent.ColumnLineageInputField model (used to deserialize the OpenLineage ColumnLineageDatasetFacet on ingestion) only captured namespace/name/field for each input field - it had no `transformations` property at all. The write path (OpenLineageDao.upsertColumnLineage) only ever read transformationDescription/transformationType off the *output column* object (ColumnLineageOutputColumn), which per the current OpenLineage spec are explicitly deprecated: https://github.com/OpenLineage/OpenLineage/blob/main/spec/facets/ColumnLineageDatasetFacet.json Modern producers report transformation details per input field instead, via a `transformations` array nested under each entry of `inputFields[]` (each with type/subtype/description/masking) - this is exactly the format shown in the issue's example JSON. Since Marquez's ingestion model didn't have a `transformations` field to deserialize that array into, and never read the deprecated output-column-level fields from producers that only emit the new format, transformation_description/transformation_type were written as null for any producer using the current (non-deprecated) facet shape - reproducing the bug exactly as reported. Fix: - Added `transformations` (List<Transformation>) to LineageEvent.ColumnLineageInputField, with a new nested Transformation class (type, subtype, description, masking) mirroring the OpenLineage spec. The existing 3-arg constructor (namespace, name, field) is preserved unchanged, so no existing call site needed to be touched. - Added OpenLineageDao.transformationOf(inputField, outputColumn), a small pure function that resolves the (description, type) pair for a given input field: prefer the first entry of that specific input field's (non-deprecated) `transformations` list; fall back to the deprecated whole-output-column transformationDescription/transformationType for producers that still only report the old format. - Updated OpenLineageDao.upsertColumnLineage to resolve the transformation per matched input field (via transformationOf) and group input fields by their resolved (description, type) before calling the existing ColumnLineageDao.upsertColumnLineageRow(...) batch API once per group. This correctly persists a distinct transformation per input/output field edge - which the column_lineage table and the read path (ColumnLineageService, which already surfaces per-input-field transformation info) both already supported - without changing ColumnLineageDao's public method signature, so no existing ColumnLineageDaoTest call sites needed to change. Tests: - api/src/test/java/marquez/db/OpenLineageDaoColumnLineageTransformationTest.java (new, tagged UnitTests, no DB required): exercises OpenLineageDao.transformationOf(...) directly against the exact scenario from the issue (an output column with no deprecated fields set, fed by input fields that each report their own `transformations` entry), plus the multi-input-field, fallback, "both formats absent", and "multiple transformations reported" cases. I verified these tests explicitly: temporarily reverted transformationOf(...) to the old behavior (always read the deprecated output-column-level fields) and confirmed 3 of 5 tests fail with the expected description/type coming back null - then restored the fix and confirmed all 5 pass. - OpenLineageDaoTest#testUpdateMarquezModelDatasetWithColumnLineageFacet_usesNewTransformationsFormat (new): full DB-backed regression test that builds a ColumnLineageDatasetFacet using only the new inputFields[].transformations[] format (deprecated fields left unset, as real modern producers do) and asserts the persisted ColumnLineageRow has the expected non-null transformation description/type. Test evidence: ./gradlew :api:testUnit passes (123 tests, including the 5 new pure-function tests). I was not able to execute OpenLineageDaoTest (Postgres-backed) locally in this sandbox - Testcontainers fails to negotiate with the local Docker Engine (old docker-java client defaults to Docker API v1.32, local engine requires >= v1.40); this is a pre-existing environment limitation that affects every DB-backed test in the suite, not something introduced by this change (verified identically on unmodified tests like RunDaoTest). ./gradlew :api:compileTestJava passes, confirming the new integration test at least compiles correctly against the real DAO/model classes. Fixes MarquezProject#3100 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Mohit Kalra <mohit2494@gmail.com>
kalra-mohit
force-pushed
the
fix/3100-column-lineage-transformation-details
branch
from
July 22, 2026 01:35
72de9f6 to
96fb5f2
Compare
When a single output field has 2+ distinct transformation groups, upsertColumnLineageRow(...) is called once per group, and its implementation always returns ALL column_lineage rows for that (datasetVersion, outputField) pair rather than just the rows from the current call. Across multiple groups the same underlying row can therefore appear duplicated in the returned List<ColumnLineageRow>. No production impact today: DatasetRecord#getColumnLineageRows(), the only consumer of this return value, isn't read anywhere in production code, only in this PR's own test (which only covers the single-transformation-group case). Documented for future maintainers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Mohit Kalra <mohit2494@gmail.com>
kalra-mohit
marked this pull request as ready for review
July 22, 2026 02:30
Author
|
@merobi-hub whenever you get a chance to take a look, happy to address any feedback! |
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.
Marquez's
LineageEvent.ColumnLineageInputField(used to deserialize the OpenLineageColumnLineageDatasetFacet) only ever capturednamespace/name/field— notransformationsproperty existed on it at all. Meanwhile the write path,OpenLineageDao.upsertColumnLineage, only readtransformationDescription/transformationTypeoff the output column (ColumnLineageOutputColumn), which the current OpenLineage spec marksdeprecated: true.Modern producers report transformation details per input field instead, nested under
inputFields[].transformations[]— exactly what's in the issue's example:Since the model had nowhere to deserialize that array into, and the write path never looked at the deprecated output-column fields for producers that only emit the new shape,
transformation_description/transformation_typelanded asnullfor anyone using the current facet format — reproducing the bug as reported (a second user hit the same thing in the issue thread).flowchart TD E["OpenLineage event: inputFields[].transformations[] (current spec)"] E --> Old["Before: ColumnLineageInputField has no transformations field"] Old --> O1["Write path only reads deprecated outputColumn.transformationDescription/Type"] O1 --> R1["null/null persisted whenever producer only emits the new per-field format"] E --> New["After: transformationOf(inputField, outputColumn)"] New --> O2{"inputField.transformations present?"} O2 -->|yes| R2["Use that field's own transformation description/type"] O2 -->|no - legacy producer| R3["Fall back to deprecated outputColumn description/type"]Fix:
transformations(List<Transformation>) toColumnLineageInputField, with a newTransformationclass (type/subtype/description/masking) mirroring the spec. Kept the existing 3-arg constructor working unchanged so no call site needed to move — verified by compiling all 8 existing usages untouched.OpenLineageDao.transformationOf(inputField, outputColumn)— a small pure function that prefers the first entry of that specific input field's owntransformationslist, and falls back to the deprecated output-column-level fields for producers still on the old format.upsertColumnLineagenow resolves the transformation per matched input field viatransformationOfand groups input fields by their resolved(description, type)before calling the existing batchColumnLineageDao.upsertColumnLineageRow(...)once per group. That persists a distinct transformation per input/output edge, whichcolumn_lineageand the read path already supported — without touchingColumnLineageDao's public signature, soColumnLineageDaoTestdidn't need to change either.Testing: new
OpenLineageDaoColumnLineageTransformationTest(unit, no DB) exercisestransformationOfdirectly against the issue's exact scenario, plus multi-field, fallback-to-deprecated, both-formats-absent, and multiple-transformations-on-one-field cases. Did the fail-then-pass check by hand: temporarily revertedtransformationOfto always read the deprecated fields, saw 3 of 5 tests fail with null description/type, restored the fix, all 5 passed.Also added
OpenLineageDaoTest#testUpdateMarquezModelDatasetWithColumnLineageFacet_usesNewTransformationsFormat, a full DB-backed test that builds a facet using only the newinputFields[].transformations[]format (deprecated fields deliberately left unset, matching real modern producers) and checks the persisted row has a non-null description/type../gradlew :api:testUnitpasses (123 tests, including the 5 new ones). Couldn't run the Postgres-backedOpenLineageDaoTestlocally — same Testcontainers/Docker API mismatch I keep hitting in this sandbox (bundleddocker-javawants API v1.32, the local engine needs >= v1.40; confirmed pre-existing by hitting it on unmodifiedRunDaoTesttoo).:api:compileTestJavapasses, so the new integration test at least compiles clean against the real classes. Would appreciate CI running the DB suite here.Known gap I'm leaving in (marked with a comment in the code)
While working on this I found a real edge case in
upsertColumnLineagethat's worth surfacing outside a code comment. When a single output column's input fields span 2+ distinct transformation groups,upsertColumnLineageRow(...)gets called once per group — but that method's existing implementation always returns allcolumn_lineagerows currently persisted for that(outputDatasetVersionUuid, outputDatasetFieldUuid)pair, not just the rows the current call wrote. So across multiple groups, the same row can show up more than once in theList<ColumnLineageRow>this method returns.It has no production impact right now: the only consumer of that return value,
DatasetRecord#getColumnLineageRows(), isn't read anywhere in production code — only in tests, and this PR's own new test only exercises the single-group case, so it doesn't trigger the duplication. The rows actually persisted incolumn_lineageare correct either way; this only affects the in-memory list handed back up the call stack.I left a
// NOTE:comment right above the relevant code explaining this, in case someone adds a real consumer ofgetColumnLineageRows()later — they'd want to either dedupe or changeupsertColumnLineageRow's contract to return only newly-written rows. Not fixing it here since it's inert today and out of scope for the null-transformation bug this PR is actually about.Fixes #3100.