Skip to content

Commit ba7dde1

Browse files
zfarrellclaude
andcommitted
fix: validate decimal precision and scale bounds
Closes #53 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 22ea714 commit ba7dde1

1 file changed

Lines changed: 142 additions & 16 deletions

File tree

src/types.rs

Lines changed: 142 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)