RelationalAI SDK for Java
This guide presents the main features of the RelationalAI SDK for Java, which is used to interact with RelationalAI’s Relational Knowledge Graph System (RKGS).
The rai-sdk-java
package is open source and is available in the GitHub repository:
RelationalAI/rai-sdk-java
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-java
, the latest iteration of the RelationalAI SDK for Java.
The relationalai-sdk
package is deprecated.
Requirements
You can check the rai-sdk-java (opens in a new tab) repository for the latest version requirements to interact with the RKGS using the RelationalAI SDK for Java.
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 first need to clone it from the GitHub repository:
git clone https://github.com/RelationalAI/rai-sdk-java
Next, you can build it using Maven. The SDK’s build life cycle is managed through the standard mvn
life cycle commands.
The following commands are issued within the rai-sdk-java
directory:
cd rai-sdk-java
This is how to compile the SDK:
mvn compile
This is how 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.
You can compile, package, run the tests, and then install the SDK like this:
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 want to compile, package, and install without running any tests.
You can clean up your source tree using:
mvn clean
Using the RelationalAI SDK for Java as a Maven Dependency
In order to use rai-sdk-java
, you need to add a dependency to your project’s POM file:
<dependency>
<groupId>com.relationalai</groupId>
<artifactId>rai-sdk</artifactId>
<version>0.4.0-alpha</version>
</dependency>
You also need to point Maven to the SDK’s GitHub packages repository, found 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
The RelationalAI SDK for Java can access your RAI Server credentials using a configuration file. See SDK Configuration 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 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, you can 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
This section covers the API functions that you need to manage users.
Each user has a role associated with specific permissions. These permissions determine the operations that the user can execute. See User Roles in the Managing Users and OAuth Clients guide for more details.
Creating a User
You can create a user using the createUser (opens in a new tab) member function of a Client (opens in a new tab) object:
var user = client.createUser(email, 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 and Enabling a User
You can disable a user through a Client
(opens in a new tab) object as follows:
client.disableUser(id);
In this case, id
is a string representing a given user’s ID, as stored within a user (opens in a new tab) object:
var user = client.createUser(email, roles);
String id = user.id;
client.disableUser(id);
Again, email
is a string and roles
is a list of strings, as per client.createUser()
.
You can reenable the user as follows:
var rsp = client.enableUser(id);
Json.print(rsp, 4);
Listing Users
You can list users as follows:
var rsp = client.listUsers();
Json.print(rsp, 4);
Getting Information for a User
You can get information for a user as follows:
var rsp = client.getUser(String id);
Again, id
is a string ID uniquely identifying the user, for example, "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:
var rsp = client.findUser("user@relational.ai")
Json.print(rsp, 4);
Deleting a User
You can delete a user through:
client.deleteUser(id);
The argument id
is a string representing a given user’s ID.
Managing OAuth Clients
This section covers the API functions that you need to manage OAuth clients.
Each OAuth client has a specific set of permissions. These permissions determine the operations that the OAuth client can execute. See [Permissions for OAuth Clients](/rkgms/console/user-management(#permissions-for-oauth-clients) in the Managing Users and OAuth Clients guide for more details.
Creating an OAuth Client
You can create an OAuth client as follows:
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: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
Listing OAuth Clients
You can get a list of OAuth clients as follows:
var oacList = client.listOAuthClients();
Json.print(oacList, 4);
Getting Information for an OAuth Client
You can get details for a specific OAuth client, identified by the string id
, as follows:
var rsp = client.getOAuthClient(String id);
You can also find the OAuth client identified by the string id
as follows:
var rsp = client.findOAuthClient(id);
Deleting an OAuth Client
You can delete an OAuth client identified by the string id
as follows:
var rsp = client.deleteOAuthClient(id);
Managing Engines
This section discusses the API methods to manage engines.
Creating an Engine
You can create a new engine as follows:
String engine = "my_engine";
var rsp = client.createEngine(engine);
Json.print(rsp, 4);
By default, the engine size is XS
.
You can create an engine of a different size by specifying the size
parameter:
String engine = "my_engine";
String size = "S";
var rsp = client.createEngine(engine, size);
Valid sizes are given as a string and can be one of:
XS
(extra small).S
(small).M
(medium).L
(large).XL
(extra large).
Your engine may take some time to reach the “PROVISIONED” state, where it is ready for queries. It is in the “PROVISIONING” state until then.
Listing Engines
You can list all engines associated with your account as follows:
var rsp = client.listEngines();
Json.print(rsp, 4);
This returns a JSON array containing details about each engine:
[
{
"id": "******",
"name": "my_engine",
"region": "us-east",
"account_name": "******",
"created_by": "******",
"created_on": "2023-07-10T17:15:22.000Z",
"size": "S",
"state": "PROVISIONED"
}
]
To list engines that are in a given state, you can use the state
parameter:
String state = "PROVISIONED";
var rsp = client.listEngines(state);
Json.print(rsp, 4);
Possible states are:
"REQUESTED"
."PROVISIONING"
."PROVISIONED"
."DEPROVISIONING"
.
If there is an error with the request, an HTTPError
exception is thrown.
Getting Information for an Engine
You can get information for a specific engine as follows:
String engine = "my_engine";
var rsp = client.getEngine(engine);
Json.print(rsp, 4);
It gives this output:
{
"id": "******",
"name": "my_engine",
"region": "us-east",
"account_name": "******",
"created_by": "******",
"created_on": "2023-07-10T17:15:22.000Z",
"size": "S",
"state": "PROVISIONED"
}
An HTTPError
exception will be thrown if the engine specified in getEngine()
does not exist.
Deleting an Engine
You can delete an engine with:
String engine = "my_engine";
var rsp = client.deleteEngine(engine);
Json.print(rsp, 4);
If successful, this is the output:
{
"id": "******",
"name": "my_engine",
"region": "us-east",
"account_name": "******",
"created_by": "******",
"created_on": "2023-07-10T17:15:22.000Z",
"size": "S",
"state": "DEPROVISIONING"
}
RelationalAI decouples computation from storage. Therefore, deleting an engine does not delete any cloud databases. See Managing Engines for more details.
Managing Databases
This section covers the API methods you need to use to manage databases.
Creating a Database
You can create a database as follows:
String database = "my_database";
String engine = "my_engine";
var rsp = client.createDatabase(database, engine);
Json.print(rsp, 4);
The result from a successful createDatabase
call looks like this:
{
"id": "6a******",
"name": "my_database",
"region": "us-east",
"account_name": "******",
"created_by": "******",
"created_on": "2023-07-10T17:15:22.000Z",
"state": "CREATED"
}
Cloning a Database
You can clone a database by specifying the target and source databases:
String engine = "my_engine";
String sourceDB = "my_database";
String targetDB = "my_clone_database";
var rsp = client.cloneDatabase(targetDB, engine, sourceDB);
Json.print(rsp, 4);
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.
Listing Databases
You can list the available databases associated with your account as follows:
var rsp = client.listDatabases();
Json.print(rsp, 4);
This returns a JSON array containing details for each database:
[
{
"id": "******",
"name": "my_database",
"region": "us-east",
"account_name": "******",
"created_by": "******",
"created_on": "2023-07-20T08:03:03.616Z",
"state": "CREATED"
}
]
To filter databases by state, you can use the state
parameter.
For instance:
var rsp = client.listDatabases("CREATED");
Json.print(rsp, 4);
Possible states are:
"CREATED"
."CREATING"
."CREATION_FAILED"
."DELETED"
.
Getting Information for a Database
You can get information for a specific database as follows:
String database = "my_database";
var rsp = client.getDatabase(database);
Json.print(rsp, 4);
This gives you the following output:
{
"id": "******",
"name": "my_database",
"region": "us-east",
"account_name": "******",
"created_by": "******",
"created_on": "2023-07-20T08:03:03.616Z",
"state": "CREATED"
}
If the database does not exist, an HTTPError
exception is thrown.
Deleting a Database
You can delete a database as follows:
String database = "my_database";
var rsp = client.deleteDatabase(database);
Json.print(rsp, 4);
If successful, the response will be of the form:
{"name": "my_database", "message": "deleted successfully"}
Deleting a database cannot be undone.
The remaining code examples in this guide assume that you have a running engine in engine
and a database in database
.
Managing Rel Models
This section covers the API methods you can use to manage 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()
function loads a Rel model in a given database.
It 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 functions are equivalent, with the small difference that the model is coming from an InputStream
or provided as a String
.
Here’s an example where the model "my_model"
is provided as a String
:
String modelString = "def R = \"hello\", \"world\"";
var rsp = client.loadModel(database, engine, "my_model", modelString);
Json.print(rsp, 4);
Similarly, if you want to read the model from a file and load it in the database:
String filename = "hello.rel";
var name = "my_model";
var input = new FileInputStream(filename);
var rsp = client.loadModel(database, engine, name, input);
Loading Multiple Models
In addition to the simple versions of providing the model either through a String
or an InputStream
, you can also provide a Map
with a collection of models, together with their names.
In this case you need to use the loadModels()
method:
loadModels(String database, String engine, Map<String,String> models);
Here’s an example that loads multiple models at once:
HashMap<String, String> my_models = new HashMap<String, String>() {{
put("model1", "def R = {1 ; \"hello\"}");
put("model2", "def P = {2 ; \"world\"}");
}};
var rsp = client.loadModels(database, engine, my_models);
If the database already contains an installed model with the same name as the newly installed model, then the new model replaces the existing one.
Listing Models
You can list the models in a database as follows:
var rsp = client.listModelNames(database, engine);
Json.print(rsp, 4);
This returns a JSON array of names:
[
"rel/alglib",
"rel/display",
"rel/graph-basics",
"rel/graph-centrality",
"rel/graph-components",
"rel/graph-degree",
"rel/graph-measures",
"rel/graph-paths",
"rel/histogram",
"rel/intrinsics",
"rel/mathopt",
"rel/mirror",
"rel/net",
"rel/stdlib",
"rel/vega",
"rel/vegalite"
]
In the example above, you can see all the built-in models associated with a database.
You can also list all the models and their code using listModels()
:
var rsp = client.listModels(database, engine);
Json.print(rsp, 4);
Getting Information for a Model
To see the contents of a given model, you can use:
String modelname = "my_model";
var rsp = client.getModel(database, engine, modelname);
Json.print(rsp, 4);
Here, modelname
is the name of the model.
This gives the following output:
{
"name": "my_model",
"value": "def my_range(x) = range(1, 10, 1, x)"
}
In the example above, my_model
defines a specific range.
Deleting Models
You can delete installed models from a database as follows:
var rsp = client.deleteModel(database, engine, model_name);
Json.print(rsp, 4);
Note that model_name
is a string vector containing the names of the model or models to be deleted.
Querying a Database
The API function for executing queries against the database is execute()
.
It is a synchronous function, meaning that the running code is blocked until the transaction is completed or there are several timeouts indicating that the system may be inaccessible.
Each query is a complete transaction, executed in the context of the provided database.
The execute()
function specifies a Rel query, which can be empty, and a set of input relations.
There are two overloaded versions of it:
execute(String database, String engine, String query, boolean readonly);
execute(String database, String engine, String query, boolean readonly, Map<String,String> inputs);
Here’s an example of a read query using execute()
:
var rsp = client.execute(
database,
engine,
"def output = {1;2;3}",
true
);
System.out.println(rsp);
This gives the output:
[{"relationId":"/:output/Int64","table":[1,2,3]}]
Write queries, which update base relations through the control relations insert
and delete
,
must use readonly=false
.
Here’s an API call to load some CSV data and store them in the base relation my_base_relation
:
String data = new StringBuilder()
.append("name,lastname,id\n")
.append("John,Smith,1\n")
.append("Peter,Jones,2\n")
.toString();
String query = 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[:my_base_relation] = my_baserelation\n")
.append("def insert[:my_base_relation] = load_csv[config]\n")
.toString();
HashMap<String, String> inputs = new HashMap<String, String>() {{
put("mydata", data);
}};
var rsp = client.execute(
database,
engine,
query,
false,
inputs
);
System.out.println(rsp);
The RelationalAI SDK for Java 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:
var rsp = client.executeAsync(
database,
engine,
"def output = {1;2;3}",
true
);
Then, you can poll the transaction until it has completed or aborted. Finally, you can fetch the results:
var id = rsp.transaction.id;
var transaction = client.getTransaction(id).transaction;
if ( "COMPLETED".equals(transaction.state) || "ABORTED".equals(transaction.state) ) {
var results = client.getTransactionResults(id);
System.out.println(results);
}
Similarly to client.getTransactionResults()
, you can also get metadata and problems for a given transaction ID:
var metadata = client.getTransactionMetadata(id);
var problems = client.getTransactionProblems(id);
The query size is limited to 64MB. An HTTPError
exception will be thrown if the request exceeds this API limit.
Getting Multiple Relations Back
In order to return multiple relations, you can define subrelations 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",
true
);
var id = rsp.transaction.id;
var results = client.getTransactionResults(id);
System.out.println(results);
This gives the following output:
{"relationId":"/:output/:one/Int64","table":[1,2]},
{"relationId":"/:output/:two/Int64","table":[3,4]}
Result Structure
The response contains the following keys:
Field | Meaning |
---|---|
Transaction | Information about the transaction status, including the identifier. |
Metadata | Metadata information about the results key. |
Results | Query output information. |
Problems | Information about any existing problems in the database — which are not necessarily caused by the query. |
Transaction
The transaction key is a JSON string with the following fields:
Field | Meaning |
---|---|
ID | Transaction identifier. |
State | Transaction state. See Transaction States for more details. |
For example:
{
"id": "******",
"state": "COMPLETED"
}
Metadata
The metadata key is a JSON string with the following fields:
Field | Meaning |
---|---|
Relation ID | This is a relation identifier, for example, "/:output/:two/Int64" . It describes the relation name /:output/:two followed by its data schema Int64 . |
Types | This is a JSON array that contains the key names of the relation and their data type. |
For example:
{
relation_id {
arguments {
tag: CONSTANT_TYPE
constant_type {
rel_type {
tag: PRIMITIVE_TYPE
primitive_type: STRING
}
value {
arguments {
tag: STRING
string_val: "output"
}
}
}
}
arguments {
tag: PRIMITIVE_TYPE
primitive_type: INT_64
}
}
}
Results
The results key is a vector with the following fields:
Field | Meaning |
---|---|
Relation ID | This is a key for the relation, for example, "v1" . It refers to the column name in the Arrow table that contains the data, where "v" stands for variable, since a relation’s tuples contain several variables. |
Table | This contains the results of the query in a JSON-array format. |
For example:
v1: [[1,2,3]]
Problems
The problems key is a JSON string with the following fields:
Field | Meaning |
---|---|
error_code | The type of error that happened, for example, "PARSE_ERROR" . |
is_error | Whether an error occurred or there was some other problem. |
is_exception | Whether an exception occurred or there was some other problem. |
message | A short description of the problem. |
path | A file path for the cases when such a path was used. |
report | A long description of the problem. |
type | The type of problem, for example, "ClientProblem" . |
For example:
{
'is_error': True,
'error_code': 'PARSE_ERROR',
'path': '',
'report': '1| def output = {1; 2; 3\n ^~~~~~~~\n', 'message': 'Missing closing `}`.',
'is_exception': False,
'type': 'ClientProblem'
}
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.
For example:
var rsp = client.execute(
database,
engine,
"def output = foo",
true,
new HashMap<String, String>() {{ put("foo", "asdf"); }}
);
System.out.println(rsp);
This will return the string "asdf"
back.
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.
Loading Data
The loadCsv()
and loadJson()
functions allow you to load data into a database.
These are not strictly necessary, since the Rel load utilities can also be used for this task.
See the CSV Import and JSON Import guides for more details.
It’s advisable to load data using built-in Rel utilities within queries, rather than these specific SDK functions. See Querying a Database for more details.
Loading CSV Data
The loadCsv()
function loads CSV data and inserts the result into the base relation named by the relation
argument.
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);
Here’s an example:
var data = new FileInputStream("my_data_file.csv");
var rsp = client.loadCsv(database, engine, "my_csv", data);
By default, loadCsv()
attempts to infer the schema of the data.
The options
argument allows you to specify how to parse a given CSV file, including the schema, delimiters, and escape characters.
See this example (opens in a new tab) for more details.
Loading JSON Data
The loadJson()
function loads JSON data and inserts them into the base relation named by the relation
argument:
loadJson(String database, String engine, String relation, InputStream data)
loadJson(String database, String engine, String relation, String data)
Here’s an example:
var rsp = client.loadJson(database, engine, "my_json", "{\"a\" : \"b\"}");
In both the LoadCsvAsync()
and LoadJsonAsync()
methods, the base relation relation
is not cleared, allowing for multipart, incremental loads.
You can clear a base relation, such as my_base_relation
, as follows:
var rsp = client.execute(
database,
engine,
"def delete[:my_base_relation] = my_base_relation",
false
);
Listing Base Relations
You can list the base relations in a given database as follows:
var rsp = client.listEdbs(database, engine);
The result is a JSON list of objects.
Managing Transactions
This section covers the API functions you can use to manage transactions.
Canceling Transactions
You can cancel an ongoing transaction as follows:
var rsp = client.cancelTransaction(id);
System.out.println(rsp);
The argument id
is a string that represents a transaction ID.