@@ -16,7 +16,7 @@ pub fn ducklake_to_arrow_type(ducklake_type: &str) -> Result<DataType> {
1616 let normalized = ducklake_type. trim ( ) . to_lowercase ( ) ;
1717
1818 // Handle parameterized types first
19- if let Some ( decimal_params) = parse_decimal ( & normalized) {
19+ if let Some ( decimal_params) = parse_decimal ( & normalized) ? {
2020 return Ok ( decimal_params) ;
2121 }
2222
@@ -163,39 +163,87 @@ pub fn arrow_to_ducklake_type(arrow_type: &DataType) -> Result<String> {
163163 }
164164}
165165
166+ /// Maximum precision for Arrow Decimal256
167+ const DECIMAL_MAX_PRECISION : u8 = 76 ;
168+
169+ /// Validate decimal precision and scale bounds
170+ fn validate_decimal_precision_scale ( precision : u8 , scale : i8 , type_str : & str ) -> Result < ( ) > {
171+ if precision == 0 {
172+ return Err ( DuckLakeError :: UnsupportedType ( format ! (
173+ "Decimal precision must be >= 1, got 0 in type '{}'" ,
174+ type_str
175+ ) ) ) ;
176+ }
177+ if precision > DECIMAL_MAX_PRECISION {
178+ return Err ( DuckLakeError :: UnsupportedType ( format ! (
179+ "Decimal precision must be <= {}, got {} in type '{}'" ,
180+ DECIMAL_MAX_PRECISION , precision, type_str
181+ ) ) ) ;
182+ }
183+ if scale >= 0 && scale as u8 > precision {
184+ return Err ( DuckLakeError :: UnsupportedType ( format ! (
185+ "Decimal scale ({}) must not exceed precision ({}) in type '{}'" ,
186+ scale, precision, type_str
187+ ) ) ) ;
188+ }
189+ Ok ( ( ) )
190+ }
191+
166192/// Parse decimal type with precision and scale
167193/// Format: "decimal(precision, scale)" or "decimal(precision)"
168- fn parse_decimal ( type_str : & str ) -> Option < DataType > {
194+ ///
195+ /// Returns `Ok(None)` if the type string is not a decimal type.
196+ /// Returns `Err` if it is a decimal type but has invalid precision/scale.
197+ fn parse_decimal ( type_str : & str ) -> Result < Option < DataType > > {
169198 if !type_str. starts_with ( "decimal" ) && !type_str. starts_with ( "numeric" ) {
170- return None ;
199+ return Ok ( None ) ;
171200 }
172201
173202 // Extract parameters from parentheses
174- let start = type_str. find ( '(' ) ?;
175- let end = type_str. find ( ')' ) ?;
203+ let start = match type_str. find ( '(' ) {
204+ Some ( s) => s,
205+ None => return Ok ( None ) ,
206+ } ;
207+ let end = match type_str. find ( ')' ) {
208+ Some ( e) => e,
209+ None => return Ok ( None ) ,
210+ } ;
176211 let params = & type_str[ start + 1 ..end] ;
177212
178213 let parts: Vec < & str > = params. split ( ',' ) . map ( |s| s. trim ( ) ) . collect ( ) ;
179214
180215 match parts. len ( ) {
181216 1 => {
182- // decimal(precision) with scale=0
183- let precision: u8 = parts[ 0 ] . parse ( ) . ok ( ) ?;
184- Some ( DataType :: Decimal128 ( precision, 0 ) )
217+ let precision: u8 = parts[ 0 ] . parse ( ) . map_err ( |_| {
218+ DuckLakeError :: UnsupportedType ( format ! (
219+ "Invalid decimal precision '{}' in type '{}'" ,
220+ parts[ 0 ] , type_str
221+ ) )
222+ } ) ?;
223+ validate_decimal_precision_scale ( precision, 0 , type_str) ?;
224+ Ok ( Some ( DataType :: Decimal128 ( precision, 0 ) ) )
185225 } ,
186226 2 => {
187- // decimal(precision, scale)
188- let precision: u8 = parts[ 0 ] . parse ( ) . ok ( ) ?;
189- let scale: i8 = parts[ 1 ] . parse ( ) . ok ( ) ?;
190-
191- // Use Decimal256 for high precision
227+ let precision: u8 = parts[ 0 ] . parse ( ) . map_err ( |_| {
228+ DuckLakeError :: UnsupportedType ( format ! (
229+ "Invalid decimal precision '{}' in type '{}'" ,
230+ parts[ 0 ] , type_str
231+ ) )
232+ } ) ?;
233+ let scale: i8 = parts[ 1 ] . parse ( ) . map_err ( |_| {
234+ DuckLakeError :: UnsupportedType ( format ! (
235+ "Invalid decimal scale '{}' in type '{}'" ,
236+ parts[ 1 ] , type_str
237+ ) )
238+ } ) ?;
239+ validate_decimal_precision_scale ( precision, scale, type_str) ?;
192240 if precision > 38 {
193- Some ( DataType :: Decimal256 ( precision, scale) )
241+ Ok ( Some ( DataType :: Decimal256 ( precision, scale) ) )
194242 } else {
195- Some ( DataType :: Decimal128 ( precision, scale) )
243+ Ok ( Some ( DataType :: Decimal128 ( precision, scale) ) )
196244 }
197245 } ,
198- _ => None ,
246+ _ => Ok ( None ) ,
199247 }
200248}
201249
@@ -593,6 +641,84 @@ mod tests {
593641 }
594642 }
595643
644+ #[ test]
645+ fn test_decimal_precision_zero_rejected ( ) {
646+ let result = ducklake_to_arrow_type ( "decimal(0, 0)" ) ;
647+ assert ! ( result. is_err( ) ) ;
648+ match result {
649+ Err ( DuckLakeError :: UnsupportedType ( msg) ) => {
650+ assert ! ( msg. contains( "precision must be >= 1" ) ) ;
651+ } ,
652+ _ => panic ! ( "Expected UnsupportedType error for precision=0" ) ,
653+ }
654+ }
655+
656+ #[ test]
657+ fn test_decimal_precision_too_large_rejected ( ) {
658+ let result = ducklake_to_arrow_type ( "decimal(77, 0)" ) ;
659+ assert ! ( result. is_err( ) ) ;
660+ match result {
661+ Err ( DuckLakeError :: UnsupportedType ( msg) ) => {
662+ assert ! ( msg. contains( "precision must be <= 76" ) ) ;
663+ } ,
664+ _ => panic ! ( "Expected UnsupportedType error for precision=77" ) ,
665+ }
666+ }
667+
668+ #[ test]
669+ fn test_decimal_precision_255_rejected ( ) {
670+ let result = ducklake_to_arrow_type ( "decimal(255, 0)" ) ;
671+ assert ! ( result. is_err( ) ) ;
672+ match result {
673+ Err ( DuckLakeError :: UnsupportedType ( msg) ) => {
674+ assert ! ( msg. contains( "precision must be <= 76" ) ) ;
675+ } ,
676+ _ => panic ! ( "Expected UnsupportedType error for precision=255" ) ,
677+ }
678+ }
679+
680+ #[ test]
681+ fn test_decimal_scale_exceeds_precision_rejected ( ) {
682+ let result = ducklake_to_arrow_type ( "decimal(10, 11)" ) ;
683+ assert ! ( result. is_err( ) ) ;
684+ match result {
685+ Err ( DuckLakeError :: UnsupportedType ( msg) ) => {
686+ assert ! ( msg. contains( "scale (11) must not exceed precision (10)" ) ) ;
687+ } ,
688+ _ => panic ! ( "Expected UnsupportedType error for scale > precision" ) ,
689+ }
690+ }
691+
692+ #[ test]
693+ fn test_decimal_valid_edge_cases ( ) {
694+ assert_eq ! (
695+ ducklake_to_arrow_type( "decimal(1, 0)" ) . unwrap( ) ,
696+ DataType :: Decimal128 ( 1 , 0 )
697+ ) ;
698+ assert_eq ! (
699+ ducklake_to_arrow_type( "decimal(38, 0)" ) . unwrap( ) ,
700+ DataType :: Decimal128 ( 38 , 0 )
701+ ) ;
702+ assert_eq ! (
703+ ducklake_to_arrow_type( "decimal(39, 0)" ) . unwrap( ) ,
704+ DataType :: Decimal256 ( 39 , 0 )
705+ ) ;
706+ assert_eq ! (
707+ ducklake_to_arrow_type( "decimal(76, 0)" ) . unwrap( ) ,
708+ DataType :: Decimal256 ( 76 , 0 )
709+ ) ;
710+ assert_eq ! (
711+ ducklake_to_arrow_type( "decimal(10, 10)" ) . unwrap( ) ,
712+ DataType :: Decimal128 ( 10 , 10 )
713+ ) ;
714+ }
715+
716+ #[ test]
717+ fn test_decimal_negative_precision_rejected ( ) {
718+ let result = ducklake_to_arrow_type ( "decimal(-1, 0)" ) ;
719+ assert ! ( result. is_err( ) ) ;
720+ }
721+
596722 #[ test]
597723 fn test_build_schema_with_unsupported_type ( ) {
598724 // Test that build_arrow_schema propagates complex type errors
0 commit comments