Skip to main content

uni_store/storage/
value_codec.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Arrow column value decoding utilities.
5//!
6//! Provides a unified `value_from_column` function for decoding Arrow column
7//! values to `serde_json::Value`, used by both PropertyManager and DeltaDataset.
8
9use anyhow::{Result, anyhow};
10use arrow_array::{
11    Array, BinaryArray, BooleanArray, Date32Array, FixedSizeListArray, Float32Array, Float64Array,
12    Int32Array, Int64Array, LargeBinaryArray, ListArray, StringArray, StructArray,
13    Time64NanosecondArray, TimestampNanosecondArray, UInt32Array,
14};
15use serde_json::Value;
16use uni_common::{DataType, TemporalValue};
17use uni_crdt::Crdt;
18
19/// Controls how CRDT decode errors are handled.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum CrdtDecodeMode {
22    /// Return an error on CRDT decode failure (strict validation).
23    #[default]
24    Strict,
25    /// Log a warning and return a default GCounter on failure (lenient).
26    Lenient,
27}
28
29/// Maximum recursion depth for nested List/Map decoding to prevent stack overflow.
30/// Issue #62: Added to prevent stack overflow from deeply nested structures.
31pub const MAX_DECODE_DEPTH: usize = 32;
32
33/// Decode an Arrow column value to a serde_json::Value.
34///
35/// # Arguments
36/// * `col` - The Arrow array to read from
37/// * `data_type` - The uni_common::DataType describing the column's logical type
38/// * `row` - The row index to read
39/// * `crdt_mode` - How to handle CRDT decode errors
40///
41/// # Returns
42/// The decoded JSON value, or an error if decoding fails.
43pub fn value_from_column(
44    col: &dyn Array,
45    data_type: &DataType,
46    row: usize,
47    crdt_mode: CrdtDecodeMode,
48) -> Result<Value> {
49    value_from_column_inner(col, data_type, row, crdt_mode, 0)
50}
51
52/// Internal implementation of value_from_column with depth tracking.
53fn value_from_column_inner(
54    col: &dyn Array,
55    data_type: &DataType,
56    row: usize,
57    crdt_mode: CrdtDecodeMode,
58    depth: usize,
59) -> Result<Value> {
60    if depth > MAX_DECODE_DEPTH {
61        return Err(anyhow!("decode depth exceeded (max {})", MAX_DECODE_DEPTH));
62    }
63    match data_type {
64        DataType::String => {
65            let s = col
66                .as_any()
67                .downcast_ref::<StringArray>()
68                .ok_or_else(|| anyhow!("Invalid string col"))?
69                .value(row);
70            Ok(Value::String(s.to_string()))
71        }
72        DataType::Int32 => {
73            let v = col
74                .as_any()
75                .downcast_ref::<Int32Array>()
76                .ok_or_else(|| anyhow!("Invalid int32 col"))?
77                .value(row);
78            Ok(serde_json::json!(v))
79        }
80        DataType::Int64 => {
81            let v = col
82                .as_any()
83                .downcast_ref::<Int64Array>()
84                .ok_or_else(|| anyhow!("Invalid int64 col"))?
85                .value(row);
86            Ok(serde_json::json!(v))
87        }
88        DataType::Float32 => {
89            let v = col
90                .as_any()
91                .downcast_ref::<Float32Array>()
92                .ok_or_else(|| anyhow!("Invalid float32 col"))?
93                .value(row);
94            Ok(serde_json::json!(v))
95        }
96        DataType::Float64 => {
97            let v = col
98                .as_any()
99                .downcast_ref::<Float64Array>()
100                .ok_or_else(|| anyhow!("Invalid float64 col"))?
101                .value(row);
102            Ok(serde_json::json!(v))
103        }
104        DataType::Bool => {
105            let v = col
106                .as_any()
107                .downcast_ref::<BooleanArray>()
108                .ok_or_else(|| anyhow!("Invalid bool col"))?
109                .value(row);
110            Ok(serde_json::json!(v))
111        }
112        DataType::Vector { .. } => {
113            let list_arr = col
114                .as_any()
115                .downcast_ref::<FixedSizeListArray>()
116                .ok_or_else(|| anyhow!("Invalid fixed list col for vector"))?;
117            let values = list_arr.value(row);
118            let float_values = values
119                .as_any()
120                .downcast_ref::<Float32Array>()
121                .ok_or_else(|| anyhow!("Invalid float32 inner col for vector"))?;
122
123            let vec: Vec<f32> = (0..float_values.len())
124                .map(|i| float_values.value(i))
125                .collect();
126            Ok(serde_json::json!(vec))
127        }
128        DataType::SparseVector { .. } => {
129            // Explicit arm so a sparse column is never silently nulled by the
130            // `_ => Ok(Value::Null)` fallback below. This serde_json shape
131            // (`{indices, values}`) is the lossy JSON view; full-fidelity
132            // `uni_common::Value::SparseVector` is produced by `arrow_to_value`
133            // via `decode_column_value`.
134            let struct_arr = col
135                .as_any()
136                .downcast_ref::<StructArray>()
137                .ok_or_else(|| anyhow!("Invalid struct col for sparse vector"))?;
138            if struct_arr.is_null(row) {
139                return Ok(Value::Null);
140            }
141            let indices_list = struct_arr
142                .column_by_name("indices")
143                .and_then(|c| c.as_any().downcast_ref::<ListArray>())
144                .ok_or_else(|| anyhow!("sparse vector missing list column 'indices'"))?;
145            let values_list = struct_arr
146                .column_by_name("values")
147                .and_then(|c| c.as_any().downcast_ref::<ListArray>())
148                .ok_or_else(|| anyhow!("sparse vector missing list column 'values'"))?;
149            let idx_vals = indices_list.value(row);
150            let idx_arr = idx_vals
151                .as_any()
152                .downcast_ref::<UInt32Array>()
153                .ok_or_else(|| anyhow!("sparse 'indices' inner not UInt32"))?;
154            let w_vals = values_list.value(row);
155            let w_arr = w_vals
156                .as_any()
157                .downcast_ref::<Float32Array>()
158                .ok_or_else(|| anyhow!("sparse 'values' inner not Float32"))?;
159            let indices: Vec<Value> = (0..idx_arr.len())
160                .map(|i| serde_json::json!(idx_arr.value(i)))
161                .collect();
162            let values: Vec<Value> = (0..w_arr.len())
163                .map(|i| serde_json::json!(w_arr.value(i)))
164                .collect();
165            let mut map = serde_json::Map::new();
166            map.insert("indices".to_string(), Value::Array(indices));
167            map.insert("values".to_string(), Value::Array(values));
168            Ok(Value::Object(map))
169        }
170        DataType::CypherValue => {
171            let bytes = col
172                .as_any()
173                .downcast_ref::<LargeBinaryArray>()
174                .ok_or_else(|| anyhow!("Invalid large binary col for CypherValue"))?
175                .value(row);
176            if bytes.is_empty() {
177                return Ok(Value::Null);
178            }
179            let uni_val = uni_common::cypher_value_codec::decode(bytes)
180                .map_err(|e| anyhow!("CypherValue decode error: {}", e))?;
181            // Convert uni_common::Value to serde_json::Value
182            Ok(uni_val.into())
183        }
184        DataType::Bytes => {
185            let arr = col
186                .as_any()
187                .downcast_ref::<LargeBinaryArray>()
188                .ok_or_else(|| anyhow!("Invalid large binary col for Bytes"))?;
189            if arr.is_null(row) {
190                return Ok(Value::Null);
191            }
192            // serde_json::Value has no native bytes variant; encode as JSON array of u8.
193            let bytes = arr.value(row);
194            Ok(Value::Array(
195                bytes.iter().map(|b| serde_json::json!(*b)).collect(),
196            ))
197        }
198        DataType::Crdt(_) => {
199            let bytes = col
200                .as_any()
201                .downcast_ref::<BinaryArray>()
202                .ok_or_else(|| anyhow!("Invalid binary col for CRDT"))?
203                .value(row);
204
205            match crdt_mode {
206                CrdtDecodeMode::Strict => {
207                    let crdt = Crdt::from_msgpack(bytes)
208                        .map_err(|e| anyhow!("CRDT decode error: {}", e))?;
209                    Ok(serde_json::to_value(crdt)?)
210                }
211                CrdtDecodeMode::Lenient => {
212                    let crdt = Crdt::from_msgpack(bytes).unwrap_or_else(|e| {
213                        log::warn!("Failed to deserialize CRDT: {}", e);
214                        Crdt::GCounter(uni_crdt::GCounter::new())
215                    });
216                    Ok(serde_json::to_value(crdt).unwrap_or(Value::Null))
217                }
218            }
219        }
220        DataType::List(inner) => {
221            let list_arr = col
222                .as_any()
223                .downcast_ref::<ListArray>()
224                .ok_or_else(|| anyhow!("Invalid list col"))?;
225            if list_arr.is_null(row) {
226                return Ok(Value::Null);
227            }
228            let values = list_arr.value(row);
229            let mut vec = Vec::with_capacity(values.len());
230            for i in 0..values.len() {
231                vec.push(value_from_column_inner(
232                    values.as_ref(),
233                    inner,
234                    i,
235                    crdt_mode,
236                    depth + 1,
237                )?);
238            }
239            Ok(Value::Array(vec))
240        }
241        DataType::Map(_, _) => {
242            let list_arr = col
243                .as_any()
244                .downcast_ref::<ListArray>()
245                .ok_or_else(|| anyhow!("Invalid map (list) col"))?;
246            if list_arr.is_null(row) {
247                return Ok(Value::Null);
248            }
249            // Decode through the unified map reconstructor (single source of truth with
250            // `arrow_to_value`): it handles typed scalar value children, raw-`Bytes`
251            // (`uni_raw_bytes`-marked) children, and CV-encoded nested-value fallback
252            // children uniformly by runtime Arrow type — so this is correct regardless of
253            // the declared value type (a nested value is stored as a CV LargeBinary, which
254            // a declared-type recursion would otherwise fail to downcast).
255            let struct_arr = list_arr.value(row);
256            let uni_map = super::arrow_convert::try_reconstruct_map(&struct_arr)
257                .ok_or_else(|| anyhow!("Invalid struct array inner for map"))?;
258            let mut map = serde_json::Map::with_capacity(uni_map.len());
259            for (k, v) in uni_map {
260                map.insert(
261                    k,
262                    serde_json::to_value(&v).unwrap_or(serde_json::Value::Null),
263                );
264            }
265            Ok(Value::Object(map))
266        }
267        DataType::Date => {
268            let arr = col
269                .as_any()
270                .downcast_ref::<Date32Array>()
271                .ok_or_else(|| anyhow!("Invalid date32 col"))?;
272            if arr.is_null(row) {
273                return Ok(Value::Null);
274            }
275            let days = arr.value(row);
276            let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
277            if let Some(date) = epoch.checked_add_signed(chrono::Duration::days(days as i64)) {
278                Ok(Value::String(date.format("%Y-%m-%d").to_string()))
279            } else {
280                Ok(Value::Null)
281            }
282        }
283        DataType::Time => {
284            // Preferred schema: struct{nanos_since_midnight, offset_seconds}
285            if let Some(struct_arr) = col.as_any().downcast_ref::<StructArray>()
286                && let (Some(nanos_col), Some(offset_col)) = (
287                    struct_arr.column_by_name("nanos_since_midnight"),
288                    struct_arr.column_by_name("offset_seconds"),
289                )
290                && let (Some(nanos_arr), Some(offset_arr)) = (
291                    nanos_col.as_any().downcast_ref::<Time64NanosecondArray>(),
292                    offset_col.as_any().downcast_ref::<Int32Array>(),
293                )
294            {
295                if nanos_arr.is_null(row) {
296                    return Ok(Value::Null);
297                }
298                let tv = if offset_arr.is_null(row) {
299                    TemporalValue::LocalTime {
300                        nanos_since_midnight: nanos_arr.value(row),
301                    }
302                } else {
303                    TemporalValue::Time {
304                        nanos_since_midnight: nanos_arr.value(row),
305                        offset_seconds: offset_arr.value(row),
306                    }
307                };
308                return Ok(Value::String(tv.to_string()));
309            }
310
311            // Legacy schema: plain time64 nanos, assume UTC offset=0
312            let arr = col
313                .as_any()
314                .downcast_ref::<Time64NanosecondArray>()
315                .ok_or_else(|| anyhow!("Invalid time64 col"))?;
316            if arr.is_null(row) {
317                return Ok(Value::Null);
318            }
319            let tv = TemporalValue::Time {
320                nanos_since_midnight: arr.value(row),
321                offset_seconds: 0,
322            };
323            Ok(Value::String(tv.to_string()))
324        }
325        DataType::Duration => {
326            // Duration is stored as LargeBinary via CypherValue codec
327            let arr = col
328                .as_any()
329                .downcast_ref::<LargeBinaryArray>()
330                .ok_or_else(|| anyhow!("Invalid duration col (expected LargeBinary)"))?;
331            if arr.is_null(row) {
332                return Ok(Value::Null);
333            }
334            let bytes = arr.value(row);
335            let uni_val = uni_common::cypher_value_codec::decode(bytes)
336                .map_err(|e| anyhow!("Failed to decode duration: {}", e))?;
337            // Return canonical ISO-8601 text for compatibility.
338            if let uni_common::Value::Temporal(uni_common::TemporalValue::Duration {
339                months,
340                days,
341                nanos,
342            }) = &uni_val
343            {
344                let tv = TemporalValue::Duration {
345                    months: *months,
346                    days: *days,
347                    nanos: *nanos,
348                };
349                Ok(Value::String(tv.to_string()))
350            } else {
351                Ok(serde_json::json!(uni_val.to_string()))
352            }
353        }
354        DataType::DateTime | DataType::Timestamp => {
355            // Preferred schema: struct{nanos_since_epoch, offset_seconds, timezone_name}
356            if let Some(struct_arr) = col.as_any().downcast_ref::<StructArray>()
357                && let (Some(nanos_col), Some(offset_col), Some(tz_col)) = (
358                    struct_arr.column_by_name("nanos_since_epoch"),
359                    struct_arr.column_by_name("offset_seconds"),
360                    struct_arr.column_by_name("timezone_name"),
361                )
362                && let (Some(nanos_arr), Some(offset_arr), Some(tz_arr)) = (
363                    nanos_col
364                        .as_any()
365                        .downcast_ref::<TimestampNanosecondArray>(),
366                    offset_col.as_any().downcast_ref::<Int32Array>(),
367                    tz_col.as_any().downcast_ref::<StringArray>(),
368                )
369            {
370                if nanos_arr.is_null(row) {
371                    return Ok(Value::Null);
372                }
373                let tv = if offset_arr.is_null(row) {
374                    TemporalValue::LocalDateTime {
375                        nanos_since_epoch: nanos_arr.value(row),
376                    }
377                } else {
378                    let timezone_name =
379                        (!tz_arr.is_null(row)).then(|| tz_arr.value(row).to_string());
380                    TemporalValue::DateTime {
381                        nanos_since_epoch: nanos_arr.value(row),
382                        offset_seconds: offset_arr.value(row),
383                        timezone_name,
384                    }
385                };
386                return Ok(Value::String(tv.to_string()));
387            }
388
389            // Legacy schema: plain timestamp nanos, assume UTC offset=0
390            let arr = col
391                .as_any()
392                .downcast_ref::<TimestampNanosecondArray>()
393                .ok_or_else(|| anyhow!("Invalid timestamp col"))?;
394            if arr.is_null(row) {
395                return Ok(Value::Null);
396            }
397            let tv = TemporalValue::DateTime {
398                nanos_since_epoch: arr.value(row),
399                offset_seconds: 0,
400                timezone_name: arr.timezone().map(|s| s.to_string()),
401            };
402            Ok(Value::String(tv.to_string()))
403        }
404        // Point decodes as a `serde_json` object mirroring the `Value::Map` shape
405        // `point(...)` produces, so the legacy `value_from_column` path (e.g.
406        // `delta.rs`) does not silently read `Null`. The rich
407        // `decode_column_value` path reconstructs a native `Value::Map` instead.
408        DataType::Point(_) => {
409            let v = super::arrow_convert::arrow_to_value(col, row, Some(data_type));
410            Ok(serde_json::to_value(&v).unwrap_or(Value::Null))
411        }
412        _ => Ok(Value::Null),
413    }
414}
415
416/// Decode an Arrow column value to a [`uni_common::Value`], preserving
417/// `Value::Temporal` variants for round-trip fidelity.
418///
419/// For DateTime/Timestamp/Date/Time, delegates to [`super::arrow_convert::arrow_to_value`].
420/// For all other types, decodes via [`value_from_column`] and converts.
421pub fn decode_column_value(
422    col: &dyn Array,
423    data_type: &DataType,
424    row: usize,
425    crdt_mode: CrdtDecodeMode,
426) -> anyhow::Result<uni_common::Value> {
427    match data_type {
428        DataType::DateTime
429        | DataType::Timestamp
430        | DataType::Date
431        | DataType::Time
432        | DataType::Btic
433        | DataType::Bytes
434        // Sparse vectors decode to a full-fidelity `Value::SparseVector` via
435        // `arrow_to_value`; the `value_from_column` serde_json path would lose
436        // the type (an object would round-trip back as a `Map`).
437        | DataType::SparseVector { .. }
438        // Binary vectors likewise decode to a full-fidelity `Value::BinaryVector`
439        // via `arrow_to_value` (`FixedSizeList<UInt8>` → `BinaryVector`); the
440        // serde_json path has no arm and would return `Value::Null`.
441        | DataType::BinaryVector { .. }
442        // Point columns decode to the `Value::Map` shape `point(...)` produces via
443        // the struct reconstruction in `arrow_to_value`; the `value_from_column`
444        // scalar path has no Point arm and would return `Value::Null`.
445        | DataType::Point(_)
446        // Maps decode natively (full fidelity, CV-aware) via the unified
447        // `try_reconstruct_map` path inside `arrow_to_value`, which handles typed scalar
448        // value children, raw-`Bytes` (uni_raw_bytes-marked) children, and CV-encoded
449        // nested-value fallback children uniformly by runtime Arrow type.
450        | DataType::Map(_, _) => Ok(super::arrow_convert::arrow_to_value(
451            col,
452            row,
453            Some(data_type),
454        )),
455        _ => value_from_column(col, data_type, row, crdt_mode).map(uni_common::Value::from),
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use arrow_array::builder::{Int64Builder, StringBuilder};
463
464    #[test]
465    fn test_decode_string() {
466        let mut builder = StringBuilder::new();
467        builder.append_value("hello");
468        builder.append_value("world");
469        let array = builder.finish();
470
471        let val = value_from_column(&array, &DataType::String, 0, CrdtDecodeMode::Strict).unwrap();
472        assert_eq!(val, Value::String("hello".to_string()));
473
474        let val = value_from_column(&array, &DataType::String, 1, CrdtDecodeMode::Strict).unwrap();
475        assert_eq!(val, Value::String("world".to_string()));
476    }
477
478    #[test]
479    fn test_decode_int64() {
480        let mut builder = Int64Builder::new();
481        builder.append_value(42);
482        builder.append_value(-100);
483        let array = builder.finish();
484
485        let val = value_from_column(&array, &DataType::Int64, 0, CrdtDecodeMode::Strict).unwrap();
486        assert_eq!(val, serde_json::json!(42));
487
488        let val = value_from_column(&array, &DataType::Int64, 1, CrdtDecodeMode::Strict).unwrap();
489        assert_eq!(val, serde_json::json!(-100));
490    }
491
492    #[test]
493    fn test_decode_json() {
494        use arrow_array::builder::LargeBinaryBuilder;
495
496        // Encode JSON values as JSONB binary (matching the LargeBinary storage format)
497        let mut builder = LargeBinaryBuilder::new();
498
499        let obj_cv = {
500            let val: uni_common::Value = serde_json::json!({"key": "value"}).into();
501            uni_common::cypher_value_codec::encode(&val)
502        };
503        builder.append_value(&obj_cv);
504
505        let null_cv = uni_common::cypher_value_codec::encode(&uni_common::Value::Null);
506        builder.append_value(&null_cv);
507
508        let text_cv = uni_common::cypher_value_codec::encode(&uni_common::Value::String(
509            "plain text".to_string(),
510        ));
511        builder.append_value(&text_cv);
512
513        let array = builder.finish();
514
515        let val =
516            value_from_column(&array, &DataType::CypherValue, 0, CrdtDecodeMode::Strict).unwrap();
517        assert_eq!(val, serde_json::json!({"key": "value"}));
518
519        let val =
520            value_from_column(&array, &DataType::CypherValue, 1, CrdtDecodeMode::Strict).unwrap();
521        assert_eq!(val, Value::Null);
522
523        let val =
524            value_from_column(&array, &DataType::CypherValue, 2, CrdtDecodeMode::Strict).unwrap();
525        assert_eq!(val, Value::String("plain text".to_string()));
526    }
527
528    #[test]
529    fn test_decode_bool() {
530        use arrow_array::builder::BooleanBuilder;
531        let mut builder = BooleanBuilder::new();
532        builder.append_value(true);
533        builder.append_value(false);
534        let array = builder.finish();
535
536        let val = value_from_column(&array, &DataType::Bool, 0, CrdtDecodeMode::Strict).unwrap();
537        assert_eq!(val, serde_json::json!(true));
538
539        let val = value_from_column(&array, &DataType::Bool, 1, CrdtDecodeMode::Strict).unwrap();
540        assert_eq!(val, serde_json::json!(false));
541    }
542
543    #[test]
544    fn test_decode_float64() {
545        use arrow_array::builder::Float64Builder;
546        let mut builder = Float64Builder::new();
547        builder.append_value(3.25);
548        builder.append_value(-0.5);
549        let array = builder.finish();
550
551        let val = value_from_column(&array, &DataType::Float64, 0, CrdtDecodeMode::Strict).unwrap();
552        assert_eq!(val, serde_json::json!(3.25));
553
554        let val = value_from_column(&array, &DataType::Float64, 1, CrdtDecodeMode::Strict).unwrap();
555        assert_eq!(val, serde_json::json!(-0.5));
556    }
557
558    #[test]
559    fn test_decode_int32() {
560        use arrow_array::builder::Int32Builder;
561        let mut builder = Int32Builder::new();
562        builder.append_value(42);
563        builder.append_value(-1);
564        let array = builder.finish();
565
566        let val = value_from_column(&array, &DataType::Int32, 0, CrdtDecodeMode::Strict).unwrap();
567        assert_eq!(val, serde_json::json!(42));
568
569        let val = value_from_column(&array, &DataType::Int32, 1, CrdtDecodeMode::Strict).unwrap();
570        assert_eq!(val, serde_json::json!(-1));
571    }
572
573    #[test]
574    fn test_decode_float32() {
575        use arrow_array::builder::Float32Builder;
576        let mut builder = Float32Builder::new();
577        builder.append_value(1.5);
578        let array = builder.finish();
579
580        let val = value_from_column(&array, &DataType::Float32, 0, CrdtDecodeMode::Strict).unwrap();
581        // Float32 has limited precision so compare approximately
582        let f = val.as_f64().unwrap();
583        assert!((f - 1.5).abs() < 0.001);
584    }
585
586    #[test]
587    fn test_decode_vector() {
588        use arrow_array::builder::{FixedSizeListBuilder, Float32Builder};
589        let values_builder = Float32Builder::new();
590        let mut builder = FixedSizeListBuilder::new(values_builder, 3);
591        builder.values().append_value(1.0);
592        builder.values().append_value(2.0);
593        builder.values().append_value(3.0);
594        builder.append(true);
595        let array = builder.finish();
596
597        let val = value_from_column(
598            &array,
599            &DataType::Vector { dimensions: 3 },
600            0,
601            CrdtDecodeMode::Strict,
602        )
603        .unwrap();
604        assert_eq!(val, serde_json::json!([1.0, 2.0, 3.0]));
605    }
606
607    #[test]
608    fn test_decode_date() {
609        use arrow_array::builder::Date32Builder;
610        let mut builder = Date32Builder::new();
611        // 2021-01-01 = 18628 days since epoch
612        builder.append_value(18628);
613        let array = builder.finish();
614
615        let val = value_from_column(&array, &DataType::Date, 0, CrdtDecodeMode::Strict).unwrap();
616        assert_eq!(val, Value::String("2021-01-01".to_string()));
617    }
618
619    #[test]
620    fn test_decode_date_null() {
621        use arrow_array::builder::Date32Builder;
622        let mut builder = Date32Builder::new();
623        builder.append_null();
624        let array = builder.finish();
625
626        let val = value_from_column(&array, &DataType::Date, 0, CrdtDecodeMode::Strict).unwrap();
627        assert_eq!(val, Value::Null);
628    }
629
630    #[test]
631    fn test_decode_list_of_strings() {
632        use arrow_array::builder::{ListBuilder, StringBuilder};
633        let values_builder = StringBuilder::new();
634        let mut builder = ListBuilder::new(values_builder);
635        builder.values().append_value("a");
636        builder.values().append_value("b");
637        builder.values().append_value("c");
638        builder.append(true);
639        let array = builder.finish();
640
641        let val = value_from_column(
642            &array,
643            &DataType::List(Box::new(DataType::String)),
644            0,
645            CrdtDecodeMode::Strict,
646        )
647        .unwrap();
648        assert_eq!(val, serde_json::json!(["a", "b", "c"]));
649    }
650
651    #[test]
652    fn test_decode_list_of_ints() {
653        use arrow_array::builder::{Int64Builder, ListBuilder};
654        let values_builder = Int64Builder::new();
655        let mut builder = ListBuilder::new(values_builder);
656        builder.values().append_value(1);
657        builder.values().append_value(2);
658        builder.values().append_value(3);
659        builder.append(true);
660        let array = builder.finish();
661
662        let val = value_from_column(
663            &array,
664            &DataType::List(Box::new(DataType::Int64)),
665            0,
666            CrdtDecodeMode::Strict,
667        )
668        .unwrap();
669        assert_eq!(val, serde_json::json!([1, 2, 3]));
670    }
671
672    #[test]
673    fn test_decode_list_null() {
674        use arrow_array::builder::{Int64Builder, ListBuilder};
675        let values_builder = Int64Builder::new();
676        let mut builder = ListBuilder::new(values_builder);
677        builder.append_null();
678        let array = builder.finish();
679
680        let val = value_from_column(
681            &array,
682            &DataType::List(Box::new(DataType::Int64)),
683            0,
684            CrdtDecodeMode::Strict,
685        )
686        .unwrap();
687        assert_eq!(val, Value::Null);
688    }
689
690    #[test]
691    fn test_decode_column_value_non_struct_point_is_null() {
692        // A physically-wrong column (String) declared as Point must reconstruct to
693        // Null rather than panic — the struct downcast fails and falls through.
694        use uni_common::core::schema::PointType;
695        let mut builder = StringBuilder::new();
696        builder.append_value("test");
697        let array = builder.finish();
698
699        let val = decode_column_value(
700            &array,
701            &DataType::Point(PointType::Geographic),
702            0,
703            CrdtDecodeMode::Strict,
704        )
705        .unwrap();
706        assert_eq!(val, uni_common::Value::Null);
707    }
708
709    #[test]
710    fn test_point_struct_roundtrip() {
711        // A geographic Point encoded via the storage builder must decode back to
712        // the `Value::Map` shape `point(...)` produces (previously it errored on
713        // write and decoded to Null).
714        use std::collections::HashMap;
715        use uni_common::core::schema::PointType;
716
717        let point = uni_common::Value::Map(HashMap::from([
718            (
719                "type".to_string(),
720                uni_common::Value::String("Point".into()),
721            ),
722            ("crs".to_string(), uni_common::Value::String("WGS84".into())),
723            ("latitude".to_string(), uni_common::Value::Float(51.5)),
724            ("longitude".to_string(), uni_common::Value::Float(-0.12)),
725        ]));
726
727        let arr = crate::storage::arrow_convert::values_to_point_struct_array(
728            &[point.clone(), uni_common::Value::Null],
729            PointType::Geographic,
730        );
731
732        let decoded = decode_column_value(
733            &arr,
734            &DataType::Point(PointType::Geographic),
735            0,
736            CrdtDecodeMode::Strict,
737        )
738        .unwrap();
739        assert_eq!(decoded, point);
740
741        // Row 1 was Null → the struct slot is null → decodes back to Null.
742        let decoded_null = decode_column_value(
743            &arr,
744            &DataType::Point(PointType::Geographic),
745            1,
746            CrdtDecodeMode::Strict,
747        )
748        .unwrap();
749        assert_eq!(decoded_null, uni_common::Value::Null);
750    }
751}