Overview

This tutorial will teach you how to write WebAssembly (Wasm) UDFs in in C/C++ and Rust, load them into SingleStoreDB, and evaluate them in queries.

Let's get started by learning how to setup our environment!

Setup

For each example in this tutorial, it will be useful to create yourself a separate work directory in which to put your code and compiled artifacts. This will help keep your files for each example organized.

In addition to a SQL client and SingleStore database, you’ll need a few other things to get your environment set up properly. For this, you have some options:

Setting Up a Development Container

If you have a container runtime (e.g. Docker) available to you, the easiest way to get started is to utilize one of our prebuilt Wasm development containers. To do this, first clone or download the SingleStore WASM Toolkit repository:

git clone git@github.com:singlestore-labs/singlestore-wasm-toolkit.git

The development containers provided in this repo contain all of the tools you'll need to write, build, and deploy Wasm UDFs. Additionally, the repo also includes many examples, including fully-written versions of the examples referenced in this tutorial. You may choose from the following containers, depending on your preferred workflow:

The exact setup for each of these is described in their respective sections.

VS Code Development Container Setup

This container is designed to integrate natively into VS Code using the Remote Containers extension. VS Code an IDE framework that is usable run on Mac, Windows, or Linux, and can be downloaded here.

Note MacOS Users: VS Code is usable on both x64 and M1 Macs. One way to install it for MacOS is to use the package manager brew. To get brew, please follow the instuctions here. When you have brew, run brew install --cask visual-studio-code to obtain VS Code.

  1. Ensure that VS Code is installed, and the wasm-toolkit repository has been cloned.

  2. In VS Code, install the Remote - Containers VS Code Extension if you do not already have it.

  3. In VS Code, type F1 and search for “Open Folder in Container”.

  4. Navigate to the directory where you cloned the wasm-toolkit repo, and click Open.

  5. VS Code will now build the container, which may take quite a while.

When the container build has completed, you should see wasm-toolkit repository's file tree appear in the EXPLORER panel on the left side of the VS Code window. For the purposes of this tutorial, it will be most convenient if you store your code in a directory relative to the repository. Here's one way to do this:

  1. Right-click on the empty space in the EXPLORER panel, and select New Folder.

  2. Type wasm-tutorial and press Enter. There should now be a sub-folder by this name in the tree. Put any files you create in here.

Note As you work through this tutorial, please be sure to execute your commands inside the VS Code terminal window so that you have access to the necessary tools.

Next, let's pick an example to work through.

Standalone Container Setup

This container provides a standalone shell environment configured with a variety of Wasm development tools. It will mount the directory of your choosing.

  1. Ensure that the wasm-toolkit repository has been cloned.

  2. Create a subdirectory for the code in this tutorial. This can be anywhere. For the purposes of this tutorial, we'll assume it is in /home/$USER/wasm-tutorial.

  3. At your command prompt, change to the root directory of the wasm-toolkit repository and type scripts/dev-shell /home/$USER/wasm-tutorial. Ensure that the argument reflects the actual path of the directory you created in the above step.

  4. You should now see the following prompt:

[dev-shell]:~/src %

The src directory has been mounted from /home/$USER/wasm-tutorial (or whatever alternative directory you specified in step 3). It is not necessary to write the code for this tutorial inside the container's shell; you may use your preferred editing workflow for this. However, please do be sure to run all suggested build and deployment commands inside the container's shell so that you have access to the necessary tools.

Next, let's pick an example to work through.

Local Setup

If you do not wish to use a development container and/or you’d prefer to set up your development environment on your local system, you’ll need to do the following:

  • Download the WASI SDK and decompress it somewhere. Ensure that your $PATH variable is prefixed with this location when you are running the build commands suggested in this tutorial. For example, assuming you installed the WASI SDK in /opt/wasi-sdk, at the command prompt, you can type (assuming your are using bash/zsh):

    • export PATH=/opt/wasi-sdk/bin:$PATH
  • Download and install the Rust toolchain:

    • curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

    • source $HOME/.cargo/env

  • Download and install the wit-bindgen program:

    • cargo install --git https://github.com/bytecodealliance/wit-bindgen wit-bindgen-cli
  • Download and install the cargo-wasi plugin:

    • cargo install cargo-wasi
  • (Optional) If you would like to use the testing tool writ and the deployment tool pushwasm, please follow the installation instructions in their respective repositories and ensure they are in your $PATH.

For a more detailed walkthrough on setting up for local development, also consider checking out David Lee's blog entry here.

Next, let's pick an example to work through.

Examples

In this tutorial, we'll walk you through two different examples.

power-of

This is a very small example that will show you the basics of creating a Wasm UDF. In it, we will develop a program that computes x^y (that is, "x to the power of y").

You can get started on this tutorial here.

split-str

This is a more advanced example in which we will write a Wasm TVF that splits a string into substring given a delimiting character. It will show you how to develop a Wasm function that accepts and returns complex structures such as strings and records.

You can get started on this tutorial here.

Creating a WIT Specification

  • To see how to create a WIT specification for the power-of example, please look here.
  • To see how to create a WIT specification for the split-str example, please look here.

Creating a WIT Specification for the power-of Example

Before we do any coding, let’s first define our interface. WIT is an Interface Definition Language (IDL) used to describe Wasm modules. It is provided in files with the .wit extension.

This example will compute x^y (that is, "x to the power of y").

In a new work directory, create a new file called power.wit in your text editor, and add this content:

power-of: func(b: s32, exp: s32) -> s32

This function will take two signed 32-bit integers as arguments (the base b and the exponent exp) and return a single, signed 32-bit integer.

Go ahead and save the file.

Now we’re ready to write some code.

  • If you'd like to learn about implementing this example in C, please look here.
  • If you'd like to learn about implementing this example in Rust, please look here.

Creating a WIT Specification for the split-str Example

Before we do any coding, let’s first define our interface. WIT is an Interface Definition Language (IDL) used to describe Wasm modules. It is provided in files with the .wit extension.

In this example, we’ll work with strings and nested types. Let’s create a function that takes a string, splits it at every occurrence of a delimiting string, and then returns each sub-string along with its starting indices. The output will be sent back as a list of records (aka structures).

To start, let’s create a new work directory, and inside of it we’ll make a new file called split.wit. The WIT IDL we need is below, so we can go ahead and paste that in and save it.

record subphrase {
  str: string,
  idx: s32
}
split-str: func(phrase: string, delim: string) -> list<subphrase>

Now we’re ready to write some code.

  • If you'd like to learn about implementing this example in C++, please look here.
  • If you'd like to learn about implementing this example in Rust, please look here.

Implementation and Compilation

  • To see how to implement and compile a Wasm function in C/C++, please look here.
  • To see how to implement and compile a Wasm function in Rust, please look here.

Implementing and Compiling Examples in C/C++

  • To see how to implement and compile the power-of example in C, please look here.
  • To see how to implement and compile the split-str example in C++, please look here.

Implementing the power-of Example in C

Generating Bindings

For the C language, we’ll need to explicitly generate language bindings for our functions. We can do this using the wit-bindgen program. In your work directory, run the following command:

wit-bindgen c --export power.wit

This will generate two files in your work directory: power.c and power.h.

If you look at the contents of the power.h file, you’ll see a single prototype:

int32_t power_power_of(int32_t base, int32_t exp);

The name looks odd because wit-bindgen concatenates the name of the WIT file with the name of the function. That’s ok; as we’ll see in a moment, the name that is actually exported will make more sense.

Aside from the function name, the signature is as expected: take two 32-bit integers, and return one.

Next, open the power.c file in your editor. We’ll ignore the canonical_abi_realloc and canonical_abi_free functions for now and skip to the bottom where we will find a function called __wasm_export_power_power_of. This is wrapper code that handles passing values through the Wasm Canonical ABI (a trivial operation in this case). Looking at the body of this function, we can see that it calls the power_power function that was declared in the header file. We’ll need to provide the implementation for this.

Before we continue, though, notice the following line just above the this function’s definition:

__attribute__((export_name("power-of")))

This line forces the name of this wrapper function be exported from the compiled module as power-of (the hyphen is the preferred word separator for function names in Wasm). Fortunately, consumers only need to invoke it by this name.

Implementing and Compiling

Now, let’s implement the logic we need. To the bottom of the power.c file, add the following code (we can start by copying in the prototype from the power.h file):

int32_t power_power_of(int32_t base, int32_t exp)
{
    int32_t res = 1;
    for (int32_t i = 0; i < exp; ++i)
    {
        res *= base;
    }
    return res;
}

Now let’s save the file and get back to our command line.

We can compile this program into a Wasm module by using the following command.

clang                            \
    --target=wasm32-unknown-wasi \
    -mexec-model=reactor         \
    -I.                          \
    -o power.wasm                \
    power.c

You should now see a power.wasm file in your directory.

Next, we'll do some testing.

Implementing the split-str example in C++

Generating Bindings

In this example, we’ll use C++ so that we can leverage the STL’s higher-level data structures and keep our implementation focused on the big picture as much as possible.

As before, we’ll start by generating the C language bindings. Run the wit-bindgen command:

wit-bindgen c --export split.wit

There should now be a split.c and split.h file in your work directory. Since we’ll be using C++, rename split.c to split.cpp:

Implementing and Compiling

Since we’ll be using C++, rename split.c to split.cpp:

mv split.c split.cpp

Now let’s take a look at split.h. As we might expect, there are a few more definitions in here than in the simpler power-of example. The wit-bindgen program has generated a struct definition for us to use when passing our strings, as well as ones for the subphrase record and its enclosing list. At the bottom is prototype for the function we are going to implement:

void split_split_str(split_string_t *phrase, split_string_t *delim, split_list_subphrase_t *ret0);

This, too, looks a bit different than in the power-of example. For one thing, the function doesn’t return a value. Since this function will be returning a list, for which we’ll need to dynamically allocate memory, the result is passed as an argument pointer instead.

Now let’s open up the split.cpp file. Once again, we are going to add our implementation here. At the bottom of the file, we can find the generated wrapper function, called __wasm_export_split_split_str. This wrapper delegates to the function we will be implementing, and we also can see that it is doing work required for lifting and lowering the data types on either side of the function call.

We’ll now add our code. Let’s first update the top of split.cpp as follows:

#include <memory>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>
#include <split.h>

Since C++ has a more strict compiler than C, we’ll also need to make a small change to the generated code in this file at line 38. Go ahead and change the following line:

ret->ptr = canonical_abi_realloc(NULL, 0, 1, ret->len);

... to this:

ret->ptr = reinterpret_cast<char *>(canonical_abi_realloc(NULL, 0, 1, ret->len));

And finally, to the bottom of the file, we’ll add this chunk of code:

void split_split_str(split_string_t *phrase, split_string_t *delim, split_list_subphrase_t *ret0)
{
    // Clear the result.
    memset(ret0, 0, sizeof(split_list_subphrase_t));

    // Parse the tokens.
    std::string phr(phrase->ptr, phrase->len);
    std::string dlm(delim->ptr, delim->len);
    std::string tok;
    std::vector<std::pair<std::string, size_t>> subs;
    size_t start = 0, end = 0;
    if (delim->len)
    {
        while ((end = phr.find(dlm, start)) != std::string::npos)
        {
            tok = phr.substr(start, end - start);
            subs.push_back(std::pair<std::string, size_t>(tok, start));
            start = end + dlm.length();
        }
    }
    subs.push_back(std::pair<std::string, size_t>(phr.substr(start), start));

    // Populate the result.
    bool err = false;
    auto res = (split_subphrase_t *) malloc(phr.size() * sizeof(split_subphrase_t));
    for (int i = 0; !err && i < subs.size(); ++i)
    {
        auto& sub = subs[i].first;
        res[i].idx = static_cast<int32_t>(subs[i].second);
        res[i].str.len = sub.length();
        res[i].str.ptr = strdup(sub.c_str());
        if (!res[i].str.ptr)
            err = true;
    }

    // If success, assign the result. Else, clean up and return an empty list.
    if (!err)
    {
        // Success; assign the result.
        ret0->ptr = res;
        ret0->len = subs.size();
    }
    else
    {
        if (res)
        {
            for (int i = 0; i < subs.size(); ++i)
                if (res[i].str.ptr)
                    free(res[i].str.ptr);
            free(res);
        }
    }

    // Per the Canonical ABI contract, free the input pointers.
    free(phrase->ptr);
    free(delim->ptr);
}

There is much more work going on here than in the power-of example; a fair amount of it deals with memory management.

The Wasm Canonical ABI requires that any dynamic memory passed from the host to the guest or vice-versa transfers the ownership to the receiver. This explains the last two lines of our split_split_str function, where we free the pointers to the strings we have been passed as arguments. This memory will have been allocated by the host using our guest module’s exported canonical_abi_realloc function, which takes it from the Wasm instance’s linear memory at runtime. Conversely, you may also notice that we pass dynamically allocated memory out of the function; the host is expected to free it by calling our module's canonical_abi_free routine, which will return it to the linear memory.

Now let’s save the file and build the module. We will use a similar approach as we did in the simple example above, but with a couple of tweaks. First, we’ll use clang++, since this is C++ code. And second, we’ll need to include the option -fno-exceptions because Wasm doesn’t yet support exception handling (there is a proposal, however).

clang++                          \
    -fno-exceptions              \
    --target=wasm32-unknown-wasi \
    -mexec-model=reactor         \
    -s                           \
    -I.                          \
    -o split.wasm                \
    split.cpp

As expected, there should now be a split.wasm file in your work directory.

Next, we'll do some testing.

Implementing and Compiling Examples in Rust

  • To see how to implement and compile the power-of example in Rust, please look here.
  • To see how to implement and compile the split-str example in Rust, please look here.

Developing the power-of example in Rust

Initialize Your Source Tree

To setup our project, we'll need to do a few things:

  1. Ensure that you are in a new directory. It can be called anything you want.

  2. In the work directory, run the following to set up a skeltal Rust source tree:

    cargo init --vcs none --lib
    
  3. Now, create a special configuration file to tell utilities like the rust-analyzer that the default build target of this project is Wasm. Run this command:

    mkdir .cargo && echo -e "[build]\ntarget = \"wasm32-wasi\"\n" > .cargo/config.toml   
    

Copy the WIT File

We’ll need the power.wit file we created earlier. Copy it into this directory if it is not already there.

Implementing and Compiling

Now, edit the file called Cargo.toml so that it looks like the following:

[package]
name = "power"
version = "0.1.0"
edition = "2018"

[dependencies]
wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen.git", rev = "60e3c5b41e616fee239304d92128e117dd9be0a7" }

[lib]
crate-type = ["cdylib"]

Now we’re almost ready to roll. Edit the file src/lib.rs and replace its content with this:

wit_bindgen_rust::export!("power.wit");
struct Power;

impl power::Power for Power {
    fn power_of(base: i32, exp: i32) -> i32 {
        let mut res = 1;
        for _i in 0..exp {
            res *= base;
        }
        res
    }
}

The syntax at the top of the code is boiler-plate. The export macro generates code that declares a trait named after our WIT file (and some other things). So, our main job is here is to implement this trait. If you are curious what the macro actually generates, you can run cargo expand and it will show you the fully expanded source code.

The WIT IDL is heavily inspired by the Rust language syntax, so it was pretty easy to derive the Rust function signature we needed from the IDL:

  • Replaced hyphens with underscores

  • Changed s32 types to i32

Now we can compile the program into a wasm module using this command:

cargo wasi build --lib

The new Wasm file should be written to target/wasm32-wasi/debug/power.wasm.

Next, we'll do some testing.

Developing the split-str example in Rust

Initialize Your Source Tree

To setup our project, we'll need to do a few things:

  1. Ensure that you are in a new directory. It can be called anything you want.

  2. In the work directory, run the following to set up a skeltal Rust source tree:

    cargo init --vcs none --lib
    
  3. Now, create a special configuration file to tell utilities like the rust-analyzer that the default build target of this project is Wasm. Run this command:

    mkdir .cargo && echo -e "[build]\ntarget = \"wasm32-wasi\"\n" > .cargo/config.toml   
    

Copy the WIT File

We'll need the split.wit file we created earlier. Copy it into this directory if it is not already there.

Implementing and Compiling

Much of this will be similar to the techniques we used in the power-of example.

Edit the Cargo.toml file so it looks like this:

[package]
name = "split"
version = "0.1.0"
edition = "2018"

[dependencies]
wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen.git", rev = "60e3c5b41e616fee239304d92128e117dd9be0a7" }

[lib]
crate-type = ["cdylib"]

And, for the implementation, edit the src/lib.rs file and replace its contents with this:

wit_bindgen_rust::export!("split.wit");
struct Split;
use crate::split::Subphrase;

impl split::Split for Split {

    fn split_str(phrase: String, delim: String) -> Vec<Subphrase> {
        phrase
            .split(&delim)
            .scan(0, |idx, s| {
                let current = Subphrase {
                    str: s.to_string(),
                    idx: *idx as i32
                };
                *idx += (s.0 + delim.0) as i32;
                Some(current)
            })
            .collect()
    }
} 

Fortunately, with Rust, we have some nice language features that help make our code concise. Unlike in C/C++, we don’t need to explicitly free the parameters. Due to Rust’s move semantics, they will be freed implicitly when the function ends. Allocation for the output vector is also managed “under the hood” by Rust’s robust data structures and runtime. Finally, the declaration of the Subphrase struct happens via the wit_bindgen_rust::export macro, so we don’t need to do it.

Let’s compile the Wasm module now:

cargo wasi build --lib

The new Wasm file should be written to target/wasm32-wasi/debug/split.wasm.

Next, we'll do some testing.

Testing

  • To see how to test the power-of example, please look here.
  • To see how to test the split-str example, please look here.

Testing the power-of Example

The writ utility provides a convenient way for us to test our Wasm function in isolation before we load it into the database. It accepts JSON-formatted arguments on the command-line, casts them to the actual types defined in the Wasm function, and then passes them in.

Let's make sure our power-of program is working correctly by doing a few spot-tests. The examples below assume that the power.wasm file exists in the current directory. If you are using a Rust build, your Wasm file will be located at target/wasm32-wasi/debug/power.wasm.

$ writ --wit power.wit ./power.wasm power-of 2 3
8

$ writ --wit power.wit ./power.wasm power-of 2 0
1

$ writ --wit power.wit ./power.wasm power-of 0 0
1

$ writ --wit power.wit ./power.wasm power-of 0 2
0

$ writ --wit power.wit ./power.wasm power-of 2 -3
1

Except for the last attempt, the function seems to work correctly. To keep this example simple, we'll just assume that negative exponents won't be provided.

Now, we're ready to deploy.

Testing the split-str Example

The writ utility provides a convenient way for us to test our Wasm function in isolation before we load it into the database. It accepts JSON-formatted arguments on the command-line, casts them to the actual types defined in the Wasm function, and then passes them in.

Let's make sure our split-str program is working correctly by doing a few spot-tests. The examples below assume that the split.wasm file exists in the current directory. If you are using a Rust build, your Wasm file will be located at target/wasm32-wasi/debug/split.wasm.

$ writ --wit split.wit ./split.wasm split-str 'hello_you_fine_folks' '_'
[
  {
    "str": "hello",
    "idx": 0
  },
  {
    "str": "you",
    "idx": 6
  },
  {
    "str": "fine",
    "idx": 10
  },
  {
    "str": "folks",
    "idx": 15
  }
] 

Let's try a couple of edge cases as well.

$ writ --wit split.wit ./split.wasm split-str 'hello' '_'
[
  {
    "str": "hello",
    "idx": 0
  }
]

$ writ --wit split.wit split.wasm split-str 'hello--there-' '-'
[
  {
    "str": "hello",
    "idx": 0
  },
  {
    "str": "",
    "idx": 6
  },
  {
    "str": "there",
    "idx": 7
  },
  {
    "str": "",
    "idx": 13
  }
]

Looks good!

Now, we're ready to deploy.

Deploying Examples into SingleStoreDB

  • For information on deploying the power-of example, please look here.
  • For information on deploying the split-str example, please look here.

Deploying the power-of Example

Now that you've compiled and tested your Wasm function, it is ready for deployment into the database. This can be done in multiple ways. The easiest is probably to use the pushwasm tool, included in the wasm-toolkit development containers. Alternatively, you can "pull" the Wasm module into the database by first uploading it to cloud storage (SingleStoreDB supports pulling Wasm modules from multiple cloud providers -- GCS, Azure, and S3). We'll discuss both techniques.

Before we start, ensure that a destination database is available. To do this, using your favorite SQL client, create a new database called wasm_tutorial. For example, you might use the following statements:

CREATE DATABASE wasm_tutorial;
USE wasm_tutorial;
  • To deploy the power-of example using the pushwasm tool, please see here.
  • To deploy the power-of example from cloud storage, please see here.

Deploying power-of Using pushwasm

The pushwasm tool will upload our WIT file and compiled Wasm module into the database. To use it, we'll need the following information. Since this depends highly on your specific environment, we'll just make some generic assumptions about their values.

  • The hostname of the SingleStoreDB server (we'll call this myserver)
  • The destination database name (we'll call this wasm_tutorial)
  • The user ID and password of the database user (we'll call this user admin)
  • The path to the compiled Wasm module (we'll use ./power.wasm below, but for the Rust example, you should use located in target/wasm32-wasi/debug/power.wasm instead)
  • The path to the WIT file

Now, run the following command from within your work directory:

pushwasm --prompt --wit ./power.wit mysql://admin@myserver/wasm_tutorial ./power.wasm power_of

The --prompt option will cause a prompt to appear, where you can enter your database user's password.

When the deployment has completed, you should see the following:

Wasm UDF 'power_of' was created successfully.

Finally, we're ready to run the UDF!

Deploying power-of From Cloud Storage

You'll need the following information to do this:

  • The path to the compiled Wasm module (we'll use ./power.wasm below, but for the Rust example, you should use located in target/wasm32-wasi/debug/power.wasm instead)
  • The path to the WIT file
  • Your cloud storage credentials and path (we'll assume an S3 bucket called wasm-modules).

From your work directory, you'll need to upload your WIT (./power.wit) and compiled Wasm (.wasm) files to a bucket to the wasm-modules S3 bucket.

Now, connect to the database from your SQL client and run the following statement to pull the module from S3 into the wasm_tutorial database:

CREATE FUNCTION power_of AS WASM 
FROM S3 'wasm-modules/power.wasm'
    CREDENTIALS '{
        "aws_access_key_id": "ASIAZPIKLSJ3HM7FKAUB",
        "aws_secret_access_key": FwoGZXIvYXdzEL3fv [...]"
    }'
    CONFIG '{"region": "us-east-1"}'
WITH WIT FROM S3 'wasm-modules/power.wit'
    CREDENTIALS '{
        "aws_access_key_id": "ASIAZPIKLSJ3HM7FKAUB", 
        "aws_secret_access_key": FwoGZXIvYXdzEL3fv [...]"
    }' 
    CONFIG '{"region": "us-east-1"}';

If the UDF has been created successfully, you will see something like:

Query OK, 1 row affected (0.029 sec)

Finally, we're ready to run the UDF!

Deploying the split-str Example

Now that you've compiled and tested your Wasm function, it is ready for deployment into the database. This can be done in multiple ways. The easiest is probably to use the pushwasm tool, included in the wasm-toolkit development containers. Alternatively, you can "pull" the Wasm module into the database by first uploading it to cloud storage (SingleStoreDB supports pulling Wasm modules from multiple cloud providers -- GCS, Azure, and S3). We'll discuss both techniques.

Before we start, ensure that a destination database is available. To do this, using your favorite SQL client, create a new database called wasm_tutorial. For example, you might use the following statements:

CREATE DATABASE wasm_tutorial;
USE wasm_tutorial;
  • To deploy the split-str example using the pushwasm tool, please see here.
  • To deploy the split-str example from cloud storage, please see here.

Deploying split-str Using pushwasm

The pushwasm tool will upload our WIT file and compiled Wasm module into the database. To use it, we'll need the following information. Since this depends highly on your specific environment, we'll just make some generic assumptions about their values.

  • The hostname of the SingleStoreDB server (we'll call this myserver)
  • The destination database name (we'll call this wasm_tutorial)
  • The user ID and password of the database user (we'll call this user admin)
  • The path to the compiled Wasm module (we'll use ./power.wasm below, but for the Rust example, you should use located in target/wasm32-wasi/debug/power.wasm instead)
  • The path to the WIT file

Now, run the following command from within your work directory. Unlike the power-of example, we'll deploy the split-str function as a Table-Valued Function (TVF). This will require us to pass the --tvf flag.

pushwasm --tvf --prompt --wit ./split.wit mysql://admin@myserver/wasm_tutorial ./split.wasm split_str

The --prompt option will cause a prompt to appear, where you can enter your database user's password.

When the deployment has completed, you should see the following:

Wasm TVF 'split_str' was created successfully.

Finally, we're ready to run the TVF!

Deploying split-str From Cloud Storage

You'll need the following information to do this:

  • The path to the compiled Wasm module (we'll use ./split.wasm below, but for the Rust example, you should use located in target/wasm32-wasi/debug/split.wasm instead)
  • The path to the WIT file
  • Your cloud storage credentials and path (we'll assume an S3 bucket called wasm-modules).

From your work directory, you'll need to upload your WIT (./split.wit) and compiled Wasm (./split.wasm) files to a bucket to the wasm-modules S3 bucket.

Now, connect to the database from your SQL client and run the following statement to pull the module from S3 into the wasm_tutorial database. Note that unlike the power-of example, we'll be deploying this split-str function as a Table-Valued Function (TVF). This will require us to include the RETURNS TABLE clause.

CREATE FUNCTION split_str RETURNS TABLE AS WASM 
FROM S3 'wasm-modules/split.wasm'
    CREDENTIALS '{
        "aws_access_key_id": "ASIAZPIKLSJ3HM7FKAUB",
        "aws_secret_access_key": FwoGZXIvYXdzEL3fv [...]"
    }'
    CONFIG '{"region": "us-east-1"}'
WITH WIT FROM S3 'wasm-modules/split.wit'
    CREDENTIALS '{
        "aws_access_key_id": "ASIAZPIKLSJ3HM7FKAUB", 
        "aws_secret_access_key": FwoGZXIvYXdzEL3fv [...]"
    }' 
    CONFIG '{"region": "us-east-1"}';

If the TVF has been created successfully, you will see something like:

Query OK, 1 row affected (0.029 sec)

Finally, we're ready to run the TVF!

Running In the Database

  • To see how to run the power_of UDF, please look here.
  • To see how to run the split_str TVF, please look here.

Running In the Database

Hooray, we can now run our Wasm function in the database!

When we imported our function, the database automatically converted the hyphen s (-) in our function name to underscores (_). So, power-of is now a UDF called power_of.

Using the following syntax, we can run it as a UDF:

SELECT power_of(2, 8);

... which should return a single-column row with the value 256.

Neat!

Let's review what we've learned.

Running In the Database

Hooray, we can now run our Wasm function in the database!

When we imported our function, the database automatically converted the hyphens (-) in our function name to underscores (_). So, split-str is now a UDF called split_str.

Using the following syntax, we can run our function as a TVF:

SELECT * FROM split_str('wasm_rocks_the_house', '_');

... which will produce the following output:

+-------+-----+
| str   | idx |
+-------+-----+
| wasm  |   0 |
| rocks |   5 |
| the   |  11 |
| house |  15 |
+-------+-----+
94 rows in set (0.001 sec)

Awesome!

Let's review what we've learned.

Wrap-Up

Well, this concludes our little tutorial. In this tutorial, we learned how to turn some simple use cases into WebAssembly programs. We then examined how to test them, load them into SingleStoreDB, and run them there.

Hopefully, this helps you kickstart the creation of your own Wasm UDFs and TVFs. Thanks for tuning in!