diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d78b9b3c1de..20964900e158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ generates Markdown documentation from `.prql` files. (@vanillajonathan, #4152). - Add `prqlc lex` command to the CLI (@max-sixty) +- Join `side` parameter can take a reference that resolves to a literal (note: + this is an experimental feature which may change in the future) (@kgutwin, + #4499) **Fixes**: diff --git a/prqlc/prqlc/src/semantic/resolver/transforms.rs b/prqlc/prqlc/src/semantic/resolver/transforms.rs index f59dd01ee81d..69aa8ad64b6d 100644 --- a/prqlc/prqlc/src/semantic/resolver/transforms.rs +++ b/prqlc/prqlc/src/semantic/resolver/transforms.rs @@ -101,20 +101,42 @@ impl Resolver<'_> { let side = { let span = side.span; - let ident = side.try_cast(ExprKind::into_ident, Some("side"), "ident")?; + let ident = + side.clone() + .try_cast(ExprKind::into_ident, Some("side"), "ident")?; + + // first try to match the raw ident string as a bare word match ident.to_string().as_str() { "inner" => JoinSide::Inner, "left" => JoinSide::Left, "right" => JoinSide::Right, "full" => JoinSide::Full, - found => { - return Err(Error::new(Reason::Expected { - who: Some("`side`".to_string()), - expected: "inner, left, right or full".to_string(), - found: found.to_string(), - }) - .with_span(span)) + _ => { + // 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)) + } + } } } }; diff --git a/prqlc/prqlc/tests/integration/sql.rs b/prqlc/prqlc/tests/integration/sql.rs index 2e25d8a028e1..f89a9ce6e179 100644 --- a/prqlc/prqlc/tests/integration/sql.rs +++ b/prqlc/prqlc/tests/integration/sql.rs @@ -2385,6 +2385,80 @@ fn test_join() { compile("from x | join y {==x.id}").unwrap_err(); } +#[test] +fn test_join_side_literal() { + assert_snapshot!((compile(r###" + let my_side = "right" + + from x + join y (==id) side:my_side + "###).unwrap()), @r###" + SELECT + x.*, + y.* + FROM + x + RIGHT JOIN y ON x.id = y.id + "###); +} + +#[test] +fn test_join_side_literal_err() { + assert_snapshot!((compile(r###" + let my_side = 42 + + from x + join y (==id) side:my_side + "###).unwrap_err()), @r###" + Error: + ╭─[:5:24] + │ + 5 │ join y (==id) side:my_side + │ ───┬─── + │ ╰───── `side` expected inner, left, right or full, but found 42 + ───╯ + "###); +} + +#[test] +fn test_join_side_literal_via_func() { + assert_snapshot!((compile(r###" + 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" + "###).unwrap()), @r###" + SELECT + x.*, + y.* + FROM + x + LEFT JOIN y ON x.id = y.k + "###); +} + +#[test] +fn test_join_side_literal_via_func_err() { + assert_snapshot!((compile(r###" + 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:"four" + "###).unwrap_err()), @r###" + Error: + ╭─[:3:25] + │ + 3 │ join side:_param.s m (c == that.k) tbl + │ ─┬ + │ ╰── `side` expected inner, left, right or full, but found "four" + ───╯ + "###); +} + #[test] fn test_from_json() { // Test that the SQL generated from the JSON of the PRQL is the same as the raw PRQL