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;
}
}