Accessing rqlite

How to write data and read it back

The first thing to know is that you can send your read and writes requests to any node in your cluster. For full details on how rqlite handles requests for you, check out How rqlite handles requests.

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 write requests (INSERT, UPDATE, DELETE)
  • /db/query which accepts read requests (SELECT)
  • /db/request which accepts both read and write requests. This endpoint is known as the Unified Endpoint.

Which endpoint should you use? If you know ahead of time whether you are doing reads or writes, it’s probably best to 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 structure 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:

  1. curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
  2. "CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT, age INTEGER)"
  3. ]'

To insert an entry into the database, execute a second SQL command:

  1. curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
  2. "INSERT INTO foo(name, age) VALUES(\"fiona\", 20)"
  3. ]'

The response is of the form:

  1. {
  2. "results": [
  3. {
  4. "last_insert_id": 1,
  5. "rows_affected": 1,
  6. "time": 0.00886
  7. }
  8. ],
  9. "time": 0.0152
  10. }

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:

  1. curl -G 'localhost:4001/db/query?pretty&timings' --data-urlencode 'q=SELECT * FROM foo'

The default response is of the form:

  1. {
  2. "results": [
  3. {
  4. "columns": [
  5. "id",
  6. "name",
  7. "age"
  8. ],
  9. "types": [
  10. "integer",
  11. "text",
  12. "integer"
  13. ],
  14. "values": [
  15. [
  16. 1,
  17. "fiona",
  18. 20
  19. ]
  20. ],
  21. "time": 0.0150043
  22. }
  23. ],
  24. "time": 0.0220043
  25. }

You can also query via a HTTP POST request:

  1. curl -XPOST 'localhost:4001/db/query?pretty&timings' -H "Content-Type: application/json" -d '[
  2. "SELECT * FROM foo"
  3. ]'

The response will be in the same form as when the query is made via HTTP GET.

Associative response form

You can also request an associative form of response, by adding associative as a query parameter:

  1. curl -G 'localhost:4001/db/query?pretty&timings&associative' --data-urlencode 'q=SELECT * FROM foo'

Response:

  1. {
  2. "results": [
  3. {
  4. "types": {"id": "integer", "age": "integer", "name": "text"},
  5. "rows": [
  6. { "id": 1, "age": 20, "name": "fiona"},
  7. { "id": 2, "age": 25, "name": "declan"}
  8. ],
  9. "time": 0.000173061
  10. }
  11. ],
  12. "time": 0.000185964
  13. }

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

  1. curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
  2. ["INSERT INTO foo(name, age) VALUES(?, ?)", "fiona", 20]
  3. ]'

Reading data

  1. curl -XPOST 'localhost:4001/db/query?pretty&timings' -H "Content-Type: application/json" -d '[
  2. ["SELECT * FROM foo WHERE name=?", "fiona"]
  3. ]'

Named Parameters

Named parameters are also supported. To use this feature set the values using a dictionary like so:

Writing data

  1. curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d '[
  2. ["INSERT INTO foo(name, age) VALUES(:name, :age)", {"name": "fiona", "age": 20}]
  3. ]'

Reading data

  1. curl -XPOST 'localhost:4001/db/query?pretty&timings' -H "Content-Type: application/json" -d '[
  2. ["SELECT * FROM foo WHERE name=:name", {"name": "fiona"}]
  3. ]'

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.

  1. curl -XPOST 'localhost:4001/db/execute?pretty&transaction' -H "Content-Type: application/json" -d "[
  2. \"INSERT INTO foo(name) VALUES('fiona')\",
  3. \"INSERT INTO foo(name) VALUES('sinead')\"
  4. ]"

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.

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:

  1. curl -XPOST 'localhost:4001/db/execute?pretty&timings' -H "Content-Type: application/json" -d "[
  2. \"INSERT INTO nonsense\"
  3. ]"
  1. {
  2. "results": [
  3. {
  4. "error": "near \"nonsense\": syntax error"
  5. }
  6. ],
  7. "time": 2.478862
  8. }

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:

  1. curl -XPOST 'localhost:4001/db/request?pretty&timings' -H "Content-Type: application/json" -d '[
  2. "CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT, age INTEGER)"
  3. ]'

In this case, the output looks the same as when using the /db/execute endpoint.

  1. {
  2. "results": [
  3. {
  4. "time": 0.000277428
  5. }
  6. ],
  7. "time": 0.01125789
  8. }

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.

  1. curl -XPOST 'localhost:4001/db/request?pretty&timings&associative' -H "Content-Type: application/json" -d '[
  2. ["INSERT INTO foo(name, age) VALUES(?, ?)", "fiona", 20],
  3. ["INSERT INTO foo(name, age) VALUES(?, ?)", "declan", 30],
  4. ["SELECT * FROM foo"],
  5. ["SELECT * FROM bar"]
  6. ]'

This time the response includes both results and rows from all those operations, as well as one record indicating a query failed.

  1. {
  2. "results": [
  3. {
  4. "last_insert_id": 1,
  5. "rows_affected": 1,
  6. "time": 0.000074612,
  7. "rows": null
  8. },
  9. {
  10. "last_insert_id": 2,
  11. "rows_affected": 1,
  12. "time": 0.000044645,
  13. "rows": null
  14. },
  15. {
  16. "types": { "age": "integer", "id": "integer", "name": "text"},
  17. "rows": [
  18. {"age": 20, "id": 1, "name": "fiona"},
  19. {"age": 30, "id": 2, "name": "declan"}
  20. ],
  21. "time": 0.000055248
  22. },
  23. {
  24. "error": "no such table: bar"
  25. }
  26. ],
  27. "time": 0.010571084
  28. }

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 (since rqlite does not given direct control over its connections to the SQLite database). Furthermore some PRAGMA directives may even break rqlite.

PRAGMA directives which just return information about the SQLite database, without changing its operation, are always safe.

Issuing a PRAGMA directive

The rqlite CLI supports issuing PRAGMA directives. For example:

  1. 127.0.0.1:4001> pragma compile_options
  2. +----------------------------+
  3. | compile_options |
  4. +----------------------------+
  5. | COMPILER=gcc-7.5.0 |
  6. +----------------------------+
  7. | DEFAULT_WAL_SYNCHRONOUS=1 |
  8. +----------------------------+
  9. | ENABLE_DBSTAT_VTAB |
  10. +----------------------------+
  11. | ENABLE_FTS3 |
  12. +----------------------------+
  13. | ENABLE_FTS3_PARENTHESIS |
  14. +----------------------------+
  15. | ENABLE_JSON1 |
  16. +----------------------------+
  17. | ENABLE_RTREE |
  18. +----------------------------+
  19. | ENABLE_UPDATE_DELETE_LIMIT |
  20. +----------------------------+
  21. | OMIT_DEPRECATED |
  22. +----------------------------+
  23. | OMIT_SHARED_CACHE |
  24. +----------------------------+
  25. | SYSTEM_MALLOC |
  26. +----------------------------+
  27. | THREADSAFE=1 |
  28. +----------------------------+

PRAGMA directives may also be issued using the /db/execute, /db/query, or /db/request, endpoint. For example:

  1. $ curl -G 'localhost:4001/db/query?pretty&timings' --data-urlencode 'q=PRAGMA foreign_keys'
  2. {
  3. "results": [
  4. {
  5. "columns": [
  6. "foreign_keys"
  7. ],
  8. "types": [
  9. ""
  10. ],
  11. "values": [
  12. [
  13. 0
  14. ]
  15. ],
  16. "time": 0.000070499
  17. }
  18. ],
  19. "time": 0.000540857
  20. }$

How rqlite Handles Requests

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.

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:

  1. curl -XPOST 'localhost:4001/db/execute?timeout=2m' -H "Content-Type: application/json" -d '[
  2. ["INSERT INTO foo(name, age) VALUES(?, ?)", "fiona", 20]
  3. ]'

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

  1. $ curl -v -G 'localhost:4003/db/query?pretty&timings&redirect' --data-urlencode 'q=SELECT * FROM foo'
  2. * Trying ::1...
  3. * connect to ::1 port 4003 failed: Connection refused
  4. * Trying 127.0.0.1...
  5. * Connected to localhost (127.0.0.1) port 4003 (#0)
  6. > GET /db/query?pretty&timings&q=SELECT%20%2A%20FROM%20foo HTTP/1.1
  7. > Host: localhost:4003
  8. > User-Agent: curl/7.43.0
  9. > Accept: */*
  10. >
  11. < HTTP/1.1 301 Moved Permanently
  12. < Content-Type: application/json; charset=utf-8
  13. < Location: http://localhost:4001/db/query?pretty&timings&q=SELECT%20%2A%20FROM%20foo
  14. < X-Rqlite-Version: 4
  15. < Date: Mon, 07 Aug 2017 21:10:59 GMT
  16. < Content-Length: 116
  17. <
  18. <a href="http://localhost:4001/db/query?pretty&amp;timings&amp;q=SELECT%20%2A%20FROM%20foo">Moved Permanently</a>.
  19. * Connection #0 to host localhost left intact

Last modified May 21, 2023: Update _index.md (a7a12fe)