uni_plugin/traits/types.rs
1//! Logical type plugins — Arrow extension types as a plugin surface.
2//!
3//! Logical types are exposed via Arrow's extension-type mechanism: the
4//! type identity travels in the Arrow `Field`'s `metadata` under the
5//! standard `ARROW:extension:name` (e.g., `"geo.point"`) and
6//! `ARROW:extension:metadata` keys.
7
8use arrow_schema::DataType;
9use datafusion::logical_expr::ColumnarValue;
10use datafusion::scalar::ScalarValue;
11
12use crate::errors::FnError;
13
14/// A logical type plugin (Arrow extension type).
15pub trait LogicalTypeProvider: Send + Sync {
16 /// Extension name stored in `ARROW:extension:name`.
17 fn name(&self) -> &str;
18
19 /// Physical Arrow storage type backing this logical type.
20 fn arrow_type(&self) -> DataType;
21
22 /// Parse a Cypher / Locy literal into the logical-typed scalar.
23 ///
24 /// # Errors
25 ///
26 /// Returns [`FnError`] if the literal is malformed for this type.
27 #[allow(
28 clippy::wrong_self_convention,
29 reason = "method belongs to the provider, not the literal"
30 )]
31 fn from_literal(&self, s: &str) -> Result<ScalarValue, FnError>;
32
33 /// Render a logical-typed value for display.
34 ///
35 /// # Errors
36 ///
37 /// Returns [`FnError`] if the value cannot be rendered.
38 fn to_display(&self, v: &ScalarValue) -> Result<String, FnError>;
39
40 /// Convert this logical-typed column to a different target type.
41 ///
42 /// # Errors
43 ///
44 /// Returns [`FnError`] if the cast is unsupported.
45 fn cast_to(&self, v: &ColumnarValue, target: &DataType) -> Result<ColumnarValue, FnError>;
46
47 /// Convert from a physical column to this logical type.
48 ///
49 /// # Errors
50 ///
51 /// Returns [`FnError`] if the source representation is incompatible.
52 fn cast_from(&self, v: &ColumnarValue) -> Result<ColumnarValue, FnError>;
53
54 /// Optional opaque version stamp for the on-disk encoding.
55 ///
56 /// Default is `"1"`. Override when bumping an encoding-breaking
57 /// change so [`Self::compat_check`] can reject a reload that would
58 /// silently mis-decode already-persisted data.
59 fn encoding_version(&self) -> &str {
60 "1"
61 }
62
63 /// Reject a reload that would change the Arrow extension contract.
64 ///
65 /// Default implementation enforces the §11.2.1 invariant: the new
66 /// provider must keep the same extension `name()` *and* the same
67 /// physical `arrow_type()` *and* the same [`Self::encoding_version`]
68 /// as the old provider. Any mismatch is a hard reload error
69 /// because previously-stored values would otherwise become
70 /// unreadable.
71 ///
72 /// # Errors
73 ///
74 /// Returns [`FnError`] (code [`FnError::CODE_TYPE_COERCION`]) when
75 /// the new provider's contract differs from the old.
76 fn compat_check(&self, old: &dyn LogicalTypeProvider) -> Result<(), FnError> {
77 if self.name() != old.name() {
78 return Err(FnError::new(
79 FnError::CODE_TYPE_COERCION,
80 format!(
81 "logical-type reload changed extension name: {} → {}",
82 old.name(),
83 self.name()
84 ),
85 ));
86 }
87 if self.arrow_type() != old.arrow_type() {
88 return Err(FnError::new(
89 FnError::CODE_TYPE_COERCION,
90 format!(
91 "logical-type {} reload changed arrow type: {:?} → {:?}",
92 self.name(),
93 old.arrow_type(),
94 self.arrow_type()
95 ),
96 ));
97 }
98 if self.encoding_version() != old.encoding_version() {
99 return Err(FnError::new(
100 FnError::CODE_TYPE_COERCION,
101 format!(
102 "logical-type {} reload changed encoding version: {} → {}",
103 self.name(),
104 old.encoding_version(),
105 self.encoding_version()
106 ),
107 ));
108 }
109 Ok(())
110 }
111}