From 7beac34c44a6dc53301728feca0b17a85bfda23b Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 16 Jun 2026 06:39:21 -0700 Subject: [PATCH 1/9] terraform_dabs_map: fix DABsPathToTerraform to skip wrapper for root-level TF fields For postgres groups with a spec wrapper, DABsPathToTerraform was unconditionally prepending "spec" to all paths. Root-level TF fields (name, status.*, create_time, project_id, etc.) live outside the wrapper and must not receive the prefix. Extend the codegen to emit DABsToTerraformRootFields: for each wrapper group, the set of first-level TF field names that are at the resource root. DABsPathToTerraform now only prepends the wrapper when the path's first segment is absent from that set. Remove noRoundtrip: true from the affected translate_test.go cases; add new roundtrip cases for project_id and name. Co-authored-by: Isaac --- bundle/terraform_dabs_map/generate_test.go | 49 ++++++++++++++--- bundle/terraform_dabs_map/generated.go | 59 +++++++++++++++++++++ bundle/terraform_dabs_map/translate.go | 25 ++++++--- bundle/terraform_dabs_map/translate_test.go | 28 ++++++---- 4 files changed, 135 insertions(+), 26 deletions(-) diff --git a/bundle/terraform_dabs_map/generate_test.go b/bundle/terraform_dabs_map/generate_test.go index 0a313c071c4..21f42f88ef6 100644 --- a/bundle/terraform_dabs_map/generate_test.go +++ b/bundle/terraform_dabs_map/generate_test.go @@ -75,14 +75,15 @@ var tfKnownSegments = map[string]bool{ } type groupResult struct { - group string - tfType string - hasTFType bool - renames map[string]string // TF path → DABs path (renamed fields only) - unwraps []string // TF paths that are structural wrappers (Unwrap: true) - dabsOnly map[string]bool // DABs clean paths with no Terraform equivalent - tfOnly map[string]bool // TF clean paths with no DABs equivalent - matchCount int // used for stats output only, not written to generated.go + group string + tfType string + hasTFType bool + renames map[string]string // TF path → DABs path (renamed fields only) + unwraps []string // TF paths that are structural wrappers (Unwrap: true) + dabsOnly map[string]bool // DABs clean paths with no Terraform equivalent + tfOnly map[string]bool // TF clean paths with no DABs equivalent + matchCount int // used for stats output only, not written to generated.go + tfRootFirstSegs map[string]bool // first-level TF field names not under any wrapper (only set when unwraps is non-empty) } func buildAll() ([]groupResult, error) { @@ -248,6 +249,22 @@ func buildGroup(group string, adapter *dresources.Adapter) (groupResult, error) res.unwraps = append(res.unwraps, wrapper) } + // Collect root-level TF field first segments for groups with wrappers. + // These are top-level TF field names NOT under any confirmed wrapper. + if len(res.unwraps) > 0 { + unwrapSet := make(map[string]bool) + for _, w := range res.unwraps { + unwrapSet[w] = true + } + res.tfRootFirstSegs = make(map[string]bool) + for tf := range tfFields { + head := topSegment(tf) + if !tfKnownFields[head] && !unwrapSet[head] { + res.tfRootFirstSegs[head] = true + } + } + } + // Step 4: remaining unmatched fields. for dabs := range dabsFields { if !matchedDABs[dabs] && !dabsKnownFields[topSegment(dabs)] { @@ -463,6 +480,22 @@ func renderSource(results []groupResult) ([]byte, error) { w("\t%q: %q,\n", r.group, wrapper) } } + w("}\n\n") + + w("// DABsToTerraformRootFields maps DABs group name → first-level TF field names at the\n") + w("// resource root (not under any wrapper). For wrapper groups, a DABs path whose first\n") + w("// segment is in this set bypasses the wrapper prepend in DABsPathToTerraform.\n") + w("var DABsToTerraformRootFields = map[string]FieldSet{\n") + for _, r := range results { + if !r.hasTFType || len(r.tfRootFirstSegs) == 0 { + continue + } + w("\t%q: {\n", r.group) + for _, key := range slices.Sorted(maps.Keys(r.tfRootFirstSegs)) { + w("\t\t%q: {},\n", key) + } + w("\t},\n") + } w("}\n") return format.Source([]byte(b.String())) diff --git a/bundle/terraform_dabs_map/generated.go b/bundle/terraform_dabs_map/generated.go index f4c8ec8d7a2..41014575f44 100644 --- a/bundle/terraform_dabs_map/generated.go +++ b/bundle/terraform_dabs_map/generated.go @@ -637,3 +637,62 @@ var DABsToTerraformWrappers = map[string]string{ "postgres_roles": "spec", "postgres_synced_tables": "spec", } + +// DABsToTerraformRootFields maps DABs group name → first-level TF field names at the +// resource root (not under any wrapper). For wrapper groups, a DABs path whose first +// segment is in this set bypasses the wrapper prepend in DABsPathToTerraform. +var DABsToTerraformRootFields = map[string]FieldSet{ + "postgres_branches": { + "branch_id": {}, + "create_time": {}, + "name": {}, + "parent": {}, + "provider_config": {}, + "purge_on_delete": {}, + "replace_existing": {}, + "status": {}, + "uid": {}, + "update_time": {}, + }, + "postgres_catalogs": { + "catalog_id": {}, + "create_time": {}, + "name": {}, + "provider_config": {}, + "status": {}, + "uid": {}, + "update_time": {}, + }, + "postgres_endpoints": { + "create_time": {}, + "endpoint_id": {}, + "name": {}, + "parent": {}, + "provider_config": {}, + "replace_existing": {}, + "status": {}, + "uid": {}, + "update_time": {}, + }, + "postgres_projects": { + "create_time": {}, + "delete_time": {}, + "initial_endpoint_spec": {}, + "name": {}, + "project_id": {}, + "provider_config": {}, + "purge_on_delete": {}, + "purge_time": {}, + "status": {}, + "uid": {}, + "update_time": {}, + }, + "postgres_synced_tables": { + "create_time": {}, + "name": {}, + "provider_config": {}, + "status": {}, + "synced_table_id": {}, + "uid": {}, + }, +} diff --git a/bundle/terraform_dabs_map/translate.go b/bundle/terraform_dabs_map/translate.go index d3d0517cffb..74127ce99db 100644 --- a/bundle/terraform_dabs_map/translate.go +++ b/bundle/terraform_dabs_map/translate.go @@ -9,12 +9,13 @@ import ( // DABsPathToTerraform translates a field path from DABs naming conventions // to Terraform naming conventions for the given resource group. // -// It is the inverse of TerraformPathToDABs. For groups whose TF schema wraps fields -// under a structural prefix (e.g. "spec"), that prefix is prepended to the result. -// Each field name segment is looked up in the DABsToTerraformRenameMap: when found the TF -// name is used and the tree descends for the remainder of the path. Array indices pass -// through unchanged without advancing the tree position. An unrecognised segment stops -// further renaming; remaining segments are kept as-is. Returns nil when path is nil. +// It is the inverse of TerraformPathToDABs. For groups whose TF schema wraps config fields +// under a structural prefix (e.g. "spec"), that prefix is prepended unless the path's first +// segment is a root-level TF field (listed in DABsToTerraformRootFields). Each field name +// segment is looked up in the DABsToTerraformRenameMap: when found the TF name is used and +// the tree descends for the remainder of the path. Array indices pass through unchanged +// without advancing the tree position. An unrecognised segment stops further renaming; +// remaining segments are kept as-is. Returns nil when path is nil. // Returns an error when path is a known DABs-only field with no Terraform equivalent. // // The path must be relative to the resource root (e.g. "tasks", not @@ -28,10 +29,18 @@ func DABsPathToTerraform(group string, path *structpath.PathNode) (*structpath.P return nil, fmt.Errorf("%s: %q is a DABs-only field with no Terraform equivalent", group, path) } - // For groups with a TF wrapper (Unwrap inverse), prepend it as the first segment. + // For groups with a TF wrapper, prepend it only when the first segment is not a + // root-level TF field. Root-level fields and pass-through unknowns bypass the wrapper. var result *structpath.PathNode if wrapper, ok := DABsToTerraformWrappers[group]; ok { - result = structpath.NewDotString(nil, wrapper) + segs := path.AsSlice() + if len(segs) > 0 { + if firstKey, ok := segs[0].StringKey(); ok { + if _, isRoot := DABsToTerraformRootFields[group][firstKey]; !isRoot { + result = structpath.NewDotString(nil, wrapper) + } + } + } } tree := DABsToTerraformRenameMap[group] diff --git a/bundle/terraform_dabs_map/translate_test.go b/bundle/terraform_dabs_map/translate_test.go index c61858a5039..85150cec88e 100644 --- a/bundle/terraform_dabs_map/translate_test.go +++ b/bundle/terraform_dabs_map/translate_test.go @@ -140,19 +140,27 @@ func TestTerraformPathToDABs(t *testing.T) { dabsPath: "endpoint_type", }, - // TF-computed paths (status, timestamps) pass through unchanged but are not - // roundtrippable: DABsPathToTerraform would incorrectly prepend the spec wrapper. + // TF root-level paths (status, timestamps, IDs) pass through unchanged and + // round-trip correctly: DABsPathToTerraform recognises them as root fields. { - group: "postgres_projects", - terrPath: "status.display_name", - dabsPath: "status.display_name", - noRoundtrip: true, + group: "postgres_projects", + terrPath: "status.display_name", + dabsPath: "status.display_name", + }, + { + group: "postgres_projects", + terrPath: "create_time", + dabsPath: "create_time", + }, + { + group: "postgres_projects", + terrPath: "project_id", + dabsPath: "project_id", }, { - group: "postgres_projects", - terrPath: "create_time", - dabsPath: "create_time", - noRoundtrip: true, + group: "postgres_projects", + terrPath: "name", + dabsPath: "name", }, // Terraform-only fields: must return an error From 014705f4084351fa4ded2e0637d2d2237b9bd3e4 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 16 Jun 2026 06:42:46 -0700 Subject: [PATCH 2/9] terraform_dabs_map: remove noRoundtrip escape hatch from translate_test.go All TerraformPathToDABs test cases now round-trip correctly through DABsPathToTerraform, so the field and its guard are no longer needed. Co-authored-by: Isaac --- bundle/terraform_dabs_map/translate_test.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/bundle/terraform_dabs_map/translate_test.go b/bundle/terraform_dabs_map/translate_test.go index 85150cec88e..39b3d01f528 100644 --- a/bundle/terraform_dabs_map/translate_test.go +++ b/bundle/terraform_dabs_map/translate_test.go @@ -11,11 +11,10 @@ import ( func TestTerraformPathToDABs(t *testing.T) { tests := []struct { - group string - terrPath string - dabsPath string - noRoundtrip bool // if true, DABsPathToTerraform(dabsPath) != terrPath (non-invertible path) - expectErr bool // if true, translation must return an error (Terraform-only field) + group string + terrPath string + dabsPath string + expectErr bool // if true, translation must return an error (Terraform-only field) }{ // Top-level renames - jobs { @@ -207,12 +206,10 @@ func TestTerraformPathToDABs(t *testing.T) { require.NotNil(t, result) assert.Equal(t, tt.dabsPath, result.String()) - if !tt.noRoundtrip { - back, err := terraform_dabs_map.DABsPathToTerraform(tt.group, result) - require.NoError(t, err) - require.NotNil(t, back) - assert.Equal(t, tt.terrPath, back.String(), "roundtrip DABsPathToTerraform(TerraformPathToDABs(terrPath))") - } + back, err := terraform_dabs_map.DABsPathToTerraform(tt.group, result) + require.NoError(t, err) + require.NotNil(t, back) + assert.Equal(t, tt.terrPath, back.String(), "roundtrip DABsPathToTerraform(TerraformPathToDABs(terrPath))") }) } } From db9fb9c1e2320167c562d20443e1c2ff2f33e01c Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 16 Jun 2026 06:46:44 -0700 Subject: [PATCH 3/9] terraform_dabs_map: add fixed-point check to TestTerraformPathToDABs TerraformPathToDABs should be idempotent: a DABs-format path passed through a second time must come back unchanged. Assert this for every non-error test case. Co-authored-by: Isaac --- bundle/terraform_dabs_map/translate_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bundle/terraform_dabs_map/translate_test.go b/bundle/terraform_dabs_map/translate_test.go index 39b3d01f528..b4eacb9cd43 100644 --- a/bundle/terraform_dabs_map/translate_test.go +++ b/bundle/terraform_dabs_map/translate_test.go @@ -206,6 +206,13 @@ func TestTerraformPathToDABs(t *testing.T) { require.NotNil(t, result) assert.Equal(t, tt.dabsPath, result.String()) + // Fixed-point: the DABs result is already in DABs format, so a second + // TerraformPathToDABs pass must leave it unchanged. + result2, err := terraform_dabs_map.TerraformPathToDABs(tt.group, result) + require.NoError(t, err) + require.NotNil(t, result2) + assert.Equal(t, result.String(), result2.String(), "TerraformPathToDABs(TerraformPathToDABs(terrPath)) == TerraformPathToDABs(terrPath)") + back, err := terraform_dabs_map.DABsPathToTerraform(tt.group, result) require.NoError(t, err) require.NotNil(t, back) From 8e69f8380615dc362f70a7b615c9408aecd13f63 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 16 Jun 2026 06:47:50 -0700 Subject: [PATCH 4/9] terraform_dabs_map: note why DABsPathToTerraform lacks the fixed-point property Co-authored-by: Isaac --- bundle/terraform_dabs_map/translate_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bundle/terraform_dabs_map/translate_test.go b/bundle/terraform_dabs_map/translate_test.go index b4eacb9cd43..bdcea91192a 100644 --- a/bundle/terraform_dabs_map/translate_test.go +++ b/bundle/terraform_dabs_map/translate_test.go @@ -208,6 +208,9 @@ func TestTerraformPathToDABs(t *testing.T) { // Fixed-point: the DABs result is already in DABs format, so a second // TerraformPathToDABs pass must leave it unchanged. + // DABsPathToTerraform does NOT have this property: spec fields like + // "display_name" map to "spec.display_name", and applying the function + // again would incorrectly prepend another "spec." prefix. result2, err := terraform_dabs_map.TerraformPathToDABs(tt.group, result) require.NoError(t, err) require.NotNil(t, result2) From da32f7fda1973b2d0b15c1c4ac97b276027cfd49 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 16 Jun 2026 18:12:39 -0700 Subject: [PATCH 5/9] terraform_dabs_map: regenerate with postgres_roles Rebase onto main picked up the new databricks_postgres_role resource; regenerate generated.go to include it. Co-authored-by: Isaac --- bundle/terraform_dabs_map/generated.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bundle/terraform_dabs_map/generated.go b/bundle/terraform_dabs_map/generated.go index 41014575f44..51a9b09aff1 100644 --- a/bundle/terraform_dabs_map/generated.go +++ b/bundle/terraform_dabs_map/generated.go @@ -687,6 +687,15 @@ var DABsToTerraformRootFields = map[string]FieldSet{ "uid": {}, "update_time": {}, }, + "postgres_roles": { + "create_time": {}, + "name": {}, + "parent": {}, + "provider_config": {}, + "role_id": {}, + "status": {}, + "update_time": {}, + }, "postgres_synced_tables": { "create_time": {}, "name": {}, From d5daae7994d13bdfe997b6f6dcc2025700f1d5e1 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 16 Jun 2026 18:20:44 -0700 Subject: [PATCH 6/9] terraform_dabs_map: switch wrapper logic to positive field set Replace DABsToTerraformRootFields (exception-based: fields NOT under the wrapper) with DABsToTerraformWrapperFields (positive: fields that ARE under the wrapper). DABsPathToTerraform now prepends the wrapper only when the first path segment appears in this set; unknown and root-level fields pass through unchanged without requiring explicit enumeration. Co-authored-by: Isaac --- bundle/terraform_dabs_map/generate_test.go | 35 ++++---- bundle/terraform_dabs_map/generated.go | 98 ++++++++++------------ bundle/terraform_dabs_map/translate.go | 19 +++-- 3 files changed, 71 insertions(+), 81 deletions(-) diff --git a/bundle/terraform_dabs_map/generate_test.go b/bundle/terraform_dabs_map/generate_test.go index 21f42f88ef6..d2784977483 100644 --- a/bundle/terraform_dabs_map/generate_test.go +++ b/bundle/terraform_dabs_map/generate_test.go @@ -83,7 +83,7 @@ type groupResult struct { dabsOnly map[string]bool // DABs clean paths with no Terraform equivalent tfOnly map[string]bool // TF clean paths with no DABs equivalent matchCount int // used for stats output only, not written to generated.go - tfRootFirstSegs map[string]bool // first-level TF field names not under any wrapper (only set when unwraps is non-empty) + tfWrapperFirstSegs map[string]bool // first-level DABs field names that go under the wrapper (only set when unwraps is non-empty) } func buildAll() ([]groupResult, error) { @@ -249,18 +249,17 @@ func buildGroup(group string, adapter *dresources.Adapter) (groupResult, error) res.unwraps = append(res.unwraps, wrapper) } - // Collect root-level TF field first segments for groups with wrappers. - // These are top-level TF field names NOT under any confirmed wrapper. + // Collect first-level DABs field names that go under the wrapper for groups with wrappers. if len(res.unwraps) > 0 { - unwrapSet := make(map[string]bool) - for _, w := range res.unwraps { - unwrapSet[w] = true - } - res.tfRootFirstSegs = make(map[string]bool) - for tf := range tfFields { - head := topSegment(tf) - if !tfKnownFields[head] && !unwrapSet[head] { - res.tfRootFirstSegs[head] = true + res.tfWrapperFirstSegs = make(map[string]bool) + for _, wrapper := range res.unwraps { + prefix := wrapper + "." + for tf := range tfFields { + if matchedTF[tf] { + if after, ok := strings.CutPrefix(tf, prefix); ok { + res.tfWrapperFirstSegs[topSegment(after)] = true + } + } } } } @@ -482,16 +481,16 @@ func renderSource(results []groupResult) ([]byte, error) { } w("}\n\n") - w("// DABsToTerraformRootFields maps DABs group name → first-level TF field names at the\n") - w("// resource root (not under any wrapper). For wrapper groups, a DABs path whose first\n") - w("// segment is in this set bypasses the wrapper prepend in DABsPathToTerraform.\n") - w("var DABsToTerraformRootFields = map[string]FieldSet{\n") + w("// DABsToTerraformWrapperFields maps DABs group name → first-level DABs field names that\n") + w("// live under the TF wrapper. For wrapper groups, a DABs path is prefixed with the wrapper\n") + w("// in DABsPathToTerraform only when its first segment appears here.\n") + w("var DABsToTerraformWrapperFields = map[string]FieldSet{\n") for _, r := range results { - if !r.hasTFType || len(r.tfRootFirstSegs) == 0 { + if !r.hasTFType || len(r.tfWrapperFirstSegs) == 0 { continue } w("\t%q: {\n", r.group) - for _, key := range slices.Sorted(maps.Keys(r.tfRootFirstSegs)) { + for _, key := range slices.Sorted(maps.Keys(r.tfWrapperFirstSegs)) { w("\t\t%q: {},\n", key) } w("\t},\n") diff --git a/bundle/terraform_dabs_map/generated.go b/bundle/terraform_dabs_map/generated.go index 51a9b09aff1..cff1ac70990 100644 --- a/bundle/terraform_dabs_map/generated.go +++ b/bundle/terraform_dabs_map/generated.go @@ -638,70 +638,60 @@ var DABsToTerraformWrappers = map[string]string{ "postgres_synced_tables": "spec", } -// DABsToTerraformRootFields maps DABs group name → first-level TF field names at the -// resource root (not under any wrapper). For wrapper groups, a DABs path whose first -// segment is in this set bypasses the wrapper prepend in DABsPathToTerraform. -var DABsToTerraformRootFields = map[string]FieldSet{ +// DABsToTerraformWrapperFields maps DABs group name → first-level DABs field names that +// live under the TF wrapper. For wrapper groups, a DABs path is prefixed with the wrapper +// in DABsPathToTerraform only when its first segment appears here. +var DABsToTerraformWrapperFields = map[string]FieldSet{ "postgres_branches": { - "branch_id": {}, - "create_time": {}, - "name": {}, - "parent": {}, - "provider_config": {}, - "purge_on_delete": {}, - "replace_existing": {}, - "status": {}, - "uid": {}, - "update_time": {}, + "expire_time": {}, + "is_protected": {}, + "no_expiry": {}, + "source_branch": {}, + "source_branch_lsn": {}, + "source_branch_time": {}, + "ttl": {}, }, "postgres_catalogs": { - "catalog_id": {}, - "create_time": {}, - "name": {}, - "provider_config": {}, - "status": {}, - "uid": {}, - "update_time": {}, + "branch": {}, + "create_database_if_missing": {}, + "postgres_database": {}, }, "postgres_endpoints": { - "create_time": {}, - "endpoint_id": {}, - "name": {}, - "parent": {}, - "provider_config": {}, - "replace_existing": {}, - "status": {}, - "uid": {}, - "update_time": {}, + "autoscaling_limit_max_cu": {}, + "autoscaling_limit_min_cu": {}, + "disabled": {}, + "endpoint_type": {}, + "group": {}, + "no_suspension": {}, + "settings": {}, + "suspend_timeout_duration": {}, }, "postgres_projects": { - "create_time": {}, - "delete_time": {}, - "initial_endpoint_spec": {}, - "name": {}, - "project_id": {}, - "provider_config": {}, - "purge_on_delete": {}, - "purge_time": {}, - "status": {}, - "uid": {}, - "update_time": {}, + "budget_policy_id": {}, + "custom_tags": {}, + "default_branch": {}, + "default_endpoint_settings": {}, + "display_name": {}, + "enable_pg_native_login": {}, + "history_retention_duration": {}, + "pg_version": {}, }, "postgres_roles": { - "create_time": {}, - "name": {}, - "parent": {}, - "provider_config": {}, - "role_id": {}, - "status": {}, - "update_time": {}, + "attributes": {}, + "auth_method": {}, + "identity_type": {}, + "membership_roles": {}, + "postgres_role": {}, }, "postgres_synced_tables": { - "create_time": {}, - "name": {}, - "provider_config": {}, - "status": {}, - "synced_table_id": {}, - "uid": {}, + "branch": {}, + "create_database_objects_if_missing": {}, + "existing_pipeline_id": {}, + "new_pipeline_spec": {}, + "postgres_database": {}, + "primary_key_columns": {}, + "scheduling_policy": {}, + "source_table_full_name": {}, + "timeseries_key": {}, }, } diff --git a/bundle/terraform_dabs_map/translate.go b/bundle/terraform_dabs_map/translate.go index 74127ce99db..5f725e7f4a0 100644 --- a/bundle/terraform_dabs_map/translate.go +++ b/bundle/terraform_dabs_map/translate.go @@ -10,12 +10,13 @@ import ( // to Terraform naming conventions for the given resource group. // // It is the inverse of TerraformPathToDABs. For groups whose TF schema wraps config fields -// under a structural prefix (e.g. "spec"), that prefix is prepended unless the path's first -// segment is a root-level TF field (listed in DABsToTerraformRootFields). Each field name -// segment is looked up in the DABsToTerraformRenameMap: when found the TF name is used and -// the tree descends for the remainder of the path. Array indices pass through unchanged -// without advancing the tree position. An unrecognised segment stops further renaming; -// remaining segments are kept as-is. Returns nil when path is nil. +// under a structural prefix (e.g. "spec"), that prefix is prepended when the path's first +// segment is listed in DABsToTerraformWrapperFields. Root-level fields and unrecognised +// segments pass through without the wrapper. Each field name segment is looked up in the +// DABsToTerraformRenameMap: when found the TF name is used and the tree descends for the +// remainder of the path. Array indices pass through unchanged without advancing the tree +// position. An unrecognised segment stops further renaming; remaining segments are kept +// as-is. Returns nil when path is nil. // Returns an error when path is a known DABs-only field with no Terraform equivalent. // // The path must be relative to the resource root (e.g. "tasks", not @@ -29,14 +30,14 @@ func DABsPathToTerraform(group string, path *structpath.PathNode) (*structpath.P return nil, fmt.Errorf("%s: %q is a DABs-only field with no Terraform equivalent", group, path) } - // For groups with a TF wrapper, prepend it only when the first segment is not a - // root-level TF field. Root-level fields and pass-through unknowns bypass the wrapper. + // For groups with a TF wrapper, prepend it only when the first segment is a known + // spec field. Unknown paths (root-level outputs, unrecognised segments) pass through unchanged. var result *structpath.PathNode if wrapper, ok := DABsToTerraformWrappers[group]; ok { segs := path.AsSlice() if len(segs) > 0 { if firstKey, ok := segs[0].StringKey(); ok { - if _, isRoot := DABsToTerraformRootFields[group][firstKey]; !isRoot { + if _, isWrapped := DABsToTerraformWrapperFields[group][firstKey]; isWrapped { result = structpath.NewDotString(nil, wrapper) } } From d3cde7759b5d61c1e751f34b8e30cfef18778517 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 17 Jun 2026 14:23:52 -0700 Subject: [PATCH 7/9] terraform_dabs_map: fix gofmt alignment in generate_test.go Co-authored-by: Isaac --- bundle/terraform_dabs_map/generate_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bundle/terraform_dabs_map/generate_test.go b/bundle/terraform_dabs_map/generate_test.go index d2784977483..d5d745ef4ef 100644 --- a/bundle/terraform_dabs_map/generate_test.go +++ b/bundle/terraform_dabs_map/generate_test.go @@ -75,14 +75,14 @@ var tfKnownSegments = map[string]bool{ } type groupResult struct { - group string - tfType string - hasTFType bool - renames map[string]string // TF path → DABs path (renamed fields only) - unwraps []string // TF paths that are structural wrappers (Unwrap: true) - dabsOnly map[string]bool // DABs clean paths with no Terraform equivalent - tfOnly map[string]bool // TF clean paths with no DABs equivalent - matchCount int // used for stats output only, not written to generated.go + group string + tfType string + hasTFType bool + renames map[string]string // TF path → DABs path (renamed fields only) + unwraps []string // TF paths that are structural wrappers (Unwrap: true) + dabsOnly map[string]bool // DABs clean paths with no Terraform equivalent + tfOnly map[string]bool // TF clean paths with no DABs equivalent + matchCount int // used for stats output only, not written to generated.go tfWrapperFirstSegs map[string]bool // first-level DABs field names that go under the wrapper (only set when unwraps is non-empty) } From f2e6aeea3ba9af38a831c43805c6e802f91c2693 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 18 Jun 2026 15:40:43 -0700 Subject: [PATCH 8/9] terraform_dabs_map: regenerate after rebase for postgres_databases Co-authored-by: Isaac --- bundle/terraform_dabs_map/generated.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bundle/terraform_dabs_map/generated.go b/bundle/terraform_dabs_map/generated.go index cff1ac70990..4776bc6f191 100644 --- a/bundle/terraform_dabs_map/generated.go +++ b/bundle/terraform_dabs_map/generated.go @@ -656,6 +656,10 @@ var DABsToTerraformWrapperFields = map[string]FieldSet{ "create_database_if_missing": {}, "postgres_database": {}, }, + "postgres_databases": { + "postgres_database": {}, + "role": {}, + }, "postgres_endpoints": { "autoscaling_limit_max_cu": {}, "autoscaling_limit_min_cu": {}, @@ -684,6 +688,7 @@ var DABsToTerraformWrapperFields = map[string]FieldSet{ "postgres_role": {}, }, "postgres_synced_tables": { + "accelerated_sync": {}, "branch": {}, "create_database_objects_if_missing": {}, "existing_pipeline_id": {}, @@ -693,5 +698,6 @@ var DABsToTerraformWrapperFields = map[string]FieldSet{ "scheduling_policy": {}, "source_table_full_name": {}, "timeseries_key": {}, + "type_overrides": {}, }, } From 5f016044073348d69cac1c7afacd96b0b974d3a0 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 18 Jun 2026 15:47:52 -0700 Subject: [PATCH 9/9] terraform_dabs_map: map model_id to registered_model_id The mlflow model's numeric ID is a state-computed field named model_id in the direct engine and registered_model_id in Terraform. It lives in RemoteType, not the config struct, and the two names are lexically unrelated, so the codegen's heuristic matcher cannot derive the rename. Add a small manualRenames table so the rename flows into both translation maps and registered_model_id is no longer classified Terraform-only. This lets a reference to registered_model_id resolve on both engines, covered by a new acceptance test. Co-authored-by: Isaac --- .../resource_deps/model_id_ref/databricks.yml | 16 ++++++++++++ .../resource_deps/model_id_ref/out.test.toml | 3 +++ .../resource_deps/model_id_ref/output.txt | 26 +++++++++++++++++++ .../bundle/resource_deps/model_id_ref/script | 19 ++++++++++++++ bundle/terraform_dabs_map/generate_test.go | 20 ++++++++++++++ bundle/terraform_dabs_map/generated.go | 11 +++++--- bundle/terraform_dabs_map/translate_test.go | 8 ++++++ 7 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 acceptance/bundle/resource_deps/model_id_ref/databricks.yml create mode 100644 acceptance/bundle/resource_deps/model_id_ref/out.test.toml create mode 100644 acceptance/bundle/resource_deps/model_id_ref/output.txt create mode 100644 acceptance/bundle/resource_deps/model_id_ref/script diff --git a/acceptance/bundle/resource_deps/model_id_ref/databricks.yml b/acceptance/bundle/resource_deps/model_id_ref/databricks.yml new file mode 100644 index 00000000000..7dedadc6432 --- /dev/null +++ b/acceptance/bundle/resource_deps/model_id_ref/databricks.yml @@ -0,0 +1,16 @@ +bundle: + name: test-bundle + +# A model's numeric ID is named registered_model_id in Terraform state and model_id in +# direct state. terraform_dabs_map bridges the two names, so a reference to +# registered_model_id resolves on both engines. +resources: + models: + my_model: + name: my-model + description: my model + + jobs: + consumer: + name: consumer + description: model id is ${resources.models.my_model.registered_model_id} diff --git a/acceptance/bundle/resource_deps/model_id_ref/out.test.toml b/acceptance/bundle/resource_deps/model_id_ref/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/bundle/resource_deps/model_id_ref/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resource_deps/model_id_ref/output.txt b/acceptance/bundle/resource_deps/model_id_ref/output.txt new file mode 100644 index 00000000000..c10a3c79920 --- /dev/null +++ b/acceptance/bundle/resource_deps/model_id_ref/output.txt @@ -0,0 +1,26 @@ + +=== the consumer job implicitly depends on the model +>>> [CLI] bundle plan +create jobs.consumer +create models.my_model + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +=== after deploy, registered_model_id is resolved to the model's numeric id +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> print_requests +{ + "description": "my model", + "name": "my-model", + "path": "/api/2.0/mlflow/registered-models/create" +} +{ + "description": "model id is [MY_MODEL_ID]", + "name": "consumer", + "path": "/api/2.2/jobs/create" +} diff --git a/acceptance/bundle/resource_deps/model_id_ref/script b/acceptance/bundle/resource_deps/model_id_ref/script new file mode 100644 index 00000000000..b678e97dc5c --- /dev/null +++ b/acceptance/bundle/resource_deps/model_id_ref/script @@ -0,0 +1,19 @@ +# Show the create requests with the model-id reference resolved in the job description. +print_requests() { + jq --sort-keys 'select(.method != "GET" and (.path | contains("/jobs/create") or contains("/mlflow/registered-models/create"))) | {path, name: (.body.name // .body.new_settings.name), description: (.body.description // .body.new_settings.description)}' < out.requests.txt + rm out.requests.txt +} + +title "the consumer job implicitly depends on the model" +trace $CLI bundle plan + +title "after deploy, registered_model_id is resolved to the model's numeric id" +trace $CLI bundle deploy + +# Bind the model's exact numeric id to [MY_MODEL_ID] so the resolved reference is matched +# precisely rather than by the broad [NUMID] pattern. The id comes from the API (independent +# of the deploy engine), so terraform and direct must both resolve to this exact value. +model_id=$($CLI model-registry get-model my-model | jq -r '.registered_model_databricks.id') +add_repl.py "$model_id" MY_MODEL_ID + +trace print_requests diff --git a/bundle/terraform_dabs_map/generate_test.go b/bundle/terraform_dabs_map/generate_test.go index d5d745ef4ef..f576949b086 100644 --- a/bundle/terraform_dabs_map/generate_test.go +++ b/bundle/terraform_dabs_map/generate_test.go @@ -74,6 +74,15 @@ var tfKnownSegments = map[string]bool{ "provider_config": true, // Terraform provider metadata, not a DABs concept } +// manualRenames maps DABs group → DABs field path → TF field path for renames the +// heuristic matcher cannot derive. These are state-computed fields that live in the +// RemoteType (not the config struct), whose DABs and TF names are semantically equivalent +// but lexically unrelated, so neither exact nor stemmed matching can connect them. +var manualRenames = map[string]map[string]string{ + // models.model_id is the numeric model ID; TF names it registered_model_id. + "models": {"model_id": "registered_model_id"}, +} + type groupResult struct { group string tfType string @@ -264,6 +273,17 @@ func buildGroup(group string, adapter *dresources.Adapter) (groupResult, error) } } + // Apply manual renames for fields the heuristic matcher cannot derive. These connect a + // state-computed DABs field to its differently-named TF counterpart; recording them as + // renames also marks the TF field matched so it does not surface as Terraform-only. + for dabsPath, tfPath := range manualRenames[group] { + if !tfFields[tfPath] { + return groupResult{}, fmt.Errorf("manual rename %s.%s: TF field %q not found", group, dabsPath, tfPath) + } + res.renames[tfPath] = dabsPath + matchedTF[tfPath] = true + } + // Step 4: remaining unmatched fields. for dabs := range dabsFields { if !matchedDABs[dabs] && !dabsKnownFields[topSegment(dabs)] { diff --git a/bundle/terraform_dabs_map/generated.go b/bundle/terraform_dabs_map/generated.go index 4776bc6f191..dd974dd626e 100644 --- a/bundle/terraform_dabs_map/generated.go +++ b/bundle/terraform_dabs_map/generated.go @@ -15,7 +15,7 @@ package terraform_dabs_map // jobs / databricks_job: 10 dabs-only // jobs / databricks_job: 257 tf-only // model_serving_endpoints / databricks_model_serving: 2 tf-only -// models / databricks_mlflow_model: 1 tf-only +// models / databricks_mlflow_model: 1 renames // pipelines / databricks_pipeline: 3 renames // pipelines / databricks_pipeline: 7 dabs-only // pipelines / databricks_pipeline: 2 tf-only @@ -56,6 +56,9 @@ var TerraformToDABsFieldMap = map[string]RenameTree{ "library": {NewName: "libraries"}, }}, }, + "models": { + "registered_model_id": {NewName: "model_id"}, + }, "pipelines": { "cluster": {NewName: "clusters"}, "library": {NewName: "libraries"}, @@ -570,9 +573,6 @@ var TerraformOnlyFields = map[string]FieldSet{ "endpoint_url": {}, "serving_endpoint_id": {}, }, - "models": { - "registered_model_id": {}, - }, "pipelines": { "expected_last_modified": {}, "url": {}, @@ -618,6 +618,9 @@ var DABsToTerraformRenameMap = map[string]RenameTree{ "libraries": {NewName: "library"}, }}, }, + "models": { + "model_id": {NewName: "registered_model_id"}, + }, "pipelines": { "clusters": {NewName: "cluster"}, "libraries": {NewName: "library"}, diff --git a/bundle/terraform_dabs_map/translate_test.go b/bundle/terraform_dabs_map/translate_test.go index bdcea91192a..2f3ae5cd7ae 100644 --- a/bundle/terraform_dabs_map/translate_test.go +++ b/bundle/terraform_dabs_map/translate_test.go @@ -162,6 +162,14 @@ func TestTerraformPathToDABs(t *testing.T) { dabsPath: "name", }, + // Manual rename: registered_model_id is a state-computed field whose DABs name + // (model_id) is lexically unrelated, so it is wired up via manualRenames in codegen. + { + group: "models", + terrPath: "registered_model_id", + dabsPath: "model_id", + }, + // Terraform-only fields: must return an error { group: "jobs",