December 31, 2021·4 min read·engineeringrqlitesqlparametershttp

rqlite 6.9 carries named SQL parameters through the server

JSON dictionaries now bind named values through rqlite's command model and database/sql. The new form complements positional parameters while preserving request-scoped transaction behavior.

Positional bindings make the relationship between SQL and values depend on array order. Named bindings make that relationship explicit in both the statement and its request data. The December 31 release of rqlite 6.9.0 adds named parameters to the server's HTTP API. Its tagged changelog identifies the addition as the release's new feature.

The change extends an existing parameterized request form. A statement still arrives as the first element of a nested JSON array. Values can now arrive in a dictionary whose keys correspond to the names used by SQL placeholders. The server carries those names into its internal command representation and then into the database layer.

I like rqlite's direct HTTP interface, especially when a request can describe its SQL and data separately. Named parameters make a larger statement easier to review because the values identify their roles. The useful distinction is that this support now exists in the server path, independently of any client library that substitutes values into SQL itself.

A dictionary accompanies the statement

The data API documentation in the 6.9.0 tag shows named bindings for both writes and reads. The SQL uses placeholders such as :name, while the accompanying object uses the key name without the colon. An outer array remains the list of statements.

[
  [
    "INSERT INTO person(name, age) VALUES(:name, :age)",
    {"name": "Ada", "age": 36}
  ]
]

This illustrative body assumes a corresponding person table. It follows the documented write form for /db/execute. A read uses /db/query with the same statement-and-dictionary structure, so clients do not need to invent another binding convention just because the statement returns rows.

curl -X POST 'http://localhost:4001/db/query' \
  -H 'Content-Type: application/json' \
  -d '[["SELECT :name AS supplied_name", {"name":"Ada"}]]'

The example selects the supplied parameter directly and needs no application table. The SQL contains the placeholder. Its JSON object contains the value. There is no reason for a client to concatenate that value into the statement text before sending this request.

Positional bindings remain supported. The documentation retains arrays in which the SQL contains ? placeholders and subsequent elements supply the values. Named support complements that form rather than requiring every existing parameterized request to be rewritten.

Names survive parsing and internal representation

The HTTP request parser first attempts to parse a simple list of SQL strings. If that fails, it attempts the nested parameterized form. For each nested statement, the first value must be a string containing SQL. Later elements supply parameters.

When one of those elements is a map, the parser iterates its keys and values. It creates a command parameter carrying both the value and its name. For an ordinary positional element, it creates a parameter with an empty name. The command model can therefore represent both forms without replacing the original SQL string with an interpolated statement.

The database conversion function then wraps each supported value with sql.Named, using the name stored in that command parameter. This is the final link in the mechanism. The name reaches Go's SQL interface with the value that belongs to it, instead of being used only as a convenient label during HTTP parsing.

The parser's map iteration order is consequently not the basis for a named binding's meaning. The association survives in each parameter's name. That is the practical advantage over asking a reader to match the seventh placeholder with the seventh element of a long array.

Parsing is not SQL validation

The tagged request-parser tests verify the new dictionary shape. They also verify malformed JSON, empty statements, non-string statement fields, and a mixture of incompatible outer request shapes. These are tests of how HTTP data becomes statement objects.

A successfully parsed request still has to be a valid SQL statement with acceptable bindings. The parser does not execute the statement while identifying its JSON structure. Some of its test strings are deliberately suitable only for inspecting that structure, so they should not be copied as examples of valid SQL parameter placement.

The parser tests also accept a nested statement containing only the SQL string and no actual parameters. This keeps the parameterized outer shape usable for a batch that includes statements with no values to bind. It is a structural convenience, not a promise that missing values for placeholders will pass the later database execution step.

There are value-type limits too. The parser's parameter constructor handles specified scalar forms and byte data in its internal interface. Its default branch returns an unsupported-type error. In particular, the shown constructor does not contain a null case, so the presence of JSON dictionaries should not be described as accepting arbitrary JSON values, nested documents, or null bindings without qualification.

I prefer that boundary to be visible in a client implementation. A named parameter object is a mapping of binding names to supported values. It is not a request to store an unrestricted JSON document merely because both use JSON object syntax.

Transaction and routing rules remain separate

The data API still scopes transactions through the transaction query parameter on a request. Its documentation says processing stops when a statement in such a transaction fails. Named bindings change how each statement receives its values. They do not create a transaction spanning multiple independent HTTP calls.

The same documentation says writes go through the leader and Raft log. A follower can transparently forward a leader-dependent request. A query's consistency level separately determines whether it can use local state or must involve the leader and consensus path. Naming parameters does not change those routing and consistency decisions.

The December addition is therefore an interface improvement with a concrete implementation through several layers. A JSON dictionary becomes named command parameters, and the database wrapper passes those names through sql.Named. Applications can retain positional requests or adopt names where they make a statement clearer, while keeping the existing request-level rules for transactions and cluster access.