Skip to main content

wasm_dbms_api/dbms/table/
column_def.rs

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