Skip to main content

toasty_core/schema/db/
column.rs

1use super::{TableId, Type, table};
2use crate::stmt;
3
4use std::fmt;
5
6/// A column in a database table.
7///
8/// Each column has a logical type ([`stmt::Type`]) used by the query engine and
9/// a storage type ([`Type`]) representing how the value is stored in the database.
10///
11/// # Examples
12///
13/// ```ignore
14/// use toasty_core::schema::db::{Column, ColumnId, TableId, Type};
15/// use toasty_core::stmt;
16///
17/// let column = Column {
18///     id: ColumnId { table: TableId(0), index: 0 },
19///     name: "email".to_string(),
20///     ty: stmt::Type::String,
21///     storage_ty: Type::VarChar(255),
22///     nullable: false,
23///     primary_key: false,
24///     auto_increment: false,
25/// };
26///
27/// assert_eq!(column.name, "email");
28/// assert!(!column.nullable);
29/// ```
30#[derive(Debug, Clone, PartialEq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct Column {
33    /// Uniquely identifies the column in the schema.
34    pub id: ColumnId,
35
36    /// The name of the column in the database.
37    pub name: String,
38
39    /// The column type, from Toasty's point of view.
40    pub ty: stmt::Type,
41
42    /// The database storage type of the column.
43    pub storage_ty: Type,
44
45    /// Whether or not the column is nullable
46    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
47    pub nullable: bool,
48
49    /// True if the column is part of the table's primary key
50    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
51    pub primary_key: bool,
52
53    /// True if the column is an integer that should be auto-incremented
54    /// with each insertion of a new row. This should be false if a `storage_ty`
55    /// of type `Serial` is used.
56    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
57    pub auto_increment: bool,
58
59    /// True if the column tracks an OCC version counter.
60    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))]
61    pub versionable: bool,
62}
63
64impl Column {
65    /// Whether this column stores a `#[document]` embed: a bare document
66    /// (`stmt::Type::Object`) or a collection of documents (`List(Object)`).
67    pub fn is_document(&self) -> bool {
68        match &self.ty {
69            stmt::Type::Object => true,
70            stmt::Type::List(elem) => matches!(**elem, stmt::Type::Object),
71            _ => false,
72        }
73    }
74}
75
76#[cfg(feature = "serde")]
77fn is_false(b: &bool) -> bool {
78    !*b
79}
80
81/// Uniquely identifies a column within a schema.
82///
83/// A `ColumnId` combines the [`TableId`] of the owning table with the column's
84/// positional index within that table's column list.
85///
86/// # Examples
87///
88/// ```ignore
89/// use toasty_core::schema::db::{ColumnId, TableId};
90///
91/// let id = ColumnId { table: TableId(0), index: 2 };
92/// assert_eq!(id.index, 2);
93/// ```
94#[derive(PartialEq, Eq, Clone, Copy, Hash)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub struct ColumnId {
97    /// The table this column belongs to.
98    pub table: TableId,
99    /// Zero-based position of this column in the table's column list.
100    pub index: usize,
101}
102
103impl ColumnId {
104    pub(crate) fn placeholder() -> Self {
105        Self {
106            table: table::TableId::placeholder(),
107            index: usize::MAX,
108        }
109    }
110}
111
112impl From<&Column> for ColumnId {
113    fn from(value: &Column) -> Self {
114        value.id
115    }
116}
117
118impl fmt::Debug for ColumnId {
119    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(fmt, "ColumnId({}/{})", self.table.0, self.index)
121    }
122}
123
124#[cfg(all(test, feature = "serde"))]
125mod serde_tests {
126    use crate::schema::db::{Column, ColumnId, TableId, Type};
127    use crate::stmt;
128
129    fn base_column() -> Column {
130        Column {
131            id: ColumnId {
132                table: TableId(0),
133                index: 0,
134            },
135            name: "test".to_string(),
136            ty: stmt::Type::String,
137            storage_ty: Type::Text,
138            nullable: false,
139            primary_key: false,
140            auto_increment: false,
141            versionable: false,
142        }
143    }
144
145    #[test]
146    fn false_booleans_are_omitted() {
147        let toml = toml::to_string(&base_column()).unwrap();
148        assert!(!toml.contains("nullable"), "toml: {toml}");
149        assert!(!toml.contains("primary_key"), "toml: {toml}");
150        assert!(!toml.contains("auto_increment"), "toml: {toml}");
151        assert!(!toml.contains("versionable"), "toml: {toml}");
152    }
153
154    #[test]
155    fn nullable_true_is_included() {
156        let col = Column {
157            nullable: true,
158            ..base_column()
159        };
160        let toml = toml::to_string(&col).unwrap();
161        assert!(toml.contains("nullable = true"), "toml: {toml}");
162    }
163
164    #[test]
165    fn primary_key_true_is_included() {
166        let col = Column {
167            primary_key: true,
168            ..base_column()
169        };
170        let toml = toml::to_string(&col).unwrap();
171        assert!(toml.contains("primary_key = true"), "toml: {toml}");
172    }
173
174    #[test]
175    fn auto_increment_true_is_included() {
176        let col = Column {
177            auto_increment: true,
178            ..base_column()
179        };
180        let toml = toml::to_string(&col).unwrap();
181        assert!(toml.contains("auto_increment = true"), "toml: {toml}");
182    }
183
184    #[test]
185    fn missing_bool_fields_deserialize_as_false() {
186        let toml = "name = \"test\"\nty = \"String\"\nstorage_ty = \"Text\"\n\n[id]\ntable = 0\nindex = 0\n";
187        let col: Column = toml::from_str(toml).unwrap();
188        assert!(!col.nullable);
189        assert!(!col.primary_key);
190        assert!(!col.auto_increment);
191        assert!(!col.versionable);
192    }
193
194    #[test]
195    fn round_trip_all_true() {
196        let original = Column {
197            nullable: true,
198            primary_key: true,
199            auto_increment: true,
200            ..base_column()
201        };
202        let decoded: Column = toml::from_str(&toml::to_string(&original).unwrap()).unwrap();
203        assert_eq!(original, decoded);
204    }
205}