Skip to content
This repository has been archived by the owner on Mar 30, 2022. It is now read-only.

Make Squeel compatible with Rails 4.1 and Rails 4.2.0.alpha #317

Merged
merged 7 commits into from Jul 15, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -3,3 +3,5 @@
Gemfile.lock
pkg/*
.rvmrc
.ruby-version
.ruby-gemset
3 changes: 3 additions & 0 deletions .rspec
@@ -0,0 +1,3 @@
--color
--format documentation
--backtrace
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this file be in version control?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah... I didn't notice it. I just want to let the output of rspec more readable. Does it make any troubles?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't care much, @ernie?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's OK by me, or I wouldn't have merged :D

4 changes: 3 additions & 1 deletion .travis.yml
Expand Up @@ -2,9 +2,11 @@ rvm:
- 1.9.3
- 2.0.0
- 2.1.1
- 2.1.2

env:
- RAILS=4-1-stable AREL=master
- RAILS=master AREL=master
- RAILS=4-1-stable AREL=5-0-stable
- RAILS=4-0-stable AREL=4-0-stable
- RAILS=3-2-stable AREL=3-0-stable
- RAILS=3-1-stable AREL=2-2-stable
Expand Down
4 changes: 3 additions & 1 deletion Gemfile
Expand Up @@ -3,7 +3,7 @@ gemspec

gem 'rake'

rails = ENV['RAILS'] || '4-1-stable'
rails = ENV['RAILS'] || 'master'
arel = ENV['AREL'] || 'master'

arel_opts = case arel
Expand Down Expand Up @@ -35,3 +35,5 @@ else
gem 'activerecord'
end
end

gem 'polyamorous', git: 'git://github.com/activerecord-hackery/polyamorous.git'
43 changes: 42 additions & 1 deletion README.md
Expand Up @@ -23,6 +23,8 @@ just a simple example -- Squeel's capable of a whole lot more. Keep reading.
In your Gemfile:

```ruby
# Make sure you are using the latest version of polyamorous
gem "polyamorous", :git => "git://github.com/activerecord-hackery/polyamorous.git"
gem "squeel" # Last officially released gem
# gem "squeel", :git => "git://github.com/activerecord-hackery/squeel.git" # Track git repo
```
Expand Down Expand Up @@ -86,6 +88,24 @@ A Squeel keypath is essentially a more concise and readable alternative to a
deeply nested hash. For instance, in standard Active Record, you might join several
associations like this to perform a query:

#### Rails 4+

```ruby
Person.joins(:articles => {:comments => :person}).references(:all)
# => SELECT "people".* FROM "people"
# LEFT OUTER JOIN "articles" ON "articles"."person_id" = "people"."id"
# LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id"
# LEFT OUTER JOIN "people" "people_comments" ON "people_comments"."id" = "comments"."person_id"
```

With a keypath, this would look like:

```ruby
Person.joins{articles.comments.person}.references(:all)
```

#### Rails 3.x

```ruby
Person.joins(:articles => {:comments => :person})
# => SELECT "people".* FROM "people"
Expand Down Expand Up @@ -413,6 +433,27 @@ Person.joins{children.parent.children}.

Keypaths were used here for clarity, but nested hashes would work just as well.

You can also use a subquery in a join.

Notice:
1. Squeel can only accept an ActiveRecord::Relation class of subqueries in a join.
2. Use the chain with caution. You should call `as` first to get a Nodes::As, then call `on` to get a join node.

```ruby
subquery = OrderItem.group(:orderable_id).select { [orderable_id, sum(quantity * unit_price).as(amount)] }
Seat.joins { [payment.outer, subquery.as('seat_order_items').on { id == seat_order_items.orderable_id}.outer] }.
select { [seat_order_items.amount, "seats.*"] }
# => SELECT "seat_order_items"."amount", seats.*
# FROM "seats"
# LEFT OUTER JOIN "payments" ON "payments"."id" = "seats"."payment_id"
# LEFT OUTER JOIN (
# SELECT "order_items"."orderable_id",
# sum("order_items"."quantity" * "order_items"."unit_price") AS amount
# FROM "order_items"
# GROUP BY "order_items"."orderable_id"
# ) seat_order_items ON "seats"."id" = "seat_order_items"."orderable_id"
```

### Functions

You can call SQL functions just like you would call a method in Ruby...
Expand Down Expand Up @@ -470,7 +511,7 @@ As you can see, just like functions, these operations can be given aliases.
To select more than one attribute (or calculated attribute) simply put them into an array:

```ruby
p = Person.select{[ name.op('||', '-diddly').as(flanderized_name),
p = Person.select{[ name.op('||', '-diddly').as(flanderized_name),
coalesce(name, '<no name given>').as(name_with_default) ]}.first
p.flanderized_name
# => "Aric Smith-diddly"
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Expand Up @@ -4,7 +4,7 @@ require 'rspec/core/rake_task'
Bundler::GemHelper.install_tasks

RSpec::Core::RakeTask.new(:spec) do |rspec|
rspec.rspec_opts = ['--backtrace']
rspec.rspec_opts = ['--backtrace', '--color', '--format documentation']
end

task :default => :spec
Expand Down
10 changes: 9 additions & 1 deletion lib/squeel.rb
@@ -1,7 +1,16 @@
require 'squeel/configuration'
require 'polyamorous'

module Squeel

if defined?(Arel::InnerJoin)
InnerJoin = Arel::InnerJoin
OuterJoin = Arel::OuterJoin
else
InnerJoin = Arel::Nodes::InnerJoin
OuterJoin = Arel::Nodes::OuterJoin
end

extend Configuration

# Prevent warnings on the console when doing things some might describe as "evil"
Expand Down Expand Up @@ -30,7 +39,6 @@ def self.sane_arity?
alias_predicate aliaz, original
end
end

end

require 'squeel/dsl'
Expand Down
1 change: 0 additions & 1 deletion lib/squeel/adapters/active_record.rb
@@ -1,7 +1,6 @@
case ActiveRecord::VERSION::MAJOR
when 3, 4
ActiveRecord::Relation.send :include, Squeel::Nodes::Aliasing
require 'squeel/adapters/active_record/join_dependency_extensions'
require 'squeel/adapters/active_record/base_extensions'

adapter_directory = "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
Expand Down
@@ -0,0 +1 @@
require 'squeel/adapters/active_record/join_dependency_extensions'
13 changes: 12 additions & 1 deletion lib/squeel/adapters/active_record/3.0/relation_extensions.rb
Expand Up @@ -54,8 +54,9 @@ def build_join_dependency(relation, joins)
end

stashed_association_joins = joins.grep(::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)
subquery_joins = joins.grep(Nodes::SubqueryJoin)

non_association_joins = (joins - association_joins - stashed_association_joins)
non_association_joins = (joins - association_joins - stashed_association_joins - subquery_joins)
custom_joins = custom_join_sql(*non_association_joins)

self.join_dependency = JoinDependency.new(@klass, association_joins, custom_joins)
Expand All @@ -79,6 +80,16 @@ def build_join_dependency(relation, joins)
relation = relation.join(left, join_type).on(*right)
end

subquery_joins.each do |join|
relation = relation.
join(
Arel::Nodes::TableAlias.new(
join.subquery.right,
Arel::Nodes::Grouping.new(join.subquery.left.arel.ast)),
join.type).
on(*where_visit(join.constraints))
end

relation = relation.join(custom_joins)
end

Expand Down
@@ -0,0 +1 @@
require 'squeel/adapters/active_record/join_dependency_extensions'
@@ -0,0 +1 @@
require 'squeel/adapters/active_record/join_dependency_extensions'
@@ -0,0 +1 @@
require 'squeel/adapters/active_record/join_dependency_extensions'
55 changes: 53 additions & 2 deletions lib/squeel/adapters/active_record/4.0/relation_extensions.rb
Expand Up @@ -30,6 +30,24 @@ def where(opts = :chain, *rest)
end
end

def where_unscoping(target_value)
target_value = target_value.to_s

where_values.reject! do |rel|
case rel
when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
subrelation.name == target_value
when Hash
rel.stringify_keys.has_key?(target_value)
when Squeel::Nodes::Predicate
rel.expr.symbol.to_s == target_value
end
end

bind_values.reject! { |col,_| col.name == target_value }
end

def build_arel
arel = Arel::SelectManager.new(table.engine, table)

Expand Down Expand Up @@ -70,8 +88,7 @@ def build_where(opts, other = [])
self.bind_values += rel.bind_values
end

case attrs.flatten.first
when Symbol, Squeel::Nodes::Stub, Squeel::Nodes::Predicate
unless attrs.keys.grep(Squeel::Nodes::Node).empty? && attrs.keys.grep(Symbol).empty?
attrs
else
super
Expand Down Expand Up @@ -143,6 +160,40 @@ def order!(*args)
self
end

def where_values_hash_with_squeel(relation_table_name = table_name)
equalities = find_equality_predicates(where_visit(with_default_scope.where_values), relation_table_name)

binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]

Hash[equalities.map { |where|
name = where.left.name
[name, binds.fetch(name.to_s) { where.right }]
}]
end

def to_sql_with_binding_params
@to_sql ||= begin
relation = self
connection = klass.connection

if eager_loading?
find_with_associations { |rel| relation = rel }
end

ast = relation.arel.ast
binds = relation.bind_values.dup

visitor = connection.visitor.clone
visitor.class_eval do
include ::Arel::Visitors::BindVisitor
end

visitor.accept(ast) do
connection.quote(*binds.shift.reverse)
end
end
end

private

def dehashified_order_values
Expand Down
15 changes: 15 additions & 0 deletions lib/squeel/adapters/active_record/4.1/compat.rb
@@ -0,0 +1,15 @@
require 'squeel/adapters/active_record/compat'

module ActiveRecord
module Associations
class AssociationScope
def eval_scope(klass, scope, owner)
if scope.is_a?(Relation)
scope
else
klass.unscoped.instance_exec(owner, &scope).visited
end
end
end
end
end
88 changes: 88 additions & 0 deletions lib/squeel/adapters/active_record/4.1/context.rb
@@ -0,0 +1,88 @@
require 'squeel/adapters/active_record/context'

module Squeel
module Adapters
module ActiveRecord
class Context < ::Squeel::Context
class NoParentFoundError < RuntimeError; end

def initialize(object)
super
@base = object.join_root
@engine = @base.base_klass.arel_engine
@arel_visitor = get_arel_visitor
@default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
end

def find(object, parent = @base)
if ::ActiveRecord::Associations::JoinDependency::JoinPart === parent
case object
when String, Symbol, Nodes::Stub
assoc_name = object.to_s
find_string_symbol_stub_association(@base.children, @base, assoc_name, parent)
when Nodes::Join
find_node_join_association(@base.children, @base, object, parent)
else
find_other_association(@base.children, @base, object, parent)
end
end
end

def find!(object, parent = @base)
if ::ActiveRecord::Associations::JoinDependency::JoinPart === parent
result =
case object
when String, Symbol, Nodes::Stub
assoc_name = object.to_s
find_string_symbol_stub_association(@base.children, @base, assoc_name, parent)
when Nodes::Join
find_node_join_association(@base.children, @base, object, parent)
else
find_other_association(@base.children, @base, object, parent)
end

result || raise(NoParentFoundError, "can't find #{object} in #{parent}")
else
raise NoParentFoundError, "can't find #{object} in #{parent}"
end
end

def traverse!(keypath, parent = @base, include_endpoint = false)
parent = @base if keypath.absolute?
keypath.path_without_endpoint.each do |key|
parent = find!(key, parent)
end
parent = find!(keypath.endpoint, parent) if include_endpoint

parent
end

private
def find_string_symbol_stub_association(join_associations, current_parent, assoc_name, target_parent)
join_associations.each do |assoc|
return assoc if assoc.reflection.name.to_s == assoc_name && current_parent.equal?(target_parent)
child_assoc = find_string_symbol_stub_association(assoc.children, assoc, assoc_name, target_parent)
return child_assoc if child_assoc
end && false
end

def find_node_join_association(join_associations, current_parent, object, target_parent)
join_associations.each do |assoc|
return assoc if assoc.reflection.name == object._name && current_parent.equal?(target_parent) &&
(object.polymorphic? ? assoc.reflection.klass == object._klass : true)
child_assoc = find_node_join_association(assoc.children, assoc, object, target_parent)
return child_assoc if child_assoc
end && false
end

def find_other_association(join_associations, current_parent, object, target_parent)
join_associations.each do |assoc|
return assoc if assoc.reflection == object && current_parent.equal?(target_parent)
child_assoc = find_other_association(assoc.children, assoc, object, target_parent)
return child_assoc if child_assoc
end && false
end
end
end
end
end
31 changes: 31 additions & 0 deletions lib/squeel/adapters/active_record/4.1/preloader_extensions.rb
@@ -0,0 +1,31 @@
module Squeel
module Adapters
module ActiveRecord
module PreloaderExtensions

def self.included(base)
base.class_eval do
alias_method_chain :preload, :squeel
end
end

def preload_with_squeel(records, associations, preload_scope = nil)
records = Array.wrap(records).compact.uniq
associations = Array.wrap(associations)
preload_scope = preload_scope || ::ActiveRecord::Associations::Preloader::NULL_RELATION

if records.empty?
[]
else
Visitors::PreloadVisitor.new.accept(associations).each do |association|
preloaders_on(association, records, preload_scope)
end
end
end

end
end
end
end

ActiveRecord::Associations::Preloader.send :include, Squeel::Adapters::ActiveRecord::PreloaderExtensions