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