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