1use serde::{Deserialize, Serialize};
2use serde_json::Value as Json;
3use std::cmp::Ordering;
4
5use crate::model::LogicalType;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum Scalar {
10 Boolean(bool),
12 Int64(i64),
14 Float64(f64),
16 Utf8(String),
18 DateTime(i64),
20}
21
22#[must_use]
26#[allow(
27 clippy::cast_possible_truncation,
28 clippy::cast_precision_loss,
29 clippy::float_cmp,
30 reason = "exact round-trip checks are the definition of canonical integer/float equality"
31)]
32pub fn scalar_equal(left: &Scalar, right: &Scalar) -> bool {
33 match (left, right) {
34 (Scalar::Boolean(a), Scalar::Boolean(b)) => a == b,
35 (Scalar::Utf8(a), Scalar::Utf8(b)) => a == b,
36 (Scalar::DateTime(a), Scalar::DateTime(b)) | (Scalar::Int64(a), Scalar::Int64(b)) => a == b,
37 (Scalar::Float64(a), Scalar::Float64(b)) => a.is_finite() && b.is_finite() && a == b,
38 (Scalar::Int64(integer), Scalar::Float64(float))
39 | (Scalar::Float64(float), Scalar::Int64(integer)) => {
40 if !float.is_finite() || float.fract() != 0.0 {
41 return false;
42 }
43 if *float >= 9_223_372_036_854_775_808.0 || *float < -9_223_372_036_854_775_808.0 {
48 return false;
49 }
50 let converted = *float as i64;
51 converted == *integer && (converted as f64) == *float
52 }
53 _ => false,
54 }
55}
56
57#[must_use]
61pub fn scalar_cmp(left: &Scalar, right: &Scalar) -> Option<Ordering> {
62 match (left, right) {
63 (Scalar::Boolean(a), Scalar::Boolean(b)) => Some(a.cmp(b)),
64 (Scalar::Utf8(a), Scalar::Utf8(b)) => Some(a.cmp(b)),
65 (Scalar::DateTime(a), Scalar::DateTime(b)) | (Scalar::Int64(a), Scalar::Int64(b)) => {
66 Some(a.cmp(b))
67 }
68 (Scalar::Float64(a), Scalar::Float64(b)) => a.partial_cmp(b),
69 (Scalar::Int64(integer), Scalar::Float64(float)) => cmp_int_float(*integer, *float),
70 (Scalar::Float64(float), Scalar::Int64(integer)) => {
71 cmp_int_float(*integer, *float).map(Ordering::reverse)
72 }
73 _ => None,
74 }
75}
76
77#[must_use]
80pub fn json_scalar_cmp(left: &Json, right: &Json) -> Option<Ordering> {
81 let (_, left) = infer_scalar(left)?;
82 let (_, right) = infer_scalar(right)?;
83 scalar_cmp(&left, &right)
84}
85
86#[allow(
87 clippy::cast_possible_truncation,
88 clippy::cast_precision_loss,
89 reason = "bounds and exact integer-part comparison are intentional"
90)]
91fn cmp_int_float(integer: i64, float: f64) -> Option<Ordering> {
92 if !float.is_finite() {
93 return float.partial_cmp(&0.0).map(Ordering::reverse);
94 }
95 let truncated = float.trunc();
96 if truncated >= 9_223_372_036_854_775_808.0 {
100 return Some(Ordering::Less);
101 }
102 if truncated < -9_223_372_036_854_775_808.0 {
103 return Some(Ordering::Greater);
104 }
105 let integer_part = truncated as i64;
106 match integer.cmp(&integer_part) {
107 Ordering::Equal if float.fract() > 0.0 => Some(Ordering::Less),
108 Ordering::Equal if float.fract() < 0.0 => Some(Ordering::Greater),
109 ordering => Some(ordering),
110 }
111}
112
113#[must_use]
125pub fn normalize_name(name: &str) -> String {
126 let mut out = String::with_capacity(name.len());
127 for c in name.chars() {
128 if c.is_ascii_uppercase() {
129 out.push(c.to_ascii_lowercase());
130 } else if c.is_alphanumeric() {
131 out.push(c);
132 } else {
133 out.push('_');
134 }
135 }
136 out
137}
138
139pub const ID_FIELD: &str = "__id";
142pub const LABEL_FIELD: &str = "__label";
144pub const TYPE_FIELD: &str = "__type";
146pub const SOURCE_FIELD: &str = "__source_id";
148pub const TARGET_FIELD: &str = "__target_id";
150
151#[must_use]
167pub fn is_reserved_name(normalized: &str) -> bool {
168 normalized.starts_with("__")
169}
170
171#[must_use]
173pub fn scalar_to_json(s: &Scalar) -> Json {
174 match s {
175 Scalar::Boolean(b) => Json::Bool(*b),
176 Scalar::Int64(i) => Json::Number((*i).into()),
177 Scalar::Float64(f) => serde_json::Number::from_f64(*f).map_or(Json::Null, Json::Number),
178 Scalar::Utf8(st) => Json::String(st.clone()),
179 Scalar::DateTime(ms) => Json::Number((*ms).into()),
180 }
181}
182
183#[must_use]
188pub fn infer_scalar(v: &Json) -> Option<(LogicalType, Scalar)> {
189 match v {
190 Json::Bool(b) => Some((LogicalType::Boolean, Scalar::Boolean(*b))),
191 Json::Number(n) => {
192 if let Some(i) = n.as_i64() {
193 Some((LogicalType::Int64, Scalar::Int64(i)))
194 } else if n.as_u64().is_some() {
195 None
200 } else {
201 n.as_f64()
202 .map(|f| (LogicalType::Float64, Scalar::Float64(f)))
203 }
204 }
205 Json::String(s) => Some((LogicalType::Utf8, Scalar::Utf8(s.clone()))),
206 _ => None,
207 }
208}
209
210#[must_use]
212pub fn json_type_name(v: &Json) -> &'static str {
213 match v {
214 Json::Null => "null",
215 Json::Bool(_) => "boolean",
216 Json::Number(_) => "number",
217 Json::String(_) => "string",
218 Json::Array(_) => "array",
219 Json::Object(_) => "object",
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn scalar_ordering_preserves_integer_precision() {
229 assert_eq!(
230 scalar_cmp(
231 &Scalar::Int64(9_007_199_254_740_993),
232 &Scalar::Float64(9_007_199_254_740_992.0)
233 ),
234 Some(Ordering::Greater)
235 );
236 assert_eq!(
237 scalar_cmp(&Scalar::Int64(2), &Scalar::Float64(2.5)),
238 Some(Ordering::Less)
239 );
240 }
241
242 #[test]
243 fn out_of_domain_unsigned_json_integer_is_not_widened() {
244 let value = serde_json::json!(u64::MAX);
245 assert!(infer_scalar(&value).is_none());
246 }
247
248 #[test]
249 fn json_scalar_ordering_preserves_integer_precision() {
250 let left = Json::Number(9_007_199_254_740_993i64.into());
251 let right = Json::Number(9_007_199_254_740_992i64.into());
252 assert_eq!(json_scalar_cmp(&left, &right), Some(Ordering::Greater));
253 assert_eq!(json_scalar_cmp(&Json::String("a".into()), &right), None);
254 }
255
256 #[test]
257 fn scalar_equality_preserves_integer_precision() {
258 assert!(scalar_equal(&Scalar::Int64(1), &Scalar::Float64(1.0)));
259 assert!(!scalar_equal(
260 &Scalar::Int64(9_007_199_254_740_993),
261 &Scalar::Float64(9_007_199_254_740_992.0)
262 ));
263 assert!(!scalar_equal(
264 &Scalar::Float64(f64::NAN),
265 &Scalar::Float64(f64::NAN)
266 ));
267 assert!(!scalar_equal(&Scalar::Utf8("1".into()), &Scalar::Int64(1)));
268 assert!(!scalar_equal(
269 &Scalar::Int64(i64::MAX),
270 &Scalar::Float64(9_223_372_036_854_775_808.0)
271 ));
272 }
273
274 #[test]
275 fn scalar_ordering_rejects_saturating_integer_boundary_aliases() {
276 assert_eq!(
277 scalar_cmp(
278 &Scalar::Int64(i64::MAX),
279 &Scalar::Float64(9_223_372_036_854_775_808.0)
280 ),
281 Some(Ordering::Less)
282 );
283 assert_eq!(
284 scalar_cmp(
285 &Scalar::Int64(i64::MIN),
286 &Scalar::Float64(-9_223_372_036_854_775_808.0)
287 ),
288 Some(Ordering::Equal)
289 );
290 }
291}