Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create column lineage endpoint proposal #2077

Merged
merged 4 commits into from
Sep 12, 2022

Conversation

julienledem
Copy link
Member

Problem

This is the proposal document for #2045

@codecov
Copy link

codecov bot commented Aug 19, 2022

Codecov Report

Merging #2077 (071fdc1) into main (54d0e58) will not change coverage.
The diff coverage is n/a.

@@            Coverage Diff            @@
##               main    #2077   +/-   ##
=========================================
  Coverage     75.14%   75.14%           
  Complexity     1023     1023           
=========================================
  Files           202      202           
  Lines          4836     4836           
  Branches        392      392           
=========================================
  Hits           3634     3634           
  Misses          762      762           
  Partials        440      440           

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

Signed-off-by: Julien Le Dem <julien@apache.org>
proposals/2045-column-lineage-endpoint.md Outdated Show resolved Hide resolved
### Use cases
- Find the current upstream dependencies of a column. A column in a dataset is derived from columns in upstream datasets.
- See column-level lineage in the dataset level lineage when available.
- Retrieve point-in-time upstream lineage for a dataset or a column. What did the lineage look like yesterday compared to today?
Copy link
Member

Choose a reason for hiding this comment

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

What did the lineage look like yesterday compared to today?

The compare use case needs a proposal on it's own 😉

Copy link
Member Author

Choose a reason for hiding this comment

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

fair enough, I'm hoping someone else can take over that part and go in the details

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm thinking to start with just point-in-time upstream lineage. And have compare later

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this proposal should be limited to point-in-time within column-level lineage. We should leave compare feature and also point-in-time for lineage endpoint which has nothing to do with column level.

"fields": [{
...
}],
> columnLineage: {
Copy link
Member

Choose a reason for hiding this comment

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

Calls to GET /lineage returns an array of nodes. Meaning, we'll want to add columnLineage to DatasetData. We can use the generated classes in the openlineage-java lib. for column level lineage (vs maintaining our own). Which, I think we still need to generate @julienledem? I don't see them in the javadocs for OpenLineage.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think yes, we would reuse the columnLineage facet object. The OL javadoc needs to be updated. It is not an automated process at the moment

### add a column-level-lineage endpoint:

```
GET /column-lineage?nodeId=dataset:food_delivery:public.delivery_7_days&column=a
Copy link
Member

@wslulciuc wslulciuc Aug 19, 2022

Choose a reason for hiding this comment

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

Given that we have the following endpoint to query lineage:

GET /lineage?nodeId=<node-id>

I'm not sure there's much advantage to defining a separate endpoint for column-level lineage. Although a new endpoint would contextualize the API call; with proper documentation, we can extend out current lineage endpoint to support columns:

GET /lineage?nodeId=<node-id>,column=<column>

If the query param column is present, the backend will assume the nodeId to be a dataset node ID and return an upstream lineage graph with only dataset-to-dataset relationships. That said, column-level lineage is an upstream query from the origin <node-id> (as defined in this proposal). The /lineage call assumes both upstream and downstream lineage. To further contextualize the call, we should (and would prefer) an upstream specific lineage endpoint:

GET /lineage/upstream?nodeId=<node-id>
GET /lineage/downstream?nodeId=<node-id> # Add for completeness

On the backend, these calls would be handled differently. When querying for upstream lineage, the graph returned would consists of only nodes upstream of <node-id>; similarly for upstream lineage, only nodes downstream.

LineageService.upstreamOf(NodeID)
LineageService.downstreamOf(NodeID)

You can then recursively follow the in edges to traverse the upstream graph consisting of job-to-dataset relationships:

{
  .
  .
  "inEdges": [{
    "origin": "job:{namespace}:{job}",
    "destination": "dataset:{namespace}:{dataset}"
  }],
  "outEdges": [{
    "origin": "job:{namespace}:{job}",
    "destination": "dataset:{namespace}:{dataset}"
  }]
}

For column-level lineage, the in / out node edges in the upstream lineage graph would contain both job and dataset node IDs, though only dataset nodes would be present. This means, the in / out edges would still be a job-to-dataset relationships, but now you wouldn't be able to recursively follow the in edges as before given that the job nodes in the returned upstream graph aren't present; and though the dataset node contains column-level lineage metadata via columnLineage, the in/out edges of the node doesn't feel consistent.

By consistent, I mean that backend can assist in better representing the dataset-to-dataset relationship (or rather dataset-column-to-dataset-column relationship) on a given dataset for a particular column by defining the following node ID:

dataset:{namespace}:{dataset}#{field}

Note: As an alternative, we can use datasetField:{namespace}:{dataset}:{field}.

For example, with the node ID defined, an upstream lineage call would now be:

GET /lineage/upstream?nodeId=dataset:my-namespace:my-dataset#my-field
{
  .
  .
  "inEdges": [{
    "origin": "dataset:my-namespace:my-dataset#my-field",
    "destination": "dataset:my-namespace:my-other-dataset#my-other-field"
  }],
  "outEdges": [{
    "origin": "dataset:my-namespace:my-other-dataset#my-other-field",
    "destination": "dataset:my-namespace:some-other-dataset#some-other-field"
  }]
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm proposing a different endpoint fot /column-lineage because the payload would be different, containing only datasets. I was considering that the columnLineage facet was already providing edges and that the inEdges and outEdges fields of the lineage graph became unnecessary.

To me /upstream or /downstream is not an endpoint as they are more of a filter on the lineage than a different result.

Copy link
Member

@wslulciuc wslulciuc Aug 20, 2022

Choose a reason for hiding this comment

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

I was considering that the columnLineage facet was already providing edges and that the inEdges and outEdges fields of the lineage graph became unnecessary

I would then change the payload from a graph consisting of nodes (with in/out edges that aren't really relevant), to more an array of datasets objects that don't have in / out edges as much of the metadata that is relevant for lineage, wouldn't apply here.

My thinking is this: the lineage call returns a set of nodes, but doesn't specify if they all have to be datasets, or all have to be jobs. It's generic in that way. What matters are the nodeIDs and that the origin and destination point to a node in the return node set. Adding a new node type datasetField would fulfill the API contract. But, like you said, whether a query is upstream or downstream can be a backend implementation that can be based on the node type:

GET /lineage?nodeId=datasetField:{namespace}:{dataset}:{field}

Basically, I think column-level lineage should still be represented via a graph data structure. If we will only be using columnLineage to establish relationships between datasets, then it's less of a graph and more of a list of objects that are assumed to be connected.

Copy link
Collaborator

@pawel-big-lebowski pawel-big-lebowski Aug 30, 2022

Choose a reason for hiding this comment

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

Ohh man, it's great discussion although it took me 10 times reading to get to know what are you talking about.

I tried to include the initial idea of Julien mixed with the feedback of Willy.

Some clue design decisions:

  • existing lineage endpoint will be enriched with column lineage as-is (column lineage facet included within dataset)
  • new column-lineage will return column lineage graph with edges between dataset fields. It will reuse existing Graph data structure with new new dataset_field node type.
  • Jobs won't be included in the graph, as a single job may have tons of edges to the fields. Edges will connect dataset_field nodes directly.

Other:

  • graph depth can be controlled by url parameter,
  • downstream lineage will be turned off by default and can be turned on when requested,
  • depth of a returned graph can be controlled by URL parameter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the update @pawel-big-lebowski This looks good to me. I left a minor comment bellow

Copy link
Member Author

@julienledem julienledem left a comment

Choose a reason for hiding this comment

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

This looks good to me, just a minor comment on the column-lineage payload

Comment on lines +117 to +91
"inEdges": [
{
"origin": "datasetField:db1:table1:a",
"destination": "datasetField:DBA:tableA:columnA"
},
{
"origin": "datasetField:db1:table1:a",
"destination": "datasetField:DBB:tableB:columnB"
},
{
"origin": "datasetField:db1:table1:a",
"destination": "datasetField:DBB:tableB:columnC"
}
],
Copy link
Member Author

Choose a reason for hiding this comment

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

This is redundant with inputFields. namespace and name in inputfields are always matching origin in inEdges. Maybe we combine those?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I considered it may be useful to present inputFields on WEB UI. If so, it is beneficial to have this information redundant to avoid parsing edges to inputFields.

@julienledem
Copy link
Member Author

I can't approve my own PR, but I approve @pawel-big-lebowski 's changes to it :)

Signed-off-by: Pawel Leszczynski <leszczynski.pawel@gmail.com>
@julienledem
Copy link
Member Author

Are we ready to merge this? Did you want to add something?

@pawel-big-lebowski pawel-big-lebowski merged commit 00226b2 into main Sep 12, 2022
@pawel-big-lebowski pawel-big-lebowski deleted the column-lineage-proposal branch September 12, 2022 05:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants