Skip to main content

ormkit/schema/
column.rs

1//! Column trait and type-safe column references
2//!
3//! This module defines the `Column` type which provides type-safe references to database columns.
4//! Columns are parameterized by their table type, Rust type, and a unique marker type.
5//!
6//! ## Type Safety
7//!
8//! The `Column<T, Type, M>` type ensures at compile time that:
9//! - Columns can only be used with their parent table (via the `T` parameter)
10//! - Column types are checked (via the `Type` parameter)
11//! - Each column has a unique identity (via the `M` marker parameter)
12//!
13//! ## Example
14//!
15//! ```rust,ignore
16//! use ormkit::schema::{Table, Column};
17//!
18//! // Table marker type
19//! #[derive(Debug, Clone, Copy)]
20//! pub struct UsersTable;
21//!
22//! impl Table for UsersTable {
23//!     const NAME: &'static str = "users";
24//! }
25//!
26//! // Column marker types
27//! #[derive(Debug, Clone, Copy)]
28//! pub struct EmailColumn;
29//!
30//! // Type-safe column reference
31//! pub const email: Column<UsersTable, String, EmailColumn> = Column::new("email");
32//!
33//! // Usage
34//! assert_eq!(email.name(), "email");
35//! assert_eq!(email.table_name(), "users");
36//! ```
37//!
38//! ## Zero-Sized Types
39//!
40//! `Column` instances are zero-sized when used as constants. The compiler optimizes
41//! them away completely, leaving zero runtime overhead for type safety.
42
43use std::fmt;
44use std::marker::PhantomData;
45
46use super::table::Table;
47
48/// A type-safe reference to a database column
49///
50/// # Type Parameters
51///
52/// - `T`: The table type this column belongs to (enforces ownership)
53/// - `Type`: The Rust type of values in this column (String, i32, etc.)
54/// - `M`: A unique marker type for this specific column (prevents column confusion)
55///
56/// # Type Safety
57///
58/// The type parameter `T` ensures that columns cannot be used with the wrong table.
59/// This is enforced at compile time, preventing SQL injection through type confusion.
60///
61/// # Zero-Sized Type
62///
63/// When used as a constant, `Column` is a zero-sized type. All data is stored in the
64/// type system itself, not in runtime values.
65///
66/// # Example
67///
68/// ```rust,ignore
69/// use ormkit::schema::{Table, Column};
70///
71/// # #[derive(Debug, Clone, Copy)]
72/// # pub struct UsersTable;
73/// # impl Table for UsersTable { const NAME: &'static str = "users"; }
74/// # #[derive(Debug, Clone, Copy)]
75/// # pub struct EmailColumn;
76///
77/// pub const email: Column<UsersTable, String, EmailColumn> = Column::new("email");
78///
79/// // Access column metadata
80/// assert_eq!(email.name(), "email");
81/// assert_eq!(email.table_name(), "users");
82/// ```
83#[derive(Debug, Clone, Copy)]
84pub struct Column<T, Type, M = ()>
85where
86    T: Table,
87    M: 'static + Send + Sync + Copy + fmt::Debug,
88{
89    /// The column name in the database
90    name: &'static str,
91
92    /// Phantom data for the table this column belongs to
93    ///
94    /// This ensures the column can only be used with its parent table.
95    phantom_table: PhantomData<T>,
96
97    /// Phantom data for the Rust type of this column
98    ///
99    /// This enables type-safe queries and operations.
100    phantom_type: PhantomData<Type>,
101
102    /// Phantom data for the unique column marker
103    ///
104    /// This ensures each column has a unique type identity.
105    phantom_marker: PhantomData<M>,
106}
107
108impl<T, Type, M> Column<T, Type, M>
109where
110    T: Table,
111    M: 'static + Send + Sync + Copy + fmt::Debug,
112{
113    /// Create a new column reference
114    ///
115    /// This should only be called in generated code. Users should use the
116    /// pre-defined column constants (e.g., `users::email`).
117    ///
118    /// # Example
119    ///
120    /// ```rust,ignore
121    /// # use ormkit::schema::{Table, Column};
122    /// # #[derive(Debug, Clone, Copy)] pub struct UsersTable;
123    /// # impl Table for UsersTable { const NAME: &'static str = "users"; }
124    /// # #[derive(Debug, Clone, Copy)] pub struct EmailColumn;
125    /// pub const email: Column<UsersTable, String, EmailColumn> = Column::new("email");
126    /// ```
127    #[inline]
128    pub const fn new(name: &'static str) -> Self {
129        Self {
130            name,
131            phantom_table: PhantomData,
132            phantom_type: PhantomData,
133            phantom_marker: PhantomData,
134        }
135    }
136
137    /// Get the column name
138    ///
139    /// # Example
140    ///
141    /// ```rust,ignore
142    /// # use ormkit::schema::{Table, Column};
143    /// # #[derive(Debug, Clone, Copy)] pub struct UsersTable;
144    /// # impl Table for UsersTable { const NAME: &'static str = "users"; }
145    /// # #[derive(Debug, Clone, Copy)] pub struct EmailColumn;
146    /// # pub const email: Column<UsersTable, String, EmailColumn> = Column::new("email");
147    /// assert_eq!(email.name(), "email");
148    /// ```
149    #[inline]
150    pub fn name(&self) -> &'static str {
151        self.name
152    }
153
154    /// Get the table name this column belongs to
155    ///
156    /// # Example
157    ///
158    /// ```rust,ignore
159    /// # use ormkit::schema::{Table, Column};
160    /// # #[derive(Debug, Clone, Copy)] pub struct UsersTable;
161    /// # impl Table for UsersTable { const NAME: &'static str = "users"; }
162    /// # #[derive(Debug, Clone, Copy)] pub struct EmailColumn;
163    /// # pub const email: Column<UsersTable, String, EmailColumn> = Column::new("email");
164    /// assert_eq!(email.table_name(), "users");
165    /// ```
166    #[inline]
167    pub fn table_name(&self) -> &'static str {
168        T::NAME
169    }
170
171    /// Get the fully qualified column name (table.column)
172    ///
173    /// This is useful for generating SQL queries.
174    ///
175    /// # Example
176    ///
177    /// ```rust,ignore
178    /// # use ormkit::schema::{Table, Column};
179    /// # #[derive(Debug, Clone, Copy)] pub struct UsersTable;
180    /// # impl Table for UsersTable { const NAME: &'static str = "users"; }
181    /// # #[derive(Debug, Clone, Copy)] pub struct EmailColumn;
182    /// # pub const email: Column<UsersTable, String, EmailColumn> = Column::new("email");
183    /// assert_eq!(email.qualified_name(), "users.email");
184    /// ```
185    #[inline]
186    pub fn qualified_name(&self) -> String {
187        format!("{}.{}", T::NAME, self.name)
188    }
189
190    /// Check if this column is the given column name
191    ///
192    /// # Example
193    ///
194    /// ```rust,ignore
195    /// # use ormkit::schema::{Table, Column};
196    /// # #[derive(Debug, Clone, Copy)] pub struct UsersTable;
197    /// # impl Table for UsersTable { const NAME: &'static str = "users"; }
198    /// # #[derive(Debug, Clone, Copy)] pub struct EmailColumn;
199    /// # pub const email: Column<UsersTable, String, EmailColumn> = Column::new("email");
200    /// assert!(email.is_column("email"));
201    /// assert!(!email.is_column("name"));
202    /// ```
203    #[inline]
204    pub fn is_column(&self, name: &str) -> bool {
205        self.name == name
206    }
207
208    /// Get a reference to this column (for ZSTs, this is essentially a no-op)
209    ///
210    /// This method is useful when you need a reference to the column.
211    #[inline]
212    pub fn as_ref(&self) -> &Self {
213        self
214    }
215}
216
217impl<T, Type, M> fmt::Display for Column<T, Type, M>
218where
219    T: Table,
220    M: 'static + Send + Sync + Copy + fmt::Debug,
221{
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(f, "{}", self.qualified_name())
224    }
225}
226
227/// Implement ColumnSealed for valid column types
228///
229/// This sealed trait prevents external implementations of Column extensions.
230pub trait ColumnSealed<T, Type>: 'static + Send + Sync + Copy + fmt::Debug
231where
232    T: Table,
233{
234}
235
236/// Blanket implementation for all valid marker types
237impl<T, Type, M> ColumnSealed<T, Type> for M
238where
239    T: Table,
240    M: 'static + Send + Sync + Copy + fmt::Debug,
241{
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    // Test table and column types
249    #[derive(Debug, Clone, Copy)]
250    struct TestTable;
251
252    impl Table for TestTable {
253        const NAME: &'static str = "test_table";
254    }
255
256    #[derive(Debug, Clone, Copy)]
257    struct TestColumn1;
258
259    #[derive(Debug, Clone, Copy)]
260    struct TestColumn2;
261
262    type TestColumn1Type = Column<TestTable, String, TestColumn1>;
263    type TestColumn2Type = Column<TestTable, i32, TestColumn2>;
264
265    const TEST_COL1: TestColumn1Type = Column::new("col1");
266    const TEST_COL2: TestColumn2Type = Column::new("col2");
267
268    #[test]
269    fn test_column_name() {
270        assert_eq!(TEST_COL1.name(), "col1");
271        assert_eq!(TEST_COL2.name(), "col2");
272    }
273
274    #[test]
275    fn test_table_name() {
276        assert_eq!(TEST_COL1.table_name(), "test_table");
277        assert_eq!(TEST_COL2.table_name(), "test_table");
278    }
279
280    #[test]
281    fn test_qualified_name() {
282        assert_eq!(TEST_COL1.qualified_name(), "test_table.col1");
283        assert_eq!(TEST_COL2.qualified_name(), "test_table.col2");
284    }
285
286    #[test]
287    fn test_is_column() {
288        assert!(TEST_COL1.is_column("col1"));
289        assert!(!TEST_COL1.is_column("col2"));
290        assert!(!TEST_COL1.is_column("other"));
291    }
292
293    #[test]
294    fn test_column_display() {
295        assert_eq!(TEST_COL1.to_string(), "test_table.col1");
296        assert_eq!(TEST_COL2.to_string(), "test_table.col2");
297    }
298
299    #[test]
300    fn test_column_copy() {
301        let col = TEST_COL1;
302        let _copy = col; // Tests that Column is Copy
303        let _copy2 = col; // Tests that Column is Copy (can use again)
304    }
305
306    #[test]
307    fn test_column_clone() {
308        let col = TEST_COL1;
309        let _clone = col; // Tests that Column is Clone
310    }
311
312    #[test]
313    fn test_column_zst() {
314        assert_eq!(std::mem::size_of::<TestColumn1Type>(), 0);
315        assert_eq!(std::mem::size_of::<TestColumn2Type>(), 0);
316    }
317
318    #[test]
319    fn test_multiple_columns() {
320        // Multiple columns with different types
321        #[derive(Debug, Clone, Copy)]
322        struct StrCol;
323        #[derive(Debug, Clone, Copy)]
324        struct IntCol;
325        #[derive(Debug, Clone, Copy)]
326        struct BoolCol;
327
328        type StrColumnType = Column<TestTable, String, StrCol>;
329        type IntColumnType = Column<TestTable, i32, IntCol>;
330        type BoolColumnType = Column<TestTable, bool, BoolCol>;
331
332        const STR_COL: StrColumnType = Column::new("str_col");
333        const INT_COL: IntColumnType = Column::new("int_col");
334        const BOOL_COL: BoolColumnType = Column::new("bool_col");
335
336        assert_eq!(STR_COL.name(), "str_col");
337        assert_eq!(INT_COL.name(), "int_col");
338        assert_eq!(BOOL_COL.name(), "bool_col");
339    }
340}