1use crate::executor::eval::eval;
4use crate::executor::operators::create::{
5 is_system_property, is_temporal_edge_property, SYSTEM_PROP_UPDATED_AT,
6};
7use crate::executor::{ExecutionError, Params, Record, ScalarFnLookup, TriggerLookup, Value};
8use crate::parser::ast::*;
9use cypherlite_core::{LabelRegistry, PropertyValue};
10use cypherlite_storage::StorageEngine;
11
12pub fn execute_set(
16 source_records: Vec<Record>,
17 items: &[SetItem],
18 engine: &mut StorageEngine,
19 params: &Params,
20 scalar_fns: &dyn ScalarFnLookup,
21 trigger_fns: &dyn TriggerLookup,
22) -> Result<Vec<Record>, ExecutionError> {
23 for record in &source_records {
24 for item in items {
25 match item {
26 SetItem::Property { target, value } => {
27 apply_set_property(
28 target,
29 value,
30 record,
31 engine,
32 params,
33 scalar_fns,
34 trigger_fns,
35 )?;
36 }
37 }
38 }
39 }
40
41 Ok(source_records)
42}
43
44fn get_query_timestamp(params: &Params) -> i64 {
46 match params.get("__query_start_ms__") {
47 Some(Value::Int64(ms)) => *ms,
48 _ => std::time::SystemTime::now()
49 .duration_since(std::time::UNIX_EPOCH)
50 .map(|d| d.as_millis() as i64)
51 .unwrap_or(0),
52 }
53}
54
55fn inject_updated_at(
57 props: &mut Vec<(u32, PropertyValue)>,
58 engine: &mut StorageEngine,
59 params: &Params,
60) {
61 let now = get_query_timestamp(params);
62 let updated_key = engine.get_or_create_prop_key(SYSTEM_PROP_UPDATED_AT);
63 let mut found = false;
64 for (k, v) in props.iter_mut() {
65 if *k == updated_key {
66 *v = PropertyValue::DateTime(now);
67 found = true;
68 break;
69 }
70 }
71 if !found {
72 props.push((updated_key, PropertyValue::DateTime(now)));
73 }
74}
75
76fn apply_set_property(
78 target: &Expression,
79 value_expr: &Expression,
80 record: &Record,
81 engine: &mut StorageEngine,
82 params: &Params,
83 scalar_fns: &dyn ScalarFnLookup,
84 trigger_fns: &dyn TriggerLookup,
85) -> Result<(), ExecutionError> {
86 match target {
88 Expression::Property(var_expr, prop_name) => {
89 if is_system_property(prop_name) {
92 return Err(ExecutionError {
93 message: format!("System property is read-only: {}", prop_name),
94 });
95 }
96
97 let entity = eval(var_expr, record, &*engine, params, scalar_fns)?;
98 let new_value = eval(value_expr, record, &*engine, params, scalar_fns)?;
99 let pv = PropertyValue::try_from(new_value).map_err(|e| ExecutionError {
100 message: format!("invalid property value: {}", e),
101 })?;
102
103 let temporal_enabled = engine.config().temporal_tracking_enabled;
104
105 match entity {
106 Value::Node(nid) => {
107 if is_temporal_edge_property(prop_name) {
109 return Err(ExecutionError {
110 message: format!(
111 "Property '{}' is only valid on edges, not nodes",
112 prop_name
113 ),
114 });
115 }
116
117 let prop_key_id = engine.get_or_create_prop_key(prop_name);
118 let node = engine.get_node(nid).ok_or_else(|| ExecutionError {
120 message: format!("node {} not found", nid.0),
121 })?;
122 let mut props = node.properties.clone();
123 let label_name =
124 node.labels.first().copied().and_then(|lid| {
125 engine.catalog().label_name(lid).map(|s| s.to_string())
126 });
127 let _ = node;
129
130 let mut found = false;
132 for (k, v) in &mut props {
133 if *k == prop_key_id {
134 *v = pv.clone();
135 found = true;
136 break;
137 }
138 }
139 if !found {
140 props.push((prop_key_id, pv));
141 }
142
143 if temporal_enabled {
145 inject_updated_at(&mut props, engine, params);
146 }
147
148 let ctx = cypherlite_core::TriggerContext {
150 entity_type: cypherlite_core::EntityType::Node,
151 entity_id: nid.0,
152 label_or_type: label_name,
153 properties: props
154 .iter()
155 .map(|(k, v)| {
156 let name = engine
157 .catalog()
158 .prop_key_name(*k)
159 .unwrap_or("?")
160 .to_string();
161 (name, v.clone())
162 })
163 .collect(),
164 operation: cypherlite_core::TriggerOperation::Update,
165 };
166 trigger_fns.fire_before_update(&ctx)?;
167
168 engine.update_node(nid, props).map_err(|e| ExecutionError {
169 message: format!("failed to update node: {}", e),
170 })?;
171
172 trigger_fns.fire_after_update(&ctx)?;
173 }
174 Value::Edge(eid) => {
175 let prop_key_id = engine.get_or_create_prop_key(prop_name);
176 let edge = engine.get_edge(eid).ok_or_else(|| ExecutionError {
178 message: format!("edge {} not found", eid.0),
179 })?;
180 let mut props = edge.properties.clone();
181 let rel_type_name = engine
182 .catalog()
183 .rel_type_name(edge.rel_type_id)
184 .map(|s| s.to_string());
185 let _ = edge;
187
188 let mut found = false;
190 for (k, v) in &mut props {
191 if *k == prop_key_id {
192 *v = pv.clone();
193 found = true;
194 break;
195 }
196 }
197 if !found {
198 props.push((prop_key_id, pv));
199 }
200
201 if temporal_enabled {
203 inject_updated_at(&mut props, engine, params);
204 }
205
206 let ctx = cypherlite_core::TriggerContext {
208 entity_type: cypherlite_core::EntityType::Edge,
209 entity_id: eid.0,
210 label_or_type: rel_type_name,
211 properties: props
212 .iter()
213 .map(|(k, v)| {
214 let name = engine
215 .catalog()
216 .prop_key_name(*k)
217 .unwrap_or("?")
218 .to_string();
219 (name, v.clone())
220 })
221 .collect(),
222 operation: cypherlite_core::TriggerOperation::Update,
223 };
224 trigger_fns.fire_before_update(&ctx)?;
225
226 engine.update_edge(eid, props).map_err(|e| ExecutionError {
227 message: format!("failed to update edge: {}", e),
228 })?;
229
230 trigger_fns.fire_after_update(&ctx)?;
231 }
232 Value::Null => {
233 }
235 _ => {
236 return Err(ExecutionError {
237 message: "SET target must be a node or edge property".to_string(),
238 });
239 }
240 }
241 }
242 _ => {
243 return Err(ExecutionError {
244 message: "SET target must be a property access expression".to_string(),
245 });
246 }
247 }
248
249 Ok(())
250}
251
252pub fn execute_remove(
254 source_records: Vec<Record>,
255 items: &[RemoveItem],
256 engine: &mut StorageEngine,
257 params: &Params,
258 scalar_fns: &dyn ScalarFnLookup,
259 trigger_fns: &dyn TriggerLookup,
260) -> Result<Vec<Record>, ExecutionError> {
261 for record in &source_records {
262 for item in items {
263 match item {
264 RemoveItem::Property(prop_expr) => {
265 apply_remove_property(
266 prop_expr,
267 record,
268 engine,
269 params,
270 scalar_fns,
271 trigger_fns,
272 )?;
273 }
274 RemoveItem::Label { variable, label } => {
275 apply_remove_label(variable, label, record, engine)?;
276 }
277 }
278 }
279 }
280
281 Ok(source_records)
282}
283
284fn apply_remove_property(
286 prop_expr: &Expression,
287 record: &Record,
288 engine: &mut StorageEngine,
289 params: &Params,
290 scalar_fns: &dyn ScalarFnLookup,
291 trigger_fns: &dyn TriggerLookup,
292) -> Result<(), ExecutionError> {
293 match prop_expr {
294 Expression::Property(var_expr, prop_name) => {
295 if is_system_property(prop_name) {
297 return Err(ExecutionError {
298 message: format!("System property is read-only: {}", prop_name),
299 });
300 }
301
302 let entity = eval(var_expr, record, &*engine, params, scalar_fns)?;
303 let temporal_enabled = engine.config().temporal_tracking_enabled;
304
305 match entity {
306 Value::Node(nid) => {
307 let prop_key_id = match engine.catalog().prop_key_id(prop_name) {
308 Some(id) => id,
309 None => return Ok(()), };
311
312 let node = engine.get_node(nid).ok_or_else(|| ExecutionError {
313 message: format!("node {} not found", nid.0),
314 })?;
315 let label_name =
316 node.labels.first().copied().and_then(|lid| {
317 engine.catalog().label_name(lid).map(|s| s.to_string())
318 });
319 let mut props: Vec<_> = node
320 .properties
321 .iter()
322 .filter(|(k, _)| *k != prop_key_id)
323 .cloned()
324 .collect();
325
326 if temporal_enabled {
328 inject_updated_at(&mut props, engine, params);
329 }
330
331 let ctx = cypherlite_core::TriggerContext {
332 entity_type: cypherlite_core::EntityType::Node,
333 entity_id: nid.0,
334 label_or_type: label_name,
335 properties: props
336 .iter()
337 .map(|(k, v)| {
338 let name = engine
339 .catalog()
340 .prop_key_name(*k)
341 .unwrap_or("?")
342 .to_string();
343 (name, v.clone())
344 })
345 .collect(),
346 operation: cypherlite_core::TriggerOperation::Update,
347 };
348 trigger_fns.fire_before_update(&ctx)?;
349
350 engine.update_node(nid, props).map_err(|e| ExecutionError {
351 message: format!("failed to update node: {}", e),
352 })?;
353
354 trigger_fns.fire_after_update(&ctx)?;
355 }
356 Value::Edge(eid) => {
357 let prop_key_id = match engine.catalog().prop_key_id(prop_name) {
358 Some(id) => id,
359 None => return Ok(()),
360 };
361
362 let edge = engine.get_edge(eid).ok_or_else(|| ExecutionError {
363 message: format!("edge {} not found", eid.0),
364 })?;
365 let rel_type_name = engine
366 .catalog()
367 .rel_type_name(edge.rel_type_id)
368 .map(|s| s.to_string());
369 let mut props: Vec<_> = edge
370 .properties
371 .iter()
372 .filter(|(k, _)| *k != prop_key_id)
373 .cloned()
374 .collect();
375
376 if temporal_enabled {
377 inject_updated_at(&mut props, engine, params);
378 }
379
380 let ctx = cypherlite_core::TriggerContext {
381 entity_type: cypherlite_core::EntityType::Edge,
382 entity_id: eid.0,
383 label_or_type: rel_type_name,
384 properties: props
385 .iter()
386 .map(|(k, v)| {
387 let name = engine
388 .catalog()
389 .prop_key_name(*k)
390 .unwrap_or("?")
391 .to_string();
392 (name, v.clone())
393 })
394 .collect(),
395 operation: cypherlite_core::TriggerOperation::Update,
396 };
397 trigger_fns.fire_before_update(&ctx)?;
398
399 engine.update_edge(eid, props).map_err(|e| ExecutionError {
400 message: format!("failed to update edge: {}", e),
401 })?;
402
403 trigger_fns.fire_after_update(&ctx)?;
404 }
405 Value::Null => {} _ => {
407 return Err(ExecutionError {
408 message: "REMOVE target must be a node or edge property".to_string(),
409 });
410 }
411 }
412 }
413 _ => {
414 return Err(ExecutionError {
415 message: "REMOVE property must be a property access expression".to_string(),
416 });
417 }
418 }
419
420 Ok(())
421}
422
423fn apply_remove_label(
425 variable: &str,
426 label: &str,
427 record: &Record,
428 engine: &mut StorageEngine,
429) -> Result<(), ExecutionError> {
430 let entity = record.get(variable).cloned().unwrap_or(Value::Null);
431
432 match entity {
433 Value::Node(nid) => {
434 let label_id = match engine.catalog().label_id(label) {
435 Some(id) => id,
436 None => return Ok(()), };
438
439 let node = engine.get_node(nid).ok_or_else(|| ExecutionError {
440 message: format!("node {} not found", nid.0),
441 })?;
442
443 let _ = label_id;
450 let _ = node;
451
452 Ok(())
455 }
456 Value::Null => Ok(()),
457 _ => Err(ExecutionError {
458 message: "REMOVE label target must be a node".to_string(),
459 }),
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use crate::executor::Record;
467 use cypherlite_core::{DatabaseConfig, SyncMode};
468 use tempfile::tempdir;
469
470 fn test_engine(dir: &std::path::Path) -> StorageEngine {
471 let config = DatabaseConfig {
472 path: dir.join("test.cyl"),
473 wal_sync_mode: SyncMode::Normal,
474 ..Default::default()
475 };
476 StorageEngine::open(config).expect("open")
477 }
478
479 #[test]
480 fn test_set_property_on_node() {
481 let dir = tempdir().expect("tempdir");
482 let mut engine = test_engine(dir.path());
483
484 let name_key = engine.get_or_create_prop_key("name");
485 let nid = engine.create_node(
486 vec![],
487 vec![(name_key, PropertyValue::String("Alice".into()))],
488 );
489
490 let mut record = Record::new();
491 record.insert("n".to_string(), Value::Node(nid));
492
493 let items = vec![SetItem::Property {
494 target: Expression::Property(
495 Box::new(Expression::Variable("n".to_string())),
496 "name".to_string(),
497 ),
498 value: Expression::Literal(Literal::String("Bob".into())),
499 }];
500
501 let params = Params::new();
502 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
503 assert!(result.is_ok());
504
505 let node = engine.get_node(nid).expect("node exists");
507 let name_val = node
508 .properties
509 .iter()
510 .find(|(k, _)| *k == name_key)
511 .map(|(_, v)| v);
512 assert_eq!(name_val, Some(&PropertyValue::String("Bob".into())));
513 }
514
515 #[test]
516 fn test_set_new_property() {
517 let dir = tempdir().expect("tempdir");
518 let mut engine = test_engine(dir.path());
519
520 let nid = engine.create_node(vec![], vec![]);
521
522 let mut record = Record::new();
523 record.insert("n".to_string(), Value::Node(nid));
524
525 let items = vec![SetItem::Property {
526 target: Expression::Property(
527 Box::new(Expression::Variable("n".to_string())),
528 "age".to_string(),
529 ),
530 value: Expression::Literal(Literal::Integer(30)),
531 }];
532
533 let params = Params::new();
534 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
535 assert!(result.is_ok());
536
537 let age_key = engine.catalog().prop_key_id("age").expect("age key");
538 let node = engine.get_node(nid).expect("node exists");
539 let age_val = node
540 .properties
541 .iter()
542 .find(|(k, _)| *k == age_key)
543 .map(|(_, v)| v);
544 assert_eq!(age_val, Some(&PropertyValue::Int64(30)));
545 }
546
547 #[test]
548 fn test_set_on_null_is_noop() {
549 let dir = tempdir().expect("tempdir");
550 let mut engine = test_engine(dir.path());
551
552 let mut record = Record::new();
553 record.insert("n".to_string(), Value::Null);
554
555 let items = vec![SetItem::Property {
556 target: Expression::Property(
557 Box::new(Expression::Variable("n".to_string())),
558 "name".to_string(),
559 ),
560 value: Expression::Literal(Literal::String("test".into())),
561 }];
562
563 let params = Params::new();
564 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
565 assert!(result.is_ok());
566 }
567
568 #[test]
569 fn test_remove_property() {
570 let dir = tempdir().expect("tempdir");
571 let mut engine = test_engine(dir.path());
572
573 let name_key = engine.get_or_create_prop_key("name");
574 let age_key = engine.get_or_create_prop_key("age");
575 let nid = engine.create_node(
576 vec![],
577 vec![
578 (name_key, PropertyValue::String("Alice".into())),
579 (age_key, PropertyValue::Int64(30)),
580 ],
581 );
582
583 let mut record = Record::new();
584 record.insert("n".to_string(), Value::Node(nid));
585
586 let items = vec![RemoveItem::Property(Expression::Property(
587 Box::new(Expression::Variable("n".to_string())),
588 "age".to_string(),
589 ))];
590
591 let params = Params::new();
592 let result = execute_remove(vec![record], &items, &mut engine, ¶ms, &(), &());
593 assert!(result.is_ok());
594
595 let node = engine.get_node(nid).expect("node exists");
596 assert_eq!(node.properties.len(), 2);
598 assert!(node.properties.iter().any(|(k, _)| *k == name_key));
599 let updated_key = engine.catalog().prop_key_id("_updated_at").expect("key");
600 assert!(node.properties.iter().any(|(k, _)| *k == updated_key));
601 }
602
603 #[test]
605 fn test_set_property_on_edge() {
606 let dir = tempdir().expect("tempdir");
607 let mut engine = test_engine(dir.path());
608
609 let weight_key = engine.get_or_create_prop_key("weight");
610 let n1 = engine.create_node(vec![], vec![]);
611 let n2 = engine.create_node(vec![], vec![]);
612 let eid = engine
613 .create_edge(n1, n2, 1, vec![(weight_key, PropertyValue::Float64(1.0))])
614 .expect("edge");
615
616 let mut record = Record::new();
617 record.insert("r".to_string(), Value::Edge(eid));
618
619 let items = vec![SetItem::Property {
620 target: Expression::Property(
621 Box::new(Expression::Variable("r".to_string())),
622 "weight".to_string(),
623 ),
624 value: Expression::Literal(Literal::Float(2.5)),
625 }];
626
627 let params = Params::new();
628 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
629 assert!(result.is_ok());
630
631 let edge = engine.get_edge(eid).expect("edge exists");
632 let weight_val = edge
633 .properties
634 .iter()
635 .find(|(k, _)| *k == weight_key)
636 .map(|(_, v)| v);
637 assert_eq!(weight_val, Some(&PropertyValue::Float64(2.5)));
638 }
639
640 #[test]
642 fn test_set_on_edge_updates_updated_at() {
643 let dir = tempdir().expect("tempdir");
644 let mut engine = test_engine(dir.path());
645
646 let n1 = engine.create_node(vec![], vec![]);
647 let n2 = engine.create_node(vec![], vec![]);
648 let eid = engine.create_edge(n1, n2, 1, vec![]).expect("edge");
649
650 let mut record = Record::new();
651 record.insert("r".to_string(), Value::Edge(eid));
652
653 let items = vec![SetItem::Property {
654 target: Expression::Property(
655 Box::new(Expression::Variable("r".to_string())),
656 "weight".to_string(),
657 ),
658 value: Expression::Literal(Literal::Float(1.0)),
659 }];
660
661 let mut params = Params::new();
662 params.insert("__query_start_ms__".to_string(), Value::Int64(9_999_999));
663 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
664 assert!(result.is_ok());
665
666 let edge = engine.get_edge(eid).expect("edge exists");
667 let updated_key = engine
668 .catalog()
669 .prop_key_id("_updated_at")
670 .expect("updated key");
671 let updated_val = edge
672 .properties
673 .iter()
674 .find(|(k, _)| *k == updated_key)
675 .map(|(_, v)| v);
676 assert_eq!(updated_val, Some(&PropertyValue::DateTime(9_999_999)));
677 }
678
679 #[test]
681 fn test_set_valid_from_on_edge_allowed() {
682 let dir = tempdir().expect("tempdir");
683 let mut engine = test_engine(dir.path());
684
685 let n1 = engine.create_node(vec![], vec![]);
686 let n2 = engine.create_node(vec![], vec![]);
687 let eid = engine.create_edge(n1, n2, 1, vec![]).expect("edge");
688
689 let mut record = Record::new();
690 record.insert("r".to_string(), Value::Edge(eid));
691
692 let items = vec![SetItem::Property {
693 target: Expression::Property(
694 Box::new(Expression::Variable("r".to_string())),
695 "_valid_from".to_string(),
696 ),
697 value: Expression::Literal(Literal::Integer(1_700_000_000_000)),
698 }];
699
700 let params = Params::new();
701 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
702 assert!(result.is_ok());
703
704 let edge = engine.get_edge(eid).expect("edge exists");
705 let vf_key = engine
706 .catalog()
707 .prop_key_id("_valid_from")
708 .expect("valid_from key");
709 let vf_val = edge
710 .properties
711 .iter()
712 .find(|(k, _)| *k == vf_key)
713 .map(|(_, v)| v);
714 assert_eq!(vf_val, Some(&PropertyValue::Int64(1_700_000_000_000)));
715 }
716
717 #[test]
719 fn test_set_valid_to_on_edge_allowed() {
720 let dir = tempdir().expect("tempdir");
721 let mut engine = test_engine(dir.path());
722
723 let n1 = engine.create_node(vec![], vec![]);
724 let n2 = engine.create_node(vec![], vec![]);
725 let eid = engine.create_edge(n1, n2, 1, vec![]).expect("edge");
726
727 let mut record = Record::new();
728 record.insert("r".to_string(), Value::Edge(eid));
729
730 let items = vec![SetItem::Property {
731 target: Expression::Property(
732 Box::new(Expression::Variable("r".to_string())),
733 "_valid_to".to_string(),
734 ),
735 value: Expression::Literal(Literal::Integer(1_800_000_000_000)),
736 }];
737
738 let params = Params::new();
739 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
740 assert!(result.is_ok());
741 }
742
743 #[test]
745 fn test_set_valid_from_on_node_blocked() {
746 let dir = tempdir().expect("tempdir");
747 let mut engine = test_engine(dir.path());
748
749 let nid = engine.create_node(vec![], vec![]);
750
751 let mut record = Record::new();
752 record.insert("n".to_string(), Value::Node(nid));
753
754 let items = vec![SetItem::Property {
755 target: Expression::Property(
756 Box::new(Expression::Variable("n".to_string())),
757 "_valid_from".to_string(),
758 ),
759 value: Expression::Literal(Literal::Integer(1_700_000_000_000)),
760 }];
761
762 let params = Params::new();
763 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
764 assert!(result.is_err());
765 }
766
767 #[test]
769 fn test_set_created_at_on_edge_blocked() {
770 let dir = tempdir().expect("tempdir");
771 let mut engine = test_engine(dir.path());
772
773 let n1 = engine.create_node(vec![], vec![]);
774 let n2 = engine.create_node(vec![], vec![]);
775 let eid = engine.create_edge(n1, n2, 1, vec![]).expect("edge");
776
777 let mut record = Record::new();
778 record.insert("r".to_string(), Value::Edge(eid));
779
780 let items = vec![SetItem::Property {
781 target: Expression::Property(
782 Box::new(Expression::Variable("r".to_string())),
783 "_created_at".to_string(),
784 ),
785 value: Expression::Literal(Literal::Integer(999)),
786 }];
787
788 let params = Params::new();
789 let result = execute_set(vec![record], &items, &mut engine, ¶ms, &(), &());
790 assert!(result.is_err());
791 }
792
793 #[test]
795 fn test_remove_property_on_edge() {
796 let dir = tempdir().expect("tempdir");
797 let mut engine = test_engine(dir.path());
798
799 let weight_key = engine.get_or_create_prop_key("weight");
800 let color_key = engine.get_or_create_prop_key("color");
801 let n1 = engine.create_node(vec![], vec![]);
802 let n2 = engine.create_node(vec![], vec![]);
803 let eid = engine
804 .create_edge(
805 n1,
806 n2,
807 1,
808 vec![
809 (weight_key, PropertyValue::Float64(1.0)),
810 (color_key, PropertyValue::String("red".into())),
811 ],
812 )
813 .expect("edge");
814
815 let mut record = Record::new();
816 record.insert("r".to_string(), Value::Edge(eid));
817
818 let items = vec![RemoveItem::Property(Expression::Property(
819 Box::new(Expression::Variable("r".to_string())),
820 "weight".to_string(),
821 ))];
822
823 let params = Params::new();
824 let result = execute_remove(vec![record], &items, &mut engine, ¶ms, &(), &());
825 assert!(result.is_ok());
826
827 let edge = engine.get_edge(eid).expect("edge exists");
828 assert!(edge.properties.iter().any(|(k, _)| *k == color_key));
830 assert!(!edge.properties.iter().any(|(k, _)| *k == weight_key));
831 }
832}