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