Skip to main content

drizzle_types/postgres/ddl/
column.rs

1//! `PostgreSQL` columns: [`ColumnDef`] (const) and [`Column`] (runtime), with
2//! their generated-column and identity settings.
3
4use crate::alloc_prelude::*;
5
6#[cfg(feature = "serde")]
7use crate::serde_helpers::{cow_from_string, cow_option_from_string};
8
9// =============================================================================
10// Generated Column Types
11// =============================================================================
12
13/// Whether a generated column is `STORED` or `VIRTUAL` (`VIRTUAL` needs
14/// PostgreSQL 18 or later).
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
18pub enum GeneratedType {
19    /// Stored generated column
20    #[default]
21    Stored,
22    /// Virtual generated column
23    Virtual,
24}
25
26/// The `GENERATED ALWAYS AS (...)` part of a [`ColumnDef`].
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub struct GeneratedDef {
29    /// SQL expression for generation
30    pub expression: &'static str,
31    /// `STORED` or `VIRTUAL`
32    pub gen_type: GeneratedType,
33}
34
35impl GeneratedDef {
36    /// Creates a `STORED` generated column from a SQL expression.
37    #[must_use]
38    pub const fn stored(expression: &'static str) -> Self {
39        Self {
40            expression,
41            gen_type: GeneratedType::Stored,
42        }
43    }
44
45    /// Creates a `VIRTUAL` generated column from a SQL expression.
46    #[must_use]
47    pub const fn virtual_col(expression: &'static str) -> Self {
48        Self {
49            expression,
50            gen_type: GeneratedType::Virtual,
51        }
52    }
53
54    /// Converts to the runtime [`Generated`].
55    #[must_use]
56    pub const fn into_generated(self) -> Generated {
57        Generated {
58            expression: Cow::Borrowed(self.expression),
59            gen_type: self.gen_type,
60        }
61    }
62}
63
64/// The `GENERATED ALWAYS AS (...)` part of a [`Column`]. With `serde`, the
65/// fields are named `as` and `type`.
66#[derive(Clone, Debug, PartialEq, Eq)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
69pub struct Generated {
70    /// SQL expression for generation
71    #[cfg_attr(
72        feature = "serde",
73        serde(rename = "as", deserialize_with = "cow_from_string")
74    )]
75    pub expression: Cow<'static, str>,
76    /// `STORED` or `VIRTUAL`
77    #[cfg_attr(feature = "serde", serde(rename = "type"))]
78    pub gen_type: GeneratedType,
79}
80
81// =============================================================================
82// Identity Column Types
83// =============================================================================
84
85/// Whether an identity column is `GENERATED ALWAYS` or `GENERATED BY DEFAULT`.
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
89pub enum IdentityType {
90    /// GENERATED ALWAYS AS IDENTITY
91    #[default]
92    Always,
93    /// GENERATED BY DEFAULT AS IDENTITY
94    ByDefault,
95}
96
97/// The `GENERATED ... AS IDENTITY` part of a [`ColumnDef`], with the options
98/// of its backing sequence. Numeric options are SQL literals stored as
99/// strings, so they can hold any `bigint`.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
101pub struct IdentityDef {
102    /// Name of the backing sequence
103    pub name: &'static str,
104    /// Schema of the backing sequence
105    pub schema: Option<&'static str>,
106    /// `ALWAYS` or `BY DEFAULT`
107    pub type_: IdentityType,
108    /// `INCREMENT BY`
109    pub increment: Option<&'static str>,
110    /// `MINVALUE`
111    pub min_value: Option<&'static str>,
112    /// `MAXVALUE`
113    pub max_value: Option<&'static str>,
114    /// `START WITH`
115    pub start_with: Option<&'static str>,
116    /// `CACHE`
117    pub cache: Option<i32>,
118    /// `CYCLE`
119    pub cycle: bool,
120}
121
122impl IdentityDef {
123    /// Creates an identity backed by the sequence `name`, with default options.
124    #[must_use]
125    pub const fn new(name: &'static str, type_: IdentityType) -> Self {
126        Self {
127            name,
128            schema: None,
129            type_,
130            increment: None,
131            min_value: None,
132            max_value: None,
133            start_with: None,
134            cache: None,
135            cycle: false,
136        }
137    }
138
139    /// Sets the sequence's schema.
140    #[must_use]
141    pub const fn schema(self, schema: &'static str) -> Self {
142        Self {
143            schema: Some(schema),
144            ..self
145        }
146    }
147
148    /// Sets `INCREMENT BY`.
149    #[must_use]
150    pub const fn increment(self, value: &'static str) -> Self {
151        Self {
152            increment: Some(value),
153            ..self
154        }
155    }
156
157    /// Sets `MINVALUE`.
158    #[must_use]
159    pub const fn min_value(self, value: &'static str) -> Self {
160        Self {
161            min_value: Some(value),
162            ..self
163        }
164    }
165
166    /// Sets `MAXVALUE`.
167    #[must_use]
168    pub const fn max_value(self, value: &'static str) -> Self {
169        Self {
170            max_value: Some(value),
171            ..self
172        }
173    }
174
175    /// Sets `START WITH`.
176    #[must_use]
177    pub const fn start_with(self, value: &'static str) -> Self {
178        Self {
179            start_with: Some(value),
180            ..self
181        }
182    }
183
184    /// Sets `CACHE`.
185    #[must_use]
186    pub const fn cache(self, value: i32) -> Self {
187        Self {
188            cache: Some(value),
189            ..self
190        }
191    }
192
193    /// Sets `CYCLE`.
194    #[must_use]
195    pub const fn cycle(self) -> Self {
196        Self {
197            cycle: true,
198            ..self
199        }
200    }
201
202    /// Converts to the runtime [`Identity`].
203    #[must_use]
204    pub const fn into_identity(self) -> Identity {
205        Identity {
206            name: Cow::Borrowed(self.name),
207            schema: match self.schema {
208                Some(s) => Some(Cow::Borrowed(s)),
209                None => None,
210            },
211            type_: self.type_,
212            increment: match self.increment {
213                Some(s) => Some(Cow::Borrowed(s)),
214                None => None,
215            },
216            min_value: match self.min_value {
217                Some(s) => Some(Cow::Borrowed(s)),
218                None => None,
219            },
220            max_value: match self.max_value {
221                Some(s) => Some(Cow::Borrowed(s)),
222                None => None,
223            },
224            start_with: match self.start_with {
225                Some(s) => Some(Cow::Borrowed(s)),
226                None => None,
227            },
228            cache: self.cache,
229            cycle: if self.cycle { Some(true) } else { None },
230        }
231    }
232}
233
234/// The `GENERATED ... AS IDENTITY` part of a [`Column`]; see [`IdentityDef`].
235#[derive(Clone, Debug, PartialEq, Eq)]
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
238pub struct Identity {
239    /// Name of the backing sequence
240    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
241    pub name: Cow<'static, str>,
242
243    /// Schema of the backing sequence
244    #[cfg_attr(
245        feature = "serde",
246        serde(
247            default,
248            skip_serializing_if = "Option::is_none",
249            deserialize_with = "cow_option_from_string"
250        )
251    )]
252    pub schema: Option<Cow<'static, str>>,
253
254    /// `ALWAYS` or `BY DEFAULT` (serialized as `type`: `"always"` or
255    /// `"byDefault"`)
256    #[cfg_attr(feature = "serde", serde(rename = "type"))]
257    pub type_: IdentityType,
258
259    /// `INCREMENT BY`
260    #[cfg_attr(
261        feature = "serde",
262        serde(
263            default,
264            skip_serializing_if = "Option::is_none",
265            deserialize_with = "cow_option_from_string"
266        )
267    )]
268    pub increment: Option<Cow<'static, str>>,
269
270    /// `MINVALUE`
271    #[cfg_attr(
272        feature = "serde",
273        serde(
274            default,
275            skip_serializing_if = "Option::is_none",
276            deserialize_with = "cow_option_from_string"
277        )
278    )]
279    pub min_value: Option<Cow<'static, str>>,
280
281    /// `MAXVALUE`
282    #[cfg_attr(
283        feature = "serde",
284        serde(
285            default,
286            skip_serializing_if = "Option::is_none",
287            deserialize_with = "cow_option_from_string"
288        )
289    )]
290    pub max_value: Option<Cow<'static, str>>,
291
292    /// `START WITH`
293    #[cfg_attr(
294        feature = "serde",
295        serde(
296            default,
297            skip_serializing_if = "Option::is_none",
298            deserialize_with = "cow_option_from_string"
299        )
300    )]
301    pub start_with: Option<Cow<'static, str>>,
302
303    /// `CACHE`
304    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
305    pub cache: Option<i32>,
306
307    /// `CYCLE`
308    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
309    pub cycle: Option<bool>,
310}
311
312impl Identity {
313    /// Creates a `GENERATED ALWAYS AS IDENTITY` backed by the sequence `name`.
314    #[must_use]
315    pub fn always(name: impl Into<Cow<'static, str>>) -> Self {
316        Self {
317            name: name.into(),
318            schema: None,
319            type_: IdentityType::Always,
320            increment: None,
321            min_value: None,
322            max_value: None,
323            start_with: None,
324            cache: None,
325            cycle: None,
326        }
327    }
328
329    /// Creates a `GENERATED BY DEFAULT AS IDENTITY` backed by the sequence
330    /// `name`.
331    #[must_use]
332    pub fn by_default(name: impl Into<Cow<'static, str>>) -> Self {
333        Self {
334            name: name.into(),
335            schema: None,
336            type_: IdentityType::ByDefault,
337            increment: None,
338            min_value: None,
339            max_value: None,
340            start_with: None,
341            cache: None,
342            cycle: None,
343        }
344    }
345
346    /// Sets the sequence's schema.
347    #[must_use]
348    pub fn schema(mut self, schema: impl Into<Cow<'static, str>>) -> Self {
349        self.schema = Some(schema.into());
350        self
351    }
352}
353
354// =============================================================================
355// Const-friendly Definition Type
356// =============================================================================
357
358/// A column definition that can be built in a `const`.
359///
360/// Primary keys and unique constraints are separate entities
361/// ([`PrimaryKeyDef`](super::PrimaryKeyDef),
362/// [`UniqueConstraintDef`](super::UniqueConstraintDef)).
363///
364/// # Examples
365///
366/// ```
367/// use drizzle_types::postgres::ddl::{ColumnDef, IdentityDef, IdentityType};
368///
369/// const ID: ColumnDef = ColumnDef::new("public", "users", "id", "integer")
370///     .not_null()
371///     .identity(IdentityDef::new("users_id_seq", IdentityType::Always));
372///
373/// let column = ID.into_column();
374/// assert!(column.identity.is_some());
375/// ```
376#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
377pub struct ColumnDef {
378    /// Schema name
379    pub schema: &'static str,
380    /// Parent table name
381    pub table: &'static str,
382    /// Column name
383    pub name: &'static str,
384    /// SQL type, such as `"integer"` or `"varchar(255)"`; for an enum, the
385    /// enum's type name
386    pub sql_type: &'static str,
387    /// Schema of a custom or enum type
388    pub type_schema: Option<&'static str>,
389    /// `NOT NULL`
390    pub not_null: bool,
391    /// `DEFAULT` value as SQL
392    pub default: Option<&'static str>,
393    /// Generated column configuration
394    pub generated: Option<GeneratedDef>,
395    /// Identity column configuration
396    pub identity: Option<IdentityDef>,
397    /// Number of array dimensions: `Some(1)` renders `type[]`
398    pub dimensions: Option<i32>,
399    /// Column comment, emitted as `COMMENT ON COLUMN`
400    pub comment: Option<&'static str>,
401    /// Collation name (e.g. `"en_US"`, `"C"`, `"POSIX"`, or a custom
402    /// `CREATE COLLATION` name). `None` uses the type's default collation and
403    /// emits no `COLLATE` clause.
404    pub collate: Option<&'static str>,
405}
406
407impl ColumnDef {
408    /// Creates a nullable column with no default.
409    #[must_use]
410    pub const fn new(
411        schema: &'static str,
412        table: &'static str,
413        name: &'static str,
414        sql_type: &'static str,
415    ) -> Self {
416        Self {
417            schema,
418            table,
419            name,
420            sql_type,
421            type_schema: None,
422            not_null: false,
423            default: None,
424            generated: None,
425            identity: None,
426            dimensions: None,
427            comment: None,
428            collate: None,
429        }
430    }
431
432    /// Sets the schema of a custom or enum type.
433    #[must_use]
434    pub const fn type_schema(self, schema: &'static str) -> Self {
435        Self {
436            type_schema: Some(schema),
437            ..self
438        }
439    }
440
441    /// Adds `NOT NULL`.
442    #[must_use]
443    pub const fn not_null(self) -> Self {
444        Self {
445            not_null: true,
446            ..self
447        }
448    }
449
450    /// Sets the `DEFAULT` value, written as SQL.
451    #[must_use]
452    pub const fn default_value(self, value: &'static str) -> Self {
453        Self {
454            default: Some(value),
455            ..self
456        }
457    }
458
459    /// Makes the column `GENERATED ALWAYS AS (expression) STORED`.
460    #[must_use]
461    pub const fn generated_stored(self, expression: &'static str) -> Self {
462        Self {
463            generated: Some(GeneratedDef::stored(expression)),
464            ..self
465        }
466    }
467
468    /// Makes the column `GENERATED ALWAYS AS (expression) VIRTUAL`.
469    #[must_use]
470    pub const fn generated_virtual(self, expression: &'static str) -> Self {
471        Self {
472            generated: Some(GeneratedDef::virtual_col(expression)),
473            ..self
474        }
475    }
476
477    /// Makes the column an identity column.
478    #[must_use]
479    pub const fn identity(self, identity: IdentityDef) -> Self {
480        Self {
481            identity: Some(identity),
482            ..self
483        }
484    }
485
486    /// Sets the number of array dimensions.
487    #[must_use]
488    pub const fn dimensions(self, dims: i32) -> Self {
489        Self {
490            dimensions: Some(dims),
491            ..self
492        }
493    }
494
495    /// Sets the column comment.
496    #[must_use]
497    pub const fn comment(self, comment: &'static str) -> Self {
498        Self {
499            comment: Some(comment),
500            ..self
501        }
502    }
503
504    /// Sets the column's collation.
505    ///
506    /// Pass the bare name, such as `en_US`; it is rendered quoted, as in
507    /// `COLLATE "en_US"`.
508    #[must_use]
509    pub const fn collate(self, name: &'static str) -> Self {
510        Self {
511            collate: Some(name),
512            ..self
513        }
514    }
515
516    /// Converts to the runtime [`Column`].
517    #[must_use]
518    pub const fn into_column(self) -> Column {
519        Column {
520            schema: Cow::Borrowed(self.schema),
521            table: Cow::Borrowed(self.table),
522            name: Cow::Borrowed(self.name),
523            sql_type: Cow::Borrowed(self.sql_type),
524            type_schema: match self.type_schema {
525                Some(s) => Some(Cow::Borrowed(s)),
526                None => None,
527            },
528            not_null: self.not_null,
529            default: match self.default {
530                Some(s) => Some(Cow::Borrowed(s)),
531                None => None,
532            },
533            generated: match self.generated {
534                Some(g) => Some(g.into_generated()),
535                None => None,
536            },
537            identity: match self.identity {
538                Some(i) => Some(i.into_identity()),
539                None => None,
540            },
541            dimensions: self.dimensions,
542            comment: match self.comment {
543                Some(s) => Some(Cow::Borrowed(s)),
544                None => None,
545            },
546            collate: match self.collate {
547                Some(s) => Some(Cow::Borrowed(s)),
548                None => None,
549            },
550            ordinal_position: None,
551        }
552    }
553}
554
555impl Default for ColumnDef {
556    fn default() -> Self {
557        Self::new("public", "", "", "")
558    }
559}
560
561// =============================================================================
562// Runtime Type for Serde
563// =============================================================================
564
565/// A table column, as stored in migration snapshots.
566#[derive(Clone, Debug, PartialEq, Eq)]
567#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
568#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
569pub struct Column {
570    /// Schema name
571    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
572    pub schema: Cow<'static, str>,
573
574    /// Parent table name
575    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
576    pub table: Cow<'static, str>,
577
578    /// Column name
579    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
580    pub name: Cow<'static, str>,
581
582    /// SQL type, such as `"integer"` (serialized as `type`)
583    #[cfg_attr(
584        feature = "serde",
585        serde(rename = "type", deserialize_with = "cow_from_string")
586    )]
587    pub sql_type: Cow<'static, str>,
588
589    /// Schema of a custom or enum type
590    #[cfg_attr(
591        feature = "serde",
592        serde(
593            default,
594            skip_serializing_if = "Option::is_none",
595            deserialize_with = "cow_option_from_string"
596        )
597    )]
598    pub type_schema: Option<Cow<'static, str>>,
599
600    /// `NOT NULL`
601    #[cfg_attr(feature = "serde", serde(default))]
602    pub not_null: bool,
603
604    /// `DEFAULT` value as SQL
605    #[cfg_attr(
606        feature = "serde",
607        serde(default, deserialize_with = "cow_option_from_string")
608    )]
609    pub default: Option<Cow<'static, str>>,
610
611    /// Generated column configuration
612    #[cfg_attr(feature = "serde", serde(default))]
613    pub generated: Option<Generated>,
614
615    /// Identity column configuration
616    #[cfg_attr(feature = "serde", serde(default))]
617    pub identity: Option<Identity>,
618
619    /// Number of array dimensions
620    #[cfg_attr(feature = "serde", serde(default))]
621    pub dimensions: Option<i32>,
622
623    /// Column comment, emitted as `COMMENT ON COLUMN`
624    #[cfg_attr(
625        feature = "serde",
626        serde(
627            default,
628            skip_serializing_if = "Option::is_none",
629            deserialize_with = "cow_option_from_string"
630        )
631    )]
632    pub comment: Option<Cow<'static, str>>,
633
634    /// Collation name (e.g. `"en_US"`, `"C"`). `None` means the database
635    /// default collation and no `COLLATE` clause is emitted.
636    #[cfg_attr(
637        feature = "serde",
638        serde(default, deserialize_with = "cow_option_from_string")
639    )]
640    pub collate: Option<Cow<'static, str>>,
641
642    /// Ordinal position within the table (1-based).
643    ///
644    /// This is primarily populated by introspection and used for stable codegen ordering.
645    #[cfg_attr(
646        feature = "serde",
647        serde(default, skip_serializing_if = "Option::is_none")
648    )]
649    pub ordinal_position: Option<i32>,
650}
651
652impl Column {
653    /// Creates a nullable column with no default.
654    #[must_use]
655    pub fn new(
656        schema: impl Into<Cow<'static, str>>,
657        table: impl Into<Cow<'static, str>>,
658        name: impl Into<Cow<'static, str>>,
659        sql_type: impl Into<Cow<'static, str>>,
660    ) -> Self {
661        Self {
662            schema: schema.into(),
663            table: table.into(),
664            name: name.into(),
665            sql_type: sql_type.into(),
666            type_schema: None,
667            not_null: false,
668            default: None,
669            generated: None,
670            identity: None,
671            dimensions: None,
672            comment: None,
673            collate: None,
674            ordinal_position: None,
675        }
676    }
677
678    /// Adds `NOT NULL`.
679    #[must_use]
680    pub const fn not_null(mut self) -> Self {
681        self.not_null = true;
682        self
683    }
684
685    /// Sets the `DEFAULT` value, written as SQL.
686    #[must_use]
687    pub fn default_value(mut self, value: impl Into<Cow<'static, str>>) -> Self {
688        self.default = Some(value.into());
689        self
690    }
691
692    /// Makes the column an identity column.
693    #[must_use]
694    pub fn identity(mut self, identity: Identity) -> Self {
695        self.identity = Some(identity);
696        self
697    }
698
699    /// Sets the column comment.
700    #[must_use]
701    pub fn comment(mut self, comment: impl Into<Cow<'static, str>>) -> Self {
702        self.comment = Some(comment.into());
703        self
704    }
705
706    /// Returns the schema name.
707    #[inline]
708    #[must_use]
709    pub fn schema(&self) -> &str {
710        &self.schema
711    }
712
713    /// Returns the table name.
714    #[inline]
715    #[must_use]
716    pub fn table(&self) -> &str {
717        &self.table
718    }
719
720    /// Returns the column name.
721    #[inline]
722    #[must_use]
723    pub fn name(&self) -> &str {
724        &self.name
725    }
726
727    /// Returns the SQL type.
728    #[inline]
729    #[must_use]
730    pub fn sql_type(&self) -> &str {
731        &self.sql_type
732    }
733}
734
735impl Default for Column {
736    fn default() -> Self {
737        Self::new("public", "", "", "")
738    }
739}
740
741impl From<ColumnDef> for Column {
742    fn from(def: ColumnDef) -> Self {
743        def.into_column()
744    }
745}
746
747#[cfg(test)]
748mod tests {
749    use super::*;
750
751    #[test]
752    fn test_const_column_def() {
753        const COLDEF: ColumnDef = ColumnDef::new("public", "users", "id", "INTEGER").not_null();
754
755        assert_eq!(COLDEF.schema, "public");
756        assert_eq!(COLDEF.name, "id");
757        assert_eq!(COLDEF.table, "users");
758        assert_eq!(COLDEF.sql_type, "INTEGER");
759        const {
760            assert!(COLDEF.not_null);
761        }
762
763        let col: Column = COLDEF.into_column();
764
765        assert_eq!(col.schema, Cow::Borrowed("public"));
766        assert_eq!(col.name, Cow::Borrowed("id"));
767        assert_eq!(col.table, Cow::Borrowed("users"));
768        assert_eq!(col.sql_type, Cow::Borrowed("INTEGER"));
769        assert!(col.not_null);
770    }
771
772    #[test]
773    fn test_identity_column() {
774        const IDENTITY_DEF: IdentityDef = IdentityDef::new("users_id_seq", IdentityType::Always)
775            .increment("1")
776            .start_with("1");
777
778        const COL: ColumnDef =
779            ColumnDef::new("public", "users", "id", "INTEGER").identity(IDENTITY_DEF);
780
781        assert!(COL.identity.is_some());
782    }
783
784    #[test]
785    fn test_generated_virtual_column() {
786        const COL: ColumnDef = ColumnDef::new("public", "users", "name_len", "INTEGER")
787            .generated_virtual("length(name)");
788
789        assert_eq!(
790            COL.generated.expect("generated column").gen_type,
791            GeneratedType::Virtual
792        );
793
794        let col = COL.into_column();
795        assert_eq!(
796            col.generated.expect("generated column").gen_type,
797            GeneratedType::Virtual
798        );
799    }
800
801    #[cfg(feature = "serde")]
802    #[test]
803    fn test_serde_roundtrip() {
804        let col = Column::new("public", "users", "id", "INTEGER");
805        let json = serde_json::to_string(&col).unwrap();
806        let parsed: Column = serde_json::from_str(&json).unwrap();
807        assert_eq!(parsed.name(), "id");
808    }
809}