Skip to main content

grafeo_common/types/
logical_type.rs

1//! The type system for schemas and query type checking.
2//!
3//! [`LogicalType`] describes the types that can appear in schemas and query
4//! results. Used by the query optimizer for type inference and coercion.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// Describes the type of a value or column.
10///
11/// Follows the GQL type system. Used for schema definitions and query type
12/// checking. Supports coercion rules (e.g., Int32 can coerce to Int64).
13#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[non_exhaustive]
15pub enum LogicalType {
16    /// Unknown or any type (used during type inference)
17    Any,
18
19    /// Null type (only value is NULL)
20    Null,
21
22    /// Boolean type
23    Bool,
24
25    /// 8-bit signed integer
26    Int8,
27
28    /// 16-bit signed integer
29    Int16,
30
31    /// 32-bit signed integer
32    Int32,
33
34    /// 64-bit signed integer
35    Int64,
36
37    /// 32-bit floating point
38    Float32,
39
40    /// 64-bit floating point
41    Float64,
42
43    /// Variable-length UTF-8 string
44    String,
45
46    /// Binary data
47    Bytes,
48
49    /// Date (year, month, day)
50    Date,
51
52    /// Time (hour, minute, second, nanosecond)
53    Time,
54
55    /// Timestamp with timezone
56    Timestamp,
57
58    /// Duration/interval
59    Duration,
60
61    /// Time of day with a required UTC offset
62    ZonedTime,
63
64    /// Datetime with a fixed UTC offset
65    ZonedDatetime,
66
67    /// Homogeneous list of elements
68    List(Box<LogicalType>),
69
70    /// Key-value map
71    Map {
72        /// Type of map keys (usually String)
73        key: Box<LogicalType>,
74        /// Type of map values
75        value: Box<LogicalType>,
76    },
77
78    /// Struct with named fields
79    Struct(Vec<(String, LogicalType)>),
80
81    /// Node reference
82    Node,
83
84    /// Edge reference
85    Edge,
86
87    /// Path (sequence of nodes and edges)
88    Path,
89
90    /// Fixed-dimension vector of floats (for embeddings).
91    ///
92    /// The usize parameter is the dimension (e.g., 384, 768, 1536).
93    /// Common embedding sizes: 384 (MiniLM), 768 (BERT), 1536 (OpenAI).
94    Vector(usize),
95}
96
97impl LogicalType {
98    /// Returns true if this type is numeric (integer or floating point).
99    #[must_use]
100    pub const fn is_numeric(&self) -> bool {
101        matches!(
102            self,
103            LogicalType::Int8
104                | LogicalType::Int16
105                | LogicalType::Int32
106                | LogicalType::Int64
107                | LogicalType::Float32
108                | LogicalType::Float64
109        )
110    }
111
112    /// Returns true if this type is an integer type.
113    #[must_use]
114    pub const fn is_integer(&self) -> bool {
115        matches!(
116            self,
117            LogicalType::Int8 | LogicalType::Int16 | LogicalType::Int32 | LogicalType::Int64
118        )
119    }
120
121    /// Returns true if this type is a floating point type.
122    #[must_use]
123    pub const fn is_float(&self) -> bool {
124        matches!(self, LogicalType::Float32 | LogicalType::Float64)
125    }
126
127    /// Returns true if this type is a temporal type.
128    #[must_use]
129    pub const fn is_temporal(&self) -> bool {
130        matches!(
131            self,
132            LogicalType::Date
133                | LogicalType::Time
134                | LogicalType::Timestamp
135                | LogicalType::Duration
136                | LogicalType::ZonedTime
137                | LogicalType::ZonedDatetime
138        )
139    }
140
141    /// Returns true if this type is a graph element type.
142    #[must_use]
143    pub const fn is_graph_element(&self) -> bool {
144        matches!(
145            self,
146            LogicalType::Node | LogicalType::Edge | LogicalType::Path
147        )
148    }
149
150    /// Returns true if this type is nullable (can hold NULL values).
151    ///
152    /// In Grafeo, all types except Null itself are nullable by default.
153    #[must_use]
154    pub const fn is_nullable(&self) -> bool {
155        true
156    }
157
158    /// Returns the element type if this is a List, otherwise None.
159    #[must_use]
160    pub fn list_element_type(&self) -> Option<&LogicalType> {
161        match self {
162            LogicalType::List(elem) => Some(elem),
163            _ => None,
164        }
165    }
166
167    /// Returns true if this type is a vector type.
168    #[must_use]
169    pub const fn is_vector(&self) -> bool {
170        matches!(self, LogicalType::Vector(_))
171    }
172
173    /// Returns the vector dimensions if this is a Vector type.
174    #[must_use]
175    pub const fn vector_dimensions(&self) -> Option<usize> {
176        match self {
177            LogicalType::Vector(dim) => Some(*dim),
178            _ => None,
179        }
180    }
181
182    /// Checks if a value of `other` type can be implicitly coerced to this type.
183    #[must_use]
184    pub fn can_coerce_from(&self, other: &LogicalType) -> bool {
185        if self == other {
186            return true;
187        }
188
189        // Any accepts everything
190        if matches!(self, LogicalType::Any) {
191            return true;
192        }
193
194        // Null coerces to any nullable type
195        if matches!(other, LogicalType::Null) && self.is_nullable() {
196            return true;
197        }
198
199        // Numeric coercion: smaller integers coerce to larger
200        match (other, self) {
201            (LogicalType::Int8, LogicalType::Int16 | LogicalType::Int32 | LogicalType::Int64) => {
202                true
203            }
204            (LogicalType::Int16, LogicalType::Int32 | LogicalType::Int64) => true,
205            (LogicalType::Int32, LogicalType::Int64) => true,
206            (LogicalType::Float32, LogicalType::Float64) => true,
207            // Integers coerce to floats
208            (
209                LogicalType::Int8 | LogicalType::Int16 | LogicalType::Int32,
210                LogicalType::Float32 | LogicalType::Float64,
211            ) => true,
212            (LogicalType::Int64, LogicalType::Float64) => true,
213            // Temporal coercion: Time <-> ZonedTime, Timestamp <-> ZonedDatetime
214            (LogicalType::Time, LogicalType::ZonedTime) => true,
215            (LogicalType::Timestamp, LogicalType::ZonedDatetime) => true,
216            _ => false,
217        }
218    }
219
220    /// Returns the common supertype of two types, if one exists.
221    #[must_use]
222    pub fn common_type(&self, other: &LogicalType) -> Option<LogicalType> {
223        if self == other {
224            return Some(self.clone());
225        }
226
227        // Handle Any
228        if matches!(self, LogicalType::Any) {
229            return Some(other.clone());
230        }
231        if matches!(other, LogicalType::Any) {
232            return Some(self.clone());
233        }
234
235        // Handle Null
236        if matches!(self, LogicalType::Null) {
237            return Some(other.clone());
238        }
239        if matches!(other, LogicalType::Null) {
240            return Some(self.clone());
241        }
242
243        // Numeric promotion
244        if self.is_numeric() && other.is_numeric() {
245            // Float64 is the ultimate numeric type
246            if self.is_float() || other.is_float() {
247                return Some(LogicalType::Float64);
248            }
249            // Otherwise promote to largest integer
250            return Some(LogicalType::Int64);
251        }
252
253        // Temporal promotion: zoned wins over local
254        match (self, other) {
255            (LogicalType::Time, LogicalType::ZonedTime)
256            | (LogicalType::ZonedTime, LogicalType::Time) => {
257                return Some(LogicalType::ZonedTime);
258            }
259            (LogicalType::Timestamp, LogicalType::ZonedDatetime)
260            | (LogicalType::ZonedDatetime, LogicalType::Timestamp) => {
261                return Some(LogicalType::ZonedDatetime);
262            }
263            _ => {}
264        }
265
266        None
267    }
268}
269
270impl fmt::Display for LogicalType {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        match self {
273            LogicalType::Any => write!(f, "ANY"),
274            LogicalType::Null => write!(f, "NULL"),
275            LogicalType::Bool => write!(f, "BOOL"),
276            LogicalType::Int8 => write!(f, "INT8"),
277            LogicalType::Int16 => write!(f, "INT16"),
278            LogicalType::Int32 => write!(f, "INT32"),
279            LogicalType::Int64 => write!(f, "INT64"),
280            LogicalType::Float32 => write!(f, "FLOAT32"),
281            LogicalType::Float64 => write!(f, "FLOAT64"),
282            LogicalType::String => write!(f, "STRING"),
283            LogicalType::Bytes => write!(f, "BYTES"),
284            LogicalType::Date => write!(f, "DATE"),
285            LogicalType::Time => write!(f, "TIME"),
286            LogicalType::Timestamp => write!(f, "TIMESTAMP"),
287            LogicalType::Duration => write!(f, "DURATION"),
288            LogicalType::ZonedTime => write!(f, "ZONED TIME"),
289            LogicalType::ZonedDatetime => write!(f, "ZONED DATETIME"),
290            LogicalType::List(elem) => write!(f, "LIST<{elem}>"),
291            LogicalType::Map { key, value } => write!(f, "MAP<{key}, {value}>"),
292            LogicalType::Struct(fields) => {
293                write!(f, "STRUCT<")?;
294                for (i, (name, ty)) in fields.iter().enumerate() {
295                    if i > 0 {
296                        write!(f, ", ")?;
297                    }
298                    write!(f, "{name}: {ty}")?;
299                }
300                write!(f, ">")
301            }
302            LogicalType::Node => write!(f, "NODE"),
303            LogicalType::Edge => write!(f, "EDGE"),
304            LogicalType::Path => write!(f, "PATH"),
305            LogicalType::Vector(dim) => write!(f, "VECTOR({dim})"),
306        }
307    }
308}
309
310impl Default for LogicalType {
311    fn default() -> Self {
312        LogicalType::Any
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn test_numeric_checks() {
322        assert!(LogicalType::Int64.is_numeric());
323        assert!(LogicalType::Float64.is_numeric());
324        assert!(!LogicalType::String.is_numeric());
325
326        assert!(LogicalType::Int64.is_integer());
327        assert!(!LogicalType::Float64.is_integer());
328
329        assert!(LogicalType::Float64.is_float());
330        assert!(!LogicalType::Int64.is_float());
331    }
332
333    #[test]
334    fn test_coercion() {
335        // Same type always coerces
336        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Int64));
337
338        // Null coerces to anything
339        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Null));
340        assert!(LogicalType::String.can_coerce_from(&LogicalType::Null));
341
342        // Integer widening
343        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Int32));
344        assert!(LogicalType::Int32.can_coerce_from(&LogicalType::Int16));
345        assert!(!LogicalType::Int32.can_coerce_from(&LogicalType::Int64));
346
347        // Float widening
348        assert!(LogicalType::Float64.can_coerce_from(&LogicalType::Float32));
349
350        // Int to float
351        assert!(LogicalType::Float64.can_coerce_from(&LogicalType::Int64));
352        assert!(LogicalType::Float32.can_coerce_from(&LogicalType::Int32));
353    }
354
355    #[test]
356    fn test_common_type() {
357        // Same types
358        assert_eq!(
359            LogicalType::Int64.common_type(&LogicalType::Int64),
360            Some(LogicalType::Int64)
361        );
362
363        // Numeric promotion
364        assert_eq!(
365            LogicalType::Int32.common_type(&LogicalType::Int64),
366            Some(LogicalType::Int64)
367        );
368        assert_eq!(
369            LogicalType::Int64.common_type(&LogicalType::Float64),
370            Some(LogicalType::Float64)
371        );
372
373        // Null handling
374        assert_eq!(
375            LogicalType::Null.common_type(&LogicalType::String),
376            Some(LogicalType::String)
377        );
378
379        // Incompatible types
380        assert_eq!(LogicalType::String.common_type(&LogicalType::Int64), None);
381    }
382
383    #[test]
384    fn test_display() {
385        assert_eq!(LogicalType::Int64.to_string(), "INT64");
386        assert_eq!(
387            LogicalType::List(Box::new(LogicalType::String)).to_string(),
388            "LIST<STRING>"
389        );
390        assert_eq!(
391            LogicalType::Map {
392                key: Box::new(LogicalType::String),
393                value: Box::new(LogicalType::Int64)
394            }
395            .to_string(),
396            "MAP<STRING, INT64>"
397        );
398    }
399
400    #[test]
401    fn test_vector_type() {
402        let v384 = LogicalType::Vector(384);
403        let v768 = LogicalType::Vector(768);
404        let v1536 = LogicalType::Vector(1536);
405
406        // Type checks
407        assert!(v384.is_vector());
408        assert!(v768.is_vector());
409        assert!(!LogicalType::Float64.is_vector());
410        assert!(!LogicalType::List(Box::new(LogicalType::Float32)).is_vector());
411
412        // Dimensions
413        assert_eq!(v384.vector_dimensions(), Some(384));
414        assert_eq!(v768.vector_dimensions(), Some(768));
415        assert_eq!(v1536.vector_dimensions(), Some(1536));
416        assert_eq!(LogicalType::Float64.vector_dimensions(), None);
417
418        // Display
419        assert_eq!(v384.to_string(), "VECTOR(384)");
420        assert_eq!(v768.to_string(), "VECTOR(768)");
421        assert_eq!(v1536.to_string(), "VECTOR(1536)");
422
423        // Equality
424        assert_eq!(LogicalType::Vector(384), LogicalType::Vector(384));
425        assert_ne!(LogicalType::Vector(384), LogicalType::Vector(768));
426
427        // Not numeric
428        assert!(!v384.is_numeric());
429        assert!(!v384.is_integer());
430        assert!(!v384.is_float());
431    }
432}