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            skip_serializing_if = "Option::is_none",
247            deserialize_with = "cow_option_from_string"
248        )
249    )]
250    pub schema: Option<Cow<'static, str>>,
251
252    /// Identity type: always or `by_default`
253    #[cfg_attr(feature = "serde", serde(rename = "type"))]
254    pub type_: IdentityType,
255
256    /// Increment value
257    #[cfg_attr(
258        feature = "serde",
259        serde(
260            skip_serializing_if = "Option::is_none",
261            deserialize_with = "cow_option_from_string"
262        )
263    )]
264    pub increment: Option<Cow<'static, str>>,
265
266    /// Minimum value
267    #[cfg_attr(
268        feature = "serde",
269        serde(
270            skip_serializing_if = "Option::is_none",
271            deserialize_with = "cow_option_from_string"
272        )
273    )]
274    pub min_value: Option<Cow<'static, str>>,
275
276    /// Maximum value
277    #[cfg_attr(
278        feature = "serde",
279        serde(
280            skip_serializing_if = "Option::is_none",
281            deserialize_with = "cow_option_from_string"
282        )
283    )]
284    pub max_value: Option<Cow<'static, str>>,
285
286    /// Start value
287    #[cfg_attr(
288        feature = "serde",
289        serde(
290            skip_serializing_if = "Option::is_none",
291            deserialize_with = "cow_option_from_string"
292        )
293    )]
294    pub start_with: Option<Cow<'static, str>>,
295
296    /// Cache value
297    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
298    pub cache: Option<i32>,
299
300    /// Cycle flag
301    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
302    pub cycle: Option<bool>,
303}
304
305impl Identity {
306    /// Create a new identity with GENERATED ALWAYS AS IDENTITY
307    #[must_use]
308    pub fn always(name: impl Into<Cow<'static, str>>) -> Self {
309        Self {
310            name: name.into(),
311            schema: None,
312            type_: IdentityType::Always,
313            increment: None,
314            min_value: None,
315            max_value: None,
316            start_with: None,
317            cache: None,
318            cycle: None,
319        }
320    }
321
322    /// Create a new identity with GENERATED BY DEFAULT AS IDENTITY
323    #[must_use]
324    pub fn by_default(name: impl Into<Cow<'static, str>>) -> Self {
325        Self {
326            name: name.into(),
327            schema: None,
328            type_: IdentityType::ByDefault,
329            increment: None,
330            min_value: None,
331            max_value: None,
332            start_with: None,
333            cache: None,
334            cycle: None,
335        }
336    }
337
338    /// Set schema
339    #[must_use]
340    pub fn schema(mut self, schema: impl Into<Cow<'static, str>>) -> Self {
341        self.schema = Some(schema.into());
342        self
343    }
344}
345
346// =============================================================================
347// Const-friendly Definition Type
348// =============================================================================
349
350/// Const-friendly column definition for compile-time schema definitions.
351#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
352pub struct ColumnDef {
353    /// Schema name
354    pub schema: &'static str,
355    /// Parent table name
356    pub table: &'static str,
357    /// Column name
358    pub name: &'static str,
359    /// SQL type (e.g., "INTEGER", "TEXT", "VARCHAR")
360    pub sql_type: &'static str,
361    /// Type schema (for custom types)
362    pub type_schema: Option<&'static str>,
363    /// Is this column NOT NULL?
364    pub not_null: bool,
365    /// Default value as string (if any)
366    pub default: Option<&'static str>,
367    /// Generated column configuration
368    pub generated: Option<GeneratedDef>,
369    /// Identity column configuration
370    pub identity: Option<IdentityDef>,
371    /// Array dimensions (for array types)
372    pub dimensions: Option<i32>,
373    /// Column comment emitted through COMMENT ON COLUMN.
374    pub comment: Option<&'static str>,
375    /// Collation name (e.g. `"en_US"`, `"C"`, `"POSIX"`, or any custom
376    /// `CREATE COLLATION` value). `None` means "use the database default
377    /// collation for this column type" and no `COLLATE` clause is emitted.
378    pub collate: Option<&'static str>,
379}
380
381impl ColumnDef {
382    /// Create a new column definition
383    #[must_use]
384    pub const fn new(
385        schema: &'static str,
386        table: &'static str,
387        name: &'static str,
388        sql_type: &'static str,
389    ) -> Self {
390        Self {
391            schema,
392            table,
393            name,
394            sql_type,
395            type_schema: None,
396            not_null: false,
397            default: None,
398            generated: None,
399            identity: None,
400            dimensions: None,
401            comment: None,
402            collate: None,
403        }
404    }
405
406    /// Set type schema (for custom types)
407    #[must_use]
408    pub const fn type_schema(self, schema: &'static str) -> Self {
409        Self {
410            type_schema: Some(schema),
411            ..self
412        }
413    }
414
415    /// Set NOT NULL constraint
416    #[must_use]
417    pub const fn not_null(self) -> Self {
418        Self {
419            not_null: true,
420            ..self
421        }
422    }
423
424    /// Set default value
425    #[must_use]
426    pub const fn default_value(self, value: &'static str) -> Self {
427        Self {
428            default: Some(value),
429            ..self
430        }
431    }
432
433    /// Set as generated stored column
434    #[must_use]
435    pub const fn generated_stored(self, expression: &'static str) -> Self {
436        Self {
437            generated: Some(GeneratedDef::stored(expression)),
438            ..self
439        }
440    }
441
442    /// Set as generated virtual column
443    #[must_use]
444    pub const fn generated_virtual(self, expression: &'static str) -> Self {
445        Self {
446            generated: Some(GeneratedDef::virtual_col(expression)),
447            ..self
448        }
449    }
450
451    /// Set as identity column
452    #[must_use]
453    pub const fn identity(self, identity: IdentityDef) -> Self {
454        Self {
455            identity: Some(identity),
456            ..self
457        }
458    }
459
460    /// Set array dimensions
461    #[must_use]
462    pub const fn dimensions(self, dims: i32) -> Self {
463        Self {
464            dimensions: Some(dims),
465            ..self
466        }
467    }
468
469    /// Set the column comment.
470    #[must_use]
471    pub const fn comment(self, comment: &'static str) -> Self {
472        Self {
473            comment: Some(comment),
474            ..self
475        }
476    }
477
478    /// Set the collation for this column.
479    ///
480    /// PostgreSQL treats `COLLATE` identifiers as quoted names — e.g.
481    /// `COLLATE "en_US"`, `COLLATE "C"`, `COLLATE "POSIX"`. Pass the bare
482    /// name here; the DDL emitter wraps it in double quotes.
483    #[must_use]
484    pub const fn collate(self, name: &'static str) -> Self {
485        Self {
486            collate: Some(name),
487            ..self
488        }
489    }
490
491    /// Convert to runtime [`Column`] type
492    ///
493    /// Note: This method cannot be const because it needs to convert nested Option types
494    /// (generated and identity) which require runtime method calls.
495    #[must_use]
496    pub const fn into_column(self) -> Column {
497        Column {
498            schema: Cow::Borrowed(self.schema),
499            table: Cow::Borrowed(self.table),
500            name: Cow::Borrowed(self.name),
501            sql_type: Cow::Borrowed(self.sql_type),
502            type_schema: match self.type_schema {
503                Some(s) => Some(Cow::Borrowed(s)),
504                None => None,
505            },
506            not_null: self.not_null,
507            default: match self.default {
508                Some(s) => Some(Cow::Borrowed(s)),
509                None => None,
510            },
511            generated: match self.generated {
512                Some(g) => Some(g.into_generated()),
513                None => None,
514            },
515            identity: match self.identity {
516                Some(i) => Some(i.into_identity()),
517                None => None,
518            },
519            dimensions: self.dimensions,
520            comment: match self.comment {
521                Some(s) => Some(Cow::Borrowed(s)),
522                None => None,
523            },
524            collate: match self.collate {
525                Some(s) => Some(Cow::Borrowed(s)),
526                None => None,
527            },
528            ordinal_position: None,
529        }
530    }
531}
532
533impl Default for ColumnDef {
534    fn default() -> Self {
535        Self::new("public", "", "", "")
536    }
537}
538
539// =============================================================================
540// Runtime Type for Serde
541// =============================================================================
542
543/// Runtime column entity for serde serialization.
544#[derive(Clone, Debug, PartialEq, Eq)]
545#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
546#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
547pub struct Column {
548    /// Schema name
549    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
550    pub schema: Cow<'static, str>,
551
552    /// Parent table name
553    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
554    pub table: Cow<'static, str>,
555
556    /// Column name
557    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
558    pub name: Cow<'static, str>,
559
560    /// SQL type (e.g., "INTEGER", "TEXT", "VARCHAR")
561    #[cfg_attr(
562        feature = "serde",
563        serde(rename = "type", deserialize_with = "cow_from_string")
564    )]
565    pub sql_type: Cow<'static, str>,
566
567    /// Type schema (for custom types)
568    #[cfg_attr(
569        feature = "serde",
570        serde(
571            default,
572            skip_serializing_if = "Option::is_none",
573            deserialize_with = "cow_option_from_string"
574        )
575    )]
576    pub type_schema: Option<Cow<'static, str>>,
577
578    /// Is this column NOT NULL?
579    #[cfg_attr(feature = "serde", serde(default))]
580    pub not_null: bool,
581
582    /// Default value as string
583    #[cfg_attr(
584        feature = "serde",
585        serde(default, deserialize_with = "cow_option_from_string")
586    )]
587    pub default: Option<Cow<'static, str>>,
588
589    /// Generated column configuration
590    #[cfg_attr(feature = "serde", serde(default))]
591    pub generated: Option<Generated>,
592
593    /// Identity column configuration
594    #[cfg_attr(feature = "serde", serde(default))]
595    pub identity: Option<Identity>,
596
597    /// Array dimensions (for array types)
598    #[cfg_attr(feature = "serde", serde(default))]
599    pub dimensions: Option<i32>,
600
601    /// Column comment emitted through COMMENT ON COLUMN.
602    #[cfg_attr(
603        feature = "serde",
604        serde(
605            default,
606            skip_serializing_if = "Option::is_none",
607            deserialize_with = "cow_option_from_string"
608        )
609    )]
610    pub comment: Option<Cow<'static, str>>,
611
612    /// Collation name (e.g. `"en_US"`, `"C"`). `None` means the database
613    /// default collation and no `COLLATE` clause is emitted.
614    #[cfg_attr(
615        feature = "serde",
616        serde(default, deserialize_with = "cow_option_from_string")
617    )]
618    pub collate: Option<Cow<'static, str>>,
619
620    /// Ordinal position within the table (1-based).
621    ///
622    /// This is primarily populated by introspection and used for stable codegen ordering.
623    #[cfg_attr(
624        feature = "serde",
625        serde(default, skip_serializing_if = "Option::is_none")
626    )]
627    pub ordinal_position: Option<i32>,
628}
629
630impl Column {
631    /// Create a new column (runtime)
632    #[must_use]
633    pub fn new(
634        schema: impl Into<Cow<'static, str>>,
635        table: impl Into<Cow<'static, str>>,
636        name: impl Into<Cow<'static, str>>,
637        sql_type: impl Into<Cow<'static, str>>,
638    ) -> Self {
639        Self {
640            schema: schema.into(),
641            table: table.into(),
642            name: name.into(),
643            sql_type: sql_type.into(),
644            type_schema: None,
645            not_null: false,
646            default: None,
647            generated: None,
648            identity: None,
649            dimensions: None,
650            comment: None,
651            collate: None,
652            ordinal_position: None,
653        }
654    }
655
656    /// Set NOT NULL
657    #[must_use]
658    pub const fn not_null(mut self) -> Self {
659        self.not_null = true;
660        self
661    }
662
663    /// Set default value
664    #[must_use]
665    pub fn default_value(mut self, value: impl Into<Cow<'static, str>>) -> Self {
666        self.default = Some(value.into());
667        self
668    }
669
670    /// Set identity configuration
671    #[must_use]
672    pub fn identity(mut self, identity: Identity) -> Self {
673        self.identity = Some(identity);
674        self
675    }
676
677    /// Set the column comment.
678    #[must_use]
679    pub fn comment(mut self, comment: impl Into<Cow<'static, str>>) -> Self {
680        self.comment = Some(comment.into());
681        self
682    }
683
684    /// Get the schema name
685    #[inline]
686    #[must_use]
687    pub fn schema(&self) -> &str {
688        &self.schema
689    }
690
691    /// Get the table name
692    #[inline]
693    #[must_use]
694    pub fn table(&self) -> &str {
695        &self.table
696    }
697
698    /// Get the column name
699    #[inline]
700    #[must_use]
701    pub fn name(&self) -> &str {
702        &self.name
703    }
704
705    /// Get the SQL type
706    #[inline]
707    #[must_use]
708    pub fn sql_type(&self) -> &str {
709        &self.sql_type
710    }
711}
712
713impl Default for Column {
714    fn default() -> Self {
715        Self::new("public", "", "", "")
716    }
717}
718
719impl From<ColumnDef> for Column {
720    fn from(def: ColumnDef) -> Self {
721        def.into_column()
722    }
723}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728
729    #[test]
730    fn test_const_column_def() {
731        const COLDEF: ColumnDef = ColumnDef::new("public", "users", "id", "INTEGER").not_null();
732
733        assert_eq!(COLDEF.schema, "public");
734        assert_eq!(COLDEF.name, "id");
735        assert_eq!(COLDEF.table, "users");
736        assert_eq!(COLDEF.sql_type, "INTEGER");
737        const {
738            assert!(COLDEF.not_null);
739        }
740
741        let col: Column = COLDEF.into_column();
742
743        assert_eq!(col.schema, Cow::Borrowed("public"));
744        assert_eq!(col.name, Cow::Borrowed("id"));
745        assert_eq!(col.table, Cow::Borrowed("users"));
746        assert_eq!(col.sql_type, Cow::Borrowed("INTEGER"));
747        assert!(col.not_null);
748    }
749
750    #[test]
751    fn test_identity_column() {
752        const IDENTITY_DEF: IdentityDef = IdentityDef::new("users_id_seq", IdentityType::Always)
753            .increment("1")
754            .start_with("1");
755
756        const COL: ColumnDef =
757            ColumnDef::new("public", "users", "id", "INTEGER").identity(IDENTITY_DEF);
758
759        assert!(COL.identity.is_some());
760    }
761
762    #[test]
763    fn test_generated_virtual_column() {
764        const COL: ColumnDef = ColumnDef::new("public", "users", "name_len", "INTEGER")
765            .generated_virtual("length(name)");
766
767        assert_eq!(
768            COL.generated.expect("generated column").gen_type,
769            GeneratedType::Virtual
770        );
771
772        let col = COL.into_column();
773        assert_eq!(
774            col.generated.expect("generated column").gen_type,
775            GeneratedType::Virtual
776        );
777    }
778
779    #[cfg(feature = "serde")]
780    #[test]
781    fn test_serde_roundtrip() {
782        let col = Column::new("public", "users", "id", "INTEGER");
783        let json = serde_json::to_string(&col).unwrap();
784        let parsed: Column = serde_json::from_str(&json).unwrap();
785        assert_eq!(parsed.name(), "id");
786    }
787}