D1 restores result rows to run
Cloudflare reverted a D1 response change after three days. The correction separates an intended API contract from the behavior existing Workers already depend on.
An API can disagree with its documentation for long enough that correcting the implementation breaks applications. Cloudflare's February 16 D1 update is a concrete example. The service has restored result rows to run(), reversing a change made on February 13. Existing Workers that read those rows can use the response shape they previously received.
The reversal announcement says run() again returns a D1Result, including rows. Cloudflare still describes a metadata-oriented result as the intended and documented behavior. It now says a future change to that behavior will require a compatibility date. The implementation can move toward the intended contract while existing applications retain a way to preserve their current behavior.
I value SQLite's small operational footprint, but a hosted service adds its own interface decisions around the engine. D1's response objects are part of that interface. Correct SQL execution does not make an application compatible with a changed JavaScript result.
Three methods had different problems
The February 13 release note explains the preceding change. It addressed raw(), all(), and run() together, with ORM compatibility among the reasons given. Their response shapes differ in ways that matter to callers independently of the SQL query.
For raw(), Cloudflare corrected the result to an array of arrays. An object uses property names to identify values. If a join returns two columns with the same name, a single object cannot represent both as distinct properties with that identical name. Positional arrays can retain both values because their positions distinguish them. This is a representation problem even when the query and its returned columns are valid.
The announcement also documents raw({columnNames: true}) for including column names in the result. That gives callers a way to obtain names while retaining a positional row representation. The unchanged all() method returns an array of objects, so the choice between the methods expresses a choice about how a caller wants to consume rows.
The run() change concerned whether the result contained rows at all. February 13 replaced its D1Result with a D1ExecResult. February 16 reverses that part of the update. Nothing in the reversal says that raw() should return to its former object representation, and conflating those changes would obscure which applications need which behavior.
A result object is observable behavior
The difference between execution metadata and result rows is substantial for application code. Metadata can tell a caller about execution, but it cannot substitute for values selected by the query. A program that consumes rows needs that content even if the method name suggests an operation whose main purpose is execution.
Conversely, a caller concerned only with whether a statement completed might never read the rows. That caller can remain unaffected by their removal while another caller fails immediately. The compatibility impact depends on the fields the application observes. Describing a change as a return-type correction does not establish how much deployed code depends on those fields.
This makes the February 16 note useful as a precise statement of present behavior. After the reversal, run() includes result rows. A migration discussion needs to begin with that fact, even if an older description or an intended design points elsewhere. The service's authors explicitly acknowledge the discrepancy and identify a mechanism for a later transition.
I consider the reversal appropriate because an application cannot infer a missing row from a developer's intended API design. Restoring the old response gives the service a chance to introduce the intended behavior with a controlled compatibility boundary. It also makes the response available to applications that had already been written against it.
Compatibility needs a transition mechanism
Cloudflare's commitment to gate a future run() change behind a compatibility date addresses the deployment problem directly. A service provider can update implementation code centrally. Application owners deploy on separate schedules, and their response handling may remain unchanged. A global response change couples those schedules even though the owners have not chosen to update their applications.
A compatibility boundary gives the provider a way to distinguish callers that have accepted a changed contract. The February note promises that boundary for this particular transition. It does not announce when the transition will happen, so there is no date to schedule an application migration against yet. Treating the stated intention as an already active change would repeat the same confusion about intended and actual behavior.
The distinction also helps interpret the methods. raw() solves positional row representation, while all() retains object-shaped rows. The restored run() behavior includes results despite the narrower result Cloudflare originally intended. Those facts belong to the D1 client interface, rather than to SQLite's SQL language.
For applications using an embedded database directly, the call boundary usually belongs to the selected library. A hosted SQLite service introduces another owner for that boundary. I find D1 interesting for precisely that reason. It keeps SQLite as the database engine while making interface compatibility an ongoing service responsibility. February's reversal defines the current run() contract and records how Cloudflare intends to change it without removing rows from existing Workers unexpectedly.