use sqlparser::ast;
#[derive(Debug, PartialEq)]
pub enum RowBound {
Rows(usize),
Unbounded,
}
impl RowBound {
pub fn limit(self) -> Option<usize> {
match self {
RowBound::Rows(n) => Some(n),
RowBound::Unbounded => None,
}
}
pub fn offset(self) -> usize {
match self {
RowBound::Rows(n) => n,
RowBound::Unbounded => 0,
}
}
}
#[derive(Debug, PartialEq)]
pub struct InvalidRowBoundLiteral {
pub raw: String,
}
pub fn as_usize_literal(value: &ast::Value) -> Result<usize, InvalidRowBoundLiteral> {
match value {
ast::Value::Number(n, _) => n
.parse::<usize>()
.map_err(|_| InvalidRowBoundLiteral { raw: n.clone() }),
ast::Value::SingleQuotedString(s) => s
.parse::<usize>()
.map_err(|_| InvalidRowBoundLiteral { raw: s.clone() }),
other => Err(InvalidRowBoundLiteral {
raw: other.to_string(),
}),
}
}
pub fn expr_as_usize_literal(expr: &ast::Expr) -> Result<usize, InvalidRowBoundLiteral> {
match expr {
ast::Expr::Value(v) => as_usize_literal(&v.value),
other => Err(InvalidRowBoundLiteral {
raw: other.to_string(),
}),
}
}
pub fn checked_row_bound(clause: &'static str, expr: &ast::Expr) -> crate::error::Result<RowBound> {
if matches!(expr, ast::Expr::Value(v) if matches!(v.value, ast::Value::Null)) {
return Ok(RowBound::Unbounded);
}
expr_as_usize_literal(expr)
.map(RowBound::Rows)
.map_err(|e| crate::error::SqlError::InvalidLimitValue {
clause,
value: e.raw,
})
}
pub fn as_f64_literal(value: &ast::Value) -> Option<f64> {
match value {
ast::Value::Number(n, _) => n.parse::<f64>().ok(),
ast::Value::SingleQuotedString(s) => s.parse::<f64>().ok(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usize_from_number() {
assert_eq!(
as_usize_literal(&ast::Value::Number("42".into(), false)),
Ok(42)
);
}
#[test]
fn usize_from_unknown_param_text() {
assert_eq!(
as_usize_literal(&ast::Value::SingleQuotedString("42".into())),
Ok(42)
);
}
#[test]
fn usize_rejects_non_numeric_text() {
assert_eq!(
as_usize_literal(&ast::Value::SingleQuotedString("abc".into())),
Err(InvalidRowBoundLiteral { raw: "abc".into() })
);
}
#[test]
fn usize_rejects_negative() {
assert_eq!(
as_usize_literal(&ast::Value::SingleQuotedString("-1".into())),
Err(InvalidRowBoundLiteral { raw: "-1".into() })
);
}
#[test]
fn f64_from_unknown_param_text() {
assert_eq!(
as_f64_literal(&ast::Value::SingleQuotedString("1.5".into())),
Some(1.5)
);
}
#[test]
fn usize_rejects_overflow_number() {
let huge = format!("{}0", usize::MAX);
assert_eq!(
as_usize_literal(&ast::Value::Number(huge.clone(), false)),
Err(InvalidRowBoundLiteral { raw: huge })
);
}
#[test]
fn usize_rejects_overflow_text() {
let huge = format!("{}0", usize::MAX);
assert_eq!(
as_usize_literal(&ast::Value::SingleQuotedString(huge.clone())),
Err(InvalidRowBoundLiteral { raw: huge })
);
}
#[test]
fn usize_rejects_fractional_number() {
assert_eq!(
as_usize_literal(&ast::Value::Number("1.5".into(), false)),
Err(InvalidRowBoundLiteral { raw: "1.5".into() })
);
}
#[test]
fn usize_rejects_fractional_text() {
assert_eq!(
as_usize_literal(&ast::Value::SingleQuotedString("1.5".into())),
Err(InvalidRowBoundLiteral { raw: "1.5".into() })
);
}
#[test]
fn usize_rejects_scientific_notation() {
assert_eq!(
as_usize_literal(&ast::Value::Number("1e3".into(), false)),
Err(InvalidRowBoundLiteral { raw: "1e3".into() })
);
}
#[test]
fn usize_accepts_zero() {
assert_eq!(
as_usize_literal(&ast::Value::Number("0".into(), false)),
Ok(0)
);
}
#[test]
fn usize_accepts_max() {
let max_str = usize::MAX.to_string();
assert_eq!(
as_usize_literal(&ast::Value::Number(max_str, false)),
Ok(usize::MAX)
);
}
#[test]
fn f64_accepts_negative() {
assert_eq!(
as_f64_literal(&ast::Value::SingleQuotedString("-1.5".into())),
Some(-1.5)
);
}
#[test]
fn f64_overflow_produces_infinity() {
let out = as_f64_literal(&ast::Value::Number("1e400".into(), false));
assert!(matches!(out, Some(f) if f.is_infinite()));
}
#[test]
fn f64_rejects_non_numeric_text() {
assert_eq!(
as_f64_literal(&ast::Value::SingleQuotedString("foo".into())),
None
);
}
}