Dynamic Function Calling

PL/Rust provides the ability to dynamically call any function (callable to the current user) directly from a Rust function. These functions can be in any language, including sql, plpgsql, plrust, plperl, etc.

The call interface is dynamic in that the callee is resolved at runtime and its argument and return types are also checked at runtime. While this does introduce a small bit of overhead, it's significantly less than doing what might be the equivalent operation via Spi.

The ability to dynamically call functions enables users to write functions in the language that makes the most sense for the operation being performed. In many cases, a LANGUAGE plpgsql function is exactly what's needed, and a LANGUAGE plrust function can now use its result to execute further, possibly CPU-intensive, work.

Important Rust Types

This dynamic calling interface introduces two new types that are used to facilitate dynamically calling functions: Arg and FnCallError.

Arg

Arg describes the style of a user-provided function argument.

#![allow(unused)]
fn main() {
/// The kinds of [`fn_call`] arguments.  
pub enum Arg<T> {
    /// The argument value is a SQL NULL
    Null,

    /// The argument's `DEFAULT` value should be used
    Default,

    /// Use this actual value
    Value(T),
}
}

Rust doesn't exactly have the concept of "NULL" nor does it have direct support for overloaded functions. This is where the Null and Default variants come in.

There's a sealed trait that corresponds to this enum named FnCallArg. It is not a trait that users needs to implement, but is used by PL/Rust to dynamically represent a set of heterogeneous argument types.

FnCallError

There's also a set of runtime error conditions if function resolution fails. These are recoverable errors in that user code could match on the return value and potentially make different decisions, or just raise a panic with the error to immediately abort the current transaction.

#![allow(unused)]
fn main() {
/// [`FnCallError`]s represent the set of conditions that could case [`fn_call()`] to fail in a
/// user-recoverable manner.
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
pub enum FnCallError {
    #[error("Invalid identifier: `{0}`")]
    InvalidIdentifier(String),

    #[error("The specified function does not exist")]
    UndefinedFunction,

    #[error("The specified function exists, but has overloaded versions which are ambiguous given the argument types provided")]
    AmbiguousFunction,

    #[error("Can only dynamically call plain functions")]
    UnsupportedFunctionType,

    #[error("Functions with OUT/IN_OUT/TABLE arguments are not supported")]
    UnsupportedArgumentModes,

    #[error("Functions with argument or return types of `internal` are not supported")]
    InternalTypeNotSupported,

    #[error("The requested return type `{0}` is not compatible with the actual return type `{1}`")]
    IncompatibleReturnType(pg_sys::Oid, pg_sys::Oid),

    #[error("Function call has more arguments than are supported")]
    TooManyArguments,

    #[error("Did not provide enough non-default arguments")]
    NotEnoughArguments,

    #[error("Function has no default arguments")]
    NoDefaultArguments,

    #[error("Argument #{0} does not have a DEFAULT value")]
    NotDefaultArgument(usize),

    #[error("Argument's default value is not a constant expression")]
    DefaultNotConstantExpression,
}
}

Calling a Function

The top-level function fn_call() is what is used to dynamically call a function. Its signature is:

#![allow(unused)]
fn main() {
pub fn fn_call<R: FromDatum + IntoDatum>(
    fname: &str,
    args: &[&dyn FnCallArg],
) -> Result<Option<R>, FnCallError>
}

fn_call itself takes two arguments. The first, fname is the (possibly schema-qualified) function name, as a string.

The second argument, args, is a slice of FnCallArg dyn references (these are written using &Arg::XXX). And it returns a Result<Option<R>, FnCallError>.

An Ok response will either contain Some(R) if the called function returned a non-null value, or None if it did.

An Err response will contain one of the FnCallError variants detailed above, indicating the problem encountered while trying to call the function. It is guaranteed that if fn_call returns an Err, then the desired function was not called.

If the called function raises a Postgres ERROR then the current transaction is aborted and control is returned back to Postgres, not the caller. This is typical Postgres and PL/Rust behavior in the face of an ERROR or Rust panic.

Simple Example

First, define a SQL function that sums the elements of an int[]. We're using a LANGUAGE sql function here to demonstrate how PL/Rust can call functions of any other language:

CREATE OR REPLACE FUNCTION sum_array(a int[]) RETURNS int STRICT LANGUAGE sql AS $$ SELECT sum(e) FROM unnest(a) e $$;

Now, call this function from a PL/Rust function:

CREATE OR REPLACE FUNCTION transform_array(a int[]) RETURNS int STRICT LANGUAGE plrust AS $$
    let a = a.into_iter().map(|e| e.unwrap_or(0) + 1).collect::<Vec<_>>();  // add one to every element of the array
    Ok(fn_call("sum_array", &[&Arg::Value(a)])?)
$$;

SELECT transform_array(ARRAY[1,2,3]);
transform_array 
-----------------
               9
(1 row)

Complex Example

This is contrived, of course, but let's make a PL/Rust function with a few different argument types and have it simply convert their values to a debug-formatted String. Then we'll call that function from another PL/Rust function.

CREATE OR REPLACE FUNCTION debug_format_args(a text, b bigint, c float4 DEFAULT 0.99) RETURNS text LANGUAGE plrust AS $$
    Ok(Some(format!("{:?}, {:?}, {:?}", a, b, c)))  
$$;

SELECT debug_format_args('hi', NULL);
      debug_format_args       
------------------------------
 Some("hi"), None, Some(0.99)
(1 row)

Now, call it from another PL/Rust function using these same argument values. Which is 'hi' for the first argument, NULL for the second, and using the default value for the third:

CREATE OR REPLACE FUNCTION complex_example() RETURNS text LANGUAGE plrust AS $$
    let result = fn_call("debug_format_args", &[&Arg::Value("hi"), &Arg::<i64>::Null, &Arg::<f32>::Default])?;
    Ok(result)    
$$;

SELECT complex_example();
complex_example        
------------------------------
 Some("hi"), None, Some(0.99)
(1 row)

You'll notice here that the Arg::Null and Arg::Default argument values are typed with ::<i64> and ::<f32> respectively. It is necessary for PL/Rust to know the types of each argument at compile time, so that during runtime the proper function can be chosen. This helps to ensure there's no ambiguity related to Postgres' function overloading features. For example, let's overload debug_format_args with a different type for the second argument:

CREATE OR REPLACE FUNCTION debug_format_args(a text, b bool, c float4 DEFAULT 0.99) RETURNS text LANGUAGE plrust AS $$
    Ok(Some(format!("{:?}, {:?}, {:?}", a, b, c)))  
$$;

SELECT debug_format_args('hi', NULL);
ERROR:  42725: function debug_format_args(unknown, unknown) is not unique
LINE 1: SELECT debug_format_args('hi', NULL);
               ^
HINT:  Could not choose a best candidate function. You might need to add explicit type casts.

As you can see, even Postgres can't figure out which debug_format_args function to call as it doesn't know the intended type of the second NULL argument. We can tell it, of course:

SELECT debug_format_args('hi', NULL::bool);
      debug_format_args       
------------------------------
 Some("hi"), None, Some(0.99)
(1 row)

Note that if we call our complex_example function again, now that we've added another version of debug_format_args, it still calls the correct one -- the version with an int as the second argument.

Limitations

PL/Rust does not support dynamically calling functions with OUT or IN OUT arguments. Nor does it support calling functions that return SETOF $type or TABLE(...).

It is possible these limitations will be lifted in a future version.