Skip to main content

graphforge_value/
literal.rs

1//! Compiler-independent literal carrier with the existing tagged JSON encoding.
2
3use graphforge_core::SpatialValue;
4use serde::de::{self, MapAccess, Visitor};
5use serde::ser::SerializeMap;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8// ---------------------------------------------------------------------------
9// Literal
10// ---------------------------------------------------------------------------
11
12/// A scalar constant value in the Graph IR.
13///
14/// Temporal values are stored as microseconds since the Unix epoch (UTC),
15/// consistent with the Arrow `Timestamp(Microsecond, "UTC")` convention used
16/// throughout the project.
17///
18/// ## Float serialisation
19///
20/// Finite `Float` values serialise as JSON numbers.  Non-finite IEEE-754 values
21/// (`NaN`, `+Infinity`, `-Infinity`) that `serde_json` cannot represent as JSON
22/// numbers are encoded as a tagged object `{"$float": "<tag>"}` where `<tag>` is
23/// `"NaN"`, `"+Infinity"`, or `"-Infinity"`.  This preserves full round-trip
24/// fidelity rather than silently collapsing to `null`.
25#[derive(Debug, Clone, PartialEq)]
26pub enum Literal {
27    /// The Cypher `null` value.
28    Null,
29    /// A boolean constant.
30    Bool(bool),
31    /// A 64-bit integer constant.
32    Int(i64),
33    /// A 64-bit floating-point constant.
34    ///
35    /// Non-finite values (NaN, ±Infinity) are supported and round-trip through
36    /// JSON via a tagged encoding; see the enum-level docs.
37    Float(f64),
38    /// A UTF-8 string constant.
39    Str(String),
40    /// A typed UUID query parameter, stored as its canonical 16-byte identity.
41    /// This is not Cypher syntax and must not be inferred from strings.
42    Uuid([u8; 16]),
43    /// A Cypher duration as signed months/days/nanos (ADR 0009): months and days
44    /// kept distinct from sub-day time. Persisted as a `Struct{months,days,nanos}`
45    /// (Parquet cannot store Arrow `Interval`). (#920)
46    Duration {
47        /// Signed whole months.
48        months: i64,
49        /// Signed whole days.
50        days: i64,
51        /// Signed whole sub-day seconds (split from `nanos` so billion-year spans
52        /// fit `i64`). (#1011)
53        seconds: i64,
54        /// Signed nanoseconds-of-second, `(-1e9, 1e9)`, same sign as `seconds`.
55        nanos: i64,
56    },
57    /// A point-in-time expressed as microseconds since the Unix epoch (UTC).
58    DateTime(i64),
59    /// A calendar date as **i64 days** since the Unix epoch — the full openCypher
60    /// year range −999,999,999..+999,999,999. Persisted as a self-describing
61    /// `Struct{epoch_day: Int64}` (a bare Int64 is indistinguishable from an
62    /// integer property). (#920/#1011)
63    Date(i64),
64    /// A Cypher `localdatetime` (ADR 0009): a date (`days` since the Unix epoch)
65    /// plus a time-of-day (`nanos` since midnight), with no zone. Persisted as a
66    /// `Struct{date: Int64, time: Time64(ns)}`. (#920/#1011)
67    LocalDateTime {
68        /// Days since the Unix epoch (i64, full year range).
69        days: i64,
70        /// Nanoseconds since midnight (Arrow `Time64(ns)`).
71        nanos: i64,
72    },
73    /// A Cypher `localtime` (ADR 0009): a time-of-day in nanoseconds since
74    /// midnight, no zone. Persisted as a native Arrow `Time64(ns)` column. (#920)
75    Time(i64),
76    /// A Cypher `time` (ADR 0009): a time-of-day plus its UTC offset in seconds.
77    /// Persisted as a `Struct{time: Time64(ns), offset: Int32}`. (#920)
78    ZonedTime {
79        /// Nanoseconds since midnight (Arrow `Time64(ns)`).
80        nanos: i64,
81        /// UTC offset in seconds.
82        offset: i32,
83    },
84    /// A Cypher `datetime` (ADR 0009): a date+time, its UTC offset in seconds,
85    /// and an optional named IANA zone. Persisted as a
86    /// `Struct{date: Date32, time: Time64(ns), offset: Int32, zone: Utf8}`.
87    /// Distinct from [`Literal::DateTime`] (a bare UTC micros instant). (#920/#1011)
88    ZonedDateTime {
89        /// Days since the Unix epoch (i64, full year range).
90        days: i64,
91        /// Nanoseconds since midnight (Arrow `Time64(ns)`).
92        nanos: i64,
93        /// UTC offset in seconds.
94        offset: i32,
95        /// Named IANA zone, or `None` for an offset-only datetime.
96        zone: Option<String>,
97    },
98    /// A canonical typed spatial property value.
99    Spatial(SpatialValue),
100    /// A homogeneous list of values, persisted as an Arrow `List<inner>` column
101    /// (the inner type is inferred from the elements). Stores e.g. a property
102    /// whose value is `[date(…), date(…)]`. Heterogeneous lists are out of scope
103    /// (#1005). (#1006)
104    List(Vec<Literal>),
105    /// A query-parameter map value. Map literals in parsed Cypher still lower as
106    /// `IrExpr::MapLiteral`; this variant lets callers bind `$param` to a map
107    /// through `execute_with_params` and then use Cypher value access on it.
108    Map(Vec<(String, Literal)>),
109}
110
111// ---------------------------------------------------------------------------
112// Literal — custom Serialize
113// ---------------------------------------------------------------------------
114
115impl Serialize for Literal {
116    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
117        // Delegate all variants except Float to the derived representation by
118        // using an internal helper that derives Serialize on a mirrored enum.
119        match self {
120            Self::Null => LiteralSer::Null.serialize(s),
121            Self::Bool(b) => LiteralSer::Bool(*b).serialize(s),
122            Self::Int(i) => LiteralSer::Int(*i).serialize(s),
123            Self::Str(v) => LiteralSer::Str(v).serialize(s),
124            Self::Uuid(v) => LiteralSer::Uuid(v).serialize(s),
125            Self::Duration {
126                months,
127                days,
128                seconds,
129                nanos,
130            } => LiteralSer::Duration(*months, *days, *seconds, *nanos).serialize(s),
131            Self::DateTime(dt) => LiteralSer::DateTime(*dt).serialize(s),
132            Self::Date(d) => LiteralSer::Date(*d).serialize(s),
133            Self::LocalDateTime { days, nanos } => {
134                LiteralSer::LocalDateTime(*days, *nanos).serialize(s)
135            }
136            Self::Time(n) => LiteralSer::Time(*n).serialize(s),
137            Self::ZonedTime { nanos, offset } => {
138                LiteralSer::ZonedTime(*nanos, *offset).serialize(s)
139            }
140            Self::ZonedDateTime {
141                days,
142                nanos,
143                offset,
144                zone,
145            } => LiteralSer::ZonedDateTime(*days, *nanos, *offset, zone.clone()).serialize(s),
146            Self::Spatial(value) => LiteralSer::Spatial(value).serialize(s),
147            // Each element serialises via `Literal`'s own impl (so nested
148            // non-finite floats keep their tagged encoding).
149            Self::List(items) => LiteralSer::List(items).serialize(s),
150            Self::Map(entries) => LiteralSer::Map(entries).serialize(s),
151            Self::Float(f) => {
152                if f.is_finite() {
153                    LiteralSer::Float(*f).serialize(s)
154                } else {
155                    // Encode non-finite as {"$float": "<tag>"}
156                    let tag = if f.is_nan() {
157                        "NaN"
158                    } else if *f > 0.0 {
159                        "+Infinity"
160                    } else {
161                        "-Infinity"
162                    };
163                    let mut map = s.serialize_map(Some(1))?;
164                    map.serialize_entry("$float", tag)?;
165                    map.end()
166                }
167            }
168        }
169    }
170}
171
172/// Internal mirror enum used purely to drive derived `Serialize` for the
173/// finite/non-float variants of [`Literal`].
174#[derive(Serialize)]
175#[serde(tag = "type", content = "value")]
176enum LiteralSer<'a> {
177    Null,
178    Bool(bool),
179    Int(i64),
180    Float(f64),
181    Str(&'a str),
182    Uuid(&'a [u8; 16]),
183    Duration(i64, i64, i64, i64),
184    DateTime(i64),
185    Date(i64),
186    LocalDateTime(i64, i64),
187    Time(i64),
188    ZonedTime(i64, i32),
189    ZonedDateTime(i64, i64, i32, Option<String>),
190    Spatial(&'a SpatialValue),
191    List(&'a [Literal]),
192    Map(&'a [(String, Literal)]),
193}
194
195// ---------------------------------------------------------------------------
196// Literal — custom Deserialize
197// ---------------------------------------------------------------------------
198
199impl<'de> Deserialize<'de> for Literal {
200    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
201        d.deserialize_any(LiteralVisitor)
202    }
203}
204
205struct LiteralVisitor;
206
207impl<'de> Visitor<'de> for LiteralVisitor {
208    type Value = Literal;
209
210    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        write!(
212            f,
213            "an IrLiteral (tagged object with \"type\"/\"value\" fields, \
214             or {{\"$float\": \"NaN\"/\"+Infinity\"/\"-Infinity\"}})"
215        )
216    }
217
218    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
219        // Two shapes are valid:
220        //   {"$float": "<tag>"}  — non-finite float
221        //   {"type": "<Variant>", "value": <data>}  — everything else
222
223        let first_key: String = map
224            .next_key()?
225            .ok_or_else(|| de::Error::custom("expected at least one map key"))?;
226
227        if first_key == "$float" {
228            let tag: String = map.next_value()?;
229            let f = match tag.as_str() {
230                "NaN" => f64::NAN,
231                "+Infinity" => f64::INFINITY,
232                "-Infinity" => f64::NEG_INFINITY,
233                other => {
234                    return Err(de::Error::unknown_variant(
235                        other,
236                        &["NaN", "+Infinity", "-Infinity"],
237                    ));
238                }
239            };
240            return Ok(Literal::Float(f));
241        }
242
243        if first_key != "type" {
244            return Err(de::Error::unknown_field(&first_key, &["type", "$float"]));
245        }
246
247        let variant: String = map.next_value()?;
248        // Scan forward through remaining map entries for "value", ignoring
249        // any unknown fields along the way so field-order is irrelevant.
250        match variant.as_str() {
251            "Null" => Ok(Literal::Null),
252            "Bool" => Ok(Literal::Bool(read_value_field(&mut map)?)),
253            "Int" => Ok(Literal::Int(read_value_field(&mut map)?)),
254            "Float" => Ok(Literal::Float(read_value_field(&mut map)?)),
255            "Str" => Ok(Literal::Str(read_value_field(&mut map)?)),
256            "Uuid" => Ok(Literal::Uuid(read_value_field(&mut map)?)),
257            "Duration" => {
258                let (months, days, seconds, nanos) = read_value_field(&mut map)?;
259                Ok(Literal::Duration {
260                    months,
261                    days,
262                    seconds,
263                    nanos,
264                })
265            }
266            "DateTime" => Ok(Literal::DateTime(read_value_field(&mut map)?)),
267            "Date" => Ok(Literal::Date(read_value_field(&mut map)?)),
268            "LocalDateTime" => {
269                let (days, nanos) = read_value_field(&mut map)?;
270                Ok(Literal::LocalDateTime { days, nanos })
271            }
272            "Time" => Ok(Literal::Time(read_value_field(&mut map)?)),
273            "ZonedTime" => {
274                let (nanos, offset) = read_value_field(&mut map)?;
275                Ok(Literal::ZonedTime { nanos, offset })
276            }
277            "ZonedDateTime" => {
278                let (days, nanos, offset, zone) = read_value_field(&mut map)?;
279                Ok(Literal::ZonedDateTime {
280                    days,
281                    nanos,
282                    offset,
283                    zone,
284                })
285            }
286            "Spatial" => Ok(Literal::Spatial(read_value_field(&mut map)?)),
287            "List" => Ok(Literal::List(read_value_field(&mut map)?)),
288            "Map" => Ok(Literal::Map(read_value_field(&mut map)?)),
289            other => Err(de::Error::unknown_variant(
290                other,
291                &[
292                    "Null",
293                    "Bool",
294                    "Int",
295                    "Float",
296                    "Str",
297                    "Uuid",
298                    "Duration",
299                    "DateTime",
300                    "Date",
301                    "LocalDateTime",
302                    "Time",
303                    "ZonedTime",
304                    "ZonedDateTime",
305                    "Spatial",
306                    "List",
307                    "Map",
308                ],
309            )),
310        }
311    }
312}
313
314/// Scan `map` for a key named `"value"`, skipping any unrecognised keys, and
315/// deserialise its value as `T`.  Returns `de::Error::missing_field("value")`
316/// if the key is not found.
317fn read_value_field<'de, T, A>(map: &mut A) -> Result<T, A::Error>
318where
319    T: Deserialize<'de>,
320    A: MapAccess<'de>,
321{
322    while let Some(key) = map.next_key::<String>()? {
323        if key == "value" {
324            return map.next_value();
325        }
326        let _: de::IgnoredAny = map.next_value()?;
327    }
328    Err(de::Error::missing_field("value"))
329}
330
331#[cfg(test)]
332mod tests {
333    use super::Literal;
334    use graphforge_core::{
335        SpatialCoordinates, SpatialCrs, SpatialGeometryType, SpatialType, SpatialValue,
336    };
337
338    #[test]
339    fn literal_json_golden_vectors_preserve_existing_wire_encoding() {
340        // These bytes pin the pre-extraction IrLiteral serializer: adjacent
341        // type/value tags, tuple temporal payloads, and ordered map entries.
342        let vectors = [
343            (Literal::Null, r#"{"type":"Null"}"#),
344            (Literal::Bool(true), r#"{"type":"Bool","value":true}"#),
345            (Literal::Int(-7), r#"{"type":"Int","value":-7}"#),
346            (Literal::Float(2.5), r#"{"type":"Float","value":2.5}"#),
347            (
348                Literal::Str("line\n\"quoted\"".into()),
349                r#"{"type":"Str","value":"line\n\"quoted\""}"#,
350            ),
351            (
352                Literal::Uuid([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
353                r#"{"type":"Uuid","value":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]}"#,
354            ),
355            (
356                Literal::Duration {
357                    months: 1,
358                    days: -2,
359                    seconds: 3,
360                    nanos: 4,
361                },
362                r#"{"type":"Duration","value":[1,-2,3,4]}"#,
363            ),
364            (Literal::DateTime(-1), r#"{"type":"DateTime","value":-1}"#),
365            (
366                Literal::Date(3_000_000_000),
367                r#"{"type":"Date","value":3000000000}"#,
368            ),
369            (
370                Literal::LocalDateTime {
371                    days: 3_000_000_000,
372                    nanos: 42,
373                },
374                r#"{"type":"LocalDateTime","value":[3000000000,42]}"#,
375            ),
376            (Literal::Time(42), r#"{"type":"Time","value":42}"#),
377            (
378                Literal::ZonedTime {
379                    nanos: 42,
380                    offset: -3600,
381                },
382                r#"{"type":"ZonedTime","value":[42,-3600]}"#,
383            ),
384            (
385                Literal::ZonedDateTime {
386                    days: 1,
387                    nanos: 42,
388                    offset: 3600,
389                    zone: Some("Europe/Paris".into()),
390                },
391                r#"{"type":"ZonedDateTime","value":[1,42,3600,"Europe/Paris"]}"#,
392            ),
393            (
394                Literal::ZonedDateTime {
395                    days: 1,
396                    nanos: 42,
397                    offset: 0,
398                    zone: None,
399                },
400                r#"{"type":"ZonedDateTime","value":[1,42,0,null]}"#,
401            ),
402            (
403                Literal::Spatial(SpatialValue {
404                    spatial_type: SpatialType {
405                        geometry: SpatialGeometryType::Point,
406                        crs: SpatialCrs::Epsg4326,
407                    },
408                    coordinates: SpatialCoordinates::Point([1.0, 2.0]),
409                    extension_name: None,
410                    extension_metadata: None,
411                }),
412                r#"{"type":"Spatial","value":{"spatial_type":{"geometry":"point","crs":"EPSG:4326"},"coordinates":{"Point":[1.0,2.0]}}}"#,
413            ),
414            (
415                Literal::List(vec![Literal::Int(1), Literal::Null]),
416                r#"{"type":"List","value":[{"type":"Int","value":1},{"type":"Null"}]}"#,
417            ),
418            (
419                Literal::Map(vec![
420                    ("z".into(), Literal::Bool(false)),
421                    ("a".into(), Literal::List(vec![])),
422                ]),
423                r#"{"type":"Map","value":[["z",{"type":"Bool","value":false}],["a",{"type":"List","value":[]}]]}"#,
424            ),
425        ];
426        for (value, expected) in vectors {
427            assert_eq!(serde_json::to_string(&value).unwrap(), expected);
428            assert_eq!(serde_json::from_str::<Literal>(expected).unwrap(), value);
429        }
430    }
431
432    #[test]
433    fn nonfinite_literal_json_golden_vectors_preserve_nested_tags() {
434        for (number, expected) in [
435            (f64::NAN, r#"{"$float":"NaN"}"#),
436            (f64::INFINITY, r#"{"$float":"+Infinity"}"#),
437            (f64::NEG_INFINITY, r#"{"$float":"-Infinity"}"#),
438        ] {
439            assert_eq!(
440                serde_json::to_string(&Literal::Float(number)).unwrap(),
441                expected
442            );
443            let Literal::Float(decoded) = serde_json::from_str::<Literal>(expected).unwrap() else {
444                panic!("nonfinite tag must remain a float");
445            };
446            if number.is_nan() {
447                assert!(decoded.is_nan());
448            } else {
449                assert_eq!(decoded, number);
450            }
451        }
452        let nested = Literal::List(vec![Literal::Map(vec![
453            ("nan".into(), Literal::Float(f64::NAN)),
454            (
455                "infinities".into(),
456                Literal::List(vec![
457                    Literal::Float(f64::INFINITY),
458                    Literal::Float(f64::NEG_INFINITY),
459                ]),
460            ),
461        ])]);
462        let expected = r#"{"type":"List","value":[{"type":"Map","value":[["nan",{"$float":"NaN"}],["infinities",{"type":"List","value":[{"$float":"+Infinity"},{"$float":"-Infinity"}]}]]}]}"#;
463        assert_eq!(serde_json::to_string(&nested).unwrap(), expected);
464        let decoded: Literal = serde_json::from_str(expected).unwrap();
465        assert_eq!(serde_json::to_string(&decoded).unwrap(), expected);
466    }
467}