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
16struct Edm {
19 typ: String,
20 tag: String,
21}
22
23impl Edm {
24 fn from_field(field: &Arc<Field>) -> Result<Self, UnsupportedDataType> {
25 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
66pub 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 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 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 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
285pub 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 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 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
453fn 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
534fn 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
546fn 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
583fn encode_date(d: &chrono::NaiveDate) -> BytesText<'static> {
586 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
599fn 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#[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 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 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 let ts_milli = Arc::new(
667 TimestampMillisecondArray::from(vec![
668 1_577_880_000_001,
670 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 let ts_micro = Arc::new(
683 TimestampMicrosecondArray::from(vec![
684 1_577_880_000_000_001,
686 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 let ts_second = Arc::new(
699 TimestampSecondArray::from(vec![
700 1_577_880_001,
702 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 let ts_micro_no_tz = Arc::new(TimestampMicrosecondArray::from(vec![
715 1_577_880_000_000_000,
717 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}