Skip to main content

drizzle_types/postgres/ddl/
column.rs

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