1use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use serde::{de::DeserializeOwned, Serialize};
9use std::collections::HashMap;
10use std::fmt::Debug;
11
12#[derive(Debug, thiserror::Error)]
18pub enum RepositoryError {
19 #[error("Entity not found")]
20 NotFound,
21
22 #[error("Entity already exists: {0}")]
23 AlreadyExists(String),
24
25 #[error("Validation error: {0}")]
26 ValidationError(String),
27
28 #[error("Database error: {0}")]
29 DatabaseError(String),
30
31 #[error("Serialization error: {0}")]
32 SerializationError(String),
33
34 #[error("Conflict: {0}")]
35 Conflict(String),
36
37 #[error("Internal error: {0}")]
38 InternalError(String),
39
40 #[error("validation failed: {}", .0.iter().map(|v| v.message.as_str()).collect::<Vec<_>>().join("; "))]
44 Violations(Vec<crate::violation::Violation>),
45}
46
47impl RepositoryError {
48 pub fn from_source(e: impl Into<anyhow::Error>) -> Self {
53 let e: anyhow::Error = e.into();
54 #[cfg(feature = "postgres")]
55 let e = match e.downcast::<sqlx::Error>() {
56 Ok(db) => return RepositoryError::from(db),
57 Err(e) => e,
58 };
59 RepositoryError::DatabaseError(e.to_string())
60 }
61
62 pub fn from_batch_source(e: impl Into<anyhow::Error>) -> Self {
66 let e: anyhow::Error = e.into();
67 match e.downcast::<backbone_orm::BatchItemError>() {
68 Ok(item) => Self::from_source(item.error).at_batch_index(item.index),
69 Err(e) => Self::from_source(e),
70 }
71 }
72
73 pub fn at_batch_index(self, index: usize) -> Self {
77 let item = |message: String| format!("item {index}: {message}");
78 match self {
79 RepositoryError::Violations(violations) => RepositoryError::Violations(
80 violations
81 .into_iter()
82 .map(|mut v| {
83 v.path = crate::violation::batch_item_path(index, &v.path);
84 v
85 })
86 .collect(),
87 ),
88 RepositoryError::NotFound => RepositoryError::ValidationError(item(
89 "the record no longer exists or was deleted".to_string(),
90 )),
91 RepositoryError::AlreadyExists(m) => RepositoryError::AlreadyExists(item(m)),
92 RepositoryError::ValidationError(m) => RepositoryError::ValidationError(item(m)),
93 RepositoryError::DatabaseError(m) => RepositoryError::DatabaseError(item(m)),
94 RepositoryError::SerializationError(m) => RepositoryError::SerializationError(item(m)),
95 RepositoryError::Conflict(m) => RepositoryError::Conflict(item(m)),
96 RepositoryError::InternalError(m) => RepositoryError::InternalError(item(m)),
97 }
98 }
99}
100
101impl From<serde_json::Error> for RepositoryError {
102 fn from(e: serde_json::Error) -> Self {
103 RepositoryError::SerializationError(e.to_string())
104 }
105}
106
107#[cfg(feature = "postgres")]
108impl From<sqlx::Error> for RepositoryError {
109 fn from(e: sqlx::Error) -> Self {
110 match e {
111 sqlx::Error::RowNotFound => RepositoryError::NotFound,
112 sqlx::Error::Database(db_err) => {
113 let pg = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>();
114 if let Some(v) = constraint_violation(
115 db_err.kind(),
116 db_err.code().as_deref(),
117 db_err.constraint(),
118 pg.and_then(|p| p.detail()).unwrap_or(""),
119 pg.and_then(|p| p.column()),
120 pg.and_then(|p| p.table()),
121 ) {
122 return RepositoryError::Violations(vec![v]);
123 }
124 let msg = db_err.message().to_string();
125 if msg.contains("duplicate key") || msg.contains("unique constraint") {
126 RepositoryError::AlreadyExists(msg)
127 } else {
128 RepositoryError::DatabaseError(msg)
129 }
130 }
131 _ => RepositoryError::DatabaseError(e.to_string()),
132 }
133 }
134}
135
136#[cfg(feature = "postgres")]
150fn constraint_violation(
151 kind: sqlx::error::ErrorKind,
152 sqlstate: Option<&str>,
153 constraint: Option<&str>,
154 detail: &str,
155 column: Option<&str>,
156 table: Option<&str>,
157) -> Option<crate::violation::Violation> {
158 use crate::violation::{code_from_constraint, Violation};
159 use sqlx::error::ErrorKind;
160 if kind == ErrorKind::NotNullViolation || sqlstate == Some("23502") {
161 let column = column.filter(|c| !c.is_empty())?;
163 return Some(Violation::new(
164 column,
165 "required",
166 format!("{column} is required"),
167 ));
168 }
169 let constraint = constraint?;
170 let code = code_from_constraint(constraint);
171 let path = column
172 .filter(|c| !c.is_empty())
173 .map(str::to_string)
174 .or_else(|| key_column(detail))
175 .or_else(|| table.and_then(|t| check_column(constraint, t, &code)))
176 .unwrap_or_default();
177 Some(match kind {
178 ErrorKind::ForeignKeyViolation if detail.contains("still referenced") => Violation::new(
179 "",
180 "still_referenced",
181 "This record is still referenced by another record, so it cannot be removed",
182 ),
183 ErrorKind::ForeignKeyViolation => {
184 let message = match path.as_str() {
185 "" => "The record refers to a record that does not exist".to_string(),
186 p => format!("{p} refers to a record that does not exist"),
187 };
188 Violation::new(path, "reference_missing", message)
189 }
190 ErrorKind::CheckViolation => {
191 let message = check_sentence(&path, &code);
192 Violation::new(path, code, message)
193 }
194 ErrorKind::UniqueViolation if constraint.starts_with("uq_") => {
195 let message = match path.as_str() {
196 "" => "This value is already taken by another record".to_string(),
197 p => format!("{p} is already taken by another record"),
198 };
199 Violation::new(path, code, message)
200 }
201 _ if sqlstate == Some("23P01") => Violation::new(
202 "",
203 code,
204 "The record overlaps another one it may not overlap",
205 ),
206 _ => return None,
207 })
208}
209
210#[cfg(feature = "postgres")]
215fn check_sentence(path: &str, code: &str) -> String {
216 let field = if path.is_empty() {
217 "A value".to_string()
218 } else {
219 path.to_string()
220 };
221 let predicate = match code {
222 "required" => "is required",
223 "max_length" => "is too long",
224 "min_length" => "is too short",
225 "length" => "does not have the required length",
226 "max" => "is too large",
227 "min" => "is too small",
228 "range" => "is out of the allowed range",
229 "positive" => "must be positive",
230 "non_negative" => "must not be negative",
231 "pattern" => "does not have the expected form",
232 "alpha_dash" => "may hold only letters, digits, '_' and '-'",
233 "alpha_num" => "may hold only letters and digits",
234 "values" => "is not one of the allowed values",
235 "email" => "must be an email address",
236 "url" => "must be an absolute URL",
237 "state_transition_not_declared" => "cannot move to that state from its current one",
238 _ if path.is_empty() => return "The record does not satisfy one of its rules".to_string(),
239 _ => "does not satisfy one of its rules",
240 };
241 format!("{field} {predicate}")
242}
243
244#[cfg(feature = "postgres")]
250fn check_column(constraint: &str, table: &str, code: &str) -> Option<String> {
251 let head = constraint.strip_prefix("ck_")?.split_once("__")?.0;
252 let rest = head.strip_prefix(table)?.strip_prefix('_')?;
253 let field = rest.strip_suffix(code)?.strip_suffix('_')?;
254 (!field.is_empty()).then(|| field.to_string())
255}
256
257#[cfg(feature = "postgres")]
259const SCOPE_COLUMNS: &[&str] = &[
260 "company_id",
261 "tenant_id",
262 "org_unit_id",
263 "organization_id",
264 "deleted_at",
265];
266
267#[cfg(feature = "postgres")]
271fn key_column(detail: &str) -> Option<String> {
272 let rest = detail.strip_prefix("Key (")?;
273 let cols = &rest[..rest.find(")=")?];
274 let names: Vec<String> = cols
275 .split(',')
276 .filter_map(|part| {
277 let inner = part.rsplit('(').next().unwrap_or(part);
279 let ident: String = inner
280 .trim()
281 .trim_matches('"')
282 .chars()
283 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
284 .collect();
285 (!ident.is_empty()).then_some(ident)
286 })
287 .collect();
288 names
289 .iter()
290 .rev()
291 .find(|n| !SCOPE_COLUMNS.contains(&n.as_str()))
292 .or_else(|| (names.len() == 1).then(|| &names[0]))
293 .cloned()
294}
295
296#[cfg(all(test, feature = "postgres"))]
297mod constraint_violation_tests {
298 use super::*;
299 use sqlx::error::ErrorKind;
300
301 #[test]
302 fn a_missing_reference_names_the_column_and_never_the_table() {
303 let v = constraint_violation(
304 ErrorKind::ForeignKeyViolation,
305 Some("23503"),
306 Some("fk_subdistricts_district_id"),
307 "Key (district_id)=(1484f475-14be-4abc-985b-7291543d32bc) is not present in table \"districts\".",
308 None,
309 None,
310 )
311 .unwrap();
312 assert_eq!((v.path.as_str(), v.code.as_str()), ("district_id", "reference_missing"));
313 assert!(!v.message.contains("districts\"") && !v.message.contains("fk_"), "{}", v.message);
314 }
315
316 #[test]
317 fn removing_a_referenced_record_is_still_referenced() {
318 let v = constraint_violation(
319 ErrorKind::ForeignKeyViolation,
320 Some("23503"),
321 Some("fk_provinces_country_id"),
322 "Key (id)=(866f1b4e-a9f3-4626-95d1-13c183916b4b) is still referenced from table \"provinces\".",
323 None,
324 None,
325 )
326 .unwrap();
327 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "still_referenced"));
328 }
329
330 #[test]
331 fn a_rule_constraint_answers_its_rules_code() {
332 let check = constraint_violation(ErrorKind::CheckViolation, Some("23514"), Some("ck_countries_name_max_length__max_length"), "", None, Some("countries")).unwrap();
334 assert_eq!((check.path.as_str(), check.code.as_str()), ("name", "max_length"));
335 let unique = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_accounts__account_code_taken"), "Key (company_id, code)=(a, b) already exists.", None, Some("accounts")).unwrap();
336 assert_eq!((unique.path.as_str(), unique.code.as_str()), ("code", "account_code_taken"));
337 let overlap = constraint_violation(ErrorKind::Other, Some("23P01"), Some("ex_tax_rates__overlapping_validity"), "", None, Some("tax_rates")).unwrap();
338 assert_eq!(overlap.code, "overlapping_validity");
339 }
340
341 #[test]
342 fn a_missing_required_value_is_a_required_violation_on_its_column() {
343 let v = constraint_violation(
345 ErrorKind::NotNullViolation,
346 Some("23502"),
347 None,
348 "",
349 Some("employee_id"),
350 Some("payslips"),
351 )
352 .unwrap();
353 assert_eq!(
354 (v.path.as_str(), v.code.as_str()),
355 ("employee_id", "required")
356 );
357 assert_eq!(v.message, "employee_id is required");
358 }
359
360 #[test]
361 fn a_composite_unique_key_names_its_own_column_not_the_tenant() {
362 let v = constraint_violation(
363 ErrorKind::UniqueViolation,
364 Some("23505"),
365 Some("uq_accounts__code_taken"),
366 "Key (company_id, code)=(7c0a…, 1100) already exists.",
367 None,
368 Some("accounts"),
369 )
370 .unwrap();
371 assert_eq!(v.path, "code");
372 assert!(
373 v.message.starts_with("code "),
374 "a sentence with a subject: {}",
375 v.message
376 );
377 let scoped = constraint_violation(
378 ErrorKind::UniqueViolation,
379 Some("23505"),
380 Some("uq_items__sku_taken"),
381 "Key (org_unit_id, lower(sku::text))=(a, b) already exists.",
382 None,
383 Some("items"),
384 )
385 .unwrap();
386 assert_eq!(scoped.path, "sku");
387 let bare = constraint_violation(
388 ErrorKind::UniqueViolation,
389 Some("23505"),
390 Some("uq_x__taken"),
391 "",
392 None,
393 None,
394 )
395 .unwrap();
396 assert_eq!(bare.path, "");
397 assert!(
398 bare.message.starts_with(char::is_uppercase),
399 "{}",
400 bare.message
401 );
402 }
403
404 #[test]
405 fn a_check_reads_as_a_sentence_never_as_the_rule_identifier() {
406 let v = constraint_violation(
407 ErrorKind::CheckViolation,
408 Some("23514"),
409 Some("ck_tickets_title_max_length__max_length"),
410 "",
411 None,
412 Some("tickets"),
413 )
414 .unwrap();
415 assert_eq!(v.code, "max_length", "the code stays machine-readable");
416 assert!(
417 !v.message.contains('`') && !v.message.contains("max_length"),
418 "{}",
419 v.message
420 );
421 assert!(v.message.starts_with("title "), "{}", v.message);
422 let row = constraint_violation(
424 ErrorKind::CheckViolation,
425 Some("23514"),
426 Some("ck_journals_g_jrn_1__unbalanced"),
427 "",
428 None,
429 Some("journals"),
430 )
431 .unwrap();
432 assert_eq!((row.path.as_str(), row.code.as_str()), ("", "unbalanced"));
433 assert!(
434 !row.message.contains("unbalanced") && !row.message.contains('`'),
435 "{}",
436 row.message
437 );
438 let tg = constraint_violation(
440 ErrorKind::CheckViolation,
441 Some("23514"),
442 Some("tg_journals__state_transition_not_declared"),
443 "",
444 Some("status"),
445 Some("journals"),
446 )
447 .unwrap();
448 assert_eq!(
449 (tg.path.as_str(), tg.code.as_str()),
450 ("status", "state_transition_not_declared")
451 );
452 assert!(
453 !tg.message.contains("state_transition_not_declared"),
454 "{}",
455 tg.message
456 );
457 }
458
459 #[test]
460 fn a_database_error_inside_any_error_converts_as_the_database_error_does() {
461 let wrapped = anyhow::Error::new(sqlx::Error::RowNotFound).context("loading the row");
462 assert!(matches!(RepositoryError::from_source(wrapped), RepositoryError::NotFound));
463 assert!(matches!(RepositoryError::from_source(sqlx::Error::RowNotFound), RepositoryError::NotFound));
464 let other = RepositoryError::from_source(anyhow::anyhow!("pool closed"));
465 assert!(matches!(other, RepositoryError::DatabaseError(ref m) if m == "pool closed"), "{other:?}");
466 }
467
468 #[test]
469 fn an_error_that_names_no_rule_keeps_its_old_answer() {
470 assert!(constraint_violation(
471 ErrorKind::UniqueViolation,
472 Some("23505"),
473 Some("users_email_key"),
474 "",
475 None,
476 None
477 )
478 .is_none());
479 assert!(
480 constraint_violation(ErrorKind::Other, Some("42P01"), None, "", None, None).is_none()
481 );
482 assert!(constraint_violation(
484 ErrorKind::NotNullViolation,
485 Some("23502"),
486 None,
487 "",
488 None,
489 None
490 )
491 .is_none());
492 }
493}
494
495pub trait PersistentEntity: Clone + Send + Sync + Debug + Serialize + DeserializeOwned + 'static {
504 fn entity_id(&self) -> String;
506
507 fn set_entity_id(&mut self, id: String);
509
510 fn created_at(&self) -> Option<DateTime<Utc>>;
512
513 fn set_created_at(&mut self, ts: DateTime<Utc>);
515
516 fn updated_at(&self) -> Option<DateTime<Utc>>;
518
519 fn set_updated_at(&mut self, ts: DateTime<Utc>);
521
522 fn deleted_at(&self) -> Option<DateTime<Utc>>;
524
525 fn set_deleted_at(&mut self, ts: Option<DateTime<Utc>>);
527
528 fn is_deleted(&self) -> bool {
530 self.deleted_at().is_some()
531 }
532
533 fn mark_deleted(&mut self) {
535 self.set_deleted_at(Some(Utc::now()));
536 self.set_updated_at(Utc::now());
537 }
538
539 fn restore(&mut self) {
541 self.set_deleted_at(None);
542 self.set_updated_at(Utc::now());
543 }
544
545 fn touch(&mut self) {
547 self.set_updated_at(Utc::now());
548 }
549
550 fn generate_id() -> String {
552 uuid::Uuid::new_v4().to_string()
553 }
554
555 fn write_protected_fields() -> &'static [&'static str] {
566 &[]
567 }
568
569 fn state_fields() -> &'static [&'static str] {
579 &[]
580 }
581}
582
583pub trait PartialUpdatable: PersistentEntity {
585 fn apply_partial_update(&mut self, fields: &HashMap<String, serde_json::Value>) -> Result<(), RepositoryError>;
587}
588
589pub trait Versioned {
591 fn version(&self) -> u64;
592 fn set_version(&mut self, version: u64);
593 fn increment_version(&mut self) {
594 self.set_version(self.version() + 1);
595 }
596}
597
598#[async_trait]
607pub trait CrudRepository<E>: Send + Sync
608where
609 E: PersistentEntity,
610{
611 async fn create(&self, entity: E) -> Result<E, RepositoryError>;
613
614 async fn find_by_id(&self, id: &str) -> Result<Option<E>, RepositoryError>;
616
617 async fn find_by_id_including_deleted(&self, id: &str) -> Result<Option<E>, RepositoryError>;
619
620 async fn update(&self, entity: E) -> Result<E, RepositoryError>;
622
623 async fn update_changed(&self, before: &E, after: E) -> Result<Option<E>, RepositoryError> {
634 let _ = before;
635 match self.update(after).await {
636 Ok(saved) => Ok(Some(saved)),
637 Err(RepositoryError::NotFound) => Ok(None),
638 Err(e) => Err(e),
639 }
640 }
641
642 async fn soft_delete(&self, id: &str) -> Result<bool, RepositoryError>;
644
645 async fn restore(&self, id: &str) -> Result<Option<E>, RepositoryError>;
647
648 async fn hard_delete(&self, id: &str) -> Result<bool, RepositoryError>;
650
651 async fn list(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
653
654 async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
656
657 async fn list_deleted_filtered(
663 &self,
664 page: u32,
665 limit: u32,
666 filters: HashMap<String, String>,
667 ) -> Result<(Vec<E>, u64), RepositoryError> {
668 let _ = filters;
669 self.list_deleted(page, limit).await
670 }
671
672 async fn count(&self) -> Result<u64, RepositoryError>;
674
675 async fn count_deleted(&self) -> Result<u64, RepositoryError>;
677
678 async fn bulk_create(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError>;
680
681 async fn empty_trash(&self) -> Result<u64, RepositoryError>;
683
684 async fn list_filtered(
689 &self,
690 page: u32,
691 limit: u32,
692 filters: HashMap<String, String>,
693 ) -> Result<(Vec<E>, u64), RepositoryError> {
694 let _ = filters; self.list(page, limit).await
696 }
697
698 async fn list_filtered_with_info(
703 &self,
704 page: u32,
705 limit: u32,
706 filters: HashMap<String, String>,
707 ) -> Result<(Vec<E>, backbone_orm::repository::PaginationInfo), RepositoryError> {
708 let (rows, total) = self.list_filtered(page, limit, filters).await?;
709 Ok((
710 rows,
711 backbone_orm::repository::PaginationInfo::new(page, limit, total),
712 ))
713 }
714
715 fn table_name(&self) -> Option<&str> {
727 None
728 }
729
730 async fn aggregate_filtered(
731 &self,
732 spec: &backbone_orm::repository::AggregateSpec,
733 filters: HashMap<String, String>,
734 ) -> Result<backbone_orm::repository::AggregateResult, RepositoryError> {
735 let _ = (spec, filters);
736 Err(RepositoryError::DatabaseError(
737 "aggregate is not supported by this repository".to_string(),
738 ))
739 }
740
741 async fn fetch_related_json(
746 &self,
747 _table: &str,
748 _ids: &[String],
749 ) -> Vec<serde_json::Value> {
750 Vec::new()
751 }
752
753 async fn exists(&self, id: &str) -> Result<bool, RepositoryError> {
755 Ok(self.find_by_id(id).await?.is_some())
756 }
757
758 async fn bulk_soft_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
768 let mut n = 0;
769 for id in ids {
770 if self.soft_delete(id).await? {
771 n += 1;
772 }
773 }
774 Ok(n)
775 }
776
777 async fn bulk_restore(&self, ids: &[String]) -> Result<Vec<E>, RepositoryError> {
779 let mut out = Vec::with_capacity(ids.len());
780 for id in ids {
781 if let Some(e) = self.restore(id).await? {
782 out.push(e);
783 }
784 }
785 Ok(out)
786 }
787
788 async fn bulk_hard_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
790 let mut n = 0;
791 for id in ids {
792 if self.hard_delete(id).await? {
793 n += 1;
794 }
795 }
796 Ok(n)
797 }
798
799 async fn restore_all(&self) -> Result<Vec<E>, RepositoryError> {
801 let mut restored = Vec::new();
802 loop {
805 let (batch, _) = self.list_deleted(1, 500).await?;
806 if batch.is_empty() {
807 break;
808 }
809 for entity in &batch {
810 if let Some(e) = self.restore(&entity.entity_id()).await? {
811 restored.push(e);
812 }
813 }
814 }
815 Ok(restored)
816 }
817
818 async fn bulk_update(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError> {
820 let mut out = Vec::with_capacity(entities.len());
821 for entity in entities {
822 out.push(self.update(entity).await?);
823 }
824 Ok(out)
825 }
826
827 async fn bulk_update_changed(&self, pairs: Vec<(E, E)>) -> Result<Vec<E>, RepositoryError> {
835 let mut out = Vec::with_capacity(pairs.len());
836 for (index, (before, after)) in pairs.into_iter().enumerate() {
837 match self.update_changed(&before, after).await {
838 Ok(Some(saved)) => out.push(saved),
839 Ok(None) => return Err(RepositoryError::NotFound.at_batch_index(index)),
840 Err(e) => return Err(e.at_batch_index(index)),
841 }
842 }
843 Ok(out)
844 }
845}
846
847#[async_trait]
849pub trait SearchableRepository<E>: CrudRepository<E>
850where
851 E: PersistentEntity,
852{
853 async fn search(
855 &self,
856 filters: HashMap<String, String>,
857 page: u32,
858 limit: u32,
859 ) -> Result<(Vec<E>, u64), RepositoryError>;
860
861 async fn find_by_field(&self, field: &str, value: &str) -> Result<Option<E>, RepositoryError>;
863
864 async fn find_all_by_field(
866 &self,
867 field: &str,
868 value: &str,
869 page: u32,
870 limit: u32,
871 ) -> Result<(Vec<E>, u64), RepositoryError>;
872}
873
874#[cfg(feature = "postgres")]
879pub use postgres_traits::*;
880
881#[cfg(feature = "postgres")]
882mod postgres_traits {
883 use super::*;
884 use sqlx::postgres::PgRow;
885 use sqlx::FromRow;
886
887 pub trait PostgresEntity: PersistentEntity + for<'r> FromRow<'r, PgRow> + Unpin {
925 fn table_name() -> &'static str;
927
928 fn id_column() -> &'static str {
930 "id"
931 }
932
933 fn select_columns() -> &'static [&'static str];
935
936 fn insert_columns() -> &'static [&'static str];
938
939 fn update_columns() -> &'static [&'static str] {
941 Self::insert_columns()
942 }
943
944 fn bind_for_insert<'q>(
948 entity: &'q Self,
949 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
950 ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>;
951
952 fn bind_for_update<'q>(
956 entity: &'q Self,
957 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
958 ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
959 Self::bind_for_insert(entity, query)
961 }
962
963 fn select_by_id_query() -> String {
965 format!(
966 "SELECT {} FROM {} WHERE {} = $1 AND deleted_at IS NULL",
967 Self::select_columns().join(", "),
968 Self::table_name(),
969 Self::id_column()
970 )
971 }
972
973 fn select_by_id_including_deleted_query() -> String {
975 format!(
976 "SELECT {} FROM {} WHERE {} = $1",
977 Self::select_columns().join(", "),
978 Self::table_name(),
979 Self::id_column()
980 )
981 }
982
983 fn list_query() -> String {
985 format!(
986 "SELECT {} FROM {} WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT $1 OFFSET $2",
987 Self::select_columns().join(", "),
988 Self::table_name()
989 )
990 }
991
992 fn count_query() -> String {
994 format!(
995 "SELECT COUNT(*) FROM {} WHERE deleted_at IS NULL",
996 Self::table_name()
997 )
998 }
999
1000 fn list_deleted_query() -> String {
1002 format!(
1003 "SELECT {} FROM {} WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC LIMIT $1 OFFSET $2",
1004 Self::select_columns().join(", "),
1005 Self::table_name()
1006 )
1007 }
1008
1009 fn count_deleted_query() -> String {
1011 format!(
1012 "SELECT COUNT(*) FROM {} WHERE deleted_at IS NOT NULL",
1013 Self::table_name()
1014 )
1015 }
1016
1017 fn soft_delete_query() -> String {
1019 format!(
1020 "UPDATE {} SET deleted_at = NOW(), updated_at = NOW() WHERE {} = $1 AND deleted_at IS NULL",
1021 Self::table_name(),
1022 Self::id_column()
1023 )
1024 }
1025
1026 fn restore_query() -> String {
1028 format!(
1029 "UPDATE {} SET deleted_at = NULL, updated_at = NOW() WHERE {} = $1 AND deleted_at IS NOT NULL RETURNING {}",
1030 Self::table_name(),
1031 Self::id_column(),
1032 Self::select_columns().join(", ")
1033 )
1034 }
1035
1036 fn hard_delete_query() -> String {
1038 format!(
1039 "DELETE FROM {} WHERE {} = $1",
1040 Self::table_name(),
1041 Self::id_column()
1042 )
1043 }
1044
1045 fn empty_trash_query() -> String {
1047 format!(
1048 "DELETE FROM {} WHERE deleted_at IS NOT NULL",
1049 Self::table_name()
1050 )
1051 }
1052 }
1053}