November 14, 2023·4 min read·engineeringrqliteclientsapi

rqlite documents its client library ecosystem

A new listing makes rqlite clients easier to find. Request encoding and connection behavior explain what an application needs from those libraries beyond convenient SQL methods.

A library can make a database easier to call while still leaving important behavior to the application. On November 14, Philip O'Toole announced a listing of known rqlite client libraries. The short announcement adds a discovery point for developers. It does not claim that every listed library implements identical semantics or has passed a common compatibility suite.

The useful engineering question is what a client must preserve. rqlite's HTTP interface accepts SQL statements, but a wrapper also chooses how to encode parameters and handle connection changes. Those choices are observable application behavior. The project's existing parser and CLI client provide concrete examples of the work involved, without making unsupported claims about individual third-party libraries.

I value rqlite's relatively small deployment model. A good client should preserve that clarity by exposing the decisions that affect results. Finding a familiar language binding is helpful, but the binding's treatment of requests matters more than the appearance of its method names.

Parameters have a defined wire representation

The versioned request parser accepts a simple array of SQL strings or a parameterized array-of-arrays representation. In the latter form, each inner array begins with the SQL string. Subsequent values supply parameters. A map supplies named parameters, while ordinary values become positional parameters.

A client that supports positional bindings can represent an illustrative insert with this JSON request body. The table and value are examples, not a schema supplied by the project.

[
  ["INSERT INTO accounts(name) VALUES(?)", "example"]
]

This shape keeps the SQL template and its parameter distinct. A wrapper that instead concatenates the value into SQL is implementing a different encoding strategy. Its string-escaping behavior then becomes part of correctness, even though the server already supports structured parameter values.

The parser also distinguishes data types. It maps JSON strings, booleans, and null values into corresponding command parameter representations. Unsupported types return an error. A library's host-language conversions need to agree with that accepted set, rather than assuming any object can become a database parameter through ordinary JSON serialization.

Numeric handling deserves specific attention. The parser uses json.Decoder.UseNumber() and attempts an int64 conversion before falling back to a floating-point conversion. This preserves a distinction the server can represent. If a client rounds a large integer before serializing it, the server's careful decoder cannot recover the original value.

Endpoints and request choices remain visible

The HTTP service implementation handles separate execute and query routes, together with the unified request route. Its query handler accepts GET and POST. The execute and request paths parse bodies into statements before constructing their store requests.

The handlers also extract options such as transactions, timeouts, and read consistency. These do not disappear because a library offers a short query method. A wrapper may expose them as arguments or configuration, but its defaults still select behavior for callers who do not specify them.

That observation is a reason to read a library's contract, rather than a reason to reproduce its implementation in every application. Shared code can centralize encoding and option handling. It becomes more useful when an application can tell which server operation its method will invoke and which settings will accompany it.

The service also has a path for explicit redirection to a leader. When requested, a node that cannot perform the leader-only operation can return a permanent redirect with the leader API address. Otherwise, relevant handlers can use the cluster connection to forward work. A client should understand the behavior it requests before deciding how it follows responses.

The CLI illustrates host fallback

The CLI's HTTP client wraps Go's standard HTTP client with a configured host list. If a request fails, it advances through that list. Once every candidate has been tried, it returns an error indicating that no host is available.

It tracks the current host between calls and reports a HostChangedError when a request succeeds through another host. That return value carries information even though a response exists. Callers need to interpret the response and error together according to this particular client's contract.

The wrapper also implements an explicit redirect limit. It recognizes permanent redirects and stops after exceeding its configured bound. This illustrates why following a URL is a behavior with limits, rather than an unlimited promise that a library will always locate the correct destination.

Its source warns that this wrapper is not safe for concurrent use. That matters for code tempted to treat a CLI-oriented helper as a general application connection pool. A shared application client needs an ownership and synchronization model appropriate to its implementation.

Retrying a mutation needs more information

Host fallback improves the chance of reaching a responsive node, but it does not by itself establish the outcome of an earlier request. A transport failure can leave a caller without a usable response. Whether the caller can repeat a mutation safely depends on the operation and the information retained about its outcome.

This is a client contract question, not a claim that the new library directory introduces a retry guarantee. A method that retries requests needs to state which failures trigger another attempt and what happens to the original request body. The HTTP wrapper's use of an io.Reader for execution makes body ownership a concrete implementation concern.

I prefer a binding that states those choices plainly. Parameter representation can be verified against the parser, and supported options can be checked against the handlers. Concurrency and retry behavior need equally explicit treatment. The newly announced listing helps developers find clients, while the server contract supplies the details needed to judge whether a selected client fits an application's requests.