@@ -96,8 +96,11 @@ impl NodeDbPgHandler {
9696 return self . forward_sql ( sql, tenant_id, leader) . await ;
9797 }
9898
99+ let needs_dedup = tasks. iter ( ) . any ( |t| t. post_dedup ) ;
100+ let mut dedup_payloads: Vec < Vec < u8 > > = Vec :: new ( ) ;
101+ let mut dedup_plan_kind = None ;
99102 let mut responses = Vec :: with_capacity ( tasks. len ( ) ) ;
100- for task in tasks {
103+ for mut task in tasks {
101104 if task. tenant_id != tenant_id {
102105 tracing:: error!(
103106 expected = %tenant_id,
@@ -131,6 +134,7 @@ impl NodeDbPgHandler {
131134
132135 let plan_kind = describe_plan ( & task. plan ) ;
133136 let collection_for_si = extract_collection ( & task. plan ) . map ( String :: from) ;
137+ let resp_post_dedup = task. post_dedup ;
134138
135139 // --- Trigger interception for DML writes ---
136140 let dml_info = crate :: control:: trigger:: dml_hook:: classify_dml_write ( & task. plan ) ;
@@ -157,7 +161,8 @@ impl NodeDbPgHandler {
157161 } ;
158162
159163 if let Some ( ref info) = dml_info {
160- let proceed = crate :: control:: trigger:: dml_hook:: fire_pre_dispatch_triggers (
164+ use crate :: control:: trigger:: dml_hook:: PreDispatchResult ;
165+ let result = crate :: control:: trigger:: dml_hook:: fire_pre_dispatch_triggers (
161166 & self . state ,
162167 identity,
163168 tenant_id,
@@ -175,10 +180,23 @@ impl NodeDbPgHandler {
175180 ) ) )
176181 } ) ?;
177182
178- if !proceed {
179- // INSTEAD OF trigger handled the write — skip dispatch.
180- responses. push ( Response :: Execution ( Tag :: new ( "OK" ) ) ) ;
181- continue ;
183+ match result {
184+ PreDispatchResult :: Handled => {
185+ // INSTEAD OF trigger handled the write — skip dispatch.
186+ responses. push ( Response :: Execution ( Tag :: new ( "OK" ) ) ) ;
187+ continue ;
188+ }
189+ PreDispatchResult :: Proceed {
190+ mutated_fields : Some ( ref fields) ,
191+ } => {
192+ // BEFORE trigger mutated the row — patch the task.
193+ crate :: control:: trigger:: dml_hook:: patch_task_with_mutated_fields (
194+ & mut task, fields,
195+ ) ;
196+ }
197+ PreDispatchResult :: Proceed {
198+ mutated_fields : None ,
199+ } => { }
182200 }
183201 }
184202
@@ -237,7 +255,19 @@ impl NodeDbPgHandler {
237255 . record_read ( addr, collection, String :: new ( ) , resp. watermark_lsn ) ;
238256 }
239257
240- responses. push ( payload_to_response ( & resp. payload , plan_kind) ) ;
258+ if needs_dedup && resp_post_dedup {
259+ dedup_payloads. push ( resp. payload . to_vec ( ) ) ;
260+ dedup_plan_kind = Some ( plan_kind) ;
261+ } else {
262+ responses. push ( payload_to_response ( & resp. payload , plan_kind) ) ;
263+ }
264+ }
265+
266+ // UNION DISTINCT: merge all sub-query payloads and deduplicate rows.
267+ if needs_dedup && !dedup_payloads. is_empty ( ) {
268+ let kind = dedup_plan_kind. unwrap_or ( PlanKind :: MultiRow ) ;
269+ let merged = dedup_union_payloads ( & dedup_payloads) ;
270+ responses. push ( payload_to_response ( & merged, kind) ) ;
241271 }
242272
243273 Ok ( responses)
@@ -370,3 +400,97 @@ impl NodeDbPgHandler {
370400 }
371401 }
372402}
403+
404+ /// Merge multiple Data Plane response payloads and deduplicate rows (UNION DISTINCT).
405+ ///
406+ /// Each payload is a msgpack-encoded array of rows. Deduplication is performed
407+ /// at the binary level: each row's raw msgpack bytes serve as the canonical key,
408+ /// eliminating the decode → JSON string → re-encode round-trip.
409+ ///
410+ /// Output: a single msgpack array containing all unique rows in encounter order.
411+ fn dedup_union_payloads ( payloads : & [ Vec < u8 > ] ) -> Vec < u8 > {
412+ use nodedb_query:: msgpack_scan;
413+
414+ let mut seen: std:: collections:: HashSet < Vec < u8 > > = std:: collections:: HashSet :: new ( ) ;
415+ // Collect raw byte slices of unique rows before writing output.
416+ let mut unique_row_bytes: Vec < Vec < u8 > > = Vec :: new ( ) ;
417+
418+ for payload in payloads {
419+ if payload. is_empty ( ) {
420+ continue ;
421+ }
422+
423+ let bytes = payload. as_slice ( ) ;
424+ let first = bytes[ 0 ] ;
425+
426+ // Decode msgpack array header to get element count and header length.
427+ let ( count, hdr_len) = if ( 0x90 ..=0x9f ) . contains ( & first) {
428+ ( ( first & 0x0f ) as usize , 1 )
429+ } else if first == 0xdc && bytes. len ( ) >= 3 {
430+ ( u16:: from_be_bytes ( [ bytes[ 1 ] , bytes[ 2 ] ] ) as usize , 3 )
431+ } else if first == 0xdd && bytes. len ( ) >= 5 {
432+ (
433+ u32:: from_be_bytes ( [ bytes[ 1 ] , bytes[ 2 ] , bytes[ 3 ] , bytes[ 4 ] ] ) as usize ,
434+ 5 ,
435+ )
436+ } else {
437+ // Not a msgpack array — treat the whole payload as one row.
438+ tracing:: warn!(
439+ payload_len = bytes. len( ) ,
440+ "dedup_union_payloads: payload is not a msgpack array; treating as single row"
441+ ) ;
442+ let key = bytes. to_vec ( ) ;
443+ if seen. insert ( key. clone ( ) ) {
444+ unique_row_bytes. push ( key) ;
445+ }
446+ continue ;
447+ } ;
448+
449+ let mut pos = hdr_len;
450+ for _ in 0 ..count {
451+ if pos >= bytes. len ( ) {
452+ break ;
453+ }
454+ let elem_start = pos;
455+ match msgpack_scan:: skip_value ( bytes, pos) {
456+ Some ( next_pos) => {
457+ let row_bytes = bytes[ elem_start..next_pos] . to_vec ( ) ;
458+ if seen. insert ( row_bytes. clone ( ) ) {
459+ unique_row_bytes. push ( row_bytes) ;
460+ }
461+ pos = next_pos;
462+ }
463+ None => {
464+ tracing:: warn!(
465+ pos,
466+ payload_len = bytes. len( ) ,
467+ "dedup_union_payloads: could not skip msgpack element; stopping early"
468+ ) ;
469+ break ;
470+ }
471+ }
472+ }
473+ }
474+
475+ // Write output: msgpack array header + concatenated unique row bytes.
476+ let row_count = unique_row_bytes. len ( ) ;
477+ let total_data: usize = unique_row_bytes. iter ( ) . map ( |r| r. len ( ) ) . sum ( ) ;
478+ let mut out = Vec :: with_capacity ( total_data + 5 ) ;
479+
480+ // Write array header.
481+ if row_count < 16 {
482+ out. push ( 0x90 | row_count as u8 ) ;
483+ } else if row_count <= u16:: MAX as usize {
484+ out. push ( 0xdc ) ;
485+ out. extend_from_slice ( & ( row_count as u16 ) . to_be_bytes ( ) ) ;
486+ } else {
487+ out. push ( 0xdd ) ;
488+ out. extend_from_slice ( & ( row_count as u32 ) . to_be_bytes ( ) ) ;
489+ }
490+
491+ for row in unique_row_bytes {
492+ out. extend_from_slice ( & row) ;
493+ }
494+
495+ out
496+ }
0 commit comments