rqlite carries SQL parameters separately from statement text
rqlite 5.5.0 adds positional parameters to its HTTP SQL API. The request format separates values from SQL and carries that distinction through replication to SQLite execution.
An HTTP database API that accepts SQL strings still needs a way to distinguish application values from SQL syntax. Otherwise every client that incorporates input has to construct SQL literals correctly. rqlite 5.5.0 adds parameterized statements for reads and writes. The September 27 announcement links the new capability to an updated data API, which packages statement text and values as separate elements of an array.
The change is more substantial than another way to quote strings. It preserves a distinction between the operation and its inputs as the request moves through HTTP parsing, replicated commands and SQLite execution. That is the useful property for an application that receives values from outside its own code. A user's text can remain a value without becoming part of the SQL grammar.
I value rqlite's explicit API around a small relational database. Parameters improve that interface because a client can state its query and supply its data separately. The database remains responsible for interpreting the placeholder values, instead of asking each application to reproduce literal construction inside an HTTP request.
Each statement becomes an array
The data API documentation at the release tag shows the existing raw form as an outer JSON array of SQL strings. In the parameterized form, each statement becomes an inner array within that collection. Its SQL string comes first, followed by the values for its placeholders.
For a pre-existing foo table with a name column, the documented insert has this shape. The URL selects the write endpoint, while the JSON body describes one parameterized statement.
curl -XPOST 'http://localhost:4001/db/execute?pretty&timings' \
-H 'Content-Type: application/json' \
-d '[["INSERT INTO foo(name) VALUES(?)", "fiona"]]'
The nested brackets have separate meanings. An outer list supplies the request's statement collection, while each inner list associates one SQL string with its argument values. Inside the SQL, the question mark occupies the position where SQLite will use the supplied name. A client should not interpolate the value into that SQL string before creating the JSON document.
The query endpoint accepts the corresponding read form through POST. It uses the same association between SQL and parameters.
curl -XPOST 'http://localhost:4001/db/query?pretty&timings' \
-H 'Content-Type: application/json' \
-d '[["SELECT * FROM foo WHERE name=?", "fiona"]]'
This is a POST carrying a read operation. HTTP method alone is not the distinction between a database write and query in this API, since both endpoints can accept POST bodies. The endpoint and SQL operation still need to agree. The documentation explicitly stresses choosing the correct endpoint for the intended work.
The first interface has deliberate limits
Version 5.5.0 documents simple positional ? parameters. Named parameters are not supported yet. That restriction matters for clients that already have a preferred named-binding convention. SQLite's broader capabilities do not automatically establish that rqlite's HTTP representation exposes every binding form in this release.
The request also cannot mix parameterized and nonparameterized statement forms. A collection of nested statement arrays is one supported shape, and a collection of plain strings is another. Combining them in the same request is outside the documented interface. A client constructing batches needs to select the representation for the complete request.
The request parser explains this limitation. It first attempts to decode the body as a list of strings. If that succeeds, it creates statement objects without parameters. Otherwise it tries the nested-array form and separates each statement's first element from the remaining values.
This parsing sequence preserves the old request format while adding the new one. Existing clients sending plain SQL arrays can continue using that shape. New clients can use parameter arrays without introducing a different endpoint family. The compatibility mechanism is in the request parser, which recognizes alternative complete representations.
The parser also exposes why an example must show the nesting correctly. A SQL string followed by a value at the outer level would resemble two entries in the plain string collection when both are strings. It would not express the intended relationship between one statement and its parameter. The inner array is what makes that association explicit.
Parameters remain distinct inside the store
The store code in this version represents a statement with a query string and a parameter slice. Query requests and execute requests contain collections of those statements. When constructing the replicated database command, the store copies the queries and parameter collections into the command representation.
That step matters because rqlite replicates database operations. A write's values cannot disappear after HTTP parsing and leave only placeholder SQL in the command stream. The store must preserve the inputs that give the statement its meaning. The source shows those values as part of the represented operation, alongside the query text.
At the database layer, execution passes stmt.Query and stmt.Parameters separately to the driver. The database implementation does this for both modifying statements and row-returning queries. It does not join the parameters back into the SQL string through textual replacement before calling the driver.
This is the mechanism behind the injection-related benefit described in the API documentation. A bound value reaches the argument interface instead of being interpreted as newly assembled SQL syntax. The client still chooses the SQL structure. Parameters are appropriate for its values, while an application's decision about which operation to expose remains an application responsibility.
I prefer the implementation's separation because it can be followed through each stage. The HTTP body associates a statement with inputs, the store carries them in the command, and the database passes them to its execution interface. The property is visible in data structures and calls, rather than depending on an assurance that clients quote every possible input correctly.
A batch is not automatically a transaction
The data API already has a transaction URL parameter for executing a request's statements within a transaction. The documentation says processing stops on a statement error in that mode, with the grouped outcome described as either all statements succeeding or neither. Parameter binding and transaction grouping solve different problems, so the new nested array should not be read as automatically selecting that mode.
An application might send several parameterized inserts for convenience, while separately deciding whether those inserts must succeed together. The outer JSON collection identifies the batch. The transaction option supplies the additional grouping semantics documented by the server. Those meanings remain distinct even though one HTTP request can carry both choices.
The documentation also warns that manually issuing BEGIN, COMMIT, ROLLBACK, SAVEPOINT and RELEASE has undefined behavior in rqlite's exposed interface. It directs clients to the URL-level transaction mechanism instead. The addition of parameters does not change that documented restriction or make a series of separate requests into a connection-scoped transaction.
Execution results also remain part of the API contract. The documentation shows write responses containing affected-row and last-insert-ID information, with optional timing fields. A statement error appears in the corresponding result. Supplying parameters does not remove the caller's need to inspect those results before treating the operation as successful.
The database implementation records driver execution errors in the statement result and treats transaction mode as a reason to stop and roll back the grouped work. Outside that mode, its loop can continue to other statements after an error. This makes the distinction between a request collection and a transaction observable in failure handling as well as in the request options.
Read consistency still belongs to the request
Queries can select a consistency level independently of whether they use parameters. The API documentation says writes go to the leader, while reads depend on their consistency requirements and default to a leader-oriented path. A parameterized predicate changes how its value reaches SQLite. It does not establish which replica state is acceptable to read.
The same document describes follower redirects for writes and leader-oriented queries. Clients remain responsible for reissuing the request to the leader. The parameterized request therefore still needs to preserve its body when following the documented operation path. Encoding a query correctly is one part of communicating with the cluster, alongside reaching a node that can serve the requested operation.
The practical result is a more precise input contract within the existing API. SQL text defines the operation, positional values supply its data, and request options describe grouping or read coordination. Version 5.5.0 carries the first two through the server separately, allowing applications to use bound values without discarding the cluster controls that determine how the operation runs.