Skip to content
RKGS
SDK GUIDES
Go

RelationalAI SDK for Go

This guide presents the main features of the RelationalAI SDK for Go, which is used to interact with RelationalAI’s Relational Knowledge Graph System (RKGS).

Go logo

The rai-sdk-go SDK is open source and is available in the GitHub repository:


RelationalAI/rai-sdk-go

It includes self-contained examples (opens in a new tab) of the main API functionality. Contributions and pull requests are welcome.

Note: This guide applies to rai-sdk-go, the latest iteration of the RelationalAI SDK for Go. The relationalai-sdk package is deprecated.

Requirements

You can check the rai-sdk-go (opens in a new tab) repository in pkg.go.dev for the latest version requirements to interact with the RKGS using the RelationalAI SDK for Go.

Installation

To use the RelationalAI SDK for Go, you should include it in your Go code as follows:

import  "github.com/relationalai/rai-sdk-go/rai"

Then simply run go mod tidy to update your go.mod file:

go mod tidy

Configuration

The RelationalAI SDK for Go can access your RAI Server credentials using a configuration file. See SDK Configuration for more details.

The Go rai.NewClientFromConfig() function allows you to specify a configuration to use for connecting to the RAI Server. You can load the configuration within a Go application as follows:

package main
 
import "github.com/relationalai/rai-sdk-go/rai"
 
func main() {
    rai.NewClientFromConfig("default")
}

To load a different configuration, you can replace "default" with a different profile name.

Creating a Client

Most API operations use a client, which contains the necessary settings for making requests against the RelationalAI REST APIs.

You can test your client by listing the databases as follows:

package main
 
import (
	"log"
	"github.com/relationalai/rai-sdk-go/rai"
)
 
func main() {
	// create client with default profile
	client, err := rai.NewClientFromConfig("default")
	if err != nil {
		log.Fatal(err)
	}
 
	// get list of databases
	rsp, err := client.ListDatabases()
	if err != nil {
		log.Fatal(err)
	}
 
	// print them
	rai.Print(rsp, 4)
}

This should print a list with the database info, assuming your keys have the corresponding permissions. See the Listing Databases section below.

The remaining code examples in this document assume that you have a valid client in the client Go variable and that you have imported the necessary packages, such as the ones in the previous examples. Here’s an example:

import (
	"log"
	"github.com/relationalai/rai-sdk-go/rai"
)
...

Additionally, most of the Go API calls return an error together with the expected result if something goes wrong. Therefore, you can typically handle the errors by logging them as above, or in any other way that may be appropriate. This is an example for logging:

rsp, err := client.ListDatabases()
if err != nil {
    log.Fatal(err)
}

Managing Users

A client with the right permissions can create, disable, and list the users under the account.

Creating a User

You can create a user using the CreateUser function of the client. To create a user, you need to specify an email and a set of roles for the user:

email := "user@relational.ai"
roles := []string{ "user" }
 
rsp, err := client.CreateUser(email, roles)
rai.Print(rsp, 4)

Here, email is a string identifying the user, and roles is a list of strings, each one representing a role. The roles currently supported are user and admin. user is the default role if no role is specified.

Deleting a User

You can delete a user through:

rsp, err := client.DeleteUser(id)
rai.Print(rsp, 4)

The argument id is a string representing a given user’s ID.

Disabling and Enabling a User

You can disable a user using the DisableUser function of the client as follows:

rsp, err := client.DisableUser(id)
rai.Print(rsp, 4)

In this case, id is a string representing a given user’s ID, as returned within the response in the user creation example above.

You can reenable the user by using the EnableUser function as follows:

rsp, err = client.EnableUser(id);
rai.Print(rsp, 4);

Listing Users

You can list users using the ListUsers function of the client as follows:

rsp, err := client.ListUsers()
rai.Print(rsp, 4)

Retrieving User Details

You can retrieve user details from the system using the GetUser function of the client as follows:

rsp, err := client.GetUser(id)
rai.Print(rsp, 4)

Here, similarly to DisableUser above, id is a string ID uniquely identifying the user, such as "auth0|XXXXXXXXXXXXXXXXXX".

Finding Users Using Email

You can look up a user’s details by specifying their email through the FindUser function of the client:

rsp, err := client.FindUser("user@relational.ai")
rai.Print(rsp, 4)

Managing OAuth Clients

OAuth clients can be managed with the following functions, provided you have the corresponding permissions:

rsp, err := client.CreateOAuthClient(name, permissions)

Here, name is a string identifying the client. permissions is a list of permissions from the following supported permissions:

  • create:accesskey
  • create:engine
  • create:oauth_client
  • create:user
  • delete:engine
  • delete:database
  • delete:oauth_client
  • list:accesskey
  • list:engine
  • list:database
  • list:oauth_client
  • list:permission
  • list:role
  • list:user
  • read:engine
  • read:credits_usage
  • read:oauth_client
  • read:role
  • read:user
  • rotate:oauth_client_secret
  • run:transaction
  • update:database
  • update:oauth_client
  • update:user

You can get a list of OAuth clients as follows:

rsp, err := client.ListOAuthClients()
rai.Print(rsp, 4)

You can get details for a specific OAuth client, identified by the string id, as follows:

rsp, err := client.GetOAuthClient(id)

This is how to delete the OAuth client identified by the string id:

rsp, err := client.DeleteOAuthClient(id)

Each OAuth client has its own set of permissions, which determine the operations it can execute. Depending on the permissions, some operations, such as listing other users and creating or deleting engines, may fail. See the RAI Console Managing Users guide for more details.

Managing Engines

To query and update RelationalAI databases, you will need a running engine. The following API calls create and manage them.

Creating an Engine

You can create a new engine as follows. Note that the default size is XS:

package main
 
import (
	"log"
	"github.com/relationalai/rai-sdk-go/rai"
)
 
func main() {
	// create client with default profile
	client, err := rai.NewClientFromConfig("default")
	if err != nil {
		log.Fatal(err)
	}
 
    // create engine
	engine := "my-engine"
	size := "XS"
	rsp, err := client.CreateEngine(engine, size)
	if err != nil {
		log.Fatal(err)
	}
 
	rai.Print(rsp, 4)
}

Valid sizes are given as a string and can be one of:

  • XS (extra small).
  • S (small).
  • M (medium).
  • L (large).
  • XL (extra large).

Note: It may take some time before your engine is in the “PROVISIONED” state, where it is ready for queries. It will be in the “PROVISIONING” state before that.

To list all the engines that are currently provisioned, you can use ListEngines():

rsp, err := client.ListEngines()

If you want to list engines that are in a given state, you can use the "state" parameter as follows:

rsp, err := client.ListEngines("state", "DELETED")

Most of the API examples below assume there is a running engine (in the “PROVISIONED” state) in the engine string variable, and a test database in the database string variable:

// replace with your values for testing:
database := "mydatabase";
engine := "myengine";

Deleting an Engine

You can delete an engine with:

rsp := client.DeleteEngine(engine)

Note that since RelationalAI decouples computation from storage, deleting an engine does not delete any cloud databases. See Managing Engines for more details.

Getting Info for an Engine

The details for a specific engine can be retrieved with GetEngine():

rsp, err := client.GetEngine(opts.Engine)
rai.Print(rsp, 4)

Managing Databases

Creating a Database

You can create a database by calling the client’s CreateDatabase() method as follows:

database := "mydatabase";
rsp, err := client.CreateDatabase(database)
rai.Print(rsp, 4)

The result from a successful CreateDatabase() call will look like this:

{
    "id": "xxxx-xxxx-xxx-xxxx-xxx",
    "name": "mydatabase",
    "region": "us-east",
    "account_name": "acount-name",
    "created_by": "xxxx@xyzw",
    "deleted_on": "",
    "state": "CREATED"
}

Cloning a Database

You can use CloneDatabase() to clone a database by specifying source and destination arguments:

sourceDB := "myDB";
targetDB := "cloneDB";
 
rsp, err := client.CloneDatabase(targetDB, sourceDB)
rai.Print(rsp, 4);

With this API call, you can clone a database from myDB to "cloneDB", creating an identical copy. Any subsequent changes to either database will not affect the other. Cloning a database fails if the source database does not exist.

You cannot clone from a database until an engine has executed at least one transaction on that database.

Retrieving Database Details

You can view the details of a database using the GetDatabase() method of a client:

rsp, err := client.GetDatabase(sourceDB)
rai.Print(rsp, 4);

The response is a JSON object. If the database does not exist, there will be an error in err.

Listing Databases

Using ListDatabases() will list the databases available to the account:

rsp, err := client.ListDatabases()
rai.Print(rsp, 4);

A variation of the ListDatabases() method takes a parameter state as input that can be used to filter databases by state. Example states are: “CREATED”, “CREATING”, or “CREATION_FAILED”.

rsp, err := client.ListDatabases("state", "CREATED")
rai.Print(rsp, 4);

Deleting Databases

You can delete a database with DeleteDatabase() if the client has the right permissions.

The database is identified by its name, as used in CreateDatabase():

err := client.DeleteDatabase("my-old-db")

Deleting a database cannot be undone.

Rel Models

Rel models are collections of Rel code that can be added, updated, or deleted from a dedicated database. A running engine — and a database — is required to perform operations on models.

Loading a Model

The LoadModel() method of the client loads a Rel model in a given database. LoadModel() takes as input the database, the engine, a name for the model, and an io.Reader where the model is read from:

func (c *Client) LoadModel(
	database, engine, name string, r io.Reader,
) (*TransactionResult, error)

For example, consider the following simple Rel model, stored in a file named hello.rel:

def R = "hello", "world"

You can then use the following complete example, which loads a model named hello from the hello.rel file into the my-database database using the my-engine engine:

package main
 
import (
	"log"
	"os"
	"github.com/relationalai/rai-sdk-go/rai"
)
 
func main() {
	// create client with default profile
	client, err := rai.NewClientFromConfig("default")
	if err != nil {
		log.Fatal(err)
	}
 
	file := "hello.rel"			// file with model
	name := "hello"				// model name
	database := "my-database"
	engine := "my-engine"
 
	// create reader
	r, err := os.Open(file)
	if err != nil {
		log.Fatal(err)
	}
 
	// load the model
	rsp, err := client.LoadModel(database, engine, name, r)
	if err != nil {
		log.Fatal(err)
	}
 
	rai.Print(rsp, 4);
}

You can also provide the model code through a string, instead of a file, by first creating a Reader:

package main
 
import (
	"log"
	"strings"
	"github.com/relationalai/rai-sdk-go/rai"
)
 
func main() {
 
	// create client with default profile
	client, err := rai.NewClientFromConfig("default")
	if err != nil {
		log.Fatal(err)
	}
 
	model := "def R = \"hello\", \"world\""
	name := "hello"				// model name
	database := "my-database"
	engine := "my-engine"
 
	// create reader from string
	r := strings.NewReader(model)
 
	// load the model
	rsp, err := client.LoadModel(database, engine, name,  r)
	if err != nil {
		log.Fatal(err)
	}
 
	rai.Print(rsp, 4);
}

Loading Multiple Models

In addition to the simple version of providing one model either through a file or a string, you can provide a number of models using a map through the LoadModels() method:

func (c *Client) LoadModels(
	database, engine string, models map[string]io.Reader,
) (*TransactionResult, error)

The map contains the model names as string together with the respective Reader. Here is an example that loads three different models coming from three different files:

package main
 
import (
	"io"
	"log"
	"os"
	"github.com/relationalai/rai-sdk-go/rai"
)
 
func main() {
	// create client with default profile
	client, err := rai.NewClientFromConfig("default")
	if err != nil {
		log.Fatal(err)
	}
 
	// assume the files are model1.rel, model2.rel, etc.
	modelNames := []string{"model1", "model2", "model3"}
	modelMap := make(map[string]io.Reader)
 
	for _, m := range modelNames {
		r, err := os.Open(m + ".rel")
		if err != nil {
			log.Fatal(err)
		}
		modelMap[m] = r
	}
 
	database := "my-database"
	engine := "my-engine"
 
	// load all the models
	rsp, err := client.LoadModels(database, engine, modelMap)
	if err != nil {
		log.Fatal(err)
	}
 
	rai.Print(rsp, 4);
}

Note that if the database already contains an installed model with the same given name, it is replaced by the new source.

Deleting a Model

You can delete a model from a database using the DeleteModel() method and providing the model model_name like this:

rsp, err := client.DeleteModel(database, engine, model_name)

Deleting Multiple Models

You can also delete multiple models at once using the DeleteModels():

rsp, err := client.DeleteModels(database, engine, model_name)

Note that model_name is a string map containing the names of the models to be deleted.

Listing Installed Models

You can list the models in a database using the ListModelNames() method like this:

rsp, err := client.ListModelNames(database, engine)
rai.Print(rsp, 4);

This returns a JSON array of names.

To see the contents of a named model, you can use:

rsp, err := client.GetModel(database, engine, name)
rai.Print(rsp, 4);

name is the name of the model.

You can also list all models together with their code using ListModels():

rsp, err := client.ListModels(database, engine)
rai.Print(rsp, 4);

Querying a Database

The high-level API method for executing queries against the database is Execute(). The function call blocks until the transaction is completed or there are several timeouts indicating that the system may be inaccessible. It specifies a Rel source, which can be empty, and a set of input relations.

The Execute() function is defined as follows:

func (c *Client) Execute(
	database, engine, source string,
	inputs map[string]string,
	readonly bool,
) (*TransactionAsyncResult, error)

Here is an example of Execute() for a read query:

source := "def output[x in {1;2;3}] = x * 2"
 
rsp, err := client.Execute(database, engine, source, nil, true)
 
if err != nil {
    log.Fatal(err)
}

In the code above, rsp contains a TransactionAsyncResult struct that has different fields containing the results and the status details of the transaction that was just executed.

For example, here is how to check the status of the transaction:

fmt.Println(rsp.Transaction.State)  // ABORTED or COMPLETED

Similarly, you can view the results of the transaction as follows:

rai.Print(rsp, 4)

This gives the following output:

{
    "GotCompleteResult": true,
    "Transaction": {
        "id": "12345678-1234-1234-1234-123456789012",
        "state": "COMPLETED"
    },
    "Results": [
        {
            "RelationID": "v1",
            "Table": [
                1,
                2,
                3
            ]
        },
        {
            "RelationID": "v2",
            "Table": [
                2,
                4,
                6
            ]
        }
    ],
    "Metadata": [
        {
            "relationId": "/:output/Int64/Int64",
            "types": [
                ":output",
                "Int64",
                "Int64"
            ]
        }
    ],
    "Problems": []
}

If the transaction was aborted, you would see a description of the problem in the Problems field of the JSON output.

Note that in Execute(), the readonly parameter is false by default. Write queries, which update base relations through the control relations insert and delete, must use readonly=false.

Here is an API call to load some CSV data and store them in the base relation mybaserelation:

data := `name,lastname,id
John,Smith,1
Peter,Jones,2`
 
source := `def config:schema:name="string"
def config:schema:lastname="string"
def config:schema:id="int"
def config:syntax:header_row=1
def config:data = mydata
def delete[:mybaserelation] = mybaserelation
def insert[:mybaserelation] = load_csv[config]`
 
inputs := map[string]string{"mydata" : data}
readonly := false
 
rsp, err := client.Execute(database, engine, source, inputs, readonly)
fmt.Println(rsp.Transaction.State)
rai.Print(rsp, 4)

The RelationalAI SDK for Go also supports asynchronous transactions, through ExecuteAsync() that takes similar parameters to Execute(). In summary, when you issue a query to the database, the return output contains a transaction ID that can subsequently be used to retrieve the actual query results.

ExecuteAsync() is defined similarly to Execute(), but in this case the running process is not blocked. You can obtain the query results whenever they are ready by polling the transaction state:

package main
import (
    "log"
    "time"
    "github.com/relationalai/rai-sdk-go/rai"
)
 
func main() {
    // create client with default profile
    client, err := rai.NewClientFromConfig("default")
    if err != nil {
        log.Fatal(err)
    }
 
    database := "my-database"
    engine := "my-engine"
    source := "def output[x in {1;2;3}] = x * 2"
 
    rsp, err := client.ExecuteAsync(database, engine, source, nil, true)
 
    if err != nil {
        log.Fatal(err)
    }
 
    // get the transaction ID
    id := rsp.Transaction.ID
    for {       // poll until transaction is either completed or aborted
        txn, err := client.GetTransaction(id)
 
        if err != nil {
            log.Fatal(err)
        }
 
        if txn.Transaction.State == "COMPLETED" || txn.Transaction.State == "ABORTED" {
            results, _ := client.GetTransactionResults(id)
            rai.Print(results, 4)
            break
        }
 
        time.Sleep(2 * time.Second)
    }
}
 

Similarly to client.GetTransactionResults(id), you can also get metadata and problems for a given transaction ID:

metadata, _ := client.GetTransactionMetadata(id)
problems, _ := client.GetTransactionProblems(id)

The query size is limited to 64MB. An error will be returned if the request exceeds this API limit.

Getting Multiple Relations Back

In order to return multiple relations, you can define subrelations of output. Here’s an example:

source := `def a = 1;2 def b = 3;4 def output:one = a def output:two = b`
rsp, _ := client.Execute(database, engine, source, nil, true)
 
rai.Print(rsp, 4)

It gives the following output:

{
    "GotCompleteResult": true,
    "Transaction": {
        "id": "12345678-1234-1234-1234-123456789012",
        "state": "COMPLETED"
    },
    "Results": [
        {
            "RelationID": "v1",
            "Table": [
                1,
                2
            ]
        },
        {
            "RelationID": "v1",
            "Table": [
                3,
                4
            ]
        }
    ],
    "Metadata": [
        {
            "relationId": "/:output/:one/Int64",
            "types": [
                ":output",
                ":one",
                "Int64"
            ]
        },
        {
            "relationId": "/:output/:two/Int64",
            "types": [
                ":output",
                ":two",
                "Int64"
            ]
        }
    ],
    "Problems": []
}

Note that each section of metadata corresponds to the respective section of results, i.e., the first metadata section corresponds to the first section of results.

Result Structure

The response is a JSON string with the following fields:

FieldMeaning
RelationIDThis is a key for the relation, for example, "/:output/Int64".
TableThis contains the results of the query in a JSON-array format.

Each query is a complete transaction, executed in the context of the provided database.

The metadata are also a JSON string with the following fields:

FieldMeaning
relationIDThis is a key for the relation, for example, "/:output/:two/Int64". This key describes the keys of the relation together with the type of data.
typesThis is a JSON-array that contains the key names of the relation and their data type.

Finally, problems also contain a JSON string with the following fields:

FieldMeaning
error_codeThe type of error that happened, for example, "PARSE_ERROR".
is_errorWhether an error occurred or there was some other problem.
is_exceptionWhether an exception occurred or there was some other problem.
messageA short description of the problem.
pathA file path for the cases when such a path was used.
reportA long description of the problem.
typeType of the problem, for example, "ClientProblem".

Specifying Inputs

The Execute() and ExecuteAsync() functions take an optional inputs map, which can be used to map relation names to string constants for the duration of the query. Here is an example:

inputs := map[string]string{ "foo" : "bar" }
source := "def output = foo"
 
rsp, _ := client.Execute(database, engine, source, inputs, true)
 
rai.Print(rsp, 4)

This will return the string "bar".

Functions that transform a file and write the results to a base relation can be written like this. The Rel calls load_csv and load_json can be used in this way, via the data parameter, to write results to a base relation. See, for example, the sample code using load_csv in Querying a Database.

Printing Responses

The rai.Print() function prints API responses that come back in JSON format. Here’s an example:

source := "def output = 'a';'b';'c'"
rsp, _ := client.Execute(database, engine, source, nil, true)
 
rai.Print(rsp, 4)

This gives the following output:

{
    "GotCompleteResult": true,
    "Transaction": {
        "id": "12345678-1234-1234-1234-123456789012",
        "state": "COMPLETED"
    },
    "Results": [
        {
            "RelationID": "v1",
            "Table": [
                97,
                98,
                99
            ]
        }
    ],
    "Metadata": [
        {
            "relationId": "/:output/Char",
            "types": [
                ":output",
                "Char"
            ]
        }
    ],
    "Problems": []
}

You can also view the results only, instead of all the information of the transaction, as follows:

source := "def output = 'a';'b';'c'"
rsp, _ := client.Execute(database, engine, source, nil, true)
 
id := rsp.Transaction.ID
results, _ := client.GetTransactionResults(id)
 
rai.Print(results, 4)

This gives the following output:

[
    {
        "RelationID": "v1",
        "Table": [
            97,
            98,
            99
        ]
    }
]

In case there are problems, you can view the problems as follows:

source := "def output = aaa"     // aaa is undefined
rsp, _ := client.Execute(database, engine, source, nil, true)
 
id := rsp.Transaction.ID
problems, _ := client.GetTransactionProblems(id)
 
rai.Print(problems, 4)

This gives the following output:

[
    {
        "error_code": "UNDEFINED",
        "is_error": true,
        "is_exception": false,
        "message": "`aaa` is undefined.",
        "path": "",
        "report": "1| def output = aaa\n                ^^^\n\nThe problem occurred while compiling declaration `output`:\n1| def output = aaa\n   ^^^^^^^^^^^^^^^^\n\n",
        "type": "ClientProblem"
    }
]

Loading Data: LoadCSV and LoadJSON

As a convenience, the Go API includes LoadCSV and LoadJSON functions. These are not strictly necessary, since the load utilities in Rel itself can be used in a non-read-only Execute() or ExecuteAsync() query that uses the inputs parameter. See, for example, the sample code using the Rel load_csv in Querying a Database.

The Go function LoadCSV() loads data and inserts the result into the base relation named by the relation argument. Additionally, LoadCSV() attempts to guess the schema of the data. For more control over the schema, use a non-read-only Execute() or ExecuteAsync() query using the inputs option.

func (c *Client) LoadCSV(
	database, engine, relation string, r io.Reader, opts *CSVOptions,
) (*TransactionResult, error)

The CSVOptions (opens in a new tab) class allows you to specify how to parse a given CSV file. Through a CSVOptions object, you can specify, for example, the delimiter and the escape character of a given file.

Similarly to LoadCSV(), LoadJson() loads the data string as JSON and inserts it into the base relation named by the relation argument:

func (c *Client) LoadJSON(
	database, engine, relation string, r io.Reader,
) (*TransactionResult, error)

Here is an example:

data := "{\"name\": \"John\", \"age\": 20}"
r := strings.NewReader(data)
 
rsp, _ := client.LoadJSON(database, engine, "myjson", r)
 
rai.Print(rsp, 4)

Note: In both cases, the relation base relation is not cleared, allowing for multipart, incremental loads. To clear it, you can do:

def delete[:relation] = relation

Listing Base Relations

You can list base relations as follows:

rsp, _ := client.ListEDBs(database, engine)
rai.Print(rsp, 4)

This will list the base relations in the given database. The result is a JSON list of objects.

Transaction Cancellation

You can cancel an ongoing transaction by calling the following function:

rsp, _ := client.CancelTransaction(opts.ID)
rai.Print(rsp, 4)

The argument opts.ID is a string that represents the transaction ID. An example is rsp.Transaction.ID from a previous client.ExecuteAsync() API call.

Was this doc helpful?