Skip to main content

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}