Skip to main content

bottle_orm/
model.rs

1//! # Model Module
2//!
3//! This module defines the core `Model` trait and associated structures for Bottle ORM.
4//! It provides the interface that all database entities must implement, along with
5//! metadata structures for describing table columns.
6//!
7//! ## Overview
8//!
9//! The `Model` trait is the foundation of Bottle ORM. It defines how Rust structs
10//! map to database tables, including:
11//!
12//! - Table name resolution
13//! - Column metadata (types, constraints, relationships)
14//! - Serialization to/from database format
15//!
16//! ## Automatic Implementation
17//!
18//! The `Model` trait is typically implemented automatically via the `#[derive(Model)]`
19//! procedural macro, which analyzes struct fields and `#[orm(...)]` attributes to
20//! generate the necessary implementation.
21//!
22//! ## Example Usage
23//!
24//! ```rust,ignore
25//! use bottle_orm::Model;
26//! use uuid::Uuid;
27//! use chrono::{DateTime, Utc};
28//! use serde::{Deserialize, Serialize};
29//! use sqlx::FromRow;
30//!
31//! #[derive(Model, Debug, Clone, Serialize, Deserialize, FromRow)]
32//! struct User {
33//!     #[orm(primary_key)]
34//!     id: Uuid,
35//!
36//!     #[orm(size = 50, unique, index)]
37//!     username: String,
38//!
39//!     #[orm(size = 100)]
40//!     email: String,
41//!
42//!     age: Option<i32>,
43//!
44//!     #[orm(create_time)]
45//!     created_at: DateTime<Utc>,
46//! }
47//!
48//! #[derive(Model, Debug, Clone, Serialize, Deserialize, FromRow)]
49//! struct Post {
50//!     #[orm(primary_key)]
51//!     id: Uuid,
52//!
53//!     #[orm(foreign_key = "User::id")]
54//!     user_id: Uuid,
55//!
56//!     #[orm(size = 200)]
57//!     title: String,
58//!
59//!     content: String,
60//!
61//!     #[orm(create_time)]
62//!     created_at: DateTime<Utc>,
63//! }
64//! ```
65//!
66//! ## Supported ORM Attributes
67//!
68//! - `#[orm(primary_key)]` - Marks field as primary key
69//! - `#[orm(unique)]` - Adds UNIQUE constraint
70//! - `#[orm(index)]` - Creates database index
71//! - `#[orm(size = N)]` - Sets VARCHAR size (for String fields)
72//! - `#[orm(create_time)]` - Auto-populate with current timestamp on creation
73//! - `#[orm(update_time)]` - Auto-update timestamp on modification (future feature)
74//! - `#[orm(foreign_key = "Table::Column")]` - Defines foreign key relationship
75
76// ============================================================================
77// External Crate Imports
78// ============================================================================
79
80use std::collections::HashMap;
81use futures::future::BoxFuture;
82use crate::database::Connection;
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum RelationType {
86    HasOne,
87    HasMany,
88    BelongsTo,
89}
90
91#[derive(Debug, Clone)]
92pub struct RelationInfo {
93    pub name: &'static str,
94    pub rel_type: RelationType,
95    pub target_table: &'static str,
96    pub foreign_key: &'static str,
97    pub local_key: &'static str,
98}
99
100// ============================================================================
101// Column Metadata Structure
102// ============================================================================
103
104/// Metadata information about a database column.
105///
106/// This structure contains all the information needed to generate SQL table
107/// definitions and handle type conversions between Rust and SQL. It is populated
108/// automatically by the `#[derive(Model)]` macro based on struct field types
109/// and `#[orm(...)]` attributes.
110///
111/// # Fields
112///
113/// * `name` - Column name (field name from struct)
114/// * `sql_type` - SQL type string (e.g., "INTEGER", "TEXT", "UUID", "TIMESTAMPTZ")
115/// * `is_primary_key` - Whether this is the primary key column
116/// * `is_nullable` - Whether NULL values are allowed (from Option<T>)
117/// * `create_time` - Auto-populate with CURRENT_TIMESTAMP on insert
118/// * `update_time` - Auto-update timestamp on modification (future feature)
119/// * `unique` - Whether UNIQUE constraint should be added
120/// * `index` - Whether to create an index on this column
121/// * `foreign_table` - Name of referenced table (for foreign keys)
122/// * `foreign_key` - Name of referenced column (for foreign keys)
123///
124/// # Example
125///
126/// ```rust,ignore
127/// // For this field:
128/// #[orm(size = 50, unique, index)]
129/// username: String,
130///
131/// // The generated ColumnInfo would be:
132/// ColumnInfo {
133///     name: "username",
134///     sql_type: "VARCHAR(50)",
135///     is_primary_key: false,
136///     is_nullable: false,
137///     create_time: false,
138///     update_time: false,
139///     unique: true,
140///     index: true,
141///     foreign_table: None,
142///     foreign_key: None,
143/// }
144/// ```
145///
146/// # SQL Type Mapping
147///
148/// The `sql_type` field contains the SQL type based on the Rust type:
149///
150/// - `i32` → `"INTEGER"`
151/// - `i64` → `"BIGINT"`
152/// - `String` → `"TEXT"` or `"VARCHAR(N)"` with size attribute
153/// - `bool` → `"BOOLEAN"`
154/// - `f64` → `"DOUBLE PRECISION"`
155/// - `Uuid` → `"UUID"`
156/// - `DateTime<Utc>` → `"TIMESTAMPTZ"`
157/// - `NaiveDateTime` → `"TIMESTAMP"`
158/// - `NaiveDate` → `"DATE"`
159/// - `NaiveTime` → `"TIME"`
160/// - `Option<T>` → Same as T, but `is_nullable = true`
161#[derive(Debug, Clone)]
162pub struct ColumnInfo {
163    /// The column name in the database.
164    ///
165    /// This is derived from the struct field name and is typically converted
166    /// to snake_case when generating SQL. The `r#` prefix is stripped if present
167    /// (for Rust keywords used as field names).
168    ///
169    /// # Example
170    /// ```rust,ignore
171    /// // Field: user_id: i32
172    /// name: "user_id"
173    ///
174    /// // Field: r#type: String (type is a Rust keyword)
175    /// name: "r#type" // The r# will be stripped in SQL generation
176    /// ```
177    pub name: &'static str,
178
179    /// The SQL type of the column (e.g., "TEXT", "INTEGER", "TIMESTAMPTZ").
180    ///
181    /// This string is used directly in CREATE TABLE statements. It must be
182    /// a valid SQL type for the target database.
183    ///
184    /// # Example
185    /// ```rust,ignore
186    /// // i32 field
187    /// sql_type: "INTEGER"
188    ///
189    /// // UUID field
190    /// sql_type: "UUID"
191    ///
192    /// // String with size = 100
193    /// sql_type: "VARCHAR(100)"
194    /// ```
195    pub sql_type: &'static str,
196
197    /// Whether this column is a Primary Key.
198    ///
199    /// Set to `true` via `#[orm(primary_key)]` attribute. A table should have
200    /// exactly one primary key column.
201    ///
202    /// # SQL Impact
203    /// - Adds `PRIMARY KEY` constraint
204    /// - Implicitly makes column `NOT NULL`
205    /// - Creates a unique index automatically
206    ///
207    /// # Example
208    /// ```rust,ignore
209    /// #[orm(primary_key)]
210    /// id: Uuid,
211    /// // is_primary_key: true
212    /// ```
213    pub is_primary_key: bool,
214
215    /// Whether this column allows NULL values.
216    ///
217    /// Automatically set to `true` when the field type is `Option<T>`,
218    /// otherwise `false` for non-optional types.
219    ///
220    /// # SQL Impact
221    /// - `false`: Adds `NOT NULL` constraint
222    /// - `true`: Allows NULL values
223    ///
224    /// # Example
225    /// ```rust,ignore
226    /// // Required field
227    /// username: String,
228    /// // is_nullable: false → NOT NULL
229    ///
230    /// // Optional field
231    /// middle_name: Option<String>,
232    /// // is_nullable: true → allows NULL
233    /// ```
234    pub is_nullable: bool,
235
236    /// Whether this column should be automatically populated with the creation timestamp.
237    ///
238    /// Set via `#[orm(create_time)]` attribute. When `true`, the column gets
239    /// a `DEFAULT CURRENT_TIMESTAMP` constraint.
240    ///
241    /// # SQL Impact
242    /// - Adds `DEFAULT CURRENT_TIMESTAMP`
243    /// - Column is auto-populated on INSERT
244    ///
245    /// # Example
246    /// ```rust,ignore
247    /// #[orm(create_time)]
248    /// created_at: DateTime<Utc>,
249    /// // create_time: true
250    /// // SQL: created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
251    /// ```
252    pub create_time: bool,
253
254    /// Whether this column should be automatically updated on modification.
255    ///
256    /// Set via `#[orm(update_time)]` attribute. This is a **future feature**
257    /// not yet fully implemented.
258    ///
259    /// # Future Implementation
260    /// When implemented, this will:
261    /// - Add database trigger or application-level update
262    /// - Auto-update timestamp on every UPDATE
263    ///
264    /// # Example
265    /// ```rust,ignore
266    /// #[orm(update_time)]
267    /// updated_at: DateTime<Utc>,
268    /// // update_time: true (future feature)
269    /// ```
270    pub update_time: bool,
271
272    /// Whether this column has a UNIQUE constraint.
273    ///
274    /// Set via `#[orm(unique)]` attribute. Ensures no two rows can have
275    /// the same value in this column (NULL values may be exempt depending
276    /// on database).
277    ///
278    /// # SQL Impact
279    /// - Adds `UNIQUE` constraint
280    /// - Creates a unique index automatically
281    ///
282    /// # Example
283    /// ```rust,ignore
284    /// #[orm(unique)]
285    /// username: String,
286    /// // unique: true
287    /// // SQL: username VARCHAR(255) UNIQUE
288    /// ```
289    pub unique: bool,
290
291    /// Whether an index should be created for this column.
292    ///
293    /// Set via `#[orm(index)]` attribute. Creates a database index to speed
294    /// up queries that filter or sort by this column.
295    ///
296    /// # SQL Impact
297    /// - Creates separate `CREATE INDEX` statement
298    /// - Index name: `idx_{table}_{column}`
299    ///
300    /// # Example
301    /// ```rust,ignore
302    /// #[orm(index)]
303    /// email: String,
304    /// // index: true
305    /// // SQL: CREATE INDEX idx_user_email ON user (email)
306    /// ```
307    pub index: bool,
308
309    /// The name of the foreign table, if this is a Foreign Key.
310    ///
311    /// Set via `#[orm(foreign_key = "Table::Column")]` attribute. Contains
312    /// the name of the referenced table.
313    ///
314    /// # Example
315    /// ```rust,ignore
316    /// #[orm(foreign_key = "User::id")]
317    /// user_id: Uuid,
318    /// // foreign_table: Some("User")
319    /// ```
320    pub foreign_table: Option<&'static str>,
321
322    /// The name of the foreign column, if this is a Foreign Key.
323    ///
324    /// Set via `#[orm(foreign_key = "Table::Column")]` attribute. Contains
325    /// the name of the referenced column in the foreign table.
326    ///
327    /// # Example
328    /// ```rust,ignore
329    /// #[orm(foreign_key = "User::id")]
330    /// user_id: Uuid,
331    /// // foreign_key: Some("id")
332    /// // SQL: FOREIGN KEY (user_id) REFERENCES user (id)
333    /// ```
334    pub foreign_key: Option<&'static str>,
335
336    /// Whether this field should be omitted from queries by default.
337    ///
338    /// Set via `#[orm(omit)]` attribute. When `true`, this column will be
339    /// excluded from query results unless explicitly selected.
340    ///
341    /// # Example
342    /// ```rust,ignore
343    /// #[orm(omit)]
344    /// password: String,
345    /// // omit: true
346    /// // This field will not be included in SELECT * queries
347    /// ```
348    pub omit: bool,
349
350    /// Whether this field is used for soft delete functionality.
351    ///
352    /// Set via `#[orm(soft_delete)]` attribute. When `true`, this column
353    /// will be used to track deletion timestamps. Queries will automatically
354    /// filter out records where this column is not NULL.
355    ///
356    /// # Example
357    /// ```rust,ignore
358    /// #[orm(soft_delete)]
359    /// deleted_at: Option<DateTime<Utc>>,
360    /// // soft_delete: true
361    /// // Records with deleted_at set will be excluded from queries
362    /// ```
363    pub soft_delete: bool,
364}
365
366// ============================================================================
367// Model Trait
368// ============================================================================
369
370/// The core trait defining a Database Model (Table) in Bottle ORM.
371///
372/// This trait must be implemented by all structs that represent database tables.
373/// It provides methods for retrieving table metadata, column information, and
374/// converting instances to/from database format.
375///
376/// # Automatic Implementation
377///
378/// This trait is typically implemented automatically via the `#[derive(Model)]`
379/// procedural macro. Manual implementation is possible but not recommended.
380///
381/// # Required Methods
382///
383/// * `table_name()` - Returns the table name
384/// * `columns()` - Returns column metadata
385/// * `active_columns()` - Returns column names
386/// * `to_map()` - Serializes instance to a HashMap
387///
388/// # Example with Derive
389///
390/// ```rust,ignore
391/// use bottle_orm::Model;
392/// use uuid::Uuid;
393///
394/// #[derive(Model)]
395/// struct User {
396///     #[orm(primary_key)]
397///     id: Uuid,
398///     username: String,
399///     age: i32,
400/// }
401///
402/// // Now you can use:
403/// assert_eq!(User::table_name(), "User");
404/// assert_eq!(User::active_columns(), vec!["id", "username", "age"]);
405/// ```
406///
407/// # Example Manual Implementation
408///
409/// ```rust,ignore
410/// use bottle_orm::{Model, ColumnInfo};
411/// use std::collections::HashMap;
412///
413/// struct CustomUser {
414///     id: i32,
415///     name: String,
416/// }
417///
418/// impl Model for CustomUser {
419///     fn table_name() -> &'static str {
420///         "custom_users"
421///     }
422///
423///     fn columns() -> Vec<ColumnInfo> {
424///         vec![
425///             ColumnInfo {
426///                 name: "id",
427///                 sql_type: "INTEGER",
428///                 is_primary_key: true,
429///                 is_nullable: false,
430///                 create_time: false,
431///                 update_time: false,
432///                 unique: false,
433///                 index: false,
434///                 foreign_table: None,
435///                 foreign_key: None,
436///             },
437///             ColumnInfo {
438///                 name: "name",
439///                 sql_type: "TEXT",
440///                 is_primary_key: false,
441///                 is_nullable: false,
442///                 create_time: false,
443///                 update_time: false,
444///                 unique: false,
445///                 index: false,
446///                 foreign_table: None,
447///                 foreign_key: None,
448///             },
449///         ]
450///     }
451///
452///     fn active_columns() -> Vec<&'static str> {
453///         vec!["id", "name"]
454///     }
455///
456///     fn to_map(&self) -> HashMap<String, Option<String>> {
457///         let mut map = HashMap::new();
458///         map.insert("id".to_string(), Some(self.id.to_string()));
459///         map.insert("name".to_string(), Some(self.name.clone()));
460///         map
461///     }/// }
462/// ```
463pub trait Model {
464    /// Returns the table name associated with this model.
465    ///
466    /// The table name is derived from the struct name and is used in all
467    /// SQL queries. By default, the derive macro uses the struct name as-is,
468    /// which is then converted to snake_case when generating SQL.
469    ///
470    /// # Returns
471    ///
472    /// A static string slice containing the table name
473    ///
474    /// # Example
475    ///
476    /// ```rust,ignore
477    /// #[derive(Model)]
478    /// struct UserProfile {
479    ///     // ...
480    /// }
481    ///
482    /// // Returns "UserProfile"
483    /// // SQL will use: "user_profile" (snake_case)
484    /// assert_eq!(UserProfile::table_name(), "UserProfile");
485    /// ```
486    fn table_name() -> &'static str;
487
488    /// Returns the list of column definitions for this model.
489    ///
490    /// This method provides complete metadata about each column, including
491    /// SQL types, constraints, and relationships. The information is used
492    /// for table creation, query building, and type conversion.
493    ///
494    /// # Returns
495    ///
496    /// A vector of `ColumnInfo` structs describing each column
497    ///
498    /// # Example
499    ///
500    /// ```rust,ignore
501    /// #[derive(Model)]
502    /// struct User {
503    ///     #[orm(primary_key)]
504    ///     id: Uuid,
505    ///     username: String,
506    /// }
507    ///
508    /// let columns = User::columns();
509    /// assert_eq!(columns.len(), 2);
510    /// assert!(columns[0].is_primary_key);
511    /// assert_eq!(columns[1].sql_type, "TEXT");
512    /// ```
513    fn columns() -> Vec<ColumnInfo>;
514
515    /// Returns the names of active columns (struct fields).
516    ///
517    /// This method returns a simple list of column names without metadata.
518    /// It's used for query building and SELECT statement generation.
519    ///
520    /// # Returns
521    ///
522    /// A vector of static string slices containing column names
523    ///
524    /// # Example
525    ///
526    /// ```rust,ignore
527    /// #[derive(Model)]
528    /// struct User {
529    ///     #[orm(primary_key)]
530    ///     id: Uuid,
531    ///     username: String,
532    ///     email: String,
533    /// }
534    ///
535    /// assert_eq!(
536    ///     User::active_columns(),
537    ///     vec!["id", "username", "email"]
538    /// );
539    /// ```
540    fn active_columns() -> Vec<&'static str>;
541
542    /// Returns the list of relations for this model.
543    ///
544    /// This method provides metadata about the relationships defined in the model.
545    ///
546    /// # Returns
547    ///
548    /// A vector of `RelationInfo` structs describing each relation
549    fn relations() -> Vec<RelationInfo> {
550        Vec::new()
551    }
552
553    /// Loads a specific relation for a collection of models.
554    ///
555    /// This method is used by the Query Builder to implement eager loading (with).
556    /// It should fetch the related records and inject them into the models.
557    ///
558    /// # Arguments
559    ///
560    /// * `relation_name` - The name of the relation to load
561    /// * `models` - A mutable slice of model instances
562    /// * `tx` - The database connection
563    fn load_relations<'a>(
564        _relation_name: &'a str,
565        _models: &'a mut [Self],
566        _tx: &'a dyn Connection,
567    ) -> BoxFuture<'a, Result<(), sqlx::Error>>
568    where
569        Self: Sized,
570    {
571        Box::pin(async move { Ok(()) })
572    }
573
574    /// Converts the model instance into a value map (Column Name → String Value).
575    ///
576    /// This method serializes the model instance into a HashMap where keys are
577    /// column names and values are string representations. It's used primarily
578    /// for INSERT operations.
579    ///
580    /// # Returns
581    ///
582    /// A HashMap mapping column names to string values
583    ///
584    /// # Type Conversion
585    ///
586    /// All values are converted to strings via the `ToString` trait:
587    /// - Primitives: Direct conversion (e.g., `42` → `"42"`)
588    /// - UUID: Hyphenated format (e.g., `"550e8400-e29b-41d4-a716-446655440000"`)
589    /// - DateTime: RFC 3339 format
590    /// - Option<T>: Only included if Some, omitted if None
591    ///
592    /// # Example
593    ///
594    /// ```rust,ignore
595    /// use uuid::Uuid;
596    ///
597    /// #[derive(Model)]
598    /// struct User {
599    ///     #[orm(primary_key)]
600    ///     id: Uuid,
601    ///     username: String,
602    ///     age: i32,
603    /// }
604    ///
605    /// let user = User {
606    ///     id: Uuid::new_v4(),
607    ///     username: "john_doe".to_string(),
608    ///     age: 25,
609    /// };
610    ///
611    /// let map = user.to_map();
612    /// assert!(map.contains_key("id"));
613    /// assert_eq!(map.get("username"), Some(&Some("john_doe".to_string())));
614    /// assert_eq!(map.get("age"), Some(&Some("25".to_string())));
615    /// ```
616    fn to_map(&self) -> HashMap<String, Option<String>>;
617}
618
619// ============================================================================
620// Tests
621// ============================================================================
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626
627    #[test]
628    fn test_column_info_creation() {
629        let col = ColumnInfo {
630            name: "test_column",
631            sql_type: "INTEGER",
632            is_primary_key: true,
633            is_nullable: false,
634            create_time: false,
635            update_time: false,
636            unique: false,
637            index: false,
638            foreign_table: None,
639            foreign_key: None,
640            omit: false,
641            soft_delete: false,
642        };
643
644        assert_eq!(col.name, "test_column");
645        assert_eq!(col.sql_type, "INTEGER");
646        assert!(col.is_primary_key);
647        assert!(!col.is_nullable);
648    }
649
650    #[test]
651    fn test_column_info_with_foreign_key() {
652        let col = ColumnInfo {
653            name: "user_id",
654            sql_type: "UUID",
655            is_primary_key: false,
656            is_nullable: false,
657            create_time: false,
658            update_time: false,
659            unique: false,
660            index: false,
661            foreign_table: Some("User"),
662            foreign_key: Some("id"),
663            omit: false,
664            soft_delete: false,
665        };
666
667        assert_eq!(col.foreign_table, Some("User"));
668        assert_eq!(col.foreign_key, Some("id"));
669    }
670}