baby_squeel 0.3.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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