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