Skip to main content

wasm_dbms_api/dbms/table/
column_def.rs

1use serde::{Deserialize, Serialize};
2
3use crate::dbms::types::DataTypeKind;
4use crate::dbms::value::Value;
5
6/// Constructor for a column's default value.
7///
8/// Stored as a function pointer rather than a `Value` so [`ColumnDef`] can stay
9/// `Copy`. The migration planner calls the constructor whenever it needs to
10/// materialise the default for an `AddColumn` op or to fill a fresh row.
11pub type DefaultValueFn = fn() -> Value;
12
13/// Defines a column in a database table.
14#[derive(Clone, Copy, Debug)]
15pub struct ColumnDef {
16    /// The name of the column.
17    pub name: &'static str,
18    /// The data type of the column.
19    pub data_type: DataTypeKind,
20    /// Indicates if this column is auto-incrementing (applicable for integer types).
21    /// Cannot be `nullable`.
22    pub auto_increment: bool,
23    /// Indicates if this column can contain NULL values.
24    pub nullable: bool,
25    /// Indicates if this column is part of the primary key.
26    pub primary_key: bool,
27    /// Indicates if this column has unique values across all records.
28    pub unique: bool,
29    /// Foreign key definition, if any.
30    pub foreign_key: Option<ForeignKeyDef>,
31    /// Default value constructor, if any.
32    ///
33    /// Populated by the `#[default = ...]` attribute on a `#[derive(Table)]`
34    /// field. Consumed by the migration planner when adding a non-nullable
35    /// column to satisfy the
36    /// [`DefaultMissing`](crate::dbms::migration::MigrationError::DefaultMissing)
37    /// check.
38    pub default: Option<DefaultValueFn>,
39    /// Previous names this column was known by, in chronological order.
40    ///
41    /// Populated by the `#[renamed_from("old1", "old2", ...)]` attribute. The
42    /// migration planner walks this list to detect a `RenameColumn` op when a
43    /// stored column with one of these names matches the compiled column.
44    pub renamed_from: &'static [&'static str],
45}
46
47impl PartialEq for ColumnDef {
48    fn eq(&self, other: &Self) -> bool {
49        self.name == other.name
50            && self.data_type == other.data_type
51            && self.auto_increment == other.auto_increment
52            && self.nullable == other.nullable
53            && self.primary_key == other.primary_key
54            && self.unique == other.unique
55            && self.foreign_key == other.foreign_key
56            && self.default.map(|f| f as usize) == other.default.map(|f| f as usize)
57            && self.renamed_from == other.renamed_from
58    }
59}
60
61impl Eq for ColumnDef {}
62
63/// Defines a foreign key relationship for a column.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub struct ForeignKeyDef {
66    /// Name of the local column that holds the foreign key (es: "user_id")
67    pub local_column: &'static str,
68    /// Name of the foreign table (e.g., "users")
69    pub foreign_table: &'static str,
70    /// Name of the foreign column that the FK points to (e.g., "id")
71    pub foreign_column: &'static str,
72}
73
74/// Defines an index on one or more columns of a table.
75///
76/// Contains a static slice of column names that make up the index, in the order they are defined.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct IndexDef(pub &'static [&'static str]);
79
80impl IndexDef {
81    /// Returns the column names that make up this index.
82    pub fn columns(&self) -> &'static [&'static str] {
83        self.0
84    }
85}
86
87/// Serializable data type kind for API boundaries.
88///
89/// Mirrors [`DataTypeKind`] but uses owned `String` for the `Custom` variant,
90/// making it suitable for serialization across API boundaries.
91#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
92#[cfg_attr(feature = "candid", derive(candid::CandidType))]
93pub enum CandidDataTypeKind {
94    Blob,
95    Boolean,
96    Date,
97    DateTime,
98    Decimal,
99    Int8,
100    Int16,
101    Int32,
102    Int64,
103    Json,
104    Text,
105    Uint8,
106    Uint16,
107    Uint32,
108    Uint64,
109    Uuid,
110    Custom(String),
111}
112
113impl From<DataTypeKind> for CandidDataTypeKind {
114    fn from(kind: DataTypeKind) -> Self {
115        match kind {
116            DataTypeKind::Blob => Self::Blob,
117            DataTypeKind::Boolean => Self::Boolean,
118            DataTypeKind::Date => Self::Date,
119            DataTypeKind::DateTime => Self::DateTime,
120            DataTypeKind::Decimal => Self::Decimal,
121            DataTypeKind::Int8 => Self::Int8,
122            DataTypeKind::Int16 => Self::Int16,
123            DataTypeKind::Int32 => Self::Int32,
124            DataTypeKind::Int64 => Self::Int64,
125            DataTypeKind::Json => Self::Json,
126            DataTypeKind::Text => Self::Text,
127            DataTypeKind::Uint8 => Self::Uint8,
128            DataTypeKind::Uint16 => Self::Uint16,
129            DataTypeKind::Uint32 => Self::Uint32,
130            DataTypeKind::Uint64 => Self::Uint64,
131            DataTypeKind::Uuid => Self::Uuid,
132            DataTypeKind::Custom { tag, .. } => Self::Custom(tag.to_string()),
133        }
134    }
135}
136
137/// Serializable column definition for API boundaries.
138///
139/// This type mirrors [`ColumnDef`] but uses owned `String` fields instead
140/// of `&'static str`, making it suitable for serialization across API boundaries.
141#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
142#[cfg_attr(feature = "candid", derive(candid::CandidType))]
143pub struct JoinColumnDef {
144    /// The source table name. `Some` for join results, `None` for single-table queries.
145    pub table: Option<String>,
146    /// The name of the column.
147    pub name: String,
148    /// The data type of the column.
149    pub data_type: CandidDataTypeKind,
150    /// Indicates if this column can contain NULL values.
151    pub nullable: bool,
152    /// Indicates if this column is part of the primary key.
153    pub primary_key: bool,
154    /// Foreign key definition, if any.
155    pub foreign_key: Option<CandidForeignKeyDef>,
156}
157
158/// Serializable foreign key definition for API boundaries.
159///
160/// This type mirrors [`ForeignKeyDef`] but uses owned `String` fields instead
161/// of `&'static str`, making it suitable for serialization across API boundaries.
162#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
163#[cfg_attr(feature = "candid", derive(candid::CandidType))]
164pub struct CandidForeignKeyDef {
165    /// Name of the local column that holds the foreign key (e.g., "user_id").
166    pub local_column: String,
167    /// Name of the foreign table (e.g., "users").
168    pub foreign_table: String,
169    /// Name of the foreign column that the FK points to (e.g., "id").
170    pub foreign_column: String,
171}
172
173impl From<ColumnDef> for JoinColumnDef {
174    fn from(def: ColumnDef) -> Self {
175        Self {
176            table: None,
177            name: def.name.to_string(),
178            data_type: CandidDataTypeKind::from(def.data_type),
179            nullable: def.nullable,
180            primary_key: def.primary_key,
181            foreign_key: def.foreign_key.map(CandidForeignKeyDef::from),
182        }
183    }
184}
185
186impl From<ForeignKeyDef> for CandidForeignKeyDef {
187    fn from(def: ForeignKeyDef) -> Self {
188        Self {
189            local_column: def.local_column.to_string(),
190            foreign_table: def.foreign_table.to_string(),
191            foreign_column: def.foreign_column.to_string(),
192        }
193    }
194}
195
196#[cfg(test)]
197mod test {
198
199    use super::*;
200    use crate::dbms::types::DataTypeKind;
201
202    #[test]
203    fn test_should_create_column_def() {
204        let column = ColumnDef {
205            name: "id",
206            data_type: DataTypeKind::Uint32,
207            auto_increment: false,
208            nullable: false,
209            primary_key: true,
210            unique: false,
211            foreign_key: None,
212            default: None,
213            renamed_from: &[],
214        };
215
216        assert_eq!(column.name, "id");
217        assert_eq!(column.data_type, DataTypeKind::Uint32);
218        assert!(!column.auto_increment);
219        assert!(!column.nullable);
220        assert!(column.primary_key);
221        assert!(!column.unique);
222        assert!(column.foreign_key.is_none());
223    }
224
225    #[test]
226    fn test_should_create_column_def_with_foreign_key() {
227        let fk = ForeignKeyDef {
228            local_column: "user_id",
229            foreign_table: "users",
230            foreign_column: "id",
231        };
232
233        let column = ColumnDef {
234            name: "user_id",
235            data_type: DataTypeKind::Uint32,
236            auto_increment: false,
237            nullable: false,
238            primary_key: false,
239            unique: false,
240            foreign_key: Some(fk),
241            default: None,
242            renamed_from: &[],
243        };
244
245        assert_eq!(column.name, "user_id");
246        assert!(column.foreign_key.is_some());
247        let fk_def = column.foreign_key.unwrap();
248        assert_eq!(fk_def.local_column, "user_id");
249        assert_eq!(fk_def.foreign_table, "users");
250        assert_eq!(fk_def.foreign_column, "id");
251    }
252
253    #[test]
254    #[allow(clippy::clone_on_copy)]
255    fn test_should_clone_column_def() {
256        let column = ColumnDef {
257            name: "email",
258            data_type: DataTypeKind::Text,
259            auto_increment: false,
260            nullable: true,
261            primary_key: false,
262            unique: true,
263            foreign_key: None,
264            default: None,
265            renamed_from: &[],
266        };
267
268        let cloned = column.clone();
269        assert_eq!(column, cloned);
270    }
271
272    #[test]
273    fn test_should_compare_column_defs() {
274        let column1 = ColumnDef {
275            name: "id",
276            data_type: DataTypeKind::Uint32,
277            auto_increment: false,
278            nullable: false,
279            primary_key: true,
280            unique: false,
281            foreign_key: None,
282            default: None,
283            renamed_from: &[],
284        };
285
286        let column2 = ColumnDef {
287            name: "id",
288            data_type: DataTypeKind::Uint32,
289            auto_increment: false,
290            nullable: false,
291            primary_key: true,
292            unique: false,
293            foreign_key: None,
294            default: None,
295            renamed_from: &[],
296        };
297
298        let column3 = ColumnDef {
299            name: "name",
300            data_type: DataTypeKind::Text,
301            auto_increment: false,
302            nullable: true,
303            primary_key: false,
304            unique: true,
305            foreign_key: None,
306            default: None,
307            renamed_from: &[],
308        };
309
310        assert_eq!(column1, column2);
311        assert_ne!(column1, column3);
312    }
313
314    #[test]
315    fn test_should_create_foreign_key_def() {
316        let fk = ForeignKeyDef {
317            local_column: "post_id",
318            foreign_table: "posts",
319            foreign_column: "id",
320        };
321
322        assert_eq!(fk.local_column, "post_id");
323        assert_eq!(fk.foreign_table, "posts");
324        assert_eq!(fk.foreign_column, "id");
325    }
326
327    #[test]
328    #[allow(clippy::clone_on_copy)]
329    fn test_should_clone_foreign_key_def() {
330        let fk = ForeignKeyDef {
331            local_column: "author_id",
332            foreign_table: "authors",
333            foreign_column: "id",
334        };
335
336        let cloned = fk.clone();
337        assert_eq!(fk, cloned);
338    }
339
340    #[test]
341    fn test_should_compare_foreign_key_defs() {
342        let fk1 = ForeignKeyDef {
343            local_column: "user_id",
344            foreign_table: "users",
345            foreign_column: "id",
346        };
347
348        let fk2 = ForeignKeyDef {
349            local_column: "user_id",
350            foreign_table: "users",
351            foreign_column: "id",
352        };
353
354        let fk3 = ForeignKeyDef {
355            local_column: "category_id",
356            foreign_table: "categories",
357            foreign_column: "id",
358        };
359
360        assert_eq!(fk1, fk2);
361        assert_ne!(fk1, fk3);
362    }
363
364    #[test]
365    fn test_should_create_candid_column_def_with_table() {
366        let col = JoinColumnDef {
367            table: Some("users".to_string()),
368            name: "id".to_string(),
369            data_type: CandidDataTypeKind::Uint32,
370            nullable: false,
371            primary_key: true,
372            foreign_key: None,
373        };
374        assert_eq!(col.table, Some("users".to_string()));
375    }
376
377    #[test]
378    fn test_should_convert_column_def_to_candid_with_none_table() {
379        let col = ColumnDef {
380            name: "id",
381            data_type: DataTypeKind::Uint32,
382            auto_increment: false,
383            nullable: false,
384            primary_key: true,
385            unique: false,
386            foreign_key: None,
387            default: None,
388            renamed_from: &[],
389        };
390        let candid_col = JoinColumnDef::from(col);
391        assert_eq!(candid_col.table, None);
392        assert_eq!(candid_col.name, "id");
393    }
394
395    #[test]
396    fn test_should_convert_custom_data_type_kind_to_candid() {
397        use crate::dbms::table::WireSize;
398        let kind = DataTypeKind::Custom {
399            tag: "role",
400            wire_size: WireSize::Fixed(1),
401        };
402        let candid_kind = CandidDataTypeKind::from(kind);
403        assert_eq!(candid_kind, CandidDataTypeKind::Custom("role".to_string()));
404    }
405
406    #[test]
407    fn test_should_convert_builtin_data_type_kind_to_candid() {
408        let kind = DataTypeKind::Text;
409        let candid_kind = CandidDataTypeKind::from(kind);
410        assert_eq!(candid_kind, CandidDataTypeKind::Text);
411    }
412
413    #[test]
414    fn test_should_create_candid_column_def_with_custom_type() {
415        use crate::dbms::table::WireSize;
416        let col = ColumnDef {
417            name: "role",
418            data_type: DataTypeKind::Custom {
419                tag: "role",
420                wire_size: WireSize::Fixed(1),
421            },
422            auto_increment: false,
423            nullable: false,
424            primary_key: false,
425            unique: false,
426            foreign_key: None,
427            default: None,
428            renamed_from: &[],
429        };
430        let candid_col = JoinColumnDef::from(col);
431        assert_eq!(
432            candid_col.data_type,
433            CandidDataTypeKind::Custom("role".to_string())
434        );
435    }
436}