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
63impl From<serde_json::Error> for RepositoryError {
64 fn from(e: serde_json::Error) -> Self {
65 RepositoryError::SerializationError(e.to_string())
66 }
67}
68
69#[cfg(feature = "postgres")]
70impl From<sqlx::Error> for RepositoryError {
71 fn from(e: sqlx::Error) -> Self {
72 match e {
73 sqlx::Error::RowNotFound => RepositoryError::NotFound,
74 sqlx::Error::Database(db_err) => {
75 let pg = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>();
76 if let Some(v) = constraint_violation(
77 db_err.kind(),
78 db_err.code().as_deref(),
79 db_err.constraint(),
80 pg.and_then(|p| p.detail()).unwrap_or(""),
81 pg.and_then(|p| p.column()),
82 pg.and_then(|p| p.table()),
83 ) {
84 return RepositoryError::Violations(vec![v]);
85 }
86 let msg = db_err.message().to_string();
87 if msg.contains("duplicate key") || msg.contains("unique constraint") {
88 RepositoryError::AlreadyExists(msg)
89 } else {
90 RepositoryError::DatabaseError(msg)
91 }
92 }
93 _ => RepositoryError::DatabaseError(e.to_string()),
94 }
95 }
96}
97
98#[cfg(feature = "postgres")]
112fn constraint_violation(
113 kind: sqlx::error::ErrorKind,
114 sqlstate: Option<&str>,
115 constraint: Option<&str>,
116 detail: &str,
117 column: Option<&str>,
118 table: Option<&str>,
119) -> Option<crate::violation::Violation> {
120 use crate::violation::{code_from_constraint, Violation};
121 use sqlx::error::ErrorKind;
122 if kind == ErrorKind::NotNullViolation || sqlstate == Some("23502") {
123 let column = column.filter(|c| !c.is_empty())?;
125 return Some(Violation::new(
126 column,
127 "required",
128 format!("{column} is required"),
129 ));
130 }
131 let constraint = constraint?;
132 let code = code_from_constraint(constraint);
133 let path = column
134 .filter(|c| !c.is_empty())
135 .map(str::to_string)
136 .or_else(|| key_column(detail))
137 .or_else(|| table.and_then(|t| check_column(constraint, t, &code)))
138 .unwrap_or_default();
139 Some(match kind {
140 ErrorKind::ForeignKeyViolation if detail.contains("still referenced") => Violation::new(
141 "",
142 "still_referenced",
143 "This record is still referenced by another record, so it cannot be removed",
144 ),
145 ErrorKind::ForeignKeyViolation => {
146 let message = match path.as_str() {
147 "" => "The record refers to a record that does not exist".to_string(),
148 p => format!("{p} refers to a record that does not exist"),
149 };
150 Violation::new(path, "reference_missing", message)
151 }
152 ErrorKind::CheckViolation => {
153 let message = check_sentence(&path, &code);
154 Violation::new(path, code, message)
155 }
156 ErrorKind::UniqueViolation if constraint.starts_with("uq_") => {
157 let message = match path.as_str() {
158 "" => "This value is already taken by another record".to_string(),
159 p => format!("{p} is already taken by another record"),
160 };
161 Violation::new(path, code, message)
162 }
163 _ if sqlstate == Some("23P01") => Violation::new(
164 "",
165 code,
166 "The record overlaps another one it may not overlap",
167 ),
168 _ => return None,
169 })
170}
171
172#[cfg(feature = "postgres")]
177fn check_sentence(path: &str, code: &str) -> String {
178 let field = if path.is_empty() {
179 "A value".to_string()
180 } else {
181 path.to_string()
182 };
183 let predicate = match code {
184 "required" => "is required",
185 "max_length" => "is too long",
186 "min_length" => "is too short",
187 "length" => "does not have the required length",
188 "max" => "is too large",
189 "min" => "is too small",
190 "range" => "is out of the allowed range",
191 "positive" => "must be positive",
192 "non_negative" => "must not be negative",
193 "pattern" => "does not have the expected form",
194 "alpha_dash" => "may hold only letters, digits, '_' and '-'",
195 "alpha_num" => "may hold only letters and digits",
196 "values" => "is not one of the allowed values",
197 "email" => "must be an email address",
198 "url" => "must be an absolute URL",
199 "state_transition_not_declared" => "cannot move to that state from its current one",
200 _ if path.is_empty() => return "The record does not satisfy one of its rules".to_string(),
201 _ => "does not satisfy one of its rules",
202 };
203 format!("{field} {predicate}")
204}
205
206#[cfg(feature = "postgres")]
212fn check_column(constraint: &str, table: &str, code: &str) -> Option<String> {
213 let head = constraint.strip_prefix("ck_")?.split_once("__")?.0;
214 let rest = head.strip_prefix(table)?.strip_prefix('_')?;
215 let field = rest.strip_suffix(code)?.strip_suffix('_')?;
216 (!field.is_empty()).then(|| field.to_string())
217}
218
219#[cfg(feature = "postgres")]
221const SCOPE_COLUMNS: &[&str] = &[
222 "company_id",
223 "tenant_id",
224 "org_unit_id",
225 "organization_id",
226 "deleted_at",
227];
228
229#[cfg(feature = "postgres")]
233fn key_column(detail: &str) -> Option<String> {
234 let rest = detail.strip_prefix("Key (")?;
235 let cols = &rest[..rest.find(")=")?];
236 let names: Vec<String> = cols
237 .split(',')
238 .filter_map(|part| {
239 let inner = part.rsplit('(').next().unwrap_or(part);
241 let ident: String = inner
242 .trim()
243 .trim_matches('"')
244 .chars()
245 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
246 .collect();
247 (!ident.is_empty()).then_some(ident)
248 })
249 .collect();
250 names
251 .iter()
252 .rev()
253 .find(|n| !SCOPE_COLUMNS.contains(&n.as_str()))
254 .or_else(|| (names.len() == 1).then(|| &names[0]))
255 .cloned()
256}
257
258#[cfg(all(test, feature = "postgres"))]
259mod constraint_violation_tests {
260 use super::*;
261 use sqlx::error::ErrorKind;
262
263 #[test]
264 fn a_missing_reference_names_the_column_and_never_the_table() {
265 let v = constraint_violation(
266 ErrorKind::ForeignKeyViolation,
267 Some("23503"),
268 Some("fk_subdistricts_district_id"),
269 "Key (district_id)=(1484f475-14be-4abc-985b-7291543d32bc) is not present in table \"districts\".",
270 None,
271 None,
272 )
273 .unwrap();
274 assert_eq!((v.path.as_str(), v.code.as_str()), ("district_id", "reference_missing"));
275 assert!(!v.message.contains("districts\"") && !v.message.contains("fk_"), "{}", v.message);
276 }
277
278 #[test]
279 fn removing_a_referenced_record_is_still_referenced() {
280 let v = constraint_violation(
281 ErrorKind::ForeignKeyViolation,
282 Some("23503"),
283 Some("fk_provinces_country_id"),
284 "Key (id)=(866f1b4e-a9f3-4626-95d1-13c183916b4b) is still referenced from table \"provinces\".",
285 None,
286 None,
287 )
288 .unwrap();
289 assert_eq!((v.path.as_str(), v.code.as_str()), ("", "still_referenced"));
290 }
291
292 #[test]
293 fn a_rule_constraint_answers_its_rules_code() {
294 let check = constraint_violation(ErrorKind::CheckViolation, Some("23514"), Some("ck_countries_name_max_length__max_length"), "", None, Some("countries")).unwrap();
296 assert_eq!((check.path.as_str(), check.code.as_str()), ("name", "max_length"));
297 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();
298 assert_eq!((unique.path.as_str(), unique.code.as_str()), ("code", "account_code_taken"));
299 let overlap = constraint_violation(ErrorKind::Other, Some("23P01"), Some("ex_tax_rates__overlapping_validity"), "", None, Some("tax_rates")).unwrap();
300 assert_eq!(overlap.code, "overlapping_validity");
301 }
302
303 #[test]
304 fn a_missing_required_value_is_a_required_violation_on_its_column() {
305 let v = constraint_violation(
307 ErrorKind::NotNullViolation,
308 Some("23502"),
309 None,
310 "",
311 Some("employee_id"),
312 Some("payslips"),
313 )
314 .unwrap();
315 assert_eq!(
316 (v.path.as_str(), v.code.as_str()),
317 ("employee_id", "required")
318 );
319 assert_eq!(v.message, "employee_id is required");
320 }
321
322 #[test]
323 fn a_composite_unique_key_names_its_own_column_not_the_tenant() {
324 let v = constraint_violation(
325 ErrorKind::UniqueViolation,
326 Some("23505"),
327 Some("uq_accounts__code_taken"),
328 "Key (company_id, code)=(7c0a…, 1100) already exists.",
329 None,
330 Some("accounts"),
331 )
332 .unwrap();
333 assert_eq!(v.path, "code");
334 assert!(
335 v.message.starts_with("code "),
336 "a sentence with a subject: {}",
337 v.message
338 );
339 let scoped = constraint_violation(
340 ErrorKind::UniqueViolation,
341 Some("23505"),
342 Some("uq_items__sku_taken"),
343 "Key (org_unit_id, lower(sku::text))=(a, b) already exists.",
344 None,
345 Some("items"),
346 )
347 .unwrap();
348 assert_eq!(scoped.path, "sku");
349 let bare = constraint_violation(
350 ErrorKind::UniqueViolation,
351 Some("23505"),
352 Some("uq_x__taken"),
353 "",
354 None,
355 None,
356 )
357 .unwrap();
358 assert_eq!(bare.path, "");
359 assert!(
360 bare.message.starts_with(char::is_uppercase),
361 "{}",
362 bare.message
363 );
364 }
365
366 #[test]
367 fn a_check_reads_as_a_sentence_never_as_the_rule_identifier() {
368 let v = constraint_violation(
369 ErrorKind::CheckViolation,
370 Some("23514"),
371 Some("ck_tickets_title_max_length__max_length"),
372 "",
373 None,
374 Some("tickets"),
375 )
376 .unwrap();
377 assert_eq!(v.code, "max_length", "the code stays machine-readable");
378 assert!(
379 !v.message.contains('`') && !v.message.contains("max_length"),
380 "{}",
381 v.message
382 );
383 assert!(v.message.starts_with("title "), "{}", v.message);
384 let row = constraint_violation(
386 ErrorKind::CheckViolation,
387 Some("23514"),
388 Some("ck_journals_g_jrn_1__unbalanced"),
389 "",
390 None,
391 Some("journals"),
392 )
393 .unwrap();
394 assert_eq!((row.path.as_str(), row.code.as_str()), ("", "unbalanced"));
395 assert!(
396 !row.message.contains("unbalanced") && !row.message.contains('`'),
397 "{}",
398 row.message
399 );
400 let tg = constraint_violation(
402 ErrorKind::CheckViolation,
403 Some("23514"),
404 Some("tg_journals__state_transition_not_declared"),
405 "",
406 Some("status"),
407 Some("journals"),
408 )
409 .unwrap();
410 assert_eq!(
411 (tg.path.as_str(), tg.code.as_str()),
412 ("status", "state_transition_not_declared")
413 );
414 assert!(
415 !tg.message.contains("state_transition_not_declared"),
416 "{}",
417 tg.message
418 );
419 }
420
421 #[test]
422 fn a_database_error_inside_any_error_converts_as_the_database_error_does() {
423 let wrapped = anyhow::Error::new(sqlx::Error::RowNotFound).context("loading the row");
424 assert!(matches!(RepositoryError::from_source(wrapped), RepositoryError::NotFound));
425 assert!(matches!(RepositoryError::from_source(sqlx::Error::RowNotFound), RepositoryError::NotFound));
426 let other = RepositoryError::from_source(anyhow::anyhow!("pool closed"));
427 assert!(matches!(other, RepositoryError::DatabaseError(ref m) if m == "pool closed"), "{other:?}");
428 }
429
430 #[test]
431 fn an_error_that_names_no_rule_keeps_its_old_answer() {
432 assert!(constraint_violation(
433 ErrorKind::UniqueViolation,
434 Some("23505"),
435 Some("users_email_key"),
436 "",
437 None,
438 None
439 )
440 .is_none());
441 assert!(
442 constraint_violation(ErrorKind::Other, Some("42P01"), None, "", None, None).is_none()
443 );
444 assert!(constraint_violation(
446 ErrorKind::NotNullViolation,
447 Some("23502"),
448 None,
449 "",
450 None,
451 None
452 )
453 .is_none());
454 }
455}
456
457pub trait PersistentEntity: Clone + Send + Sync + Debug + Serialize + DeserializeOwned + 'static {
466 fn entity_id(&self) -> String;
468
469 fn set_entity_id(&mut self, id: String);
471
472 fn created_at(&self) -> Option<DateTime<Utc>>;
474
475 fn set_created_at(&mut self, ts: DateTime<Utc>);
477
478 fn updated_at(&self) -> Option<DateTime<Utc>>;
480
481 fn set_updated_at(&mut self, ts: DateTime<Utc>);
483
484 fn deleted_at(&self) -> Option<DateTime<Utc>>;
486
487 fn set_deleted_at(&mut self, ts: Option<DateTime<Utc>>);
489
490 fn is_deleted(&self) -> bool {
492 self.deleted_at().is_some()
493 }
494
495 fn mark_deleted(&mut self) {
497 self.set_deleted_at(Some(Utc::now()));
498 self.set_updated_at(Utc::now());
499 }
500
501 fn restore(&mut self) {
503 self.set_deleted_at(None);
504 self.set_updated_at(Utc::now());
505 }
506
507 fn touch(&mut self) {
509 self.set_updated_at(Utc::now());
510 }
511
512 fn generate_id() -> String {
514 uuid::Uuid::new_v4().to_string()
515 }
516
517 fn write_protected_fields() -> &'static [&'static str] {
528 &[]
529 }
530
531 fn state_fields() -> &'static [&'static str] {
541 &[]
542 }
543}
544
545pub trait PartialUpdatable: PersistentEntity {
547 fn apply_partial_update(&mut self, fields: &HashMap<String, serde_json::Value>) -> Result<(), RepositoryError>;
549}
550
551pub trait Versioned {
553 fn version(&self) -> u64;
554 fn set_version(&mut self, version: u64);
555 fn increment_version(&mut self) {
556 self.set_version(self.version() + 1);
557 }
558}
559
560#[async_trait]
569pub trait CrudRepository<E>: Send + Sync
570where
571 E: PersistentEntity,
572{
573 async fn create(&self, entity: E) -> Result<E, RepositoryError>;
575
576 async fn find_by_id(&self, id: &str) -> Result<Option<E>, RepositoryError>;
578
579 async fn find_by_id_including_deleted(&self, id: &str) -> Result<Option<E>, RepositoryError>;
581
582 async fn update(&self, entity: E) -> Result<E, RepositoryError>;
584
585 async fn update_changed(&self, before: &E, after: E) -> Result<Option<E>, RepositoryError> {
596 let _ = before;
597 match self.update(after).await {
598 Ok(saved) => Ok(Some(saved)),
599 Err(RepositoryError::NotFound) => Ok(None),
600 Err(e) => Err(e),
601 }
602 }
603
604 async fn soft_delete(&self, id: &str) -> Result<bool, RepositoryError>;
606
607 async fn restore(&self, id: &str) -> Result<Option<E>, RepositoryError>;
609
610 async fn hard_delete(&self, id: &str) -> Result<bool, RepositoryError>;
612
613 async fn list(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
615
616 async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
618
619 async fn count(&self) -> Result<u64, RepositoryError>;
621
622 async fn count_deleted(&self) -> Result<u64, RepositoryError>;
624
625 async fn bulk_create(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError>;
627
628 async fn empty_trash(&self) -> Result<u64, RepositoryError>;
630
631 async fn list_filtered(
636 &self,
637 page: u32,
638 limit: u32,
639 filters: HashMap<String, String>,
640 ) -> Result<(Vec<E>, u64), RepositoryError> {
641 let _ = filters; self.list(page, limit).await
643 }
644
645 async fn list_filtered_with_info(
650 &self,
651 page: u32,
652 limit: u32,
653 filters: HashMap<String, String>,
654 ) -> Result<(Vec<E>, backbone_orm::repository::PaginationInfo), RepositoryError> {
655 let (rows, total) = self.list_filtered(page, limit, filters).await?;
656 Ok((
657 rows,
658 backbone_orm::repository::PaginationInfo::new(page, limit, total),
659 ))
660 }
661
662 fn table_name(&self) -> Option<&str> {
674 None
675 }
676
677 async fn aggregate_filtered(
678 &self,
679 spec: &backbone_orm::repository::AggregateSpec,
680 filters: HashMap<String, String>,
681 ) -> Result<backbone_orm::repository::AggregateResult, RepositoryError> {
682 let _ = (spec, filters);
683 Err(RepositoryError::DatabaseError(
684 "aggregate is not supported by this repository".to_string(),
685 ))
686 }
687
688 async fn fetch_related_json(
693 &self,
694 _table: &str,
695 _ids: &[String],
696 ) -> Vec<serde_json::Value> {
697 Vec::new()
698 }
699
700 async fn exists(&self, id: &str) -> Result<bool, RepositoryError> {
702 Ok(self.find_by_id(id).await?.is_some())
703 }
704
705 async fn bulk_soft_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
715 let mut n = 0;
716 for id in ids {
717 if self.soft_delete(id).await? {
718 n += 1;
719 }
720 }
721 Ok(n)
722 }
723
724 async fn bulk_restore(&self, ids: &[String]) -> Result<Vec<E>, RepositoryError> {
726 let mut out = Vec::with_capacity(ids.len());
727 for id in ids {
728 if let Some(e) = self.restore(id).await? {
729 out.push(e);
730 }
731 }
732 Ok(out)
733 }
734
735 async fn bulk_hard_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
737 let mut n = 0;
738 for id in ids {
739 if self.hard_delete(id).await? {
740 n += 1;
741 }
742 }
743 Ok(n)
744 }
745
746 async fn restore_all(&self) -> Result<Vec<E>, RepositoryError> {
748 let mut restored = Vec::new();
749 loop {
752 let (batch, _) = self.list_deleted(1, 500).await?;
753 if batch.is_empty() {
754 break;
755 }
756 for entity in &batch {
757 if let Some(e) = self.restore(&entity.entity_id()).await? {
758 restored.push(e);
759 }
760 }
761 }
762 Ok(restored)
763 }
764
765 async fn bulk_update(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError> {
767 let mut out = Vec::with_capacity(entities.len());
768 for entity in entities {
769 out.push(self.update(entity).await?);
770 }
771 Ok(out)
772 }
773}
774
775#[async_trait]
777pub trait SearchableRepository<E>: CrudRepository<E>
778where
779 E: PersistentEntity,
780{
781 async fn search(
783 &self,
784 filters: HashMap<String, String>,
785 page: u32,
786 limit: u32,
787 ) -> Result<(Vec<E>, u64), RepositoryError>;
788
789 async fn find_by_field(&self, field: &str, value: &str) -> Result<Option<E>, RepositoryError>;
791
792 async fn find_all_by_field(
794 &self,
795 field: &str,
796 value: &str,
797 page: u32,
798 limit: u32,
799 ) -> Result<(Vec<E>, u64), RepositoryError>;
800}
801
802#[cfg(feature = "postgres")]
807pub use postgres_traits::*;
808
809#[cfg(feature = "postgres")]
810mod postgres_traits {
811 use super::*;
812 use sqlx::postgres::PgRow;
813 use sqlx::FromRow;
814
815 pub trait PostgresEntity: PersistentEntity + for<'r> FromRow<'r, PgRow> + Unpin {
853 fn table_name() -> &'static str;
855
856 fn id_column() -> &'static str {
858 "id"
859 }
860
861 fn select_columns() -> &'static [&'static str];
863
864 fn insert_columns() -> &'static [&'static str];
866
867 fn update_columns() -> &'static [&'static str] {
869 Self::insert_columns()
870 }
871
872 fn bind_for_insert<'q>(
876 entity: &'q Self,
877 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
878 ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>;
879
880 fn bind_for_update<'q>(
884 entity: &'q Self,
885 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
886 ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
887 Self::bind_for_insert(entity, query)
889 }
890
891 fn select_by_id_query() -> String {
893 format!(
894 "SELECT {} FROM {} WHERE {} = $1 AND deleted_at IS NULL",
895 Self::select_columns().join(", "),
896 Self::table_name(),
897 Self::id_column()
898 )
899 }
900
901 fn select_by_id_including_deleted_query() -> String {
903 format!(
904 "SELECT {} FROM {} WHERE {} = $1",
905 Self::select_columns().join(", "),
906 Self::table_name(),
907 Self::id_column()
908 )
909 }
910
911 fn list_query() -> String {
913 format!(
914 "SELECT {} FROM {} WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT $1 OFFSET $2",
915 Self::select_columns().join(", "),
916 Self::table_name()
917 )
918 }
919
920 fn count_query() -> String {
922 format!(
923 "SELECT COUNT(*) FROM {} WHERE deleted_at IS NULL",
924 Self::table_name()
925 )
926 }
927
928 fn list_deleted_query() -> String {
930 format!(
931 "SELECT {} FROM {} WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC LIMIT $1 OFFSET $2",
932 Self::select_columns().join(", "),
933 Self::table_name()
934 )
935 }
936
937 fn count_deleted_query() -> String {
939 format!(
940 "SELECT COUNT(*) FROM {} WHERE deleted_at IS NOT NULL",
941 Self::table_name()
942 )
943 }
944
945 fn soft_delete_query() -> String {
947 format!(
948 "UPDATE {} SET deleted_at = NOW(), updated_at = NOW() WHERE {} = $1 AND deleted_at IS NULL",
949 Self::table_name(),
950 Self::id_column()
951 )
952 }
953
954 fn restore_query() -> String {
956 format!(
957 "UPDATE {} SET deleted_at = NULL, updated_at = NOW() WHERE {} = $1 AND deleted_at IS NOT NULL RETURNING {}",
958 Self::table_name(),
959 Self::id_column(),
960 Self::select_columns().join(", ")
961 )
962 }
963
964 fn hard_delete_query() -> String {
966 format!(
967 "DELETE FROM {} WHERE {} = $1",
968 Self::table_name(),
969 Self::id_column()
970 )
971 }
972
973 fn empty_trash_query() -> String {
975 format!(
976 "DELETE FROM {} WHERE deleted_at IS NOT NULL",
977 Self::table_name()
978 )
979 }
980 }
981}