44 "context"
55 "errors"
66 "fmt"
7+ "maps"
78 "reflect"
89
910 "github.com/databricks/cli/bundle/deployplan"
@@ -43,7 +44,16 @@ type IResource interface {
4344 // [Optional] FieldTriggers returns actions to trigger when given fields are changed.
4445 // Keys are field paths (e.g., "name", "catalog_name"). Values are actions.
4546 // Unspecified changed fields default to ActionTypeUpdate.
46- FieldTriggers () map [string ]deployplan.ActionType
47+ //
48+ // FieldTriggers(true) is applied on every change between state (last deployed config)
49+ // and new state (current config) to determine action based on config changes.
50+ //
51+ // FieldTriggers(false) is called on every change between state and remote state to
52+ // determine action based on remote drift.
53+ //
54+ // Note: these functions are called once per resource implementation initialization,
55+ // not once per resource.
56+ FieldTriggers (isLocal bool ) map [string ]deployplan.ActionType
4757}
4858
4959// IResourceNoRefresh describes additional methods for resource to implement.
@@ -77,8 +87,9 @@ type IResourceNoRefresh interface {
7787 // [Optional] WaitAfterUpdate waits for the resource to become ready after update.
7888 WaitAfterUpdate (ctx context.Context , newState any ) error
7989
80- // [Optional] ClassifyChange classifies a set of changes using custom logic.
81- ClassifyChange (change structdiff.Change , remoteState any ) (deployplan.ActionType , error )
90+ // [Optional] ClassifyChange classifies a change using custom logic.
91+ // The isLocal parameter indicates whether this is a local change (true) or remote change (false).
92+ ClassifyChange (change structdiff.Change , remoteState any , isLocal bool ) (deployplan.ActionType , error )
8293}
8394
8495// IResourceWithRefresh is an alternative to IResourceNoRefresh but every method can return remoteState.
@@ -121,7 +132,8 @@ type Adapter struct {
121132 classifyChange * calladapt.BoundCaller
122133 doResize * calladapt.BoundCaller
123134
124- fieldTriggers map [string ]deployplan.ActionType
135+ fieldTriggersLocal map [string ]deployplan.ActionType
136+ fieldTriggersRemote map [string ]deployplan.ActionType
125137}
126138
127139func NewAdapter (typedNil any , client * databricks.WorkspaceClient ) (* Adapter , error ) {
@@ -138,18 +150,19 @@ func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, err
138150 }
139151 impl := outs [0 ]
140152 adapter := & Adapter {
141- prepareState : nil ,
142- remapState : nil ,
143- doRefresh : nil ,
144- doDelete : nil ,
145- doCreate : nil ,
146- doUpdate : nil ,
147- doUpdateWithID : nil ,
148- doResize : nil ,
149- waitAfterCreate : nil ,
150- waitAfterUpdate : nil ,
151- classifyChange : nil ,
152- fieldTriggers : map [string ]deployplan.ActionType {},
153+ prepareState : nil ,
154+ remapState : nil ,
155+ doRefresh : nil ,
156+ doDelete : nil ,
157+ doCreate : nil ,
158+ doUpdate : nil ,
159+ doUpdateWithID : nil ,
160+ doResize : nil ,
161+ waitAfterCreate : nil ,
162+ waitAfterUpdate : nil ,
163+ classifyChange : nil ,
164+ fieldTriggersLocal : map [string ]deployplan.ActionType {},
165+ fieldTriggersRemote : map [string ]deployplan.ActionType {},
153166 }
154167
155168 err = adapter .initMethods (impl )
@@ -163,14 +176,28 @@ func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, err
163176 return nil , err
164177 }
165178 if triggerCall != nil {
166- outs , err := triggerCall . Call ()
167- if err != nil || len ( outs ) != 1 {
168- return nil , fmt . Errorf ( "failed to call FieldTriggers: %w" , err )
179+ // Validate FieldTriggers signature: func(bool) map[string]deployplan.ActionType
180+ if len ( triggerCall . InTypes ) != 1 || triggerCall . InTypes [ 0 ] != reflect . TypeOf ( false ) {
181+ return nil , errors . New ( "FieldTriggers must take a single bool parameter (isLocal)" )
169182 }
170- fields := outs [0 ].(map [string ]deployplan.ActionType )
171- adapter .fieldTriggers = make (map [string ]deployplan.ActionType , len (fields ))
172- for k , v := range fields {
173- adapter .fieldTriggers [k ] = v
183+ if len (triggerCall .OutTypes ) != 1 {
184+ return nil , errors .New ("FieldTriggers must return a single value" )
185+ }
186+ expectedReturnType := reflect .TypeOf (map [string ]deployplan.ActionType {})
187+ if triggerCall .OutTypes [0 ] != expectedReturnType {
188+ return nil , fmt .Errorf ("FieldTriggers must return map[string]deployplan.ActionType, got %v" , triggerCall .OutTypes [0 ])
189+ }
190+
191+ // Call with isLocal=true for local triggers
192+ adapter .fieldTriggersLocal , err = loadFieldTriggers (triggerCall , true )
193+ if err != nil {
194+ return nil , err
195+ }
196+
197+ // Call with isLocal=false for remote triggers
198+ adapter .fieldTriggersRemote , err = loadFieldTriggers (triggerCall , false )
199+ if err != nil {
200+ return nil , err
174201 }
175202 }
176203
@@ -182,6 +209,18 @@ func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, err
182209 return adapter , nil
183210}
184211
212+ // loadFieldTriggers calls FieldTriggers with isLocal parameter and returns the resulting map.
213+ func loadFieldTriggers (triggerCall * calladapt.BoundCaller , isLocal bool ) (map [string ]deployplan.ActionType , error ) {
214+ outs , err := triggerCall .Call (isLocal )
215+ if err != nil || len (outs ) != 1 {
216+ return nil , fmt .Errorf ("failed to call FieldTriggers(%v): %w" , isLocal , err )
217+ }
218+ fields := outs [0 ].(map [string ]deployplan.ActionType )
219+ result := make (map [string ]deployplan.ActionType , len (fields ))
220+ maps .Copy (result , fields )
221+ return result , nil
222+ }
223+
185224func (a * Adapter ) initMethods (resource any ) error {
186225 err := calladapt .EnsureNoExtraMethods (resource , calladapt .TypeOf [IResource ](), calladapt .TypeOf [IResourceNoRefresh ](), calladapt .TypeOf [IResourceWithRefresh ]())
187226 if err != nil {
@@ -332,7 +371,10 @@ func (a *Adapter) validate() error {
332371 }
333372
334373 if a .classifyChange != nil {
335- validations = append (validations , "ClassifyChange remoteState" , a .classifyChange .InTypes [1 ], remoteType )
374+ validations = append (validations ,
375+ "ClassifyChange remoteState" , a .classifyChange .InTypes [1 ], remoteType ,
376+ "ClassifyChange isLocal" , a .classifyChange .InTypes [2 ], reflect .TypeOf (false ),
377+ )
336378 }
337379
338380 err = validateTypes (validations ... )
@@ -342,7 +384,12 @@ func (a *Adapter) validate() error {
342384
343385 // FieldTriggers validation
344386 hasUpdateWithIDTrigger := false
345- for _ , action := range a .fieldTriggers {
387+ for _ , action := range a .fieldTriggersLocal {
388+ if action == deployplan .ActionTypeUpdateWithID {
389+ hasUpdateWithIDTrigger = true
390+ }
391+ }
392+ for _ , action := range a .fieldTriggersRemote {
346393 if action == deployplan .ActionTypeUpdateWithID {
347394 hasUpdateWithIDTrigger = true
348395 }
@@ -485,10 +532,20 @@ func (a *Adapter) DoResize(ctx context.Context, id string, newState any) error {
485532 return err
486533}
487534
488- // ClassifyByTriggers classifies a single using FieldTriggers.
535+ // classifyByTriggers classifies a change using FieldTriggers.
489536// Defaults to ActionTypeUpdate.
490- func (a * Adapter ) ClassifyByTriggers (change structdiff.Change ) deployplan.ActionType {
491- action , ok := a .fieldTriggers [change .Path .String ()]
537+ // The isLocal parameter determines which trigger map to use:
538+ // - isLocal=true uses triggers from FieldTriggers(true)
539+ // - isLocal=false uses triggers from FieldTriggers(false)
540+ func (a * Adapter ) classifyByTriggers (change structdiff.Change , isLocal bool ) deployplan.ActionType {
541+ var triggers map [string ]deployplan.ActionType
542+ if isLocal {
543+ triggers = a .fieldTriggersLocal
544+ } else {
545+ triggers = a .fieldTriggersRemote
546+ }
547+
548+ action , ok := triggers [change .Path .String ()]
492549 if ok {
493550 return action
494551 }
@@ -539,21 +596,25 @@ func (a *Adapter) WaitAfterUpdate(ctx context.Context, newState any) (any, error
539596 }
540597}
541598
542- func (a * Adapter ) ClassifyChange (change structdiff.Change , remoteState any ) (deployplan.ActionType , error ) {
543- // If ClassifyChange is not implemented, use FieldTriggers.
544- if a .classifyChange == nil {
545- return a .ClassifyByTriggers (change ), nil
546- }
599+ // ClassifyChange classifies a change using custom logic or FieldTriggers.
600+ // The isLocal parameter determines whether this is a local or remote change:
601+ // - isLocal=true: classifying local changes (user modifications)
602+ // - isLocal=false: classifying remote changes (drift detection)
603+ func (a * Adapter ) ClassifyChange (change structdiff.Change , remoteState any , isLocal bool ) (deployplan.ActionType , error ) {
604+ actionType := deployplan .ActionTypeUndefined
547605
548- outs , err := a .classifyChange .Call (change , remoteState )
549- if err != nil {
550- return deployplan .ActionTypeSkip , err
606+ // If ClassifyChange is implemented, use it.
607+ if a .classifyChange != nil {
608+ outs , err := a .classifyChange .Call (change , remoteState , isLocal )
609+ if err != nil {
610+ return deployplan .ActionTypeUndefined , err
611+ }
612+ actionType = outs [0 ].(deployplan.ActionType )
551613 }
552614
553- actionType := outs [0 ].(deployplan.ActionType )
554- // If the action type is unset, use FieldTriggers.
555- if actionType == deployplan .ActionTypeUnset {
556- return a .ClassifyByTriggers (change ), nil
615+ // If ClassifyChange is not implemented or is implemented but returns undefined, use FieldTriggers.
616+ if actionType == deployplan .ActionTypeUndefined {
617+ return a .classifyByTriggers (change , isLocal ), nil
557618 }
558619 return actionType , nil
559620}
0 commit comments