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}