@@ -163,6 +163,47 @@ fn deserialize_value_to_fields(value: &[u8]) -> HashMap<String, nodedb_types::Va
163163 HashMap :: new ( )
164164}
165165
166+ /// Patch a `PhysicalTask` with mutated fields from a BEFORE trigger.
167+ ///
168+ /// Serializes the mutated fields to MessagePack and replaces the value
169+ /// payload in the underlying `PointPut` or `Upsert` operation.
170+ /// For `PointUpdate`, the updates are re-derived from the mutated fields.
171+ pub fn patch_task_with_mutated_fields (
172+ task : & mut crate :: control:: planner:: physical:: PhysicalTask ,
173+ mutated : & HashMap < String , nodedb_types:: Value > ,
174+ ) {
175+ use crate :: bridge:: envelope:: PhysicalPlan ;
176+
177+ let json_obj: serde_json:: Map < String , serde_json:: Value > = mutated
178+ . iter ( )
179+ . map ( |( k, v) | ( k. clone ( ) , serde_json:: Value :: from ( v. clone ( ) ) ) )
180+ . collect ( ) ;
181+ let json_val = serde_json:: Value :: Object ( json_obj) ;
182+ let new_bytes = match nodedb_types:: value_to_msgpack ( & nodedb_types:: Value :: from ( json_val) ) {
183+ Ok ( b) => b,
184+ Err ( _) => return ,
185+ } ;
186+
187+ match & mut task. plan {
188+ PhysicalPlan :: Document ( DocumentOp :: PointPut { value, .. } )
189+ | PhysicalPlan :: Document ( DocumentOp :: Upsert { value, .. } ) => {
190+ * value = new_bytes;
191+ }
192+ PhysicalPlan :: Document ( DocumentOp :: PointUpdate { updates, .. } ) => {
193+ // Re-derive field-level updates from the full mutated row.
194+ * updates = mutated
195+ . iter ( )
196+ . filter_map ( |( k, v) | {
197+ nodedb_types:: value_to_msgpack ( v)
198+ . ok ( )
199+ . map ( |b| ( k. clone ( ) , b) )
200+ } )
201+ . collect ( ) ;
202+ }
203+ _ => { }
204+ }
205+ }
206+
166207/// Fetch the current document as a field map (for OLD row bindings).
167208///
168209/// Issues a PointGet to the Data Plane and deserializes the response.
@@ -230,11 +271,24 @@ pub fn has_triggers(state: &SharedState, tenant_id: TenantId, collection: &str)
230271 . is_empty ( )
231272}
232273
233- /// Fire BEFORE + INSTEAD OF triggers for a point write, returning whether
234- /// the dispatch should proceed.
274+ /// Result of firing BEFORE + INSTEAD OF triggers before dispatch.
275+ pub enum PreDispatchResult {
276+ /// INSTEAD OF trigger handled the write — skip normal dispatch.
277+ Handled ,
278+ /// Proceed with dispatch. If a BEFORE trigger mutated the row,
279+ /// `mutated_fields` contains the new fields to use instead of the original.
280+ Proceed {
281+ mutated_fields : Option < HashMap < String , nodedb_types:: Value > > ,
282+ } ,
283+ }
284+
285+ /// Fire BEFORE + INSTEAD OF triggers for a point write.
235286///
236- /// Returns `true` if the caller should proceed with normal dispatch.
237- /// Returns `false` if an INSTEAD OF trigger handled the write (skip dispatch).
287+ /// Returns `PreDispatchResult::Proceed` if the caller should dispatch normally.
288+ /// The `mutated_fields` inside may contain fields modified by a BEFORE trigger —
289+ /// the caller MUST use these to patch the task before dispatch.
290+ ///
291+ /// Returns `PreDispatchResult::Handled` if an INSTEAD OF trigger handled the write.
238292///
239293/// On BEFORE trigger error (RAISE EXCEPTION), the error propagates and
240294/// the caller should abort the write.
@@ -246,7 +300,7 @@ pub async fn fire_pre_dispatch_triggers(
246300 info : & DmlWriteInfo ,
247301 old_row : & Option < HashMap < String , nodedb_types:: Value > > ,
248302 cascade_depth : u32 ,
249- ) -> crate :: Result < bool > {
303+ ) -> crate :: Result < PreDispatchResult > {
250304 // Check INSTEAD OF first — if it handles the write, skip everything else.
251305 match info. event {
252306 DmlEvent :: Insert => {
@@ -261,7 +315,7 @@ pub async fn fire_pre_dispatch_triggers(
261315 )
262316 . await ?
263317 {
264- InsteadOfResult :: Handled => return Ok ( false ) ,
318+ InsteadOfResult :: Handled => return Ok ( PreDispatchResult :: Handled ) ,
265319 InsteadOfResult :: NoTrigger => { }
266320 }
267321 }
@@ -281,7 +335,7 @@ pub async fn fire_pre_dispatch_triggers(
281335 )
282336 . await ?
283337 {
284- InsteadOfResult :: Handled => return Ok ( false ) ,
338+ InsteadOfResult :: Handled => return Ok ( PreDispatchResult :: Handled ) ,
285339 InsteadOfResult :: NoTrigger => { }
286340 }
287341 }
@@ -298,19 +352,17 @@ pub async fn fire_pre_dispatch_triggers(
298352 )
299353 . await ?
300354 {
301- InsteadOfResult :: Handled => return Ok ( false ) ,
355+ InsteadOfResult :: Handled => return Ok ( PreDispatchResult :: Handled ) ,
302356 InsteadOfResult :: NoTrigger => { }
303357 }
304358 }
305359 }
306360
307- // Fire BEFORE triggers.
308- match info. event {
361+ // Fire BEFORE triggers — capture mutated fields from INSERT/UPDATE .
362+ let mutated_fields = match info. event {
309363 DmlEvent :: Insert => {
310364 if let Some ( ref new_fields) = info. new_fields {
311- // BEFORE INSERT can validate/reject. Mutation support is via the
312- // checklist's TODO — for now it validates only.
313- fire_before:: fire_before_insert (
365+ let mutated = fire_before:: fire_before_insert (
314366 state,
315367 identity,
316368 tenant_id,
@@ -319,13 +371,20 @@ pub async fn fire_pre_dispatch_triggers(
319371 cascade_depth,
320372 )
321373 . await ?;
374+ if mutated != * new_fields {
375+ Some ( mutated)
376+ } else {
377+ None
378+ }
379+ } else {
380+ None
322381 }
323382 }
324383 DmlEvent :: Update => {
325384 let empty = HashMap :: new ( ) ;
326385 let old_fields = old_row. as_ref ( ) . unwrap_or ( & empty) ;
327386 let new_fields = info. new_fields . as_ref ( ) . unwrap_or ( & empty) ;
328- fire_before:: fire_before_update (
387+ let mutated = fire_before:: fire_before_update (
329388 state,
330389 identity,
331390 tenant_id,
@@ -335,6 +394,11 @@ pub async fn fire_pre_dispatch_triggers(
335394 cascade_depth,
336395 )
337396 . await ?;
397+ if mutated != * new_fields {
398+ Some ( mutated)
399+ } else {
400+ None
401+ }
338402 }
339403 DmlEvent :: Delete => {
340404 let empty = HashMap :: new ( ) ;
@@ -348,10 +412,11 @@ pub async fn fire_pre_dispatch_triggers(
348412 cascade_depth,
349413 )
350414 . await ?;
415+ None
351416 }
352- }
417+ } ;
353418
354- Ok ( true )
419+ Ok ( PreDispatchResult :: Proceed { mutated_fields } )
355420}
356421
357422/// Fire SYNC AFTER ROW + SYNC AFTER STATEMENT triggers post-dispatch.
0 commit comments