Skip to main content

surreal_sync_runtime/pipeline/apply/
transform.rs

1//! Batch transform step (Pipeline + test doubles).
2
3use crate::pipeline::apply::ApplyEvent;
4use crate::pipeline::pipeline::Pipeline;
5use anyhow::{bail, Result};
6use async_trait::async_trait;
7use surreal_sync_core::{Change, Relation, RelationChange, Row};
8
9/// Executes a transform for one numbered batch.
10///
11/// The apply runtime correlates in-flight work by `batch_id`. Responses may
12/// complete out of order; sink apply + watermark advance stay strictly ordered.
13///
14/// [`Pipeline`] implements this: empty pipelines take the same JoinSet path
15/// as non-empty ones via [`Self::is_identity`] (async no-op); non-empty
16/// pipelines walk in-place and External stages asynchronously. Test-support
17/// provides scripted delays / failures so W≥2 reliability scenarios are real
18/// without a child process.
19///
20/// # Relations
21///
22/// Batches may contain [`ApplyEvent::RelationChange`] interleaved with row
23/// changes. **Fail closed:** the default
24/// [`transform_relation_changes`](Self::transform_relation_changes) /
25/// [`transform_relations`](Self::transform_relations) return `Err` so a custom
26/// impl that only overrides [`transform_changes`](Self::transform_changes)
27/// cannot silently pass relation edges through. Override those methods, or use
28/// [`Pipeline`] (which implements them). The default
29/// [`transform_events`](Self::transform_events) splits, transforms, and
30/// recombines by index (length of each kind must be preserved).
31#[async_trait]
32pub trait BatchTransformer: Send + Sync {
33    /// Whether transform dispatch can be skipped entirely (empty pipeline).
34    ///
35    /// For [`Pipeline`], this is [`Pipeline::is_identity`] — only an empty
36    /// stage list, not a lone passthrough stage.
37    fn is_identity(&self) -> bool;
38
39    /// Transform an owned change batch. `batch_id` is monotonic per apply run.
40    async fn transform_changes(&self, batch_id: u64, changes: Vec<Change>) -> Result<Vec<Change>>;
41
42    /// Transform an owned row batch.
43    async fn transform_rows(&self, batch_id: u64, rows: Vec<Row>) -> Result<Vec<Row>>;
44
45    /// Transform an owned relation-change batch.
46    ///
47    /// **Default: fail closed** — returns an error so relation edges are never
48    /// silently skipped when only [`transform_changes`](Self::transform_changes)
49    /// is overridden. Implement this method (or use [`Pipeline`]) for
50    /// relation-aware transformers. Explicit passthrough is `Ok(changes)`.
51    async fn transform_relation_changes(
52        &self,
53        _batch_id: u64,
54        _changes: Vec<RelationChange>,
55    ) -> Result<Vec<RelationChange>> {
56        bail!(
57            "BatchTransformer::transform_relation_changes is not implemented; \
58             override it (or return Ok(changes) for explicit passthrough), or use Pipeline"
59        )
60    }
61
62    /// Transform an owned relation (full-sync) batch.
63    ///
64    /// **Default: fail closed** — see [`transform_relation_changes`](Self::transform_relation_changes).
65    async fn transform_relations(
66        &self,
67        _batch_id: u64,
68        _relations: Vec<Relation>,
69    ) -> Result<Vec<Relation>> {
70        bail!(
71            "BatchTransformer::transform_relations is not implemented; \
72             override it (or return Ok(relations) for explicit passthrough), or use Pipeline"
73        )
74    }
75
76    /// Transform a mixed apply-event batch, preserving order.
77    ///
78    /// Default implementation splits into changes / relation changes, calls
79    /// [`transform_changes`](Self::transform_changes) /
80    /// [`transform_relation_changes`](Self::transform_relation_changes), and
81    /// recombines. Length of each kind must be preserved (filter/fan-out of a
82    /// single kind in a mixed batch is not supported here — use homogeneous
83    /// batches or External on row-only feeds).
84    async fn transform_events(
85        &self,
86        batch_id: u64,
87        events: Vec<ApplyEvent>,
88    ) -> Result<Vec<ApplyEvent>> {
89        let mut change_idxs = Vec::new();
90        let mut changes = Vec::new();
91        let mut rel_idxs = Vec::new();
92        let mut rels = Vec::new();
93        let mut out: Vec<Option<ApplyEvent>> = Vec::with_capacity(events.len());
94        for (i, event) in events.into_iter().enumerate() {
95            out.push(None);
96            match event {
97                ApplyEvent::Change(c) => {
98                    change_idxs.push(i);
99                    changes.push(c);
100                }
101                ApplyEvent::RelationChange(r) => {
102                    rel_idxs.push(i);
103                    rels.push(*r);
104                }
105            }
106        }
107
108        if !changes.is_empty() {
109            let n = changes.len();
110            let transformed = self.transform_changes(batch_id, changes).await?;
111            if transformed.len() != n {
112                bail!(
113                    "transform_changes changed length ({n} → {}) in a mixed ApplyEvent batch; \
114                     use homogeneous batches for filter/fan-out",
115                    transformed.len()
116                );
117            }
118            for (idx, c) in change_idxs.into_iter().zip(transformed) {
119                out[idx] = Some(ApplyEvent::Change(c));
120            }
121        }
122
123        if !rels.is_empty() {
124            let n = rels.len();
125            let transformed = self.transform_relation_changes(batch_id, rels).await?;
126            if transformed.len() != n {
127                bail!(
128                    "transform_relation_changes changed length ({n} → {}) in a mixed \
129                     ApplyEvent batch; use homogeneous batches for filter/fan-out",
130                    transformed.len()
131                );
132            }
133            for (idx, r) in rel_idxs.into_iter().zip(transformed) {
134                out[idx] = Some(ApplyEvent::relation_change(r));
135            }
136        }
137
138        Ok(out
139            .into_iter()
140            .map(|e| e.expect("every slot filled"))
141            .collect())
142    }
143}
144
145#[async_trait]
146impl BatchTransformer for Pipeline {
147    fn is_identity(&self) -> bool {
148        Pipeline::is_identity(self)
149    }
150
151    async fn transform_changes(&self, batch_id: u64, changes: Vec<Change>) -> Result<Vec<Change>> {
152        self.apply_changes_async(batch_id, changes).await
153    }
154
155    async fn transform_rows(&self, batch_id: u64, rows: Vec<Row>) -> Result<Vec<Row>> {
156        self.apply_rows_async(batch_id, rows).await
157    }
158
159    async fn transform_relation_changes(
160        &self,
161        batch_id: u64,
162        changes: Vec<RelationChange>,
163    ) -> Result<Vec<RelationChange>> {
164        self.apply_relation_changes_async(batch_id, changes).await
165    }
166
167    async fn transform_relations(
168        &self,
169        batch_id: u64,
170        relations: Vec<Relation>,
171    ) -> Result<Vec<Relation>> {
172        self.apply_relations_async(batch_id, relations).await
173    }
174
175    async fn transform_events(
176        &self,
177        batch_id: u64,
178        events: Vec<ApplyEvent>,
179    ) -> Result<Vec<ApplyEvent>> {
180        self.apply_events_async(batch_id, events).await
181    }
182}