lume 0.13.1

A simple and intuitive Query Builder inspired by Drizzle
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
#![warn(missing_docs)]

//! # Schema Module
//!
//! This module provides the core schema definition functionality for Lume.
//! It includes the schema trait, column definitions, and the powerful `define_schema!` macro.
//!
//! ## Key Components
//!
//! - [`Schema`] trait: Core trait for all database schemas
//! - [`Column<T>`]: Type-safe column definition with constraints
//! - [`Value`]: Enum for storing and converting between database values
//! - [`define_schema!`]: Macro for ergonomic schema definition
//!
//! ## Example
//!
//! ```no_run,ignore
//! use lume::define_schema;
//!
//! define_schema! {
//!     User {
//!         id: i32 [primary_key().not_null()],
//!         name: String [not_null()],
//!         email: String [unique()],
//!         age: i32,
//!         active: bool [default_value(true)],
//!     }
//! }
//! ```

mod column;
mod constraints;
mod default;
mod macros;
mod uuid;
mod validators;
mod value;

use std::collections::HashMap;
use std::marker::PhantomData;

pub use crate::schema::constraints::ColumnConstraint;
pub use crate::schema::constraints::GeneratedColumn;
pub use crate::schema::default::DefaultToSql;
pub use crate::schema::default::DefaultValueEnum;
pub use crate::schema::validators::ColumnValidators;
use crate::table::TableDefinition;
pub use column::Column;
use std::fmt::Debug;
pub use uuid::Uuid;
pub use value::Value;
pub use value::convert_to_value;

/// Core trait that all database schemas must implement.
///
/// This trait provides the interface for schema registration, column retrieval,
/// and table metadata. It's automatically implemented by the `define_schema!` macro.
///
/// # Example
///
/// ```rust
/// use lume::define_schema;
/// use lume::schema::{Schema, ColumnInfo};
///
/// define_schema! {
///     Product {
///         id: i32 [primary_key()],
///         name: String [not_null()],
///     }
/// }
///
/// // The Schema trait is automatically implemented
/// assert_eq!(Product::table_name(), "Product");
/// let columns = Product::get_all_columns();
/// ```
pub trait Schema {
    /// Returns the table name for this schema.
    ///
    /// This is used for SQL generation and table registry.
    fn table_name() -> &'static str;

    /// Returns metadata for all columns in this schema.
    ///
    /// This includes column names, types, constraints, and other metadata
    /// needed for SQL generation and type checking.
    fn get_all_columns() -> Vec<ColumnInfo<'static>>;

    /// Ensures the schema is registered in the table registry.
    ///
    /// This method is idempotent and can be called multiple times safely.
    /// It's automatically called when using the generated schema methods.
    fn ensure_registered();

    /// Returns a map of column names to their corresponding values for this schema instance.
    ///
    /// This method is used to extract the values of all fields in the schema as a
    /// `HashMap<String, Value>`, where each key is the column name and each value is
    /// the associated database value. This is primarily used for insert and update
    /// operations to serialize the struct into a form suitable for database interaction.
    fn values(&self) -> HashMap<String, Value>;
}

/// Trait for extracting updated column values for SQL UPDATE operations.
///
/// The `UpdateTrait` trait is implemented by update structs generated by the
/// `define_schema!` macro. It provides a method to retrieve a vector of
/// column-value pairs representing the fields to be updated in the database.
pub trait UpdateTrait {
    /// Returns a vector of (column name, value) pairs to be updated.
    ///
    /// Only fields that are set (e.g., `Some(value)`) should be included.
    fn get_updated(self) -> Vec<(&'static str, Value)>;
}

/// Trait for specifying column selection in queries.
///
/// The `Select` trait is used to define which columns should be included
/// in a SQL SELECT statement. It is typically implemented by selection
/// structs generated by the `define_schema!` macro, and is used by the
/// query builder to determine which columns to fetch from the database.
///
/// # Example
///
/// ```rust
/// use lume::define_schema;
/// use lume::schema::Select;
/// use lume::schema::ColumnInfo;
/// use lume::schema::Schema;
///
/// define_schema! {
///     User {
///         id: i32 [primary_key()],
///         name: String [not_null()],
///         age: i32,
///     }
/// }
///
/// // The macro generates a selection struct (e.g., SelectUser) that implements Select.
/// // You can use it to specify which columns to select in a query.
/// // let selection = SelectUser::default();
/// // let columns = selection.get_selected();
/// ```
pub trait Select {
    /// Returns the default selection schema.
    ///
    /// This is typically all columns, but can be customized.
    fn default() -> Self;

    /// Returns a vector of column names to be selected.
    ///
    /// This determines which columns will be included in the SQL SELECT clause.
    fn get_selected(self) -> Vec<&'static str>;
}

/// Metadata information for a database column.
///
/// This struct contains all the necessary information about a column
/// for SQL generation, type checking, and constraint validation.
///
/// # Fields
///
/// - `name`: The column name in the database
/// - `data_type`: The SQL data type (e.g., "INTEGER", "VARCHAR(255)")
/// - `nullable`: Whether the column allows NULL values
/// - `unique`: Whether the column has a UNIQUE constraint
/// - `primary_key`: Whether the column is a primary key
/// - `indexed`: Whether the column has an index
/// - `has_default`: Whether the column has a default value
/// - `default_sql`: The SQL representation of the default value
#[derive(Debug, Clone)]
pub struct ColumnInfo<'a> {
    /// The column name in the database
    pub name: &'static str,
    /// The SQL data type (e.g., "INTEGER", "VARCHAR(255)")
    pub data_type: &'static str,
    /// Whether the column allows NULL values
    pub has_default: bool,
    /// The SQL representation of the default value
    pub default_sql: Option<DefaultValueEnum<String>>,
    /// Optional column comment (MySQL COMMENT)
    pub comment: Option<&'static str>,
    /// Optional character set (MySQL CHARACTER SET)
    pub charset: Option<&'static str>,
    /// Optional collation (MySQL COLLATE)
    pub collate: Option<&'static str>,
    /// Validators applied to this column's values at runtime.
    pub validators: &'a Vec<ColumnValidators>,
    /// Constraints applied to this column (e.g., NOT NULL, UNIQUE, PRIMARY KEY).
    pub constraints: &'a Vec<ColumnConstraint>,
}

/// Converts a Rust type to its corresponding SQL type string.
///
/// This function provides the mapping between Rust types and SQL column types
/// used in database schema generation.
///
/// # Supported Types
///
/// - `String` → `"VARCHAR(255)"`
/// - `i8` → `"TINYINT"`
/// - `i16` → `"SMALLINT"`
/// - `i32` → `"INTEGER"`
/// - `i64` → `"BIGINT"`
/// - `u8` → `"TINYINT UNSIGNED"`
/// - `u16` → `"SMALLINT UNSIGNED"`
/// - `u32` → `"INTEGER UNSIGNED"`
/// - `u64` → `"BIGINT UNSIGNED"`
/// - `f32` → `"FLOAT"`
/// - `f64` → `"DOUBLE"`
/// - `bool` → `"BOOLEAN"`
/// - All other types → `"TEXT"` (fallback)
///
/// # Example
///
/// ```rust
/// use lume::schema::type_to_sql_string;
///
/// assert_eq!(type_to_sql_string::<String>(), "VARCHAR(255)");
/// assert_eq!(type_to_sql_string::<i32>(), "INT");
/// assert_eq!(type_to_sql_string::<i64>(), "BIGINT");
/// assert_eq!(type_to_sql_string::<u64>(), "BIGINT UNSIGNED");
/// assert_eq!(type_to_sql_string::<bool>(), "BOOLEAN");
/// ```
pub fn type_to_sql_string<T: 'static>() -> &'static str {
    use std::any::TypeId;

    let type_id = TypeId::of::<T>();

    #[cfg(feature = "postgres")]
    {
        if type_id == TypeId::of::<Vec<String>>() {
            return "TEXT[]";
        } else if type_id == TypeId::of::<Vec<bool>>() {
            return "BOOLEAN[]";
        } else if type_id == TypeId::of::<Vec<i8>>() || type_id == TypeId::of::<Vec<i16>>() {
            return "SMALLINT[]";
        } else if type_id == TypeId::of::<Vec<i32>>()
            || type_id == TypeId::of::<Vec<u16>>()
            || type_id == TypeId::of::<Vec<u32>>()
        {
            return "INT[]";
        } else if type_id == TypeId::of::<Vec<i64>>() || type_id == TypeId::of::<Vec<u64>>() {
            return "BIGINT[]";
        } else if type_id == TypeId::of::<Vec<f32>>() {
            return "REAL[]";
        } else if type_id == TypeId::of::<Vec<f64>>() {
            return "DOUBLE PRECISION[]";
        }
    }

    #[cfg(any(feature = "mysql", feature = "sqlite"))]
    {
        if type_id == TypeId::of::<Vec<String>>()
            || type_id == TypeId::of::<Vec<bool>>()
            || type_id == TypeId::of::<Vec<i8>>()
            || type_id == TypeId::of::<Vec<i16>>()
            || type_id == TypeId::of::<Vec<i32>>()
            || type_id == TypeId::of::<Vec<i64>>()
            || type_id == TypeId::of::<Vec<u8>>()
            || type_id == TypeId::of::<Vec<u16>>()
            || type_id == TypeId::of::<Vec<u32>>()
            || type_id == TypeId::of::<Vec<u64>>()
            || type_id == TypeId::of::<Vec<f32>>()
            || type_id == TypeId::of::<Vec<f64>>()
        {
            return "JSON";
        }
    }

    if type_id == TypeId::of::<crate::schema::Uuid>() {
        #[cfg(feature = "postgres")]
        {
            return "UUID";
        }
        #[cfg(any(feature = "mysql", feature = "sqlite"))]
        {
            return "CHAR(36)";
        }
    } else if type_id == TypeId::of::<String>() {
        "VARCHAR(255)"
    } else if type_id == TypeId::of::<i8>() {
        "TINYINT"
    } else if type_id == TypeId::of::<i16>() {
        "SMALLINT"
    } else if type_id == TypeId::of::<i32>() {
        "INT"
    } else if type_id == TypeId::of::<i64>() {
        "BIGINT"
    } else if type_id == TypeId::of::<u8>() {
        "TINYINT UNSIGNED"
    } else if type_id == TypeId::of::<u16>() {
        "SMALLINT UNSIGNED"
    } else if type_id == TypeId::of::<u32>() {
        "INT UNSIGNED"
    } else if type_id == TypeId::of::<u64>() {
        "BIGINT UNSIGNED"
    } else if type_id == TypeId::of::<f32>() {
        "FLOAT"
    } else if type_id == TypeId::of::<f64>() {
        "DOUBLE"
    } else if type_id == TypeId::of::<bool>() {
        "BOOLEAN"
    } else if type_id == TypeId::of::<time::Date>() {
        "DATE"
    } else if type_id == TypeId::of::<time::OffsetDateTime>() {
        "DATETIME"
    } else {
        "VARCHAR(255)" // fallback
    }
}

/// A wrapper around a schema type that implements [`TableDefinition`].
///
/// This struct is used internally to bridge between the [`Schema`] trait
/// and the [`TableDefinition`] trait for table registry and SQL generation.
///
/// # Type Parameters
///
/// - `T`: The schema type that implements [`Schema`]
#[derive(Debug)]
pub(crate) struct SchemaWrapper<T: Schema + Debug> {
    _phantom: PhantomData<T>,
}

// Implement Clone for SchemaWrapper<T>
impl<T: Schema + Debug> Clone for SchemaWrapper<T> {
    fn clone(&self) -> Self {
        Self {
            _phantom: PhantomData,
        }
    }
}

impl<T: Schema + Debug> SchemaWrapper<T> {
    /// Creates a new `SchemaWrapper` instance.
    pub(crate) fn new() -> Self {
        Self {
            _phantom: PhantomData,
        }
    }
}

impl<T: Schema + Debug + Sync + Send + 'static> TableDefinition for SchemaWrapper<T> {
    fn table_name(&self) -> &'static str {
        T::table_name()
    }

    fn get_columns(&self) -> Vec<ColumnInfo<'static>> {
        T::get_all_columns()
    }

    fn to_create_sql(&self) -> String {
        let table_name = self.table_name();
        let columns = self.get_columns();

        let mut sql = format!("CREATE TABLE IF NOT EXISTS {} (\n", table_name);

        let column_definitions: Vec<String> = columns
            .iter()
            .map(|col| {
                let mut def = format!("    {} {}", col.name, col.data_type);
                let constraints = col.constraints;

                for constraint in constraints {
                    match constraint {
                        ColumnConstraint::NonNullable => {
                            def.push_str(" NOT NULL");
                        }
                        ColumnConstraint::Unique => {
                            def.push_str(" UNIQUE");
                        }
                        ColumnConstraint::PrimaryKey => {
                            def.push_str(" PRIMARY KEY");
                        }
                        ColumnConstraint::Indexed => {}
                        ColumnConstraint::AutoIncrement => {
                            if is_mysql_integer_type(col.data_type) {
                                def.push_str(" AUTO_INCREMENT");
                            }
                        }
                        ColumnConstraint::Invisible => {
                            def.push_str(" INVISIBLE");
                        }
                        ColumnConstraint::OnUpdateCurrentTimestamp => {
                            def.push_str(" ON UPDATE CURRENT_TIMESTAMP");
                        }
                        ColumnConstraint::Check(expression) => {
                            def.push_str(&format!(" CHECK ({})", expression));
                        }
                        ColumnConstraint::Generated(generated) => {
                            def.push_str(&format!(" GENERATED {}", generated));
                        }
                    }
                }

                if col.comment.is_some() {
                    let escaped = col.comment.unwrap().replace("'", "''");
                    def.push_str(&format!(" COMMENT '{}'", escaped));
                }

                if col.charset.is_some() {
                    def.push_str(&format!(" CHARACTER SET {}", col.charset.unwrap()));
                }

                if col.collate.is_some() {
                    def.push_str(&format!(" COLLATE {}", col.collate.unwrap()));
                }

                if col.has_default {
                    if let Some(ref default) = col.default_sql {
                        if let DefaultValueEnum::Value(default) = default {
                            // Skip empty string defaults for primary keys
                            let is_empty_string = default == "" || default == "''";
                            let is_primary_key =
                                col.constraints.contains(&ColumnConstraint::PrimaryKey);

                            if is_primary_key && is_empty_string {
                                // Skip default for primary keys with empty string
                            } else {
                                // Add quotes for string default values if not already quoted
                                let needs_quotes = col.data_type == "TEXT"
                                    || col.data_type.starts_with("VARCHAR")
                                    || col.data_type == "CHAR"
                                    || col.data_type == "STRING"
                                    || col.data_type == "UUID";
                                if needs_quotes
                                    && !(default.starts_with('\'') && default.ends_with('\''))
                                {
                                    def.push_str(&format!(
                                        " DEFAULT '{}'",
                                        default.replace('\'', "''")
                                    ));
                                } else {
                                    def.push_str(&format!(" DEFAULT {}", default));
                                }
                            }
                        } else if &DefaultValueEnum::CurrentTimestamp == default {
                            def.push_str(" DEFAULT CURRENT_TIMESTAMP");
                        } else if &DefaultValueEnum::Random == default {
                            def.push_str(" DEFAULT (UUID())");
                        }
                    }
                }

                def
            })
            .collect();

        sql.push_str(&column_definitions.join(",\n"));
        sql.push_str("\n);");

        // Add indexes
        let indexes: Vec<String> = columns
            .iter()
            .filter(|col| {
                col.constraints.contains(&ColumnConstraint::Indexed)
                    && !col.constraints.contains(&ColumnConstraint::PrimaryKey)
            })
            .map(|col| {
                format!(
                    "CREATE INDEX idx_{}_{} ON {} ({});",
                    table_name, col.name, table_name, col.name
                )
            })
            .collect();

        if !indexes.is_empty() {
            sql.push_str("\n\n");
            sql.push_str(&indexes.join("\n"));
        }

        sql
    }

    fn clone_box(&self) -> Box<dyn TableDefinition> {
        Box::new(self.clone())
    }
}

/// Returns true if a MySQL data type string represents an integer type.
fn is_mysql_integer_type(data_type: &str) -> bool {
    match data_type {
        // Signed
        "TINYINT" | "SMALLINT" | "MEDIUMINT" | "INT" | "INTEGER" | "BIGINT" |
        // Unsigned variants may appear with a space and suffix
        "TINYINT UNSIGNED" | "SMALLINT UNSIGNED" | "MEDIUMINT UNSIGNED" |
        "INT UNSIGNED" | "INTEGER UNSIGNED" | "BIGINT UNSIGNED" => true,
        _ => false,
    }
}

/// Marker trait for user-defined types that should use generic `DefaultToSql`.
///
/// Users can implement this trait for their custom enums or types to enable
/// default value support in schema definitions. Types implementing this trait
/// must also implement `ToString` to provide SQL string representation.
pub trait CustomSqlType {}