1use std::cmp::Ordering;
25use std::collections::BTreeMap;
26
27use uqa_core::{Edge, Value, Vertex};
28
29#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
30pub enum AgtypeConversionError {
31 #[error("graph id {id} cannot be represented as an agtype integer")]
32 GraphIdOutOfRange { id: u64 },
33}
34
35fn graph_id_value(id: u64) -> Result<Value, AgtypeConversionError> {
36 i64::try_from(id)
37 .map(Value::Int)
38 .map_err(|_| AgtypeConversionError::GraphIdOutOfRange { id })
39}
40
41pub const AGTYPE_KIND_KEY: &str = "@agtype";
45
46const KIND_VERTEX: &str = "vertex";
47const KIND_EDGE: &str = "edge";
48const KIND_PATH: &str = "path";
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum EntityKind {
53 Vertex,
54 Edge,
55 Path,
56}
57
58pub fn vertex_to_value(vertex: &Vertex) -> Result<Value, AgtypeConversionError> {
60 let mut map = BTreeMap::new();
61 map.insert(AGTYPE_KIND_KEY.into(), Value::Str(KIND_VERTEX.into()));
62 map.insert("id".into(), graph_id_value(vertex.vertex_id)?);
63 map.insert("label".into(), Value::Str(vertex.label.clone()));
64 map.insert("properties".into(), Value::Map(vertex.properties.clone()));
65 Ok(Value::Map(map))
66}
67
68pub fn edge_to_value(edge: &Edge) -> Result<Value, AgtypeConversionError> {
70 let mut map = BTreeMap::new();
71 map.insert(AGTYPE_KIND_KEY.into(), Value::Str(KIND_EDGE.into()));
72 map.insert("id".into(), graph_id_value(edge.edge_id)?);
73 map.insert("label".into(), Value::Str(edge.label.clone()));
74 map.insert("start_id".into(), graph_id_value(edge.source_id)?);
75 map.insert("end_id".into(), graph_id_value(edge.target_id)?);
76 map.insert("properties".into(), Value::Map(edge.properties.clone()));
77 Ok(Value::Map(map))
78}
79
80pub fn path_to_value(elements: Vec<Value>) -> Value {
83 let mut map = BTreeMap::new();
84 map.insert(AGTYPE_KIND_KEY.into(), Value::Str(KIND_PATH.into()));
85 map.insert("elements".into(), Value::List(elements));
86 Value::Map(map)
87}
88
89pub fn entity_kind(value: &Value) -> Option<EntityKind> {
91 let Value::Map(map) = value else {
92 return None;
93 };
94 match map.get(AGTYPE_KIND_KEY) {
95 Some(Value::Str(kind)) if kind == KIND_VERTEX => Some(EntityKind::Vertex),
96 Some(Value::Str(kind)) if kind == KIND_EDGE => Some(EntityKind::Edge),
97 Some(Value::Str(kind)) if kind == KIND_PATH => Some(EntityKind::Path),
98 _ => None,
99 }
100}
101
102pub fn entity_id(value: &Value) -> Option<i64> {
104 match (entity_kind(value)?, value) {
105 (EntityKind::Vertex | EntityKind::Edge, Value::Map(map)) => match map.get("id") {
106 Some(Value::Int(id)) => Some(*id),
107 _ => None,
108 },
109 _ => None,
110 }
111}
112
113pub fn entity_label(value: &Value) -> Option<&str> {
115 match (entity_kind(value)?, value) {
116 (EntityKind::Vertex | EntityKind::Edge, Value::Map(map)) => match map.get("label") {
117 Some(Value::Str(label)) => Some(label),
118 _ => None,
119 },
120 _ => None,
121 }
122}
123
124pub fn entity_properties(value: &Value) -> Option<&BTreeMap<String, Value>> {
126 match (entity_kind(value)?, value) {
127 (EntityKind::Vertex | EntityKind::Edge, Value::Map(map)) => match map.get("properties") {
128 Some(Value::Map(props)) => Some(props),
129 _ => None,
130 },
131 _ => None,
132 }
133}
134
135pub fn edge_start_id(value: &Value) -> Option<i64> {
137 match (entity_kind(value)?, value) {
138 (EntityKind::Edge, Value::Map(map)) => match map.get("start_id") {
139 Some(Value::Int(id)) => Some(*id),
140 _ => None,
141 },
142 _ => None,
143 }
144}
145
146pub fn edge_end_id(value: &Value) -> Option<i64> {
148 match (entity_kind(value)?, value) {
149 (EntityKind::Edge, Value::Map(map)) => match map.get("end_id") {
150 Some(Value::Int(id)) => Some(*id),
151 _ => None,
152 },
153 _ => None,
154 }
155}
156
157pub fn path_elements(value: &Value) -> Option<&[Value]> {
159 match (entity_kind(value)?, value) {
160 (EntityKind::Path, Value::Map(map)) => match map.get("elements") {
161 Some(Value::List(elements)) => Some(elements),
162 _ => None,
163 },
164 _ => None,
165 }
166}
167
168pub fn agtype_type_ordinal(value: &Value) -> u8 {
171 match entity_kind(value) {
172 Some(EntityKind::Vertex) => 6,
173 Some(EntityKind::Edge) => 7,
174 Some(EntityKind::Path) => 8,
175 None => match value {
176 Value::Null => 0,
177 Value::Void | Value::Str(_) | Value::FixedChar(_) => 1,
178 Value::Decimal(_) => 2,
179 Value::Int(_) => 3,
180 Value::Float(_) => 4,
181 Value::Bool(_) => 5,
182 Value::Array(_) | Value::List(_) | Value::Row(_) => 9,
183 Value::Record(_) | Value::Map(_) => 10,
184 Value::Json(text) | Value::JsonB(text) => json_type_ordinal(text),
185 Value::Bytes(_) | Value::Temporal(_) => 11,
186 },
187 }
188}
189
190pub fn agtype_type_name(value: &Value) -> &'static str {
193 match entity_kind(value) {
194 Some(EntityKind::Vertex) => "vertex",
195 Some(EntityKind::Edge) => "edge",
196 Some(EntityKind::Path) => "path",
197 None => match value {
198 Value::Null => "null",
199 Value::Void | Value::Str(_) | Value::FixedChar(_) => "string",
200 Value::Bool(_) => "boolean",
201 Value::Int(_) => "integer",
202 Value::Float(_) => "float",
203 Value::Decimal(_) => "numeric",
204 Value::Array(_) | Value::List(_) | Value::Row(_) => "list",
205 Value::Record(_) | Value::Map(_) => "map",
206 Value::Json(text) | Value::JsonB(text) => json_type_name(text),
207 Value::Bytes(_) => "bytea",
208 Value::Temporal(_) => "temporal",
209 },
210 }
211}
212
213pub fn format_float_pg(f: f64) -> String {
221 if f.is_nan() {
222 return "NaN".into();
223 }
224 if f.is_infinite() {
225 return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
226 }
227 let sci = format!("{f:e}");
230 let Some((mantissa, exp)) = sci.split_once('e') else {
231 return sci;
232 };
233 let Ok(exp) = exp.parse::<i32>() else {
234 return sci;
235 };
236 let negative = mantissa.starts_with('-');
237 let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
238 let sign = if negative { "-" } else { "" };
239
240 if (-4..15).contains(&exp) {
241 if exp >= 0 {
242 let Ok(int_len) = usize::try_from(exp + 1) else {
243 return sci;
244 };
245 if digits.len() > int_len {
246 format!("{sign}{}.{}", &digits[..int_len], &digits[int_len..])
247 } else {
248 let zeros = "0".repeat(int_len - digits.len());
249 format!("{sign}{digits}{zeros}")
250 }
251 } else {
252 let Ok(zero_count) = usize::try_from(-exp - 1) else {
253 return sci;
254 };
255 let zeros = "0".repeat(zero_count);
256 format!("{sign}0.{zeros}{digits}")
257 }
258 } else {
259 let mantissa_text = if digits.len() > 1 {
260 format!("{}.{}", &digits[..1], &digits[1..])
261 } else {
262 digits
263 };
264 format!("{sign}{mantissa_text}e{exp:+03}")
265 }
266}
267
268pub fn format_float_agtype(f: f64) -> String {
272 let text = format_float_pg(f);
273 if text.bytes().any(|b| matches!(b, b'.' | b'e' | b'N' | b'I')) {
274 text
275 } else {
276 format!("{text}.0")
277 }
278}
279
280pub fn render(value: &Value) -> String {
288 let mut out = String::new();
289 render_into(value, &mut out);
290 out
291}
292
293fn render_into(value: &Value, out: &mut String) {
294 match entity_kind(value) {
295 Some(EntityKind::Vertex) => {
296 render_entity_body(value, &["id", "label", "properties"], out);
297 out.push_str("::vertex");
298 }
299 Some(EntityKind::Edge) => {
300 render_entity_body(
301 value,
302 &["id", "label", "end_id", "start_id", "properties"],
303 out,
304 );
305 out.push_str("::edge");
306 }
307 Some(EntityKind::Path) => {
308 out.push('[');
309 if let Some(elements) = path_elements(value) {
310 for (i, element) in elements.iter().enumerate() {
311 if i > 0 {
312 out.push_str(", ");
313 }
314 render_into(element, out);
315 }
316 }
317 out.push_str("]::path");
318 }
319 None => match value {
320 Value::Null => out.push_str("null"),
321 Value::Void => render_json_string("", out),
322 Value::Bool(true) => out.push_str("true"),
323 Value::Bool(false) => out.push_str("false"),
324 Value::Int(n) => out.push_str(&n.to_string()),
325 Value::Float(f) => out.push_str(&format_float_agtype(*f)),
326 Value::Decimal(d) => {
327 out.push_str(&d.to_sql_string());
328 out.push_str("::numeric");
329 }
330 Value::Str(s) => render_json_string(s, out),
331 Value::FixedChar(s) => render_json_string(s.trim_end_matches(' '), out),
332 Value::Bytes(b) => render_json_string(&String::from_utf8_lossy(b), out),
333 Value::Temporal(t) => render_json_string(&t.to_sql_string(), out),
334 Value::Json(text) | Value::JsonB(text) => out.push_str(text),
335 Value::Array(array) => {
336 out.push('[');
337 for (index, item) in array.elements().iter().enumerate() {
338 if index > 0 {
339 out.push_str(", ");
340 }
341 render_into(item, out);
342 }
343 out.push(']');
344 }
345 Value::List(items) | Value::Row(items) => {
346 out.push('[');
347 for (i, item) in items.iter().enumerate() {
348 if i > 0 {
349 out.push_str(", ");
350 }
351 render_into(item, out);
352 }
353 out.push(']');
354 }
355 Value::Record(fields) => {
356 out.push('{');
357 for (index, (name, value)) in fields.iter().enumerate() {
358 if index > 0 {
359 out.push_str(", ");
360 }
361 render_json_string(name, out);
362 out.push_str(": ");
363 render_into(value, out);
364 }
365 out.push('}');
366 }
367 Value::Map(map) => {
368 out.push('{');
369 let mut keys: Vec<&String> = map.keys().collect();
370 keys.sort_by(|a, b| jsonb_key_cmp(a, b));
371 for (i, key) in keys.iter().enumerate() {
372 if i > 0 {
373 out.push_str(", ");
374 }
375 render_json_string(key, out);
376 out.push_str(": ");
377 render_into(&map[*key], out);
378 }
379 out.push('}');
380 }
381 },
382 }
383}
384
385fn render_entity_body(value: &Value, fields: &[&str], out: &mut String) {
388 let Value::Map(map) = value else {
389 return;
390 };
391 out.push('{');
392 for (i, field) in fields.iter().enumerate() {
393 if i > 0 {
394 out.push_str(", ");
395 }
396 render_json_string(field, out);
397 out.push_str(": ");
398 render_into(map.get(*field).unwrap_or(&Value::Null), out);
399 }
400 out.push('}');
401}
402
403pub fn jsonb_key_cmp(a: &str, b: &str) -> Ordering {
405 a.len()
406 .cmp(&b.len())
407 .then_with(|| a.as_bytes().cmp(b.as_bytes()))
408}
409
410fn render_json_string(s: &str, out: &mut String) {
411 use std::fmt::Write as _;
412 out.push('"');
413 for ch in s.chars() {
414 match ch {
415 '"' => out.push_str("\\\""),
416 '\\' => out.push_str("\\\\"),
417 '\n' => out.push_str("\\n"),
418 '\r' => out.push_str("\\r"),
419 '\t' => out.push_str("\\t"),
420 '\u{08}' => out.push_str("\\b"),
421 '\u{0c}' => out.push_str("\\f"),
422 c if (c as u32) < 0x20 => {
423 let _ = write!(out, "\\u{:04x}", c as u32);
424 }
425 c => out.push(c),
426 }
427 }
428 out.push('"');
429}
430
431fn sort_priority(value: &Value) -> u8 {
438 match entity_kind(value) {
439 Some(EntityKind::Path) => 0,
440 Some(EntityKind::Edge) => 1,
441 Some(EntityKind::Vertex) => 2,
442 None => match value {
443 Value::Record(_) | Value::Map(_) => 3,
444 Value::Array(_) | Value::List(_) | Value::Row(_) => 4,
445 Value::Json(text) | Value::JsonB(text) => json_sort_priority(text),
446 Value::Void
447 | Value::Str(_)
448 | Value::FixedChar(_)
449 | Value::Bytes(_)
450 | Value::Temporal(_) => 5,
451 Value::Bool(_) => 6,
452 Value::Int(_) | Value::Float(_) | Value::Decimal(_) => 7,
453 Value::Null => 8,
454 },
455 }
456}
457
458fn json_type_ordinal(text: &str) -> u8 {
459 match serde_json::from_str::<serde_json::Value>(text) {
460 Ok(serde_json::Value::Null) => 0,
461 Ok(serde_json::Value::String(_)) | Err(_) => 1,
462 Ok(serde_json::Value::Number(number)) if number.is_i64() || number.is_u64() => 3,
463 Ok(serde_json::Value::Number(_)) => 4,
464 Ok(serde_json::Value::Bool(_)) => 5,
465 Ok(serde_json::Value::Array(_)) => 9,
466 Ok(serde_json::Value::Object(_)) => 10,
467 }
468}
469
470fn json_type_name(text: &str) -> &'static str {
471 match serde_json::from_str::<serde_json::Value>(text) {
472 Ok(serde_json::Value::Null) => "null",
473 Ok(serde_json::Value::Bool(_)) => "boolean",
474 Ok(serde_json::Value::Number(number)) if number.is_i64() || number.is_u64() => "integer",
475 Ok(serde_json::Value::Number(_)) => "float",
476 Ok(serde_json::Value::String(_)) | Err(_) => "string",
477 Ok(serde_json::Value::Array(_)) => "list",
478 Ok(serde_json::Value::Object(_)) => "map",
479 }
480}
481
482fn json_sort_priority(text: &str) -> u8 {
483 match serde_json::from_str::<serde_json::Value>(text) {
484 Ok(serde_json::Value::Object(_)) => 3,
485 Ok(serde_json::Value::Array(_)) => 4,
486 Ok(serde_json::Value::String(_)) | Err(_) => 5,
487 Ok(serde_json::Value::Bool(_)) => 6,
488 Ok(serde_json::Value::Number(_)) => 7,
489 Ok(serde_json::Value::Null) => 8,
490 }
491}
492
493pub fn cmp(a: &Value, b: &Value) -> Ordering {
496 let pa = sort_priority(a);
497 let pb = sort_priority(b);
498 if pa != pb {
499 return pa.cmp(&pb);
500 }
501 match (entity_kind(a), entity_kind(b)) {
502 (Some(EntityKind::Vertex), Some(EntityKind::Vertex))
503 | (Some(EntityKind::Edge), Some(EntityKind::Edge)) => entity_id(a).cmp(&entity_id(b)),
504 (Some(EntityKind::Path), Some(EntityKind::Path)) => {
505 let ea = path_elements(a).unwrap_or(&[]);
506 let eb = path_elements(b).unwrap_or(&[]);
507 cmp_slices(ea, eb)
508 }
509 _ => match (a, b) {
510 (Value::Null, Value::Null) => Ordering::Equal,
511 (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
512 _ if is_number(a) && is_number(b) => cmp_numbers(a, b),
513 (Value::Str(x), Value::Str(y)) => x.as_bytes().cmp(y.as_bytes()),
514 (Value::List(x), Value::List(y)) => cmp_slices(x, y),
515 (Value::Map(x), Value::Map(y)) => {
516 x.len().cmp(&y.len()).then_with(|| {
519 let mut xs: Vec<(&String, &Value)> = x.iter().collect();
520 let mut ys: Vec<(&String, &Value)> = y.iter().collect();
521 xs.sort_by(|l, r| jsonb_key_cmp(l.0, r.0));
522 ys.sort_by(|l, r| jsonb_key_cmp(l.0, r.0));
523 for ((ka, va), (kb, vb)) in xs.iter().zip(ys.iter()) {
524 let key_cmp = jsonb_key_cmp(ka, kb);
525 if key_cmp != Ordering::Equal {
526 return key_cmp;
527 }
528 let val_cmp = cmp(va, vb);
529 if val_cmp != Ordering::Equal {
530 return val_cmp;
531 }
532 }
533 Ordering::Equal
534 })
535 }
536 (Value::Bytes(x), Value::Bytes(y)) => x.cmp(y),
537 (Value::Temporal(x), Value::Temporal(y)) => x.cmp(y),
538 _ => render(a).cmp(&render(b)),
540 },
541 }
542}
543
544fn cmp_slices(a: &[Value], b: &[Value]) -> Ordering {
545 for (x, y) in a.iter().zip(b.iter()) {
546 let c = cmp(x, y);
547 if c != Ordering::Equal {
548 return c;
549 }
550 }
551 a.len().cmp(&b.len())
552}
553
554fn is_number(v: &Value) -> bool {
555 matches!(v, Value::Int(_) | Value::Float(_) | Value::Decimal(_))
556}
557
558fn cmp_numbers(a: &Value, b: &Value) -> Ordering {
559 a.cmp(b)
560}
561
562pub fn eq(a: &Value, b: &Value) -> bool {
566 cmp(a, b) == Ordering::Equal
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 fn vertex(id: u64, label: &str, props: &[(&str, Value)]) -> Vertex {
574 let mut v = Vertex::new(id, label);
575 for (k, val) in props {
576 v.properties.insert((*k).into(), val.clone());
577 }
578 v
579 }
580
581 #[test]
582 fn renders_vertex_in_age_format() {
583 let v = vertex(
584 844_424_930_131_969,
585 "Person",
586 &[
587 ("age", Value::Int(30)),
588 ("name", Value::Str("Alice".into())),
589 ],
590 );
591 assert_eq!(
592 render(&vertex_to_value(&v).unwrap()),
593 "{\"id\": 844424930131969, \"label\": \"Person\", \
594 \"properties\": {\"age\": 30, \"name\": \"Alice\"}}::vertex"
595 );
596 }
597
598 #[test]
599 fn renders_edge_in_age_format() {
600 let mut e = Edge::new(
601 1_125_899_906_842_625,
602 844_424_930_131_969,
603 844_424_930_131_970,
604 "KNOWS",
605 );
606 e.properties.insert("since".into(), Value::Int(2020));
607 assert_eq!(
608 render(&edge_to_value(&e).unwrap()),
609 "{\"id\": 1125899906842625, \"label\": \"KNOWS\", \
610 \"end_id\": 844424930131970, \"start_id\": 844424930131969, \
611 \"properties\": {\"since\": 2020}}::edge"
612 );
613 }
614
615 #[test]
616 fn renders_path_with_suffix_at_end() {
617 let a = vertex(1, "A", &[]);
618 let b = vertex(2, "B", &[]);
619 let e = Edge::new(10, 1, 2, "R");
620 let path = path_to_value(vec![
621 vertex_to_value(&a).unwrap(),
622 edge_to_value(&e).unwrap(),
623 vertex_to_value(&b).unwrap(),
624 ]);
625 let text = render(&path);
626 assert!(text.starts_with("[{\"id\": 1, \"label\": \"A\""));
627 assert!(text.ends_with("::vertex]::path"));
628 assert!(text.contains("}::edge, {"));
629 }
630
631 #[test]
632 fn map_keys_use_jsonb_order() {
633 let mut map = BTreeMap::new();
634 map.insert("aa".into(), Value::Int(2));
635 map.insert("b".into(), Value::Int(1));
636 assert_eq!(render(&Value::Map(map)), "{\"b\": 1, \"aa\": 2}");
637 }
638
639 #[test]
640 fn float_formatting_matches_age() {
641 assert_eq!(format_float_agtype(1.0 / 3.0), "0.3333333333333333");
642 assert_eq!(
643 format_float_agtype(9_223_372_036_854_775_807.0_f64),
644 "9.223372036854776e+18"
645 );
646 assert_eq!(format_float_agtype(0.1 + 0.2), "0.30000000000000004");
647 assert_eq!(format_float_agtype(4.0), "4.0");
648 assert_eq!(format_float_agtype(100.0), "100.0");
649 assert_eq!(format_float_agtype(1_000_000.0), "1000000.0");
650 assert_eq!(format_float_agtype(-0.0), "-0.0");
651 assert_eq!(format_float_agtype(1e15), "1e+15");
652 assert_eq!(format_float_agtype(1e14), "100000000000000.0");
653 assert_eq!(format_float_agtype(0.0001), "0.0001");
654 assert_eq!(format_float_agtype(0.00001), "1e-05");
655 assert_eq!(format_float_agtype(1e100), "1e+100");
656 assert_eq!(format_float_agtype(123_456_789.123), "123456789.123");
657 assert_eq!(format_float_agtype(f64::NAN), "NaN");
658 assert_eq!(format_float_agtype(f64::INFINITY), "Infinity");
659 assert_eq!(format_float_agtype(f64::NEG_INFINITY), "-Infinity");
660 assert_eq!(format_float_agtype(1.5), "1.5");
661 assert_eq!(
662 format_float_agtype(std::f64::consts::E),
663 "2.718281828459045"
664 );
665 }
666
667 #[test]
668 fn float_pg_formatting_omits_integral_suffix() {
669 assert_eq!(format_float_pg(1.0), "1");
670 assert_eq!(format_float_pg(1.5), "1.5");
671 assert_eq!(format_float_pg(100.0), "100");
672 }
673
674 #[test]
675 fn scalar_rendering() {
676 assert_eq!(render(&Value::Null), "null");
677 assert_eq!(render(&Value::Bool(true)), "true");
678 assert_eq!(render(&Value::Bool(false)), "false");
679 assert_eq!(render(&Value::Int(42)), "42");
680 assert_eq!(render(&Value::Str("a\"b\n".into())), "\"a\\\"b\\n\"");
681 assert_eq!(
682 render(&Value::List(vec![
683 Value::Int(1),
684 Value::Null,
685 Value::Str("x".into()),
686 ])),
687 "[1, null, \"x\"]"
688 );
689 }
690
691 #[test]
692 fn total_order_matches_age_type_ranks() {
693 let vertex_value = vertex_to_value(&vertex(1, "A", &[])).unwrap();
694 let edge_value = edge_to_value(&Edge::new(2, 1, 1, "R")).unwrap();
695 let path_value = path_to_value(vec![vertex_to_value(&vertex(1, "A", &[])).unwrap()]);
696 let mut values = vec![
697 Value::Null,
698 Value::Float(2.5),
699 Value::Int(1),
700 Value::Bool(true),
701 Value::Str("a".into()),
702 Value::List(vec![Value::Int(1)]),
703 Value::Map(BTreeMap::from([("x".to_string(), Value::Int(1))])),
704 vertex_value.clone(),
705 edge_value.clone(),
706 path_value.clone(),
707 ];
708 values.sort_by(cmp);
709 let ranks: Vec<u8> = values.iter().map(sort_priority).collect();
710 assert_eq!(ranks, vec![0, 1, 2, 3, 4, 5, 6, 7, 7, 8]);
711 assert_eq!(values[7], Value::Int(1));
713 assert_eq!(values[8], Value::Float(2.5));
714 }
715
716 #[test]
717 fn string_order_is_bytewise_not_length_first() {
718 assert_eq!(
719 cmp(&Value::Str("ab".into()), &Value::Str("b".into())),
720 Ordering::Less
721 );
722 }
723
724 #[test]
725 fn numeric_equality_spans_int_and_float() {
726 assert!(eq(&Value::Int(1), &Value::Float(1.0)));
727 assert!(!eq(&Value::Int(1), &Value::Str("1".into())));
728 }
729}