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}