1use std::collections::{BTreeMap, BTreeSet};
4
5use corium_core::{Cardinality, Datom, EntityId, Partition, Unique, Value};
6use corium_db::{Db, FIRST_USER_ID, bootstrap};
7use thiserror::Error;
8
9pub type TempId = String;
11
12pub const TX_TEMPID: &str = "datomic.tx";
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub enum EntityRef {
23 Id(EntityId),
25 Temp(TempId),
27 Lookup(EntityId, Value),
29}
30
31#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum TxOp {
34 Add(EntityRef, EntityId, Value),
36 Retract(EntityRef, EntityId, Value),
38 Cas(EntityRef, EntityId, Option<Value>, Value),
40 RetractEntity(EntityRef),
42}
43
44#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct EntityMap {
47 pub entity: EntityRef,
49 pub attributes: Vec<(EntityId, Vec<Value>)>,
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub enum TxItem {
56 Op(TxOp),
58 Map(EntityMap),
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct PreparedTx {
65 pub datoms: Vec<Datom>,
67 pub tempids: BTreeMap<TempId, EntityId>,
69}
70
71#[derive(Debug, Error, Eq, PartialEq)]
73pub enum TxError {
74 #[error("unknown attribute {0:?}")]
76 UnknownAttribute(EntityId),
77 #[error("value has wrong type for attribute {0:?}")]
79 TypeMismatch(EntityId),
80 #[error("lookup ref did not resolve")]
82 LookupNotFound,
83 #[error("lookup attribute is not unique")]
85 LookupNotUnique,
86 #[error("unique value conflict")]
88 UniqueConflict,
89 #[error("compare-and-swap failed")]
91 CasFailed,
92 #[error("compare-and-swap requires cardinality one")]
94 CasCardinality,
95 #[error("supplied :db/txInstant {supplied} is not after the last commit's {last}")]
99 TxInstantNotMonotonic {
100 supplied: i64,
102 last: i64,
104 },
105 #[error(":db/txInstant may only be added once to the current transaction entity")]
109 InvalidTxInstantOperation,
110}
111
112#[allow(clippy::too_many_lines)]
122pub fn prepare(
123 db: &Db,
124 items: impl IntoIterator<Item = TxItem>,
125 tx: EntityId,
126 next_user_sequence: u64,
127) -> Result<PreparedTx, TxError> {
128 let mut ops = Vec::new();
129 for item in items {
130 match item {
131 TxItem::Op(op) => ops.push(op),
132 TxItem::Map(map) => {
133 for (a, values) in map.attributes {
134 for value in values {
135 ops.push(TxOp::Add(map.entity.clone(), a, value));
136 }
137 }
138 }
139 }
140 }
141 let mut tempids = BTreeMap::new();
142 for op in &ops {
144 if let TxOp::Add(EntityRef::Temp(temp), a, value) = op
145 && temp != TX_TEMPID
146 && db.schema().get(*a).and_then(|x| x.unique) == Some(Unique::Identity)
147 && let Some(e) = db.lookup(*a, value)
148 {
149 tempids.insert(temp.clone(), e);
150 }
151 }
152 let mut next = next_user_sequence.max(FIRST_USER_ID);
153 for op in &ops {
154 let entity = match op {
155 TxOp::Add(e, ..) | TxOp::Retract(e, ..) | TxOp::Cas(e, ..) | TxOp::RetractEntity(e) => {
156 e
157 }
158 };
159 if let EntityRef::Temp(temp) = entity {
160 if temp == TX_TEMPID {
163 tempids.insert(temp.clone(), tx);
164 continue;
165 }
166 tempids.entry(temp.clone()).or_insert_with(|| {
167 let e = EntityId::new(Partition::User as u32, next);
168 next += 1;
169 e
170 });
171 }
172 }
173 let resolve = |entity: &EntityRef| -> Result<EntityId, TxError> {
174 match entity {
175 EntityRef::Id(e) => Ok(*e),
176 EntityRef::Temp(t) => Ok(tempids[t]),
177 EntityRef::Lookup(a, v) => {
178 let attr = db.schema().get(*a).ok_or(TxError::UnknownAttribute(*a))?;
179 if attr.unique.is_none() {
180 return Err(TxError::LookupNotUnique);
181 }
182 db.lookup(*a, v).ok_or(TxError::LookupNotFound)
183 }
184 }
185 };
186 let mut datoms = Vec::new();
187 let mut working = db.clone();
188 let mut tx_instant_asserted = false;
189 for op in ops {
190 let start = datoms.len();
191 match op {
192 TxOp::Add(entity, a, v) => {
193 let e = resolve(&entity)?;
194 if a == bootstrap::TX_INSTANT {
195 if e != tx || tx_instant_asserted {
196 return Err(TxError::InvalidTxInstantOperation);
197 }
198 tx_instant_asserted = true;
199 }
200 validate(&working, a, &v)?;
201 if let Some(attr) = working.schema().get(a) {
202 if attr.unique.is_some()
203 && working.lookup(a, &v).is_some_and(|owner| owner != e)
204 {
205 return Err(TxError::UniqueConflict);
206 }
207 let current = working.values(e, a);
208 if current.contains(&v) {
210 continue;
211 }
212 if attr.cardinality == Cardinality::One {
213 for old in current {
214 datoms.push(Datom {
215 e,
216 a,
217 v: old,
218 tx,
219 added: false,
220 });
221 }
222 }
223 }
224 datoms.push(Datom {
225 e,
226 a,
227 v,
228 tx,
229 added: true,
230 });
231 }
232 TxOp::Retract(entity, a, v) => {
233 if a == bootstrap::TX_INSTANT {
234 return Err(TxError::InvalidTxInstantOperation);
235 }
236 validate(&working, a, &v)?;
237 let e = resolve(&entity)?;
238 if working.values(e, a).contains(&v) {
240 datoms.push(Datom {
241 e,
242 a,
243 v,
244 tx,
245 added: false,
246 });
247 }
248 }
249 TxOp::Cas(entity, a, old, new) => {
250 if a == bootstrap::TX_INSTANT {
251 return Err(TxError::InvalidTxInstantOperation);
252 }
253 validate(&working, a, &new)?;
254 let e = resolve(&entity)?;
255 if working
256 .schema()
257 .get(a)
258 .is_none_or(|x| x.cardinality != Cardinality::One)
259 {
260 return Err(TxError::CasCardinality);
261 }
262 let current = working.values(e, a).into_iter().next();
263 if current != old {
264 return Err(TxError::CasFailed);
265 }
266 if let Some(value) = current {
267 datoms.push(Datom {
268 e,
269 a,
270 v: value,
271 tx,
272 added: false,
273 });
274 }
275 datoms.push(Datom {
276 e,
277 a,
278 v: new,
279 tx,
280 added: true,
281 });
282 }
283 TxOp::RetractEntity(entity) => {
284 let mut facts = BTreeSet::new();
285 collect_entity_retractions(
286 &working,
287 resolve(&entity)?,
288 &mut facts,
289 &mut BTreeSet::new(),
290 );
291 if facts.iter().any(|(_, a, _)| *a == bootstrap::TX_INSTANT) {
292 return Err(TxError::InvalidTxInstantOperation);
293 }
294 datoms.extend(facts.into_iter().map(|(e, a, v)| Datom {
295 e,
296 a,
297 v,
298 tx,
299 added: false,
300 }));
301 }
302 }
303 working = working.with_transaction(working.basis_t() + 1, &datoms[start..]);
304 }
305 Ok(PreparedTx { datoms, tempids })
306}
307
308fn validate(db: &Db, a: EntityId, value: &Value) -> Result<(), TxError> {
309 let attr = db.schema().get(a).ok_or(TxError::UnknownAttribute(a))?;
310 if !value.has_type(attr.value_type) {
311 return Err(TxError::TypeMismatch(a));
312 }
313 Ok(())
314}
315
316fn collect_entity_retractions(
321 db: &Db,
322 e: EntityId,
323 facts: &mut BTreeSet<(EntityId, EntityId, Value)>,
324 seen: &mut BTreeSet<EntityId>,
325) {
326 if !seen.insert(e) {
327 return;
328 }
329 for datom in db.datoms() {
330 if datom.e == e {
331 if db.schema().get(datom.a).is_some_and(|a| a.is_component)
332 && let Value::Ref(child) = &datom.v
333 {
334 collect_entity_retractions(db, *child, facts, seen);
335 }
336 facts.insert((datom.e, datom.a, datom.v));
337 } else if datom.v == Value::Ref(e) {
338 facts.insert((datom.e, datom.a, datom.v));
339 }
340 }
341}