use crate::command::ExpressionContext;
use crate::core::failed;
use crate::event::Event;
use crate::expr::{self, Scope};
use crate::failure::FailureCode;
use crate::schema::{FieldDef, FieldKind, FieldValue, TableDef};
pub(crate) fn prepare_insert(
table: &TableDef,
values: &[FieldValue],
) -> Result<Vec<FieldValue>, String> {
if values.len() != table.fields.len() {
return Err(format!(
"insert into {:?}: expected {} values, found {}",
table.name,
table.fields.len(),
values.len()
));
}
let mut out = values.to_vec();
for (index, field) in table.fields.iter().enumerate() {
if matches!(field.kind, FieldKind::AutoNumber(_)) && matches!(out[index], FieldValue::Null)
{
continue;
}
if let Some(default) = &field.properties.default_value
&& matches!(out[index], FieldValue::Null)
{
out[index] = eval_standalone(default, field, "default")?;
}
refuse_calculated(field, &out[index])?;
field
.validate_value(&out[index])
.map_err(|e| format!("field {:?}: {e}", field.name))?;
}
compute_calculated(table, &mut out)?;
enforce_rules(table, &out)?;
Ok(out)
}
pub(crate) fn prepare_update(
table: &TableDef,
current: &[FieldValue],
values: &[FieldValue],
) -> Result<Vec<FieldValue>, String> {
if values.len() != table.fields.len() {
return Err(format!(
"update {:?}: expected {} values, found {}",
table.name,
table.fields.len(),
values.len()
));
}
let mut out = values.to_vec();
for (index, field) in table.fields.iter().enumerate() {
if matches!(field.kind, FieldKind::AutoNumber(_)) {
if matches!(out[index], FieldValue::Null) {
out[index] = current[index].clone();
} else if out[index] != current[index] {
return Err(format!(
"field {:?}: AutoNumber values are engine-assigned and cannot be changed",
field.name
));
}
continue;
}
refuse_calculated(field, &out[index])?;
field
.validate_value(&out[index])
.map_err(|e| format!("field {:?}: {e}", field.name))?;
}
compute_calculated(table, &mut out)?;
enforce_rules(table, &out)?;
Ok(out)
}
fn refuse_calculated(field: &FieldDef, value: &FieldValue) -> Result<(), String> {
if field.properties.calculated.is_some() && !matches!(value, FieldValue::Null) {
return Err(format!(
"field {:?}: calculated values are engine-computed — leave the cell empty",
field.name
));
}
Ok(())
}
fn compute_calculated(table: &TableDef, out: &mut [FieldValue]) -> Result<(), String> {
for index in 0..table.fields.len() {
let field = &table.fields[index];
let Some(expression) = &field.properties.calculated else {
continue;
};
let value = row_scope(table, out).evaluate_prepared(expression, field)?;
out[index] = value;
field
.validate_value(&out[index])
.map_err(|e| format!("field {:?}: {e}", field.name))?;
}
Ok(())
}
fn enforce_rules(table: &TableDef, out: &[FieldValue]) -> Result<(), String> {
let scope = row_scope(table, out);
for field in &table.fields {
let Some(rule) = &field.properties.validation_rule else {
continue;
};
let accepted = scope.evaluate_prepared(rule, field)?;
if !matches!(accepted, FieldValue::Boolean(true)) {
let message = field
.properties
.validation_text
.clone()
.unwrap_or_else(|| format!("validation rule failed: {rule}"));
return Err(format!("field {:?}: {message}", field.name));
}
}
Ok(())
}
fn eval_standalone(source: &str, field: &FieldDef, role: &str) -> Result<FieldValue, String> {
expr::parse(source)
.and_then(|parsed| parsed.evaluate(&Scope::new()))
.map_err(|e| format!("field {:?}: {role} {source:?}: {e}", field.name))
}
pub(crate) fn row_scope(table: &TableDef, values: &[FieldValue]) -> Scope {
let mut scope = Scope::new();
for (field, value) in table.fields.iter().zip(values) {
scope.set_field(table.name.clone(), field.name.clone(), value.clone());
}
scope
}
trait ScopeEvaluate {
fn evaluate_prepared(&self, source: &str, field: &FieldDef) -> Result<FieldValue, String>;
}
impl ScopeEvaluate for Scope {
fn evaluate_prepared(&self, source: &str, field: &FieldDef) -> Result<FieldValue, String> {
expr::parse(source)
.and_then(|parsed| parsed.evaluate(self))
.map_err(|e| format!("field {:?}: expression {source:?}: {e}", field.name))
}
}
impl crate::core::Core {
pub fn run_expression(
&mut self,
context: Option<ExpressionContext>,
expression: &str,
) -> Result<FieldValue, String> {
let value = match context {
None => expr::parse(expression)
.and_then(|parsed| parsed.evaluate(&Scope::new()))
.map_err(|e| e.to_string())?,
Some(ExpressionContext { table, row }) => {
let Some(def) = self.table_definition(&table) else {
return Err(crate::port::PortError::TableNotFound(table).to_string());
};
let def = def.clone();
let current = self
.port
.read_rows(&def.name, crate::port::RowRange { start: row, len: 1 })
.map_err(|e| e.to_string())?;
let Some(row_values) = current.into_iter().next() else {
return Err(format!("row {row} of {:?} is out of range", def.name));
};
expr::parse(expression)
.and_then(|parsed| parsed.evaluate(&row_scope(&def, &row_values)))
.map_err(|e| e.to_string())?
}
};
self.state.expression_result = Some(value.clone());
Ok(value)
}
pub(crate) fn run_expression_command(
&mut self,
context: Option<ExpressionContext>,
expression: &str,
) -> Vec<Event> {
match self.run_expression(context, expression) {
Ok(value) => vec![Event::ExpressionEvaluated {
result: crate::present::display_value(&value),
}],
Err(reason) => {
self.state.expression_result = None;
failed(FailureCode::Parse, reason)
}
}
}
pub fn expression_result(&self) -> Option<&FieldValue> {
self.state.expression_result.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::NumericKind;
fn table() -> TableDef {
TableDef::new(
"T",
vec![
FieldDef::new("qty", FieldKind::Number(NumericKind::LongInteger)),
FieldDef::new("price", FieldKind::Currency),
],
)
}
#[test]
fn row_scope_resolves_bare_and_qualified_names() {
let scope = row_scope(
&table(),
&[FieldValue::Integer(2), FieldValue::Currency(300)],
);
let parsed = expr::parse("[qty] + [T].[price]").expect("parses");
assert!(parsed.evaluate(&scope).is_ok());
}
#[test]
fn prepare_insert_enforces_arity() {
let reason =
prepare_insert(&table(), &[FieldValue::Integer(1)]).expect_err("arity must fail");
assert!(reason.contains("expected 2 values"), "{reason}");
}
#[test]
fn rules_see_engine_assigned_autonumbers_as_null() {
let mut table = table();
table.fields.insert(
0,
FieldDef::new(
"id",
FieldKind::AutoNumber(crate::schema::AutoKind::LongInteger),
),
);
table.fields[1].properties.validation_rule = Some("[id] Is Null".into());
let out = prepare_insert(
&table,
&[
FieldValue::Null,
FieldValue::Integer(1),
FieldValue::Currency(1),
],
)
.expect("engine-assigned slots stay Null for the rule");
assert_eq!(out[0], FieldValue::Null);
}
}