Skip to main content

uqa_sql/semantics/
row_count.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL LIMIT and OFFSET bigint conversion and diagnostics.
8
9use 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}