use crate::error::{Result, SqlError};
use crate::planner::declared_type_coerce::coerce_rows_to_declared_types;
use crate::types::*;
pub(crate) fn coerce_and_check_rows(
info: &CollectionInfo,
rows: &mut [Vec<(String, SqlValue)>],
) -> Result<()> {
coerce_rows_to_declared_types(&info.columns, rows, info.primary_key.as_deref())?;
check_declared_int_ranges(&info.columns, rows)?;
check_declared_float_ranges(&info.columns, rows)
}
pub(crate) fn check_declared_int_ranges(
columns: &[ColumnInfo],
rows: &[Vec<(String, SqlValue)>],
) -> Result<()> {
if !columns.iter().any(|c| {
matches!(
c.int_width,
Some(nodedb_types::columnar::IntWidth::I16 | nodedb_types::columnar::IntWidth::I32)
)
}) {
return Ok(());
}
for row in rows {
for (name, value) in row {
let SqlValue::Int(v) = value else { continue };
let Some(width) = columns
.iter()
.find(|c| c.name.eq_ignore_ascii_case(name))
.and_then(|c| c.int_width)
else {
continue;
};
if !width.contains(*v) {
return Err(SqlError::IntegerOutOfRange {
column: name.clone(),
value: *v,
declared_type: width.pg_type_name(),
});
}
}
}
Ok(())
}
pub(crate) fn check_declared_float_ranges(
columns: &[ColumnInfo],
rows: &[Vec<(String, SqlValue)>],
) -> Result<()> {
if !columns
.iter()
.any(|c| c.float_width == Some(nodedb_types::columnar::FloatWidth::F32))
{
return Ok(());
}
for row in rows {
for (name, value) in row {
let SqlValue::Float(v) = value else { continue };
let Some(width) = columns
.iter()
.find(|c| c.name.eq_ignore_ascii_case(name))
.and_then(|c| c.float_width)
else {
continue;
};
if width != nodedb_types::columnar::FloatWidth::F32 {
continue;
}
if v.is_finite() && !(*v as f32).is_finite() {
return Err(SqlError::FloatOutOfRange {
column: name.clone(),
value: *v,
declared_type: width.pg_type_name(),
});
}
}
}
Ok(())
}
pub(crate) fn check_declared_int_ranges_in_assignments(
columns: &[ColumnInfo],
assignments: &[(String, SqlExpr)],
) -> Result<()> {
let literals: Vec<(String, SqlValue)> = assignments
.iter()
.filter_map(|(col, expr)| match expr {
SqlExpr::Literal(v @ SqlValue::Int(_)) => Some((col.clone(), v.clone())),
_ => None,
})
.collect();
if literals.is_empty() {
return Ok(());
}
check_declared_int_ranges(columns, std::slice::from_ref(&literals))
}
pub(crate) fn check_declared_float_ranges_in_assignments(
columns: &[ColumnInfo],
assignments: &[(String, SqlExpr)],
) -> Result<()> {
let literals: Vec<(String, SqlValue)> = assignments
.iter()
.filter_map(|(col, expr)| match expr {
SqlExpr::Literal(v @ SqlValue::Float(_)) => Some((col.clone(), v.clone())),
_ => None,
})
.collect();
if literals.is_empty() {
return Ok(());
}
check_declared_float_ranges(columns, std::slice::from_ref(&literals))
}
#[cfg(test)]
mod float_range_tests {
use super::*;
use nodedb_types::columnar::FloatWidth;
fn float_column(name: &str, width: Option<FloatWidth>) -> ColumnInfo {
ColumnInfo {
name: name.to_string(),
data_type: SqlDataType::Float64,
nullable: true,
is_primary_key: false,
default: None,
raw_type: None,
int_width: None,
float_width: width,
}
}
fn row(name: &str, value: f64) -> Vec<Vec<(String, SqlValue)>> {
vec![vec![(name.to_string(), SqlValue::Float(value))]]
}
#[test]
fn out_of_f32_range_literal_into_real_column_is_rejected() {
let columns = [float_column("r", Some(FloatWidth::F32))];
let rows = row("r", 1e300);
let err = check_declared_float_ranges(&columns, &rows).expect_err("must overflow f32");
assert!(matches!(err, SqlError::FloatOutOfRange { .. }));
}
#[test]
fn same_literal_into_double_column_is_accepted() {
let columns = [float_column("r", Some(FloatWidth::F64))];
let rows = row("r", 1e300);
check_declared_float_ranges(&columns, &rows).expect("f64 has no narrower width to check");
}
#[test]
fn merely_rounding_value_into_real_column_is_accepted() {
let columns = [float_column("r", Some(FloatWidth::F32))];
let rows = row("r", 1.1);
check_declared_float_ranges(&columns, &rows).expect("rounding must not be rejected");
}
#[test]
fn f32_max_itself_is_accepted() {
let columns = [float_column("r", Some(FloatWidth::F32))];
let rows = row("r", f32::MAX as f64);
check_declared_float_ranges(&columns, &rows).expect("f32::MAX fits f32 exactly");
}
#[test]
fn already_infinite_or_nan_literal_is_accepted() {
let columns = [float_column("r", Some(FloatWidth::F32))];
for v in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN] {
let rows = row("r", v);
check_declared_float_ranges(&columns, &rows)
.expect("already-infinite/NaN source values are not an overflow");
}
}
#[test]
fn no_declared_float_width_skips_check() {
let columns = [float_column("r", None)];
let rows = row("r", 1e300);
check_declared_float_ranges(&columns, &rows).expect("untyped float column is unchecked");
}
}