Notes from upgrading Rails

Since Rails 5 was released a month ago, we took some time to upgrade to Rails 4.1.x to not be so far behind.

Upgrading Rails is always a pain because it is connected to almost every bit of the application and you never know what will happen. Changes between 4.0.x and 4.1.x are documented and make for a good starting point, although, not everything is covered and there were three significant surprises.

The first one was peppered throughout the test suite and manifested in an invalid SQL SELECTs:

SELECT COUNT(column_a, column_b) FROM table

A little digging revealed that all problematic queries contained a call to select (limiting the returned columns) and called empty? before only conditionally retrieving all the records. In the old days, Rails would retrieve all the records, shove them into an array and then call empty? on that array. This behaviour changed — if the query is not loaded yet, a separate query is issued, otherwise the old behaviour remains. The generated query uses COUNT but it just wraps the column list in it. Well, it used to until 4.1.2, so an upgrade forward was enough.

The second problem was a bit more insidious. It manifested in Stack too deep errors. Those are always fun to debug! After some digging, we realized that it is connected to the changes in handling of JSON. Specifically serialization of test doubles lead to an infinite loop. The solution was to improve our tests so that no object serialized to JSON links to a double.

The last notable problem was with JSON serialized model fields — not fields which get serialized to JSON and stored in text columns, but fields backed by JSON datatype in Postgres. In several places, we use such fields to store additional information about a row, e.g. all inputs and intermediate steps of an invoice item price calculation.

Previously, such fields behaved like normal hashes — you could do anything with them and only when persisted they got serialized to JSON. Rails 4.1.x changed three things: it uses only string keys, it serializes the values immediately and most importantly does not allow assignment of a item in the hash!

The string keys are easy to handle by writing custom accessor. The serialization is really annoying to work with because we often store BigDecimal values in this way. Immediate serialization converts them into strings so we ended up writing a reader function which converts the string back to BigDecimal. It’s pretty straightforward but the access pattern had to change all over the application.

The last change was the most annoying one. Where previously we could write:

record.field[:key] = :value

to assign/change the :key, we now have to completely replace the field:

record.field = record.field.merge(key: :value)

To avoid this, we wrapped the assignment into a custom function. It required modifications all over the place, but the assignment is at least more explicit than a merge (which someone might see and replace as a way too complex).

All in all, this upgrade wasn’t without problems but not too painful either.

We're looking for developers to help us save energy

If you're interested in what we do and you would like to help us save energy, drop us a line at jobs@enectiva.cz.

comments powered by Disqus