Introducing BabySqueel: Elegant queries for Rails

Introducing a new Ruby gem for building elegant queries without SQL strings.

by Ray Zane

Remember the Squeel Ruby gem? It is a really nice way to build complex queries for Active Record in Ruby instead of resorting to SQL strings.

Squeel lets you rewrite...

Article.where('created_at >= ?', 2.weeks.ago)

Article.where { created_at >= 2.weeks.ago }


Person.where('(name LIKE ? AND salary < ?) OR (name LIKE ? AND salary > ?)', 'Ernie%', 50000, 'Joe%', 100000)

Person.where { (name =~ 'Ernie%') & (salary < 50000) | (name =~ 'Joe%') & (salary > 100000) }

Isn't that nice? Yes. Yes it is.

It promised a world of beautiful, elegant queries. And yet, we still have to resort to SQL strings and those ugly Arel queries when we have a complex query to write. Why is that?

Why isn't everyone using Squeel?

Historically, Squeel has had trouble keeping up with the latest versions of Active Record. Currently, it is incompatible with Rails 5 and efforts to upgrade it have been frustrating.

Squeel was originally intended to become part of the official Active Record API and is implemented by monkey-patching Active Record internals and overriding Active Record's query methods. Unfortunately for all of us, that inclusion never happened, and it left Squeel susceptible to breakage from arbitrary changes in Active Record.

Introducing: BabySqueel

BabySqueel provides a Squeel-like query DSL for Active Record while attempting to avoid version upgrade difficulties that hamstrung Squeel by avoiding monkey-patches to the ever-changing Active Record internals.

Here are some random examples from the README:

Post.selecting { (id + 5).as('id_plus_five') }
# SELECT ("posts"."id" + 5) AS id_plus_five FROM "posts"

Post.joins(:author).selecting { [id,] }
# SELECT "posts"."id", "authors"."id" FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"

Post.joins(author: :posts).where.has { author.posts.title =~ '%fun%' }
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# INNER JOIN "posts" "posts_authors" ON "posts_authors"."author_id" = "authors"."id"
# WHERE ("posts_authors"."title" LIKE '%fun%')

Post.joining { author.outer.posts }
# SELECT "posts".* FROM "posts"
# LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# INNER JOIN "posts" "posts_authors" ON "posts_authors"."author_id" = "authors"."id"

Querying with strings works fine, until you need composability. Take this example:

class Dog < ActiveRecord::Base
  scope :old,        -> { where('age > ?', 5) }
  scope :named_fido, -> { where(name: 'Fido') }

  def self.named_fido_or_old
    where('age > ? OR name = ?', 5, 'Fido')

Ugh... repetition. That's a contrived example, but you can imagine what happens on larger projects with more complicated business logic. Here is the equivalent code, built using BabySqueel. Pay attention to the fact that sifters can be combined and return little bits of logic using Arel.

class Dog < ActiveRecord::Base
  sifter(:old)        { age > 5 }
  sifter(:named_fido) { name == 'Fido' }

  scope :old,        -> { where.has { sift(:old) } }
  scope :named_fido, -> { where.has { sift(:named_fido) } }

  def self.old_or_named_fido
    where.has { sift(:old) | sift(:named_fido) }

(Bad) reasons not to use BabySqueel

Won't BabySqueel be brittle? What about Rails 6?

BabySqueel currently supports both Rails 4 and 5 with very little code needed to accommodate specific versions. BabySqueel's goal is to avoid the majority of version upgrade difficulties by keeping monkey-patching to an absolute minimum.

Like Squeel and Ransack, BabySqueel depends on Polyamorous. While Polyamorous does monkey-patch Active Record's JoinDependency to allow outer joining specific relations, it is extremely well-tested and is always up to date with the latest versions of Active Record.

In the future, it is very unlikely that BabySqueel would prevent you from upgrading Rails.

I'm already using Squeel. Won't migrating be hard?

Nope, it's easy. Because the API is so similar to Squeel's, the migration process can usually be completed with a couple of find-and-replaces. Check out the migration guide for more details.

If you're desperate, you can turn on compatibility mode, which makes it even easier to move to BabySqueel. But try to only use Compatibility Mode as a stepping stone.

Try it out

So try it out. I look forward to your issues and pull requests.

Thanks to Ernie Miller whose beautiful vision for Squeel inspired BabySqueel.