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}