hdbconnect_arrow/types/hana.rs
1//! HANA SQL type representations with compile-time safety.
2//!
3//! Uses phantom types and newtypes to ensure type safety at compile time.
4//! Phantom types encode type categories without runtime cost, while newtypes
5//! like [`DecimalPrecision`] provide validated values.
6//!
7//! # Phantom Types
8//!
9//! The type category markers (e.g., [`Numeric`], [`Decimal`]) are used as
10//! phantom type parameters to [`TypedColumn`], enabling type-safe operations
11//! at compile time.
12//!
13//! ```rust,ignore
14//! let numeric_col: TypedColumn<Numeric> = TypedColumn::new("amount", true);
15//! let decimal_col: TypedColumn<Decimal> = TypedColumn::new("price", true)
16//! .with_precision(18)
17//! .with_scale(2);
18//!
19//! // These are different types - cannot be mixed up!
20//! ```
21
22use std::marker::PhantomData;
23
24use crate::traits::sealed::private::Sealed;
25
26/// Marker trait for HANA type categories.
27///
28/// Sealed to prevent external implementations.
29/// Each category groups related HANA SQL types.
30pub trait HanaTypeCategory: Sealed {
31 /// The name of this category for debugging/logging.
32 const CATEGORY_NAME: &'static str;
33}
34
35// ═══════════════════════════════════════════════════════════════════════════
36// Type Category Markers
37// ═══════════════════════════════════════════════════════════════════════════
38
39/// Marker for numeric types (TINYINT, SMALLINT, INT, BIGINT, REAL, DOUBLE).
40///
41/// These types map directly to Arrow primitive arrays.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Numeric {}
44
45impl Sealed for Numeric {}
46impl HanaTypeCategory for Numeric {
47 const CATEGORY_NAME: &'static str = "Numeric";
48}
49
50/// Marker for decimal types (DECIMAL, SMALLDECIMAL).
51///
52/// These types require precision and scale tracking and map to Arrow Decimal128.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum Decimal {}
55
56impl Sealed for Decimal {}
57impl HanaTypeCategory for Decimal {
58 const CATEGORY_NAME: &'static str = "Decimal";
59}
60
61/// Marker for string types (CHAR, VARCHAR, NCHAR, NVARCHAR, SHORTTEXT, etc.).
62///
63/// All string types map to Arrow Utf8 or `LargeUtf8`.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum StringType {}
66
67impl Sealed for StringType {}
68impl HanaTypeCategory for StringType {
69 const CATEGORY_NAME: &'static str = "String";
70}
71
72/// Marker for binary types (BINARY, VARBINARY).
73///
74/// Maps to Arrow Binary or `FixedSizeBinary`.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum Binary {}
77
78impl Sealed for Binary {}
79impl HanaTypeCategory for Binary {
80 const CATEGORY_NAME: &'static str = "Binary";
81}
82
83/// Marker for LOB types (CLOB, NCLOB, BLOB, TEXT).
84///
85/// LOB types require special streaming handling for large values.
86/// Maps to Arrow `LargeUtf8` or `LargeBinary`.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum Lob {}
89
90impl Sealed for Lob {}
91impl HanaTypeCategory for Lob {
92 const CATEGORY_NAME: &'static str = "LOB";
93}
94
95/// Marker for temporal types (DATE, TIME, TIMESTAMP, SECONDDATE, etc.).
96///
97/// Maps to Arrow Date32, Time64, or Timestamp.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum Temporal {}
100
101impl Sealed for Temporal {}
102impl HanaTypeCategory for Temporal {
103 const CATEGORY_NAME: &'static str = "Temporal";
104}
105
106/// Marker for spatial types (`ST_GEOMETRY`, `ST_POINT`).
107///
108/// Spatial types are serialized as WKB binary.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum Spatial {}
111
112impl Sealed for Spatial {}
113impl HanaTypeCategory for Spatial {
114 const CATEGORY_NAME: &'static str = "Spatial";
115}
116
117// ═══════════════════════════════════════════════════════════════════════════
118// TypedColumn with Phantom Type
119// ═══════════════════════════════════════════════════════════════════════════
120
121/// A HANA column descriptor with phantom type for category.
122///
123/// The phantom type parameter encodes the column's type category,
124/// enabling type-safe operations at compile time.
125///
126/// # Example
127///
128/// ```rust,ignore
129/// use hdbconnect_arrow::types::hana::{TypedColumn, Numeric, Decimal};
130///
131/// let numeric: TypedColumn<Numeric> = TypedColumn::new("count", false);
132/// let decimal: TypedColumn<Decimal> = TypedColumn::new("price", true)
133/// .with_precision(18)
134/// .with_scale(2);
135///
136/// // Type system prevents mixing column categories
137/// ```
138#[derive(Debug, Clone)]
139pub struct TypedColumn<C: HanaTypeCategory> {
140 name: String,
141 nullable: bool,
142 precision: Option<u8>,
143 scale: Option<i8>,
144 _category: PhantomData<C>,
145}
146
147impl<C: HanaTypeCategory> TypedColumn<C> {
148 /// Create a new typed column descriptor.
149 ///
150 /// # Arguments
151 ///
152 /// * `name` - Column name
153 /// * `nullable` - Whether the column allows NULL values
154 #[must_use]
155 pub fn new(name: impl Into<String>, nullable: bool) -> Self {
156 Self {
157 name: name.into(),
158 nullable,
159 precision: None,
160 scale: None,
161 _category: PhantomData,
162 }
163 }
164
165 /// Set precision for decimal/numeric columns.
166 #[must_use]
167 pub const fn with_precision(mut self, precision: u8) -> Self {
168 self.precision = Some(precision);
169 self
170 }
171
172 /// Set scale for decimal columns.
173 #[must_use]
174 pub const fn with_scale(mut self, scale: i8) -> Self {
175 self.scale = Some(scale);
176 self
177 }
178
179 /// Returns the column name.
180 #[must_use]
181 pub fn name(&self) -> &str {
182 &self.name
183 }
184
185 /// Returns whether the column is nullable.
186 #[must_use]
187 pub const fn nullable(&self) -> bool {
188 self.nullable
189 }
190
191 /// Returns the precision, if set.
192 #[must_use]
193 pub const fn precision(&self) -> Option<u8> {
194 self.precision
195 }
196
197 /// Returns the scale, if set.
198 #[must_use]
199 pub const fn scale(&self) -> Option<i8> {
200 self.scale
201 }
202
203 /// Returns the type category name.
204 #[must_use]
205 pub const fn category_name(&self) -> &'static str {
206 C::CATEGORY_NAME
207 }
208}
209
210// ═══════════════════════════════════════════════════════════════════════════
211// Validated Newtypes
212// ═══════════════════════════════════════════════════════════════════════════
213
214/// Validated precision for DECIMAL types.
215///
216/// HANA DECIMAL supports precision 1-38. This newtype ensures values
217/// are validated at construction time.
218///
219/// # Example
220///
221/// ```rust,ignore
222/// use hdbconnect_arrow::types::hana::DecimalPrecision;
223///
224/// let precision = DecimalPrecision::new(18)?; // OK
225/// let invalid = DecimalPrecision::new(0); // Error
226/// let invalid = DecimalPrecision::new(39); // Error
227/// ```
228#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
229pub struct DecimalPrecision(u8);
230
231impl DecimalPrecision {
232 /// Maximum precision for HANA DECIMAL (38 digits).
233 pub const MAX: u8 = 38;
234
235 /// Minimum precision for HANA DECIMAL (1 digit).
236 pub const MIN: u8 = 1;
237
238 /// Create a new validated precision value.
239 ///
240 /// # Errors
241 ///
242 /// Returns an error if precision is 0 or greater than 38.
243 pub fn new(precision: u8) -> crate::Result<Self> {
244 if !(Self::MIN..=Self::MAX).contains(&precision) {
245 return Err(crate::ArrowConversionError::invalid_precision(format!(
246 "precision must be {}-{}, got {}",
247 Self::MIN,
248 Self::MAX,
249 precision
250 )));
251 }
252 Ok(Self(precision))
253 }
254
255 /// Create precision without validation (for internal use).
256 ///
257 /// # Safety
258 ///
259 /// Caller must ensure precision is in valid range.
260 #[must_use]
261 #[allow(dead_code)]
262 pub(crate) const fn new_unchecked(precision: u8) -> Self {
263 Self(precision)
264 }
265
266 /// Returns the precision value.
267 #[must_use]
268 pub const fn value(self) -> u8 {
269 self.0
270 }
271}
272
273impl TryFrom<u8> for DecimalPrecision {
274 type Error = crate::ArrowConversionError;
275
276 fn try_from(value: u8) -> Result<Self, Self::Error> {
277 Self::new(value)
278 }
279}
280
281/// Validated scale for DECIMAL types.
282///
283/// Scale must be non-negative and not exceed precision.
284/// This newtype ensures values are validated at construction time.
285///
286/// # Example
287///
288/// ```rust,ignore
289/// use hdbconnect_arrow::types::hana::{DecimalPrecision, DecimalScale};
290///
291/// let precision = DecimalPrecision::new(18)?;
292/// let scale = DecimalScale::new(2, precision)?; // OK: 2 <= 18
293/// let invalid = DecimalScale::new(20, precision); // Error: 20 > 18
294/// ```
295#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
296pub struct DecimalScale(i8);
297
298impl DecimalScale {
299 /// Create a new validated scale value.
300 ///
301 /// # Arguments
302 ///
303 /// * `scale` - The scale value (must be non-negative)
304 /// * `precision` - The precision this scale is associated with
305 ///
306 /// # Errors
307 ///
308 /// Returns an error if scale is negative or exceeds precision.
309 pub fn new(scale: i8, precision: DecimalPrecision) -> crate::Result<Self> {
310 if scale < 0 {
311 return Err(crate::ArrowConversionError::invalid_scale(format!(
312 "scale must be non-negative, got {scale}"
313 )));
314 }
315 // Safe: scale is already checked to be non-negative
316 #[allow(clippy::cast_sign_loss)]
317 if scale as u8 > precision.value() {
318 return Err(crate::ArrowConversionError::invalid_scale(format!(
319 "scale ({}) cannot exceed precision ({})",
320 scale,
321 precision.value()
322 )));
323 }
324 Ok(Self(scale))
325 }
326
327 /// Create scale without validation (for internal use).
328 ///
329 /// # Safety
330 ///
331 /// Caller must ensure scale is valid for the associated precision.
332 #[must_use]
333 #[allow(dead_code)]
334 pub(crate) const fn new_unchecked(scale: i8) -> Self {
335 Self(scale)
336 }
337
338 /// Returns the scale value.
339 #[must_use]
340 pub const fn value(self) -> i8 {
341 self.0
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_decimal_precision_valid() {
351 assert!(DecimalPrecision::new(1).is_ok());
352 assert!(DecimalPrecision::new(18).is_ok());
353 assert!(DecimalPrecision::new(38).is_ok());
354 }
355
356 #[test]
357 fn test_decimal_precision_invalid() {
358 assert!(DecimalPrecision::new(0).is_err());
359 assert!(DecimalPrecision::new(39).is_err());
360 }
361
362 #[test]
363 fn test_decimal_scale_valid() {
364 let prec = DecimalPrecision::new(18).unwrap();
365 assert!(DecimalScale::new(0, prec).is_ok());
366 assert!(DecimalScale::new(2, prec).is_ok());
367 assert!(DecimalScale::new(18, prec).is_ok());
368 }
369
370 #[test]
371 fn test_decimal_scale_invalid() {
372 let prec = DecimalPrecision::new(18).unwrap();
373 assert!(DecimalScale::new(-1, prec).is_err());
374 assert!(DecimalScale::new(19, prec).is_err());
375 }
376
377 #[test]
378 fn test_typed_column() {
379 let col: TypedColumn<Numeric> = TypedColumn::new("amount", false);
380 assert_eq!(col.name(), "amount");
381 assert!(!col.nullable());
382 assert_eq!(col.category_name(), "Numeric");
383 }
384
385 #[test]
386 fn test_typed_column_decimal() {
387 let col: TypedColumn<Decimal> = TypedColumn::new("price", true)
388 .with_precision(18)
389 .with_scale(2);
390
391 assert_eq!(col.name(), "price");
392 assert!(col.nullable());
393 assert_eq!(col.precision(), Some(18));
394 assert_eq!(col.scale(), Some(2));
395 assert_eq!(col.category_name(), "Decimal");
396 }
397
398 #[test]
399 fn test_category_names() {
400 assert_eq!(Numeric::CATEGORY_NAME, "Numeric");
401 assert_eq!(Decimal::CATEGORY_NAME, "Decimal");
402 assert_eq!(StringType::CATEGORY_NAME, "String");
403 assert_eq!(Binary::CATEGORY_NAME, "Binary");
404 assert_eq!(Lob::CATEGORY_NAME, "LOB");
405 assert_eq!(Temporal::CATEGORY_NAME, "Temporal");
406 assert_eq!(Spatial::CATEGORY_NAME, "Spatial");
407 }
408}