Skip to content
RKGMS
SDK GUIDES
Julia

RelationalAI SDK for Julia

This guide presents the API for the RelationalAI SDK For Julia.

julia logo

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

The rai-sdk-julia package is open source and is available in this Github repository:


RelationalAI/rai-sdk-julia

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

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

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

Requirements

Check the rai-sdk-julia repository for the latest version requirements to interact with the RKGMS using the RelationalAI SDK for Julia.

Installation

The RelationalAI SDK for Julia is a standalone package. It can be installed using the Julia REPL:

using Pkg; Pkg.add("RAI")

Configuration

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

The Julia API load_config() function takes the configuration file and the profile name as optional arguments:

using RAI: load_config
cfg = load_config(fname="~/.rai/config", profile = "default")

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

Creating a Context

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

using RAI: Context, load_config
cfg = load_config()
# to specify a non-default profile use:
# cfg = load_config(profile = "myprofile")
ctx = Context(cfg)

The remaining code examples in this document assume that you have a valid context in the ctx Julia variable and that you have brought the RAI module into the current namespace:

using RAI

You can test your configuration and context by running:

list_databases(ctx)

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

Additionally, most of the Julia API calls throw an HTTPError exception when there is an issue. Therefore you can typically wrap the API calls discussed here in a try ... catch block similar to:

try
    list_databases(ctx)
catch e
    e isa HTTPError ? show(e) : rethrow()
end

You can find the full test example here.

Managing Users

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

Creating a User

create_user(ctx, email, roles)

Here, email is a string, identifying the user, and roles is a list of roles. The roles currently supported are user and admin, with user being the default role.

Disabling a User

disable_user(ctx, user)

Listing Users

list_users(ctx)

Retrieving User Details

get_user(ctx, user)

Here, user is a string ID, e.g. "auth0|XXXXXXXXXXXXXXXXXX".

Managing OAuth Clients

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

create_oauth_client(ctx, name, permissions)

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

Get a list of OAuth clients:

list_oauth_clients(ctx)

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

get_oauth_client(ctx, id)

Delete the OAuth client identified by the string id:

delete_oauth_client(ctx, 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 (the default size is XS):

engine = "julia_sdk_engine"
size = "XS"

rsp = create_engine(ctx, engine, size=size)
println(rsp)

API requests return a JSON value. Here is a sample result for create_engine:

{'engine': {
    'account_name': '#########',
    'created_by': '#########',
    'id': '#########',
    'name': 'julia_sdk_engine',
    'region': '#########',
    'requested_on': '2022-02-19T19:22:32.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 list_engines:

list_engines(ctx, state="PROVISIONED")

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 “PROVISIONED” state) in the engine variable, and a test database in database:

# replace by your values for testing:
database = "mydatabase"
engine = "myengine"

Deleting an Engine

An engine can be deleted with:

rsp = delete_engine(ctx, engine)
println(rsp)

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 get_engine:

rsp = get_engine(ctx, engine)
println(rsp)

An HTTPError exception will be thrown if the engine specified in get_engine does not exist.

Managing Databases

Creating a Database

You can create a database with create_database, as follows:

database = "mydatabase" # adjust as needed
engine = "myengine"

# Create a database
rsp = create_database(ctx, database, engine)
println(rsp)

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 also use create_database to clone a database by specifying a source argument:

# Clone a database
rsp = create_database(
    ctx, "mydatabase-clone", engine,
    source=database
)

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

rsp = get_database(ctx, database)
println(rsp)

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

Note that this call does not require a running engine.

Listing Databases

Using list_databases will list the databases available to the account:

rsp = list_databases(ctx)
# rsp = list_databases(ctx, state)
println(rsp)

The optional variable state (default: nothing) can be used to filter databases by state. For example, “CREATED”, “CREATING”, or “CREATION_FAILED”.

Deleting Databases

A database can be deleted with delete_database, if the config has the right permissions.

The database is identified by its name, as used in create_database.

rsp = delete_database(ctx, database)
println(rsp)

If successful, the response will be of the form:

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

Deleting a database cannot be undone.

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 load_model function installs a Rel model source in a given database. In addition to the usual context, the database and engine arguments, it takes a Julia Dictionary. This dictionary maps names to models, so that more than one named model can be installed at one time.

For example, to add a Rel model source code file to a database:

source_string = """
def countries = {"United States of America"; "Germany"; "Japan"; "Greece"}
def oceans = {"Arctic"; "Atlantic"; "Indian"; "Pacific"; "Southern"}
"""

load_model(ctx, database, engine, Dict("mysource" => source_string))

If the database already contains an installed source with the same given name, then it is replaced by the new source.

If you need to install from a file, read it into a string first. For example:

source_string = read("mymodel.rel", String)

load_model(ctx, database, engine, Dict("mysource" => source_string))

Deleting a Rel Model Source

You can delete a source from a database using the following code:

delete_model(ctx, database, engine, "mysource")

Listing Installed Rel Model Sources

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

list_models(ctx, database, engine)

This returns a JSON array of names.

To see the contents of a named source, use:

get_model(ctx, database, engine, sourcename)

where sourcename is the name of the model.

Querying a Database

The high-level API call for running a single query/transaction against the database is exec. 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.

function exec(
    ctx::Context,
    database::AbstractString,
    engine::AbstractString,
    source;
    inputs = nothing,
    readonly = false,
    kw...
)

For example:

rsp = exec(
    ctx,
    database,
    engine,
    "def output = {1;2;3}"
)
show_result(rsp)

By convention, readonly is false.

Queries meant to update base relations – with insert and delete – must use readonly=false.

For example, here is an API call to load some CSV data and store it in the base relation my_base_relation:

data = """
name,lastname,id
John,Smith,1
Peter,Jones,2
"""

exec(
    ctx, database, engine,
    """
    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[:my_base_relation] = my_base_relation
    def insert[:my_base_relation] = load_csv[config]
    """,
    inputs = Dict("mydata" => data),
    readonly=false
)

The RAI Julia SDK also supports asynchronous transactions, through exec_async. 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.

exec_async is defined as exec, but in this case the running processes are not blocked:

rsp_async= exec_async(
    ctx, 
    database, engine, 
    "def output = {1;2;3}"
)

Then, you can poll the transaction until it has completed or aborted. Finally, you can fetch the results:

if rsp_async.transaction["state"] == "COMPLETED"
    results = get_transaction_results(ctx, rsp_async.transaction["id"])
    display(results)
end

Similar to get_transaction_results, you can also get metadata and problems for a given transaction ID:

metadata = get_transaction_metadata(ctx, rsp_async.transaction["id"])
problems = get_transaction_problems(ctx, rsp_async.transaction["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

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

rsp = exec(
    ctx,
    database,
    engine,
    "def a = 1;2 def b = 3;4 def output:one = a def output:two = b"
)

show_result(rsp)

This gives the following output:

/:output/:two/Int64
 (3,)
 (4,)

/:output/:one/Int64
 (1,)
 (2,)

Result Structure

The response is a Julia dictionary with the following keys:

FieldMeaning
metadataMetadata information about the results key.
problemsInformation about any existing problems in the DB – which are not necessarily caused by the query.
resultsQuery output information.
transactionInformation about transaction status, including identifier.

The results key is a vector with the following fields:

FieldMeaning
relationIDThis is a key for the relation, for example, "v1".
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 key is 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 the data.
typesThis is a JSON-array that contains the key names of the relation and their data type.

Finally, the problems key is also 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 exec API call takes an optional inputs dictionary 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. Here’s an example:

rsp = exec(
    ctx,
    database,
    engine,
    "def output = foo",
    inputs = Dict("foo" => "asdf")
)
show_result(rsp)

This will return the string "asdf" back.

Functions that transform a file and write the results to a base relation can be easily written in this way. The calls load_csv and load_json can actually 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 show_result function prints API responses. See previous examples.

See the examples folder in the Github repository for examples on how to handle HTTP exceptions.

Loading Data: load_csv and load_json

As a convenience, the Julia API includes a load_csv and load_json function. These are not strictly necessary, since the load utilities in Rel itself can be used in a non-read-only exec query that uses the inputs option. See, for example, the sample code using load_csv in Querying a Database.

The Julia function load_csv loads data and inserts the result into the base relation named by the relation argument. Additionally, load_csv attempts to guess the schema of the data. For more control over the schema, use a non-read-only exec query using the inputs option.

function load_csv(
    ctx::Context,
    database::AbstractString,
    engine::AbstractString,
    relation::AbstractString,
    data;
    delim = nothing,    # default: ,
    header = nothing,   # a Dict from col number to name (base 1)
    header_row = nothing,   # row number of header, nothing for no header
    escapechar = nothing,   # default: \
    quotechar = nothing,    # default: "
    kw...
)

Similarly, load_json loads the data string as JSON, and inserts it into the base relation named by the relation argument:

function load_json(
    ctx::Context,
    database::AbstractString,
    engine::AbstractString,
    relation::AbstractString,
    data;
    kw...
)

Example:

load_json(ctx, database, engine, "myjson",  """{"a" : "b"}""")

Note: In both cases, the relation base relation 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 Base Relations

list_edbs(ctx, database, engine)

will list the base 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 Julia SDK API calls.

Query Cells

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

rsp = exec(ctx, database, engine, "def output = {(1,); (2,); (3,)}")
show_result(rsp)

This gives the following output:

/:output/Int64
 (1,)
 (2,)
 (3,)

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

Install Cells

To persist derived relation definitions, use the load_model API call. That is, the Install cell in a Rel Console notebook corresponds to the load_model Julia function call, as shown below:

rsp = load_model(
    ctx, database, engine,
    Dict("arity1_k_def" => "def k = {2;3;50}")
)

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

rsp = exec(ctx, database, engine, "def output = k")
show_result(rsp)

Update Cells

The Update cell in a Rel Console notebook corresponds to an exec function call with the flag readonly=false. Note that this is the default value for this flag. Here’s an example:

rsp = exec(
    ctx, database, engine,
    """def insert:employee = {(1, \"Han Solo\"); (2, \"Bart Simpson\")}""",
    readonly=false
)

You can modify the employee base relation:

rsp = exec(
    ctx, database, engine,
    """def insert:employee = {(3, \"King\"); (4, \"Queen\")}""",
    readonly=false
)

You can now query the modified employee base relation:

rsp = exec(ctx, database, engine, "def output=employee")

This gives the following output:

/:output/Int64/String
 (1, "Han Solo")
 (2, "Bart Simpson")
 (3, "King")
 (4, "Queen")

To update base relations, always use the readonly=false option in the exec call.