Skip to main content

surreal_sync_core/
transform.rs

1//! In-place transform trait and passthrough implementation.
2//!
3//! # Schema-aware transforms (FK → record links)
4//!
5//! [`InPlaceTransform`] does not take schema itself. Library code may construct
6//! a transform with a schema/catalog (e.g. wrap source-crate FK helpers) and
7//! push it into a pipeline. The apply path runs that stage like any other
8//! in-place transform — including over [`crate::Relation`] /
9//! [`crate::RelationChange`] via the relation methods below.
10//!
11//! Full join-table → relation **source** logic may remain in PostgreSQL (or
12//! other) source crates; relation **edges are first-class in the apply engine**.
13
14use crate::{Change, Relation, RelationChange, Row, Value};
15use anyhow::Result;
16use std::collections::HashMap;
17
18/// Mutate-only, same-length transform over sync docs.
19///
20/// This is the **only** in-process mutation primitive. Filter / fan-out belong
21/// in an external transform or outside this trait.
22///
23/// Implement [`transform`](Self::transform) once for the shared document surface
24/// (`table` / `id` / optional `fields`). [`transform_row`](Self::transform_row)
25/// and [`transform_change`](Self::transform_change) default to calling it.
26/// Deletes arrive with `fields == None`.
27///
28/// # Slice defaults
29///
30/// [`transform_rows_inplace`](Self::transform_rows_inplace) and
31/// [`transform_changes_inplace`](Self::transform_changes_inplace) loop over
32/// items by default. Override only when a batched implementation is faster.
33///
34/// Relation methods default to no-op so row-only transforms stay trivial.
35pub trait InPlaceTransform: Send + Sync {
36    /// Mutate the shared document surface (id + optional field map).
37    ///
38    /// `fields` is `None` for delete changes.
39    fn transform(
40        &self,
41        table: &str,
42        id: &mut Value,
43        fields: Option<&mut HashMap<String, Value>>,
44    ) -> Result<()>;
45
46    /// Transform a single row in place.
47    fn transform_row(&self, row: &mut Row) -> Result<()> {
48        self.transform(&row.table, &mut row.id, Some(&mut row.fields))
49    }
50
51    /// Transform a single change in place (including deletes).
52    fn transform_change(&self, change: &mut Change) -> Result<()> {
53        self.transform(&change.table, &mut change.id, change.fields.as_mut())
54    }
55
56    /// Transform a single relation in place. Default: no-op.
57    fn transform_relation(&self, _relation: &mut Relation) -> Result<()> {
58        Ok(())
59    }
60
61    /// Transform a single relation change in place. Default: no-op.
62    fn transform_relation_change(&self, _change: &mut RelationChange) -> Result<()> {
63        Ok(())
64    }
65
66    /// Transform a slice of owned rows in place (true zero-copy path).
67    fn transform_rows_inplace(&self, rows: &mut [Row]) -> Result<()> {
68        for row in rows {
69            self.transform_row(row)?;
70        }
71        Ok(())
72    }
73
74    /// Transform a slice of owned changes in place (true zero-copy path).
75    fn transform_changes_inplace(&self, changes: &mut [Change]) -> Result<()> {
76        for change in changes {
77            self.transform_change(change)?;
78        }
79        Ok(())
80    }
81
82    /// Transform a slice of owned relations in place. Default: loop.
83    fn transform_relations_inplace(&self, relations: &mut [Relation]) -> Result<()> {
84        for relation in relations {
85            self.transform_relation(relation)?;
86        }
87        Ok(())
88    }
89
90    /// Transform a slice of owned relation changes in place. Default: loop.
91    fn transform_relation_changes_inplace(&self, changes: &mut [RelationChange]) -> Result<()> {
92        for change in changes {
93            self.transform_relation_change(change)?;
94        }
95        Ok(())
96    }
97}
98
99/// No-op [`InPlaceTransform`]. Useful for tests and library completeness.
100///
101/// Operators configuring surreal-sync via TOML should omit transforms entirely
102/// for identity; they do not need an explicit passthrough stage.
103///
104/// Pushing `Passthrough` into a pipeline does **not** make that pipeline's
105/// identity check true — that requires an empty stage list. Config loading
106/// should collapse passthrough-only TOML to empty.
107#[derive(Debug, Default, Clone, Copy)]
108pub struct Passthrough;
109
110impl InPlaceTransform for Passthrough {
111    fn transform(
112        &self,
113        _table: &str,
114        _id: &mut Value,
115        _fields: Option<&mut HashMap<String, Value>>,
116    ) -> Result<()> {
117        Ok(())
118    }
119}