Accessing rqlite
Each rqlite node exposes an HTTP API allowing data to be inserted into, and read back from, the database. Specifically there are three endpoints to know:
/db/execute
which accepts only write requests (INSERT
,UPDATE
,DELETE
)/db/query
which accepts only read requests (SELECT
)/db/request
which accepts both read and write requests. This endpoint is known as the Unified Endpoint.
You can send your read and writes requests to any node in your cluster.
Which endpoint should you use? If you know ahead of time whether you are doing reads or writes, you should choose the endpoint dedicated to that type of request (either /db/execute
or /db/query
), as you will know precisely what to expect when rqlite responds. This encourages the most robust interaction with rqlite.
In contrast the format of the response from /db/request
will depend on whether you send read or write requests, and may require you (or your code) to inspect the response more closely before parsing it. But /db/request
can be more convenient in some cases, as you don’t need to worry about choosing a particular endpoint ahead of time – just send all your requests to /db/request
.
The best way to understand the API is to work through the simple examples below. There are also client libraries available.
Writing Data
To write data successfully to the database, you must create at least 1 table. To do this perform a HTTP POST on the /db/execute
endpoint on any rqlite node. Encapsulate the CREATE TABLE
SQL command in a JSON array, and put it in the body of the request. An example via curl:
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
"CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT, age INTEGER)"
]'
You can also perform a POST with the SQL command placed directly in the body:
curl -XPOST 'localhost:4001/db/execute?pretty' -H "Content-Type: text/plain" -d \
'CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT, age INTEGER)'
The
plain/text
format can be convenient for quick prototyping viacurl
, but it is strongly recommended you use the JSON request format for production code.
To insert an entry into the database, execute a second SQL command:
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
"INSERT INTO foo(name, age) VALUES(\"fiona\", 20)"
]'
The response is of the form:
{
"results": [
{
"last_insert_id": 1,
"rows_affected": 1,
"time": 0.00886
}
],
"time": 0.0152
}
The use of the URL param pretty
is optional, and results in pretty-printed JSON responses. Time is measured in seconds. If you do not want timings, do not pass timings
as a URL parameter.
Querying Data
Querying data is easy. For a single query simply perform an HTTP GET on the /db/query
endpoint, setting the query statement as the query parameter q
:
curl -G 'localhost:4001/db/query?pretty&timings' --data-urlencode 'q=SELECT * FROM foo'
The default response is of the form:
{
"results": [
{
"columns": [
"id",
"name",
"age"
],
"types": [
"integer",
"text",
"integer"
],
"values": [
[
1,
"fiona",
20
]
],
"time": 0.0150043
}
],
"time": 0.0220043
}
You can also query via a HTTP POST request, with both JSON and Plain Text supported as the request body:
curl -XPOST 'localhost:4001/db/query?pretty&timings' -H "Content-Type: application/json" -d '[
"SELECT * FROM foo"
]'
curl -XPOST 'localhost:4001/db/query?pretty&timings' -H "Content-Type: text/plain" -d 'SELECT * FROM foo'
In both cases the response will be in the same form as when the query is made via HTTP GET.
The
plain/text
format can be convenient for quick prototyping viacurl
, but it is strongly recommended you use the JSON request format for production code.
Associative response form
You can also request an associative form of response, by adding associative
as a query parameter:
curl -G 'localhost:4001/db/query?pretty&timings&associative' --data-urlencode 'q=SELECT * FROM foo'
Response:
{
"results": [
{
"types": {"id": "integer", "age": "integer", "name": "text"},
"rows": [
{ "id": 1, "age": 20, "name": "fiona"},
{ "id": 2, "age": 25, "name": "declan"}
],
"time": 0.000173061
}
],
"time": 0.000185964
}
This form will have a map per row returned, with each column name as a key. This form can be more convenient for clients, as many programming languages will support loading the rows
object directly into an array-of-maps data type.
Parameterized Statements
While the “raw” API described above can be convenient and simple to use, it is vulnerable to SQL Injection attacks. To protect against this issue, rqlite also supports SQLite parameterized statements, for both read and writes. To use this feature, send the SQL statement and values as distinct elements within a new JSON array, as follows:
Writing data
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
["INSERT INTO foo(name, age) VALUES(?, ?)", "fiona", 20]
]'
Reading data
curl -XPOST 'localhost:4001/db/query?pretty&timings' -H "Content-Type: application/json" -d '[
["SELECT * FROM foo WHERE name=?", "fiona"]
]'
Named Parameters
Named parameters are also supported. To use this feature set the values using a dictionary like so:
Writing data
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
["INSERT INTO foo(name, age) VALUES(:name, :age)", {"name": "fiona", "age": 20}]
]'
Reading data
curl -XPOST 'localhost:4001/db/query?pretty&timings' -H "Content-Type: application/json" -d '[
["SELECT * FROM foo WHERE name=:name", {"name": "fiona"}]
]'
Transactions
A form of transactions are supported. To execute statements within a transaction, add transaction
to the URL. An example of the above operation executed within a transaction is shown below.
curl -XPOST 'localhost:4001/db/execute?pretty&transaction' -H "Content-Type: application/json" -d "[
\"INSERT INTO foo(name) VALUES('fiona')\",
\"INSERT INTO foo(name) VALUES('sinead')\"
]"
When a transaction takes place either both statements will succeed, or neither. Performance is much, much better if multiple SQL INSERTs or UPDATEs are executed via a transaction. Note that processing of the request ceases the moment any single query results in an error.
The behaviour of rqlite if you explicitly issue BEGIN
, COMMIT
, ROLLBACK
, SAVEPOINT
, and RELEASE
to control your own transactions is not defined. This is because the behavior of a cluster if it fails while such a manually-controlled transaction is not yet defined. It is important to control transactions only through the query parameters shown above.
BLOB data
Working with BLOB data may require specialized handling, depending on your use case. Examples of writing BLOB data, and reading it back, are shown below.
Writing BLOBs
A simple way to insert BLOB data is to use the X''
syntax. These are string literals containing hexadecimal data and preceded by a single “x” or “X” character, for example: x'53514C697465'
.
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
"CREATE TABLE foo (data BLOB) STRICT"
]'
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
"INSERT INTO foo(data) VALUES(x'\''53514C697465'\'')"
]'
In the example above it is up to you to encode your data before passing it to rqlite. In Go you can do this using EncodeToString.
You can also insert BLOB data as a parameterized value. Both the “X” form and byte arrays are supported as values.
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
["INSERT INTO foo (data) VALUES (?)", "x'\''68656C6C6F20776F726C64'\''"]
]'
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
["INSERT INTO foo(data) VALUES(?)", [83,81,76,105,116,101]]
]'
Reading BLOBs
Reading BLOB data back can be done using a regular query request.
curl -G 'localhost:4001/db/query?pretty' --data-urlencode 'q=SELECT * FROM foo'
{
"results": [
{
"columns": [
"data"
],
"types": [
"blob"
],
"values": [
[
"3q2+7w=="
]
]
}
]
}
Note that BLOB data is returned as base64-encoded strings. While this works well for many applications, it makes it difficult to know if the returned string represents an actual string that was inserted in the row, or an actual BLOB data1. To instead have BLOB data returned as an array of byte values, add blob_array
to your URL:
curl -G 'localhost:4001/db/query?pretty&blob_array' --data-urlencode 'q=SELECT * FROM foo'
{
"results": [
{
"columns": [
"data"
],
"types": [
"blob"
],
"values": [
[
[
222,
173,
190,
239
]
]
]
}
]
}
Handling Errors
Errors are indicated in two ways. Some error conditions will be flagged via 4xx
or 5xx
HTTP status codes, so you should always check that first. However, even if rqlite responds with HTTP 200
you should check for any errors that occurred while processing the request, which usually indicates an error at the database-level. These will be indicated via the presence of an error
key in the JSON response. For example:
curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d "[
\"INSERT INTO nonsense\"
]"
{
"results": [
{
"error": "near \"nonsense\": syntax error"
}
],
"time": 2.478862
}
Query Timeouts
By default, SQL queries do not timeout. You can set a timeout by setting the db_timeout
URL parameter. This parameter allows you to specify the maximum amount of time to spend processing the query before it is interrupted and an error returned. Note that this timeout is applied per SQL statement, not the entire HTTP request. An example request with a 2 second timeout is shown below.
curl -XPOST 'localhost:4001/db/execute?db_timeout=2s' -H "Content-Type: application/json" -d '[
["INSERT INTO foo(name, age) VALUES(?, ?)", "fiona", 20]
]'
Unified Endpoint
With the Unified Endpoint you can send read and writes requests in one operation, to the same endpoint. Let’s work through an example.
rqlite uses the SQLite function sqlite3_stmt_readonly() to determine if a SQL statement is a read or a write.
Create a table:
curl -XPOST 'localhost:4001/db/request?pretty&timings' -H "Content-Type: application/json" -d '[
"CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT, age INTEGER)"
]'
In this case, the output looks the same as when using the /db/execute
endpoint.
{
"results": [
{
"time": 0.000277428
}
],
"time": 0.01125789
}
Now let’s perform an INSERT
and SELECT
in the same request, including attempting to access a non-existent table. And let’s request the Associative response form.
curl -XPOST 'localhost:4001/db/request?pretty&timings&associative' -H "Content-Type: application/json" -d '[
["INSERT INTO foo(name, age) VALUES(?, ?)", "fiona", 20],
["INSERT INTO foo(name, age) VALUES(?, ?)", "declan", 30],
["SELECT * FROM foo"],
["SELECT * FROM bar"]
]'
This time the response includes both results and rows from all those operations, as well as one record indicating a query failed.
{
"results": [
{
"last_insert_id": 1,
"rows_affected": 1,
"time": 0.000074612,
"rows": null
},
{
"last_insert_id": 2,
"rows_affected": 1,
"time": 0.000044645,
"rows": null
},
{
"types": { "age": "integer", "id": "integer", "name": "text"},
"rows": [
{"age": 20, "id": 1, "name": "fiona"},
{"age": 30, "id": 2, "name": "declan"}
],
"time": 0.000055248
},
{
"error": "no such table: bar"
}
],
"time": 0.010571084
}
The Unified Endpoint supports transactions, Associative responses, Read Consistency levels, and Parameterized Statements. Just set the relevant Query URL parameters, as described earlier on this page. Queued Writes, however, are not supported by the Unified Endpoint.
PRAGMA Directives
You can issue PRAGMA
directives to rqlite, and they will be passed to the underlying SQLite database. Certain PRAGMA
directives, which alter the operation of the SQLite database, may not make sense in the context of rqlite – and some PRAGMA
directives may cause rqlite to fail. PRAGMA
directives which just return information about the SQLite database, without changing its operation, are always safe.
Some PRAGMA
directives will intefere with the proper operation of rqlite. They include:
PRAGMA journal_mode
- rqlite requires the SQLite database to be in WAL mode at all times. Don’t change the journaling mode.PRAGMA wal_checkpoint
- rqlite requires exlusive control over the WAL. Don’t checkpoint rqlite.PRAGMA wal_autocheckpoint=N
- rqlite requires exlusive control over the WAL. Don’t modify WAL checkpointing.PRAGMA synchronous=N
- Don’t change how rqlite manages writes to disk.
If you have a question about a particular PRAGMA
command, you should discuss it on the rqlite Slack, or ask a question on GitHub.
Issuing a PRAGMA
directive
The rqlite CLI supports issuing many other PRAGMA
directives. For example:
127.0.0.1:4001> pragma compile_options
+----------------------------+
| compile_options |
+----------------------------+
| COMPILER=gcc-7.5.0 |
+----------------------------+
| DEFAULT_WAL_SYNCHRONOUS=1 |
+----------------------------+
| ENABLE_DBSTAT_VTAB |
+----------------------------+
| ENABLE_FTS3 |
+----------------------------+
| ENABLE_FTS3_PARENTHESIS |
+----------------------------+
| ENABLE_JSON1 |
+----------------------------+
| ENABLE_RTREE |
+----------------------------+
| ENABLE_UPDATE_DELETE_LIMIT |
+----------------------------+
| OMIT_DEPRECATED |
+----------------------------+
| OMIT_SHARED_CACHE |
+----------------------------+
| SYSTEM_MALLOC |
+----------------------------+
| THREADSAFE=1 |
+----------------------------+
PRAGMA
directives may also be issued using the /db/execute
, /db/query
, or /db/request
, endpoint. For example:
$ curl -G 'localhost:4001/db/query?pretty&timings' --data-urlencode 'q=PRAGMA foreign_keys'
{
"results": [
{
"columns": [
"foreign_keys"
],
"types": [
""
],
"values": [
[
0
]
],
"time": 0.000070499
}
],
"time": 0.000540857
}$
Technical details
This section assumes a basic familiarity with the Raft protocol. A simple introduction to Raft can be found here.
To make the very best use of the rqlite API, there are some details to know. But understanding the following information is not required to make use of rqlite.
With any rqlite cluster, all write-requests must be serviced by the cluster Leader – this is due to the way the Raft consensus protocol works. If a client sends a write request to a Follower (or read-only, non-voting, node), the Follower transparently forwards the request to the Leader. The Follower waits for the response from the Leader, and returns it to the client. Any credential information included in the original HTTP request to the Follower is included with the forwarded request (assuming that permission checking also passes first on the Follower), and permission checking is performed on the Leader.
Queries, by default, are also serviced by the cluster Leader. Like write-requests, Followers will, by default, transparently forward queries to the Leader, and respond to the client after receiving the response from the Leader. However, depending on the read-consistency specified with the request, if a Follower received the query request it may serve that request directly and not contact the Leader. Which read-consistency level makes sense depends on your application.
If one node, when trying to contact a second node, fails to contact that node, it will return an error to the user. You can you have the first node automatically retry the communication by setting the URL query parameter
retries
to the number of retries you wish the node to execute. For exampleretries=2
will instruct the first node to retry 2 times.
Data and the Raft log
Any writes to the SQLite database go through the Raft log, ensuring only changes committed by a quorum of rqlite nodes are actually applied to the SQLite database. Queries do not necessarily go through the Raft log, however, since they do not change the state of the database, and therefore do not need to be captured in the log. Only if Strong read consistency is requested does a query go through the Raft log.
Request Forwarding Timeouts
If a Follower forwards a request to a Leader, by default the Leader must respond within 30 seconds. You can control this timeout by setting the timeout
parameter. For example, to set a 2 minute timeout, you would issue the following request:
curl -XPOST 'localhost:4001/db/execute?timeout=2m' -H "Content-Type: application/json" -d '[
["INSERT INTO foo(name, age) VALUES(?, ?)", "fiona", 20]
]'
Disabling Request Forwarding
If you do not wish a Follower to transparently forward a request to a Leader, add redirect
to the URL as a query parameter. In that case if a Follower receives a request that can only be serviced by the Leader, the Follower will respond with HTTP 301 Moved Permanently and include the address of the Leader as the Location
header in the response. It is then up the clients to re-issue the command to the Leader.
This option was made available as it provides maximum visibility to the clients, should they prefer it. For example, if a Follower transparently forwarded a request to the Leader, and one of the nodes then crashed during processing, it may be difficult for the client to determine where in the chain of nodes the processing failed.
Example of redirect on query
$ curl -v -G 'localhost:4003/db/query?pretty&timings&redirect' --data-urlencode 'q=SELECT * FROM foo'
* Trying ::1...
* connect to ::1 port 4003 failed: Connection refused
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 4003 (#0)
> GET /db/query?pretty&timings&q=SELECT%20%2A%20FROM%20foo HTTP/1.1
> Host: localhost:4003
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Content-Type: application/json; charset=utf-8
< Location: http://localhost:4001/db/query?pretty&timings&q=SELECT%20%2A%20FROM%20foo
< X-Rqlite-Version: 4
< Date: Mon, 07 Aug 2017 21:10:59 GMT
< Content-Length: 116
<
<a href="http://localhost:4001/db/query?pretty&timings&q=SELECT%20%2A%20FROM%20foo">Moved Permanently</a>.
* Connection #0 to host localhost left intact