Skip to content

v0.8 Procedural Language

Brennan Lamey edited this page Mar 22, 2024 · 2 revisions

Overview

In this release, we are upgrading Kuneiform with three goals in mind:

  1. Enhanced logic, control flow, and arithmetic
  2. Easier composability between schemas (not relying on extensions)
  3. Co-location of calculations (to improve efficiency, especially for operations spanning many DBs)

Kuneiform

Procedure

To avoid confusion with actions, Kuneiform will also contains a procedure, similar to actions. We won't make any breaking changes to actions, but we will plan on deprecating them in favor of procedures.

// returns no data
procedure my_proc($id int) public {}

// returns data
procedure my_proc($id int) public returns table($id int) {}

Procedures will not be able to call actions, and actions will not be able to call procedures. I would consider what we are building in procedures as "our second iteration" of actions, now that we know a lot more.

We can make procedures in plpgsql private using:

REVOKE EXECUTE ON FUNCTION my_schema.my_function() FROM PUBLIC;

Imports

To avoid confusion with extension syntax, we will also define a new way to import. The old use syntax will be kept around for importing extensions into actions:

import xdbid as alias;

Users will be able to specify procedures from imports:

database db1; // dbid: xabc

table users public {}

procedure create_user($name text) public {}
database db2;

import xabc as db1;

action do_something($name text) public {
	db1.create_user($name);
}

Extra Types

We will support the following types:

  • BLOB: equivalent of Golang's []byte
  • uint256: for storing token information from smart contracts
  • uuid: for primary keys. users are currently storing them as 36 char strings
  • boolean
  • some sort of fixed point variable (if possible? this could seriously help Truflation, but there are some really hard technical limitations to overcome here)

Composite Types

After deliberation, I have decided that we have no choice but to support composite types in Kwil. Composite types are simply objects with fields that are other types:

type User {
	name text,
	age int
}

type School {
	name text,
	address text,
	students User[]
}

// register_user registers a user with a school
procedure register_user(user User, school name) public {}

// assign constant
procedure other_proc() public {
	$user = User{
		name: text,
	}
}

With first-class object support, we will also need to adopt a coherent object serialization scheme. We can use our current RLP system, however NEAR's Borsch seems to have some merits here as well. We will have to serialize this to a format postgres understands, but we should not expose this to the client as to avoid SQL injection. We will have to perform validation on the incoming data before passing to postgres.

Contextual Variables

Right now, Kwil supports the @caller contextual variable. It will also support:

  • @txid: the Kwil transaction id that called the procedure. If not a transaction, this will be empty.
  • @height: the block height that the transaction exists in. If not a transaction, the node will supply any height it wishes.

Procedural Language

Capturing Selects

The procedural language can capture selects by returning them from a procedure:

procedure my_proc() public returns table(name text, age int) {
	return SELECT name, age FROM users;
}

This can be accessed in other statements, through either loops, selects, or joins:

Loops

procedure proc2() public {
	for $row in my_proc() {
		// can access $row.name, $row.age, etc.
	};
}

Select

Procedures that return type table() can be selected. Should we support this for any procedure? I think we can.

procedure proc2() public {
	select age from my_proc();
}

Join

Procedures that return type table() can be joined against.

procedure proc2() public {
	select * from tbl
	INNER JOIN my_proc() as p
	ON tbl.id = p.id;
}

Return Values

Basic values themselves can be returned. Only one value can be returned

procedure my_proc() public returns int {
	$my_int int := 10;

	return $my_int
}

To select individual values, we can do the following. This will exit on the first iteration, regardless of the amount of rows returned. If no rows are returned, it will raise an error.

procedure my_proc() public returns int {
	for $row in SELECT id FROM users; {
		return $row.id;
	};

	error('no row returned');
}

Composite types can also be assigned and returned:

type User {
	name text,
	age int
}

procedure my_proc() public returns User[] {
	$user1 User := User{
		name: 'satoshi',
		age: '40'
	}

	$user2 User := User{
		name: 'Yaiba',
		age: '420'
	}

	return [$user1, $user2]
}

If/Else

If, else, and elseif will all be supported:

procedure my_proc($id int) public {
	if $id == 1 {
		// smthn
	} else if $id == 2 {
		// smthn2
	} else {
		// smthn3
	};
}

Arrays

All types can also be made arrays. Arrays can be iterated over:

procedure my_proc($stmts text[]) public {
	for $stmt in $stmts {
		// $stmt is type text
	}
}

For Loops

I have already covered for loop usage for procedures returning tables and arrays. For loops can also be done for numbers:

procedure my_proc($end int) public {
	// $end == 5
	for $i in 1:$end {
		// $i == 1,2,3,4,5
	}
}