River 0.4.0: A few breaking changes

This morning River 0.4.0 was cut, which for the first time in the project's release history, contains breaking changes. There was a bit of an internal debate on the philosophy of whether breaking changes were are worth it even if they do improve the API, but since the project's pre-V1 and the breakages are all quite small (most projects probably won't need to be updated), they shipped.

It'll sound counterintuitive, but in pursuit of minimizing pain, they're all going out in a single release. The hope is that any fixing effort involved is consolidated in a single upgrade, and that this kind of rare event won't happen again for a long time.

Job list API

JobListParams.State becomes JobListParams.States and can take more than one job state to filter on.

listRes, err := client.JobList(ctx, NewJobListParams().States(

Job listing now defaults to being order by ID instead of best fit timestamp. Projects can keep using the old behavior by explicitly specifying JobListOrderByTime:

river.NewJobListParams().OrderBy(JobListOrderByTime, SortOrderAsc)

JobListCursorFromJob no longer takes a sort order, instead procuring it when applied to a set of list parameters using JobListParams.After. The function now takes only a single parameter, the job row to act as cursor:

cursor := river.JobListCursorFromJob(jobRow)

Insert results

The client's Insert and InsertTx functions now return a JobInsertResult struct instead of raw JobRow. This leaves room in the result to include useful metadata like the new UniqueSkippedAsDuplicate property, so callers can tell whether an inserted job was skipped due to unique constraint.

insertRes, err := riverClient.Insert(ctx, MyArgs{...}, nil)

// true if job was skipped because of a preexisting unique equivalent

Pointer-sized ints preferred

The client's InsertMany and InsertManyTx now return number of jobs inserted as int instead of int64. This is a tiny tweak made so that the type in use is a little more idiomatic according to Go convention. We wouldn't have bothered unless other breaking changes were going out at the same time.

Job state aliases removed

The river.JobState* type aliases have been removed. All job state constants should be accessed through the canonical set of rivertype.JobState* constants instead.

This is another one that we wouldn't have otherwise bothered with, but removing the aliases tightens up the API in small ways that are desirable. The change means that there's now only one way of doing things (instead of having to choose between river.JobStateAvailable or rivertype.JobStateAvailable, both of which used to work), and aliases make the IDE experience a little suboptimal because using "jump to definition" requires an intermediate hop through the alias first.

Shallow cuts

We hate shipping API breakages as much as programmers hate integrating them, but there's some good news:

  • None of River's core APIs around working jobs change, and since the return value of Insert/InsertTx is often ignored anyway, many projects using River won't have to change any code.

  • All problems will be detected by Go's compiler. Since resolutions for each are trivial, upgrading projects that were affected will be fast.

An initial goal of the project was to take enough care with the initial API design that it'll never need a /v2. The changes above represent a few misses, but we consider it good news that nothing more fundamental has come up in the intervening months, and we're still on track for a single major version.