Skip to main content

Kuneiform Procedures

Kuneiform supports a basic procedural language for building logic into schemas. Procedures can be used for building access control logic, performing arithmetic, and managing control flow.

Declaring Procedures

Procedures are declared using the procedure keyword. They can be given a list of parameters and their corresponding types, return types, and access modifiers. Procedures are strongly typed, and all inputs and outputs must exactly match the declared types.

The example below shows the signature for a get_user procedure. It takes one input (the id of the user, of type uuid), and returns the user's name and age. It will return exactly one record, containing a name and age:

// get_user takes one UUID, and will return a single name and age.
// It has a view tag, which means it does not mutate state, and can
// be called without submitting a blockchain transaction
procedure get_user($id uuid) public view returns (name text, age int) {
// procedure implementation
}

Procedures can also return tables, which are a collection of many records. The below example returns all user IDs and names of a certain age:

// get_users takes one integer and will return a table with two columns:
// "name" of type text, and "age" of type int
// It has a view tag, which means it does not mutate state, and can
// be called without submitting a blockchain transaction
procedure get_users($age int) public view returns table(name text, age int) {
// procedure implementation
}

If a procedure does not return anything, the return should not be specified:

// create_user takes a text and int, and returns nothing.
// It does not have a view tag so that it can mutate state,
// and therefore can only be called with a blockchain transaction.
procedure create_user($name text, $age int) public {
// procedure implementation
}

Access Modifiers

Procedures can be given access modifiers to specify how/when they can be called. All procedures must be specified as public or private: public procedures can be called externally, while private procedures can only be called by other procedues within the same schema. This works exactly like public and private in JavaScript:

database social_network;

// internal_procedure can only be called by other procedures
// within this schema
procedure internal_procedure() private returns (id int) {
return 1;
}

// external_procedure can be called externally by anybody
procedure external_procedure() public {
$id := internal_procedure();
// ...
}

Additionally, procedures can be given owner and view tags. owner means that a procedure can only be called by the wallet that deployed the schema. view means that the procedure does not mutate state, and can therefore be called without submitting a blockchain transaction.

database social_network;

// owner_only_procedure can only be called by the schema owner
procedure owner_only_procedure() public owner {
// ...
}

// view_procedure can be called without submitting a transaction
procedure view_procedure() public view returns (id int) {
// ...
}

Procedure Syntax

Declaring and Assigning Variables

All variables in procedures are strongly typed. Variables can be assigned at declaration, or can be nil and assigned after declaration. They can also be type casted:

procedure declare_vars() public {
// declare and assign new int var:
$var1 int := 1;

// declare a new var to be assigned to later
$var2 text;

// assign to $var2. type cast to match type text:
$var2 := $var1::text;

// declare and assign a new variable. The type will be inferred as text:
$var3 := 'hello world';
}

Arithmetic

Basic arithmetic can be performed in procedures. For information on operator precedence, see Operators.

procedure do_math($num1 int, $num2 int) public {
// add and divide, with precedence:
$avg int := ($num1 + $num2)/2;

// modulo:
$mod int := $num2%10;

// negate, multiply
$neg_mul int := (-$avg)*$mod;
}

Executing SQL

SQL statements can be executed at any point in a procedure. In the example below, we generate a deterministic UUID using a random UUID namespace and the transaction ID. We then use the generated id as the user's primary key. To learn more about the supported functions, see Functions.

procedure create_user($name text, $age int) public {
$id uuid := uuid_generate_v5('455f60aa-0569-4aaa-8469-63be2ec4dd96'::uuid, @txid);
// do something
// ...
INSERT INTO users (id, name, age)
VALUES ($id, $name, $age);
}

Procedures returning tables can also be joined against:

procedure get_users() public view returns table(id uuid, name text, age int) {
// ...
}

procedure join_procedure() public {
SELECT * FROM posts AS p
INNER JOIN get_users() AS u
ON p.author_id = u.id;
// ...
}

Handling Arrays

Procedures support basic array operations. Arrays use 1-based indexing.

procedure handle_array() public view returns (val text) {
$arr text[] := ['hello', 'world'];
$arr := array_append($arr, 'goodbye');

$len int := array_length($arr); // $len == 3

// return the first value.
return $arr[1];
}

If/Else

If/else/elseif statements can be used to conditionally execute code:

procedure age_conditional($age int) public {
if $age < 0 {
// do something
} elseif $age < 18 {
// do something
} else {
// something else
}
}

They can be logically combined with and and or:

procedure age_conditional($age int) public {
if $age < 0 or $age > 100 {
// do something
}
}

For Loop

For loops can be declared over 3 different targets: arrays, ranges, and SQL statements.

Array Loop

procedure iterate_over_array($names text[]) public {
for $name in $names {
// $name is of type `text`
}
}

Range Loop

procedure iterate_over_range() public {
// iterating over all numbers from 1 to 25
for $i in 1..25 {
// $i is type int
}
}

procedure iterate_range_dynamic($start int, $end int) public {
// can also iterate over dynamic ranges
for $i in $start..$end {
// ...
}
}

SQL Loops

You can also iterate over each record returned from a SQL query. The following examples reference an example table users, with columns id, name, and age.

procedure iterate_over_sql() public {
for $row in SELECT id, name, age FROM users {
// can access $row.id, $row.name, and $row.age
}
}

Procedures that return tables can also be iterated over:

procedure get_users() public view returns table(id uuid, name text, age int) {
// ...
}

procedure iterate_over_users() public {
for $row in SELECT * FROM get_users() {
// can access $row.id, $row.name, and $row.age
}
}

Breaking From a Loop

To conditionally exit a loop, you can use break:

procedure get_users() public {
$count int := 0;
for $row in SELECT * FROM users {
// do something

// break if too many records have been read
$count := $count + 1;
if $count >= 50 {
break;
}
}
}

Returning Values

Return Single Records

To return a single record, simply use the return key word:

procedure get_numbers() public view returns (num1 int, num2 int) {
$number1 int := 1;
return $number1, 2;
}

To return a single row from a SQL query, you should iterate over the result and return on the first iteration:

procedure get_user($id uuid) public view returns (name text, age int) {
for $row in SELECT name, age FROM users WHERE id = $id {
// this will always return on the first iteration.
return $row.name, $row.age;
}

// if no iterations, then there is no matching user.
error('user not found');
}

Return Table

To return a table from a procedure, you can simply return any SELECT statement:

procedure get_users() public view returns table(id uuid, name text, age int) {
return SELECT id, name, age FROM users;
}

Return Next

If a procedure returns a table and should selectively return rows, you can use return next to return a record inside of a for loop. Unlike return, return next will not exit the procedure, but instead continue to the next iteration of the loop.

In the below example, we query a table prices with columns value and day, and return the net change from each day. We assume that our first day is day 1. If a day is missing, it returns an error.

procedure get_change() public returns table(day int, change int) {
$last_value int := 0;
$last_day int := 0;
for $row in SELECT day, value FROM prices ORDER BY day ASC {
// if we received a day out of order, return error
if $row.day != $last_day + 1 {
error(format('missing day %d', $last_day+1));
}

// add the change to the result set
return next $row.day, $row.value - $last_value;
// update our last seen value
$last_value := $row.value;
}
}