Skip to main content

reinhardt_db/orm/
generated_field.rs

1//! Generated field support for database-generated columns
2//!
3//! This module provides support for database-generated columns (computed columns),
4//! similar to Django's GeneratedField and SQLAlchemy's Computed columns.
5//!
6//! Generated columns are automatically computed by the database based on an expression
7//! and can be either STORED (persisted to disk) or VIRTUAL (computed on-the-fly).
8
9use super::fields::{BaseField, Field, FieldDeconstruction, FieldKwarg};
10use serde::{Deserialize, Serialize};
11
12/// Storage type for generated columns
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
14pub enum StorageType {
15	/// Column value is computed and stored on disk (persistent)
16	/// Provides better read performance but uses more disk space
17	Stored,
18	/// Column value is computed on-the-fly when queried (ephemeral)
19	/// Saves disk space but requires computation on every read
20	#[default]
21	Virtual,
22}
23
24impl StorageType {
25	/// Convert storage type to SQL keyword
26	///
27	/// # Examples
28	///
29	/// ```
30	/// use reinhardt_db::orm::generated_field::StorageType;
31	///
32	/// assert_eq!(StorageType::Stored.to_sql(), "STORED");
33	/// assert_eq!(StorageType::Virtual.to_sql(), "VIRTUAL");
34	/// ```
35	pub fn to_sql(&self) -> &'static str {
36		match self {
37			StorageType::Stored => "STORED",
38			StorageType::Virtual => "VIRTUAL",
39		}
40	}
41}
42
43/// GeneratedField - a database column with auto-computed value
44///
45/// Generated fields automatically compute their value based on an expression
46/// and cannot be manually set. They are similar to Django's GeneratedField
47/// and SQLAlchemy's computed() columns.
48///
49/// # Examples
50///
51/// ```
52/// use reinhardt_db::orm::generated_field::{GeneratedField, StorageType};
53///
54/// // Virtual generated field (computed on-the-fly)
55/// let full_name = GeneratedField::new(
56///     "CONCAT(first_name, ' ', last_name)",
57///     StorageType::Virtual
58/// );
59///
60/// // Stored generated field (persisted to disk)
61/// let total_price = GeneratedField::new(
62///     "quantity * unit_price",
63///     StorageType::Stored
64/// );
65/// ```
66#[derive(Debug, Clone)]
67pub struct GeneratedField {
68	/// The base.
69	pub base: BaseField,
70	/// SQL expression that generates the column value
71	pub expression: String,
72	/// Whether the value is STORED or VIRTUAL
73	pub storage_type: StorageType,
74	/// Database backend specific options
75	pub db_persist: bool,
76}
77
78impl GeneratedField {
79	/// Create a new generated field with an expression and storage type
80	///
81	/// # Examples
82	///
83	/// ```
84	/// use reinhardt_db::orm::generated_field::{GeneratedField, StorageType};
85	///
86	/// let field = GeneratedField::new(
87	///     "price * 1.1",
88	///     StorageType::Virtual
89	/// );
90	/// assert_eq!(field.expression, "price * 1.1");
91	/// assert_eq!(field.storage_type, StorageType::Virtual);
92	/// ```
93	pub fn new(expression: impl Into<String>, storage_type: StorageType) -> Self {
94		let mut base = BaseField::new();
95		// Generated fields are not editable
96		base.editable = false;
97
98		Self {
99			base,
100			expression: expression.into(),
101			storage_type,
102			db_persist: storage_type == StorageType::Stored,
103		}
104	}
105
106	/// Create a virtual generated field (computed on-the-fly)
107	///
108	/// # Examples
109	///
110	/// ```
111	/// use reinhardt_db::orm::generated_field::{GeneratedField, StorageType};
112	///
113	/// let field = GeneratedField::virtual_field("UPPER(name)");
114	/// assert_eq!(field.storage_type, StorageType::Virtual);
115	/// ```
116	pub fn virtual_field(expression: impl Into<String>) -> Self {
117		Self::new(expression, StorageType::Virtual)
118	}
119
120	/// Create a stored generated field (persisted to disk)
121	///
122	/// # Examples
123	///
124	/// ```
125	/// use reinhardt_db::orm::generated_field::{GeneratedField, StorageType};
126	///
127	/// let field = GeneratedField::stored_field("quantity * price");
128	/// assert_eq!(field.storage_type, StorageType::Stored);
129	/// ```
130	pub fn stored_field(expression: impl Into<String>) -> Self {
131		Self::new(expression, StorageType::Stored)
132	}
133
134	/// Generate the SQL definition for this generated column
135	///
136	/// # Examples
137	///
138	/// ```
139	/// use reinhardt_db::orm::generated_field::{GeneratedField, StorageType};
140	///
141	/// let field = GeneratedField::new("price * 1.2", StorageType::Stored);
142	/// assert!(field.to_sql().contains("GENERATED ALWAYS AS"));
143	/// assert!(field.to_sql().contains("STORED"));
144	/// ```
145	pub fn to_sql(&self) -> String {
146		format!(
147			"GENERATED ALWAYS AS ({}) {}",
148			self.expression,
149			self.storage_type.to_sql()
150		)
151	}
152
153	/// Generate PostgreSQL-specific SQL
154	///
155	/// # Examples
156	///
157	/// ```
158	/// use reinhardt_db::orm::generated_field::{GeneratedField, StorageType};
159	///
160	/// let field = GeneratedField::stored_field("first_name || ' ' || last_name");
161	/// assert!(field.to_postgres_sql().contains("GENERATED ALWAYS AS"));
162	/// assert!(field.to_postgres_sql().contains("STORED"));
163	/// ```
164	pub fn to_postgres_sql(&self) -> String {
165		// PostgreSQL syntax
166		format!(
167			"GENERATED ALWAYS AS ({}) {}",
168			self.expression,
169			self.storage_type.to_sql()
170		)
171	}
172
173	/// Generate MySQL-specific SQL
174	///
175	/// # Examples
176	///
177	/// ```
178	/// use reinhardt_db::orm::generated_field::{GeneratedField, StorageType};
179	///
180	/// let field = GeneratedField::virtual_field("price * 1.1");
181	/// assert!(field.to_mysql_sql().contains("GENERATED ALWAYS AS"));
182	/// assert!(field.to_mysql_sql().contains("VIRTUAL"));
183	/// ```
184	pub fn to_mysql_sql(&self) -> String {
185		// MySQL 5.7+ syntax
186		format!(
187			"GENERATED ALWAYS AS ({}) {}",
188			self.expression,
189			self.storage_type.to_sql()
190		)
191	}
192
193	/// Generate SQLite-specific SQL
194	///
195	/// # Examples
196	///
197	/// ```
198	/// use reinhardt_db::orm::generated_field::{GeneratedField, StorageType};
199	///
200	/// let field = GeneratedField::stored_field("json_extract(data, '$.name')");
201	/// assert!(field.to_sqlite_sql().contains("GENERATED ALWAYS AS"));
202	/// assert!(field.to_sqlite_sql().contains("STORED"));
203	/// ```
204	pub fn to_sqlite_sql(&self) -> String {
205		// SQLite 3.31.0+ syntax
206		format!(
207			"GENERATED ALWAYS AS ({}) {}",
208			self.expression,
209			self.storage_type.to_sql()
210		)
211	}
212}
213
214impl Field for GeneratedField {
215	fn deconstruct(&self) -> FieldDeconstruction {
216		let mut kwargs = self.base.get_kwargs();
217
218		// Add generated field specific kwargs
219		kwargs.insert(
220			"expression".to_string(),
221			FieldKwarg::String(self.expression.clone()),
222		);
223		kwargs.insert(
224			"storage_type".to_string(),
225			FieldKwarg::String(self.storage_type.to_sql().to_string()),
226		);
227		if self.db_persist {
228			kwargs.insert("db_persist".to_string(), FieldKwarg::Bool(true));
229		}
230
231		// Generated fields are always not editable
232		kwargs.remove("editable");
233
234		FieldDeconstruction {
235			name: self.base.name.clone(),
236			path: "reinhardt.orm.models.GeneratedField".to_string(),
237			args: vec![],
238			kwargs,
239		}
240	}
241
242	fn set_attributes_from_name(&mut self, name: &str) {
243		self.base.name = Some(name.to_string());
244	}
245
246	fn name(&self) -> Option<&str> {
247		self.base.name.as_deref()
248	}
249
250	fn is_null(&self) -> bool {
251		self.base.null
252	}
253}
254
255#[cfg(test)]
256mod tests {
257	use super::*;
258
259	#[test]
260	fn test_storage_type_to_sql() {
261		assert_eq!(StorageType::Stored.to_sql(), "STORED");
262		assert_eq!(StorageType::Virtual.to_sql(), "VIRTUAL");
263	}
264
265	#[test]
266	fn test_storage_type_default() {
267		assert_eq!(StorageType::default(), StorageType::Virtual);
268	}
269
270	#[test]
271	fn test_generated_field_new() {
272		let field = GeneratedField::new("price * 1.1", StorageType::Stored);
273		assert_eq!(field.expression, "price * 1.1");
274		assert_eq!(field.storage_type, StorageType::Stored);
275		assert!(!field.base.editable);
276		assert!(field.db_persist);
277	}
278
279	#[test]
280	fn test_generated_field_virtual() {
281		let field = GeneratedField::virtual_field("UPPER(name)");
282		assert_eq!(field.expression, "UPPER(name)");
283		assert_eq!(field.storage_type, StorageType::Virtual);
284		assert!(!field.db_persist);
285	}
286
287	#[test]
288	fn test_generated_field_stored() {
289		let field = GeneratedField::stored_field("quantity * price");
290		assert_eq!(field.expression, "quantity * price");
291		assert_eq!(field.storage_type, StorageType::Stored);
292		assert!(field.db_persist);
293	}
294
295	#[test]
296	fn test_generated_field_to_sql() {
297		let field = GeneratedField::new("a + b", StorageType::Virtual);
298		let sql = field.to_sql();
299		assert_eq!(
300			sql, "GENERATED ALWAYS AS (a + b) VIRTUAL",
301			"Expected exact generated field SQL, got: {}",
302			sql
303		);
304	}
305
306	#[test]
307	fn test_generated_field_to_sql_stored() {
308		let field = GeneratedField::stored_field("CONCAT(first_name, ' ', last_name)");
309		let sql = field.to_sql();
310		assert_eq!(
311			sql, "GENERATED ALWAYS AS (CONCAT(first_name, ' ', last_name)) STORED",
312			"Expected exact stored generated field SQL, got: {}",
313			sql
314		);
315	}
316
317	#[test]
318	fn test_generated_field_postgres_sql() {
319		let field = GeneratedField::virtual_field("price * 1.2");
320		let sql = field.to_postgres_sql();
321		assert_eq!(
322			sql, "GENERATED ALWAYS AS (price * 1.2) VIRTUAL",
323			"Expected exact PostgreSQL generated field SQL, got: {}",
324			sql
325		);
326	}
327
328	#[test]
329	fn test_generated_field_mysql_sql() {
330		let field = GeneratedField::stored_field("quantity * unit_price");
331		let sql = field.to_mysql_sql();
332		assert_eq!(
333			sql, "GENERATED ALWAYS AS (quantity * unit_price) STORED",
334			"Expected exact MySQL generated field SQL, got: {}",
335			sql
336		);
337	}
338
339	#[test]
340	fn test_generated_field_sqlite_sql() {
341		let field = GeneratedField::virtual_field("json_extract(data, '$.name')");
342		let sql = field.to_sqlite_sql();
343		assert_eq!(
344			sql, "GENERATED ALWAYS AS (json_extract(data, '$.name')) VIRTUAL",
345			"Expected exact SQLite generated field SQL, got: {}",
346			sql
347		);
348	}
349
350	#[test]
351	fn test_generated_field_deconstruct() {
352		let mut field = GeneratedField::stored_field("price + tax");
353		field.set_attributes_from_name("total_price");
354
355		let dec = field.deconstruct();
356		assert_eq!(dec.name, Some("total_price".to_string()));
357		assert_eq!(dec.path, "reinhardt.orm.models.GeneratedField");
358		assert_eq!(
359			dec.kwargs.get("expression"),
360			Some(&FieldKwarg::String("price + tax".to_string()))
361		);
362		assert_eq!(
363			dec.kwargs.get("storage_type"),
364			Some(&FieldKwarg::String("STORED".to_string()))
365		);
366		assert_eq!(dec.kwargs.get("db_persist"), Some(&FieldKwarg::Bool(true)));
367	}
368
369	#[test]
370	fn test_generated_field_not_editable() {
371		let field = GeneratedField::virtual_field("col1 + col2");
372		assert!(!field.base.editable);
373
374		let dec = field.deconstruct();
375		// Should not include editable=False since it's the default for generated fields
376		assert!(!dec.kwargs.contains_key("editable"));
377	}
378
379	#[test]
380	fn test_field_trait_implementation() {
381		let mut field = GeneratedField::virtual_field("x * y");
382		assert!(field.name().is_none());
383
384		field.set_attributes_from_name("result");
385		assert_eq!(field.name(), Some("result"));
386	}
387
388	#[test]
389	fn test_complex_expression() {
390		let field = GeneratedField::stored_field(
391			"CASE WHEN status = 'active' THEN price * 0.9 ELSE price END",
392		);
393		let sql = field.to_sql();
394		assert_eq!(
395			sql,
396			"GENERATED ALWAYS AS (CASE WHEN status = 'active' THEN price * 0.9 ELSE price END) STORED",
397			"Expected exact complex expression SQL, got: {}",
398			sql
399		);
400	}
401
402	#[test]
403	fn test_concat_expression() {
404		let field = GeneratedField::virtual_field("CONCAT(first_name, ' ', last_name)");
405		assert_eq!(field.expression, "CONCAT(first_name, ' ', last_name)");
406		assert_eq!(field.storage_type, StorageType::Virtual);
407	}
408
409	#[test]
410	fn test_arithmetic_expression() {
411		let field = GeneratedField::stored_field("(price - discount) * quantity");
412		let sql = field.to_sql();
413		assert_eq!(
414			sql, "GENERATED ALWAYS AS ((price - discount) * quantity) STORED",
415			"Expected exact arithmetic expression SQL, got: {}",
416			sql
417		);
418	}
419
420	#[test]
421	fn test_json_extract_expression() {
422		let field = GeneratedField::virtual_field("json_extract(metadata, '$.title')");
423		assert_eq!(field.expression, "json_extract(metadata, '$.title')");
424	}
425
426	#[test]
427	fn test_null_field() {
428		let mut field = GeneratedField::virtual_field("col1 + col2");
429		field.base.null = true;
430
431		assert!(field.is_null());
432		let dec = field.deconstruct();
433		assert_eq!(dec.kwargs.get("null"), Some(&FieldKwarg::Bool(true)));
434	}
435
436	#[test]
437	fn test_multiple_backends_sql_generation() {
438		let field = GeneratedField::stored_field("price * tax_rate");
439
440		let pg_sql = field.to_postgres_sql();
441		let mysql_sql = field.to_mysql_sql();
442		let sqlite_sql = field.to_sqlite_sql();
443
444		let expected = "GENERATED ALWAYS AS (price * tax_rate) STORED";
445
446		assert_eq!(
447			pg_sql, expected,
448			"Expected exact PostgreSQL SQL, got: {}",
449			pg_sql
450		);
451		assert_eq!(
452			mysql_sql, expected,
453			"Expected exact MySQL SQL, got: {}",
454			mysql_sql
455		);
456		assert_eq!(
457			sqlite_sql, expected,
458			"Expected exact SQLite SQL, got: {}",
459			sqlite_sql
460		);
461	}
462
463	#[test]
464	fn test_storage_type_equality() {
465		let stored1 = StorageType::Stored;
466		let stored2 = StorageType::Stored;
467		let virtual1 = StorageType::Virtual;
468
469		assert_eq!(stored1, stored2);
470		assert_ne!(stored1, virtual1);
471	}
472
473	#[test]
474	fn test_field_name_setting() {
475		let mut field = GeneratedField::virtual_field("a + b");
476		assert!(field.name().is_none());
477
478		field.set_attributes_from_name("sum_field");
479		assert_eq!(field.name(), Some("sum_field"));
480
481		field.set_attributes_from_name("total");
482		assert_eq!(field.name(), Some("total"));
483	}
484
485	#[test]
486	fn test_db_persist_flag() {
487		let virtual_field = GeneratedField::virtual_field("col1");
488		assert!(!virtual_field.db_persist);
489
490		let stored_field = GeneratedField::stored_field("col2");
491		assert!(stored_field.db_persist);
492	}
493
494	#[test]
495	fn test_date_expression() {
496		let field = GeneratedField::stored_field("DATE_ADD(created_at, INTERVAL 30 DAY)");
497		let sql = field.to_sql();
498		assert_eq!(
499			sql, "GENERATED ALWAYS AS (DATE_ADD(created_at, INTERVAL 30 DAY)) STORED",
500			"Expected exact date expression SQL, got: {}",
501			sql
502		);
503	}
504
505	#[test]
506	fn test_string_function_expression() {
507		let field = GeneratedField::virtual_field("LOWER(TRIM(email))");
508		assert_eq!(field.expression, "LOWER(TRIM(email))");
509	}
510
511	#[test]
512	fn test_conditional_expression() {
513		let field = GeneratedField::stored_field("IF(quantity > 10, price * 0.9, price)");
514		let sql = field.to_sql();
515		assert_eq!(
516			sql, "GENERATED ALWAYS AS (IF(quantity > 10, price * 0.9, price)) STORED",
517			"Expected exact conditional expression SQL, got: {}",
518			sql
519		);
520	}
521
522	#[test]
523	fn test_subquery_expression() {
524		let field = GeneratedField::virtual_field(
525			"(SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id)",
526		);
527		assert_eq!(
528			field.expression, "(SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id)",
529			"Expected exact subquery expression, got: {}",
530			field.expression
531		);
532	}
533
534	#[test]
535	fn test_aggregate_expression() {
536		let field = GeneratedField::stored_field("COALESCE(discount, 0) + base_price");
537		let sql = field.to_sql();
538		assert_eq!(
539			sql, "GENERATED ALWAYS AS (COALESCE(discount, 0) + base_price) STORED",
540			"Expected exact aggregate expression SQL, got: {}",
541			sql
542		);
543	}
544
545	#[test]
546	fn test_deconstruct_virtual_field() {
547		let field = GeneratedField::virtual_field("x + y");
548		let dec = field.deconstruct();
549
550		assert_eq!(
551			dec.kwargs.get("storage_type"),
552			Some(&FieldKwarg::String("VIRTUAL".to_string()))
553		);
554		assert!(!dec.kwargs.contains_key("db_persist"));
555	}
556
557	#[test]
558	fn test_expression_with_special_characters() {
559		let field = GeneratedField::stored_field("regexp_replace(text, '[^a-zA-Z]', '')");
560		assert_eq!(field.expression, "regexp_replace(text, '[^a-zA-Z]', '')");
561	}
562}