use std::cmp::Ordering;
use sqlparser::ast::{
BinaryOperator, Expr, FunctionArg, FunctionArgExpr, FunctionArguments, UnaryOperator, Value,
};
use super::names::ident_name;
use crate::schema::{BaseType, Column, LengthRange, NumberBound, ValueRange};
pub(super) struct RangeCheck {
pub(super) column: String,
bounds: Vec<Bound>,
}
struct Bound {
length: bool,
lower: bool,
strict: bool,
literal: Decimal,
}
pub(super) fn range_check(expr: &Expr) -> Option<RangeCheck> {
let mut bounds = Vec::new();
let column = collect_bounds(expr, &mut bounds)?;
Some(RangeCheck { column, bounds })
}
fn collect_bounds(expr: &Expr, bounds: &mut Vec<Bound>) -> Option<String> {
match expr {
Expr::Nested(inner) => collect_bounds(inner, bounds),
Expr::BinaryOp {
left,
op: BinaryOperator::And,
right,
} => {
let column = collect_bounds(left, bounds)?;
(collect_bounds(right, bounds)? == column).then_some(column)
}
Expr::BinaryOp { left, op, right } => {
let (lower, strict) = match op {
BinaryOperator::Gt => (true, true),
BinaryOperator::GtEq => (true, false),
BinaryOperator::Lt => (false, true),
BinaryOperator::LtEq => (false, false),
_ => return None,
};
let ((column, length), literal, lower) = match (subject(left), literal(right)) {
(Some(subject), Some(literal)) => (subject, literal, lower),
_ => (subject(right)?, literal(left)?, !lower),
};
bounds.push(Bound {
length,
lower,
strict,
literal,
});
Some(column)
}
Expr::Between {
expr,
negated: false,
low,
high,
} => {
let (column, length) = subject(expr)?;
for (lower, literal) in [(true, literal(low)?), (false, literal(high)?)] {
bounds.push(Bound {
length,
lower,
strict: false,
literal,
});
}
Some(column)
}
_ => None,
}
}
fn subject(expr: &Expr) -> Option<(String, bool)> {
match expr {
Expr::Nested(inner) => subject(inner),
Expr::Identifier(column) => Some((ident_name(column), false)),
Expr::Function(function)
if function.filter.is_none()
&& function.over.is_none()
&& function.within_group.is_empty()
&& matches!(function.parameters, FunctionArguments::None) =>
{
let [name] = function.name.0.as_slice() else {
return None;
};
let name = name.as_ident()?.value.to_ascii_lowercase();
if !matches!(name.as_str(), "char_length" | "character_length" | "length") {
return None;
}
let FunctionArguments::List(list) = &function.args else {
return None;
};
let [FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Identifier(column)))] =
list.args.as_slice()
else {
return None;
};
(list.duplicate_treatment.is_none() && list.clauses.is_empty())
.then(|| (ident_name(column), true))
}
_ => None,
}
}
fn literal(expr: &Expr) -> Option<Decimal> {
match expr {
Expr::Nested(inner) => literal(inner),
Expr::UnaryOp {
op: UnaryOperator::Minus,
expr,
} => literal(expr)?.negated(),
Expr::UnaryOp {
op: UnaryOperator::Plus,
expr,
} => literal(expr),
Expr::Value(value) => match &value.value {
Value::Number(text, _) => Decimal::parse(text),
_ => None,
},
_ => None,
}
}
pub(super) fn apply_range_check(column: &mut Column, check: RangeCheck) {
for bound in check.bounds {
if bound.length {
narrow_length(column, &bound);
} else {
narrow_value(column, bound);
}
}
}
fn narrow_length(column: &mut Column, bound: &Bound) {
if column.base_type() != BaseType::Text {
return;
}
let Some(limit) = bound
.literal
.integer()
.and_then(|limit| inclusive(limit, bound))
else {
return;
};
let range = column.length_range.get_or_insert(LengthRange {
min: None,
max: None,
});
if bound.lower {
if let Ok(min) = u32::try_from(limit)
&& min > 0
{
range.min = Some(range.min.map_or(min, |earlier| earlier.max(min)));
}
} else if let Ok(max) = u32::try_from(limit) {
range.max = Some(range.max.map_or(max, |earlier| earlier.min(max)));
}
if range.min.is_none() && range.max.is_none() {
column.length_range = None;
}
}
fn narrow_value(column: &mut Column, bound: Bound) {
let Some(number) = number_bound(column, &bound) else {
return;
};
let range = column.value_range.get_or_insert(ValueRange {
min: None,
max: None,
});
let (slot, tighter) = if bound.lower {
(&mut range.min, Ordering::Greater)
} else {
(&mut range.max, Ordering::Less)
};
let replace = match slot {
None => true,
Some(earlier) => {
let earlier_value = Decimal::parse(&earlier.value);
let new_value = Decimal::parse(&number.value);
match earlier_value
.zip(new_value)
.and_then(|(a, b)| b.compare(&a))
{
Some(Ordering::Equal) => number.exclusive && !earlier.exclusive,
Some(order) => order == tighter,
None => false,
}
}
};
if replace {
*slot = Some(number);
}
}
fn number_bound(column: &Column, bound: &Bound) -> Option<NumberBound> {
let literal = &bound.literal;
let exact = match column.base_type() {
BaseType::Integer => {
let (min, max) = BaseType::integer_bounds(&column.sqlx_type)?;
let value = inclusive(literal.integer()?, bound)?;
return (min..=max).contains(&value).then(|| NumberBound {
value: value.to_string(),
exclusive: false,
});
}
BaseType::Float => literal.fits_binary(BaseType::float_mantissa_digits(&column.sqlx_type)?),
BaseType::Decimal => {
if !literal.fits_rust_decimal() {
return None;
}
true
}
_ => return None,
};
if bound.strict && !exact {
return None;
}
Some(NumberBound {
value: literal.to_string(),
exclusive: bound.strict,
})
}
fn inclusive(limit: i128, bound: &Bound) -> Option<i128> {
match (bound.strict, bound.lower) {
(false, _) => Some(limit),
(true, true) => limit.checked_add(1),
(true, false) => limit.checked_sub(1),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Decimal {
mantissa: i128,
scale: u32,
}
impl Decimal {
fn parse(text: &str) -> Option<Self> {
let (negative, text) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text),
};
let (whole, fraction) = text.split_once('.').unwrap_or((text, ""));
if whole.is_empty() && fraction.is_empty() {
return None;
}
let fraction = fraction.trim_end_matches('0');
let mut mantissa: i128 = 0;
for character in whole.chars().chain(fraction.chars()) {
let digit = character.to_digit(10)?;
mantissa = mantissa.checked_mul(10)?.checked_add(i128::from(digit))?;
}
let decimal = Self {
mantissa,
scale: u32::try_from(fraction.len()).ok()?,
};
if negative {
decimal.negated()
} else {
Some(decimal)
}
}
fn negated(self) -> Option<Self> {
Some(Self {
mantissa: self.mantissa.checked_neg()?,
..self
})
}
fn integer(&self) -> Option<i128> {
(self.scale == 0).then_some(self.mantissa)
}
fn compare(&self, other: &Self) -> Option<Ordering> {
let scale = self.scale.max(other.scale);
let left = self
.mantissa
.checked_mul(10i128.checked_pow(scale - self.scale)?)?;
let right = other
.mantissa
.checked_mul(10i128.checked_pow(scale - other.scale)?)?;
Some(left.cmp(&right))
}
fn fits_binary(&self, digits: u32) -> bool {
let Some(fives) = 5u128.checked_pow(self.scale) else {
return false;
};
let magnitude = self.mantissa.unsigned_abs();
if !magnitude.is_multiple_of(fives) {
return false;
}
let odd = magnitude / fives;
let odd = if odd == 0 {
0
} else {
odd >> odd.trailing_zeros()
};
odd < 1u128 << digits
}
fn fits_rust_decimal(&self) -> bool {
self.scale <= 28 && self.mantissa.unsigned_abs() < 1u128 << 96
}
}
impl std::fmt::Display for Decimal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let sign = if self.mantissa < 0 { "-" } else { "" };
let digits = self.mantissa.unsigned_abs().to_string();
let scale = self.scale as usize;
if scale == 0 {
return write!(formatter, "{sign}{digits}");
}
let digits = format!("{digits:0>width$}", width = scale + 1);
let (whole, fraction) = digits.split_at(digits.len() - scale);
write!(formatter, "{sign}{whole}.{fraction}")
}
}