Skip to main content

vespertide_core/schema/
column.rs

1use serde::{Deserialize, Serialize};
2
3use crate::schema::{
4    foreign_key::ForeignKeySyntax,
5    names::ColumnName,
6    primary_key::PrimaryKeySyntax,
7    str_or_bool::{StrOrBoolOrArray, StringOrBool},
8};
9
10/// Definition of a single table column, including its type, nullability, and inline constraints.
11///
12/// Inline constraints (`primary_key`, `unique`, `index`, `foreign_key`) are the preferred way to
13/// declare constraints in model JSON files. Call [`TableDef::normalize`] to convert them into
14/// table-level [`TableConstraint`] entries before diffing or SQL generation.
15///
16/// Use [`ColumnDef::new`] to construct a column programmatically, then chain the setter methods
17/// (`.primary_key()`, `.unique()`, `.index()`, `.foreign_key()`, `.default()`, `.comment()`) to
18/// attach optional fields.
19///
20/// [`TableDef::normalize`]: crate::schema::TableDef::normalize
21/// [`TableConstraint`]: crate::schema::TableConstraint
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
24#[serde(rename_all = "snake_case")]
25pub struct ColumnDef {
26    pub name: ColumnName,
27    pub r#type: ColumnType,
28    pub nullable: bool,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub default: Option<StringOrBool>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub comment: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub primary_key: Option<PrimaryKeySyntax>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub unique: Option<StrOrBoolOrArray>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub index: Option<StrOrBoolOrArray>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub foreign_key: Option<ForeignKeySyntax>,
41}
42
43/// The SQL type of a column, either a parameter-free simple type or a parameterised complex type.
44///
45/// In JSON model files a simple type is written as a plain string (`"integer"`, `"text"`, etc.)
46/// while a complex type is written as an object with a `"kind"` discriminant
47/// (`{"kind": "varchar", "length": 255}`).
48///
49/// Always construct via the wrapped variants:
50/// ```
51/// use vespertide_core::{ColumnType, SimpleColumnType, ComplexColumnType};
52/// let t1 = ColumnType::Simple(SimpleColumnType::Integer);
53/// let t2 = ColumnType::Complex(ComplexColumnType::Varchar { length: 255 });
54/// ```
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[serde(rename_all = "snake_case", untagged)]
58pub enum ColumnType {
59    /// A parameter-free SQL type such as `INTEGER`, `TEXT`, or `UUID`.
60    Simple(SimpleColumnType),
61    /// A parameterised SQL type such as `VARCHAR(n)`, `NUMERIC(p,s)`, or a named enum.
62    Complex(ComplexColumnType),
63}
64
65impl ColumnType {
66    /// Returns true if this type supports `auto_increment` (integer types only)
67    pub fn supports_auto_increment(&self) -> bool {
68        match self {
69            ColumnType::Simple(ty) => ty.supports_auto_increment(),
70            ColumnType::Complex(_) => false,
71        }
72    }
73
74    /// Check if two column types require a migration.
75    /// For integer enums, no migration is ever needed because the underlying DB type is always INTEGER.
76    /// The enum name and values only affect code generation (`SeaORM` entities), not the database schema.
77    pub fn requires_migration(&self, other: &ColumnType) -> bool {
78        match (self, other) {
79            (
80                ColumnType::Complex(ComplexColumnType::Enum {
81                    values: values1, ..
82                }),
83                ColumnType::Complex(ComplexColumnType::Enum {
84                    values: values2, ..
85                }),
86            ) => {
87                // Both are integer enums - never require migration (DB type is always INTEGER)
88                if values1.is_integer() && values2.is_integer() {
89                    false
90                } else {
91                    // String enums: compare only values, not name.
92                    // The enum name is a user-facing label; the actual DB type name
93                    // is auto-generated with a table prefix at SQL generation time.
94                    // Different labels with identical values don't require a migration.
95                    values1 != values2
96                }
97            }
98            _ => self != other,
99        }
100    }
101
102    /// Convert column type to Rust type string (for `SeaORM` entity generation)
103    pub fn to_rust_type(&self, nullable: bool) -> String {
104        let base = match self {
105            ColumnType::Simple(ty) => match ty {
106                SimpleColumnType::SmallInt => "i16".to_string(),
107                SimpleColumnType::Integer => "i32".to_string(),
108                SimpleColumnType::BigInt => "i64".to_string(),
109                SimpleColumnType::Real => "f32".to_string(),
110                SimpleColumnType::DoublePrecision => "f64".to_string(),
111                SimpleColumnType::Text
112                | SimpleColumnType::Interval
113                | SimpleColumnType::Inet
114                | SimpleColumnType::Cidr
115                | SimpleColumnType::Macaddr
116                | SimpleColumnType::Xml => "String".to_string(),
117                SimpleColumnType::Boolean => "bool".to_string(),
118                SimpleColumnType::Date => "Date".to_string(),
119                SimpleColumnType::Time => "Time".to_string(),
120                SimpleColumnType::Timestamp => "DateTime".to_string(),
121                SimpleColumnType::Timestamptz => "DateTimeWithTimeZone".to_string(),
122                SimpleColumnType::Bytea => "Vec<u8>".to_string(),
123                SimpleColumnType::Uuid => "Uuid".to_string(),
124                SimpleColumnType::Json => "Json".to_string(),
125            },
126            ColumnType::Complex(ty) => match ty {
127                ComplexColumnType::Numeric { .. } => "Decimal".to_string(),
128                ComplexColumnType::Varchar { .. }
129                | ComplexColumnType::Char { .. }
130                | ComplexColumnType::Custom { .. }
131                | ComplexColumnType::Enum { .. } => "String".to_string(),
132            },
133        };
134
135        if nullable {
136            format!("Option<{base}>")
137        } else {
138            base
139        }
140    }
141
142    /// Convert column type to human-readable display string (for CLI prompts)
143    /// Examples: "integer", "text", "varchar(255)", "numeric(10,2)"
144    pub fn to_display_string(&self) -> String {
145        match self {
146            ColumnType::Simple(ty) => ty.to_display_string(),
147            ColumnType::Complex(ty) => ty.to_display_string(),
148        }
149    }
150
151    /// Get the default fill value for this column type (for CLI prompts)
152    /// Returns None if no sensible default exists for the type
153    pub fn default_fill_value(&self) -> &'static str {
154        match self {
155            ColumnType::Simple(ty) => ty.default_fill_value(),
156            ColumnType::Complex(ty) => ty.default_fill_value(),
157        }
158    }
159
160    /// Get enum variant names if this is an enum type
161    /// Returns None if not an enum, Some(names) otherwise
162    pub fn enum_variant_names(&self) -> Option<Vec<String>> {
163        match self {
164            ColumnType::Complex(ComplexColumnType::Enum { values, .. }) => Some(
165                values
166                    .variant_names()
167                    .into_iter()
168                    .map(String::from)
169                    .collect(),
170            ),
171            _ => None,
172        }
173    }
174}
175
176impl ColumnDef {
177    /// Construct a new column with required fields only.
178    /// Use the `.primary_key()`, `.unique()`, `.index()`, `.foreign_key()`,
179    /// `.default()`, `.comment()` setters to add optional fields.
180    ///
181    /// # Examples
182    /// ```
183    /// use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType};
184    /// let id = ColumnDef::new("id", ColumnType::Simple(SimpleColumnType::Integer), false);
185    /// ```
186    #[must_use]
187    pub fn new(name: impl Into<ColumnName>, r#type: ColumnType, nullable: bool) -> Self {
188        Self {
189            name: name.into(),
190            r#type,
191            nullable,
192            default: None,
193            comment: None,
194            primary_key: None,
195            unique: None,
196            index: None,
197            foreign_key: None,
198        }
199    }
200
201    /// Mark this column as part of the primary key.
202    #[must_use]
203    pub fn primary_key(mut self, pk: PrimaryKeySyntax) -> Self {
204        self.primary_key = Some(pk);
205        self
206    }
207
208    /// Add a unique constraint to this column.
209    #[must_use]
210    pub fn unique(mut self, unique: StrOrBoolOrArray) -> Self {
211        self.unique = Some(unique);
212        self
213    }
214
215    /// Add an index on this column.
216    #[must_use]
217    pub fn index(mut self, index: StrOrBoolOrArray) -> Self {
218        self.index = Some(index);
219        self
220    }
221
222    /// Add a foreign key reference from this column.
223    #[must_use]
224    pub fn foreign_key(mut self, fk: ForeignKeySyntax) -> Self {
225        self.foreign_key = Some(fk);
226        self
227    }
228
229    /// Set the column default value.
230    #[must_use]
231    pub fn default(mut self, default: StringOrBool) -> Self {
232        self.default = Some(default);
233        self
234    }
235
236    /// Add a column comment.
237    #[must_use]
238    pub fn comment(mut self, comment: impl Into<String>) -> Self {
239        self.comment = Some(comment.into());
240        self
241    }
242}
243
244/// Parameter-free SQL column types supported across all backends.
245///
246/// Each variant maps directly to a standard SQL type. Use these via
247/// [`ColumnType::Simple`] when no length, precision, or scale is needed.
248///
249/// This enum is `#[non_exhaustive]`: new variants may be added in future releases.
250/// Downstream `match` expressions should include a wildcard arm.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
252#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
253#[serde(rename_all = "snake_case")]
254#[non_exhaustive]
255pub enum SimpleColumnType {
256    /// 16-bit signed integer (`SMALLINT`).
257    SmallInt,
258    /// 32-bit signed integer (`INTEGER`). Supports `auto_increment`.
259    Integer,
260    /// 64-bit signed integer (`BIGINT`). Supports `auto_increment`.
261    BigInt,
262    /// 32-bit floating-point number (`REAL`).
263    Real,
264    /// 64-bit floating-point number (`DOUBLE PRECISION`).
265    DoublePrecision,
266
267    // Text types
268    /// Unbounded Unicode text (`TEXT`).
269    Text,
270
271    // Boolean type
272    /// Boolean true/false value (`BOOLEAN`).
273    Boolean,
274
275    // Date/Time types
276    /// Calendar date without time (`DATE`).
277    Date,
278    /// Time of day without date (`TIME`).
279    Time,
280    /// Date and time without timezone (`TIMESTAMP`).
281    Timestamp,
282    /// Date and time with timezone (`TIMESTAMPTZ`). Prefer this over `Timestamp`.
283    Timestamptz,
284    /// Time span / duration (`INTERVAL`).
285    Interval,
286
287    // Binary type
288    /// Variable-length binary data (`BYTEA`).
289    Bytea,
290
291    // UUID type
292    /// Universally unique identifier (`UUID`).
293    Uuid,
294
295    // JSON types
296    /// JSON value stored as text (`JSON`). Cross-backend compatible; prefer over `jsonb`.
297    Json,
298
299    // Network types
300    /// IPv4 or IPv6 host address (`INET`). PostgreSQL-specific.
301    Inet,
302    /// IPv4 or IPv6 network address (`CIDR`). PostgreSQL-specific.
303    Cidr,
304    /// MAC address (`MACADDR`). PostgreSQL-specific.
305    Macaddr,
306
307    // XML type
308    /// XML document (`XML`). PostgreSQL-specific.
309    Xml,
310}
311
312impl SimpleColumnType {
313    /// Returns the SQL type name for this simple column type.
314    #[must_use]
315    pub fn sql_type(&self) -> &'static str {
316        match self {
317            SimpleColumnType::SmallInt => "SMALLINT",
318            SimpleColumnType::Integer => "INTEGER",
319            SimpleColumnType::BigInt => "BIGINT",
320            SimpleColumnType::Real => "REAL",
321            SimpleColumnType::DoublePrecision => "DOUBLE PRECISION",
322            SimpleColumnType::Text => "TEXT",
323            SimpleColumnType::Boolean => "BOOLEAN",
324            SimpleColumnType::Date => "DATE",
325            SimpleColumnType::Time => "TIME",
326            SimpleColumnType::Timestamp => "TIMESTAMP",
327            SimpleColumnType::Timestamptz => "TIMESTAMPTZ",
328            SimpleColumnType::Interval => "INTERVAL",
329            SimpleColumnType::Bytea => "BYTEA",
330            SimpleColumnType::Uuid => "UUID",
331            SimpleColumnType::Json => "JSON",
332            SimpleColumnType::Inet => "INET",
333            SimpleColumnType::Cidr => "CIDR",
334            SimpleColumnType::Macaddr => "MACADDR",
335            SimpleColumnType::Xml => "XML",
336        }
337    }
338
339    /// Returns true if this type supports `auto_increment` (integer types only)
340    pub fn supports_auto_increment(&self) -> bool {
341        matches!(
342            self,
343            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt
344        )
345    }
346
347    /// Convert to human-readable display string
348    pub fn to_display_string(&self) -> String {
349        match self {
350            SimpleColumnType::SmallInt => "smallint".to_string(),
351            SimpleColumnType::Integer => "integer".to_string(),
352            SimpleColumnType::BigInt => "bigint".to_string(),
353            SimpleColumnType::Real => "real".to_string(),
354            SimpleColumnType::DoublePrecision => "double precision".to_string(),
355            SimpleColumnType::Text => "text".to_string(),
356            SimpleColumnType::Boolean => "boolean".to_string(),
357            SimpleColumnType::Date => "date".to_string(),
358            SimpleColumnType::Time => "time".to_string(),
359            SimpleColumnType::Timestamp => "timestamp".to_string(),
360            SimpleColumnType::Timestamptz => "timestamptz".to_string(),
361            SimpleColumnType::Interval => "interval".to_string(),
362            SimpleColumnType::Bytea => "bytea".to_string(),
363            SimpleColumnType::Uuid => "uuid".to_string(),
364            SimpleColumnType::Json => "json".to_string(),
365            SimpleColumnType::Inet => "inet".to_string(),
366            SimpleColumnType::Cidr => "cidr".to_string(),
367            SimpleColumnType::Macaddr => "macaddr".to_string(),
368            SimpleColumnType::Xml => "xml".to_string(),
369        }
370    }
371
372    /// Get the default fill value for this type
373    /// Returns None if no sensible default exists
374    pub fn default_fill_value(&self) -> &'static str {
375        match self {
376            SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt => {
377                "0"
378            }
379            SimpleColumnType::Real | SimpleColumnType::DoublePrecision => "0.0",
380            SimpleColumnType::Boolean => "false",
381            SimpleColumnType::Text | SimpleColumnType::Bytea => "''",
382            SimpleColumnType::Date => "'1970-01-01'",
383            SimpleColumnType::Time => "'00:00:00'",
384            SimpleColumnType::Timestamp | SimpleColumnType::Timestamptz => "CURRENT_TIMESTAMP",
385            SimpleColumnType::Interval => "'0'",
386            SimpleColumnType::Uuid => "'00000000-0000-0000-0000-000000000000'",
387            SimpleColumnType::Json => "'{}'",
388            SimpleColumnType::Inet | SimpleColumnType::Cidr => "'0.0.0.0'",
389            SimpleColumnType::Macaddr => "'00:00:00:00:00:00'",
390            SimpleColumnType::Xml => "'<xml/>'",
391        }
392    }
393}
394
395/// A single variant of an integer-backed enum, pairing a Rust-friendly name with its stored value.
396///
397/// Used inside [`EnumValues::Integer`] to define enums that are stored as `INTEGER` in the
398/// database. Leave gaps between values (e.g. 0, 10, 20) so new variants can be inserted later
399/// without renumbering.
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
402pub struct NumValue {
403    /// The variant name used in generated code (e.g. `"active"`).
404    pub name: String,
405    /// The integer value stored in the database column.
406    pub value: i64,
407}
408
409/// The set of allowed values for an enum column, either string-based or integer-based.
410///
411/// **String enums** map to a native `PostgreSQL` `ENUM` type. Adding or removing values requires a
412/// database migration (`ALTER TYPE`).
413///
414/// **Integer enums** are stored as `INTEGER`. New variants can be added to the model without any
415/// database migration because the underlying column type never changes.
416///
417/// Choose integer enums for expandable value sets (roles, priorities) and string enums for
418/// stable, human-readable status fields.
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
420#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
421#[serde(untagged)]
422pub enum EnumValues {
423    /// String enum: each variant is a plain string stored in a native DB enum type.
424    String(Vec<String>),
425    /// Integer enum: each variant has an explicit numeric value stored as `INTEGER`.
426    Integer(Vec<NumValue>),
427}
428
429impl EnumValues {
430    /// Check if this is a string enum
431    pub fn is_string(&self) -> bool {
432        matches!(self, EnumValues::String(_))
433    }
434
435    /// Check if this is an integer enum
436    pub fn is_integer(&self) -> bool {
437        matches!(self, EnumValues::Integer(_))
438    }
439
440    /// Get all variant names
441    pub fn variant_names(&self) -> Vec<&str> {
442        match self {
443            EnumValues::String(values) => values.iter().map(std::string::String::as_str).collect(),
444            EnumValues::Integer(values) => values.iter().map(|v| v.name.as_str()).collect(),
445        }
446    }
447
448    /// Get the number of variants
449    pub fn len(&self) -> usize {
450        match self {
451            EnumValues::String(values) => values.len(),
452            EnumValues::Integer(values) => values.len(),
453        }
454    }
455
456    /// Check if there are no variants
457    pub fn is_empty(&self) -> bool {
458        match self {
459            EnumValues::String(values) => values.is_empty(),
460            EnumValues::Integer(values) => values.is_empty(),
461        }
462    }
463
464    /// Get SQL values for CREATE TYPE ENUM (only for string enums)
465    /// Returns quoted strings like 'value1', 'value2'
466    pub fn to_sql_values(&self) -> Vec<String> {
467        match self {
468            EnumValues::String(values) => values
469                .iter()
470                .map(|s| format!("'{}'", s.replace('\'', "''")))
471                .collect(),
472            EnumValues::Integer(values) => values.iter().map(|v| v.value.to_string()).collect(),
473        }
474    }
475}
476
477impl From<Vec<String>> for EnumValues {
478    fn from(values: Vec<String>) -> Self {
479        EnumValues::String(values)
480    }
481}
482
483impl From<Vec<&str>> for EnumValues {
484    fn from(values: Vec<&str>) -> Self {
485        EnumValues::String(
486            values
487                .into_iter()
488                .map(std::string::ToString::to_string)
489                .collect(),
490        )
491    }
492}
493
494/// Parameterised SQL column types that require additional configuration beyond a simple keyword.
495///
496/// In JSON model files these are written as objects with a `"kind"` discriminant, for example
497/// `{"kind": "varchar", "length": 255}` or `{"kind": "enum", "name": "status", "values": [...]}`.
498///
499/// Use these via [`ColumnType::Complex`].
500///
501/// This enum is `#[non_exhaustive]`: new variants may be added in future releases.
502/// Downstream `match` expressions should include a wildcard arm.
503#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
504#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
505#[serde(rename_all = "snake_case", tag = "kind")]
506#[non_exhaustive]
507pub enum ComplexColumnType {
508    /// Variable-length character string with a maximum byte length (`VARCHAR(n)`).
509    Varchar { length: u32 },
510    /// Exact fixed-point number with configurable precision and scale (`NUMERIC(p, s)`).
511    Numeric { precision: u32, scale: u32 },
512    /// Fixed-length character string padded with spaces (`CHAR(n)`).
513    Char { length: u32 },
514    /// Escape hatch for database-specific types not covered by other variants.
515    /// Breaks cross-database portability; avoid unless absolutely necessary.
516    Custom { custom_type: String },
517    /// Named enum type. String enums map to a native DB enum; integer enums store as `INTEGER`.
518    /// See [`EnumValues`] for the distinction.
519    Enum { name: String, values: EnumValues },
520}
521
522impl ComplexColumnType {
523    /// Returns the base SQL type name for this complex column type, without parameters.
524    #[must_use]
525    pub fn sql_type(&self) -> &'static str {
526        match self {
527            ComplexColumnType::Varchar { .. } => "VARCHAR",
528            ComplexColumnType::Numeric { .. } => "NUMERIC",
529            ComplexColumnType::Char { .. } => "CHAR",
530            ComplexColumnType::Custom { .. } => "CUSTOM",
531            ComplexColumnType::Enum { .. } => "ENUM",
532        }
533    }
534
535    /// Convert to human-readable display string
536    pub fn to_display_string(&self) -> String {
537        match self {
538            ComplexColumnType::Varchar { length } => format!("varchar({length})"),
539            ComplexColumnType::Numeric { precision, scale } => {
540                format!("numeric({precision},{scale})")
541            }
542            ComplexColumnType::Char { length } => format!("char({length})"),
543            ComplexColumnType::Custom { custom_type } => custom_type.to_lowercase(),
544            ComplexColumnType::Enum { name, values } => {
545                if values.is_integer() {
546                    format!("enum<{name}> (integer)")
547                } else {
548                    format!("enum<{name}>")
549                }
550            }
551        }
552    }
553
554    /// Get the default fill value for this type.
555    pub fn default_fill_value(&self) -> &'static str {
556        match self {
557            ComplexColumnType::Numeric { .. } => "0",
558            ComplexColumnType::Varchar { .. }
559            | ComplexColumnType::Char { .. }
560            | ComplexColumnType::Custom { .. }
561            | ComplexColumnType::Enum { .. } => "''",
562        }
563    }
564}