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