From f99564c63a9ed761efc0f9f03eab7f40d1d060f8 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 04:00:48 +0200 Subject: [PATCH 01/10] [wip] add support for registered_models for direct deployment --- .../basic/databricks.yml.tmpl | 11 +++ .../registered_models/basic/out.test.toml | 5 ++ .../deploy/registered_models/basic/output.txt | 31 +++++++ .../deploy/registered_models/basic/script | 12 +++ .../deploy/registered_models/basic/test.toml | 2 + bundle/direct/dresources/all.go | 1 + bundle/direct/dresources/registered_model.go | 89 +++++++++++++++++++ 7 files changed, 151 insertions(+) create mode 100644 acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl create mode 100644 acceptance/bundle/deploy/registered_models/basic/out.test.toml create mode 100644 acceptance/bundle/deploy/registered_models/basic/output.txt create mode 100644 acceptance/bundle/deploy/registered_models/basic/script create mode 100644 acceptance/bundle/deploy/registered_models/basic/test.toml create mode 100644 bundle/direct/dresources/registered_model.go diff --git a/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl b/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl new file mode 100644 index 0000000000..76172e67bc --- /dev/null +++ b/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl @@ -0,0 +1,11 @@ +bundle: + name: deploy-registered-models-basic-$UNIQUE_NAME + +resources: + registered_models: + my_registered_model: + name: $NAME + comment: $COMMENT + catalog_name: $CATALOG_NAME + schema_name: $SCHEMA_NAME + storage_location: $STORAGE_LOCATION diff --git a/acceptance/bundle/deploy/registered_models/basic/out.test.toml b/acceptance/bundle/deploy/registered_models/basic/out.test.toml new file mode 100644 index 0000000000..c3a1b55592 --- /dev/null +++ b/acceptance/bundle/deploy/registered_models/basic/out.test.toml @@ -0,0 +1,5 @@ +Local = false +Cloud = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"] diff --git a/acceptance/bundle/deploy/registered_models/basic/output.txt b/acceptance/bundle/deploy/registered_models/basic/output.txt new file mode 100644 index 0000000000..f08e4220c0 --- /dev/null +++ b/acceptance/bundle/deploy/registered_models/basic/output.txt @@ -0,0 +1,31 @@ + +=== create the registered model +>>> export NAME=my-registered-model-[UNIQUE_NAME] + +>>> export COMMENT=original comment + +>>> export CATALOG_NAME=main + +>>> export SCHEMA_NAME=default + +>>> export STORAGE_LOCATION=s3://my-bucket/my-path + +>>> [CLI] bundle plan +create registered_models.my_registered_model + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] registered-models get main.default.my-registered-model-[UNIQUE_NAME] +{ + "name": "my-registered-model-[UNIQUE_NAME]", + "comment": "original comment", + "catalog_name": "main", + "schema_name": "default", + "storage_location": "abfss://decotestprod-unity-iso@decotestprodunityiso.dfs.core.windows.net/[UUID]/models/[UUID]" +} diff --git a/acceptance/bundle/deploy/registered_models/basic/script b/acceptance/bundle/deploy/registered_models/basic/script new file mode 100644 index 0000000000..f4a63847d1 --- /dev/null +++ b/acceptance/bundle/deploy/registered_models/basic/script @@ -0,0 +1,12 @@ +title "create the registered model" + +trace export NAME="my-registered-model-$UNIQUE_NAME" +trace export COMMENT="original comment" +trace export CATALOG_NAME="main" +trace export SCHEMA_NAME="default" +trace export STORAGE_LOCATION="s3://my-bucket/my-path" +envsubst < databricks.yml.tmpl > databricks.yml +trace $CLI bundle plan +trace $CLI bundle deploy +registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') +trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name, storage_location}' diff --git a/acceptance/bundle/deploy/registered_models/basic/test.toml b/acceptance/bundle/deploy/registered_models/basic/test.toml new file mode 100644 index 0000000000..1c1fa982aa --- /dev/null +++ b/acceptance/bundle/deploy/registered_models/basic/test.toml @@ -0,0 +1,2 @@ +Cloud = true +Local = false diff --git a/bundle/direct/dresources/all.go b/bundle/direct/dresources/all.go index a93d723e01..63c8fb9a65 100644 --- a/bundle/direct/dresources/all.go +++ b/bundle/direct/dresources/all.go @@ -17,6 +17,7 @@ var SupportedResources = map[string]any{ "database_catalogs": (*ResourceDatabaseCatalog)(nil), "synced_database_tables": (*ResourceSyncedDatabaseTable)(nil), "alerts": (*ResourceAlert)(nil), + "registered_models": (*ResourceRegisteredModel)(nil), } func InitAll(client *databricks.WorkspaceClient) (map[string]*Adapter, error) { diff --git a/bundle/direct/dresources/registered_model.go b/bundle/direct/dresources/registered_model.go new file mode 100644 index 0000000000..45e94d136e --- /dev/null +++ b/bundle/direct/dresources/registered_model.go @@ -0,0 +1,89 @@ +package dresources + +import ( + "context" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/deployplan" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +type ResourceRegisteredModel struct { + client *databricks.WorkspaceClient +} + +func (*ResourceRegisteredModel) New(client *databricks.WorkspaceClient) *ResourceRegisteredModel { + return &ResourceRegisteredModel{ + client: client, + } +} + +func (*ResourceRegisteredModel) PrepareState(input *resources.RegisteredModel) *catalog.CreateRegisteredModelRequest { + return &input.CreateRegisteredModelRequest +} + +func (*ResourceRegisteredModel) RemapState(model *catalog.RegisteredModelInfo) *catalog.CreateRegisteredModelRequest { + return &catalog.CreateRegisteredModelRequest{ + CatalogName: model.CatalogName, + Comment: model.Comment, + Name: model.Name, + SchemaName: model.SchemaName, + StorageLocation: model.StorageLocation, + ForceSendFields: filterFields[catalog.CreateRegisteredModelRequest](model.ForceSendFields), + } +} + +func (r *ResourceRegisteredModel) DoRefresh(ctx context.Context, id string) (*catalog.RegisteredModelInfo, error) { + return r.client.RegisteredModels.Get(ctx, catalog.GetRegisteredModelRequest{ + FullName: id, + }) +} + +func (r *ResourceRegisteredModel) DoCreate(ctx context.Context, config *catalog.CreateRegisteredModelRequest) (string, *catalog.RegisteredModelInfo, error) { + response, err := r.client.RegisteredModels.Create(ctx, *config) + if err != nil { + return "", nil, err + } + + return response.FullName, response, nil +} + +func (r *ResourceRegisteredModel) DoUpdate(ctx context.Context, id string, config *catalog.CreateRegisteredModelRequest) (*catalog.RegisteredModelInfo, error) { + updateRequest := catalog.UpdateRegisteredModelRequest{ + FullName: id, + Comment: config.Comment, + ForceSendFields: filterFields[catalog.UpdateRegisteredModelRequest](config.ForceSendFields, "Owner", "NewName"), + + // Owner is not part of the configuration tree + Owner: "", + + // name updates are not supported yet. Can be added as a follow-up. + NewName: "", + } + + response, err := r.client.RegisteredModels.Update(ctx, updateRequest) + if err != nil { + return nil, err + } + + return response, nil +} + +func (r *ResourceRegisteredModel) DoDelete(ctx context.Context, id string) error { + return r.client.RegisteredModels.Delete(ctx, catalog.DeleteRegisteredModelRequest{ + FullName: id, + }) +} + +func (*ResourceRegisteredModel) FieldTriggers() map[string]deployplan.ActionType { + return map[string]deployplan.ActionType{ + // The name can technically be updated without recreated. We recreate for now `though + // to match TF implementation. + "name": deployplan.ActionTypeRecreate, + + "catalog_name": deployplan.ActionTypeRecreate, + "schema_name": deployplan.ActionTypeRecreate, + "storage_location": deployplan.ActionTypeRecreate, + } +} From 6aa0df8ceed6799fce1807e638914bc08d0dfcf5 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 16:55:39 +0200 Subject: [PATCH 02/10] - --- .../basic/databricks.yml.tmpl | 2 +- .../deploy/registered_models/basic/output.txt | 143 +++++++++++++++++- .../deploy/registered_models/basic/script | 55 ++++++- bundle/direct/dresources/registered_model.go | 2 +- 4 files changed, 192 insertions(+), 10 deletions(-) diff --git a/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl b/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl index 76172e67bc..7e5afcc674 100644 --- a/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl +++ b/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl @@ -1,6 +1,7 @@ bundle: name: deploy-registered-models-basic-$UNIQUE_NAME +# TODO: Add a sepatate test for storage_location resources: registered_models: my_registered_model: @@ -8,4 +9,3 @@ resources: comment: $COMMENT catalog_name: $CATALOG_NAME schema_name: $SCHEMA_NAME - storage_location: $STORAGE_LOCATION diff --git a/acceptance/bundle/deploy/registered_models/basic/output.txt b/acceptance/bundle/deploy/registered_models/basic/output.txt index f08e4220c0..3f6bffd852 100644 --- a/acceptance/bundle/deploy/registered_models/basic/output.txt +++ b/acceptance/bundle/deploy/registered_models/basic/output.txt @@ -1,5 +1,4 @@ -=== create the registered model >>> export NAME=my-registered-model-[UNIQUE_NAME] >>> export COMMENT=original comment @@ -8,8 +7,52 @@ >>> export SCHEMA_NAME=default ->>> export STORAGE_LOCATION=s3://my-bucket/my-path +=== create catalog and schema to test diff functionality +>>> [CLI] catalogs create mycatalog-[UNIQUE_NAME] +{ + "browse_only":false, + "catalog_type":"MANAGED_CATALOG", + "created_at":[UNIX_TIME_MILLIS], + "created_by":"[USERNAME]", + "effective_predictive_optimization_flag": { + "inherited_from_name":"metastore_azure_eastus2", + "inherited_from_type":"METASTORE", + "value":"DISABLE" + }, + "enable_predictive_optimization":"INHERIT", + "full_name":"mycatalog-[UNIQUE_NAME]", + "isolation_mode":"OPEN", + "metastore_id":"[UUID]", + "name":"mycatalog-[UNIQUE_NAME]", + "owner":"[USERNAME]", + "securable_type":"CATALOG", + "updated_at":[UNIX_TIME_MILLIS], + "updated_by":"[USERNAME]" +} + +>>> [CLI] schemas create myschema-[UNIQUE_NAME] mycatalog-[UNIQUE_NAME] +{ + "browse_only":false, + "catalog_name":"mycatalog-[UNIQUE_NAME]", + "catalog_type":"MANAGED_CATALOG", + "created_at":[UNIX_TIME_MILLIS], + "created_by":"[USERNAME]", + "effective_predictive_optimization_flag": { + "inherited_from_name":"metastore_azure_eastus2", + "inherited_from_type":"METASTORE", + "value":"DISABLE" + }, + "enable_predictive_optimization":"INHERIT", + "full_name":"mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME]", + "metastore_id":"[UUID]", + "name":"myschema-[UNIQUE_NAME]", + "owner":"[USERNAME]", + "schema_id":"[UUID]", + "updated_at":[UNIX_TIME_MILLIS], + "updated_by":"[USERNAME]" +} +=== create the registered model >>> [CLI] bundle plan create registered_models.my_registered_model @@ -26,6 +69,98 @@ Deployment complete! "name": "my-registered-model-[UNIQUE_NAME]", "comment": "original comment", "catalog_name": "main", - "schema_name": "default", - "storage_location": "abfss://decotestprod-unity-iso@decotestprodunityiso.dfs.core.windows.net/[UUID]/models/[UUID]" + "schema_name": "default" +} + +=== update the comment, this should not recreate +>>> [CLI] bundle plan +update registered_models.my_registered_model + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] registered-models get main.default.my-registered-model-[UNIQUE_NAME] +{ + "name": "my-registered-model-[UNIQUE_NAME]", + "comment": "updated comment", + "catalog_name": "main", + "schema_name": "default" +} + +=== update the name, this should recreate +>>> [CLI] bundle plan +recreate registered_models.my_registered_model + +Plan: 1 to add, 0 to change, 1 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] registered-models get main.default.my-registered-model-updated-[UNIQUE_NAME] +{ + "name": "my-registered-model-updated-[UNIQUE_NAME]", + "comment": "updated comment", + "catalog_name": "main", + "schema_name": "default" +} + +=== update the catalog name, this should recreate +>>> [CLI] bundle plan +recreate registered_models.my_registered_model + +Plan: 1 to add, 0 to change, 1 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] registered-models get mycatalog-[UNIQUE_NAME].default.my-registered-model-updated-[UNIQUE_NAME] +{ + "name": "my-registered-model-updated-[UNIQUE_NAME]", + "comment": "updated comment", + "catalog_name": "mycatalog-[UNIQUE_NAME]", + "schema_name": "default" +} + +=== update the schema name, this should recreate +>>> [CLI] bundle plan +recreate registered_models.my_registered_model + +Plan: 1 to add, 0 to change, 1 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] registered-models get mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME].my-registered-model-updated-[UNIQUE_NAME] +{ + "name": "my-registered-model-updated-[UNIQUE_NAME]", + "comment": "updated comment", + "catalog_name": "mycatalog-[UNIQUE_NAME]", + "schema_name": "myschema-[UNIQUE_NAME]" } + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete registered_model my_registered_model + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-basic-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! + +>>> [CLI] schemas delete mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME] --force + +>>> [CLI] catalogs delete mycatalog-[UNIQUE_NAME] --force diff --git a/acceptance/bundle/deploy/registered_models/basic/script b/acceptance/bundle/deploy/registered_models/basic/script index f4a63847d1..fc32b1304e 100644 --- a/acceptance/bundle/deploy/registered_models/basic/script +++ b/acceptance/bundle/deploy/registered_models/basic/script @@ -1,12 +1,59 @@ -title "create the registered model" - trace export NAME="my-registered-model-$UNIQUE_NAME" trace export COMMENT="original comment" trace export CATALOG_NAME="main" trace export SCHEMA_NAME="default" -trace export STORAGE_LOCATION="s3://my-bucket/my-path" +envsubst < databricks.yml.tmpl > databricks.yml + +title "create catalog and schema to test diff functionality" +catalog_name="mycatalog-${UNIQUE_NAME}" +schema_name="myschema-${UNIQUE_NAME}" +trace $CLI catalogs create ${catalog_name} +trace $CLI schemas create ${schema_name} ${catalog_name} + + +cleanup() { + trace $CLI bundle destroy --auto-approve + trace $CLI schemas delete ${catalog_name}.${schema_name} --force + trace $CLI catalogs delete ${catalog_name} --force +} +trap cleanup EXIT + +title "create the registered model" +trace $CLI bundle plan +trace $CLI bundle deploy +registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') +trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' + +export COMMENT="updated comment" +envsubst < databricks.yml.tmpl > databricks.yml + +title "update the comment, this should not recreate" +trace $CLI bundle plan +trace $CLI bundle deploy +registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') +trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' + +export NAME="my-registered-model-updated-$UNIQUE_NAME" +envsubst < databricks.yml.tmpl > databricks.yml + +title "update the name, this should recreate" +trace $CLI bundle plan +trace $CLI bundle deploy +registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') +trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' + +title "update the catalog name, this should recreate" +export CATALOG_NAME="${catalog_name}" +envsubst < databricks.yml.tmpl > databricks.yml +trace $CLI bundle plan +trace $CLI bundle deploy +registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') +trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' + +title "update the schema name, this should recreate" +export SCHEMA_NAME="${schema_name}" envsubst < databricks.yml.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') -trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name, storage_location}' +trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' diff --git a/bundle/direct/dresources/registered_model.go b/bundle/direct/dresources/registered_model.go index 45e94d136e..060ac02391 100644 --- a/bundle/direct/dresources/registered_model.go +++ b/bundle/direct/dresources/registered_model.go @@ -58,7 +58,7 @@ func (r *ResourceRegisteredModel) DoUpdate(ctx context.Context, id string, confi // Owner is not part of the configuration tree Owner: "", - // name updates are not supported yet. Can be added as a follow-up. + // Name updates are not supported yet without recreating. Can be added as a follow-up. NewName: "", } From 26653673f28645a3d3adb7e983ba9901fa45295a Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 21:32:01 +0200 Subject: [PATCH 03/10] test and server stubs --- .../registered_models/basic/out.test.toml | 2 +- .../deploy/registered_models/basic/output.txt | 38 +------- .../deploy/registered_models/basic/script | 4 +- .../deploy/registered_models/basic/test.toml | 2 +- acceptance/bundle/refschema/out.fields.txt | 27 ++++++ bundle/direct/dresources/all_test.go | 26 ++++-- libs/testserver/catalogs.go | 90 +++++++++++++++++++ libs/testserver/fake_workspace.go | 34 +++---- libs/testserver/handlers.go | 36 ++++++++ libs/testserver/registered_models.go | 87 ++++++++++++++++++ 10 files changed, 285 insertions(+), 61 deletions(-) create mode 100644 libs/testserver/catalogs.go create mode 100644 libs/testserver/registered_models.go diff --git a/acceptance/bundle/deploy/registered_models/basic/out.test.toml b/acceptance/bundle/deploy/registered_models/basic/out.test.toml index c3a1b55592..43c8f792f5 100644 --- a/acceptance/bundle/deploy/registered_models/basic/out.test.toml +++ b/acceptance/bundle/deploy/registered_models/basic/out.test.toml @@ -1,4 +1,4 @@ -Local = false +Local = true Cloud = true [EnvMatrix] diff --git a/acceptance/bundle/deploy/registered_models/basic/output.txt b/acceptance/bundle/deploy/registered_models/basic/output.txt index 3f6bffd852..f03508fe3a 100644 --- a/acceptance/bundle/deploy/registered_models/basic/output.txt +++ b/acceptance/bundle/deploy/registered_models/basic/output.txt @@ -10,46 +10,12 @@ === create catalog and schema to test diff functionality >>> [CLI] catalogs create mycatalog-[UNIQUE_NAME] { - "browse_only":false, - "catalog_type":"MANAGED_CATALOG", - "created_at":[UNIX_TIME_MILLIS], - "created_by":"[USERNAME]", - "effective_predictive_optimization_flag": { - "inherited_from_name":"metastore_azure_eastus2", - "inherited_from_type":"METASTORE", - "value":"DISABLE" - }, - "enable_predictive_optimization":"INHERIT", - "full_name":"mycatalog-[UNIQUE_NAME]", - "isolation_mode":"OPEN", - "metastore_id":"[UUID]", - "name":"mycatalog-[UNIQUE_NAME]", - "owner":"[USERNAME]", - "securable_type":"CATALOG", - "updated_at":[UNIX_TIME_MILLIS], - "updated_by":"[USERNAME]" + "full_name": "mycatalog-[UNIQUE_NAME]" } >>> [CLI] schemas create myschema-[UNIQUE_NAME] mycatalog-[UNIQUE_NAME] { - "browse_only":false, - "catalog_name":"mycatalog-[UNIQUE_NAME]", - "catalog_type":"MANAGED_CATALOG", - "created_at":[UNIX_TIME_MILLIS], - "created_by":"[USERNAME]", - "effective_predictive_optimization_flag": { - "inherited_from_name":"metastore_azure_eastus2", - "inherited_from_type":"METASTORE", - "value":"DISABLE" - }, - "enable_predictive_optimization":"INHERIT", - "full_name":"mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME]", - "metastore_id":"[UUID]", - "name":"myschema-[UNIQUE_NAME]", - "owner":"[USERNAME]", - "schema_id":"[UUID]", - "updated_at":[UNIX_TIME_MILLIS], - "updated_by":"[USERNAME]" + "full_name": "mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME]" } === create the registered model diff --git a/acceptance/bundle/deploy/registered_models/basic/script b/acceptance/bundle/deploy/registered_models/basic/script index fc32b1304e..6ed8271dd6 100644 --- a/acceptance/bundle/deploy/registered_models/basic/script +++ b/acceptance/bundle/deploy/registered_models/basic/script @@ -7,8 +7,8 @@ envsubst < databricks.yml.tmpl > databricks.yml title "create catalog and schema to test diff functionality" catalog_name="mycatalog-${UNIQUE_NAME}" schema_name="myschema-${UNIQUE_NAME}" -trace $CLI catalogs create ${catalog_name} -trace $CLI schemas create ${schema_name} ${catalog_name} +trace $CLI catalogs create ${catalog_name} | jq '{full_name}' +trace $CLI schemas create ${schema_name} ${catalog_name} | jq '{full_name}' cleanup() { diff --git a/acceptance/bundle/deploy/registered_models/basic/test.toml b/acceptance/bundle/deploy/registered_models/basic/test.toml index 1c1fa982aa..e93d23f722 100644 --- a/acceptance/bundle/deploy/registered_models/basic/test.toml +++ b/acceptance/bundle/deploy/registered_models/basic/test.toml @@ -1,2 +1,2 @@ Cloud = true -Local = false +Local = true diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index 513ecea1f1..77f04b5e8a 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -2513,6 +2513,33 @@ resources.pipelines.*.trigger.cron.quartz_cron_schedule string INPUT STATE resources.pipelines.*.trigger.cron.timezone_id string INPUT STATE resources.pipelines.*.trigger.manual *pipelines.ManualTrigger INPUT STATE resources.pipelines.*.url string INPUT +resources.registered_models.*.aliases []catalog.RegisteredModelAlias REMOTE +resources.registered_models.*.aliases[*] catalog.RegisteredModelAlias REMOTE +resources.registered_models.*.aliases[*].alias_name string REMOTE +resources.registered_models.*.aliases[*].version_num int REMOTE +resources.registered_models.*.browse_only bool REMOTE +resources.registered_models.*.catalog_name string ALL +resources.registered_models.*.comment string ALL +resources.registered_models.*.created_at int64 REMOTE +resources.registered_models.*.created_by string REMOTE +resources.registered_models.*.full_name string REMOTE +resources.registered_models.*.grants []resources.Grant INPUT +resources.registered_models.*.grants[*] resources.Grant INPUT +resources.registered_models.*.grants[*].principal string INPUT +resources.registered_models.*.grants[*].privileges []string INPUT +resources.registered_models.*.grants[*].privileges[*] string INPUT +resources.registered_models.*.id string INPUT +resources.registered_models.*.lifecycle resources.Lifecycle INPUT +resources.registered_models.*.lifecycle.prevent_destroy bool INPUT +resources.registered_models.*.metastore_id string REMOTE +resources.registered_models.*.modified_status string INPUT +resources.registered_models.*.name string ALL +resources.registered_models.*.owner string REMOTE +resources.registered_models.*.schema_name string ALL +resources.registered_models.*.storage_location string ALL +resources.registered_models.*.updated_at int64 REMOTE +resources.registered_models.*.updated_by string REMOTE +resources.registered_models.*.url string INPUT resources.schemas.*.browse_only bool REMOTE resources.schemas.*.catalog_name string ALL resources.schemas.*.catalog_type catalog.CatalogType REMOTE diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 9ffd43329a..c858a055b0 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -60,6 +60,16 @@ var testConfig map[string]any = map[string]any{ Name: "main.myschema.my_synced_table", }, }, + + "registered_models": &resources.RegisteredModel{ + CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ + Name: "my_registered_model", + Comment: "Test registered model", + CatalogName: "main", + SchemaName: "default", + StorageLocation: "s3://my-bucket/my-path", + }, + }, } type prepareWorkspace func(client *databricks.WorkspaceClient) error @@ -137,22 +147,26 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W require.Equal(t, remote, remoteStateFromWaitCreate) } + remappedState, err := adapter.RemapState(remote) + require.NoError(t, err) + require.NotNil(t, remappedState) + remoteStateFromUpdate, err := adapter.DoUpdate(ctx, createdID, newState) require.NoError(t, err, "DoUpdate failed") if remoteStateFromUpdate != nil { - require.Equal(t, remote, remoteStateFromUpdate) + remappedStateFromUpdate, err := adapter.RemapState(remoteStateFromUpdate) + require.NoError(t, err) + require.Equal(t, remappedState, remappedStateFromUpdate) } remoteStateFromWaitUpdate, err := adapter.WaitAfterUpdate(ctx, newState) require.NoError(t, err) if remoteStateFromWaitUpdate != nil { - require.Equal(t, remote, remoteStateFromWaitUpdate) + remappedStateFromWaitUpdate, err := adapter.RemapState(remoteStateFromWaitUpdate) + require.NoError(t, err) + require.Equal(t, remappedState, remappedStateFromWaitUpdate) } - remappedState, err := adapter.RemapState(remote) - require.NoError(t, err) - require.NotNil(t, remappedState) - require.NoError(t, structwalk.Walk(newState, func(path *structpath.PathNode, val any, field *reflect.StructField) { remoteValue, err := structaccess.Get(remappedState, path) if err != nil { diff --git a/libs/testserver/catalogs.go b/libs/testserver/catalogs.go new file mode 100644 index 0000000000..25d7cbd5f7 --- /dev/null +++ b/libs/testserver/catalogs.go @@ -0,0 +1,90 @@ +package testserver + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/google/uuid" +) + +func (s *FakeWorkspace) CatalogsCreate(req Request) Response { + defer s.LockUnlock()() + + var createRequest catalog.CreateCatalog + if err := json.Unmarshal(req.Body, &createRequest); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + catalogInfo := catalog.CatalogInfo{ + Name: createRequest.Name, + Comment: createRequest.Comment, + StorageRoot: createRequest.StorageRoot, + ProviderName: createRequest.ProviderName, + ShareName: createRequest.ShareName, + Options: createRequest.Options, + Properties: createRequest.Properties, + FullName: createRequest.Name, + CreatedAt: time.Now().UnixMilli(), + CreatedBy: s.CurrentUser().UserName, + UpdatedAt: time.Now().UnixMilli(), + UpdatedBy: s.CurrentUser().UserName, + MetastoreId: uuid.New().String(), + Owner: s.CurrentUser().UserName, + CatalogType: catalog.CatalogTypeManagedCatalog, + } + + s.Catalogs[createRequest.Name] = catalogInfo + return Response{ + Body: catalogInfo, + } +} + +func (s *FakeWorkspace) CatalogsUpdate(req Request, name string) Response { + defer s.LockUnlock()() + + existing, ok := s.Catalogs[name] + if !ok { + return Response{ + StatusCode: http.StatusNotFound, + Body: fmt.Sprintf("catalog %s not found", name), + } + } + + var updateRequest catalog.UpdateCatalog + if err := json.Unmarshal(req.Body, &updateRequest); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + // Update only the fields that can be updated + if updateRequest.Comment != "" { + existing.Comment = updateRequest.Comment + } + if updateRequest.Owner != "" { + existing.Owner = updateRequest.Owner + } + if updateRequest.NewName != "" { + existing.Name = updateRequest.NewName + existing.FullName = updateRequest.NewName + + // Delete the old entry and create with new name + delete(s.Catalogs, name) + name = updateRequest.NewName + } + + existing.UpdatedAt = time.Now().UnixMilli() + existing.UpdatedBy = s.CurrentUser().UserName + + s.Catalogs[name] = existing + return Response{ + Body: existing, + } +} diff --git a/libs/testserver/fake_workspace.go b/libs/testserver/fake_workspace.go index 9918cc1888..b9efe69918 100644 --- a/libs/testserver/fake_workspace.go +++ b/libs/testserver/fake_workspace.go @@ -60,21 +60,23 @@ type FakeWorkspace struct { repoIdByPath map[string]int64 // normally, ids are not sequential, but we make them sequential for deterministic diff - nextJobId int64 - nextJobRunId int64 - Jobs map[int64]jobs.Job - JobRuns map[int64]jobs.Run - JobPermissions map[string][]jobs.JobAccessControlRequest - Pipelines map[string]pipelines.GetPipelineResponse - PipelineUpdates map[string]bool - Monitors map[string]catalog.MonitorInfo - Apps map[string]apps.App - Schemas map[string]catalog.SchemaInfo - SchemasGrants map[string][]catalog.PrivilegeAssignment - Volumes map[string]catalog.VolumeInfo - Dashboards map[string]dashboards.Dashboard - SqlWarehouses map[string]sql.GetWarehouseResponse - Alerts map[string]sql.AlertV2 + nextJobId int64 + nextJobRunId int64 + Jobs map[int64]jobs.Job + JobRuns map[int64]jobs.Run + JobPermissions map[string][]jobs.JobAccessControlRequest + Pipelines map[string]pipelines.GetPipelineResponse + PipelineUpdates map[string]bool + Monitors map[string]catalog.MonitorInfo + Apps map[string]apps.App + Catalogs map[string]catalog.CatalogInfo + Schemas map[string]catalog.SchemaInfo + SchemasGrants map[string][]catalog.PrivilegeAssignment + RegisteredModels map[string]catalog.RegisteredModelInfo + Volumes map[string]catalog.VolumeInfo + Dashboards map[string]dashboards.Dashboard + SqlWarehouses map[string]sql.GetWarehouseResponse + Alerts map[string]sql.AlertV2 Acls map[string][]workspace.AclItem @@ -162,7 +164,9 @@ func NewFakeWorkspace(url, token string) *FakeWorkspace { PipelineUpdates: map[string]bool{}, Monitors: map[string]catalog.MonitorInfo{}, Apps: map[string]apps.App{}, + Catalogs: map[string]catalog.CatalogInfo{}, Schemas: map[string]catalog.SchemaInfo{}, + RegisteredModels: map[string]catalog.RegisteredModelInfo{}, Volumes: map[string]catalog.VolumeInfo{}, Dashboards: map[string]dashboards.Dashboard{}, SqlWarehouses: map[string]sql.GetWarehouseResponse{}, diff --git a/libs/testserver/handlers.go b/libs/testserver/handlers.go index a0ab2070eb..e110e921cf 100644 --- a/libs/testserver/handlers.go +++ b/libs/testserver/handlers.go @@ -357,6 +357,42 @@ func AddDefaultHandlers(server *Server) { return req.Workspace.SchemasGetGrants(req, req.Vars["full_name"]) }) + // Catalogs: + + server.Handle("GET", "/api/2.1/unity-catalog/catalogs/{name}", func(req Request) any { + return MapGet(req.Workspace, req.Workspace.Catalogs, req.Vars["name"]) + }) + + server.Handle("POST", "/api/2.1/unity-catalog/catalogs", func(req Request) any { + return req.Workspace.CatalogsCreate(req) + }) + + server.Handle("PATCH", "/api/2.1/unity-catalog/catalogs/{name}", func(req Request) any { + return req.Workspace.CatalogsUpdate(req, req.Vars["name"]) + }) + + server.Handle("DELETE", "/api/2.1/unity-catalog/catalogs/{name}", func(req Request) any { + return MapDelete(req.Workspace, req.Workspace.Catalogs, req.Vars["name"]) + }) + + // Registered Models: + + server.Handle("GET", "/api/2.1/unity-catalog/models/{full_name}", func(req Request) any { + return MapGet(req.Workspace, req.Workspace.RegisteredModels, req.Vars["full_name"]) + }) + + server.Handle("POST", "/api/2.1/unity-catalog/models", func(req Request) any { + return req.Workspace.RegisteredModelsCreate(req) + }) + + server.Handle("PATCH", "/api/2.1/unity-catalog/models/{full_name}", func(req Request) any { + return req.Workspace.RegisteredModelsUpdate(req, req.Vars["full_name"]) + }) + + server.Handle("DELETE", "/api/2.1/unity-catalog/models/{full_name}", func(req Request) any { + return MapDelete(req.Workspace, req.Workspace.RegisteredModels, req.Vars["full_name"]) + }) + // Volumes: server.Handle("GET", "/api/2.1/unity-catalog/volumes/{full_name}", func(req Request) any { diff --git a/libs/testserver/registered_models.go b/libs/testserver/registered_models.go new file mode 100644 index 0000000000..865e6c3b5a --- /dev/null +++ b/libs/testserver/registered_models.go @@ -0,0 +1,87 @@ +package testserver + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/google/uuid" +) + +func (s *FakeWorkspace) RegisteredModelsCreate(req Request) Response { + defer s.LockUnlock()() + + var createRequest catalog.CreateRegisteredModelRequest + if err := json.Unmarshal(req.Body, &createRequest); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + // Build full name from catalog.schema.name + fullName := createRequest.CatalogName + "." + createRequest.SchemaName + "." + createRequest.Name + + registeredModel := catalog.RegisteredModelInfo{ + CatalogName: createRequest.CatalogName, + Comment: createRequest.Comment, + Name: createRequest.Name, + SchemaName: createRequest.SchemaName, + StorageLocation: createRequest.StorageLocation, + FullName: fullName, + CreatedAt: time.Now().UnixMilli(), + CreatedBy: s.CurrentUser().UserName, + UpdatedAt: time.Now().UnixMilli(), + UpdatedBy: s.CurrentUser().UserName, + MetastoreId: uuid.New().String(), + Owner: s.CurrentUser().UserName, + } + + s.RegisteredModels[fullName] = registeredModel + return Response{ + Body: registeredModel, + } +} + +func (s *FakeWorkspace) RegisteredModelsUpdate(req Request, fullName string) Response { + defer s.LockUnlock()() + + existing, ok := s.RegisteredModels[fullName] + if !ok { + return Response{ + StatusCode: http.StatusNotFound, + Body: fmt.Sprintf("registered model %s not found", fullName), + } + } + + var updateRequest catalog.UpdateRegisteredModelRequest + if err := json.Unmarshal(req.Body, &updateRequest); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + // Update only the fields that can be updated + if updateRequest.Comment != "" { + existing.Comment = updateRequest.Comment + } + if updateRequest.Owner != "" { + existing.Owner = updateRequest.Owner + } + if updateRequest.NewName != "" { + existing.Name = updateRequest.NewName + + // Delete the old entry and set full name to the new name + delete(s.RegisteredModels, fullName) + fullName = existing.CatalogName + "." + existing.SchemaName + "." + updateRequest.NewName + } + + existing.UpdatedAt = time.Now().UnixMilli() + s.RegisteredModels[fullName] = existing + return Response{ + Body: existing, + } +} From 20588ff73efc1b170265fb9d699cf30d0133ba73 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 21:35:44 +0200 Subject: [PATCH 04/10] - --- .../bundle/deploy/registered_models/basic/databricks.yml.tmpl | 1 - 1 file changed, 1 deletion(-) diff --git a/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl b/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl index 7e5afcc674..9c10c6bbc4 100644 --- a/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl +++ b/acceptance/bundle/deploy/registered_models/basic/databricks.yml.tmpl @@ -1,7 +1,6 @@ bundle: name: deploy-registered-models-basic-$UNIQUE_NAME -# TODO: Add a sepatate test for storage_location resources: registered_models: my_registered_model: From 6571c003a1646d5ea230a2b77c6cd272ff47cadc Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 21:47:58 +0200 Subject: [PATCH 05/10] - --- bundle/direct/dresources/registered_model.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bundle/direct/dresources/registered_model.go b/bundle/direct/dresources/registered_model.go index 060ac02391..c4a80a1589 100644 --- a/bundle/direct/dresources/registered_model.go +++ b/bundle/direct/dresources/registered_model.go @@ -36,7 +36,10 @@ func (*ResourceRegisteredModel) RemapState(model *catalog.RegisteredModelInfo) * func (r *ResourceRegisteredModel) DoRefresh(ctx context.Context, id string) (*catalog.RegisteredModelInfo, error) { return r.client.RegisteredModels.Get(ctx, catalog.GetRegisteredModelRequest{ - FullName: id, + FullName: id, + IncludeAliases: false, + IncludeBrowse: false, + ForceSendFields: nil, }) } From 9df2d0843d0b23e84ab1e43e1b08911c5e800ee4 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 21:55:49 +0200 Subject: [PATCH 06/10] refactor the bash script --- .../deploy/registered_models/basic/script | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/acceptance/bundle/deploy/registered_models/basic/script b/acceptance/bundle/deploy/registered_models/basic/script index 6ed8271dd6..25906fe249 100644 --- a/acceptance/bundle/deploy/registered_models/basic/script +++ b/acceptance/bundle/deploy/registered_models/basic/script @@ -18,42 +18,34 @@ cleanup() { } trap cleanup EXIT +deploy_registered_model() { + trace $CLI bundle plan + trace $CLI bundle deploy + registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') + trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' +} + title "create the registered model" -trace $CLI bundle plan -trace $CLI bundle deploy -registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') -trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' +deploy_registered_model export COMMENT="updated comment" envsubst < databricks.yml.tmpl > databricks.yml title "update the comment, this should not recreate" -trace $CLI bundle plan -trace $CLI bundle deploy -registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') -trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' +deploy_registered_model export NAME="my-registered-model-updated-$UNIQUE_NAME" envsubst < databricks.yml.tmpl > databricks.yml title "update the name, this should recreate" -trace $CLI bundle plan -trace $CLI bundle deploy -registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') -trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' +deploy_registered_model title "update the catalog name, this should recreate" export CATALOG_NAME="${catalog_name}" envsubst < databricks.yml.tmpl > databricks.yml -trace $CLI bundle plan -trace $CLI bundle deploy -registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') -trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' +deploy_registered_model title "update the schema name, this should recreate" export SCHEMA_NAME="${schema_name}" envsubst < databricks.yml.tmpl > databricks.yml -trace $CLI bundle plan -trace $CLI bundle deploy -registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id') -trace $CLI registered-models get "${registered_model_id}" | jq '{name, comment, catalog_name, schema_name}' +deploy_registered_model From 1e8cf0291458fc3e48d54477f22d67942fdb0500 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 21:56:15 +0200 Subject: [PATCH 07/10] - --- acceptance/bundle/deploy/registered_models/basic/script | 1 - 1 file changed, 1 deletion(-) diff --git a/acceptance/bundle/deploy/registered_models/basic/script b/acceptance/bundle/deploy/registered_models/basic/script index 25906fe249..42313057e0 100644 --- a/acceptance/bundle/deploy/registered_models/basic/script +++ b/acceptance/bundle/deploy/registered_models/basic/script @@ -10,7 +10,6 @@ schema_name="myschema-${UNIQUE_NAME}" trace $CLI catalogs create ${catalog_name} | jq '{full_name}' trace $CLI schemas create ${schema_name} ${catalog_name} | jq '{full_name}' - cleanup() { trace $CLI bundle destroy --auto-approve trace $CLI schemas delete ${catalog_name}.${schema_name} --force From c3f24b1d2789af8013fa0d1c18c006a040da04eb Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 21:58:51 +0200 Subject: [PATCH 08/10] - --- bundle/direct/dresources/registered_model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle/direct/dresources/registered_model.go b/bundle/direct/dresources/registered_model.go index c4a80a1589..58110f050b 100644 --- a/bundle/direct/dresources/registered_model.go +++ b/bundle/direct/dresources/registered_model.go @@ -81,7 +81,7 @@ func (r *ResourceRegisteredModel) DoDelete(ctx context.Context, id string) error func (*ResourceRegisteredModel) FieldTriggers() map[string]deployplan.ActionType { return map[string]deployplan.ActionType{ - // The name can technically be updated without recreated. We recreate for now `though + // The name can technically be updated without recreated. We recreate for now though // to match TF implementation. "name": deployplan.ActionTypeRecreate, From afb8d96b99a9d8b9f9229daff046b49417200d80 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 1 Oct 2025 12:49:14 +0200 Subject: [PATCH 09/10] add TF comment --- bundle/direct/dresources/registered_model.go | 1 + 1 file changed, 1 insertion(+) diff --git a/bundle/direct/dresources/registered_model.go b/bundle/direct/dresources/registered_model.go index 58110f050b..b01b90b319 100644 --- a/bundle/direct/dresources/registered_model.go +++ b/bundle/direct/dresources/registered_model.go @@ -62,6 +62,7 @@ func (r *ResourceRegisteredModel) DoUpdate(ctx context.Context, id string, confi Owner: "", // Name updates are not supported yet without recreating. Can be added as a follow-up. + // Note: TF also does not support changing name without a recreate so the current behavior matches TF. NewName: "", } From 0a91856ef987110e4ad24f361a7af7aec9339659 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 1 Oct 2025 13:25:10 +0200 Subject: [PATCH 10/10] update tesT: --- acceptance/bundle/deploy/registered_models/basic/out.test.toml | 1 + acceptance/bundle/deploy/registered_models/basic/test.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/acceptance/bundle/deploy/registered_models/basic/out.test.toml b/acceptance/bundle/deploy/registered_models/basic/out.test.toml index 43c8f792f5..c969c92e84 100644 --- a/acceptance/bundle/deploy/registered_models/basic/out.test.toml +++ b/acceptance/bundle/deploy/registered_models/basic/out.test.toml @@ -1,5 +1,6 @@ Local = true Cloud = true +RequiresUnityCatalog = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"] diff --git a/acceptance/bundle/deploy/registered_models/basic/test.toml b/acceptance/bundle/deploy/registered_models/basic/test.toml index e93d23f722..80d5c3424e 100644 --- a/acceptance/bundle/deploy/registered_models/basic/test.toml +++ b/acceptance/bundle/deploy/registered_models/basic/test.toml @@ -1,2 +1,3 @@ Cloud = true Local = true +RequiresUnityCatalog = true