sqlmodel_core/model.rs
1//! Model trait for ORM-style struct mapping.
2//!
3//! The `Model` trait defines the contract for structs that can be
4//! mapped to database tables. It is typically derived using the
5//! `#[derive(Model)]` macro from `sqlmodel-macros`.
6
7use crate::Result;
8use crate::field::{FieldInfo, InheritanceInfo};
9use crate::relationship::RelationshipInfo;
10use crate::row::Row;
11use crate::value::Value;
12
13/// Behavior for handling extra fields during validation.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum ExtraFieldsBehavior {
16 /// Allow extra fields (ignore them).
17 #[default]
18 Ignore,
19 /// Forbid extra fields (return validation error).
20 Forbid,
21 /// Allow extra fields and preserve them.
22 Allow,
23}
24
25/// Model-level configuration matching Pydantic's model_config.
26///
27/// This struct holds configuration options that affect model behavior
28/// during validation, serialization, and database operations.
29///
30/// # Example
31///
32/// ```ignore
33/// #[derive(Model)]
34/// #[sqlmodel(
35/// table,
36/// from_attributes,
37/// validate_assignment,
38/// extra = "forbid",
39/// strict,
40/// populate_by_name,
41/// use_enum_values
42/// )]
43/// struct User {
44/// // ...
45/// }
46/// ```
47#[derive(Debug, Clone, Default)]
48pub struct ModelConfig {
49 /// Whether this model maps to a database table.
50 /// If true, DDL can be generated for this model.
51 pub table: bool,
52
53 /// Allow reading data from object attributes (ORM mode).
54 /// When true, model_validate can accept objects with attributes
55 /// in addition to dicts.
56 pub from_attributes: bool,
57
58 /// Validate field values when they are assigned.
59 /// When true, assignment to fields triggers validation.
60 pub validate_assignment: bool,
61
62 /// How to handle extra fields during validation.
63 pub extra: ExtraFieldsBehavior,
64
65 /// Enable strict type checking during validation.
66 /// When true, types must match exactly (no coercion).
67 pub strict: bool,
68
69 /// Allow populating fields by either name or alias.
70 /// When true, both the field name and any aliases are accepted
71 /// during deserialization.
72 pub populate_by_name: bool,
73
74 /// Use enum values instead of names during serialization.
75 /// When true, enum fields serialize to their underlying values
76 /// rather than variant names.
77 pub use_enum_values: bool,
78
79 /// Allow arbitrary types in fields.
80 /// When true, fields can use types that aren't natively supported
81 /// by the validation system.
82 pub arbitrary_types_allowed: bool,
83
84 /// Defer model validation to allow forward references.
85 /// When true, validation of field types is deferred until
86 /// the model is first used.
87 pub defer_build: bool,
88
89 /// Revalidate instances when converting to this model.
90 /// Controls whether existing model instances are revalidated.
91 pub revalidate_instances: bool,
92
93 /// Custom JSON schema extra data.
94 /// Additional data to include in generated JSON schema.
95 pub json_schema_extra: Option<&'static str>,
96
97 /// Title for JSON schema generation.
98 pub title: Option<&'static str>,
99}
100
101impl ModelConfig {
102 /// Create a new ModelConfig with all defaults.
103 pub const fn new() -> Self {
104 Self {
105 table: false,
106 from_attributes: false,
107 validate_assignment: false,
108 extra: ExtraFieldsBehavior::Ignore,
109 strict: false,
110 populate_by_name: false,
111 use_enum_values: false,
112 arbitrary_types_allowed: false,
113 defer_build: false,
114 revalidate_instances: false,
115 json_schema_extra: None,
116 title: None,
117 }
118 }
119
120 /// Create a config for a database table model.
121 pub const fn table() -> Self {
122 Self {
123 table: true,
124 from_attributes: false,
125 validate_assignment: false,
126 extra: ExtraFieldsBehavior::Ignore,
127 strict: false,
128 populate_by_name: false,
129 use_enum_values: false,
130 arbitrary_types_allowed: false,
131 defer_build: false,
132 revalidate_instances: false,
133 json_schema_extra: None,
134 title: None,
135 }
136 }
137}
138
139/// Trait for types that can be mapped to database tables.
140///
141/// This trait provides metadata about the table structure and
142/// methods for converting between Rust structs and database rows.
143///
144/// # Example
145///
146/// ```ignore
147/// use sqlmodel::Model;
148///
149/// #[derive(Model)]
150/// #[sqlmodel(table = "heroes")]
151/// struct Hero {
152/// #[sqlmodel(primary_key)]
153/// id: Option<i64>,
154/// name: String,
155/// secret_name: String,
156/// age: Option<i32>,
157/// }
158/// ```
159pub trait Model: Sized + Send + Sync {
160 /// The name of the database table.
161 const TABLE_NAME: &'static str;
162
163 /// The primary key column name(s).
164 const PRIMARY_KEY: &'static [&'static str];
165
166 /// Relationship metadata for this model.
167 ///
168 /// The derive macro will populate this for relationship fields; models with
169 /// no relationships can rely on the default empty slice.
170 const RELATIONSHIPS: &'static [RelationshipInfo] = &[];
171
172 /// Inheritance metadata for this model.
173 ///
174 /// Returns information about table inheritance if this model participates
175 /// in an inheritance hierarchy (single, joined, or concrete table).
176 /// The default implementation returns no inheritance.
177 fn inheritance() -> InheritanceInfo {
178 InheritanceInfo::none()
179 }
180
181 /// Get field metadata for all columns.
182 fn fields() -> &'static [FieldInfo];
183
184 /// Convert this model instance to a row of values.
185 fn to_row(&self) -> Vec<(&'static str, Value)>;
186
187 /// Construct a model instance from a database row.
188 #[allow(clippy::result_large_err)]
189 fn from_row(row: &Row) -> Result<Self>;
190
191 /// If this is a joined-table inheritance *child* model, return the base (parent) table row.
192 ///
193 /// This enables query builders to implement joined inheritance DML (base+child insert/update/delete)
194 /// without runtime reflection.
195 ///
196 /// The derive macro implements this automatically when a joined child declares exactly one
197 /// `#[sqlmodel(parent)]` embedded parent field. Non-joined models (and joined bases) return `None`.
198 #[must_use]
199 fn joined_parent_row(&self) -> Option<Vec<(&'static str, Value)>> {
200 None
201 }
202
203 /// Get the value of the primary key field(s).
204 fn primary_key_value(&self) -> Vec<Value>;
205
206 /// Check if this is a new record (primary key is None/default).
207 fn is_new(&self) -> bool;
208
209 /// Get the model configuration.
210 ///
211 /// Returns model-level configuration that affects validation,
212 /// serialization, and database operations.
213 fn model_config() -> ModelConfig {
214 ModelConfig::new()
215 }
216
217 /// The shard key field name for horizontal sharding.
218 ///
219 /// Returns `None` if the model doesn't use sharding. When set,
220 /// the sharded pool will use this field's value to determine
221 /// which shard to route queries to.
222 const SHARD_KEY: Option<&'static str> = None;
223
224 /// Get the shard key value for this model instance.
225 ///
226 /// Returns `None` if the model doesn't have a shard key defined.
227 /// The returned value is used by `ShardedPool` to determine the
228 /// appropriate shard for insert/update/delete operations.
229 fn shard_key_value(&self) -> Option<Value> {
230 None
231 }
232}
233
234/// Marker trait for models that support automatic ID generation.
235pub trait AutoIncrement: Model {
236 /// Set the auto-generated ID after insert.
237 fn set_id(&mut self, id: i64);
238}
239
240/// Trait for models that track creation/update timestamps.
241pub trait Timestamps: Model {
242 /// Set the created_at timestamp.
243 fn set_created_at(&mut self, timestamp: i64);
244
245 /// Set the updated_at timestamp.
246 fn set_updated_at(&mut self, timestamp: i64);
247}
248
249/// Trait for soft-deletable models.
250pub trait SoftDelete: Model {
251 /// Mark the model as deleted.
252 fn mark_deleted(&mut self);
253
254 /// Check if the model is deleted.
255 fn is_deleted(&self) -> bool;
256}
257
258/// Lifecycle event hooks for model instances.
259///
260/// Models can implement this trait to receive callbacks at various points
261/// in the persistence lifecycle: before/after insert, update, and delete.
262///
263/// All methods have default no-op implementations, so you only need to
264/// override the ones you care about.
265///
266/// # Example
267///
268/// ```ignore
269/// use sqlmodel_core::{Model, ModelEvents, Result};
270///
271/// #[derive(Model)]
272/// struct User {
273/// id: Option<i64>,
274/// name: String,
275/// created_at: Option<i64>,
276/// updated_at: Option<i64>,
277/// }
278///
279/// impl ModelEvents for User {
280/// fn before_insert(&mut self) -> Result<()> {
281/// let now = std::time::SystemTime::now()
282/// .duration_since(std::time::UNIX_EPOCH)
283/// .unwrap()
284/// .as_secs() as i64;
285/// self.created_at = Some(now);
286/// self.updated_at = Some(now);
287/// Ok(())
288/// }
289///
290/// fn before_update(&mut self) -> Result<()> {
291/// let now = std::time::SystemTime::now()
292/// .duration_since(std::time::UNIX_EPOCH)
293/// .unwrap()
294/// .as_secs() as i64;
295/// self.updated_at = Some(now);
296/// Ok(())
297/// }
298/// }
299/// ```
300pub trait ModelEvents: Model {
301 /// Called before a new instance is inserted into the database.
302 ///
303 /// Use this to set default values, validate data, or perform
304 /// any pre-insert logic. Return an error to abort the insert.
305 #[allow(unused_variables, clippy::result_large_err)]
306 fn before_insert(&mut self) -> Result<()> {
307 Ok(())
308 }
309
310 /// Called after an instance has been successfully inserted.
311 ///
312 /// The instance now has its auto-generated ID (if applicable).
313 /// Use this for post-insert notifications or logging.
314 #[allow(unused_variables, clippy::result_large_err)]
315 fn after_insert(&mut self) -> Result<()> {
316 Ok(())
317 }
318
319 /// Called before an existing instance is updated in the database.
320 ///
321 /// Use this to update timestamps, validate changes, or perform
322 /// any pre-update logic. Return an error to abort the update.
323 #[allow(unused_variables, clippy::result_large_err)]
324 fn before_update(&mut self) -> Result<()> {
325 Ok(())
326 }
327
328 /// Called after an instance has been successfully updated.
329 ///
330 /// Use this for post-update notifications or logging.
331 #[allow(unused_variables, clippy::result_large_err)]
332 fn after_update(&mut self) -> Result<()> {
333 Ok(())
334 }
335
336 /// Called before an instance is deleted from the database.
337 ///
338 /// Use this for cleanup, validation, or any pre-delete logic.
339 /// Return an error to abort the delete.
340 #[allow(unused_variables, clippy::result_large_err)]
341 fn before_delete(&mut self) -> Result<()> {
342 Ok(())
343 }
344
345 /// Called after an instance has been successfully deleted.
346 ///
347 /// Use this for post-delete notifications or logging.
348 #[allow(unused_variables, clippy::result_large_err)]
349 fn after_delete(&mut self) -> Result<()> {
350 Ok(())
351 }
352
353 /// Called after an instance has been loaded from the database.
354 ///
355 /// Use this to perform post-load initialization or validation.
356 #[allow(unused_variables, clippy::result_large_err)]
357 fn on_load(&mut self) -> Result<()> {
358 Ok(())
359 }
360
361 /// Called after an instance has been refreshed from the database.
362 ///
363 /// Use this to handle any logic needed after a refresh operation.
364 #[allow(unused_variables, clippy::result_large_err)]
365 fn on_refresh(&mut self) -> Result<()> {
366 Ok(())
367 }
368
369 /// Called when individual attributes are detected as changed.
370 ///
371 /// This is invoked during flush when the change tracker detects that
372 /// specific fields have been modified. Each change includes the field
373 /// name and the old/new values as JSON.
374 ///
375 /// Return an error to abort the flush.
376 ///
377 /// # Example
378 ///
379 /// ```ignore
380 /// impl ModelEvents for User {
381 /// fn on_attribute_change(&mut self, changes: &[AttributeChange]) -> Result<()> {
382 /// for change in changes {
383 /// if change.field_name == "email" {
384 /// // trigger verification
385 /// }
386 /// }
387 /// Ok(())
388 /// }
389 /// }
390 /// ```
391 #[allow(unused_variables, clippy::result_large_err)]
392 fn on_attribute_change(&mut self, changes: &[AttributeChange]) -> Result<()> {
393 Ok(())
394 }
395}
396
397/// Describes a single attribute change detected by the change tracker.
398#[derive(Debug, Clone)]
399pub struct AttributeChange {
400 /// The field name that changed.
401 pub field_name: &'static str,
402 /// The old value (as JSON).
403 pub old_value: serde_json::Value,
404 /// The new value (as JSON).
405 pub new_value: serde_json::Value,
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use crate::{FieldInfo, Row, SqlType, Value};
412
413 #[derive(Debug)]
414 struct TestModel;
415
416 impl Model for TestModel {
417 const TABLE_NAME: &'static str = "test_models";
418 const PRIMARY_KEY: &'static [&'static str] = &["id"];
419
420 fn fields() -> &'static [FieldInfo] {
421 static FIELDS: &[FieldInfo] =
422 &[FieldInfo::new("id", "id", SqlType::Integer).primary_key(true)];
423 FIELDS
424 }
425
426 fn to_row(&self) -> Vec<(&'static str, Value)> {
427 vec![]
428 }
429
430 fn from_row(_row: &Row) -> Result<Self> {
431 Ok(Self)
432 }
433
434 fn primary_key_value(&self) -> Vec<Value> {
435 vec![Value::from(1_i64)]
436 }
437
438 fn is_new(&self) -> bool {
439 false
440 }
441 }
442
443 #[test]
444 fn test_default_relationships_is_empty() {
445 assert!(TestModel::RELATIONSHIPS.is_empty());
446 }
447
448 // Test default ModelEvents implementation
449 impl ModelEvents for TestModel {}
450
451 #[test]
452 fn test_model_events_default_before_insert() {
453 let mut model = TestModel;
454 assert!(model.before_insert().is_ok());
455 }
456
457 #[test]
458 fn test_model_events_default_after_insert() {
459 let mut model = TestModel;
460 assert!(model.after_insert().is_ok());
461 }
462
463 #[test]
464 fn test_model_events_default_before_update() {
465 let mut model = TestModel;
466 assert!(model.before_update().is_ok());
467 }
468
469 #[test]
470 fn test_model_events_default_after_update() {
471 let mut model = TestModel;
472 assert!(model.after_update().is_ok());
473 }
474
475 #[test]
476 fn test_model_events_default_before_delete() {
477 let mut model = TestModel;
478 assert!(model.before_delete().is_ok());
479 }
480
481 #[test]
482 fn test_model_events_default_after_delete() {
483 let mut model = TestModel;
484 assert!(model.after_delete().is_ok());
485 }
486
487 #[test]
488 fn test_model_events_default_on_load() {
489 let mut model = TestModel;
490 assert!(model.on_load().is_ok());
491 }
492
493 #[test]
494 fn test_model_events_default_on_refresh() {
495 let mut model = TestModel;
496 assert!(model.on_refresh().is_ok());
497 }
498
499 // Test custom ModelEvents implementation that modifies state
500 #[derive(Debug)]
501 struct TimestampedModel {
502 id: Option<i64>,
503 created_at: i64,
504 updated_at: i64,
505 }
506
507 impl Model for TimestampedModel {
508 const TABLE_NAME: &'static str = "timestamped_models";
509 const PRIMARY_KEY: &'static [&'static str] = &["id"];
510
511 fn fields() -> &'static [FieldInfo] {
512 static FIELDS: &[FieldInfo] =
513 &[FieldInfo::new("id", "id", SqlType::Integer).primary_key(true)];
514 FIELDS
515 }
516
517 fn to_row(&self) -> Vec<(&'static str, Value)> {
518 vec![("id", self.id.map_or(Value::Null, Value::from))]
519 }
520
521 fn from_row(_row: &Row) -> Result<Self> {
522 Ok(Self {
523 id: Some(1),
524 created_at: 0,
525 updated_at: 0,
526 })
527 }
528
529 fn primary_key_value(&self) -> Vec<Value> {
530 vec![self.id.map_or(Value::Null, Value::from)]
531 }
532
533 fn is_new(&self) -> bool {
534 self.id.is_none()
535 }
536 }
537
538 impl ModelEvents for TimestampedModel {
539 fn before_insert(&mut self) -> Result<()> {
540 // Simulate setting created_at timestamp
541 self.created_at = 1000;
542 self.updated_at = 1000;
543 Ok(())
544 }
545
546 fn before_update(&mut self) -> Result<()> {
547 // Simulate updating updated_at timestamp
548 self.updated_at = 2000;
549 Ok(())
550 }
551 }
552
553 #[test]
554 fn test_model_events_custom_before_insert_sets_timestamps() {
555 let mut model = TimestampedModel {
556 id: None,
557 created_at: 0,
558 updated_at: 0,
559 };
560
561 assert_eq!(model.created_at, 0);
562 assert_eq!(model.updated_at, 0);
563
564 model.before_insert().unwrap();
565
566 assert_eq!(model.created_at, 1000);
567 assert_eq!(model.updated_at, 1000);
568 }
569
570 #[test]
571 fn test_model_events_custom_before_update_sets_timestamp() {
572 let mut model = TimestampedModel {
573 id: Some(1),
574 created_at: 1000,
575 updated_at: 1000,
576 };
577
578 model.before_update().unwrap();
579
580 // created_at should remain unchanged
581 assert_eq!(model.created_at, 1000);
582 // updated_at should be updated
583 assert_eq!(model.updated_at, 2000);
584 }
585
586 #[test]
587 fn test_model_events_custom_defaults_still_work() {
588 // Ensure overriding some methods doesn't break the defaults
589 let mut model = TimestampedModel {
590 id: Some(1),
591 created_at: 0,
592 updated_at: 0,
593 };
594
595 // These use default implementations
596 assert!(model.after_insert().is_ok());
597 assert!(model.after_update().is_ok());
598 assert!(model.before_delete().is_ok());
599 assert!(model.after_delete().is_ok());
600 assert!(model.on_load().is_ok());
601 assert!(model.on_refresh().is_ok());
602 }
603
604 // ==================== ModelConfig Tests ====================
605
606 #[test]
607 fn test_model_config_new_defaults() {
608 let config = ModelConfig::new();
609 assert!(!config.table);
610 assert!(!config.from_attributes);
611 assert!(!config.validate_assignment);
612 assert_eq!(config.extra, ExtraFieldsBehavior::Ignore);
613 assert!(!config.strict);
614 assert!(!config.populate_by_name);
615 assert!(!config.use_enum_values);
616 assert!(!config.arbitrary_types_allowed);
617 assert!(!config.defer_build);
618 assert!(!config.revalidate_instances);
619 assert!(config.json_schema_extra.is_none());
620 assert!(config.title.is_none());
621 }
622
623 #[test]
624 fn test_model_config_table_constructor() {
625 let config = ModelConfig::table();
626 assert!(config.table);
627 assert!(!config.from_attributes);
628 }
629
630 #[test]
631 fn test_extra_fields_behavior_default() {
632 let behavior = ExtraFieldsBehavior::default();
633 assert_eq!(behavior, ExtraFieldsBehavior::Ignore);
634 }
635
636 #[test]
637 fn test_model_default_config() {
638 // TestModel uses default implementation of model_config()
639 let config = TestModel::model_config();
640 assert!(!config.table);
641 assert!(!config.from_attributes);
642 }
643}