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