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 /// Whether this field is used for soft delete functionality.
333 ///
334 /// Set via `#[orm(soft_delete)]` attribute. When `true`, this column
335 /// will be used to track deletion timestamps. Queries will automatically
336 /// filter out records where this column is not NULL.
337 ///
338 /// # Example
339 /// ```rust,ignore
340 /// #[orm(soft_delete)]
341 /// deleted_at: Option<DateTime<Utc>>,
342 /// // soft_delete: true
343 /// // Records with deleted_at set will be excluded from queries
344 /// ```
345 pub soft_delete: bool,
346}
347
348// ============================================================================
349// Model Trait
350// ============================================================================
351
352/// The core trait defining a Database Model (Table) in Bottle ORM.
353///
354/// This trait must be implemented by all structs that represent database tables.
355/// It provides methods for retrieving table metadata, column information, and
356/// converting instances to/from database format.
357///
358/// # Automatic Implementation
359///
360/// This trait is typically implemented automatically via the `#[derive(Model)]`
361/// procedural macro. Manual implementation is possible but not recommended.
362///
363/// # Required Methods
364///
365/// * `table_name()` - Returns the table name
366/// * `columns()` - Returns column metadata
367/// * `active_columns()` - Returns column names
368/// * `to_map()` - Serializes instance to a HashMap
369///
370/// # Example with Derive
371///
372/// ```rust,ignore
373/// use bottle_orm::Model;
374/// use uuid::Uuid;
375///
376/// #[derive(Model)]
377/// struct User {
378/// #[orm(primary_key)]
379/// id: Uuid,
380/// username: String,
381/// age: i32,
382/// }
383///
384/// // Now you can use:
385/// assert_eq!(User::table_name(), "User");
386/// assert_eq!(User::active_columns(), vec!["id", "username", "age"]);
387/// ```
388///
389/// # Example Manual Implementation
390///
391/// ```rust,ignore
392/// use bottle_orm::{Model, ColumnInfo};
393/// use std::collections::HashMap;
394///
395/// struct CustomUser {
396/// id: i32,
397/// name: String,
398/// }
399///
400/// impl Model for CustomUser {
401/// fn table_name() -> &'static str {
402/// "custom_users"
403/// }
404///
405/// fn columns() -> Vec<ColumnInfo> {
406/// vec![
407/// ColumnInfo {
408/// name: "id",
409/// sql_type: "INTEGER",
410/// is_primary_key: true,
411/// is_nullable: false,
412/// create_time: false,
413/// update_time: false,
414/// unique: false,
415/// index: false,
416/// foreign_table: None,
417/// foreign_key: None,
418/// },
419/// ColumnInfo {
420/// name: "name",
421/// sql_type: "TEXT",
422/// is_primary_key: false,
423/// is_nullable: false,
424/// create_time: false,
425/// update_time: false,
426/// unique: false,
427/// index: false,
428/// foreign_table: None,
429/// foreign_key: None,
430/// },
431/// ]
432/// }
433///
434/// fn active_columns() -> Vec<&'static str> {
435/// vec!["id", "name"]
436/// }
437///
438/// fn to_map(&self) -> HashMap<String, Option<String>> {
439/// let mut map = HashMap::new();
440/// map.insert("id".to_string(), Some(self.id.to_string()));
441/// map.insert("name".to_string(), Some(self.name.clone()));
442/// map
443/// }/// }
444/// ```
445pub trait Model {
446 /// Returns the table name associated with this model.
447 ///
448 /// The table name is derived from the struct name and is used in all
449 /// SQL queries. By default, the derive macro uses the struct name as-is,
450 /// which is then converted to snake_case when generating SQL.
451 ///
452 /// # Returns
453 ///
454 /// A static string slice containing the table name
455 ///
456 /// # Example
457 ///
458 /// ```rust,ignore
459 /// #[derive(Model)]
460 /// struct UserProfile {
461 /// // ...
462 /// }
463 ///
464 /// // Returns "UserProfile"
465 /// // SQL will use: "user_profile" (snake_case)
466 /// assert_eq!(UserProfile::table_name(), "UserProfile");
467 /// ```
468 fn table_name() -> &'static str;
469
470 /// Returns the list of column definitions for this model.
471 ///
472 /// This method provides complete metadata about each column, including
473 /// SQL types, constraints, and relationships. The information is used
474 /// for table creation, query building, and type conversion.
475 ///
476 /// # Returns
477 ///
478 /// A vector of `ColumnInfo` structs describing each column
479 ///
480 /// # Example
481 ///
482 /// ```rust,ignore
483 /// #[derive(Model)]
484 /// struct User {
485 /// #[orm(primary_key)]
486 /// id: Uuid,
487 /// username: String,
488 /// }
489 ///
490 /// let columns = User::columns();
491 /// assert_eq!(columns.len(), 2);
492 /// assert!(columns[0].is_primary_key);
493 /// assert_eq!(columns[1].sql_type, "TEXT");
494 /// ```
495 fn columns() -> Vec<ColumnInfo>;
496
497 /// Returns the names of active columns (struct fields).
498 ///
499 /// This method returns a simple list of column names without metadata.
500 /// It's used for query building and SELECT statement generation.
501 ///
502 /// # Returns
503 ///
504 /// A vector of static string slices containing column names
505 ///
506 /// # Example
507 ///
508 /// ```rust,ignore
509 /// #[derive(Model)]
510 /// struct User {
511 /// #[orm(primary_key)]
512 /// id: Uuid,
513 /// username: String,
514 /// email: String,
515 /// }
516 ///
517 /// assert_eq!(
518 /// User::active_columns(),
519 /// vec!["id", "username", "email"]
520 /// );
521 /// ```
522 fn active_columns() -> Vec<&'static str>;
523
524 /// Converts the model instance into a value map (Column Name → String Value).
525 ///
526 /// This method serializes the model instance into a HashMap where keys are
527 /// column names and values are string representations. It's used primarily
528 /// for INSERT operations.
529 ///
530 /// # Returns
531 ///
532 /// A HashMap mapping column names to string values
533 ///
534 /// # Type Conversion
535 ///
536 /// All values are converted to strings via the `ToString` trait:
537 /// - Primitives: Direct conversion (e.g., `42` → `"42"`)
538 /// - UUID: Hyphenated format (e.g., `"550e8400-e29b-41d4-a716-446655440000"`)
539 /// - DateTime: RFC 3339 format
540 /// - Option<T>: Only included if Some, omitted if None
541 ///
542 /// # Example
543 ///
544 /// ```rust,ignore
545 /// use uuid::Uuid;
546 ///
547 /// #[derive(Model)]
548 /// struct User {
549 /// #[orm(primary_key)]
550 /// id: Uuid,
551 /// username: String,
552 /// age: i32,
553 /// }
554 ///
555 /// let user = User {
556 /// id: Uuid::new_v4(),
557 /// username: "john_doe".to_string(),
558 /// age: 25,
559 /// };
560 ///
561 /// let map = user.to_map();
562 /// assert!(map.contains_key("id"));
563 /// assert_eq!(map.get("username"), Some(&Some("john_doe".to_string())));
564 /// assert_eq!(map.get("age"), Some(&Some("25".to_string())));
565 /// ```
566 fn to_map(&self) -> HashMap<String, Option<String>>;
567}
568
569// ============================================================================
570// Tests
571// ============================================================================
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn test_column_info_creation() {
579 let col = ColumnInfo {
580 name: "test_column",
581 sql_type: "INTEGER",
582 is_primary_key: true,
583 is_nullable: false,
584 create_time: false,
585 update_time: false,
586 unique: false,
587 index: false,
588 foreign_table: None,
589 foreign_key: None,
590 omit: false,
591 soft_delete: false,
592 };
593
594 assert_eq!(col.name, "test_column");
595 assert_eq!(col.sql_type, "INTEGER");
596 assert!(col.is_primary_key);
597 assert!(!col.is_nullable);
598 }
599
600 #[test]
601 fn test_column_info_with_foreign_key() {
602 let col = ColumnInfo {
603 name: "user_id",
604 sql_type: "UUID",
605 is_primary_key: false,
606 is_nullable: false,
607 create_time: false,
608 update_time: false,
609 unique: false,
610 index: false,
611 foreign_table: Some("User"),
612 foreign_key: Some("id"),
613 omit: false,
614 soft_delete: false,
615 };
616
617 assert_eq!(col.foreign_table, Some("User"));
618 assert_eq!(col.foreign_key, Some("id"));
619 }
620}