mz_expr/scalar/func/impls/
try_cast.rs1use std::fmt;
11use std::sync::atomic::{AtomicBool, Ordering};
12
13use mz_repr::{Datum, RowArena, SqlColumnType};
14use serde::{Deserialize, Serialize};
15
16use crate::scalar::func::{LazyUnaryFunc, UnaryFunc};
17use crate::{Eval, EvalError, MirScalarExpr};
18
19#[derive(
31 Ord,
32 PartialOrd,
33 Clone,
34 Debug,
35 Eq,
36 PartialEq,
37 Serialize,
38 Deserialize,
39 Hash
40)]
41pub struct TryCast<E = MirScalarExpr> {
42 inner: Box<UnaryFunc<E>>,
43}
44
45impl<E> TryCast<E> {
46 pub fn inner(&self) -> &UnaryFunc<E> {
48 &self.inner
49 }
50}
51
52struct FirstColumn;
61
62static FIRST_COLUMN: FirstColumn = FirstColumn;
63
64impl Eval for FirstColumn {
65 fn eval<'a>(
66 &'a self,
67 datums: &[Datum<'a>],
68 _temp_storage: &'a RowArena,
69 ) -> Result<Datum<'a>, EvalError> {
70 Ok(datums[0])
71 }
72
73 fn could_error(&self) -> bool {
74 false
75 }
76}
77
78impl<E: Eval> LazyUnaryFunc for TryCast<E> {
79 fn eval<'a>(
80 &'a self,
81 datums: &[Datum<'a>],
82 temp_storage: &'a RowArena,
83 a: &'a impl Eval,
84 ) -> Result<Datum<'a>, EvalError> {
85 let a = a.eval(datums, temp_storage)?;
86 match self.inner.eval(&[a], temp_storage, &FIRST_COLUMN) {
87 Ok(datum) => Ok(datum),
88 Err(EvalError::Internal(message)) => {
96 static REPORTED: AtomicBool = AtomicBool::new(false);
97 if mz_ore::assert::soft_assertions_enabled()
98 || !REPORTED.swap(true, Ordering::Relaxed)
99 {
100 mz_ore::soft_panic_or_log!(
101 "TRY_CAST swallowed an internal error from {}: {message}",
102 self.inner
103 );
104 }
105 Ok(Datum::Null)
106 }
107 Err(_) => Ok(Datum::Null),
108 }
109 }
110
111 fn output_sql_type(&self, input_type: SqlColumnType) -> SqlColumnType {
112 self.inner.output_sql_type(input_type).nullable(true)
113 }
114
115 fn propagates_nulls(&self) -> bool {
116 self.inner.propagates_nulls()
117 }
118
119 fn introduces_nulls(&self) -> bool {
120 true
121 }
122
123 fn could_error(&self) -> bool {
124 false
125 }
126
127 fn preserves_uniqueness(&self) -> bool {
128 false
134 }
135
136 fn inverse(&self) -> Option<crate::UnaryFunc> {
137 None
138 }
139
140 fn is_monotone(&self) -> bool {
141 false
147 }
148
149 fn is_eliminable_cast(&self) -> bool {
150 false
151 }
152}
153
154impl<E: Eval> UnaryFunc<E> {
155 pub fn try_cast_if_fallible(inner: impl Into<UnaryFunc<E>>) -> UnaryFunc<E> {
159 let inner = inner.into();
160 if inner.could_error() {
161 UnaryFunc::TryCast(TryCast {
162 inner: Box::new(inner),
163 })
164 } else {
165 inner
166 }
167 }
168}
169
170impl<E> TryCast<E> {
171 pub fn try_map_expr<'a, E2: TryFrom<&'a E>>(&'a self) -> Result<TryCast<E2>, E2::Error> {
174 Ok(TryCast {
175 inner: Box::new(self.inner.try_map_expr()?),
176 })
177 }
178
179 pub fn map_expr<'a, E2: From<&'a E>>(&'a self) -> TryCast<E2> {
182 TryCast {
183 inner: Box::new(self.inner.map_expr()),
184 }
185 }
186}
187
188impl<E> fmt::Display for TryCast<E> {
189 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
190 write!(f, "try_cast[{}]", self.inner)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use mz_repr::{Datum, ReprScalarType, RowArena};
199
200 use crate::scalar::func::{CastInt64ToInt32, CastStringToInt32, CastStringToInt64};
201 use crate::{Eval, EvalError, MirScalarExpr, UnaryFunc};
202
203 fn try_cast(inner: impl Into<UnaryFunc>) -> UnaryFunc {
204 let func = UnaryFunc::try_cast_if_fallible(inner.into());
205 assert!(
206 matches!(func, UnaryFunc::TryCast(_)),
207 "test needs a fallible cast"
208 );
209 func
210 }
211
212 fn eval(expr: &MirScalarExpr) -> Result<Datum<'_>, EvalError> {
213 let arena = Box::leak(Box::new(RowArena::new()));
215 expr.eval(&[], arena)
216 }
217
218 fn text(s: &str) -> MirScalarExpr {
219 MirScalarExpr::literal_ok(Datum::String(s), ReprScalarType::String)
220 }
221
222 #[mz_ore::test]
223 fn cast_error_becomes_null() {
224 let expr = text("abc").call_unary(try_cast(CastStringToInt32));
225 assert_eq!(eval(&expr), Ok(Datum::Null));
226 }
227
228 #[mz_ore::test]
229 fn success_agrees_with_strict_cast() {
230 let lenient = text("42").call_unary(try_cast(CastStringToInt32));
231 let strict = text("42").call_unary(CastStringToInt32);
232 assert_eq!(eval(&lenient), Ok(Datum::Int32(42)));
233 assert_eq!(eval(&lenient), eval(&strict));
234 }
235
236 #[mz_ore::test]
237 fn null_argument_stays_null() {
238 let expr = MirScalarExpr::literal_null(ReprScalarType::String)
239 .call_unary(try_cast(CastStringToInt32));
240 assert_eq!(eval(&expr), Ok(Datum::Null));
241 }
242
243 #[mz_ore::test]
244 fn argument_error_propagates() {
245 let expr = MirScalarExpr::literal(Err(EvalError::DivisionByZero), ReprScalarType::String)
246 .call_unary(try_cast(CastStringToInt32));
247 assert_eq!(eval(&expr), Err(EvalError::DivisionByZero));
248 }
249
250 #[mz_ore::test]
254 fn chain_nulls_at_either_stage() {
255 let chain = |s: &str| {
256 text(s)
257 .call_unary(try_cast(CastStringToInt64))
258 .call_unary(try_cast(CastInt64ToInt32))
259 };
260 assert_eq!(eval(&chain("abc")), Ok(Datum::Null), "fails at stage one");
261 assert_eq!(
262 eval(&chain("3000000000")),
263 Ok(Datum::Null),
264 "fails at stage two"
265 );
266 assert_eq!(eval(&chain("5")), Ok(Datum::Int32(5)));
267 }
268
269 #[mz_ore::test]
273 fn internal_error_is_a_soft_panic() {
274 let expr = MirScalarExpr::literal_ok(Datum::Int64(7), ReprScalarType::Int64)
275 .call_unary(try_cast(CastStringToInt32));
276 let outcome = mz_ore::panic::catch_unwind(|| eval(&expr));
277 if mz_ore::assert::soft_assertions_enabled() {
278 assert!(outcome.is_err(), "soft assertion should have fired");
279 } else {
280 assert_eq!(outcome.expect("no panic"), Ok(Datum::Null));
281 }
282 }
283
284 #[mz_ore::test]
285 fn metadata() {
286 let func = try_cast(CastStringToInt32);
287 assert!(!func.could_error());
288 assert!(func.introduces_nulls());
289 assert!(func.propagates_nulls());
290 assert!(!func.is_monotone());
291 assert!(!func.preserves_uniqueness());
292 assert_eq!(func.inverse(), None);
293 let input = mz_repr::SqlScalarType::String.nullable(false);
294 assert!(func.output_sql_type(input).nullable);
295 assert_eq!(func.to_string(), "try_cast[text_to_integer]");
296 }
297}