Skip to main content

sqlmodel_core/
field.rs

1//! Field and column definitions.
2
3use crate::types::SqlType;
4
5/// Referential action for foreign key constraints (ON DELETE / ON UPDATE).
6///
7/// These define what happens to referencing rows when the referenced row is
8/// deleted or updated.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ReferentialAction {
11    /// No action - raise error if any references exist.
12    /// This is the default and most restrictive option.
13    #[default]
14    NoAction,
15    /// Restrict - same as NO ACTION (alias for compatibility).
16    Restrict,
17    /// Cascade - automatically delete/update referencing rows.
18    Cascade,
19    /// Set null - set referencing columns to NULL.
20    SetNull,
21    /// Set default - set referencing columns to their default values.
22    SetDefault,
23}
24
25impl ReferentialAction {
26    /// Get the SQL representation of this action.
27    #[must_use]
28    pub const fn as_sql(&self) -> &'static str {
29        match self {
30            ReferentialAction::NoAction => "NO ACTION",
31            ReferentialAction::Restrict => "RESTRICT",
32            ReferentialAction::Cascade => "CASCADE",
33            ReferentialAction::SetNull => "SET NULL",
34            ReferentialAction::SetDefault => "SET DEFAULT",
35        }
36    }
37
38    /// Parse a referential action from a string (case-insensitive).
39    ///
40    /// Returns `None` if the string is not a recognized action.
41    #[must_use]
42    pub fn from_str(s: &str) -> Option<Self> {
43        match s.to_uppercase().as_str() {
44            "NO ACTION" | "NOACTION" | "NO_ACTION" => Some(ReferentialAction::NoAction),
45            "RESTRICT" => Some(ReferentialAction::Restrict),
46            "CASCADE" => Some(ReferentialAction::Cascade),
47            "SET NULL" | "SETNULL" | "SET_NULL" => Some(ReferentialAction::SetNull),
48            "SET DEFAULT" | "SETDEFAULT" | "SET_DEFAULT" => Some(ReferentialAction::SetDefault),
49            _ => None,
50        }
51    }
52}
53
54/// Metadata about a model field/column.
55#[derive(Debug, Clone)]
56pub struct FieldInfo {
57    /// Rust field name
58    pub name: &'static str,
59    /// Database column name (may differ from field name)
60    pub column_name: &'static str,
61    /// SQL type for this field
62    pub sql_type: SqlType,
63    /// Explicit SQL type override string (e.g., "VARCHAR(255)", "DECIMAL(10,2)")
64    /// When set, this takes precedence over `sql_type` in DDL generation.
65    pub sql_type_override: Option<&'static str>,
66    /// Precision for DECIMAL/NUMERIC types (total digits)
67    pub precision: Option<u8>,
68    /// Scale for DECIMAL/NUMERIC types (digits after decimal point)
69    pub scale: Option<u8>,
70    /// Whether this field is nullable
71    pub nullable: bool,
72    /// Whether this is a primary key
73    pub primary_key: bool,
74    /// Whether this field auto-increments
75    pub auto_increment: bool,
76    /// Whether this field has a unique constraint
77    pub unique: bool,
78    /// Default value expression (SQL)
79    pub default: Option<&'static str>,
80    /// Foreign key reference (table.column)
81    pub foreign_key: Option<&'static str>,
82    /// Referential action for ON DELETE (only valid with foreign_key)
83    pub on_delete: Option<ReferentialAction>,
84    /// Referential action for ON UPDATE (only valid with foreign_key)
85    pub on_update: Option<ReferentialAction>,
86    /// Index name if indexed
87    pub index: Option<&'static str>,
88    /// Alias for both input and output (like serde rename).
89    /// When set, this name is used instead of `name` for serialization/deserialization.
90    pub alias: Option<&'static str>,
91    /// Alias used only during deserialization/validation (input-only).
92    /// Accepts this name as an alternative to `name` or `alias` during parsing.
93    pub validation_alias: Option<&'static str>,
94    /// Alias used only during serialization (output-only).
95    /// Overrides `alias` when outputting the field name.
96    pub serialization_alias: Option<&'static str>,
97    /// Whether this is a computed field (not stored in database).
98    /// Computed fields are excluded from database operations but included
99    /// in serialization (model_dump) unless exclude_computed_fields is set.
100    pub computed: bool,
101    /// Whether to exclude this field from serialization (model_dump).
102    /// When true, the field will never appear in serialized output.
103    pub exclude: bool,
104    /// Schema title for JSON Schema generation.
105    /// Used as the "title" property in the generated JSON Schema.
106    pub title: Option<&'static str>,
107    /// Schema description for JSON Schema generation.
108    /// Used as the "description" property in the generated JSON Schema.
109    pub description: Option<&'static str>,
110    /// Extra JSON Schema properties (as JSON string, merged into schema).
111    /// The string should be valid JSON that will be merged into the field's schema.
112    pub schema_extra: Option<&'static str>,
113    /// JSON representation of the field's default value (for exclude_defaults).
114    /// When set, model_dump with exclude_defaults=true will compare the current
115    /// value against this and exclude the field if they match.
116    pub default_json: Option<&'static str>,
117    /// Whether this field has a default value (used for exclude_unset tracking).
118    /// Fields with defaults can be distinguished from fields that were explicitly set.
119    pub has_default: bool,
120    /// Whether this field is constant (immutable after creation).
121    /// Const fields cannot be modified after initial construction.
122    /// This is enforced at validation/session level, not compile-time.
123    pub const_field: bool,
124    /// Additional SQL constraints for DDL generation (e.g., CHECK constraints).
125    /// Each string is a SQL constraint expression that will be added to the column definition.
126    pub column_constraints: &'static [&'static str],
127    /// SQL comment for the column (used in DDL generation).
128    /// This maps to the COMMENT ON COLUMN or inline COMMENT clause depending on the database.
129    pub column_comment: Option<&'static str>,
130    /// Extra metadata as JSON string (for custom extensions/info).
131    /// This can be used to store additional information that doesn't fit in other fields.
132    pub column_info: Option<&'static str>,
133    /// SQL expression for hybrid properties.
134    ///
135    /// When set, this field is a hybrid property: it has both a Rust-side computed
136    /// value and a SQL expression that can be used in queries (WHERE, ORDER BY, etc.).
137    /// The macro generates a `{field}_expr()` associated function that returns
138    /// `Expr::raw(this_sql)`.
139    pub hybrid_sql: Option<&'static str>,
140    /// Discriminator field name for union types.
141    ///
142    /// When this field contains a union/enum type, the discriminator specifies
143    /// which field within the union variants is used to determine the type.
144    /// This maps to Pydantic's `Field(discriminator='field_name')`.
145    ///
146    /// In Rust, discriminated unions are typically handled by serde's
147    /// `#[serde(tag = "field_name")]` attribute on the enum. This field
148    /// stores the discriminator info for:
149    /// - JSON Schema generation (OpenAPI discriminator)
150    /// - Documentation purposes
151    /// - Runtime validation hints
152    ///
153    /// Example:
154    /// ```ignore
155    /// #[derive(Model)]
156    /// struct Owner {
157    ///     #[sqlmodel(discriminator = "pet_type")]
158    ///     pet: PetUnion, // PetUnion should have #[serde(tag = "pet_type")]
159    /// }
160    /// ```
161    pub discriminator: Option<&'static str>,
162}
163
164impl FieldInfo {
165    /// Create a new field info with minimal required data.
166    pub const fn new(name: &'static str, column_name: &'static str, sql_type: SqlType) -> Self {
167        Self {
168            name,
169            column_name,
170            sql_type,
171            sql_type_override: None,
172            precision: None,
173            scale: None,
174            nullable: false,
175            primary_key: false,
176            auto_increment: false,
177            unique: false,
178            default: None,
179            foreign_key: None,
180            on_delete: None,
181            on_update: None,
182            index: None,
183            alias: None,
184            validation_alias: None,
185            serialization_alias: None,
186            computed: false,
187            exclude: false,
188            title: None,
189            description: None,
190            schema_extra: None,
191            default_json: None,
192            has_default: false,
193            const_field: false,
194            column_constraints: &[],
195            column_comment: None,
196            column_info: None,
197            hybrid_sql: None,
198            discriminator: None,
199        }
200    }
201
202    /// Set the database column name.
203    pub const fn column(mut self, name: &'static str) -> Self {
204        self.column_name = name;
205        self
206    }
207
208    /// Set explicit SQL type override.
209    ///
210    /// When set, this string will be used directly in DDL generation instead
211    /// of the `sql_type.sql_name()`. Use this for database-specific types like
212    /// `VARCHAR(255)`, `DECIMAL(10,2)`, `TINYINT UNSIGNED`, etc.
213    pub const fn sql_type_override(mut self, type_str: &'static str) -> Self {
214        self.sql_type_override = Some(type_str);
215        self
216    }
217
218    /// Set SQL type override from optional.
219    pub const fn sql_type_override_opt(mut self, type_str: Option<&'static str>) -> Self {
220        self.sql_type_override = type_str;
221        self
222    }
223
224    /// Set precision for DECIMAL/NUMERIC types.
225    ///
226    /// Precision is the total number of digits (before and after decimal point).
227    /// Typical range: 1-38, depends on database.
228    ///
229    /// # Example
230    ///
231    /// ```ignore
232    /// // DECIMAL(10, 2) - 10 total digits, 2 after decimal
233    /// FieldInfo::new("price", "price", SqlType::Decimal { precision: 10, scale: 2 })
234    ///     .precision(10)
235    ///     .scale(2)
236    /// ```
237    pub const fn precision(mut self, value: u8) -> Self {
238        self.precision = Some(value);
239        self
240    }
241
242    /// Set precision from optional.
243    pub const fn precision_opt(mut self, value: Option<u8>) -> Self {
244        self.precision = value;
245        self
246    }
247
248    /// Set scale for DECIMAL/NUMERIC types.
249    ///
250    /// Scale is the number of digits after the decimal point.
251    /// Must be less than or equal to precision.
252    pub const fn scale(mut self, value: u8) -> Self {
253        self.scale = Some(value);
254        self
255    }
256
257    /// Set scale from optional.
258    pub const fn scale_opt(mut self, value: Option<u8>) -> Self {
259        self.scale = value;
260        self
261    }
262
263    /// Set both precision and scale for DECIMAL/NUMERIC types.
264    ///
265    /// # Example
266    ///
267    /// ```ignore
268    /// // DECIMAL(10, 2) for currency
269    /// FieldInfo::new("price", "price", SqlType::Decimal { precision: 10, scale: 2 })
270    ///     .decimal_precision(10, 2)
271    /// ```
272    pub const fn decimal_precision(mut self, precision: u8, scale: u8) -> Self {
273        self.precision = Some(precision);
274        self.scale = Some(scale);
275        self
276    }
277
278    /// Get the effective SQL type name for DDL generation.
279    ///
280    /// Priority:
281    /// 1. `sql_type_override` if set
282    /// 2. For DECIMAL/NUMERIC: uses `precision` and `scale` fields if set
283    /// 3. Falls back to `sql_type.sql_name()`
284    #[must_use]
285    pub fn effective_sql_type(&self) -> String {
286        // sql_type_override takes highest precedence
287        if let Some(override_str) = self.sql_type_override {
288            return override_str.to_string();
289        }
290
291        // For Decimal/Numeric types, use precision/scale fields if available
292        match self.sql_type {
293            SqlType::Decimal { .. } | SqlType::Numeric { .. } => {
294                if let (Some(p), Some(s)) = (self.precision, self.scale) {
295                    let type_name = if matches!(self.sql_type, SqlType::Decimal { .. }) {
296                        "DECIMAL"
297                    } else {
298                        "NUMERIC"
299                    };
300                    return format!("{}({}, {})", type_name, p, s);
301                }
302            }
303            _ => {}
304        }
305
306        // Fall back to sql_type's own name generation
307        self.sql_type.sql_name()
308    }
309
310    /// Set nullable flag.
311    pub const fn nullable(mut self, value: bool) -> Self {
312        self.nullable = value;
313        self
314    }
315
316    /// Set primary key flag.
317    pub const fn primary_key(mut self, value: bool) -> Self {
318        self.primary_key = value;
319        self
320    }
321
322    /// Set auto-increment flag.
323    pub const fn auto_increment(mut self, value: bool) -> Self {
324        self.auto_increment = value;
325        self
326    }
327
328    /// Set unique flag.
329    pub const fn unique(mut self, value: bool) -> Self {
330        self.unique = value;
331        self
332    }
333
334    /// Set default value.
335    pub const fn default(mut self, expr: &'static str) -> Self {
336        self.default = Some(expr);
337        self
338    }
339
340    /// Set default value from optional.
341    pub const fn default_opt(mut self, expr: Option<&'static str>) -> Self {
342        self.default = expr;
343        self
344    }
345
346    /// Set foreign key reference.
347    pub const fn foreign_key(mut self, reference: &'static str) -> Self {
348        self.foreign_key = Some(reference);
349        self
350    }
351
352    /// Set foreign key reference from optional.
353    pub const fn foreign_key_opt(mut self, reference: Option<&'static str>) -> Self {
354        self.foreign_key = reference;
355        self
356    }
357
358    /// Set ON DELETE action for foreign key.
359    ///
360    /// This is only meaningful when `foreign_key` is also set.
361    pub const fn on_delete(mut self, action: ReferentialAction) -> Self {
362        self.on_delete = Some(action);
363        self
364    }
365
366    /// Set ON DELETE action from optional.
367    pub const fn on_delete_opt(mut self, action: Option<ReferentialAction>) -> Self {
368        self.on_delete = action;
369        self
370    }
371
372    /// Set ON UPDATE action for foreign key.
373    ///
374    /// This is only meaningful when `foreign_key` is also set.
375    pub const fn on_update(mut self, action: ReferentialAction) -> Self {
376        self.on_update = Some(action);
377        self
378    }
379
380    /// Set ON UPDATE action from optional.
381    pub const fn on_update_opt(mut self, action: Option<ReferentialAction>) -> Self {
382        self.on_update = action;
383        self
384    }
385
386    /// Set index name.
387    pub const fn index(mut self, name: &'static str) -> Self {
388        self.index = Some(name);
389        self
390    }
391
392    /// Set index name from optional.
393    pub const fn index_opt(mut self, name: Option<&'static str>) -> Self {
394        self.index = name;
395        self
396    }
397
398    /// Set alias for both input and output.
399    ///
400    /// When set, this name is used instead of the field name for both
401    /// serialization and deserialization.
402    pub const fn alias(mut self, name: &'static str) -> Self {
403        self.alias = Some(name);
404        self
405    }
406
407    /// Set alias from optional.
408    pub const fn alias_opt(mut self, name: Option<&'static str>) -> Self {
409        self.alias = name;
410        self
411    }
412
413    /// Set validation alias (input-only).
414    ///
415    /// This name is accepted as an alternative during deserialization,
416    /// in addition to the field name and regular alias.
417    pub const fn validation_alias(mut self, name: &'static str) -> Self {
418        self.validation_alias = Some(name);
419        self
420    }
421
422    /// Set validation alias from optional.
423    pub const fn validation_alias_opt(mut self, name: Option<&'static str>) -> Self {
424        self.validation_alias = name;
425        self
426    }
427
428    /// Set serialization alias (output-only).
429    ///
430    /// This name is used instead of the field name or regular alias
431    /// when serializing the field.
432    pub const fn serialization_alias(mut self, name: &'static str) -> Self {
433        self.serialization_alias = Some(name);
434        self
435    }
436
437    /// Set serialization alias from optional.
438    pub const fn serialization_alias_opt(mut self, name: Option<&'static str>) -> Self {
439        self.serialization_alias = name;
440        self
441    }
442
443    /// Mark this field as computed (not stored in database).
444    ///
445    /// Computed fields are:
446    /// - Excluded from database operations (INSERT, UPDATE, SELECT)
447    /// - Initialized with Default::default() when loading from database
448    /// - Included in serialization (model_dump) unless exclude_computed_fields is set
449    ///
450    /// Use this for fields whose value is derived from other fields at access time.
451    pub const fn computed(mut self, value: bool) -> Self {
452        self.computed = value;
453        self
454    }
455
456    /// Mark this field as excluded from serialization (model_dump).
457    ///
458    /// Excluded fields will never appear in serialized output, regardless
459    /// of other serialization settings. This is useful for sensitive data
460    /// like passwords or internal fields.
461    ///
462    /// # Example
463    ///
464    /// ```ignore
465    /// #[derive(Model)]
466    /// struct User {
467    ///     id: i32,
468    ///     #[sqlmodel(exclude)]
469    ///     password_hash: String,  // Never serialized
470    /// }
471    /// ```
472    pub const fn exclude(mut self, value: bool) -> Self {
473        self.exclude = value;
474        self
475    }
476
477    /// Set the schema title for JSON Schema generation.
478    ///
479    /// The title appears as the "title" property in the field's JSON Schema.
480    pub const fn title(mut self, value: &'static str) -> Self {
481        self.title = Some(value);
482        self
483    }
484
485    /// Set the schema title from optional.
486    pub const fn title_opt(mut self, value: Option<&'static str>) -> Self {
487        self.title = value;
488        self
489    }
490
491    /// Set the schema description for JSON Schema generation.
492    ///
493    /// The description appears as the "description" property in the field's JSON Schema.
494    pub const fn description(mut self, value: &'static str) -> Self {
495        self.description = Some(value);
496        self
497    }
498
499    /// Set the schema description from optional.
500    pub const fn description_opt(mut self, value: Option<&'static str>) -> Self {
501        self.description = value;
502        self
503    }
504
505    /// Set extra JSON Schema properties (as JSON string).
506    ///
507    /// The string should be valid JSON that will be merged into the field's schema.
508    /// For example: `{"examples": ["John Doe"], "minLength": 1}`
509    pub const fn schema_extra(mut self, value: &'static str) -> Self {
510        self.schema_extra = Some(value);
511        self
512    }
513
514    /// Set schema_extra from optional.
515    pub const fn schema_extra_opt(mut self, value: Option<&'static str>) -> Self {
516        self.schema_extra = value;
517        self
518    }
519
520    /// Set the JSON representation of the field's default value.
521    ///
522    /// This is used by model_dump with exclude_defaults=true to compare
523    /// the current value against the default.
524    ///
525    /// # Example
526    ///
527    /// ```ignore
528    /// FieldInfo::new("count", "count", SqlType::Integer)
529    ///     .default_json("0")
530    ///     .has_default(true)
531    /// ```
532    pub const fn default_json(mut self, value: &'static str) -> Self {
533        self.default_json = Some(value);
534        self.has_default = true;
535        self
536    }
537
538    /// Set default_json from optional.
539    pub const fn default_json_opt(mut self, value: Option<&'static str>) -> Self {
540        self.default_json = value;
541        if value.is_some() {
542            self.has_default = true;
543        }
544        self
545    }
546
547    /// Mark whether this field has a default value.
548    ///
549    /// Used for exclude_unset tracking - fields with defaults may not need
550    /// to be explicitly set.
551    pub const fn has_default(mut self, value: bool) -> Self {
552        self.has_default = value;
553        self
554    }
555
556    /// Mark this field as constant (immutable after creation).
557    ///
558    /// Const fields cannot be modified after the model is initially created.
559    /// This is useful for version numbers, creation timestamps, or other
560    /// immutable identifiers.
561    ///
562    /// # Example
563    ///
564    /// ```ignore
565    /// FieldInfo::new("version", "version", SqlType::Text)
566    ///     .const_field(true)
567    /// ```
568    pub const fn const_field(mut self, value: bool) -> Self {
569        self.const_field = value;
570        self
571    }
572
573    /// Set additional SQL constraints for DDL generation.
574    ///
575    /// These constraints are added to the column definition.
576    /// Common uses include CHECK constraints.
577    ///
578    /// # Example
579    ///
580    /// ```ignore
581    /// FieldInfo::new("age", "age", SqlType::Integer)
582    ///     .column_constraints(&["CHECK(age >= 0)", "CHECK(age <= 150)"])
583    /// ```
584    pub const fn column_constraints(mut self, constraints: &'static [&'static str]) -> Self {
585        self.column_constraints = constraints;
586        self
587    }
588
589    /// Set a SQL comment for the column.
590    ///
591    /// The comment will be included in DDL generation.
592    pub const fn column_comment(mut self, comment: &'static str) -> Self {
593        self.column_comment = Some(comment);
594        self
595    }
596
597    /// Set column comment from optional.
598    pub const fn column_comment_opt(mut self, comment: Option<&'static str>) -> Self {
599        self.column_comment = comment;
600        self
601    }
602
603    /// Set extra metadata as JSON string.
604    ///
605    /// This can be used for custom extensions or information
606    /// that doesn't fit in other fields.
607    pub const fn column_info(mut self, info: &'static str) -> Self {
608        self.column_info = Some(info);
609        self
610    }
611
612    /// Set column info from optional.
613    pub const fn column_info_opt(mut self, info: Option<&'static str>) -> Self {
614        self.column_info = info;
615        self
616    }
617
618    /// Set hybrid SQL expression.
619    pub const fn hybrid_sql(mut self, sql: &'static str) -> Self {
620        self.hybrid_sql = Some(sql);
621        self
622    }
623
624    /// Set hybrid SQL expression from optional.
625    pub const fn hybrid_sql_opt(mut self, sql: Option<&'static str>) -> Self {
626        self.hybrid_sql = sql;
627        self
628    }
629
630    /// Set discriminator field name for union types.
631    ///
632    /// This specifies which field in the union variants is used to determine
633    /// the concrete type during deserialization. The union type should have
634    /// a matching `#[serde(tag = "discriminator_field")]` attribute.
635    pub const fn discriminator(mut self, field: &'static str) -> Self {
636        self.discriminator = Some(field);
637        self
638    }
639
640    /// Set discriminator from optional.
641    pub const fn discriminator_opt(mut self, field: Option<&'static str>) -> Self {
642        self.discriminator = field;
643        self
644    }
645
646    /// Get the name to use when serializing (output).
647    ///
648    /// Priority: serialization_alias > alias > name
649    #[must_use]
650    pub const fn output_name(&self) -> &'static str {
651        if let Some(ser_alias) = self.serialization_alias {
652            ser_alias
653        } else if let Some(alias) = self.alias {
654            alias
655        } else {
656            self.name
657        }
658    }
659
660    /// Check if a given name matches this field for input (deserialization).
661    ///
662    /// Matches: name, alias, or validation_alias
663    #[must_use]
664    pub fn matches_input_name(&self, input: &str) -> bool {
665        if input == self.name {
666            return true;
667        }
668        if let Some(alias) = self.alias {
669            if input == alias {
670                return true;
671            }
672        }
673        if let Some(val_alias) = self.validation_alias {
674            if input == val_alias {
675                return true;
676            }
677        }
678        false
679    }
680
681    /// Check if this field has any alias configuration.
682    #[must_use]
683    pub const fn has_alias(&self) -> bool {
684        self.alias.is_some()
685            || self.validation_alias.is_some()
686            || self.serialization_alias.is_some()
687    }
688}
689
690/// A column reference used in queries.
691#[derive(Debug, Clone)]
692pub struct Column {
693    /// Table name (optional, for joins)
694    pub table: Option<String>,
695    /// Column name
696    pub name: String,
697    /// Alias (AS name)
698    pub alias: Option<String>,
699}
700
701impl Column {
702    /// Create a new column reference.
703    pub fn new(name: impl Into<String>) -> Self {
704        Self {
705            table: None,
706            name: name.into(),
707            alias: None,
708        }
709    }
710
711    /// Create a column reference with table prefix.
712    pub fn qualified(table: impl Into<String>, name: impl Into<String>) -> Self {
713        Self {
714            table: Some(table.into()),
715            name: name.into(),
716            alias: None,
717        }
718    }
719
720    /// Set an alias for this column.
721    pub fn alias(mut self, alias: impl Into<String>) -> Self {
722        self.alias = Some(alias.into());
723        self
724    }
725
726    /// Generate SQL for this column reference.
727    pub fn to_sql(&self) -> String {
728        let mut sql = if let Some(table) = &self.table {
729            format!("{}.{}", table, self.name)
730        } else {
731            self.name.clone()
732        };
733
734        if let Some(alias) = &self.alias {
735            sql.push_str(" AS ");
736            sql.push_str(alias);
737        }
738
739        sql
740    }
741}
742
743/// A field reference for type-safe column access.
744///
745/// This is used by generated code to provide compile-time
746/// checked column references.
747#[derive(Debug, Clone, Copy)]
748pub struct Field<T> {
749    /// The column name
750    pub name: &'static str,
751    /// Phantom data for the field type
752    _marker: std::marker::PhantomData<T>,
753}
754
755impl<T> Field<T> {
756    /// Create a new typed field reference.
757    pub const fn new(name: &'static str) -> Self {
758        Self {
759            name,
760            _marker: std::marker::PhantomData,
761        }
762    }
763}
764
765/// Table inheritance strategy.
766///
767/// Determines how model hierarchies are mapped to database tables.
768#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
769pub enum InheritanceStrategy {
770    /// No inheritance (default). Model is standalone.
771    #[default]
772    None,
773    /// Single table inheritance: all subclasses share one table with discriminator column.
774    ///
775    /// The base model specifies this strategy and the discriminator column name.
776    /// Child models inherit from the base and specify their discriminator value.
777    ///
778    /// Example:
779    /// ```ignore
780    /// #[derive(Model)]
781    /// #[sqlmodel(table, inheritance = "single", discriminator = "type")]
782    /// struct Employee { type_: String, ... }
783    ///
784    /// #[derive(Model)]
785    /// #[sqlmodel(inherits = "Employee", discriminator_value = "manager")]
786    /// struct Manager { department: String, ... }
787    /// ```
788    Single,
789    /// Joined table inheritance: each class has its own table with FK to parent.
790    ///
791    /// Base and child models each have their own table. Child tables have a foreign
792    /// key column referencing the parent's primary key. Queries join the tables.
793    ///
794    /// Example:
795    /// ```ignore
796    /// #[derive(Model)]
797    /// #[sqlmodel(table, inheritance = "joined")]
798    /// struct Person { id: i64, name: String }
799    ///
800    /// #[derive(Model)]
801    /// #[sqlmodel(table, inherits = "Person")]
802    /// struct Employee { employee_id: i64, department: String }
803    /// ```
804    Joined,
805    /// Concrete table inheritance: each class is independent, no DB-level inheritance.
806    ///
807    /// Each model has its own complete table with all columns. There's no database
808    /// relationship between parent and child tables. Useful for shared behavior
809    /// without database relationships.
810    Concrete,
811}
812
813impl InheritanceStrategy {
814    /// Check if this strategy uses a discriminator column.
815    #[must_use]
816    pub const fn uses_discriminator(&self) -> bool {
817        matches!(self, Self::Single)
818    }
819
820    /// Check if this strategy requires table joins for child models.
821    #[must_use]
822    pub const fn requires_join(&self) -> bool {
823        matches!(self, Self::Joined)
824    }
825
826    /// Check if this is any form of inheritance (not None).
827    #[must_use]
828    pub const fn is_inheritance(&self) -> bool {
829        !matches!(self, Self::None)
830    }
831}
832
833/// Inheritance metadata for a model.
834///
835/// This struct captures the inheritance configuration for models that participate
836/// in table inheritance hierarchies.
837#[derive(Debug, Clone, Default)]
838pub struct InheritanceInfo {
839    /// The inheritance strategy for this model.
840    pub strategy: InheritanceStrategy,
841    /// The parent table name (for child models).
842    ///
843    /// When set, this model inherits from the specified parent table.
844    pub parent: Option<&'static str>,
845    /// Function returning the parent's field metadata (for joined inheritance).
846    ///
847    /// This is used by query builders to project and alias parent columns when selecting
848    /// a joined-table inheritance child.
849    pub parent_fields_fn: Option<fn() -> &'static [FieldInfo]>,
850    /// The discriminator column name (for single table inheritance base models).
851    ///
852    /// For single table inheritance, this specifies which column contains the
853    /// type discriminator values that distinguish between different model types.
854    pub discriminator_column: Option<&'static str>,
855    /// The discriminator value for this model (single table inheritance child).
856    ///
857    /// For single table inheritance, this value is stored in the discriminator
858    /// column to identify rows belonging to this specific model type.
859    pub discriminator_value: Option<&'static str>,
860}
861
862impl InheritanceInfo {
863    /// Create a new InheritanceInfo with no inheritance.
864    pub const fn none() -> Self {
865        Self {
866            strategy: InheritanceStrategy::None,
867            parent: None,
868            parent_fields_fn: None,
869            discriminator_column: None,
870            discriminator_value: None,
871        }
872    }
873
874    /// Create inheritance info for a base model with single table inheritance.
875    pub const fn single_table() -> Self {
876        Self {
877            strategy: InheritanceStrategy::Single,
878            parent: None,
879            parent_fields_fn: None,
880            discriminator_column: None,
881            discriminator_value: None,
882        }
883    }
884
885    /// Create inheritance info for a base model with joined table inheritance.
886    pub const fn joined_table() -> Self {
887        Self {
888            strategy: InheritanceStrategy::Joined,
889            parent: None,
890            parent_fields_fn: None,
891            discriminator_column: None,
892            discriminator_value: None,
893        }
894    }
895
896    /// Create inheritance info for a base model with concrete table inheritance.
897    pub const fn concrete_table() -> Self {
898        Self {
899            strategy: InheritanceStrategy::Concrete,
900            parent: None,
901            parent_fields_fn: None,
902            discriminator_column: None,
903            discriminator_value: None,
904        }
905    }
906
907    /// Create inheritance info for a child model.
908    pub const fn child(parent_table: &'static str) -> Self {
909        Self {
910            strategy: InheritanceStrategy::None, // Inherits from parent's strategy
911            parent: Some(parent_table),
912            parent_fields_fn: None,
913            discriminator_column: None,
914            discriminator_value: None,
915        }
916    }
917
918    /// Set the discriminator column name (builder pattern, for base models).
919    pub const fn with_discriminator_column(mut self, column: &'static str) -> Self {
920        self.discriminator_column = Some(column);
921        self
922    }
923
924    /// Set the discriminator value (builder pattern, for child models).
925    pub const fn with_discriminator_value(mut self, value: &'static str) -> Self {
926        self.discriminator_value = Some(value);
927        self
928    }
929
930    /// Check if this model is a child in an inheritance hierarchy.
931    #[must_use]
932    pub const fn is_child(&self) -> bool {
933        self.parent.is_some()
934    }
935
936    /// Check if this model is a base model in an inheritance hierarchy.
937    #[must_use]
938    pub const fn is_base(&self) -> bool {
939        self.parent.is_none() && self.strategy.is_inheritance()
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946    use crate::SqlType;
947
948    #[test]
949    fn test_field_info_new() {
950        let field = FieldInfo::new(
951            "price",
952            "price",
953            SqlType::Decimal {
954                precision: 10,
955                scale: 2,
956            },
957        );
958        assert_eq!(field.name, "price");
959        assert_eq!(field.column_name, "price");
960        assert!(field.precision.is_none());
961        assert!(field.scale.is_none());
962    }
963
964    #[test]
965    fn test_field_info_precision_scale() {
966        let field = FieldInfo::new(
967            "amount",
968            "amount",
969            SqlType::Decimal {
970                precision: 10,
971                scale: 2,
972            },
973        )
974        .precision(12)
975        .scale(4);
976        assert_eq!(field.precision, Some(12));
977        assert_eq!(field.scale, Some(4));
978    }
979
980    #[test]
981    fn test_field_info_decimal_precision() {
982        let field = FieldInfo::new(
983            "total",
984            "total",
985            SqlType::Numeric {
986                precision: 10,
987                scale: 2,
988            },
989        )
990        .decimal_precision(18, 6);
991        assert_eq!(field.precision, Some(18));
992        assert_eq!(field.scale, Some(6));
993    }
994
995    #[test]
996    fn test_effective_sql_type_override_takes_precedence() {
997        let field = FieldInfo::new(
998            "amount",
999            "amount",
1000            SqlType::Decimal {
1001                precision: 10,
1002                scale: 2,
1003            },
1004        )
1005        .sql_type_override("MONEY")
1006        .precision(18)
1007        .scale(4);
1008        // Override should take precedence over precision/scale
1009        assert_eq!(field.effective_sql_type(), "MONEY");
1010    }
1011
1012    #[test]
1013    fn test_effective_sql_type_uses_precision_scale() {
1014        let field = FieldInfo::new(
1015            "price",
1016            "price",
1017            SqlType::Decimal {
1018                precision: 10,
1019                scale: 2,
1020            },
1021        )
1022        .precision(15)
1023        .scale(3);
1024        assert_eq!(field.effective_sql_type(), "DECIMAL(15, 3)");
1025    }
1026
1027    #[test]
1028    fn test_effective_sql_type_numeric_uses_precision_scale() {
1029        let field = FieldInfo::new(
1030            "value",
1031            "value",
1032            SqlType::Numeric {
1033                precision: 10,
1034                scale: 2,
1035            },
1036        )
1037        .precision(20)
1038        .scale(8);
1039        assert_eq!(field.effective_sql_type(), "NUMERIC(20, 8)");
1040    }
1041
1042    #[test]
1043    fn test_effective_sql_type_fallback_to_sql_type() {
1044        let field = FieldInfo::new("count", "count", SqlType::BigInt);
1045        assert_eq!(field.effective_sql_type(), "BIGINT");
1046    }
1047
1048    #[test]
1049    fn test_effective_sql_type_decimal_without_precision_scale() {
1050        // When precision/scale not set on FieldInfo, use SqlType's values
1051        let field = FieldInfo::new(
1052            "amount",
1053            "amount",
1054            SqlType::Decimal {
1055                precision: 10,
1056                scale: 2,
1057            },
1058        );
1059        // Falls back to sql_type.sql_name() which should generate "DECIMAL(10, 2)"
1060        assert_eq!(field.effective_sql_type(), "DECIMAL(10, 2)");
1061    }
1062
1063    #[test]
1064    fn test_precision_opt() {
1065        let field = FieldInfo::new(
1066            "test",
1067            "test",
1068            SqlType::Decimal {
1069                precision: 10,
1070                scale: 2,
1071            },
1072        )
1073        .precision_opt(Some(16));
1074        assert_eq!(field.precision, Some(16));
1075
1076        let field2 = FieldInfo::new(
1077            "test2",
1078            "test2",
1079            SqlType::Decimal {
1080                precision: 10,
1081                scale: 2,
1082            },
1083        )
1084        .precision_opt(None);
1085        assert_eq!(field2.precision, None);
1086    }
1087
1088    #[test]
1089    fn test_scale_opt() {
1090        let field = FieldInfo::new(
1091            "test",
1092            "test",
1093            SqlType::Decimal {
1094                precision: 10,
1095                scale: 2,
1096            },
1097        )
1098        .scale_opt(Some(5));
1099        assert_eq!(field.scale, Some(5));
1100
1101        let field2 = FieldInfo::new(
1102            "test2",
1103            "test2",
1104            SqlType::Decimal {
1105                precision: 10,
1106                scale: 2,
1107            },
1108        )
1109        .scale_opt(None);
1110        assert_eq!(field2.scale, None);
1111    }
1112
1113    // ========================================================================
1114    // Field alias tests
1115    // ========================================================================
1116
1117    #[test]
1118    fn test_field_info_alias() {
1119        let field = FieldInfo::new("name", "name", SqlType::Text).alias("userName");
1120        assert_eq!(field.alias, Some("userName"));
1121        assert!(field.validation_alias.is_none());
1122        assert!(field.serialization_alias.is_none());
1123    }
1124
1125    #[test]
1126    fn test_field_info_validation_alias() {
1127        let field = FieldInfo::new("name", "name", SqlType::Text).validation_alias("user_name");
1128        assert!(field.alias.is_none());
1129        assert_eq!(field.validation_alias, Some("user_name"));
1130        assert!(field.serialization_alias.is_none());
1131    }
1132
1133    #[test]
1134    fn test_field_info_serialization_alias() {
1135        let field = FieldInfo::new("name", "name", SqlType::Text).serialization_alias("user-name");
1136        assert!(field.alias.is_none());
1137        assert!(field.validation_alias.is_none());
1138        assert_eq!(field.serialization_alias, Some("user-name"));
1139    }
1140
1141    #[test]
1142    fn test_field_info_all_aliases() {
1143        let field = FieldInfo::new("name", "name", SqlType::Text)
1144            .alias("nm")
1145            .validation_alias("input_name")
1146            .serialization_alias("outputName");
1147
1148        assert_eq!(field.alias, Some("nm"));
1149        assert_eq!(field.validation_alias, Some("input_name"));
1150        assert_eq!(field.serialization_alias, Some("outputName"));
1151    }
1152
1153    #[test]
1154    fn test_field_info_alias_opt() {
1155        let field1 = FieldInfo::new("name", "name", SqlType::Text).alias_opt(Some("userName"));
1156        assert_eq!(field1.alias, Some("userName"));
1157
1158        let field2 = FieldInfo::new("name", "name", SqlType::Text).alias_opt(None);
1159        assert!(field2.alias.is_none());
1160    }
1161
1162    #[test]
1163    fn test_field_info_validation_alias_opt() {
1164        let field1 =
1165            FieldInfo::new("name", "name", SqlType::Text).validation_alias_opt(Some("user_name"));
1166        assert_eq!(field1.validation_alias, Some("user_name"));
1167
1168        let field2 = FieldInfo::new("name", "name", SqlType::Text).validation_alias_opt(None);
1169        assert!(field2.validation_alias.is_none());
1170    }
1171
1172    #[test]
1173    fn test_field_info_serialization_alias_opt() {
1174        let field1 = FieldInfo::new("name", "name", SqlType::Text)
1175            .serialization_alias_opt(Some("user-name"));
1176        assert_eq!(field1.serialization_alias, Some("user-name"));
1177
1178        let field2 = FieldInfo::new("name", "name", SqlType::Text).serialization_alias_opt(None);
1179        assert!(field2.serialization_alias.is_none());
1180    }
1181
1182    #[test]
1183    fn test_field_info_output_name() {
1184        // No aliases - uses field name
1185        let field1 = FieldInfo::new("name", "name", SqlType::Text);
1186        assert_eq!(field1.output_name(), "name");
1187
1188        // Only alias - uses alias
1189        let field2 = FieldInfo::new("name", "name", SqlType::Text).alias("nm");
1190        assert_eq!(field2.output_name(), "nm");
1191
1192        // serialization_alias takes precedence
1193        let field3 = FieldInfo::new("name", "name", SqlType::Text)
1194            .alias("nm")
1195            .serialization_alias("outputName");
1196        assert_eq!(field3.output_name(), "outputName");
1197
1198        // Only serialization_alias
1199        let field4 = FieldInfo::new("name", "name", SqlType::Text).serialization_alias("userName");
1200        assert_eq!(field4.output_name(), "userName");
1201    }
1202
1203    #[test]
1204    fn test_field_info_matches_input_name() {
1205        // No aliases - only matches field name
1206        let field1 = FieldInfo::new("name", "name", SqlType::Text);
1207        assert!(field1.matches_input_name("name"));
1208        assert!(!field1.matches_input_name("userName"));
1209
1210        // With alias - matches both
1211        let field2 = FieldInfo::new("name", "name", SqlType::Text).alias("nm");
1212        assert!(field2.matches_input_name("name"));
1213        assert!(field2.matches_input_name("nm"));
1214        assert!(!field2.matches_input_name("userName"));
1215
1216        // With validation_alias - matches both
1217        let field3 = FieldInfo::new("name", "name", SqlType::Text).validation_alias("user_name");
1218        assert!(field3.matches_input_name("name"));
1219        assert!(field3.matches_input_name("user_name"));
1220        assert!(!field3.matches_input_name("userName"));
1221
1222        // With both - matches all three
1223        let field4 = FieldInfo::new("name", "name", SqlType::Text)
1224            .alias("nm")
1225            .validation_alias("user_name");
1226        assert!(field4.matches_input_name("name"));
1227        assert!(field4.matches_input_name("nm"));
1228        assert!(field4.matches_input_name("user_name"));
1229    }
1230
1231    #[test]
1232    fn test_field_info_has_alias() {
1233        let field1 = FieldInfo::new("name", "name", SqlType::Text);
1234        assert!(!field1.has_alias());
1235
1236        let field2 = FieldInfo::new("name", "name", SqlType::Text).alias("nm");
1237        assert!(field2.has_alias());
1238
1239        let field3 = FieldInfo::new("name", "name", SqlType::Text).validation_alias("user_name");
1240        assert!(field3.has_alias());
1241
1242        let field4 = FieldInfo::new("name", "name", SqlType::Text).serialization_alias("userName");
1243        assert!(field4.has_alias());
1244
1245        let field5 = FieldInfo::new("name", "name", SqlType::Text)
1246            .alias("nm")
1247            .validation_alias("user_name")
1248            .serialization_alias("userName");
1249        assert!(field5.has_alias());
1250    }
1251
1252    #[test]
1253    fn test_field_info_exclude() {
1254        // Default: not excluded
1255        let field1 = FieldInfo::new("name", "name", SqlType::Text);
1256        assert!(!field1.exclude);
1257
1258        // Excluded field
1259        let field2 = FieldInfo::new("password", "password", SqlType::Text).exclude(true);
1260        assert!(field2.exclude);
1261
1262        // Exclude can be explicitly set to false
1263        let field3 = FieldInfo::new("email", "email", SqlType::Text).exclude(false);
1264        assert!(!field3.exclude);
1265    }
1266
1267    #[test]
1268    fn test_field_info_exclude_combined_with_other_attrs() {
1269        // Exclude can be combined with other attributes
1270        let field = FieldInfo::new("secret", "secret", SqlType::Text)
1271            .exclude(true)
1272            .nullable(true)
1273            .alias("hidden_value");
1274
1275        assert!(field.exclude);
1276        assert!(field.nullable);
1277        assert_eq!(field.alias, Some("hidden_value"));
1278    }
1279
1280    #[test]
1281    fn test_field_info_title() {
1282        let field1 = FieldInfo::new("name", "name", SqlType::Text);
1283        assert_eq!(field1.title, None);
1284
1285        let field2 = FieldInfo::new("name", "name", SqlType::Text).title("User Name");
1286        assert_eq!(field2.title, Some("User Name"));
1287    }
1288
1289    #[test]
1290    fn test_field_info_description() {
1291        let field1 = FieldInfo::new("name", "name", SqlType::Text);
1292        assert_eq!(field1.description, None);
1293
1294        let field2 =
1295            FieldInfo::new("name", "name", SqlType::Text).description("The full name of the user");
1296        assert_eq!(field2.description, Some("The full name of the user"));
1297    }
1298
1299    #[test]
1300    fn test_field_info_schema_extra() {
1301        let field1 = FieldInfo::new("name", "name", SqlType::Text);
1302        assert_eq!(field1.schema_extra, None);
1303
1304        let field2 =
1305            FieldInfo::new("name", "name", SqlType::Text).schema_extra(r#"{"examples": ["John"]}"#);
1306        assert_eq!(field2.schema_extra, Some(r#"{"examples": ["John"]}"#));
1307    }
1308
1309    #[test]
1310    fn test_field_info_all_schema_metadata() {
1311        let field = FieldInfo::new("name", "name", SqlType::Text)
1312            .title("User Name")
1313            .description("The full name of the user")
1314            .schema_extra(r#"{"examples": ["John Doe"]}"#);
1315
1316        assert_eq!(field.title, Some("User Name"));
1317        assert_eq!(field.description, Some("The full name of the user"));
1318        assert_eq!(field.schema_extra, Some(r#"{"examples": ["John Doe"]}"#));
1319    }
1320
1321    #[test]
1322    fn test_field_info_const_field() {
1323        // Default should be false
1324        let field1 = FieldInfo::new("version", "version", SqlType::Text);
1325        assert!(!field1.const_field);
1326
1327        // Explicitly set to true
1328        let field2 = FieldInfo::new("version", "version", SqlType::Text).const_field(true);
1329        assert!(field2.const_field);
1330
1331        // Can combine with other attributes
1332        let field3 = FieldInfo::new("version", "version", SqlType::Text)
1333            .const_field(true)
1334            .default("'1.0.0'");
1335        assert!(field3.const_field);
1336        assert_eq!(field3.default, Some("'1.0.0'"));
1337    }
1338
1339    #[test]
1340    fn test_field_info_column_constraints() {
1341        static CONSTRAINTS: &[&str] = &["CHECK(price > 0)", "CHECK(price < 1000)"];
1342
1343        // Default should be empty
1344        let field1 = FieldInfo::new("price", "price", SqlType::Integer);
1345        assert!(field1.column_constraints.is_empty());
1346
1347        // With constraints
1348        let field2 =
1349            FieldInfo::new("price", "price", SqlType::Integer).column_constraints(CONSTRAINTS);
1350        assert_eq!(field2.column_constraints.len(), 2);
1351        assert_eq!(field2.column_constraints[0], "CHECK(price > 0)");
1352    }
1353
1354    #[test]
1355    fn test_field_info_column_comment() {
1356        // Default should be None
1357        let field1 = FieldInfo::new("name", "name", SqlType::Text);
1358        assert_eq!(field1.column_comment, None);
1359
1360        // With comment
1361        let field2 =
1362            FieldInfo::new("name", "name", SqlType::Text).column_comment("User's display name");
1363        assert_eq!(field2.column_comment, Some("User's display name"));
1364    }
1365
1366    #[test]
1367    fn test_field_info_column_info() {
1368        // Default should be None
1369        let field1 = FieldInfo::new("email", "email", SqlType::Text);
1370        assert_eq!(field1.column_info, None);
1371
1372        // With info
1373        let field2 =
1374            FieldInfo::new("email", "email", SqlType::Text).column_info(r#"{"deprecated": true}"#);
1375        assert_eq!(field2.column_info, Some(r#"{"deprecated": true}"#));
1376    }
1377
1378    #[test]
1379    fn test_field_info_discriminator() {
1380        // Default should be None
1381        let field1 = FieldInfo::new("pet", "pet", SqlType::Json);
1382        assert_eq!(field1.discriminator, None);
1383
1384        // With discriminator
1385        let field2 = FieldInfo::new("pet", "pet", SqlType::Json).discriminator("pet_type");
1386        assert_eq!(field2.discriminator, Some("pet_type"));
1387
1388        // With discriminator_opt(None)
1389        let field3 = FieldInfo::new("pet", "pet", SqlType::Json).discriminator_opt(None);
1390        assert_eq!(field3.discriminator, None);
1391
1392        // With discriminator_opt(Some)
1393        let field4 = FieldInfo::new("pet", "pet", SqlType::Json).discriminator_opt(Some("kind"));
1394        assert_eq!(field4.discriminator, Some("kind"));
1395    }
1396
1397    // =========================================================================
1398    // InheritanceStrategy Tests
1399    // =========================================================================
1400
1401    #[test]
1402    fn test_inheritance_strategy_default() {
1403        let strategy = InheritanceStrategy::default();
1404        assert_eq!(strategy, InheritanceStrategy::None);
1405        assert!(!strategy.is_inheritance());
1406        assert!(!strategy.uses_discriminator());
1407        assert!(!strategy.requires_join());
1408    }
1409
1410    #[test]
1411    fn test_inheritance_strategy_single() {
1412        let strategy = InheritanceStrategy::Single;
1413        assert!(strategy.is_inheritance());
1414        assert!(strategy.uses_discriminator());
1415        assert!(!strategy.requires_join());
1416    }
1417
1418    #[test]
1419    fn test_inheritance_strategy_joined() {
1420        let strategy = InheritanceStrategy::Joined;
1421        assert!(strategy.is_inheritance());
1422        assert!(!strategy.uses_discriminator());
1423        assert!(strategy.requires_join());
1424    }
1425
1426    #[test]
1427    fn test_inheritance_strategy_concrete() {
1428        let strategy = InheritanceStrategy::Concrete;
1429        assert!(strategy.is_inheritance());
1430        assert!(!strategy.uses_discriminator());
1431        assert!(!strategy.requires_join());
1432    }
1433
1434    // =========================================================================
1435    // InheritanceInfo Tests
1436    // =========================================================================
1437
1438    #[test]
1439    fn test_inheritance_info_none() {
1440        let info = InheritanceInfo::none();
1441        assert_eq!(info.strategy, InheritanceStrategy::None);
1442        assert!(info.parent.is_none());
1443        assert!(info.discriminator_value.is_none());
1444        assert!(!info.is_child());
1445        assert!(!info.is_base());
1446    }
1447
1448    #[test]
1449    fn test_inheritance_info_single_table_base() {
1450        let info = InheritanceInfo::single_table();
1451        assert_eq!(info.strategy, InheritanceStrategy::Single);
1452        assert!(info.parent.is_none());
1453        assert!(info.is_base());
1454        assert!(!info.is_child());
1455    }
1456
1457    #[test]
1458    fn test_inheritance_info_joined_table_base() {
1459        let info = InheritanceInfo::joined_table();
1460        assert_eq!(info.strategy, InheritanceStrategy::Joined);
1461        assert!(info.parent.is_none());
1462        assert!(info.is_base());
1463        assert!(!info.is_child());
1464    }
1465
1466    #[test]
1467    fn test_inheritance_info_concrete_table_base() {
1468        let info = InheritanceInfo::concrete_table();
1469        assert_eq!(info.strategy, InheritanceStrategy::Concrete);
1470        assert!(info.parent.is_none());
1471        assert!(info.is_base());
1472        assert!(!info.is_child());
1473    }
1474
1475    #[test]
1476    fn test_inheritance_info_child() {
1477        let info = InheritanceInfo::child("employees");
1478        assert_eq!(info.parent, Some("employees"));
1479        assert!(info.is_child());
1480        assert!(!info.is_base());
1481    }
1482
1483    #[test]
1484    fn test_inheritance_info_child_with_discriminator_value() {
1485        let info = InheritanceInfo::child("employees").with_discriminator_value("manager");
1486        assert_eq!(info.parent, Some("employees"));
1487        assert_eq!(info.discriminator_value, Some("manager"));
1488        assert!(info.is_child());
1489    }
1490
1491    #[test]
1492    fn test_inheritance_info_single_table_with_discriminator_column() {
1493        let info = InheritanceInfo::single_table().with_discriminator_column("type");
1494        assert_eq!(info.strategy, InheritanceStrategy::Single);
1495        assert_eq!(info.discriminator_column, Some("type"));
1496        assert!(info.is_base());
1497    }
1498}