Skip to main content

polars_core/series/ops/
from_physical.rs

1use crate::prelude::*;
2
3impl Series {
4    /// Restore `dtype` from its physical representation with safety and Arrow import checks.
5    /// Safe counterpart of [`Series::from_physical_unchecked`].
6    ///
7    /// - `Map`: validate and canonicalize storage before constructing the Map.
8    /// - `Categorical` / `Enum`: every code must name a category.
9    /// - `Decimal`: validate the precision and scale, and that the values fit.
10    /// - `Object`, `Unknown`: reject reconstruction.
11    /// - Temporal types: no per-value checks, matching Arrow import. Out-of-range `Time`
12    ///   values may fail during formatting.
13    ///
14    /// Errors for unsupported dtypes.
15    pub fn try_from_physical(&self, dtype: &DataType) -> PolarsResult<Series> {
16        // These are physical types themselves, so the recursion would pass them through
17        // untouched rather than reach an arm that rejects them.
18        polars_ensure!(
19            !dtype.contains_objects(),
20            InvalidOperation:
21            "cannot restore `{dtype}` from its physical representation: objects are process-local"
22        );
23        polars_ensure!(
24            !dtype.contains_unknown(),
25            InvalidOperation: "cannot restore an unknown dtype from its physical representation"
26        );
27
28        let physical = dtype.to_physical();
29        polars_ensure!(
30            self.dtype() == &physical,
31            InvalidOperation:
32            "cannot restore `{dtype}` on a Series of type `{}`: its physical type is `{physical}`",
33            self.dtype()
34        );
35        // SAFETY: the physical dtypes match, recursively.
36        unsafe { try_from_physical_rec(self, dtype) }
37    }
38}
39
40/// # Safety
41/// `series.dtype()` must equal `dtype.to_physical()`.
42unsafe fn try_from_physical_rec(series: &Series, dtype: &DataType) -> PolarsResult<Series> {
43    use DataType as D;
44
45    // No logical types remain, even in nested values.
46    if series.dtype() == dtype {
47        return Ok(series.clone());
48    }
49
50    // SAFETY: every recursive call descends into both the physical Series and `dtype`.
51    match dtype {
52        #[cfg(feature = "dtype-map")]
53        D::Map(_, _) => {
54            let storage_dtype = dtype.map_storage_dtype().unwrap();
55            let storage = unsafe { try_from_physical_rec(series, &storage_dtype)? };
56            Ok(MapChunked::try_from_storage(dtype.clone(), storage)?.into_series())
57        },
58        D::List(inner) => {
59            let ca = series.list().unwrap();
60            let values = unsafe { try_from_physical_rec(&ca.get_inner(), inner)? };
61            Ok(ca.with_inner_values(&values).into_series())
62        },
63        #[cfg(feature = "dtype-array")]
64        D::Array(inner, _) => {
65            let ca = series.array().unwrap();
66            let values = unsafe { try_from_physical_rec(&ca.get_inner(), inner)? };
67            Ok(ca.with_inner_values(&values).into_series())
68        },
69        #[cfg(feature = "dtype-struct")]
70        D::Struct(fields) => {
71            let mut dtypes = fields.iter().map(|field| &field.dtype);
72            // `try_apply_fields` keeps the outer validity.
73            let ca = series.struct_().unwrap().try_apply_fields(|field| unsafe {
74                try_from_physical_rec(field, dtypes.next().unwrap())
75            })?;
76            Ok(ca.into_series())
77        },
78        #[cfg(feature = "dtype-extension")]
79        D::Extension(typ, storage_dtype) => {
80            // Check the `into_extension` precondition before reconstruction.
81            polars_ensure!(
82                !storage_dtype.is_extension(),
83                InvalidOperation: "cannot restore `{dtype}`: extension types cannot be nested directly"
84            );
85            let storage = unsafe { try_from_physical_rec(series, storage_dtype)? };
86            Ok(storage.into_extension(typ.clone()))
87        },
88        #[cfg(feature = "dtype-categorical")]
89        D::Categorical(_, _) | D::Enum(_, _) => Series::from_cats_and_dtype(series, dtype, true),
90        #[cfg(feature = "dtype-decimal")]
91        D::Decimal(precision, scale) => {
92            // Relabelling keeps the physical integers; casting would rescale them.
93            Ok(series
94                .i128()?
95                .clone()
96                .into_decimal(*precision, *scale)?
97                .into_series())
98        },
99        D::Date | D::Datetime(_, _) | D::Duration(_) | D::Time => unsafe {
100            series.from_physical_unchecked(dtype)
101        },
102        _ => polars_bail!(
103            InvalidOperation: "cannot validate the physical representation of `{dtype}`"
104        ),
105    }
106}
107
108#[cfg(test)]
109mod test {
110    use polars_arrow::array::PrimitiveArray;
111
112    use crate::prelude::*;
113
114    #[test]
115    fn try_from_physical_rejects_objects_and_unknown() {
116        // Both are their own physical type, so the recursion would hand the Series back
117        // untouched; only the guard up front rejects them.
118        let s = Series::new(PlSmallStr::from_static("x"), &[1i64]);
119
120        #[cfg(feature = "object")]
121        {
122            let dtype = DataType::List(Box::new(DataType::Object("x")));
123            let err = s.try_from_physical(&dtype).unwrap_err();
124            assert!(
125                err.to_string().contains("objects are process-local"),
126                "{err}"
127            );
128        }
129
130        let err = s
131            .try_from_physical(&DataType::Unknown(UnknownKind::Any))
132            .unwrap_err();
133        assert!(err.to_string().contains("unknown dtype"), "{err}");
134    }
135
136    #[cfg(feature = "dtype-extension")]
137    #[test]
138    fn try_from_physical_rejects_directly_nested_extensions() {
139        use crate::datatypes::extension::get_extension_type_or_generic;
140
141        let inner = DataType::Extension(
142            get_extension_type_or_generic("inner", &DataType::Int64, None),
143            Box::new(DataType::Int64),
144        );
145        let dtype = DataType::Extension(
146            get_extension_type_or_generic("outer", &inner, None),
147            Box::new(inner),
148        );
149        let s = Series::new(PlSmallStr::from_static("e"), &[1i64, 2]);
150        assert_eq!(s.dtype(), &dtype.to_physical());
151        let err = s.try_from_physical(&dtype).err().unwrap();
152        assert!(err.to_string().contains("nested directly"), "{err}");
153    }
154
155    /// Run with `--no-default-features --features dtype-date` to catch a `Date` arm
156    /// accidentally gated on `dtype-time`.
157    #[cfg(feature = "dtype-date")]
158    #[test]
159    fn from_chunk_and_dtype_builds_dates() {
160        let chunk = PrimitiveArray::<i32>::from_vec(vec![0, 1]).boxed();
161        let s = Series::from_chunk_and_dtype(PlSmallStr::from_static("d"), chunk, &DataType::Date)
162            .unwrap();
163        assert_eq!(s.dtype(), &DataType::Date);
164        assert_eq!(s.len(), 2);
165    }
166
167    #[cfg(feature = "dtype-categorical")]
168    #[test]
169    fn from_chunk_and_dtype_rejects_out_of_range_enum_codes() {
170        use polars_dtype::categorical::FrozenCategories;
171
172        let dtype = DataType::from_frozen_categories(FrozenCategories::new(["a", "b"]).unwrap());
173        let physical = dtype.to_physical();
174        let codes = |codes: &[u32]| {
175            Series::new(PlSmallStr::from_static("e"), codes)
176                .cast(&physical)
177                .unwrap()
178                .chunks()[0]
179                .clone()
180        };
181
182        let err =
183            Series::from_chunk_and_dtype(PlSmallStr::from_static("e"), codes(&[0, 7]), &dtype)
184                .unwrap_err();
185        assert!(err.to_string().contains("invalid category"), "{err}");
186
187        let s = Series::from_chunk_and_dtype(PlSmallStr::from_static("e"), codes(&[0, 1]), &dtype)
188            .unwrap();
189        assert_eq!(s.dtype(), &dtype);
190        assert_eq!(s.null_count(), 0);
191    }
192
193    #[cfg(feature = "object")]
194    #[test]
195    fn from_chunk_and_dtype_rejects_objects_before_reinterpreting() {
196        let chunk = PrimitiveArray::<i64>::from_vec(vec![1]).boxed();
197        for dtype in [
198            DataType::Object("x"),
199            DataType::List(Box::new(DataType::Object("x"))),
200        ] {
201            let err =
202                Series::from_chunk_and_dtype(PlSmallStr::from_static("o"), chunk.clone(), &dtype)
203                    .unwrap_err();
204            assert!(err.to_string().contains("objects"), "{err}");
205        }
206    }
207
208    #[cfg(feature = "dtype-decimal")]
209    #[test]
210    fn from_chunk_and_dtype_validates_decimals() {
211        let name = PlSmallStr::from_static("d");
212        let empty = PrimitiveArray::<i128>::new_empty(ArrowDataType::Int128).boxed();
213
214        // Validate metadata even for empty arrays.
215        let err =
216            Series::from_chunk_and_dtype(name.clone(), empty.clone(), &DataType::Decimal(50, 2))
217                .unwrap_err();
218        assert!(err.to_string().contains("precision"), "{err}");
219        let err = Series::from_chunk_and_dtype(name.clone(), empty, &DataType::Decimal(5, 9))
220            .unwrap_err();
221        assert!(err.to_string().contains("scale"), "{err}");
222
223        let too_big = PrimitiveArray::<i128>::from_vec(vec![100_000]).boxed();
224        let err = Series::from_chunk_and_dtype(name.clone(), too_big, &DataType::Decimal(5, 0))
225            .unwrap_err();
226        assert!(err.to_string().contains("can't fit"), "{err}");
227
228        let fits = PrimitiveArray::<i128>::from_vec(vec![99_999]).boxed();
229        let s = Series::from_chunk_and_dtype(name, fits, &DataType::Decimal(5, 0)).unwrap();
230        assert_eq!(s.dtype(), &DataType::Decimal(5, 0));
231    }
232}