Skip to main content

graphos_common/types/
logical_type.rs

1//! Logical type system for Graphos.
2//!
3//! This module defines the type system used for schema definitions and
4//! type checking in queries.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// Logical type for values in Graphos.
10///
11/// These types correspond to the GQL/Cypher type system and are used for:
12/// - Schema definitions (column types in node/edge tables)
13/// - Query type checking
14/// - Value coercion rules
15#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum LogicalType {
17    /// Unknown or any type (used during type inference)
18    Any,
19
20    /// Null type (only value is NULL)
21    Null,
22
23    /// Boolean type
24    Bool,
25
26    /// 8-bit signed integer
27    Int8,
28
29    /// 16-bit signed integer
30    Int16,
31
32    /// 32-bit signed integer
33    Int32,
34
35    /// 64-bit signed integer
36    Int64,
37
38    /// 32-bit floating point
39    Float32,
40
41    /// 64-bit floating point
42    Float64,
43
44    /// Variable-length UTF-8 string
45    String,
46
47    /// Binary data
48    Bytes,
49
50    /// Date (year, month, day)
51    Date,
52
53    /// Time (hour, minute, second, nanosecond)
54    Time,
55
56    /// Timestamp with timezone
57    Timestamp,
58
59    /// Duration/interval
60    Duration,
61
62    /// Homogeneous list of elements
63    List(Box<LogicalType>),
64
65    /// Key-value map
66    Map {
67        /// Type of map keys (usually String)
68        key: Box<LogicalType>,
69        /// Type of map values
70        value: Box<LogicalType>,
71    },
72
73    /// Struct with named fields
74    Struct(Vec<(String, LogicalType)>),
75
76    /// Node reference
77    Node,
78
79    /// Edge reference
80    Edge,
81
82    /// Path (sequence of nodes and edges)
83    Path,
84}
85
86impl LogicalType {
87    /// Returns true if this type is numeric (integer or floating point).
88    #[must_use]
89    pub const fn is_numeric(&self) -> bool {
90        matches!(
91            self,
92            LogicalType::Int8
93                | LogicalType::Int16
94                | LogicalType::Int32
95                | LogicalType::Int64
96                | LogicalType::Float32
97                | LogicalType::Float64
98        )
99    }
100
101    /// Returns true if this type is an integer type.
102    #[must_use]
103    pub const fn is_integer(&self) -> bool {
104        matches!(
105            self,
106            LogicalType::Int8 | LogicalType::Int16 | LogicalType::Int32 | LogicalType::Int64
107        )
108    }
109
110    /// Returns true if this type is a floating point type.
111    #[must_use]
112    pub const fn is_float(&self) -> bool {
113        matches!(self, LogicalType::Float32 | LogicalType::Float64)
114    }
115
116    /// Returns true if this type is a temporal type.
117    #[must_use]
118    pub const fn is_temporal(&self) -> bool {
119        matches!(
120            self,
121            LogicalType::Date | LogicalType::Time | LogicalType::Timestamp | LogicalType::Duration
122        )
123    }
124
125    /// Returns true if this type is a graph element type.
126    #[must_use]
127    pub const fn is_graph_element(&self) -> bool {
128        matches!(
129            self,
130            LogicalType::Node | LogicalType::Edge | LogicalType::Path
131        )
132    }
133
134    /// Returns true if this type is nullable (can hold NULL values).
135    ///
136    /// In Graphos, all types except Null itself are nullable by default.
137    #[must_use]
138    pub const fn is_nullable(&self) -> bool {
139        true
140    }
141
142    /// Returns the element type if this is a List, otherwise None.
143    #[must_use]
144    pub fn list_element_type(&self) -> Option<&LogicalType> {
145        match self {
146            LogicalType::List(elem) => Some(elem),
147            _ => None,
148        }
149    }
150
151    /// Checks if a value of `other` type can be implicitly coerced to this type.
152    #[must_use]
153    pub fn can_coerce_from(&self, other: &LogicalType) -> bool {
154        if self == other {
155            return true;
156        }
157
158        // Any accepts everything
159        if matches!(self, LogicalType::Any) {
160            return true;
161        }
162
163        // Null coerces to any nullable type
164        if matches!(other, LogicalType::Null) && self.is_nullable() {
165            return true;
166        }
167
168        // Numeric coercion: smaller integers coerce to larger
169        match (other, self) {
170            (LogicalType::Int8, LogicalType::Int16 | LogicalType::Int32 | LogicalType::Int64) => {
171                true
172            }
173            (LogicalType::Int16, LogicalType::Int32 | LogicalType::Int64) => true,
174            (LogicalType::Int32, LogicalType::Int64) => true,
175            (LogicalType::Float32, LogicalType::Float64) => true,
176            // Integers coerce to floats
177            (
178                LogicalType::Int8 | LogicalType::Int16 | LogicalType::Int32,
179                LogicalType::Float32 | LogicalType::Float64,
180            ) => true,
181            (LogicalType::Int64, LogicalType::Float64) => true,
182            _ => false,
183        }
184    }
185
186    /// Returns the common supertype of two types, if one exists.
187    #[must_use]
188    pub fn common_type(&self, other: &LogicalType) -> Option<LogicalType> {
189        if self == other {
190            return Some(self.clone());
191        }
192
193        // Handle Any
194        if matches!(self, LogicalType::Any) {
195            return Some(other.clone());
196        }
197        if matches!(other, LogicalType::Any) {
198            return Some(self.clone());
199        }
200
201        // Handle Null
202        if matches!(self, LogicalType::Null) {
203            return Some(other.clone());
204        }
205        if matches!(other, LogicalType::Null) {
206            return Some(self.clone());
207        }
208
209        // Numeric promotion
210        if self.is_numeric() && other.is_numeric() {
211            // Float64 is the ultimate numeric type
212            if self.is_float() || other.is_float() {
213                return Some(LogicalType::Float64);
214            }
215            // Otherwise promote to largest integer
216            return Some(LogicalType::Int64);
217        }
218
219        None
220    }
221}
222
223impl fmt::Display for LogicalType {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        match self {
226            LogicalType::Any => write!(f, "ANY"),
227            LogicalType::Null => write!(f, "NULL"),
228            LogicalType::Bool => write!(f, "BOOL"),
229            LogicalType::Int8 => write!(f, "INT8"),
230            LogicalType::Int16 => write!(f, "INT16"),
231            LogicalType::Int32 => write!(f, "INT32"),
232            LogicalType::Int64 => write!(f, "INT64"),
233            LogicalType::Float32 => write!(f, "FLOAT32"),
234            LogicalType::Float64 => write!(f, "FLOAT64"),
235            LogicalType::String => write!(f, "STRING"),
236            LogicalType::Bytes => write!(f, "BYTES"),
237            LogicalType::Date => write!(f, "DATE"),
238            LogicalType::Time => write!(f, "TIME"),
239            LogicalType::Timestamp => write!(f, "TIMESTAMP"),
240            LogicalType::Duration => write!(f, "DURATION"),
241            LogicalType::List(elem) => write!(f, "LIST<{elem}>"),
242            LogicalType::Map { key, value } => write!(f, "MAP<{key}, {value}>"),
243            LogicalType::Struct(fields) => {
244                write!(f, "STRUCT<")?;
245                for (i, (name, ty)) in fields.iter().enumerate() {
246                    if i > 0 {
247                        write!(f, ", ")?;
248                    }
249                    write!(f, "{name}: {ty}")?;
250                }
251                write!(f, ">")
252            }
253            LogicalType::Node => write!(f, "NODE"),
254            LogicalType::Edge => write!(f, "EDGE"),
255            LogicalType::Path => write!(f, "PATH"),
256        }
257    }
258}
259
260impl Default for LogicalType {
261    fn default() -> Self {
262        LogicalType::Any
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_numeric_checks() {
272        assert!(LogicalType::Int64.is_numeric());
273        assert!(LogicalType::Float64.is_numeric());
274        assert!(!LogicalType::String.is_numeric());
275
276        assert!(LogicalType::Int64.is_integer());
277        assert!(!LogicalType::Float64.is_integer());
278
279        assert!(LogicalType::Float64.is_float());
280        assert!(!LogicalType::Int64.is_float());
281    }
282
283    #[test]
284    fn test_coercion() {
285        // Same type always coerces
286        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Int64));
287
288        // Null coerces to anything
289        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Null));
290        assert!(LogicalType::String.can_coerce_from(&LogicalType::Null));
291
292        // Integer widening
293        assert!(LogicalType::Int64.can_coerce_from(&LogicalType::Int32));
294        assert!(LogicalType::Int32.can_coerce_from(&LogicalType::Int16));
295        assert!(!LogicalType::Int32.can_coerce_from(&LogicalType::Int64));
296
297        // Float widening
298        assert!(LogicalType::Float64.can_coerce_from(&LogicalType::Float32));
299
300        // Int to float
301        assert!(LogicalType::Float64.can_coerce_from(&LogicalType::Int64));
302        assert!(LogicalType::Float32.can_coerce_from(&LogicalType::Int32));
303    }
304
305    #[test]
306    fn test_common_type() {
307        // Same types
308        assert_eq!(
309            LogicalType::Int64.common_type(&LogicalType::Int64),
310            Some(LogicalType::Int64)
311        );
312
313        // Numeric promotion
314        assert_eq!(
315            LogicalType::Int32.common_type(&LogicalType::Int64),
316            Some(LogicalType::Int64)
317        );
318        assert_eq!(
319            LogicalType::Int64.common_type(&LogicalType::Float64),
320            Some(LogicalType::Float64)
321        );
322
323        // Null handling
324        assert_eq!(
325            LogicalType::Null.common_type(&LogicalType::String),
326            Some(LogicalType::String)
327        );
328
329        // Incompatible types
330        assert_eq!(LogicalType::String.common_type(&LogicalType::Int64), None);
331    }
332
333    #[test]
334    fn test_display() {
335        assert_eq!(LogicalType::Int64.to_string(), "INT64");
336        assert_eq!(
337            LogicalType::List(Box::new(LogicalType::String)).to_string(),
338            "LIST<STRING>"
339        );
340        assert_eq!(
341            LogicalType::Map {
342                key: Box::new(LogicalType::String),
343                value: Box::new(LogicalType::Int64)
344            }
345            .to_string(),
346            "MAP<STRING, INT64>"
347        );
348    }
349}