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
84impl LogicalType {
85    /// Returns true if this type is numeric (integer or floating point).
86    #[must_use]
87    pub const fn is_numeric(&self) -> bool {
88        matches!(
89            self,
90            LogicalType::Int8
91                | LogicalType::Int16
92                | LogicalType::Int32
93                | LogicalType::Int64
94                | LogicalType::Float32
95                | LogicalType::Float64
96        )
97    }
98
99    /// Returns true if this type is an integer type.
100    #[must_use]
101    pub const fn is_integer(&self) -> bool {
102        matches!(
103            self,
104            LogicalType::Int8 | LogicalType::Int16 | LogicalType::Int32 | LogicalType::Int64
105        )
106    }
107
108    /// Returns true if this type is a floating point type.
109    #[must_use]
110    pub const fn is_float(&self) -> bool {
111        matches!(self, LogicalType::Float32 | LogicalType::Float64)
112    }
113
114    /// Returns true if this type is a temporal type.
115    #[must_use]
116    pub const fn is_temporal(&self) -> bool {
117        matches!(
118            self,
119            LogicalType::Date | LogicalType::Time | LogicalType::Timestamp | LogicalType::Duration
120        )
121    }
122
123    /// Returns true if this type is a graph element type.
124    #[must_use]
125    pub const fn is_graph_element(&self) -> bool {
126        matches!(
127            self,
128            LogicalType::Node | LogicalType::Edge | LogicalType::Path
129        )
130    }
131
132    /// Returns true if this type is nullable (can hold NULL values).
133    ///
134    /// In Grafeo, all types except Null itself are nullable by default.
135    #[must_use]
136    pub const fn is_nullable(&self) -> bool {
137        true
138    }
139
140    /// Returns the element type if this is a List, otherwise None.
141    #[must_use]
142    pub fn list_element_type(&self) -> Option<&LogicalType> {
143        match self {
144            LogicalType::List(elem) => Some(elem),
145            _ => None,
146        }
147    }
148
149    /// Checks if a value of `other` type can be implicitly coerced to this type.
150    #[must_use]
151    pub fn can_coerce_from(&self, other: &LogicalType) -> bool {
152        if self == other {
153            return true;
154        }
155
156        // Any accepts everything
157        if matches!(self, LogicalType::Any) {
158            return true;
159        }
160
161        // Null coerces to any nullable type
162        if matches!(other, LogicalType::Null) && self.is_nullable() {
163            return true;
164        }
165
166        // Numeric coercion: smaller integers coerce to larger
167        match (other, self) {
168            (LogicalType::Int8, LogicalType::Int16 | LogicalType::Int32 | LogicalType::Int64) => {
169                true
170            }
171            (LogicalType::Int16, LogicalType::Int32 | LogicalType::Int64) => true,
172            (LogicalType::Int32, LogicalType::Int64) => true,
173            (LogicalType::Float32, LogicalType::Float64) => true,
174            // Integers coerce to floats
175            (
176                LogicalType::Int8 | LogicalType::Int16 | LogicalType::Int32,
177                LogicalType::Float32 | LogicalType::Float64,
178            ) => true,
179            (LogicalType::Int64, LogicalType::Float64) => true,
180            _ => false,
181        }
182    }
183
184    /// Returns the common supertype of two types, if one exists.
185    #[must_use]
186    pub fn common_type(&self, other: &LogicalType) -> Option<LogicalType> {
187        if self == other {
188            return Some(self.clone());
189        }
190
191        // Handle Any
192        if matches!(self, LogicalType::Any) {
193            return Some(other.clone());
194        }
195        if matches!(other, LogicalType::Any) {
196            return Some(self.clone());
197        }
198
199        // Handle Null
200        if matches!(self, LogicalType::Null) {
201            return Some(other.clone());
202        }
203        if matches!(other, LogicalType::Null) {
204            return Some(self.clone());
205        }
206
207        // Numeric promotion
208        if self.is_numeric() && other.is_numeric() {
209            // Float64 is the ultimate numeric type
210            if self.is_float() || other.is_float() {
211                return Some(LogicalType::Float64);
212            }
213            // Otherwise promote to largest integer
214            return Some(LogicalType::Int64);
215        }
216
217        None
218    }
219}
220
221impl fmt::Display for LogicalType {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            LogicalType::Any => write!(f, "ANY"),
225            LogicalType::Null => write!(f, "NULL"),
226            LogicalType::Bool => write!(f, "BOOL"),
227            LogicalType::Int8 => write!(f, "INT8"),
228            LogicalType::Int16 => write!(f, "INT16"),
229            LogicalType::Int32 => write!(f, "INT32"),
230            LogicalType::Int64 => write!(f, "INT64"),
231            LogicalType::Float32 => write!(f, "FLOAT32"),
232            LogicalType::Float64 => write!(f, "FLOAT64"),
233            LogicalType::String => write!(f, "STRING"),
234            LogicalType::Bytes => write!(f, "BYTES"),
235            LogicalType::Date => write!(f, "DATE"),
236            LogicalType::Time => write!(f, "TIME"),
237            LogicalType::Timestamp => write!(f, "TIMESTAMP"),
238            LogicalType::Duration => write!(f, "DURATION"),
239            LogicalType::List(elem) => write!(f, "LIST<{elem}>"),
240            LogicalType::Map { key, value } => write!(f, "MAP<{key}, {value}>"),
241            LogicalType::Struct(fields) => {
242                write!(f, "STRUCT<")?;
243                for (i, (name, ty)) in fields.iter().enumerate() {
244                    if i > 0 {
245                        write!(f, ", ")?;
246                    }
247                    write!(f, "{name}: {ty}")?;
248                }
249                write!(f, ">")
250            }
251            LogicalType::Node => write!(f, "NODE"),
252            LogicalType::Edge => write!(f, "EDGE"),
253            LogicalType::Path => write!(f, "PATH"),
254        }
255    }
256}
257
258impl Default for LogicalType {
259    fn default() -> Self {
260        LogicalType::Any
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_numeric_checks() {
270        assert!(LogicalType::Int64.is_numeric());
271        assert!(LogicalType::Float64.is_numeric());
272        assert!(!LogicalType::String.is_numeric());
273
274        assert!(LogicalType::Int64.is_integer());
275        assert!(!LogicalType::Float64.is_integer());
276
277        assert!(LogicalType::Float64.is_float());
278        assert!(!LogicalType::Int64.is_float());
279    }
280
281    #[test]
282    fn test_coercion() {
283        // Same type always coerces
284        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Int64));
285
286        // Null coerces to anything
287        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Null));
288        assert!(LogicalType::String.can_coerce_from(&LogicalType::Null));
289
290        // Integer widening
291        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Int32));
292        assert!(LogicalType::Int32.can_coerce_from(&LogicalType::Int16));
293        assert!(!LogicalType::Int32.can_coerce_from(&LogicalType::Int64));
294
295        // Float widening
296        assert!(LogicalType::Float64.can_coerce_from(&LogicalType::Float32));
297
298        // Int to float
299        assert!(LogicalType::Float64.can_coerce_from(&LogicalType::Int64));
300        assert!(LogicalType::Float32.can_coerce_from(&LogicalType::Int32));
301    }
302
303    #[test]
304    fn test_common_type() {
305        // Same types
306        assert_eq!(
307            LogicalType::Int64.common_type(&LogicalType::Int64),
308            Some(LogicalType::Int64)
309        );
310
311        // Numeric promotion
312        assert_eq!(
313            LogicalType::Int32.common_type(&LogicalType::Int64),
314            Some(LogicalType::Int64)
315        );
316        assert_eq!(
317            LogicalType::Int64.common_type(&LogicalType::Float64),
318            Some(LogicalType::Float64)
319        );
320
321        // Null handling
322        assert_eq!(
323            LogicalType::Null.common_type(&LogicalType::String),
324            Some(LogicalType::String)
325        );
326
327        // Incompatible types
328        assert_eq!(LogicalType::String.common_type(&LogicalType::Int64), None);
329    }
330
331    #[test]
332    fn test_display() {
333        assert_eq!(LogicalType::Int64.to_string(), "INT64");
334        assert_eq!(
335            LogicalType::List(Box::new(LogicalType::String)).to_string(),
336            "LIST<STRING>"
337        );
338        assert_eq!(
339            LogicalType::Map {
340                key: Box::new(LogicalType::String),
341                value: Box::new(LogicalType::Int64)
342            }
343            .to_string(),
344            "MAP<STRING, INT64>"
345        );
346    }
347}