Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions prqlc/prqlc/src/semantic/resolver/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,29 @@ impl Resolver<'_> {

inner_closure.env = func_env.into_exprs();

let (got, missing) = inner_closure.params.split_at(inner_closure.args.len());
let missing = missing.to_vec();
inner_closure.params = got.to_vec();
// Get the missing params (params that don't have args yet)
let missing = inner_closure.params[inner_closure.args.len()..].to_vec();

// Create wrapper params and add references to them as args to the inner closure
let mut wrapper_params = Vec::with_capacity(missing.len());
for (i, param) in missing.iter().enumerate() {
let param_name = format!("_partial_{i}");
let substitute_arg = Expr::new(Ident::from_path(vec![
NS_PARAM.to_string(),
param_name.clone(),
]));
inner_closure.args.push(substitute_arg);
wrapper_params.push(FuncParam {
name: param_name,
ty: param.ty.clone(),
default_value: None,
});
}

Expr::new(ExprKind::Func(Box::new(Func {
name_hint: None,
args: vec![],
params: missing,
params: wrapper_params,
body: Box::new(Expr::new(ExprKind::Func(inner_closure))),

// these don't matter
Expand Down
36 changes: 36 additions & 0 deletions prqlc/prqlc/tests/integration/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6969,3 +6969,39 @@ fn test_aggregate_with_operations_and_filter() {
COALESCE(SUM(customer_id), 0) * 2 > 0
");
}

/// Regression test for issue #5661: partial application of transforms in
/// user-defined functions.
///
/// When a user defines a function that wraps a transform with fewer parameters
/// than the transform requires (e.g., `let foo = a -> take a`), the missing
/// parameters should be propagated to the wrapper function, allowing it to work
/// correctly in pipelines.
#[test]
fn test_partial_application_of_transform() {
// Basic case: wrapping `take` with a single parameter
assert_snapshot!(compile(r#"
let foo = a -> take a
from invoices | foo 10
"#).unwrap(), @r"
SELECT
*
FROM
invoices
LIMIT
10
");

// Same behavior as explicit two-parameter version
assert_snapshot!(compile(r#"
let foo = a r -> take a r
from invoices | foo 10
"#).unwrap(), @r"
SELECT
*
FROM
invoices
LIMIT
10
");
}
41 changes: 41 additions & 0 deletions web/book/src/reference/declarations/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,44 @@ derive {
overhead_share = (cost_share overhead),
}
```

## Partial application

Functions can be partially applied, which is useful for creating reusable
transform wrappers. When a function returns a partially-applied transform, the
missing parameters are automatically propagated.

For example, we can create a `top_n` function that wraps `take`:

```prql
let top_n = n -> take n
from invoices
top_n 10
```

This works because `take` requires two arguments (the count and the relation),
but `top_n` only provides one. The relation parameter is automatically filled in
from the pipeline.

We can also compose multiple partial applications:

```prql
let top_n = n -> take n
let add_constant = x -> derive {constant = x}
let my_pipeline = (top_n 5 | add_constant 42)
from invoices
my_pipeline
```

Or store a fully-configured transform for reuse:

```prql
let top_n = n -> take n
let top_5 = top_n 5
from invoices
top_5
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: web/book/tests/documentation/book.rs
expression: "let top_n = n -> take n\n\nfrom invoices\ntop_n 10\n"
---
SELECT
*
FROM
invoices
LIMIT
10
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: web/book/tests/documentation/book.rs
expression: "let top_n = n -> take n\nlet add_constant = x -> derive {constant = x}\n\nlet my_pipeline = (top_n 5 | add_constant 42)\n\nfrom invoices\nmy_pipeline\n"
---
SELECT
*,
42 AS constant
FROM
invoices
LIMIT
5
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: web/book/tests/documentation/book.rs
expression: "let top_n = n -> take n\nlet top_5 = top_n 5\n\nfrom invoices\ntop_5\n"
---
SELECT
*
FROM
invoices
LIMIT
5