uqa_sql/semantics/
row_count.rs1use crate::{SQLError, ScalarExpr};
10use uqa_core::Value;
11
12pub fn coerce_limit_offset(
13 value: Value,
14 expression: &ScalarExpr,
15 label: &str,
16) -> Result<Option<u64>, SQLError> {
17 if matches!(value, Value::Null) {
18 return Ok(None);
19 }
20 let allow_unknown_string = matches!(
21 expression,
22 ScalarExpr::Literal(Value::Str(_)) | ScalarExpr::Param(_)
23 );
24 let value = match &value {
25 Value::Int(value) => Value::Int(*value),
26 Value::Float(value) => Value::Int(
27 i64::try_from(float_limit_offset(*value, label)?)
28 .expect("PostgreSQL bigint row count fits i64"),
29 ),
30 Value::Decimal(decimal) if decimal.is_nan() => {
31 return Err(SQLError::Routine {
32 sqlstate: "0A000".into(),
33 message: "cannot convert NaN to bigint".into(),
34 });
35 }
36 Value::Decimal(decimal) if decimal.is_infinite() => {
37 return Err(SQLError::Routine {
38 sqlstate: "0A000".into(),
39 message: "cannot convert infinity to bigint".into(),
40 });
41 }
42 Value::Decimal(_) => crate::expr::cast_value(&value, "bigint")?,
43 Value::Str(_) if allow_unknown_string => crate::expr::cast_value(&value, "bigint")?,
44 other => {
45 return Err(SQLError::TypeMismatch(format!(
46 "argument of {label} must be type bigint, got {other:?}"
47 )));
48 }
49 };
50 let Value::Int(value) = value else {
51 return Err(SQLError::Internal(
52 "row-count bigint coercion did not return an integer".into(),
53 ));
54 };
55 if value < 0 {
56 return Err(negative_row_count(label));
57 }
58 Ok(Some(u64::try_from(value).map_err(|_| {
59 SQLError::Internal("non-negative bigint did not fit u64".into())
60 })?))
61}
62
63fn negative_row_count(label: &str) -> SQLError {
64 if label == "OFFSET" {
65 SQLError::Routine {
66 sqlstate: "2201X".into(),
67 message: "OFFSET must not be negative".into(),
68 }
69 } else {
70 SQLError::Routine {
71 sqlstate: "2201W".into(),
72 message: "LIMIT must not be negative".into(),
73 }
74 }
75}
76
77pub fn float_limit_offset(value: f64, label: &str) -> Result<u64, SQLError> {
78 let Value::Int(value) = crate::expr::cast_value(&Value::Float(value), "bigint")? else {
79 return Err(SQLError::Internal(
80 "float row-count coercion did not return an integer".into(),
81 ));
82 };
83 if value < 0 {
84 return Err(negative_row_count(label));
85 }
86 Ok(u64::try_from(value).expect("non-negative bigint fits u64"))
87}
88
89#[cfg(test)]
90mod tests {
91 use super::float_limit_offset;
92 #[test]
93 fn floating_limit_uses_postgresql_bigint_rounding_and_range_checks() {
94 assert_eq!(float_limit_offset(42.0, "LIMIT").unwrap(), 42);
95 assert_eq!(float_limit_offset(1.5, "LIMIT").unwrap(), 2);
96 assert_eq!(float_limit_offset(2.5, "LIMIT").unwrap(), 2);
97 assert_eq!(float_limit_offset(-0.5, "LIMIT").unwrap(), 0);
98 for value in [f64::NAN, f64::INFINITY, -1.0, 9_223_372_036_854_775_808.0] {
99 assert!(float_limit_offset(value, "LIMIT").is_err(), "{value}");
100 }
101 }
102}