Skip to main content

akar_processor/physical/write_ops/
set.rs

1//! Auto-extracted from physical_operator.rs
2use crate::expression_evaluator::ExpressionEvaluator;
3use crate::physical::types::{OperatorResult, PhysicalOperatorExec};
4use crate::physical::write_ops::delete::{ast_constant_to_value, row_id_column_index};
5use akar_common::arrow_vector::VectorAccess;
6use akar_common::types::Value;
7use akar_common::types::{PhysicalTypeID, physical_type_from_logical};
8use akar_common::vector::{DataChunk, ValueVector};
9use akar_function::registry::FunctionRegistry;
10use akar_function::scalar::evaluate_scalar;
11use akar_parser::ast::{BinaryOp, Expression};
12use akar_planner::logical_operator::SetItem;
13use akar_storage::table::{ColumnDefinition, TableCatalog};
14use akar_transaction::UndoRecord;
15use std::sync::{Arc, Mutex};
16
17// ==================== Set ====================
18
19/// Physical operator for SET — updates properties on matched rows.
20///
21/// All items of a SET clause are evaluated against the SAME pre-update
22/// snapshot of the table, then written (atomic semantics, P53.17). This means
23/// `SET n.a = n.a + 1, n.b = n.a * 10` computes both RHS values from the
24/// pre-update `n.a`, matching Cypher/Neo4j behavior.
25pub struct PhysicalSet {
26    pub table_name: String,
27    pub table_id: u64,
28    pub is_node: bool,
29    pub items: Vec<SetItem>,
30    pub table_catalog: Arc<TableCatalog>,
31    /// Active transaction id (P52.18).
32    pub txn_id: Option<u64>,
33    /// Undo sink for rollback records (P52.18).
34    pub undo_sink: Option<Arc<Mutex<Vec<UndoRecord>>>>,
35    /// Function registry for evaluating non-constant SET value expressions
36    /// (arithmetic, property reads, function calls) against old row data (P53.17).
37    pub function_registry: Option<Arc<Mutex<FunctionRegistry>>>,
38}
39
40impl PhysicalOperatorExec for PhysicalSet {
41    fn operator_type(&self) -> &str {
42        "set"
43    }
44
45    fn execute(&self, input: Vec<DataChunk>) -> OperatorResult {
46        // Collect row indices from input chunks. The scan emits the physical
47        // row index as the `<alias>._id` column (last column); reading column 0
48        // would treat the first *property* value as a row index.
49        let mut rows_to_update: Vec<u64> = Vec::new();
50        // For each target row, remember which input (chunk, row) it came from so
51        // the snapshot can carry pipeline-only columns (e.g. an UNWIND variable)
52        // into the SET value expressions (P53.26).
53        let mut source_rows: Vec<(usize, usize)> = Vec::new();
54
55        for (ci, chunk) in input.iter().enumerate() {
56            let row_id_col = row_id_column_index(chunk);
57            for row in 0..chunk.size {
58                if !chunk.fields.is_empty()
59                    && let Some(Value::Int64(val)) = chunk.get_value(row_id_col.unwrap_or(0), row)
60                {
61                    rows_to_update.push(val as u64);
62                    source_rows.push((ci, row));
63                }
64            }
65        }
66
67        if rows_to_update.is_empty() {
68            return Ok(vec![count_chunk(0)]);
69        }
70
71        // Build ONE pre-update snapshot chunk from the table for all target rows.
72        let snapshot = self.build_snapshot_chunk(&rows_to_update, &input, &source_rows)?;
73
74        // Evaluate every item against the SAME snapshot (P53.17). This reads
75        // true pre-write values: `SET n.x = n.x + 1` increments, and a
76        // multi-item `SET n.a = ..., n.b = n.a * 10` computes `n.b` from the
77        // pre-update `n.a`, matching Cypher/Neo4j semantics.
78        let mut all_values: Vec<Vec<Value>> = Vec::with_capacity(self.items.len());
79        for item in &self.items {
80            all_values.push(self.evaluate_item(item, &snapshot)?);
81        }
82
83        // Apply updates to the table
84        let mut updated = 0u64;
85        if self.is_node {
86            if let Some(mut table) = self.table_catalog.get_node_table_by_name_mut(&self.table_name) {
87                for (item_idx, item) in self.items.iter().enumerate() {
88                    // The binder hardcodes `column_idx: 0` ("resolved by catalog
89                    // lookup") but never resolves it — resolve by name at runtime
90                    // so `SET n.prop = v` writes to `prop`, not column 0.
91                    let col_idx = table
92                        .columns
93                        .iter()
94                        .position(|c| c.name == item.column_name)
95                        .unwrap_or(item.column_idx);
96                    for (i, row_idx) in rows_to_update.iter().enumerate() {
97                        // Capture the pre-update cell for rollback (P52.18).
98                        if let Some(sink) = self.undo_sink.as_ref()
99                            && let Ok(mut u) = sink.lock()
100                        {
101                            let old_data = table.cell_undo_bytes(*row_idx, col_idx);
102                            u.push(UndoRecord::update(self.table_id, *row_idx, col_idx as u32, old_data));
103                        }
104                        if table
105                            .update_cell(*row_idx, col_idx, all_values[item_idx][i].clone())
106                            .is_ok()
107                        {
108                            updated += 1;
109                        }
110                    }
111                }
112            } else {
113                return Err(format!("Node table '{}' not found for SET", self.table_name).into());
114            }
115        } else {
116            if let Some(mut table) = self.table_catalog.get_rel_table_by_name_mut(&self.table_name) {
117                for (item_idx, item) in self.items.iter().enumerate() {
118                    let col_idx = table
119                        .columns
120                        .iter()
121                        .position(|c| c.name == item.column_name)
122                        .unwrap_or(item.column_idx);
123                    for (i, edge_idx) in rows_to_update.iter().enumerate() {
124                        if let Some(sink) = self.undo_sink.as_ref()
125                            && let Ok(mut u) = sink.lock()
126                        {
127                            let old_data = table.edge_cell_undo_bytes(*edge_idx as usize, col_idx);
128                            u.push(UndoRecord::update(self.table_id, *edge_idx, col_idx as u32, old_data));
129                        }
130                        if table
131                            .update_cell(*edge_idx as usize, col_idx, all_values[item_idx][i].clone())
132                            .is_ok()
133                        {
134                            updated += 1;
135                        }
136                    }
137                }
138            } else {
139                return Err(format!("Rel table '{}' not found for SET", self.table_name).into());
140            }
141        }
142
143        tracing::info!("SET: updated {updated} rows in '{}'", self.table_name);
144
145        // Carry the updated rows forward (P53.30): [count, <table columns>,
146        // <pipeline columns>, <_id>]. Column 0 keeps the updated count so
147        // `get_i64(0, 0)` checks stay valid; a following RETURN resolves
148        // `<alias>.prop` against the named table columns instead of the count.
149        let mut output = self.build_output_chunk(&rows_to_update, &input, &source_rows)?;
150        let mut count_v = ValueVector::new(PhysicalTypeID::Int64, rows_to_update.len());
151        count_v.resize(rows_to_update.len());
152        for i in 0..rows_to_update.len() {
153            count_v.set_i64(i, updated as i64);
154        }
155        output
156            .fields
157            .insert(0, akar_common::arrow_vector::ArrowVector::from_legacy(&count_v).array);
158        output.field_types.insert(0, PhysicalTypeID::Int64);
159        output.field_names.insert(0, String::new());
160        Ok(vec![output])
161    }
162}
163
164fn count_chunk(count: u64) -> DataChunk {
165    let mut v = ValueVector::new(PhysicalTypeID::Int64, 1);
166    v.resize(1);
167    v.set_i64(0, count as i64);
168    let arr = akar_common::arrow_vector::ArrowVector::from_legacy(&v).array;
169    DataChunk::new(vec![arr], vec![PhysicalTypeID::Int64])
170}
171
172impl PhysicalSet {
173    /// Build a `DataChunk` of the target rows' pre-update cell values, one
174    /// column per table column, keyed by the physical row offsets in `rows`.
175    /// Pipeline-only columns from the input chunks (e.g. an UNWIND variable)
176    /// are appended so SET value expressions can reference them (P53.26).
177    fn build_snapshot_chunk(
178        &self,
179        rows: &[u64],
180        input: &[DataChunk],
181        source_rows: &[(usize, usize)],
182    ) -> Result<DataChunk, String> {
183        let mut snapshot = if self.is_node {
184            let table = self
185                .table_catalog
186                .get_node_table_by_name(&self.table_name)
187                .ok_or_else(|| format!("Node table '{}' not found for SET", self.table_name))?;
188            build_old_row_chunk(&table.columns, rows, &|row, col| {
189                table.get_value(row as usize, col).cloned()
190            })?
191        } else {
192            let table = self
193                .table_catalog
194                .get_rel_table_by_name(&self.table_name)
195                .ok_or_else(|| format!("Rel table '{}' not found for SET", self.table_name))?;
196            build_old_row_chunk(&table.columns, rows, &|row, col| {
197                table.get_edge_properties(row as usize).get(col).cloned()
198            })?
199        };
200        append_pipeline_columns(&mut snapshot, input, source_rows)?;
201        Ok(snapshot)
202    }
203}
204
205/// Append input-chunk columns that are not table columns (nor the internal
206/// `_id` pseudo-column) to the chunk, aligned by the source row index of each
207/// target row. This makes UNWIND variables visible to later clauses, e.g.
208/// `UNWIND $ids AS iid ... SET n.x = iid` (P53.26).
209pub(crate) fn append_pipeline_columns(
210    snapshot: &mut DataChunk,
211    input: &[DataChunk],
212    source_rows: &[(usize, usize)],
213) -> Result<(), String> {
214    let n = source_rows.len();
215    for (ci, chunk) in input.iter().enumerate() {
216        let mut appended: Vec<(usize, String)> = Vec::new();
217        for (col_idx, name) in chunk.field_names.iter().enumerate() {
218            if name == "_id" || name.ends_with("._id") {
219                continue;
220            }
221            if snapshot.field_names.iter().any(|existing| existing == name) {
222                continue;
223            }
224            // Qualified copies (r.weight) whose base matches a plain snapshot
225            // column (weight) are pre-write stale reads from the pipeline; the
226            // evaluator's bare-name fallback resolves to the fresh table column
227            // instead, so a following `RETURN r.weight` sees the written value
228            // (P53.37a).
229            if let Some((_, base)) = name.rsplit_once('.') {
230                if snapshot.field_names.iter().any(|existing| existing == base) {
231                    continue;
232                }
233            }
234            appended.push((col_idx, name.clone()));
235        }
236        if appended.is_empty() {
237            continue;
238        }
239
240        for (col_idx, name) in appended {
241            // Gather each target row's value from this input chunk.
242            let mut col_values: Vec<Value> = vec![Value::Null; n];
243            for (i, &(cci, rowi)) in source_rows.iter().enumerate() {
244                if cci == ci {
245                    col_values[i] = chunk.get_value(col_idx, rowi).unwrap_or(Value::Null);
246                }
247            }
248            // Build via Arrow so complex values (map/struct) survive the
249            // round-trip; `ValueVector::set_value` rejects them.
250            let phys_type = col_values
251                .iter()
252                .find(|v| !matches!(v, Value::Null))
253                .map(|v| v.physical_type())
254                .unwrap_or(chunk.field_types[col_idx]);
255            let arr = crate::expression_evaluator::build_arrow_from_values(&col_values, phys_type, n)
256                .map_err(|e| e.to_string())?;
257            snapshot.fields.push(arr.array);
258            snapshot.field_types.push(arr.physical_type);
259            snapshot.field_names.push(name);
260        }
261    }
262    Ok(())
263}
264
265impl PhysicalSet {
266    /// Build the SET operator's output chunk: the post-update table columns
267    /// (named, so a following RETURN can resolve `<alias>.<prop>`), the input
268    /// pipeline columns (e.g. UNWIND variables), and the `_id` pseudo-column
269    /// with the physical row indices (so a following write op can re-target the
270    /// same rows). Previously SET returned only a count chunk, so `MATCH ... SET
271    /// ... RETURN n.prop` evaluated the projection against the count (P53.30).
272    fn build_output_chunk(
273        &self,
274        rows: &[u64],
275        input: &[DataChunk],
276        source_rows: &[(usize, usize)],
277    ) -> Result<DataChunk, String> {
278        let mut chunk = if self.is_node {
279            let table = self
280                .table_catalog
281                .get_node_table_by_name(&self.table_name)
282                .ok_or_else(|| format!("Node table '{}' not found for SET", self.table_name))?;
283            build_old_row_chunk(&table.columns, rows, &|row, col| {
284                table.get_value(row as usize, col).cloned()
285            })?
286        } else {
287            let table = self
288                .table_catalog
289                .get_rel_table_by_name(&self.table_name)
290                .ok_or_else(|| format!("Rel table '{}' not found for SET", self.table_name))?;
291            build_old_row_chunk(&table.columns, rows, &|row, col| {
292                table.get_edge_properties(row as usize).get(col).cloned()
293            })?
294        };
295        append_pipeline_columns(&mut chunk, input, source_rows)?;
296        // Append the `_id` pseudo-column (physical row indices) so a following
297        // write op can target the same rows.
298        let mut v = ValueVector::new(PhysicalTypeID::Int64, rows.len());
299        v.resize(rows.len());
300        for (i, r) in rows.iter().enumerate() {
301            v.set_i64(i, *r as i64);
302        }
303        chunk
304            .fields
305            .push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
306        chunk.field_types.push(PhysicalTypeID::Int64);
307        chunk.field_names.push("_id".to_string());
308        Ok(chunk)
309    }
310
311    /// Evaluate one SET item's value expression for every row of the snapshot
312    /// chunk, returning one `Value` per row.
313    fn evaluate_item(&self, item: &SetItem, chunk: &DataChunk) -> Result<Vec<Value>, String> {
314        if let Some(registry) = self.function_registry.as_ref() {
315            let evaluator = ExpressionEvaluator::new(registry.clone());
316            // Arrow-native evaluation so complex literals (list/map) survive the
317            // round-trip: `evaluate` builds a legacy ValueVector with no List
318            // storage, which silently produces `Value::Null` (P53.29).
319            let vec = evaluator
320                .evaluate_to_arrow(&item.value, chunk)
321                .map_err(|e| e.to_string())?;
322            Ok((0..chunk.size)
323                .map(|i| vec.get_value(i).unwrap_or(Value::Null))
324                .collect())
325        } else {
326            Ok((0..chunk.size)
327                .map(|i| evaluate_expression_for_row(&item.value, chunk, i))
328                .collect())
329        }
330    }
331}
332
333/// Build a `DataChunk` of the target rows' cell values, one column per table
334/// column, using the plain column names as `field_names` so the evaluator can
335/// resolve `<alias>.<prop>` and `<prop>` references.
336///
337/// The chunk is built via Arrow (`build_arrow_from_values`) rather than
338/// `ValueVector::set_value` so complex values (map/struct/list, e.g. FLOAT[]
339/// embeddings) survive the round-trip — the legacy vector has no List arm and
340/// silently produced `Value::Null` (P53.29).
341pub(crate) fn build_old_row_chunk(
342    columns: &[ColumnDefinition],
343    rows: &[u64],
344    get_cell: &dyn Fn(u64, usize) -> Option<Value>,
345) -> Result<DataChunk, String> {
346    let n = rows.len();
347    let mut fields = Vec::with_capacity(columns.len());
348    let mut field_types = Vec::with_capacity(columns.len());
349    let mut field_names = Vec::with_capacity(columns.len());
350    for (col_idx, col) in columns.iter().enumerate() {
351        let phys_type = physical_type_from_logical(col.logical_type);
352        let col_values: Vec<Value> = rows
353            .iter()
354            .map(|row| get_cell(*row, col_idx).unwrap_or(Value::Null))
355            .collect();
356        let arr = crate::expression_evaluator::build_arrow_from_values(&col_values, phys_type, n)
357            .map_err(|e| e.to_string())?;
358        fields.push(arr.array);
359        field_types.push(phys_type);
360        field_names.push(col.name.clone());
361    }
362    Ok(DataChunk::new(fields, field_types).with_names(field_names))
363}
364
365/// Simple expression evaluator for SET value expressions against a DataChunk row.
366pub fn evaluate_expression_for_row(
367    expr: &akar_parser::ast::Expression,
368    chunk: &DataChunk,
369    row: usize,
370) -> akar_common::types::Value {
371    match expr {
372        akar_parser::ast::Expression::Constant(c) => match c {
373            akar_parser::ast::Constant::Null => akar_common::types::Value::Null,
374            akar_parser::ast::Constant::Bool(b) => akar_common::types::Value::Bool(*b),
375            akar_parser::ast::Constant::Integer(i) => akar_common::types::Value::Int64(*i),
376            akar_parser::ast::Constant::Float(f) => akar_common::types::Value::Double(*f),
377            akar_parser::ast::Constant::String(s) => akar_common::types::Value::String(s.clone()),
378        },
379        akar_parser::ast::Expression::Variable(name) => chunk
380            .field_names
381            .iter()
382            .position(|n| n == name)
383            .and_then(|i| chunk.get_value(i, row))
384            .unwrap_or(akar_common::types::Value::Null),
385        akar_parser::ast::Expression::PropertyAccess(obj, prop) => {
386            let qualified = match obj.as_ref() {
387                akar_parser::ast::Expression::Variable(var) => format!("{var}.{prop}"),
388                _ => prop.clone(),
389            };
390            if let Some(i) = chunk.field_names.iter().position(|n| *n == qualified) {
391                chunk.get_value(i, row).unwrap_or(akar_common::types::Value::Null)
392            } else if let akar_parser::ast::Expression::Variable(var) = obj.as_ref()
393                && let Some(i) = chunk.field_names.iter().position(|n| n == var)
394            {
395                // P53.26: `row.id` where `row` is a map/struct column — extract
396                // the key. Runs before the bare-name match so a plain `content`
397                // table column does not shadow an UNWIND map variable.
398                let obj_val = chunk.get_value(i, row).unwrap_or(akar_common::types::Value::Null);
399                crate::expression_evaluator::map_property_value(&obj_val, prop)
400            } else if let Some(i) = chunk.field_names.iter().position(|n| *n == *prop) {
401                chunk.get_value(i, row).unwrap_or(akar_common::types::Value::Null)
402            } else {
403                akar_common::types::Value::Null
404            }
405        }
406        akar_parser::ast::Expression::UnaryOp(op, inner) => {
407            let v = evaluate_expression_for_row(inner, chunk, row);
408            match op {
409                akar_parser::ast::UnaryOp::Negate => match v {
410                    akar_common::types::Value::Int64(n) => akar_common::types::Value::Int64(-n),
411                    akar_common::types::Value::Double(n) => akar_common::types::Value::Double(-n),
412                    _ => akar_common::types::Value::Null,
413                },
414                _ => akar_common::types::Value::Null,
415            }
416        }
417        akar_parser::ast::Expression::BinaryOp(op, left, right) => {
418            let l = evaluate_expression_for_row(left, chunk, row);
419            let r = evaluate_expression_for_row(right, chunk, row);
420            binary_value_op(op, &l, &r)
421        }
422        akar_parser::ast::Expression::List(items) => {
423            let vals = items
424                .iter()
425                .map(|i| evaluate_expression_for_row(i, chunk, row))
426                .collect();
427            akar_common::types::Value::List(vals)
428        }
429        akar_parser::ast::Expression::Map(items) => {
430            let entries = items
431                .iter()
432                .map(|(k, v)| {
433                    (
434                        akar_common::types::Value::String(k.clone()),
435                        evaluate_expression_for_row(v, chunk, row),
436                    )
437                })
438                .collect();
439            akar_common::types::Value::Map(entries)
440        }
441        _ => akar_common::types::Value::Null,
442    }
443}
444
445fn as_f64(v: &Value) -> Option<f64> {
446    match v {
447        Value::Int64(n) => Some(*n as f64),
448        Value::Int32(n) => Some(*n as f64),
449        Value::Int128(n) => Some(*n as f64),
450        Value::Double(n) => Some(*n),
451        Value::Float(n) => Some(*n as f64),
452        _ => None,
453    }
454}
455
456/// Minimal binary-operator evaluation used when no function registry is
457/// available (unit-test path). The full evaluator is preferred when a registry
458/// is present.
459fn binary_value_op(op: &BinaryOp, l: &Value, r: &Value) -> Value {
460    match op {
461        BinaryOp::Add => match (l, r) {
462            (Value::String(a), Value::String(b)) => Value::String(format!("{a}{b}")),
463            _ => match (as_f64(l), as_f64(r)) {
464                (Some(a), Some(b)) => {
465                    if matches!(l, Value::Int64(_)) && matches!(r, Value::Int64(_)) {
466                        Value::Int64(a as i64 + b as i64)
467                    } else {
468                        Value::Double(a + b)
469                    }
470                }
471                _ => Value::Null,
472            },
473        },
474        BinaryOp::Subtract => match (as_f64(l), as_f64(r)) {
475            (Some(a), Some(b)) => {
476                if matches!(l, Value::Int64(_)) && matches!(r, Value::Int64(_)) {
477                    Value::Int64(a as i64 - b as i64)
478                } else {
479                    Value::Double(a - b)
480                }
481            }
482            _ => Value::Null,
483        },
484        BinaryOp::Multiply => match (as_f64(l), as_f64(r)) {
485            (Some(a), Some(b)) => {
486                if matches!(l, Value::Int64(_)) && matches!(r, Value::Int64(_)) {
487                    Value::Int64(a as i64 * b as i64)
488                } else {
489                    Value::Double(a * b)
490                }
491            }
492            _ => Value::Null,
493        },
494        BinaryOp::Divide => match (as_f64(l), as_f64(r)) {
495            (Some(a), Some(b)) if b != 0.0 => {
496                if matches!(l, Value::Int64(_)) && matches!(r, Value::Int64(_)) {
497                    Value::Int64(a as i64 / b as i64)
498                } else {
499                    Value::Double(a / b)
500                }
501            }
502            _ => Value::Null,
503        },
504        BinaryOp::Modulo => match (l, r) {
505            (Value::Int64(a), Value::Int64(b)) if *b != 0 => Value::Int64(a % b),
506            _ => Value::Null,
507        },
508        BinaryOp::Equal => Value::Bool(l == r),
509        BinaryOp::NotEqual => Value::Bool(l != r),
510        BinaryOp::LessThan => match (as_f64(l), as_f64(r)) {
511            (Some(a), Some(b)) => Value::Bool(a < b),
512            _ => Value::Null,
513        },
514        BinaryOp::LessThanOrEqual => match (as_f64(l), as_f64(r)) {
515            (Some(a), Some(b)) => Value::Bool(a <= b),
516            _ => Value::Null,
517        },
518        BinaryOp::GreaterThan => match (as_f64(l), as_f64(r)) {
519            (Some(a), Some(b)) => Value::Bool(a > b),
520            _ => Value::Null,
521        },
522        BinaryOp::GreaterThanOrEqual => match (as_f64(l), as_f64(r)) {
523            (Some(a), Some(b)) => Value::Bool(a >= b),
524            _ => Value::Null,
525        },
526        BinaryOp::And => match (l, r) {
527            (Value::Bool(a), Value::Bool(b)) => Value::Bool(*a && *b),
528            _ => Value::Null,
529        },
530        BinaryOp::Or => match (l, r) {
531            (Value::Bool(a), Value::Bool(b)) => Value::Bool(*a || *b),
532            _ => Value::Null,
533        },
534        _ => Value::Null,
535    }
536}
537
538/// Evaluate a constant-only expression (literal or function call over
539/// literals) into a `Value`, using the function registry. Used by the
540/// CREATE DML write path to support expressions like `DATE('2024-01-15')`.
541/// Returns `Value::Null` for expressions that reference variables or
542/// otherwise cannot be folded without a row context.
543pub fn evaluate_constant_expr(expr: &Expression, registry: &FunctionRegistry) -> Value {
544    match expr {
545        Expression::Constant(c) => ast_constant_to_value(c),
546        Expression::List(items) => Value::List(items.iter().map(|i| evaluate_constant_expr(i, registry)).collect()),
547        Expression::Map(items) => Value::Map(
548            items
549                .iter()
550                .map(|(k, v)| (Value::String(k.clone()), evaluate_constant_expr(v, registry)))
551                .collect(),
552        ),
553        Expression::FunctionCall(name, args) => {
554            let arg_values: Vec<Value> = args.iter().map(|a| evaluate_constant_expr(a, registry)).collect();
555            if arg_values.iter().any(|v| matches!(v, Value::Null)) {
556                return Value::Null;
557            }
558            let func = match registry.get_scalar(name).cloned() {
559                Some(f) => f,
560                None => return Value::Null,
561            };
562            evaluate_scalar(&func, &arg_values).unwrap_or(Value::Null)
563        }
564        _ => Value::Null,
565    }
566}