ArelToolkit
Overview
Installation
Add this line to your application's Gemfile:
gem 'arel_toolkit'
And then execute:
$ bundle
Or install it yourself as:
$ gem install arel_toolkit
Sql to Arel
Convert your (PostgreSQL) SQL into an Arel AST.
[1] > sql = 'SELECT id FROM users'
=> "SELECT id FROM users;"
[2] > arel = Arel.sql_to_arel(sql)
=> #<Arel::SelectManager:0x00007fe4e39823d8>
[3] > arel.to_sql
=> "SELECT \"id\" FROM \"users\""
Enhanced Arel AST
Arel.enhance(arel)
adds additional information and helper methods to the existing Arel AST. This allows for mutating the AST, adding contextual information to the AST and querying for nodes. Some examples:
Query for Arel nodes with certain properties
arel = Post.select(:id, :public).where(id: 1).arel
enhanced_arel = Arel.enhance(arel)
enhanced_arel.query(class: Arel::Table).each { ... }
Query for Arel nodes with an enhanced context
An Arel::Table
is used in multiple different places inside the AST, and those locations will give the Arel::Table
a different meaning. Used within a projection (column_reference) like SELECT posts.id
has a different meaning than within a from SELECT * FROM posts
(range_variable). The following example results in Arel::Table
nodes where the object is used in the context of referencing a column:
enhanced_arel.query(class: Arel::Table, context: { column_reference: true }).each { ... }
Get an Arel node at a certain path
enhanced_arel.child_at_path(['ast', 'cores', 0, 'projections', 1]).object
=> #<struct Arel::Attributes::Attribute>
Replace or remove nodes without modifying the original arel
remove
and replace
allow for modifications to the Arel AST. The changes are aplied to a new copy of the AST, making sure the original AST is not touched. The replace
method accepts any Arel node or a precomputed enhanced node for improved performance.
enhanced_arel.child_at_path(['ast', 'cores', 0, 'projections', 1]).replace(Post.arel_table[:content])
enhanced_arel.child_at_path(['ast', 'cores', 0, 'projections', 0]).remove
enhanced_arel.to_sql
=> SELECT "posts"."content" FROM "posts" WHERE "posts"."id" = $1
Middleware
Creating Arel from SQL and enhancing Arel is just the beginning, where this gem really shines is the ability to modify Arel ASTs using middleware.
Middleware sits between ActiveRecord and the database, it allows you to alter the Arel (the SQL query) before it's send to the database. Multiple middlewares are supported by passing the results from a finished middleware to the next. Next to the arel object, a context object is used that acts as a intermediate storage between middlewares.
The middleware works out of the box in combination with Rails. If using ActiveRecord standalone you need to run the following after setting up the database connection:
Arel::Middleware::Railtie.insert
Example
Create middleware which can be any Ruby object as long as it responds to call
. Middleware accepts 2 or 3 arguments, context is optional. Calling .call
on next_middleware
invokes the next middleware in the chain, returning the response from the database.
In this example, we're creating a middleware that will reorder any query. Next to reordering, we're adding an additional middleware that prints out the result of the reorder middleware.
class ReorderMiddleware
def self.call(arel, next_middleware)
enhanced_arel = Arel.enhance(arel)
enhanced_arel.query(class: Arel::Nodes::SelectStatement).each do |node|
arel_table = node.child_at_path(['cores', 0, 'source', 'left']).object
node['orders'].replace([arel_table[:id].asc])
end
new_arel = arel.order(Post.arel_table[:id].asc)
next_middleware.call(new_arel)
end
end
class LoggingMiddleware
def self.call(arel, next_middleware, context)
puts "User executing query: `#{context[:current_user_id]}`"
puts "Original SQL: `#{context[:original_sql]}`"
puts "Modified SQL: `#{arel.to_sql}`"
next_middleware.call(arel)
end
end
Now that we've defined our middelwares, it's time to see them in action:
[1] > Arel.middleware.apply([ReorderMiddleware, LoggingMiddleware]).context(current_user_id: 1) { Post.all.load }
User executing query: `1`
Original SQL: `SELECT "posts".* FROM "posts"`
Modified SQL: `SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC`
Post Load (4.1ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC
=> []
This gem ships with a couple of middelware methods that allow you to fine-tune what and when to apply middelware.
Arel.middleware.apply([SomeMiddleware]) { ... }
Arel.middleware.only([OnlyMe]) { ... }
Arel.middleware.none { ... }
Arel.middleware.except(RemoveMe) { ... }
Arel.middleware.insert_before(RunBefore, ThisMiddleware) { ... }
Arel.middleware.insert_after(RunAfter, ThisMiddleware) { ... }
Extensions
This gem aims to have full support for PostgreSQL's SQL. In order to do so, it needs to add missing Arel nodes and extends the existing visitors. A full list of extensions on Arel can be found here: lib/arel/extensions.
Development
- Check out the repo
- Run
bin/setup
to install dependencies. - Start the postgres database
docker compose up
- Run
bundle exec rspec
to run the tests
You can also run bin/console
for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run bundle exec rake install
.
Releasing
- Update version in
version.rb
- Create a new branch
v<<VERSION_HERE>>
- Run
bundle install
- Run
bundle exec rake changelog
- Commit the changes
- Open a PR with name
Version <<VERSION_HERE>>
(example) - Merge the PR
- Checkout the master branch and pull latest changes
- Run
bundle exec rake release
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/mvgijssel/arel_toolkit. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ArelToolkit project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.