1use crate::{ArrowError, DataType, extension::ExtensionType};
23
24#[derive(Debug, Default, Clone, Copy, PartialEq)]
57pub struct TimestampWithOffset;
58
59const TIMESTAMP_FIELD_NAME: &str = "timestamp";
60const OFFSET_FIELD_NAME: &str = "offset_minutes";
61
62impl ExtensionType for TimestampWithOffset {
63 const NAME: &'static str = "arrow.timestamp_with_offset";
64
65 type Metadata = ();
66
67 fn metadata(&self) -> &Self::Metadata {
68 &()
69 }
70
71 fn serialize_metadata(&self) -> Option<String> {
72 None
73 }
74
75 fn deserialize_metadata(metadata: Option<&str>) -> Result<Self::Metadata, ArrowError> {
76 metadata.map_or_else(
77 || Ok(()),
78 |v| {
79 if !v.is_empty() {
80 Err(ArrowError::InvalidArgumentError(
81 "TimestampWithOffset extension type expects no metadata".to_owned(),
82 ))
83 } else {
84 Ok(())
85 }
86 },
87 )
88 }
89
90 fn supports_data_type(&self, data_type: &DataType) -> Result<(), ArrowError> {
91 let ok = match data_type {
92 DataType::Struct(fields) => match fields.len() {
93 2 => {
94 let maybe_timestamp = fields.first().unwrap();
95 let maybe_offset = fields.get(1).unwrap();
96
97 let timestamp_type_ok = matches!(maybe_timestamp.data_type(), DataType::Timestamp(_, tz) if {
98 match tz {
99 Some(tz) => {
100 tz.as_ref() == "UTC"
101 },
102 None => false
103 }
104 });
105
106 let offset_type_ok = match maybe_offset.data_type() {
107 DataType::Int16 => true,
108 DataType::Dictionary(key_type, value_type) => {
109 key_type.is_dictionary_key_type()
110 && matches!(value_type.as_ref(), DataType::Int16)
111 }
112 DataType::RunEndEncoded(run_ends, values) => {
113 run_ends.data_type().is_run_ends_type()
114 && matches!(values.data_type(), DataType::Int16)
115 }
116 _ => false,
117 };
118
119 maybe_timestamp.name() == TIMESTAMP_FIELD_NAME
120 && timestamp_type_ok
121 && !maybe_timestamp.is_nullable()
122 && maybe_offset.name() == OFFSET_FIELD_NAME
123 && offset_type_ok
124 && !maybe_offset.is_nullable()
125 }
126 _ => false,
127 },
128 _ => false,
129 };
130
131 match ok {
132 true => Ok(()),
133 false => Err(ArrowError::InvalidArgumentError(format!(
134 "TimestampWithOffset data type mismatch, expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found {data_type}"
135 ))),
136 }
137 }
138
139 fn try_new(data_type: &DataType, _metadata: Self::Metadata) -> Result<Self, ArrowError> {
140 Self.supports_data_type(data_type).map(|()| Self)
141 }
142
143 fn validate(data_type: &DataType, _metadata: Self::Metadata) -> Result<(), ArrowError> {
144 Self.supports_data_type(data_type)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use std::sync::Arc;
151
152 #[cfg(feature = "canonical_extension_types")]
153 use crate::extension::CanonicalExtensionType;
154 use crate::{
155 Field, Fields, TimeUnit,
156 extension::{EXTENSION_TYPE_METADATA_KEY, EXTENSION_TYPE_NAME_KEY},
157 };
158
159 use super::*;
160
161 fn make_valid_field_primitive(time_unit: TimeUnit) -> Field {
162 Field::new(
163 "",
164 DataType::Struct(Fields::from_iter([
165 Field::new(
166 TIMESTAMP_FIELD_NAME,
167 DataType::Timestamp(time_unit, Some("UTC".into())),
168 false,
169 ),
170 Field::new(OFFSET_FIELD_NAME, DataType::Int16, false),
171 ])),
172 false,
173 )
174 }
175
176 fn make_valid_field_dict_encoded(time_unit: TimeUnit, key_type: DataType) -> Field {
177 assert!(key_type.is_dictionary_key_type());
178
179 Field::new(
180 "",
181 DataType::Struct(Fields::from_iter([
182 Field::new(
183 TIMESTAMP_FIELD_NAME,
184 DataType::Timestamp(time_unit, Some("UTC".into())),
185 false,
186 ),
187 Field::new(
188 OFFSET_FIELD_NAME,
189 DataType::Dictionary(Box::new(key_type), Box::new(DataType::Int16)),
190 false,
191 ),
192 ])),
193 false,
194 )
195 }
196
197 fn make_valid_field_run_end_encoded(time_unit: TimeUnit, run_ends_type: DataType) -> Field {
198 assert!(run_ends_type.is_run_ends_type());
199 Field::new(
200 "",
201 DataType::Struct(Fields::from_iter([
202 Field::new(
203 TIMESTAMP_FIELD_NAME,
204 DataType::Timestamp(time_unit, Some("UTC".into())),
205 false,
206 ),
207 Field::new(
208 OFFSET_FIELD_NAME,
209 DataType::RunEndEncoded(
210 Arc::new(Field::new(
211 Field::REE_RUN_ENDS_FIELD_DEFAULT_NAME,
212 run_ends_type,
213 false,
214 )),
215 Arc::new(Field::new(
216 Field::REE_VALUES_FIELD_DEFAULT_NAME,
217 DataType::Int16,
218 false,
219 )),
220 ),
221 false,
222 ),
223 ])),
224 false,
225 )
226 }
227
228 #[test]
229 fn valid_primitive_offsets() -> Result<(), ArrowError> {
230 let time_units = [
231 TimeUnit::Second,
232 TimeUnit::Millisecond,
233 TimeUnit::Microsecond,
234 TimeUnit::Nanosecond,
235 ];
236
237 for time_unit in time_units {
238 let mut field = make_valid_field_primitive(time_unit);
239 field.try_with_extension_type(TimestampWithOffset)?;
240 field.try_extension_type::<TimestampWithOffset>()?;
241 #[cfg(feature = "canonical_extension_types")]
242 assert_eq!(
243 field.try_canonical_extension_type()?,
244 CanonicalExtensionType::TimestampWithOffset(TimestampWithOffset)
245 );
246 }
247
248 Ok(())
249 }
250
251 #[test]
252 fn valid_dict_encoded_offsets() -> Result<(), ArrowError> {
253 let time_units = [
254 TimeUnit::Second,
255 TimeUnit::Millisecond,
256 TimeUnit::Microsecond,
257 TimeUnit::Nanosecond,
258 ];
259
260 let key_types = [
261 DataType::UInt8,
262 DataType::UInt16,
263 DataType::UInt32,
264 DataType::UInt64,
265 DataType::Int8,
266 DataType::Int16,
267 DataType::Int32,
268 DataType::Int64,
269 ];
270
271 for time_unit in time_units {
272 for key_type in &key_types {
273 let mut field = make_valid_field_dict_encoded(time_unit, key_type.clone());
274 field.try_with_extension_type(TimestampWithOffset)?;
275 field.try_extension_type::<TimestampWithOffset>()?;
276 #[cfg(feature = "canonical_extension_types")]
277 assert_eq!(
278 field.try_canonical_extension_type()?,
279 CanonicalExtensionType::TimestampWithOffset(TimestampWithOffset)
280 );
281 }
282 }
283
284 Ok(())
285 }
286
287 #[test]
288 fn valid_run_end_encoded_offsets() -> Result<(), ArrowError> {
289 let time_units = [
290 TimeUnit::Second,
291 TimeUnit::Millisecond,
292 TimeUnit::Microsecond,
293 TimeUnit::Nanosecond,
294 ];
295
296 let run_ends_types = [DataType::Int16, DataType::Int32, DataType::Int64];
297
298 for time_unit in time_units {
299 for run_ends_type in &run_ends_types {
300 let mut field = make_valid_field_run_end_encoded(time_unit, run_ends_type.clone());
301 field.try_with_extension_type(TimestampWithOffset)?;
302 field.try_extension_type::<TimestampWithOffset>()?;
303 #[cfg(feature = "canonical_extension_types")]
304 assert_eq!(
305 field.try_canonical_extension_type()?,
306 CanonicalExtensionType::TimestampWithOffset(TimestampWithOffset)
307 );
308 }
309 }
310
311 Ok(())
312 }
313
314 #[test]
315 #[should_panic(expected = "Extension type name missing")]
316 fn missing_name() {
317 let field = make_valid_field_primitive(TimeUnit::Second)
318 .with_metadata([(EXTENSION_TYPE_METADATA_KEY, "")]);
319 field.extension_type::<TimestampWithOffset>();
320 }
321
322 #[test]
323 #[should_panic(
324 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Boolean"
325 )]
326 fn invalid_type_top_level() {
327 Field::new("", DataType::Boolean, false).with_extension_type(TimestampWithOffset);
328 }
329
330 #[test]
331 #[should_panic(
332 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
333 )]
334 fn invalid_type_struct_field_count() {
335 let data_type =
336 DataType::Struct(Fields::from_iter([Field::new("", DataType::Int16, false)]));
337 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
338 }
339
340 #[test]
341 #[should_panic(
342 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
343 )]
344 fn invalid_type_wrong_timestamp_type() {
345 let data_type = DataType::Struct(Fields::from_iter([
346 Field::new(TIMESTAMP_FIELD_NAME, DataType::Int16, false),
347 Field::new(OFFSET_FIELD_NAME, DataType::Int16, false),
348 ]));
349 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
350 }
351
352 #[test]
353 #[should_panic(
354 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
355 )]
356 fn invalid_type_wrong_offset_type() {
357 let data_type = DataType::Struct(Fields::from_iter([
358 Field::new(
359 TIMESTAMP_FIELD_NAME,
360 DataType::Timestamp(TimeUnit::Second, Some("UTC".into())),
361 false,
362 ),
363 Field::new(OFFSET_FIELD_NAME, DataType::UInt64, false),
364 ]));
365 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
366 }
367
368 #[test]
369 #[should_panic(
370 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
371 )]
372 fn invalid_type_wrong_offset_key_dict_encoded() {
373 let data_type = DataType::Struct(Fields::from_iter([
374 Field::new(
375 TIMESTAMP_FIELD_NAME,
376 DataType::Timestamp(TimeUnit::Second, Some("UTC".into())),
377 false,
378 ),
379 Field::new(
380 OFFSET_FIELD_NAME,
381 DataType::Dictionary(Box::new(DataType::Boolean), Box::new(DataType::Int16)),
382 false,
383 ),
384 ]));
385 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
386 }
387
388 #[test]
389 #[should_panic(
390 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
391 )]
392 fn invalid_type_wrong_offset_value_dict_encoded() {
393 let data_type = DataType::Struct(Fields::from_iter([
394 Field::new(
395 TIMESTAMP_FIELD_NAME,
396 DataType::Timestamp(TimeUnit::Second, Some("UTC".into())),
397 false,
398 ),
399 Field::new(
400 OFFSET_FIELD_NAME,
401 DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Int32)),
402 false,
403 ),
404 ]));
405 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
406 }
407
408 #[test]
409 #[should_panic(
410 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
411 )]
412 fn invalid_type_wrong_run_ends_run_end_encoded() {
413 let data_type = DataType::Struct(Fields::from_iter([
414 Field::new(
415 TIMESTAMP_FIELD_NAME,
416 DataType::Timestamp(TimeUnit::Second, Some("UTC".into())),
417 false,
418 ),
419 Field::new(
420 OFFSET_FIELD_NAME,
421 DataType::RunEndEncoded(
422 Arc::new(Field::new(
423 Field::REE_RUN_ENDS_FIELD_DEFAULT_NAME,
424 DataType::Boolean,
425 false,
426 )),
427 Arc::new(Field::new(
428 Field::REE_VALUES_FIELD_DEFAULT_NAME,
429 DataType::Int16,
430 false,
431 )),
432 ),
433 false,
434 ),
435 ]));
436 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
437 }
438
439 #[test]
440 #[should_panic(
441 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
442 )]
443 fn invalid_type_wrong_values_run_end_encoded() {
444 let data_type = DataType::Struct(Fields::from_iter([
445 Field::new(
446 TIMESTAMP_FIELD_NAME,
447 DataType::Timestamp(TimeUnit::Second, Some("UTC".into())),
448 false,
449 ),
450 Field::new(
451 OFFSET_FIELD_NAME,
452 DataType::RunEndEncoded(
453 Arc::new(Field::new(
454 Field::REE_RUN_ENDS_FIELD_DEFAULT_NAME,
455 DataType::UInt16,
456 false,
457 )),
458 Arc::new(Field::new(
459 Field::REE_VALUES_FIELD_DEFAULT_NAME,
460 DataType::Int32,
461 false,
462 )),
463 ),
464 false,
465 ),
466 ]));
467 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
468 }
469
470 #[test]
471 #[should_panic(
472 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
473 )]
474 fn invalid_type_nullable_timestamp() {
475 let data_type = DataType::Struct(Fields::from_iter([
476 Field::new(
477 TIMESTAMP_FIELD_NAME,
478 DataType::Timestamp(TimeUnit::Second, Some("UTC".into())),
479 true,
480 ),
481 Field::new(OFFSET_FIELD_NAME, DataType::Int16, false),
482 ]));
483 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
484 }
485
486 #[test]
487 #[should_panic(
488 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
489 )]
490 fn invalid_type_nullable_offset() {
491 let data_type = DataType::Struct(Fields::from_iter([
492 Field::new(
493 TIMESTAMP_FIELD_NAME,
494 DataType::Timestamp(TimeUnit::Second, Some("UTC".into())),
495 false,
496 ),
497 Field::new(OFFSET_FIELD_NAME, DataType::Int16, true),
498 ]));
499 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
500 }
501
502 #[test]
503 #[should_panic(
504 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
505 )]
506 fn invalid_type_no_timezone() {
507 let data_type = DataType::Struct(Fields::from_iter([
508 Field::new(
509 TIMESTAMP_FIELD_NAME,
510 DataType::Timestamp(TimeUnit::Second, None),
511 false,
512 ),
513 Field::new(OFFSET_FIELD_NAME, DataType::Int16, false),
514 ]));
515 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
516 }
517
518 #[test]
519 #[should_panic(
520 expected = "expected Struct(\"timestamp\": Timestamp(_, Some(\"UTC\")), \"offset_minutes\": Int16), found Struct"
521 )]
522 fn invalid_type_wrong_timezone() {
523 let data_type = DataType::Struct(Fields::from_iter([
524 Field::new(
525 TIMESTAMP_FIELD_NAME,
526 DataType::Timestamp(TimeUnit::Second, Some("Americas/Sao_Paulo".into())),
527 false,
528 ),
529 Field::new(OFFSET_FIELD_NAME, DataType::Int16, false),
530 ]));
531 Field::new("", data_type, false).with_extension_type(TimestampWithOffset);
532 }
533
534 #[test]
535 fn no_metadata() {
536 let field = make_valid_field_primitive(TimeUnit::Second)
537 .with_metadata([(EXTENSION_TYPE_NAME_KEY, TimestampWithOffset::NAME)]);
538 field.extension_type::<TimestampWithOffset>();
539 }
540
541 #[test]
542 fn empty_metadata() {
543 let field = make_valid_field_primitive(TimeUnit::Second).with_metadata([
544 (EXTENSION_TYPE_NAME_KEY, TimestampWithOffset::NAME),
545 (EXTENSION_TYPE_METADATA_KEY, ""),
546 ]);
547 field.extension_type::<TimestampWithOffset>();
548 }
549}