Skip to content

Commit df83b99

Browse files
committed
feat: return mutated row fields from BEFORE triggers via PreDispatchResult
fire_pre_dispatch_triggers now returns PreDispatchResult instead of a plain bool. The Handled variant signals that an INSTEAD OF trigger consumed the write (unchanged behaviour). The new Proceed variant carries an optional mutated_fields map populated when a BEFORE INSERT or BEFORE UPDATE trigger modifies the row. patch_task_with_mutated_fields applies those mutations back onto the PhysicalTask before it is dispatched to the Data Plane, replacing the value payload in PointPut/Upsert ops and re-deriving field-level update entries for PointUpdate ops.
1 parent 556e316 commit df83b99

1 file changed

Lines changed: 81 additions & 16 deletions

File tree

nodedb/src/control/trigger/dml_hook.rs

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)