From 62a8cdf4a3b076a09313310c80d437b394bdc2db Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 17:29:55 +0100 Subject: [PATCH 1/9] Extend implicit dependency detection to registered models and volume catalogs The CaptureSchemaDependency mutator now captures implicit dependencies for: - Registered models: both schema_name and catalog_name are resolved - Volumes: catalog_name is now also resolved (schema_name was already handled) Also fixes an ordering bug where resolveSchema modified schema.CatalogName before volumes/pipelines could use it for findSchema lookups. Schemas are now resolved last in Apply(). Co-authored-by: Isaac --- .../databricks.yml | 26 ++ .../out.test.toml | 5 + .../implicit_deps_registered_model/output.txt | 37 +++ .../implicit_deps_registered_model/script | 1 + .../implicit_deps_registered_model/test.toml | 1 + .../implicit_deps_volume/databricks.yml | 27 ++ .../implicit_deps_volume/out.test.toml | 5 + .../implicit_deps_volume/output.txt | 38 +++ .../resource_deps/implicit_deps_volume/script | 1 + .../implicit_deps_volume/test.toml | 1 + .../registered_models/databricks.yml.tmpl | 5 +- .../capture_schema_dependency.go | 52 +++- .../capture_schema_dependency_test.go | 257 ++++++++++++++++++ .../resourcemutator/resource_mutator.go | 6 +- 14 files changed, 443 insertions(+), 19 deletions(-) create mode 100644 acceptance/bundle/resource_deps/implicit_deps_registered_model/databricks.yml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_registered_model/out.test.toml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_registered_model/output.txt create mode 100644 acceptance/bundle/resource_deps/implicit_deps_registered_model/script create mode 100644 acceptance/bundle/resource_deps/implicit_deps_registered_model/test.toml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_volume/databricks.yml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_volume/out.test.toml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_volume/output.txt create mode 100644 acceptance/bundle/resource_deps/implicit_deps_volume/script create mode 100644 acceptance/bundle/resource_deps/implicit_deps_volume/test.toml diff --git a/acceptance/bundle/resource_deps/implicit_deps_registered_model/databricks.yml b/acceptance/bundle/resource_deps/implicit_deps_registered_model/databricks.yml new file mode 100644 index 0000000000..ece630fd2e --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_registered_model/databricks.yml @@ -0,0 +1,26 @@ +bundle: + name: test-bundle + +# Tests implicit dependency detection for registered models: +# - registered model should implicitly depend on schema (schema_name resolved) +# - registered model should implicitly depend on catalog (catalog_name resolved) +resources: + catalogs: + my_catalog: + name: mycatalog + schemas: + my_schema: + catalog_name: mycatalog + name: myschema + registered_models: + my_model: + catalog_name: mycatalog + schema_name: myschema + name: mymodel + pipelines: + mypipeline: + name: pipeline for ${resources.registered_models.my_model.catalog_name}.${resources.registered_models.my_model.schema_name}.${resources.registered_models.my_model.name} + +targets: + dev: + mode: development diff --git a/acceptance/bundle/resource_deps/implicit_deps_registered_model/out.test.toml b/acceptance/bundle/resource_deps/implicit_deps_registered_model/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_registered_model/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resource_deps/implicit_deps_registered_model/output.txt b/acceptance/bundle/resource_deps/implicit_deps_registered_model/output.txt new file mode 100644 index 0000000000..dc48ffa6b7 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_registered_model/output.txt @@ -0,0 +1,37 @@ + +>>> [CLI] bundle validate -t dev -o json +{ + "catalogs": { + "my_catalog": { + "name": "mycatalog" + } + }, + "pipelines": { + "mypipeline": { + "channel": "CURRENT", + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/dev/state/metadata.json" + }, + "development": true, + "edition": "ADVANCED", + "name": "[dev [USERNAME]] pipeline for ${resources.registered_models.my_model.catalog_name}.${resources.registered_models.my_model.schema_name}.${resources.registered_models.my_model.name}", + "tags": { + "dev": "[USERNAME]" + } + } + }, + "registered_models": { + "my_model": { + "catalog_name": "${resources.catalogs.my_catalog.name}", + "name": "dev_[USERNAME]_mymodel", + "schema_name": "${resources.schemas.my_schema.name}" + } + }, + "schemas": { + "my_schema": { + "catalog_name": "${resources.catalogs.my_catalog.name}", + "name": "dev_[USERNAME]_myschema" + } + } +} diff --git a/acceptance/bundle/resource_deps/implicit_deps_registered_model/script b/acceptance/bundle/resource_deps/implicit_deps_registered_model/script new file mode 100644 index 0000000000..30cb3ec2e5 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_registered_model/script @@ -0,0 +1 @@ +trace $CLI bundle validate -t dev -o json | jq .resources diff --git a/acceptance/bundle/resource_deps/implicit_deps_registered_model/test.toml b/acceptance/bundle/resource_deps/implicit_deps_registered_model/test.toml new file mode 100644 index 0000000000..a030353d57 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_registered_model/test.toml @@ -0,0 +1 @@ +RecordRequests = false diff --git a/acceptance/bundle/resource_deps/implicit_deps_volume/databricks.yml b/acceptance/bundle/resource_deps/implicit_deps_volume/databricks.yml new file mode 100644 index 0000000000..95bd981c54 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_volume/databricks.yml @@ -0,0 +1,27 @@ +bundle: + name: test-bundle + +# Tests implicit dependency detection for volumes: +# - volume should implicitly depend on schema (schema_name resolved) +# - volume should implicitly depend on catalog (catalog_name resolved) +resources: + catalogs: + my_catalog: + name: mycatalog + schemas: + my_schema: + catalog_name: mycatalog + name: myschema + volumes: + my_volume: + catalog_name: mycatalog + schema_name: myschema + name: myvolume + volume_type: MANAGED + pipelines: + mypipeline: + name: pipeline for ${resources.volumes.my_volume.catalog_name}.${resources.volumes.my_volume.schema_name}.${resources.volumes.my_volume.name} + +targets: + dev: + mode: development diff --git a/acceptance/bundle/resource_deps/implicit_deps_volume/out.test.toml b/acceptance/bundle/resource_deps/implicit_deps_volume/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_volume/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resource_deps/implicit_deps_volume/output.txt b/acceptance/bundle/resource_deps/implicit_deps_volume/output.txt new file mode 100644 index 0000000000..2c936df73b --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_volume/output.txt @@ -0,0 +1,38 @@ + +>>> [CLI] bundle validate -t dev -o json +{ + "catalogs": { + "my_catalog": { + "name": "mycatalog" + } + }, + "pipelines": { + "mypipeline": { + "channel": "CURRENT", + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/dev/state/metadata.json" + }, + "development": true, + "edition": "ADVANCED", + "name": "[dev [USERNAME]] pipeline for ${resources.volumes.my_volume.catalog_name}.${resources.volumes.my_volume.schema_name}.${resources.volumes.my_volume.name}", + "tags": { + "dev": "[USERNAME]" + } + } + }, + "schemas": { + "my_schema": { + "catalog_name": "${resources.catalogs.my_catalog.name}", + "name": "dev_[USERNAME]_myschema" + } + }, + "volumes": { + "my_volume": { + "catalog_name": "${resources.catalogs.my_catalog.name}", + "name": "myvolume", + "schema_name": "${resources.schemas.my_schema.name}", + "volume_type": "MANAGED" + } + } +} diff --git a/acceptance/bundle/resource_deps/implicit_deps_volume/script b/acceptance/bundle/resource_deps/implicit_deps_volume/script new file mode 100644 index 0000000000..30cb3ec2e5 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_volume/script @@ -0,0 +1 @@ +trace $CLI bundle validate -t dev -o json | jq .resources diff --git a/acceptance/bundle/resource_deps/implicit_deps_volume/test.toml b/acceptance/bundle/resource_deps/implicit_deps_volume/test.toml new file mode 100644 index 0000000000..a030353d57 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_volume/test.toml @@ -0,0 +1 @@ +RecordRequests = false diff --git a/acceptance/bundle/resources/grants/registered_models/databricks.yml.tmpl b/acceptance/bundle/resources/grants/registered_models/databricks.yml.tmpl index f94bf8fd69..f0344c4980 100644 --- a/acceptance/bundle/resources/grants/registered_models/databricks.yml.tmpl +++ b/acceptance/bundle/resources/grants/registered_models/databricks.yml.tmpl @@ -11,9 +11,8 @@ resources: name: mymodel comment: mycomment catalog_name: main - # this does not work because we don't create implicit dependency like we do with volumes: - #schema_name: myschema_$UNIQUE_NAME - schema_name: ${resources.schemas.my_schema.name} + # implicit dependency detection resolves this to ${resources.schemas.my_schema.name} + schema_name: myschema_$UNIQUE_NAME grants: - principal: deco-test-user@databricks.com privileges: ["APPLY_TAG"] diff --git a/bundle/config/mutator/resourcemutator/capture_schema_dependency.go b/bundle/config/mutator/resourcemutator/capture_schema_dependency.go index ef1581393a..764f733d27 100644 --- a/bundle/config/mutator/resourcemutator/capture_schema_dependency.go +++ b/bundle/config/mutator/resourcemutator/capture_schema_dependency.go @@ -11,14 +11,14 @@ import ( type captureSchemaDependency struct{} -// If a user defines a UC schema in the bundle, they can refer to it in DLT pipelines -// or UC Volumes using the `${resources.schemas..name}` syntax. Using this -// syntax allows TF to capture the deploy time dependency this DLT pipeline or UC Volume -// has on the schema and deploy changes to the schema before deploying the pipeline or volume. +// If a user defines a UC schema in the bundle, they can refer to it in DLT pipelines, +// UC Volumes, or Registered Models using the `${resources.schemas..name}` syntax. +// Using this syntax allows TF to capture the deploy time dependency this resource +// has on the schema and deploy changes to the schema before deploying the dependent resource. // -// Similarly, if a user defines a UC catalog in the bundle, they can refer to it in UC schemas -// using the `${resources.catalogs..name}` syntax. This captures the deploy time -// dependency the schema has on the catalog. +// Similarly, if a user defines a UC catalog in the bundle, they can refer to it in UC schemas, +// UC Volumes, or Registered Models using the `${resources.catalogs..name}` syntax. +// This captures the deploy time dependency the resource has on the catalog. // // This mutator translates any implicit catalog or schema references to the explicit syntax. func CaptureSchemaDependency() bundle.Mutator { @@ -54,12 +54,32 @@ func resolveVolume(v *resources.Volume, b *bundle.Bundle) { if v == nil { return } + // Resolve schema first since findSchema needs the original v.CatalogName. schemaK, schema := findSchema(b, v.CatalogName, v.SchemaName) - if schema == nil { + if schema != nil { + v.SchemaName = schemaNameRef(schemaK) + } + + catalogK, catalog := findCatalog(b, v.CatalogName) + if catalog != nil { + v.CatalogName = catalogNameRef(catalogK) + } +} + +func resolveRegisteredModel(rm *resources.RegisteredModel, b *bundle.Bundle) { + if rm == nil { return } + // Resolve schema first since findSchema needs the original rm.CatalogName. + schemaK, schema := findSchema(b, rm.CatalogName, rm.SchemaName) + if schema != nil { + rm.SchemaName = schemaNameRef(schemaK) + } - v.SchemaName = schemaNameRef(schemaK) + catalogK, catalog := findCatalog(b, rm.CatalogName) + if catalog != nil { + rm.CatalogName = catalogNameRef(catalogK) + } } func resolvePipelineSchema(p *resources.Pipeline, b *bundle.Bundle) { @@ -117,8 +137,14 @@ func resolveSchema(s *resources.Schema, b *bundle.Bundle) { } func (m *captureSchemaDependency) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - for _, s := range b.Config.Resources.Schemas { - resolveSchema(s, b) + // Resolve resources that depend on schemas before resolving schemas themselves. + // resolveSchema modifies schema.CatalogName, and findSchema (used by the other + // resolve functions) matches against the original schema.CatalogName value. + for _, v := range b.Config.Resources.Volumes { + resolveVolume(v, b) + } + for _, rm := range b.Config.Resources.RegisteredModels { + resolveRegisteredModel(rm, b) } for _, p := range b.Config.Resources.Pipelines { // "schema" and "target" have the same semantics in the DLT API but are mutually @@ -128,8 +154,8 @@ func (m *captureSchemaDependency) Apply(ctx context.Context, b *bundle.Bundle) d resolvePipelineTarget(p, b) resolvePipelineSchema(p, b) } - for _, v := range b.Config.Resources.Volumes { - resolveVolume(v, b) + for _, s := range b.Config.Resources.Schemas { + resolveSchema(s, b) } return nil } diff --git a/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go b/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go index 3993f45041..fc70dc9304 100644 --- a/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go +++ b/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go @@ -275,6 +275,263 @@ func TestCaptureSchemaDependencyForPipelinesWithSchema(t *testing.T) { } } +func TestCaptureSchemaDependencyForRegisteredModel(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Schemas: map[string]*resources.Schema{ + "schema1": { + CreateSchema: catalog.CreateSchema{ + CatalogName: "catalog1", + Name: "foobar", + }, + }, + "schema2": { + CreateSchema: catalog.CreateSchema{ + CatalogName: "catalog2", + Name: "foobar", + }, + }, + "schema3": { + CreateSchema: catalog.CreateSchema{ + CatalogName: "catalog1", + Name: "barfoo", + }, + }, + "nilschema": nil, + "emptyschema": {}, + }, + RegisteredModels: map[string]*resources.RegisteredModel{ + "model1": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "catalog1", + SchemaName: "foobar", + }, + }, + "model2": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "catalog2", + SchemaName: "foobar", + }, + }, + "model3": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "catalog1", + SchemaName: "barfoo", + }, + }, + "model4": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "catalogX", + SchemaName: "foobar", + }, + }, + "model5": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "catalog1", + SchemaName: "schemaX", + }, + }, + "nilModel": nil, + "emptyModel": {}, + }, + }, + }, + } + + d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + require.Nil(t, d) + + assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.RegisteredModels["model1"].SchemaName) + assert.Equal(t, "${resources.schemas.schema2.name}", b.Config.Resources.RegisteredModels["model2"].SchemaName) + assert.Equal(t, "${resources.schemas.schema3.name}", b.Config.Resources.RegisteredModels["model3"].SchemaName) + assert.Equal(t, "foobar", b.Config.Resources.RegisteredModels["model4"].SchemaName) + assert.Equal(t, "schemaX", b.Config.Resources.RegisteredModels["model5"].SchemaName) + + assert.Nil(t, b.Config.Resources.RegisteredModels["nilModel"]) +} + +func TestCaptureCatalogDependencyForVolume(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Catalogs: map[string]*resources.Catalog{ + "catalog1": { + CreateCatalog: catalog.CreateCatalog{ + Name: "catalog1", + }, + }, + "catalog2": { + CreateCatalog: catalog.CreateCatalog{ + Name: "catalog2", + }, + }, + "nilcatalog": nil, + "emptycatalog": {}, + }, + Volumes: map[string]*resources.Volume{ + "volume1": { + CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ + CatalogName: "catalog1", + SchemaName: "foobar", + }, + }, + "volume2": { + CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ + CatalogName: "catalog2", + SchemaName: "foobar", + }, + }, + "volume3": { + CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ + CatalogName: "catalogX", + SchemaName: "foobar", + }, + }, + "volume4": { + CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ + CatalogName: "", + SchemaName: "foobar", + }, + }, + "nilVolume": nil, + "emptyVolume": {}, + }, + }, + }, + } + + d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + require.Nil(t, d) + + assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Volumes["volume1"].CatalogName) + assert.Equal(t, "${resources.catalogs.catalog2.name}", b.Config.Resources.Volumes["volume2"].CatalogName) + assert.Equal(t, "catalogX", b.Config.Resources.Volumes["volume3"].CatalogName) + assert.Equal(t, "", b.Config.Resources.Volumes["volume4"].CatalogName) + + assert.Nil(t, b.Config.Resources.Volumes["nilVolume"]) +} + +func TestCaptureCatalogDependencyForRegisteredModel(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Catalogs: map[string]*resources.Catalog{ + "catalog1": { + CreateCatalog: catalog.CreateCatalog{ + Name: "catalog1", + }, + }, + "catalog2": { + CreateCatalog: catalog.CreateCatalog{ + Name: "catalog2", + }, + }, + "nilcatalog": nil, + "emptycatalog": {}, + }, + RegisteredModels: map[string]*resources.RegisteredModel{ + "model1": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "catalog1", + SchemaName: "foobar", + }, + }, + "model2": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "catalog2", + SchemaName: "foobar", + }, + }, + "model3": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "catalogX", + SchemaName: "foobar", + }, + }, + "model4": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "", + SchemaName: "foobar", + }, + }, + "nilModel": nil, + "emptyModel": {}, + }, + }, + }, + } + + d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + require.Nil(t, d) + + assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.RegisteredModels["model1"].CatalogName) + assert.Equal(t, "${resources.catalogs.catalog2.name}", b.Config.Resources.RegisteredModels["model2"].CatalogName) + assert.Equal(t, "catalogX", b.Config.Resources.RegisteredModels["model3"].CatalogName) + assert.Equal(t, "", b.Config.Resources.RegisteredModels["model4"].CatalogName) + + assert.Nil(t, b.Config.Resources.RegisteredModels["nilModel"]) +} + +// Test that when a catalog, schema, and dependent resources (volume, registered model) +// are all defined in the same bundle, all implicit dependencies are correctly captured. +// This verifies the ordering fix: schemas must be resolved last since resolveSchema +// modifies schema.CatalogName, and findSchema (used by volume/model resolution) matches +// against the original value. +func TestCaptureImplicitDependencyWithCatalogSchemaAndVolume(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Catalogs: map[string]*resources.Catalog{ + "my_catalog": { + CreateCatalog: catalog.CreateCatalog{ + Name: "mycatalog", + }, + }, + }, + Schemas: map[string]*resources.Schema{ + "my_schema": { + CreateSchema: catalog.CreateSchema{ + CatalogName: "mycatalog", + Name: "myschema", + }, + }, + }, + Volumes: map[string]*resources.Volume{ + "my_volume": { + CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ + CatalogName: "mycatalog", + SchemaName: "myschema", + }, + }, + }, + RegisteredModels: map[string]*resources.RegisteredModel{ + "my_model": { + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "mycatalog", + SchemaName: "myschema", + }, + }, + }, + }, + }, + } + + d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + require.Nil(t, d) + + // Schema should have catalog dependency. + assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.Schemas["my_schema"].CatalogName) + + // Volume should have both schema and catalog dependencies. + assert.Equal(t, "${resources.schemas.my_schema.name}", b.Config.Resources.Volumes["my_volume"].SchemaName) + assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.Volumes["my_volume"].CatalogName) + + // Registered model should have both schema and catalog dependencies. + assert.Equal(t, "${resources.schemas.my_schema.name}", b.Config.Resources.RegisteredModels["my_model"].SchemaName) + assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.RegisteredModels["my_model"].CatalogName) +} + func TestCaptureCatalogDependencyForSchema(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ diff --git a/bundle/config/mutator/resourcemutator/resource_mutator.go b/bundle/config/mutator/resourcemutator/resource_mutator.go index 87069d6f84..009fd5af9d 100644 --- a/bundle/config/mutator/resourcemutator/resource_mutator.go +++ b/bundle/config/mutator/resourcemutator/resource_mutator.go @@ -171,9 +171,9 @@ func applyNormalizeMutators(ctx context.Context, b *bundle.Bundle) { // Updates (dynamic): same paths — merges grant entries by principal and deduplicates privileges MergeGrants(), - // Reads (typed): resources.pipelines.*.{catalog,schema,target}, resources.volumes.*.{catalog_name,schema_name} (checks for schema references) - // Updates (typed): resources.pipelines.*.{schema,target}, resources.volumes.*.schema_name (converts implicit schema references to explicit ${resources.schemas..name} syntax) - // Translates implicit schema references in DLT pipelines or UC Volumes to explicit syntax to capture dependencies + // Reads (typed): resources.pipelines.*.{catalog,schema,target}, resources.volumes.*.{catalog_name,schema_name}, resources.registered_models.*.{catalog_name,schema_name} + // Updates (typed): resources.pipelines.*.{schema,target}, resources.volumes.*.{catalog_name,schema_name}, resources.registered_models.*.{catalog_name,schema_name}, resources.schemas.*.catalog_name + // Translates implicit schema and catalog references in DLT pipelines, UC Volumes, and Registered Models to explicit syntax to capture dependencies CaptureSchemaDependency(), // Reads (dynamic): resources.dashboards.*.file_path From 2efcdb17c30d31733accbaea6966ec739d5d47f8 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 17:55:44 +0100 Subject: [PATCH 2/9] Add implicit dependency detection for quality monitors and model serving endpoints Extends CaptureSchemaDependency to also resolve: - Quality monitors: OutputSchemaName (compound "catalog.schema" format) - Model serving endpoints: AiGateway.InferenceTableConfig and Config.AutoCaptureConfig catalog/schema references Co-authored-by: Isaac --- .../databricks.yml | 25 +++ .../out.test.toml | 5 + .../output.txt | 27 +++ .../script | 1 + .../test.toml | 1 + .../databricks.yml | 21 +++ .../out.test.toml | 5 + .../implicit_deps_quality_monitor/output.txt | 25 +++ .../implicit_deps_quality_monitor/script | 1 + .../implicit_deps_quality_monitor/test.toml | 1 + .../capture_schema_dependency.go | 88 +++++++++- .../capture_schema_dependency_test.go | 155 ++++++++++++++++++ .../resourcemutator/resource_mutator.go | 7 +- 13 files changed, 354 insertions(+), 8 deletions(-) create mode 100644 acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/databricks.yml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/out.test.toml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/output.txt create mode 100644 acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/script create mode 100644 acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/test.toml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_quality_monitor/databricks.yml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_quality_monitor/out.test.toml create mode 100644 acceptance/bundle/resource_deps/implicit_deps_quality_monitor/output.txt create mode 100644 acceptance/bundle/resource_deps/implicit_deps_quality_monitor/script create mode 100644 acceptance/bundle/resource_deps/implicit_deps_quality_monitor/test.toml diff --git a/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/databricks.yml b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/databricks.yml new file mode 100644 index 0000000000..cdbf76cc8f --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/databricks.yml @@ -0,0 +1,25 @@ +bundle: + name: test-bundle + +# Tests implicit dependency detection for model serving endpoints: +# - ai_gateway.inference_table_config.{catalog_name, schema_name} should resolve +resources: + catalogs: + my_catalog: + name: mycatalog + schemas: + my_schema: + catalog_name: mycatalog + name: myschema + model_serving_endpoints: + my_endpoint: + name: my-endpoint + ai_gateway: + inference_table_config: + catalog_name: mycatalog + schema_name: myschema + enabled: true + +targets: + dev: + mode: development diff --git a/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/out.test.toml b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/output.txt b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/output.txt new file mode 100644 index 0000000000..42be076238 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/output.txt @@ -0,0 +1,27 @@ + +>>> [CLI] bundle validate -t dev -o json +{ + "catalogs": { + "my_catalog": { + "name": "mycatalog" + } + }, + "model_serving_endpoints": { + "my_endpoint": { + "ai_gateway": { + "inference_table_config": { + "catalog_name": "${resources.catalogs.my_catalog.name}", + "enabled": true, + "schema_name": "${resources.schemas.my_schema.name}" + } + }, + "name": "dev_[USERNAME]_my-endpoint" + } + }, + "schemas": { + "my_schema": { + "catalog_name": "${resources.catalogs.my_catalog.name}", + "name": "dev_[USERNAME]_myschema" + } + } +} diff --git a/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/script b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/script new file mode 100644 index 0000000000..30cb3ec2e5 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/script @@ -0,0 +1 @@ +trace $CLI bundle validate -t dev -o json | jq .resources diff --git a/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/test.toml b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/test.toml new file mode 100644 index 0000000000..a030353d57 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_model_serving_endpoint/test.toml @@ -0,0 +1 @@ +RecordRequests = false diff --git a/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/databricks.yml b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/databricks.yml new file mode 100644 index 0000000000..8fe5f47176 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/databricks.yml @@ -0,0 +1,21 @@ +bundle: + name: test-bundle + +# Tests implicit dependency detection for quality monitors: +# - output_schema_name (format "catalog.schema") should resolve both parts +resources: + catalogs: + my_catalog: + name: mycatalog + schemas: + my_schema: + catalog_name: mycatalog + name: myschema + quality_monitors: + my_monitor: + table_name: mycatalog.myschema.mytable + output_schema_name: mycatalog.myschema + +targets: + dev: + mode: development diff --git a/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/out.test.toml b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/output.txt b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/output.txt new file mode 100644 index 0000000000..c4c40a691f --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/output.txt @@ -0,0 +1,25 @@ + +>>> [CLI] bundle validate -t dev -o json +Warning: required field "assets_dir" is not set + at resources.quality_monitors.my_monitor + in databricks.yml:16:7 + +{ + "catalogs": { + "my_catalog": { + "name": "mycatalog" + } + }, + "quality_monitors": { + "my_monitor": { + "output_schema_name": "${resources.catalogs.my_catalog.name}.${resources.schemas.my_schema.name}", + "table_name": "mycatalog.myschema.mytable" + } + }, + "schemas": { + "my_schema": { + "catalog_name": "${resources.catalogs.my_catalog.name}", + "name": "dev_[USERNAME]_myschema" + } + } +} diff --git a/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/script b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/script new file mode 100644 index 0000000000..30cb3ec2e5 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/script @@ -0,0 +1 @@ +trace $CLI bundle validate -t dev -o json | jq .resources diff --git a/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/test.toml b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/test.toml new file mode 100644 index 0000000000..a030353d57 --- /dev/null +++ b/acceptance/bundle/resource_deps/implicit_deps_quality_monitor/test.toml @@ -0,0 +1 @@ +RecordRequests = false diff --git a/bundle/config/mutator/resourcemutator/capture_schema_dependency.go b/bundle/config/mutator/resourcemutator/capture_schema_dependency.go index 764f733d27..ca53ac16db 100644 --- a/bundle/config/mutator/resourcemutator/capture_schema_dependency.go +++ b/bundle/config/mutator/resourcemutator/capture_schema_dependency.go @@ -3,6 +3,7 @@ package resourcemutator import ( "context" "fmt" + "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config/resources" @@ -12,13 +13,15 @@ import ( type captureSchemaDependency struct{} // If a user defines a UC schema in the bundle, they can refer to it in DLT pipelines, -// UC Volumes, or Registered Models using the `${resources.schemas..name}` syntax. -// Using this syntax allows TF to capture the deploy time dependency this resource -// has on the schema and deploy changes to the schema before deploying the dependent resource. +// UC Volumes, Registered Models, Quality Monitors, or Model Serving Endpoints using the +// `${resources.schemas..name}` syntax. Using this syntax allows TF to capture +// the deploy time dependency this resource has on the schema and deploy changes to the +// schema before deploying the dependent resource. // // Similarly, if a user defines a UC catalog in the bundle, they can refer to it in UC schemas, -// UC Volumes, or Registered Models using the `${resources.catalogs..name}` syntax. -// This captures the deploy time dependency the resource has on the catalog. +// UC Volumes, Registered Models, or Model Serving Endpoints using the +// `${resources.catalogs..name}` syntax. This captures the deploy time +// dependency the resource has on the catalog. // // This mutator translates any implicit catalog or schema references to the explicit syntax. func CaptureSchemaDependency() bundle.Mutator { @@ -124,6 +127,75 @@ func findCatalog(b *bundle.Bundle, catalogName string) (string, *resources.Catal return "", nil } +// resolveQualityMonitor resolves the OutputSchemaName field which is a compound +// "catalog.schema" string. +func resolveQualityMonitor(qm *resources.QualityMonitor, b *bundle.Bundle) { + if qm == nil { + return + } + if qm.OutputSchemaName == "" { + return + } + + parts := strings.SplitN(qm.OutputSchemaName, ".", 2) + if len(parts) != 2 { + return + } + catalogName, schemaName := parts[0], parts[1] + + resolvedCatalog, resolvedSchema := catalogName, schemaName + + schemaK, schema := findSchema(b, catalogName, schemaName) + if schema != nil { + resolvedSchema = schemaNameRef(schemaK) + } + + catalogK, catalog := findCatalog(b, catalogName) + if catalog != nil { + resolvedCatalog = catalogNameRef(catalogK) + } + + if resolvedCatalog != catalogName || resolvedSchema != schemaName { + qm.OutputSchemaName = resolvedCatalog + "." + resolvedSchema + } +} + +func resolveModelServingEndpoint(mse *resources.ModelServingEndpoint, b *bundle.Bundle) { + if mse == nil { + return + } + + // Resolve AiGateway.InferenceTableConfig. + if mse.AiGateway != nil && mse.AiGateway.InferenceTableConfig != nil { + itc := mse.AiGateway.InferenceTableConfig + + schemaK, schema := findSchema(b, itc.CatalogName, itc.SchemaName) + if schema != nil { + itc.SchemaName = schemaNameRef(schemaK) + } + + catalogK, catalog := findCatalog(b, itc.CatalogName) + if catalog != nil { + itc.CatalogName = catalogNameRef(catalogK) + } + } + + // Resolve Config.AutoCaptureConfig (deprecated but still in use). + if mse.Config != nil && mse.Config.AutoCaptureConfig != nil { + acc := mse.Config.AutoCaptureConfig + + schemaK, schema := findSchema(b, acc.CatalogName, acc.SchemaName) + if schema != nil { + acc.SchemaName = schemaNameRef(schemaK) + } + + catalogK, catalog := findCatalog(b, acc.CatalogName) + if catalog != nil { + acc.CatalogName = catalogNameRef(catalogK) + } + } +} + func resolveSchema(s *resources.Schema, b *bundle.Bundle) { if s == nil { return @@ -154,6 +226,12 @@ func (m *captureSchemaDependency) Apply(ctx context.Context, b *bundle.Bundle) d resolvePipelineTarget(p, b) resolvePipelineSchema(p, b) } + for _, qm := range b.Config.Resources.QualityMonitors { + resolveQualityMonitor(qm, b) + } + for _, mse := range b.Config.Resources.ModelServingEndpoints { + resolveModelServingEndpoint(mse, b) + } for _, s := range b.Config.Resources.Schemas { resolveSchema(s, b) } diff --git a/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go b/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go index fc70dc9304..01984dcfbb 100644 --- a/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go +++ b/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/databricks-sdk-go/service/catalog" "github.com/databricks/databricks-sdk-go/service/pipelines" + "github.com/databricks/databricks-sdk-go/service/serving" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -532,6 +533,160 @@ func TestCaptureImplicitDependencyWithCatalogSchemaAndVolume(t *testing.T) { assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.RegisteredModels["my_model"].CatalogName) } +func TestCaptureImplicitDependencyForQualityMonitor(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Catalogs: map[string]*resources.Catalog{ + "my_catalog": { + CreateCatalog: catalog.CreateCatalog{ + Name: "mycatalog", + }, + }, + }, + Schemas: map[string]*resources.Schema{ + "my_schema": { + CreateSchema: catalog.CreateSchema{ + CatalogName: "mycatalog", + Name: "myschema", + }, + }, + }, + QualityMonitors: map[string]*resources.QualityMonitor{ + // Both catalog and schema match. + "monitor1": { + CreateMonitor: catalog.CreateMonitor{ + OutputSchemaName: "mycatalog.myschema", + }, + }, + // Only catalog matches (schema not in bundle). + "monitor2": { + CreateMonitor: catalog.CreateMonitor{ + OutputSchemaName: "mycatalog.otherschema", + }, + }, + // Neither matches. + "monitor3": { + CreateMonitor: catalog.CreateMonitor{ + OutputSchemaName: "othercatalog.otherschema", + }, + }, + // Empty output schema. + "monitor4": { + CreateMonitor: catalog.CreateMonitor{ + OutputSchemaName: "", + }, + }, + // No dot separator (invalid format, should be left alone). + "monitor5": { + CreateMonitor: catalog.CreateMonitor{ + OutputSchemaName: "nodot", + }, + }, + "nilMonitor": nil, + }, + }, + }, + } + + d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + require.Nil(t, d) + + assert.Equal(t, "${resources.catalogs.my_catalog.name}.${resources.schemas.my_schema.name}", b.Config.Resources.QualityMonitors["monitor1"].OutputSchemaName) + assert.Equal(t, "${resources.catalogs.my_catalog.name}.otherschema", b.Config.Resources.QualityMonitors["monitor2"].OutputSchemaName) + assert.Equal(t, "othercatalog.otherschema", b.Config.Resources.QualityMonitors["monitor3"].OutputSchemaName) + assert.Equal(t, "", b.Config.Resources.QualityMonitors["monitor4"].OutputSchemaName) + assert.Equal(t, "nodot", b.Config.Resources.QualityMonitors["monitor5"].OutputSchemaName) + assert.Nil(t, b.Config.Resources.QualityMonitors["nilMonitor"]) +} + +func TestCaptureImplicitDependencyForModelServingEndpoint(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Catalogs: map[string]*resources.Catalog{ + "my_catalog": { + CreateCatalog: catalog.CreateCatalog{ + Name: "mycatalog", + }, + }, + }, + Schemas: map[string]*resources.Schema{ + "my_schema": { + CreateSchema: catalog.CreateSchema{ + CatalogName: "mycatalog", + Name: "myschema", + }, + }, + }, + ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{ + // AiGateway inference table config with matching catalog+schema. + "endpoint1": { + CreateServingEndpoint: serving.CreateServingEndpoint{ + AiGateway: &serving.AiGatewayConfig{ + InferenceTableConfig: &serving.AiGatewayInferenceTableConfig{ + CatalogName: "mycatalog", + SchemaName: "myschema", + }, + }, + }, + }, + // AutoCaptureConfig with matching catalog+schema. + "endpoint2": { + CreateServingEndpoint: serving.CreateServingEndpoint{ + Config: &serving.EndpointCoreConfigInput{ + AutoCaptureConfig: &serving.AutoCaptureConfigInput{ + CatalogName: "mycatalog", + SchemaName: "myschema", + }, + }, + }, + }, + // No matching catalog/schema. + "endpoint3": { + CreateServingEndpoint: serving.CreateServingEndpoint{ + AiGateway: &serving.AiGatewayConfig{ + InferenceTableConfig: &serving.AiGatewayInferenceTableConfig{ + CatalogName: "othercatalog", + SchemaName: "otherschema", + }, + }, + }, + }, + // Nil AiGateway and Config. + "endpoint4": { + CreateServingEndpoint: serving.CreateServingEndpoint{}, + }, + // AiGateway set but InferenceTableConfig is nil. + "endpoint5": { + CreateServingEndpoint: serving.CreateServingEndpoint{ + AiGateway: &serving.AiGatewayConfig{}, + }, + }, + "nilEndpoint": nil, + }, + }, + }, + } + + d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + require.Nil(t, d) + + // AiGateway inference table config resolved. + assert.Equal(t, "${resources.schemas.my_schema.name}", b.Config.Resources.ModelServingEndpoints["endpoint1"].AiGateway.InferenceTableConfig.SchemaName) + assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.ModelServingEndpoints["endpoint1"].AiGateway.InferenceTableConfig.CatalogName) + + // AutoCaptureConfig resolved. + assert.Equal(t, "${resources.schemas.my_schema.name}", b.Config.Resources.ModelServingEndpoints["endpoint2"].Config.AutoCaptureConfig.SchemaName) + assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.ModelServingEndpoints["endpoint2"].Config.AutoCaptureConfig.CatalogName) + + // No match, left unchanged. + assert.Equal(t, "othercatalog", b.Config.Resources.ModelServingEndpoints["endpoint3"].AiGateway.InferenceTableConfig.CatalogName) + assert.Equal(t, "otherschema", b.Config.Resources.ModelServingEndpoints["endpoint3"].AiGateway.InferenceTableConfig.SchemaName) + + assert.Nil(t, b.Config.Resources.ModelServingEndpoints["nilEndpoint"]) +} + func TestCaptureCatalogDependencyForSchema(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ diff --git a/bundle/config/mutator/resourcemutator/resource_mutator.go b/bundle/config/mutator/resourcemutator/resource_mutator.go index 009fd5af9d..f97bbc8ca9 100644 --- a/bundle/config/mutator/resourcemutator/resource_mutator.go +++ b/bundle/config/mutator/resourcemutator/resource_mutator.go @@ -171,9 +171,10 @@ func applyNormalizeMutators(ctx context.Context, b *bundle.Bundle) { // Updates (dynamic): same paths — merges grant entries by principal and deduplicates privileges MergeGrants(), - // Reads (typed): resources.pipelines.*.{catalog,schema,target}, resources.volumes.*.{catalog_name,schema_name}, resources.registered_models.*.{catalog_name,schema_name} - // Updates (typed): resources.pipelines.*.{schema,target}, resources.volumes.*.{catalog_name,schema_name}, resources.registered_models.*.{catalog_name,schema_name}, resources.schemas.*.catalog_name - // Translates implicit schema and catalog references in DLT pipelines, UC Volumes, and Registered Models to explicit syntax to capture dependencies + // Reads (typed): resources.{volumes,registered_models,pipelines,quality_monitors,model_serving_endpoints}.*.{catalog_name,schema_name,...} + // Updates (typed): same paths — converts implicit schema/catalog references to explicit ${resources.schemas/catalogs..name} syntax + // Also updates: resources.schemas.*.catalog_name (catalog dependency for schemas) + // Translates implicit schema and catalog references across all UC resources to explicit syntax to capture dependencies CaptureSchemaDependency(), // Reads (dynamic): resources.dashboards.*.file_path From fbf4ba4dadd1c09cfc7aae6b5f945b205443ca96 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 18:04:56 +0100 Subject: [PATCH 3/9] Rename CaptureSchemaDependency to ResolveUCReferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old name was too narrow — the mutator now resolves both schema and catalog references across all UC resource types, not just schema dependencies. Co-authored-by: Isaac --- ...dependency.go => resolve_uc_references.go} | 12 +++--- ..._test.go => resolve_uc_references_test.go} | 40 +++++++++---------- .../resourcemutator/resource_mutator.go | 2 +- 3 files changed, 27 insertions(+), 27 deletions(-) rename bundle/config/mutator/resourcemutator/{capture_schema_dependency.go => resolve_uc_references.go} (95%) rename bundle/config/mutator/resourcemutator/{capture_schema_dependency_test.go => resolve_uc_references_test.go} (94%) diff --git a/bundle/config/mutator/resourcemutator/capture_schema_dependency.go b/bundle/config/mutator/resourcemutator/resolve_uc_references.go similarity index 95% rename from bundle/config/mutator/resourcemutator/capture_schema_dependency.go rename to bundle/config/mutator/resourcemutator/resolve_uc_references.go index ca53ac16db..52aa8bfd80 100644 --- a/bundle/config/mutator/resourcemutator/capture_schema_dependency.go +++ b/bundle/config/mutator/resourcemutator/resolve_uc_references.go @@ -10,7 +10,7 @@ import ( "github.com/databricks/cli/libs/diag" ) -type captureSchemaDependency struct{} +type resolveUCReferences struct{} // If a user defines a UC schema in the bundle, they can refer to it in DLT pipelines, // UC Volumes, Registered Models, Quality Monitors, or Model Serving Endpoints using the @@ -24,12 +24,12 @@ type captureSchemaDependency struct{} // dependency the resource has on the catalog. // // This mutator translates any implicit catalog or schema references to the explicit syntax. -func CaptureSchemaDependency() bundle.Mutator { - return &captureSchemaDependency{} +func ResolveUCReferences() bundle.Mutator { + return &resolveUCReferences{} } -func (m *captureSchemaDependency) Name() string { - return "CaptureSchemaDependency" +func (m *resolveUCReferences) Name() string { + return "ResolveUCReferences" } func schemaNameRef(key string) string { @@ -208,7 +208,7 @@ func resolveSchema(s *resources.Schema, b *bundle.Bundle) { s.CatalogName = catalogNameRef(catalogK) } -func (m *captureSchemaDependency) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { +func (m *resolveUCReferences) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { // Resolve resources that depend on schemas before resolving schemas themselves. // resolveSchema modifies schema.CatalogName, and findSchema (used by the other // resolve functions) matches against the original schema.CatalogName value. diff --git a/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go b/bundle/config/mutator/resourcemutator/resolve_uc_references_test.go similarity index 94% rename from bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go rename to bundle/config/mutator/resourcemutator/resolve_uc_references_test.go index 01984dcfbb..72beaac40a 100644 --- a/bundle/config/mutator/resourcemutator/capture_schema_dependency_test.go +++ b/bundle/config/mutator/resourcemutator/resolve_uc_references_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCaptureSchemaDependencyForVolume(t *testing.T) { +func TestResolveUCReferencesForVolume(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -77,7 +77,7 @@ func TestCaptureSchemaDependencyForVolume(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Volumes["volume1"].SchemaName) @@ -90,7 +90,7 @@ func TestCaptureSchemaDependencyForVolume(t *testing.T) { // assert.Nil(t, b.Config.Resources.Volumes["emptyVolume"].CreateVolumeRequestContent) } -func TestCaptureSchemaDependencyForPipelinesWithTarget(t *testing.T) { +func TestResolveUCReferencesForPipelinesWithTarget(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -167,7 +167,7 @@ func TestCaptureSchemaDependencyForPipelinesWithTarget(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Pipelines["pipeline1"].Schema) @@ -186,7 +186,7 @@ func TestCaptureSchemaDependencyForPipelinesWithTarget(t *testing.T) { } } -func TestCaptureSchemaDependencyForPipelinesWithSchema(t *testing.T) { +func TestResolveUCReferencesForPipelinesWithSchema(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -261,7 +261,7 @@ func TestCaptureSchemaDependencyForPipelinesWithSchema(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Pipelines["pipeline1"].Target) assert.Equal(t, "${resources.schemas.schema2.name}", b.Config.Resources.Pipelines["pipeline2"].Target) @@ -276,7 +276,7 @@ func TestCaptureSchemaDependencyForPipelinesWithSchema(t *testing.T) { } } -func TestCaptureSchemaDependencyForRegisteredModel(t *testing.T) { +func TestResolveUCReferencesForRegisteredModel(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -340,7 +340,7 @@ func TestCaptureSchemaDependencyForRegisteredModel(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.RegisteredModels["model1"].SchemaName) @@ -352,7 +352,7 @@ func TestCaptureSchemaDependencyForRegisteredModel(t *testing.T) { assert.Nil(t, b.Config.Resources.RegisteredModels["nilModel"]) } -func TestCaptureCatalogDependencyForVolume(t *testing.T) { +func TestResolveUCReferencesForVolumeCatalog(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -402,7 +402,7 @@ func TestCaptureCatalogDependencyForVolume(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Volumes["volume1"].CatalogName) @@ -413,7 +413,7 @@ func TestCaptureCatalogDependencyForVolume(t *testing.T) { assert.Nil(t, b.Config.Resources.Volumes["nilVolume"]) } -func TestCaptureCatalogDependencyForRegisteredModel(t *testing.T) { +func TestResolveUCReferencesForRegisteredModelCatalog(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -463,7 +463,7 @@ func TestCaptureCatalogDependencyForRegisteredModel(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.RegisteredModels["model1"].CatalogName) @@ -479,7 +479,7 @@ func TestCaptureCatalogDependencyForRegisteredModel(t *testing.T) { // This verifies the ordering fix: schemas must be resolved last since resolveSchema // modifies schema.CatalogName, and findSchema (used by volume/model resolution) matches // against the original value. -func TestCaptureImplicitDependencyWithCatalogSchemaAndVolume(t *testing.T) { +func TestResolveUCReferencesWithCatalogSchemaAndVolume(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -518,7 +518,7 @@ func TestCaptureImplicitDependencyWithCatalogSchemaAndVolume(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) // Schema should have catalog dependency. @@ -533,7 +533,7 @@ func TestCaptureImplicitDependencyWithCatalogSchemaAndVolume(t *testing.T) { assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.RegisteredModels["my_model"].CatalogName) } -func TestCaptureImplicitDependencyForQualityMonitor(t *testing.T) { +func TestResolveUCReferencesForQualityMonitor(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -589,7 +589,7 @@ func TestCaptureImplicitDependencyForQualityMonitor(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) assert.Equal(t, "${resources.catalogs.my_catalog.name}.${resources.schemas.my_schema.name}", b.Config.Resources.QualityMonitors["monitor1"].OutputSchemaName) @@ -600,7 +600,7 @@ func TestCaptureImplicitDependencyForQualityMonitor(t *testing.T) { assert.Nil(t, b.Config.Resources.QualityMonitors["nilMonitor"]) } -func TestCaptureImplicitDependencyForModelServingEndpoint(t *testing.T) { +func TestResolveUCReferencesForModelServingEndpoint(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -669,7 +669,7 @@ func TestCaptureImplicitDependencyForModelServingEndpoint(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) // AiGateway inference table config resolved. @@ -687,7 +687,7 @@ func TestCaptureImplicitDependencyForModelServingEndpoint(t *testing.T) { assert.Nil(t, b.Config.Resources.ModelServingEndpoints["nilEndpoint"]) } -func TestCaptureCatalogDependencyForSchema(t *testing.T) { +func TestResolveUCReferencesForSchemaCatalog(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -737,7 +737,7 @@ func TestCaptureCatalogDependencyForSchema(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, CaptureSchemaDependency()) + d := bundle.Apply(t.Context(), b, ResolveUCReferences()) require.Nil(t, d) assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Schemas["schema1"].CatalogName) diff --git a/bundle/config/mutator/resourcemutator/resource_mutator.go b/bundle/config/mutator/resourcemutator/resource_mutator.go index f97bbc8ca9..03646b708a 100644 --- a/bundle/config/mutator/resourcemutator/resource_mutator.go +++ b/bundle/config/mutator/resourcemutator/resource_mutator.go @@ -175,7 +175,7 @@ func applyNormalizeMutators(ctx context.Context, b *bundle.Bundle) { // Updates (typed): same paths — converts implicit schema/catalog references to explicit ${resources.schemas/catalogs..name} syntax // Also updates: resources.schemas.*.catalog_name (catalog dependency for schemas) // Translates implicit schema and catalog references across all UC resources to explicit syntax to capture dependencies - CaptureSchemaDependency(), + ResolveUCReferences(), // Reads (dynamic): resources.dashboards.*.file_path // Updates (dynamic): resources.dashboards.*.serialized_dashboard From a634f9b36dfe0a6c80a6acdf3e25bedba7acb23f Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 18:11:31 +0100 Subject: [PATCH 4/9] Rename ResolveUCReferences to CaptureUCDependencies Co-authored-by: Isaac --- ...ferences.go => capture_uc_dependencies.go} | 12 +++--- ...est.go => capture_uc_dependencies_test.go} | 40 +++++++++---------- .../resourcemutator/resource_mutator.go | 2 +- 3 files changed, 27 insertions(+), 27 deletions(-) rename bundle/config/mutator/resourcemutator/{resolve_uc_references.go => capture_uc_dependencies.go} (95%) rename bundle/config/mutator/resourcemutator/{resolve_uc_references_test.go => capture_uc_dependencies_test.go} (94%) diff --git a/bundle/config/mutator/resourcemutator/resolve_uc_references.go b/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go similarity index 95% rename from bundle/config/mutator/resourcemutator/resolve_uc_references.go rename to bundle/config/mutator/resourcemutator/capture_uc_dependencies.go index 52aa8bfd80..ab38f7be04 100644 --- a/bundle/config/mutator/resourcemutator/resolve_uc_references.go +++ b/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go @@ -10,7 +10,7 @@ import ( "github.com/databricks/cli/libs/diag" ) -type resolveUCReferences struct{} +type captureUCDependencies struct{} // If a user defines a UC schema in the bundle, they can refer to it in DLT pipelines, // UC Volumes, Registered Models, Quality Monitors, or Model Serving Endpoints using the @@ -24,12 +24,12 @@ type resolveUCReferences struct{} // dependency the resource has on the catalog. // // This mutator translates any implicit catalog or schema references to the explicit syntax. -func ResolveUCReferences() bundle.Mutator { - return &resolveUCReferences{} +func CaptureUCDependencies() bundle.Mutator { + return &captureUCDependencies{} } -func (m *resolveUCReferences) Name() string { - return "ResolveUCReferences" +func (m *captureUCDependencies) Name() string { + return "CaptureUCDependencies" } func schemaNameRef(key string) string { @@ -208,7 +208,7 @@ func resolveSchema(s *resources.Schema, b *bundle.Bundle) { s.CatalogName = catalogNameRef(catalogK) } -func (m *resolveUCReferences) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { +func (m *captureUCDependencies) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { // Resolve resources that depend on schemas before resolving schemas themselves. // resolveSchema modifies schema.CatalogName, and findSchema (used by the other // resolve functions) matches against the original schema.CatalogName value. diff --git a/bundle/config/mutator/resourcemutator/resolve_uc_references_test.go b/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go similarity index 94% rename from bundle/config/mutator/resourcemutator/resolve_uc_references_test.go rename to bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go index 72beaac40a..996569906a 100644 --- a/bundle/config/mutator/resourcemutator/resolve_uc_references_test.go +++ b/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestResolveUCReferencesForVolume(t *testing.T) { +func TestCaptureUCDependenciesForVolume(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -77,7 +77,7 @@ func TestResolveUCReferencesForVolume(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Volumes["volume1"].SchemaName) @@ -90,7 +90,7 @@ func TestResolveUCReferencesForVolume(t *testing.T) { // assert.Nil(t, b.Config.Resources.Volumes["emptyVolume"].CreateVolumeRequestContent) } -func TestResolveUCReferencesForPipelinesWithTarget(t *testing.T) { +func TestCaptureUCDependenciesForPipelinesWithTarget(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -167,7 +167,7 @@ func TestResolveUCReferencesForPipelinesWithTarget(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Pipelines["pipeline1"].Schema) @@ -186,7 +186,7 @@ func TestResolveUCReferencesForPipelinesWithTarget(t *testing.T) { } } -func TestResolveUCReferencesForPipelinesWithSchema(t *testing.T) { +func TestCaptureUCDependenciesForPipelinesWithSchema(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -261,7 +261,7 @@ func TestResolveUCReferencesForPipelinesWithSchema(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Pipelines["pipeline1"].Target) assert.Equal(t, "${resources.schemas.schema2.name}", b.Config.Resources.Pipelines["pipeline2"].Target) @@ -276,7 +276,7 @@ func TestResolveUCReferencesForPipelinesWithSchema(t *testing.T) { } } -func TestResolveUCReferencesForRegisteredModel(t *testing.T) { +func TestCaptureUCDependenciesForRegisteredModel(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -340,7 +340,7 @@ func TestResolveUCReferencesForRegisteredModel(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.RegisteredModels["model1"].SchemaName) @@ -352,7 +352,7 @@ func TestResolveUCReferencesForRegisteredModel(t *testing.T) { assert.Nil(t, b.Config.Resources.RegisteredModels["nilModel"]) } -func TestResolveUCReferencesForVolumeCatalog(t *testing.T) { +func TestCaptureUCDependenciesForVolumeCatalog(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -402,7 +402,7 @@ func TestResolveUCReferencesForVolumeCatalog(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Volumes["volume1"].CatalogName) @@ -413,7 +413,7 @@ func TestResolveUCReferencesForVolumeCatalog(t *testing.T) { assert.Nil(t, b.Config.Resources.Volumes["nilVolume"]) } -func TestResolveUCReferencesForRegisteredModelCatalog(t *testing.T) { +func TestCaptureUCDependenciesForRegisteredModelCatalog(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -463,7 +463,7 @@ func TestResolveUCReferencesForRegisteredModelCatalog(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.RegisteredModels["model1"].CatalogName) @@ -479,7 +479,7 @@ func TestResolveUCReferencesForRegisteredModelCatalog(t *testing.T) { // This verifies the ordering fix: schemas must be resolved last since resolveSchema // modifies schema.CatalogName, and findSchema (used by volume/model resolution) matches // against the original value. -func TestResolveUCReferencesWithCatalogSchemaAndVolume(t *testing.T) { +func TestCaptureUCDependenciesWithCatalogSchemaAndVolume(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -518,7 +518,7 @@ func TestResolveUCReferencesWithCatalogSchemaAndVolume(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) // Schema should have catalog dependency. @@ -533,7 +533,7 @@ func TestResolveUCReferencesWithCatalogSchemaAndVolume(t *testing.T) { assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.RegisteredModels["my_model"].CatalogName) } -func TestResolveUCReferencesForQualityMonitor(t *testing.T) { +func TestCaptureUCDependenciesForQualityMonitor(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -589,7 +589,7 @@ func TestResolveUCReferencesForQualityMonitor(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) assert.Equal(t, "${resources.catalogs.my_catalog.name}.${resources.schemas.my_schema.name}", b.Config.Resources.QualityMonitors["monitor1"].OutputSchemaName) @@ -600,7 +600,7 @@ func TestResolveUCReferencesForQualityMonitor(t *testing.T) { assert.Nil(t, b.Config.Resources.QualityMonitors["nilMonitor"]) } -func TestResolveUCReferencesForModelServingEndpoint(t *testing.T) { +func TestCaptureUCDependenciesForModelServingEndpoint(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -669,7 +669,7 @@ func TestResolveUCReferencesForModelServingEndpoint(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) // AiGateway inference table config resolved. @@ -687,7 +687,7 @@ func TestResolveUCReferencesForModelServingEndpoint(t *testing.T) { assert.Nil(t, b.Config.Resources.ModelServingEndpoints["nilEndpoint"]) } -func TestResolveUCReferencesForSchemaCatalog(t *testing.T) { +func TestCaptureUCDependenciesForSchemaCatalog(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ @@ -737,7 +737,7 @@ func TestResolveUCReferencesForSchemaCatalog(t *testing.T) { }, } - d := bundle.Apply(t.Context(), b, ResolveUCReferences()) + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Schemas["schema1"].CatalogName) diff --git a/bundle/config/mutator/resourcemutator/resource_mutator.go b/bundle/config/mutator/resourcemutator/resource_mutator.go index 03646b708a..9616de202a 100644 --- a/bundle/config/mutator/resourcemutator/resource_mutator.go +++ b/bundle/config/mutator/resourcemutator/resource_mutator.go @@ -175,7 +175,7 @@ func applyNormalizeMutators(ctx context.Context, b *bundle.Bundle) { // Updates (typed): same paths — converts implicit schema/catalog references to explicit ${resources.schemas/catalogs..name} syntax // Also updates: resources.schemas.*.catalog_name (catalog dependency for schemas) // Translates implicit schema and catalog references across all UC resources to explicit syntax to capture dependencies - ResolveUCReferences(), + CaptureUCDependencies(), // Reads (dynamic): resources.dashboards.*.file_path // Updates (dynamic): resources.dashboards.*.serialized_dashboard From efa7e8d2d5d4e22a4c645d4ab593af08ec6c4bdc Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 18:12:59 +0100 Subject: [PATCH 5/9] Refactor: extract resolveSchema/resolveCatalog helpers, inline Apply - Extract resolveSchema() and resolveCatalog() helpers that return the resolved value (or the original if no match), eliminating duplicated find-then-set logic across every resource type. - Inline nil checks and field assignments directly in Apply() instead of separate per-resource functions, reducing indirection. - Group findSchema/findCatalog together with resolveSchema/resolveCatalog for better locality. Co-authored-by: Isaac --- .../capture_uc_dependencies.go | 215 ++++++------------ 1 file changed, 67 insertions(+), 148 deletions(-) diff --git a/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go b/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go index ab38f7be04..ba0e9eee0b 100644 --- a/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go +++ b/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go @@ -53,67 +53,6 @@ func findSchema(b *bundle.Bundle, catalogName, schemaName string) (string, *reso return "", nil } -func resolveVolume(v *resources.Volume, b *bundle.Bundle) { - if v == nil { - return - } - // Resolve schema first since findSchema needs the original v.CatalogName. - schemaK, schema := findSchema(b, v.CatalogName, v.SchemaName) - if schema != nil { - v.SchemaName = schemaNameRef(schemaK) - } - - catalogK, catalog := findCatalog(b, v.CatalogName) - if catalog != nil { - v.CatalogName = catalogNameRef(catalogK) - } -} - -func resolveRegisteredModel(rm *resources.RegisteredModel, b *bundle.Bundle) { - if rm == nil { - return - } - // Resolve schema first since findSchema needs the original rm.CatalogName. - schemaK, schema := findSchema(b, rm.CatalogName, rm.SchemaName) - if schema != nil { - rm.SchemaName = schemaNameRef(schemaK) - } - - catalogK, catalog := findCatalog(b, rm.CatalogName) - if catalog != nil { - rm.CatalogName = catalogNameRef(catalogK) - } -} - -func resolvePipelineSchema(p *resources.Pipeline, b *bundle.Bundle) { - if p == nil { - return - } - if p.Schema == "" { - return - } - schemaK, schema := findSchema(b, p.Catalog, p.Schema) - if schema == nil { - return - } - - p.Schema = schemaNameRef(schemaK) -} - -func resolvePipelineTarget(p *resources.Pipeline, b *bundle.Bundle) { - if p == nil { - return - } - if p.Target == "" { - return - } - schemaK, schema := findSchema(b, p.Catalog, p.Target) - if schema == nil { - return - } - p.Target = schemaNameRef(schemaK) -} - func findCatalog(b *bundle.Bundle, catalogName string) (string, *resources.Catalog) { if catalogName == "" { return "", nil @@ -127,113 +66,93 @@ func findCatalog(b *bundle.Bundle, catalogName string) (string, *resources.Catal return "", nil } -// resolveQualityMonitor resolves the OutputSchemaName field which is a compound -// "catalog.schema" string. -func resolveQualityMonitor(qm *resources.QualityMonitor, b *bundle.Bundle) { - if qm == nil { - return - } - if qm.OutputSchemaName == "" { - return - } - - parts := strings.SplitN(qm.OutputSchemaName, ".", 2) - if len(parts) != 2 { - return - } - catalogName, schemaName := parts[0], parts[1] - - resolvedCatalog, resolvedSchema := catalogName, schemaName - - schemaK, schema := findSchema(b, catalogName, schemaName) - if schema != nil { - resolvedSchema = schemaNameRef(schemaK) - } - - catalogK, catalog := findCatalog(b, catalogName) - if catalog != nil { - resolvedCatalog = catalogNameRef(catalogK) - } - - if resolvedCatalog != catalogName || resolvedSchema != schemaName { - qm.OutputSchemaName = resolvedCatalog + "." + resolvedSchema - } -} - -func resolveModelServingEndpoint(mse *resources.ModelServingEndpoint, b *bundle.Bundle) { - if mse == nil { - return - } - - // Resolve AiGateway.InferenceTableConfig. - if mse.AiGateway != nil && mse.AiGateway.InferenceTableConfig != nil { - itc := mse.AiGateway.InferenceTableConfig - - schemaK, schema := findSchema(b, itc.CatalogName, itc.SchemaName) - if schema != nil { - itc.SchemaName = schemaNameRef(schemaK) - } - - catalogK, catalog := findCatalog(b, itc.CatalogName) - if catalog != nil { - itc.CatalogName = catalogNameRef(catalogK) - } - } - - // Resolve Config.AutoCaptureConfig (deprecated but still in use). - if mse.Config != nil && mse.Config.AutoCaptureConfig != nil { - acc := mse.Config.AutoCaptureConfig - - schemaK, schema := findSchema(b, acc.CatalogName, acc.SchemaName) - if schema != nil { - acc.SchemaName = schemaNameRef(schemaK) - } - - catalogK, catalog := findCatalog(b, acc.CatalogName) - if catalog != nil { - acc.CatalogName = catalogNameRef(catalogK) - } +// resolveSchema returns the explicit schema reference if the given catalogName +// and schemaName match a schema defined in the bundle. Otherwise returns schemaName +// unchanged. Must be called before resolveCatalog on the same resource since +// findSchema needs the original (unmutated) catalogName. +func resolveSchema(b *bundle.Bundle, catalogName, schemaName string) string { + k, s := findSchema(b, catalogName, schemaName) + if s != nil { + return schemaNameRef(k) } + return schemaName } -func resolveSchema(s *resources.Schema, b *bundle.Bundle) { - if s == nil { - return +// resolveCatalog returns the explicit catalog reference if catalogName matches +// a catalog defined in the bundle. Otherwise returns catalogName unchanged. +func resolveCatalog(b *bundle.Bundle, catalogName string) string { + k, c := findCatalog(b, catalogName) + if c != nil { + return catalogNameRef(k) } - catalogK, catalog := findCatalog(b, s.CatalogName) - if catalog == nil { - return - } - - s.CatalogName = catalogNameRef(catalogK) + return catalogName } func (m *captureUCDependencies) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { // Resolve resources that depend on schemas before resolving schemas themselves. - // resolveSchema modifies schema.CatalogName, and findSchema (used by the other - // resolve functions) matches against the original schema.CatalogName value. + // The schema resolution below modifies schema.CatalogName, and findSchema + // (used by resolveSchema) matches against the original schema.CatalogName value. for _, v := range b.Config.Resources.Volumes { - resolveVolume(v, b) + if v == nil { + continue + } + v.SchemaName = resolveSchema(b, v.CatalogName, v.SchemaName) + v.CatalogName = resolveCatalog(b, v.CatalogName) } for _, rm := range b.Config.Resources.RegisteredModels { - resolveRegisteredModel(rm, b) + if rm == nil { + continue + } + rm.SchemaName = resolveSchema(b, rm.CatalogName, rm.SchemaName) + rm.CatalogName = resolveCatalog(b, rm.CatalogName) } for _, p := range b.Config.Resources.Pipelines { + if p == nil { + continue + } // "schema" and "target" have the same semantics in the DLT API but are mutually - // exclusive i.e. only one can be set at a time. If schema is set, the pipeline - // is in direct publishing mode and can write tables to multiple schemas - // (vs target which is limited to a single schema). - resolvePipelineTarget(p, b) - resolvePipelineSchema(p, b) + // exclusive i.e. only one can be set at a time. + p.Schema = resolveSchema(b, p.Catalog, p.Schema) + p.Target = resolveSchema(b, p.Catalog, p.Target) } for _, qm := range b.Config.Resources.QualityMonitors { - resolveQualityMonitor(qm, b) + if qm == nil || qm.OutputSchemaName == "" { + continue + } + // OutputSchemaName is a compound "catalog.schema" string. + parts := strings.SplitN(qm.OutputSchemaName, ".", 2) + if len(parts) != 2 { + continue + } + catalogName, schemaName := parts[0], parts[1] + resolved := resolveCatalog(b, catalogName) + "." + resolveSchema(b, catalogName, schemaName) + if resolved != qm.OutputSchemaName { + qm.OutputSchemaName = resolved + } } for _, mse := range b.Config.Resources.ModelServingEndpoints { - resolveModelServingEndpoint(mse, b) + if mse == nil { + continue + } + if mse.AiGateway != nil && mse.AiGateway.InferenceTableConfig != nil { + itc := mse.AiGateway.InferenceTableConfig + itc.SchemaName = resolveSchema(b, itc.CatalogName, itc.SchemaName) + itc.CatalogName = resolveCatalog(b, itc.CatalogName) + } + // AutoCaptureConfig is deprecated but still in use. + if mse.Config != nil && mse.Config.AutoCaptureConfig != nil { + acc := mse.Config.AutoCaptureConfig + acc.SchemaName = resolveSchema(b, acc.CatalogName, acc.SchemaName) + acc.CatalogName = resolveCatalog(b, acc.CatalogName) + } } + + // Schemas are resolved last. See comment at the top of Apply. for _, s := range b.Config.Resources.Schemas { - resolveSchema(s, b) + if s == nil { + continue + } + s.CatalogName = resolveCatalog(b, s.CatalogName) } return nil } From 30b11d9d3c8f3af7e64363a686c08748611c7475 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 18:22:07 +0100 Subject: [PATCH 6/9] Add implicit catalog dependency detection for DLT pipelines Pipelines have a Catalog field that was used to look up schemas but was never itself resolved to an explicit ${resources.catalogs..name} reference. Co-authored-by: Isaac --- .../capture_uc_dependencies.go | 5 ++- .../capture_uc_dependencies_test.go | 45 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go b/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go index ba0e9eee0b..92d22333e7 100644 --- a/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go +++ b/bundle/config/mutator/resourcemutator/capture_uc_dependencies.go @@ -114,6 +114,7 @@ func (m *captureUCDependencies) Apply(ctx context.Context, b *bundle.Bundle) dia // exclusive i.e. only one can be set at a time. p.Schema = resolveSchema(b, p.Catalog, p.Schema) p.Target = resolveSchema(b, p.Catalog, p.Target) + p.Catalog = resolveCatalog(b, p.Catalog) } for _, qm := range b.Config.Resources.QualityMonitors { if qm == nil || qm.OutputSchemaName == "" { @@ -147,7 +148,9 @@ func (m *captureUCDependencies) Apply(ctx context.Context, b *bundle.Bundle) dia } } - // Schemas are resolved last. See comment at the top of Apply. + // Schemas are resolved last because the schema catalog resolution modifies + // schema.CatalogName, and findSchema (used by resolveSchema above) matches + // against the original schema.CatalogName value. for _, s := range b.Config.Resources.Schemas { if s == nil { continue diff --git a/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go b/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go index 996569906a..34d4a04e61 100644 --- a/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go +++ b/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go @@ -276,6 +276,51 @@ func TestCaptureUCDependenciesForPipelinesWithSchema(t *testing.T) { } } +func TestCaptureUCDependenciesForPipelineCatalog(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Catalogs: map[string]*resources.Catalog{ + "catalog1": { + CreateCatalog: catalog.CreateCatalog{ + Name: "catalog1", + }, + }, + }, + Pipelines: map[string]*resources.Pipeline{ + "pipeline1": { + CreatePipeline: pipelines.CreatePipeline{ + Catalog: "catalog1", + Schema: "foobar", + }, + }, + "pipeline2": { + CreatePipeline: pipelines.CreatePipeline{ + Catalog: "catalogX", + Schema: "foobar", + }, + }, + "pipeline3": { + CreatePipeline: pipelines.CreatePipeline{ + Catalog: "", + Schema: "foobar", + }, + }, + "nilPipeline": nil, + }, + }, + }, + } + + d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) + require.Nil(t, d) + + assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Pipelines["pipeline1"].Catalog) + assert.Equal(t, "catalogX", b.Config.Resources.Pipelines["pipeline2"].Catalog) + assert.Equal(t, "", b.Config.Resources.Pipelines["pipeline3"].Catalog) + assert.Nil(t, b.Config.Resources.Pipelines["nilPipeline"]) +} + func TestCaptureUCDependenciesForRegisteredModel(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ From 9abc78401e3bb53b09f29e9d50ef41a185d99595 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 18:25:52 +0100 Subject: [PATCH 7/9] Remove unnecessary pipelines from volume and registered model acceptance tests Co-authored-by: Isaac --- .../implicit_deps_registered_model/databricks.yml | 3 --- .../implicit_deps_registered_model/output.txt | 15 --------------- .../implicit_deps_volume/databricks.yml | 3 --- .../resource_deps/implicit_deps_volume/output.txt | 15 --------------- 4 files changed, 36 deletions(-) diff --git a/acceptance/bundle/resource_deps/implicit_deps_registered_model/databricks.yml b/acceptance/bundle/resource_deps/implicit_deps_registered_model/databricks.yml index ece630fd2e..2224d140b2 100644 --- a/acceptance/bundle/resource_deps/implicit_deps_registered_model/databricks.yml +++ b/acceptance/bundle/resource_deps/implicit_deps_registered_model/databricks.yml @@ -17,9 +17,6 @@ resources: catalog_name: mycatalog schema_name: myschema name: mymodel - pipelines: - mypipeline: - name: pipeline for ${resources.registered_models.my_model.catalog_name}.${resources.registered_models.my_model.schema_name}.${resources.registered_models.my_model.name} targets: dev: diff --git a/acceptance/bundle/resource_deps/implicit_deps_registered_model/output.txt b/acceptance/bundle/resource_deps/implicit_deps_registered_model/output.txt index dc48ffa6b7..86d56ae426 100644 --- a/acceptance/bundle/resource_deps/implicit_deps_registered_model/output.txt +++ b/acceptance/bundle/resource_deps/implicit_deps_registered_model/output.txt @@ -6,21 +6,6 @@ "name": "mycatalog" } }, - "pipelines": { - "mypipeline": { - "channel": "CURRENT", - "deployment": { - "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/dev/state/metadata.json" - }, - "development": true, - "edition": "ADVANCED", - "name": "[dev [USERNAME]] pipeline for ${resources.registered_models.my_model.catalog_name}.${resources.registered_models.my_model.schema_name}.${resources.registered_models.my_model.name}", - "tags": { - "dev": "[USERNAME]" - } - } - }, "registered_models": { "my_model": { "catalog_name": "${resources.catalogs.my_catalog.name}", diff --git a/acceptance/bundle/resource_deps/implicit_deps_volume/databricks.yml b/acceptance/bundle/resource_deps/implicit_deps_volume/databricks.yml index 95bd981c54..6589107d7b 100644 --- a/acceptance/bundle/resource_deps/implicit_deps_volume/databricks.yml +++ b/acceptance/bundle/resource_deps/implicit_deps_volume/databricks.yml @@ -18,9 +18,6 @@ resources: schema_name: myschema name: myvolume volume_type: MANAGED - pipelines: - mypipeline: - name: pipeline for ${resources.volumes.my_volume.catalog_name}.${resources.volumes.my_volume.schema_name}.${resources.volumes.my_volume.name} targets: dev: diff --git a/acceptance/bundle/resource_deps/implicit_deps_volume/output.txt b/acceptance/bundle/resource_deps/implicit_deps_volume/output.txt index 2c936df73b..2e454e8152 100644 --- a/acceptance/bundle/resource_deps/implicit_deps_volume/output.txt +++ b/acceptance/bundle/resource_deps/implicit_deps_volume/output.txt @@ -6,21 +6,6 @@ "name": "mycatalog" } }, - "pipelines": { - "mypipeline": { - "channel": "CURRENT", - "deployment": { - "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/dev/state/metadata.json" - }, - "development": true, - "edition": "ADVANCED", - "name": "[dev [USERNAME]] pipeline for ${resources.volumes.my_volume.catalog_name}.${resources.volumes.my_volume.schema_name}.${resources.volumes.my_volume.name}", - "tags": { - "dev": "[USERNAME]" - } - } - }, "schemas": { "my_schema": { "catalog_name": "${resources.catalogs.my_catalog.name}", From 4ad29e379e3c2ce3a659ea78085bbc8d814bda39 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 18:30:58 +0100 Subject: [PATCH 8/9] Refactor tests to use table-driven style for readability Co-authored-by: Isaac --- .../capture_uc_dependencies_test.go | 821 ++++-------------- 1 file changed, 163 insertions(+), 658 deletions(-) diff --git a/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go b/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go index 34d4a04e61..12aac0c472 100644 --- a/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go +++ b/bundle/config/mutator/resourcemutator/capture_uc_dependencies_test.go @@ -13,435 +13,123 @@ import ( "github.com/stretchr/testify/require" ) -func TestCaptureUCDependenciesForVolume(t *testing.T) { - b := &bundle.Bundle{ +// Shared bundle with schemas for resolveSchema tests. +func bundleWithSchemas() *bundle.Bundle { + return &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ Schemas: map[string]*resources.Schema{ - "schema1": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "foobar", - }, - }, - "schema2": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog2", - Name: "foobar", - }, - }, - "schema3": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "barfoo", - }, - }, - "nilschema": nil, - "emptyschema": {}, - }, - Volumes: map[string]*resources.Volume{ - "volume1": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "catalog1", - SchemaName: "foobar", - }, - }, - "volume2": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "catalog2", - SchemaName: "foobar", - }, - }, - "volume3": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "catalog1", - SchemaName: "barfoo", - }, - }, - "volume4": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "catalogX", - SchemaName: "foobar", - }, - }, - "volume5": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "catalog1", - SchemaName: "schemaX", - }, - }, - "nilVolume": nil, - "emptyVolume": {}, + "schema1": {CreateSchema: catalog.CreateSchema{CatalogName: "catalog1", Name: "foobar"}}, + "schema2": {CreateSchema: catalog.CreateSchema{CatalogName: "catalog2", Name: "foobar"}}, + "schema3": {CreateSchema: catalog.CreateSchema{CatalogName: "catalog1", Name: "barfoo"}}, }, }, }, } - - d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) - require.Nil(t, d) - - assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Volumes["volume1"].SchemaName) - assert.Equal(t, "${resources.schemas.schema2.name}", b.Config.Resources.Volumes["volume2"].SchemaName) - assert.Equal(t, "${resources.schemas.schema3.name}", b.Config.Resources.Volumes["volume3"].SchemaName) - assert.Equal(t, "foobar", b.Config.Resources.Volumes["volume4"].SchemaName) - assert.Equal(t, "schemaX", b.Config.Resources.Volumes["volume5"].SchemaName) - - assert.Nil(t, b.Config.Resources.Volumes["nilVolume"]) - // assert.Nil(t, b.Config.Resources.Volumes["emptyVolume"].CreateVolumeRequestContent) } -func TestCaptureUCDependenciesForPipelinesWithTarget(t *testing.T) { - b := &bundle.Bundle{ +// Shared bundle with catalogs for resolveCatalog tests. +func bundleWithCatalogs() *bundle.Bundle { + return &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ - Schemas: map[string]*resources.Schema{ - "schema1": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "foobar", - }, - }, - "schema2": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog2", - Name: "foobar", - }, - }, - "schema3": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "barfoo", - }, - }, - "nilschema": nil, - "emptyschema": {}, - }, - Pipelines: map[string]*resources.Pipeline{ - "pipeline1": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog1", - Schema: "foobar", - }, - }, - "pipeline2": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog2", - Schema: "foobar", - }, - }, - "pipeline3": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog1", - Schema: "barfoo", - }, - }, - "pipeline4": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalogX", - Schema: "foobar", - }, - }, - "pipeline5": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog1", - Schema: "schemaX", - }, - }, - "pipeline6": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "", - Schema: "foobar", - }, - }, - "pipeline7": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "", - Schema: "", - Name: "whatever", - }, - }, - "nilPipeline": nil, - "emptyPipeline": {}, + Catalogs: map[string]*resources.Catalog{ + "dev_catalog": {CreateCatalog: catalog.CreateCatalog{Name: "catalog1"}}, + "prod_catalog": {CreateCatalog: catalog.CreateCatalog{Name: "catalog2"}}, }, }, }, } +} - d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) - require.Nil(t, d) - - assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Pipelines["pipeline1"].Schema) - assert.Equal(t, "${resources.schemas.schema2.name}", b.Config.Resources.Pipelines["pipeline2"].Schema) - assert.Equal(t, "${resources.schemas.schema3.name}", b.Config.Resources.Pipelines["pipeline3"].Schema) - assert.Equal(t, "foobar", b.Config.Resources.Pipelines["pipeline4"].Schema) - assert.Equal(t, "schemaX", b.Config.Resources.Pipelines["pipeline5"].Schema) - assert.Equal(t, "foobar", b.Config.Resources.Pipelines["pipeline6"].Schema) - assert.Equal(t, "", b.Config.Resources.Pipelines["pipeline7"].Schema) - - assert.Nil(t, b.Config.Resources.Pipelines["nilPipeline"]) - assert.Empty(t, b.Config.Resources.Pipelines["emptyPipeline"].Catalog) +func TestResolveSchema(t *testing.T) { + b := bundleWithSchemas() + + tests := []struct { + name string + catalogName string + schemaName string + expected string + }{ + {"match_catalog1_foobar", "catalog1", "foobar", "${resources.schemas.schema1.name}"}, + {"match_catalog2_foobar", "catalog2", "foobar", "${resources.schemas.schema2.name}"}, + {"match_catalog1_barfoo", "catalog1", "barfoo", "${resources.schemas.schema3.name}"}, + {"no_match_wrong_catalog", "catalogX", "foobar", "foobar"}, + {"no_match_wrong_schema", "catalog1", "schemaX", "schemaX"}, + {"empty_catalog", "", "foobar", "foobar"}, + {"empty_schema", "catalog1", "", ""}, + {"both_empty", "", "", ""}, + } - for _, k := range []string{"pipeline1", "pipeline2", "pipeline3", "pipeline4", "pipeline5", "pipeline6", "pipeline7"} { - assert.Empty(t, b.Config.Resources.Pipelines[k].Target) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, resolveSchema(b, tc.catalogName, tc.schemaName)) + }) } } -func TestCaptureUCDependenciesForPipelinesWithSchema(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Schemas: map[string]*resources.Schema{ - "schema1": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "foobar", - }, - }, - "schema2": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog2", - Name: "foobar", - }, - }, - "schema3": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "barfoo", - }, - }, - "nilschema": nil, - "emptyschema": {}, - }, - Pipelines: map[string]*resources.Pipeline{ - "pipeline1": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog1", - Target: "foobar", - }, - }, - "pipeline2": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog2", - Target: "foobar", - }, - }, - "pipeline3": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog1", - Target: "barfoo", - }, - }, - "pipeline4": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalogX", - Target: "foobar", - }, - }, - "pipeline5": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog1", - Target: "schemaX", - }, - }, - "pipeline6": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "", - Target: "foobar", - }, - }, - "pipeline7": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "", - Target: "", - Name: "whatever", - }, - }, - }, - }, - }, +func TestResolveCatalog(t *testing.T) { + b := bundleWithCatalogs() + + tests := []struct { + name string + catalogName string + expected string + }{ + {"match_catalog1", "catalog1", "${resources.catalogs.dev_catalog.name}"}, + {"match_catalog2", "catalog2", "${resources.catalogs.prod_catalog.name}"}, + {"no_match", "catalogX", "catalogX"}, + {"empty", "", ""}, } - d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) - require.Nil(t, d) - assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.Pipelines["pipeline1"].Target) - assert.Equal(t, "${resources.schemas.schema2.name}", b.Config.Resources.Pipelines["pipeline2"].Target) - assert.Equal(t, "${resources.schemas.schema3.name}", b.Config.Resources.Pipelines["pipeline3"].Target) - assert.Equal(t, "foobar", b.Config.Resources.Pipelines["pipeline4"].Target) - assert.Equal(t, "schemaX", b.Config.Resources.Pipelines["pipeline5"].Target) - assert.Equal(t, "foobar", b.Config.Resources.Pipelines["pipeline6"].Target) - assert.Equal(t, "", b.Config.Resources.Pipelines["pipeline7"].Target) - - for _, k := range []string{"pipeline1", "pipeline2", "pipeline3", "pipeline4", "pipeline5", "pipeline6", "pipeline7"} { - assert.Empty(t, b.Config.Resources.Pipelines[k].Schema) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, resolveCatalog(b, tc.catalogName)) + }) } } -func TestCaptureUCDependenciesForPipelineCatalog(t *testing.T) { +// Test that all resource types are wired correctly by defining a catalog, schema, +// and one of each resource type in a single bundle. Also verifies the ordering fix: +// schemas must be resolved last since their CatalogName gets mutated. +func TestCaptureUCDependencies(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ Catalogs: map[string]*resources.Catalog{ - "catalog1": { - CreateCatalog: catalog.CreateCatalog{ - Name: "catalog1", - }, - }, + "my_catalog": {CreateCatalog: catalog.CreateCatalog{Name: "mycatalog"}}, }, - Pipelines: map[string]*resources.Pipeline{ - "pipeline1": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalog1", - Schema: "foobar", - }, - }, - "pipeline2": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "catalogX", - Schema: "foobar", - }, - }, - "pipeline3": { - CreatePipeline: pipelines.CreatePipeline{ - Catalog: "", - Schema: "foobar", - }, - }, - "nilPipeline": nil, - }, - }, - }, - } - - d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) - require.Nil(t, d) - - assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Pipelines["pipeline1"].Catalog) - assert.Equal(t, "catalogX", b.Config.Resources.Pipelines["pipeline2"].Catalog) - assert.Equal(t, "", b.Config.Resources.Pipelines["pipeline3"].Catalog) - assert.Nil(t, b.Config.Resources.Pipelines["nilPipeline"]) -} - -func TestCaptureUCDependenciesForRegisteredModel(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ Schemas: map[string]*resources.Schema{ - "schema1": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "foobar", - }, - }, - "schema2": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog2", - Name: "foobar", - }, - }, - "schema3": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "barfoo", - }, - }, - "nilschema": nil, - "emptyschema": {}, + "my_schema": {CreateSchema: catalog.CreateSchema{CatalogName: "mycatalog", Name: "myschema"}}, + }, + Volumes: map[string]*resources.Volume{ + "my_volume": {CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ + CatalogName: "mycatalog", SchemaName: "myschema", + }}, }, RegisteredModels: map[string]*resources.RegisteredModel{ - "model1": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "catalog1", - SchemaName: "foobar", - }, - }, - "model2": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "catalog2", - SchemaName: "foobar", - }, - }, - "model3": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "catalog1", - SchemaName: "barfoo", - }, - }, - "model4": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "catalogX", - SchemaName: "foobar", - }, - }, - "model5": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "catalog1", - SchemaName: "schemaX", - }, - }, - "nilModel": nil, - "emptyModel": {}, + "my_model": {CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + CatalogName: "mycatalog", SchemaName: "myschema", + }}, }, - }, - }, - } - - d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) - require.Nil(t, d) - - assert.Equal(t, "${resources.schemas.schema1.name}", b.Config.Resources.RegisteredModels["model1"].SchemaName) - assert.Equal(t, "${resources.schemas.schema2.name}", b.Config.Resources.RegisteredModels["model2"].SchemaName) - assert.Equal(t, "${resources.schemas.schema3.name}", b.Config.Resources.RegisteredModels["model3"].SchemaName) - assert.Equal(t, "foobar", b.Config.Resources.RegisteredModels["model4"].SchemaName) - assert.Equal(t, "schemaX", b.Config.Resources.RegisteredModels["model5"].SchemaName) - - assert.Nil(t, b.Config.Resources.RegisteredModels["nilModel"]) -} - -func TestCaptureUCDependenciesForVolumeCatalog(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Catalogs: map[string]*resources.Catalog{ - "catalog1": { - CreateCatalog: catalog.CreateCatalog{ - Name: "catalog1", - }, - }, - "catalog2": { - CreateCatalog: catalog.CreateCatalog{ - Name: "catalog2", - }, - }, - "nilcatalog": nil, - "emptycatalog": {}, + Pipelines: map[string]*resources.Pipeline{ + "my_pipeline": {CreatePipeline: pipelines.CreatePipeline{ + Catalog: "mycatalog", Schema: "myschema", + }}, }, - Volumes: map[string]*resources.Volume{ - "volume1": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "catalog1", - SchemaName: "foobar", - }, - }, - "volume2": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "catalog2", - SchemaName: "foobar", - }, - }, - "volume3": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "catalogX", - SchemaName: "foobar", - }, - }, - "volume4": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "", - SchemaName: "foobar", + QualityMonitors: map[string]*resources.QualityMonitor{ + "my_monitor": {CreateMonitor: catalog.CreateMonitor{ + OutputSchemaName: "mycatalog.myschema", + }}, + }, + ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{ + "my_endpoint": {CreateServingEndpoint: serving.CreateServingEndpoint{ + AiGateway: &serving.AiGatewayConfig{ + InferenceTableConfig: &serving.AiGatewayInferenceTableConfig{ + CatalogName: "mycatalog", SchemaName: "myschema", + }, }, - }, - "nilVolume": nil, - "emptyVolume": {}, + }}, }, }, }, @@ -450,114 +138,45 @@ func TestCaptureUCDependenciesForVolumeCatalog(t *testing.T) { d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) - assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Volumes["volume1"].CatalogName) - assert.Equal(t, "${resources.catalogs.catalog2.name}", b.Config.Resources.Volumes["volume2"].CatalogName) - assert.Equal(t, "catalogX", b.Config.Resources.Volumes["volume3"].CatalogName) - assert.Equal(t, "", b.Config.Resources.Volumes["volume4"].CatalogName) + schemaRef := "${resources.schemas.my_schema.name}" + catalogRef := "${resources.catalogs.my_catalog.name}" - assert.Nil(t, b.Config.Resources.Volumes["nilVolume"]) -} + // Schema catalog dependency. + assert.Equal(t, catalogRef, b.Config.Resources.Schemas["my_schema"].CatalogName) -func TestCaptureUCDependenciesForRegisteredModelCatalog(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Catalogs: map[string]*resources.Catalog{ - "catalog1": { - CreateCatalog: catalog.CreateCatalog{ - Name: "catalog1", - }, - }, - "catalog2": { - CreateCatalog: catalog.CreateCatalog{ - Name: "catalog2", - }, - }, - "nilcatalog": nil, - "emptycatalog": {}, - }, - RegisteredModels: map[string]*resources.RegisteredModel{ - "model1": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "catalog1", - SchemaName: "foobar", - }, - }, - "model2": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "catalog2", - SchemaName: "foobar", - }, - }, - "model3": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "catalogX", - SchemaName: "foobar", - }, - }, - "model4": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "", - SchemaName: "foobar", - }, - }, - "nilModel": nil, - "emptyModel": {}, - }, - }, - }, - } + // Volume. + assert.Equal(t, schemaRef, b.Config.Resources.Volumes["my_volume"].SchemaName) + assert.Equal(t, catalogRef, b.Config.Resources.Volumes["my_volume"].CatalogName) - d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) - require.Nil(t, d) + // Registered model. + assert.Equal(t, schemaRef, b.Config.Resources.RegisteredModels["my_model"].SchemaName) + assert.Equal(t, catalogRef, b.Config.Resources.RegisteredModels["my_model"].CatalogName) - assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.RegisteredModels["model1"].CatalogName) - assert.Equal(t, "${resources.catalogs.catalog2.name}", b.Config.Resources.RegisteredModels["model2"].CatalogName) - assert.Equal(t, "catalogX", b.Config.Resources.RegisteredModels["model3"].CatalogName) - assert.Equal(t, "", b.Config.Resources.RegisteredModels["model4"].CatalogName) + // Pipeline. + assert.Equal(t, schemaRef, b.Config.Resources.Pipelines["my_pipeline"].Schema) + assert.Equal(t, catalogRef, b.Config.Resources.Pipelines["my_pipeline"].Catalog) - assert.Nil(t, b.Config.Resources.RegisteredModels["nilModel"]) + // Quality monitor (compound "catalog.schema" field). + assert.Equal(t, catalogRef+"."+schemaRef, b.Config.Resources.QualityMonitors["my_monitor"].OutputSchemaName) + + // Model serving endpoint. + itc := b.Config.Resources.ModelServingEndpoints["my_endpoint"].AiGateway.InferenceTableConfig + assert.Equal(t, schemaRef, itc.SchemaName) + assert.Equal(t, catalogRef, itc.CatalogName) } -// Test that when a catalog, schema, and dependent resources (volume, registered model) -// are all defined in the same bundle, all implicit dependencies are correctly captured. -// This verifies the ordering fix: schemas must be resolved last since resolveSchema -// modifies schema.CatalogName, and findSchema (used by volume/model resolution) matches -// against the original value. -func TestCaptureUCDependenciesWithCatalogSchemaAndVolume(t *testing.T) { +// Pipeline schema and target are mutually exclusive; only the populated field +// should be resolved. +func TestCaptureUCDependenciesPipelineSchemaTarget(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ - Catalogs: map[string]*resources.Catalog{ - "my_catalog": { - CreateCatalog: catalog.CreateCatalog{ - Name: "mycatalog", - }, - }, - }, Schemas: map[string]*resources.Schema{ - "my_schema": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "mycatalog", - Name: "myschema", - }, - }, + "s": {CreateSchema: catalog.CreateSchema{CatalogName: "c", Name: "n"}}, }, - Volumes: map[string]*resources.Volume{ - "my_volume": { - CreateVolumeRequestContent: catalog.CreateVolumeRequestContent{ - CatalogName: "mycatalog", - SchemaName: "myschema", - }, - }, - }, - RegisteredModels: map[string]*resources.RegisteredModel{ - "my_model": { - CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ - CatalogName: "mycatalog", - SchemaName: "myschema", - }, - }, + Pipelines: map[string]*resources.Pipeline{ + "with_schema": {CreatePipeline: pipelines.CreatePipeline{Catalog: "c", Schema: "n"}}, + "with_target": {CreatePipeline: pipelines.CreatePipeline{Catalog: "c", Target: "n"}}, }, }, }, @@ -566,69 +185,31 @@ func TestCaptureUCDependenciesWithCatalogSchemaAndVolume(t *testing.T) { d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) - // Schema should have catalog dependency. - assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.Schemas["my_schema"].CatalogName) + ref := "${resources.schemas.s.name}" - // Volume should have both schema and catalog dependencies. - assert.Equal(t, "${resources.schemas.my_schema.name}", b.Config.Resources.Volumes["my_volume"].SchemaName) - assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.Volumes["my_volume"].CatalogName) + assert.Equal(t, ref, b.Config.Resources.Pipelines["with_schema"].Schema) + assert.Empty(t, b.Config.Resources.Pipelines["with_schema"].Target) - // Registered model should have both schema and catalog dependencies. - assert.Equal(t, "${resources.schemas.my_schema.name}", b.Config.Resources.RegisteredModels["my_model"].SchemaName) - assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.RegisteredModels["my_model"].CatalogName) + assert.Equal(t, ref, b.Config.Resources.Pipelines["with_target"].Target) + assert.Empty(t, b.Config.Resources.Pipelines["with_target"].Schema) } -func TestCaptureUCDependenciesForQualityMonitor(t *testing.T) { +func TestCaptureUCDependenciesQualityMonitorEdgeCases(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ Catalogs: map[string]*resources.Catalog{ - "my_catalog": { - CreateCatalog: catalog.CreateCatalog{ - Name: "mycatalog", - }, - }, + "my_catalog": {CreateCatalog: catalog.CreateCatalog{Name: "mycatalog"}}, }, Schemas: map[string]*resources.Schema{ - "my_schema": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "mycatalog", - Name: "myschema", - }, - }, + "my_schema": {CreateSchema: catalog.CreateSchema{CatalogName: "mycatalog", Name: "myschema"}}, }, QualityMonitors: map[string]*resources.QualityMonitor{ - // Both catalog and schema match. - "monitor1": { - CreateMonitor: catalog.CreateMonitor{ - OutputSchemaName: "mycatalog.myschema", - }, - }, - // Only catalog matches (schema not in bundle). - "monitor2": { - CreateMonitor: catalog.CreateMonitor{ - OutputSchemaName: "mycatalog.otherschema", - }, - }, - // Neither matches. - "monitor3": { - CreateMonitor: catalog.CreateMonitor{ - OutputSchemaName: "othercatalog.otherschema", - }, - }, - // Empty output schema. - "monitor4": { - CreateMonitor: catalog.CreateMonitor{ - OutputSchemaName: "", - }, - }, - // No dot separator (invalid format, should be left alone). - "monitor5": { - CreateMonitor: catalog.CreateMonitor{ - OutputSchemaName: "nodot", - }, - }, - "nilMonitor": nil, + "catalog_only": {CreateMonitor: catalog.CreateMonitor{OutputSchemaName: "mycatalog.other"}}, + "no_match": {CreateMonitor: catalog.CreateMonitor{OutputSchemaName: "other.other"}}, + "empty": {CreateMonitor: catalog.CreateMonitor{OutputSchemaName: ""}}, + "no_dot": {CreateMonitor: catalog.CreateMonitor{OutputSchemaName: "nodot"}}, + "nil_monitor": nil, }, }, }, @@ -637,78 +218,44 @@ func TestCaptureUCDependenciesForQualityMonitor(t *testing.T) { d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) - assert.Equal(t, "${resources.catalogs.my_catalog.name}.${resources.schemas.my_schema.name}", b.Config.Resources.QualityMonitors["monitor1"].OutputSchemaName) - assert.Equal(t, "${resources.catalogs.my_catalog.name}.otherschema", b.Config.Resources.QualityMonitors["monitor2"].OutputSchemaName) - assert.Equal(t, "othercatalog.otherschema", b.Config.Resources.QualityMonitors["monitor3"].OutputSchemaName) - assert.Equal(t, "", b.Config.Resources.QualityMonitors["monitor4"].OutputSchemaName) - assert.Equal(t, "nodot", b.Config.Resources.QualityMonitors["monitor5"].OutputSchemaName) - assert.Nil(t, b.Config.Resources.QualityMonitors["nilMonitor"]) + assert.Equal(t, "${resources.catalogs.my_catalog.name}.other", b.Config.Resources.QualityMonitors["catalog_only"].OutputSchemaName) + assert.Equal(t, "other.other", b.Config.Resources.QualityMonitors["no_match"].OutputSchemaName) + assert.Equal(t, "", b.Config.Resources.QualityMonitors["empty"].OutputSchemaName) + assert.Equal(t, "nodot", b.Config.Resources.QualityMonitors["no_dot"].OutputSchemaName) + assert.Nil(t, b.Config.Resources.QualityMonitors["nil_monitor"]) } -func TestCaptureUCDependenciesForModelServingEndpoint(t *testing.T) { +func TestCaptureUCDependenciesModelServingEndpointEdgeCases(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ Catalogs: map[string]*resources.Catalog{ - "my_catalog": { - CreateCatalog: catalog.CreateCatalog{ - Name: "mycatalog", - }, - }, + "my_catalog": {CreateCatalog: catalog.CreateCatalog{Name: "mycatalog"}}, }, Schemas: map[string]*resources.Schema{ - "my_schema": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "mycatalog", - Name: "myschema", - }, - }, + "my_schema": {CreateSchema: catalog.CreateSchema{CatalogName: "mycatalog", Name: "myschema"}}, }, ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{ - // AiGateway inference table config with matching catalog+schema. - "endpoint1": { - CreateServingEndpoint: serving.CreateServingEndpoint{ - AiGateway: &serving.AiGatewayConfig{ - InferenceTableConfig: &serving.AiGatewayInferenceTableConfig{ - CatalogName: "mycatalog", - SchemaName: "myschema", - }, + // AutoCaptureConfig path. + "auto_capture": {CreateServingEndpoint: serving.CreateServingEndpoint{ + Config: &serving.EndpointCoreConfigInput{ + AutoCaptureConfig: &serving.AutoCaptureConfigInput{ + CatalogName: "mycatalog", SchemaName: "myschema", }, }, - }, - // AutoCaptureConfig with matching catalog+schema. - "endpoint2": { - CreateServingEndpoint: serving.CreateServingEndpoint{ - Config: &serving.EndpointCoreConfigInput{ - AutoCaptureConfig: &serving.AutoCaptureConfigInput{ - CatalogName: "mycatalog", - SchemaName: "myschema", - }, + }}, + // No match. + "no_match": {CreateServingEndpoint: serving.CreateServingEndpoint{ + AiGateway: &serving.AiGatewayConfig{ + InferenceTableConfig: &serving.AiGatewayInferenceTableConfig{ + CatalogName: "other", SchemaName: "other", }, }, - }, - // No matching catalog/schema. - "endpoint3": { - CreateServingEndpoint: serving.CreateServingEndpoint{ - AiGateway: &serving.AiGatewayConfig{ - InferenceTableConfig: &serving.AiGatewayInferenceTableConfig{ - CatalogName: "othercatalog", - SchemaName: "otherschema", - }, - }, - }, - }, - // Nil AiGateway and Config. - "endpoint4": { - CreateServingEndpoint: serving.CreateServingEndpoint{}, - }, - // AiGateway set but InferenceTableConfig is nil. - "endpoint5": { - CreateServingEndpoint: serving.CreateServingEndpoint{ - AiGateway: &serving.AiGatewayConfig{}, - }, - }, - "nilEndpoint": nil, + }}, + // Various nil nesting levels. + "nil_gateway": {CreateServingEndpoint: serving.CreateServingEndpoint{}}, + "nil_inference_table": {CreateServingEndpoint: serving.CreateServingEndpoint{AiGateway: &serving.AiGatewayConfig{}}}, + "nil_endpoint": nil, }, }, }, @@ -717,78 +264,36 @@ func TestCaptureUCDependenciesForModelServingEndpoint(t *testing.T) { d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) - // AiGateway inference table config resolved. - assert.Equal(t, "${resources.schemas.my_schema.name}", b.Config.Resources.ModelServingEndpoints["endpoint1"].AiGateway.InferenceTableConfig.SchemaName) - assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.ModelServingEndpoints["endpoint1"].AiGateway.InferenceTableConfig.CatalogName) + schemaRef := "${resources.schemas.my_schema.name}" + catalogRef := "${resources.catalogs.my_catalog.name}" - // AutoCaptureConfig resolved. - assert.Equal(t, "${resources.schemas.my_schema.name}", b.Config.Resources.ModelServingEndpoints["endpoint2"].Config.AutoCaptureConfig.SchemaName) - assert.Equal(t, "${resources.catalogs.my_catalog.name}", b.Config.Resources.ModelServingEndpoints["endpoint2"].Config.AutoCaptureConfig.CatalogName) + acc := b.Config.Resources.ModelServingEndpoints["auto_capture"].Config.AutoCaptureConfig + assert.Equal(t, schemaRef, acc.SchemaName) + assert.Equal(t, catalogRef, acc.CatalogName) - // No match, left unchanged. - assert.Equal(t, "othercatalog", b.Config.Resources.ModelServingEndpoints["endpoint3"].AiGateway.InferenceTableConfig.CatalogName) - assert.Equal(t, "otherschema", b.Config.Resources.ModelServingEndpoints["endpoint3"].AiGateway.InferenceTableConfig.SchemaName) + itc := b.Config.Resources.ModelServingEndpoints["no_match"].AiGateway.InferenceTableConfig + assert.Equal(t, "other", itc.CatalogName) + assert.Equal(t, "other", itc.SchemaName) - assert.Nil(t, b.Config.Resources.ModelServingEndpoints["nilEndpoint"]) + assert.Nil(t, b.Config.Resources.ModelServingEndpoints["nil_endpoint"]) } -func TestCaptureUCDependenciesForSchemaCatalog(t *testing.T) { +// Nil and empty resources should not panic. +func TestCaptureUCDependenciesNilResources(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Resources: config.Resources{ - Catalogs: map[string]*resources.Catalog{ - "catalog1": { - CreateCatalog: catalog.CreateCatalog{ - Name: "catalog1", - }, - }, - "catalog2": { - CreateCatalog: catalog.CreateCatalog{ - Name: "catalog2", - }, - }, - "nilcatalog": nil, - "emptycatalog": {}, - }, - Schemas: map[string]*resources.Schema{ - "schema1": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog1", - Name: "schema1", - }, - }, - "schema2": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalog2", - Name: "schema2", - }, - }, - "schema3": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "catalogX", - Name: "schema3", - }, - }, - "schema4": { - CreateSchema: catalog.CreateSchema{ - CatalogName: "", - Name: "schema4", - }, - }, - "nilschema": nil, - "emptyschema": {}, - }, + Schemas: map[string]*resources.Schema{"nil": nil, "empty": {}}, + Catalogs: map[string]*resources.Catalog{"nil": nil, "empty": {}}, + Volumes: map[string]*resources.Volume{"nil": nil, "empty": {}}, + RegisteredModels: map[string]*resources.RegisteredModel{"nil": nil, "empty": {}}, + Pipelines: map[string]*resources.Pipeline{"nil": nil, "empty": {}}, + QualityMonitors: map[string]*resources.QualityMonitor{"nil": nil, "empty": {}}, + ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{"nil": nil, "empty": {}}, }, }, } d := bundle.Apply(t.Context(), b, CaptureUCDependencies()) require.Nil(t, d) - - assert.Equal(t, "${resources.catalogs.catalog1.name}", b.Config.Resources.Schemas["schema1"].CatalogName) - assert.Equal(t, "${resources.catalogs.catalog2.name}", b.Config.Resources.Schemas["schema2"].CatalogName) - assert.Equal(t, "catalogX", b.Config.Resources.Schemas["schema3"].CatalogName) - assert.Equal(t, "", b.Config.Resources.Schemas["schema4"].CatalogName) - - assert.Nil(t, b.Config.Resources.Schemas["nilschema"]) } From 29883392d1cdaf55ef2c5b7b57fed54a5e6b90fc Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 26 Mar 2026 18:58:23 +0100 Subject: [PATCH 9/9] Fix catalog-var-ref acceptance test: volume now has depends_on The CaptureUCDependencies mutator correctly resolves volume catalog and schema references, fixing the previously tracked Badness. Co-authored-by: Isaac --- .../resources/volumes/catalog-var-ref/output.txt | 10 ++++++++++ .../bundle/resources/volumes/catalog-var-ref/test.toml | 2 -- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/acceptance/bundle/resources/volumes/catalog-var-ref/output.txt b/acceptance/bundle/resources/volumes/catalog-var-ref/output.txt index 56e088656d..e16db7ed26 100644 --- a/acceptance/bundle/resources/volumes/catalog-var-ref/output.txt +++ b/acceptance/bundle/resources/volumes/catalog-var-ref/output.txt @@ -28,6 +28,16 @@ } }, "resources.volumes.metadata": { + "depends_on": [ + { + "node": "resources.catalogs.main_catalog", + "label": "${resources.catalogs.main_catalog.name}" + }, + { + "node": "resources.schemas.raw_schema", + "label": "${resources.schemas.raw_schema.name}" + } + ], "action": "create", "new_state": { "value": { diff --git a/acceptance/bundle/resources/volumes/catalog-var-ref/test.toml b/acceptance/bundle/resources/volumes/catalog-var-ref/test.toml index 63b6228ab7..613ff598f6 100644 --- a/acceptance/bundle/resources/volumes/catalog-var-ref/test.toml +++ b/acceptance/bundle/resources/volumes/catalog-var-ref/test.toml @@ -1,5 +1,3 @@ -Badness = "No depends_on for volume resource" - Local = true Cloud = false RecordRequests = false