February 6, 2026·4 min read·engineeringrqlitereplicationsqlapi

rqlite 9.4.0 exposes the SQL that replication will use

The new /db/sql endpoint shows how rqlite rewrites nondeterministic SQL. It makes the distinction between agreeing on a statement and agreeing on its inputs visible to callers.

Replicating random() to several databases does not make them choose the same random value. rqlite handles supported nondeterministic expressions by rewriting them before replication, and 9.4.0, released on 6 February 2026, adds an endpoint for inspecting that transformation. /db/sql returns the original statement and its rewritten form without executing the database operation.

I value rqlite's proportionate approach to distributed relational storage. My year with it has left me interested in the exact point where ordinary SQLite semantics meet replication requirements. This endpoint exposes one of those points directly. It gives a developer something concrete to inspect when a statement contains a clock read or a random value.

A statement log needs agreement about external inputs

SQLite supplies the relational engine, while Raft supplies agreement on replicated operations. That still leaves the meaning of operations whose results depend on inputs outside the database state. A clock and a random-number generator can produce different answers on different machines even when the statement text is identical.

The tagged SQL processor shows the implementation. It parses relevant statements and walks their syntax tree. Supported calls to random() become numeric literals, and supported randomblob() calls become blob literals. Calls with a now argument in supported date/time functions replace that argument with a numeric Julian-day value.

The resulting statement carries a selected input explicitly. Each database evaluating that literal no longer has to obtain that particular input independently. This is the reason the transformation belongs before replicated execution. Agreement on the text after substitution includes the selected value as part of the operation.

This changes the useful debugging question. Asking whether SQLite supports a function is insufficient when the application depends on replicated results. The question also concerns which part of the expression rqlite transforms and which parts SQLite still evaluates when applying the operation.

Inspection does not require the table to exist

The HTTP handler accepts GET and POST. GET reads the statement from q, while POST parses the request body as statements. Its analysis path parses and rewrites SQL without issuing the operation to the store. Query permission controls access to the endpoint.

A compact request can inspect random-number rewriting without creating a table. The identifier in this example is only part of the statement being analyzed.

curl -G 'http://localhost:4001/db/sql?pretty' \
  --data-urlencode 'q=INSERT INTO samples VALUES(random())'

The response's results entries contain original, with rewritten or an error where appropriate. Analysis chooses a random value, so copying a particular numeric response into documentation would give a misleading impression of stable output. What matters is that the supported call becomes a literal in the inspected statement.

Inspection also does not reserve a value for a later execution request. The handler constructs a new rewriter for analysis and returns its result. A later request containing the original SQL goes through its own processing. An application that requires a chosen value to remain fixed should make that value part of the operation it actually submits.

Time rewriting preserves the surrounding expression

The HTTP tests cover date, time, datetime, julianday, and unixepoch forms using now. They check for a numeric Julian-day argument in the output. The suite also covers strftime, where the time input occupies a different argument position, and timediff.

This behavior differs from precomputing every function's final textual result. Replacing the time input lets SQLite still apply the surrounding function. A reader inspecting a rewritten datetime expression can see a fixed input to the function instead of an instruction to consult the local clock.

SQLite's date/time documentation defines the accepted time-value forms and modifiers. A Julian-day value is one supported representation. That documented input form makes the rewrite intelligible as SQL instead of requiring a separate replication-only date format.

The distinction matters for application semantics. A timestamp chosen during preprocessing represents the selected time input for that operation. It is not evidence of the instant each replica finished applying the write. Those instants belong to execution and can differ even when the stored value agrees.

Rewriting has boundaries

The rewriter treats random calls inside ordering terms differently, and the tests preserve ORDER BY RANDOM() in the inspected output. It also requires a literal numeric size for the supported random-blob substitution. These are specific rules implemented by the syntax-tree visitor, so the endpoint is useful precisely because a broad description such as automatic determinism can hide them.

The handler accepts norwrandom and norwtime query flags to disable the corresponding transformation during inspection. Comparing the forms can explain which differences come from rewriting and which come from the parser's SQL formatting. Textual changes alone do not establish that the database operation's meaning has changed.

A parser error is also distinct from executing against a schema. The analysis endpoint can report that it cannot parse a statement. A statement that it does parse may still name a missing table or violate a constraint if later executed. Inspection answers a question about preprocessing, and database execution answers the additional questions about the actual state.

The API makes a design constraint inspectable

The release changelog lists this API alongside an SQL-parser upgrade. That pairing reflects the dependency between the SQL accepted by the analyzer and the transformations its syntax tree can represent. Applications using unusual expressions can inspect the exact statements they intend to submit instead of guessing from the function name alone.

My preference is for this kind of explicit interface. It exposes the work required to keep a statement-based replicated database coherent without asking the user to infer it from a stored row afterward. For a statement containing a supported random or current-time expression, the developer can now see the literal input that analysis selects and the surrounding SQL that remains to be evaluated.