use crate::ast::Span;
use crate::ast::Statement;
use crate::ast::ddl::VectorMetric;
use crate::ast::expr::{BinaryOp, Expr, ExprKind, Literal, Quantifier as AstQuantifier, UnaryOp};
use crate::catalog::{Catalog, ColumnMetadata, TableMetadata};
use crate::planner::aggregate_expr::{AggregateExpr, AggregateFunction};
use crate::planner::error::PlannerError;
use crate::planner::logical_plan::LogicalPlan;
use crate::planner::name_resolver::NameResolver;
use crate::planner::typed_expr::{Quantifier, TypedExpr, TypedExprKind};
use crate::planner::types::ResolvedType;
#[derive(Debug, Clone)]
pub struct ScopedTable {
pub table: TableMetadata,
pub start_index: usize,
}
impl ScopedTable {
pub fn new(table: TableMetadata, start_index: usize) -> Self {
Self { table, start_index }
}
}
pub type SubqueryPlanner<'p> = dyn Fn(&Statement, &[ScopedTable]) -> Result<(LogicalPlan, Vec<ColumnMetadata>), PlannerError>
+ 'p;
pub struct TypeChecker<'a, C: Catalog + ?Sized> {
catalog: &'a C,
}
impl<'a, C: Catalog + ?Sized> TypeChecker<'a, C> {
pub fn new(catalog: &'a C) -> Self {
Self { catalog }
}
pub fn catalog(&self) -> &'a C {
self.catalog
}
pub fn infer_type(
&self,
expr: &Expr,
table: &TableMetadata,
) -> Result<TypedExpr, PlannerError> {
let scope = [ScopedTable::new(table.clone(), 0)];
self.infer_type_with_scope(expr, &scope, &|stmt, _outer| {
let planner = crate::planner::Planner::new(self.catalog);
let plan = planner.plan(stmt)?;
Ok((plan, Vec::new()))
})
}
pub fn infer_type_with_scope(
&self,
expr: &Expr,
scope: &[ScopedTable],
plan_subquery: &SubqueryPlanner<'_>,
) -> Result<TypedExpr, PlannerError> {
let span = expr.span;
match &expr.kind {
ExprKind::Literal { literal: lit } => self.infer_literal_type(lit, span),
ExprKind::ColumnRef {
table: table_qualifier,
column,
} => self.infer_column_ref_type_with_scope(
scope,
table_qualifier.as_deref(),
column,
span,
),
ExprKind::BinaryOp { left, op, right } => {
self.infer_binary_op_type_with_scope(left, *op, right, scope, plan_subquery, span)
}
ExprKind::UnaryOp { op, operand } => {
self.infer_unary_op_type_with_scope(*op, operand, scope, plan_subquery, span)
}
ExprKind::FunctionCall {
name,
args,
distinct,
star,
} => self.infer_function_call_type_with_scope(
name,
args,
*distinct,
*star,
scope,
plan_subquery,
span,
),
ExprKind::Between {
expr,
low,
high,
negated,
} => self.infer_between_type_with_scope(
expr,
low,
high,
*negated,
scope,
plan_subquery,
span,
),
ExprKind::Like {
expr,
pattern,
escape,
negated,
} => self.infer_like_type_with_scope(
expr,
pattern,
escape.as_deref(),
*negated,
scope,
plan_subquery,
span,
),
ExprKind::InList {
expr,
list,
negated,
} => {
self.infer_in_list_type_with_scope(expr, list, *negated, scope, plan_subquery, span)
}
ExprKind::IsNull { expr, negated } => {
self.infer_is_null_type_with_scope(expr, *negated, scope, plan_subquery, span)
}
ExprKind::VectorLiteral { values } => self.infer_vector_literal_type(values, span),
ExprKind::ScalarSubquery { subquery } => {
let (plan, schema) = plan_subquery(subquery, scope)?;
let value_type = single_column_type(&schema, span)?;
Ok(TypedExpr {
kind: TypedExprKind::ScalarSubquery(Box::new(plan)),
resolved_type: value_type,
span,
})
}
ExprKind::InSubquery {
expr,
subquery,
negated,
} => {
let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
let (plan, schema) = plan_subquery(subquery, scope)?;
let value_type = single_column_type(&schema, span)?;
self.check_comparison_op(&expr_typed.resolved_type, &value_type, span)?;
Ok(TypedExpr {
kind: TypedExprKind::InSubquery {
expr: Box::new(expr_typed),
subquery: Box::new(plan),
negated: *negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
ExprKind::Exists { subquery, negated } => {
let (plan, _schema) = plan_subquery(subquery, scope)?;
Ok(TypedExpr {
kind: TypedExprKind::Exists {
subquery: Box::new(plan),
negated: *negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
ExprKind::Quantified {
expr,
op,
quantifier,
subquery,
} => {
let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
let (plan, schema) = plan_subquery(subquery, scope)?;
let value_type = single_column_type(&schema, span)?;
self.check_binary_op(*op, &expr_typed.resolved_type, &value_type, span)?;
Ok(TypedExpr {
kind: TypedExprKind::Quantified {
expr: Box::new(expr_typed),
op: *op,
quantifier: match quantifier {
AstQuantifier::Any => Quantifier::Any,
AstQuantifier::All => Quantifier::All,
},
subquery: Box::new(plan),
},
resolved_type: ResolvedType::Boolean,
span,
})
}
}
}
fn infer_literal_type(&self, lit: &Literal, span: Span) -> Result<TypedExpr, PlannerError> {
let (kind, resolved_type) = match lit {
Literal::Number(s) => {
let resolved_type = if s.contains('.') || s.contains('e') || s.contains('E') {
ResolvedType::Double
} else {
if s.parse::<i32>().is_ok() {
ResolvedType::Integer
} else {
ResolvedType::BigInt
}
};
(TypedExprKind::Literal(lit.clone()), resolved_type)
}
Literal::String(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Text),
Literal::Boolean(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Boolean),
Literal::Null => (TypedExprKind::Literal(lit.clone()), ResolvedType::Null),
};
Ok(TypedExpr {
kind,
resolved_type,
span,
})
}
#[allow(dead_code)]
fn infer_column_ref_type(
&self,
table: &TableMetadata,
column_name: &str,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let (column_index, column) = table
.columns
.iter()
.enumerate()
.find(|(_, c)| c.name == column_name)
.ok_or_else(|| PlannerError::ColumnNotFound {
column: column_name.to_string(),
table: table.name.clone(),
line: span.start.line,
col: span.start.column,
})?;
Ok(TypedExpr {
kind: TypedExprKind::ColumnRef {
table: table.name.clone(),
column: column_name.to_string(),
column_index,
},
resolved_type: column.data_type.clone(),
span,
})
}
fn infer_column_ref_type_with_scope(
&self,
scope: &[ScopedTable],
table_qualifier: Option<&str>,
column_name: &str,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let tables = scope.iter().map(|s| &s.table).collect::<Vec<_>>();
let resolver = NameResolver::new(self.catalog);
let resolved =
resolver.resolve_column_with_scope(&tables, table_qualifier, column_name, span)?;
let scoped = scope
.iter()
.find(|s| s.table.name == resolved.table_name)
.ok_or_else(|| PlannerError::table_not_found(&resolved.table_name, span))?;
Ok(TypedExpr {
kind: TypedExprKind::ColumnRef {
table: resolved.table_name,
column: resolved.column_name,
column_index: scoped.start_index + resolved.column_index,
},
resolved_type: resolved.resolved_type,
span,
})
}
#[allow(dead_code)]
fn infer_binary_op_type(
&self,
left: &Expr,
op: BinaryOp,
right: &Expr,
table: &TableMetadata,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let left_typed = self.infer_type(left, table)?;
let right_typed = self.infer_type(right, table)?;
let result_type = self.check_binary_op(
op,
&left_typed.resolved_type,
&right_typed.resolved_type,
span,
)?;
Ok(TypedExpr {
kind: TypedExprKind::BinaryOp {
left: Box::new(left_typed),
op,
right: Box::new(right_typed),
},
resolved_type: result_type,
span,
})
}
fn infer_binary_op_type_with_scope(
&self,
left: &Expr,
op: BinaryOp,
right: &Expr,
scope: &[ScopedTable],
plan_subquery: &SubqueryPlanner<'_>,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let left_typed = self.infer_type_with_scope(left, scope, plan_subquery)?;
let right_typed = self.infer_type_with_scope(right, scope, plan_subquery)?;
let result_type = self.check_binary_op(
op,
&left_typed.resolved_type,
&right_typed.resolved_type,
span,
)?;
Ok(TypedExpr {
kind: TypedExprKind::BinaryOp {
left: Box::new(left_typed),
op,
right: Box::new(right_typed),
},
resolved_type: result_type,
span,
})
}
pub fn check_binary_op(
&self,
op: BinaryOp,
left: &ResolvedType,
right: &ResolvedType,
span: Span,
) -> Result<ResolvedType, PlannerError> {
use BinaryOp::*;
use ResolvedType::*;
match op {
Add | Sub | Mul | Div | Mod => {
let result = self.check_arithmetic_op(left, right, span)?;
Ok(result)
}
Eq | Neq | Lt | Gt | LtEq | GtEq => {
self.check_comparison_op(left, right, span)?;
Ok(Boolean)
}
And | Or => {
self.check_logical_op(left, right, span)?;
Ok(Boolean)
}
StringConcat => {
self.check_string_concat_op(left, right, span)?;
Ok(Text)
}
}
}
fn check_arithmetic_op(
&self,
left: &ResolvedType,
right: &ResolvedType,
span: Span,
) -> Result<ResolvedType, PlannerError> {
use ResolvedType::*;
if matches!(left, Null) || matches!(right, Null) {
return Ok(Null);
}
match (left, right) {
(Integer, Integer) => Ok(Integer),
(Integer, BigInt) | (BigInt, Integer) | (BigInt, BigInt) => Ok(BigInt),
(Integer, Float) | (Float, Integer) | (Float, Float) => Ok(Float),
(Integer, Double)
| (Double, Integer)
| (BigInt, Float)
| (Float, BigInt)
| (BigInt, Double)
| (Double, BigInt)
| (Float, Double)
| (Double, Float)
| (Double, Double) => Ok(Double),
_ => Err(PlannerError::InvalidOperator {
op: "arithmetic".to_string(),
type_name: format!("{} and {}", left.type_name(), right.type_name()),
line: span.start.line,
column: span.start.column,
}),
}
}
pub(crate) fn check_comparison_op(
&self,
left: &ResolvedType,
right: &ResolvedType,
span: Span,
) -> Result<(), PlannerError> {
use ResolvedType::*;
if matches!(left, Null) || matches!(right, Null) {
return Ok(());
}
let compatible = match (left, right) {
(a, b) if a == b => true,
(Integer | BigInt | Float | Double, Integer | BigInt | Float | Double) => true,
(Text, Text) => true,
(Boolean, Boolean) => true,
(Timestamp, Timestamp) => true,
(Vector { dimension: d1, .. }, Vector { dimension: d2, .. }) => d1 == d2,
_ => false,
};
if compatible {
Ok(())
} else {
Err(PlannerError::TypeMismatch {
expected: left.type_name().to_string(),
found: right.type_name().to_string(),
line: span.start.line,
column: span.start.column,
})
}
}
fn check_logical_op(
&self,
left: &ResolvedType,
right: &ResolvedType,
span: Span,
) -> Result<(), PlannerError> {
use ResolvedType::*;
let left_ok = matches!(left, Boolean | Null);
let right_ok = matches!(right, Boolean | Null);
if !left_ok {
return Err(PlannerError::TypeMismatch {
expected: "Boolean".to_string(),
found: left.type_name().to_string(),
line: span.start.line,
column: span.start.column,
});
}
if !right_ok {
return Err(PlannerError::TypeMismatch {
expected: "Boolean".to_string(),
found: right.type_name().to_string(),
line: span.start.line,
column: span.start.column,
});
}
Ok(())
}
fn check_string_concat_op(
&self,
left: &ResolvedType,
right: &ResolvedType,
span: Span,
) -> Result<(), PlannerError> {
use ResolvedType::*;
let left_ok = matches!(left, Text | Null);
let right_ok = matches!(right, Text | Null);
if !left_ok {
return Err(PlannerError::TypeMismatch {
expected: "Text".to_string(),
found: left.type_name().to_string(),
line: span.start.line,
column: span.start.column,
});
}
if !right_ok {
return Err(PlannerError::TypeMismatch {
expected: "Text".to_string(),
found: right.type_name().to_string(),
line: span.start.line,
column: span.start.column,
});
}
Ok(())
}
#[allow(dead_code)]
fn infer_unary_op_type(
&self,
op: UnaryOp,
operand: &Expr,
table: &TableMetadata,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let operand_typed = self.infer_type(operand, table)?;
let result_type = match op {
UnaryOp::Not => {
if !matches!(
operand_typed.resolved_type,
ResolvedType::Boolean | ResolvedType::Null
) {
return Err(PlannerError::TypeMismatch {
expected: "Boolean".to_string(),
found: operand_typed.resolved_type.type_name().to_string(),
line: span.start.line,
column: span.start.column,
});
}
ResolvedType::Boolean
}
UnaryOp::Minus => {
match &operand_typed.resolved_type {
ResolvedType::Integer => ResolvedType::Integer,
ResolvedType::BigInt => ResolvedType::BigInt,
ResolvedType::Float => ResolvedType::Float,
ResolvedType::Double => ResolvedType::Double,
ResolvedType::Null => ResolvedType::Null,
other => {
return Err(PlannerError::InvalidOperator {
op: "unary minus".to_string(),
type_name: other.type_name().to_string(),
line: span.start.line,
column: span.start.column,
});
}
}
}
};
Ok(TypedExpr {
kind: TypedExprKind::UnaryOp {
op,
operand: Box::new(operand_typed),
},
resolved_type: result_type,
span,
})
}
fn infer_unary_op_type_with_scope(
&self,
op: UnaryOp,
operand: &Expr,
scope: &[ScopedTable],
plan_subquery: &SubqueryPlanner<'_>,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let operand_typed = self.infer_type_with_scope(operand, scope, plan_subquery)?;
let result_type = match op {
UnaryOp::Not => {
if !matches!(
operand_typed.resolved_type,
ResolvedType::Boolean | ResolvedType::Null
) {
return Err(PlannerError::TypeMismatch {
expected: "Boolean".to_string(),
found: operand_typed.resolved_type.type_name().to_string(),
line: span.start.line,
column: span.start.column,
});
}
ResolvedType::Boolean
}
UnaryOp::Minus => match &operand_typed.resolved_type {
ResolvedType::Integer => ResolvedType::Integer,
ResolvedType::BigInt => ResolvedType::BigInt,
ResolvedType::Float => ResolvedType::Float,
ResolvedType::Double => ResolvedType::Double,
ResolvedType::Null => ResolvedType::Null,
other => {
return Err(PlannerError::InvalidOperator {
op: "unary minus".to_string(),
type_name: other.type_name().to_string(),
line: span.start.line,
column: span.start.column,
});
}
},
};
Ok(TypedExpr {
kind: TypedExprKind::UnaryOp {
op,
operand: Box::new(operand_typed),
},
resolved_type: result_type,
span,
})
}
#[allow(dead_code)]
fn infer_function_call_type(
&self,
name: &str,
args: &[Expr],
distinct: bool,
star: bool,
table: &TableMetadata,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let typed_args: Vec<TypedExpr> = args
.iter()
.map(|arg| self.infer_type(arg, table))
.collect::<Result<Vec<_>, _>>()?;
let result_type = self.check_function_call(name, &typed_args, distinct, star, span)?;
Ok(TypedExpr {
kind: TypedExprKind::FunctionCall {
name: name.to_string(),
args: typed_args,
distinct,
star,
},
resolved_type: result_type,
span,
})
}
#[allow(clippy::too_many_arguments)]
fn infer_function_call_type_with_scope(
&self,
name: &str,
args: &[Expr],
distinct: bool,
star: bool,
scope: &[ScopedTable],
plan_subquery: &SubqueryPlanner<'_>,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let typed_args: Vec<TypedExpr> = args
.iter()
.map(|arg| self.infer_type_with_scope(arg, scope, plan_subquery))
.collect::<Result<Vec<_>, _>>()?;
let result_type = self.check_function_call(name, &typed_args, distinct, star, span)?;
Ok(TypedExpr {
kind: TypedExprKind::FunctionCall {
name: name.to_string(),
args: typed_args,
distinct,
star,
},
resolved_type: result_type,
span,
})
}
#[allow(dead_code)]
fn infer_between_type(
&self,
expr: &Expr,
low: &Expr,
high: &Expr,
negated: bool,
table: &TableMetadata,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let expr_typed = self.infer_type(expr, table)?;
let low_typed = self.infer_type(low, table)?;
let high_typed = self.infer_type(high, table)?;
self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
Ok(TypedExpr {
kind: TypedExprKind::Between {
expr: Box::new(expr_typed),
low: Box::new(low_typed),
high: Box::new(high_typed),
negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
#[allow(clippy::too_many_arguments)]
fn infer_between_type_with_scope(
&self,
expr: &Expr,
low: &Expr,
high: &Expr,
negated: bool,
scope: &[ScopedTable],
plan_subquery: &SubqueryPlanner<'_>,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
let low_typed = self.infer_type_with_scope(low, scope, plan_subquery)?;
let high_typed = self.infer_type_with_scope(high, scope, plan_subquery)?;
self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
Ok(TypedExpr {
kind: TypedExprKind::Between {
expr: Box::new(expr_typed),
low: Box::new(low_typed),
high: Box::new(high_typed),
negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
#[allow(dead_code)]
fn infer_like_type(
&self,
expr: &Expr,
pattern: &Expr,
escape: Option<&Expr>,
negated: bool,
table: &TableMetadata,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let expr_typed = self.infer_type(expr, table)?;
let pattern_typed = self.infer_type(pattern, table)?;
if !matches!(
expr_typed.resolved_type,
ResolvedType::Text | ResolvedType::Null
) {
return Err(PlannerError::TypeMismatch {
expected: "Text".to_string(),
found: expr_typed.resolved_type.type_name().to_string(),
line: expr.span.start.line,
column: expr.span.start.column,
});
}
if !matches!(
pattern_typed.resolved_type,
ResolvedType::Text | ResolvedType::Null
) {
return Err(PlannerError::TypeMismatch {
expected: "Text".to_string(),
found: pattern_typed.resolved_type.type_name().to_string(),
line: pattern.span.start.line,
column: pattern.span.start.column,
});
}
let escape_typed = if let Some(esc) = escape {
let typed = self.infer_type(esc, table)?;
if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
return Err(PlannerError::TypeMismatch {
expected: "Text".to_string(),
found: typed.resolved_type.type_name().to_string(),
line: esc.span.start.line,
column: esc.span.start.column,
});
}
Some(Box::new(typed))
} else {
None
};
Ok(TypedExpr {
kind: TypedExprKind::Like {
expr: Box::new(expr_typed),
pattern: Box::new(pattern_typed),
escape: escape_typed,
negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
#[allow(clippy::too_many_arguments)]
fn infer_like_type_with_scope(
&self,
expr: &Expr,
pattern: &Expr,
escape: Option<&Expr>,
negated: bool,
scope: &[ScopedTable],
plan_subquery: &SubqueryPlanner<'_>,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
let pattern_typed = self.infer_type_with_scope(pattern, scope, plan_subquery)?;
if !matches!(
expr_typed.resolved_type,
ResolvedType::Text | ResolvedType::Null
) {
return Err(PlannerError::TypeMismatch {
expected: "Text".to_string(),
found: expr_typed.resolved_type.type_name().to_string(),
line: expr.span.start.line,
column: expr.span.start.column,
});
}
if !matches!(
pattern_typed.resolved_type,
ResolvedType::Text | ResolvedType::Null
) {
return Err(PlannerError::TypeMismatch {
expected: "Text".to_string(),
found: pattern_typed.resolved_type.type_name().to_string(),
line: pattern.span.start.line,
column: pattern.span.start.column,
});
}
let escape_typed = if let Some(esc) = escape {
let typed = self.infer_type_with_scope(esc, scope, plan_subquery)?;
if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
return Err(PlannerError::TypeMismatch {
expected: "Text".to_string(),
found: typed.resolved_type.type_name().to_string(),
line: esc.span.start.line,
column: esc.span.start.column,
});
}
Some(Box::new(typed))
} else {
None
};
Ok(TypedExpr {
kind: TypedExprKind::Like {
expr: Box::new(expr_typed),
pattern: Box::new(pattern_typed),
escape: escape_typed,
negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
#[allow(dead_code)]
fn infer_in_list_type(
&self,
expr: &Expr,
list: &[Expr],
negated: bool,
table: &TableMetadata,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let expr_typed = self.infer_type(expr, table)?;
let typed_list: Vec<TypedExpr> = list
.iter()
.map(|item| {
let typed = self.infer_type(item, table)?;
self.check_comparison_op(
&expr_typed.resolved_type,
&typed.resolved_type,
item.span,
)?;
Ok(typed)
})
.collect::<Result<Vec<_>, PlannerError>>()?;
Ok(TypedExpr {
kind: TypedExprKind::InList {
expr: Box::new(expr_typed),
list: typed_list,
negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
fn infer_in_list_type_with_scope(
&self,
expr: &Expr,
list: &[Expr],
negated: bool,
scope: &[ScopedTable],
plan_subquery: &SubqueryPlanner<'_>,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
let typed_list: Vec<TypedExpr> = list
.iter()
.map(|item| {
let typed = self.infer_type_with_scope(item, scope, plan_subquery)?;
self.check_comparison_op(
&expr_typed.resolved_type,
&typed.resolved_type,
item.span,
)?;
Ok(typed)
})
.collect::<Result<Vec<_>, PlannerError>>()?;
Ok(TypedExpr {
kind: TypedExprKind::InList {
expr: Box::new(expr_typed),
list: typed_list,
negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
#[allow(dead_code)]
fn infer_is_null_type(
&self,
expr: &Expr,
negated: bool,
table: &TableMetadata,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let expr_typed = self.infer_type(expr, table)?;
Ok(TypedExpr {
kind: TypedExprKind::IsNull {
expr: Box::new(expr_typed),
negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
fn infer_is_null_type_with_scope(
&self,
expr: &Expr,
negated: bool,
scope: &[ScopedTable],
plan_subquery: &SubqueryPlanner<'_>,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
Ok(TypedExpr {
kind: TypedExprKind::IsNull {
expr: Box::new(expr_typed),
negated,
},
resolved_type: ResolvedType::Boolean,
span,
})
}
fn infer_vector_literal_type(
&self,
values: &[f64],
span: Span,
) -> Result<TypedExpr, PlannerError> {
Ok(TypedExpr {
kind: TypedExprKind::VectorLiteral(values.to_vec()),
resolved_type: ResolvedType::Vector {
dimension: values.len() as u32,
metric: VectorMetric::Cosine, },
span,
})
}
pub fn normalize_metric(&self, metric: &str, span: Span) -> Result<VectorMetric, PlannerError> {
match metric.to_lowercase().as_str() {
"cosine" => Ok(VectorMetric::Cosine),
"l2" => Ok(VectorMetric::L2),
"inner" => Ok(VectorMetric::Inner),
_ => Err(PlannerError::InvalidMetric {
value: metric.to_string(),
line: span.start.line,
column: span.start.column,
}),
}
}
pub fn check_function_call(
&self,
name: &str,
args: &[TypedExpr],
distinct: bool,
star: bool,
span: Span,
) -> Result<ResolvedType, PlannerError> {
let lower_name = name.to_lowercase();
match lower_name.as_str() {
"count" => self.check_count(args, distinct, star, span),
"sum" => self.check_sum(args, distinct, star, span),
"total" => self.check_total(args, distinct, star, span),
"avg" => self.check_avg(args, distinct, star, span),
"min" => self.check_min_max(args, distinct, star, span),
"max" => self.check_min_max(args, distinct, star, span),
"group_concat" => self.check_group_concat(args, distinct, star, span),
"string_agg" => self.check_string_agg(args, distinct, star, span),
"vector_distance" => self.check_vector_distance(args, span),
"vector_similarity" => self.check_vector_similarity(args, span),
"vector_dims" => self.check_vector_dims(args, span),
"vector_norm" => self.check_vector_norm(args, span),
_ => {
Err(PlannerError::UnsupportedFeature {
feature: format!("function '{}'", name),
version: "future".to_string(),
line: span.start.line,
column: span.start.column,
})
}
}
}
pub fn validate_having_expr(
&self,
expr: &TypedExpr,
group_keys: &[TypedExpr],
aggregates: &[AggregateExpr],
) -> Result<(), PlannerError> {
use std::collections::HashSet;
let group_key_indices: HashSet<usize> = group_keys
.iter()
.filter_map(|expr| match &expr.kind {
TypedExprKind::ColumnRef { column_index, .. } => Some(*column_index),
_ => None,
})
.collect();
let aggregate_signatures: HashSet<AggregateSignature> = aggregates
.iter()
.map(aggregate_signature_from_expr)
.collect();
fn walk(
expr: &TypedExpr,
group_key_indices: &HashSet<usize>,
aggregate_signatures: &HashSet<AggregateSignature>,
) -> Result<(), PlannerError> {
match &expr.kind {
TypedExprKind::ColumnRef { column_index, .. } => {
if group_key_indices.contains(column_index) {
Ok(())
} else {
Err(PlannerError::invalid_expression(
"column in HAVING must be in GROUP BY or be aggregated".to_string(),
))
}
}
TypedExprKind::FunctionCall {
name,
args,
distinct,
star,
} if is_aggregate_name(name) => {
let signature = aggregate_signature_from_call(name, args, *distinct, *star)?;
if aggregate_signatures.contains(&signature) {
Ok(())
} else {
Err(PlannerError::invalid_expression(
"aggregate in HAVING must appear in plan".to_string(),
))
}
}
TypedExprKind::BinaryOp { left, right, .. } => {
walk(left, group_key_indices, aggregate_signatures)?;
walk(right, group_key_indices, aggregate_signatures)
}
TypedExprKind::UnaryOp { operand, .. } => {
walk(operand, group_key_indices, aggregate_signatures)
}
TypedExprKind::FunctionCall { args, .. } => {
for arg in args {
walk(arg, group_key_indices, aggregate_signatures)?;
}
Ok(())
}
TypedExprKind::Between {
expr, low, high, ..
} => {
walk(expr, group_key_indices, aggregate_signatures)?;
walk(low, group_key_indices, aggregate_signatures)?;
walk(high, group_key_indices, aggregate_signatures)
}
TypedExprKind::Like {
expr,
pattern,
escape,
..
} => {
walk(expr, group_key_indices, aggregate_signatures)?;
walk(pattern, group_key_indices, aggregate_signatures)?;
if let Some(esc) = escape {
walk(esc, group_key_indices, aggregate_signatures)?;
}
Ok(())
}
TypedExprKind::InList { expr, list, .. } => {
walk(expr, group_key_indices, aggregate_signatures)?;
for item in list {
walk(item, group_key_indices, aggregate_signatures)?;
}
Ok(())
}
TypedExprKind::IsNull { expr, .. } => {
walk(expr, group_key_indices, aggregate_signatures)
}
_ => Ok(()),
}
}
walk(expr, &group_key_indices, &aggregate_signatures)
}
fn check_count(
&self,
args: &[TypedExpr],
distinct: bool,
star: bool,
span: Span,
) -> Result<ResolvedType, PlannerError> {
if star {
if distinct {
return Err(PlannerError::unsupported_feature(
"COUNT(DISTINCT *)",
"future",
span,
));
}
if !args.is_empty() {
return Err(PlannerError::type_mismatch(
"no arguments with COUNT(*)",
format!("{} arguments", args.len()),
span,
));
}
return Ok(ResolvedType::BigInt);
}
if args.len() != 1 {
return Err(PlannerError::type_mismatch(
"1 argument",
format!("{} arguments", args.len()),
span,
));
}
if distinct {
return Ok(ResolvedType::BigInt);
}
Ok(ResolvedType::BigInt)
}
fn check_sum(
&self,
args: &[TypedExpr],
distinct: bool,
star: bool,
span: Span,
) -> Result<ResolvedType, PlannerError> {
if star {
return Err(PlannerError::type_mismatch(
"numeric argument",
"COUNT(*) style",
span,
));
}
if distinct {
return Err(PlannerError::unsupported_feature(
"SUM(DISTINCT ...)",
"future",
span,
));
}
let arg = self.require_single_arg(args, span)?;
if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
return Err(PlannerError::type_mismatch(
"numeric",
arg.resolved_type.type_name().to_string(),
arg.span,
));
}
Ok(ResolvedType::Double)
}
fn check_total(
&self,
args: &[TypedExpr],
distinct: bool,
star: bool,
span: Span,
) -> Result<ResolvedType, PlannerError> {
if star {
return Err(PlannerError::type_mismatch(
"numeric argument",
"COUNT(*) style",
span,
));
}
if distinct {
return Err(PlannerError::unsupported_feature(
"TOTAL(DISTINCT ...)",
"future",
span,
));
}
let arg = self.require_single_arg(args, span)?;
if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
return Err(PlannerError::type_mismatch(
"numeric",
arg.resolved_type.type_name().to_string(),
arg.span,
));
}
Ok(ResolvedType::Double)
}
fn check_avg(
&self,
args: &[TypedExpr],
distinct: bool,
star: bool,
span: Span,
) -> Result<ResolvedType, PlannerError> {
if star {
return Err(PlannerError::type_mismatch(
"numeric argument",
"COUNT(*) style",
span,
));
}
if distinct {
return Err(PlannerError::unsupported_feature(
"AVG(DISTINCT ...)",
"future",
span,
));
}
let arg = self.require_single_arg(args, span)?;
if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
return Err(PlannerError::type_mismatch(
"numeric",
arg.resolved_type.type_name().to_string(),
arg.span,
));
}
Ok(ResolvedType::Double)
}
fn check_min_max(
&self,
args: &[TypedExpr],
distinct: bool,
star: bool,
span: Span,
) -> Result<ResolvedType, PlannerError> {
if star {
return Err(PlannerError::type_mismatch(
"argument",
"COUNT(*) style",
span,
));
}
if distinct {
return Err(PlannerError::unsupported_feature(
"MIN/MAX(DISTINCT ...)",
"future",
span,
));
}
let arg = self.require_single_arg(args, span)?;
if matches!(arg.resolved_type, ResolvedType::Vector { .. }) {
return Err(PlannerError::type_mismatch(
"comparable",
arg.resolved_type.type_name().to_string(),
arg.span,
));
}
Ok(arg.resolved_type.clone())
}
fn check_group_concat(
&self,
args: &[TypedExpr],
distinct: bool,
star: bool,
span: Span,
) -> Result<ResolvedType, PlannerError> {
if star {
return Err(PlannerError::type_mismatch(
"text argument",
"COUNT(*) style",
span,
));
}
if distinct {
return Err(PlannerError::unsupported_feature(
"GROUP_CONCAT(DISTINCT ...)",
"future",
span,
));
}
if args.is_empty() || args.len() > 2 {
return Err(PlannerError::type_mismatch(
"1 or 2 arguments",
format!("{} arguments", args.len()),
span,
));
}
if !matches!(
args[0].resolved_type,
ResolvedType::Text | ResolvedType::Null
) {
return Err(PlannerError::type_mismatch(
"Text",
args[0].resolved_type.type_name().to_string(),
args[0].span,
));
}
if args.len() == 2
&& !matches!(
args[1].resolved_type,
ResolvedType::Text | ResolvedType::Null
)
{
return Err(PlannerError::type_mismatch(
"Text",
args[1].resolved_type.type_name().to_string(),
args[1].span,
));
}
Ok(ResolvedType::Text)
}
fn check_string_agg(
&self,
args: &[TypedExpr],
distinct: bool,
star: bool,
span: Span,
) -> Result<ResolvedType, PlannerError> {
if star {
return Err(PlannerError::type_mismatch(
"text argument",
"COUNT(*) style",
span,
));
}
if distinct {
return Err(PlannerError::unsupported_feature(
"STRING_AGG(DISTINCT ...)",
"future",
span,
));
}
if args.len() != 2 {
return Err(PlannerError::type_mismatch(
"2 arguments",
format!("{} arguments", args.len()),
span,
));
}
if !matches!(
args[0].resolved_type,
ResolvedType::Text | ResolvedType::Null
) {
return Err(PlannerError::type_mismatch(
"Text",
args[0].resolved_type.type_name().to_string(),
args[0].span,
));
}
if !matches!(
args[1].resolved_type,
ResolvedType::Text | ResolvedType::Null
) {
return Err(PlannerError::type_mismatch(
"Text",
args[1].resolved_type.type_name().to_string(),
args[1].span,
));
}
Ok(ResolvedType::Text)
}
fn check_vector_dims(
&self,
args: &[TypedExpr],
span: Span,
) -> Result<ResolvedType, PlannerError> {
let arg = self.require_single_arg(args, span)?;
if !matches!(
arg.resolved_type,
ResolvedType::Vector { .. } | ResolvedType::Null
) {
return Err(PlannerError::type_mismatch(
"Vector",
arg.resolved_type.type_name().to_string(),
arg.span,
));
}
Ok(ResolvedType::Integer)
}
fn check_vector_norm(
&self,
args: &[TypedExpr],
span: Span,
) -> Result<ResolvedType, PlannerError> {
let arg = self.require_single_arg(args, span)?;
if !matches!(
arg.resolved_type,
ResolvedType::Vector { .. } | ResolvedType::Null
) {
return Err(PlannerError::type_mismatch(
"Vector",
arg.resolved_type.type_name().to_string(),
arg.span,
));
}
Ok(ResolvedType::Double)
}
fn require_single_arg<'b>(
&self,
args: &'b [TypedExpr],
span: Span,
) -> Result<&'b TypedExpr, PlannerError> {
if args.len() != 1 {
return Err(PlannerError::type_mismatch(
"1 argument",
format!("{} arguments", args.len()),
span,
));
}
Ok(&args[0])
}
pub fn check_vector_distance(
&self,
args: &[TypedExpr],
span: Span,
) -> Result<ResolvedType, PlannerError> {
if args.len() != 3 {
return Err(PlannerError::TypeMismatch {
expected: "3 arguments".to_string(),
found: format!("{} arguments", args.len()),
line: span.start.line,
column: span.start.column,
});
}
let col_dim = match &args[0].resolved_type {
ResolvedType::Vector { dimension, .. } => *dimension,
other => {
return Err(PlannerError::TypeMismatch {
expected: "Vector".to_string(),
found: other.type_name().to_string(),
line: args[0].span.start.line,
column: args[0].span.start.column,
});
}
};
let vec_dim = match &args[1].resolved_type {
ResolvedType::Vector { dimension, .. } => *dimension,
other => {
return Err(PlannerError::TypeMismatch {
expected: "Vector".to_string(),
found: other.type_name().to_string(),
line: args[1].span.start.line,
column: args[1].span.start.column,
});
}
};
self.check_vector_dimension(col_dim, vec_dim, args[1].span)?;
match &args[2].resolved_type {
ResolvedType::Text => {
if let TypedExprKind::Literal(Literal::String(s)) = &args[2].kind {
self.normalize_metric(s, args[2].span)?;
}
}
ResolvedType::Null => {
return Err(PlannerError::TypeMismatch {
expected: "Text (metric)".to_string(),
found: "Null".to_string(),
line: args[2].span.start.line,
column: args[2].span.start.column,
});
}
other => {
return Err(PlannerError::TypeMismatch {
expected: "Text (metric)".to_string(),
found: other.type_name().to_string(),
line: args[2].span.start.line,
column: args[2].span.start.column,
});
}
}
Ok(ResolvedType::Double)
}
pub fn check_vector_similarity(
&self,
args: &[TypedExpr],
span: Span,
) -> Result<ResolvedType, PlannerError> {
self.check_vector_distance(args, span)
}
pub fn check_vector_dimension(
&self,
expected: u32,
found: u32,
span: Span,
) -> Result<(), PlannerError> {
if expected != found {
Err(PlannerError::VectorDimensionMismatch {
expected,
found,
line: span.start.line,
column: span.start.column,
})
} else {
Ok(())
}
}
pub fn check_insert_values(
&self,
table: &TableMetadata,
columns: &[String],
values: &[Vec<Expr>],
span: Span,
) -> Result<Vec<Vec<TypedExpr>>, PlannerError> {
let target_columns: Vec<&str> = if columns.is_empty() {
table.column_names()
} else {
columns.iter().map(|s| s.as_str()).collect()
};
let mut typed_rows = Vec::with_capacity(values.len());
for row in values {
if row.len() != target_columns.len() {
return Err(PlannerError::ColumnValueCountMismatch {
columns: target_columns.len(),
values: row.len(),
line: span.start.line,
column: span.start.column,
});
}
let mut typed_values = Vec::with_capacity(row.len());
for (value, col_name) in row.iter().zip(target_columns.iter()) {
let col_meta =
table
.get_column(col_name)
.ok_or_else(|| PlannerError::ColumnNotFound {
column: col_name.to_string(),
table: table.name.clone(),
line: span.start.line,
col: span.start.column,
})?;
let typed_value = self.infer_type(value, table)?;
self.check_null_constraint(col_meta, &typed_value, value.span)?;
self.check_type_compatibility(
&col_meta.data_type,
&typed_value.resolved_type,
value.span,
)?;
if let (
ResolvedType::Vector {
dimension: expected_dim,
..
},
ResolvedType::Vector {
dimension: actual_dim,
..
},
) = (&col_meta.data_type, &typed_value.resolved_type)
{
self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
}
typed_values.push(typed_value);
}
typed_rows.push(typed_values);
}
Ok(typed_rows)
}
pub fn check_assignment(
&self,
table: &TableMetadata,
column: &str,
value: &Expr,
span: Span,
) -> Result<TypedExpr, PlannerError> {
let col_meta = table
.get_column(column)
.ok_or_else(|| PlannerError::ColumnNotFound {
column: column.to_string(),
table: table.name.clone(),
line: span.start.line,
col: span.start.column,
})?;
let typed_value = self.infer_type(value, table)?;
self.check_null_constraint(col_meta, &typed_value, value.span)?;
self.check_type_compatibility(&col_meta.data_type, &typed_value.resolved_type, value.span)?;
if let (
ResolvedType::Vector {
dimension: expected_dim,
..
},
ResolvedType::Vector {
dimension: actual_dim,
..
},
) = (&col_meta.data_type, &typed_value.resolved_type)
{
self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
}
Ok(typed_value)
}
pub fn check_null_constraint(
&self,
column: &crate::catalog::ColumnMetadata,
value: &TypedExpr,
span: Span,
) -> Result<(), PlannerError> {
if column.not_null && matches!(value.resolved_type, ResolvedType::Null) {
Err(PlannerError::NullConstraintViolation {
column: column.name.clone(),
line: span.start.line,
col: span.start.column,
})
} else {
Ok(())
}
}
fn check_type_compatibility(
&self,
expected: &ResolvedType,
actual: &ResolvedType,
span: Span,
) -> Result<(), PlannerError> {
if expected == actual {
return Ok(());
}
if actual.can_cast_to(expected) {
return Ok(());
}
if let (
ResolvedType::Vector {
dimension: d1,
metric: _,
},
ResolvedType::Vector {
dimension: d2,
metric: _,
},
) = (expected, actual)
{
if *d1 == *d2 {
return Ok(());
}
}
Err(PlannerError::TypeMismatch {
expected: expected.type_name().to_string(),
found: actual.type_name().to_string(),
line: span.start.line,
column: span.start.column,
})
}
}
fn is_numeric_type(ty: &ResolvedType) -> bool {
matches!(
ty,
ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Float | ResolvedType::Double
)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct AggregateSignature {
name: String,
distinct: bool,
star: bool,
arg_key: Option<String>,
separator: Option<String>,
}
fn is_aggregate_name(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"count" | "sum" | "total" | "avg" | "min" | "max" | "group_concat" | "string_agg"
)
}
fn aggregate_signature_from_expr(expr: &AggregateExpr) -> AggregateSignature {
let (name, separator, star, arg) = match &expr.function {
AggregateFunction::Count => (
"count".to_string(),
None,
expr.arg.is_none(),
expr.arg.as_ref(),
),
AggregateFunction::Sum => ("sum".to_string(), None, false, expr.arg.as_ref()),
AggregateFunction::Total => ("total".to_string(), None, false, expr.arg.as_ref()),
AggregateFunction::Avg => ("avg".to_string(), None, false, expr.arg.as_ref()),
AggregateFunction::Min => ("min".to_string(), None, false, expr.arg.as_ref()),
AggregateFunction::Max => ("max".to_string(), None, false, expr.arg.as_ref()),
AggregateFunction::GroupConcat { separator } => (
"group_concat".to_string(),
separator.clone(),
false,
expr.arg.as_ref(),
),
AggregateFunction::StringAgg { separator } => (
"string_agg".to_string(),
separator.clone(),
false,
expr.arg.as_ref(),
),
};
AggregateSignature {
name,
distinct: expr.distinct,
star,
arg_key: arg.map(typed_expr_signature),
separator,
}
}
fn aggregate_signature_from_call(
name: &str,
args: &[TypedExpr],
distinct: bool,
star: bool,
) -> Result<AggregateSignature, PlannerError> {
let separator = if name.eq_ignore_ascii_case("group_concat") && args.len() == 2 {
if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
Some(value.clone())
} else {
return Err(PlannerError::invalid_expression(
"GROUP_CONCAT separator must be a string literal".to_string(),
));
}
} else if name.eq_ignore_ascii_case("string_agg") && args.len() == 2 {
if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
Some(value.clone())
} else {
return Err(PlannerError::invalid_expression(
"STRING_AGG separator must be a string literal".to_string(),
));
}
} else {
None
};
Ok(AggregateSignature {
name: name.to_ascii_lowercase(),
distinct,
star,
arg_key: args.first().map(typed_expr_signature),
separator,
})
}
fn typed_expr_signature(expr: &TypedExpr) -> String {
format!("{:?}", expr.kind)
}
fn single_column_type(schema: &[ColumnMetadata], span: Span) -> Result<ResolvedType, PlannerError> {
match schema {
[column] => Ok(column.data_type.clone()),
[] => Err(PlannerError::type_mismatch(
"one-column subquery",
"zero-column subquery",
span,
)),
_ => Err(PlannerError::type_mismatch(
"one-column subquery",
format!("{} columns", schema.len()),
span,
)),
}
}
#[cfg(test)]
#[path = "type_checker/tests.rs"]
mod tests;