1pub mod change_tracker;
43pub mod flush;
44pub mod identity_map;
45pub mod n1_detection;
46pub mod unit_of_work;
47
48pub use change_tracker::{ChangeTracker, ObjectSnapshot};
49pub use flush::{
50 FlushOrderer, FlushPlan, FlushResult, LinkTableOp, PendingOp, execute_link_table_ops,
51};
52pub use identity_map::{IdentityMap, ModelReadGuard, ModelRef, ModelWriteGuard, WeakIdentityMap};
53pub use n1_detection::{CallSite, N1DetectionScope, N1QueryTracker, N1Stats};
54pub use unit_of_work::{PendingCounts, UnitOfWork, UowError};
55
56use asupersync::{Cx, Outcome};
57use serde::{Deserialize, Serialize};
58use sqlmodel_core::{Connection, Error, Lazy, LazyLoader, Model, Value};
59use std::any::{Any, TypeId};
60use std::collections::HashMap;
61use std::future::Future;
62use std::hash::{Hash, Hasher};
63
64type SessionEventFn = Box<dyn FnMut() -> Result<(), Error> + Send>;
73
74#[derive(Default)]
79pub struct SessionEventCallbacks {
80 before_flush: Vec<SessionEventFn>,
81 after_flush: Vec<SessionEventFn>,
82 before_commit: Vec<SessionEventFn>,
83 after_commit: Vec<SessionEventFn>,
84 after_rollback: Vec<SessionEventFn>,
85}
86
87impl std::fmt::Debug for SessionEventCallbacks {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("SessionEventCallbacks")
90 .field("before_flush", &self.before_flush.len())
91 .field("after_flush", &self.after_flush.len())
92 .field("before_commit", &self.before_commit.len())
93 .field("after_commit", &self.after_commit.len())
94 .field("after_rollback", &self.after_rollback.len())
95 .finish()
96 }
97}
98
99impl SessionEventCallbacks {
100 #[allow(clippy::result_large_err)]
101 fn fire(&mut self, event: SessionEvent) -> Result<(), Error> {
102 let callbacks = match event {
103 SessionEvent::BeforeFlush => &mut self.before_flush,
104 SessionEvent::AfterFlush => &mut self.after_flush,
105 SessionEvent::BeforeCommit => &mut self.before_commit,
106 SessionEvent::AfterCommit => &mut self.after_commit,
107 SessionEvent::AfterRollback => &mut self.after_rollback,
108 };
109 for cb in callbacks.iter_mut() {
110 cb()?;
111 }
112 Ok(())
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum SessionEvent {
119 BeforeFlush,
121 AfterFlush,
123 BeforeCommit,
125 AfterCommit,
127 AfterRollback,
129}
130
131#[derive(Debug, Clone)]
137pub struct SessionConfig {
138 pub auto_begin: bool,
140 pub auto_flush: bool,
142 pub expire_on_commit: bool,
144}
145
146impl Default for SessionConfig {
147 fn default() -> Self {
148 Self {
149 auto_begin: true,
150 auto_flush: false,
151 expire_on_commit: true,
152 }
153 }
154}
155
156#[derive(Debug, Clone, Default)]
158pub struct GetOptions {
159 pub with_for_update: bool,
161 pub skip_locked: bool,
163 pub nowait: bool,
165}
166
167impl GetOptions {
168 #[must_use]
170 pub fn new() -> Self {
171 Self::default()
172 }
173
174 #[must_use]
176 pub fn with_for_update(mut self, value: bool) -> Self {
177 self.with_for_update = value;
178 self
179 }
180
181 #[must_use]
183 pub fn skip_locked(mut self, value: bool) -> Self {
184 self.skip_locked = value;
185 self
186 }
187
188 #[must_use]
190 pub fn nowait(mut self, value: bool) -> Self {
191 self.nowait = value;
192 self
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
202pub struct ObjectKey {
203 type_id: TypeId,
205 pk_hash: u64,
207}
208
209impl ObjectKey {
210 pub fn from_model<M: Model + 'static>(obj: &M) -> Self {
212 let pk_values = obj.primary_key_value();
213 Self {
214 type_id: TypeId::of::<M>(),
215 pk_hash: hash_values(&pk_values),
216 }
217 }
218
219 pub fn from_pk<M: Model + 'static>(pk: &[Value]) -> Self {
221 Self {
222 type_id: TypeId::of::<M>(),
223 pk_hash: hash_values(pk),
224 }
225 }
226
227 pub fn pk_hash(&self) -> u64 {
229 self.pk_hash
230 }
231
232 pub fn type_id(&self) -> TypeId {
234 self.type_id
235 }
236}
237
238fn hash_values(values: &[Value]) -> u64 {
240 use std::collections::hash_map::DefaultHasher;
241 let mut hasher = DefaultHasher::new();
242 for v in values {
243 match v {
245 Value::Null => 0u8.hash(&mut hasher),
246 Value::Bool(b) => {
247 1u8.hash(&mut hasher);
248 b.hash(&mut hasher);
249 }
250 Value::TinyInt(i) => {
251 2u8.hash(&mut hasher);
252 i.hash(&mut hasher);
253 }
254 Value::SmallInt(i) => {
255 3u8.hash(&mut hasher);
256 i.hash(&mut hasher);
257 }
258 Value::Int(i) => {
259 4u8.hash(&mut hasher);
260 i.hash(&mut hasher);
261 }
262 Value::BigInt(i) => {
263 5u8.hash(&mut hasher);
264 i.hash(&mut hasher);
265 }
266 Value::Float(f) => {
267 6u8.hash(&mut hasher);
268 f.to_bits().hash(&mut hasher);
269 }
270 Value::Double(f) => {
271 7u8.hash(&mut hasher);
272 f.to_bits().hash(&mut hasher);
273 }
274 Value::Decimal(s) => {
275 8u8.hash(&mut hasher);
276 s.hash(&mut hasher);
277 }
278 Value::Text(s) => {
279 9u8.hash(&mut hasher);
280 s.hash(&mut hasher);
281 }
282 Value::Bytes(b) => {
283 10u8.hash(&mut hasher);
284 b.hash(&mut hasher);
285 }
286 Value::Date(d) => {
287 11u8.hash(&mut hasher);
288 d.hash(&mut hasher);
289 }
290 Value::Time(t) => {
291 12u8.hash(&mut hasher);
292 t.hash(&mut hasher);
293 }
294 Value::Timestamp(ts) => {
295 13u8.hash(&mut hasher);
296 ts.hash(&mut hasher);
297 }
298 Value::TimestampTz(ts) => {
299 14u8.hash(&mut hasher);
300 ts.hash(&mut hasher);
301 }
302 Value::Uuid(u) => {
303 15u8.hash(&mut hasher);
304 u.hash(&mut hasher);
305 }
306 Value::Json(j) => {
307 16u8.hash(&mut hasher);
308 j.to_string().hash(&mut hasher);
310 }
311 Value::Array(arr) => {
312 17u8.hash(&mut hasher);
313 arr.len().hash(&mut hasher);
315 for item in arr {
316 hash_value(item, &mut hasher);
317 }
318 }
319 Value::Default => {
320 18u8.hash(&mut hasher);
321 }
322 }
323 }
324 hasher.finish()
325}
326
327fn hash_value(v: &Value, hasher: &mut impl Hasher) {
329 match v {
330 Value::Null => 0u8.hash(hasher),
331 Value::Bool(b) => {
332 1u8.hash(hasher);
333 b.hash(hasher);
334 }
335 Value::TinyInt(i) => {
336 2u8.hash(hasher);
337 i.hash(hasher);
338 }
339 Value::SmallInt(i) => {
340 3u8.hash(hasher);
341 i.hash(hasher);
342 }
343 Value::Int(i) => {
344 4u8.hash(hasher);
345 i.hash(hasher);
346 }
347 Value::BigInt(i) => {
348 5u8.hash(hasher);
349 i.hash(hasher);
350 }
351 Value::Float(f) => {
352 6u8.hash(hasher);
353 f.to_bits().hash(hasher);
354 }
355 Value::Double(f) => {
356 7u8.hash(hasher);
357 f.to_bits().hash(hasher);
358 }
359 Value::Decimal(s) => {
360 8u8.hash(hasher);
361 s.hash(hasher);
362 }
363 Value::Text(s) => {
364 9u8.hash(hasher);
365 s.hash(hasher);
366 }
367 Value::Bytes(b) => {
368 10u8.hash(hasher);
369 b.hash(hasher);
370 }
371 Value::Date(d) => {
372 11u8.hash(hasher);
373 d.hash(hasher);
374 }
375 Value::Time(t) => {
376 12u8.hash(hasher);
377 t.hash(hasher);
378 }
379 Value::Timestamp(ts) => {
380 13u8.hash(hasher);
381 ts.hash(hasher);
382 }
383 Value::TimestampTz(ts) => {
384 14u8.hash(hasher);
385 ts.hash(hasher);
386 }
387 Value::Uuid(u) => {
388 15u8.hash(hasher);
389 u.hash(hasher);
390 }
391 Value::Json(j) => {
392 16u8.hash(hasher);
393 j.to_string().hash(hasher);
394 }
395 Value::Array(arr) => {
396 17u8.hash(hasher);
397 arr.len().hash(hasher);
398 for item in arr {
399 hash_value(item, hasher);
400 }
401 }
402 Value::Default => {
403 18u8.hash(hasher);
404 }
405 }
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub enum ObjectState {
411 New,
413 Persistent,
415 Deleted,
417 Detached,
419 Expired,
421}
422
423struct TrackedObject {
425 object: Box<dyn Any + Send + Sync>,
427 original_state: Option<Vec<u8>>,
429 state: ObjectState,
431 table_name: &'static str,
433 column_names: Vec<&'static str>,
435 values: Vec<Value>,
437 pk_columns: Vec<&'static str>,
439 pk_values: Vec<Value>,
441 relationships: &'static [sqlmodel_core::RelationshipInfo],
443 expired_attributes: Option<std::collections::HashSet<String>>,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Hash)]
449struct CascadeChildDeleteKey {
450 table: &'static str,
451 fk_cols: Vec<&'static str>,
452}
453
454pub struct Session<C: Connection> {
463 connection: C,
465 in_transaction: bool,
467 identity_map: HashMap<ObjectKey, TrackedObject>,
469 pending_new: Vec<ObjectKey>,
471 pending_delete: Vec<ObjectKey>,
473 pending_dirty: Vec<ObjectKey>,
475 config: SessionConfig,
477 n1_tracker: Option<N1QueryTracker>,
479 event_callbacks: SessionEventCallbacks,
481}
482
483impl<C: Connection> Session<C> {
484 pub fn new(connection: C) -> Self {
486 Self::with_config(connection, SessionConfig::default())
487 }
488
489 pub fn with_config(connection: C, config: SessionConfig) -> Self {
491 Self {
492 connection,
493 in_transaction: false,
494 identity_map: HashMap::new(),
495 pending_new: Vec::new(),
496 pending_delete: Vec::new(),
497 pending_dirty: Vec::new(),
498 config,
499 n1_tracker: None,
500 event_callbacks: SessionEventCallbacks::default(),
501 }
502 }
503
504 pub fn connection(&self) -> &C {
506 &self.connection
507 }
508
509 pub fn config(&self) -> &SessionConfig {
511 &self.config
512 }
513
514 pub fn on_before_flush(&mut self, f: impl FnMut() -> Result<(), Error> + Send + 'static) {
522 self.event_callbacks.before_flush.push(Box::new(f));
523 }
524
525 pub fn on_after_flush(&mut self, f: impl FnMut() -> Result<(), Error> + Send + 'static) {
527 self.event_callbacks.after_flush.push(Box::new(f));
528 }
529
530 pub fn on_before_commit(&mut self, f: impl FnMut() -> Result<(), Error> + Send + 'static) {
534 self.event_callbacks.before_commit.push(Box::new(f));
535 }
536
537 pub fn on_after_commit(&mut self, f: impl FnMut() -> Result<(), Error> + Send + 'static) {
539 self.event_callbacks.after_commit.push(Box::new(f));
540 }
541
542 pub fn on_after_rollback(&mut self, f: impl FnMut() -> Result<(), Error> + Send + 'static) {
544 self.event_callbacks.after_rollback.push(Box::new(f));
545 }
546
547 pub fn add<M: Model + Clone + Send + Sync + Serialize + 'static>(&mut self, obj: &M) {
555 let key = ObjectKey::from_model(obj);
556
557 if let Some(tracked) = self.identity_map.get_mut(&key) {
559 tracked.object = Box::new(obj.clone());
560
561 let row_data = obj.to_row();
563 tracked.column_names = row_data.iter().map(|(name, _)| *name).collect();
564 tracked.values = row_data.into_iter().map(|(_, v)| v).collect();
565 tracked.pk_values = obj.primary_key_value();
566
567 if tracked.state == ObjectState::Deleted {
568 self.pending_delete.retain(|k| k != &key);
570
571 if tracked.original_state.is_some() {
572 tracked.state = ObjectState::Persistent;
574 } else {
575 tracked.state = ObjectState::New;
577 if !self.pending_new.contains(&key) {
578 self.pending_new.push(key);
579 }
580 }
581 }
582 return;
583 }
584
585 let row_data = obj.to_row();
587 let column_names: Vec<&'static str> = row_data.iter().map(|(name, _)| *name).collect();
588 let values: Vec<Value> = row_data.into_iter().map(|(_, v)| v).collect();
589
590 let pk_columns: Vec<&'static str> = M::PRIMARY_KEY.to_vec();
592 let pk_values = obj.primary_key_value();
593
594 let tracked = TrackedObject {
595 object: Box::new(obj.clone()),
596 original_state: None, state: ObjectState::New,
598 table_name: M::TABLE_NAME,
599 column_names,
600 values,
601 pk_columns,
602 pk_values,
603 relationships: M::RELATIONSHIPS,
604 expired_attributes: None,
605 };
606
607 self.identity_map.insert(key, tracked);
608 self.pending_new.push(key);
609 }
610
611 pub fn add_all<'a, M, I>(&mut self, objects: I)
628 where
629 M: Model + Clone + Send + Sync + Serialize + 'static,
630 I: IntoIterator<Item = &'a M>,
631 {
632 for obj in objects {
633 self.add(obj);
634 }
635 }
636
637 pub fn delete<M: Model + 'static>(&mut self, obj: &M) {
641 let key = ObjectKey::from_model(obj);
642
643 if let Some(tracked) = self.identity_map.get_mut(&key) {
644 match tracked.state {
645 ObjectState::New => {
646 self.identity_map.remove(&key);
648 self.pending_new.retain(|k| k != &key);
649 }
650 ObjectState::Persistent | ObjectState::Expired => {
651 tracked.state = ObjectState::Deleted;
652 self.pending_delete.push(key);
653 self.pending_dirty.retain(|k| k != &key);
654 }
655 ObjectState::Deleted | ObjectState::Detached => {
656 }
658 }
659 }
660 }
661
662 pub fn mark_dirty<M: Model + Clone + Send + Sync + Serialize + 'static>(&mut self, obj: &M) {
676 let key = ObjectKey::from_model(obj);
677
678 if let Some(tracked) = self.identity_map.get_mut(&key) {
679 if tracked.state != ObjectState::Persistent {
681 return;
682 }
683
684 tracked.object = Box::new(obj.clone());
686 let row_data = obj.to_row();
687 tracked.column_names = row_data.iter().map(|(name, _)| *name).collect();
688 tracked.values = row_data.into_iter().map(|(_, v)| v).collect();
689 tracked.pk_values = obj.primary_key_value();
690
691 if !self.pending_dirty.contains(&key) {
693 self.pending_dirty.push(key);
694 }
695 }
696 }
697
698 pub async fn get<
702 M: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
703 >(
704 &mut self,
705 cx: &Cx,
706 pk: impl Into<Value>,
707 ) -> Outcome<Option<M>, Error> {
708 let pk_value = pk.into();
709 let pk_values = vec![pk_value.clone()];
710 let key = ObjectKey::from_pk::<M>(&pk_values);
711
712 if let Some(tracked) = self.identity_map.get(&key) {
714 match tracked.state {
715 ObjectState::Deleted | ObjectState::Detached => {
716 }
718 ObjectState::Expired => {
719 tracing::debug!("Object is expired, reloading from database");
721 }
722 ObjectState::New | ObjectState::Persistent => {
723 if let Some(obj) = tracked.object.downcast_ref::<M>() {
724 return Outcome::Ok(Some(obj.clone()));
725 }
726 }
727 }
728 }
729
730 let pk_col = M::PRIMARY_KEY.first().unwrap_or(&"id");
732 let sql = format!(
733 "SELECT * FROM \"{}\" WHERE \"{}\" = $1 LIMIT 1",
734 M::TABLE_NAME,
735 pk_col
736 );
737
738 let rows = match self.connection.query(cx, &sql, &[pk_value]).await {
739 Outcome::Ok(rows) => rows,
740 Outcome::Err(e) => return Outcome::Err(e),
741 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
742 Outcome::Panicked(p) => return Outcome::Panicked(p),
743 };
744
745 if rows.is_empty() {
746 return Outcome::Ok(None);
747 }
748
749 let obj = match M::from_row(&rows[0]) {
751 Ok(obj) => obj,
752 Err(e) => return Outcome::Err(e),
753 };
754
755 let row_data = obj.to_row();
757 let column_names: Vec<&'static str> = row_data.iter().map(|(name, _)| *name).collect();
758 let values: Vec<Value> = row_data.into_iter().map(|(_, v)| v).collect();
759
760 let serialized = serde_json::to_vec(&values).ok();
762
763 let pk_columns: Vec<&'static str> = M::PRIMARY_KEY.to_vec();
765 let obj_pk_values = obj.primary_key_value();
766
767 let tracked = TrackedObject {
768 object: Box::new(obj.clone()),
769 original_state: serialized,
770 state: ObjectState::Persistent,
771 table_name: M::TABLE_NAME,
772 column_names,
773 values,
774 pk_columns,
775 pk_values: obj_pk_values,
776 relationships: M::RELATIONSHIPS,
777 expired_attributes: None,
778 };
779
780 self.identity_map.insert(key, tracked);
781
782 Outcome::Ok(Some(obj))
783 }
784
785 pub async fn get_by_pk<
799 M: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
800 >(
801 &mut self,
802 cx: &Cx,
803 pk_values: &[Value],
804 ) -> Outcome<Option<M>, Error> {
805 self.get_with_options::<M>(cx, pk_values, &GetOptions::default())
806 .await
807 }
808
809 pub async fn get_with_options<
822 M: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
823 >(
824 &mut self,
825 cx: &Cx,
826 pk_values: &[Value],
827 options: &GetOptions,
828 ) -> Outcome<Option<M>, Error> {
829 let key = ObjectKey::from_pk::<M>(pk_values);
830
831 if !options.with_for_update
833 && let Some(tracked) = self.identity_map.get(&key)
834 {
835 match tracked.state {
836 ObjectState::Deleted | ObjectState::Detached => {
837 }
839 ObjectState::Expired => {
840 tracing::debug!("Object is expired, reloading from database");
842 }
843 ObjectState::New | ObjectState::Persistent => {
844 if let Some(obj) = tracked.object.downcast_ref::<M>() {
845 return Outcome::Ok(Some(obj.clone()));
846 }
847 }
848 }
849 }
850
851 let pk_columns = M::PRIMARY_KEY;
853 if pk_columns.len() != pk_values.len() {
854 return Outcome::Err(Error::Custom(format!(
855 "Primary key mismatch: expected {} values, got {}",
856 pk_columns.len(),
857 pk_values.len()
858 )));
859 }
860
861 let where_parts: Vec<String> = pk_columns
862 .iter()
863 .enumerate()
864 .map(|(i, col)| format!("\"{}\" = ${}", col, i + 1))
865 .collect();
866
867 let mut sql = format!(
868 "SELECT * FROM \"{}\" WHERE {} LIMIT 1",
869 M::TABLE_NAME,
870 where_parts.join(" AND ")
871 );
872
873 if options.with_for_update {
875 sql.push_str(" FOR UPDATE");
876 if options.skip_locked {
877 sql.push_str(" SKIP LOCKED");
878 } else if options.nowait {
879 sql.push_str(" NOWAIT");
880 }
881 }
882
883 let rows = match self.connection.query(cx, &sql, pk_values).await {
884 Outcome::Ok(rows) => rows,
885 Outcome::Err(e) => return Outcome::Err(e),
886 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
887 Outcome::Panicked(p) => return Outcome::Panicked(p),
888 };
889
890 if rows.is_empty() {
891 return Outcome::Ok(None);
892 }
893
894 let obj = match M::from_row(&rows[0]) {
896 Ok(obj) => obj,
897 Err(e) => return Outcome::Err(e),
898 };
899
900 let row_data = obj.to_row();
902 let column_names: Vec<&'static str> = row_data.iter().map(|(name, _)| *name).collect();
903 let values: Vec<Value> = row_data.into_iter().map(|(_, v)| v).collect();
904
905 let serialized = serde_json::to_vec(&values).ok();
907
908 let pk_cols: Vec<&'static str> = M::PRIMARY_KEY.to_vec();
910 let obj_pk_values = obj.primary_key_value();
911
912 let tracked = TrackedObject {
913 object: Box::new(obj.clone()),
914 original_state: serialized,
915 state: ObjectState::Persistent,
916 table_name: M::TABLE_NAME,
917 column_names,
918 values,
919 pk_columns: pk_cols,
920 pk_values: obj_pk_values,
921 relationships: M::RELATIONSHIPS,
922 expired_attributes: None,
923 };
924
925 self.identity_map.insert(key, tracked);
926
927 Outcome::Ok(Some(obj))
928 }
929
930 pub fn contains<M: Model + 'static>(&self, obj: &M) -> bool {
932 let key = ObjectKey::from_model(obj);
933 self.identity_map.contains_key(&key)
934 }
935
936 pub fn expunge<M: Model + 'static>(&mut self, obj: &M) {
938 let key = ObjectKey::from_model(obj);
939 if let Some(tracked) = self.identity_map.get_mut(&key) {
940 tracked.state = ObjectState::Detached;
941 }
942 self.pending_new.retain(|k| k != &key);
943 self.pending_delete.retain(|k| k != &key);
944 self.pending_dirty.retain(|k| k != &key);
945 }
946
947 pub fn expunge_all(&mut self) {
949 for tracked in self.identity_map.values_mut() {
950 tracked.state = ObjectState::Detached;
951 }
952 self.pending_new.clear();
953 self.pending_delete.clear();
954 self.pending_dirty.clear();
955 }
956
957 pub fn is_modified<M: Model + Serialize + 'static>(&self, obj: &M) -> bool {
986 let key = ObjectKey::from_model(obj);
987
988 let Some(tracked) = self.identity_map.get(&key) else {
989 return false;
990 };
991
992 match tracked.state {
993 ObjectState::New => true,
995
996 ObjectState::Deleted => true,
998
999 ObjectState::Detached | ObjectState::Expired => false,
1001
1002 ObjectState::Persistent => {
1004 if self.pending_dirty.contains(&key) {
1006 return true;
1007 }
1008
1009 let current_state = serde_json::to_vec(&tracked.values).unwrap_or_default();
1011 tracked.original_state.as_ref() != Some(¤t_state)
1012 }
1013 }
1014 }
1015
1016 pub fn modified_attributes<M: Model + Serialize + 'static>(
1035 &self,
1036 obj: &M,
1037 ) -> Vec<&'static str> {
1038 let key = ObjectKey::from_model(obj);
1039
1040 let Some(tracked) = self.identity_map.get(&key) else {
1041 return Vec::new();
1042 };
1043
1044 if tracked.state != ObjectState::Persistent {
1046 return Vec::new();
1047 }
1048
1049 let Some(original_bytes) = &tracked.original_state else {
1051 return Vec::new();
1052 };
1053
1054 let Ok(original_values): Result<Vec<Value>, _> = serde_json::from_slice(original_bytes)
1056 else {
1057 return Vec::new();
1058 };
1059
1060 let mut modified = Vec::new();
1062 for (i, col) in tracked.column_names.iter().enumerate() {
1063 let current = tracked.values.get(i);
1064 let original = original_values.get(i);
1065
1066 if current != original {
1067 modified.push(*col);
1068 }
1069 }
1070
1071 modified
1072 }
1073
1074 pub fn object_state<M: Model + 'static>(&self, obj: &M) -> Option<ObjectState> {
1078 let key = ObjectKey::from_model(obj);
1079 self.identity_map.get(&key).map(|t| t.state)
1080 }
1081
1082 #[tracing::instrument(level = "debug", skip(self, obj), fields(table = M::TABLE_NAME))]
1116 pub fn expire<M: Model + 'static>(&mut self, obj: &M, attributes: Option<&[&str]>) {
1117 let key = ObjectKey::from_model(obj);
1118
1119 let Some(tracked) = self.identity_map.get_mut(&key) else {
1120 tracing::debug!("Object not tracked, nothing to expire");
1121 return;
1122 };
1123
1124 match tracked.state {
1126 ObjectState::New | ObjectState::Detached | ObjectState::Deleted => {
1127 tracing::debug!(state = ?tracked.state, "Cannot expire object in this state");
1128 return;
1129 }
1130 ObjectState::Persistent | ObjectState::Expired => {}
1131 }
1132
1133 match attributes {
1134 None => {
1135 tracked.state = ObjectState::Expired;
1137 tracked.expired_attributes = None;
1138 tracing::debug!("Expired all attributes");
1139 }
1140 Some(attrs) => {
1141 let mut expired = tracked.expired_attributes.take().unwrap_or_default();
1143 for attr in attrs {
1144 expired.insert((*attr).to_string());
1145 }
1146 tracked.expired_attributes = Some(expired);
1147
1148 if tracked.state == ObjectState::Persistent {
1150 tracked.state = ObjectState::Expired;
1151 }
1152 tracing::debug!(attributes = ?attrs, "Expired specific attributes");
1153 }
1154 }
1155 }
1156
1157 #[tracing::instrument(level = "debug", skip(self))]
1178 pub fn expire_all(&mut self) {
1179 let mut expired_count = 0;
1180 for tracked in self.identity_map.values_mut() {
1181 if tracked.state == ObjectState::Persistent {
1182 tracked.state = ObjectState::Expired;
1183 tracked.expired_attributes = None;
1184 expired_count += 1;
1185 }
1186 }
1187 tracing::debug!(count = expired_count, "Expired all session objects");
1188 }
1189
1190 pub fn is_expired<M: Model + 'static>(&self, obj: &M) -> bool {
1195 let key = ObjectKey::from_model(obj);
1196 self.identity_map
1197 .get(&key)
1198 .is_some_and(|t| t.state == ObjectState::Expired)
1199 }
1200
1201 pub fn expired_attributes<M: Model + 'static>(
1208 &self,
1209 obj: &M,
1210 ) -> Option<Option<&std::collections::HashSet<String>>> {
1211 let key = ObjectKey::from_model(obj);
1212 let tracked = self.identity_map.get(&key)?;
1213
1214 if tracked.state != ObjectState::Expired {
1215 return None;
1216 }
1217
1218 Some(tracked.expired_attributes.as_ref())
1219 }
1220
1221 #[tracing::instrument(level = "debug", skip(self, cx, obj), fields(table = M::TABLE_NAME))]
1256 pub async fn refresh<
1257 M: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
1258 >(
1259 &mut self,
1260 cx: &Cx,
1261 obj: &M,
1262 ) -> Outcome<Option<M>, Error> {
1263 let pk_values = obj.primary_key_value();
1264 let key = ObjectKey::from_model(obj);
1265
1266 tracing::debug!(pk = ?pk_values, "Refreshing object from database");
1267
1268 self.pending_dirty.retain(|k| k != &key);
1270
1271 self.identity_map.remove(&key);
1273
1274 let result = self.get_by_pk::<M>(cx, &pk_values).await;
1276
1277 match &result {
1278 Outcome::Ok(Some(_)) => {
1279 tracing::debug!("Object refreshed successfully");
1280 }
1281 Outcome::Ok(None) => {
1282 tracing::debug!("Object no longer exists in database");
1283 }
1284 _ => {}
1285 }
1286
1287 result
1288 }
1289
1290 pub async fn begin(&mut self, cx: &Cx) -> Outcome<(), Error> {
1296 if self.in_transaction {
1297 return Outcome::Ok(());
1298 }
1299
1300 match self.connection.execute(cx, "BEGIN", &[]).await {
1301 Outcome::Ok(_) => {
1302 self.in_transaction = true;
1303 Outcome::Ok(())
1304 }
1305 Outcome::Err(e) => Outcome::Err(e),
1306 Outcome::Cancelled(r) => Outcome::Cancelled(r),
1307 Outcome::Panicked(p) => Outcome::Panicked(p),
1308 }
1309 }
1310
1311 pub async fn flush(&mut self, cx: &Cx) -> Outcome<(), Error> {
1315 if let Err(e) = self.event_callbacks.fire(SessionEvent::BeforeFlush) {
1317 return Outcome::Err(e);
1318 }
1319
1320 if self.config.auto_begin && !self.in_transaction {
1322 match self.begin(cx).await {
1323 Outcome::Ok(()) => {}
1324 Outcome::Err(e) => return Outcome::Err(e),
1325 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
1326 Outcome::Panicked(p) => return Outcome::Panicked(p),
1327 }
1328 }
1329
1330 let dialect = self.connection.dialect();
1331
1332 let deletes: Vec<ObjectKey> = std::mem::take(&mut self.pending_delete);
1334
1335 let mut cascade_child_deletes_single: HashMap<(&'static str, &'static str), Vec<Value>> =
1340 HashMap::new();
1341 let mut cascade_child_deletes_composite: HashMap<CascadeChildDeleteKey, Vec<Vec<Value>>> =
1342 HashMap::new();
1343 let mut cascade_link_deletes_single: HashMap<(&'static str, &'static str), Vec<Value>> =
1344 HashMap::new();
1345 let mut cascade_link_deletes_composite: HashMap<CascadeChildDeleteKey, Vec<Vec<Value>>> =
1346 HashMap::new();
1347
1348 for key in &deletes {
1349 let Some(tracked) = self.identity_map.get(key) else {
1350 continue;
1351 };
1352 if tracked.state != ObjectState::Deleted {
1353 continue;
1354 }
1355 let parent_pk_values = tracked.pk_values.clone();
1356
1357 for rel in tracked.relationships {
1358 if !rel.cascade_delete || rel.is_passive_deletes_all() {
1359 continue;
1360 }
1361
1362 match rel.kind {
1363 sqlmodel_core::RelationshipKind::OneToMany
1364 | sqlmodel_core::RelationshipKind::OneToOne => {
1365 if matches!(rel.passive_deletes, sqlmodel_core::PassiveDeletes::Passive) {
1368 continue;
1369 }
1370 let fk_cols = rel.remote_key_cols();
1371 if fk_cols.is_empty() {
1372 continue;
1373 }
1374 if fk_cols.len() == 1 && parent_pk_values.len() == 1 {
1375 cascade_child_deletes_single
1376 .entry((rel.related_table, fk_cols[0]))
1377 .or_default()
1378 .push(parent_pk_values[0].clone());
1379 } else {
1380 if fk_cols.len() != parent_pk_values.len() {
1382 continue;
1383 }
1384 cascade_child_deletes_composite
1385 .entry(CascadeChildDeleteKey {
1386 table: rel.related_table,
1387 fk_cols: fk_cols.to_vec(),
1388 })
1389 .or_default()
1390 .push(parent_pk_values.clone());
1391 }
1392 }
1393 sqlmodel_core::RelationshipKind::ManyToMany => {
1394 if matches!(rel.passive_deletes, sqlmodel_core::PassiveDeletes::Passive) {
1395 continue;
1396 }
1397 let Some(link) = rel.link_table else {
1398 continue;
1399 };
1400 let local_cols = link.local_cols();
1401 if local_cols.is_empty() {
1402 continue;
1403 }
1404 if local_cols.len() == 1 && parent_pk_values.len() == 1 {
1405 cascade_link_deletes_single
1406 .entry((link.table_name, local_cols[0]))
1407 .or_default()
1408 .push(parent_pk_values[0].clone());
1409 } else {
1410 if local_cols.len() != parent_pk_values.len() {
1411 continue;
1412 }
1413 cascade_link_deletes_composite
1414 .entry(CascadeChildDeleteKey {
1415 table: link.table_name,
1416 fk_cols: local_cols.to_vec(),
1417 })
1418 .or_default()
1419 .push(parent_pk_values.clone());
1420 }
1421 }
1422 sqlmodel_core::RelationshipKind::ManyToOne => {}
1423 }
1424 }
1425 }
1426
1427 let dedup_by_hash = |vals: &mut Vec<Value>| {
1428 let mut seen: std::collections::HashSet<u64> = std::collections::HashSet::new();
1429 vals.retain(|v| seen.insert(hash_values(std::slice::from_ref(v))));
1430 };
1431
1432 for ((child_table, fk_col), mut pks) in cascade_child_deletes_single {
1434 dedup_by_hash(&mut pks);
1435 if pks.is_empty() {
1436 continue;
1437 }
1438
1439 let placeholders: Vec<String> =
1440 (1..=pks.len()).map(|i| dialect.placeholder(i)).collect();
1441 let sql = format!(
1442 "DELETE FROM {} WHERE {} IN ({})",
1443 dialect.quote_identifier(child_table),
1444 dialect.quote_identifier(fk_col),
1445 placeholders.join(", ")
1446 );
1447
1448 match self.connection.execute(cx, &sql, &pks).await {
1449 Outcome::Ok(_) => {}
1450 Outcome::Err(e) => {
1451 self.pending_delete = deletes;
1452 return Outcome::Err(e);
1453 }
1454 Outcome::Cancelled(r) => {
1455 self.pending_delete = deletes;
1456 return Outcome::Cancelled(r);
1457 }
1458 Outcome::Panicked(p) => {
1459 self.pending_delete = deletes;
1460 return Outcome::Panicked(p);
1461 }
1462 }
1463
1464 let pk_hashes: std::collections::HashSet<u64> = pks
1466 .iter()
1467 .map(|v| hash_values(std::slice::from_ref(v)))
1468 .collect();
1469 let mut to_remove: Vec<ObjectKey> = Vec::new();
1470 for (k, t) in &self.identity_map {
1471 if t.table_name != child_table {
1472 continue;
1473 }
1474 let Some(idx) = t.column_names.iter().position(|col| *col == fk_col) else {
1475 continue;
1476 };
1477 let fk_val = &t.values[idx];
1478 if pk_hashes.contains(&hash_values(std::slice::from_ref(fk_val))) {
1479 to_remove.push(*k);
1480 }
1481 }
1482 for k in &to_remove {
1483 self.identity_map.remove(k);
1484 }
1485 self.pending_new.retain(|k| !to_remove.contains(k));
1486 self.pending_dirty.retain(|k| !to_remove.contains(k));
1487 self.pending_delete.retain(|k| !to_remove.contains(k));
1488 }
1489
1490 for (key, mut tuples) in cascade_child_deletes_composite {
1492 if tuples.is_empty() {
1493 continue;
1494 }
1495
1496 let mut seen: std::collections::HashSet<u64> = std::collections::HashSet::new();
1497 tuples.retain(|t| seen.insert(hash_values(t)));
1498
1499 if tuples.is_empty() {
1500 continue;
1501 }
1502
1503 let col_list = key
1504 .fk_cols
1505 .iter()
1506 .map(|c| dialect.quote_identifier(c))
1507 .collect::<Vec<_>>()
1508 .join(", ");
1509
1510 let mut params: Vec<Value> = Vec::with_capacity(tuples.len() * key.fk_cols.len());
1511 let mut idx = 1;
1512 let tuple_sql: Vec<String> = tuples
1513 .iter()
1514 .map(|t| {
1515 for v in t {
1516 params.push(v.clone());
1517 }
1518 let inner = (0..key.fk_cols.len())
1519 .map(|_| {
1520 let ph = dialect.placeholder(idx);
1521 idx += 1;
1522 ph
1523 })
1524 .collect::<Vec<_>>()
1525 .join(", ");
1526 format!("({})", inner)
1527 })
1528 .collect();
1529
1530 let sql = format!(
1531 "DELETE FROM {} WHERE ({}) IN ({})",
1532 dialect.quote_identifier(key.table),
1533 col_list,
1534 tuple_sql.join(", ")
1535 );
1536
1537 match self.connection.execute(cx, &sql, ¶ms).await {
1538 Outcome::Ok(_) => {}
1539 Outcome::Err(e) => {
1540 self.pending_delete = deletes;
1541 return Outcome::Err(e);
1542 }
1543 Outcome::Cancelled(r) => {
1544 self.pending_delete = deletes;
1545 return Outcome::Cancelled(r);
1546 }
1547 Outcome::Panicked(p) => {
1548 self.pending_delete = deletes;
1549 return Outcome::Panicked(p);
1550 }
1551 }
1552
1553 let tuple_hashes: std::collections::HashSet<u64> =
1555 tuples.iter().map(|t| hash_values(t)).collect();
1556 let mut to_remove: Vec<ObjectKey> = Vec::new();
1557 for (k, t) in &self.identity_map {
1558 if t.table_name != key.table {
1559 continue;
1560 }
1561
1562 let mut child_fk: Vec<Value> = Vec::with_capacity(key.fk_cols.len());
1563 let mut missing = false;
1564 for fk_col in &key.fk_cols {
1565 let Some(idx) = t.column_names.iter().position(|col| col == fk_col) else {
1566 missing = true;
1567 break;
1568 };
1569 child_fk.push(t.values[idx].clone());
1570 }
1571 if missing {
1572 continue;
1573 }
1574 if tuple_hashes.contains(&hash_values(&child_fk)) {
1575 to_remove.push(*k);
1576 }
1577 }
1578 for k in &to_remove {
1579 self.identity_map.remove(k);
1580 }
1581 self.pending_new.retain(|k| !to_remove.contains(k));
1582 self.pending_dirty.retain(|k| !to_remove.contains(k));
1583 self.pending_delete.retain(|k| !to_remove.contains(k));
1584 }
1585
1586 for ((link_table, local_col), mut pks) in cascade_link_deletes_single {
1588 dedup_by_hash(&mut pks);
1589 if pks.is_empty() {
1590 continue;
1591 }
1592
1593 let placeholders: Vec<String> =
1594 (1..=pks.len()).map(|i| dialect.placeholder(i)).collect();
1595 let sql = format!(
1596 "DELETE FROM {} WHERE {} IN ({})",
1597 dialect.quote_identifier(link_table),
1598 dialect.quote_identifier(local_col),
1599 placeholders.join(", ")
1600 );
1601
1602 match self.connection.execute(cx, &sql, &pks).await {
1603 Outcome::Ok(_) => {}
1604 Outcome::Err(e) => {
1605 self.pending_delete = deletes;
1606 return Outcome::Err(e);
1607 }
1608 Outcome::Cancelled(r) => {
1609 self.pending_delete = deletes;
1610 return Outcome::Cancelled(r);
1611 }
1612 Outcome::Panicked(p) => {
1613 self.pending_delete = deletes;
1614 return Outcome::Panicked(p);
1615 }
1616 }
1617 }
1618
1619 for (key, mut tuples) in cascade_link_deletes_composite {
1621 if tuples.is_empty() {
1622 continue;
1623 }
1624
1625 let mut seen: std::collections::HashSet<u64> = std::collections::HashSet::new();
1626 tuples.retain(|t| seen.insert(hash_values(t)));
1627
1628 if tuples.is_empty() {
1629 continue;
1630 }
1631
1632 let col_list = key
1633 .fk_cols
1634 .iter()
1635 .map(|c| dialect.quote_identifier(c))
1636 .collect::<Vec<_>>()
1637 .join(", ");
1638
1639 let mut params: Vec<Value> = Vec::with_capacity(tuples.len() * key.fk_cols.len());
1640 let mut idx = 1;
1641 let tuple_sql: Vec<String> = tuples
1642 .iter()
1643 .map(|t| {
1644 for v in t {
1645 params.push(v.clone());
1646 }
1647 let inner = (0..key.fk_cols.len())
1648 .map(|_| {
1649 let ph = dialect.placeholder(idx);
1650 idx += 1;
1651 ph
1652 })
1653 .collect::<Vec<_>>()
1654 .join(", ");
1655 format!("({})", inner)
1656 })
1657 .collect();
1658
1659 let sql = format!(
1660 "DELETE FROM {} WHERE ({}) IN ({})",
1661 dialect.quote_identifier(key.table),
1662 col_list,
1663 tuple_sql.join(", ")
1664 );
1665
1666 match self.connection.execute(cx, &sql, ¶ms).await {
1667 Outcome::Ok(_) => {}
1668 Outcome::Err(e) => {
1669 self.pending_delete = deletes;
1670 return Outcome::Err(e);
1671 }
1672 Outcome::Cancelled(r) => {
1673 self.pending_delete = deletes;
1674 return Outcome::Cancelled(r);
1675 }
1676 Outcome::Panicked(p) => {
1677 self.pending_delete = deletes;
1678 return Outcome::Panicked(p);
1679 }
1680 }
1681 }
1682
1683 let mut actually_deleted: Vec<ObjectKey> = Vec::new();
1684 for key in &deletes {
1685 if let Some(tracked) = self.identity_map.get(key) {
1686 if tracked.state != ObjectState::Deleted {
1688 continue;
1689 }
1690
1691 if tracked.pk_columns.is_empty() || tracked.pk_values.is_empty() {
1693 tracing::warn!(
1694 table = tracked.table_name,
1695 "Skipping DELETE for object without primary key - cannot identify row"
1696 );
1697 continue;
1698 }
1699
1700 let pk_columns = tracked.pk_columns.clone();
1702 let pk_values = tracked.pk_values.clone();
1703 let table_name = tracked.table_name;
1704 let relationships = tracked.relationships;
1705
1706 let where_parts: Vec<String> = pk_columns
1708 .iter()
1709 .enumerate()
1710 .map(|(i, col)| {
1711 format!(
1712 "{} = {}",
1713 dialect.quote_identifier(col),
1714 dialect.placeholder(i + 1)
1715 )
1716 })
1717 .collect();
1718
1719 let sql = format!(
1720 "DELETE FROM {} WHERE {}",
1721 dialect.quote_identifier(table_name),
1722 where_parts.join(" AND ")
1723 );
1724
1725 match self.connection.execute(cx, &sql, &pk_values).await {
1726 Outcome::Ok(_) => {
1727 actually_deleted.push(*key);
1728
1729 if !pk_values.is_empty() {
1732 let mut to_remove: Vec<ObjectKey> = Vec::new();
1733 for rel in relationships {
1734 if !rel.cascade_delete
1735 || !matches!(
1736 rel.passive_deletes,
1737 sqlmodel_core::PassiveDeletes::Passive
1738 )
1739 {
1740 continue;
1741 }
1742 if !matches!(
1743 rel.kind,
1744 sqlmodel_core::RelationshipKind::OneToMany
1745 | sqlmodel_core::RelationshipKind::OneToOne
1746 ) {
1747 continue;
1748 }
1749
1750 let fk_cols = rel.remote_key_cols();
1751 if fk_cols.is_empty() || fk_cols.len() != pk_values.len() {
1752 continue;
1753 }
1754
1755 for (k, t) in &self.identity_map {
1756 if t.table_name != rel.related_table {
1757 continue;
1758 }
1759 let mut matches_parent = true;
1760 for (fk_col, parent_val) in fk_cols.iter().zip(&pk_values) {
1761 let Some(idx) =
1762 t.column_names.iter().position(|col| col == fk_col)
1763 else {
1764 matches_parent = false;
1765 break;
1766 };
1767 if &t.values[idx] != parent_val {
1768 matches_parent = false;
1769 break;
1770 }
1771 }
1772 if matches_parent {
1773 to_remove.push(*k);
1774 }
1775 }
1776 }
1777
1778 for k in &to_remove {
1779 self.identity_map.remove(k);
1780 }
1781 self.pending_new.retain(|k| !to_remove.contains(k));
1782 self.pending_dirty.retain(|k| !to_remove.contains(k));
1783 self.pending_delete.retain(|k| !to_remove.contains(k));
1784 }
1785 }
1786 Outcome::Err(e) => {
1787 self.pending_delete = deletes
1790 .into_iter()
1791 .filter(|k| !actually_deleted.contains(k))
1792 .collect();
1793 for key in &actually_deleted {
1795 self.identity_map.remove(key);
1796 }
1797 return Outcome::Err(e);
1798 }
1799 Outcome::Cancelled(r) => {
1800 self.pending_delete = deletes
1802 .into_iter()
1803 .filter(|k| !actually_deleted.contains(k))
1804 .collect();
1805 for key in &actually_deleted {
1806 self.identity_map.remove(key);
1807 }
1808 return Outcome::Cancelled(r);
1809 }
1810 Outcome::Panicked(p) => {
1811 self.pending_delete = deletes
1813 .into_iter()
1814 .filter(|k| !actually_deleted.contains(k))
1815 .collect();
1816 for key in &actually_deleted {
1817 self.identity_map.remove(key);
1818 }
1819 return Outcome::Panicked(p);
1820 }
1821 }
1822 }
1823 }
1824
1825 for key in &actually_deleted {
1827 self.identity_map.remove(key);
1828 }
1829
1830 let inserts: Vec<ObjectKey> = std::mem::take(&mut self.pending_new);
1832 for key in &inserts {
1833 if let Some(tracked) = self.identity_map.get_mut(key) {
1834 if tracked.state == ObjectState::Persistent {
1836 continue;
1837 }
1838
1839 let columns = &tracked.column_names;
1841 let columns_sql: Vec<String> = columns
1842 .iter()
1843 .map(|c| dialect.quote_identifier(c))
1844 .collect();
1845 let placeholders: Vec<String> = (1..=columns.len())
1846 .map(|i| dialect.placeholder(i))
1847 .collect();
1848
1849 let sql = format!(
1850 "INSERT INTO {} ({}) VALUES ({})",
1851 dialect.quote_identifier(tracked.table_name),
1852 columns_sql.join(", "),
1853 placeholders.join(", ")
1854 );
1855
1856 match self.connection.execute(cx, &sql, &tracked.values).await {
1857 Outcome::Ok(_) => {
1858 tracked.state = ObjectState::Persistent;
1859 tracked.original_state =
1861 Some(serde_json::to_vec(&tracked.values).unwrap_or_default());
1862 }
1863 Outcome::Err(e) => {
1864 self.pending_new = inserts;
1866 return Outcome::Err(e);
1867 }
1868 Outcome::Cancelled(r) => {
1869 self.pending_new = inserts;
1871 return Outcome::Cancelled(r);
1872 }
1873 Outcome::Panicked(p) => {
1874 self.pending_new = inserts;
1876 return Outcome::Panicked(p);
1877 }
1878 }
1879 }
1880 }
1881
1882 let dirty: Vec<ObjectKey> = std::mem::take(&mut self.pending_dirty);
1884 for key in &dirty {
1885 if let Some(tracked) = self.identity_map.get_mut(key) {
1886 if tracked.state != ObjectState::Persistent {
1888 continue;
1889 }
1890
1891 if tracked.pk_columns.is_empty() || tracked.pk_values.is_empty() {
1893 tracing::warn!(
1894 table = tracked.table_name,
1895 "Skipping UPDATE for object without primary key - cannot identify row"
1896 );
1897 continue;
1898 }
1899
1900 let current_state = serde_json::to_vec(&tracked.values).unwrap_or_default();
1902 let is_dirty = tracked.original_state.as_ref() != Some(¤t_state);
1903
1904 if !is_dirty {
1905 continue;
1906 }
1907
1908 let mut set_parts = Vec::new();
1910 let mut params = Vec::new();
1911 let mut param_idx = 1;
1912
1913 for (i, col) in tracked.column_names.iter().enumerate() {
1914 if !tracked.pk_columns.contains(col) {
1916 set_parts.push(format!(
1917 "{} = {}",
1918 dialect.quote_identifier(col),
1919 dialect.placeholder(param_idx)
1920 ));
1921 params.push(tracked.values[i].clone());
1922 param_idx += 1;
1923 }
1924 }
1925
1926 let where_parts: Vec<String> = tracked
1928 .pk_columns
1929 .iter()
1930 .map(|col| {
1931 let clause = format!(
1932 "{} = {}",
1933 dialect.quote_identifier(col),
1934 dialect.placeholder(param_idx)
1935 );
1936 param_idx += 1;
1937 clause
1938 })
1939 .collect();
1940
1941 params.extend(tracked.pk_values.clone());
1943
1944 if set_parts.is_empty() {
1945 continue; }
1947
1948 let sql = format!(
1949 "UPDATE {} SET {} WHERE {}",
1950 dialect.quote_identifier(tracked.table_name),
1951 set_parts.join(", "),
1952 where_parts.join(" AND ")
1953 );
1954
1955 match self.connection.execute(cx, &sql, ¶ms).await {
1956 Outcome::Ok(_) => {
1957 tracked.original_state = Some(current_state);
1959 }
1960 Outcome::Err(e) => {
1961 self.pending_dirty = dirty;
1963 return Outcome::Err(e);
1964 }
1965 Outcome::Cancelled(r) => {
1966 self.pending_dirty = dirty;
1968 return Outcome::Cancelled(r);
1969 }
1970 Outcome::Panicked(p) => {
1971 self.pending_dirty = dirty;
1973 return Outcome::Panicked(p);
1974 }
1975 }
1976 }
1977 }
1978
1979 if let Err(e) = self.event_callbacks.fire(SessionEvent::AfterFlush) {
1981 return Outcome::Err(e);
1982 }
1983
1984 Outcome::Ok(())
1985 }
1986
1987 pub async fn commit(&mut self, cx: &Cx) -> Outcome<(), Error> {
1989 match self.flush(cx).await {
1991 Outcome::Ok(()) => {}
1992 Outcome::Err(e) => return Outcome::Err(e),
1993 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
1994 Outcome::Panicked(p) => return Outcome::Panicked(p),
1995 }
1996
1997 if let Err(e) = self.event_callbacks.fire(SessionEvent::BeforeCommit) {
1999 return Outcome::Err(e);
2000 }
2001
2002 if self.in_transaction {
2003 match self.connection.execute(cx, "COMMIT", &[]).await {
2004 Outcome::Ok(_) => {
2005 self.in_transaction = false;
2006 }
2007 Outcome::Err(e) => return Outcome::Err(e),
2008 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
2009 Outcome::Panicked(p) => return Outcome::Panicked(p),
2010 }
2011 }
2012
2013 if self.config.expire_on_commit {
2015 for tracked in self.identity_map.values_mut() {
2016 if tracked.state == ObjectState::Persistent {
2017 tracked.state = ObjectState::Expired;
2018 }
2019 }
2020 }
2021
2022 if let Err(e) = self.event_callbacks.fire(SessionEvent::AfterCommit) {
2024 return Outcome::Err(e);
2025 }
2026
2027 Outcome::Ok(())
2028 }
2029
2030 pub async fn rollback(&mut self, cx: &Cx) -> Outcome<(), Error> {
2032 if self.in_transaction {
2033 match self.connection.execute(cx, "ROLLBACK", &[]).await {
2034 Outcome::Ok(_) => {
2035 self.in_transaction = false;
2036 }
2037 Outcome::Err(e) => return Outcome::Err(e),
2038 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
2039 Outcome::Panicked(p) => return Outcome::Panicked(p),
2040 }
2041 }
2042
2043 self.pending_new.clear();
2045 self.pending_delete.clear();
2046 self.pending_dirty.clear();
2047
2048 let mut to_remove = Vec::new();
2050 for (key, tracked) in &mut self.identity_map {
2051 match tracked.state {
2052 ObjectState::New => {
2053 to_remove.push(*key);
2054 }
2055 ObjectState::Deleted => {
2056 tracked.state = ObjectState::Persistent;
2057 }
2058 _ => {}
2059 }
2060 }
2061
2062 for key in to_remove {
2063 self.identity_map.remove(&key);
2064 }
2065
2066 if let Err(e) = self.event_callbacks.fire(SessionEvent::AfterRollback) {
2068 return Outcome::Err(e);
2069 }
2070
2071 Outcome::Ok(())
2072 }
2073
2074 #[tracing::instrument(level = "debug", skip(self, lazy, cx))]
2090 pub async fn load_lazy<
2091 T: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
2092 >(
2093 &mut self,
2094 lazy: &Lazy<T>,
2095 cx: &Cx,
2096 ) -> Outcome<bool, Error> {
2097 tracing::debug!(
2098 model = std::any::type_name::<T>(),
2099 fk = ?lazy.fk(),
2100 already_loaded = lazy.is_loaded(),
2101 "Loading lazy relationship"
2102 );
2103
2104 if lazy.is_loaded() {
2106 tracing::trace!("Already loaded");
2107 return Outcome::Ok(lazy.get().is_some());
2108 }
2109
2110 let Some(fk) = lazy.fk() else {
2112 let _ = lazy.set_loaded(None);
2113 return Outcome::Ok(false);
2114 };
2115
2116 let obj = match self.get::<T>(cx, fk.clone()).await {
2118 Outcome::Ok(obj) => obj,
2119 Outcome::Err(e) => return Outcome::Err(e),
2120 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
2121 Outcome::Panicked(p) => return Outcome::Panicked(p),
2122 };
2123
2124 let found = obj.is_some();
2125
2126 let _ = lazy.set_loaded(obj);
2128
2129 tracing::debug!(found = found, "Lazy load complete");
2130
2131 Outcome::Ok(found)
2132 }
2133
2134 #[tracing::instrument(level = "debug", skip(self, cx, objects, accessor))]
2158 pub async fn load_many<P, T, F>(
2159 &mut self,
2160 cx: &Cx,
2161 objects: &[P],
2162 accessor: F,
2163 ) -> Outcome<usize, Error>
2164 where
2165 P: Model + 'static,
2166 T: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
2167 F: Fn(&P) -> &Lazy<T>,
2168 {
2169 let mut fk_values: Vec<Value> = Vec::new();
2171 let mut fk_indices: Vec<usize> = Vec::new();
2172
2173 for (idx, obj) in objects.iter().enumerate() {
2174 let lazy = accessor(obj);
2175 if !lazy.is_loaded()
2176 && !lazy.is_empty()
2177 && let Some(fk) = lazy.fk()
2178 {
2179 fk_values.push(fk.clone());
2180 fk_indices.push(idx);
2181 }
2182 }
2183
2184 let fk_count = fk_values.len();
2185 tracing::info!(
2186 parent_model = std::any::type_name::<P>(),
2187 related_model = std::any::type_name::<T>(),
2188 parent_count = objects.len(),
2189 fk_count = fk_count,
2190 "Batch loading lazy relationships"
2191 );
2192
2193 if fk_values.is_empty() {
2194 for obj in objects {
2196 let lazy = accessor(obj);
2197 if !lazy.is_loaded() && lazy.is_empty() {
2198 let _ = lazy.set_loaded(None);
2199 }
2200 }
2201 return Outcome::Ok(0);
2202 }
2203
2204 let dialect = self.connection.dialect();
2206 let pk_col = T::PRIMARY_KEY.first().unwrap_or(&"id");
2207 let placeholders: Vec<String> = (1..=fk_values.len())
2208 .map(|i| dialect.placeholder(i))
2209 .collect();
2210 let sql = format!(
2211 "SELECT * FROM {} WHERE {} IN ({})",
2212 dialect.quote_identifier(T::TABLE_NAME),
2213 dialect.quote_identifier(pk_col),
2214 placeholders.join(", ")
2215 );
2216
2217 let rows = match self.connection.query(cx, &sql, &fk_values).await {
2218 Outcome::Ok(rows) => rows,
2219 Outcome::Err(e) => return Outcome::Err(e),
2220 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
2221 Outcome::Panicked(p) => return Outcome::Panicked(p),
2222 };
2223
2224 let mut lookup: HashMap<u64, T> = HashMap::new();
2226 for row in &rows {
2227 match T::from_row(row) {
2228 Ok(obj) => {
2229 let pk_values = obj.primary_key_value();
2230 let pk_hash = hash_values(&pk_values);
2231
2232 let key = ObjectKey::from_pk::<T>(&pk_values);
2234
2235 let row_data = obj.to_row();
2237 let column_names: Vec<&'static str> =
2238 row_data.iter().map(|(name, _)| *name).collect();
2239 let values: Vec<Value> = row_data.into_iter().map(|(_, v)| v).collect();
2240
2241 let serialized = serde_json::to_vec(&values).ok();
2243
2244 let tracked = TrackedObject {
2245 object: Box::new(obj.clone()),
2246 original_state: serialized,
2247 state: ObjectState::Persistent,
2248 table_name: T::TABLE_NAME,
2249 column_names,
2250 values,
2251 pk_columns: T::PRIMARY_KEY.to_vec(),
2252 pk_values: pk_values.clone(),
2253 relationships: T::RELATIONSHIPS,
2254 expired_attributes: None,
2255 };
2256 self.identity_map.insert(key, tracked);
2257
2258 lookup.insert(pk_hash, obj);
2260 }
2261 Err(_) => continue,
2262 }
2263 }
2264
2265 let mut loaded_count = 0;
2267 for obj in objects {
2268 let lazy = accessor(obj);
2269 if !lazy.is_loaded() {
2270 if let Some(fk) = lazy.fk() {
2271 let fk_hash = hash_values(std::slice::from_ref(fk));
2272 let related = lookup.get(&fk_hash).cloned();
2273 let found = related.is_some();
2274 let _ = lazy.set_loaded(related);
2275 if found {
2276 loaded_count += 1;
2277 }
2278 } else {
2279 let _ = lazy.set_loaded(None);
2280 }
2281 }
2282 }
2283
2284 tracing::debug!(
2285 query_count = 1,
2286 loaded_count = loaded_count,
2287 "Batch load complete"
2288 );
2289
2290 Outcome::Ok(loaded_count)
2291 }
2292
2293 #[tracing::instrument(level = "debug", skip(self, cx, objects, accessor, parent_pk))]
2317 pub async fn load_many_to_many<P, Child, FA, FP>(
2318 &mut self,
2319 cx: &Cx,
2320 objects: &mut [P],
2321 accessor: FA,
2322 parent_pk: FP,
2323 link_table: &sqlmodel_core::LinkTableInfo,
2324 ) -> Outcome<usize, Error>
2325 where
2326 P: Model + 'static,
2327 Child: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
2328 FA: Fn(&mut P) -> &mut sqlmodel_core::RelatedMany<Child>,
2329 FP: Fn(&P) -> Value,
2330 {
2331 self.load_many_to_many_pk(cx, objects, accessor, |p| vec![parent_pk(p)], link_table)
2332 .await
2333 }
2334
2335 #[tracing::instrument(level = "debug", skip(self, cx, objects, accessor, parent_pk))]
2340 pub async fn load_many_to_many_pk<P, Child, FA, FP>(
2341 &mut self,
2342 cx: &Cx,
2343 objects: &mut [P],
2344 accessor: FA,
2345 parent_pk: FP,
2346 link_table: &sqlmodel_core::LinkTableInfo,
2347 ) -> Outcome<usize, Error>
2348 where
2349 P: Model + 'static,
2350 Child: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
2351 FA: Fn(&mut P) -> &mut sqlmodel_core::RelatedMany<Child>,
2352 FP: Fn(&P) -> Vec<Value>,
2353 {
2354 let mut pk_tuples: Vec<Vec<Value>> = Vec::with_capacity(objects.len());
2356 let mut pk_by_index: Vec<(usize, Vec<Value>)> = Vec::new();
2357 for (idx, obj) in objects.iter().enumerate() {
2358 let pk = parent_pk(obj);
2359 pk_tuples.push(pk.clone());
2360 pk_by_index.push((idx, pk));
2361 }
2362
2363 tracing::info!(
2364 parent_model = std::any::type_name::<P>(),
2365 related_model = std::any::type_name::<Child>(),
2366 parent_count = pk_tuples.len(),
2367 link_table = link_table.table_name,
2368 "Batch loading many-to-many relationships"
2369 );
2370
2371 if pk_tuples.is_empty() {
2372 return Outcome::Ok(0);
2373 }
2374
2375 let dialect = self.connection.dialect();
2381 let local_cols = link_table.local_cols();
2382 let remote_cols = link_table.remote_cols();
2383 if local_cols.is_empty() || remote_cols.is_empty() {
2384 return Outcome::Err(Error::Custom(
2385 "link_table must specify local/remote columns".to_string(),
2386 ));
2387 }
2388 if remote_cols.len() != Child::PRIMARY_KEY.len() {
2389 return Outcome::Err(Error::Custom(format!(
2390 "link_table remote cols count ({}) must match child PRIMARY_KEY len ({})",
2391 remote_cols.len(),
2392 Child::PRIMARY_KEY.len()
2393 )));
2394 }
2395
2396 let child_table = dialect.quote_identifier(Child::TABLE_NAME);
2397 let link_table_q = dialect.quote_identifier(link_table.table_name);
2398
2399 let parent_select_parts: String = local_cols
2400 .iter()
2401 .enumerate()
2402 .map(|(i, col)| {
2403 format!(
2404 "{link_table_q}.{} AS __parent_pk{}",
2405 dialect.quote_identifier(col),
2406 i
2407 )
2408 })
2409 .collect::<Vec<_>>()
2410 .join(", ");
2411
2412 let join_parts: String = remote_cols
2413 .iter()
2414 .zip(Child::PRIMARY_KEY.iter().copied())
2415 .map(|(link_col, child_col)| {
2416 format!(
2417 "{child_table}.{} = {link_table_q}.{}",
2418 dialect.quote_identifier(child_col),
2419 dialect.quote_identifier(link_col)
2420 )
2421 })
2422 .collect::<Vec<_>>()
2423 .join(" AND ");
2424
2425 let (where_sql, params) = if local_cols.len() == 1 {
2426 let mut params: Vec<Value> = Vec::with_capacity(pk_tuples.len());
2427 for t in &pk_tuples {
2428 if let Some(v) = t.first() {
2429 params.push(v.clone());
2430 }
2431 }
2432 let placeholders: Vec<String> =
2433 (1..=params.len()).map(|i| dialect.placeholder(i)).collect();
2434 let where_sql = format!(
2435 "{link_table_q}.{} IN ({})",
2436 dialect.quote_identifier(local_cols[0]),
2437 placeholders.join(", ")
2438 );
2439 (where_sql, params)
2440 } else {
2441 let mut tuples: Vec<Vec<Value>> = Vec::with_capacity(pk_tuples.len());
2442 for t in &pk_tuples {
2443 if t.len() == local_cols.len() {
2444 tuples.push(t.clone());
2445 }
2446 }
2447
2448 let mut params: Vec<Value> = Vec::with_capacity(tuples.len() * local_cols.len());
2449 let mut idx = 1;
2450 let tuple_sql: Vec<String> = tuples
2451 .iter()
2452 .map(|t| {
2453 for v in t {
2454 params.push(v.clone());
2455 }
2456 let inner = (0..local_cols.len())
2457 .map(|_| {
2458 let ph = dialect.placeholder(idx);
2459 idx += 1;
2460 ph
2461 })
2462 .collect::<Vec<_>>()
2463 .join(", ");
2464 format!("({})", inner)
2465 })
2466 .collect();
2467
2468 let col_list = local_cols
2469 .iter()
2470 .map(|c| format!("{link_table_q}.{}", dialect.quote_identifier(c)))
2471 .collect::<Vec<_>>()
2472 .join(", ");
2473
2474 let where_sql = format!("({}) IN ({})", col_list, tuple_sql.join(", "));
2475 (where_sql, params)
2476 };
2477
2478 let sql = format!(
2479 "SELECT {child_table}.*, {parent_select_parts} FROM {child_table} \
2480 JOIN {link_table_q} ON {join_parts} \
2481 WHERE {where_sql}"
2482 );
2483
2484 tracing::trace!(sql = %sql, "Many-to-many batch SQL");
2485
2486 let rows = match self.connection.query(cx, &sql, ¶ms).await {
2487 Outcome::Ok(rows) => rows,
2488 Outcome::Err(e) => return Outcome::Err(e),
2489 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
2490 Outcome::Panicked(p) => return Outcome::Panicked(p),
2491 };
2492
2493 let mut by_parent: HashMap<u64, Vec<Child>> = HashMap::new();
2495 for row in &rows {
2496 let mut parent_tuple: Vec<Value> = Vec::with_capacity(local_cols.len());
2498 let mut missing = false;
2499 for i in 0..local_cols.len() {
2500 let col = format!("__parent_pk{}", i);
2501 let Some(v) = row.get_by_name(&col) else {
2502 missing = true;
2503 break;
2504 };
2505 parent_tuple.push(v.clone());
2506 }
2507 if missing {
2508 continue;
2509 }
2510 let parent_pk_hash = hash_values(&parent_tuple);
2511
2512 match Child::from_row(row) {
2514 Ok(child) => {
2515 by_parent.entry(parent_pk_hash).or_default().push(child);
2516 }
2517 Err(_) => continue,
2518 }
2519 }
2520
2521 let mut loaded_count = 0;
2523 for (idx, pk_tuple) in pk_by_index {
2524 let pk_hash = hash_values(&pk_tuple);
2525 let children = by_parent.get(&pk_hash).cloned().unwrap_or_default();
2527 let child_count = children.len();
2528
2529 let related = accessor(&mut objects[idx]);
2530 if pk_tuple.len() == 1 {
2531 related.set_parent_pk(pk_tuple[0].clone());
2532 } else {
2533 related.set_parent_pk(Value::Array(pk_tuple.clone()));
2534 }
2535 let _ = related.set_loaded(children);
2536 loaded_count += child_count;
2537 }
2538
2539 tracing::debug!(
2540 query_count = 1,
2541 total_children = loaded_count,
2542 "Many-to-many batch load complete"
2543 );
2544
2545 Outcome::Ok(loaded_count)
2546 }
2547
2548 #[tracing::instrument(level = "debug", skip(self, cx, objects, accessor, parent_pk))]
2557 pub async fn load_one_to_many<P, Child, FA, FP>(
2558 &mut self,
2559 cx: &Cx,
2560 objects: &mut [P],
2561 accessor: FA,
2562 parent_pk: FP,
2563 ) -> Outcome<usize, Error>
2564 where
2565 P: Model + 'static,
2566 Child: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
2567 FA: Fn(&mut P) -> &mut sqlmodel_core::RelatedMany<Child>,
2568 FP: Fn(&P) -> Value,
2569 {
2570 let mut pks: Vec<Value> = Vec::new();
2572 let mut pk_by_index: Vec<(usize, Value)> = Vec::new();
2573 for (idx, obj) in objects.iter_mut().enumerate() {
2574 let pk = parent_pk(&*obj);
2575 let related = accessor(obj);
2576 if related.is_loaded() {
2577 continue;
2578 }
2579
2580 related.set_parent_pk(pk.clone());
2581
2582 if matches!(pk, Value::Null) {
2583 let _ = related.set_loaded(Vec::new());
2585 continue;
2586 }
2587
2588 pks.push(pk.clone());
2589 pk_by_index.push((idx, pk));
2590 }
2591
2592 tracing::info!(
2593 parent_model = std::any::type_name::<P>(),
2594 related_model = std::any::type_name::<Child>(),
2595 parent_count = objects.len(),
2596 query_parent_count = pks.len(),
2597 "Batch loading one-to-many relationships"
2598 );
2599
2600 if pks.is_empty() {
2601 return Outcome::Ok(0);
2602 }
2603
2604 let fk_column = accessor(&mut objects[pk_by_index[0].0]).fk_column();
2606 let dialect = self.connection.dialect();
2607 let placeholders: Vec<String> = (1..=pks.len()).map(|i| dialect.placeholder(i)).collect();
2608 let child_table = dialect.quote_identifier(Child::TABLE_NAME);
2609 let fk_q = dialect.quote_identifier(fk_column);
2610 let sql = format!(
2611 "SELECT *, {fk_q} AS __parent_pk FROM {child_table} WHERE {fk_q} IN ({})",
2612 placeholders.join(", ")
2613 );
2614
2615 tracing::trace!(sql = %sql, "One-to-many batch SQL");
2616
2617 let rows = match self.connection.query(cx, &sql, &pks).await {
2618 Outcome::Ok(rows) => rows,
2619 Outcome::Err(e) => return Outcome::Err(e),
2620 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
2621 Outcome::Panicked(p) => return Outcome::Panicked(p),
2622 };
2623
2624 let mut by_parent: HashMap<u64, Vec<Child>> = HashMap::new();
2626 for row in &rows {
2627 let parent_pk_value: Value = match row.get_by_name("__parent_pk") {
2628 Some(v) => v.clone(),
2629 None => continue,
2630 };
2631 let parent_pk_hash = hash_values(std::slice::from_ref(&parent_pk_value));
2632 match Child::from_row(row) {
2633 Ok(child) => {
2634 let pk_values = child.primary_key_value();
2636 let key = ObjectKey::from_pk::<Child>(&pk_values);
2637
2638 self.identity_map.entry(key).or_insert_with(|| {
2639 let row_data = child.to_row();
2641 let column_names: Vec<&'static str> =
2642 row_data.iter().map(|(name, _)| *name).collect();
2643 let values: Vec<Value> = row_data.into_iter().map(|(_, v)| v).collect();
2644
2645 let serialized = serde_json::to_vec(&values).ok();
2647
2648 TrackedObject {
2649 object: Box::new(child.clone()),
2650 original_state: serialized,
2651 state: ObjectState::Persistent,
2652 table_name: Child::TABLE_NAME,
2653 column_names,
2654 values,
2655 pk_columns: Child::PRIMARY_KEY.to_vec(),
2656 pk_values: pk_values.clone(),
2657 relationships: Child::RELATIONSHIPS,
2658 expired_attributes: None,
2659 }
2660 });
2661
2662 by_parent.entry(parent_pk_hash).or_default().push(child);
2663 }
2664 Err(_) => continue,
2665 }
2666 }
2667
2668 let mut loaded_count = 0;
2670 for (idx, pk) in pk_by_index {
2671 let pk_hash = hash_values(std::slice::from_ref(&pk));
2672 let children = by_parent.get(&pk_hash).cloned().unwrap_or_default();
2674 loaded_count += children.len();
2675
2676 let related = accessor(&mut objects[idx]);
2677 let _ = related.set_loaded(children);
2678 }
2679
2680 Outcome::Ok(loaded_count)
2681 }
2682
2683 #[tracing::instrument(level = "debug", skip(self, cx, objects, accessor, parent_pk))]
2702 pub async fn flush_related_many<P, Child, FA, FP>(
2703 &mut self,
2704 cx: &Cx,
2705 objects: &mut [P],
2706 accessor: FA,
2707 parent_pk: FP,
2708 link_table: &sqlmodel_core::LinkTableInfo,
2709 ) -> Outcome<usize, Error>
2710 where
2711 P: Model + 'static,
2712 Child: Model + 'static,
2713 FA: Fn(&mut P) -> &mut sqlmodel_core::RelatedMany<Child>,
2714 FP: Fn(&P) -> Value,
2715 {
2716 self.flush_related_many_pk(cx, objects, accessor, |p| vec![parent_pk(p)], link_table)
2717 .await
2718 }
2719
2720 #[tracing::instrument(level = "debug", skip(self, cx, objects, accessor, parent_pk))]
2722 pub async fn flush_related_many_pk<P, Child, FA, FP>(
2723 &mut self,
2724 cx: &Cx,
2725 objects: &mut [P],
2726 accessor: FA,
2727 parent_pk: FP,
2728 link_table: &sqlmodel_core::LinkTableInfo,
2729 ) -> Outcome<usize, Error>
2730 where
2731 P: Model + 'static,
2732 Child: Model + 'static,
2733 FA: Fn(&mut P) -> &mut sqlmodel_core::RelatedMany<Child>,
2734 FP: Fn(&P) -> Vec<Value>,
2735 {
2736 let mut ops = Vec::new();
2737 let local_cols = link_table.local_cols();
2738 let remote_cols = link_table.remote_cols();
2739 if local_cols.is_empty() || remote_cols.is_empty() {
2740 return Outcome::Err(Error::Custom(
2741 "link_table must specify local/remote columns".to_string(),
2742 ));
2743 }
2744
2745 for obj in objects.iter_mut() {
2747 let parent_pk_values = parent_pk(obj);
2748 if parent_pk_values.len() != local_cols.len() {
2749 return Outcome::Err(Error::Custom(format!(
2750 "parent_pk len ({}) must match link_table local cols len ({})",
2751 parent_pk_values.len(),
2752 local_cols.len()
2753 )));
2754 }
2755 let related = accessor(obj);
2756
2757 for child_pk_values in related.take_pending_links() {
2759 if child_pk_values.len() != remote_cols.len() {
2760 return Outcome::Err(Error::Custom(format!(
2761 "child pk len ({}) must match link_table remote cols len ({})",
2762 child_pk_values.len(),
2763 remote_cols.len()
2764 )));
2765 }
2766 ops.push(LinkTableOp::link_multi(
2767 link_table.table_name.to_string(),
2768 local_cols.iter().map(|c| (*c).to_string()).collect(),
2769 parent_pk_values.clone(),
2770 remote_cols.iter().map(|c| (*c).to_string()).collect(),
2771 child_pk_values,
2772 ));
2773 }
2774
2775 for child_pk_values in related.take_pending_unlinks() {
2777 if child_pk_values.len() != remote_cols.len() {
2778 return Outcome::Err(Error::Custom(format!(
2779 "child pk len ({}) must match link_table remote cols len ({})",
2780 child_pk_values.len(),
2781 remote_cols.len()
2782 )));
2783 }
2784 ops.push(LinkTableOp::unlink_multi(
2785 link_table.table_name.to_string(),
2786 local_cols.iter().map(|c| (*c).to_string()).collect(),
2787 parent_pk_values.clone(),
2788 remote_cols.iter().map(|c| (*c).to_string()).collect(),
2789 child_pk_values,
2790 ));
2791 }
2792 }
2793
2794 if ops.is_empty() {
2795 return Outcome::Ok(0);
2796 }
2797
2798 tracing::info!(
2799 parent_model = std::any::type_name::<P>(),
2800 related_model = std::any::type_name::<Child>(),
2801 link_count = ops
2802 .iter()
2803 .filter(|o| matches!(o, LinkTableOp::Link { .. }))
2804 .count(),
2805 unlink_count = ops
2806 .iter()
2807 .filter(|o| matches!(o, LinkTableOp::Unlink { .. }))
2808 .count(),
2809 link_table = link_table.table_name,
2810 "Flushing many-to-many relationship changes"
2811 );
2812
2813 flush::execute_link_table_ops(cx, &self.connection, &ops).await
2814 }
2815
2816 pub fn relate_to_one<Child, Parent, FC, FP, FK>(
2841 &self,
2842 child: &mut Child,
2843 child_accessor: FC,
2844 set_fk: FK,
2845 parent: &mut Parent,
2846 parent_accessor: FP,
2847 ) where
2848 Child: Model + Clone + 'static,
2849 Parent: Model + Clone + 'static,
2850 FC: FnOnce(&mut Child) -> &mut sqlmodel_core::Related<Parent>,
2851 FP: FnOnce(&mut Parent) -> &mut sqlmodel_core::RelatedMany<Child>,
2852 FK: FnOnce(&mut Child),
2853 {
2854 let related = child_accessor(child);
2856 let _ = related.set_loaded(Some(parent.clone()));
2857
2858 set_fk(child);
2860
2861 let related_many = parent_accessor(parent);
2863 related_many.link(child);
2864
2865 tracing::debug!(
2866 child_model = std::any::type_name::<Child>(),
2867 parent_model = std::any::type_name::<Parent>(),
2868 "Established bidirectional ManyToOne <-> OneToMany relationship"
2869 );
2870 }
2871
2872 pub fn unrelate_from_one<Child, Parent, FC, FP, FK>(
2888 &self,
2889 child: &mut Child,
2890 child_accessor: FC,
2891 clear_fk: FK,
2892 parent: &mut Parent,
2893 parent_accessor: FP,
2894 ) where
2895 Child: Model + Clone + 'static,
2896 Parent: Model + Clone + 'static,
2897 FC: FnOnce(&mut Child) -> &mut sqlmodel_core::Related<Parent>,
2898 FP: FnOnce(&mut Parent) -> &mut sqlmodel_core::RelatedMany<Child>,
2899 FK: FnOnce(&mut Child),
2900 {
2901 let related = child_accessor(child);
2903 *related = sqlmodel_core::Related::empty();
2904
2905 clear_fk(child);
2907
2908 let related_many = parent_accessor(parent);
2910 related_many.unlink(child);
2911
2912 tracing::debug!(
2913 child_model = std::any::type_name::<Child>(),
2914 parent_model = std::any::type_name::<Parent>(),
2915 "Removed bidirectional ManyToOne <-> OneToMany relationship"
2916 );
2917 }
2918
2919 pub fn relate_many_to_many<Left, Right, FL, FR>(
2938 &self,
2939 left: &mut Left,
2940 left_accessor: FL,
2941 right: &mut Right,
2942 right_accessor: FR,
2943 ) where
2944 Left: Model + Clone + 'static,
2945 Right: Model + Clone + 'static,
2946 FL: FnOnce(&mut Left) -> &mut sqlmodel_core::RelatedMany<Right>,
2947 FR: FnOnce(&mut Right) -> &mut sqlmodel_core::RelatedMany<Left>,
2948 {
2949 let left_coll = left_accessor(left);
2951 left_coll.link(right);
2952
2953 let right_coll = right_accessor(right);
2955 right_coll.link(left);
2956
2957 tracing::debug!(
2958 left_model = std::any::type_name::<Left>(),
2959 right_model = std::any::type_name::<Right>(),
2960 "Established bidirectional ManyToMany relationship"
2961 );
2962 }
2963
2964 pub fn unrelate_many_to_many<Left, Right, FL, FR>(
2968 &self,
2969 left: &mut Left,
2970 left_accessor: FL,
2971 right: &mut Right,
2972 right_accessor: FR,
2973 ) where
2974 Left: Model + Clone + 'static,
2975 Right: Model + Clone + 'static,
2976 FL: FnOnce(&mut Left) -> &mut sqlmodel_core::RelatedMany<Right>,
2977 FR: FnOnce(&mut Right) -> &mut sqlmodel_core::RelatedMany<Left>,
2978 {
2979 let left_coll = left_accessor(left);
2981 left_coll.unlink(right);
2982
2983 let right_coll = right_accessor(right);
2985 right_coll.unlink(left);
2986
2987 tracing::debug!(
2988 left_model = std::any::type_name::<Left>(),
2989 right_model = std::any::type_name::<Right>(),
2990 "Removed bidirectional ManyToMany relationship"
2991 );
2992 }
2993
2994 pub fn enable_n1_detection(&mut self, threshold: usize) {
3019 self.n1_tracker = Some(N1QueryTracker::new().with_threshold(threshold));
3020 }
3021
3022 pub fn disable_n1_detection(&mut self) {
3024 self.n1_tracker = None;
3025 }
3026
3027 #[must_use]
3029 pub fn n1_detection_enabled(&self) -> bool {
3030 self.n1_tracker.is_some()
3031 }
3032
3033 pub fn n1_tracker_mut(&mut self) -> Option<&mut N1QueryTracker> {
3035 self.n1_tracker.as_mut()
3036 }
3037
3038 #[must_use]
3040 pub fn n1_stats(&self) -> Option<N1Stats> {
3041 self.n1_tracker.as_ref().map(|t| t.stats())
3042 }
3043
3044 pub fn reset_n1_tracking(&mut self) {
3046 if let Some(tracker) = &mut self.n1_tracker {
3047 tracker.reset();
3048 }
3049 }
3050
3051 #[track_caller]
3055 pub fn record_lazy_load(&mut self, parent_type: &'static str, relationship: &'static str) {
3056 if let Some(tracker) = &mut self.n1_tracker {
3057 tracker.record_load(parent_type, relationship);
3058 }
3059 }
3060
3061 #[tracing::instrument(level = "debug", skip(self, cx, model), fields(table = M::TABLE_NAME))]
3105 pub async fn merge<
3106 M: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
3107 >(
3108 &mut self,
3109 cx: &Cx,
3110 model: M,
3111 load: bool,
3112 ) -> Outcome<M, Error> {
3113 let pk_values = model.primary_key_value();
3114 let key = ObjectKey::from_model(&model);
3115
3116 tracing::debug!(
3117 pk = ?pk_values,
3118 load = load,
3119 in_identity_map = self.identity_map.contains_key(&key),
3120 "Merging object"
3121 );
3122
3123 if let Some(tracked) = self.identity_map.get_mut(&key) {
3125 if tracked.state == ObjectState::Detached {
3127 tracing::debug!("Found detached object, treating as new");
3128 } else {
3129 tracing::debug!(
3130 state = ?tracked.state,
3131 "Found tracked object, updating with merged values"
3132 );
3133
3134 let row_data = model.to_row();
3136 tracked.object = Box::new(model.clone());
3137 tracked.column_names = row_data.iter().map(|(name, _)| *name).collect();
3138 tracked.values = row_data.into_iter().map(|(_, v)| v).collect();
3139 tracked.pk_values.clone_from(&pk_values);
3140
3141 if tracked.state == ObjectState::Persistent && !self.pending_dirty.contains(&key) {
3143 self.pending_dirty.push(key);
3144 }
3145
3146 if let Some(obj) = tracked.object.downcast_ref::<M>() {
3148 return Outcome::Ok(obj.clone());
3149 }
3150 }
3151 }
3152
3153 if load {
3155 let has_valid_pk = pk_values
3157 .iter()
3158 .all(|v| !matches!(v, Value::Null | Value::Default));
3159
3160 if has_valid_pk {
3161 tracing::debug!("Loading from database");
3162
3163 let db_result = self.get_by_pk::<M>(cx, &pk_values).await;
3164 match db_result {
3165 Outcome::Ok(Some(_existing)) => {
3166 if let Some(tracked) = self.identity_map.get_mut(&key) {
3169 let row_data = model.to_row();
3170 tracked.object = Box::new(model.clone());
3171 tracked.column_names = row_data.iter().map(|(name, _)| *name).collect();
3172 tracked.values = row_data.into_iter().map(|(_, v)| v).collect();
3173 if !self.pending_dirty.contains(&key) {
3177 self.pending_dirty.push(key);
3178 }
3179
3180 tracing::debug!("Merged values onto DB object");
3181
3182 if let Some(obj) = tracked.object.downcast_ref::<M>() {
3183 return Outcome::Ok(obj.clone());
3184 }
3185 }
3186 }
3187 Outcome::Ok(None) => {
3188 tracing::debug!("Object not found in database, treating as new");
3189 }
3190 Outcome::Err(e) => return Outcome::Err(e),
3191 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
3192 Outcome::Panicked(p) => return Outcome::Panicked(p),
3193 }
3194 }
3195 }
3196
3197 tracing::debug!("Adding as new object");
3199 self.add(&model);
3200
3201 Outcome::Ok(model)
3202 }
3203
3204 pub async fn merge_without_load<
3216 M: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
3217 >(
3218 &mut self,
3219 cx: &Cx,
3220 model: M,
3221 ) -> Outcome<M, Error> {
3222 self.merge(cx, model, false).await
3223 }
3224
3225 pub fn pending_new_count(&self) -> usize {
3231 self.pending_new.len()
3232 }
3233
3234 pub fn pending_delete_count(&self) -> usize {
3236 self.pending_delete.len()
3237 }
3238
3239 pub fn pending_dirty_count(&self) -> usize {
3241 self.pending_dirty.len()
3242 }
3243
3244 pub fn tracked_count(&self) -> usize {
3246 self.identity_map.len()
3247 }
3248
3249 pub fn in_transaction(&self) -> bool {
3251 self.in_transaction
3252 }
3253
3254 pub fn debug_state(&self) -> SessionDebugInfo {
3256 SessionDebugInfo {
3257 tracked: self.tracked_count(),
3258 pending_new: self.pending_new_count(),
3259 pending_delete: self.pending_delete_count(),
3260 pending_dirty: self.pending_dirty_count(),
3261 in_transaction: self.in_transaction,
3262 }
3263 }
3264
3265 pub async fn bulk_insert<M: Model + Clone + Send + Sync + 'static>(
3279 &mut self,
3280 cx: &Cx,
3281 models: &[M],
3282 ) -> Outcome<u64, Error> {
3283 self.bulk_insert_with_batch_size(cx, models, 1000).await
3284 }
3285
3286 pub async fn bulk_insert_with_batch_size<M: Model + Clone + Send + Sync + 'static>(
3288 &mut self,
3289 cx: &Cx,
3290 models: &[M],
3291 batch_size: usize,
3292 ) -> Outcome<u64, Error> {
3293 if models.is_empty() {
3294 return Outcome::Ok(0);
3295 }
3296
3297 let batch_size = batch_size.max(1);
3298 let mut total_inserted: u64 = 0;
3299
3300 for chunk in models.chunks(batch_size) {
3301 let builder = sqlmodel_query::InsertManyBuilder::new(chunk);
3302 match builder.execute(cx, &self.connection).await {
3303 Outcome::Ok(count) => total_inserted += count,
3304 Outcome::Err(e) => return Outcome::Err(e),
3305 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
3306 Outcome::Panicked(p) => return Outcome::Panicked(p),
3307 }
3308 }
3309
3310 Outcome::Ok(total_inserted)
3311 }
3312
3313 pub async fn bulk_update<M: Model + Clone + Send + Sync + 'static>(
3321 &mut self,
3322 cx: &Cx,
3323 models: &[M],
3324 ) -> Outcome<u64, Error> {
3325 if models.is_empty() {
3326 return Outcome::Ok(0);
3327 }
3328
3329 let mut total_updated: u64 = 0;
3330
3331 for model in models {
3332 let builder = sqlmodel_query::UpdateBuilder::new(model);
3333 let (sql, params) = builder.build_with_dialect(self.connection.dialect());
3334
3335 if sql.is_empty() {
3336 continue;
3337 }
3338
3339 match self.connection.execute(cx, &sql, ¶ms).await {
3340 Outcome::Ok(count) => total_updated += count,
3341 Outcome::Err(e) => return Outcome::Err(e),
3342 Outcome::Cancelled(r) => return Outcome::Cancelled(r),
3343 Outcome::Panicked(p) => return Outcome::Panicked(p),
3344 }
3345 }
3346
3347 Outcome::Ok(total_updated)
3348 }
3349}
3350
3351impl<C, M> LazyLoader<M> for Session<C>
3352where
3353 C: Connection,
3354 M: Model + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
3355{
3356 fn get(
3357 &mut self,
3358 cx: &Cx,
3359 pk: Value,
3360 ) -> impl Future<Output = Outcome<Option<M>, Error>> + Send {
3361 Session::get(self, cx, pk)
3362 }
3363}
3364
3365#[derive(Debug, Clone)]
3367pub struct SessionDebugInfo {
3368 pub tracked: usize,
3370 pub pending_new: usize,
3372 pub pending_delete: usize,
3374 pub pending_dirty: usize,
3376 pub in_transaction: bool,
3378}
3379
3380#[cfg(test)]
3385#[allow(clippy::manual_async_fn)] mod tests {
3387 use super::*;
3388 use asupersync::runtime::RuntimeBuilder;
3389 use sqlmodel_core::Row;
3390 use std::sync::{Arc, Mutex};
3391
3392 #[test]
3393 fn test_session_config_defaults() {
3394 let config = SessionConfig::default();
3395 assert!(config.auto_begin);
3396 assert!(!config.auto_flush);
3397 assert!(config.expire_on_commit);
3398 }
3399
3400 #[test]
3401 fn test_object_key_hash_consistency() {
3402 let values1 = vec![Value::BigInt(42)];
3403 let values2 = vec![Value::BigInt(42)];
3404 let hash1 = hash_values(&values1);
3405 let hash2 = hash_values(&values2);
3406 assert_eq!(hash1, hash2);
3407 }
3408
3409 #[test]
3410 fn test_object_key_hash_different_values() {
3411 let values1 = vec![Value::BigInt(42)];
3412 let values2 = vec![Value::BigInt(43)];
3413 let hash1 = hash_values(&values1);
3414 let hash2 = hash_values(&values2);
3415 assert_ne!(hash1, hash2);
3416 }
3417
3418 #[test]
3419 fn test_object_key_hash_different_types() {
3420 let values1 = vec![Value::BigInt(42)];
3421 let values2 = vec![Value::Text("42".to_string())];
3422 let hash1 = hash_values(&values1);
3423 let hash2 = hash_values(&values2);
3424 assert_ne!(hash1, hash2);
3425 }
3426
3427 #[test]
3428 fn test_session_debug_info() {
3429 let info = SessionDebugInfo {
3430 tracked: 5,
3431 pending_new: 2,
3432 pending_delete: 1,
3433 pending_dirty: 0,
3434 in_transaction: true,
3435 };
3436 assert_eq!(info.tracked, 5);
3437 assert_eq!(info.pending_new, 2);
3438 assert!(info.in_transaction);
3439 }
3440
3441 fn unwrap_outcome<T: std::fmt::Debug>(outcome: Outcome<T, Error>) -> T {
3442 match outcome {
3443 Outcome::Ok(v) => v,
3444 other => std::panic::panic_any(format!("unexpected outcome: {other:?}")),
3445 }
3446 }
3447
3448 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3449 struct Team {
3450 id: Option<i64>,
3451 name: String,
3452 }
3453
3454 impl Model for Team {
3455 const TABLE_NAME: &'static str = "teams";
3456 const PRIMARY_KEY: &'static [&'static str] = &["id"];
3457
3458 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
3459 &[]
3460 }
3461
3462 fn to_row(&self) -> Vec<(&'static str, Value)> {
3463 vec![
3464 ("id", self.id.map_or(Value::Null, Value::BigInt)),
3465 ("name", Value::Text(self.name.clone())),
3466 ]
3467 }
3468
3469 fn from_row(row: &Row) -> sqlmodel_core::Result<Self> {
3470 let id: i64 = row.get_named("id")?;
3471 let name: String = row.get_named("name")?;
3472 Ok(Self { id: Some(id), name })
3473 }
3474
3475 fn primary_key_value(&self) -> Vec<Value> {
3476 self.id
3477 .map_or_else(|| vec![Value::Null], |id| vec![Value::BigInt(id)])
3478 }
3479
3480 fn is_new(&self) -> bool {
3481 self.id.is_none()
3482 }
3483 }
3484
3485 #[derive(Debug, Clone, Serialize, Deserialize)]
3486 struct Hero {
3487 id: Option<i64>,
3488 team: Lazy<Team>,
3489 }
3490
3491 impl Model for Hero {
3492 const TABLE_NAME: &'static str = "heroes";
3493 const PRIMARY_KEY: &'static [&'static str] = &["id"];
3494
3495 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
3496 &[]
3497 }
3498
3499 fn to_row(&self) -> Vec<(&'static str, Value)> {
3500 vec![]
3501 }
3502
3503 fn from_row(_row: &Row) -> sqlmodel_core::Result<Self> {
3504 Ok(Self {
3505 id: None,
3506 team: Lazy::empty(),
3507 })
3508 }
3509
3510 fn primary_key_value(&self) -> Vec<Value> {
3511 self.id
3512 .map_or_else(|| vec![Value::Null], |id| vec![Value::BigInt(id)])
3513 }
3514
3515 fn is_new(&self) -> bool {
3516 self.id.is_none()
3517 }
3518 }
3519
3520 #[derive(Debug, Default)]
3521 struct MockState {
3522 query_calls: usize,
3523 last_sql: Option<String>,
3524 execute_calls: usize,
3525 executed: Vec<(String, Vec<Value>)>,
3526 }
3527
3528 #[derive(Debug, Clone)]
3529 struct MockConnection {
3530 state: Arc<Mutex<MockState>>,
3531 dialect: sqlmodel_core::Dialect,
3532 }
3533
3534 impl MockConnection {
3535 fn new(state: Arc<Mutex<MockState>>) -> Self {
3536 Self {
3537 state,
3538 dialect: sqlmodel_core::Dialect::Postgres,
3539 }
3540 }
3541 }
3542
3543 impl sqlmodel_core::Connection for MockConnection {
3544 type Tx<'conn>
3545 = MockTransaction
3546 where
3547 Self: 'conn;
3548
3549 fn dialect(&self) -> sqlmodel_core::Dialect {
3550 self.dialect
3551 }
3552
3553 fn query(
3554 &self,
3555 _cx: &Cx,
3556 sql: &str,
3557 params: &[Value],
3558 ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
3559 let params = params.to_vec();
3560 let state = Arc::clone(&self.state);
3561 let sql = sql.to_string();
3562 async move {
3563 {
3564 let mut guard = state.lock().expect("lock poisoned");
3565 guard.query_calls += 1;
3566 guard.last_sql = Some(sql.clone());
3567 }
3568
3569 let mut rows = Vec::new();
3570 let is_teams = sql.contains("teams");
3571 let is_heroes = sql.contains("heroes");
3572
3573 for v in params {
3574 if is_teams {
3575 match v {
3576 Value::BigInt(1) => rows.push(Row::new(
3577 vec!["id".into(), "name".into()],
3578 vec![Value::BigInt(1), Value::Text("Avengers".into())],
3579 )),
3580 Value::BigInt(2) => rows.push(Row::new(
3581 vec!["id".into(), "name".into()],
3582 vec![Value::BigInt(2), Value::Text("X-Men".into())],
3583 )),
3584 _ => {}
3585 }
3586 } else if is_heroes {
3587 match v {
3589 Value::BigInt(1) => {
3590 rows.push(Row::new(
3591 vec!["id".into(), "team_id".into(), "__parent_pk".into()],
3592 vec![Value::BigInt(101), Value::BigInt(1), Value::BigInt(1)],
3593 ));
3594 rows.push(Row::new(
3595 vec!["id".into(), "team_id".into(), "__parent_pk".into()],
3596 vec![Value::BigInt(102), Value::BigInt(1), Value::BigInt(1)],
3597 ));
3598 }
3599 Value::BigInt(2) => rows.push(Row::new(
3600 vec!["id".into(), "team_id".into(), "__parent_pk".into()],
3601 vec![Value::BigInt(201), Value::BigInt(2), Value::BigInt(2)],
3602 )),
3603 _ => {}
3604 }
3605 }
3606 }
3607
3608 Outcome::Ok(rows)
3609 }
3610 }
3611
3612 fn query_one(
3613 &self,
3614 _cx: &Cx,
3615 _sql: &str,
3616 _params: &[Value],
3617 ) -> impl Future<Output = Outcome<Option<Row>, Error>> + Send {
3618 async { Outcome::Ok(None) }
3619 }
3620
3621 fn execute(
3622 &self,
3623 _cx: &Cx,
3624 sql: &str,
3625 params: &[Value],
3626 ) -> impl Future<Output = Outcome<u64, Error>> + Send {
3627 let state = Arc::clone(&self.state);
3628 let sql = sql.to_string();
3629 let params = params.to_vec();
3630 async move {
3631 let mut guard = state.lock().expect("lock poisoned");
3632 guard.execute_calls += 1;
3633 guard.executed.push((sql, params));
3634 Outcome::Ok(0)
3635 }
3636 }
3637
3638 fn insert(
3639 &self,
3640 _cx: &Cx,
3641 _sql: &str,
3642 _params: &[Value],
3643 ) -> impl Future<Output = Outcome<i64, Error>> + Send {
3644 async { Outcome::Ok(0) }
3645 }
3646
3647 fn batch(
3648 &self,
3649 _cx: &Cx,
3650 _statements: &[(String, Vec<Value>)],
3651 ) -> impl Future<Output = Outcome<Vec<u64>, Error>> + Send {
3652 async { Outcome::Ok(vec![]) }
3653 }
3654
3655 fn begin(&self, _cx: &Cx) -> impl Future<Output = Outcome<Self::Tx<'_>, Error>> + Send {
3656 async { Outcome::Ok(MockTransaction) }
3657 }
3658
3659 fn begin_with(
3660 &self,
3661 _cx: &Cx,
3662 _isolation: sqlmodel_core::connection::IsolationLevel,
3663 ) -> impl Future<Output = Outcome<Self::Tx<'_>, Error>> + Send {
3664 async { Outcome::Ok(MockTransaction) }
3665 }
3666
3667 fn prepare(
3668 &self,
3669 _cx: &Cx,
3670 _sql: &str,
3671 ) -> impl Future<Output = Outcome<sqlmodel_core::connection::PreparedStatement, Error>> + Send
3672 {
3673 async {
3674 Outcome::Ok(sqlmodel_core::connection::PreparedStatement::new(
3675 0,
3676 String::new(),
3677 0,
3678 ))
3679 }
3680 }
3681
3682 fn query_prepared(
3683 &self,
3684 _cx: &Cx,
3685 _stmt: &sqlmodel_core::connection::PreparedStatement,
3686 _params: &[Value],
3687 ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
3688 async { Outcome::Ok(vec![]) }
3689 }
3690
3691 fn execute_prepared(
3692 &self,
3693 _cx: &Cx,
3694 _stmt: &sqlmodel_core::connection::PreparedStatement,
3695 _params: &[Value],
3696 ) -> impl Future<Output = Outcome<u64, Error>> + Send {
3697 async { Outcome::Ok(0) }
3698 }
3699
3700 fn ping(&self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
3701 async { Outcome::Ok(()) }
3702 }
3703
3704 fn close(self, _cx: &Cx) -> impl Future<Output = sqlmodel_core::Result<()>> + Send {
3705 async { Ok(()) }
3706 }
3707 }
3708
3709 struct MockTransaction;
3710
3711 impl sqlmodel_core::connection::TransactionOps for MockTransaction {
3712 fn query(
3713 &self,
3714 _cx: &Cx,
3715 _sql: &str,
3716 _params: &[Value],
3717 ) -> impl Future<Output = Outcome<Vec<Row>, Error>> + Send {
3718 async { Outcome::Ok(vec![]) }
3719 }
3720
3721 fn query_one(
3722 &self,
3723 _cx: &Cx,
3724 _sql: &str,
3725 _params: &[Value],
3726 ) -> impl Future<Output = Outcome<Option<Row>, Error>> + Send {
3727 async { Outcome::Ok(None) }
3728 }
3729
3730 fn execute(
3731 &self,
3732 _cx: &Cx,
3733 _sql: &str,
3734 _params: &[Value],
3735 ) -> impl Future<Output = Outcome<u64, Error>> + Send {
3736 async { Outcome::Ok(0) }
3737 }
3738
3739 fn savepoint(
3740 &self,
3741 _cx: &Cx,
3742 _name: &str,
3743 ) -> impl Future<Output = Outcome<(), Error>> + Send {
3744 async { Outcome::Ok(()) }
3745 }
3746
3747 fn rollback_to(
3748 &self,
3749 _cx: &Cx,
3750 _name: &str,
3751 ) -> impl Future<Output = Outcome<(), Error>> + Send {
3752 async { Outcome::Ok(()) }
3753 }
3754
3755 fn release(
3756 &self,
3757 _cx: &Cx,
3758 _name: &str,
3759 ) -> impl Future<Output = Outcome<(), Error>> + Send {
3760 async { Outcome::Ok(()) }
3761 }
3762
3763 fn commit(self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
3764 async { Outcome::Ok(()) }
3765 }
3766
3767 fn rollback(self, _cx: &Cx) -> impl Future<Output = Outcome<(), Error>> + Send {
3768 async { Outcome::Ok(()) }
3769 }
3770 }
3771
3772 #[test]
3773 fn test_load_many_single_query_and_populates_lazy() {
3774 let rt = RuntimeBuilder::current_thread()
3775 .build()
3776 .expect("create asupersync runtime");
3777 let cx = Cx::for_testing();
3778
3779 let state = Arc::new(Mutex::new(MockState::default()));
3780 let conn = MockConnection::new(Arc::clone(&state));
3781 let mut session = Session::new(conn);
3782
3783 let heroes = vec![
3784 Hero {
3785 id: Some(1),
3786 team: Lazy::from_fk(1_i64),
3787 },
3788 Hero {
3789 id: Some(2),
3790 team: Lazy::from_fk(2_i64),
3791 },
3792 Hero {
3793 id: Some(3),
3794 team: Lazy::from_fk(1_i64),
3795 },
3796 Hero {
3797 id: Some(4),
3798 team: Lazy::empty(),
3799 },
3800 Hero {
3801 id: Some(5),
3802 team: Lazy::from_fk(999_i64),
3803 },
3804 ];
3805
3806 rt.block_on(async {
3807 let loaded = unwrap_outcome(
3808 session
3809 .load_many::<Hero, Team, _>(&cx, &heroes, |h| &h.team)
3810 .await,
3811 );
3812 assert_eq!(loaded, 3);
3813
3814 assert!(heroes[0].team.is_loaded());
3816 assert_eq!(heroes[0].team.get().unwrap().name, "Avengers");
3817 assert_eq!(heroes[1].team.get().unwrap().name, "X-Men");
3818 assert_eq!(heroes[2].team.get().unwrap().name, "Avengers");
3819
3820 assert!(heroes[3].team.is_loaded());
3822 assert!(heroes[3].team.get().is_none());
3823
3824 assert!(heroes[4].team.is_loaded());
3826 assert!(heroes[4].team.get().is_none());
3827
3828 let team1 = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await);
3830 assert_eq!(
3831 team1,
3832 Some(Team {
3833 id: Some(1),
3834 name: "Avengers".to_string()
3835 })
3836 );
3837 });
3838
3839 assert_eq!(state.lock().expect("lock poisoned").query_calls, 1);
3840 }
3841
3842 #[derive(Debug, Clone, Serialize, Deserialize)]
3843 struct HeroChild {
3844 id: Option<i64>,
3845 team_id: i64,
3846 }
3847
3848 impl Model for HeroChild {
3849 const TABLE_NAME: &'static str = "heroes";
3850 const PRIMARY_KEY: &'static [&'static str] = &["id"];
3851
3852 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
3853 &[]
3854 }
3855
3856 fn to_row(&self) -> Vec<(&'static str, Value)> {
3857 vec![
3858 ("id", self.id.map_or(Value::Null, Value::BigInt)),
3859 ("team_id", Value::BigInt(self.team_id)),
3860 ]
3861 }
3862
3863 fn from_row(row: &Row) -> sqlmodel_core::Result<Self> {
3864 let id: i64 = row.get_named("id")?;
3865 let team_id: i64 = row.get_named("team_id")?;
3866 Ok(Self {
3867 id: Some(id),
3868 team_id,
3869 })
3870 }
3871
3872 fn primary_key_value(&self) -> Vec<Value> {
3873 self.id
3874 .map_or_else(|| vec![Value::Null], |id| vec![Value::BigInt(id)])
3875 }
3876
3877 fn is_new(&self) -> bool {
3878 self.id.is_none()
3879 }
3880 }
3881
3882 #[derive(Debug, Clone, Serialize, Deserialize)]
3883 struct TeamWithHeroes {
3884 id: Option<i64>,
3885 heroes: sqlmodel_core::RelatedMany<HeroChild>,
3886 }
3887
3888 impl Model for TeamWithHeroes {
3889 const TABLE_NAME: &'static str = "teams";
3890 const PRIMARY_KEY: &'static [&'static str] = &["id"];
3891 const RELATIONSHIPS: &'static [sqlmodel_core::RelationshipInfo] =
3892 &[sqlmodel_core::RelationshipInfo::new(
3893 "heroes",
3894 "heroes",
3895 sqlmodel_core::RelationshipKind::OneToMany,
3896 )
3897 .remote_key("team_id")
3898 .cascade_delete(true)];
3899
3900 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
3901 &[]
3902 }
3903
3904 fn to_row(&self) -> Vec<(&'static str, Value)> {
3905 vec![("id", self.id.map_or(Value::Null, Value::BigInt))]
3906 }
3907
3908 fn from_row(row: &Row) -> sqlmodel_core::Result<Self> {
3909 let id: i64 = row.get_named("id")?;
3910 Ok(Self {
3911 id: Some(id),
3912 heroes: sqlmodel_core::RelatedMany::new("team_id"),
3913 })
3914 }
3915
3916 fn primary_key_value(&self) -> Vec<Value> {
3917 self.id
3918 .map_or_else(|| vec![Value::Null], |id| vec![Value::BigInt(id)])
3919 }
3920
3921 fn is_new(&self) -> bool {
3922 self.id.is_none()
3923 }
3924 }
3925
3926 #[derive(Debug, Clone, Serialize, Deserialize)]
3927 struct TeamWithHeroesPassive {
3928 id: Option<i64>,
3929 heroes: sqlmodel_core::RelatedMany<HeroChild>,
3930 }
3931
3932 impl Model for TeamWithHeroesPassive {
3933 const TABLE_NAME: &'static str = "teams_passive";
3934 const PRIMARY_KEY: &'static [&'static str] = &["id"];
3935 const RELATIONSHIPS: &'static [sqlmodel_core::RelationshipInfo] =
3936 &[sqlmodel_core::RelationshipInfo::new(
3937 "heroes",
3938 "heroes",
3939 sqlmodel_core::RelationshipKind::OneToMany,
3940 )
3941 .remote_key("team_id")
3942 .cascade_delete(true)
3943 .passive_deletes(sqlmodel_core::PassiveDeletes::Passive)];
3944
3945 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
3946 &[]
3947 }
3948
3949 fn to_row(&self) -> Vec<(&'static str, Value)> {
3950 vec![("id", self.id.map_or(Value::Null, Value::BigInt))]
3951 }
3952
3953 fn from_row(row: &Row) -> sqlmodel_core::Result<Self> {
3954 let id: i64 = row.get_named("id")?;
3955 Ok(Self {
3956 id: Some(id),
3957 heroes: sqlmodel_core::RelatedMany::new("team_id"),
3958 })
3959 }
3960
3961 fn primary_key_value(&self) -> Vec<Value> {
3962 self.id
3963 .map_or_else(|| vec![Value::Null], |id| vec![Value::BigInt(id)])
3964 }
3965
3966 fn is_new(&self) -> bool {
3967 self.id.is_none()
3968 }
3969 }
3970
3971 #[derive(Debug, Clone, Serialize, Deserialize)]
3972 struct HeroCompositeChild {
3973 id: Option<i64>,
3974 team_id1: i64,
3975 team_id2: i64,
3976 }
3977
3978 impl Model for HeroCompositeChild {
3979 const TABLE_NAME: &'static str = "heroes_composite";
3980 const PRIMARY_KEY: &'static [&'static str] = &["id"];
3981
3982 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
3983 &[]
3984 }
3985
3986 fn to_row(&self) -> Vec<(&'static str, Value)> {
3987 vec![
3988 ("id", self.id.map_or(Value::Null, Value::BigInt)),
3989 ("team_id1", Value::BigInt(self.team_id1)),
3990 ("team_id2", Value::BigInt(self.team_id2)),
3991 ]
3992 }
3993
3994 fn from_row(row: &Row) -> sqlmodel_core::Result<Self> {
3995 let id: i64 = row.get_named("id")?;
3996 let team_id1: i64 = row.get_named("team_id1")?;
3997 let team_id2: i64 = row.get_named("team_id2")?;
3998 Ok(Self {
3999 id: Some(id),
4000 team_id1,
4001 team_id2,
4002 })
4003 }
4004
4005 fn primary_key_value(&self) -> Vec<Value> {
4006 self.id
4007 .map_or_else(|| vec![Value::Null], |id| vec![Value::BigInt(id)])
4008 }
4009
4010 fn is_new(&self) -> bool {
4011 self.id.is_none()
4012 }
4013 }
4014
4015 #[derive(Debug, Clone, Serialize, Deserialize)]
4016 struct TeamComposite {
4017 id1: Option<i64>,
4018 id2: Option<i64>,
4019 }
4020
4021 impl Model for TeamComposite {
4022 const TABLE_NAME: &'static str = "teams_composite";
4023 const PRIMARY_KEY: &'static [&'static str] = &["id1", "id2"];
4024 const RELATIONSHIPS: &'static [sqlmodel_core::RelationshipInfo] =
4025 &[sqlmodel_core::RelationshipInfo::new(
4026 "heroes",
4027 "heroes_composite",
4028 sqlmodel_core::RelationshipKind::OneToMany,
4029 )
4030 .remote_keys(&["team_id1", "team_id2"])
4031 .cascade_delete(true)];
4032
4033 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
4034 &[]
4035 }
4036
4037 fn to_row(&self) -> Vec<(&'static str, Value)> {
4038 vec![
4039 ("id1", self.id1.map_or(Value::Null, Value::BigInt)),
4040 ("id2", self.id2.map_or(Value::Null, Value::BigInt)),
4041 ]
4042 }
4043
4044 fn from_row(row: &Row) -> sqlmodel_core::Result<Self> {
4045 let id1: i64 = row.get_named("id1")?;
4046 let id2: i64 = row.get_named("id2")?;
4047 Ok(Self {
4048 id1: Some(id1),
4049 id2: Some(id2),
4050 })
4051 }
4052
4053 fn primary_key_value(&self) -> Vec<Value> {
4054 match (self.id1, self.id2) {
4055 (Some(a), Some(b)) => vec![Value::BigInt(a), Value::BigInt(b)],
4056 _ => vec![Value::Null, Value::Null],
4057 }
4058 }
4059
4060 fn is_new(&self) -> bool {
4061 self.id1.is_none() || self.id2.is_none()
4062 }
4063 }
4064
4065 #[derive(Debug, Clone, Serialize, Deserialize)]
4066 struct TeamCompositePassive {
4067 id1: Option<i64>,
4068 id2: Option<i64>,
4069 }
4070
4071 impl Model for TeamCompositePassive {
4072 const TABLE_NAME: &'static str = "teams_composite_passive";
4073 const PRIMARY_KEY: &'static [&'static str] = &["id1", "id2"];
4074 const RELATIONSHIPS: &'static [sqlmodel_core::RelationshipInfo] =
4075 &[sqlmodel_core::RelationshipInfo::new(
4076 "heroes",
4077 "heroes_composite",
4078 sqlmodel_core::RelationshipKind::OneToMany,
4079 )
4080 .remote_keys(&["team_id1", "team_id2"])
4081 .cascade_delete(true)
4082 .passive_deletes(sqlmodel_core::PassiveDeletes::Passive)];
4083
4084 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
4085 &[]
4086 }
4087
4088 fn to_row(&self) -> Vec<(&'static str, Value)> {
4089 vec![
4090 ("id1", self.id1.map_or(Value::Null, Value::BigInt)),
4091 ("id2", self.id2.map_or(Value::Null, Value::BigInt)),
4092 ]
4093 }
4094
4095 fn from_row(row: &Row) -> sqlmodel_core::Result<Self> {
4096 let id1: i64 = row.get_named("id1")?;
4097 let id2: i64 = row.get_named("id2")?;
4098 Ok(Self {
4099 id1: Some(id1),
4100 id2: Some(id2),
4101 })
4102 }
4103
4104 fn primary_key_value(&self) -> Vec<Value> {
4105 match (self.id1, self.id2) {
4106 (Some(a), Some(b)) => vec![Value::BigInt(a), Value::BigInt(b)],
4107 _ => vec![Value::Null, Value::Null],
4108 }
4109 }
4110
4111 fn is_new(&self) -> bool {
4112 self.id1.is_none() || self.id2.is_none()
4113 }
4114 }
4115
4116 #[test]
4117 fn test_load_one_to_many_single_query_and_populates_related_many() {
4118 let rt = RuntimeBuilder::current_thread()
4119 .build()
4120 .expect("create asupersync runtime");
4121 let cx = Cx::for_testing();
4122
4123 let state = Arc::new(Mutex::new(MockState::default()));
4124 let conn = MockConnection::new(Arc::clone(&state));
4125 let mut session = Session::new(conn);
4126
4127 let mut teams = vec![
4128 TeamWithHeroes {
4129 id: Some(1),
4130 heroes: sqlmodel_core::RelatedMany::new("team_id"),
4131 },
4132 TeamWithHeroes {
4133 id: Some(2),
4134 heroes: sqlmodel_core::RelatedMany::new("team_id"),
4135 },
4136 TeamWithHeroes {
4137 id: None,
4138 heroes: sqlmodel_core::RelatedMany::new("team_id"),
4139 },
4140 ];
4141
4142 rt.block_on(async {
4143 let loaded = unwrap_outcome(
4144 session
4145 .load_one_to_many::<TeamWithHeroes, HeroChild, _, _>(
4146 &cx,
4147 &mut teams,
4148 |t| &mut t.heroes,
4149 |t| t.id.map_or(Value::Null, Value::BigInt),
4150 )
4151 .await,
4152 );
4153 assert_eq!(loaded, 3);
4154
4155 assert!(teams[0].heroes.is_loaded());
4156 assert_eq!(teams[0].heroes.len(), 2);
4157 assert_eq!(teams[0].heroes.parent_pk(), Some(&Value::BigInt(1)));
4158
4159 assert!(teams[1].heroes.is_loaded());
4160 assert_eq!(teams[1].heroes.len(), 1);
4161 assert_eq!(teams[1].heroes.parent_pk(), Some(&Value::BigInt(2)));
4162
4163 assert!(teams[2].heroes.is_loaded());
4165 assert_eq!(teams[2].heroes.len(), 0);
4166 assert_eq!(teams[2].heroes.parent_pk(), Some(&Value::Null));
4167 });
4168
4169 assert_eq!(state.lock().expect("lock poisoned").query_calls, 1);
4170 let sql = state
4171 .lock()
4172 .expect("lock poisoned")
4173 .last_sql
4174 .clone()
4175 .expect("sql captured");
4176 assert!(sql.contains("FROM"), "expected SQL to contain FROM");
4177 assert!(
4178 sql.contains("heroes"),
4179 "expected SQL to target heroes table"
4180 );
4181 assert!(
4182 sql.contains("$1"),
4183 "expected Postgres-style placeholders ($1, $2, ...)"
4184 );
4185 assert!(
4186 sql.contains("$2"),
4187 "expected Postgres-style placeholders ($1, $2, ...)"
4188 );
4189 }
4190
4191 #[test]
4192 fn test_flush_cascade_delete_one_to_many_deletes_children_first() {
4193 let rt = RuntimeBuilder::current_thread()
4194 .build()
4195 .expect("create asupersync runtime");
4196 let cx = Cx::for_testing();
4197
4198 let state = Arc::new(Mutex::new(MockState::default()));
4199 let conn = MockConnection::new(Arc::clone(&state));
4200 let mut session = Session::with_config(
4201 conn,
4202 SessionConfig {
4203 auto_begin: false,
4204 auto_flush: false,
4205 expire_on_commit: true,
4206 },
4207 );
4208
4209 rt.block_on(async {
4210 let team = unwrap_outcome(session.get::<TeamWithHeroes>(&cx, 1_i64).await).unwrap();
4212
4213 let mut teams = vec![team.clone()];
4215 let loaded = unwrap_outcome(
4216 session
4217 .load_one_to_many::<TeamWithHeroes, HeroChild, _, _>(
4218 &cx,
4219 &mut teams,
4220 |t| &mut t.heroes,
4221 |t| t.id.map_or(Value::Null, Value::BigInt),
4222 )
4223 .await,
4224 );
4225 assert_eq!(loaded, 2);
4226
4227 session.delete(&team);
4229 unwrap_outcome(session.flush(&cx).await);
4230
4231 assert_eq!(session.tracked_count(), 0);
4233 });
4234
4235 let guard = state.lock().expect("lock poisoned");
4236 assert!(
4237 guard.execute_calls >= 2,
4238 "expected at least cascade + parent delete"
4239 );
4240 let (sql0, _params0) = &guard.executed[0];
4241 let (sql1, _params1) = &guard.executed[1];
4242 assert!(
4243 sql0.contains("DELETE") && sql0.contains("heroes"),
4244 "expected first delete to target child table"
4245 );
4246 assert!(
4247 sql1.contains("DELETE") && sql1.contains("teams"),
4248 "expected second delete to target parent table"
4249 );
4250 }
4251
4252 #[test]
4253 fn test_flush_passive_deletes_does_not_emit_child_delete_but_detaches_children() {
4254 let rt = RuntimeBuilder::current_thread()
4255 .build()
4256 .expect("create asupersync runtime");
4257 let cx = Cx::for_testing();
4258
4259 let state = Arc::new(Mutex::new(MockState::default()));
4260 let conn = MockConnection::new(Arc::clone(&state));
4261 let mut session = Session::with_config(
4262 conn,
4263 SessionConfig {
4264 auto_begin: false,
4265 auto_flush: false,
4266 expire_on_commit: true,
4267 },
4268 );
4269
4270 rt.block_on(async {
4271 let team =
4272 unwrap_outcome(session.get::<TeamWithHeroesPassive>(&cx, 1_i64).await).unwrap();
4273
4274 let mut teams = vec![team.clone()];
4276 let loaded = unwrap_outcome(
4277 session
4278 .load_one_to_many::<TeamWithHeroesPassive, HeroChild, _, _>(
4279 &cx,
4280 &mut teams,
4281 |t| &mut t.heroes,
4282 |t| t.id.map_or(Value::Null, Value::BigInt),
4283 )
4284 .await,
4285 );
4286 assert_eq!(loaded, 2);
4287
4288 session.delete(&team);
4289 unwrap_outcome(session.flush(&cx).await);
4290
4291 assert_eq!(session.tracked_count(), 0);
4292 });
4293
4294 let guard = state.lock().expect("lock poisoned");
4295 assert_eq!(guard.execute_calls, 1, "expected only the parent delete");
4296 let (sql0, _params0) = &guard.executed[0];
4297 assert!(
4298 sql0.contains("teams_passive"),
4299 "expected delete to target parent table"
4300 );
4301 assert!(
4302 !sql0.contains("heroes"),
4303 "did not expect a child-table delete for passive_deletes"
4304 );
4305 }
4306
4307 #[test]
4308 fn test_flush_cascade_delete_composite_keys_deletes_children_first() {
4309 let rt = RuntimeBuilder::current_thread()
4310 .build()
4311 .expect("create asupersync runtime");
4312 let cx = Cx::for_testing();
4313
4314 let state = Arc::new(Mutex::new(MockState::default()));
4315 let conn = MockConnection::new(Arc::clone(&state));
4316 let mut session = Session::with_config(
4317 conn,
4318 SessionConfig {
4319 auto_begin: false,
4320 auto_flush: false,
4321 expire_on_commit: true,
4322 },
4323 );
4324
4325 let team = TeamComposite {
4326 id1: Some(1),
4327 id2: Some(2),
4328 };
4329 let team_key = ObjectKey::from_model(&team);
4330
4331 session.identity_map.insert(
4333 team_key,
4334 TrackedObject {
4335 object: Box::new(team.clone()),
4336 original_state: None,
4337 state: ObjectState::Persistent,
4338 table_name: TeamComposite::TABLE_NAME,
4339 column_names: vec!["id1", "id2"],
4340 values: vec![Value::BigInt(1), Value::BigInt(2)],
4341 pk_columns: vec!["id1", "id2"],
4342 pk_values: vec![Value::BigInt(1), Value::BigInt(2)],
4343 relationships: TeamComposite::RELATIONSHIPS,
4344 expired_attributes: None,
4345 },
4346 );
4347
4348 let child1 = HeroCompositeChild {
4350 id: Some(10),
4351 team_id1: 1,
4352 team_id2: 2,
4353 };
4354 let child2 = HeroCompositeChild {
4355 id: Some(11),
4356 team_id1: 1,
4357 team_id2: 2,
4358 };
4359 for child in [child1, child2] {
4360 let child_id = child.id.expect("child id");
4361 let key = ObjectKey::from_model(&child);
4362 session.identity_map.insert(
4363 key,
4364 TrackedObject {
4365 object: Box::new(child),
4366 original_state: None,
4367 state: ObjectState::Persistent,
4368 table_name: HeroCompositeChild::TABLE_NAME,
4369 column_names: vec!["id", "team_id1", "team_id2"],
4370 values: vec![Value::BigInt(child_id), Value::BigInt(1), Value::BigInt(2)],
4371 pk_columns: vec!["id"],
4372 pk_values: vec![Value::BigInt(child_id)],
4373 relationships: HeroCompositeChild::RELATIONSHIPS,
4374 expired_attributes: None,
4375 },
4376 );
4377 }
4378
4379 rt.block_on(async {
4380 session.delete(&team);
4381 unwrap_outcome(session.flush(&cx).await);
4382 assert_eq!(session.tracked_count(), 0);
4383 });
4384
4385 let guard = state.lock().expect("lock poisoned");
4386 assert!(
4387 guard.execute_calls >= 2,
4388 "expected at least composite cascade + parent delete"
4389 );
4390 let (sql0, _params0) = &guard.executed[0];
4391 assert!(sql0.contains("DELETE"), "expected DELETE SQL");
4392 assert!(
4393 sql0.contains("heroes_composite"),
4394 "expected composite cascade to target child table"
4395 );
4396 assert!(sql0.contains("team_id1"), "expected fk col team_id1");
4397 assert!(sql0.contains("team_id2"), "expected fk col team_id2");
4398 assert!(
4399 sql0.contains("$1") && sql0.contains("$2"),
4400 "expected Postgres-style placeholders for composite tuple"
4401 );
4402 }
4403
4404 #[test]
4405 fn test_flush_passive_deletes_composite_keys_detaches_children_no_child_delete_sql() {
4406 let rt = RuntimeBuilder::current_thread()
4407 .build()
4408 .expect("create asupersync runtime");
4409 let cx = Cx::for_testing();
4410
4411 let state = Arc::new(Mutex::new(MockState::default()));
4412 let conn = MockConnection::new(Arc::clone(&state));
4413 let mut session = Session::with_config(
4414 conn,
4415 SessionConfig {
4416 auto_begin: false,
4417 auto_flush: false,
4418 expire_on_commit: true,
4419 },
4420 );
4421
4422 let team = TeamCompositePassive {
4423 id1: Some(1),
4424 id2: Some(2),
4425 };
4426 let team_key = ObjectKey::from_model(&team);
4427
4428 session.identity_map.insert(
4429 team_key,
4430 TrackedObject {
4431 object: Box::new(team.clone()),
4432 original_state: None,
4433 state: ObjectState::Persistent,
4434 table_name: TeamCompositePassive::TABLE_NAME,
4435 column_names: vec!["id1", "id2"],
4436 values: vec![Value::BigInt(1), Value::BigInt(2)],
4437 pk_columns: vec!["id1", "id2"],
4438 pk_values: vec![Value::BigInt(1), Value::BigInt(2)],
4439 relationships: TeamCompositePassive::RELATIONSHIPS,
4440 expired_attributes: None,
4441 },
4442 );
4443
4444 let child = HeroCompositeChild {
4445 id: Some(10),
4446 team_id1: 1,
4447 team_id2: 2,
4448 };
4449 session.identity_map.insert(
4450 ObjectKey::from_model(&child),
4451 TrackedObject {
4452 object: Box::new(child),
4453 original_state: None,
4454 state: ObjectState::Persistent,
4455 table_name: HeroCompositeChild::TABLE_NAME,
4456 column_names: vec!["id", "team_id1", "team_id2"],
4457 values: vec![Value::BigInt(10), Value::BigInt(1), Value::BigInt(2)],
4458 pk_columns: vec!["id"],
4459 pk_values: vec![Value::BigInt(10)],
4460 relationships: HeroCompositeChild::RELATIONSHIPS,
4461 expired_attributes: None,
4462 },
4463 );
4464
4465 rt.block_on(async {
4466 session.delete(&team);
4467 unwrap_outcome(session.flush(&cx).await);
4468 assert_eq!(session.tracked_count(), 0);
4469 });
4470
4471 let guard = state.lock().expect("lock poisoned");
4472 assert_eq!(guard.execute_calls, 1, "expected only the parent delete");
4473 let (sql0, _params0) = &guard.executed[0];
4474 assert!(
4475 sql0.contains("teams_composite_passive"),
4476 "expected delete to target composite parent table"
4477 );
4478 assert!(
4479 !sql0.contains("heroes_composite"),
4480 "did not expect a child-table delete for passive_deletes"
4481 );
4482 }
4483
4484 #[derive(Debug, Clone, Serialize, Deserialize)]
4485 struct MmChildComposite {
4486 id1: i64,
4487 id2: i64,
4488 }
4489
4490 impl Model for MmChildComposite {
4491 const TABLE_NAME: &'static str = "mm_children";
4492 const PRIMARY_KEY: &'static [&'static str] = &["id1", "id2"];
4493
4494 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
4495 &[]
4496 }
4497
4498 fn to_row(&self) -> Vec<(&'static str, Value)> {
4499 vec![
4500 ("id1", Value::BigInt(self.id1)),
4501 ("id2", Value::BigInt(self.id2)),
4502 ]
4503 }
4504
4505 fn from_row(_row: &Row) -> sqlmodel_core::Result<Self> {
4506 Ok(Self { id1: 0, id2: 0 })
4507 }
4508
4509 fn primary_key_value(&self) -> Vec<Value> {
4510 vec![Value::BigInt(self.id1), Value::BigInt(self.id2)]
4511 }
4512
4513 fn is_new(&self) -> bool {
4514 false
4515 }
4516 }
4517
4518 #[derive(Debug, Clone, Serialize, Deserialize)]
4519 struct MmParentComposite {
4520 id1: i64,
4521 id2: i64,
4522 children: sqlmodel_core::RelatedMany<MmChildComposite>,
4523 }
4524
4525 impl Model for MmParentComposite {
4526 const TABLE_NAME: &'static str = "mm_parents";
4527 const PRIMARY_KEY: &'static [&'static str] = &["id1", "id2"];
4528 const RELATIONSHIPS: &'static [sqlmodel_core::RelationshipInfo] =
4529 &[sqlmodel_core::RelationshipInfo::new(
4530 "children",
4531 MmChildComposite::TABLE_NAME,
4532 sqlmodel_core::RelationshipKind::ManyToMany,
4533 )
4534 .link_table(sqlmodel_core::LinkTableInfo::composite(
4535 "mm_link",
4536 &["parent_id1", "parent_id2"],
4537 &["child_id1", "child_id2"],
4538 ))
4539 .cascade_delete(true)];
4540
4541 fn fields() -> &'static [sqlmodel_core::FieldInfo] {
4542 &[]
4543 }
4544
4545 fn to_row(&self) -> Vec<(&'static str, Value)> {
4546 vec![
4547 ("id1", Value::BigInt(self.id1)),
4548 ("id2", Value::BigInt(self.id2)),
4549 ]
4550 }
4551
4552 fn from_row(_row: &Row) -> sqlmodel_core::Result<Self> {
4553 Ok(Self {
4554 id1: 0,
4555 id2: 0,
4556 children: sqlmodel_core::RelatedMany::with_link_table(
4557 sqlmodel_core::LinkTableInfo::composite(
4558 "mm_link",
4559 &["parent_id1", "parent_id2"],
4560 &["child_id1", "child_id2"],
4561 ),
4562 ),
4563 })
4564 }
4565
4566 fn primary_key_value(&self) -> Vec<Value> {
4567 vec![Value::BigInt(self.id1), Value::BigInt(self.id2)]
4568 }
4569
4570 fn is_new(&self) -> bool {
4571 false
4572 }
4573 }
4574
4575 #[test]
4576 fn test_flush_cascade_delete_many_to_many_composite_parent_keys_deletes_link_rows_first() {
4577 let rt = RuntimeBuilder::current_thread()
4578 .build()
4579 .expect("create asupersync runtime");
4580 let cx = Cx::for_testing();
4581
4582 let state = Arc::new(Mutex::new(MockState::default()));
4583 let conn = MockConnection::new(Arc::clone(&state));
4584 let mut session = Session::with_config(
4585 conn,
4586 SessionConfig {
4587 auto_begin: false,
4588 auto_flush: false,
4589 expire_on_commit: true,
4590 },
4591 );
4592
4593 let parent = MmParentComposite {
4594 id1: 1,
4595 id2: 2,
4596 children: sqlmodel_core::RelatedMany::with_link_table(
4597 sqlmodel_core::LinkTableInfo::composite(
4598 "mm_link",
4599 &["parent_id1", "parent_id2"],
4600 &["child_id1", "child_id2"],
4601 ),
4602 ),
4603 };
4604 let key = ObjectKey::from_model(&parent);
4605
4606 session.identity_map.insert(
4607 key,
4608 TrackedObject {
4609 object: Box::new(parent.clone()),
4610 original_state: None,
4611 state: ObjectState::Persistent,
4612 table_name: MmParentComposite::TABLE_NAME,
4613 column_names: vec!["id1", "id2"],
4614 values: vec![Value::BigInt(1), Value::BigInt(2)],
4615 pk_columns: vec!["id1", "id2"],
4616 pk_values: vec![Value::BigInt(1), Value::BigInt(2)],
4617 relationships: MmParentComposite::RELATIONSHIPS,
4618 expired_attributes: None,
4619 },
4620 );
4621
4622 rt.block_on(async {
4623 session.delete(&parent);
4624 unwrap_outcome(session.flush(&cx).await);
4625 assert_eq!(session.tracked_count(), 0);
4626 });
4627
4628 let guard = state.lock().expect("lock poisoned");
4629 assert!(
4630 guard.execute_calls >= 2,
4631 "expected at least link-table cascade + parent delete"
4632 );
4633 let (sql0, _params0) = &guard.executed[0];
4634 let (sql1, _params1) = &guard.executed[1];
4635 assert!(
4636 sql0.contains("DELETE") && sql0.contains("mm_link"),
4637 "expected first delete to target link table"
4638 );
4639 assert!(
4640 sql0.contains("parent_id1") && sql0.contains("parent_id2"),
4641 "expected composite local cols in link delete"
4642 );
4643 assert!(
4644 sql1.contains("DELETE") && sql1.contains("mm_parents"),
4645 "expected second delete to target parent table"
4646 );
4647 }
4648
4649 #[test]
4650 fn test_flush_related_many_composite_link_and_unlink() {
4651 let rt = RuntimeBuilder::current_thread()
4652 .build()
4653 .expect("create asupersync runtime");
4654 let cx = Cx::for_testing();
4655
4656 let state = Arc::new(Mutex::new(MockState::default()));
4657 let conn = MockConnection::new(Arc::clone(&state));
4658 let mut session = Session::with_config(
4659 conn,
4660 SessionConfig {
4661 auto_begin: false,
4662 auto_flush: false,
4663 expire_on_commit: true,
4664 },
4665 );
4666
4667 let link = sqlmodel_core::LinkTableInfo::composite(
4668 "mm_link",
4669 &["parent_id1", "parent_id2"],
4670 &["child_id1", "child_id2"],
4671 );
4672
4673 let mut parents = vec![MmParentComposite {
4674 id1: 1,
4675 id2: 2,
4676 children: sqlmodel_core::RelatedMany::with_link_table(link),
4677 }];
4678
4679 let child = MmChildComposite { id1: 7, id2: 9 };
4680
4681 parents[0].children.link(&child);
4682 parents[0].children.unlink(&child);
4683
4684 rt.block_on(async {
4685 let n = unwrap_outcome(
4686 session
4687 .flush_related_many_pk::<MmParentComposite, MmChildComposite, _, _>(
4688 &cx,
4689 &mut parents,
4690 |p| &mut p.children,
4691 |p| vec![Value::BigInt(p.id1), Value::BigInt(p.id2)],
4692 &link,
4693 )
4694 .await,
4695 );
4696 assert_eq!(n, 2);
4697 });
4698
4699 let guard = state.lock().expect("lock poisoned");
4700 assert_eq!(guard.execute_calls, 2);
4701 let (sql0, _params0) = &guard.executed[0];
4702 let (sql1, _params1) = &guard.executed[1];
4703
4704 assert!(sql0.contains("INSERT INTO"));
4705 assert!(sql0.contains("mm_link"));
4706 assert!(sql0.contains("parent_id1"));
4707 assert!(sql0.contains("parent_id2"));
4708 assert!(sql0.contains("child_id1"));
4709 assert!(sql0.contains("child_id2"));
4710 assert!(sql0.contains("$1") && sql0.contains("$4"));
4711
4712 assert!(sql1.contains("DELETE FROM"));
4713 assert!(sql1.contains("mm_link"));
4714 assert!(sql1.contains("parent_id1"));
4715 assert!(sql1.contains("child_id2"));
4716 assert!(sql1.contains("$1") && sql1.contains("$4"));
4717 }
4718
4719 #[test]
4720 fn test_load_many_to_many_pk_composite_builds_tuple_where_clause() {
4721 let rt = RuntimeBuilder::current_thread()
4722 .build()
4723 .expect("create asupersync runtime");
4724 let cx = Cx::for_testing();
4725
4726 let state = Arc::new(Mutex::new(MockState::default()));
4727 let conn = MockConnection::new(Arc::clone(&state));
4728 let mut session = Session::new(conn);
4729
4730 let link = sqlmodel_core::LinkTableInfo::composite(
4731 "mm_link",
4732 &["parent_id1", "parent_id2"],
4733 &["child_id1", "child_id2"],
4734 );
4735
4736 let mut parents = vec![MmParentComposite {
4737 id1: 1,
4738 id2: 2,
4739 children: sqlmodel_core::RelatedMany::with_link_table(link),
4740 }];
4741
4742 rt.block_on(async {
4743 let loaded = unwrap_outcome(
4744 session
4745 .load_many_to_many_pk::<MmParentComposite, MmChildComposite, _, _>(
4746 &cx,
4747 &mut parents,
4748 |p| &mut p.children,
4749 |p| vec![Value::BigInt(p.id1), Value::BigInt(p.id2)],
4750 &link,
4751 )
4752 .await,
4753 );
4754 assert_eq!(loaded, 0);
4755 });
4756
4757 let guard = state.lock().expect("lock poisoned");
4758 assert_eq!(guard.query_calls, 1);
4759 let sql = guard.last_sql.clone().expect("sql captured");
4760 assert!(sql.contains("JOIN"));
4761 assert!(sql.contains("mm_link"));
4762 assert!(sql.contains("WHERE"));
4763 assert!(sql.contains("parent_id1") && sql.contains("parent_id2"));
4764 assert!(sql.contains("IN (("), "expected tuple IN clause");
4765 }
4766
4767 #[test]
4768 fn test_add_all_with_vec() {
4769 let state = Arc::new(Mutex::new(MockState::default()));
4770 let conn = MockConnection::new(Arc::clone(&state));
4771 let mut session = Session::new(conn);
4772
4773 let teams = vec![
4776 Team {
4777 id: Some(100),
4778 name: "Team A".to_string(),
4779 },
4780 Team {
4781 id: Some(101),
4782 name: "Team B".to_string(),
4783 },
4784 Team {
4785 id: Some(102),
4786 name: "Team C".to_string(),
4787 },
4788 ];
4789
4790 session.add_all(&teams);
4791
4792 let info = session.debug_state();
4793 assert_eq!(info.pending_new, 3);
4794 assert_eq!(info.tracked, 3);
4795 }
4796
4797 #[test]
4798 fn test_add_all_with_empty_collection() {
4799 let state = Arc::new(Mutex::new(MockState::default()));
4800 let conn = MockConnection::new(Arc::clone(&state));
4801 let mut session = Session::new(conn);
4802
4803 let teams: Vec<Team> = vec![];
4804 session.add_all(&teams);
4805
4806 let info = session.debug_state();
4807 assert_eq!(info.pending_new, 0);
4808 assert_eq!(info.tracked, 0);
4809 }
4810
4811 #[test]
4812 fn test_add_all_with_iterator() {
4813 let state = Arc::new(Mutex::new(MockState::default()));
4814 let conn = MockConnection::new(Arc::clone(&state));
4815 let mut session = Session::new(conn);
4816
4817 let teams = [
4818 Team {
4819 id: Some(200),
4820 name: "Team X".to_string(),
4821 },
4822 Team {
4823 id: Some(201),
4824 name: "Team Y".to_string(),
4825 },
4826 ];
4827
4828 session.add_all(teams.iter());
4830
4831 let info = session.debug_state();
4832 assert_eq!(info.pending_new, 2);
4833 assert_eq!(info.tracked, 2);
4834 }
4835
4836 #[test]
4837 fn test_add_all_with_slice() {
4838 let state = Arc::new(Mutex::new(MockState::default()));
4839 let conn = MockConnection::new(Arc::clone(&state));
4840 let mut session = Session::new(conn);
4841
4842 let teams = [
4843 Team {
4844 id: Some(300),
4845 name: "Team 1".to_string(),
4846 },
4847 Team {
4848 id: Some(301),
4849 name: "Team 2".to_string(),
4850 },
4851 ];
4852
4853 session.add_all(&teams);
4854
4855 let info = session.debug_state();
4856 assert_eq!(info.pending_new, 2);
4857 assert_eq!(info.tracked, 2);
4858 }
4859
4860 #[test]
4863 fn test_merge_new_object_without_load() {
4864 let rt = RuntimeBuilder::current_thread()
4865 .build()
4866 .expect("create asupersync runtime");
4867 let cx = Cx::for_testing();
4868
4869 let state = Arc::new(Mutex::new(MockState::default()));
4870 let conn = MockConnection::new(Arc::clone(&state));
4871 let mut session = Session::new(conn);
4872
4873 rt.block_on(async {
4874 let team = Team {
4876 id: Some(100),
4877 name: "New Team".to_string(),
4878 };
4879
4880 let merged = unwrap_outcome(session.merge(&cx, team.clone(), false).await);
4881
4882 assert_eq!(merged.id, Some(100));
4884 assert_eq!(merged.name, "New Team");
4885
4886 let info = session.debug_state();
4888 assert_eq!(info.pending_new, 1);
4889 assert_eq!(info.tracked, 1);
4890 });
4891
4892 assert_eq!(state.lock().expect("lock poisoned").query_calls, 0);
4894 }
4895
4896 #[test]
4897 fn test_merge_updates_existing_tracked_object() {
4898 let rt = RuntimeBuilder::current_thread()
4899 .build()
4900 .expect("create asupersync runtime");
4901 let cx = Cx::for_testing();
4902
4903 let state = Arc::new(Mutex::new(MockState::default()));
4904 let conn = MockConnection::new(Arc::clone(&state));
4905 let mut session = Session::new(conn);
4906
4907 rt.block_on(async {
4908 let original = Team {
4910 id: Some(1),
4911 name: "Original".to_string(),
4912 };
4913 session.add(&original);
4914
4915 let updated = Team {
4917 id: Some(1),
4918 name: "Updated".to_string(),
4919 };
4920
4921 let merged = unwrap_outcome(session.merge(&cx, updated, false).await);
4922
4923 assert_eq!(merged.id, Some(1));
4925 assert_eq!(merged.name, "Updated");
4926
4927 let info = session.debug_state();
4929 assert_eq!(info.tracked, 1);
4930 });
4931 }
4932
4933 #[test]
4934 fn test_merge_with_load_queries_database() {
4935 let rt = RuntimeBuilder::current_thread()
4936 .build()
4937 .expect("create asupersync runtime");
4938 let cx = Cx::for_testing();
4939
4940 let state = Arc::new(Mutex::new(MockState::default()));
4941 let conn = MockConnection::new(Arc::clone(&state));
4942 let mut session = Session::new(conn);
4943
4944 rt.block_on(async {
4945 let detached = Team {
4947 id: Some(1),
4948 name: "Detached Update".to_string(),
4949 };
4950
4951 let merged = unwrap_outcome(session.merge(&cx, detached, true).await);
4952
4953 assert_eq!(merged.id, Some(1));
4955 assert_eq!(merged.name, "Detached Update");
4956
4957 let info = session.debug_state();
4959 assert_eq!(info.tracked, 1);
4960 assert_eq!(info.pending_dirty, 1);
4961 });
4962
4963 assert_eq!(state.lock().expect("lock poisoned").query_calls, 1);
4965 }
4966
4967 #[test]
4968 fn test_merge_with_load_not_found_creates_new() {
4969 let rt = RuntimeBuilder::current_thread()
4970 .build()
4971 .expect("create asupersync runtime");
4972 let cx = Cx::for_testing();
4973
4974 let state = Arc::new(Mutex::new(MockState::default()));
4975 let conn = MockConnection::new(Arc::clone(&state));
4976 let mut session = Session::new(conn);
4977
4978 rt.block_on(async {
4979 let detached = Team {
4981 id: Some(999),
4982 name: "Not In DB".to_string(),
4983 };
4984
4985 let merged = unwrap_outcome(session.merge(&cx, detached, true).await);
4986
4987 assert_eq!(merged.id, Some(999));
4989 assert_eq!(merged.name, "Not In DB");
4990
4991 let info = session.debug_state();
4993 assert_eq!(info.pending_new, 1);
4994 assert_eq!(info.tracked, 1);
4995 });
4996
4997 assert_eq!(state.lock().expect("lock poisoned").query_calls, 1);
4999 }
5000
5001 #[test]
5002 fn test_merge_without_load_convenience() {
5003 let rt = RuntimeBuilder::current_thread()
5004 .build()
5005 .expect("create asupersync runtime");
5006 let cx = Cx::for_testing();
5007
5008 let state = Arc::new(Mutex::new(MockState::default()));
5009 let conn = MockConnection::new(Arc::clone(&state));
5010 let mut session = Session::new(conn);
5011
5012 rt.block_on(async {
5013 let team = Team {
5014 id: Some(42),
5015 name: "Test".to_string(),
5016 };
5017
5018 let merged = unwrap_outcome(session.merge_without_load(&cx, team).await);
5020
5021 assert_eq!(merged.id, Some(42));
5022 assert_eq!(merged.name, "Test");
5023
5024 let info = session.debug_state();
5025 assert_eq!(info.pending_new, 1);
5026 });
5027
5028 assert_eq!(state.lock().expect("lock poisoned").query_calls, 0);
5030 }
5031
5032 #[test]
5033 fn test_merge_null_pk_treated_as_new() {
5034 let rt = RuntimeBuilder::current_thread()
5035 .build()
5036 .expect("create asupersync runtime");
5037 let cx = Cx::for_testing();
5038
5039 let state = Arc::new(Mutex::new(MockState::default()));
5040 let conn = MockConnection::new(Arc::clone(&state));
5041 let mut session = Session::new(conn);
5042
5043 rt.block_on(async {
5044 let new_team = Team {
5046 id: None,
5047 name: "Brand New".to_string(),
5048 };
5049
5050 let merged = unwrap_outcome(session.merge(&cx, new_team, true).await);
5051
5052 assert_eq!(merged.id, None);
5054 assert_eq!(merged.name, "Brand New");
5055
5056 let info = session.debug_state();
5058 assert_eq!(info.pending_new, 1);
5059 });
5060
5061 assert_eq!(state.lock().expect("lock poisoned").query_calls, 0);
5063 }
5064
5065 #[test]
5068 fn test_is_modified_new_object_returns_true() {
5069 let state = Arc::new(Mutex::new(MockState::default()));
5070 let conn = MockConnection::new(Arc::clone(&state));
5071 let mut session = Session::new(conn);
5072
5073 let team = Team {
5074 id: Some(100),
5075 name: "New Team".to_string(),
5076 };
5077
5078 session.add(&team);
5080 assert!(session.is_modified(&team));
5081 }
5082
5083 #[test]
5084 fn test_is_modified_untracked_returns_false() {
5085 let state = Arc::new(Mutex::new(MockState::default()));
5086 let conn = MockConnection::new(Arc::clone(&state));
5087 let session = Session::<MockConnection>::new(conn);
5088
5089 let team = Team {
5090 id: Some(100),
5091 name: "Not Tracked".to_string(),
5092 };
5093
5094 assert!(!session.is_modified(&team));
5096 }
5097
5098 #[test]
5099 fn test_is_modified_after_load_returns_false() {
5100 let rt = RuntimeBuilder::current_thread()
5101 .build()
5102 .expect("create asupersync runtime");
5103 let cx = Cx::for_testing();
5104
5105 let state = Arc::new(Mutex::new(MockState::default()));
5106 let conn = MockConnection::new(Arc::clone(&state));
5107 let mut session = Session::new(conn);
5108
5109 rt.block_on(async {
5110 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5112
5113 assert!(!session.is_modified(&team));
5115 });
5116 }
5117
5118 #[test]
5119 fn test_is_modified_after_mark_dirty_returns_true() {
5120 let rt = RuntimeBuilder::current_thread()
5121 .build()
5122 .expect("create asupersync runtime");
5123 let cx = Cx::for_testing();
5124
5125 let state = Arc::new(Mutex::new(MockState::default()));
5126 let conn = MockConnection::new(Arc::clone(&state));
5127 let mut session = Session::new(conn);
5128
5129 rt.block_on(async {
5130 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5132 assert!(!session.is_modified(&team));
5133
5134 let mut modified_team = team.clone();
5136 modified_team.name = "Modified Name".to_string();
5137 session.mark_dirty(&modified_team);
5138
5139 assert!(session.is_modified(&modified_team));
5141 });
5142 }
5143
5144 #[test]
5145 fn test_is_modified_deleted_returns_true() {
5146 let rt = RuntimeBuilder::current_thread()
5147 .build()
5148 .expect("create asupersync runtime");
5149 let cx = Cx::for_testing();
5150
5151 let state = Arc::new(Mutex::new(MockState::default()));
5152 let conn = MockConnection::new(Arc::clone(&state));
5153 let mut session = Session::new(conn);
5154
5155 rt.block_on(async {
5156 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5158 assert!(!session.is_modified(&team));
5159
5160 session.delete(&team);
5162
5163 assert!(session.is_modified(&team));
5165 });
5166 }
5167
5168 #[test]
5169 fn test_is_modified_detached_returns_false() {
5170 let rt = RuntimeBuilder::current_thread()
5171 .build()
5172 .expect("create asupersync runtime");
5173 let cx = Cx::for_testing();
5174
5175 let state = Arc::new(Mutex::new(MockState::default()));
5176 let conn = MockConnection::new(Arc::clone(&state));
5177 let mut session = Session::new(conn);
5178
5179 rt.block_on(async {
5180 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5182
5183 session.expunge(&team);
5185
5186 assert!(!session.is_modified(&team));
5188 });
5189 }
5190
5191 #[test]
5192 fn test_object_state_returns_correct_state() {
5193 let rt = RuntimeBuilder::current_thread()
5194 .build()
5195 .expect("create asupersync runtime");
5196 let cx = Cx::for_testing();
5197
5198 let state = Arc::new(Mutex::new(MockState::default()));
5199 let conn = MockConnection::new(Arc::clone(&state));
5200 let mut session = Session::new(conn);
5201
5202 let untracked = Team {
5204 id: Some(999),
5205 name: "Untracked".to_string(),
5206 };
5207 assert_eq!(session.object_state(&untracked), None);
5208
5209 let new_team = Team {
5211 id: Some(100),
5212 name: "New".to_string(),
5213 };
5214 session.add(&new_team);
5215 assert_eq!(session.object_state(&new_team), Some(ObjectState::New));
5216
5217 rt.block_on(async {
5218 let persistent = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5220 assert_eq!(
5221 session.object_state(&persistent),
5222 Some(ObjectState::Persistent)
5223 );
5224
5225 session.delete(&persistent);
5227 assert_eq!(
5228 session.object_state(&persistent),
5229 Some(ObjectState::Deleted)
5230 );
5231 });
5232 }
5233
5234 #[test]
5235 fn test_modified_attributes_returns_changed_columns() {
5236 let rt = RuntimeBuilder::current_thread()
5237 .build()
5238 .expect("create asupersync runtime");
5239 let cx = Cx::for_testing();
5240
5241 let state = Arc::new(Mutex::new(MockState::default()));
5242 let conn = MockConnection::new(Arc::clone(&state));
5243 let mut session = Session::new(conn);
5244
5245 rt.block_on(async {
5246 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5248
5249 let modified = session.modified_attributes(&team);
5251 assert!(modified.is_empty());
5252
5253 let mut modified_team = team.clone();
5255 modified_team.name = "Changed Name".to_string();
5256 session.mark_dirty(&modified_team);
5257
5258 let modified = session.modified_attributes(&modified_team);
5260 assert!(modified.contains(&"name"));
5261 });
5262 }
5263
5264 #[test]
5265 fn test_modified_attributes_untracked_returns_empty() {
5266 let state = Arc::new(Mutex::new(MockState::default()));
5267 let conn = MockConnection::new(Arc::clone(&state));
5268 let session = Session::<MockConnection>::new(conn);
5269
5270 let team = Team {
5271 id: Some(100),
5272 name: "Not Tracked".to_string(),
5273 };
5274
5275 let modified = session.modified_attributes(&team);
5276 assert!(modified.is_empty());
5277 }
5278
5279 #[test]
5280 fn test_modified_attributes_new_returns_empty() {
5281 let state = Arc::new(Mutex::new(MockState::default()));
5282 let conn = MockConnection::new(Arc::clone(&state));
5283 let mut session = Session::new(conn);
5284
5285 let team = Team {
5286 id: Some(100),
5287 name: "New".to_string(),
5288 };
5289 session.add(&team);
5290
5291 let modified = session.modified_attributes(&team);
5293 assert!(modified.is_empty());
5294 }
5295
5296 #[test]
5299 fn test_expire_marks_object_as_expired() {
5300 let rt = RuntimeBuilder::current_thread()
5301 .build()
5302 .expect("create asupersync runtime");
5303 let cx = Cx::for_testing();
5304
5305 let state = Arc::new(Mutex::new(MockState::default()));
5306 let conn = MockConnection::new(Arc::clone(&state));
5307 let mut session = Session::new(conn);
5308
5309 rt.block_on(async {
5310 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await);
5312 assert!(team.is_some());
5313 let team = team.unwrap();
5314
5315 assert!(!session.is_expired(&team));
5317 assert_eq!(session.object_state(&team), Some(ObjectState::Persistent));
5318
5319 session.expire(&team, None);
5321
5322 assert!(session.is_expired(&team));
5324 assert_eq!(session.object_state(&team), Some(ObjectState::Expired));
5325 });
5326 }
5327
5328 #[test]
5329 fn test_expire_specific_attributes() {
5330 let rt = RuntimeBuilder::current_thread()
5331 .build()
5332 .expect("create asupersync runtime");
5333 let cx = Cx::for_testing();
5334
5335 let state = Arc::new(Mutex::new(MockState::default()));
5336 let conn = MockConnection::new(Arc::clone(&state));
5337 let mut session = Session::new(conn);
5338
5339 rt.block_on(async {
5340 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5342
5343 session.expire(&team, Some(&["name"]));
5345
5346 assert!(session.is_expired(&team));
5348
5349 let expired = session.expired_attributes(&team);
5351 assert!(expired.is_some());
5352 let expired_set = expired.unwrap();
5353 assert!(expired_set.is_some());
5354 assert!(expired_set.unwrap().contains("name"));
5355 });
5356 }
5357
5358 #[test]
5359 fn test_expire_all_marks_all_objects_expired() {
5360 let rt = RuntimeBuilder::current_thread()
5361 .build()
5362 .expect("create asupersync runtime");
5363 let cx = Cx::for_testing();
5364
5365 let state = Arc::new(Mutex::new(MockState::default()));
5366 let conn = MockConnection::new(Arc::clone(&state));
5367 let mut session = Session::new(conn);
5368
5369 rt.block_on(async {
5370 let team1 = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5372 let team2 = unwrap_outcome(session.get::<Team>(&cx, 2_i64).await).unwrap();
5373
5374 assert!(!session.is_expired(&team1));
5376 assert!(!session.is_expired(&team2));
5377
5378 session.expire_all();
5380
5381 assert!(session.is_expired(&team1));
5383 assert!(session.is_expired(&team2));
5384 });
5385 }
5386
5387 #[test]
5388 fn test_expire_does_not_affect_new_objects() {
5389 let state = Arc::new(Mutex::new(MockState::default()));
5390 let conn = MockConnection::new(Arc::clone(&state));
5391 let mut session = Session::new(conn);
5392
5393 let team = Team {
5395 id: Some(100),
5396 name: "New Team".to_string(),
5397 };
5398 session.add(&team);
5399
5400 session.expire(&team, None);
5402
5403 assert_eq!(session.object_state(&team), Some(ObjectState::New));
5405 assert!(!session.is_expired(&team));
5406 }
5407
5408 #[test]
5409 fn test_expired_object_reloads_on_get() {
5410 let rt = RuntimeBuilder::current_thread()
5411 .build()
5412 .expect("create asupersync runtime");
5413 let cx = Cx::for_testing();
5414
5415 let state = Arc::new(Mutex::new(MockState::default()));
5416 let conn = MockConnection::new(Arc::clone(&state));
5417 let mut session = Session::new(conn);
5418
5419 rt.block_on(async {
5420 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5422 assert_eq!(team.name, "Avengers");
5423
5424 let team2 = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5426 assert_eq!(team2.name, "Avengers");
5427
5428 {
5430 let s = state.lock().expect("lock poisoned");
5431 assert_eq!(s.query_calls, 1);
5432 }
5433
5434 session.expire(&team, None);
5436
5437 let team3 = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5439 assert_eq!(team3.name, "Avengers");
5440
5441 {
5443 let s = state.lock().expect("lock poisoned");
5444 assert_eq!(s.query_calls, 2);
5445 }
5446
5447 assert!(!session.is_expired(&team3));
5449 assert_eq!(session.object_state(&team3), Some(ObjectState::Persistent));
5450 });
5451 }
5452
5453 #[test]
5454 fn test_is_expired_returns_false_for_untracked() {
5455 let state = Arc::new(Mutex::new(MockState::default()));
5456 let conn = MockConnection::new(Arc::clone(&state));
5457 let session = Session::<MockConnection>::new(conn);
5458
5459 let team = Team {
5460 id: Some(999),
5461 name: "Not Tracked".to_string(),
5462 };
5463
5464 assert!(!session.is_expired(&team));
5466 }
5467
5468 #[test]
5469 fn test_expired_attributes_returns_none_for_persistent() {
5470 let rt = RuntimeBuilder::current_thread()
5471 .build()
5472 .expect("create asupersync runtime");
5473 let cx = Cx::for_testing();
5474
5475 let state = Arc::new(Mutex::new(MockState::default()));
5476 let conn = MockConnection::new(Arc::clone(&state));
5477 let mut session = Session::new(conn);
5478
5479 rt.block_on(async {
5480 let team = unwrap_outcome(session.get::<Team>(&cx, 1_i64).await).unwrap();
5482
5483 let expired = session.expired_attributes(&team);
5485 assert!(expired.is_none());
5486 });
5487 }
5488}