• Stars
    star
    502
  • Rank 87,851 (Top 2 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 8 years ago
  • Updated 11 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🐷 An expressive query DSL for Active Record

BabySqueel 🐷

Build Version

Have you ever used the Squeel gem? It's a really nice way to build complex queries. However, Squeel monkeypatches Active Record internals, because it was aimed at enhancing the existing API with the aim of inclusion into Rails. However, that inclusion never happened, and it left Squeel susceptible to breakage from arbitrary changes in Active Record, eventually burning out the maintainer.

BabySqueel provides a Squeel-like query DSL for Active Record while hopefully avoiding the majority of the version upgrade difficulties via a minimum of monkeypatching. ❤️

Installation

Add this line to your application's Gemfile:

gem 'baby_squeel'

And then execute:

$ bundle

Or install it yourself as:

$ gem install baby_squeel

Introduction

With Active Record, you might write something like this:

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

But then someone tells you, "Hey, you should use Arel!". So you convert your query to use Arel:

Post.where(Post.arel_table[:created_at].gteq(2.weeks.ago))

Well, that's great, but it's also pretty verbose. Why don't you give BabySqueel a try:

Post.where.has { created_at >= 2.weeks.ago }

Quick note

BabySqueel's blocks use instance_eval, which means you won't have access to your instance variables or methods. Don't worry, there's a really easy solution. Just give arity to the block:

Post.where.has { |post| post.created_at >= 2.weeks.ago }

Usage

Okay, so we have some models:

class Post < ActiveRecord::Base
  belongs_to :author
  has_many :comments
end

class Author < ActiveRecord::Base
  has_many :posts
  has_many :comments, through: :posts
end

class Comment < ActiveRecord::Base
  belongs_to :post
end
Selects
Post.selecting { (id + 5).as('id_plus_five') }
# SELECT ("posts"."id" + 5) AS id_plus_five FROM "posts"

Post.selecting { id.sum }
# SELECT SUM("posts"."id") FROM "posts"

Post.joins(:author).selecting { [id, author.id] }
# SELECT "posts"."id", "authors"."id" FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
Wheres
Post.where.has { title == 'My Post' }
# SELECT "posts".* FROM "posts"
# WHERE "posts"."title" = 'My Post'

Post.where.has { title =~ 'My P%' }
# SELECT "posts".* FROM "posts"
# WHERE ("posts"."title" LIKE 'My P%')

Author.where.has { (name =~ 'Ray%') & (id < 5) | (name.lower =~ 'zane%') & (id > 100) }
# SELECT "authors".* FROM "authors"
# WHERE ("authors"."name" LIKE 'Ray%' AND "authors"."id" < 5 OR LOWER("authors"."name") LIKE 'zane%' AND "authors"."id" > 100)

Post.joins(:author).where.has { author.name == 'Ray' }
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# WHERE "authors"."name" = 'Ray'

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%')
Orders
Post.ordering { [id.desc, title.asc] }
# SELECT "posts".* FROM "posts"
# ORDER BY "posts"."id" DESC, "posts"."title" ASC

Post.ordering { (id * 5).desc }
# SELECT "posts".* FROM "posts"
# ORDER BY "posts"."id" * 5 DESC

Post.select(:author_id).group(:author_id).ordering { id.count.desc }
# SELECT "posts"."author_id" FROM "posts"
# GROUP BY "posts"."author_id"
# ORDER BY COUNT("posts"."id") DESC

Post.joins(:author).ordering { author.id.desc }
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# ORDER BY "authors"."id" DESC
Joins
Post.joining { author }
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"

Post.joining { [author.outer, comments] }
# SELECT "posts".* FROM "posts"
# LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"

Post.joining { author.comments }
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# INNER JOIN "posts" "posts_authors_join" ON "posts_authors_join"."author_id" = "authors"."id"
# INNER JOIN "comments" ON "comments"."post_id" = "posts_authors_join"."id"

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

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

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"

Post.joining { author.on((author.id == author_id) | (author.name == title)) }
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" ON ("authors"."id" = "posts"."author_id" OR "authors"."name" = "posts"."title")

Post.joining { |post| post.author.as('a').on { (id == post.author_id) | (name == post.title) } }
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" "a" ON ("a"."id" = "posts"."author_id" OR "a"."name" = "posts"."title")

Picture.joining { imageable.of(Post) }
# SELECT "pictures".* FROM "pictures"
# INNER JOIN "posts" ON "posts"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = 'Post'

Picture.joining { imageable.of(Post).outer }
# SELECT "pictures".* FROM "pictures"
# LEFT OUTER JOIN "posts" ON "posts"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = 'Post'
Grouping
Post.selecting { id.count }.grouping { author_id }.when_having { id.count > 5 }
# SELECT COUNT("posts"."id") FROM "posts"
# GROUP BY "posts"."author_id"
# HAVING (COUNT("posts"."id") > 5)
Functions
Post.selecting { coalesce(author_id, 5).as('author_id_with_default') }
# SELECT coalesce("posts"."author_id", 5) AS author_id_with_default FROM "posts"
Subqueries
Post.joins(:author).where.has {
  author.id.in Author.select(:id).where(name: 'Ray')
}
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# WHERE "authors"."id" IN (
#   SELECT "authors"."id" FROM "authors"
#   WHERE "authors"."name" = 'Ray'
# )
Exists
Post.where.has {
  exists Post.where.has { author_id == 1 }
}
# SELECT "posts".* FROM "posts"
# WHERE (
#   EXISTS(
#     SELECT "posts".* FROM "posts"
#     WHERE "posts"."author_id" = 1
#   )
# )
Custom SQL Operators
authors = Author.selecting { name.op('||', quoted('-dizzle')).as('swag') }
# SELECT "authors"."name" || '-dizzle' AS swag FROM "authors"

authors.first.swag #=> 'Ray Zane-dizzle'
Querying tables without Active Record models
table = BabySqueel[:some_table]

Post.joining {
  table.on(table.post_id == id)
}.where.has {
  table.some_column == 1
}
Polymorphism

Given this polymorphism:

# app/models/picture.rb
belongs_to :imageable, polymorphic: true

# app/models/post.rb
has_many :pictures, as: :imageable

The query might look like this:

Picture.
  joining { imageable.of(Post) }.
  selecting { imageable.of(Post).id }
Helpers
# SQL Literals
Post.select('1 as one').ordering { sql('one').desc }

# Quoting
Post.selecting { title.op('||', quoted('diddly')) }

# Functions
Post.selecting { func('coalesce', id, 1) }

Sifters

Sifters are like little snippets of conditions that can take arguments.

class Post < ActiveRecord::Base
  sifter :funny do
    title == 'rabies'
  end
end

class Author < ActiveRecord::Base
  sifter :name_contains do |string|
    name =~ "%#{string}%"
  end
end

Post.joins(:author).where.has {
  sift(:funny) | author.sift(:name_contains, 'blergh')
}
# SELECT "posts".* FROM "posts"
# INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# WHERE ("posts"."title" = 'rabies' OR "authors"."name" LIKE '%blergh%')

What's what?

The following methods give you access to BabySqueel's DSL:

BabySqueel Active Record Equivalent
selecting select
ordering order
joining joins
grouping group
where.has where
when_having having

Migrating from Squeel

Check out the migration guide.

Development

  1. Pick an Active Record version to develop against, then export it: export AR=6.1.4.
  2. Run bin/setup to install dependencies.
  3. Run rake to run the specs.

You can also run bin/console to open up a prompt where you'll have access to some models to experiment with.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/rzane/baby_squeel.

License

The gem is available as open source under the terms of the MIT License.

More Repositories

1

docker2exe

Convert a Docker image to an executable
Go
120
star
2

upload

An opinionated file uploader for Elixir projects
Elixir
29
star
3

grape-cancan

Use CanCan to authorize your Grape endpoints
Ruby
28
star
4

graphql-extras

Custom scalars, file uploads, and testing tools for building GraphQL APIs in Ruby
Ruby
21
star
5

file_store

🗄️ A unified interface for file storage backends
Elixir
19
star
6

contextual

🌈 Generate your Ecto contexts using this macro and eliminate boilerplate
Elixir
19
star
7

dlnanow

Stream video to DLNA-capable devices from the commandline
JavaScript
18
star
8

snapshot_testing

📷 Snapshot testing for all Ruby test frameworks
Ruby
9
star
9

classnames

A simple utility for conditionally joining class names in Elixir
Elixir
8
star
10

magic_models

🎩 Generate Active Record models from an existing schema
Ruby
8
star
11

rspec-raml

RSpec matchers for working with RAML (http://raml.org/)
Ruby
7
star
12

file_type

Detect the MIME type of a file based on it's content.
Elixir
7
star
13

advanced

A way to organize your Active Record queries
Ruby
7
star
14

react-sql

Because why not...
JavaScript
6
star
15

proc_utils

A set of functional utilities for working with callables in Ruby
Ruby
6
star
16

zendesk_rails

A mountable Rails Engine for the Zendesk API
Ruby
6
star
17

dotfiles

Lua
4
star
18

validate

A functional schema validation library
TypeScript
3
star
19

absinthe_conn_test

Helpful utilities for testing your GraphQL API
Elixir
3
star
20

active_cursor

Ruby
3
star
21

influxdb-metrics

Track your Rails app's performance metrics with InfluxDB
Ruby
2
star
22

deep-prune

✂️ Recursively remove null or undefined values from an object
TypeScript
2
star
23

apollo-link-upload

⬆️ An Apollo Link for simpler file uploads
TypeScript
2
star
24

react-auto-mount

Mount React components unobtrusively and automagically
JavaScript
2
star
25

activerecord-publishable

Push changes to your ActiveRecord models to Redis for PubSub.
Ruby
2
star
26

parallel_includes

Experimenting with loading Active Record associations in parallel
Ruby
2
star
27

tiny_auth

A collection of utilities for authenticating users.
Ruby
2
star
28

codegen

A zero-config tool to generate GraphQL type definitions
TypeScript
2
star
29

authenticator

Provides the glue for authenticating HTTP requests.
Elixir
2
star
30

sensible-maybe

Maybe you should use a Maybe?
TypeScript
2
star
31

sdl

A generic schema definition language
Ruby
1
star
32

ex_unit_let

Elixir
1
star
33

pg_partitions

ActiveRecord::Migration utility for managing partitions in PostgreSQL
Ruby
1
star
34

geny

A framework for building code generators
Ruby
1
star
35

join_dependency

Ruby
1
star
36

turbo-animate

JavaScript
1
star
37

form

A type-safe approach to managing complex form state in React.
TypeScript
1
star
38

datamapper

Python
1
star
39

apollo-test-client

Utilities for testing with Apollo Client
JavaScript
1
star
40

url-fmt

JavaScript
1
star
41

argy

Yet another command line option parser
Ruby
1
star
42

lil_tweets

A tiny lil graphql demo
Ruby
1
star
43

turbo-demo

Ruby
1
star
44

hooked-on-storage

💾 A React hook for storage with support for rehydration
TypeScript
1
star
45

gsub

A CLI program to do find and replace written in Crystal
Crystal
1
star
46

pipey

Functional pipelines for Ruby
Ruby
1
star