use crate::error::SqlError;
use crate::value::{ColumnInfo, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SizeGuards {
pub max_cell_bytes: usize,
pub max_row_bytes: usize,
pub max_total_buffered_bytes: usize,
}
impl Default for SizeGuards {
fn default() -> Self {
Self {
max_cell_bytes: 64 * 1024 * 1024,
max_row_bytes: 256 * 1024 * 1024,
max_total_buffered_bytes: 1024 * 1024 * 1024,
}
}
}
impl SizeGuards {
#[must_use]
pub fn unlimited() -> Self {
Self {
max_cell_bytes: 0,
max_row_bytes: 0,
max_total_buffered_bytes: 0,
}
}
pub fn check_row(
&self,
row_ordinal: u64,
row: &[Value],
columns: &[ColumnInfo],
) -> Result<(), SqlError> {
let mut row_total: usize = 0;
for (i, cell) in row.iter().enumerate() {
let size = cell.byte_size();
if self.max_cell_bytes != 0 && size > self.max_cell_bytes {
return Err(SqlError::CellTooLarge {
row: row_ordinal,
column: column_label(columns, i),
size,
cap: self.max_cell_bytes,
});
}
row_total = row_total.saturating_add(size);
if self.max_row_bytes != 0 && row_total > self.max_row_bytes {
return Err(SqlError::RowTooLarge {
row: row_ordinal,
size: row_total,
cap: self.max_row_bytes,
});
}
}
Ok(())
}
#[must_use]
pub fn caps_total(&self) -> bool {
self.max_total_buffered_bytes != 0
}
}
fn column_label(columns: &[ColumnInfo], idx: usize) -> String {
columns
.get(idx)
.map(|c| c.name.clone())
.unwrap_or_else(|| format!("#{idx}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::TypeHint;
fn col(name: &str) -> ColumnInfo {
ColumnInfo {
name: name.to_string(),
type_hint: TypeHint::Other,
nullable: true,
}
}
#[test]
fn unlimited_passes_everything() {
let g = SizeGuards::unlimited();
let row = vec![Value::Bytes(vec![0u8; 10_000_000]), Value::Int64(1)];
g.check_row(0, &row, &[col("blob"), col("n")])
.expect("unlimited guards never trip");
}
#[test]
fn oversized_cell_fails_with_column_name() {
let g = SizeGuards {
max_cell_bytes: 1024,
max_row_bytes: 0,
max_total_buffered_bytes: 0,
};
let row = vec![Value::Int64(7), Value::String("x".repeat(2048))];
let err = g
.check_row(3, &row, &[col("id"), col("payload")])
.expect_err("oversized cell must fail");
match err {
SqlError::CellTooLarge {
row,
column,
size,
cap,
} => {
assert_eq!(row, 3);
assert_eq!(column, "payload");
assert_eq!(size, 2048);
assert_eq!(cap, 1024);
}
other => panic!("expected CellTooLarge, got {other:?}"),
}
}
#[test]
fn oversized_row_fails_even_when_each_cell_fits() {
let g = SizeGuards {
max_cell_bytes: 1000,
max_row_bytes: 1000,
max_total_buffered_bytes: 0,
};
let row = vec![
Value::String("a".repeat(600)),
Value::String("b".repeat(600)),
];
let err = g
.check_row(0, &row, &[col("a"), col("b")])
.expect_err("oversized row must fail");
assert!(matches!(err, SqlError::RowTooLarge { row: 0, .. }));
}
#[test]
fn cell_label_falls_back_to_index_without_metadata() {
let g = SizeGuards {
max_cell_bytes: 8,
max_row_bytes: 0,
max_total_buffered_bytes: 0,
};
let row = vec![Value::String("too long".repeat(4))];
let err = g.check_row(0, &row, &[]).expect_err("must fail");
match err {
SqlError::CellTooLarge { column, .. } => assert_eq!(column, "#0"),
other => panic!("expected CellTooLarge, got {other:?}"),
}
}
}