Skip to main content

backbone_core/persistence/
traits.rs

1//! Repository Traits - Contracts for Entity Persistence
2//!
3//! These traits define the contracts that entities and repositories must implement
4//! to work with the generic repository implementations.
5
6use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use serde::{de::DeserializeOwned, Serialize};
9use std::collections::HashMap;
10use std::fmt::Debug;
11
12// ============================================================
13// Error Types
14// ============================================================
15
16/// Repository error types
17#[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    /// The database refused the write through a constraint that stands for a
41    /// rule: a foreign key, a CHECK, a generated unique index or an exclusion.
42    /// One violation, coded as the application-side check is.
43    #[error("validation failed: {}", .0.iter().map(|v| v.message.as_str()).collect::<Vec<_>>().join("; "))]
44    Violations(Vec<crate::violation::Violation>),
45}
46
47impl From<serde_json::Error> for RepositoryError {
48    fn from(e: serde_json::Error) -> Self {
49        RepositoryError::SerializationError(e.to_string())
50    }
51}
52
53#[cfg(feature = "postgres")]
54impl From<sqlx::Error> for RepositoryError {
55    fn from(e: sqlx::Error) -> Self {
56        match e {
57            sqlx::Error::RowNotFound => RepositoryError::NotFound,
58            sqlx::Error::Database(db_err) => {
59                let pg = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>();
60                if let Some(v) = constraint_violation(
61                    db_err.kind(),
62                    db_err.code().as_deref(),
63                    db_err.constraint(),
64                    pg.and_then(|p| p.detail()).unwrap_or(""),
65                    pg.and_then(|p| p.column()),
66                ) {
67                    return RepositoryError::Violations(vec![v]);
68                }
69                let msg = db_err.message().to_string();
70                if msg.contains("duplicate key") || msg.contains("unique constraint") {
71                    RepositoryError::AlreadyExists(msg)
72                } else {
73                    RepositoryError::DatabaseError(msg)
74                }
75            }
76            _ => RepositoryError::DatabaseError(e.to_string()),
77        }
78    }
79}
80
81/// The violation a database constraint error stands for, or `None` for an error
82/// that names no rule. A foreign key answers `reference_missing` on the column
83/// that names a missing record, or `still_referenced` when a removed record is
84/// still pointed at. A CHECK, a generated unique index (`uq_…`) and an exclusion
85/// answer the code their constraint name carries (see
86/// [`crate::violation::code_from_constraint`]). Another unique index keeps its
87/// `AlreadyExists` answer. The sentence names no table or constraint.
88#[cfg(feature = "postgres")]
89fn constraint_violation(
90    kind: sqlx::error::ErrorKind,
91    sqlstate: Option<&str>,
92    constraint: Option<&str>,
93    detail: &str,
94    column: Option<&str>,
95) -> Option<crate::violation::Violation> {
96    use crate::violation::{code_from_constraint, Violation};
97    use sqlx::error::ErrorKind;
98    let constraint = constraint?;
99    let path = column.map(str::to_string).or_else(|| key_column(detail)).unwrap_or_default();
100    let code = code_from_constraint(constraint);
101    let on = |path: &str| if path.is_empty() { String::new() } else { format!("`{path}` ") };
102    Some(match kind {
103        ErrorKind::ForeignKeyViolation if detail.contains("still referenced") => Violation::new(
104            "",
105            "still_referenced",
106            "This record is still referenced by another record, so it cannot be removed",
107        ),
108        ErrorKind::ForeignKeyViolation => {
109            let message = format!("{}refers to a record that does not exist", on(&path));
110            Violation::new(path, "reference_missing", message)
111        }
112        ErrorKind::CheckViolation => {
113            let message = format!("{}breaks the rule `{code}`", on(&path));
114            Violation::new(path, code, message)
115        }
116        ErrorKind::UniqueViolation if constraint.starts_with("uq_") => {
117            let message = format!("{}is already taken under the rule `{code}`", on(&path));
118            Violation::new(path, code, message)
119        }
120        _ if sqlstate == Some("23P01") => {
121            let message = format!("The record overlaps another one under the rule `{code}`");
122            Violation::new("", code, message)
123        }
124        _ => return None,
125    })
126}
127
128/// The one column a constraint error's detail names (`Key (district_id)=(…)`),
129/// or `None` for a detail naming several or none.
130#[cfg(feature = "postgres")]
131fn key_column(detail: &str) -> Option<String> {
132    let rest = detail.strip_prefix("Key (")?;
133    let cols = &rest[..rest.find(")=")?];
134    (!cols.contains(',')).then(|| cols.trim().to_string())
135}
136
137#[cfg(all(test, feature = "postgres"))]
138mod constraint_violation_tests {
139    use super::*;
140    use sqlx::error::ErrorKind;
141
142    #[test]
143    fn a_missing_reference_names_the_column_and_never_the_table() {
144        let v = constraint_violation(
145            ErrorKind::ForeignKeyViolation,
146            Some("23503"),
147            Some("fk_subdistricts_district_id"),
148            "Key (district_id)=(1484f475-14be-4abc-985b-7291543d32bc) is not present in table \"districts\".",
149            None,
150        )
151        .unwrap();
152        assert_eq!((v.path.as_str(), v.code.as_str()), ("district_id", "reference_missing"));
153        assert!(!v.message.contains("districts\"") && !v.message.contains("fk_"), "{}", v.message);
154    }
155
156    #[test]
157    fn removing_a_referenced_record_is_still_referenced() {
158        let v = constraint_violation(
159            ErrorKind::ForeignKeyViolation,
160            Some("23503"),
161            Some("fk_provinces_country_id"),
162            "Key (id)=(866f1b4e-a9f3-4626-95d1-13c183916b4b) is still referenced from table \"provinces\".",
163            None,
164        )
165        .unwrap();
166        assert_eq!((v.path.as_str(), v.code.as_str()), ("", "still_referenced"));
167    }
168
169    #[test]
170    fn a_rule_constraint_answers_its_rules_code() {
171        let check = constraint_violation(ErrorKind::CheckViolation, Some("23514"), Some("ck_countries_name__max_length"), "", Some("name")).unwrap();
172        assert_eq!((check.path.as_str(), check.code.as_str()), ("name", "max_length"));
173        let unique = constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("uq_accounts__account_code_taken"), "Key (company_id, code)=(a, b) already exists.", None).unwrap();
174        assert_eq!((unique.path.as_str(), unique.code.as_str()), ("", "account_code_taken"));
175        let overlap = constraint_violation(ErrorKind::Other, Some("23P01"), Some("ex_tax_rates__overlapping_validity"), "", None).unwrap();
176        assert_eq!(overlap.code, "overlapping_validity");
177    }
178
179    #[test]
180    fn an_error_that_names_no_rule_keeps_its_old_answer() {
181        assert!(constraint_violation(ErrorKind::UniqueViolation, Some("23505"), Some("users_email_key"), "", None).is_none());
182        assert!(constraint_violation(ErrorKind::Other, Some("42P01"), None, "", None).is_none());
183        assert!(constraint_violation(ErrorKind::NotNullViolation, Some("23502"), Some("x"), "", None).is_none());
184    }
185}
186
187// ============================================================
188// Entity Traits
189// ============================================================
190
191/// Trait for entities that can be persisted.
192///
193/// This trait defines the common fields and behavior required by all entities
194/// that can be stored and retrieved from a repository.
195pub trait PersistentEntity: Clone + Send + Sync + Debug + Serialize + DeserializeOwned + 'static {
196    /// Get the entity's unique identifier
197    fn entity_id(&self) -> String;
198
199    /// Set the entity's unique identifier
200    fn set_entity_id(&mut self, id: String);
201
202    /// Get creation timestamp
203    fn created_at(&self) -> Option<DateTime<Utc>>;
204
205    /// Set creation timestamp
206    fn set_created_at(&mut self, ts: DateTime<Utc>);
207
208    /// Get last update timestamp
209    fn updated_at(&self) -> Option<DateTime<Utc>>;
210
211    /// Set last update timestamp
212    fn set_updated_at(&mut self, ts: DateTime<Utc>);
213
214    /// Get soft delete timestamp (None if not deleted)
215    fn deleted_at(&self) -> Option<DateTime<Utc>>;
216
217    /// Set soft delete timestamp
218    fn set_deleted_at(&mut self, ts: Option<DateTime<Utc>>);
219
220    /// Check if entity is soft-deleted
221    fn is_deleted(&self) -> bool {
222        self.deleted_at().is_some()
223    }
224
225    /// Mark entity as deleted (soft delete)
226    fn mark_deleted(&mut self) {
227        self.set_deleted_at(Some(Utc::now()));
228        self.set_updated_at(Utc::now());
229    }
230
231    /// Restore a soft-deleted entity
232    fn restore(&mut self) {
233        self.set_deleted_at(None);
234        self.set_updated_at(Utc::now());
235    }
236
237    /// Touch the entity (update timestamp)
238    fn touch(&mut self) {
239        self.set_updated_at(Utc::now());
240    }
241
242    /// Generate a new ID for this entity type
243    fn generate_id() -> String {
244        uuid::Uuid::new_v4().to_string()
245    }
246
247    /// Fields that a generic write (PUT, PATCH and their bulk forms) may not
248    /// change, by their serialized name.
249    ///
250    /// The generic service refuses a write that would give one of these a new
251    /// value, and still accepts a write that carries the stored value
252    /// unchanged, so a form that sends the whole record keeps working. Code
253    /// that owns the field — a verb, a write service — changes it through its
254    /// own path. `id` and `metadata` are refused for every entity regardless
255    /// of this list. The schema generator fills it from the fields the schema
256    /// declares as not freely writable; the default protects nothing more.
257    fn write_protected_fields() -> &'static [&'static str] {
258        &[]
259    }
260}
261
262/// Trait for entities that support partial updates via field map
263pub trait PartialUpdatable: PersistentEntity {
264    /// Apply partial updates from a field map
265    fn apply_partial_update(&mut self, fields: &HashMap<String, serde_json::Value>) -> Result<(), RepositoryError>;
266}
267
268/// Trait for entities with version/optimistic locking
269pub trait Versioned {
270    fn version(&self) -> u64;
271    fn set_version(&mut self, version: u64);
272    fn increment_version(&mut self) {
273        self.set_version(self.version() + 1);
274    }
275}
276
277// ============================================================
278// Repository Traits
279// ============================================================
280
281/// Core CRUD repository trait
282///
283/// This trait defines the basic CRUD operations that all repositories must implement.
284/// It's designed to work with the `CrudService` trait from the HTTP layer.
285#[async_trait]
286pub trait CrudRepository<E>: Send + Sync
287where
288    E: PersistentEntity,
289{
290    /// Create a new entity
291    async fn create(&self, entity: E) -> Result<E, RepositoryError>;
292
293    /// Find entity by ID (excluding soft-deleted)
294    async fn find_by_id(&self, id: &str) -> Result<Option<E>, RepositoryError>;
295
296    /// Find entity by ID (including soft-deleted, for trash operations)
297    async fn find_by_id_including_deleted(&self, id: &str) -> Result<Option<E>, RepositoryError>;
298
299    /// Update an existing entity
300    async fn update(&self, entity: E) -> Result<E, RepositoryError>;
301
302    /// Soft delete an entity
303    async fn soft_delete(&self, id: &str) -> Result<bool, RepositoryError>;
304
305    /// Restore a soft-deleted entity
306    async fn restore(&self, id: &str) -> Result<Option<E>, RepositoryError>;
307
308    /// Permanently delete an entity
309    async fn hard_delete(&self, id: &str) -> Result<bool, RepositoryError>;
310
311    /// List entities with pagination (excluding soft-deleted)
312    async fn list(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
313
314    /// List soft-deleted entities with pagination
315    async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<E>, u64), RepositoryError>;
316
317    /// Count all entities (excluding soft-deleted)
318    async fn count(&self) -> Result<u64, RepositoryError>;
319
320    /// Count soft-deleted entities
321    async fn count_deleted(&self) -> Result<u64, RepositoryError>;
322
323    /// Bulk create entities
324    async fn bulk_create(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError>;
325
326    /// Permanently delete all soft-deleted entities
327    async fn empty_trash(&self) -> Result<u64, RepositoryError>;
328
329    /// List entities with pagination and filters (excluding soft-deleted)
330    ///
331    /// Default implementation ignores filters and delegates to `list()`.
332    /// Override in repository implementations that support filter pushdown.
333    async fn list_filtered(
334        &self,
335        page: u32,
336        limit: u32,
337        filters: HashMap<String, String>,
338    ) -> Result<(Vec<E>, u64), RepositoryError> {
339        let _ = filters; // ignored by default
340        self.list(page, limit).await
341    }
342
343    /// `list_filtered`, carrying the pagination info (cursor positions
344    /// included) so the HTTP layer can surface keyset paging. Default: the
345    /// tuple form with the cursor fields absent; the generated Postgres
346    /// repositories override this with the real keyset walk.
347    async fn list_filtered_with_info(
348        &self,
349        page: u32,
350        limit: u32,
351        filters: HashMap<String, String>,
352    ) -> Result<(Vec<E>, backbone_orm::repository::PaginationInfo), RepositoryError> {
353        let (rows, total) = self.list_filtered(page, limit, filters).await?;
354        Ok((
355            rows,
356            backbone_orm::repository::PaginationInfo::new(page, limit, total),
357        ))
358    }
359
360    /// Group and reduce entities under the same filters as `list_filtered`.
361    ///
362    /// There is deliberately no useful default. A repository that cannot group
363    /// must SAY so: an honest error tells the caller its chart is unavailable,
364    /// whereas a default returning zeros would render an empty chart that looks
365    /// like a real answer about a real, empty table. The Postgres-backed
366    /// generated repositories override this via `impl_crud_repository!`.
367    /// The schema-qualified table this repository reads, when it knows it.
368    ///
369    /// The Postgres-backed generated repositories override this; anything that
370    /// cannot name a table returns `None` and simply has no history.
371    fn table_name(&self) -> Option<&str> {
372        None
373    }
374
375    async fn aggregate_filtered(
376        &self,
377        spec: &backbone_orm::repository::AggregateSpec,
378        filters: HashMap<String, String>,
379    ) -> Result<backbone_orm::repository::AggregateResult, RepositoryError> {
380        let _ = (spec, filters);
381        Err(RepositoryError::DatabaseError(
382            "aggregate is not supported by this repository".to_string(),
383        ))
384    }
385
386    /// Hydrate `?include=` relations: fetch rows from `table` by id list, as JSON
387    /// (raw `row_to_json`). `table` comes from `EntityRepoMeta::relations()`
388    /// (generator-emitted), never client input. Default: no expansion. The
389    /// Postgres-backed generated repos override this via `impl_crud_repository!`.
390    async fn fetch_related_json(
391        &self,
392        _table: &str,
393        _ids: &[String],
394    ) -> Vec<serde_json::Value> {
395        Vec::new()
396    }
397
398    /// Check if an entity exists by ID
399    async fn exists(&self, id: &str) -> Result<bool, RepositoryError> {
400        Ok(self.find_by_id(id).await?.is_some())
401    }
402
403    // ── Atomic batch operations ───────────────────────────────────────────────
404    //
405    // The default implementations loop over the single-row methods and are NOT
406    // transactional — they exist so non-Postgres implementors (mocks, custom
407    // adapters) keep compiling. The macro-generated repositories override these
408    // (via `impl_crud_repository!`) with truly atomic, single-transaction
409    // versions backed by `GenericCrudRepository`.
410
411    /// Soft-delete many entities by id. Returns the number affected.
412    async fn bulk_soft_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
413        let mut n = 0;
414        for id in ids {
415            if self.soft_delete(id).await? {
416                n += 1;
417            }
418        }
419        Ok(n)
420    }
421
422    /// Restore many soft-deleted entities by id. Returns the restored entities.
423    async fn bulk_restore(&self, ids: &[String]) -> Result<Vec<E>, RepositoryError> {
424        let mut out = Vec::with_capacity(ids.len());
425        for id in ids {
426            if let Some(e) = self.restore(id).await? {
427                out.push(e);
428            }
429        }
430        Ok(out)
431    }
432
433    /// Permanently delete many entities by id. Returns the number affected.
434    async fn bulk_hard_delete(&self, ids: &[String]) -> Result<u64, RepositoryError> {
435        let mut n = 0;
436        for id in ids {
437            if self.hard_delete(id).await? {
438                n += 1;
439            }
440        }
441        Ok(n)
442    }
443
444    /// Restore every soft-deleted entity. Returns the restored entities.
445    async fn restore_all(&self) -> Result<Vec<E>, RepositoryError> {
446        let mut restored = Vec::new();
447        // Restoring removes rows from the deleted set, so page 1 always returns
448        // the next batch of still-deleted rows until none remain.
449        loop {
450            let (batch, _) = self.list_deleted(1, 500).await?;
451            if batch.is_empty() {
452                break;
453            }
454            for entity in &batch {
455                if let Some(e) = self.restore(&entity.entity_id()).await? {
456                    restored.push(e);
457                }
458            }
459        }
460        Ok(restored)
461    }
462
463    /// Update many entities atomically. Returns the updated entities.
464    async fn bulk_update(&self, entities: Vec<E>) -> Result<Vec<E>, RepositoryError> {
465        let mut out = Vec::with_capacity(entities.len());
466        for entity in entities {
467            out.push(self.update(entity).await?);
468        }
469        Ok(out)
470    }
471}
472
473/// Extended repository with search/filter capabilities
474#[async_trait]
475pub trait SearchableRepository<E>: CrudRepository<E>
476where
477    E: PersistentEntity,
478{
479    /// Search entities with filters
480    async fn search(
481        &self,
482        filters: HashMap<String, String>,
483        page: u32,
484        limit: u32,
485    ) -> Result<(Vec<E>, u64), RepositoryError>;
486
487    /// Find by a specific field value
488    async fn find_by_field(&self, field: &str, value: &str) -> Result<Option<E>, RepositoryError>;
489
490    /// Find all by a specific field value
491    async fn find_all_by_field(
492        &self,
493        field: &str,
494        value: &str,
495        page: u32,
496        limit: u32,
497    ) -> Result<(Vec<E>, u64), RepositoryError>;
498}
499
500// ============================================================
501// PostgreSQL-Specific Traits
502// ============================================================
503
504#[cfg(feature = "postgres")]
505pub use postgres_traits::*;
506
507#[cfg(feature = "postgres")]
508mod postgres_traits {
509    use super::*;
510    use sqlx::postgres::PgRow;
511    use sqlx::FromRow;
512
513    /// Trait for mapping entities to/from PostgreSQL rows
514    ///
515    /// Implement this trait to enable automatic PostgreSQL persistence for your entity.
516    ///
517    /// # Example
518    ///
519    /// ```ignore
520    /// use backbone_core::persistence::{PostgresEntity, PersistentEntity};
521    /// use sqlx::FromRow;
522    ///
523    /// #[derive(Clone, Debug, Serialize, Deserialize, FromRow)]
524    /// struct User {
525    ///     id: String,
526    ///     name: String,
527    ///     email: String,
528    ///     created_at: Option<DateTime<Utc>>,
529    ///     updated_at: Option<DateTime<Utc>>,
530    ///     deleted_at: Option<DateTime<Utc>>,
531    /// }
532    ///
533    /// impl PostgresEntity for User {
534    ///     fn table_name() -> &'static str { "users" }
535    ///     fn select_columns() -> &'static [&'static str] {
536    ///         &["id", "name", "email", "created_at", "updated_at", "deleted_at"]
537    ///     }
538    ///     fn insert_columns() -> &'static [&'static str] {
539    ///         &["id", "name", "email", "created_at", "updated_at"]
540    ///     }
541    ///     fn update_columns() -> &'static [&'static str] {
542    ///         &["name", "email", "updated_at"]
543    ///     }
544    ///     fn bind_for_insert(entity: &Self, query: Query<'_, ...>) -> Query<'_, ...> {
545    ///         query.bind(&entity.id).bind(&entity.name).bind(&entity.email)
546    ///             .bind(&entity.created_at).bind(&entity.updated_at)
547    ///     }
548    /// }
549    /// ```
550    pub trait PostgresEntity: PersistentEntity + for<'r> FromRow<'r, PgRow> + Unpin {
551        /// Table name for this entity
552        fn table_name() -> &'static str;
553
554        /// Primary key column name (default: "id")
555        fn id_column() -> &'static str {
556            "id"
557        }
558
559        /// Column names for SELECT queries (excluding computed columns)
560        fn select_columns() -> &'static [&'static str];
561
562        /// Column names for INSERT (all columns that should be inserted)
563        fn insert_columns() -> &'static [&'static str];
564
565        /// Column names for UPDATE (columns that can be updated, excluding id)
566        fn update_columns() -> &'static [&'static str] {
567            Self::insert_columns()
568        }
569
570        /// Bind entity values to a query for INSERT
571        ///
572        /// The order must match insert_columns()
573        fn bind_for_insert<'q>(
574            entity: &'q Self,
575            query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
576        ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>;
577
578        /// Bind entity values to a query for UPDATE
579        ///
580        /// The order must match update_columns()
581        fn bind_for_update<'q>(
582            entity: &'q Self,
583            query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
584        ) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
585            // Default: same as insert (override if different)
586            Self::bind_for_insert(entity, query)
587        }
588
589        /// Build SELECT by ID query
590        fn select_by_id_query() -> String {
591            format!(
592                "SELECT {} FROM {} WHERE {} = $1 AND deleted_at IS NULL",
593                Self::select_columns().join(", "),
594                Self::table_name(),
595                Self::id_column()
596            )
597        }
598
599        /// Build SELECT by ID including deleted
600        fn select_by_id_including_deleted_query() -> String {
601            format!(
602                "SELECT {} FROM {} WHERE {} = $1",
603                Self::select_columns().join(", "),
604                Self::table_name(),
605                Self::id_column()
606            )
607        }
608
609        /// Build paginated list query
610        fn list_query() -> String {
611            format!(
612                "SELECT {} FROM {} WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT $1 OFFSET $2",
613                Self::select_columns().join(", "),
614                Self::table_name()
615            )
616        }
617
618        /// Build count query
619        fn count_query() -> String {
620            format!(
621                "SELECT COUNT(*) FROM {} WHERE deleted_at IS NULL",
622                Self::table_name()
623            )
624        }
625
626        /// Build list deleted (trash) query
627        fn list_deleted_query() -> String {
628            format!(
629                "SELECT {} FROM {} WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC LIMIT $1 OFFSET $2",
630                Self::select_columns().join(", "),
631                Self::table_name()
632            )
633        }
634
635        /// Build count deleted query
636        fn count_deleted_query() -> String {
637            format!(
638                "SELECT COUNT(*) FROM {} WHERE deleted_at IS NOT NULL",
639                Self::table_name()
640            )
641        }
642
643        /// Build soft delete query
644        fn soft_delete_query() -> String {
645            format!(
646                "UPDATE {} SET deleted_at = NOW(), updated_at = NOW() WHERE {} = $1 AND deleted_at IS NULL",
647                Self::table_name(),
648                Self::id_column()
649            )
650        }
651
652        /// Build restore query
653        fn restore_query() -> String {
654            format!(
655                "UPDATE {} SET deleted_at = NULL, updated_at = NOW() WHERE {} = $1 AND deleted_at IS NOT NULL RETURNING {}",
656                Self::table_name(),
657                Self::id_column(),
658                Self::select_columns().join(", ")
659            )
660        }
661
662        /// Build hard delete query
663        fn hard_delete_query() -> String {
664            format!(
665                "DELETE FROM {} WHERE {} = $1",
666                Self::table_name(),
667                Self::id_column()
668            )
669        }
670
671        /// Build empty trash query
672        fn empty_trash_query() -> String {
673            format!(
674                "DELETE FROM {} WHERE deleted_at IS NOT NULL",
675                Self::table_name()
676            )
677        }
678    }
679}