From d1fe5cded988349b0bc5dc5dc5cd7b7c9cb115c0 Mon Sep 17 00:00:00 2001 From: Eric Kidd Date: Sun, 29 Oct 2023 07:53:54 -0400 Subject: [PATCH] Infer types for lots of operators --- src/ast.rs | 233 ++++++++++++++++------- src/infer.rs | 194 +++++++++++++++++-- src/scope.rs | 26 ++- src/tokenizer.rs | 4 +- src/transforms/if_to_case.rs | 6 +- src/transforms/in_unnest_to_in_select.rs | 6 +- src/types.rs | 5 +- tests/sql/operators/is.sql | 41 ++++ 8 files changed, 416 insertions(+), 99 deletions(-) create mode 100644 tests/sql/operators/is.sql diff --git a/src/ast.rs b/src/ast.rs index 16539e8..802de51 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -729,61 +729,14 @@ pub enum Expression { ColumnName(Ident), TableAndColumnName(TableAndColumnName), Cast(Cast), - Is { - left: Box, - is_token: Keyword, - not_token: Option, - right: Box, - }, - In { - left: Box, - not_token: Option, - in_token: Keyword, - value_set: InValueSet, - }, - Between { - left: Box, - not_token: Option, - between_token: Keyword, - middle: Box, - and_token: Keyword, - right: Box, - }, - And { - left: Box, - and_token: Keyword, - right: Box, - }, - Or { - left: Box, - or_token: Keyword, - right: Box, - }, - Not { - not_token: Keyword, - expression: Box, - }, - If { - if_token: Keyword, - paren1: Punct, - condition: Box, - comma1: Punct, - then_expression: Box, - comma2: Punct, - else_expression: Box, - paren2: Punct, - }, - Case { - case_token: Keyword, - when_clauses: Vec, - else_clause: Option, - end_token: Keyword, - }, - Binop { - left: Box, - op_token: Punct, - right: Box, - }, + Is(IsExpression), + In(InExpression), + Between(BetweenExpression), + KeywordBinop(KeywordBinopExpression), + Not(NotExpression), + If(IfExpression), + Case(CaseExpression), + Binop(BinopExpression), Parens { paren1: Punct, expression: Box, @@ -800,13 +753,22 @@ pub enum Expression { } impl Expression { + /// Create a new keyword binary operator expression. + fn keyword_binop(left: Expression, op_keyword: Keyword, right: Expression) -> Expression { + Expression::KeywordBinop(KeywordBinopExpression { + left: Box::new(left), + op_keyword, + right: Box::new(right), + }) + } + /// Create a new binary operator expression. fn binop(left: Expression, op_token: Punct, right: Expression) -> Expression { - Expression::Binop { + Expression::Binop(BinopExpression { left: Box::new(left), op_token, right: Box::new(right), - } + }) } } @@ -860,6 +822,75 @@ impl Emit for CastType { } } +/// An `IS` expression. +#[derive(Clone, Debug, Drive, DriveMut, EmitDefault, Spanned, ToTokens)] +pub struct IsExpression { + pub left: Box, + pub is_token: Keyword, + pub not_token: Option, + pub predicate: IsExpressionPredicate, +} + +impl Emit for IsExpression { + fn emit(&self, t: Target, f: &mut TokenWriter<'_>) -> io::Result<()> { + match (&self.predicate, t) { + // BigQuery allows anything. + (_, Target::BigQuery) => self.emit_default(t, f), + // `UNKNOWN` will be translated to `NULL` everywhere else. + (IsExpressionPredicate::Null(_), _) | (IsExpressionPredicate::Unknown(_), _) => { + self.emit_default(t, f) + } + // `TRUE` and `FALSE` work on SQLite3. + (IsExpressionPredicate::True(_), Target::SQLite3) + | (IsExpressionPredicate::False(_), Target::SQLite3) => self.emit_default(t, f), + // For everyone else, we need to use CASE. + (IsExpressionPredicate::True(keyword), _) + | (IsExpressionPredicate::False(keyword), _) => { + f.write_token_start("CASE")?; + self.left.emit(t, f)?; + f.write_token_start("WHEN")?; + keyword.emit(t, f)?; + f.write_token_start("THEN")?; + f.write_token_start("TRUE")?; + f.write_token_start("ELSE")?; + f.write_token_start("FALSE")?; + f.write_token_start("END") + } + } + } +} + +/// An `IS` predicate. +#[derive(Clone, Debug, Drive, DriveMut, EmitDefault, Spanned, ToTokens)] +pub enum IsExpressionPredicate { + Null(Keyword), + True(Keyword), + False(Keyword), + Unknown(PseudoKeyword), +} + +impl Emit for IsExpressionPredicate { + fn emit(&self, t: Target, f: &mut TokenWriter<'_>) -> io::Result<()> { + // Remap UNKNOWN to NULL for everyone but BigQuery. + match (self, t) { + (IsExpressionPredicate::Unknown(_), Target::BigQuery) => self.emit_default(t, f), + (IsExpressionPredicate::Unknown(unknown_token), _) => { + unknown_token.ident.token.with_str("NULL").emit(t, f) + } + _ => self.emit_default(t, f), + } + } +} + +/// An `IN` expression. +#[derive(Clone, Debug, Drive, DriveMut, Emit, EmitDefault, Spanned, ToTokens)] +pub struct InExpression { + pub left: Box, + pub not_token: Option, + pub in_token: Keyword, + pub value_set: InValueSet, +} + /// A value set for an `IN` expression. See the [official grammar][]. /// /// [official grammar]: @@ -885,6 +916,62 @@ pub enum InValueSet { }, } +/// A `BETWEEN` expression. +#[derive(Clone, Debug, Drive, DriveMut, Emit, EmitDefault, Spanned, ToTokens)] +pub struct BetweenExpression { + pub left: Box, + pub not_token: Option, + pub between_token: Keyword, + pub middle: Box, + pub and_token: Keyword, + pub right: Box, +} + +/// A binary operator represented as a keyword. +#[derive(Clone, Debug, Drive, DriveMut, Emit, EmitDefault, Spanned, ToTokens)] +pub struct KeywordBinopExpression { + pub left: Box, + pub op_keyword: Keyword, + pub right: Box, +} + +/// A `NOT` expression. +#[derive(Clone, Debug, Drive, DriveMut, Emit, EmitDefault, Spanned, ToTokens)] +pub struct NotExpression { + pub not_token: Keyword, + pub expression: Box, +} + +/// An `IF` expression. +#[derive(Clone, Debug, Drive, DriveMut, Emit, EmitDefault, Spanned, ToTokens)] +pub struct IfExpression { + pub if_token: Keyword, + pub paren1: Punct, + pub condition: Box, + pub comma1: Punct, + pub then_expression: Box, + pub comma2: Punct, + pub else_expression: Box, + pub paren2: Punct, +} + +/// A `CASE` expression. +#[derive(Clone, Debug, Drive, DriveMut, Emit, EmitDefault, Spanned, ToTokens)] +pub struct CaseExpression { + pub case_token: Keyword, + pub when_clauses: Vec, + pub else_clause: Option, + pub end_token: Keyword, +} + +/// A binary operator. +#[derive(Clone, Debug, Drive, DriveMut, Emit, EmitDefault, Spanned, ToTokens)] +pub struct BinopExpression { + pub left: Box, + pub op_token: Punct, + pub right: Box, +} + /// An `ARRAY` expression. This takes a bunch of different forms in BigQuery. /// /// Not all combinations of our fields are valid. For example, we can't have @@ -1895,34 +1982,36 @@ peg::parser! { /// [precedence table]: /// https://cloud.google.com/bigquery/docs/reference/standard-sql/operators#operator_precedence pub rule expression() -> Expression = precedence! { - left:(@) or_token:k("OR") right:@ { Expression::Or { left: Box::new(left), or_token, right: Box::new(right) } } + left:(@) or_token:k("OR") right:@ { Expression::keyword_binop(left, or_token, right) } -- - left:(@) and_token:k("AND") right:@ { Expression::And { left: Box::new(left), and_token, right: Box::new(right) } } + left:(@) and_token:k("AND") right:@ { Expression::keyword_binop(left, and_token, right) } -- expr:expression_no_and() { expr } } rule expression_no_and() -> Expression = precedence! { - not_token:k("NOT") expression:@ { Expression::Not { not_token, expression: Box::new(expression) } } + not_token:k("NOT") expression:@ { Expression::Not(NotExpression { not_token, expression: Box::new(expression) }) } -- - left:(@) is_token:k("IS") not_token:k("NOT")? right:@ { Expression::Is { left: Box::new(left), is_token, not_token, right: Box::new(right) } } + left:(@) is_token:k("IS") not_token:k("NOT")? predicate:is_expression_predicate() { + Expression::Is(IsExpression { left: Box::new(left), is_token, not_token, predicate }) + } left:(@) not_token:k("NOT")? in_token:k("IN") value_set:in_value_set() { - Expression::In { + Expression::In(InExpression { left: Box::new(left), not_token, in_token, value_set, - } + }) } left:(@) not_token:k("NOT")? between_token:k("BETWEEN") middle:expression_no_and() and_token:k("AND") right:@ { - Expression::Between { + Expression::Between(BetweenExpression { left: Box::new(left), not_token, between_token, middle: Box::new(middle), and_token, right: Box::new(right), - } + }) } left:(@) op_token:p("=") right:@ { Expression::binop(left, op_token, right) } left:(@) op_token:p("!=") right:@ { Expression::binop(left, op_token, right) } @@ -1947,15 +2036,15 @@ peg::parser! { } -- case_token:k("CASE") when_clauses:(case_when_clause()*) else_clause:case_else_clause()? end_token:k("END") { - Expression::Case { + Expression::Case(CaseExpression { case_token, when_clauses, else_clause, end_token, - } + }) } if_token:k("IF") paren1:p("(") condition:expression() comma1:p(",") then_expression:expression() comma2:p(",") else_expression:expression() paren2:p(")") { - Expression::If { + Expression::If(IfExpression { if_token, paren1, condition: Box::new(condition), @@ -1964,7 +2053,7 @@ peg::parser! { comma2, else_expression: Box::new(else_expression), paren2, - } + }) } array_expression:array_expression() { Expression::Array(array_expression) } struct_expression:struct_expression() { Expression::Struct(struct_expression) } @@ -2001,6 +2090,12 @@ peg::parser! { DatePart { date_part_token } } + rule is_expression_predicate() -> IsExpressionPredicate + = null_token:k("NULL") { IsExpressionPredicate::Null(null_token) } + / true_token:k("TRUE") { IsExpressionPredicate::True(true_token) } + / false_token:k("FALSE") { IsExpressionPredicate::False(false_token) } + / unknown_token:pk("UNKNOWN") { IsExpressionPredicate::Unknown(unknown_token) } + rule in_value_set() -> InValueSet = paren1:p("(") query_expression:query_expression() paren2:p(")") { InValueSet::QueryExpression { diff --git a/src/infer.rs b/src/infer.rs index 9490e86..4b45965 100644 --- a/src/infer.rs +++ b/src/infer.rs @@ -380,12 +380,20 @@ impl InferTypes for ast::Expression { let arg = ArgumentType::Value; let arg_simple = |ty| arg(ValueType::Simple(ty)); match self { - ast::Expression::BoolValue(_) => Ok((arg_simple(SimpleType::Bool), scope.clone())), ast::Expression::Literal(Literal { value, .. }) => value.infer_types(scope), + ast::Expression::BoolValue(_) => Ok((arg_simple(SimpleType::Bool), scope.clone())), ast::Expression::Null { .. } => Ok((arg_simple(SimpleType::Null), scope.clone())), ast::Expression::ColumnName(ident) => ident.infer_types(scope), ast::Expression::TableAndColumnName(name) => name.infer_types(scope), ast::Expression::Cast(cast) => cast.infer_types(scope), + ast::Expression::Is(is) => is.infer_types(scope), + ast::Expression::In(in_expr) => in_expr.infer_types(scope), + ast::Expression::Between(between) => between.infer_types(scope), + ast::Expression::KeywordBinop(binop) => binop.infer_types(scope), + ast::Expression::Not(not) => not.infer_types(scope), + ast::Expression::If(if_expr) => if_expr.infer_types(scope), + ast::Expression::Case(case) => case.infer_types(scope), + ast::Expression::Binop(binop) => binop.infer_types(scope), ast::Expression::FunctionCall(fcall) => fcall.infer_types(scope), _ => Err(nyi(self, "expression")), } @@ -431,8 +439,6 @@ impl InferTypes for ast::TableAndColumnName { let table = ident_from_table_name(table_name)?; let table_type = scope.get_or_err(&table)?.try_as_table_type(&table)?; let column_type = table_type.column_by_name_or_err(column_name)?; - // TODO: Actually, we ought to be able store `ArgumentType` in a - // column. See the test case `scoped_aggregates.sql`. Ok((column_type.ty.to_owned(), scope.clone())) } } @@ -453,34 +459,162 @@ impl InferTypes for ast::Cast { } } -impl InferTypes for ast::FunctionCall { +impl InferTypes for ast::IsExpression { type Type = ArgumentType; fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { - let ast::FunctionCall { - name, - args, - over_clause, - .. - } = self; + // We need to do this manually because our second argument isn't an + // expression. + let func_name = &CaseInsensitiveIdent::new("%IS", self.is_token.span()); + let func_ty = scope + .get_or_err(func_name)? + .try_as_function_type(func_name)?; + let arg_types = [ + self.left.infer_types(scope)?.0, + self.predicate.infer_types(scope)?.0, + ]; + let ret_ty = func_ty.return_type_for(&arg_types, func_name)?; + Ok((ret_ty, scope.clone())) + } +} - let name = ident_from_function_name(name)?; - let func_ty = scope.get_or_err(&name)?.try_as_function_type(&name)?; +impl InferTypes for ast::IsExpressionPredicate { + type Type = ArgumentType; - if over_clause.is_some() { - return Err(nyi(over_clause, "over clause")); + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + // Return either BOOL or NULL type, depending on the predicate. The + // types for the %IS primitive will use this to verify that the left + // argument and predicate are compatible. + match self { + ast::IsExpressionPredicate::Null(_) | ast::IsExpressionPredicate::Unknown(_) => Ok(( + ArgumentType::Value(ValueType::Simple(SimpleType::Null)), + scope.clone(), + )), + ast::IsExpressionPredicate::True(_) | ast::IsExpressionPredicate::False(_) => Ok(( + ArgumentType::Value(ValueType::Simple(SimpleType::Bool)), + scope.clone(), + )), } + } +} - let mut arg_types = vec![]; - for arg in args.node_iter_mut() { - let (ty, _scope) = arg.infer_types(scope)?; - arg_types.push(ty); - } - let ret_ty = func_ty.return_type_for(&arg_types, &name)?; +impl InferTypes for ast::InExpression { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + // We need to do this manually because our second argument isn't an + // expression. + let func_name = &CaseInsensitiveIdent::new("%IN", self.in_token.span()); + let func_ty = scope + .get_or_err(func_name)? + .try_as_function_type(func_name)?; + let arg_types = [ + self.left.infer_types(scope)?.0, + self.value_set.infer_types(scope)?.0, + ]; + let ret_ty = func_ty.return_type_for(&arg_types, func_name)?; Ok((ret_ty, scope.clone())) } } +impl InferTypes for ast::InValueSet { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + match self { + ast::InValueSet::QueryExpression { .. } => Err(nyi(self, "IN subquery")), + ast::InValueSet::ExpressionList { .. } => Err(nyi(self, "IN expression list")), + ast::InValueSet::Unnest { .. } => Err(nyi(self, "IN unnest")), + } + } +} + +impl InferTypes for ast::BetweenExpression { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + let func_name = &CaseInsensitiveIdent::new("%BETWEEN", self.between_token.span()); + let args = [ + self.left.as_mut(), + self.middle.as_mut(), + self.right.as_mut(), + ]; + infer_call(func_name, args, scope) + } +} + +impl InferTypes for ast::KeywordBinopExpression { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + let func_name = &CaseInsensitiveIdent::new( + &format!("%{}", self.op_keyword.ident.token.as_str()), + self.op_keyword.span(), + ); + let args = [self.left.as_mut(), self.right.as_mut()]; + infer_call(func_name, args, scope) + } +} + +impl InferTypes for ast::NotExpression { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + let func_name = &CaseInsensitiveIdent::new("%NOT", self.not_token.span()); + let args = [self.expression.as_mut()]; + infer_call(func_name, args, scope) + } +} + +impl InferTypes for ast::IfExpression { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + let args = [ + self.condition.as_mut(), + self.then_expression.as_mut(), + self.else_expression.as_mut(), + ]; + infer_call( + &CaseInsensitiveIdent::new("%IF", self.if_token.span()), + args, + scope, + ) + } +} + +impl InferTypes for ast::CaseExpression { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + Err(nyi(self, "CASE")) + } +} + +impl InferTypes for ast::BinopExpression { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + let prim_name = CaseInsensitiveIdent::new( + &format!("%{}", self.op_token.token.as_str()), + self.op_token.span(), + ); + infer_call(&prim_name, [self.left.as_mut(), self.right.as_mut()], scope) + } +} + +impl InferTypes for ast::FunctionCall { + type Type = ArgumentType; + + fn infer_types(&mut self, scope: &ScopeHandle) -> Result<(Self::Type, ScopeHandle)> { + let name = ident_from_function_name(&self.name)?; + if self.over_clause.is_some() { + return Err(nyi(&self.over_clause, "over clause")); + } + infer_call(&name, self.args.node_iter_mut(), scope) + } +} + /// Figure out whether an expression defines an implicit column name. pub trait InferColumnName { /// Infer the column name, if any. @@ -530,6 +664,26 @@ fn ident_from_function_name(function_name: &ast::FunctionName) -> Result( + func_name: &CaseInsensitiveIdent, + args: ArgExprs, + scope: &ScopeHandle, +) -> Result<(ArgumentType, ScopeHandle)> +where + ArgExprs: IntoIterator, +{ + let func_ty = scope + .get_or_err(func_name)? + .try_as_function_type(func_name)?; + let mut arg_types = vec![]; + for arg in args { + arg_types.push(arg.infer_types(scope)?.0); + } + let ret_ty = func_ty.return_type_for(&arg_types, func_name)?; + Ok((ret_ty, scope.clone())) +} + /// Print a pretty error message when we haven't implemented something. fn nyi(spanned: &dyn Spanned, name: &str) -> Error { Error::annotated( diff --git a/src/scope.rs b/src/scope.rs index 4f53ae4..f83628c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -224,10 +224,34 @@ impl Scope { /// Built-in function declarations in the default scope. static BUILT_IN_FUNCTIONS: &str = " + +-- Primitives. + +%AND = Fn(BOOL, BOOL) -> BOOL; +%BETWEEN = Fn(?T, ?T, ?T) -> BOOL; +%IF = Fn(BOOL, ?T, ?T) -> ?T; +%IN = Fn(?T, ARRAY) -> BOOL; +%IS = Fn(?T, NULL) -> BOOL | Fn(BOOL, BOOL) -> BOOL; +%NOT = Fn(BOOL) -> BOOL; +%OR = Fn(BOOL, BOOL) -> BOOL; + +%= = Fn(?T, ?T) -> BOOL; +%!= = Fn(?T, ?T) -> BOOL; +%<= = Fn(?T, ?T) -> BOOL; +%< = Fn(?T, ?T) -> BOOL; +%> = Fn(?T, ?T) -> BOOL; +%>= = Fn(?T, ?T) -> BOOL; +%+ = Fn(INT64, INT64) -> INT64 | Fn(FLOAT64, FLOAT64) -> FLOAT64; +%- = Fn(INT64, INT64) -> INT64 | Fn(FLOAT64, FLOAT64) -> FLOAT64; +%* = Fn(INT64, INT64) -> INT64 | Fn(FLOAT64, FLOAT64) -> FLOAT64; +%/ = Fn(INT64, INT64) -> INT64 | Fn(FLOAT64, FLOAT64) -> FLOAT64; + +-- Functions. + ANY_VALUE = FnAgg(Agg) -> ?T; ARRAY_LENGTH = Fn(ARRAY) -> INT64; ARRAY_TO_STRING = Fn(ARRAY, STRING) -> STRING; -AVG = FnAgg(Agg) -> FLOAT64 | FnAgg(Agg) -> FLOAT64; +AVG = FnAgg(Agg) -> INT64 | FnAgg(Agg) -> FLOAT64; COALESCE = Fn(?T, ..?T) -> ?T; CONCAT = Fn(STRING, ..STRING) -> STRING | Fn(BYTES, ..BYTES) -> BYTES; COUNTIF = FnAgg(Agg) -> INT64 | FnOver(Agg) -> INT64; diff --git a/src/tokenizer.rs b/src/tokenizer.rs index d013054..7527ac0 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1300,12 +1300,12 @@ mod test { let if_expr = sql_quote! { IF(TRUE, 2.0, 1.0) } .try_into_expression() .unwrap(); - if let ast::Expression::If { + if let ast::Expression::If(ast::IfExpression { condition, then_expression, else_expression, .. - } = &if_expr + }) = &if_expr { let case_expr = sql_quote! { CASE WHEN #condition THEN #then_expression ELSE #else_expression END } diff --git a/src/transforms/if_to_case.rs b/src/transforms/if_to_case.rs index d9e0407..d4898e5 100644 --- a/src/transforms/if_to_case.rs +++ b/src/transforms/if_to_case.rs @@ -2,7 +2,7 @@ use derive_visitor::{DriveMut, VisitorMut}; use joinery_macros::sql_quote; use crate::{ - ast::{self, Expression}, + ast::{self, Expression, IfExpression}, errors::Result, }; @@ -16,12 +16,12 @@ pub struct IfToCase; impl IfToCase { fn enter_expression(&mut self, expr: &mut Expression) { - if let Expression::If { + if let Expression::If(IfExpression { condition, then_expression, else_expression, .. - } = expr + }) = expr { let replacement = sql_quote! { CASE WHEN #condition THEN #then_expression ELSE #else_expression END diff --git a/src/transforms/in_unnest_to_in_select.rs b/src/transforms/in_unnest_to_in_select.rs index e2ff9bf..5fae2d6 100644 --- a/src/transforms/in_unnest_to_in_select.rs +++ b/src/transforms/in_unnest_to_in_select.rs @@ -2,7 +2,7 @@ use derive_visitor::{DriveMut, VisitorMut}; use joinery_macros::sql_quote; use crate::{ - ast::{self, Expression, InValueSet}, + ast::{self, Expression, InExpression, InValueSet}, errors::Result, }; @@ -15,12 +15,12 @@ pub struct InUnnestToInSelect; impl InUnnestToInSelect { fn enter_expression(&mut self, expr: &mut Expression) { - if let Expression::In { + if let Expression::In(InExpression { left, not_token, in_token, value_set: InValueSet::Unnest { expression, .. }, - } = expr + }) = expr { let replacement = sql_quote! { #left #not_token #in_token (SELECT * FROM UNNEST(#expression)) diff --git a/src/types.rs b/src/types.rs index cb039a1..aacadca 100644 --- a/src/types.rs +++ b/src/types.rs @@ -998,7 +998,7 @@ peg::parser! { } rule function_decl() -> (Ident, FunctionType) - = name:ident() _? "=" _? ty:function_type() { + = name:(prim() / ident()) _? "=" _? ty:function_type() { (name, ty) } @@ -1102,6 +1102,9 @@ peg::parser! { Ident::new(name, Span::Unknown) } + rule prim() -> Ident + = name:$("%" [^ ' ' | '\t' ]+) { Ident::new(name, Span::Unknown )} + rule type_var() -> TypeVar = "?" name:$(['A'..='Z']['a'..='z' | 'A'..='Z' | '0'..='9' | '_']*) { TypeVar::new(name).unwrap() diff --git a/tests/sql/operators/is.sql b/tests/sql/operators/is.sql new file mode 100644 index 0000000..bbbc5d9 --- /dev/null +++ b/tests/sql/operators/is.sql @@ -0,0 +1,41 @@ +-- IS operator tests. +CREATE OR REPLACE TABLE __result1 AS +SELECT + 1 IS NULL AS one_is_null, + 1 IS NOT NULL AS one_is_not_null, + NULL IS NULL AS null_is_null, + NULL IS NOT NULL AS null_is_not_null, + TRUE IS TRUE AS true_is_true, + NULL IS TRUE AS null_is_true, + FALSE IS FALSE AS false_is_false, + NULL IS FALSE AS null_is_false, + NULL IS UNKNOWN AS null_is_unknown, + NULL IS NOT UNKNOWN AS null_is_not_unknown, + 1 IS UNKNOWN AS one_is_unknown; + +CREATE OR REPLACE TABLE __expected1 ( + one_is_null BOOL, + one_is_not_null BOOL, + null_is_null BOOL, + null_is_not_null BOOL, + true_is_true BOOL, + null_is_true BOOL, + false_is_false BOOL, + null_is_false BOOL, + null_is_unknown BOOL, + null_is_not_unknown BOOL, + one_is_unknown BOOL, +); +INSERT INTO __expected1 VALUES ( + FALSE, -- one_is_null + TRUE, -- one_is_not_null + TRUE, -- null_is_null + FALSE, -- null_is_not_null + TRUE, -- true_is_true + FALSE, -- null_is_true + TRUE, -- false_is_false + FALSE, -- null_is_false + TRUE, -- null_is_unknown + FALSE, -- null_is_not_unknown + FALSE -- one_is_unknown +); \ No newline at end of file