RelationalAI SDK for Java

This guide presents the API for the RelationalAI SDK for Java.

This guide presents the main features of the RelationalAI SDK for Java, used to interact with the Relational Knowledge Graph Management System (RKGMS).

The rai-sdk-java package is open source and is available in the Github repository

RelationalAI/rai-sdk-java.

It includes self-contained examples of the main API functionality. Contributions and pull requests are welcome.

Note: This guide applies to rai-sdk-java, the latest iteration of the Java SDK. The relationalai-sdk package is deprecated.

The last section describes how the different Console notebook cell types can be implemented with the Java SDK API.

Requirements

Check the rai-sdk-java repository for the latest version requirements to interact with the RKGMS using the RelationalAI SDK for Java. Apache Maven version 3.8.5 or higher is also required for building the SDK.

Installation

The SDK has a single runtime dependency (jsoniter), a dependency for running the SDK examples (commons-cli), and several additional dependencies for running the unit and integration tests.

To build the SDK you need to first clone it from the git repository:

git clone https://github.com/RelationalAI/rai-sdk-java

Next, you can build it using Maven. The SDK’s build lifecycle is managed through the standard mvn lifecycle commands. The following commands are issued within the rai-sdk-java directory:

cd rai-sdk-java

To compile the SDK:

mvn compile

To run the various SDK tests:

mvn test

Note that the tests are run against the account configured in your SDK config file, as shown in the next section.

If you’d like to compile, package, run the tests, and then install the SDK:

mvn install

Note that mvn install is required to build and run the examples. You can also specify the -Dskiptests parameter in nvm install if you would like to compile, package, and install without running any tests.

You can clean up your source tree using:

mvn clean

Using the Java SDK as a Maven Dependency

In order to use the rai-sdk-java SDK, you need to add a dependency to your project’s POM file:

<dependency>
    <groupId>com.relationalai</groupId>
    <artifactId>rai-sdk</artifactId>
    <version>0.0.1</version>
</dependency>

You also need to point Maven to the SDK’s GitHub packages repository, also in the project’s POM file:

<repositories>
    <repository>
        <id>github</id>
        <name>The RelationalAI SDK for Apache Maven</name>
        <url>https://maven.pkg.github.com/RelationalAI/rai-sdk-java</url>
    </repository>
</repositories>

The registry access is generally available through the GitHub API, which is user-password protected. As a result, you need to add your GitHub credentials to $HOME/.m2/settings.xml:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
        ...
        <servers>
            <server>
                <id>github</id>
                <username>GITHUB_USERNAME</username>
                <password>GITHUB_ACCESS_TOKEN</password>
            </server>
        </servers>
        ...
    </settings>

In this case, GITHUB_USERNAME is your GitHub login name. Similarly, GITHUB_ACCESS_TOKEN is a GitHub-generated access token specifically assigned to you. You can generate a new token from GitHub by going to GitHub > Settings > Developer Settings > Personal access tokens > Generate new token. Your new token needs to have at least the read:packages scope.

Configuration

To connect to the RAI Server using the Java SDK, you need a configuration file. See the SDK Configuration guide for more details.

The Java Config class represents a configuration to connect to the RAI Server. You can load the configuration with a Java application as follows:

import java.io.IOException;
import com.relationalai.Config;

public class MyJava {
    public static void main(String[] args) {
        try {
            var cfg = Config.loadConfig("~/.rai/config", "default");
        }
        catch (IOException e) {
            System.out.println("Error occured.");
        }
    }
}

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

Creating a Client

Most API operations use a Client object that contains the necessary settings for making requests against the RelationalAI REST APIs. To create a client using the default profile in your ~/.rai/config file, use:

import java.io.IOException;
import com.relationalai.Client;
import com.relationalai.Config;

public class MyJava {
    public static void main(String[] args) {
        try {
            var cfg = Config.loadConfig("~/.rai/config", "default");
            var client = new Client(cfg);
        }
        catch (IOException e) {
            System.out.println("Error occured.");
        }
    }
}

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

import java.io.IOException;
import com.relationalai.Client;
import com.relationalai.Config;
import com.relationalai.Json;

public class MyJava {
    public static void main(String[] args) {
        try {
            var cfg = Config.loadConfig("~/.rai/config", "default");
            var client = new Client(cfg);
            var rsp = client.listDatabases();
            Json.print(rsp, 4);
        }
        catch (Exception e) {
            System.out.println("Error occured.");
        }
    }
}

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

The remaining code examples in this document assume that you have a valid client in the client Java variable and that you have imported the necessary com.relationalai.* packages, such as the ones in the previous examples. For example:

import com.relationalai.Client;
import com.relationalai.Config;
import com.relationalai.Json;
...

Additionally, most of the Java API calls throw an HTTPError, InterruptedException or IOException exception when there is an issue. Therefore, you can typically wrap the API calls discussed here in a try ... catch block similar to the ones above. For example:

try {
    ...
    var rsp = client.listDatabases();
}
catch (HTTPError e) {
    // handle error and/or rethrow
}

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 member function of a Client object:

var user = client.createUser(String email);

or

var user = client.createUser​(String email, String[] roles);

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.

Disabling a User

You can disable a user through a Client object as follows:

client.disableUser(String id);

In this case, id is a string reflecting a given user’s ID, as stored within a User object:

var user = client.createUser(String email);
String id = user.id;

client.disableUser(id);

Listing Users

You can list users through a Client object as follows:

var rsp = client.listUsers();
Json.print(rsp, 4);

Retrieving User Details

You can retrieve user details from the system as follows:

var rsp = client.getUser(String id);

Here, similar to disableUser above, id is a string ID uniquely identifying the user, for example, "auth0|XXXXXXXXXXXXXXXXXX".

Managing OAuth Clients

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

var oa = client.createOAuthClient(String name);

or

var oa = client.createOAuthClient(String name, String[] permissions);

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

  • create:accesskey
  • create:compute
  • create:oauth_client
  • create:user
  • delete:compute
  • delete:database
  • delete:oauth_client
  • list:accesskey
  • list:compute
  • list:database
  • list:oauth_client
  • list:permission
  • list:role
  • list:user
  • read:compute
  • 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:

var oacList = client.listOAuthClients();
Json.print(oacList, 4);

Get details for a specific OAuth client, identified by the string id:

var rsp = client.getOAuthClient(String id);

Delete the OAuth client identified by the string id:

var rsp = client.deleteOAuthClient(id);

Managing Engines

To query and update RAICloud 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:

String engine = "java_sdk_engine"

var rsp = client.createEngine(engine);
Json.print(rsp, 4);

Or, you can specify the size:

String engine = "java_sdk_engine";
String size = "XS";

var rsp = client.createEngine(engine, size);
Json.print(rsp, 4);

API requests return a JSON value. Here is a sample result for createEngine():

{'engine': {
    'account_name': '#########',
    'created_by': '#########',
    'id': '#########',
    'name': 'java_sdk_engine',
    'region': '#########',
    'requested_on': '2022-03-22T18:11:23.121Z',
    'size': 'XS',
    'state': 'REQUESTED'
    }
}

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():

var rsp = client.listEngines();
Json.print(rsp, 4);

If there is an error with the request, an HTTPError exception will be thrown.

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 by your values for testing:
String database = "mydatabase";
String engine = "myengine";

Deleting an Engine

An engine can be deleted with:

var rsp = client.deleteEngine(engine);
Json.print(rsp, 4);

If successful, this will return:

{'status':
    {'name': XXXX
    'state': 'DELETING',
    'message': 'engine XXXX deleted successfully'}
}

Note that since RAICloud 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 compute engine can be retrieved with getEngine():

var rsp = client.getEngine(engine);
Json.print(rsp, 4);

An HTTPError exception will be thrown if the engine specified in getEngine() does not exist.

Managing Databases

Creating a Database

You can create a database by calling a client object’s createDatabase() method, as follows:

String database = "mydatabase";
String engine = "myengine";

// Create a database
var rsp = client.createDatabase(database, engine);
Json.print(rsp, 4);

The result from a successful create_database call will look like this:

{
  "output": [],
  "version": 2,
  "problems": [],
  "actions": [],
  "debug_level": 0,
  "aborted": false,
  "type": "TransactionResult"
}

Cloning a Database

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

String sourceDB = "myDB";
String targetDB = "cloneDB";

// Clone a database
var rsp = client.cloneDatabase(targetDB, engine, sourceDB);
Json.print(rsp, 4);

With this API call, you can clone a database from mydatabase to "mydatabase-clone", creating an identical copy. Any subsequent changes to either database will not affect the other. Cloning a database only succeeds if the source database already exists.

Retrieving Database Details

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

var rsp = client.getDatabase(database);
Json.print(rsp, 4);

The response is a JSON object. If the database does not exist, an HTTPError exception is thrown.

Listing Databases

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

var rsp = client.listDatabases();
Json.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”.

var rsp = client.listDatabases("CREATED");
Json.print(rsp, 4);

Deleting Databases

A database can be deleted with deleteDatabase() if the client has the right permissions.

The database is identified by its name, as used in createDatabase().

var rsp = client.deleteDatabase(database);
Json.print(rsp, 4);

If successful, the response will be of the form:

{'name': 'XXXXXXX', 'message': 'deleted successfully'}

Rel Model Sources

Rel model sources are collections of Rel code that can be added, updated, or deleted from a particular database. Since they update a database, a running engine is required to perform operations on sources.

Installing Rel Model Sources

The loadModel() method of a client object installs a Rel model source in a given database. The loadModel() method is overloaded in two different variations:

loadModel​(String database, String engine, String name, String model);
loadModel​(String database, String engine, String name, InputStream model);

These methods are equivalent, with the small difference that the model is coming from an InputStream or provided as a String.

Here is an example where the model is provided as a String:

String sourceString = "def R = \"hello\", \"world\"";
var rsp = client.loadModel(database, engine, "mysource", sourceString);
Json.print(rsp, 4);

Similarly, if we wanted to read the model from a file and load it in the database:

String filename = "hello.rel";
var name = "mysource";
var input = new FileInputStream(filename);
var rsp = client.loadModel(database, engine, name, input);
Json.print(rsp, 4);

In addition to the simple versions of providing the model either through a String or an InputStream, we can also provide a Map with a collection of models, together with their names. In this case we need to use the loadModels() method:

loadModels​(String database, String engine, Map<String,​String> models);

Here is an example that loads multiple models at once:

HashMap<String, String> myModels  = new HashMap<String, String>() {{
    put("source1", "def R = {1 ; \"hello\"}");
    put("source2", "def P = {2 ; \"world\"}");
}};
var rsp = client.loadModels(database, engine, myModels);
Json.print(rsp, 4);

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

Deleting a Rel Model Source

You can delete a source from a database using the deleteModel() method:

var rsp = client.deleteModel(database, engine, "mysource");
Json.print(rsp, 4);

Listing Installed Rel Model Sources

You can list the sources in a database using the following code:

var rsp = client.listModelNames(database, engine);
Json.print(rsp, 4);

This returns a JSON array of names.

To see the contents of a named source, use:

String sourcename = "mysource";
var rsp = client.getModel(database, engine, sourcename);
Json.print(rsp, 4);

where sourcename is the name of the model.

You can also list all models and their sources using listModels():

var rsp = client.listModels(database, engine);
Json.print(rsp, 4);

Querying a Database

The high-level API method for running a single query/transaction against the database is execute(). It specifies a Rel source – which can be empty – and a set of input relations.

There are three overloaded versions of execute():

execute​(String database, String engine, String source);
execute​(String database, String engine, String source, boolean readonly);
execute​(String database, String engine, String source,
            boolean readonly, Map<String,​String> inputs);

Queries meant to update EDB relations (with insert and delete) must use readonly=false. By convention, only the output relation is returned, as in Console notebooks, and the default value for readonly is false in the first method.

Here is an example of execute():

var rsp = client.execute(
    database,
    engine,
    "def output[x in {1;2;3}] = x * 2"
);
Json.print(rsp, 4);

This gives the output:

{
    "aborted": false,
    "output": [
        {
            "rel_key": {
                "name": "output",
                "keys": [
                    "Int64"
                ],
                "values": [
                    "Int64"
                ]
            },
            "columns": [
                [
                    1,
                    2,
                    3
                ],
                [
                    2,
                    4,
                    6
                ]
            ]
        }
    ],
    "problems": []
}

As another example, here is an API call to load some CSV data and store it in the EDB relation myedb:

String data = new StringBuilder()
    .append("name,lastname,id\n")
    .append("John,Smith,1\n")
    .append("Peter,Jones,2\n")
    .toString();

String source =     new StringBuilder()
    .append("def config:schema:name=\"string\"\n")
    .append("def config:schema:lastname=\"string\"\n")
    .append("def config:schema:id=\"int\"\n")
    .append("def config:syntax:header_row=1\n")
    .append("def config:data = mydata\n")
    .append("def delete[:myedb] = myedb\n")
    .append("def insert[:myedb] = load_csv[config]\n")
    .toString();

HashMap<String, String> inputs = new HashMap<String, String>() {{
    put("mydata", data);
}};

var rsp = client.execute(
    database,
    engine,
    source,
    false,
    inputs
);

Json.print(rsp, 4);

Getting Multiple Relations Back

As in the RAI Console, if you want to return multiple relations, you can define sub-relations of output. For example:

var rsp = client.execute(
    database,
    engine,
    "def a = 1;2 def b = 3;4 def output:one = a def output:two = b"
);
Json.print(rsp, 4);

Giving the output:

{
    "aborted": false,
    "output": [
        {
            "rel_key": {
                "name": "output",
                "keys": [
                    ":one",
                    "Int64"
                ],
                "values": []
            },
            "columns": [
                [
                    1,
                    2
                ]
            ]
        },
        {
            "rel_key": {
                "name": "output",
                "keys": [
                    ":two",
                    "Int64"
                ],
                "values": []
            },
            "columns": [
                [
                    3,
                    4
                ]
            ]
        }
    ],
    "problems": []
}

Result Structure

The response is a JSON String with one or more of the following fields:

FieldMeaning
outputA JSON array, where each position corresponds to an output relation — in this case, only one, output. Future releases of the SDK may support multiple relations here.
problemsInformation about any existing problems in the DB. Note that these problems are not necessarily caused by the query.
abortedIndicator of whether an abort resulted. For example, if an integrity constraint was violated.

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

Specifying Inputs

The execute() member function takes an optional inputs Map that can be used to map relation names to string constants for the duration of the query, analogous to the file upload feature of Rel Console notebooks. Thus, for example,

var rsp = client.execute(
    database,
    engine,
    "def output = foo",
    false,
    new HashMap<String, String>() {{ put("foo", "asdf"); }}
);
Json.print(rsp, 4);

will return the string "asdf" back.

Functions that transform a file and write the results to an EDB relation can easily 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 an EDB relation. See, for example, the sample code using load_csv in Querying a Database.

Printing Responses

The Json.print() member function prints API responses. For example:

var rsp = client.execute(database, engine, "def output = 'a';'b';'c'");
Json.print(rsp, 4);

gives the output:

{
    "aborted": false,
    "output": [
        {
            "rel_key": {
                "name": "output",
                "keys": [
                    "Char"
                ],
                "values": []
            },
            "columns": [
                [
                    "a",
                    "b",
                    "c"
                ]
            ]
        }
    ],
    "problems": []
}

Loading Data: load_csv and load_json

As a convenience, the Java API includes a loadCSV and loadJSON member function in the Client class. These are not strictly necessary, since the load utilities in Rel itself can be used in a non-read-only execute() query that uses the inputs parameter. See, for example, the sample code using load_csv in Querying a Database.

The Java function loadCSV() loads data and inserts the result into the EDB 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() query using the inputs option.

loadCsv​(String database, String engine, String relation, InputStream data);
loadCsv​(String database, String engine, String relation,
        InputStream data, CsvOptions options);
loadCsv​(String database, String engine, String relation,
        String data);
loadCsv​(String database, String engine, String relation,
        String data, CsvOptions options);

The CSVOptions 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 EDB named by the relation argument:

loadJson​(String database, String engine, String relation, InputStream data)
loadJson​(String database, String engine, String relation, String data)

Example:

var rsp = client.loadJson(database, engine, "myjson",  "{\"a\" : \"b\"}");
Json.print(rsp, 4);

Note: In both cases, the relation EDB is not cleared, allowing for multi-part, incremental loads. To clear it, issue a non-read-only query of the form:

def delete[:relation] = relation

Listing EDB Relations

You can list EDB relations as follows:

var rsp = client.listEdbs(database, engine);
Json.print(rsp, 4);

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

Notebook Cell Types

As explained in Working with RAI Notebooks there are four notebook cell types: Query, Install, Update, and Markdown. This section describes how the three cell types Query, Install, and Update map to Java SDK API calls.

Query Cells

The Query cell in a Rel Console notebook corresponds to a simple execute() function call, as shown below:

var rsp = client.execute(
    database,
    engine,
    "def output = {(1,); (2,); (3,)}"
);
Json.print(rsp, 4);

Giving the output:

{
    "aborted": false,
    "output": [
        {
            "rel_key": {
                "name": "output",
                "keys": [
                    "Int64"
                ],
                "values": []
            },
            "columns": [
                [
                    1,
                    2,
                    3
                ]
            ]
        }
    ],
    "problems": []
}

As you can see from the empty problems array, the transaction did not produce any problems.

Note that no rules/definitions are persisted in this case.

Install Cells

To persist IDB definitions, use the loadModel() API function of a Client object. That is, the Install cell in a Rel Console notebook corresponds to the loadModel() Java function call, as shown below:

var rsp = client.loadModel(
    database,
    engine,
    "arity1_k_def",
    "def k = {2;3;50}"
);
Json.print(rsp, 4);

Check that the above relation was persisted using the exec API call:

var rsp = client.execute(database, engine, "def output = k");
Json.print(rsp, 4);

Output:

{
    "aborted": false,
    "output": [
        {
            "rel_key": {
                "name": "output",
                "keys": [
                    "Int64"
                ],
                "values": []
            },
            "columns": [
                [
                    2,
                    3,
                    50
                ]
            ]
        }
    ],
    "problems": []
}

Update Cells

The Update cell in a Rel Console notebook corresponds to an execute() function call with the parameter readonly=false. Note that this is the default value for this flag. For example:

var rsp = client.execute(
    database,
    engine,
    "def insert:employee = {(1, \"Han Solo\"); (2, \"Bart Simpson\")}"
);
Json.print(rsp, 4);

You can modify the employee EDB:

var rsp = client.execute(
    database,
    engine,
    "def insert:employee = {(3, \"King\"); (4, \"Queen\")}",
    false
);
Json.print(rsp, 4);

You can now query the modified employee EDB:

var rsp = client.execute(
    database,
    engine,
    "def output=employee"
);
Json.print(rsp, 4);
{
    "aborted": false,
    "output": [
        {
            "rel_key": {
                "name": "output",
                "keys": [
                    "Int64",
                    "String"
                ],
                "values": []
            },
            "columns": [
                [
                    1,
                    2,
                    3,
                    4
                ],
                [
                    "Han Solo",
                    "Bart Simpson",
                    "King",
                    "Queen"
                ]
            ]
        }
    ],
    "problems": []
}

To update EDB relations, always use the readonly=false parameter in the execute() call.