bottle-orm 0.5.9

A lightweight and simple ORM for Rust built on top of sqlx
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
//! # Model Module
//!
//! This module defines the core `Model` trait and associated structures for Bottle ORM.
//! It provides the interface that all database entities must implement, along with
//! metadata structures for describing table columns.
//!
//! ## Overview
//!
//! The `Model` trait is the foundation of Bottle ORM. It defines how Rust structs
//! map to database tables, including:
//!
//! - Table name resolution
//! - Column metadata (types, constraints, relationships)
//! - Serialization to/from database format
//!
//! ## Automatic Implementation
//!
//! The `Model` trait is typically implemented automatically via the `#[derive(Model)]`
//! procedural macro, which analyzes struct fields and `#[orm(...)]` attributes to
//! generate the necessary implementation.
//!
//! ## Example Usage
//!
//! ```rust,ignore
//! use bottle_orm::Model;
//! use uuid::Uuid;
//! use chrono::{DateTime, Utc};
//! use serde::{Deserialize, Serialize};
//! use sqlx::FromRow;
//!
//! #[derive(Model, Debug, Clone, Serialize, Deserialize, FromRow)]
//! struct User {
//!     #[orm(primary_key)]
//!     id: Uuid,
//!
//!     #[orm(size = 50, unique, index)]
//!     username: String,
//!
//!     #[orm(size = 100)]
//!     email: String,
//!
//!     age: Option<i32>,
//!
//!     #[orm(create_time)]
//!     created_at: DateTime<Utc>,
//! }
//!
//! #[derive(Model, Debug, Clone, Serialize, Deserialize, FromRow)]
//! struct Post {
//!     #[orm(primary_key)]
//!     id: Uuid,
//!
//!     #[orm(foreign_key = "User::id")]
//!     user_id: Uuid,
//!
//!     #[orm(size = 200)]
//!     title: String,
//!
//!     content: String,
//!
//!     #[orm(create_time)]
//!     created_at: DateTime<Utc>,
//! }
//! ```
//!
//! ## Supported ORM Attributes
//!
//! - `#[orm(primary_key)]` - Marks field as primary key
//! - `#[orm(unique)]` - Adds UNIQUE constraint
//! - `#[orm(index)]` - Creates database index
//! - `#[orm(size = N)]` - Sets VARCHAR size (for String fields)
//! - `#[orm(create_time)]` - Auto-populate with current timestamp on creation
//! - `#[orm(update_time)]` - Auto-update timestamp on modification (future feature)
//! - `#[orm(foreign_key = "Table::Column")]` - Defines foreign key relationship

// ============================================================================
// External Crate Imports
// ============================================================================

use std::collections::HashMap;
use futures::future::BoxFuture;
use crate::database::Connection;
use crate::query_builder::QueryBuilder;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelationType {
    HasOne,
    HasMany,
    BelongsTo,
}

#[derive(Debug, Clone)]
pub struct RelationInfo {
    pub name: &'static str,
    pub rel_type: RelationType,
    pub target_table: &'static str,
    pub foreign_key: &'static str,
    pub local_key: &'static str,
}

// ============================================================================
// Column Metadata Structure
// ============================================================================

/// Metadata information about a database column.
///
/// This structure contains all the information needed to generate SQL table
/// definitions and handle type conversions between Rust and SQL. It is populated
/// automatically by the `#[derive(Model)]` macro based on struct field types
/// and `#[orm(...)]` attributes.
///
/// # Fields
///
/// * `name` - Column name (field name from struct)
/// * `sql_type` - SQL type string (e.g., "INTEGER", "TEXT", "UUID", "TIMESTAMPTZ")
/// * `is_primary_key` - Whether this is the primary key column
/// * `is_nullable` - Whether NULL values are allowed (from Option<T>)
/// * `create_time` - Auto-populate with CURRENT_TIMESTAMP on insert
/// * `update_time` - Auto-update timestamp on modification (future feature)
/// * `unique` - Whether UNIQUE constraint should be added
/// * `index` - Whether to create an index on this column
/// * `foreign_table` - Name of referenced table (for foreign keys)
/// * `foreign_key` - Name of referenced column (for foreign keys)
///
/// # Example
///
/// ```rust,ignore
/// // For this field:
/// #[orm(size = 50, unique, index)]
/// username: String,
///
/// // The generated ColumnInfo would be:
/// ColumnInfo {
///     name: "username",
///     sql_type: "VARCHAR(50)",
///     is_primary_key: false,
///     is_nullable: false,
///     create_time: false,
///     update_time: false,
///     unique: true,
///     index: true,
///     foreign_table: None,
///     foreign_key: None,
/// }
/// ```
///
/// # SQL Type Mapping
///
/// The `sql_type` field contains the SQL type based on the Rust type:
///
/// - `i32` → `"INTEGER"`
/// - `i64` → `"BIGINT"`
/// - `String` → `"TEXT"` or `"VARCHAR(N)"` with size attribute
/// - `bool` → `"BOOLEAN"`
/// - `f64` → `"DOUBLE PRECISION"`
/// - `Uuid` → `"UUID"`
/// - `DateTime<Utc>` → `"TIMESTAMPTZ"`
/// - `NaiveDateTime` → `"TIMESTAMP"`
/// - `NaiveDate` → `"DATE"`
/// - `NaiveTime` → `"TIME"`
/// - `Option<T>` → Same as T, but `is_nullable = true`
#[derive(Debug, Clone)]
pub struct ColumnInfo {
    /// The column name in the database.
    ///
    /// This is derived from the struct field name and is typically converted
    /// to snake_case when generating SQL. The `r#` prefix is stripped if present
    /// (for Rust keywords used as field names).
    ///
    /// # Example
    /// ```rust,ignore
    /// // Field: user_id: i32
    /// name: "user_id"
    ///
    /// // Field: r#type: String (type is a Rust keyword)
    /// name: "r#type" // The r# will be stripped in SQL generation
    /// ```
    pub name: &'static str,

    /// The SQL type of the column (e.g., "TEXT", "INTEGER", "TIMESTAMPTZ").
    ///
    /// This string is used directly in CREATE TABLE statements. It must be
    /// a valid SQL type for the target database.
    ///
    /// # Example
    /// ```rust,ignore
    /// // i32 field
    /// sql_type: "INTEGER"
    ///
    /// // UUID field
    /// sql_type: "UUID"
    ///
    /// // String with size = 100
    /// sql_type: "VARCHAR(100)"
    /// ```
    pub sql_type: &'static str,

    /// Whether this column is a Primary Key.
    ///
    /// Set to `true` via `#[orm(primary_key)]` attribute. A table should have
    /// exactly one primary key column.
    ///
    /// # SQL Impact
    /// - Adds `PRIMARY KEY` constraint
    /// - Implicitly makes column `NOT NULL`
    /// - Creates a unique index automatically
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(primary_key)]
    /// id: Uuid,
    /// // is_primary_key: true
    /// ```
    pub is_primary_key: bool,

    /// Whether this column allows NULL values.
    ///
    /// Automatically set to `true` when the field type is `Option<T>`,
    /// otherwise `false` for non-optional types.
    ///
    /// # SQL Impact
    /// - `false`: Adds `NOT NULL` constraint
    /// - `true`: Allows NULL values
    ///
    /// # Example
    /// ```rust,ignore
    /// // Required field
    /// username: String,
    /// // is_nullable: false → NOT NULL
    ///
    /// // Optional field
    /// middle_name: Option<String>,
    /// // is_nullable: true → allows NULL
    /// ```
    pub is_nullable: bool,

    /// Whether this column should be automatically populated with the creation timestamp.
    ///
    /// Set via `#[orm(create_time)]` attribute. When `true`, the column gets
    /// a `DEFAULT CURRENT_TIMESTAMP` constraint.
    ///
    /// # SQL Impact
    /// - Adds `DEFAULT CURRENT_TIMESTAMP`
    /// - Column is auto-populated on INSERT
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(create_time)]
    /// created_at: DateTime<Utc>,
    /// // create_time: true
    /// // SQL: created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
    /// ```
    pub create_time: bool,

    /// Whether this column should be automatically updated on modification.
    ///
    /// Set via `#[orm(update_time)]` attribute. This is a **future feature**
    /// not yet fully implemented.
    ///
    /// # Future Implementation
    /// When implemented, this will:
    /// - Add database trigger or application-level update
    /// - Auto-update timestamp on every UPDATE
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(update_time)]
    /// updated_at: DateTime<Utc>,
    /// // update_time: true (future feature)
    /// ```
    pub update_time: bool,

    /// Whether this column has a UNIQUE constraint.
    ///
    /// Set via `#[orm(unique)]` attribute. Ensures no two rows can have
    /// the same value in this column (NULL values may be exempt depending
    /// on database).
    ///
    /// # SQL Impact
    /// - Adds `UNIQUE` constraint
    /// - Creates a unique index automatically
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(unique)]
    /// username: String,
    /// // unique: true
    /// // SQL: username VARCHAR(255) UNIQUE
    /// ```
    pub unique: bool,

    /// Whether an index should be created for this column.
    ///
    /// Set via `#[orm(index)]` attribute. Creates a database index to speed
    /// up queries that filter or sort by this column.
    ///
    /// # SQL Impact
    /// - Creates separate `CREATE INDEX` statement
    /// - Index name: `idx_{table}_{column}`
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(index)]
    /// email: String,
    /// // index: true
    /// // SQL: CREATE INDEX idx_user_email ON user (email)
    /// ```
    pub index: bool,

    /// The name of the foreign table, if this is a Foreign Key.
    ///
    /// Set via `#[orm(foreign_key = "Table::Column")]` attribute. Contains
    /// the name of the referenced table.
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(foreign_key = "User::id")]
    /// user_id: Uuid,
    /// // foreign_table: Some("User")
    /// ```
    pub foreign_table: Option<&'static str>,

    /// The name of the foreign column, if this is a Foreign Key.
    ///
    /// Set via `#[orm(foreign_key = "Table::Column")]` attribute. Contains
    /// the name of the referenced column in the foreign table.
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(foreign_key = "User::id")]
    /// user_id: Uuid,
    /// // foreign_key: Some("id")
    /// // SQL: FOREIGN KEY (user_id) REFERENCES user (id)
    /// ```
    pub foreign_key: Option<&'static str>,

    /// Whether this field should be omitted from queries by default.
    ///
    /// Set via `#[orm(omit)]` attribute. When `true`, this column will be
    /// excluded from query results unless explicitly selected.
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(omit)]
    /// password: String,
    /// // omit: true
    /// // This field will not be included in SELECT * queries
    /// ```
    pub omit: bool,

    /// Whether this field is used for soft delete functionality.
    ///
    /// Set via `#[orm(soft_delete)]` attribute. When `true`, this column
    /// will be used to track deletion timestamps. Queries will automatically
    /// filter out records where this column is not NULL.
    ///
    /// # Example
    /// ```rust,ignore
    /// #[orm(soft_delete)]
    /// deleted_at: Option<DateTime<Utc>>,
    /// // soft_delete: true
    /// // Records with deleted_at set will be excluded from queries
    /// ```
    pub soft_delete: bool,
}

// ============================================================================
// Model Trait
// ============================================================================

/// The core trait defining a Database Model (Table) in Bottle ORM.
///
/// This trait must be implemented by all structs that represent database tables.
/// It provides methods for retrieving table metadata, column information, and
/// converting instances to/from database format.
///
/// # Automatic Implementation
///
/// This trait is typically implemented automatically via the `#[derive(Model)]`
/// procedural macro. Manual implementation is possible but not recommended.
///
/// # Required Methods
///
/// * `table_name()` - Returns the table name
/// * `columns()` - Returns column metadata
/// * `active_columns()` - Returns column names
/// * `to_map()` - Serializes instance to a HashMap
///
/// # Example with Derive
///
/// ```rust,ignore
/// use bottle_orm::Model;
/// use uuid::Uuid;
///
/// #[derive(Model)]
/// struct User {
///     #[orm(primary_key)]
///     id: Uuid,
///     username: String,
///     age: i32,
/// }
///
/// // Now you can use:
/// assert_eq!(User::table_name(), "User");
/// assert_eq!(User::active_columns(), vec!["id", "username", "age"]);
/// ```
///
/// # Example Manual Implementation
///
/// ```rust,ignore
/// use bottle_orm::{Model, ColumnInfo};
/// use std::collections::HashMap;
///
/// struct CustomUser {
///     id: i32,
///     name: String,
/// }
///
/// impl Model for CustomUser {
///     fn table_name() -> &'static str {
///         "custom_users"
///     }
///
///     fn columns() -> Vec<ColumnInfo> {
///         vec![
///             ColumnInfo {
///                 name: "id",
///                 sql_type: "INTEGER",
///                 is_primary_key: true,
///                 is_nullable: false,
///                 create_time: false,
///                 update_time: false,
///                 unique: false,
///                 index: false,
///                 foreign_table: None,
///                 foreign_key: None,
///             },
///             ColumnInfo {
///                 name: "name",
///                 sql_type: "TEXT",
///                 is_primary_key: false,
///                 is_nullable: false,
///                 create_time: false,
///                 update_time: false,
///                 unique: false,
///                 index: false,
///                 foreign_table: None,
///                 foreign_key: None,
///             },
///         ]
///     }
///
///     fn active_columns() -> Vec<&'static str> {
///         vec!["id", "name"]
///     }
///
///     fn to_map(&self) -> HashMap<String, Option<String>> {
///         let mut map = HashMap::new();
///         map.insert("id".to_string(), Some(self.id.to_string()));
///         map.insert("name".to_string(), Some(self.name.clone()));
///         map
///     }/// }
/// ```
pub trait Model {
    /// Returns the table name associated with this model.
    ///
    /// The table name is derived from the struct name and is used in all
    /// SQL queries. By default, the derive macro uses the struct name as-is,
    /// which is then converted to snake_case when generating SQL.
    ///
    /// # Returns
    ///
    /// A static string slice containing the table name
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// #[derive(Model)]
    /// struct UserProfile {
    ///     // ...
    /// }
    ///
    /// // Returns "UserProfile"
    /// // SQL will use: "user_profile" (snake_case)
    /// assert_eq!(UserProfile::table_name(), "UserProfile");
    /// ```
    fn table_name() -> &'static str;

    /// Returns the list of column definitions for this model.
    ///
    /// This method provides complete metadata about each column, including
    /// SQL types, constraints, and relationships. The information is used
    /// for table creation, query building, and type conversion.
    ///
    /// # Returns
    ///
    /// A vector of `ColumnInfo` structs describing each column
    fn columns() -> Vec<ColumnInfo>;

    /// Returns the names of all columns in the model.
    ///
    /// # Returns
    ///
    /// A vector of Strings containing column names
    fn column_names() -> Vec<String>;

    /// Returns the names of active columns (struct fields).
    ///
    /// This method returns a simple list of column names without metadata.
    /// It's used for query building and SELECT statement generation.
    ///
    /// # Returns
    ///
    /// A vector of static string slices containing column names
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// #[derive(Model)]
    /// struct User {
    ///     #[orm(primary_key)]
    ///     id: Uuid,
    ///     username: String,
    ///     email: String,
    /// }
    ///
    /// assert_eq!(
    ///     User::active_columns(),
    ///     vec!["id", "username", "email"]
    /// );
    /// ```
    fn active_columns() -> Vec<&'static str>;

    /// Returns the list of relations for this model.
    ///
    /// This method provides metadata about the relationships defined in the model.
    ///
    /// # Returns
    ///
    /// A vector of `RelationInfo` structs describing each relation
    fn relations() -> Vec<RelationInfo> {
        Vec::new()
    }

    /// Loads a specific relation for a collection of models.
    ///
    /// This method is used by the Query Builder to implement eager loading (with).
    /// It should fetch the related records and inject them into the models.
    ///
    /// # Arguments
    ///
    /// * `relation_name` - The name of the relation to load
    /// * `models` - A mutable slice of model instances
    /// * `tx` - The database connection
    /// * `query_modifier` - An optional closure to modify the query
    fn load_relations<'a>(
        _relation_name: &'a str,
        _models: &'a mut [Self],
        _tx: &'a dyn Connection,
        _query_modifier: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
    ) -> BoxFuture<'a, Result<(), sqlx::Error>>
    where
        Self: Sized,
    {
        Box::pin(async move { Ok(()) })
    }

    /// Converts the model instance into a value map (Column Name → String Value).
    ///
    /// This method serializes the model instance into a HashMap where keys are
    /// column names and values are string representations. It's used primarily
    /// for INSERT operations.
    ///
    /// # Returns
    ///
    /// A HashMap mapping column names to string values
    ///
    /// # Type Conversion
    ///
    /// All values are converted to strings via the `ToString` trait:
    /// - Primitives: Direct conversion (e.g., `42` → `"42"`)
    /// - UUID: Hyphenated format (e.g., `"550e8400-e29b-41d4-a716-446655440000"`)
    /// - DateTime: RFC 3339 format
    /// - Option<T>: Only included if Some, omitted if None
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use uuid::Uuid;
    ///
    /// #[derive(Model)]
    /// struct User {
    ///     #[orm(primary_key)]
    ///     id: Uuid,
    ///     username: String,
    ///     age: i32,
    /// }
    ///
    /// let user = User {
    ///     id: Uuid::new_v4(),
    ///     username: "john_doe".to_string(),
    ///     age: 25,
    /// };
    ///
    /// let map = user.to_map();
    /// assert!(map.contains_key("id"));
    /// assert_eq!(map.get("username"), Some(&Some("john_doe".to_string())));
    /// assert_eq!(map.get("age"), Some(&Some("25".to_string())));
    /// ```
    fn to_map(&self) -> HashMap<String, Option<String>>;
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_column_info_creation() {
        let col = ColumnInfo {
            name: "test_column",
            sql_type: "INTEGER",
            is_primary_key: true,
            is_nullable: false,
            create_time: false,
            update_time: false,
            unique: false,
            index: false,
            foreign_table: None,
            foreign_key: None,
            omit: false,
            soft_delete: false,
        };

        assert_eq!(col.name, "test_column");
        assert_eq!(col.sql_type, "INTEGER");
        assert!(col.is_primary_key);
        assert!(!col.is_nullable);
    }

    #[test]
    fn test_column_info_with_foreign_key() {
        let col = ColumnInfo {
            name: "user_id",
            sql_type: "UUID",
            is_primary_key: false,
            is_nullable: false,
            create_time: false,
            update_time: false,
            unique: false,
            index: false,
            foreign_table: Some("User"),
            foreign_key: Some("id"),
            omit: false,
            soft_delete: false,
        };

        assert_eq!(col.foreign_table, Some("User"));
        assert_eq!(col.foreign_key, Some("id"));
    }
}