baby_squeel 0.3.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d7d31d8ac2c6564c5116b4fcfb2175c7747e1689
4
- data.tar.gz: a1882420d085c5d2e071434ae8da6d28951ad3f9
3
+ metadata.gz: 394545743835687d87f67feb11e76e787cebc34a
4
+ data.tar.gz: 4a3f636a36f4e83a889e3948ea6acf35f235b402
5
5
  SHA512:
6
- metadata.gz: 3fdc066c0f04ebe17744c95cbc4499a492c993ca23d06fc6b11b41077d7d2a45591504f7a831b4fdf1e21aa16c640b6847a1bbf58e7fc3b1b232e0366209b0d9
7
- data.tar.gz: dac5d32acfb013e580aa4e51433a6b1e2bafe17288001feb09295f1da2b26e93a61486598c8884707e687ee591c024ac7ad82091b5a1833ed239f2ee009cfb42
6
+ metadata.gz: ba300fa29631298a8143a580ab34bb66298b022aa9e32d0204f4855e23a464090c3ba0032f1c63152bd3de90a46f12f2ed609234b735de41cce50cd841a512e6
7
+ data.tar.gz: 6e565e66b5e8dd456f4a388f5b1e0b903ddcc2f1baaee86aed0ca0c295238a66d9a0d16f31cd6f9ce4e4679700354b2c98782284e418d2c981d7db88fab864fc
data/CHANGELOG.md CHANGED
@@ -1,6 +1,21 @@
1
- ## Unreleased
1
+ ## [1.0.0] - 2016-9-9
2
+ ### Added
3
+ - Polyamorous. Unfortunately, this *does* monkey-patch Active Record internals, but there just isn't any other reliable way to generate outer joins. Baby Squeel, itself, will still keep monkey patching to an absolute minimum.
4
+ - Within DSL blocks, you can use `exists` and `not_exists` with Active Record relations. For example: `Post.where.has { exists Post.where(title: 'Fun') }`.`
5
+ - Support for polymorphic associations.
6
+
7
+ ### Deprecations
8
+ - Removed support for Active Record 4.0.x
2
9
 
3
- Nothing yet.
10
+ ### Changed
11
+ - BabySqueel::JoinDependency is no longer a class responsible for creating Arel joins. It is now a namespace for utilities used when working with the ActiveRecord::Association::JoinDependency class.
12
+ - BabySqueel::Nodes::Generic is now BabySqueel::Nodes::Node.
13
+ - Arel nodes are only extended with the behaviors they need. Previously, all Arel nodes were being extended with `Arel::AliasPredication`, `Arel::OrderPredications`, and `Arel::Math`.
14
+
15
+ ### Fixed
16
+ - Fixed deprecation warnings on Active Record 5 when initializing an Arel::Table without a type caster.
17
+ - No more duplicate joins. Previously, Baby Squeel did a very poor job of ensuring that you didn't join an association twice.
18
+ - Alias detection should now *actually* work. The previous implementation was naive.
4
19
 
5
20
  ## [0.3.1] - 2016-08-02
6
21
  ### Added
data/ISSUE_TEMPLATE.md ADDED
@@ -0,0 +1,39 @@
1
+ #### Issue
2
+
3
+ Please explain this issue you're encountering to the best of your ability.
4
+
5
+ #### Reproduction
6
+
7
+ ```ruby
8
+ require 'bundler/inline'
9
+ require 'minitest/spec'
10
+ require 'minitest/autorun'
11
+
12
+ gemfile true do
13
+ source 'https://rubygems.org'
14
+ gem 'activerecord', '~> 5.0.0' # which Active Record version?
15
+ gem 'sqlite3'
16
+ gem 'baby_squeel', github: 'rzane/baby_squeel'
17
+ end
18
+
19
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
20
+
21
+ ActiveRecord::Schema.define do
22
+ create_table :dogs, force: true do |t|
23
+ t.string :name
24
+ end
25
+ end
26
+
27
+ class Dog < ActiveRecord::Base
28
+ end
29
+
30
+ class BabySqueelTest < Minitest::Spec
31
+ it 'works' do
32
+ scope = Dog.where.has { name == 'Fido' }
33
+
34
+ scope.to_sql.must_equal %{
35
+ SELECT "dogs".* FROM "dogs" WHERE "dogs"."name" = 'Fido'
36
+ }.squish
37
+ end
38
+ end
39
+ ```
data/README.md CHANGED
@@ -175,6 +175,14 @@ Post.joining { author.outer.posts }
175
175
  Post.joining { author.alias('a').on((author.id == author_id) | (author.name == title)) }
176
176
  # SELECT "posts".* FROM "posts"
177
177
  # INNER JOIN "authors" "a" ON ("authors"."id" = "posts"."author_id" OR "authors"."name" = "posts"."title")
178
+
179
+ Picture.joining { imageable.of(Post) }
180
+ # SELECT "pictures".* FROM "pictures"
181
+ # INNER JOIN "posts" ON "posts"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = 'Post'
182
+
183
+ Picture.joining { imageable.of(Post).outer }
184
+ # SELECT "pictures".* FROM "pictures"
185
+ # LEFT OUTER JOIN "posts" ON "posts"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = 'Post'
178
186
  ```
179
187
 
180
188
  ##### Grouping
@@ -279,18 +287,9 @@ The following methods give you access to BabySqueel's DSL:
279
287
  | `where.has` | `where` |
280
288
  | `when_having` | `having` |
281
289
 
282
- ## Compatibility Mode
283
-
284
- If you want `select`, `order`, `joins`, `group`, and `having` to be able to accept DSL blocks, you can do so by adding the following to an initializer.
285
-
286
- ```ruby
287
- # config/initializers/baby_squeel.rb
288
- BabySqueel.configure do |config|
289
- config.enable_compat!
290
- end
291
- ```
290
+ ## Migrating from Squeel
292
291
 
293
- Calling `enable_compat!` will override methods in Active Record, so use caution.
292
+ Check out the [migration guide](https://github.com/rzane/baby_squeel/wiki/Migrating-from-Squeel).
294
293
 
295
294
  ## Development
296
295
 
data/baby_squeel.gemspec CHANGED
@@ -19,7 +19,8 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.files = Dir.glob('{lib/**/*,*.{md,txt,gemspec}}')
21
21
 
22
- spec.add_dependency 'activerecord', '>= 4.0.0'
22
+ spec.add_dependency 'activerecord', '>= 4.1.0'
23
+ spec.add_dependency 'polyamorous', '~> 1.3'
23
24
 
24
25
  spec.add_development_dependency 'bundler', '~> 1.11'
25
26
  spec.add_development_dependency 'rake', '~> 10.0'
data/lib/baby_squeel.rb CHANGED
@@ -1,31 +1,47 @@
1
1
  require 'active_record'
2
2
  require 'active_record/relation'
3
+ require 'polyamorous'
3
4
  require 'baby_squeel/version'
4
5
  require 'baby_squeel/errors'
5
- require 'baby_squeel/active_record'
6
+ require 'baby_squeel/active_record/base'
7
+ require 'baby_squeel/active_record/query_methods'
8
+ require 'baby_squeel/active_record/where_chain'
6
9
 
7
10
  module BabySqueel
8
11
  class << self
12
+ # Configures BabySqueel using the given block
9
13
  def configure
10
14
  yield self
11
15
  end
12
16
 
17
+ # Turn on BabySqueel's compatibility mode. This will
18
+ # make BabySqueel act more like Squeel.
13
19
  def enable_compatibility!
14
20
  require 'baby_squeel/compat'
15
21
  BabySqueel::Compat.enable!
16
22
  end
17
23
 
18
- def [](thing)
24
+ # Get a BabySqueel table instance.
25
+ #
26
+ # ==== Examples
27
+ # BabySqueel[Post]
28
+ # BabySqueel[:posts]
29
+ # BabySqueel[Post.arel_table]
30
+ #
31
+ def [](thing, **kwargs)
19
32
  if thing.respond_to?(:model_name)
20
33
  Relation.new(thing)
34
+ elsif thing.kind_of?(Arel::Table)
35
+ Table.new(thing)
21
36
  else
22
- Table.new(Arel::Table.new(thing))
37
+ Table.new(Arel::Table.new(thing, **kwargs))
23
38
  end
24
39
  end
25
40
  end
26
41
  end
27
42
 
28
- ::ActiveRecord::Base.extend BabySqueel::ActiveRecord::Sifting
29
- ::ActiveRecord::Base.extend BabySqueel::ActiveRecord::QueryMethods
30
- ::ActiveRecord::Relation.prepend BabySqueel::ActiveRecord::QueryMethods
31
- ::ActiveRecord::QueryMethods::WhereChain.prepend BabySqueel::ActiveRecord::WhereChain
43
+ ActiveSupport.on_load :active_record do
44
+ ::ActiveRecord::Base.extend BabySqueel::ActiveRecord::Base
45
+ ::ActiveRecord::Relation.prepend BabySqueel::ActiveRecord::QueryMethods
46
+ ::ActiveRecord::QueryMethods::WhereChain.prepend BabySqueel::ActiveRecord::WhereChain
47
+ end
@@ -0,0 +1,27 @@
1
+ require 'baby_squeel/dsl'
2
+
3
+ module BabySqueel
4
+ module ActiveRecord
5
+ module Base
6
+ delegate :joining, :joining!, :selecting, :ordering,
7
+ :grouping, :when_having, to: :all
8
+
9
+ # Define a sifter that can be used within DSL blocks.
10
+ #
11
+ # ==== Examples
12
+ # class Post < ActiveRecord::Base
13
+ # sifter :name_contains do |string|
14
+ # name =~ "%#{string}%"
15
+ # end
16
+ # end
17
+ #
18
+ # Post.where.has { sift(:name_contains, 'joe') }
19
+ #
20
+ def sifter(name, &block)
21
+ define_singleton_method "sift_#{name}" do |*args|
22
+ DSL.evaluate_sifter(self, *args, &block)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,20 +1,12 @@
1
1
  require 'baby_squeel/dsl'
2
+ require 'baby_squeel/join_dependency/injector'
2
3
 
3
4
  module BabySqueel
4
5
  module ActiveRecord
5
- module Sifting
6
- def sifter(name, &block)
7
- define_singleton_method "sift_#{name}" do |*args|
8
- DSL.evaluate_sifter(self, *args, &block)
9
- end
10
- end
11
- end
12
-
13
6
  module QueryMethods
14
7
  # Constructs Arel for ActiveRecord::Base#joins using the DSL.
15
8
  def joining(&block)
16
- arel, binds = DSL.evaluate_joins(unscoped, &block)
17
- joins(arel).tap { |s| s.bind_values += binds }
9
+ joins DSL.evaluate(self, &block)
18
10
  end
19
11
 
20
12
  # Constructs Arel for ActiveRecord::Base#select using the DSL.
@@ -36,13 +28,15 @@ module BabySqueel
36
28
  def when_having(&block)
37
29
  having DSL.evaluate(self, &block)
38
30
  end
39
- end
40
31
 
41
- module WhereChain
42
- # Constructs Arel for ActiveRecord::Base#where using the DSL.
43
- def has(&block)
44
- @scope.where! DSL.evaluate(@scope, &block)
45
- @scope
32
+ private
33
+
34
+ # This is a monkey patch, and I'm not happy about it.
35
+ # Active Record will call `group_by` on the `joins`. The
36
+ # Injector has a custom `group_by` method that handles
37
+ # BabySqueel::JoinExpression nodes.
38
+ def build_joins(manager, joins)
39
+ super manager, BabySqueel::JoinDependency::Injector.new(joins)
46
40
  end
47
41
  end
48
42
  end
@@ -0,0 +1,13 @@
1
+ require 'baby_squeel/dsl'
2
+
3
+ module BabySqueel
4
+ module ActiveRecord
5
+ module WhereChain
6
+ # Constructs Arel for ActiveRecord::Base#where using the DSL.
7
+ def has(&block)
8
+ @scope.where! DSL.evaluate(@scope, &block)
9
+ @scope
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,12 +2,57 @@ require 'baby_squeel/relation'
2
2
 
3
3
  module BabySqueel
4
4
  class Association < Relation
5
+ # An Active Record association reflection
5
6
  attr_reader :_reflection
6
7
 
8
+ # Specifies the model that the polymorphic
9
+ # association should join with
10
+ attr_accessor :_polymorphic_klass
11
+
7
12
  def initialize(parent, reflection)
8
13
  @parent = parent
9
14
  @_reflection = reflection
10
- super(@_reflection.klass)
15
+
16
+ # In the case of a polymorphic reflection these
17
+ # attributes will be set after calling #of
18
+ unless @_reflection.polymorphic?
19
+ super @_reflection.klass
20
+ end
21
+ end
22
+
23
+ def of(klass)
24
+ unless _reflection.polymorphic?
25
+ raise PolymorphicSpecificationError.new(_reflection.name, klass)
26
+ end
27
+
28
+ clone.of! klass
29
+ end
30
+
31
+ def of!(klass)
32
+ self._scope = klass
33
+ self._table = klass.arel_table
34
+ self._polymorphic_klass = klass
35
+ self
36
+ end
37
+
38
+ def needs_polyamorous?
39
+ _join == Arel::Nodes::OuterJoin || _reflection.polymorphic?
40
+ end
41
+
42
+ # See JoinExpression#add_to_tree.
43
+ def add_to_tree(hash)
44
+ polyamorous = Polyamorous::Join.new(
45
+ _reflection.name,
46
+ _join,
47
+ _polymorphic_klass
48
+ )
49
+
50
+ hash[polyamorous] ||= {}
51
+ end
52
+
53
+ # See BabySqueel::Table#find_alias.
54
+ def find_alias(association, associations = [])
55
+ @parent.find_alias(association, [self, *associations])
11
56
  end
12
57
 
13
58
  # Intelligently constructs Arel nodes. There are three outcomes:
@@ -15,18 +60,27 @@ module BabySqueel
15
60
  # 1. The user explicitly constructed their join using #on.
16
61
  # See BabySqueel::Table#_arel.
17
62
  #
63
+ # Post.joining { author.on(author_id == author.id) }
64
+ #
18
65
  # 2. The user aliased an implicitly joined association. ActiveRecord's
19
66
  # join dependency gives us no way of handling this, so we have to
20
67
  # throw an error.
21
68
  #
69
+ # Post.joining { author.as('some_alias') }
70
+ #
22
71
  # 3. The user implicitly joined this association, so we pass this
23
72
  # association up the tree until it hits the top-level BabySqueel::Table.
24
73
  # Once it gets there, Arel join nodes will be constructed.
74
+ #
75
+ # Post.joining { author }
76
+ #
25
77
  def _arel(associations = [])
26
78
  if _on
27
79
  super
28
80
  elsif _table.is_a? Arel::Nodes::TableAlias
29
81
  raise AssociationAliasingError.new(_reflection.name, _table.right)
82
+ elsif _reflection.polymorphic? && _polymorphic_klass.nil?
83
+ raise PolymorphicNotSpecifiedError.new(_reflection.name)
30
84
  else
31
85
  @parent._arel([self, *associations])
32
86
  end
@@ -1,5 +1,7 @@
1
1
  module BabySqueel
2
2
  module Compat
3
+ # Monkey-patches BabySqueel and ActiveRecord
4
+ # in order to behave more like Squeel
3
5
  def self.enable!
4
6
  BabySqueel::DSL.prepend BabySqueel::Compat::DSL
5
7
  ::ActiveRecord::Base.singleton_class.prepend QueryMethods
@@ -7,10 +9,12 @@ module BabySqueel
7
9
  end
8
10
 
9
11
  module DSL
12
+ # An alias for BabySqueel::DSL#sql
10
13
  def `(str)
11
14
  sql(str)
12
15
  end
13
16
 
17
+ # Allows you to call out of an instance_eval'd block.
14
18
  def my(&block)
15
19
  @caller.instance_eval(&block)
16
20
  end
@@ -23,6 +27,7 @@ module BabySqueel
23
27
  end
24
28
 
25
29
  module QueryMethods
30
+ # Overrides ActiveRecord::QueryMethods#joins
26
31
  def joins(*args, &block)
27
32
  if block_given? && args.empty?
28
33
  joining(&block)
@@ -47,6 +52,7 @@ module BabySqueel
47
52
  end
48
53
  end
49
54
 
55
+ # Overrides ActiveRecord::QueryMethods#order
50
56
  def order(*args, &block)
51
57
  if block_given? && args.empty?
52
58
  ordering(&block)
@@ -55,6 +61,7 @@ module BabySqueel
55
61
  end
56
62
  end
57
63
 
64
+ # Overrides ActiveRecord::QueryMethods#group
58
65
  def group(*args, &block)
59
66
  if block_given? && args.empty?
60
67
  grouping(&block)
@@ -63,6 +70,7 @@ module BabySqueel
63
70
  end
64
71
  end
65
72
 
73
+ # Overrides ActiveRecord::QueryMethods#having
66
74
  def having(*args, &block)
67
75
  if block_given? && args.empty?
68
76
  when_having(&block)
@@ -71,6 +79,7 @@ module BabySqueel
71
79
  end
72
80
  end
73
81
 
82
+ # Overrides ActiveRecord::QueryMethods#where
74
83
  def where(*args, &block)
75
84
  if block_given? && args.empty?
76
85
  where.has(&block)
@@ -15,15 +15,6 @@ module BabySqueel
15
15
  new(scope).evaluate(&block)
16
16
  end
17
17
 
18
- # Evaluates a block specifically for a join. In this
19
- # case, we'll return an array of Arel join nodes and
20
- # a list of bind parameters.
21
- def evaluate_joins(scope, &block)
22
- dependency = evaluate!(scope, &block)._arel
23
- join_arel = Nodes.unwrap(dependency._arel)
24
- [join_arel, dependency.bind_values]
25
- end
26
-
27
18
  # Evaluates a block in the context of a new DSL instance
28
19
  # and passes all arguments to the block.
29
20
  def evaluate_sifter(scope, *args, &block)
@@ -48,6 +39,32 @@ module BabySqueel
48
39
  Nodes.wrap Arel::Nodes::NamedFunction.new(name.to_s, args)
49
40
  end
50
41
 
42
+ # Generate an EXISTS subselect from an ActiveRecord::Relation
43
+ #
44
+ # ==== Arguments
45
+ #
46
+ # * +relation+ - An ActiveRecord::Relation
47
+ #
48
+ # ==== Example
49
+ # Post.where.has { exists Post.where(id: 1) }
50
+ #
51
+ def exists(relation)
52
+ func 'EXISTS', sql(relation.to_sql)
53
+ end
54
+
55
+ # Generate a NOT EXISTS subselect from an ActiveRecord::Relation
56
+ #
57
+ # ==== Arguments
58
+ #
59
+ # * +relation+ - An ActiveRecord::Relation
60
+ #
61
+ # ==== Example
62
+ # Post.where.has { not_exists Post.where(id: 1) }
63
+ #
64
+ def not_exists(rel)
65
+ func 'NOT EXISTS', sql(rel.to_sql)
66
+ end
67
+
51
68
  # See Arel::sql
52
69
  def sql(value)
53
70
  Nodes.wrap ::Arel.sql(value)
@@ -1,24 +1,48 @@
1
1
  module BabySqueel
2
- class NotFoundError < StandardError
2
+ class NotFoundError < StandardError # :nodoc:
3
3
  def initialize(model_name, name)
4
4
  super "There is no column or association named '#{name}' for #{model_name}."
5
5
  end
6
6
  end
7
7
 
8
- class AssociationNotFoundError < StandardError
8
+ class AssociationNotFoundError < StandardError # :nodoc:
9
9
  def initialize(model_name, name)
10
10
  super "Association named '#{name}' was not found for #{model_name}."
11
11
  end
12
12
  end
13
13
 
14
- class AssociationAliasingError < StandardError
14
+ class AssociationAliasingError < StandardError # :nodoc:
15
15
  MESSAGE =
16
- 'Attempted to alias \'%{association}\' as \'%{alias_name}\', but the ' \
17
- 'association was implicitly joined. Either join the association ' \
18
- 'with `on` or remove the alias.'.freeze
16
+ "Attempted to alias '%{association}' as '%{alias_name}', but the " \
17
+ "association was implicitly joined. Either join the association " \
18
+ "with `on` or remove the alias. For example:" \
19
+ "\n\n Post.joining { author }" \
20
+ "\n Post.joining { author.on(author_id == author.id) }\n\n"
19
21
 
20
22
  def initialize(association, alias_name)
21
23
  super format(MESSAGE, association: association, alias_name: alias_name)
22
24
  end
23
25
  end
26
+
27
+ class PolymorphicSpecificationError < StandardError # :nodoc:
28
+ MESSAGE =
29
+ "'%{association}' is not a polymorphic association, therefore " \
30
+ "the following expression is invalid:" \
31
+ "\n\n %{association}.of(%{klass})\n\n"
32
+
33
+ def initialize(association, klass)
34
+ super format(MESSAGE, association: association, klass: klass)
35
+ end
36
+ end
37
+
38
+ class PolymorphicNotSpecifiedError < StandardError # :nodoc:
39
+ MESSAGE =
40
+ "'%{association}' is a polymorphic association, therefore " \
41
+ "you must call #of when referencing the association. For example:" \
42
+ "\n\n %{association}.of(SomeModel)\n\n"
43
+
44
+ def initialize(association)
45
+ super format(MESSAGE, association: association)
46
+ end
47
+ end
24
48
  end
@@ -0,0 +1,79 @@
1
+ require 'baby_squeel/join_dependency/injector'
2
+
3
+ module BabySqueel
4
+ module JoinDependency
5
+ # Unfortunately, this is mostly all duplication of
6
+ # ActiveRecord::QueryMethods#build_joins
7
+ class Builder # :nodoc:
8
+ attr_reader :relation
9
+
10
+ def initialize(relation)
11
+ @relation = relation
12
+ @joins_values = relation.joins_values.dup
13
+ end
14
+
15
+ def ensure_associated(*values)
16
+ @joins_values += values
17
+ end
18
+
19
+ def to_join_dependency
20
+ ::ActiveRecord::Associations::JoinDependency.new(
21
+ relation.model,
22
+ association_joins,
23
+ join_list
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def join_list
30
+ join_nodes + join_strings_as_ast
31
+ end
32
+
33
+ def association_joins
34
+ buckets[:association_join] || []
35
+ end
36
+
37
+ def stashed_association_joins
38
+ buckets[:stashed_join] || []
39
+ end
40
+
41
+ def join_nodes
42
+ (buckets[:join_node] || []).uniq
43
+ end
44
+
45
+ def string_joins
46
+ (buckets[:string_join] || []).map(&:strip).uniq
47
+ end
48
+
49
+ if Arel::VERSION >= '7.0.0'
50
+ def join_strings_as_ast
51
+ manager = Arel::SelectManager.new(relation.table)
52
+ relation.send(:convert_join_strings_to_ast, manager, string_joins)
53
+ end
54
+ else
55
+ def join_strings_as_ast
56
+ manager = Arel::SelectManager.new(relation.table.engine, relation.table)
57
+ relation.send(:custom_join_ast, manager, string_joins)
58
+ end
59
+ end
60
+
61
+ def buckets
62
+ @buckets ||= Injector.new(@joins_values).group_by do |join|
63
+ case join
64
+ when String
65
+ :string_join
66
+ when Hash, Symbol, Array
67
+ :association_join
68
+ when ::ActiveRecord::Associations::JoinDependency
69
+ :stashed_join
70
+ when ::Arel::Nodes::Join
71
+ :join_node
72
+ else
73
+ raise 'unknown class: %s' % join.class.name
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,36 @@
1
+ module BabySqueel
2
+ module JoinDependency
3
+ class Finder # :nodoc:
4
+ attr_reader :join_dependency
5
+
6
+ def initialize(join_dependency)
7
+ @join_dependency = join_dependency
8
+ end
9
+
10
+ def find_alias(reflection)
11
+ join_association = find_association(reflection)
12
+ join_association.tables.first if join_association
13
+ end
14
+
15
+ private
16
+
17
+ def find(&block)
18
+ deeply_find(join_dependency.join_root, &block)
19
+ end
20
+
21
+ def find_association(reflection)
22
+ find { |assoc| assoc.reflection == reflection }
23
+ end
24
+
25
+ def deeply_find(root, &block)
26
+ root.children.each do |assoc|
27
+ found = assoc if yield assoc
28
+ found ||= deeply_find(assoc, &block)
29
+ return found if found
30
+ end
31
+
32
+ nil
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ module BabySqueel
2
+ module JoinDependency
3
+ # This class allows BabySqueel to slip custom
4
+ # joins_values into Active Record's JoinDependency
5
+ class Injector < Array # :nodoc:
6
+ def initialize(joins)
7
+ @joins = joins
8
+ end
9
+
10
+ # Active Record will call group_by on this object
11
+ # in ActiveRecord::QueryMethods#build_joins. This
12
+ # allows BabySqueel::JoinExpressions to be treated
13
+ # like typical join hashes until Polyamorous can
14
+ # deal with them.
15
+ def group_by(&block)
16
+ @joins.group_by do |join|
17
+ case join
18
+ when BabySqueel::JoinExpression
19
+ :association_join
20
+ else
21
+ yield join
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ require 'baby_squeel/join_dependency/builder'
2
+ require 'baby_squeel/join_dependency/finder'
3
+
4
+ module BabySqueel
5
+ # This is the thing that gets added to Active Record's joins_values.
6
+ # By including Polyamorous::TreeNode, when this instance is found when
7
+ # traversing joins in ActiveRecord::Associations::JoinDependency::walk_tree,
8
+ # JoinExpression#add_to_tree will be called.
9
+ class JoinExpression
10
+ include Polyamorous::TreeNode
11
+
12
+ def initialize(associations)
13
+ @associations = associations
14
+ end
15
+
16
+ # Each individual association object knows how
17
+ # to build a Polyamorous::Join. Those joins
18
+ # will be added to the hash incrementally.
19
+ def add_to_tree(hash)
20
+ @associations.inject(hash) do |acc, assoc|
21
+ assoc.add_to_tree(acc)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,4 +1,7 @@
1
- require 'baby_squeel/operators'
1
+ require 'baby_squeel/nodes/node'
2
+ require 'baby_squeel/nodes/attribute'
3
+ require 'baby_squeel/nodes/function'
4
+ require 'baby_squeel/nodes/grouping'
2
5
 
3
6
  module BabySqueel
4
7
  module Nodes
@@ -6,8 +9,13 @@ module BabySqueel
6
9
  # Wraps an Arel node in a Proxy so that it can
7
10
  # be extended.
8
11
  def wrap(arel)
9
- if arel.class.parents.include?(Arel)
10
- Generic.new(arel)
12
+ case arel
13
+ when Arel::Nodes::Grouping
14
+ Grouping.new(arel)
15
+ when Arel::Nodes::Function
16
+ Function.new(arel)
17
+ when Arel::Nodes::Node, Arel::Nodes::SqlLiteral
18
+ Node.new(arel)
11
19
  else
12
20
  arel
13
21
  end
@@ -25,85 +33,5 @@ module BabySqueel
25
33
  end
26
34
  end
27
35
  end
28
-
29
- # This proxy class allows us to quack like any arel object. When a
30
- # method missing is hit, we'll instantiate a new proxy object.
31
- class Proxy < ActiveSupport::ProxyObject
32
- # Resolve constants the normal way
33
- def self.const_missing(name)
34
- ::Object.const_get(name)
35
- end
36
-
37
- attr_reader :_arel
38
-
39
- def initialize(arel)
40
- @_arel = Nodes.unwrap(arel)
41
- end
42
-
43
- def respond_to?(meth, include_private = false)
44
- meth.to_s == '_arel' || _arel.respond_to?(meth, include_private)
45
- end
46
-
47
- private
48
-
49
- def method_missing(meth, *args, &block)
50
- if _arel.respond_to?(meth)
51
- Nodes.wrap _arel.send(meth, *Nodes.unwrap(args), &block)
52
- else
53
- super
54
- end
55
- end
56
- end
57
-
58
- # This is a generic proxy class that includes all possible modules.
59
- # In the future, these proxy classes should be more specific and only
60
- # include necessary/applicable modules.
61
- class Generic < Proxy
62
- extend Operators::ArelAliasing
63
- include Operators::Comparison
64
- include Operators::Equality
65
- include Operators::Generic
66
- include Operators::Grouping
67
- include Operators::Matching
68
-
69
- # Extend the Arel node with some extra modules. For example,
70
- # Arel::Nodes::Grouping does not implement Math. InfixOperation doesn't
71
- # implement AliasPredication. Without these extensions, the interface
72
- # just seems inconsistent.
73
- def initialize(node)
74
- node.extend Arel::Math
75
- node.extend Arel::AliasPredication
76
- node.extend Arel::OrderPredications
77
- super(node)
78
- end
79
- end
80
-
81
- class Attribute < Generic
82
- def initialize(parent, name)
83
- @parent = parent
84
- @name = name
85
- super(parent._table[name])
86
- end
87
-
88
- def in(rel)
89
- if rel.is_a? ::ActiveRecord::Relation
90
- ::Arel::Nodes::In.new(self, Arel.sql(rel.to_sql))
91
- else
92
- super
93
- end
94
- end
95
-
96
- def _arel
97
- parent_arel = @parent._arel
98
- parent_arel &&= parent_arel._arel
99
- parent_arel &&= parent_arel.last
100
-
101
- if parent_arel
102
- parent_arel.left[@name]
103
- else
104
- super
105
- end
106
- end
107
- end
108
36
  end
109
37
  end
@@ -0,0 +1,30 @@
1
+ require 'baby_squeel/nodes/node'
2
+
3
+ module BabySqueel
4
+ module Nodes
5
+ class Attribute < Node
6
+ def initialize(parent, name)
7
+ @parent = parent
8
+ @name = name
9
+ super(parent._table[name])
10
+ end
11
+
12
+ def in(rel)
13
+ if rel.is_a? ::ActiveRecord::Relation
14
+ ::Arel::Nodes::In.new(self, Arel.sql(rel.to_sql))
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ def _arel
21
+ if @parent.kind_of? BabySqueel::Association
22
+ table = @parent.find_alias(@parent)
23
+ table ? table[@name] : super
24
+ else
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ require 'baby_squeel/nodes/node'
2
+
3
+ module BabySqueel
4
+ module Nodes
5
+ # See: https://github.com/rails/arel/pull/381
6
+ class Function < Node
7
+ def initialize(node)
8
+ super
9
+ node.extend Arel::OrderPredications
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ require 'baby_squeel/nodes/node'
2
+
3
+ module BabySqueel
4
+ module Nodes
5
+ # See: https://github.com/rails/arel/pull/435
6
+ class Grouping < Node
7
+ def initialize(node)
8
+ super
9
+ node.extend Arel::AliasPredication
10
+ node.extend Arel::OrderPredications
11
+ node.extend Arel::Math
12
+ node.extend Arel::Expressions
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ require 'baby_squeel/operators'
2
+ require 'baby_squeel/nodes/proxy'
3
+
4
+ module BabySqueel
5
+ module Nodes
6
+ # This is a generic proxy class that includes all possible modules.
7
+ # In the future, these proxy classes should be more specific and only
8
+ # include necessary/applicable modules.
9
+ class Node < Proxy
10
+ extend Operators::ArelAliasing
11
+ include Operators::Comparison
12
+ include Operators::Equality
13
+ include Operators::Generic
14
+ include Operators::Grouping
15
+ include Operators::Matching
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,36 @@
1
+ module BabySqueel
2
+ module Nodes
3
+ # This proxy class allows us to quack like any arel object. When a
4
+ # method missing is hit, we'll instantiate a new proxy object.
5
+ class Proxy < ActiveSupport::ProxyObject
6
+ # Resolve constants the normal way
7
+ def self.const_missing(name)
8
+ ::Object.const_get(name)
9
+ end
10
+
11
+ attr_reader :_arel
12
+
13
+ def initialize(arel)
14
+ @_arel = Nodes.unwrap(arel)
15
+ end
16
+
17
+ def inspect
18
+ "BabySqueel{#{super}}"
19
+ end
20
+
21
+ def respond_to?(meth, include_private = false)
22
+ meth.to_s == '_arel' || _arel.respond_to?(meth, include_private)
23
+ end
24
+
25
+ private
26
+
27
+ def method_missing(meth, *args, &block)
28
+ if _arel.respond_to?(meth)
29
+ Nodes.wrap _arel.send(meth, *Nodes.unwrap(args), &block)
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -9,7 +9,7 @@ module BabySqueel
9
9
  # * +arel_name+ - The name of the Arel method you want to alias.
10
10
  #
11
11
  # ==== Example
12
- # BabySqueel::Nodes::Generic.arel_alias :unlike, :does_not_match
12
+ # BabySqueel::Nodes::Node.arel_alias :unlike, :does_not_match
13
13
  # Post.where.has { title.unlike 'something' }
14
14
  #
15
15
  def arel_alias(operator, arel_name)
@@ -15,19 +15,24 @@ module BabySqueel
15
15
  if reflection = _scope.reflect_on_association(name)
16
16
  Association.new(self, reflection)
17
17
  else
18
- raise AssociationNotFoundError.new(_table_name, name)
18
+ not_found_error! name, type: AssociationNotFoundError
19
19
  end
20
20
  end
21
21
 
22
+ # Invokes a sifter defined on the model
23
+ #
24
+ # ==== Examples
25
+ # Post.where.has { sift(:name_contains, 'joe') }
26
+ #
22
27
  def sift(sifter_name, *args)
23
28
  Nodes.wrap _scope.public_send("sift_#{sifter_name}", *args)
24
29
  end
25
30
 
26
31
  private
27
32
 
28
- # @override BabySqueel::Table#_table_name
29
- def _table_name
30
- _scope.model_name
33
+ # @override BabySqueel::Table#not_found_error!
34
+ def not_found_error!(name, type: NotFoundError)
35
+ raise type.new(_scope.model_name, name)
31
36
  end
32
37
 
33
38
  # @override BabySqueel::Table#resolve
@@ -1,12 +1,12 @@
1
- require 'baby_squeel/join_dependency'
1
+ require 'baby_squeel/join_expression'
2
2
 
3
3
  module BabySqueel
4
4
  class Table
5
- attr_accessor :_on, :_join, :_table
5
+ attr_accessor :_on, :_table
6
+ attr_writer :_join
6
7
 
7
8
  def initialize(arel_table)
8
9
  @_table = arel_table
9
- @_join = Arel::Nodes::InnerJoin
10
10
  end
11
11
 
12
12
  # See Arel::Table#[]
@@ -14,23 +14,27 @@ module BabySqueel
14
14
  Nodes::Attribute.new(self, key)
15
15
  end
16
16
 
17
+ def _join
18
+ @_join ||= Arel::Nodes::InnerJoin
19
+ end
20
+
17
21
  # Alias a table. This is only possible when joining
18
22
  # an association explicitly.
19
23
  def alias(alias_name)
20
24
  clone.alias! alias_name
21
25
  end
22
26
 
23
- def alias!(alias_name)
27
+ def alias!(alias_name) # :nodoc:
24
28
  self._table = _table.alias(alias_name)
25
29
  self
26
30
  end
27
31
 
28
- # Instruct the table to be joined with an INNER JOIN.
32
+ # Instruct the table to be joined with a LEFT OUTER JOIN.
29
33
  def outer
30
34
  clone.outer!
31
35
  end
32
36
 
33
- def outer!
37
+ def outer! # :nodoc:
34
38
  self._join = Arel::Nodes::OuterJoin
35
39
  self
36
40
  end
@@ -40,7 +44,7 @@ module BabySqueel
40
44
  clone.inner!
41
45
  end
42
46
 
43
- def inner!
47
+ def inner! # :nodoc:
44
48
  self._join = Arel::Nodes::InnerJoin
45
49
  self
46
50
  end
@@ -50,26 +54,48 @@ module BabySqueel
50
54
  clone.on! node
51
55
  end
52
56
 
53
- def on!(node)
54
- self._on = Arel::Nodes::On.new(node)
57
+ def on!(node) # :nodoc:
58
+ self._on = node
55
59
  self
56
60
  end
57
61
 
62
+ # When referencing a joined table, the tables that
63
+ # attributes reference can change (due to aliasing).
64
+ # This method allows BabySqueel::Nodes::Attribute
65
+ # instances to find what their alias will be.
66
+ def find_alias(association, associations = [])
67
+ builder = JoinDependency::Builder.new(_scope.all)
68
+ builder.ensure_associated(_arel(associations))
69
+
70
+ finder = JoinDependency::Finder.new(builder.to_join_dependency)
71
+ finder.find_alias(association._reflection)
72
+ end
73
+
58
74
  # This method will be invoked by BabySqueel::Nodes::unwrap. When called,
59
- # there are two possible outcomes:
75
+ # there are three possible outcomes:
60
76
  #
61
- # 1. Join explicitly using an on clause.
62
- # 2. Resolve the assocition's join clauses using ActiveRecord.
77
+ # 1. Join explicitly using an on clause. Just return Arel.
78
+ # 2. Implicit join without using an outer join. In this case, we'll just
79
+ # give a hash to Active Record, and join the normal way.
80
+ # 3. Implicit join using an outer join. In this case, we need to use
81
+ # Polyamorous to build the join. We'll return a JoinExpression.
63
82
  #
64
83
  def _arel(associations = [])
65
- return unless _on || associations.any?
66
- JoinDependency.new(self, associations)
84
+ if _on
85
+ _join.new(_table, Arel::Nodes::On.new(_on))
86
+ elsif associations.any?(&:needs_polyamorous?)
87
+ JoinExpression.new(associations)
88
+ elsif associations.any?
89
+ associations.reverse.inject({}) do |names, assoc|
90
+ { assoc._reflection.name => names }
91
+ end
92
+ end
67
93
  end
68
94
 
69
95
  private
70
96
 
71
- def _table_name
72
- arel_table.name
97
+ def not_found_error!
98
+ raise NotImplementedError, 'BabySqueel::Table will never raise a NotFoundError'
73
99
  end
74
100
 
75
101
  def resolve(name)
@@ -82,10 +108,7 @@ module BabySqueel
82
108
 
83
109
  def method_missing(name, *args, &block)
84
110
  return super if !args.empty? || block_given?
85
-
86
- resolve(name) || begin
87
- raise NotFoundError.new(_table_name, name)
88
- end
111
+ resolve(name) || not_found_error!(name)
89
112
  end
90
113
  end
91
114
  end
@@ -1,3 +1,3 @@
1
1
  module BabySqueel
2
- VERSION = '0.3.1'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: baby_squeel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Zane
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-08-02 00:00:00.000000000 Z
11
+ date: 2016-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.0.0
19
+ version: 4.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.0.0
26
+ version: 4.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: polyamorous
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -88,17 +102,28 @@ extensions: []
88
102
  extra_rdoc_files: []
89
103
  files:
90
104
  - CHANGELOG.md
105
+ - ISSUE_TEMPLATE.md
91
106
  - LICENSE.txt
92
107
  - README.md
93
108
  - baby_squeel.gemspec
94
109
  - lib/baby_squeel.rb
95
- - lib/baby_squeel/active_record.rb
110
+ - lib/baby_squeel/active_record/base.rb
111
+ - lib/baby_squeel/active_record/query_methods.rb
112
+ - lib/baby_squeel/active_record/where_chain.rb
96
113
  - lib/baby_squeel/association.rb
97
114
  - lib/baby_squeel/compat.rb
98
115
  - lib/baby_squeel/dsl.rb
99
116
  - lib/baby_squeel/errors.rb
100
- - lib/baby_squeel/join_dependency.rb
117
+ - lib/baby_squeel/join_dependency/builder.rb
118
+ - lib/baby_squeel/join_dependency/finder.rb
119
+ - lib/baby_squeel/join_dependency/injector.rb
120
+ - lib/baby_squeel/join_expression.rb
101
121
  - lib/baby_squeel/nodes.rb
122
+ - lib/baby_squeel/nodes/attribute.rb
123
+ - lib/baby_squeel/nodes/function.rb
124
+ - lib/baby_squeel/nodes/grouping.rb
125
+ - lib/baby_squeel/nodes/node.rb
126
+ - lib/baby_squeel/nodes/proxy.rb
102
127
  - lib/baby_squeel/operators.rb
103
128
  - lib/baby_squeel/relation.rb
104
129
  - lib/baby_squeel/table.rb
@@ -123,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
148
  version: '0'
124
149
  requirements: []
125
150
  rubyforge_project:
126
- rubygems_version: 2.4.8
151
+ rubygems_version: 2.5.1
127
152
  signing_key:
128
153
  specification_version: 4
129
154
  summary: A tiny squeel implementation without all of the evil.
@@ -1,62 +0,0 @@
1
- module BabySqueel
2
- class JoinDependency
3
- delegate :_scope, :_join, :_on, :_table, to: :@table
4
-
5
- def initialize(table, associations = [])
6
- @table = table
7
- @associations = associations
8
- end
9
-
10
- if ActiveRecord::VERSION::STRING < '4.1.0'
11
- def bind_values
12
- return [] unless relation?
13
- _scope.joins(join_names(@associations)).bind_values
14
- end
15
- else
16
- def bind_values
17
- return [] unless relation?
18
- relation = _scope.joins(join_names(@associations))
19
- relation.arel.bind_values + relation.bind_values
20
- end
21
- end
22
-
23
- # Converts an array of BabySqueel::Associations into an array
24
- # of Arel join nodes.
25
- #
26
- # Each association is built individually so that the correct
27
- # Arel join node will be used for each individual association.
28
- def _arel
29
- if _on
30
- [_join.new(_table, _on)]
31
- else
32
- @associations.each.with_index.inject([]) do |joins, (assoc, i)|
33
- construct @associations[0..i], joins, assoc._join
34
- end
35
- end
36
- end
37
-
38
- private
39
-
40
- def relation?
41
- @table.kind_of? BabySqueel::Relation
42
- end
43
-
44
- def construct(associations, theirs, join_node)
45
- names = join_names associations
46
- mine = build names, join_node
47
- theirs + mine[theirs.length..-1]
48
- end
49
-
50
- def build(names, join_node)
51
- _scope.joins(names).join_sources.map do |join|
52
- join_node.new(join.left, join.right)
53
- end
54
- end
55
-
56
- def join_names(associations = [])
57
- associations.reverse.inject({}) do |names, assoc|
58
- { assoc._reflection.name => names }
59
- end
60
- end
61
- end
62
- end