UDF: MariaDB/MySQL User Defined Functions in Rust

This crate aims to make it extremely simple to implement UDFs for SQL, in a way that is as safe as possible.

UDF Theory

Basic SQL UDFs consist of three exposed functions:

This wrapper greatly simplifies the process so that you only need to worry about checking arguments and performing the task.

Quickstart

A quick overview of the workflow process is:

Detailed overview

This section goes into the details of implementing a UDF with this library, but it is non-exhaustive. For that, see the documentation, or the udf/examples directory for well-annotated examples.

Struct creation

The first step is to create a struct (or enum) that will be used to share data between all relevant SQL functions. These include:

It is quite possible, especially for simple functions, that there is no data that needs sharing. In this case, just make an empty struct and no allocation will take place.

There is a bit of a caveat for functions returning strings;

``rust /// Functionsum_int` just adds all arguments as integers struct SumInt {}

/// Function avg_float is an aggregate function. struct AvgFloat { running_total: f64 }

/// Generate random lipsum struct Lipsum { res: String } ```

BasicUdf Implementation

The next step is to implement the BasicUdf trait

```rust

[register]

impl BasicUdf for SumInt { type Returns<'a> = Option;

fn init<'a>(args: &'a ArgList<'a, Init>) -> Result<Self, String> {
    // ...
}

fn process<'a>(
    &'a mut self,
    args: &ArgList<Process>,
) -> Result<Self::Returns<'a>, ProcessError> {
    // ...
}

} ```

AggregateUdf Implementation

Compiling

Assuming the above has been followed, all that is needed is to produce a C dynamic library for the project. This can be done by specifying crate-type = ["cdylib"] in your Cargo.toml. After this, compiling with cargo build --release will produce a loadable .so file (in target/release).

Running

Once compiled, the produced object file needs to be copied to the location of the plugin_dir SQL variable - usually, this is /usr/lib/mysql/plugin/.

Once that has been done, CREATE FUNCTION can be used in MariaDB/MySql to load it.

bash cp /target/release/examples/libbasic_sum.so /usr/lib/mysql/plugin/

sql CREATE FUNCTION sum_int RETURNS integer SONAME 'libbasic_sum.so';

MariaDB [(none)]> select sum_int(1, 2, 3, 4, 5, 6, '1'); +--------------------------------+ | sum_int(1, 2, 3, 4, 5, 6, '1') | +--------------------------------+ | 22 | +--------------------------------+ 1 row in set (0.001 sec)

It is also quite possible to have more than one function in the same object file.

Building in Docker

These currently rely on a feature called generic associated types (GATs) which are not currently available on stable. For that reason, rust version >= 1.65 is required - this includes the current beta and nightly channels, and scheduled to become stable on 2022-11-03.

```sh

This will mount your current directory at /build, and use a new .docker-dargo

directory for cargo's cache. It will use your same target folder.

Change the bash -c command based on what you want to build.

docker run --rm -it \ -v $(pwd):/build \ -e CARGO_HOME=/build/.docker-cargo \ rustlang/rust:nightly \ bash -c "cd /build; cargo build --release --examples" ```

Testing in docker

sh docker run --rm -it \ -v $(pwd)/target:/target \ -e MARIADB_ROOT_PASSWORD=banana \ mariadb bash

docker run --rm -it \ -v $(pwd)/target:/target \ -e MARIADB_ROOT_PASSWORD=banana \ --name mariadb_test \ mariadb docker exec -it mariadb_test bash cp /target/release/examples/*.so /usr/lib/mysql/plugin/ mysql -pbanana CREATE FUNCTION sql_sequence returns integer soname 'libsequence.so'; select sql_sequence(1);