San Francisco, USA

5214F Diamond Heights Blvd #553
San Francisco, CA 94131

Pune, India

203, Jewel Towers, 2nd Floor
Lane Number 5, Koregaon Park
Pune 411001, India

301 - 275 - 3997
hello@BigBinary.com

Database tasks can skip test database with an environment variable

This blog is part of our Rails 6.1 series.

In Rails 6.1, Rails will skip modifications to the test database if SKIP_TEST_DATABASE is set to true.

Without the environment variable

> bundle exec rake db:create
Created database 'app_name_development'
Created database 'app_name_test'

With the environment variable

> SKIP_TEST_DATABASE=true bundle exec rake db:create
Created database 'app_name_development'

As we can see in the first example, both a development and a test database were created, which is unexpected when directly invoking db:create. One obvious solution to this problem is to force the development environment to only create a development database. However this solution will break bin/setup as mentioned in this commit. Hence the need for an environment variable to skip test database creation.

Check out the pull request for more details.


Rails 6.1 supports order DESC for find_each, find_in_batches, and in_batches

This blog is part of our Rails 6.1 series.

Before Rails 6.1, batch processing methods like find_each, find_in_batches and in_batches didn’t support the ORDER BY clause. By default the order was set to id ASC.

> User.find_each{|user| puts user.inspect}

User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1000]]

Rails 6.1 now supports ORDER BY id for ActiveRecord batch processing methods like find_each, find_in_batches, and in_batches. This would allow us to retrieve the records in ascending or descending order of ID.

> User.find_each(order: :desc){|user| puts user.inspect}

User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1000]]
> User.find_in_batches(order: :desc) do |users|
>   users.each do |user|
>     puts user.inspect
>   end
> end

User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1000]]
> User.in_batches(order: :desc) do |users|
>   users.each do |user|
>     puts user.inspect
>   end
> end

(0.2ms)  SELECT "users"."id" FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1000]]
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ?  [["id", 101]]

Points to remember:

  • The ORDER BY clause only works with the primary key column.
  • Valid values for the ORDER BY clause are [:asc,:desc] and it’s case sensitive. If we use caps or title case (like DESC or Asc) then we’ll get an ArgumentError as shown below.
> User.find_in_batches(order: :DESC) do |users|
>   users.each do |user|
>     puts user.inspect
>   end
> end

Traceback (most recent call last):
        2: from (irb):5
        1: from (irb):6:in `rescue in irb_binding'
ArgumentError (unknown keyword: :order)

Check out the pull request for more details.


Ruby 3 adds Symbol#name

This blog is part of our Ruby 3.0 series.

All are excited about what Ruby 3.0 has to offer to the Ruby developers. There is already a lot of buzz that the feature set of Ruby 3.0 will change the perspective of developers how they look at Ruby.

One of the important aspects of Ruby 3.0 is optimization. The part of that optimization is the introduction of name method for Symbol. In this blog, we will take a look at what name method of class Symbol does and why it was introduced. The new name method is introduced on Symbol to simply convert a symbol into a string. Symbol#name returns a string. Let’s see how it works.

irb(main):001:0> :simba.name
=> 'simba'
irb(main):002:0> :simba.name.class
=> String
irb(main):003:0> :simba.name === :simba.name
=> true

Wait what? Don’t we have to_s to convert a symbol into a string. Most of us have used to_s method on a Symbol. The to_s method returns a String object and we can simply use it. But why name?

Using to_s is okay in most cases. But the problem with to_s is that it creates a new String object everytime we call it on a symbol. We can verify this in irb.

irb(main):023:0> :simba.to_s.object_id
=> 260
irb(main):024:0> :simba.to_s.object_id
=> 280

Creating a new object for every symbol to a string conversion allocates new memory which increases overhead. The light was thrown on this issue by schneems (Richard Schneeman) in a talk at RubyConf Thailand where he showed how Symbol#to_s allocation causes significant overhead in ActiveRecord. This inspired Ruby community to have a new method name on Symbol which returns a frozen string object. This reduces the string allocations dramatically which results in reducing overhead.

irb(main):001:0> :simba.name.frozen?
=> true
irb(main):002:0> :simba.name.object_id
=> 200
irb(main):003:0> :simba.name.object_id
=> 200

The reason to bring this feature was that most of the times we want a simple string representation for displaying purpose or to interpolate into another string. The result of to_s is rarely mutated directly. By introducing this method we save a lot of objects which helps in optimization. Now we know the benefits of name, we should prefer using name over to_s when we don’t want to mutate a string.

For more information on discussion, official documentation, please head on to Feature #16150 discussion, Pull request and Ruby 3.0 official release preview.


React 17 delegates events to root instead of document

React has been doing event delegation automatically since its first release. It attaches one handler per event type directly at the document node.

Though it improves the performance of an application, many issues have been reported due to the event delegation on the document node.

To demonstrate one of the issues let’s take an example of a select dropdown.

CountryDropDown in the below example is a React component for country selection, which would be rendered to a div with id react-root. The react DOM container is wrapped inside a div with id main that has a change event containing stopPropagation().

<!--Div's change event contains stopPropagation()-->
<div id="main">
  <!--Div where react component will be rendered -->
  <div id="react-root">
  </div>
</div>
class CountryDropDown extends React.Component {
  state = {
    country: '',
  }
  const handleChange = e => {
    this.setState({ country: e.target.value });
  }
  render() {
    return (
      <table class="table table-striped table-condensed">
        <thead>
          <tr>
            <th>Country</th>
            <th>Selected country</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>
              <select value={this.state.country}
                onChange={this.handleChange}
              >
                <option value="">--Select--</option>
                <option value="India">India</option>
                <option value="US">US</option>
                <option value="Dubai">Dubai</option>
              </select>
            </td>
            <td>
              {this.state.country}
            </td>
          </tr>
        </tbody>
      </table>
    );
  }
}
ReactDOM.render(<CountryDropDown />, document.getElementById('react-root'));

Attaching change event to the main div

document.getElementById('main').addEventListener('change', function (e) {
  e.stopPropagation();
}, false);

When a country is selected, we cannot see the selected country. Watch this video to see it in action.

The reason for this unexpected behavior is the onChange event of dropdown which is attached to the document node. The change event of the main div containing e.stopPropagation() prevents the onChange event of dropdown.

To fix such issues, React 17 no longer attaches event handlers at the document level. Instead, it attaches them to the root DOM container into which React tree is rendered.

event delegation Image is taken from React 17 blog.

Changes in React 17

After changes in React 17, events are attached to the root DOM container into which the React tree is rendered. In our example, onChange event of dropdown would be attached to the div with id react-root. This event would be triggered when any country is selected rendering the expected behavior. To see the solution in action, check out the video.

Note

React 17 release candidate can be installed from here.

Check out the earlier discussion on event delegation here and the pull request here.


Rails 6.1 deprecates rails db:structure:dump and rails db:structure:load

This blog is part of our Rails 6.1 series.

Rails 6.1 deprecates rails db:structure:load and rails db:structure:dump tasks.

Before Rails 6.1, executing rake db:schema:dump would dump db/schema.rb file. And executing rake db:structure:dump would dump db/structure.sql file.

Rails provides config.active_record.schema_format setting for which the valid values are :ruby or :sql. However, since there are specific tasks for db:structure and db:schema this value was not really being used.

Changes in Rails 6.1

In Rails 6.1 the Rails team decided to combine the two different tasks into a single task. In Rails 6.1 rails db:structure:dump and rails db:structure:load have been deprecated and the following message would be shown.

Using `bin/rails db:structure:dump` is deprecated and will be removed in Rails 6.2. Configure the format using `config.active_record.schema_format = :sql` to use `structure.sql` and run `bin/rails db:schema:dump` instead.

Now Rails will start taking into the account value set for config.active_record.schema_format.

rails db:schema:dump and rails db:schema:load would do the right thing based on the value set for config.active_record.schema_format.

Check out the pull request for more details on this.


Ruby 2.8 adds endless method definition

This blog is part of our Ruby 2.8 series.

Ruby 2.8 adds endless method definition. It enables us to create method definitions without the need of end keyword. It is marked as an experimental feature.

# endless method definition
>> def raise_to_power(number, power) = number ** power

>> raise_to_power(2, 5)

=> 32

The discussion around it can be found here. Check out the pull request for more details on this.


Rails 6.1 adds --minimal option support

This blog is part of our Rails 6.1 series.

rails new my_app creates a new Rails application fully loaded with all the features.

If we want to omit some of the features then we needed to skip them like this.

# before Rails 6.1

$ rails new tiny_app
    --skip-action-cable
    --skip-action-mailer
    --skip-action-mailbox
    --skip-action-text
    --skip-active-storage
    --skip-bootsnap
    --skip-javascript
    --skip-spring
    --skip-system-test
    --skip-webpack-install
    --skip-turbolinks

Before Rails 6.1 it was not possible to skip things like active_job and jbuilder.

Rails 6.1

Rails 6.1 added a new option --minimal.

$ rails new tiny_app --minimal

All the following are excluded from this minimal Rails application.

  • action_cable
  • action_mailbox
  • action_mailer
  • action_text
  • active_job
  • active_storage
  • bootsnap
  • jbuilder
  • spring
  • system_tests
  • turbolinks
  • webpack

We can bundle webpack in this minimal app like this.

$ rails new tiny_app --minimal webpack=react

Database option can also be passed.

$ rails new tiny_app --minimal --database postgresql webpack=react

Check out the pull request for more details on this.


Rails 6 adds support to persist timezones of Active Job

This blog is part of our Rails 6 series. Rails 6.0 was recently released.

When a job is enqueued in Rails 6 using Active Job, the current timezone of a job is preserved and then this preserved timezone is restored when the job is finished executing.

Let’s take an example of sale at Amazon.

Amazon would like to remind users across different timezones about its upcoming sale by sending an email. This task of sending a reminder would be processed as a background job.

Before:

Before Rails 6, we had to pass timezone explicitly to the perform method of the job as shown below.

timezone = "Eastern Time (US & Canada)"

AmazonSaleJob.perform_later(Time.now, timezone)

class AmazonSaleJob < ApplicationJob
  queue_as :default

  def perform(time, timezone)

    time = time.in_time_zone(timezone)
    sale_start_time = localtime(2020, 12, 24)

    if time >= sale_start_time
      puts "Sale has started!"
      #Send an email stating Sale has started
    else
      sale_starts_in = (sale_start_time - time).div(3600)
      puts "Hang on! Sale will start in #{sale_starts_in} hours"
      #Send an email stating sales starts in sale_starts_in hours
     end
  end

  private

    def localtime(*args)
      Time.zone ? Time.zone.local(*args) : Time.utc(*args)
    end
end

After:

After the changes in Rails 6, passing timezone to Job is now taken care of by Rails.

timezone = "Eastern Time (US & Canada)"

Time.use_zone(timezone) do
  AmazonSaleJob.perform_later(Time.zone.now)
end

class AmazonSaleJob < ApplicationJob
  queue_as :default

  def perform(time)
    sale_start_time = localtime(2020, 12, 24)

    if time >= sale_start_time
      puts "Sale has started!"
      #Send an email stating Sale has started
    else
      sale_starts_in = (sale_start_time - time).div(3600)
      puts "Hang on! Sale will start in #{sale_starts_in} hours"
      #Send an email stating sales starts in sale_starts_in hours
     end
   end

  private

    def localtime(*args)
      Time.zone ? Time.zone.local(*args) : Time.utc(*args)
    end
end

Rails 6 also propagates timezone to all the subsequent nested jobs.


Ruby 2.7 adds Beginless Range

This blog is part of our Ruby 2.7 series. Ruby 2.7.0 was released on Dec 25, 2019.

Ruby 2.7 added support for Beginless Range which makes the start of range an optional parameter.

(..100) is a Beginless Range and it is equivalent to (nil..100).

Let’s see how Beginless Range could be used.

> array = (1..10).to_a

# Select first 6 elements
> array[..5]
=> [1, 2, 3, 4, 5, 6]

# Select first 5 elements
> array[...5]
=> [1, 2, 3, 4, 5]

# grep (INFINITY..5) in (1..5)
> (1..10).grep(..5)
=> [1, 2, 3, 4, 5]

# (..100) is equivalent to (nil..100)
> (..100) == (nil..100)
=> true

Here is another example where in the case statement the condition can be read as below the specified level.

case temperature
when ..-15
  puts "Deep Freeze"
when -15..8
  puts "Refrigerator"
when 8..15
  puts "Cold"
when 15..25
  puts "Room Temperature"
when (25..)   # Kindly notice the brackets here
  puts "Hot"
end

It can also be used for defining constants for ranges.

TEMPERATURE = {
  ..-15  => :deep_freeze,
  -15..8 => :refrigerator,
  8..15  => :cold,
  15..25 => :room_temperature,
  25..   => :hot
end

Using Beginless Range in DSL makes it easier to write conditions and it looks more natural.

# In Rails
User.where(created_at: (..DateTime.now))
# User Load (2.2ms)  SELECT "users".* FROM "users" WHERE "users"."created_at" <= $1 LIMIT $2  [["created_at", "2020-08-05 15:00:19.111217"], ["LIMIT", 11]]


# In RubySpec
ruby_version(..'1.9') do
# Tests for old Ruby
end

Here is the relevant commit and discussion regarding this change.


Rails 6 adds Array#including/excluding and Enumerable#including/excluding

This blog is part of our Rails 6 series. Rails 6.0 was recently released.

Rails 6 added including and excluding on Array and Enumerable.

Array#including and Enumerable#including

including can be used to extend a collection in a more object oriented way. It does not mutate the original collection but returns a new collection which is the the concatenation of the given collections.

# multiple arguments can be passed to including
>> [1, 2, 3].including(4, 5)
=> [1, 2, 3, 4, 5]

# another enumerable can also be passed to including
>> [1, 2, 3].including([4, 5])
=> [1, 2, 3, 4, 5]

>> %i(apple orange).including(:banana)
=> [:apple, :orange, :banana]

# return customers whose country_code is IN along with the prime customers
>> Customer.where(country_code: "IN").including(Customer.where(prime: true))

Array#excluding and Enumerable#excluding

It returns a copy of the enumerable excluding the given collection.

# multiple arguments can be passed to including
>> [11, 22, 33, 44].excluding([22, 33])
=> [11, 44]

>> %i(ant bat cat).excluding(:bat)
=> [:ant, :cat]

# return all prime customers except those who haven't added their phone
>> Customer.where(prime: true).excluding(Customer.where(phone: nil))

Array#excluding and Enumerable#excluding replaces the already existing method without which in Rails 6 is now aliased to excluding.

>> [11, 22, 33, 44].without([22, 33])
=> [11, 44]

excluding and including helps to shrink or extend a collection without using any operator.

Check out the commit for more details on this.


Rails 6.1 raises on db:rollback for multiple database applications

This blog is part of our Rails 6.1 series.

Rails 6.1 adds support to handle db:rollback in case of multiple database application.

Prior to this change, on executing db:rollback Rails used to rollback the latest migration from the primary database. If we passed on a [:NAME] option along with to specify the database, we used to get an error. Check out the issue for more details.

Rails 6.0.0

> rails db:rollback:secondary

rails aborted!
Don't know how to build task `db:rollback:secondary` (See the list of available tasks with `rails --tasks`)
Did you mean?  db:rollback

Staring with Rails 6.1, we need to pass the database name along with db:rollback:[NAME] otherwise a RuntimeError is raised.

Rails 6.1.0

> rails db:rollback

rails aborted!
You're using a multiple database application. To use `db:migrate:rollback` you must run the namespaced task with a VERSION. Available tasks are db:migrate:rollback:primary and db:migrate:rollback:secondary.

> rails db:rollback:primary

== 20200731130500 CreateTeams: reverting ======================================
-- drop_table(:teams)
   -> 0.0060s
== 20200731130500 CreateTeams: reverted (0.0104s) =============================

Check out the pull request for more details on this.


How to render liquid templates when the template refers to other liquid templates

Shopify’s Liquid Templates is a great way for templating in Ruby on Rails applications.

If the template is as simple as this one then there are no issues.

{% if user %}
  Hello {{ user.name }}
{% endif %}

However sometimes we have a liquid template which is using another liquid template. Here is an example.

home.liquid
<!DOCTYPE html>
<html>
  <head>
    <style>{% asset 'main.css' %}</style>
  </head>
  <body>
    {% partial 'header' %}
    <h1>Home Page</h1>
  </body>
</html>

In the above case home.liquid is using two other liquid templates main.css and header.liquid.

Let’ see what these templates look like.

main.css
* {
  color: {{ theme.text_color }};
}
a {
  color: {{ theme.link_color }};
}
header.liquid
<nav>
{{ organization.name }}
</nav>

In order to include the assets and the partials we need to create liquid tags.

Let’s create a tag which will handle assets.

# app/lib/liquid/tags/asset.rb

module Liquid
  module Tags
    class Asset < Liquid::Tag
      def initialize(tag_name, name, tokens)
        super
        @name = name.strip.remove("'")
      end

      def render(context)
        new_context = context.environments.first
        asset = Template.asset.find_by(filename: @name)

        Liquid::Template.parse(asset.content).render(new_context).html_safe
      end
    end
  end
end

Let’s create a tag that will handle partials.

# app/lib/liquid/tags/partial.rb

module Liquid
  module Tags
    class Partial < Liquid::Tag
      def initialize(tag_name, name, tokens)
        super
        @name = name.strip.remove("'")
      end

      def render(context)
        new_context = context.environments.first

        # Remember here we are not passing extension
        asset = Template.partial.find_by(filename: @name + ".liquid")

        Liquid::Template.parse(asset.content).render(new_context).html_safe
      end
    end
  end
end

Let’s create a new initializer and we need to register these tags in that initializer.

# config/initializers/liquid.rb

require 'liquid/tags/asset'
require 'liquid/tags/partial'

Liquid::Template.register_tag('asset', Liquid::Tags::Asset)
Liquid::Template.register_tag('partial', Liquid::Tags::Partial)

Restart the server and now we can render the home.liquid template like this.

template = Template.template.find_by(filename: "home.liquid")

attributes = {
  organization: {
    name: "Example"
  },
  theme: {
    text_color: "#000000",
    link_color: "#DBDBDB"
  }
}

Liquid::Template.parse(template.content).render(attributes).html_safe

Here we have a simple implementation of the tags. We can do much more, if needed, like looping over items to parse each item from the partial. That can be done by registering a separate tag for the item and passing in the id of the item so that the specific item can be found and parsed.


Rails 6.1 deprecates the use of return, break or throw to exit a transaction block

This blog is part of our Rails 6.1 series.

Rails 6.1 deprecates the use of return, break or throw to exit a transaction block.

return / break

>> Post.transaction do
>>   @post.update(post_params)
>>
>>   break # or return
>> end

# => TRANSACTION (0.1ms)  begin transaction
# => DEPRECATION WARNING: Using `return`, `break` or `throw` to exit a transaction block is
# => deprecated without replacement. If the `throw` came from
# => `Timeout.timeout(duration)`, pass an exception class as a second
# => argument so it doesn't use `throw` to abort its block. This results
# => in the transaction being committed, but in the next release of Rails
# => it will rollback.
# => TRANSACTION (0.8ms)  commit transaction

throw

>> Timeout.timeout(1) do
>>   Post.transaction do
>>     @post.update(post_params)
>>
>>     sleep 3 # simulate slow request
>>   end
>> end

# => TRANSACTION (0.1ms)  begin transaction
# => DEPRECATION WARNING: Using `return`, `break` or `throw` to exit a transaction block is
# => deprecated without replacement. If the `throw` came from
# => `Timeout.timeout(duration)`, pass an exception class as a second
# => argument so it doesn't use `throw` to abort its block. This results
# => in the transaction being committed, but in the next release of Rails
# => it will rollback.
# => TRANSACTION (1.6ms)  commit transaction
# => Completed 500 Internal Server Error in 1022ms (ActiveRecord: 3.2ms | Allocations: 9736)
# => Timeout::Error (execution expired)

Here, even when the error was thrown the transaction is committed. This is something which is going to change in the future versions.

This is done because currently, when a transaction block is wrapped in Timeout.timeout(duration) i.e. without second argument(an exception class) then it uses throw to exit the transaction.

Solution

>> Timeout.timeout(1, Timeout::Error) do
>>   Post.transaction do
>>     @post.update(post_params)
>>
>>     sleep 3 # simulate slow request
>>   end
>> end

# => TRANSACTION (0.1ms)  begin transaction
# => TRANSACTION (0.7ms)  rollback transaction
# => Timeout::Error (execution expired)

Check out the pull request for more details on this.


Rails 6.1 automatically generates an abstract class when using multiple databases

This blog is part of our Rails 6.1 series.

Rails started supporting multiple databases from Rails 6.0. To use a specific database, we can specify the database connection in the model using connects_to. In the following case we want Person model to connect to crm database.

class Person < ApplicationRecord
  connects_to database: { writing: :crm }
end

As the application grows, more and more models start sharing the same database. Now a lot of models may contain connects_to call to the same database.

class Person < ApplicationRecord
  connects_to database: { writing: :crm }
end

class Order < ApplicationRecord
  connects_to database: { writing: :crm }
end

class Sale < ApplicationRecord
  connects_to database: { writing: :crm }
end

In order to avoid the duplication, we can create an abstract class connecting to a database and manually inherit all other models from that class. This could look like this.

class CrmRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :crm }
end

class Person < CrmRecord
end

class Order < CrmRecord
end

class Sale < CrmRecord
end

Rails 6.1

Before Rails 6.1 we had no choice but to create that abstract class manually. Rails 6.1 allows us to generate an abstract class when we are generating a model using scaffold.

$ rails g scaffold Person name:string --database=crm

It creates an abstract class with the database’s name appended with Record. The generated model automatically inherits from the new abstract class.

# app/models/users_record.rb
class CrmRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :crm }
end

# app/models/admin.rb
class Person < CrmRecord
end

If the abstract class already exists, it is not created again. We can also use an existing class as the abstract class by passing parent option to the scaffold command.

$ rails g scaffold Customer name:string --database=crm --parent=PrimaryRecord

This skips generating CrmRecord class as we have specified Rails to use PrimaryRecord abstract class as its parent.

Check out the pull request for more details on this.


Rails 6.1 adds annotate_rendered_view_with_filenames to annotate HTML output

This blog is part of our Rails 6.1 series.

Rails 6.1 makes it easier to debug rendered HTML by adding the name of each template used.

Rails 6.1

Add the following line in development.rb file to enable this feature.

config.action_view.annotate_rendered_view_with_filenames = true

Now the rendered HTML will contain comment indicating the beginning and end of each template.

Here is an example.

Annotated HTML output

In the image we can see the begin and end for each of the templates. It helps a lot in debugging webpages to find out which template is rendered. Check out the pull request for more details on this.


Rails 6.1 allows enums attributes to configure the default value

Rails 6.1 makes it easier to configure a default value for Active Record enum attributes.

Let’s take an example of blog posts with status and category columns.

class Post < ApplicationRecord
  enum status: %i[draft reviewed published]
  enum category: { rails: "Rails", react: "React" }
end

Before Rails 6.1, defaults for enum attributes can be configured by applying default on the database level.

class AddColumnStatusToPosts < ActiveRecord::Migration[6.0]
  def change
    add_column :posts, :status, :integer, default: 0
    add_column :posts, :category, :string, default: "Rails"
  end
end

After Rails 6.1, defaults for enum attributes can be configured directly in the Post model using _default option.

class Post < ApplicationRecord
  enum status: %i[draft reviewed published], _default: "draft"
  enum category: { rails: "Rails", react: "React" }, _default: "Rails"
end

The new approach to set enum defaults has following advantages. Let’s understand keeping the context of Post model with category as an example.

  • When the category default value changes from Rails to React. We have to add a new migration in Rails 6 and previous versions to update the database column default.
  • Let say the default value for post category(i.e: Rails) is removed from the enum from Post model. Rails 6 and previous versions wouldn’t throw an exception and continue to work without setting any default value. Rails 6.1 with new syntax would raise an exception.

Check out the pull request for more details on this.


Rails 6.1 adds support for where with a comparison operator

Rails 6.1 adds support to comparison operator in the where clause. The four comparison operators supported are:

  • Greater than (>).
  • Greater than equal to (>=).
  • Less than (<).
  • Less than equal to (<=).

The comparison operator is also supported by the finder methods in ActiveRecord which internally uses where clause, for example: find_by, destroy_by, delete_by.

The new style for comparisons has to follow advantages:

  • The where clause with the comparison operator doesn’t raise an exception when ActiveRecord::Relation uses ambiguous column name.
  • The where clause with the comparison operator handle proper precision of the database columns.

Before Rails 6.1, to add a condition with comparison in where clause, we had to add raw SQL notation.

Rails 6.0.0

>> Post.where("DATE(published_at) > DATE(?)", Date.today)
# => <ActiveRecord::Relation [...]>

>> Post.find_by("likes < ?", 10)
# => <ActiveRecord::Relation [...]>

# Following query on execution would raise exception.
>> Post.joins(:comments).where("likes > 10")
# => ambiguous column name: id

Rails 6.1.0

>> Post.where("published_at >": Date.today)
# => <ActiveRecord::Relation [...]>

>> Post.find_by("likes <": 10)
# => <ActiveRecord::Relation [...]>

# Following query on execution would NOT raise exception.
>> Post.joins(:comments).where("likes >": 10)
# => <ActiveRecord::Relation [...]>

Check out the pull request for more details on this.


Rails 6.1 tracks Active Storage variant in the database

This blog is part of our Rails 6.1 series.

Active Storage variants are the transformation of the original image. These variants can be used as thumbnails, avatars, etc.

Active Storage generates variants on demand by downloading the original image. The image is transformed into a variant and is stored to the third party services like S3.

When a request to fetch a variant for an Active Storage object is made, Rails checks if the variant is already been processed and is already available on S3 or not. But to do so Rails has to make a call to find out if the variant is available on S3. This extra call adds to the latency.

Active Storage has to wait until the image variant check call is completed because S3 might not return the image when a GET request is made due to eventual consistency. This way Rails avoid downloading a broken image from S3 and uploading broken image variant to S3 in case the variant is not present.

In Rails 6.1, Active Storage tracks the presence of the variant in the database. This change avoids unnecessary variant presence remote request made to the S3 and directly fetches or generates a image variant.

In Rails 6.1, the configuration to allow variant tracking in the database is by default set to true.

config.active_storage.track_variants: true

Check out the pull request for more details on this.


Ruby 2.7 adds Enumerable#filter_map

This blog is part of our Ruby 2.7 series. Ruby 2.7.0 was released on Dec 25, 2019.

Ruby 2.7 adds Enumerable#filter_map which is a combination of filter + map as the name indicates. The ‘filter_map’ method filters and map the enumerable elements within a single iteration.

Before Ruby 2.7, we could have achieved the same with 2 iterations using select & map combination or map & compact combination.

irb> numbers = [3, 6, 7, 9, 5, 4]

# we can use select & map to find square of odd numbers

irb> numbers.select { |x| x.odd? }.map { |x| x**2 }
=> [9, 49, 81, 25]

# or we can use map & compact to find square of odd numbers

irb> numbers.map { |x| x**2 if x.odd? }.compact
=> [9, 49, 81, 25]

Ruby 2.7

Ruby 2.7 adds Enumerable#filter_map which can be used to filter & map the elements in a single iteration and which is more faster when compared to other options described above.

irb> numbers = [3, 6, 7, 9, 5, 4]
irb> numbers.filter_map { |x| x**2 if x.odd? }
=> [9, 49, 81, 25]

The original discussion had started 8 years back. Here is the latest thread and github commit for reference.


Ruby 2.7 deprecates conversion of keyword arguments

A notable change has been announced for Ruby 3 for which deprecation warning has been added in Ruby 2.7. Ruby 2.7 deprecated automatic conversion of keyword arguments and positional arguments. This conversion will be completely removed in Ruby 3.

Ruby 2.7 NEWS has listed the spec of keyword arguments for Ruby 3.0. We will take the examples mentioned there and for each scenario we will look into how we can fix them in the existing codebase.

Scenario 1

When method definition accepts keyword arguments as the last argument.

def sum(a: 0, b: 0)
  a + b
end

Passing exact keyword arguments in a method call is acceptable, but in applications we usually pass a hash to a method call.

sum(a: 2, b: 4) # OK

sum({ a: 2, b: 4 }) # Warned

In this case, we can add a double splat operator to the hash to avoid deprecation warning.

sum(**{ a: 2, b: 4 }) # OK

Scenario 2

When method call passes keyword arguments but does not pass enough required positional arguments.

If the number of positional arguments doesn’t match with method definition, then keyword arguments passed in method call will be considered as the last positional argument to the method.

def sum(num, x: 0)
  num.values.sum + x
end
sum(a: 2, b: 4) # Warned

sum(a: 2, b: 4, x: 6) # Warned

To avoid deprecation warning and for code to be compatible with Ruby 3, we should pass hash instead of keyword arguments in method call.

sum({ a: 2, b: 4 }) # OK

sum({ a: 2, b: 4}, x: 6) # OK

Scenario 3

When a method accepts a hash and keyword arguments but method call passes only hash or keyword arguments.

If a method arguments are a mix of symbol keys and non-symbol keys, and the method definition accepts either one of them then Ruby splits the keyword arguments but also raises a warning.

def sum(num={}, x: 0)
  num.values.sum + x
end
sum("x" => 2, x: 4) # Warned

sum(x: 2, "x" => 4) # Warned

To fix this warning, we should pass hash separately as defined in the method definition.

sum({ "x" => 4 }, x: 2) # OK

Scenario 4

When an empty hash with double splat operator is passed to a method that doesn’t accept keyword arguments.

Passing keyword arguments using double splat operator to a method that doesn’t accept keyword argument will send empty hash similar to earlier version of Ruby but will raise a warning.

def sum(num)
  num.values.sum
end
numbers = {}
sum(**numbers) # Warned

To avoid this warning, we should change method call to pass hash instead of using double splat operator.

numbers = {}
sum(numbers) # OK

Added support for non-symbol keys

In Ruby 2.6.0, support for non-symbol keys in method call was removed. It is added back in Ruby 2.7. When method accepts arbitrary keyword arguments using double splat operator then non-symbol keys can also be passed.

def sum(**num)
  num.values.sum
end
ruby 2.6.5
sum("x" => 4, "y" => 3)
=> ArgumentError (wrong number of arguments (given 1, expected 0))

sum(x: 4, y: 3)
=> 7
ruby 2.7.0
sum("x" => 4, "y" => 3)
=> 7

sum(x: 4, y: 3)
=> 7

Added support for **nil

Ruby 2.7 added support for **nil to explicitly mention if a method doesn’t accept any keyword arguments in method call.

def sum(a, b, **nil)
  a + b
end

sum(2, 3, x: 4)
=> ArgumentError (no keywords accepted)

To suppress above deprecation warnings we can use -W:no-deprecated option.

In conclusion, Ruby 2.7 has worked big steps towards changing specification of keyword arguments which will be completely changed in Ruby 3.

For more information on discussion, code changes and official documentation, please head to Feature #14183 discussion, pull request and NEWS release.