@@ -50,9 +50,9 @@ func (b *DeploymentBundle) InitForApply(ctx context.Context, client *databricks.
5050 return err
5151 }
5252
53- // Load NewState.Value from json.RawMessage into typed structs .
53+ // Eagerly parse all StructVarJSON entries to catch parsing errors early .
5454 // When the plan is read from JSON, Value contains raw JSON bytes.
55- // We need to parse them into the correct types for each resource .
55+ // We parse them into typed structs and cache them for later use .
5656 for resourceKey , entry := range plan .Plan {
5757 if entry .NewState == nil || len (entry .NewState .Value ) == 0 {
5858 continue
@@ -63,9 +63,12 @@ func (b *DeploymentBundle) InitForApply(ctx context.Context, client *databricks.
6363 return fmt .Errorf ("cannot convert plan entry %s: %w" , resourceKey , err )
6464 }
6565
66- if err := entry .NewState .Load (adapter .StateType ()); err != nil {
66+ sv , err := entry .NewState .ToStructVar (adapter .StateType ())
67+ if err != nil {
6768 return fmt .Errorf ("cannot load plan entry %s: %w" , resourceKey , err )
6869 }
70+
71+ b .StructVarCache .Store (resourceKey , sv )
6972 }
7073
7174 b .Plan = plan
@@ -153,7 +156,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
153156
154157 // Process all references in the resource using Refs map
155158 // Refs maps path inside resource to references e.g. "${resources.jobs.foo.id} ${resources.jobs.foo.name}"
156- if ! b .resolveReferences (ctx , entry , errorPrefix , true , adapter . StateType () ) {
159+ if ! b .resolveReferences (ctx , resourceKey , entry , errorPrefix , true ) {
157160 return false
158161 }
159162
@@ -180,7 +183,8 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
180183 // for integers: compare 0 with actual object ID. As long as real object IDs are never 0 we're good.
181184 // Once we add non-id fields or add per-field details to "bundle plan", we must read dynamic data and deal with references as first class citizen.
182185 // This means distinguishing between 0 that are actually object ids and 0 that are there because typed struct integer cannot contain ${...} string.
183- localDiff , err := structdiff .GetStructDiff (savedState , entry .NewState .GetValue (), adapter .KeyedSlices ())
186+ sv , _ := b .StructVarCache .Load (resourceKey )
187+ localDiff , err := structdiff .GetStructDiff (savedState , sv .Value , adapter .KeyedSlices ())
184188 if err != nil {
185189 logdiag .LogError (ctx , fmt .Errorf ("%s: diffing local state: %w" , errorPrefix , err ))
186190 return false
@@ -374,13 +378,19 @@ func (b *DeploymentBundle) LookupReferenceLocal(ctx context.Context, path *struc
374378 return nil , fmt .Errorf ("internal error: %s: action is %q missing new_state" , targetResourceKey , targetEntry .Action )
375379 }
376380
377- _ , isUnresolved := targetEntry .NewState .Refs [fieldPathS ]
381+ // Get StructVar from cache
382+ sv , ok := b .StructVarCache .Load (targetResourceKey )
383+ if ! ok {
384+ return nil , fmt .Errorf ("internal error: %s: missing cached StructVar" , targetResourceKey )
385+ }
386+
387+ _ , isUnresolved := sv .Refs [fieldPathS ]
378388 if isUnresolved {
379389 // The value that is requested is itself a reference; this means it will be resolved after apply
380390 return nil , errDelayed
381391 }
382392
383- localConfig := targetEntry . NewState . GetValue ()
393+ localConfig := sv . Value
384394
385395 targetGroup := config .GetResourceTypeFromKey (targetResourceKey )
386396 adapter := b .Adapters [targetGroup ]
@@ -437,17 +447,28 @@ func (b *DeploymentBundle) LookupReferenceLocal(ctx context.Context, path *struc
437447 return nil , errDelayed
438448}
439449
450+ // getStructVar returns the cached StructVar for the given resource key.
451+ // The StructVar must have been eagerly loaded during plan creation or InitForApply.
452+ func (b * DeploymentBundle ) getStructVar (resourceKey string ) (* structvar.StructVar , error ) {
453+ sv , ok := b .StructVarCache .Load (resourceKey )
454+ if ! ok {
455+ return nil , fmt .Errorf ("internal error: StructVar not found in cache for %s" , resourceKey )
456+ }
457+ return sv , nil
458+ }
459+
440460// resolveReferences processes all references in entry.NewState.Refs.
441461// If isLocal is true, uses LookupReferenceLocal (for planning phase).
442462// If isLocal is false, uses LookupReferenceRemote (for apply phase).
443- func (b * DeploymentBundle ) resolveReferences (ctx context.Context , entry * deployplan.PlanEntry , errorPrefix string , isLocal bool , stateType reflect.Type ) bool {
444- if err := entry .NewState .Load (stateType ); err != nil {
445- logdiag .LogError (ctx , fmt .Errorf ("%s: cannot load state: %w" , errorPrefix , err ))
463+ func (b * DeploymentBundle ) resolveReferences (ctx context.Context , resourceKey string , entry * deployplan.PlanEntry , errorPrefix string , isLocal bool ) bool {
464+ sv , err := b .getStructVar (resourceKey )
465+ if err != nil {
466+ logdiag .LogError (ctx , fmt .Errorf ("%s: %w" , errorPrefix , err ))
446467 return false
447468 }
448469
449470 var resolved bool
450- for fieldPathStr , refString := range entry . NewState .Refs {
471+ for fieldPathStr , refString := range sv .Refs {
451472 refs , ok := dynvar .NewRef (dyn .V (refString ))
452473 if ! ok {
453474 logdiag .LogError (ctx , fmt .Errorf ("%s: cannot parse %q" , errorPrefix , refString ))
@@ -480,7 +501,7 @@ func (b *DeploymentBundle) resolveReferences(ctx context.Context, entry *deployp
480501 }
481502 }
482503
483- err = entry . NewState .ResolveRef (ref , value )
504+ err = sv .ResolveRef (ref , value )
484505 if err != nil {
485506 logdiag .LogError (ctx , fmt .Errorf ("%s: cannot update %s with value of %q: %w" , errorPrefix , fieldPathStr , ref , err ))
486507 return false
@@ -489,12 +510,14 @@ func (b *DeploymentBundle) resolveReferences(ctx context.Context, entry *deployp
489510 }
490511 }
491512
513+ // Sync resolved values back to StructVarJSON for serialization
492514 if resolved {
493- if err := entry .NewState . Save ( ); err != nil {
515+ if err := sv . SyncToJSON ( entry .NewState ); err != nil {
494516 logdiag .LogError (ctx , fmt .Errorf ("%s: cannot save state: %w" , errorPrefix , err ))
495517 return false
496518 }
497519 }
520+
498521 return true
499522}
500523
@@ -574,14 +597,14 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root
574597 if err != nil {
575598 return nil , err
576599 }
577- inputConfig = inputConfigStructVar .GetValue ()
600+ inputConfig = inputConfigStructVar .Value
578601 baseRefs = inputConfigStructVar .Refs
579602 } else if strings .HasSuffix (node , ".grants" ) {
580603 inputConfigStructVar , err := dresources .PrepareGrantsInputConfig (inputConfig , node )
581604 if err != nil {
582605 return nil , err
583606 }
584- inputConfig = inputConfigStructVar .GetValue ()
607+ inputConfig = inputConfigStructVar .Value
585608 baseRefs = inputConfigStructVar .Refs
586609 }
587610
@@ -642,14 +665,23 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root
642665 return strings .Compare (a .Label , b .Label )
643666 })
644667
645- newState , err := structvar .NewStructVar (newStateConfig , refs )
668+ newState := & structvar.StructVar {
669+ Value : newStateConfig ,
670+ Refs : refs ,
671+ }
672+
673+ // Store in cache for use during planning phase
674+ b .StructVarCache .Store (node , newState )
675+
676+ // Convert to JSON for serialization in plan
677+ newStateJSON , err := newState .ToJSON ()
646678 if err != nil {
647- return nil , fmt .Errorf ("%s: cannot create state: %w" , node , err )
679+ return nil , fmt .Errorf ("%s: cannot serialize state: %w" , node , err )
648680 }
649681
650682 e := deployplan.PlanEntry {
651683 DependsOn : dependsOn ,
652- NewState : newState ,
684+ NewState : newStateJSON ,
653685 }
654686
655687 p .Plan [node ] = & e
0 commit comments