Skip to main content

chopin_orm/
active_model.rs

1use crate::{Executor, Model, OrmError, OrmResult, PgValue};
2
3/// State wrapper for a model field, tracking whether it has been modified.
4#[derive(Clone, Debug, PartialEq)]
5pub enum ActiveValue<V> {
6    /// The value has been explicitly set and should be persisted.
7    Set(V),
8    /// The value remains unchanged from its last known database state.
9    Unchanged(V),
10    /// The value has never been set and is missing for this operation.
11    NotSet,
12}
13
14impl<V> ActiveValue<V> {
15    /// Returns true if the value is `Set`.
16    pub fn is_set(&self) -> bool {
17        matches!(self, Self::Set(_))
18    }
19
20    /// Unwraps the inner value, if present.
21    pub fn into_value(self) -> Option<V> {
22        match self {
23            Self::Set(v) | Self::Unchanged(v) => Some(v),
24            Self::NotSet => None,
25        }
26    }
27}
28
29/// A smart wrapper around a `Model` tracking modified columns dynamically.
30///
31/// Used to perform targeted, minimal `UPDATE` or `INSERT` queries that only
32/// persist the columns that have actually changed.
33pub struct ActiveModel<M: Model> {
34    /// The underlying model instance.
35    pub inner: M,
36    /// Columns and their corresponding `ActiveValue` states.
37    changes: Vec<(&'static str, ActiveValue<PgValue>)>,
38    /// Whether this represents a new (unsaved) record.
39    is_new: bool,
40}
41
42impl<M: Model> ActiveModel<M> {
43    /// Create a new `ActiveModel` for an entity that hasn't been saved yet.
44    pub fn new_insert(model: M) -> Self {
45        Self {
46            inner: model,
47            changes: Vec::new(),
48            is_new: true,
49        }
50    }
51
52    /// Wrap an existing model into an `ActiveModel` state tracker for updates.
53    pub fn from_model(model: M) -> Self {
54        Self {
55            inner: model,
56            changes: Vec::new(),
57            is_new: false,
58        }
59    }
60
61    /// Flag a column as modified and stage its value for an upcoming transaction.
62    pub fn set<T: crate::ToSql>(&mut self, column: &'static str, value: T) {
63        let sql_val = value.to_sql();
64        if let Some(existing) = self.changes.iter_mut().find(|(c, _)| *c == column) {
65            existing.1 = ActiveValue::Set(sql_val);
66        } else {
67            self.changes.push((column, ActiveValue::Set(sql_val)));
68        }
69    }
70
71    /// Returns whether any columns have been modified.
72    pub fn has_changes(&self) -> bool {
73        self.changes.iter().any(|(_, v)| v.is_set())
74    }
75
76    /// Returns the list of changed column names.
77    pub fn changed_columns(&self) -> Vec<&'static str> {
78        self.changes
79            .iter()
80            .filter(|(_, v)| v.is_set())
81            .map(|(c, _)| *c)
82            .collect()
83    }
84
85    /// Evaluates whether the underlying model has not been persisted yet.
86    pub fn is_new(&self) -> bool {
87        self.is_new
88    }
89
90    /// Validates the model, returning an error if validation fails.
91    fn validate(&self) -> OrmResult<()> {
92        if let Err(errors) = self.inner.validate() {
93            return Err(OrmError::Validation(errors));
94        }
95        Ok(())
96    }
97
98    /// Intelligently issues an `INSERT` or `UPDATE` depending on `is_new()` state.
99    ///
100    /// Validates the model before persisting.
101    pub fn save(&mut self, executor: &mut impl Executor) -> OrmResult<()> {
102        self.validate()?;
103        if self.is_new() {
104            self.insert(executor)?;
105            self.is_new = false;
106            Ok(())
107        } else {
108            self.update(executor)
109        }
110    }
111
112    /// Executes a minimal `INSERT` using only tracked changes.
113    ///
114    /// Validates the model before persisting.
115    pub fn insert(&mut self, executor: &mut impl Executor) -> OrmResult<()> {
116        self.validate()?;
117
118        // If no changes, fall back to inserting everything from the inner model
119        if !self.has_changes() {
120            return self.inner.insert(executor);
121        }
122
123        let mut cols = Vec::new();
124        let mut vals = Vec::new();
125        for (c, v) in &self.changes {
126            if let ActiveValue::Set(val) = v {
127                cols.push(*c);
128                vals.push(val.clone());
129            }
130        }
131
132        let bindings: Vec<String> = (1..=cols.len()).map(|i| format!("${}", i)).collect();
133        let query = format!(
134            "INSERT INTO {} ({}) VALUES ({}) RETURNING {}",
135            M::table_name(),
136            cols.join(", "),
137            bindings.join(", "),
138            M::columns().join(", ")
139        );
140
141        let params: Vec<&dyn chopin_pg::types::ToSql> = vals.iter().map(|v| v as _).collect();
142        let rows = executor.query(&query, &params)?;
143
144        if let Some(row) = rows.first() {
145            self.inner = M::from_row(row)?;
146            self.changes.clear();
147            Ok(())
148        } else {
149            Err(OrmError::ModelError(
150                "Insert failed, no rows returned".to_string(),
151            ))
152        }
153    }
154
155    /// Executes a focused `UPDATE` persisting only the changed columns.
156    ///
157    /// Validates the model before persisting. No-op if nothing has changed.
158    pub fn update(&mut self, executor: &mut impl Executor) -> OrmResult<()> {
159        self.validate()?;
160        if !self.has_changes() {
161            return Ok(());
162        }
163
164        let mut set_clauses = Vec::new();
165        let mut query_values = Vec::new();
166        let mut param_idx = 1;
167
168        for (col, val) in &self.changes {
169            if let ActiveValue::Set(v) = val {
170                set_clauses.push(format!("{} = ${}", col, param_idx));
171                query_values.push(v.clone());
172                param_idx += 1;
173            }
174        }
175
176        let mut where_clauses = Vec::new();
177        let pk_cols = M::primary_key_columns();
178        let pk_vals = self.inner.primary_key_values();
179
180        for (i, col) in pk_cols.iter().enumerate() {
181            where_clauses.push(format!("{} = ${}", col, param_idx));
182            query_values.push(pk_vals[i].clone());
183            param_idx += 1;
184        }
185
186        let query = format!(
187            "UPDATE {} SET {} WHERE {} RETURNING {}",
188            M::table_name(),
189            set_clauses.join(", "),
190            where_clauses.join(" AND "),
191            M::columns().join(", ")
192        );
193
194        let params: Vec<&dyn chopin_pg::types::ToSql> =
195            query_values.iter().map(|v| v as _).collect();
196        let rows = executor.query(&query, &params)?;
197
198        if let Some(row) = rows.first() {
199            self.inner = M::from_row(row)?;
200            self.changes.clear();
201            Ok(())
202        } else {
203            Err(OrmError::ModelError(
204                "Update failed, no rows returned".to_string(),
205            ))
206        }
207    }
208}
209
210impl<M: Model> From<M> for ActiveModel<M> {
211    fn from(model: M) -> Self {
212        ActiveModel::from_model(model)
213    }
214}