Kuneiform Precompiles
Kuneiform precompiles are custom libraries that can be imported and called in Kuneiform actions. By implementing custom action precompiles, developers can add support for new functionality to their Kwil network's Kuneiform language. These functionalities are wide ranging, and can include anything from performing hashing algorithms to directly calling the active Postgres connection.
How Precompiles Work
Action precompiles are implemented as Go libraries that are loaded into the Kwil node at startup. All precompiles are globally available to all Kuneiform schemas. There are two distinct steps in an action precompile's lifecycle:
Initialize: The action precompile is imported into a Kuneiform schema. It is given a set of initialization values, and an alias by which it can be called. Initialize is called each time the precompile is imported into a schema. If a node is restarted, all precompiles for all persisted schemas are re-initialized.
Call: The action precompile is called by its name (given during initialization), and is given a method name and a set of arguments. It then performs the requested action and returns a result to the caller. This can only be done after the precompile has been initialized.
Precompiles must match the following function signature and interface. For more information on the available types, see the reference documententation for the common and action precompiles packages.
package actions
// Initializer initializes a new instance of a precompile.
// It is called when a Kuneiform schema is deployed that calls
// "use <precompile> {key: "value"} as <name>". It is also called
// when the node starts up, if a database is already deployed that
// uses the precompile. The key/value pairs are passed as the
// metadata parameter. When initialize is called, the dataset is not
// yet accessible.
type Initializer func(ctx *DeploymentContext, service *common.Service, metadata map[string]string) (Instance, error)
// Instance is a named initialized instance of a precompile. It is
// returned from the precompile initialization, as specified by the
// Initializer. It will exist for the lifetime of the deployed
// dataset, and a single dataset can have multiple instances of the
// same precompile.
type Instance interface {
// Call executes the requested method of the precompile. It is up
// to the instance implementation to determine if a method is
// valid, and to subsequently decode the arguments. The arguments
// passed in as args, as well as returned, are scalar values.
Call(scoper *ProcedureContext, app *common.App, method string, inputs []any) ([]any, error)
}
Using Precompiles in Kuneiform
Import and Call
Precompiles are used in Kuneiform by importing them at the top of a schema file. They are then called in actions, and can receive parameters
from the action. Precompiles can receive initialization values when imported via passing a set of key-value pairs to the use
statement. This
creates an instance of the precompile with the given name. Multiple instances of the same precompile can be imported and used in the same schema.
database mydb;
// import extension and pass initialization values
use custom_extension {
key1: 'value1',
key2: 'value2'
} as ext1;
use custom_extension {
key1: 'value3',
key2: 'value4'
} as ext2;
// call extension within action
action some_action($arg1) public {
$result1 = ext1.method_1($arg1);
$result2 = ext2.method_2($arg1);
// ...
}
Corresponding Implementation
The corresponding Go implementation for the above schema would look something like this:
package main
import (
"fmt"
"github.com/kwilteam/kwil-db/common"
"github.com/kwilteam/kwil-db/extensions/actions"
)
func init() {
// Register the precompile with the node
// "custom_extension" is the name of the precompile that
// is used in the Kuneiform schema
actions.RegisterPrecompile("custom_extension", initialize)
}
// initialize is called when the "use custom_extension" statement is
// called in the Kuneiform schema. For the above schema, it would be
// called twice, once for each instance of the precompile.
func initialize(ctx *actions.DeploymentContext, service *common.Service, metadata map[string]string) (actions.Instance, error) {
// first instance:
// metadata == map[string]string{
// "key1": "value1",
// "key2": "value2",
// }
//
// second instance:
// metadata == map[string]string{
// "key1": "value3",
// "key2": "value4",
// }
return &instance{}, nil
}
// a struct that implements the actions.Instance interface
type instance struct {}
// Call is called when the precompile is called in the Kuneiform
// schema. It is called once for each call to the precompile. In
// the above schema, it would be called twice, once for each instance
// of the precompile.
func (h *helloWorldInstance) Call(scoper *actions.ProcedureContext, app *common.App, method string, inputs []any) ([]any, error) {
// first instance:
// method == "method_1"
// inputs == []any{"arg1"}
//
// second instance:
// method == "method_2"
// inputs == []any{"arg1"}
// returns exactly one result,
// stored as $result1 and $result2 in the Kuneiform schema
return []any{"result"}, nil
}