Skip to main content

reinhardt_query/value/
core.rs

1//! Core Value enum definition.
2
3use super::ArrayType;
4
5/// Core value representation for SQL parameters.
6///
7/// This enum represents all possible SQL value types. The enum is designed
8/// to be size-optimized: larger types are boxed to maintain a consistent
9/// enum size of approximately one pointer width.
10///
11/// ## Null Values
12///
13/// All variants use `Option<T>` to represent nullable values. A `None` value
14/// will be rendered as SQL `NULL`.
15///
16/// ## Variant Naming Convention
17///
18/// Variant names align with common SQL type names:
19///
20/// | Variant | SQL Type | Rust Type |
21/// |---------|----------|-----------|
22/// | `Value::Bool` | BOOLEAN | `bool` |
23/// | `Value::TinyInt` | TINYINT | `i8` |
24/// | `Value::SmallInt` | SMALLINT | `i16` |
25/// | `Value::Int` | INTEGER | `i32` |
26/// | `Value::BigInt` | BIGINT | `i64` |
27/// | `Value::TinyUnsigned` | TINYINT UNSIGNED | `u8` |
28/// | `Value::SmallUnsigned` | SMALLINT UNSIGNED | `u16` |
29/// | `Value::Unsigned` | INTEGER UNSIGNED | `u32` |
30/// | `Value::BigUnsigned` | BIGINT UNSIGNED | `u64` |
31/// | `Value::Float` | FLOAT | `f32` |
32/// | `Value::Double` | DOUBLE | `f64` |
33/// | `Value::Char` | CHAR | `char` |
34/// | `Value::String` | VARCHAR/TEXT | `String` |
35/// | `Value::Bytes` | BLOB/BINARY | `Vec<u8>` |
36///
37/// ## Example
38///
39/// ```rust
40/// use reinhardt_query::Value;
41///
42/// // Integer types
43/// let int_val = Value::Int(Some(42));
44/// let null_int = Value::Int(None);
45/// let bigint_val = Value::BigInt(Some(9223372036854775807i64));
46///
47/// // String type (boxed for size optimization)
48/// let string_val = Value::String(Some(Box::new("hello".to_string())));
49///
50/// // Check for null
51/// assert!(!int_val.is_null());
52/// assert!(null_int.is_null());
53///
54/// // Convert to SQL literal
55/// assert_eq!(int_val.to_sql_literal(), "42");
56/// assert_eq!(null_int.to_sql_literal(), "NULL");
57/// ```
58///
59/// ## Feature-Gated Types
60///
61/// Additional types are available with feature flags:
62///
63/// - `with-chrono`: Date/time types (`Value::ChronoDate`, `Value::ChronoTime`, etc.)
64/// - `with-uuid`: UUID type (`Value::Uuid`)
65/// - `with-json`: JSON type (`Value::Json`)
66/// - `with-rust_decimal`: Decimal type (`Value::Decimal`)
67/// - `with-bigdecimal`: BigDecimal type (`Value::BigDecimal`)
68#[derive(Clone, Debug, PartialEq)]
69pub enum Value {
70	// -------------------------------------------------------------------------
71	// Primitive types (inline, not boxed)
72	// -------------------------------------------------------------------------
73	/// Boolean value
74	Bool(Option<bool>),
75	/// 8-bit signed integer
76	TinyInt(Option<i8>),
77	/// 16-bit signed integer
78	SmallInt(Option<i16>),
79	/// 32-bit signed integer
80	Int(Option<i32>),
81	/// 64-bit signed integer
82	BigInt(Option<i64>),
83	/// 8-bit unsigned integer
84	TinyUnsigned(Option<u8>),
85	/// 16-bit unsigned integer
86	SmallUnsigned(Option<u16>),
87	/// 32-bit unsigned integer
88	Unsigned(Option<u32>),
89	/// 64-bit unsigned integer
90	BigUnsigned(Option<u64>),
91	/// 32-bit floating point
92	Float(Option<f32>),
93	/// 64-bit floating point
94	Double(Option<f64>),
95	/// Single character
96	Char(Option<char>),
97
98	// -------------------------------------------------------------------------
99	// Heap-allocated types (boxed for size optimization)
100	// -------------------------------------------------------------------------
101	/// String value (boxed)
102	String(Option<Box<String>>),
103	/// Binary data (boxed)
104	Bytes(Option<Box<Vec<u8>>>),
105
106	// -------------------------------------------------------------------------
107	// Feature-gated types: chrono
108	// -------------------------------------------------------------------------
109	/// Chrono NaiveDate
110	#[cfg(feature = "with-chrono")]
111	ChronoDate(Option<Box<chrono::NaiveDate>>),
112	/// Chrono NaiveTime
113	#[cfg(feature = "with-chrono")]
114	ChronoTime(Option<Box<chrono::NaiveTime>>),
115	/// Chrono NaiveDateTime
116	#[cfg(feature = "with-chrono")]
117	ChronoDateTime(Option<Box<chrono::NaiveDateTime>>),
118	/// Chrono DateTime with UTC timezone
119	#[cfg(feature = "with-chrono")]
120	ChronoDateTimeUtc(Option<Box<chrono::DateTime<chrono::Utc>>>),
121	/// Chrono DateTime with Local timezone
122	#[cfg(feature = "with-chrono")]
123	ChronoDateTimeLocal(Option<Box<chrono::DateTime<chrono::Local>>>),
124	/// Chrono DateTime with fixed offset timezone
125	#[cfg(feature = "with-chrono")]
126	ChronoDateTimeWithTimeZone(Option<Box<chrono::DateTime<chrono::FixedOffset>>>),
127
128	// -------------------------------------------------------------------------
129	// Feature-gated types: uuid
130	// -------------------------------------------------------------------------
131	/// UUID value
132	#[cfg(feature = "with-uuid")]
133	Uuid(Option<Box<uuid::Uuid>>),
134
135	// -------------------------------------------------------------------------
136	// Feature-gated types: json
137	// -------------------------------------------------------------------------
138	/// JSON value
139	#[cfg(feature = "with-json")]
140	Json(Option<Box<serde_json::Value>>),
141
142	// -------------------------------------------------------------------------
143	// Feature-gated types: decimal
144	// -------------------------------------------------------------------------
145	/// Rust Decimal value
146	#[cfg(feature = "with-rust_decimal")]
147	Decimal(Option<Box<rust_decimal::Decimal>>),
148
149	/// BigDecimal value
150	#[cfg(feature = "with-bigdecimal")]
151	BigDecimal(Option<Box<bigdecimal::BigDecimal>>),
152
153	// -------------------------------------------------------------------------
154	// Array type
155	// -------------------------------------------------------------------------
156	/// Array of values with element type information
157	Array(ArrayType, Option<Box<Vec<Value>>>),
158}
159
160impl Value {
161	/// Returns `true` if this value is null.
162	///
163	/// # Example
164	///
165	/// ```rust
166	/// use reinhardt_query::Value;
167	///
168	/// assert!(Value::Int(None).is_null());
169	/// assert!(!Value::Int(Some(42)).is_null());
170	/// ```
171	#[must_use]
172	pub fn is_null(&self) -> bool {
173		match self {
174			Self::Bool(v) => v.is_none(),
175			Self::TinyInt(v) => v.is_none(),
176			Self::SmallInt(v) => v.is_none(),
177			Self::Int(v) => v.is_none(),
178			Self::BigInt(v) => v.is_none(),
179			Self::TinyUnsigned(v) => v.is_none(),
180			Self::SmallUnsigned(v) => v.is_none(),
181			Self::Unsigned(v) => v.is_none(),
182			Self::BigUnsigned(v) => v.is_none(),
183			Self::Float(v) => v.is_none(),
184			Self::Double(v) => v.is_none(),
185			Self::Char(v) => v.is_none(),
186			Self::String(v) => v.is_none(),
187			Self::Bytes(v) => v.is_none(),
188			#[cfg(feature = "with-chrono")]
189			Self::ChronoDate(v) => v.is_none(),
190			#[cfg(feature = "with-chrono")]
191			Self::ChronoTime(v) => v.is_none(),
192			#[cfg(feature = "with-chrono")]
193			Self::ChronoDateTime(v) => v.is_none(),
194			#[cfg(feature = "with-chrono")]
195			Self::ChronoDateTimeUtc(v) => v.is_none(),
196			#[cfg(feature = "with-chrono")]
197			Self::ChronoDateTimeLocal(v) => v.is_none(),
198			#[cfg(feature = "with-chrono")]
199			Self::ChronoDateTimeWithTimeZone(v) => v.is_none(),
200			#[cfg(feature = "with-uuid")]
201			Self::Uuid(v) => v.is_none(),
202			#[cfg(feature = "with-json")]
203			Self::Json(v) => v.is_none(),
204			#[cfg(feature = "with-rust_decimal")]
205			Self::Decimal(v) => v.is_none(),
206			#[cfg(feature = "with-bigdecimal")]
207			Self::BigDecimal(v) => v.is_none(),
208			Self::Array(_, v) => v.is_none(),
209		}
210	}
211}
212
213impl Value {
214	/// Convert this value to a SQL literal string suitable for inlining
215	/// into a SQL statement.
216	///
217	/// This is used by `QueryStatementBuilder::to_string()` to produce
218	/// SQL with values inlined (for debugging and non-parameterized use).
219	///
220	/// # Example
221	///
222	/// ```rust
223	/// use reinhardt_query::Value;
224	///
225	/// assert_eq!(Value::Int(Some(42)).to_sql_literal(), "42");
226	/// assert_eq!(Value::Int(None).to_sql_literal(), "NULL");
227	/// assert_eq!(
228	///     Value::String(Some(Box::new("hello".to_string()))).to_sql_literal(),
229	///     "'hello'"
230	/// );
231	/// assert_eq!(
232	///     Value::String(Some(Box::new("it's".to_string()))).to_sql_literal(),
233	///     "'it''s'"
234	/// );
235	/// ```
236	#[must_use]
237	pub fn to_sql_literal(&self) -> String {
238		match self {
239			Self::Bool(Some(v)) => {
240				if *v {
241					"TRUE".to_string()
242				} else {
243					"FALSE".to_string()
244				}
245			}
246			Self::Bool(None) => "NULL".to_string(),
247			Self::TinyInt(Some(v)) => v.to_string(),
248			Self::TinyInt(None) => "NULL".to_string(),
249			Self::SmallInt(Some(v)) => v.to_string(),
250			Self::SmallInt(None) => "NULL".to_string(),
251			Self::Int(Some(v)) => v.to_string(),
252			Self::Int(None) => "NULL".to_string(),
253			Self::BigInt(Some(v)) => v.to_string(),
254			Self::BigInt(None) => "NULL".to_string(),
255			Self::TinyUnsigned(Some(v)) => v.to_string(),
256			Self::TinyUnsigned(None) => "NULL".to_string(),
257			Self::SmallUnsigned(Some(v)) => v.to_string(),
258			Self::SmallUnsigned(None) => "NULL".to_string(),
259			Self::Unsigned(Some(v)) => v.to_string(),
260			Self::Unsigned(None) => "NULL".to_string(),
261			Self::BigUnsigned(Some(v)) => v.to_string(),
262			Self::BigUnsigned(None) => "NULL".to_string(),
263			Self::Float(Some(v)) => v.to_string(),
264			Self::Float(None) => "NULL".to_string(),
265			Self::Double(Some(v)) => v.to_string(),
266			Self::Double(None) => "NULL".to_string(),
267			Self::Char(Some(v)) => {
268				// Escape single quotes by doubling them
269				if *v == '\'' {
270					"''''".to_string()
271				} else {
272					format!("'{}'", v)
273				}
274			}
275			Self::Char(None) => "NULL".to_string(),
276			Self::String(Some(v)) => {
277				// Escape single quotes by doubling them
278				format!("'{}'", v.replace('\'', "''"))
279			}
280			Self::String(None) => "NULL".to_string(),
281			Self::Bytes(Some(v)) => {
282				// Render as hex-encoded string with X prefix
283				let hex: String = v.iter().map(|b| format!("{:02X}", b)).collect();
284				format!("X'{}'", hex)
285			}
286			Self::Bytes(None) => "NULL".to_string(),
287			#[cfg(feature = "with-chrono")]
288			Self::ChronoDate(Some(v)) => format!("'{}'", v),
289			#[cfg(feature = "with-chrono")]
290			Self::ChronoDate(None) => "NULL".to_string(),
291			#[cfg(feature = "with-chrono")]
292			Self::ChronoTime(Some(v)) => format!("'{}'", v),
293			#[cfg(feature = "with-chrono")]
294			Self::ChronoTime(None) => "NULL".to_string(),
295			#[cfg(feature = "with-chrono")]
296			Self::ChronoDateTime(Some(v)) => format!("'{}'", v),
297			#[cfg(feature = "with-chrono")]
298			Self::ChronoDateTime(None) => "NULL".to_string(),
299			#[cfg(feature = "with-chrono")]
300			Self::ChronoDateTimeUtc(Some(v)) => format!("'{}'", v.to_rfc3339()),
301			#[cfg(feature = "with-chrono")]
302			Self::ChronoDateTimeUtc(None) => "NULL".to_string(),
303			#[cfg(feature = "with-chrono")]
304			Self::ChronoDateTimeLocal(Some(v)) => format!("'{}'", v.to_rfc3339()),
305			#[cfg(feature = "with-chrono")]
306			Self::ChronoDateTimeLocal(None) => "NULL".to_string(),
307			#[cfg(feature = "with-chrono")]
308			Self::ChronoDateTimeWithTimeZone(Some(v)) => format!("'{}'", v.to_rfc3339()),
309			#[cfg(feature = "with-chrono")]
310			Self::ChronoDateTimeWithTimeZone(None) => "NULL".to_string(),
311			#[cfg(feature = "with-uuid")]
312			Self::Uuid(Some(v)) => format!("'{}'", v),
313			#[cfg(feature = "with-uuid")]
314			Self::Uuid(None) => "NULL".to_string(),
315			#[cfg(feature = "with-json")]
316			Self::Json(Some(v)) => {
317				let json_str = serde_json::to_string(v.as_ref()).unwrap_or_default();
318				format!("'{}'", json_str.replace('\'', "''"))
319			}
320			#[cfg(feature = "with-json")]
321			Self::Json(None) => "NULL".to_string(),
322			#[cfg(feature = "with-rust_decimal")]
323			Self::Decimal(Some(v)) => v.to_string(),
324			#[cfg(feature = "with-rust_decimal")]
325			Self::Decimal(None) => "NULL".to_string(),
326			#[cfg(feature = "with-bigdecimal")]
327			Self::BigDecimal(Some(v)) => v.to_string(),
328			#[cfg(feature = "with-bigdecimal")]
329			Self::BigDecimal(None) => "NULL".to_string(),
330			Self::Array(_, Some(values)) => {
331				let items: Vec<String> = values.iter().map(|v| v.to_sql_literal()).collect();
332				format!("ARRAY[{}]", items.join(","))
333			}
334			Self::Array(_, None) => "NULL".to_string(),
335		}
336	}
337}
338
339impl Default for Value {
340	/// Returns the default value, which is a null string.
341	fn default() -> Self {
342		Self::String(None)
343	}
344}