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}