use super::params::KvInsertParams;
use super::range_check::{
check_declared_float_ranges, check_declared_float_ranges_in_assignments,
check_declared_int_ranges, check_declared_int_ranges_in_assignments,
};
use super::value_convert::expr_to_sql_value;
use crate::error::{Result, SqlError};
use crate::planner::declared_type_coerce::{
coerce_assignments_to_declared_types, coerce_row_to_declared_types,
};
use crate::planner::defaults::ColumnDefaults;
use crate::types::*;
pub(crate) fn build_kv_insert_plan(params: KvInsertParams<'_>) -> Result<Vec<SqlPlan>> {
let KvInsertParams {
collection: table_name,
columns,
rows_ast,
intent,
mut on_conflict_updates,
pk_col,
declared_columns,
catalog,
} = params;
if columns.is_empty() {
return Err(SqlError::PositionalKvInsertUnsupported {
collection: table_name,
});
}
let key_col_name = pk_col.unwrap_or("key");
let excluded_from_value = |name: &str| {
(key_col_name == "key" && name == "key") || name == "ttl"
};
let compiled_defaults = ColumnDefaults::compile_columns(declared_columns)?;
let mut coerced_rows: Vec<Vec<(String, SqlValue)>> = Vec::with_capacity(rows_ast.len());
let mut volatile_defaults = false;
for row_exprs in rows_ast {
let mut row: Vec<(String, SqlValue)> = Vec::with_capacity(columns.len());
for (i, col) in columns.iter().enumerate() {
let Some(expr) = row_exprs.get(i) else { break };
row.push((col.clone(), expr_to_sql_value(expr)?));
}
volatile_defaults |= compiled_defaults.materialize_row(&mut row, catalog)?;
coerce_row_to_declared_types(declared_columns, &mut row, Some(key_col_name))?;
coerced_rows.push(row);
}
check_declared_int_ranges(declared_columns, &coerced_rows)?;
check_declared_float_ranges(declared_columns, &coerced_rows)?;
coerce_assignments_to_declared_types(
declared_columns,
&mut on_conflict_updates,
Some(key_col_name),
)?;
check_declared_int_ranges_in_assignments(declared_columns, &on_conflict_updates)?;
check_declared_float_ranges_in_assignments(declared_columns, &on_conflict_updates)?;
let mut entries = Vec::with_capacity(coerced_rows.len());
let mut ttl_secs: u64 = 0;
for row in &coerced_rows {
let key_val = match row.iter().find(|(name, _)| name == key_col_name) {
Some((_, value)) => value.clone(),
None => SqlValue::Null,
};
if let Some((_, value)) = row.iter().find(|(name, _)| name == "ttl") {
match value {
SqlValue::Int(n) => ttl_secs = (*n).max(0) as u64,
SqlValue::Float(f) => ttl_secs = f.max(0.0) as u64,
_ => {}
}
}
let value_cols: Vec<(String, SqlValue)> = row
.iter()
.filter(|(name, _)| !excluded_from_value(name))
.cloned()
.collect();
entries.push((key_val, value_cols));
}
Ok(vec![SqlPlan::KvInsert(KvInsertPlan {
collection: table_name,
entries,
ttl_secs,
intent,
on_conflict_updates,
volatile_defaults,
})])
}
#[cfg(test)]
mod kv_on_conflict_range_tests {
use sqlparser::ast;
use sqlparser::ast::{Expr, Value, ValueWithSpan};
use sqlparser::tokenizer::Span;
use super::*;
use crate::catalog::SqlCatalog;
use nodedb_types::columnar::{FloatWidth, IntWidth};
struct NoCatalog;
impl SqlCatalog for NoCatalog {
fn get_collection(
&self,
_database_id: nodedb_types::DatabaseId,
_name: &str,
) -> std::result::Result<Option<CollectionInfo>, crate::catalog::SqlCatalogError> {
Ok(None)
}
}
fn string_column(name: &str) -> ColumnInfo {
ColumnInfo {
name: name.to_string(),
data_type: SqlDataType::String,
nullable: true,
is_primary_key: false,
default: None,
raw_type: None,
int_width: None,
float_width: None,
}
}
fn int_column(name: &str, width: Option<IntWidth>) -> ColumnInfo {
ColumnInfo {
name: name.to_string(),
data_type: SqlDataType::Int64,
nullable: true,
is_primary_key: false,
default: None,
raw_type: None,
int_width: width,
float_width: None,
}
}
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 key_value_expr(key: &str) -> Expr {
Expr::Value(ValueWithSpan {
value: Value::SingleQuotedString(key.to_string()),
span: Span::empty(),
})
}
fn plan_with_on_conflict(updates: Vec<(String, SqlExpr)>) -> Result<Vec<SqlPlan>> {
let declared = [
string_column("key"),
int_column("n", Some(IntWidth::I32)),
float_column("r", Some(FloatWidth::F32)),
];
build_kv_insert_plan(KvInsertParams {
collection: "t".to_string(),
columns: &["key".to_string()],
rows_ast: &[ast::Parens::with_empty_span(vec![key_value_expr("a")])],
intent: KvInsertIntent::Put,
on_conflict_updates: updates,
pk_col: Some("key"),
declared_columns: &declared,
catalog: &NoCatalog,
})
}
#[test]
fn on_conflict_int_beyond_declared_width_is_rejected() {
let updates = vec![(
"n".to_string(),
SqlExpr::Literal(SqlValue::Int(9_876_543_210)),
)];
let err = plan_with_on_conflict(updates).expect_err("i32 column must reject overflow");
assert!(matches!(err, SqlError::IntegerOutOfRange { .. }));
}
#[test]
fn on_conflict_int_in_range_is_accepted() {
let updates = vec![("n".to_string(), SqlExpr::Literal(SqlValue::Int(42)))];
plan_with_on_conflict(updates).expect("in-range i32 literal must be accepted");
}
#[test]
fn on_conflict_float_beyond_f32_range_is_rejected() {
let updates = vec![("r".to_string(), SqlExpr::Literal(SqlValue::Float(1e300)))];
let err = plan_with_on_conflict(updates).expect_err("real column must reject f32 overflow");
assert!(matches!(err, SqlError::FloatOutOfRange { .. }));
}
#[test]
fn on_conflict_float_rounding_is_accepted() {
let updates = vec![("r".to_string(), SqlExpr::Literal(SqlValue::Float(1.1)))];
plan_with_on_conflict(updates).expect("rounding into f32 must not be rejected");
}
}