@@ -18,14 +18,18 @@ use std::collections::HashMap;
1818use databend_common_exception:: ErrorCode ;
1919use databend_common_exception:: Result ;
2020
21+ use super :: Cast ;
2122use super :: ColumnIndex ;
23+ use super :: ColumnRef ;
24+ use super :: ConstantFolder ;
2225use super :: Expr ;
2326use super :: FunctionCall ;
2427use super :: FunctionContext ;
2528use super :: FunctionDomain ;
2629use super :: FunctionEval ;
2730use super :: FunctionRegistry ;
2831use super :: Scalar ;
32+ use super :: conversion:: classify_conversion;
2933use super :: function_stat:: DeriveStat ;
3034use super :: stat_distribution:: ArgStat ;
3135use super :: stat_distribution:: BorrowedDistribution ;
@@ -93,10 +97,70 @@ impl<'a> StatEvaluator<'a> {
9397 Expr :: FunctionCall ( call) => Ok ( self
9498 . eval_function_call ( call, input_stats) ?
9599 . map ( CowStat :: Owned ) ) ,
96- Expr :: Cast ( _) | Expr :: LambdaFunctionCall ( _) => Ok ( None ) ,
100+ Expr :: Cast ( cast) => Ok ( self . eval_cast ( cast, input_stats) ?. map ( CowStat :: Owned ) ) ,
101+ Expr :: LambdaFunctionCall ( _) => Ok ( None ) ,
97102 }
98103 }
99104
105+ fn eval_cast < ' s , I : ColumnIndex > (
106+ & ' a self ,
107+ cast : & Cast < I > ,
108+ input_stats : & ' s HashMap < I , ArgStat < ' _ > > ,
109+ ) -> Result < Option < ReturnStat > > {
110+ let src_type = cast. expr . data_type ( ) ;
111+ if cast. is_try || !classify_conversion ( src_type, & cast. dest_type ) . is_lossless_injective ( ) {
112+ return Ok ( None ) ;
113+ }
114+
115+ let Some ( input) = self . eval ( & cast. expr , input_stats) ? else {
116+ return Ok ( None ) ;
117+ } ;
118+ let input = input. as_ref ( ) ;
119+
120+ // Reuse the cast domain implementation without making statistics
121+ // evaluation depend on the physical value evaluator. The synthetic
122+ // column represents the already-derived statistics of the inner expr.
123+ let expr = Expr :: Cast ( Cast {
124+ span : cast. span ,
125+ is_try : false ,
126+ expr : Box :: new ( Expr :: ColumnRef ( ColumnRef {
127+ span : cast. span ,
128+ id : 0 ,
129+ data_type : src_type. clone ( ) ,
130+ display_name : String :: new ( ) ,
131+ } ) ) ,
132+ dest_type : cast. dest_type . clone ( ) ,
133+ } ) ;
134+ let input_domains = HashMap :: from ( [ ( 0 , input. domain . clone ( ) ) ] ) ;
135+ let ( _, Some ( domain) ) = ConstantFolder :: fold_with_domain (
136+ & expr,
137+ & input_domains,
138+ self . func_ctx ,
139+ self . fn_registry ,
140+ ) else {
141+ return Ok ( None ) ;
142+ } ;
143+
144+ let stat = ReturnStat {
145+ domain,
146+ // A lossless injective cast preserves distinct values and NULLs.
147+ // Histograms are typed, so their boundaries cannot be reused.
148+ ndv : input. ndv ,
149+ null_count : input. null_count ,
150+ distribution : OwnedDistribution :: Unknown ,
151+ } ;
152+ if let Err ( msg) = stat. check_consistency_with_type ( Some ( & cast. dest_type ) ) {
153+ if cfg ! ( debug_assertions) {
154+ return Err ( ErrorCode :: Internal ( format ! (
155+ "Failed to derive statistics for cast: {msg}"
156+ ) ) ) ;
157+ }
158+ log:: warn!( msg; "Derived invalid cast statistics" ) ;
159+ return Ok ( None ) ;
160+ }
161+ Ok ( Some ( stat) )
162+ }
163+
100164 fn eval_function_call < ' s , I : ColumnIndex > (
101165 & ' a self ,
102166 call : & FunctionCall < I > ,
@@ -219,6 +283,8 @@ mod tests {
219283
220284 use super :: * ;
221285 use crate :: types:: DataType ;
286+ use crate :: types:: NumberDataType ;
287+ use crate :: types:: number:: NumberScalar ;
222288
223289 #[ test]
224290 fn test_constant_null_uses_exact_input_cardinality ( ) {
@@ -237,4 +303,75 @@ mod tests {
237303
238304 assert_eq ! ( stat. null_count, StatCount :: exact( 7 ) ) ;
239305 }
306+
307+ #[ test]
308+ fn test_lossless_cast_preserves_basic_statistics ( ) {
309+ let src_type = DataType :: Number ( NumberDataType :: UInt8 ) ;
310+ let dest_type = src_type. clone ( ) . wrap_nullable ( ) ;
311+ let expr = Expr :: Cast ( Cast {
312+ span : None ,
313+ is_try : false ,
314+ expr : Box :: new ( Expr :: ColumnRef ( ColumnRef {
315+ span : None ,
316+ id : 0 ,
317+ data_type : src_type. clone ( ) ,
318+ display_name : "c0" . to_string ( ) ,
319+ } ) ) ,
320+ dest_type : dest_type. clone ( ) ,
321+ } ) ;
322+ let domain = crate :: Domain :: from_min_max (
323+ Scalar :: Number ( NumberScalar :: UInt8 ( 1 ) ) ,
324+ Scalar :: Number ( NumberScalar :: UInt8 ( 3 ) ) ,
325+ & src_type,
326+ ) ;
327+ let input_stats = HashMap :: from ( [ ( 0 , ArgStat {
328+ domain,
329+ ndv : NdvEstimate :: exact ( 3.0 ) ,
330+ null_count : StatCount :: exact ( 0 ) ,
331+ distribution : BorrowedDistribution :: Unknown ,
332+ } ) ] ) ;
333+
334+ let stat = StatEvaluator :: run (
335+ & expr,
336+ & FunctionContext :: default ( ) ,
337+ & FunctionRegistry :: empty ( ) ,
338+ StatCardinality :: exact ( 3 ) ,
339+ & input_stats,
340+ )
341+ . unwrap ( )
342+ . unwrap ( )
343+ . into_owned ( ) ;
344+
345+ assert ! ( stat. domain. matches_data_type( & dest_type) ) ;
346+ assert_eq ! ( stat. ndv, NdvEstimate :: exact( 3.0 ) ) ;
347+ assert_eq ! ( stat. null_count, StatCount :: exact( 0 ) ) ;
348+ assert ! ( matches!( stat. distribution, OwnedDistribution :: Unknown ) ) ;
349+ }
350+
351+ #[ test]
352+ fn test_lossy_cast_is_not_derived ( ) {
353+ let expr = Expr :: Cast ( Cast {
354+ span : None ,
355+ is_try : false ,
356+ expr : Box :: new ( Expr :: ColumnRef ( ColumnRef {
357+ span : None ,
358+ id : 0 ,
359+ data_type : DataType :: Number ( NumberDataType :: Int64 ) ,
360+ display_name : "c0" . to_string ( ) ,
361+ } ) ) ,
362+ dest_type : DataType :: Number ( NumberDataType :: UInt8 ) ,
363+ } ) ;
364+
365+ assert ! (
366+ StatEvaluator :: run(
367+ & expr,
368+ & FunctionContext :: default ( ) ,
369+ & FunctionRegistry :: empty( ) ,
370+ StatCardinality :: exact( 3 ) ,
371+ & HashMap :: new( ) ,
372+ )
373+ . unwrap( )
374+ . is_none( )
375+ ) ;
376+ }
240377}
0 commit comments