diff --git a/crates/expr/src/expr.rs b/crates/expr/src/expr.rs index 4f4a68592a0..8ce3576a824 100644 --- a/crates/expr/src/expr.rs +++ b/crates/expr/src/expr.rs @@ -322,6 +322,8 @@ pub enum Expr { LogOp(LogOp, Box, Box), /// A typed literal expression Value(AlgebraicValue, AlgebraicType), + /// A typed literal tuple expression + Tuple(Box<[AlgebraicValue]>, AlgebraicType), /// A field projection Field(FieldProject), } @@ -335,7 +337,7 @@ impl Expr { a.visit(f); b.visit(f); } - Self::Value(..) | Self::Field(..) => {} + Self::Value(..) | Self::Tuple(..) | Self::Field(..) => {} } } @@ -347,7 +349,7 @@ impl Expr { a.visit_mut(f); b.visit_mut(f); } - Self::Value(..) | Self::Field(..) => {} + Self::Value(..) | Self::Tuple(..) | Self::Field(..) => {} } } @@ -365,7 +367,7 @@ impl Expr { pub fn ty(&self) -> &AlgebraicType { match self { Self::BinOp(..) | Self::LogOp(..) => &AlgebraicType::Bool, - Self::Value(_, ty) | Self::Field(FieldProject { ty, .. }) => ty, + Self::Value(_, ty) | Self::Tuple(_, ty) | Self::Field(FieldProject { ty, .. }) => ty, } } } diff --git a/crates/expr/src/lib.rs b/crates/expr/src/lib.rs index 4860bdc882e..f1ad903a2a0 100644 --- a/crates/expr/src/lib.rs +++ b/crates/expr/src/lib.rs @@ -14,7 +14,7 @@ use expr::AggType; use expr::{Expr, FieldProject, ProjectList, ProjectName, RelExpr}; use spacetimedb_lib::ser::Serialize; use spacetimedb_lib::Timestamp; -use spacetimedb_lib::{from_hex_pad, AlgebraicType, AlgebraicValue, ConnectionId, Identity}; +use spacetimedb_lib::{from_hex_pad, AlgebraicType, AlgebraicValue, ConnectionId, Identity, ProductType, ProductTypeElement}; use spacetimedb_sats::algebraic_type::fmt::fmt_algebraic_type; use spacetimedb_sats::algebraic_value::ser::ValueSerializer; use spacetimedb_schema::schema::ColumnSchema; @@ -90,6 +90,31 @@ pub(crate) fn type_expr(vars: &Relvars, expr: SqlExpr, expected: Option<&Algebra parse(&v, ty).map_err(|_| InvalidLiteral::new(v.into_string(), ty))?, ty.clone(), )), + (SqlExpr::Tup(_), None) => { + Err(Unresolved::Literal.into()) + } + (SqlExpr::Tup(t), Some(&AlgebraicType::Product(ref pty))) => Ok(Expr::Tuple( + t.iter().zip(pty.elements.iter()).map(|(lit, ty)| { + match (lit, ty) { + (SqlLiteral::Bool(v), ProductTypeElement { + algebraic_type: AlgebraicType::Bool, + .. + }) => Ok(AlgebraicValue::Bool(*v)), + (SqlLiteral::Bool(_), ProductTypeElement { + algebraic_type: ty, + .. + }) => Err(UnexpectedType::new(&AlgebraicType::Bool, ty).into()), + (SqlLiteral::Str(v) | SqlLiteral::Num(v) | SqlLiteral::Hex(v), ProductTypeElement { + algebraic_type: ty, + .. + }) => Ok(parse(&v, ty).map_err(|_| InvalidLiteral::new(v.clone().into_string(), ty))?), + } + }).collect::>>()?, + AlgebraicType::Product(pty.clone()), + )), + (SqlExpr::Tup(_), Some(ty)) => { + Err(UnexpectedType::new(&AlgebraicType::Product(ProductType::unit()), ty).into()) + } (SqlExpr::Field(SqlIdent(table), SqlIdent(field)), None) => { let table_type = vars.deref().get(&table).ok_or_else(|| Unresolved::var(&table))?; let ColumnSchema { col_pos, col_type, .. } = table_type @@ -145,15 +170,19 @@ pub(crate) fn type_expr(vars: &Relvars, expr: SqlExpr, expected: Option<&Algebra } /// Is this type compatible with this binary operator? -fn op_supports_type(_op: BinOp, t: &AlgebraicType) -> bool { - t.is_bool() - || t.is_integer() - || t.is_float() - || t.is_string() - || t.is_bytes() - || t.is_identity() - || t.is_connection_id() - || t.is_timestamp() +fn op_supports_type(op: BinOp, ty: &AlgebraicType) -> bool { + match (ty, op) { + (AlgebraicType::Product(_), BinOp::Eq | BinOp::Ne) => true, + _ if ty.is_bool() => true, + _ if ty.is_integer() => true, + _ if ty.is_float() => true, + _ if ty.is_string() => true, + _ if ty.is_bytes() => true, + _ if ty.is_identity() => true, + _ if ty.is_connection_id() => true, + _ if ty.is_timestamp() => true, + _ => false, + } } /// Parse an integer literal into an [AlgebraicValue] diff --git a/crates/physical-plan/src/compile.rs b/crates/physical-plan/src/compile.rs index 56215f5e354..7ed01e737d2 100644 --- a/crates/physical-plan/src/compile.rs +++ b/crates/physical-plan/src/compile.rs @@ -23,6 +23,7 @@ fn compile_expr(expr: Expr, var: &mut impl VarLabel) -> PhysicalExpr { PhysicalExpr::BinOp(op, a, b) } Expr::Value(v, _) => PhysicalExpr::Value(v), + Expr::Tuple(t, _) => PhysicalExpr::Tuple(t), Expr::Field(proj) => PhysicalExpr::Field(compile_field_project(var, proj)), } } diff --git a/crates/physical-plan/src/plan.rs b/crates/physical-plan/src/plan.rs index 6fa9b58297e..ae1aff9e56e 100644 --- a/crates/physical-plan/src/plan.rs +++ b/crates/physical-plan/src/plan.rs @@ -1031,6 +1031,8 @@ pub enum PhysicalExpr { BinOp(BinOp, Box, Box), /// A constant algebraic value Value(AlgebraicValue), + /// A tuple of constant algebraic values + Tuple(Box<[AlgebraicValue]>), /// A field projection expression Field(TupleField), } @@ -1094,6 +1096,7 @@ impl PhysicalExpr { pub fn map(self, f: &impl Fn(Self) -> Self) -> Self { match f(self) { value @ Self::Value(..) => value, + values @ Self::Tuple(..) => values, field @ Self::Field(..) => field, Self::BinOp(op, a, b) => Self::BinOp(op, Box::new(a.map(f)), Box::new(b.map(f))), Self::LogOp(op, exprs) => Self::LogOp(op, exprs.into_iter().map(|expr| expr.map(f)).collect()), @@ -1155,6 +1158,7 @@ impl PhysicalExpr { Cow::Owned(value) } Self::Value(v) => Cow::Borrowed(v), + Self::Tuple(t) => Cow::Owned(AlgebraicValue::Product(ProductValue {elements: t.clone()})), } } @@ -1173,7 +1177,7 @@ impl PhysicalExpr { .collect(), ), Self::BinOp(op, a, b) => Self::BinOp(op, Box::new(a.flatten()), Box::new(b.flatten())), - Self::Field(..) | Self::Value(..) => self, + Self::Field(..) | Self::Value(..) | Self::Tuple(..) => self, } } } diff --git a/crates/sql-parser/src/ast/mod.rs b/crates/sql-parser/src/ast/mod.rs index 776d4fc5006..b3b022e1971 100644 --- a/crates/sql-parser/src/ast/mod.rs +++ b/crates/sql-parser/src/ast/mod.rs @@ -107,6 +107,8 @@ impl Project { pub enum SqlExpr { /// A constant expression Lit(SqlLiteral), + /// A tuple of constant expressions + Tup(Vec), /// Unqualified column ref Var(SqlIdent), /// A parameter prefixed with `:` @@ -123,7 +125,7 @@ impl SqlExpr { pub fn qualify_vars(self, with: SqlIdent) -> Self { match self { Self::Var(name) => Self::Field(with, name), - Self::Lit(..) | Self::Field(..) | Self::Param(..) => self, + Self::Lit(..) | Self::Tup(..) | Self::Field(..) | Self::Param(..) => self, Self::Bin(a, b, op) => Self::Bin( Box::new(a.qualify_vars(with.clone())), Box::new(b.qualify_vars(with)), @@ -149,7 +151,7 @@ impl SqlExpr { /// We need to know in order to hash subscription queries correctly. pub fn has_parameter(&self) -> bool { match self { - Self::Lit(_) | Self::Var(_) | Self::Field(..) => false, + Self::Lit(_) | Self::Tup(_) | Self::Var(_) | Self::Field(..) => false, Self::Param(Parameter::Sender) => true, Self::Bin(a, b, _) | Self::Log(a, b, _) => a.has_parameter() || b.has_parameter(), } @@ -158,7 +160,7 @@ impl SqlExpr { /// Replace the `:sender` parameter with the [Identity] it represents pub fn resolve_sender(self, sender_identity: Identity) -> Self { match self { - Self::Lit(_) | Self::Var(_) | Self::Field(..) => self, + Self::Lit(_) | Self::Tup(_) | Self::Var(_) | Self::Field(..) => self, Self::Param(Parameter::Sender) => { Self::Lit(SqlLiteral::Hex(String::from(sender_identity.to_hex()).into_boxed_str())) } diff --git a/crates/sql-parser/src/parser/mod.rs b/crates/sql-parser/src/parser/mod.rs index 61260823a43..9220570dbd2 100644 --- a/crates/sql-parser/src/parser/mod.rs +++ b/crates/sql-parser/src/parser/mod.rs @@ -215,6 +215,12 @@ pub(crate) fn parse_expr(expr: Expr) -> SqlParseResult { Expr::Nested(expr) => parse_expr(*expr), Expr::Value(Value::Placeholder(param)) if ¶m == ":sender" => Ok(SqlExpr::Param(Parameter::Sender)), Expr::Value(v) => Ok(SqlExpr::Lit(parse_literal(v)?)), + Expr::Tuple(ref t) => Ok(SqlExpr::Tup(t.iter().map(|x| { + match x { + Expr::Value(v) => parse_literal(v.clone()), + _ => Err(SqlUnsupported::Expr(expr.clone()).into()), + } + }).collect::>>()?)), Expr::UnaryOp { op: UnaryOperator::Plus, expr,