-
Notifications
You must be signed in to change notification settings - Fork 204
Support ref() and DAG dependencies in PropertyGraph #2258
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -1,15 +1,32 @@ | ||||
| import { verifyObjectMatchesProto, VerifyProtoErrorBehaviour } from "df/common/protos"; | ||||
| import { ActionBuilder } from "df/core/actions"; | ||||
| import { Declaration } from "df/core/actions/declaration"; | ||||
| import { Session } from "df/core/session"; | ||||
| import { checkAssertionsForDependency } from "df/core/utils"; | ||||
| import { dataform } from "df/protos/ts"; | ||||
|
|
||||
| const CATALOG_NOT_SUPPORTED_MESSAGE = | ||||
| "Catalog-based data sources (BigLake/Iceberg/external catalogs) are not yet supported."; | ||||
|
|
||||
| function entityRefKey(name: string): string { | ||||
| return `entity:${name}`; | ||||
| } | ||||
|
|
||||
| function relationshipRefKey(name: string): string { | ||||
| return `relationship:${name}`; | ||||
| } | ||||
|
|
||||
| export class PropertyGraph extends ActionBuilder<dataform.PropertyGraph> { | ||||
| public session: Session; | ||||
| public dependOnDependencyAssertions: boolean = false; | ||||
|
|
||||
| private proto = dataform.PropertyGraph.create({ description: "", disabled: false }); | ||||
| // Populated during construction for entity/relationship configs that use a ref-style | ||||
| // dataSource; drained in finalize() once all actions are indexed, since the target | ||||
| // schema/database of a ref cannot be resolved until every action is known. | ||||
| // Keys are produced by entityRefKey / relationshipRefKey. | ||||
| private pendingRefs = new Map<string, dataform.ITarget>(); | ||||
| private dependencyKeys = new Set<string>(); | ||||
|
|
||||
| constructor(session?: Session, unverifiedConfig?: any, filename?: string) { | ||||
| super(session); | ||||
|
|
@@ -61,8 +78,6 @@ export class PropertyGraph extends ActionBuilder<dataform.PropertyGraph> { | |||
| this.validateUniqueNames(config.name); | ||||
| this.resolveEndpointDefaults(); | ||||
| this.validateEndpointReferences(config.name); | ||||
|
|
||||
| this.proto.graphBody = this.emitGraphBody(); | ||||
| } | ||||
|
|
||||
| public getFileName() { | ||||
|
|
@@ -74,20 +89,66 @@ export class PropertyGraph extends ActionBuilder<dataform.PropertyGraph> { | |||
| } | ||||
|
|
||||
| public compile() { | ||||
| return verifyObjectMatchesProto( | ||||
| this.proto = verifyObjectMatchesProto( | ||||
| dataform.PropertyGraph, | ||||
| this.proto, | ||||
| VerifyProtoErrorBehaviour.SUGGEST_REPORTING_TO_DATAFORM_TEAM | ||||
| ); | ||||
| return this.proto; | ||||
| } | ||||
|
|
||||
| public finalize() { | ||||
| let allResolved = true; | ||||
| for (const entity of this.proto.entities) { | ||||
| if (!this.resolveRefIntoDataSource(entity, entityRefKey(entity.name))) { | ||||
| allResolved = false; | ||||
| } | ||||
| } | ||||
| for (const rel of this.proto.relationships) { | ||||
| if (!this.resolveRefIntoDataSource(rel, relationshipRefKey(rel.name))) { | ||||
| allResolved = false; | ||||
| } | ||||
| } | ||||
| if (allResolved) { | ||||
| this.proto.graphBody = this.emitGraphBody(); | ||||
| } | ||||
| } | ||||
|
|
||||
| private resolveRefIntoDataSource( | ||||
| entityOrRel: dataform.IGraphEntity | dataform.IGraphRelationship, | ||||
| key: string | ||||
| ): boolean { | ||||
| const rawRef = this.pendingRefs.get(key); | ||||
| if (!rawRef) { | ||||
| return true; | ||||
| } | ||||
| const resolved = this.session.indexedActions.find(rawRef); | ||||
| if (resolved.length !== 1) { | ||||
| return false; | ||||
| } | ||||
| const resolvedAction = resolved[0]; | ||||
| const target = resolvedAction.getTarget(); | ||||
| if (resolvedAction instanceof Declaration) { | ||||
| entityOrRel.dataSource = target; | ||||
| } else { | ||||
| entityOrRel.dataSource = dataform.Target.create({ | ||||
| database: target.database && this.session.finalizeDatabase(target.database), | ||||
| schema: this.session.finalizeSchema(target.schema), | ||||
| name: this.session.finalizeName(target.name) | ||||
| }); | ||||
| } | ||||
|
Comment on lines
+125
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this logic repeats this a lot Line 250 in b5a1435
session.resolve method here?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. resolve() returns SQL string, we need target. I could still unify it introducing shared helper. Is this what you'd expect? |
||||
| return true; | ||||
| } | ||||
|
|
||||
| private normalizeEntitiesAndRelationships(unverifiedConfig: any) { | ||||
| for (const entity of unverifiedConfig.entities || []) { | ||||
| normalizeRef(entity); | ||||
| normalizeKeys(entity); | ||||
| normalizeFields(entity); | ||||
| normalizeFieldsOnLabels(entity); | ||||
| } | ||||
| for (const relationship of unverifiedConfig.relationships || []) { | ||||
| normalizeRef(relationship); | ||||
| normalizeKeys(relationship); | ||||
| normalizeFields(relationship); | ||||
| normalizeFieldsOnLabels(relationship); | ||||
|
|
@@ -113,7 +174,7 @@ export class PropertyGraph extends ActionBuilder<dataform.PropertyGraph> { | |||
| } | ||||
| const entityName = entityConfig.name; | ||||
| const where = `Property graph '${graphName}': entity '${entityName}'`; | ||||
| const dataSource = this.resolveDataSource(entityConfig, where); | ||||
| const dataSource = this.resolveDataSource(entityConfig, entityRefKey(entityName), where); | ||||
| if (!entityConfig.keys || entityConfig.keys.length === 0) { | ||||
| throw new Error(`${where} must declare 'keys'.`); | ||||
| } | ||||
|
|
@@ -137,7 +198,7 @@ export class PropertyGraph extends ActionBuilder<dataform.PropertyGraph> { | |||
| } | ||||
| const relName = relConfig.name; | ||||
| const where = `Property graph '${graphName}': relationship '${relName}'`; | ||||
| const dataSource = this.resolveDataSource(relConfig, where); | ||||
| const dataSource = this.resolveDataSource(relConfig, relationshipRefKey(relName), where); | ||||
|
|
||||
| if (!relConfig.source) { | ||||
| throw new Error(`${where} must declare 'source'.`); | ||||
|
|
@@ -163,6 +224,7 @@ export class PropertyGraph extends ActionBuilder<dataform.PropertyGraph> { | |||
|
|
||||
| private resolveDataSource( | ||||
| entityOrRel: dataform.IGraphEntityConfig | dataform.IGraphRelationshipConfig, | ||||
| pendingKey: string, | ||||
| where: string | ||||
| ): dataform.Target { | ||||
| if (entityOrRel.dataSourceCatalog) { | ||||
|
|
@@ -187,6 +249,29 @@ export class PropertyGraph extends ActionBuilder<dataform.PropertyGraph> { | |||
| } | ||||
| return dataform.Target.create({ name: ds.table, schema: dataset, database: project }); | ||||
| } | ||||
| if (entityOrRel.dataSourceRef) { | ||||
| const ref = entityOrRel.dataSourceRef; | ||||
| if (!ref.name) { | ||||
| throw new Error(`${where}: 'ref' must include a 'name'.`); | ||||
| } | ||||
| const rawRef: dataform.ITarget = { name: ref.name }; | ||||
| if (ref.schema) { | ||||
| rawRef.schema = ref.schema; | ||||
| } | ||||
| if (ref.database) { | ||||
| rawRef.database = ref.database; | ||||
| } | ||||
| this.pendingRefs.set(pendingKey, rawRef); | ||||
| const depKey = `${rawRef.database || ""}.${rawRef.schema || ""}.${rawRef.name}`; | ||||
| if (!this.dependencyKeys.has(depKey)) { | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this logic should respect https://docs.cloud.google.com/dataform/docs/dependencies#set-assertions-as-dependencies dataform/core/actions/table.ts Line 370 in b5a1435
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, but note graph.yaml has no surface for these flags today (DataSourceRef proto is just {name, schema, database}), so the plumbing is inert. |
||||
| this.dependencyKeys.add(depKey); | ||||
| const depTarget = checkAssertionsForDependency(this, rawRef); | ||||
| if (depTarget) { | ||||
| this.proto.dependencyTargets.push(depTarget); | ||||
| } | ||||
| } | ||||
| return undefined; | ||||
| } | ||||
| throw new Error(`${where}: must declare a data source.`); | ||||
| } | ||||
|
|
||||
|
|
@@ -268,6 +353,21 @@ export class PropertyGraph extends ActionBuilder<dataform.PropertyGraph> { | |||
| } | ||||
| } | ||||
|
|
||||
| function normalizeRef(entityOrRel: any) { | ||||
| if (entityOrRel.ref === undefined || entityOrRel.ref === null) { | ||||
| return; | ||||
| } | ||||
| if (typeof entityOrRel.ref === "string") { | ||||
| entityOrRel.dataSourceRef = { name: entityOrRel.ref }; | ||||
| delete entityOrRel.ref; | ||||
| return; | ||||
| } | ||||
| if (typeof entityOrRel.ref === "object") { | ||||
| entityOrRel.dataSourceRef = entityOrRel.ref; | ||||
| delete entityOrRel.ref; | ||||
| } | ||||
| } | ||||
|
|
||||
| function normalizeKeys(entityOrRel: any) { | ||||
| if (typeof entityOrRel.keys === "string") { | ||||
| entityOrRel.keys = [entityOrRel.keys]; | ||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.