squeel 1.1.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +36 -4
  5. data/CHANGELOG.md +15 -0
  6. data/Gemfile +1 -1
  7. data/README.md +47 -6
  8. data/Rakefile +14 -2
  9. data/lib/squeel.rb +9 -1
  10. data/lib/squeel/adapters/active_record.rb +0 -1
  11. data/lib/squeel/adapters/active_record/3.0/join_dependency_extensions.rb +1 -0
  12. data/lib/squeel/adapters/active_record/3.0/relation_extensions.rb +12 -1
  13. data/lib/squeel/adapters/active_record/3.1/join_dependency_extensions.rb +1 -0
  14. data/lib/squeel/adapters/active_record/3.2/join_dependency_extensions.rb +1 -0
  15. data/lib/squeel/adapters/active_record/4.0/join_dependency_extensions.rb +1 -0
  16. data/lib/squeel/adapters/active_record/4.0/relation_extensions.rb +92 -0
  17. data/lib/squeel/adapters/active_record/4.1/compat.rb +15 -0
  18. data/lib/squeel/adapters/active_record/4.1/context.rb +88 -0
  19. data/lib/squeel/adapters/active_record/4.1/preloader_extensions.rb +31 -0
  20. data/lib/squeel/adapters/active_record/4.1/reflection_extensions.rb +37 -0
  21. data/lib/squeel/adapters/active_record/4.1/relation_extensions.rb +307 -0
  22. data/lib/squeel/adapters/active_record/4.2/compat.rb +1 -0
  23. data/lib/squeel/adapters/active_record/4.2/context.rb +1 -0
  24. data/lib/squeel/adapters/active_record/4.2/preloader_extensions.rb +1 -0
  25. data/lib/squeel/adapters/active_record/4.2/relation_extensions.rb +108 -0
  26. data/lib/squeel/adapters/active_record/context.rb +7 -7
  27. data/lib/squeel/adapters/active_record/join_dependency_extensions.rb +9 -13
  28. data/lib/squeel/adapters/active_record/relation_extensions.rb +38 -8
  29. data/lib/squeel/core_ext/symbol.rb +3 -3
  30. data/lib/squeel/dsl.rb +1 -1
  31. data/lib/squeel/nodes.rb +1 -0
  32. data/lib/squeel/nodes/as.rb +12 -0
  33. data/lib/squeel/nodes/join.rb +8 -4
  34. data/lib/squeel/nodes/key_path.rb +10 -1
  35. data/lib/squeel/nodes/node.rb +21 -0
  36. data/lib/squeel/nodes/stub.rb +8 -4
  37. data/lib/squeel/nodes/subquery_join.rb +44 -0
  38. data/lib/squeel/version.rb +1 -1
  39. data/lib/squeel/visitors.rb +2 -0
  40. data/lib/squeel/visitors/enumeration_visitor.rb +101 -0
  41. data/lib/squeel/visitors/order_visitor.rb +9 -2
  42. data/lib/squeel/visitors/predicate_visitor.rb +11 -0
  43. data/lib/squeel/visitors/preload_visitor.rb +12 -0
  44. data/lib/squeel/visitors/visitor.rb +89 -13
  45. data/spec/config.travis.yml +13 -0
  46. data/spec/config.yml +12 -0
  47. data/spec/console.rb +3 -12
  48. data/spec/core_ext/symbol_spec.rb +3 -3
  49. data/spec/helpers/squeel_helper.rb +8 -5
  50. data/spec/spec_helper.rb +4 -16
  51. data/spec/squeel/adapters/active_record/context_spec.rb +8 -4
  52. data/spec/squeel/adapters/active_record/join_dependency_extensions_spec.rb +123 -38
  53. data/spec/squeel/adapters/active_record/relation_extensions_spec.rb +350 -124
  54. data/spec/squeel/core_ext/symbol_spec.rb +3 -3
  55. data/spec/squeel/nodes/join_spec.rb +4 -4
  56. data/spec/squeel/nodes/stub_spec.rb +3 -3
  57. data/spec/squeel/nodes/subquery_join_spec.rb +46 -0
  58. data/spec/squeel/visitors/order_visitor_spec.rb +3 -3
  59. data/spec/squeel/visitors/predicate_visitor_spec.rb +69 -36
  60. data/spec/squeel/visitors/select_visitor_spec.rb +1 -1
  61. data/spec/squeel/visitors/visitor_spec.rb +7 -6
  62. data/spec/support/models.rb +99 -15
  63. data/spec/support/schema.rb +109 -4
  64. data/squeel.gemspec +8 -6
  65. metadata +89 -107
  66. data/.ruby-gemset +0 -1
  67. data/.ruby-version +0 -1
  68. data/spec/blueprints/articles.rb +0 -5
  69. data/spec/blueprints/comments.rb +0 -5
  70. data/spec/blueprints/notes.rb +0 -3
  71. data/spec/blueprints/people.rb +0 -4
  72. data/spec/blueprints/tags.rb +0 -3
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3d3196690094de459e3e30179f68ce63ee2032e1
4
+ data.tar.gz: 827a3e061d72e29cac9b615cbc3fb66c6c1e3462
5
+ SHA512:
6
+ metadata.gz: fcf69aceda5afe483dfad4a91d40b21d8472b250c5a9eb719afaa6ccc1252d9488d176c0cc9d41d7a68e18a66f0dd5ff33dbcf4d77b59fd12c673e311cae680c
7
+ data.tar.gz: 6a934cabda1f179d9a920cf4974fe8776b68d4a58578ee2c067ae4f1e7fac78a0989268920e711845d78367d66521c297e0cb8215e5d8fe85b336d275ae0d249
data/.gitignore CHANGED
@@ -3,3 +3,6 @@
3
3
  Gemfile.lock
4
4
  pkg/*
5
5
  .rvmrc
6
+ .ruby-version
7
+ .ruby-gemset
8
+ script
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --backtrace
@@ -1,8 +1,40 @@
1
+ before_script:
2
+ - mysql -e "create database squeel_test_examples;"
3
+ - psql -c "create database squeel_test_examples;" -U postgres
4
+
5
+ script:
6
+ - bundle exec rake test:$ADAPTER
7
+
1
8
  rvm:
2
9
  - 1.9.3
10
+ - 2.0.0
11
+ - 2.1.1
12
+ - 2.1.2
3
13
 
4
14
  env:
5
- - RAILS=4-0-stable AREL=master
6
- - RAILS=3-2-stable AREL=3-0-stable
7
- - RAILS=3-1-stable AREL=2-2-stable
8
- - RAILS=3-0-stable AREL=2-0-stable
15
+ global:
16
+ - SQ_CONFIG_FILE=$TRAVIS_BUILD_DIR/spec/config.travis.yml
17
+ matrix:
18
+ - RAILS=master AREL=master ADAPTER=sqlite3
19
+ - RAILS=4-1-stable AREL=5-0-stable ADAPTER=sqlite3
20
+ - RAILS=4-0-stable AREL=4-0-stable ADAPTER=sqlite3
21
+ - RAILS=3-2-stable AREL=3-0-stable ADAPTER=sqlite3
22
+ - RAILS=3-1-stable AREL=2-2-stable ADAPTER=sqlite3
23
+ - RAILS=3-0-stable AREL=2-0-stable ADAPTER=sqlite3
24
+ - RAILS=master AREL=master ADAPTER=mysql
25
+ - RAILS=4-1-stable AREL=5-0-stable ADAPTER=mysql
26
+ - RAILS=4-0-stable AREL=4-0-stable ADAPTER=mysql
27
+ - RAILS=3-2-stable AREL=3-0-stable ADAPTER=mysql
28
+ - RAILS=3-1-stable AREL=2-2-stable ADAPTER=mysql
29
+ - RAILS=3-0-stable AREL=2-0-stable ADAPTER=mysql
30
+ - RAILS=master AREL=master ADAPTER=mysql2
31
+ - RAILS=4-1-stable AREL=5-0-stable ADAPTER=mysql2
32
+ - RAILS=4-0-stable AREL=4-0-stable ADAPTER=mysql2
33
+ - RAILS=3-2-stable AREL=3-0-stable ADAPTER=mysql2
34
+ - RAILS=3-1-stable AREL=2-2-stable ADAPTER=mysql2
35
+ - RAILS=master AREL=master ADAPTER=postgresql
36
+ - RAILS=4-1-stable AREL=5-0-stable ADAPTER=postgresql
37
+ - RAILS=4-0-stable AREL=4-0-stable ADAPTER=postgresql
38
+ - RAILS=3-2-stable AREL=3-0-stable ADAPTER=postgresql
39
+ - RAILS=3-1-stable AREL=2-2-stable ADAPTER=postgresql
40
+ - RAILS=3-0-stable AREL=2-0-stable ADAPTER=postgresql
@@ -1,3 +1,18 @@
1
+ ## 1.2.1 (Unreleased)
2
+
3
+ * Run all specs against sqlite, mysql and postgresql!
4
+ * Genereta table names correctly when joining through an association. Fixes#302.
5
+ * Enable Arel nodes in Squeel with "|" operator. Fixes #314.
6
+ * Properly append binds from a relation used in a subquery. Fixes #272.
7
+
8
+ ## 1.2.0 (2014-07-16)
9
+
10
+ * Add compatibility to Ruby 2.0+ with Rails 4.1 and 4.2.0.alpha.
11
+ Fixes #301, #305, #307
12
+ * Enable using a relation as a subquery. Fixes #309
13
+ * Bind params correctly in subquery using associations. Fixes #312
14
+ * Use the correct attribute name when finding a Join node. Fixes #273.
15
+
1
16
  ## 1.1.1 (2013-09-03)
2
17
 
3
18
  * Update relation extensions to support new count behavior in Active Record
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ gemspec
3
3
 
4
4
  gem 'rake'
5
5
 
6
- rails = ENV['RAILS'] || '4-0-stable'
6
+ rails = ENV['RAILS'] || 'master'
7
7
  arel = ENV['AREL'] || 'master'
8
8
 
9
9
  arel_opts = case arel
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Squeel [![Build Status](https://secure.travis-ci.org/ernie/squeel.png)](http://travis-ci.org/ernie/squeel) [![endorse](http://api.coderwall.com/ernie/endorsecount.png)](http://coderwall.com/ernie)
1
+ # Squeel [![Build Status](https://secure.travis-ci.org/activerecord-hackery/squeel.png)](http://travis-ci.org/activerecord-hackery/squeel) [![endorse](http://api.coderwall.com/ernie/endorsecount.png)](http://coderwall.com/ernie)
2
2
 
3
3
  Squeel lets you write your Active Record queries with fewer strings, and more Ruby,
4
4
  by making the Arel awesomeness that lies beneath Active Record more accessible.
@@ -23,8 +23,10 @@ just a simple example -- Squeel's capable of a whole lot more. Keep reading.
23
23
  In your Gemfile:
24
24
 
25
25
  ```ruby
26
+ # Make sure you are using the latest version of polyamorous
27
+ gem "polyamorous", :git => "git://github.com/activerecord-hackery/polyamorous.git"
26
28
  gem "squeel" # Last officially released gem
27
- # gem "squeel", :git => "git://github.com/ernie/squeel.git" # Track git repo
29
+ # gem "squeel", :git => "git://github.com/activerecord-hackery/squeel.git" # Track git repo
28
30
  ```
29
31
 
30
32
  Then bundle as usual.
@@ -86,6 +88,24 @@ A Squeel keypath is essentially a more concise and readable alternative to a
86
88
  deeply nested hash. For instance, in standard Active Record, you might join several
87
89
  associations like this to perform a query:
88
90
 
91
+ #### Rails 4+
92
+
93
+ ```ruby
94
+ Person.joins(:articles => {:comments => :person}).references(:all)
95
+ # => SELECT "people".* FROM "people"
96
+ # LEFT OUTER JOIN "articles" ON "articles"."person_id" = "people"."id"
97
+ # LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id"
98
+ # LEFT OUTER JOIN "people" "people_comments" ON "people_comments"."id" = "comments"."person_id"
99
+ ```
100
+
101
+ With a keypath, this would look like:
102
+
103
+ ```ruby
104
+ Person.joins{articles.comments.person}.references(:all)
105
+ ```
106
+
107
+ #### Rails 3.x
108
+
89
109
  ```ruby
90
110
  Person.joins(:articles => {:comments => :person})
91
111
  # => SELECT "people".* FROM "people"
@@ -413,6 +433,27 @@ Person.joins{children.parent.children}.
413
433
 
414
434
  Keypaths were used here for clarity, but nested hashes would work just as well.
415
435
 
436
+ You can also use a subquery in a join.
437
+
438
+ Notice:
439
+ 1. Squeel can only accept an ActiveRecord::Relation class of subqueries in a join.
440
+ 2. Use the chain with caution. You should call `as` first to get a Nodes::As, then call `on` to get a join node.
441
+
442
+ ```ruby
443
+ subquery = OrderItem.group(:orderable_id).select { [orderable_id, sum(quantity * unit_price).as(amount)] }
444
+ Seat.joins { [payment.outer, subquery.as('seat_order_items').on { id == seat_order_items.orderable_id}.outer] }.
445
+ select { [seat_order_items.amount, "seats.*"] }
446
+ # => SELECT "seat_order_items"."amount", seats.*
447
+ # FROM "seats"
448
+ # LEFT OUTER JOIN "payments" ON "payments"."id" = "seats"."payment_id"
449
+ # LEFT OUTER JOIN (
450
+ # SELECT "order_items"."orderable_id",
451
+ # sum("order_items"."quantity" * "order_items"."unit_price") AS amount
452
+ # FROM "order_items"
453
+ # GROUP BY "order_items"."orderable_id"
454
+ # ) seat_order_items ON "seats"."id" = "seat_order_items"."orderable_id"
455
+ ```
456
+
416
457
  ### Functions
417
458
 
418
459
  You can call SQL functions just like you would call a method in Ruby...
@@ -470,7 +511,7 @@ As you can see, just like functions, these operations can be given aliases.
470
511
  To select more than one attribute (or calculated attribute) simply put them into an array:
471
512
 
472
513
  ```ruby
473
- p = Person.select{[ name.op('||', '-diddly').as(flanderized_name),
514
+ p = Person.select{[ name.op('||', '-diddly').as(flanderized_name),
474
515
  coalesce(name, '<no name given>').as(name_with_default) ]}.first
475
516
  p.flanderized_name
476
517
  # => "Aric Smith-diddly"
@@ -537,9 +578,9 @@ For further information, see
537
578
  [this post to the Rails list](https://groups.google.com/forum/?fromgroups=#!topic/rubyonrails-core/NQJJzZ7R7S0),
538
579
  [this commit](https://github.com/lifo/docrails/commit/50c5005bafe7e43f81a141cd2c512379aec74325) to
539
580
  the [Active Record guides](http://edgeguides.rubyonrails.org/active_record_querying.html#hash-conditions),
540
- [#67](https://github.com/ernie/squeel/issues/67),
541
- [#75](https://github.com/ernie/squeel/issues/75), and
542
- [#171](https://github.com/ernie/squeel/issues/171).
581
+ [#67](https://github.com/activerecord-hackery/squeel/issues/67),
582
+ [#75](https://github.com/activerecord-hackery/squeel/issues/75), and
583
+ [#171](https://github.com/activerecord-hackery/squeel/issues/171).
543
584
 
544
585
  ## Compatibility with MetaWhere
545
586
 
data/Rakefile CHANGED
@@ -4,10 +4,22 @@ require 'rspec/core/rake_task'
4
4
  Bundler::GemHelper.install_tasks
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec) do |rspec|
7
- rspec.rspec_opts = ['--backtrace']
7
+ rspec.rspec_opts = ['--backtrace', '--color', '--format documentation']
8
8
  end
9
9
 
10
- task :default => :spec
10
+ task :default => 'test_sqlite3'
11
+
12
+ %w(sqlite3 mysql mysql2 postgresql).each do |adapter|
13
+ namespace :test do
14
+ task(adapter => ["#{adapter}:env", "spec"])
15
+ end
16
+
17
+ namespace adapter do
18
+ task(:env) { ENV['SQ_DB'] = adapter }
19
+ end
20
+
21
+ task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"]
22
+ end
11
23
 
12
24
  desc "Open an irb session with Squeel and the sample data used in specs"
13
25
  task :console do
@@ -1,7 +1,16 @@
1
1
  require 'squeel/configuration'
2
+ require 'polyamorous'
2
3
 
3
4
  module Squeel
4
5
 
6
+ if defined?(Arel::InnerJoin)
7
+ InnerJoin = Arel::InnerJoin
8
+ OuterJoin = Arel::OuterJoin
9
+ else
10
+ InnerJoin = Arel::Nodes::InnerJoin
11
+ OuterJoin = Arel::Nodes::OuterJoin
12
+ end
13
+
5
14
  extend Configuration
6
15
 
7
16
  # Prevent warnings on the console when doing things some might describe as "evil"
@@ -30,7 +39,6 @@ module Squeel
30
39
  alias_predicate aliaz, original
31
40
  end
32
41
  end
33
-
34
42
  end
35
43
 
36
44
  require 'squeel/dsl'
@@ -1,7 +1,6 @@
1
1
  case ActiveRecord::VERSION::MAJOR
2
2
  when 3, 4
3
3
  ActiveRecord::Relation.send :include, Squeel::Nodes::Aliasing
4
- require 'squeel/adapters/active_record/join_dependency_extensions'
5
4
  require 'squeel/adapters/active_record/base_extensions'
6
5
 
7
6
  adapter_directory = "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
@@ -0,0 +1 @@
1
+ require 'squeel/adapters/active_record/join_dependency_extensions'
@@ -54,8 +54,9 @@ module Squeel
54
54
  end
55
55
 
56
56
  stashed_association_joins = joins.grep(::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)
57
+ subquery_joins = joins.grep(Nodes::SubqueryJoin)
57
58
 
58
- non_association_joins = (joins - association_joins - stashed_association_joins)
59
+ non_association_joins = (joins - association_joins - stashed_association_joins - subquery_joins)
59
60
  custom_joins = custom_join_sql(*non_association_joins)
60
61
 
61
62
  self.join_dependency = JoinDependency.new(@klass, association_joins, custom_joins)
@@ -79,6 +80,16 @@ module Squeel
79
80
  relation = relation.join(left, join_type).on(*right)
80
81
  end
81
82
 
83
+ subquery_joins.each do |join|
84
+ relation = relation.
85
+ join(
86
+ Arel::Nodes::TableAlias.new(
87
+ join.subquery.right,
88
+ Arel::Nodes::Grouping.new(join.subquery.left.arel.ast)),
89
+ join.type).
90
+ on(*where_visit(join.constraints))
91
+ end
92
+
82
93
  relation = relation.join(custom_joins)
83
94
  end
84
95
 
@@ -0,0 +1 @@
1
+ require 'squeel/adapters/active_record/join_dependency_extensions'
@@ -0,0 +1 @@
1
+ require 'squeel/adapters/active_record/join_dependency_extensions'
@@ -0,0 +1 @@
1
+ require 'squeel/adapters/active_record/join_dependency_extensions'
@@ -30,6 +30,24 @@ module Squeel
30
30
  end
31
31
  end
32
32
 
33
+ def where_unscoping(target_value)
34
+ target_value = target_value.to_s
35
+
36
+ where_values.reject! do |rel|
37
+ case rel
38
+ when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
39
+ subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
40
+ subrelation.name == target_value
41
+ when Hash
42
+ rel.stringify_keys.has_key?(target_value)
43
+ when Squeel::Nodes::Predicate
44
+ rel.expr.symbol.to_s == target_value
45
+ end
46
+ end
47
+
48
+ bind_values.reject! { |col,_| col.name == target_value }
49
+ end
50
+
33
51
  def build_arel
34
52
  arel = Arel::SelectManager.new(table.engine, table)
35
53
 
@@ -52,9 +70,49 @@ module Squeel
52
70
  arel.from(build_from) if from_value
53
71
  arel.lock(lock_value) if lock_value
54
72
 
73
+ # Reorder bind indexes when joins or subqueries include more bindings.
74
+ # Special for PostgreSQL
75
+ if bind_values.size > 1
76
+ bvs = bind_values
77
+ arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i|
78
+ column = bvs[i].first
79
+ bp.replace connection.substitute_at(column, i)
80
+ end
81
+ end
82
+
55
83
  arel
56
84
  end
57
85
 
86
+ def build_where(opts, other = [])
87
+ case opts
88
+ when String, Array
89
+ super
90
+ else # Let's prevent PredicateBuilder from doing its thing
91
+ [opts, *other].map do |arg|
92
+ case arg
93
+ when Array # Just in case there's an array in there somewhere
94
+ @klass.send(:sanitize_sql, arg)
95
+ when Hash
96
+ attributes = @klass.send(:expand_hash_conditions_for_aggregates, arg)
97
+
98
+ attributes.values.grep(::ActiveRecord::Relation) do |rel|
99
+ self.bind_values += rel.bind_values
100
+ end
101
+
102
+ preprocess_attrs_with_ar(attributes)
103
+
104
+ when Squeel::Nodes::Node
105
+ arg.grep(::ActiveRecord::Relation) do |rel|
106
+ self.bind_values += rel.bind_values
107
+ end
108
+ arg
109
+ else
110
+ arg
111
+ end
112
+ end
113
+ end
114
+ end
115
+
58
116
  def build_from
59
117
  opts, name = from_visit(from_value)
60
118
  case opts
@@ -109,6 +167,40 @@ module Squeel
109
167
  self
110
168
  end
111
169
 
170
+ def where_values_hash_with_squeel(relation_table_name = table_name)
171
+ equalities = find_equality_predicates(where_visit(with_default_scope.where_values), relation_table_name)
172
+
173
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
174
+
175
+ Hash[equalities.map { |where|
176
+ name = where.left.name
177
+ [name, binds.fetch(name.to_s) { where.right }]
178
+ }]
179
+ end
180
+
181
+ def to_sql_with_binding_params
182
+ @to_sql ||= begin
183
+ relation = self
184
+ connection = klass.connection
185
+
186
+ if eager_loading?
187
+ find_with_associations { |rel| relation = rel }
188
+ end
189
+
190
+ ast = relation.arel.ast
191
+ binds = relation.bind_values.dup
192
+
193
+ visitor = connection.visitor.clone
194
+ visitor.class_eval do
195
+ include ::Arel::Visitors::BindVisitor
196
+ end
197
+
198
+ visitor.accept(ast) do
199
+ connection.quote(*binds.shift.reverse)
200
+ end
201
+ end
202
+ end
203
+
112
204
  private
113
205
 
114
206
  def dehashified_order_values
@@ -0,0 +1,15 @@
1
+ require 'squeel/adapters/active_record/compat'
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ class AssociationScope
6
+ def eval_scope(klass, scope, owner)
7
+ if scope.is_a?(Relation)
8
+ scope
9
+ else
10
+ klass.unscoped.instance_exec(owner, &scope).visited
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,88 @@
1
+ require 'squeel/adapters/active_record/context'
2
+
3
+ module Squeel
4
+ module Adapters
5
+ module ActiveRecord
6
+ class Context < ::Squeel::Context
7
+ class NoParentFoundError < RuntimeError; end
8
+
9
+ def initialize(object)
10
+ super
11
+ @base = object.join_root
12
+ @engine = @base.base_klass.arel_engine
13
+ @arel_visitor = get_arel_visitor
14
+ @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
15
+ end
16
+
17
+ def find(object, parent = @base)
18
+ if ::ActiveRecord::Associations::JoinDependency::JoinPart === parent
19
+ case object
20
+ when String, Symbol, Nodes::Stub
21
+ assoc_name = object.to_s
22
+ find_string_symbol_stub_association(@base.children, @base, assoc_name, parent)
23
+ when Nodes::Join
24
+ find_node_join_association(@base.children, @base, object, parent)
25
+ else
26
+ find_other_association(@base.children, @base, object, parent)
27
+ end
28
+ end
29
+ end
30
+
31
+ def find!(object, parent = @base)
32
+ if ::ActiveRecord::Associations::JoinDependency::JoinPart === parent
33
+ result =
34
+ case object
35
+ when String, Symbol, Nodes::Stub
36
+ assoc_name = object.to_s
37
+ find_string_symbol_stub_association(@base.children, @base, assoc_name, parent)
38
+ when Nodes::Join
39
+ find_node_join_association(@base.children, @base, object, parent)
40
+ else
41
+ find_other_association(@base.children, @base, object, parent)
42
+ end
43
+
44
+ result || raise(NoParentFoundError, "can't find #{object} in #{parent}")
45
+ else
46
+ raise NoParentFoundError, "can't find #{object} in #{parent}"
47
+ end
48
+ end
49
+
50
+ def traverse!(keypath, parent = @base, include_endpoint = false)
51
+ parent = @base if keypath.absolute?
52
+ keypath.path_without_endpoint.each do |key|
53
+ parent = find!(key, parent)
54
+ end
55
+ parent = find!(keypath.endpoint, parent) if include_endpoint
56
+
57
+ parent
58
+ end
59
+
60
+ private
61
+ def find_string_symbol_stub_association(join_associations, current_parent, assoc_name, target_parent)
62
+ join_associations.each do |assoc|
63
+ return assoc if assoc.reflection.name.to_s == assoc_name && current_parent.equal?(target_parent)
64
+ child_assoc = find_string_symbol_stub_association(assoc.children, assoc, assoc_name, target_parent)
65
+ return child_assoc if child_assoc
66
+ end && false
67
+ end
68
+
69
+ def find_node_join_association(join_associations, current_parent, object, target_parent)
70
+ join_associations.each do |assoc|
71
+ return assoc if assoc.reflection.name == object._name && current_parent.equal?(target_parent) &&
72
+ (object.polymorphic? ? assoc.reflection.klass == object._klass : true)
73
+ child_assoc = find_node_join_association(assoc.children, assoc, object, target_parent)
74
+ return child_assoc if child_assoc
75
+ end && false
76
+ end
77
+
78
+ def find_other_association(join_associations, current_parent, object, target_parent)
79
+ join_associations.each do |assoc|
80
+ return assoc if assoc.reflection == object && current_parent.equal?(target_parent)
81
+ child_assoc = find_other_association(assoc.children, assoc, object, target_parent)
82
+ return child_assoc if child_assoc
83
+ end && false
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end