Skip to main content

snowflake_connector_rs/result_table/
dynamic.rs

1use std::{any::type_name, fmt, mem, result::Result as StdResult, sync::Arc};
2
3use base64::Engine as _;
4use bytes::Bytes;
5use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
6
7use crate::{
8    error::{
9        CellDecodeError, DuplicateColumnNameError, InvalidColumnIndexError, PlanBuildResult,
10        RowDecodeResult, SchemaError,
11    },
12    result_table::{
13        CellConversionError, CellDecodeResult, FromRow, RowPlanContext,
14        cell::CellRef,
15        decode::{
16            Vector, decode_hex, decode_json_payload, parse_time_seconds_and_nanos,
17            parse_timestamp_epoch, parse_timestamp_tz_with_offset, parse_vector_f32_payload,
18            parse_vector_i32_payload,
19        },
20        row::RowRef,
21        schema::{ColumnType, Schema},
22    },
23};
24
25/// Result-side representation of a Snowflake `NUMBER` / `DECIMAL` cell with non-zero scale.
26///
27/// Snowflake decimals can carry up to 38 digits of precision, which does not fit in any native Rust numeric type.
28/// `DecimalValue` therefore preserves the value as the exact string Snowflake delivered.
29/// The column's declared precision and scale are available on its [`ColumnType`](crate::ColumnType).
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct DecimalValue {
32    raw: Box<str>,
33}
34
35impl DecimalValue {
36    pub(crate) fn new(raw: impl Into<Box<str>>) -> Self {
37        Self { raw: raw.into() }
38    }
39
40    /// Borrow the underlying decimal string.
41    pub fn raw(&self) -> &str {
42        &self.raw
43    }
44}
45
46impl fmt::Display for DecimalValue {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(&self.raw)
49    }
50}
51
52/// Result-side representation of a Snowflake `BINARY` cell.
53#[derive(Clone, Debug, PartialEq, Eq, Hash)]
54pub struct BinaryValue(Bytes);
55
56impl BinaryValue {
57    pub(crate) fn new(bytes: impl Into<Bytes>) -> Self {
58        Self(bytes.into())
59    }
60
61    /// Borrow the decoded bytes.
62    pub fn as_bytes(&self) -> &[u8] {
63        &self.0
64    }
65
66    /// Consume the wrapper, returning the underlying [`bytes::Bytes`].
67    pub fn into_bytes(self) -> Bytes {
68        self.0
69    }
70}
71
72impl From<Bytes> for BinaryValue {
73    fn from(bytes: Bytes) -> Self {
74        Self(bytes)
75    }
76}
77
78impl From<Vec<u8>> for BinaryValue {
79    fn from(bytes: Vec<u8>) -> Self {
80        Self(bytes.into())
81    }
82}
83
84impl AsRef<[u8]> for BinaryValue {
85    fn as_ref(&self) -> &[u8] {
86        self.as_bytes()
87    }
88}
89
90/// Decoded Snowflake `VECTOR` value on the dynamic-typing path.
91///
92/// Snowflake result metadata does not report a `VECTOR`'s element type, so the payload determines the variant:
93/// an integer-only payload decodes as [`Int`](VectorValue::Int), and a payload carrying a decimal point or a
94/// lowercase `inf` / `-inf` / `nan` token decodes as [`Float`](VectorValue::Float). Snowflake cannot produce an empty
95/// `VECTOR`, so an empty payload is rejected rather than being treated as an ambiguous element type.
96#[derive(Clone, Debug, PartialEq)]
97pub enum VectorValue {
98    /// `VECTOR(INT)` — 32-bit signed integer elements.
99    Int(Vector<i32>),
100    /// `VECTOR(FLOAT)` — 32-bit IEEE 754 float elements.
101    Float(Vector<f32>),
102}
103
104impl VectorValue {
105    /// Convert this vector into a [`serde_json::Value`] array.
106    ///
107    /// `Int` elements become JSON numbers. Finite `Float` elements become JSON numbers; the non-finite `f32` values
108    /// have no JSON representation and fall back to the strings `"inf"`, `"-inf"`, and `"nan"`, matching Snowflake's
109    /// lowercase `VECTOR` wire token spelling.
110    fn into_json_value(self) -> serde_json::Value {
111        match self {
112            VectorValue::Int(vector) => {
113                serde_json::Value::Array(vector.into_vec().into_iter().map(Into::into).collect())
114            }
115            VectorValue::Float(vector) => serde_json::Value::Array(
116                vector
117                    .into_vec()
118                    .into_iter()
119                    .map(f32_element_to_json_value)
120                    .collect(),
121            ),
122        }
123    }
124}
125
126fn f32_element_to_json_value(value: f32) -> serde_json::Value {
127    if value.is_nan() {
128        serde_json::Value::String("nan".to_string())
129    } else if value.is_infinite() {
130        let token = if value.is_sign_positive() {
131            "inf"
132        } else {
133            "-inf"
134        };
135        serde_json::Value::String(token.to_string())
136    } else {
137        match serde_json::Number::from_f64(f64::from(value)) {
138            Some(number) => serde_json::Value::Number(number),
139            None => serde_json::Value::String(value.to_string()),
140        }
141    }
142}
143
144/// Decoded value of a single cell on the dynamic-typing path.
145///
146/// Each variant corresponds to a Snowflake result type.
147///
148/// # Example
149///
150/// ```
151/// use snowflake_connector_rs::CellValue;
152///
153/// let value = CellValue::Integer(42);
154/// match value {
155///     CellValue::Integer(n) => assert_eq!(n, 42),
156///     CellValue::Null => unreachable!(),
157///     other => panic!("unexpected variant: {other:?}"),
158/// }
159/// ```
160#[derive(Clone, Debug, PartialEq)]
161#[non_exhaustive]
162pub enum CellValue {
163    /// SQL `NULL`.
164    Null,
165    /// `BOOLEAN`.
166    Boolean(bool),
167    /// Integer-shaped `NUMBER` (scale 0) that fits in `i128`.
168    Integer(i128),
169    /// `FLOAT` / `DOUBLE` / `REAL`.
170    Float(f64),
171    /// `NUMBER` with non-zero scale — preserved as text via [`DecimalValue`].
172    Decimal(DecimalValue),
173    /// `TEXT` / `VARCHAR` / `CHAR` / `STRING`.
174    String(String),
175    /// `DATE`.
176    Date(NaiveDate),
177    /// `TIME`.
178    Time(NaiveTime),
179    /// `TIMESTAMP_NTZ`.
180    TimestampNtz(NaiveDateTime),
181    /// `TIMESTAMP_LTZ`, anchored to UTC.
182    TimestampLtz(DateTime<Utc>),
183    /// `TIMESTAMP_TZ`, with the wire-reported offset preserved.
184    TimestampTz(DateTime<FixedOffset>),
185    /// `VARIANT` / `OBJECT` / `ARRAY` payloads represented as JSON.
186    ///
187    /// Snowflake non-finite float tokens (`Infinity`, `-Infinity`, `NaN`) are rejected during decoding because JSON has
188    /// no representation for them.
189    Json(serde_json::Value),
190    /// `BINARY`, decoded from Snowflake's hex representation.
191    Binary(BinaryValue),
192    /// `VECTOR`, with the element type inferred from the payload.
193    Vector(VectorValue),
194}
195
196impl CellValue {
197    /// Returns `true` for [`CellValue::Null`].
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use snowflake_connector_rs::CellValue;
203    ///
204    /// assert!(CellValue::Null.is_null());
205    /// assert!(!CellValue::Integer(0).is_null());
206    /// ```
207    pub fn is_null(&self) -> bool {
208        matches!(self, CellValue::Null)
209    }
210}
211
212/// A row decoded into the dynamic [`CellValue`] vocabulary.
213///
214/// Values are stored in column order and can be accessed by zero-based index or by raw column name.
215#[derive(Clone, Debug, PartialEq)]
216pub struct DynamicRow {
217    schema: Arc<Schema>,
218    values: Box<[CellValue]>,
219}
220
221impl DynamicRow {
222    /// Borrow the shared schema this row was decoded against.
223    pub fn schema(&self) -> &Schema {
224        &self.schema
225    }
226
227    /// Borrow all decoded values in column order.
228    pub fn values(&self) -> &[CellValue] {
229        &self.values
230    }
231
232    /// Borrow the value of a column resolved by raw label (case-sensitive).
233    ///
234    /// # Errors
235    ///
236    /// Returns [`SchemaError::MissingColumn`] when no column carries the name, or [`SchemaError::AmbiguousColumn`] when several do.
237    pub fn value(&self, name: &str) -> StdResult<&CellValue, SchemaError> {
238        let idx = self.schema.column_index(name)?;
239        self.value_at(idx)
240    }
241
242    /// Borrow the value at a zero-based column index.
243    ///
244    /// # Errors
245    ///
246    /// Returns [`SchemaError::InvalidColumnIndex`] when `index` is out of range for this row's schema.
247    pub fn value_at(&self, index: usize) -> StdResult<&CellValue, SchemaError> {
248        self.values.get(index).ok_or_else(|| {
249            SchemaError::InvalidColumnIndex(InvalidColumnIndexError::new(index, self.schema.len()))
250        })
251    }
252
253    /// Move a value out of the row by raw label, replacing the slot with
254    /// [`CellValue::Null`].
255    ///
256    /// # Errors
257    ///
258    /// Returns [`SchemaError::MissingColumn`] when no column carries the name, or [`SchemaError::AmbiguousColumn`] when several do.
259    pub fn take(&mut self, name: &str) -> StdResult<CellValue, SchemaError> {
260        let idx = self.schema.column_index(name)?;
261        self.take_at(idx)
262    }
263
264    /// Move a value out of the row by zero-based column index, replacing the slot
265    /// with [`CellValue::Null`].
266    ///
267    /// # Errors
268    ///
269    /// Returns [`SchemaError::InvalidColumnIndex`] when `index` is out of range for this row's schema.
270    pub fn take_at(&mut self, index: usize) -> StdResult<CellValue, SchemaError> {
271        let column_count = self.schema.len();
272        let slot = self.values.get_mut(index).ok_or_else(|| {
273            SchemaError::InvalidColumnIndex(InvalidColumnIndexError::new(index, column_count))
274        })?;
275
276        Ok(mem::replace(slot, CellValue::Null))
277    }
278
279    /// Consume the row, returning the shared schema and the decoded values in column order.
280    pub fn into_parts(self) -> (Arc<Schema>, Box<[CellValue]>) {
281        (self.schema, self.values)
282    }
283
284    /// Consume the row into a `serde_json::Map` keyed by raw column label.
285    ///
286    /// # Errors
287    ///
288    /// Returns [`SchemaError::DuplicateColumnName`] when two columns share the same raw label — JSON object keys must be unique.
289    pub fn into_json_object(
290        self,
291    ) -> StdResult<serde_json::Map<String, serde_json::Value>, SchemaError> {
292        let DynamicRow { schema, values } = self;
293
294        let mut map = serde_json::Map::new();
295        for (col, value) in schema.columns().iter().zip(values.into_vec()) {
296            if map.contains_key(col.name()) {
297                return Err(SchemaError::DuplicateColumnName(
298                    DuplicateColumnNameError::new(col.name()),
299                ));
300            }
301            map.insert(col.name().to_string(), value.into_json_value());
302        }
303
304        Ok(map)
305    }
306}
307
308impl CellValue {
309    /// Convert this value into a [`serde_json::Value`].
310    ///
311    /// Lossy in the cases where Snowflake's value range exceeds JSON's numeric range: `i128` integers outside `i64`/`u64` range and
312    /// non-finite floats fall back to string. `Decimal` values render as their original decimal text. Dates/times/timestamps render
313    /// in their canonical string form, and `Binary` is base64-encoded.
314    pub fn into_json_value(self) -> serde_json::Value {
315        match self {
316            CellValue::Null => serde_json::Value::Null,
317            CellValue::Boolean(b) => serde_json::Value::Bool(b),
318            CellValue::Integer(i) => match serde_json::Number::from_i128(i) {
319                Some(n) => serde_json::Value::Number(n),
320                None => serde_json::Value::String(i.to_string()),
321            },
322            CellValue::Float(f) => match serde_json::Number::from_f64(f) {
323                Some(n) => serde_json::Value::Number(n),
324                None => serde_json::Value::String(f.to_string()),
325            },
326            CellValue::Decimal(d) => serde_json::Value::String(d.raw().to_string()),
327            CellValue::String(s) => serde_json::Value::String(s),
328            CellValue::Date(d) => serde_json::Value::String(d.format("%Y-%m-%d").to_string()),
329            CellValue::Time(t) => serde_json::Value::String(t.format("%H:%M:%S%.f").to_string()),
330            CellValue::TimestampNtz(dt) => {
331                serde_json::Value::String(dt.format("%Y-%m-%dT%H:%M:%S%.f").to_string())
332            }
333            CellValue::TimestampLtz(dt) => serde_json::Value::String(dt.to_rfc3339()),
334            CellValue::TimestampTz(dt) => serde_json::Value::String(dt.to_rfc3339()),
335            CellValue::Json(v) => v,
336            CellValue::Binary(bytes) => serde_json::Value::String(
337                base64::engine::general_purpose::STANDARD.encode(bytes.as_bytes()),
338            ),
339            CellValue::Vector(vector) => vector.into_json_value(),
340        }
341    }
342}
343
344impl FromRow for DynamicRow {
345    type Plan = Arc<Schema>;
346
347    fn build_plan(ctx: RowPlanContext<'_>) -> PlanBuildResult<Self::Plan> {
348        Ok(ctx.shared_schema())
349    }
350
351    fn from_row_with_plan(row: RowRef<'_>, plan: &Self::Plan) -> RowDecodeResult<Self> {
352        let mut values = Vec::with_capacity(plan.len());
353        for (offset, col) in plan.columns().iter().enumerate() {
354            let cell = row.cell_at_offset(col, offset);
355            values.push(decode_dynamic(cell).map_err(|issue| {
356                CellDecodeError::new(
357                    cell.row_index(),
358                    cell.column().index(),
359                    cell.column().name(),
360                    type_name::<CellValue>(),
361                    cell.column().ty().clone(),
362                    cell.raw(),
363                    issue,
364                )
365            })?);
366        }
367
368        Ok(DynamicRow {
369            schema: Arc::clone(plan),
370            values: values.into_boxed_slice(),
371        })
372    }
373}
374
375fn decode_dynamic(cell: CellRef<'_>) -> CellDecodeResult<CellValue> {
376    if cell.is_null() {
377        return Ok(CellValue::Null);
378    }
379
380    let raw = cell.raw().expect("non-null checked above");
381    let ty = cell.column().ty();
382
383    match ty {
384        ColumnType::Boolean => {
385            if raw == "1" || raw.eq_ignore_ascii_case("true") {
386                Ok(CellValue::Boolean(true))
387            } else if raw == "0" || raw.eq_ignore_ascii_case("false") {
388                Ok(CellValue::Boolean(false))
389            } else {
390                Err(CellConversionError::builder(format!("'{raw}' is not bool")).build())
391            }
392        }
393        ColumnType::Fixed { scale, .. } => {
394            if scale.unwrap_or(0) == 0 {
395                if let Ok(v) = raw.parse::<i128>() {
396                    return Ok(CellValue::Integer(v));
397                }
398            }
399            Ok(CellValue::Decimal(DecimalValue::new(raw)))
400        }
401        ColumnType::Real => raw.parse::<f64>().map(CellValue::Float).map_err(|e| {
402            CellConversionError::builder(format!("parse error: {e}"))
403                .source(e)
404                .build()
405        }),
406        ColumnType::Text { .. } => Ok(CellValue::String(raw.to_string())),
407        ColumnType::Date => {
408            let days = raw.parse::<i64>().map_err(|e| {
409                CellConversionError::builder(format!("'{raw}' not Date"))
410                    .source(e)
411                    .build()
412            })?;
413            let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("epoch");
414            let date = if days >= 0 {
415                epoch.checked_add_days(chrono::Days::new(days as u64))
416            } else {
417                epoch.checked_sub_days(chrono::Days::new(days.unsigned_abs()))
418            }
419            .ok_or_else(|| CellConversionError::builder(format!("'{raw}' not Date")).build())?;
420            Ok(CellValue::Date(date))
421        }
422        ColumnType::Time { scale } => {
423            let scale = match scale {
424                None => 0usize,
425                Some(s) if (0..=9).contains(s) => *s as usize,
426                Some(s) => {
427                    return Err(
428                        CellConversionError::builder(format!("invalid time scale: {s}")).build(),
429                    );
430                }
431            };
432            let (secs, nsec) = parse_time_seconds_and_nanos(raw, scale)
433                .map_err(|m| CellConversionError::builder(m).build())?;
434            let t = NaiveTime::from_num_seconds_from_midnight_opt(secs, nsec).ok_or_else(|| {
435                CellConversionError::builder(format!("invalid time: {raw}")).build()
436            })?;
437            Ok(CellValue::Time(t))
438        }
439        ColumnType::TimestampNtz { scale } => {
440            let scale = scale.unwrap_or(9);
441            let dt = parse_timestamp_epoch(raw, scale)
442                .map_err(|m| CellConversionError::builder(m).build())?;
443            Ok(CellValue::TimestampNtz(dt.naive_utc()))
444        }
445        ColumnType::TimestampLtz { scale } => {
446            let scale = scale.unwrap_or(9);
447            let dt = parse_timestamp_epoch(raw, scale)
448                .map_err(|m| CellConversionError::builder(m).build())?;
449            Ok(CellValue::TimestampLtz(dt))
450        }
451        ColumnType::TimestampTz { scale } => {
452            let scale = scale.unwrap_or(9);
453            let dt = parse_timestamp_tz_with_offset(raw, scale)
454                .map_err(|m| CellConversionError::builder(m).build())?;
455            Ok(CellValue::TimestampTz(dt))
456        }
457        ColumnType::Variant | ColumnType::Object | ColumnType::Array => {
458            Ok(CellValue::Json(decode_json_payload(raw)?))
459        }
460        ColumnType::Binary { .. } => {
461            let bytes = decode_hex(raw).map_err(|m| CellConversionError::builder(m).build())?;
462            Ok(CellValue::Binary(BinaryValue::new(bytes)))
463        }
464        ColumnType::Vector => decode_vector_dynamic(raw).map(CellValue::Vector),
465        ColumnType::Geography | ColumnType::Geometry | ColumnType::Unknown { .. } => {
466            Ok(CellValue::String(raw.to_string()))
467        }
468    }
469}
470
471/// Decode a `VECTOR` payload, inferring the element type.
472///
473/// A `VECTOR(INT)` payload parses as [`VectorValue::Int`]. If integer parsing fails, the float parser handles decimal
474/// and lowercase non-finite tokens and returns [`VectorValue::Float`].
475fn decode_vector_dynamic(raw: &str) -> CellDecodeResult<VectorValue> {
476    if let Ok(ints) = parse_vector_i32_payload(raw) {
477        return Ok(VectorValue::Int(Vector::from_vec(ints)));
478    }
479    parse_vector_f32_payload(raw)
480        .map(|floats| VectorValue::Float(Vector::from_vec(floats)))
481        .map_err(|m| CellConversionError::builder(m).build())
482}
483
484#[cfg(test)]
485mod tests {
486    use std::ptr;
487
488    use super::*;
489    use crate::result_table::{
490        ColumnType,
491        test_data::{make_result_table_from_rows, make_schema},
492    };
493
494    fn one_cell_row(ty: ColumnType, value: &str) -> DynamicRow {
495        let schema = make_schema(vec![("PAYLOAD".to_string(), ty, true)]);
496        let table =
497            make_result_table_from_rows(schema, vec![vec![Some(value.to_string())]]).unwrap();
498        table.dynamic_rows().unwrap().next().unwrap().unwrap()
499    }
500
501    #[test]
502    fn dynamic_row_keeps_text_cells_as_strings() {
503        for value in [r#"{"a":1}"#, "plain text"] {
504            let row = one_cell_row(ColumnType::Text { length: None }, value);
505            match row.value("PAYLOAD").unwrap() {
506                CellValue::String(actual) => assert_eq!(actual, value),
507                other => panic!("expected String, got {other:?}"),
508            }
509        }
510    }
511
512    #[test]
513    fn dynamic_row_decodes_variant_cells_as_json() {
514        let row = one_cell_row(ColumnType::Variant, r#"{"a":1}"#);
515        match row.value("PAYLOAD").unwrap() {
516            CellValue::Json(value) => assert_eq!(value["a"], 1),
517            other => panic!("expected Json, got {other:?}"),
518        }
519    }
520
521    #[test]
522    fn dynamic_row_decode_failure_reports_contextual_error() {
523        let schema = make_schema(vec![("PAYLOAD".to_string(), ColumnType::Boolean, false)]);
524        let table =
525            make_result_table_from_rows(schema, vec![vec![Some("maybe".to_string())]]).unwrap();
526
527        let err = table.dynamic_rows().unwrap().next().unwrap().unwrap_err();
528        let decode = err
529            .as_cell_decode_error()
530            .expect("dynamic row decode should yield CellDecodeError");
531
532        assert_eq!(decode.row_index(), 0);
533        assert_eq!(decode.column_name(), "PAYLOAD");
534        assert_eq!(decode.conversion_error().reason(), "'maybe' is not bool");
535        assert!(decode.target_type_name().ends_with("CellValue"));
536        assert_eq!(decode.raw_value_preview(), Some("maybe"));
537    }
538
539    #[test]
540    fn dynamic_row_decodes_integer_vector() {
541        let row = one_cell_row(ColumnType::Vector, "[1,2,3]");
542        match row.value("PAYLOAD").unwrap() {
543            CellValue::Vector(VectorValue::Int(vector)) => {
544                assert_eq!(vector.as_slice(), &[1, 2, 3]);
545            }
546            other => panic!("expected Vector(Int), got {other:?}"),
547        }
548    }
549
550    #[test]
551    fn dynamic_row_decodes_float_vector_from_decimal_payload() {
552        let row = one_cell_row(ColumnType::Vector, "[1.500000,-2.250000,0.000000]");
553        match row.value("PAYLOAD").unwrap() {
554            CellValue::Vector(VectorValue::Float(vector)) => {
555                assert_eq!(vector.as_slice(), &[1.5f32, -2.25, 0.0]);
556            }
557            other => panic!("expected Vector(Float), got {other:?}"),
558        }
559    }
560
561    #[test]
562    fn dynamic_row_decodes_float_vector_from_non_finite_payload() {
563        let row = one_cell_row(ColumnType::Vector, "[1.000000,nan,-inf]");
564        match row.value("PAYLOAD").unwrap() {
565            CellValue::Vector(VectorValue::Float(vector)) => {
566                let slice = vector.as_slice();
567                assert_eq!(slice[0], 1.0f32);
568                assert!(slice[1].is_nan());
569                assert!(slice[2].is_infinite() && slice[2].is_sign_negative());
570            }
571            other => panic!("expected Vector(Float), got {other:?}"),
572        }
573    }
574
575    #[test]
576    fn dynamic_row_malformed_vector_reports_contextual_error() {
577        let schema = make_schema(vec![("PAYLOAD".to_string(), ColumnType::Vector, true)]);
578        let table =
579            make_result_table_from_rows(schema, vec![vec![Some("[abc]".to_string())]]).unwrap();
580
581        let err = table.dynamic_rows().unwrap().next().unwrap().unwrap_err();
582        let decode = err
583            .as_cell_decode_error()
584            .expect("malformed vector should yield CellDecodeError");
585        assert_eq!(decode.column_name(), "PAYLOAD");
586        assert!(
587            decode
588                .conversion_error()
589                .reason()
590                .contains("invalid VECTOR(FLOAT) element"),
591            "actual: {}",
592            decode.conversion_error().reason()
593        );
594    }
595
596    #[test]
597    fn float_vector_into_json_value_stringifies_non_finite_elements() {
598        let value = CellValue::Vector(VectorValue::Float(Vector::from_vec(vec![
599            1.5,
600            f32::INFINITY,
601            f32::NEG_INFINITY,
602            f32::NAN,
603        ])));
604        assert_eq!(
605            value.into_json_value(),
606            serde_json::json!([1.5, "inf", "-inf", "nan"])
607        );
608    }
609
610    #[test]
611    fn integer_vector_into_json_value_is_number_array() {
612        let value = CellValue::Vector(VectorValue::Int(Vector::from_vec(vec![1, -2, 3])));
613        assert_eq!(value.into_json_value(), serde_json::json!([1, -2, 3]));
614    }
615
616    #[test]
617    fn dynamic_row_value_at_rejects_invalid_indices() {
618        let row = one_cell_row(ColumnType::Text { length: None }, "value");
619        let index = 1;
620        assert!(matches!(
621            row.value_at(index),
622            Err(SchemaError::InvalidColumnIndex(error))
623                if error.index() == index && error.column_count() == 1
624        ));
625    }
626
627    #[test]
628    fn dynamic_row_value_returns_exact_label_match() {
629        let schema = make_schema(vec![
630            (
631                "ID".to_string(),
632                ColumnType::Fixed {
633                    precision: None,
634                    scale: Some(0),
635                },
636                false,
637            ),
638            (
639                "id".to_string(),
640                ColumnType::Fixed {
641                    precision: None,
642                    scale: Some(0),
643                },
644                false,
645            ),
646        ]);
647        let table = make_result_table_from_rows(
648            schema,
649            vec![vec![Some("1".to_string()), Some("2".to_string())]],
650        )
651        .unwrap();
652        let row = table.dynamic_rows().unwrap().next().unwrap().unwrap();
653
654        assert_eq!(row.value("ID").unwrap(), &CellValue::Integer(1));
655        assert_eq!(row.value("id").unwrap(), &CellValue::Integer(2));
656    }
657
658    #[test]
659    fn dynamic_row_take_at_replaces_slots_with_null() {
660        let mut row = one_cell_row(ColumnType::Text { length: None }, "value");
661        let index = row.schema().column_index("PAYLOAD").unwrap();
662
663        assert_eq!(
664            row.take_at(index).unwrap(),
665            CellValue::String("value".into())
666        );
667        assert_eq!(row.value_at(index).unwrap(), &CellValue::Null);
668        assert_eq!(row.take_at(index).unwrap(), CellValue::Null);
669    }
670
671    #[test]
672    fn dynamic_row_take_at_rejects_invalid_indices() {
673        let mut row = one_cell_row(ColumnType::Text { length: None }, "value");
674        let index = 1;
675        assert!(matches!(
676            row.take_at(index),
677            Err(SchemaError::InvalidColumnIndex(error))
678                if error.index() == index && error.column_count() == 1
679        ));
680    }
681
682    #[test]
683    fn dynamic_row_take_resolves_label_and_replaces_slot() {
684        let mut row = one_cell_row(ColumnType::Text { length: None }, "value");
685
686        assert_eq!(
687            row.take("PAYLOAD").unwrap(),
688            CellValue::String("value".into())
689        );
690        assert_eq!(row.value("PAYLOAD").unwrap(), &CellValue::Null,);
691        assert_eq!(row.take("PAYLOAD").unwrap(), CellValue::Null);
692    }
693
694    #[test]
695    fn dynamic_row_take_reports_missing_column_for_unknown_label() {
696        let mut row = one_cell_row(ColumnType::Text { length: None }, "value");
697        assert!(matches!(
698            row.take("missing"),
699            Err(SchemaError::MissingColumn(error)) if error.name() == "missing"
700        ));
701    }
702
703    #[test]
704    fn dynamic_row_into_parts_preserves_schema_and_supports_walk() {
705        let schema = make_schema(vec![
706            (
707                "ID".to_string(),
708                ColumnType::Fixed {
709                    precision: None,
710                    scale: Some(0),
711                },
712                false,
713            ),
714            (
715                "PAYLOAD".to_string(),
716                ColumnType::Text { length: None },
717                true,
718            ),
719        ]);
720        let table = make_result_table_from_rows(
721            schema,
722            vec![vec![Some("1".to_string()), Some("value".to_string())]],
723        )
724        .unwrap();
725        let row = table.dynamic_rows().unwrap().next().unwrap().unwrap();
726
727        let (schema, values) = row.into_parts();
728        assert!(ptr::eq(schema.as_ref(), table.schema()));
729        let walked = schema
730            .columns()
731            .iter()
732            .zip(values.into_vec())
733            .map(|(column, value)| (column.name().to_string(), value))
734            .collect::<Vec<_>>();
735        assert_eq!(
736            walked,
737            vec![
738                ("ID".to_string(), CellValue::Integer(1)),
739                (
740                    "PAYLOAD".to_string(),
741                    CellValue::String("value".to_string())
742                ),
743            ]
744        );
745    }
746
747    #[test]
748    fn dynamic_row_into_json_object_rejects_duplicate_labels() {
749        let schema = make_schema(vec![
750            (
751                "id".to_string(),
752                ColumnType::Fixed {
753                    precision: None,
754                    scale: Some(0),
755                },
756                false,
757            ),
758            (
759                "id".to_string(),
760                ColumnType::Fixed {
761                    precision: None,
762                    scale: Some(0),
763                },
764                false,
765            ),
766        ]);
767        let table = make_result_table_from_rows(
768            schema,
769            vec![vec![Some("1".to_string()), Some("2".to_string())]],
770        )
771        .unwrap();
772        let row = table.dynamic_rows().unwrap().next().unwrap().unwrap();
773
774        assert!(matches!(
775            row.into_json_object(),
776            Err(SchemaError::DuplicateColumnName(error)) if error.name() == "id"
777        ));
778    }
779
780    #[test]
781    fn dynamic_row_paths_share_the_table_schema() {
782        let schema = make_schema(vec![(
783            "PAYLOAD".to_string(),
784            ColumnType::Text { length: None },
785            true,
786        )]);
787        let table =
788            make_result_table_from_rows(schema, vec![vec![Some("value".to_string())]]).unwrap();
789
790        let generic = table.rows::<DynamicRow>().unwrap().next().unwrap().unwrap();
791        let alias = table.dynamic_rows().unwrap().next().unwrap().unwrap();
792
793        assert!(ptr::eq(generic.schema(), table.schema()));
794        assert!(ptr::eq(alias.schema(), table.schema()));
795        assert!(ptr::eq(generic.schema(), alias.schema()));
796    }
797}