Skip to main content

ferriorm_core/
schema.rs

1//! Validated and resolved Schema IR (Intermediate Representation).
2//!
3//! This is the **single source of truth** consumed by codegen and the migration
4//! engine. It is produced by the validator from the raw [`crate::ast`] AST.
5//!
6//! Key differences from the raw AST:
7//! - Type names are resolved to [`FieldKind::Scalar`], [`FieldKind::Enum`],
8//!   or [`FieldKind::Model`].
9//! - Table and column names are inferred (`snake_case` + plural) or taken from
10//!   `@@map` / `@map` attributes.
11//! - Relations are fully resolved with cardinality and referential actions.
12//! - Primary keys, indexes, and unique constraints are normalized.
13//!
14//! All types in this module support optional `serde` serialization (behind the
15//! `serde` feature flag) for JSON schema snapshots used by the migration engine.
16
17use crate::ast::{DefaultValue, ReferentialAction};
18use crate::types::{DatabaseProvider, ScalarType};
19
20macro_rules! serde_derive {
21    ($(#[$meta:meta])* $vis:vis struct $name:ident { $($body:tt)* }) => {
22        $(#[$meta])*
23        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24        $vis struct $name { $($body)* }
25    };
26    ($(#[$meta:meta])* $vis:vis enum $name:ident { $($body:tt)* }) => {
27        $(#[$meta])*
28        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29        $vis enum $name { $($body)* }
30    };
31}
32
33serde_derive! {
34    /// A fully validated schema.
35    #[derive(Debug, Clone)]
36    pub struct Schema {
37        pub datasource: DatasourceConfig,
38        pub generators: Vec<GeneratorConfig>,
39        pub enums: Vec<Enum>,
40        pub models: Vec<Model>,
41    }
42}
43
44serde_derive! {
45    /// Resolved datasource configuration.
46    #[derive(Debug, Clone)]
47    pub struct DatasourceConfig {
48        pub name: String,
49        pub provider: DatabaseProvider,
50        pub url: String,
51    }
52}
53
54serde_derive! {
55    /// Resolved generator configuration.
56    #[derive(Debug, Clone)]
57    pub struct GeneratorConfig {
58        pub name: String,
59        pub output: String,
60    }
61}
62
63serde_derive! {
64    /// A validated enum definition.
65    #[derive(Debug, Clone)]
66    pub struct Enum {
67        pub name: String,
68        pub db_name: String,
69        pub variants: Vec<String>,
70    }
71}
72
73serde_derive! {
74    /// A validated model definition.
75    #[derive(Debug, Clone)]
76    pub struct Model {
77        pub name: String,
78        pub db_name: String,
79        pub fields: Vec<Field>,
80        pub primary_key: PrimaryKey,
81        pub indexes: Vec<Index>,
82        pub unique_constraints: Vec<UniqueConstraint>,
83    }
84}
85
86serde_derive! {
87    /// A validated field definition.
88    #[derive(Debug, Clone)]
89    pub struct Field {
90        pub name: String,
91        pub db_name: String,
92        pub field_type: FieldKind,
93        pub is_optional: bool,
94        pub is_list: bool,
95        pub is_id: bool,
96        pub is_unique: bool,
97        pub is_updated_at: bool,
98        pub default: Option<DefaultValue>,
99        pub relation: Option<ResolvedRelation>,
100        /// `@db.*` type hint, e.g. `BigInt` for `@db.BigInt`.
101        /// Tuple is `(type_name, args)` matching `FieldAttribute::DbType`.
102        pub db_type: Option<(String, Vec<String>)>,
103    }
104}
105
106impl Field {
107    /// Returns true if this field is a scalar (stored in the database), not a relation.
108    #[must_use]
109    pub fn is_scalar(&self) -> bool {
110        !matches!(self.field_type, FieldKind::Model(_)) && !self.is_list
111    }
112
113    /// Returns true if this field has a server-side default and can be omitted on create.
114    #[must_use]
115    pub fn has_default(&self) -> bool {
116        self.default.is_some() || self.is_updated_at
117    }
118}
119
120serde_derive! {
121    /// The kind of a field's type.
122    #[derive(Debug, Clone, PartialEq, Eq)]
123    pub enum FieldKind {
124        /// A scalar type (String, Int, etc.)
125        Scalar(ScalarType),
126        /// A reference to an enum defined in the schema.
127        Enum(String),
128        /// A relation to another model.
129        Model(String),
130    }
131}
132
133serde_derive! {
134    /// A resolved relation between two models.
135    ///
136    /// `name` carries the optional disambiguator from
137    /// `@relation("Name", ...)` (or `name: "Name"`). When two fields on
138    /// the same model both relate to the same target, `name` must be
139    /// set on both sides for the validator to pair them correctly.
140    #[derive(Debug, Clone)]
141    pub struct ResolvedRelation {
142        #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
143        pub name: Option<String>,
144        pub related_model: String,
145        pub relation_type: RelationType,
146        pub fields: Vec<String>,
147        pub references: Vec<String>,
148        pub on_delete: ReferentialAction,
149        pub on_update: ReferentialAction,
150    }
151}
152
153serde_derive! {
154    /// The cardinality of a relation.
155    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
156    pub enum RelationType {
157        OneToOne,
158        OneToMany,
159        ManyToOne,
160        ManyToMany,
161    }
162}
163
164serde_derive! {
165    /// Primary key definition.
166    #[derive(Debug, Clone)]
167    pub struct PrimaryKey {
168        pub fields: Vec<String>,
169    }
170}
171
172impl PrimaryKey {
173    /// Returns true if this is a composite (multi-field) primary key.
174    #[must_use]
175    pub fn is_composite(&self) -> bool {
176        self.fields.len() > 1
177    }
178}
179
180serde_derive! {
181    /// An index definition.
182    ///
183    /// `name` overrides the auto-generated `idx_<table>_<cols>` name
184    /// when set via `@@index([..], name: "...")`.
185    #[derive(Debug, Clone)]
186    pub struct Index {
187        pub fields: Vec<String>,
188        #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
189        pub name: Option<String>,
190    }
191}
192
193serde_derive! {
194    /// A unique constraint.
195    ///
196    /// `name` overrides the auto-generated `uq_<table>_<cols>` name
197    /// when set via `@@unique([..], name: "...")`.
198    #[derive(Debug, Clone)]
199    pub struct UniqueConstraint {
200        pub fields: Vec<String>,
201        #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
202        pub name: Option<String>,
203    }
204}