Skip to main content

datafusion_odata/
atom.rs

1use std::sync::Arc;
2
3use chrono::{DateTime, Utc};
4use datafusion::arrow::{
5    array::{Array, AsArray, PrimitiveArray, RecordBatch},
6    datatypes::{DataType, *},
7};
8use quick_xml::events::*;
9
10use crate::{
11    context::{CollectionContext, OnUnsupported},
12    error::{ODataError, UnsupportedDataType, UnsupportedNetProtocol},
13    metadata::to_edm_type,
14};
15
16// TODO: Replace with an interface similar to Encoder
17// See: https://github.com/kamu-data/kamu-cli/blob/385bbf56036d4485efdf54bf458a95bfba048b2b/src/utils/data-utils/src/data/format/traits.rs#L69
18struct Edm {
19    typ: String,
20    tag: String,
21}
22
23impl Edm {
24    fn from_field(field: &Arc<Field>) -> Result<Self, UnsupportedDataType> {
25        // TODO: Escape field name
26        let tag = format!("d:{}", field.name());
27        let typ = to_edm_type(field.data_type())?.to_string();
28        Ok(Self { typ, tag })
29    }
30}
31
32fn to_edms(
33    schema: &Schema,
34    key_column: &str,
35    on_unsupported: OnUnsupported,
36) -> Result<(Vec<(Edm, usize)>, usize), UnsupportedDataType> {
37    let mut edms = Vec::new();
38    let mut key_edm_index = usize::MAX;
39
40    for (index, field) in schema.fields().iter().enumerate() {
41        if field.name() == key_column {
42            key_edm_index = index;
43            continue;
44        }
45        let edm = match Edm::from_field(field) {
46            Ok(typ) => typ,
47            Err(err) => match on_unsupported {
48                OnUnsupported::Error => return Err(err),
49                OnUnsupported::Warn => {
50                    tracing::warn!(
51                        field = field.name(),
52                        error = %err,
53                        error_dbg = ?err,
54                        "Unsupported field type - skipping",
55                    );
56                    continue;
57                }
58            },
59        };
60
61        edms.push((edm, index));
62    }
63    Ok((edms, key_edm_index))
64}
65
66///////////////////////////////////////////////////////////////////////////////
67
68// https://www.odata.org/documentation/odata-version-3-0/atom-format/
69//
70// <?xml version="1.0" encoding="utf-8"?>
71// <feed
72//   xml:base="http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/"
73//   xmlns="http://www.w3.org/2005/Atom"
74//   xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
75//   xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
76//
77//   <id>http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/tickers_spy/</id>
78//   <title type="text">tickers_spy</title>
79//   <updated>2024-03-10T00:36:45Z</updated>
80//   <link rel="self" title="tickers_spy" href="tickers_spy" />
81//
82//   <entry>
83//     <id>http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/tickers_spy(0)</id>
84//     <category term="ODataDemo.tickers_spy" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
85//     <link rel="edit" title="tickers_spy" href="tickers_spy(0)" />
86//     <title />
87//     <updated>2024-03-10T00:36:45Z</updated>
88//     <author>
89//       <name />
90//     </author>
91//     <content type="application/xml">
92//       <m:properties>
93//         <d:offset m:type="Edm.Int64">0</d:offset>
94//         <d:from_symbol m:type="Edm.String">spy</d:from_symbol>
95//         <d:to_symbol m:type="Edm.String">usd</d:to_symbol>
96//         <d:close m:type="Edm.Double">135.5625</d:close>
97//       </m:properties>
98//     </content>
99//   </entry>
100//   <entry>
101//     <id>http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/tickers_spy(1)</id>
102//     <category term="ODataDemo.tickers_spy" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
103//     <link rel="edit" title="tickers_spy" href="tickers_spy(1)" />
104//     <title />
105//     <updated>2024-03-10T00:36:45Z</updated>
106//     <author>
107//       <name />
108//     </author>
109//     <content type="application/xml">
110//       <m:properties>
111//         <d:offset m:type="Edm.Int64">1</d:offset>
112//         <d:from_symbol m:type="Edm.String">spy</d:from_symbol>
113//         <d:to_symbol m:type="Edm.String">usd</d:to_symbol>
114//         <d:close m:type="Edm.Double">136.5622</d:close>
115//       </m:properties>
116//     </content>
117//   </entry>
118// </feed>
119//
120// TODO: Use erased dyn Writer type
121// TODO: Extract `CollectionInfo` type to avoid propagating
122//       a bunch of individual parameters
123pub fn write_atom_feed_from_records<W>(
124    schema: &Schema,
125    record_batches: Vec<RecordBatch>,
126    ctx: &dyn CollectionContext,
127    updated_time: DateTime<Utc>,
128    writer: &mut quick_xml::Writer<W>,
129) -> Result<(), ODataError>
130where
131    W: std::io::Write,
132{
133    let mut service_base_url = ctx.service_base_url()?;
134    let mut collection_base_url = ctx.collection_base_url()?;
135    let collection_name = ctx.collection_name()?;
136    let type_name = ctx.collection_name()?;
137    let type_namespace = ctx.collection_namespace()?;
138
139    if !service_base_url.starts_with("http") {
140        return Err(UnsupportedNetProtocol::new(service_base_url).into());
141    }
142    if !collection_base_url.starts_with("http") {
143        return Err(UnsupportedNetProtocol::new(collection_base_url).into());
144    }
145
146    if !service_base_url.ends_with('/') {
147        service_base_url.push('/');
148    }
149    if collection_base_url.ends_with('/') {
150        collection_base_url.pop();
151    }
152
153    let fq_type = format!("{type_namespace}.{type_name}");
154
155    let (edms, key_edm_index) = to_edms(
156        schema,
157        &ctx.key_column_alias(),
158        ctx.on_unsupported_feature(),
159    )?;
160
161    writer.write_event(quick_xml::events::Event::Decl(BytesDecl::new(
162        "1.0",
163        Some("utf-8"),
164        None,
165    )))?;
166
167    let mut feed = BytesStart::new("feed");
168    feed.push_attribute(("xml:base", service_base_url.as_str()));
169    feed.push_attribute(("xmlns", "http://www.w3.org/2005/Atom"));
170    feed.push_attribute((
171        "xmlns:d",
172        "http://schemas.microsoft.com/ado/2007/08/dataservices",
173    ));
174    feed.push_attribute((
175        "xmlns:m",
176        "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
177    ));
178
179    writer.write_event(Event::Start(feed))?;
180
181    // <id>http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/tickers_spy/</id>
182    // <title type="text">tickers_spy</title>
183    // <updated>2024-03-10T00:36:45Z</updated>
184    // <link rel="self" title="tickers_spy" href="tickers_spy" />
185    writer
186        .create_element("id")
187        .write_text_content(BytesText::from_escaped(&collection_base_url))?;
188    writer
189        .create_element("title")
190        .with_attribute(("type", "text"))
191        .write_text_content(BytesText::from_escaped(&collection_name))?;
192    writer
193        .create_element("updated")
194        .write_text_content(encode_date_time(&updated_time))?;
195    writer
196        .create_element("link")
197        .with_attributes([
198            ("rel", "self"),
199            ("title", collection_name.as_str()),
200            ("href", collection_name.as_str()),
201        ])
202        .write_empty()?;
203
204    for batch in record_batches {
205        for row in 0..batch.num_rows() {
206            writer.write_event(Event::Start(BytesStart::new("entry")))?;
207
208            // <id>http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/tickers_spy(1)</id>
209            // <category term="ODataDemo.tickers_spy" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
210            // <link rel="edit" title="tickers_spy" href="tickers_spy(1)" />
211            // <title />
212            // <updated>2024-03-10T00:36:45Z</updated>
213            // <author>
214            //   <name />
215            // </author>
216
217            let id = encode_primitive_dyn(batch.column(key_edm_index), row)?.decode()?;
218
219            let entry_url_rel = format!("{collection_name}({id})");
220            let entry_url_full = format!("{collection_base_url}({id})");
221
222            writer
223                .create_element("id")
224                .write_text_content(BytesText::from_escaped(entry_url_full))?;
225            writer
226                .create_element("category")
227                .with_attributes([
228                    (
229                        "scheme",
230                        "http://schemas.microsoft.com/ado/2007/08/dataservices/scheme",
231                    ),
232                    ("term", &fq_type),
233                ])
234                .write_empty()?;
235            writer
236                .create_element("link")
237                .with_attributes([
238                    ("rel", "edit"),
239                    ("title", &collection_name),
240                    ("href", &entry_url_rel),
241                ])
242                .write_empty()?;
243            writer.create_element("title").write_empty()?;
244            writer
245                .create_element("updated")
246                .write_text_content(encode_date_time(&updated_time))?;
247            writer.write_event(Event::Start(BytesStart::new("author")))?;
248            writer.create_element("name").write_empty()?;
249            writer.write_event(Event::End(BytesEnd::new("author")))?;
250
251            // <content type="application/xml">
252            //   <m:properties>
253            //     <d:offset m:type="Edm.Int64">1</d:offset>
254            //     <d:from_symbol m:type="Edm.String">spy</d:from_symbol>
255            //     <d:to_symbol m:type="Edm.String">usd</d:to_symbol>
256            //     <d:close m:type="Edm.Double">136.5622</d:close>
257            //   </m:properties>
258            // </content>
259            writer.write_event(Event::Start(
260                BytesStart::new("content").with_attributes([("type", "application/xml")]),
261            ))?;
262            writer.write_event(Event::Start(BytesStart::new("m:properties")))?;
263
264            for (edm, index) in &edms {
265                let col = batch.column(*index);
266
267                let mut start = BytesStart::new(&edm.tag);
268                start.push_attribute(("m:type", edm.typ.as_str()));
269                writer.write_event(Event::Start(start))?;
270                writer.write_event(Event::Text(encode_primitive_dyn(col, row)?))?;
271                writer.write_event(Event::End(BytesEnd::new(&edm.tag)))?;
272            }
273
274            writer.write_event(Event::End(BytesEnd::new("m:properties")))?;
275            writer.write_event(Event::End(BytesEnd::new("content")))?;
276            writer.write_event(Event::End(BytesEnd::new("entry")))?;
277        }
278    }
279
280    writer.write_event(Event::End(BytesEnd::new("feed")))?;
281
282    Ok(())
283}
284
285///////////////////////////////////////////////////////////////////////////////
286
287// https://www.odata.org/documentation/odata-version-3-0/atom-format/
288//
289// <?xml version="1.0" encoding="utf-8"?>
290// <entry
291//   xml:base="http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/"
292//   xmlns="http://www.w3.org/2005/Atom"
293//   xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
294//   xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
295//   <id>http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/tickers_spy(0)</id>
296//   <category term="ODataDemo.tickers_spy" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
297//   <link rel="edit" title="tickers_spy" href="tickers_spy(0)" />
298//   <title />
299//   <updated>2024-03-10T00:36:45Z</updated>
300//   <author>
301//     <name />
302//   </author>
303//   <content type="application/xml">
304//     <m:properties>
305//       <d:offset m:type="Edm.Int64">0</d:offset>
306//       <d:from_symbol m:type="Edm.String">spy</d:from_symbol>
307//       <d:to_symbol m:type="Edm.String">usd</d:to_symbol>
308//       <d:close m:type="Edm.Double">135.5625</d:close>
309//     </m:properties>
310//   </content>
311// </entry>
312// TODO: Use erased dyn Writer type
313// TODO: Extract `CollectionInfo` type to avoid propagating
314//       a bunch of individual parameters
315pub fn write_atom_entry_from_record<W>(
316    schema: &Schema,
317    batch: RecordBatch,
318    ctx: &dyn CollectionContext,
319    updated_time: DateTime<Utc>,
320    writer: &mut quick_xml::Writer<W>,
321) -> Result<(), ODataError>
322where
323    W: std::io::Write,
324{
325    let mut service_base_url = ctx.service_base_url()?;
326    let mut collection_base_url = ctx.collection_base_url()?;
327    let collection_name = ctx.collection_name()?;
328    let type_name = ctx.collection_name()?;
329    let type_namespace = ctx.collection_namespace()?;
330
331    if !service_base_url.starts_with("http") {
332        return Err(UnsupportedNetProtocol::new(service_base_url).into());
333    }
334    if !collection_base_url.starts_with("http") {
335        return Err(UnsupportedNetProtocol::new(collection_base_url).into());
336    }
337
338    if !service_base_url.ends_with('/') {
339        service_base_url.push('/');
340    }
341    if collection_base_url.ends_with('/') {
342        collection_base_url.pop();
343    }
344
345    let fq_type = format!("{type_namespace}.{type_name}");
346
347    let (edms, key_edm_index) = to_edms(
348        schema,
349        &ctx.key_column_alias(),
350        ctx.on_unsupported_feature(),
351    )?;
352
353    writer.write_event(quick_xml::events::Event::Decl(BytesDecl::new(
354        "1.0",
355        Some("utf-8"),
356        None,
357    )))?;
358
359    let mut entry = BytesStart::new("entry");
360    entry.push_attribute(("xml:base", service_base_url.as_str()));
361    entry.push_attribute(("xmlns", "http://www.w3.org/2005/Atom"));
362    entry.push_attribute((
363        "xmlns:d",
364        "http://schemas.microsoft.com/ado/2007/08/dataservices",
365    ));
366    entry.push_attribute((
367        "xmlns:m",
368        "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
369    ));
370
371    writer.write_event(Event::Start(entry))?;
372
373    // <id>http://a5d4b8ec90d5144a08efb47e789d49d5-1706314482.us-west-2.elb.amazonaws.com/tickers_spy(1)</id>
374    // <category term="ODataDemo.tickers_spy" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
375    // <link rel="edit" title="tickers_spy" href="tickers_spy(1)" />
376    // <title />
377    // <updated>2024-03-10T00:36:45Z</updated>
378    // <author>
379    //   <name />
380    // </author>
381
382    let row = 0;
383    let id = encode_primitive_dyn(batch.column(key_edm_index), row)?.decode()?;
384
385    let entry_url_rel = format!("{collection_name}({id})");
386    let entry_url_full = format!("{collection_base_url}({id})");
387
388    writer
389        .create_element("id")
390        .write_text_content(BytesText::from_escaped(entry_url_full))?;
391    writer
392        .create_element("category")
393        .with_attributes([
394            (
395                "scheme",
396                "http://schemas.microsoft.com/ado/2007/08/dataservices/scheme",
397            ),
398            ("term", &fq_type),
399        ])
400        .write_empty()?;
401    writer
402        .create_element("link")
403        .with_attributes([
404            ("rel", "edit"),
405            ("title", &collection_name),
406            ("href", &entry_url_rel),
407        ])
408        .write_empty()?;
409    writer.create_element("title").write_empty()?;
410    writer
411        .create_element("updated")
412        .write_text_content(encode_date_time(&updated_time))?;
413    writer.write_event(Event::Start(BytesStart::new("author")))?;
414    writer.create_element("name").write_empty()?;
415    writer.write_event(Event::End(BytesEnd::new("author")))?;
416
417    // <content type="application/xml">
418    //   <m:properties>
419    //     <d:offset m:type="Edm.Int64">1</d:offset>
420    //     <d:from_symbol m:type="Edm.String">spy</d:from_symbol>
421    //     <d:to_symbol m:type="Edm.String">usd</d:to_symbol>
422    //     <d:close m:type="Edm.Double">136.5622</d:close>
423    //   </m:properties>
424    // </content>
425    writer.write_event(Event::Start(
426        BytesStart::new("content").with_attributes([("type", "application/xml")]),
427    ))?;
428    writer.write_event(Event::Start(BytesStart::new("m:properties")))?;
429
430    for (edm, index) in &edms {
431        let col = batch.column(*index);
432
433        let mut start = BytesStart::new(&edm.tag);
434
435        start.push_attribute(("m:type", edm.typ.as_str()));
436        if col.is_null(row) {
437            start.push_attribute(("m:null", true.to_string().as_str()));
438            writer.write_event(Event::Empty(start))?;
439            continue;
440        }
441        writer.write_event(Event::Start(start))?;
442        writer.write_event(Event::Text(encode_primitive_dyn(col, row)?))?;
443        writer.write_event(Event::End(BytesEnd::new(&edm.tag)))?;
444    }
445
446    writer.write_event(Event::End(BytesEnd::new("m:properties")))?;
447    writer.write_event(Event::End(BytesEnd::new("content")))?;
448    writer.write_event(Event::End(BytesEnd::new("entry")))?;
449
450    Ok(())
451}
452
453///////////////////////////////////////////////////////////////////////////////
454
455fn encode_primitive_dyn(
456    col: &Arc<dyn Array>,
457    row: usize,
458) -> Result<BytesText<'_>, UnsupportedDataType> {
459    let col_type = col.data_type().clone();
460
461    match col_type {
462        DataType::Boolean => {
463            let arr = col.as_boolean();
464            let val = arr.value(row).to_string();
465            Ok(BytesText::from_escaped(val))
466        }
467        DataType::Int8 => Ok(encode_primitive::<Int8Type>(col, row)),
468        DataType::Int16 => Ok(encode_primitive::<Int16Type>(col, row)),
469        DataType::Int32 => Ok(encode_primitive::<Int32Type>(col, row)),
470        DataType::Int64 => Ok(encode_primitive::<Int64Type>(col, row)),
471        DataType::UInt8 => Ok(encode_primitive::<UInt8Type>(col, row)),
472        DataType::UInt16 => Ok(encode_primitive::<UInt16Type>(col, row)),
473        DataType::UInt32 => Ok(encode_primitive::<UInt32Type>(col, row)),
474        DataType::UInt64 => Ok(encode_primitive::<UInt64Type>(col, row)),
475        DataType::Float16 => Ok(encode_primitive::<Float16Type>(col, row)),
476        DataType::Float32 => Ok(encode_primitive::<Float32Type>(col, row)),
477        DataType::Float64 => Ok(encode_primitive::<Float64Type>(col, row)),
478        DataType::Timestamp(unit, tz) => encode_timestamp(col, row, unit, tz),
479        DataType::Date32 => {
480            let arr = col.as_primitive::<Date32Type>();
481            let days_since_epoch = chrono::Duration::days(arr.value(row).into());
482            let epoch = chrono::DateTime::UNIX_EPOCH.date_naive();
483            let date = epoch + days_since_epoch;
484            Ok(encode_date(&date))
485        }
486        DataType::Date64 => {
487            let arr = col.as_primitive::<Date64Type>();
488            let ticks = arr.value(row);
489            let ts = chrono::DateTime::from_timestamp_millis(ticks)
490                .ok_or(UnsupportedDataType::new(col_type))?;
491
492            Ok(encode_date(&ts.date_naive()))
493        }
494        DataType::Null | DataType::Utf8 => {
495            let arr = col.as_string::<i32>();
496            let val = arr.value(row);
497            Ok(BytesText::from_escaped(quick_xml::escape::escape(val)))
498        }
499        DataType::Utf8View => {
500            let arr = col.as_string_view();
501            let val = arr.value(row);
502            Ok(BytesText::from_escaped(quick_xml::escape::escape(val)))
503        }
504        DataType::LargeUtf8 => {
505            let arr = col.as_string::<i64>();
506            let val = arr.value(row);
507            Ok(BytesText::from_escaped(quick_xml::escape::escape(val)))
508        }
509        DataType::Time32(_)
510        | DataType::Time64(_)
511        | DataType::Duration(_)
512        | DataType::Interval(_)
513        | DataType::Binary
514        | DataType::FixedSizeBinary(_)
515        | DataType::LargeBinary
516        | DataType::BinaryView
517        | DataType::List(_)
518        | DataType::FixedSizeList(_, _)
519        | DataType::LargeList(_)
520        | DataType::ListView(_)
521        | DataType::LargeListView(_)
522        | DataType::Struct(_)
523        | DataType::Union(_, _)
524        | DataType::Dictionary(_, _)
525        | DataType::Decimal32(_, _)
526        | DataType::Decimal64(_, _)
527        | DataType::Decimal128(_, _)
528        | DataType::Decimal256(_, _)
529        | DataType::Map(_, _)
530        | DataType::RunEndEncoded(_, _) => Err(UnsupportedDataType::new(col_type)),
531    }
532}
533
534///////////////////////////////////////////////////////////////////////////////
535
536fn encode_primitive<T>(arr: &Arc<dyn Array>, row: usize) -> BytesText<'_>
537where
538    T: ArrowPrimitiveType,
539    <T as ArrowPrimitiveType>::Native: std::fmt::Display,
540{
541    let arr = arr.as_primitive::<T>();
542    let val = arr.value(row).to_string();
543    BytesText::from_escaped(val)
544}
545
546///////////////////////////////////////////////////////////////////////////////
547
548fn encode_timestamp(
549    col: &Arc<dyn Array>,
550    index: usize,
551    unit: TimeUnit,
552    tz: Option<Arc<str>>,
553) -> Result<BytesText<'static>, UnsupportedDataType> {
554    let dt = match unit {
555        TimeUnit::Microsecond => {
556            let value = cast_primitive::<TimestampMicrosecondType>(col, index)?;
557            DateTime::from_timestamp_micros(value)
558        }
559        TimeUnit::Millisecond => {
560            let value = cast_primitive::<TimestampMillisecondType>(col, index)?;
561            DateTime::from_timestamp_millis(value)
562        }
563        TimeUnit::Nanosecond => {
564            let value = cast_primitive::<TimestampNanosecondType>(col, index)?;
565            Some(DateTime::from_timestamp_nanos(value))
566        }
567        TimeUnit::Second => {
568            let value = cast_primitive::<TimestampSecondType>(col, index)?;
569            DateTime::from_timestamp(value, 0)
570        }
571    };
572
573    match dt {
574        Some(d) => Ok(if tz.is_some() {
575            encode_date_time(&d)
576        } else {
577            encode_date_time_naive(&d.naive_utc())
578        }),
579        None => Err(UnsupportedDataType::new(DataType::Timestamp(unit, tz))),
580    }
581}
582
583///////////////////////////////////////////////////////////////////////////////
584
585fn encode_date(d: &chrono::NaiveDate) -> BytesText<'static> {
586    // Note: there is not `Date` type in Atom so we are representing dates as naive `DateTime`
587    let dt = chrono::NaiveDateTime::new(*d, chrono::NaiveTime::MIN);
588    BytesText::from_escaped(dt.format("%Y-%m-%dT%H:%M").to_string())
589}
590
591fn encode_date_time(dt: &DateTime<Utc>) -> BytesText<'static> {
592    BytesText::from_escaped(dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true))
593}
594
595fn encode_date_time_naive(dt: &chrono::NaiveDateTime) -> BytesText<'static> {
596    BytesText::from_escaped(dt.format("%Y-%m-%dT%H:%M:%S%.f").to_string())
597}
598
599///////////////////////////////////////////////////////////////////////////////
600
601fn cast_primitive<T: ArrowPrimitiveType>(
602    column: &Arc<dyn Array>,
603    index: usize,
604) -> Result<T::Native, UnsupportedDataType> {
605    let arr: &PrimitiveArray<T> = match column.as_primitive_opt() {
606        Some(a) => a,
607        None => return Err(UnsupportedDataType::new(T::DATA_TYPE)),
608    };
609
610    let value = arr.value(index);
611    Ok(value)
612}
613
614///////////////////////////////////////////////////////////////////////////////
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619
620    use datafusion::arrow::{
621        array::{
622            Array, Date32Array, Date64Array, Int64Array, TimestampMicrosecondArray,
623            TimestampMillisecondArray, TimestampSecondArray,
624        },
625        datatypes::{ArrowPrimitiveType, Date32Type, Date64Type},
626    };
627
628    #[test]
629    fn test_encode_date() {
630        // Date32
631        let values = [chrono::DateTime::from_timestamp_millis(1726012800000).unwrap()];
632        let values: Date32Array = values
633            .iter()
634            .map(|d| Date32Type::from_naive_date(d.date_naive()))
635            .collect::<Vec<<Date32Type as ArrowPrimitiveType>::Native>>()
636            .into();
637        let values = Arc::new(values) as Arc<dyn Array>;
638
639        let result = encode_primitive_dyn(&values, 0).unwrap();
640        assert_eq!(result.borrow(), BytesText::new("2024-09-11T00:00"));
641
642        // Date64
643        let values = [chrono::DateTime::from_timestamp_millis(1726012800000).unwrap()];
644        let values: Date64Array = values
645            .iter()
646            .map(|d| Date64Type::from_naive_date(d.date_naive()))
647            .collect::<Vec<<Date64Type as ArrowPrimitiveType>::Native>>()
648            .into();
649        let values = Arc::new(values) as Arc<dyn Array>;
650
651        let result = encode_primitive_dyn(&values, 0).unwrap();
652        assert_eq!(result.borrow(), BytesText::new("2024-09-11T00:00"));
653    }
654
655    #[test]
656    fn test_encode_timestamp() {
657        let assert_serializes_as = |arr: Arc<dyn Array>, expected: &[&'static str]| {
658            let actual: Vec<_> = (0..arr.len())
659                .map(|i| encode_primitive_dyn(&arr, i).unwrap())
660                .collect();
661            let expected: Vec<_> = expected.iter().map(|s| BytesText::new(s)).collect();
662            assert_eq!(actual, expected);
663        };
664
665        // Millis
666        let ts_milli = Arc::new(
667            TimestampMillisecondArray::from(vec![
668                // 2020-01-01T12:00:00Z
669                1_577_880_000_001,
670                // 2020-01-01T12:01:00Z
671                1_577_880_060_001,
672            ])
673            .with_timezone(Arc::from("UTC")),
674        ) as Arc<dyn Array>;
675
676        assert_serializes_as(
677            ts_milli,
678            &["2020-01-01T12:00:00.001Z", "2020-01-01T12:01:00.001Z"],
679        );
680
681        // Micros
682        let ts_micro = Arc::new(
683            TimestampMicrosecondArray::from(vec![
684                // 2020-01-01T12:00:00Z
685                1_577_880_000_000_001,
686                // 2020-01-01T12:01:00Z
687                1_577_880_060_000_001,
688            ])
689            .with_timezone(Arc::from("UTC")),
690        ) as Arc<dyn Array>;
691
692        assert_serializes_as(
693            ts_micro,
694            &["2020-01-01T12:00:00.000Z", "2020-01-01T12:01:00.000Z"],
695        );
696
697        // Second
698        let ts_second = Arc::new(
699            TimestampSecondArray::from(vec![
700                // 2020-01-01T12:00:01
701                1_577_880_001,
702                // 2020-01-01T12:01:01
703                1_577_880_061,
704            ])
705            .with_timezone(Arc::from("UTC")),
706        ) as Arc<dyn Array>;
707
708        assert_serializes_as(
709            ts_second,
710            &["2020-01-01T12:00:01.000Z", "2020-01-01T12:01:01.000Z"],
711        );
712
713        // No timezone
714        let ts_micro_no_tz = Arc::new(TimestampMicrosecondArray::from(vec![
715            // 2020-01-01T12:00:00
716            1_577_880_000_000_000,
717            // 2020-01-01T12:01:00.001
718            1_577_880_060_001_000,
719        ])) as Arc<dyn Array>;
720
721        assert_serializes_as(
722            ts_micro_no_tz,
723            &["2020-01-01T12:00:00", "2020-01-01T12:01:00.001"],
724        );
725    }
726
727    #[test]
728    fn test_encode_primitive_dyn() {
729        let values: Int64Array = vec![1, 2, 3].into();
730        let values = Arc::new(values) as Arc<dyn Array>;
731
732        let result = encode_primitive_dyn(&values, 0).unwrap();
733        assert_eq!(result, BytesText::new("1"));
734    }
735}