diff --git a/Cargo.lock b/Cargo.lock index 85b6bb5763..e7da5c26d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4490,6 +4490,19 @@ dependencies = [ "proptest", ] +[[package]] +name = "spacetimedb-query-planner" +version = "0.12.0" +dependencies = [ + "derive_more", + "spacetimedb-lib", + "spacetimedb-primitives", + "spacetimedb-sats", + "spacetimedb-schema", + "spacetimedb-sql-parser", + "thiserror", +] + [[package]] name = "spacetimedb-quickstart-module" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f4343bbe85..7a22802ce2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/durability", "crates/fs-utils", "crates/lib", + "crates/planner", "crates/metrics", "crates/primitives", "crates/sats", diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index b895660c3c..2ad4f9956e 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -379,6 +379,12 @@ pub fn from_hex_pad, T: AsRef<[u8]>>( hex: T, ) -> Result { let hex = hex.as_ref(); - let hex = if hex.starts_with(b"0x") { &hex[2..] } else { hex }; + let hex = if hex.starts_with(b"0x") { + &hex[2..] + } else if hex.starts_with(b"X'") { + &hex[2..hex.len()] + } else { + hex + }; hex::FromHex::from_hex(hex) } diff --git a/crates/planner/Cargo.toml b/crates/planner/Cargo.toml new file mode 100644 index 0000000000..3832810378 --- /dev/null +++ b/crates/planner/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "spacetimedb-query-planner" +version.workspace = true +edition.workspace = true +license-file = "LICENSE" + +[dependencies] +derive_more.workspace = true +thiserror.workspace = true +spacetimedb-lib.workspace = true +spacetimedb-sats.workspace = true +spacetimedb-schema.workspace = true +spacetimedb-sql-parser.workspace = true + +[dev-dependencies] +spacetimedb-lib.workspace = true +spacetimedb-primitives.workspace = true diff --git a/crates/planner/LICENSE b/crates/planner/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/planner/src/lib.rs b/crates/planner/src/lib.rs new file mode 100644 index 0000000000..82d25f24e8 --- /dev/null +++ b/crates/planner/src/lib.rs @@ -0,0 +1 @@ +pub mod logical; diff --git a/crates/planner/src/logical/bind.rs b/crates/planner/src/logical/bind.rs new file mode 100644 index 0000000000..31b64451c8 --- /dev/null +++ b/crates/planner/src/logical/bind.rs @@ -0,0 +1,414 @@ +use std::sync::Arc; + +use spacetimedb_lib::{from_hex_pad, Address, AlgebraicValue, Identity}; +use spacetimedb_sats::{AlgebraicType, ProductType, ProductTypeElement}; +use spacetimedb_schema::schema::TableSchema; +use spacetimedb_sql_parser::{ + ast::{ + self, + sub::{SqlAst, SqlSelect}, + BinOp, ProjectElem, SqlExpr, SqlFrom, SqlLiteral, + }, + parser::sub::parse_subscription, +}; + +use super::errors::{ConstraintViolation, ResolutionError, TypingError, Unsupported}; +use super::expr::{Expr, Ref, RelExpr, Type, Vars}; + +pub type TypingResult = core::result::Result; + +pub trait SchemaView { + fn schema(&self, name: &str, case_sensitive: bool) -> Option>; +} + +/// Parse and type check a subscription query +pub fn parse_and_type_sub(sql: &str, tx: &impl SchemaView) -> TypingResult { + expect_table_type(type_ast(parse_subscription(sql)?, tx)?) +} + +/// Type check and lower a [SqlAst] into a [RelExpr]. +/// This includes name resolution and variable binding. +pub fn type_ast(expr: SqlAst, tx: &impl SchemaView) -> TypingResult { + match expr { + SqlAst::Union(a, b) => { + let a = type_ast(*a, tx)?; + let b = type_ast(*b, tx)?; + assert_eq_types(a.ty(), b.ty())?; + Ok(RelExpr::Union(Box::new(a), Box::new(b))) + } + SqlAst::Minus(a, b) => { + let a = type_ast(*a, tx)?; + let b = type_ast(*b, tx)?; + assert_eq_types(a.ty(), b.ty())?; + Ok(RelExpr::Minus(Box::new(a), Box::new(b))) + } + SqlAst::Select(SqlSelect { + project, + from, + filter: None, + }) => { + let (arg, vars) = type_from(from, tx)?; + type_proj(project, arg, vars) + } + SqlAst::Select(SqlSelect { + project, + from, + filter: Some(expr), + }) => { + let (from, vars) = type_from(from, tx)?; + let arg = type_select(expr, from, vars.clone())?; + type_proj(project, arg, vars.clone()) + } + } +} + +/// Type check and lower a [SqlFrom] +pub fn type_from(from: SqlFrom, tx: &impl SchemaView) -> TypingResult<(RelExpr, Vars)> { + match from { + SqlFrom::Expr(expr, None) => type_rel(expr, tx), + SqlFrom::Expr(expr, Some(alias)) => { + let (expr, _) = type_rel(expr, tx)?; + let ty = expr.ty().clone(); + Ok((expr, vec![(alias.name, ty)].into())) + } + SqlFrom::Join(r, alias, joins) => { + let (mut vars, mut args, mut exprs) = (Vars::default(), Vec::new(), Vec::new()); + + let (r, _) = type_rel(r, tx)?; + let ty = r.ty().clone(); + + args.push(r); + vars.push((alias.name, ty)); + + for join in joins { + let (r, _) = type_rel(join.expr, tx)?; + let ty = r.ty().clone(); + + args.push(r); + vars.push((join.alias.name, ty)); + + if let Some(on) = join.on { + exprs.push(type_expr(&vars, on, Some(&Type::BOOL))?); + } + } + let types = vars.iter().map(|(_, ty)| ty.clone()).collect(); + let input = RelExpr::Join(args.into(), Type::Tup(types)); + Ok((RelExpr::select(input, vars.clone(), exprs), vars)) + } + } +} + +/// Type check and lower a [ast::RelExpr] +fn type_rel(expr: ast::RelExpr, tx: &impl SchemaView) -> TypingResult<(RelExpr, Vars)> { + match expr { + ast::RelExpr::Var(var) => tx + .schema(&var.name, var.case_sensitive) + .ok_or_else(|| ResolutionError::unresolved_table(&var.name).into()) + .map(|schema| { + ( + RelExpr::RelVar(schema.clone(), Type::Var(schema.clone())), + vec![(var.name, Type::Var(schema))].into(), + ) + }), + ast::RelExpr::Ast(ast) => Ok((type_ast(*ast, tx)?, Vars::default())), + } +} + +/// Type check and lower a [SqlExpr] +fn type_select(expr: SqlExpr, input: RelExpr, vars: Vars) -> TypingResult { + let exprs = vec![type_expr(&vars, expr, Some(&Type::BOOL))?]; + Ok(RelExpr::select(input, vars, exprs)) +} + +/// Type check and lower a [ast::Project] +fn type_proj(proj: ast::Project, input: RelExpr, vars: Vars) -> TypingResult { + match proj { + ast::Project::Star(None) => Ok(input), + ast::Project::Star(Some(var)) => { + let (i, ty) = vars.expect_var(&var.name, None)?; + let ty = ty.clone(); + let refs = vec![Ref::Var(i, ty.clone())]; + Ok(RelExpr::project(input, vars, refs, ty)) + } + ast::Project::Exprs(elems) => { + let (mut refs, mut fields) = (Vec::new(), Vec::new()); + for ProjectElem(expr, alias) in elems { + if let SqlExpr::Var(_) = expr { + return Err(Unsupported::UnqualifiedProjectExpr.into()); + } + let SqlExpr::Field(table, field) = expr else { + return Err(Unsupported::ProjectExpr.into()); + }; + let (i, j, ty) = vars.expect_field(&table.name, &field.name, None)?; + refs.push(Ref::Field(i, j, Type::Alg(ty.clone()))); + if let Some(alias) = alias { + fields.push((alias.name, ty.clone())); + } else { + fields.push((field.name, ty.clone())); + } + } + let ty = Type::Row(ProductType::from_iter( + fields + .into_iter() + .map(|(name, t)| ProductTypeElement::new_named(t, name.into_boxed_str())), + )); + Ok(RelExpr::project(input, vars, refs, ty)) + } + } +} + +/// Type check and lower a [SqlExpr] into a logical [Expr]. +fn type_expr(vars: &Vars, expr: SqlExpr, expected: Option<&Type>) -> TypingResult { + match (expr, expected) { + (SqlExpr::Lit(SqlLiteral::Bool(v)), None | Some(Type::Alg(AlgebraicType::Bool))) => Ok(Expr::bool(v)), + (SqlExpr::Lit(SqlLiteral::Bool(_)), Some(t)) => Err(unexpected_type(&Type::BOOL, t)), + (SqlExpr::Lit(SqlLiteral::Str(v)), None | Some(Type::Alg(AlgebraicType::String))) => Ok(Expr::str(v)), + (SqlExpr::Lit(SqlLiteral::Str(_)), Some(t)) => Err(unexpected_type(&Type::STR, t)), + (SqlExpr::Lit(SqlLiteral::Num(_) | SqlLiteral::Hex(_)), None) => Err(ResolutionError::UntypedLiteral.into()), + (SqlExpr::Lit(SqlLiteral::Num(v) | SqlLiteral::Hex(v)), Some(t)) => parse(v, t), + (SqlExpr::Var(var), expected) => vars.expect_var_ref(&var.name, expected), + (SqlExpr::Field(table, field), expected) => vars.expect_field_ref(&table.name, &field.name, expected), + (SqlExpr::Bin(a, b, op), None | Some(Type::Alg(AlgebraicType::Bool))) => match (*a, *b) { + (a, b @ SqlExpr::Lit(_)) | (b @ SqlExpr::Lit(_), a) | (a, b) => { + let a = expect_op_type(op, type_expr(vars, a, None)?)?; + let b = expect_op_type(op, type_expr(vars, b, Some(a.ty()))?)?; + Ok(Expr::Bin(op, Box::new(a), Box::new(b))) + } + }, + (SqlExpr::Bin(..), Some(t)) => Err(unexpected_type(&Type::BOOL, t)), + } +} + +/// Parses a source text literal as a particular type +fn parse(v: String, ty: &Type) -> TypingResult { + let constraint_err = |v, ty| TypingError::from(ConstraintViolation::lit(v, ty)); + match ty { + Type::Alg(AlgebraicType::I8) => v + .parse::() + .map(AlgebraicValue::I8) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::U8) => v + .parse::() + .map(AlgebraicValue::U8) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::I16) => v + .parse::() + .map(AlgebraicValue::I16) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::U16) => v + .parse::() + .map(AlgebraicValue::U16) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::I32) => v + .parse::() + .map(AlgebraicValue::I32) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::U32) => v + .parse::() + .map(AlgebraicValue::U32) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::I64) => v + .parse::() + .map(AlgebraicValue::I64) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::U64) => v + .parse::() + .map(AlgebraicValue::U64) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::F32) => v + .parse::() + .map(|v| AlgebraicValue::F32(v.into())) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::F64) => v + .parse::() + .map(|v| AlgebraicValue::F64(v.into())) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::I128) => v + .parse::() + .map(|v| AlgebraicValue::I128(v.into())) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(AlgebraicType::U128) => v + .parse::() + .map(|v| AlgebraicValue::U128(v.into())) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(t) if t.is_bytes() => from_hex_pad::, _>(&v) + .map(|v| AlgebraicValue::Bytes(v.into_boxed_slice())) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(t) if t.is_identity() => Identity::from_hex(&v) + .map(AlgebraicValue::from) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + Type::Alg(t) if t.is_address() => Address::from_hex(&v) + .map(AlgebraicValue::from) + .map(|v| Expr::Lit(v, ty.clone())) + .map_err(|_| constraint_err(&v, ty)), + _ => Err(constraint_err(&v, ty)), + } +} + +/// Returns a type constraint violation for an unexpected type +fn unexpected_type(expected: &Type, actual: &Type) -> TypingError { + ConstraintViolation::eq(expected, actual).into() +} + +/// Returns an error if the input type is not a table type [Type::Var] +fn expect_table_type(expr: RelExpr) -> TypingResult { + match expr.ty() { + Type::Var(_) => Ok(expr), + _ => Err(Unsupported::SubReturnType.into()), + } +} + +/// Assert that this type is compatible with this operator +fn expect_op_type(op: BinOp, expr: Expr) -> TypingResult { + match (op, expr.ty()) { + // Logic operators take booleans + (BinOp::And | BinOp::Or, Type::Alg(AlgebraicType::Bool)) => Ok(expr), + // Comparison operators take integers or floats + (BinOp::Lt | BinOp::Gt | BinOp::Lte | BinOp::Gte, Type::Alg(t)) if t.is_integer() || t.is_float() => Ok(expr), + // Equality supports numerics, strings, and bytes + (BinOp::Eq | BinOp::Ne, Type::Alg(t)) + if t.is_bool() + || t.is_integer() + || t.is_float() + || t.is_string() + || t.is_bytes() + || t.is_identity() + || t.is_address() => + { + Ok(expr) + } + (op, ty) => Err(ConstraintViolation::op(op, ty).into()), + } +} + +fn assert_eq_types(a: &Type, b: &Type) -> TypingResult<()> { + if a == b { + Ok(()) + } else { + Err(unexpected_type(a, b)) + } +} + +#[cfg(test)] +mod tests { + use spacetimedb_lib::{db::raw_def::v9::RawModuleDefV9Builder, AlgebraicType, ProductType}; + use spacetimedb_primitives::TableId; + use spacetimedb_schema::{def::ModuleDef, schema::TableSchema}; + use std::sync::Arc; + + use super::{parse_and_type_sub, SchemaView}; + + fn module_def() -> ModuleDef { + let mut builder = RawModuleDefV9Builder::new(); + builder.build_table_with_new_type( + "t", + ProductType::from([ + ("u32", AlgebraicType::U32), + ("f32", AlgebraicType::F32), + ("str", AlgebraicType::String), + ("arr", AlgebraicType::array(AlgebraicType::String)), + ]), + true, + ); + builder.build_table_with_new_type( + "s", + ProductType::from([ + ("id", AlgebraicType::identity()), + ("u32", AlgebraicType::U32), + ("arr", AlgebraicType::array(AlgebraicType::String)), + ("bytes", AlgebraicType::bytes()), + ]), + true, + ); + builder.finish().try_into().expect("failed to generate module def") + } + + struct SchemaViewer(ModuleDef); + + impl SchemaView for SchemaViewer { + fn schema(&self, name: &str, _: bool) -> Option> { + self.0.table(name).map(|def| { + Arc::new(TableSchema::from_module_def( + def, + TableId(if *def.name == *"t" { 0 } else { 1 }), + )) + }) + } + } + + #[test] + fn valid() { + let tx = SchemaViewer(module_def()); + + for sql in [ + "select * from t", + "select * from t where true", + "select * from t where t.u32 = 1", + "select * from t where t.u32 = 1 or t.str = ''", + "select * from s where s.bytes = 0xABCD", + "select * from s where s.bytes = X'ABCD'", + "select * from s as r where r.bytes = 0xABCD", + "select * from (select t.* from t join s)", + "select * from (select t.* from t join s on t.u32 = s.u32 where t.f32 = 0.1)", + "select * from (select t.* from t join (select s.u32 from s) s on t.u32 = s.u32)", + ] { + let result = parse_and_type_sub(sql, &tx).inspect_err(|_| { + // println!("sql: {}\n\n\terr: {}\n", sql, err); + }); + assert!(result.is_ok()); + } + } + + #[test] + fn invalid() { + let tx = SchemaViewer(module_def()); + + for sql in [ + // Table r does not exist + "select * from r", + // Field u32 is not in scope + "select * from t where u32 = 1", + // Field a does not exist on table t + "select * from t where t.a = 1", + // Field a does not exist on table t + "select * from t as r where r.a = 1", + // Field u32 is not a string + "select * from t where t.u32 = 'str'", + // Field u32 is not a float + "select * from t where t.u32 = 1.3", + // t is not in scope after alias + "select * from t as r where t.u32 = 5", + // Field u32 is not in scope + "select u32 from t", + // Subscriptions must be typed to a single table + "select t.u32 from t", + // Subscriptions must be typed to a single table + "select * from t join s", + // Product values are not comparable + "select * from (select t.* from t join s on t.arr = s.arr)", + // Subscriptions must be typed to a single table + "select * from (select s.* from t join (select s.u32 from s) s on t.u32 = s.u32)", + // Field bytes is no longer in scope + "select * from (select t.* from t join (select s.u32 from s) s on s.bytes = 0xABCD)", + ] { + let result = parse_and_type_sub(sql, &tx).inspect_err(|_| { + // println!("sql: {}\n\n\terr: {}\n", sql, err); + }); + assert!(result.is_err()); + } + } +} diff --git a/crates/planner/src/logical/errors.rs b/crates/planner/src/logical/errors.rs new file mode 100644 index 0000000000..3234b25159 --- /dev/null +++ b/crates/planner/src/logical/errors.rs @@ -0,0 +1,98 @@ +use spacetimedb_sql_parser::{ast::BinOp, parser::errors::SqlParseError}; +use thiserror::Error; + +use super::expr::Type; + +#[derive(Error, Debug)] +pub enum ConstraintViolation { + #[error("(expected) {expected} != (actual) {actual}")] + Eq { expected: Type, actual: Type }, + #[error("{0} is not a numeric type")] + Num(Type), + #[error("{0} cannot be interpreted as a byte array")] + Hex(Type), + #[error("{0} cannot be parsed as {1}")] + Lit(String, Type), + #[error("{1} is not supported by the binary operator {0}")] + Op(BinOp, Type), +} + +impl ConstraintViolation { + // Types are not equal + pub fn eq(expected: &Type, actual: &Type) -> Self { + let expected = expected.clone(); + let actual = actual.clone(); + Self::Eq { expected, actual } + } + + // Not a numeric type + pub fn num(t: &Type) -> Self { + Self::Num(t.clone()) + } + + // Not a type that can be compared to a hex value + pub fn hex(t: &Type) -> Self { + Self::Hex(t.clone()) + } + + // This literal expression cannot be parsed as this type + pub fn lit(v: &str, ty: &Type) -> Self { + Self::Lit(v.to_string(), ty.clone()) + } + + // This type is not supported by this operator + pub fn op(op: BinOp, ty: &Type) -> Self { + Self::Op(op, ty.clone()) + } +} + +#[derive(Error, Debug)] +pub enum ResolutionError { + #[error("Cannot resolve {0}")] + Var(String), + #[error("Cannot resolve table {0}")] + Table(String), + #[error("Cannot resolve field {1} in {0}")] + Field(String, String), + #[error("Cannot resolve type for literal expression")] + UntypedLiteral, +} + +impl ResolutionError { + /// Cannot resolve name + pub fn unresolved_var(name: &str) -> Self { + Self::Var(name.to_string()) + } + + /// Cannot resolve table name + pub fn unresolved_table(name: &str) -> Self { + Self::Table(name.to_string()) + } + + /// Cannot resolve field name within table + pub fn unresolved_field(table: &str, field: &str) -> Self { + Self::Field(table.to_string(), field.to_string()) + } +} + +#[derive(Error, Debug)] +pub enum Unsupported { + #[error("Subscriptions must return a single table type")] + SubReturnType, + #[error("Unsupported expression in projection")] + ProjectExpr, + #[error("Unqualified column projections are not supported")] + UnqualifiedProjectExpr, +} + +#[derive(Error, Debug)] +pub enum TypingError { + #[error(transparent)] + Unsupported(#[from] Unsupported), + #[error(transparent)] + Constraint(#[from] ConstraintViolation), + #[error(transparent)] + ResolutionError(#[from] ResolutionError), + #[error(transparent)] + ParseError(#[from] SqlParseError), +} diff --git a/crates/planner/src/logical/expr.rs b/crates/planner/src/logical/expr.rs new file mode 100644 index 0000000000..53f9daa34c --- /dev/null +++ b/crates/planner/src/logical/expr.rs @@ -0,0 +1,317 @@ +use std::fmt::{Display, Formatter}; +use std::sync::Arc; + +use spacetimedb_lib::AlgebraicValue; +use spacetimedb_sats::algebraic_type::fmt::{fmt_algebraic_type, fmt_product_type}; +use spacetimedb_sats::AlgebraicType; +use spacetimedb_sats::ProductType; +use spacetimedb_schema::schema::{ColumnSchema, TableSchema}; +use spacetimedb_sql_parser::ast::BinOp; + +use crate::static_assert_size; + +use super::bind::TypingResult; +use super::errors::{ConstraintViolation, ResolutionError, TypingError}; + +/// The type of a relation or scalar expression +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Type { + /// A base relation + Var(Arc), + /// A derived relation + Row(ProductType), + /// A join relation + Tup(Box<[Type]>), + /// A column type + Alg(AlgebraicType), +} + +static_assert_size!(Type, 24); + +impl Display for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Alg(ty) => write!(f, "{}", fmt_algebraic_type(ty)), + Self::Var(schema) => write!(f, "{}", fmt_product_type(schema.get_row_type())), + Self::Row(ty) => write!(f, "{}", fmt_product_type(ty)), + Self::Tup(types) => { + write!(f, "(")?; + write!(f, "{}", types[0])?; + for t in &types[1..] { + write!(f, ", {}", t)?; + } + write!(f, ")") + } + } + } +} + +impl Type { + /// A constant for the bool type + pub const BOOL: Self = Self::Alg(AlgebraicType::Bool); + + /// A constant for the string type + pub const STR: Self = Self::Alg(AlgebraicType::String); + + /// Is this a numeric type? + pub fn is_num(&self) -> bool { + match self { + Self::Alg(t) => t.is_integer() || t.is_float(), + _ => false, + } + } + + /// Is this a hex type? + pub fn is_hex(&self) -> bool { + match self { + Self::Alg(t) => t.is_bytes() || t.is_identity() || t.is_address(), + _ => false, + } + } + + /// Find a field and its position in a Row or Var type + pub fn find(&self, field: &str) -> Option<(usize, &AlgebraicType)> { + match self { + Self::Var(schema) => schema + .columns() + .iter() + .enumerate() + .find(|(_, ColumnSchema { col_name, .. })| col_name.as_ref() == field) + .map(|(i, ColumnSchema { col_type, .. })| (i, col_type)), + Self::Row(row) => row + .elements + .iter() + .enumerate() + .find(|(_, elem)| elem.has_name(field)) + .map(|(i, elem)| (i, &elem.algebraic_type)), + _ => None, + } + } +} + +/// A logical relational expression +#[derive(Debug)] +pub enum RelExpr { + /// A base table + RelVar(Arc, Type), + /// A filter + Select(Box