use std::{borrow::Cow, collections::{BTreeMap, HashMap}};
use tempest_core::tempest_str::TempestStr;
use tempest_tql::ast::{BinaryOpKind, Expr, ExprKind, InsertValues, IsPayload, PathSegment};
use crate::{
catalog::{
CatalogError, CatalogState,
schema::{FieldDef, FieldId, FlatField, TypeExpr, TypeSchema},
},
query::plan::PlanError,
types::{TempestType, TempestValue},
};
#[derive(Debug, Clone)]
pub(crate) enum CompiledExpr {
Literal(TempestValue<'static>),
ColumnRef(usize),
Binary {
left: Box<CompiledExpr>,
op: BinaryOpKind,
right: Box<CompiledExpr>,
},
IsVariant {
expr: Box<CompiledExpr>,
type_id: u32,
variant_id: u32,
payload_checks: Vec<(usize, TempestValue<'static>)>,
},
PayloadField {
source: Box<CompiledExpr>,
index: usize,
},
}
#[derive(Debug, Clone)]
pub(crate) struct PatternBinding {
pub name: String,
pub expr: CompiledExpr,
pub ty: TempestType,
}
fn check_binop(
left_ty: TempestType,
right_ty: TempestType,
op: BinaryOpKind,
) -> Result<TempestType, PlanError> {
type Op = BinaryOpKind;
match (left_ty, right_ty, op) {
(TempestType::Bool, TempestType::Bool, Op::Eq | Op::Neq | Op::And | Op::Or) => {
Ok(TempestType::Bool)
}
(
TempestType::Int64,
TempestType::Int64,
Op::Eq | Op::Neq | Op::Gt | Op::Lt | Op::Gte | Op::Lte,
) => Ok(TempestType::Bool),
(TempestType::String, TempestType::String, Op::Eq | Op::Neq) => Ok(TempestType::Bool),
(left, right, op) => Err(PlanError::UnsupportedOperand { left, right, op }),
}
}
fn path_lookup(path: &tempest_tql::ast::Path<'_>, alias: Option<&str>) -> String {
if let Some(alias) = alias {
if let tempest_tql::ast::PathSegment::Named(first) = &path.segments[0] {
if first.name.as_ref() == alias && path.segments.len() > 1 {
return path.segments[1..]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(".");
}
}
}
path.render().to_string()
}
fn try_resolve_enum_literal(
path: &tempest_tql::ast::Path<'_>,
catalog: &CatalogState,
) -> Option<TempestValue<'static>> {
use tempest_tql::ast::PathSegment;
let segs = &path.segments;
match segs.len() {
2 => {
let (PathSegment::Named(ty_name), PathSegment::Named(var_name)) = (&segs[0], &segs[1]) else {
return None;
};
let (type_id, type_schema) = catalog.get_global_type_by_name(&ty_name.name)?;
let enum_schema = type_schema.as_enum()?;
let (variant_id, _) = enum_schema.variants.iter().find(|(_, v)| v.name == var_name.name)?;
Some(TempestValue::Enum { type_id: *type_id, variant_id: **variant_id, fields: vec![] })
}
3 => {
let (PathSegment::Named(db), PathSegment::Named(ty_name), PathSegment::Named(var_name)) =
(&segs[0], &segs[1], &segs[2])
else {
return None;
};
let (db_id, _) = catalog.get_database_by_name(&db.name)?;
let (type_id, type_schema) = catalog.get_type_by_name(db_id, &ty_name.name)?;
let enum_schema = type_schema.as_enum()?;
let (variant_id, _) = enum_schema.variants.iter().find(|(_, v)| v.name == var_name.name)?;
Some(TempestValue::Enum { type_id: *type_id, variant_id: **variant_id, fields: vec![] })
}
_ => None,
}
}
pub(crate) fn compile_expr(
expr: Expr<'_>,
flat_fields: &[FlatField],
alias: Option<&str>,
catalog: &CatalogState,
) -> Result<(CompiledExpr, TempestType), PlanError> {
let mut out_bindings = Vec::new();
compile_expr_inner(expr, flat_fields, alias, catalog, &[], &mut out_bindings)
}
fn is_wildcard_pat(e: &ExprKind<'_>) -> bool {
matches!(e, ExprKind::Path(p) if p.segments.len() == 1 && matches!(&p.segments[0], PathSegment::Named(_)))
}
fn resolve_payload_field_ty(
ty_expr: &crate::catalog::schema::TypeExpr,
type_args: &[crate::catalog::schema::TypeExpr],
) -> TempestType {
use crate::catalog::schema::TypeExpr as CTypeExpr;
match ty_expr {
CTypeExpr::Primitive(t) => *t,
CTypeExpr::GenericParam(i) => match type_args.get(*i as usize) {
Some(CTypeExpr::Primitive(t)) => *t,
_ => panic!("unresolvable generic param in payload field"),
},
CTypeExpr::Ref(..) => panic!("ref type in payload field not yet supported"),
}
}
#[instrument(skip_all, level = "trace")]
fn compile_expr_inner(
expr: Expr<'_>,
flat_fields: &[FlatField],
alias: Option<&str>,
catalog: &CatalogState,
bindings: &[PatternBinding],
out_bindings: &mut Vec<PatternBinding>,
) -> Result<(CompiledExpr, TempestType), PlanError> {
match expr.kind {
ExprKind::IntegerLiteral(i) => Ok((
CompiledExpr::Literal(TempestValue::Int64(i.parse().unwrap())),
TempestType::Int64,
)),
ExprKind::StringLiteral(s) => Ok((
CompiledExpr::Literal(TempestValue::String(Cow::Owned(s.into_owned()))),
TempestType::String,
)),
ExprKind::Bool(b) => Ok((
CompiledExpr::Literal(TempestValue::Bool(b)),
TempestType::Bool,
)),
ExprKind::Path(path) => {
if path.segments.len() == 1 {
if let PathSegment::Named(ident) = &path.segments[0] {
if let Some(b) = bindings.iter().rev().find(|b| b.name == ident.name.as_ref()) {
return Ok((b.expr.clone(), b.ty));
}
}
}
let lookup = path_lookup(&path, alias);
if let Some(pos) = flat_fields
.iter()
.position(|f| f.name.as_ref() == lookup.as_str())
{
let ty = flat_fields[pos].ty;
trace!(lookup, pos, ?ty, "resolved path as column ref");
return Ok((CompiledExpr::ColumnRef(pos), ty));
}
if let Some(val) = try_resolve_enum_literal(&path, catalog) {
let ty = val.ty();
trace!(?ty, "resolved path as enum literal");
return Ok((CompiledExpr::Literal(val), ty));
}
debug!(lookup, "field not found in flat schema");
Err(PlanError::FieldNotFound(
TempestStr::from_owned(lookup).expect("field name is valid"),
))
}
ExprKind::BinaryOp { left, op, right } => {
if op == BinaryOpKind::And {
let mut lhs_bindings = Vec::new();
let (left_compiled, left_ty) =
compile_expr_inner(*left, flat_fields, alias, catalog, bindings, &mut lhs_bindings)?;
let extended: Vec<PatternBinding> = bindings.iter().chain(&lhs_bindings).cloned().collect();
let (right_compiled, right_ty) =
compile_expr_inner(*right, flat_fields, alias, catalog, &extended, out_bindings)?;
out_bindings.extend(lhs_bindings);
let ty = check_binop(left_ty, right_ty, op)?;
return Ok((
CompiledExpr::Binary {
left: Box::new(left_compiled),
op,
right: Box::new(right_compiled),
},
ty,
));
}
let (left_compiled, left_ty) =
compile_expr_inner(*left, flat_fields, alias, catalog, bindings, out_bindings)?;
let (right_compiled, right_ty) =
compile_expr_inner(*right, flat_fields, alias, catalog, bindings, out_bindings)?;
let ty = check_binop(left_ty, right_ty, op)?;
Ok((
CompiledExpr::Binary {
left: Box::new(left_compiled),
op,
right: Box::new(right_compiled),
},
ty,
))
}
ExprKind::TupleLiteral(_) | ExprKind::StructLiteral(_) | ExprKind::EnumConstructor { .. } => Err(
PlanError::UnsupportedFeature("struct/tuple/enum literals in expressions"),
),
ExprKind::Is { left, pattern } => {
let (compiled_left, left_ty) =
compile_expr_inner(*left, flat_fields, alias, catalog, bindings, out_bindings)?;
let TempestType::Enum(type_id) = left_ty else {
return Err(PlanError::UnsupportedFeature("is operator requires enum operand"));
};
let segs = &pattern.variant.segments;
let (found_type_id, type_schema, var_name) = match segs.len() {
2 => {
let (PathSegment::Named(ty_name), PathSegment::Named(var_name)) = (&segs[0], &segs[1]) else {
return Err(PlanError::UnsupportedFeature("is pattern path segments must be named"));
};
let (found_type_id, type_schema) = catalog
.get_global_type_by_name(&ty_name.name)
.ok_or_else(|| PlanError::UnsupportedFeature("is pattern: global type not found"))?;
(found_type_id, type_schema, var_name)
}
3 => {
let (PathSegment::Named(db), PathSegment::Named(ty_name), PathSegment::Named(var_name)) =
(&segs[0], &segs[1], &segs[2])
else {
return Err(PlanError::UnsupportedFeature("is pattern path segments must be named"));
};
let (db_id, _) = catalog
.get_database_by_name(&db.name)
.ok_or_else(|| PlanError::UnsupportedFeature("is pattern: database not found"))?;
let (found_type_id, type_schema) = catalog
.get_type_by_name(db_id, &ty_name.name)
.ok_or_else(|| PlanError::UnsupportedFeature("is pattern: type not found"))?;
(found_type_id, type_schema, var_name)
}
_ => return Err(PlanError::UnsupportedFeature(
"is pattern variant must be a 2-segment (Type.Variant) or 3-segment (db.Type.Variant) path",
)),
};
if *found_type_id != type_id {
return Err(PlanError::UnsupportedFeature("is pattern: type mismatch"));
}
let enum_schema = type_schema
.as_enum()
.ok_or(PlanError::UnsupportedFeature("is pattern: type is not an enum"))?;
let (variant_id, variant_def) = enum_schema
.variants
.iter()
.find(|(_, v)| v.name == var_name.name)
.ok_or_else(|| PlanError::UnsupportedFeature("is pattern: variant not found"))?;
let type_args: &[crate::catalog::schema::TypeExpr] = if let CompiledExpr::ColumnRef(pos) = &compiled_left {
&flat_fields[*pos].type_args
} else {
&[]
};
let mut payload_checks: Vec<(usize, TempestValue<'static>)> = Vec::new();
match pattern.payload {
IsPayload::Unit => {}
IsPayload::Tuple(args) => {
for (i, arg) in args.into_iter().enumerate() {
if is_wildcard_pat(&arg.kind) {
let ExprKind::Path(p) = arg.kind else { unreachable!() };
let PathSegment::Named(ident) = &p.segments[0] else { unreachable!() };
let field_ty = variant_def.fields.get(i)
.map(|ty_expr| resolve_payload_field_ty(ty_expr, type_args))
.unwrap_or(TempestType::String);
out_bindings.push(PatternBinding {
name: ident.name.as_ref().to_owned(),
expr: CompiledExpr::PayloadField {
source: Box::new(compiled_left.clone()),
index: i,
},
ty: field_ty,
});
} else {
let (compiled_arg, _) = compile_expr_inner(
arg, flat_fields, alias, catalog, bindings, out_bindings,
)?;
let val = match compiled_arg {
CompiledExpr::Literal(v) => v,
_ => return Err(PlanError::UnsupportedFeature(
"non-literal concrete in payload pattern check",
)),
};
payload_checks.push((i, val));
}
}
}
IsPayload::Struct(fields) => {
for (i, f) in fields.into_iter().enumerate() {
if is_wildcard_pat(&f.pattern.kind) {
let ExprKind::Path(p) = f.pattern.kind else { unreachable!() };
let PathSegment::Named(ident) = &p.segments[0] else { unreachable!() };
let field_ty = variant_def.fields.get(i)
.map(|ty_expr| resolve_payload_field_ty(ty_expr, type_args))
.unwrap_or(TempestType::String);
out_bindings.push(PatternBinding {
name: ident.name.as_ref().to_owned(),
expr: CompiledExpr::PayloadField {
source: Box::new(compiled_left.clone()),
index: i,
},
ty: field_ty,
});
} else {
let (compiled_arg, _) = compile_expr_inner(
f.pattern, flat_fields, alias, catalog, bindings, out_bindings,
)?;
let val = match compiled_arg {
CompiledExpr::Literal(v) => v,
_ => return Err(PlanError::UnsupportedFeature(
"non-literal concrete in struct payload pattern check",
)),
};
payload_checks.push((i, val));
}
}
}
}
Ok((
CompiledExpr::IsVariant {
expr: Box::new(compiled_left),
type_id,
variant_id: **variant_id,
payload_checks,
},
TempestType::Bool,
))
}
}
}
#[instrument(skip_all, level = "trace")]
pub(crate) fn eval_row<'a>(
values: InsertValues<'a>,
fields: &BTreeMap<FieldId, FieldDef>,
generic_args: &[TypeExpr],
catalog: &CatalogState,
row: &mut Vec<TempestValue<'static>>,
) -> Result<(), PlanError> {
match values {
InsertValues::Named(literal_fields) => {
let mut supplied: HashMap<_, _> = literal_fields
.into_iter()
.map(|lf| (lf.column.name, lf.value))
.collect();
for name in supplied.keys() {
if !fields.values().any(|f| f.name == *name) {
debug!(name = %name, "unknown field in insert");
return Err(PlanError::UnknownField(name.clone().into_owned()));
}
}
for (_, def) in fields {
let expr = supplied
.remove(&def.name)
.ok_or_else(|| PlanError::MissingField(def.name.clone()))?;
eval_field(expr, &def.ty, generic_args, catalog, row)?;
}
}
InsertValues::Unnamed(exprs) => {
for ((_, def), expr) in fields.iter().zip(exprs) {
eval_field(expr, &def.ty, generic_args, catalog, row)?;
}
}
}
Ok(())
}
fn expr_to_insert_values(expr: Expr<'_>) -> Result<InsertValues<'_>, PlanError> {
match expr.kind {
ExprKind::StructLiteral(fields) => Ok(InsertValues::Named(fields)),
ExprKind::TupleLiteral(exprs) => Ok(InsertValues::Unnamed(exprs)),
_ => Err(PlanError::UnsupportedFeature(
"expected struct or tuple literal for embedded field",
)),
}
}
fn eval_field<'a>(
expr: Expr<'a>,
ty: &TypeExpr,
generic_args: &[TypeExpr],
catalog: &CatalogState,
row: &mut Vec<TempestValue<'static>>,
) -> Result<(), PlanError> {
match ty {
TypeExpr::Primitive(prim_ty) => {
let value = eval(&expr)?;
if value.ty() != *prim_ty {
debug!(?prim_ty, got = ?value.ty(), "type mismatch on insert field");
return Err(PlanError::TypeMismatch {
expected: *prim_ty,
got: value.ty(),
});
}
row.push(value);
}
TypeExpr::Ref(type_id, ref_args) => {
let type_schema = catalog
.get_type(*type_id)
.ok_or_else(|| CatalogError::TypeNotFound(*type_id))?;
match type_schema {
TypeSchema::Enum(enum_schema) => {
let (var_name, payload_exprs): (_, Vec<Expr<'_>>) = match expr.kind {
ExprKind::Path(path) => {
let name = match path.segments.last() {
Some(tempest_tql::ast::PathSegment::Named(id)) => id.name.clone(),
_ => return Err(PlanError::UnsupportedFeature(
"enum variant path must end with a named segment",
)),
};
(name, vec![])
}
ExprKind::EnumConstructor { path, args } => {
let name = match path.segments.last() {
Some(tempest_tql::ast::PathSegment::Named(id)) => id.name.clone(),
_ => return Err(PlanError::UnsupportedFeature(
"enum constructor path must end with a named segment",
)),
};
(name, args)
}
_ => return Err(PlanError::UnsupportedFeature(
"expected variant path or constructor for enum field",
)),
};
let (variant_id, variant_def) = enum_schema
.variants
.iter()
.find(|(_, v)| v.name == var_name)
.ok_or_else(|| PlanError::FieldNotFound(var_name.clone().into_owned()))?;
let mut fields = Vec::with_capacity(payload_exprs.len());
for (payload_expr, ty_expr) in payload_exprs.into_iter().zip(variant_def.fields.iter()) {
let val = eval(&payload_expr)?;
let concrete_ty = resolve_payload_field_ty(ty_expr, ref_args);
if val.ty() != concrete_ty {
return Err(PlanError::TypeMismatch { expected: concrete_ty, got: val.ty() });
}
fields.push(val);
}
row.push(TempestValue::Enum {
type_id: **type_id,
variant_id: **variant_id,
fields,
});
}
TypeSchema::Struct(struct_schema) => {
let values = expr_to_insert_values(expr)?;
trace!("recursing into Ref field");
eval_row(values, &struct_schema.fields, ref_args, catalog, row)?;
}
}
}
TypeExpr::GenericParam(i) => match generic_args.get(*i as usize) {
Some(TypeExpr::Primitive(prim_ty)) => {
let value = eval(&expr)?;
if value.ty() != *prim_ty {
debug!(?prim_ty, got = ?value.ty(), "type mismatch on generic insert field");
return Err(PlanError::TypeMismatch {
expected: *prim_ty,
got: value.ty(),
});
}
row.push(value);
}
Some(TypeExpr::Ref(type_id, ref_args)) => {
let type_schema = catalog
.get_type(*type_id)
.ok_or_else(|| CatalogError::TypeNotFound(*type_id))?;
let struct_schema = type_schema
.as_struct()
.ok_or(PlanError::UnsupportedFeature(
"generic enum fields in tuple context not yet supported",
))?;
let values = expr_to_insert_values(expr)?;
trace!("recursing into generic Ref field");
eval_row(values, &struct_schema.fields, ref_args, catalog, row)?;
}
Some(TypeExpr::GenericParam(_)) => unreachable!("type args must be concrete"),
None => unreachable!("generic param index out of range - catalog is corrupt"),
},
}
Ok(())
}
pub(crate) fn eval(expr: &Expr<'_>) -> Result<TempestValue<'static>, PlanError> {
match &expr.kind {
ExprKind::IntegerLiteral(lit) => Ok(TempestValue::Int64(lit.parse().unwrap())),
ExprKind::StringLiteral(s) => {
Ok(TempestValue::String(Cow::Owned(s.clone().into_owned())))
}
ExprKind::Bool(b) => Ok(TempestValue::Bool(*b)),
ExprKind::BinaryOp { .. } => Err(PlanError::UnsupportedFeature(
"binary op in immediate expression",
)),
ExprKind::Path(path) => Err(PlanError::ColumnRefInLiteral(
path.render().into_owned(),
)),
ExprKind::StructLiteral(_) | ExprKind::TupleLiteral(_) | ExprKind::EnumConstructor { .. } => Err(
PlanError::UnsupportedFeature("struct/tuple/enum literal in scalar context"),
),
ExprKind::Is { .. } => Err(PlanError::UnsupportedFeature("is expression in scalar context")),
}
}
pub(crate) fn eval_compiled(
expr: &CompiledExpr,
values: &[TempestValue<'static>],
) -> TempestValue<'static> {
match expr {
CompiledExpr::Literal(val) => val.clone(),
CompiledExpr::ColumnRef(idx) => values[*idx].clone(),
CompiledExpr::Binary { left, op, right } => {
if *op == BinaryOpKind::And {
let l = eval_compiled(left, values);
let TempestValue::Bool(lb) = l else { unreachable!() };
if !lb { return TempestValue::Bool(false); }
let r = eval_compiled(right, values);
let TempestValue::Bool(rb) = r else { unreachable!() };
return TempestValue::Bool(rb);
}
if *op == BinaryOpKind::Or {
let l = eval_compiled(left, values);
let TempestValue::Bool(lb) = l else { unreachable!() };
if lb { return TempestValue::Bool(true); }
let r = eval_compiled(right, values);
let TempestValue::Bool(rb) = r else { unreachable!() };
return TempestValue::Bool(rb);
}
let l = eval_compiled(left, values);
let r = eval_compiled(right, values);
match (l, r, op) {
(TempestValue::Bool(l), TempestValue::Bool(r), op) => match (l, r, op) {
(l, r, BinaryOpKind::Eq) => return TempestValue::Bool(l == r),
(l, r, BinaryOpKind::Neq) => return TempestValue::Bool(l != r),
_ => {}
},
(TempestValue::Int64(l), TempestValue::Int64(r), op) => match (l, r, op) {
(l, r, BinaryOpKind::Eq) => return TempestValue::Bool(l == r),
(l, r, BinaryOpKind::Neq) => return TempestValue::Bool(l != r),
(l, r, BinaryOpKind::Gt) => return TempestValue::Bool(l > r),
(l, r, BinaryOpKind::Lt) => return TempestValue::Bool(l < r),
(l, r, BinaryOpKind::Gte) => return TempestValue::Bool(l >= r),
(l, r, BinaryOpKind::Lte) => return TempestValue::Bool(l <= r),
_ => {}
},
(TempestValue::String(l), TempestValue::String(r), op) => match (l, r, op) {
(l, r, BinaryOpKind::Eq) => return TempestValue::Bool(l == r),
(l, r, BinaryOpKind::Neq) => return TempestValue::Bool(l != r),
_ => {}
},
_ => {}
}
unreachable!("type checker should have caught this");
}
CompiledExpr::IsVariant { expr, type_id, variant_id, payload_checks } => {
let val = eval_compiled(expr, values);
match val {
TempestValue::Enum { type_id: t, variant_id: v, ref fields } => {
if t != *type_id || v != *variant_id {
return TempestValue::Bool(false);
}
for (idx, expected) in payload_checks {
if fields.get(*idx) != Some(expected) {
return TempestValue::Bool(false);
}
}
TempestValue::Bool(true)
}
_ => unreachable!("type checker ensures IsVariant left is Enum"),
}
}
CompiledExpr::PayloadField { source, index } => {
let val = eval_compiled(source, values);
match val {
TempestValue::Enum { fields, .. } => fields
.into_iter()
.nth(*index)
.expect("payload field index in bounds"),
_ => unreachable!("PayloadField source must be Enum"),
}
}
}
}