diff --git a/CHANGELOG.md b/CHANGELOG.md index de149fb5f79b..346f5c13e1d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ **Features**: +- Add simple enumeration type, and dedicated support for name resolution when an + enum is used as a function parameter. (@kgutwin, #6104) + **Fixes**: **Documentation**: diff --git a/prqlc/prqlc-parser/src/parser/expr.rs b/prqlc/prqlc-parser/src/parser/expr.rs index b2345c8141c8..b961383bc4dd 100644 --- a/prqlc/prqlc-parser/src/parser/expr.rs +++ b/prqlc/prqlc-parser/src/parser/expr.rs @@ -173,6 +173,41 @@ where }) } +pub(crate) fn literal_tuple<'a, I>() -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a +where + I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, +{ + use chumsky::recovery::{skip_then_retry_until, via_parser}; + + sequence(maybe_aliased(expr()).validate(|expr, extra, emit| { + let span = extra.span(); + + if expr.alias.is_none() { + emit.emit(Rich::custom(span, "must specify an alias for this value")); + } + + match expr.kind { + ExprKind::Literal(_) => (), + _ => emit.emit(Rich::custom(extra.span(), "expected a literal value")), + }; + + expr + })) + .delimited_by( + ctrl('{'), + ctrl('}') + .recover_with(via_parser(end())) + .recover_with(skip_then_retry_until( + any_ref().ignored(), + ctrl('}').ignored().or(ctrl(',').ignored()).or(end()), + )), + ) + .map(ExprKind::Tuple) + .labelled("literal tuple") + .map_with(|kind, extra| ExprKind::into_expr(kind, extra.span())) + .boxed() +} + fn tuple<'a, I>( nested_expr: impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, ) -> impl Parser<'a, I, ExprKind, ParserError<'a>> + Clone + 'a diff --git a/prqlc/prqlc-parser/src/parser/pr/types.rs b/prqlc/prqlc-parser/src/parser/pr/types.rs index a9565ad28edc..e1452c79e10f 100644 --- a/prqlc/prqlc-parser/src/parser/pr/types.rs +++ b/prqlc/prqlc-parser/src/parser/pr/types.rs @@ -3,6 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use strum::AsRefStr; +use crate::parser::pr::expr::Expr; use crate::parser::pr::ident::Ident; use crate::span::Span; @@ -32,6 +33,9 @@ pub enum TyKind { /// Type of functions with defined params and return types. Function(Option), + + /// Simple enumeration type + Enum(Box), } impl TyKind { diff --git a/prqlc/prqlc-parser/src/parser/stmt.rs b/prqlc/prqlc-parser/src/parser/stmt.rs index e7484a6cdfb3..9c146a6196c3 100644 --- a/prqlc/prqlc-parser/src/parser/stmt.rs +++ b/prqlc/prqlc-parser/src/parser/stmt.rs @@ -6,7 +6,7 @@ use chumsky::prelude::*; use itertools::Itertools; use semver::VersionReq; -use super::expr::{expr, expr_call, ident, pipeline}; +use super::expr::{expr, expr_call, ident, literal_tuple, pipeline}; use super::{ctrl, ident_part, into_stmt, keyword, new_line, pipe, with_doc_comment}; use crate::lexer::lr; use crate::lexer::lr::{Literal, TokenKind}; @@ -74,7 +74,13 @@ where let stmt_kind = new_line() .repeated() .collect::>() - .ignore_then(choice((module_def, type_def(), import_def(), var_def()))); + .ignore_then(choice(( + module_def, + type_def(), + enum_def(), + import_def(), + var_def(), + ))); // Currently doc comments need to be before the annotation; probably // should relax this? @@ -226,12 +232,32 @@ where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, { keyword("type") - .ignore_then(ident_part()) - .then(ctrl('=').ignore_then(type_expr())) - .map(|(name, value)| StmtKind::TypeDef(TypeDef { name, value })) + .ignore_then( + ident_part() + .then(ctrl('=').ignore_then(type_expr())) + .map(|(name, value)| StmtKind::TypeDef(TypeDef { name, value })), + ) .labelled("type definition") } +fn enum_def<'a, I>() -> impl Parser<'a, I, StmtKind, ParserError<'a>> + Clone +where + I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, +{ + keyword("enum") + .ignore_then(ident_part().then(literal_tuple()).map(|(name, value)| { + StmtKind::TypeDef(TypeDef { + name: name.clone(), + value: Ty { + span: value.span, + kind: TyKind::Enum(Box::new(value)), + name: Some(name), + }, + }) + })) + .labelled("enum definition") +} + fn import_def<'a, I>() -> impl Parser<'a, I, StmtKind, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, @@ -397,7 +423,7 @@ mod tests { 0:0-73, ), reason: Simple( - "Expected one of import statement, module definition, new line, pipeline, something else, type definition or variable definition, but didn't find anything before the end.", + "Expected one of enum definition, import statement, module definition, new line, pipeline, something else, type definition or variable definition, but didn't find anything before the end.", ), hints: [], code: None, @@ -698,4 +724,152 @@ mod tests { span: "0:0-139" "#); } + + #[test] + fn enums() { + assert_yaml_snapshot!(parse_module_contents(r#" + enum foo { First = 0, Second = 1 } + "#).unwrap(), @r#" + - TypeDef: + name: foo + value: + kind: + Enum: + Tuple: + - Literal: + Integer: 0 + span: "0:28-29" + alias: First + - Literal: + Integer: 1 + span: "0:40-41" + alias: Second + span: "0:18-43" + span: "0:18-43" + name: foo + span: "0:0-43" + "#) + } + + #[test] + fn enum_must_be_tuple_literal() { + assert_debug_snapshot!(parse_module_contents(r#" + enum foo + "#).unwrap_err(), @r#" + [ + Error { + kind: Error, + span: Some( + 0:17-18, + ), + reason: Expected { + who: None, + expected: "literal tuple", + found: "new line", + }, + hints: [], + code: None, + }, + ] + "#); + + assert_debug_snapshot!(parse_module_contents(r#" + enum foo 4 + "#).unwrap_err(), @r#" + [ + Error { + kind: Error, + span: Some( + 0:18-19, + ), + reason: Expected { + who: None, + expected: "literal tuple", + found: "4", + }, + hints: [], + code: None, + }, + ] + "#); + + assert_debug_snapshot!(parse_module_contents(r#" + enum foo { "First", "Second" } + "#).unwrap_err(), @r#" + [ + Error { + kind: Error, + span: Some( + 0:20-27, + ), + reason: Simple( + "must specify an alias for this value", + ), + hints: [], + code: None, + }, + Error { + kind: Error, + span: Some( + 0:29-37, + ), + reason: Simple( + "must specify an alias for this value", + ), + hints: [], + code: None, + }, + ] + "#); + + assert_debug_snapshot!(parse_module_contents(r#" + enum foo { First = 4 + 5 } + "#).unwrap_err(), @r#" + [ + Error { + kind: Error, + span: Some( + 0:20-33, + ), + reason: Simple( + "expected a literal value", + ), + hints: [], + code: None, + }, + ] + "#) + } + + #[test] + fn func_named_arg_type() { + assert_yaml_snapshot!(parse_module_contents(r#" + let f = func x :0 -> x + "#).unwrap(), @r#" + - VarDef: + kind: Let + name: f + value: + Func: + return_ty: ~ + body: + Ident: + - x + span: "0:35-36" + params: [] + named_params: + - name: x + ty: + kind: + Primitive: Int + span: "0:25-28" + name: ~ + default_value: + Literal: + Integer: 0 + span: "0:30-31" + span: "0:17-36" + span: "0:0-36" + "#) + } } diff --git a/prqlc/prqlc/src/codegen/ast.rs b/prqlc/prqlc/src/codegen/ast.rs index 8a2d72a4336f..6d0d44ff5a4c 100644 --- a/prqlc/prqlc/src/codegen/ast.rs +++ b/prqlc/prqlc/src/codegen/ast.rs @@ -438,6 +438,18 @@ impl WriteSource for pr::Stmt { } } }, + pr::StmtKind::TypeDef(pr::TypeDef { + name, + value: + pr::Ty { + kind: pr::TyKind::Enum(enum_tuple), + .. + }, + }) => { + r += opt.consume(&format!("enum {} ", name))?; + r += &enum_tuple.write(opt)?; + r += "\n"; + } pr::StmtKind::TypeDef(type_def) => { r += opt.consume(&format!("type {}", type_def.name))?; r += opt.consume(" = ")?; diff --git a/prqlc/prqlc/src/codegen/types.rs b/prqlc/prqlc/src/codegen/types.rs index 69957ddc6979..5d035a3d2a33 100644 --- a/prqlc/prqlc/src/codegen/types.rs +++ b/prqlc/prqlc/src/codegen/types.rs @@ -58,6 +58,7 @@ impl WriteSource for pr::TyKind { r += &func.return_ty.as_deref().write(opt)?; Some(r) } + Enum(tuple) => Some(format!("enum {}", tuple.write(opt)?)), } } } diff --git a/prqlc/prqlc/src/ir/pl/fold.rs b/prqlc/prqlc/src/ir/pl/fold.rs index 435947a34d6b..4420bf6a07f1 100644 --- a/prqlc/prqlc/src/ir/pl/fold.rs +++ b/prqlc/prqlc/src/ir/pl/fold.rs @@ -348,6 +348,7 @@ pub fn fold_type(fold: &mut T, ty: Ty) -> Result { .transpose()?, ), TyKind::Ident(_) | TyKind::Primitive(_) => ty.kind, + TyKind::Enum(_) => ty.kind, }, span: ty.span, name: ty.name, diff --git a/prqlc/prqlc/src/semantic/resolver/functions.rs b/prqlc/prqlc/src/semantic/resolver/functions.rs index f943689fb5d4..c10e668af40f 100644 --- a/prqlc/prqlc/src/semantic/resolver/functions.rs +++ b/prqlc/prqlc/src/semantic/resolver/functions.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use super::Resolver; use crate::ir::decl::{Decl, DeclKind, Module}; use crate::ir::pl::*; -use crate::pr::{Ty, TyFunc}; +use crate::pr::{Ty, TyFunc, TyKind}; use crate::semantic::resolver::types; use crate::semantic::{NS_PARAM, NS_THAT, NS_THIS}; use crate::Result; @@ -307,7 +307,7 @@ impl Resolver<'_> { let mut fields_new = Vec::with_capacity(fields.len()); self.in_flight_tuple_aliases.push(Vec::new()); for field in fields { - let field = self.fold_within_namespace(field, ¶m.name)?; + let field = self.fold_within_namespace(field, param)?; // add aliased columns into scope if let Some(alias) = field.alias.clone() { @@ -361,7 +361,7 @@ impl Resolver<'_> { param: &FuncParam, func_name: &Option, ) -> Result> { - let mut arg = self.fold_within_namespace(arg, ¶m.name)?; + let mut arg = self.fold_within_namespace(arg, param)?; // don't validate types of unresolved exprs if arg.id.is_some() { @@ -387,12 +387,41 @@ impl Resolver<'_> { Ok(Ok(arg)) } - fn fold_within_namespace(&mut self, expr: Expr, param_name: &str) -> Result { - let prev_namespace = self.default_namespace.take(); + fn fold_within_namespace(&mut self, expr: Expr, param: &FuncParam) -> Result { + let param_name = ¶m.name; if param_name.starts_with("noresolve.") { return Ok(expr); - } else if let Some((ns, _)) = param_name.split_once('.') { + } + + if let Some(Ty { + name: Some(name), + kind: TyKind::Enum(enum_expr), + .. + }) = ¶m.ty + { + if let crate::pr::Expr { + kind: crate::pr::ExprKind::Tuple(enum_tuple), + .. + } = &**enum_expr + { + // function parameters with a declared enum type will + // automatically have that namespace module searched first + self.current_module_path.push(name.to_string()); + let res = self.fold_expr(expr).push_hint(format!( + "perhaps you meant one of: {}", + enum_tuple + .iter() + .filter_map(|expr| expr.alias.clone()) + .join(", ") + )); + self.current_module_path.pop(); + return res; + } + } + + let prev_namespace = self.default_namespace.take(); + if let Some((ns, _)) = param_name.split_once('.') { self.default_namespace = Some(ns.to_string()); } else { self.default_namespace = None; diff --git a/prqlc/prqlc/src/semantic/resolver/stmt.rs b/prqlc/prqlc/src/semantic/resolver/stmt.rs index b4c5fc33b741..bd2853dd1bc6 100644 --- a/prqlc/prqlc/src/semantic/resolver/stmt.rs +++ b/prqlc/prqlc/src/semantic/resolver/stmt.rs @@ -3,7 +3,8 @@ use std::collections::HashMap; use crate::ir::decl::{Decl, DeclKind, Module, TableDecl, TableExpr}; use crate::ir::pl::*; use crate::pr::{Ty, TyKind, TyTupleField}; -use crate::semantic::STD_LIB_SOURCE_ID; +use crate::semantic::ast_expand::expand_expr; +use crate::semantic::{NS_SELF, STD_LIB_SOURCE_ID}; use crate::Result; use crate::WithErrorInfo; @@ -53,11 +54,72 @@ impl super::Resolver<'_> { let mut ty = self.fold_type(type_def.value)?; ty.name = Some(ident.name.clone()); - let decl = DeclKind::Ty(ty); - - self.root_mod - .declare(ident, decl, stmt.id, stmt.annotations) - .with_span(stmt.span)?; + if let TyKind::Enum(enum_expr) = ty.kind.clone() { + if let crate::pr::Expr { + kind: crate::pr::ExprKind::Tuple(enum_tuple), + .. + } = *enum_expr + { + // Enums get their own module definition, with NS_SELF pointing to the original type and a + // name entry in the module for each alias in the enum's tuple. + + self.current_module_path.push(ident.name); + + let mut module = Module { + names: HashMap::new(), + redirects: Vec::new(), + shadowed: None, + }; + + for expr in enum_tuple { + let name = expr + .alias + .clone() + .expect("enum literal tuples should always have all fields with aliases"); + let mut expr = expand_expr(expr)?; + expr.ty = Some(ty.clone()); + // here, the expr needs to have no alias, so that it doesn't clobber an alias + // used in a future tuple expression + expr.alias = None; + + module.names.insert( + name, + Decl { + declared_at: stmt.id, + kind: DeclKind::Expr(Box::new(expr)), + ..Default::default() + }, + ); + } + module.names.insert( + NS_SELF.to_string(), + Decl { + declared_at: stmt.id, + kind: DeclKind::Ty(ty), + annotations: stmt.annotations, + ..Default::default() + }, + ); + + let decl = Decl { + declared_at: stmt.id, + kind: DeclKind::Module(module), + ..Default::default() + }; + + let ident = Ident::from_path(self.current_module_path.clone()); + self.root_mod + .module + .insert(ident, decl) + .with_span(stmt.span)?; + self.current_module_path.pop(); + } + } else { + let decl = DeclKind::Ty(ty); + self.root_mod + .declare(ident, decl, stmt.id, stmt.annotations) + .with_span(stmt.span)?; + }; Ok(()) } diff --git a/prqlc/prqlc/src/semantic/resolver/transforms.rs b/prqlc/prqlc/src/semantic/resolver/transforms.rs index c0521aa58224..1026f3c0aca2 100644 --- a/prqlc/prqlc/src/semantic/resolver/transforms.rs +++ b/prqlc/prqlc/src/semantic/resolver/transforms.rs @@ -99,42 +99,26 @@ impl Resolver<'_> { let side = { let span = side.span; - let ident = - side.clone() - .try_cast(ExprKind::into_ident, Some("side"), "ident")?; + let ident = side.clone().try_cast( + ExprKind::into_literal, + Some("side"), + "inner, left, right or full", + )?; - // first try to match the raw ident string as a bare word + // these must match the values of JoinSide defined in std.prql match ident.to_string().as_str() { - "inner" => JoinSide::Inner, - "left" => JoinSide::Left, - "right" => JoinSide::Right, - "full" => JoinSide::Full, + "\"inner\"" => JoinSide::Inner, + "\"left\"" => JoinSide::Left, + "\"right\"" => JoinSide::Right, + "\"full\"" => JoinSide::Full, - _ => { - // if that fails, fold the ident and try treating the result as a literal - // this allows the join side to be passed as a function parameter - // NOTE: this is temporary, pending discussions and implementation, tracked in #4501 - let folded = self.fold_expr(side)?.try_cast( - ExprKind::into_literal, - Some("side"), - "string literal", - )?; - - match folded.to_string().as_str() { - "\"inner\"" => JoinSide::Inner, - "\"left\"" => JoinSide::Left, - "\"right\"" => JoinSide::Right, - "\"full\"" => JoinSide::Full, - - _ => { - return Err(Error::new(Reason::Expected { - who: Some("`side`".to_string()), - expected: "inner, left, right or full".to_string(), - found: folded.to_string(), - }) - .with_span(span)) - } - } + val => { + return Err(Error::new(Reason::Expected { + who: Some("`side`".to_string()), + expected: "inner, left, right or full".to_string(), + found: val.to_string(), + }) + .with_span(span)) } } }; @@ -256,13 +240,15 @@ impl Resolver<'_> { let by_name = { let span = by.span; - let ident = by - .clone() - .try_cast(ExprKind::into_ident, Some("by"), "ident")?; + let ident = by.clone().try_cast( + ExprKind::into_literal, + Some("by"), + "position or name", + )?; match ident.to_string().as_str() { - "position" => false, - "name" => true, + "\"position\"" => false, + "\"name\"" => true, _ => { return Err(Error::new(Reason::Expected { who: Some("`by`".to_string()), @@ -443,13 +429,15 @@ impl Resolver<'_> { let take_late = { let span = take.span; - let ident = - take.clone() - .try_cast(ExprKind::into_ident, Some("take"), "ident")?; + let ident = take.clone().try_cast( + ExprKind::into_literal, + Some("take"), + "early or late", + )?; match ident.to_string().as_str() { - "early" => false, - "late" => true, + "\"early\"" => false, + "\"late\"" => true, _ => { return Err(Error::new(Reason::Expected { who: Some("`take`".to_string()), @@ -538,12 +526,12 @@ impl Resolver<'_> { let res = { let span = format.span; let format = format - .try_cast(ExprKind::into_ident, Some("format"), "ident")? + .try_cast(ExprKind::into_literal, Some("format"), "csv or json")? .to_string(); match format.as_str() { - "csv" => from_text::parse_csv(&text) + "\"csv\"" => from_text::parse_csv(&text) .map_err(|r| Error::new_simple(r).with_span(span))?, - "json" => from_text::parse_json(&text) + "\"json\"" => from_text::parse_json(&text) .map_err(|r| Error::new_simple(r).with_span(span))?, _ => { diff --git a/prqlc/prqlc/src/semantic/std.prql b/prqlc/prqlc/src/semantic/std.prql index c006a79b7e53..238de7b743f7 100644 --- a/prqlc/prqlc/src/semantic/std.prql +++ b/prqlc/prqlc/src/semantic/std.prql @@ -96,10 +96,17 @@ let take = func tbl -> internal take +enum JoinSide { + inner = "inner", + left = "left", + right = "right", + full = "full", +} + let join = func `default_db.with` condition - `noresolve.side`:inner + side :inner tbl -> internal join @@ -118,8 +125,10 @@ let window = func tbl -> internal window +enum AppendBy { position = "position", name = "name" } + let append = func - `noresolve.by`:position + by :position `default_db.bottom` top -> internal append @@ -218,13 +227,16 @@ let in = pattern value -> internal in let tuple_reduce = func initial:"__missing" fn list -> internal tuple_reduce let tuple_map = func fn list -> internal tuple_map let tuple_zip = func a b -> internal tuple_zip -let tuple_uniq = func `noresolve.take`:early list -> internal tuple_uniq +enum TupleUniqTake { early = "early", late = "late" } +let tuple_uniq = func take :early list -> internal tuple_uniq let tuple_reverse = func list -> internal tuple_reverse let _eq = func a -> internal _eq let _is_null = func a -> _param.a == null ## Misc -let from_text = input `noresolve.format`:csv -> internal from_text +enum FromTextFormat { csv = "csv", json = "json" } + +let from_text = input format :csv -> internal from_text ## Text functions module text { diff --git a/prqlc/prqlc/tests/integration/error_messages.rs b/prqlc/prqlc/tests/integration/error_messages.rs index ab6f456e6bad..ada6fd25fa03 100644 --- a/prqlc/prqlc/tests/integration/error_messages.rs +++ b/prqlc/prqlc/tests/integration/error_messages.rs @@ -491,6 +491,46 @@ fn bare_lambda_expression() { "); } +#[test] +fn enum_type_1() { + assert_snapshot!(compile(r#" + enum Status { Paid = "paid", Unpaid = "unpaid" } + from invoices | filter status = Status.Canceled + "#).unwrap_err(), @" + Error: + ╭─[ :3:37 ] + │ + 3 │ from invoices | filter status = Status.Canceled + │ ───────┬─────── + │ ╰───────── Unknown name `Status.Canceled` + ───╯ + "); +} + +#[test] +fn enum_type_2() { + assert_snapshot!(compile(r###" + enum InvoiceStatus { Paid = 0, Unpaid = 1, Canceled = 2 } + + let filter_status = func status tbl -> ( + filter (this.status == _param.status) tbl + ) + + from invoices + filter_status 1 + "###).unwrap_err(), @" + Error: + ╭─[ :9:19 ] + │ + 9 │ filter_status 1 + │ ┬ + │ ╰── function filter_status, param `status` expected type `InvoiceStatus`, but found type `int` + │ + │ Help: Type `InvoiceStatus` expands to `enum {Paid = 0, Unpaid = 1, Canceled = 2}` + ───╯ + "); +} + #[test] fn append_by_wrong() { assert_snapshot!(compile(r###" @@ -502,7 +542,11 @@ fn append_by_wrong() { │ 3 │ append by:bar baz │ ─┬─ - │ ╰─── `by` expected position or name, but found bar + │ ╰─── Ambiguous name + │ + │ Help: could be any of: that.baz.bar, this.foo.bar + │ + │ Note: perhaps you meant one of: position, name ───╯ "); } @@ -518,7 +562,7 @@ fn tuple_uniq_take_wrong() { │ 3 │ select (tuple_uniq take:bar this) │ ─┬─ - │ ╰─── `take` expected early or late, but found bar + │ ╰─── take expected early or late, but found `this.foo.bar` ───╯ "); } diff --git a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_derive_select_join.snap b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_derive_select_join.snap index c96f4fc73e5e..214e33321600 100644 --- a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_derive_select_join.snap +++ b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_derive_select_join.snap @@ -116,7 +116,7 @@ nodes: - id: 5 kind: SString span: 1:278-330 - parent: 59 + parent: 60 - id: 18 kind: SString span: 1:0-46 @@ -242,15 +242,15 @@ nodes: children: - 50 - 53 - parent: 59 -- id: 55 + parent: 60 +- id: 56 kind: RqOperator span: 1:334-366 targets: - - 57 - 58 - parent: 59 -- id: 57 + - 59 + parent: 60 +- id: 58 kind: Ident span: 1:334-348 ident: !Ident @@ -259,7 +259,7 @@ nodes: - artist_id targets: - 51 -- id: 58 +- id: 59 kind: Ident span: 1:352-366 ident: !Ident @@ -268,13 +268,13 @@ nodes: - artist_id targets: - 5 -- id: 59 +- id: 60 kind: 'TransformCall: Join' span: 1:261-367 children: - 54 - 5 - - 55 + - 56 ast: name: Project stmts: diff --git a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_filter_derive_select_join.snap b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_filter_derive_select_join.snap index 3d772dd5ef31..3c610ca86781 100644 --- a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_filter_derive_select_join.snap +++ b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_filter_derive_select_join.snap @@ -135,7 +135,7 @@ nodes: - id: 5 kind: SString span: 1:315-367 - parent: 67 + parent: 68 - id: 21 kind: SString span: 1:0-46 @@ -286,15 +286,15 @@ nodes: children: - 58 - 61 - parent: 67 -- id: 63 + parent: 68 +- id: 64 kind: RqOperator span: 1:371-403 targets: - - 65 - 66 - parent: 67 -- id: 65 + - 67 + parent: 68 +- id: 66 kind: Ident span: 1:371-385 ident: !Ident @@ -303,7 +303,7 @@ nodes: - artist_id targets: - 59 -- id: 66 +- id: 67 kind: Ident span: 1:389-403 ident: !Ident @@ -312,13 +312,13 @@ nodes: - artist_id targets: - 5 -- id: 67 +- id: 68 kind: 'TransformCall: Join' span: 1:298-404 children: - 62 - 5 - - 63 + - 64 ast: name: Project stmts: diff --git a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__set_ops_remove.snap b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__set_ops_remove.snap index 5f8dabf89a31..398b2d1f6dda 100644 --- a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__set_ops_remove.snap +++ b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__set_ops_remove.snap @@ -10,167 +10,167 @@ frames: name: - t - a - target_id: 24 + target_id: 26 target_name: null inputs: - - id: 16 + - id: 18 name: t table: - default_db - - _literal_16 + - _literal_18 - - [std] - columns: - !Single name: - t - a - target_id: 24 + target_id: 26 target_name: null - !Single name: - b - a - target_id: 11 + target_id: 12 target_name: a inputs: - - id: 16 + - id: 18 name: t table: - default_db - - _literal_16 - - id: 11 + - _literal_18 + - id: 12 name: b table: - default_db - - _literal_11 + - _literal_12 - - [std] - columns: - !Single name: - t - a - target_id: 24 + target_id: 26 target_name: null - !Single name: - b - a - target_id: 11 + target_id: 12 target_name: a inputs: - - id: 16 + - id: 18 name: t table: - default_db - - _literal_16 - - id: 11 + - _literal_18 + - id: 12 name: b table: - default_db - - _literal_11 + - _literal_12 - - 1:165-238 - columns: - !Single name: - t - a - target_id: 65 + target_id: 67 target_name: null inputs: - - id: 16 + - id: 18 name: t table: - default_db - - _literal_16 - - id: 11 + - _literal_18 + - id: 12 name: b table: - default_db - - _literal_11 + - _literal_12 - - 1:239-245 - columns: - !Single name: - t - a - target_id: 65 + target_id: 67 target_name: null inputs: - - id: 16 + - id: 18 name: t table: - default_db - - _literal_16 - - id: 11 + - _literal_18 + - id: 12 name: b table: - default_db - - _literal_11 + - _literal_12 nodes: -- id: 11 +- id: 12 kind: Array span: 1:173-237 - parent: 57 -- id: 16 + parent: 59 +- id: 18 kind: Array span: 1:36-55 - parent: 42 -- id: 24 + parent: 44 +- id: 26 kind: Ident ident: !Ident - this - t - a targets: - - 16 - parent: 26 -- id: 26 + - 18 + parent: 28 +- id: 28 kind: Tuple span: 1:64-69 children: - - 24 -- id: 42 + - 26 +- id: 44 kind: 'TransformCall: Take' span: 1:71-77 children: - - 16 - - 43 - parent: 57 -- id: 43 + - 18 + - 45 + parent: 59 +- id: 45 kind: Literal - parent: 42 -- id: 48 + parent: 44 +- id: 50 kind: Ident ident: !Ident - this - t - a targets: - - 24 -- id: 50 + - 26 +- id: 52 kind: Ident ident: !Ident - that - b - a targets: - - 11 -- id: 55 + - 12 +- id: 57 kind: RqOperator span: [std] targets: - - 48 - 50 - parent: 57 -- id: 57 + - 52 + parent: 59 +- id: 59 kind: 'TransformCall: Join' span: [std] children: - - 42 - - 11 - - 55 - parent: 64 -- id: 60 + - 44 + - 12 + - 57 + parent: 66 +- id: 62 kind: Ident span: [std] ident: !Ident @@ -178,37 +178,37 @@ nodes: - b - a targets: - - 11 -- id: 64 + - 12 +- id: 66 kind: 'TransformCall: Filter' span: [std] children: - - 57 - - 4000000147 - parent: 67 -- id: 65 + - 59 + - 4000000155 + parent: 69 +- id: 67 kind: Ident ident: !Ident - this - t - a targets: - - 24 - parent: 66 -- id: 66 + - 26 + parent: 68 +- id: 68 kind: Tuple span: [std] children: - - 65 - parent: 67 -- id: 67 + - 67 + parent: 69 +- id: 69 kind: 'TransformCall: Select' span: 1:165-238 children: - - 64 - 66 - parent: 70 -- id: 68 + - 68 + parent: 72 +- id: 70 kind: Ident span: 1:244-245 ident: !Ident @@ -216,22 +216,22 @@ nodes: - t - a targets: - - 65 - parent: 70 -- id: 70 + - 67 + parent: 72 +- id: 72 kind: 'TransformCall: Sort' span: 1:239-245 children: - - 67 - - 68 -- id: 4000000147 + - 69 + - 70 +- id: 4000000155 kind: RqOperator span: [std] targets: - - 60 - - 4000000149 - parent: 64 -- id: 4000000149 + - 62 + - 4000000157 + parent: 66 +- id: 4000000157 kind: Literal span: [std] ast: diff --git a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort.snap b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort.snap index 466a28d0a6f9..f13c48d4fdec 100644 --- a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort.snap +++ b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort.snap @@ -49,19 +49,19 @@ frames: - columns: - !Single name: null - target_id: 32 + target_id: 33 target_name: null - !Single name: - e - last_name - target_id: 33 + target_id: 34 target_name: null - !Single name: - manager - first_name - target_id: 34 + target_id: 35 target_name: null inputs: - id: 17 @@ -81,7 +81,7 @@ nodes: ident: !Ident - default_db - employees - parent: 31 + parent: 32 - id: 17 kind: Ident span: 1:13-29 @@ -142,15 +142,15 @@ nodes: - 22 - 23 - 24 - parent: 31 -- id: 27 + parent: 32 +- id: 28 kind: RqOperator span: 1:179-214 targets: - - 29 - 30 - parent: 31 -- id: 29 + - 31 + parent: 32 +- id: 30 kind: Ident span: 1:179-191 ident: !Ident @@ -159,7 +159,7 @@ nodes: - reports_to targets: - 17 -- id: 30 +- id: 31 kind: Ident span: 1:195-214 ident: !Ident @@ -168,15 +168,15 @@ nodes: - employee_id targets: - 8 -- id: 31 +- id: 32 kind: 'TransformCall: Join' span: 1:145-215 children: - 26 - 8 - - 27 - parent: 36 -- id: 32 + - 28 + parent: 37 +- id: 33 kind: Ident span: 1:225-237 ident: !Ident @@ -185,8 +185,8 @@ nodes: - first_name targets: - 17 - parent: 35 -- id: 33 + parent: 36 +- id: 34 kind: Ident span: 1:239-250 ident: !Ident @@ -195,8 +195,8 @@ nodes: - last_name targets: - 17 - parent: 35 -- id: 34 + parent: 36 +- id: 35 kind: Ident span: 1:252-270 ident: !Ident @@ -205,21 +205,21 @@ nodes: - first_name targets: - 8 - parent: 35 -- id: 35 + parent: 36 +- id: 36 kind: Tuple span: 1:224-271 children: - - 32 - 33 - 34 - parent: 36 -- id: 36 + - 35 + parent: 37 +- id: 37 kind: 'TransformCall: Select' span: 1:217-271 children: - - 31 - - 35 + - 32 + - 36 ast: name: Project stmts: diff --git a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort_3.snap b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort_3.snap index aac6068808df..a07b913ea50b 100644 --- a/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort_3.snap +++ b/prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort_3.snap @@ -101,18 +101,18 @@ frames: - !Single name: - AA - target_id: 51 + target_id: 52 target_name: null - !Single name: - AT - target_id: 52 + target_id: 53 target_name: null - !Single name: - _literal_33 - genre_id - target_id: 56 + target_id: 57 target_name: null inputs: - id: 33 @@ -130,18 +130,18 @@ frames: - !Single name: - AA - target_id: 51 + target_id: 52 target_name: null - !Single name: - AT - target_id: 52 + target_id: 53 target_name: null - !Single name: - _literal_33 - genre_id - target_id: 56 + target_id: 57 target_name: null inputs: - id: 33 @@ -159,18 +159,18 @@ frames: - !Single name: - AA - target_id: 51 + target_id: 52 target_name: null - !Single name: - AT - target_id: 52 + target_id: 53 target_name: null - !Single name: - _literal_33 - genre_id - target_id: 56 + target_id: 57 target_name: null - !Single name: @@ -205,17 +205,17 @@ frames: - !Single name: - AA - target_id: 69 + target_id: 71 target_name: null - !Single name: - AT - target_id: 70 + target_id: 72 target_name: null - !Single name: - GT - target_id: 71 + target_id: 73 target_name: null inputs: - id: 33 @@ -239,7 +239,7 @@ nodes: span: 1:244-278 children: - 9 - parent: 68 + parent: 70 - id: 9 kind: Tuple span: 1:245-277 @@ -262,7 +262,7 @@ nodes: span: 1:110-145 children: - 22 - parent: 50 + parent: 51 - id: 22 kind: Tuple span: 1:111-144 @@ -370,15 +370,15 @@ nodes: children: - 42 - 43 - parent: 50 -- id: 46 + parent: 51 +- id: 47 kind: RqOperator span: 1:147-157 targets: - - 48 - 49 - parent: 50 -- id: 48 + - 50 + parent: 51 +- id: 49 kind: Ident span: 1:149-157 ident: !Ident @@ -387,7 +387,7 @@ nodes: - album_id targets: - 39 -- id: 49 +- id: 50 kind: Ident span: 1:149-157 ident: !Ident @@ -396,15 +396,15 @@ nodes: - album_id targets: - 21 -- id: 50 +- id: 51 kind: 'TransformCall: Join' span: 1:95-158 children: - 45 - 21 - - 46 - parent: 58 -- id: 51 + - 47 + parent: 59 +- id: 52 kind: Ident span: 1:168-170 ident: !Ident @@ -412,16 +412,16 @@ nodes: - AA targets: - 38 - parent: 57 -- id: 52 + parent: 58 +- id: 53 kind: RqOperator span: 1:177-201 alias: AT targets: - - 54 - 55 - parent: 57 -- id: 54 + - 56 + parent: 58 +- id: 55 kind: Ident span: 1:177-188 ident: !Ident @@ -430,10 +430,10 @@ nodes: - album_title targets: - 21 -- id: 55 +- id: 56 kind: Literal span: 1:192-201 -- id: 56 +- id: 57 kind: Ident span: 1:203-211 ident: !Ident @@ -442,55 +442,55 @@ nodes: - genre_id targets: - 40 - parent: 57 -- id: 57 + parent: 58 +- id: 58 kind: Tuple span: 1:166-213 children: - - 51 - 52 - - 56 - parent: 58 -- id: 58 + - 53 + - 57 + parent: 59 +- id: 59 kind: 'TransformCall: Select' span: 1:159-213 children: - - 50 - - 57 - parent: 63 -- id: 59 + - 51 + - 58 + parent: 64 +- id: 60 kind: RqOperator span: 1:221-228 targets: - - 61 - 62 - parent: 63 -- id: 61 + - 63 + parent: 64 +- id: 62 kind: Ident span: 1:221-223 ident: !Ident - this - AA targets: - - 51 -- id: 62 + - 52 +- id: 63 kind: Literal span: 1:226-228 -- id: 63 +- id: 64 kind: 'TransformCall: Filter' span: 1:214-228 children: - - 58 - 59 - parent: 68 -- id: 64 + - 60 + parent: 70 +- id: 66 kind: RqOperator span: 1:280-290 targets: - - 66 - - 67 - parent: 68 -- id: 66 + - 68 + - 69 + parent: 70 +- id: 68 kind: Ident span: 1:282-290 ident: !Ident @@ -498,8 +498,8 @@ nodes: - _literal_33 - genre_id targets: - - 56 -- id: 67 + - 57 +- id: 69 kind: Ident span: 1:282-290 ident: !Ident @@ -508,41 +508,41 @@ nodes: - genre_id targets: - 8 -- id: 68 +- id: 70 kind: 'TransformCall: Join' span: 1:229-291 children: - - 63 - - 8 - 64 - parent: 76 -- id: 69 + - 8 + - 66 + parent: 78 +- id: 71 kind: Ident span: 1:301-303 ident: !Ident - this - AA targets: - - 51 - parent: 75 -- id: 70 + - 52 + parent: 77 +- id: 72 kind: Ident span: 1:305-307 ident: !Ident - this - AT targets: - - 52 - parent: 75 -- id: 71 + - 53 + parent: 77 +- id: 73 kind: RqOperator span: 1:314-338 alias: GT targets: - - 73 - - 74 - parent: 75 -- id: 73 + - 75 + - 76 + parent: 77 +- id: 75 kind: Ident span: 1:314-325 ident: !Ident @@ -551,23 +551,23 @@ nodes: - genre_title targets: - 8 -- id: 74 +- id: 76 kind: Literal span: 1:329-338 -- id: 75 +- id: 77 kind: Tuple span: 1:299-340 children: - - 69 - - 70 - 71 - parent: 76 -- id: 76 + - 72 + - 73 + parent: 78 +- id: 78 kind: 'TransformCall: Select' span: 1:292-340 children: - - 68 - - 75 + - 70 + - 77 ast: name: Project stmts: diff --git a/prqlc/prqlc/tests/integration/sql.rs b/prqlc/prqlc/tests/integration/sql.rs index 3c6de08e5e45..d9265c1bfc52 100644 --- a/prqlc/prqlc/tests/integration/sql.rs +++ b/prqlc/prqlc/tests/integration/sql.rs @@ -2966,7 +2966,7 @@ fn test_join() { #[test] fn test_join_side_literal() { assert_snapshot!((compile(r###" - let my_side = "right" + let my_side = JoinSide.right from x join y (==id) side:my_side @@ -2987,26 +2987,28 @@ fn test_join_side_literal_err() { from x join y (==id) side:my_side - "###).unwrap_err()), @" + "###).unwrap_err()), @r#" Error: - ╭─[ :5:24 ] + ╭─[ :2:19 ] + │ + 2 │ let my_side = 42 + │ ─┬ + │ ╰── function std.join, param `side` expected type `JoinSide`, but found type `int` │ - 5 │ join y (==id) side:my_side - │ ───┬─── - │ ╰───── `side` expected inner, left, right or full, but found 42 + │ Help: Type `JoinSide` expands to `enum {inner = "inner", left = "left", right = "right", full = "full"}` ───╯ - "); + "#); } #[test] fn test_join_side_literal_via_func() { assert_snapshot!((compile(r###" - let my_join = func m c s :"right" tbl -> ( + let my_join = func m c s :right tbl -> ( join side:_param.s m (c == that.k) tbl ) from x - my_join default_db.y this.id s:"left" + my_join default_db.y this.id s:left "###).unwrap()), @" SELECT x.*, @@ -3020,7 +3022,7 @@ fn test_join_side_literal_via_func() { #[test] fn test_join_side_literal_via_func_err() { assert_snapshot!((compile(r###" - let my_join = func m c s :"right" tbl -> ( + let my_join = func m c s :right tbl -> ( join side:_param.s m (c == that.k) tbl ) @@ -3028,11 +3030,13 @@ fn test_join_side_literal_via_func_err() { my_join default_db.y this.id s:"four" "###).unwrap_err()), @r#" Error: - ╭─[ :3:19 ] + ╭─[ :7:36 ] + │ + 7 │ my_join default_db.y this.id s:"four" + │ ───┬── + │ ╰──── function my_join, param `s` expected type `JoinSide`, but found type `text` │ - 3 │ join side:_param.s m (c == that.k) tbl - │ ────┬─── - │ ╰───── `side` expected inner, left, right or full, but found "four" + │ Help: Type `JoinSide` expands to `enum {inner = "inner", left = "left", right = "right", full = "full"}` ───╯ "#); } @@ -7552,6 +7556,161 @@ fn test_tuple_map_aliases() { "###); } +#[test] +fn test_enum_1() { + assert_snapshot!(compile(r###" + enum InvoiceStatus { Paid = 0, Unpaid = 1, Canceled = 2 } + + from invoices + filter status == InvoiceStatus.Paid + "###).unwrap(), @" + SELECT + * + FROM + invoices + WHERE + status = 0 + "); +} + +#[test] +fn test_enum_1b() { + assert_snapshot!(compile(r###" + enum InvoiceStatus { Paid = 0, Unpaid = 1, Canceled = 2 } + + from invoices + select { status = InvoiceStatus.Paid } + "###).unwrap(), @" + SELECT + 0 AS status + FROM + invoices + "); +} + +#[test] +fn test_enum_2() { + assert_snapshot!(compile(r###" + enum InvoiceStatus { Paid = "paid", Unpaid = "unpaid", Canceled = "canceled" } + + let filter_status = func status tbl -> ( + filter (this.status == _param.status) tbl + ) + + from invoices + filter_status InvoiceStatus.Unpaid + "###).unwrap(), @" + SELECT + * + FROM + invoices + WHERE + status = 'unpaid' + "); +} + +#[test] +fn test_enum_3() { + assert_snapshot!(compile(r###" + enum InvoiceStatus { Paid = "paid", Unpaid = "unpaid", Canceled = "canceled" } + + let filter_status = func status tbl -> ( + filter (this.status == _param.status) tbl + ) + + from invoices + filter_status Canceled + "###).unwrap(), @" + SELECT + * + FROM + invoices + WHERE + status = 'canceled' + "); +} + +#[test] +fn test_enum_4() { + assert_snapshot!(compile(r###" + enum InvoiceStatus { Paid = "paid", Unpaid = "unpaid", Canceled = "canceled" } + + let filter_status = func status tbl -> ( + filter (this.status == _param.status) tbl + ) + + let expected_status = InvoiceStatus.Paid + + from invoices + filter_status expected_status + "###).unwrap(), @" + SELECT + * + FROM + invoices + WHERE + status = 'paid' + "); +} + +#[test] +fn test_enum_5() { + assert_snapshot!(compile(r###" + enum InvoiceStatus { Paid = "paid", Unpaid = "unpaid", Canceled = "canceled" } + + let filter_status = func status tbl -> ( + filter (this.status == _param.status) tbl + ) + + from invoices + select { id, status } + join ( + from invoice_history + select { id, hist_stat } + ) (==id) + filter_status hist_stat + "###).unwrap(), @" + WITH table_0 AS ( + SELECT + id, + hist_stat + FROM + invoice_history + ) + SELECT + invoices.id, + invoices.status, + table_0.id, + table_0.hist_stat + FROM + invoices + INNER JOIN table_0 ON invoices.id = table_0.id + WHERE + invoices.status = table_0.hist_stat + "); +} + +#[test] +fn test_enum_6() { + assert_snapshot!(compile(r###" + enum InvoiceStatus { Paid = "paid", Unpaid = "unpaid", Canceled = "canceled" } + + let filter_status = func status :Paid tbl -> ( + filter (this.status == _param.status) tbl + ) + + from invoices + filter_status + "###).unwrap(), @" + SELECT + * + FROM + invoices + WHERE + status = 'paid' + "); +} + #[test] fn test_tuple_reverse() { assert_snapshot!(compile(r###" diff --git a/web/book/src/reference/spec/type-system.md b/web/book/src/reference/spec/type-system.md index ae3fd439a08c..fbab2a28c257 100644 --- a/web/book/src/reference/spec/type-system.md +++ b/web/book/src/reference/spec/type-system.md @@ -1,8 +1,9 @@ # Type system -> Status: under development - -> The type system determines the allowed values of a term. + +> [!WARNING] +> The PRQL type system is currently in flux. Not all intended features are +> currently implemented. ## Purpose @@ -164,12 +165,24 @@ at compile-time. All fields are of the same type and cannot be named. type array_of_int = [int] ``` +### Simple Enumerations + +A simple enumeration is a type with an associated "literal tuple" (a tuple with +all fields having names and literal values). + +``` +enum Status { Paid = 0, Unpaid = 1, Cancelled = 2 } +``` + ### Functions ``` type floor_signature = func float -> int ``` + ## Type annotations Variable annotations and function parameters may specify type annotations: +The value of `x` (and thus `a`) must be an element of `t`. + ``` let a = x ``` -The value of `x` (and thus `a`) must be an element of `t`. +The value of argument supplied to `x` and `y` (named argument with a default) +must be an element of `t`. ``` -let my_func = func x -> y +let my_func = func x y :default -> z ``` -The value of argument supplied to `x` must be an element of `t`. +The value of function body `y` must be an element of `t`. ``` let my_func = func x -> y ``` -The value of function body `y` must be an element of `t`. +### Simple enumeration function parameters + +When a simple enumeration is used as a type on a function parameter, the value +of the parameter will be required to be a member of the enumeration. + +```prql +enum Status { + Paid = 0, + Unpaid = 1, + Cancelled = 2 +} + +let filter_status = func + status + tbl -> ( + filter (this.status == _param.status) tbl +) + +from invoices +filter_status Status.Paid +``` + +You can also specify just the name of the enumeration member. + +```prql +enum Status { + Paid = 0, + Unpaid = 1, + Cancelled = 2 +} + +let filter_status = func + status + tbl -> ( + filter (this.status == _param.status) tbl +) + +from invoices +filter_status Unpaid +``` ## Physical layout @@ -235,7 +291,12 @@ DBMSs, since the physical layout of the result will vary. In the future, PRQL may define a common physical layout of types, probably using Apache Arrow. -## Examples +## Design Examples + + +> [!WARNING] +> The examples below are not currently supported, and are retained for design +> intent. Future implementations may or may not conform to this design. ``` type my_relation = [{ diff --git a/web/book/tests/documentation/snapshots/documentation__book__reference__spec__type-system__simple-enumeration-function-parameters__0.snap b/web/book/tests/documentation/snapshots/documentation__book__reference__spec__type-system__simple-enumeration-function-parameters__0.snap new file mode 100644 index 000000000000..35676873f560 --- /dev/null +++ b/web/book/tests/documentation/snapshots/documentation__book__reference__spec__type-system__simple-enumeration-function-parameters__0.snap @@ -0,0 +1,10 @@ +--- +source: web/book/tests/documentation/book.rs +expression: "enum Status {\n Paid = 0,\n Unpaid = 1,\n Cancelled = 2\n}\n\nlet filter_status = func\n status \n tbl -> (\n filter (this.status == _param.status) tbl\n)\n\nfrom invoices\nfilter_status Status.Paid\n" +--- +SELECT + * +FROM + invoices +WHERE + status = 0 diff --git a/web/book/tests/documentation/snapshots/documentation__book__reference__spec__type-system__simple-enumeration-function-parameters__1.snap b/web/book/tests/documentation/snapshots/documentation__book__reference__spec__type-system__simple-enumeration-function-parameters__1.snap new file mode 100644 index 000000000000..30b6ee933e5f --- /dev/null +++ b/web/book/tests/documentation/snapshots/documentation__book__reference__spec__type-system__simple-enumeration-function-parameters__1.snap @@ -0,0 +1,10 @@ +--- +source: web/book/tests/documentation/book.rs +expression: "enum Status {\n Paid = 0,\n Unpaid = 1,\n Cancelled = 2\n}\n\nlet filter_status = func\n status \n tbl -> (\n filter (this.status == _param.status) tbl\n)\n\nfrom invoices\nfilter_status Unpaid\n" +--- +SELECT + * +FROM + invoices +WHERE + status = 1