scoped_search 4.1.8 → 4.1.9

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
  SHA256:
3
- metadata.gz: 76592b4cfe81be4cef968ea35cd9659957af349738fb799ce5e34608247b320a
4
- data.tar.gz: 43e889676986012a6827d6904c616e66e73eecd7a4381ed4e4eb573da270f3f1
3
+ metadata.gz: 9c1453ebb8fe2391f71ea35171f2115ed89bb7920a1a042d86a4393fe218888c
4
+ data.tar.gz: 61d2d0e948a952f7e955240747dae7e529103c5871ad3160cb2213574f0f155a
5
5
  SHA512:
6
- metadata.gz: 6d0d6000ce3a827d33c24591548fcbc6c477c0a9d878dfa39c4a92c5eaaa9bdfe619a5df6c410b2469ebfff4c88a41aef7669afeb591e5d5d6e9a47704bf873c
7
- data.tar.gz: d9a20afd3c5328f2195b694a3c305a4f00e4a104680fcf8292afb2b1e7cd838da86ce07c03648312d670d010ec7cda38684bc5be24e9841ffbc445dfc5e9e6d8
6
+ metadata.gz: 04e61c564c6117d60b6f82b970df5c6c7a68362d828d89d1cf5d2bf7f6b7c9ea777cf641dbb2a21be895905305c0c7305105c9840d391aa702cd69eab706fd4d
7
+ data.tar.gz: 842c31b1714558d8bd4578d47b87bca4fa4298617a16168b66a77e6ff06ee1f43406b30be3557a9c50222ba5effaa9c5eec27832839e75bb11faf085f95bb7bc
@@ -21,6 +21,7 @@ rvm:
21
21
  - "2.4.0"
22
22
  - "2.5.1"
23
23
  - "2.6.0"
24
+ - "2.7.1"
24
25
  - ruby-head
25
26
  - jruby-19mode
26
27
  - jruby-head
@@ -76,3 +77,13 @@ matrix:
76
77
  gemfile: Gemfile.activerecord60_with_activesupport60
77
78
  - rvm: "2.4.0"
78
79
  gemfile: Gemfile.activerecord60_with_activesupport60
80
+ - rvm: "2.7.1"
81
+ gemfile: Gemfile.activerecord42
82
+ - rvm: "2.7.1"
83
+ gemfile: Gemfile.activerecord50
84
+ - rvm: "2.7.1"
85
+ gemfile: Gemfile.activerecord51
86
+ - rvm: "2.7.1"
87
+ gemfile: Gemfile.activerecord52
88
+ - rvm: "2.7.1"
89
+ gemfile: Gemfile.activerecord52_with_activesupport52
@@ -229,11 +229,13 @@ module ScopedSearch
229
229
 
230
230
  if field.relation && definition.reflection_by_name(field.definition.klass, field.relation).macro == :has_many
231
231
  connection = field.definition.klass.connection
232
- primary_key = "#{connection.quote_table_name(field.definition.klass.table_name)}.#{connection.quote_column_name(field.definition.klass.primary_key)}"
233
- key, join_table = if definition.reflection_by_name(field.definition.klass, field.relation).options.has_key?(:through)
232
+ reflection = definition.reflection_by_name(field.definition.klass, field.relation)
233
+ primary_key_col = reflection.options[:primary_key] || field.definition.klass.primary_key
234
+ primary_key = "#{connection.quote_table_name(field.definition.klass.table_name)}.#{connection.quote_column_name(primary_key_col)}"
235
+ key, join_table = if reflection.options.has_key?(:through)
234
236
  [primary_key, has_many_through_join(field)]
235
237
  else
236
- [connection.quote_column_name(field.reflection_keys(definition.reflection_by_name(field.definition.klass, field.relation))[1]),
238
+ [connection.quote_column_name(field.reflection_keys(reflection)[1]),
237
239
  connection.quote_table_name(field.klass.table_name)]
238
240
  end
239
241
 
@@ -269,46 +271,46 @@ module ScopedSearch
269
271
  middle_table_association
270
272
  end
271
273
 
274
+ # Walk the chain of has-many-throughs, collecting all tables we will need to join
275
+ def nested_has_many(many_class, relation)
276
+ acc = [relation]
277
+ while (reflection = definition.reflection_by_name(many_class, relation))
278
+ break if reflection.nil? || reflection.options[:through].nil?
279
+ relation = reflection.options[:through]
280
+ acc.unshift(relation)
281
+ end
282
+ acc.map { |relation| definition.reflection_by_name(many_class, relation) }
283
+ end
284
+
272
285
  def has_many_through_join(field)
273
286
  many_class = field.definition.klass
274
- through = definition.reflection_by_name(many_class, field.relation).options[:through]
275
- through_class = definition.reflection_by_name(many_class, through).klass
276
-
277
287
  connection = many_class.connection
278
-
279
- # table names
280
- endpoint_table_name = field.klass.table_name
281
- many_table_name = many_class.table_name
282
- middle_table_name = through_class.table_name
283
-
284
- # primary and foreign keys + optional conditions for the joins
285
- pk1, fk1 = field.reflection_keys(definition.reflection_by_name(many_class, through))
286
- condition_many_to_middle = if with_polymorphism?(many_class, field.klass, through, through_class)
287
- field.reflection_conditions(definition.reflection_by_name(field.klass, many_table_name))
288
- else
289
- ''
290
- end
291
- condition_middle_to_end = field.reflection_conditions(definition.reflection_by_name(field.klass, middle_table_name))
292
-
293
- # primary and foreign keys + optional condition for the endpoint to middle join
294
- middle_table_association = find_has_many_through_association(field, through) || middle_table_name
295
- pk2, fk2 = field.reflection_keys(definition.reflection_by_name(field.klass, middle_table_association))
296
- condition2 = field.reflection_conditions(definition.reflection_by_name(many_class, field.relation))
297
-
298
- <<-SQL
299
- #{connection.quote_table_name(many_table_name)}
300
- INNER JOIN #{connection.quote_table_name(middle_table_name)}
301
- ON #{connection.quote_table_name(many_table_name)}.#{connection.quote_column_name(pk1)} = #{connection.quote_table_name(middle_table_name)}.#{connection.quote_column_name(fk1)} #{condition_many_to_middle} #{condition_middle_to_end}
302
- INNER JOIN #{connection.quote_table_name(endpoint_table_name)}
303
- ON #{connection.quote_table_name(middle_table_name)}.#{connection.quote_column_name(fk2)} = #{connection.quote_table_name(endpoint_table_name)}.#{connection.quote_column_name(pk2)} #{condition2}
304
- SQL
288
+ sql = connection.quote_table_name(many_class.table_name)
289
+ join_reflections = nested_has_many(many_class, field.relation)
290
+ table_names = [many_class.table_name] + join_reflections.map(&:table_name)
291
+
292
+ join_reflections.zip(table_names.zip(join_reflections.drop(1))).reduce(sql) do |acc, (reflection, (previous_table, next_reflection))|
293
+ klass = reflection.method(:join_keys).arity == 1 ? [reflection.klass] : [] # ActiveRecord <5.2 workaround
294
+ fk1, pk1 = reflection.join_keys(*klass).values # We are joining the tables "in reverse", so the PK and FK are swapped
295
+
296
+ # primary and foreign keys + optional conditions for the joins
297
+ join_condition = if with_polymorphism?(reflection)
298
+ field.reflection_conditions(definition.reflection_by_name(next_reflection.klass, previous_table))
299
+ else
300
+ ''
301
+ end
302
+
303
+ acc + <<-SQL
304
+ INNER JOIN #{connection.quote_table_name(reflection.table_name)}
305
+ ON #{connection.quote_table_name(previous_table)}.#{connection.quote_column_name(pk1)} = #{connection.quote_table_name(reflection.table_name)}.#{connection.quote_column_name(fk1)} #{join_condition}
306
+ SQL
307
+ end
305
308
  end
306
309
 
307
- def with_polymorphism?(many_class, endpoint_class, through, through_class)
308
- reflections = [definition.reflection_by_name(endpoint_class, through), definition.reflection_by_name(many_class, through)].compact
309
- as = reflections.map(&:options).compact.map { |opt| opt[:as] }.compact
310
- return false if as.empty?
311
- definition.reflection_by_name(through_class, as.first).options[:polymorphic]
310
+ def with_polymorphism?(reflection)
311
+ as = reflection.options[:as]
312
+ return unless as
313
+ definition.reflection_by_name(reflection.klass, as).options[:polymorphic]
312
314
  end
313
315
 
314
316
  # This module gets included into the Field class to add SQL generation.
@@ -1,3 +1,3 @@
1
1
  module ScopedSearch
2
- VERSION = "4.1.8"
2
+ VERSION = "4.1.9"
3
3
  end
@@ -0,0 +1,100 @@
1
+ require "spec_helper"
2
+
3
+ # These specs will run on all databases that are defined in the spec/database.yml file.
4
+ # Comment out any databases that you do not have available for testing purposes if needed.
5
+ ScopedSearch::RSpec::Database.test_databases.each do |db|
6
+
7
+ describe ScopedSearch, "using a #{db} database" do
8
+
9
+ before(:all) do
10
+ ScopedSearch::RSpec::Database.establish_named_connection(db)
11
+ end
12
+
13
+ after(:all) do
14
+ ScopedSearch::RSpec::Database.close_connection
15
+ end
16
+
17
+ context 'quering on associations which are behind multiple has-many-through associations' do
18
+
19
+ before(:all) do
20
+ ActiveRecord::Migration.create_table(:sources) { |t| t.string :name }
21
+ ActiveRecord::Migration.create_table(:first_jumps) { |t| t.string :name; t.integer :source_id }
22
+ ActiveRecord::Migration.create_table(:join_jumps) { |t| t.string :name; t.integer :first_jump_id; t.integer :destination_id }
23
+ ActiveRecord::Migration.create_table(:destinations) { |t| t.string :name; }
24
+
25
+ class Source < ActiveRecord::Base
26
+ has_many :first_jumps
27
+ has_many :join_jumps, :through => :first_jumps
28
+ has_many :destinations, :through => :join_jumps
29
+
30
+ scoped_search :relation => :first_jumps, :on => :name, :rename => 'first_jump.name'
31
+ scoped_search :relation => :join_jumps, :on => :name, :rename => 'join_jump.name'
32
+ scoped_search :relation => :destinations, :on => :name, :rename => 'destination.name'
33
+ end
34
+
35
+ class FirstJump < ActiveRecord::Base
36
+ belongs_to :source
37
+ has_many :join_jumps
38
+ has_many :destinations, :through => :join_jumps
39
+ end
40
+
41
+ class JoinJump < ActiveRecord::Base
42
+ has_one :source, :through => :first_jump
43
+ belongs_to :first_jump
44
+ belongs_to :destination
45
+ end
46
+
47
+ class Destination < ActiveRecord::Base
48
+ has_many :join_jumps
49
+ has_many :first_jumps, :through => :join_jumps
50
+ has_many :sources, :through => :first_jumps
51
+ end
52
+
53
+ @destination1 = Destination.create!(:name => 'dest-1')
54
+ @destination2 = Destination.create!(:name => 'dest-2')
55
+ @destination3 = Destination.create!(:name => 'dest-3')
56
+ @source1 = Source.create!(:name => 'src1')
57
+ @first_jump1 = FirstJump.create!(:name => 'jump-1-1', :source => @source1)
58
+ @first_jump2 = FirstJump.create!(:name => 'jump-1-2', :source => @source1)
59
+
60
+ @source2 = Source.create!(:name => 'src2')
61
+ @first_jump_2_1 = FirstJump.create!(:name => 'jump-2-1', :source => @source2)
62
+ @first_jump_2_2 = FirstJump.create!(:name => 'jump-2-2', :source => @source2)
63
+ @first_jump_2_3 = FirstJump.create!(:name => 'jump-2-3', :source => @source2)
64
+ @first_jump_2_4 = FirstJump.create!(:name => 'jump-2-4', :source => @source2)
65
+
66
+ JoinJump.create!(:name => 'join-1-1', :destination => @destination1, :first_jump => @first_jump1)
67
+ JoinJump.create!(:name => 'join-1-2', :destination => @destination2, :first_jump => @first_jump2)
68
+
69
+ JoinJump.create!(:name => 'join-2-1', :destination => @destination1, :first_jump => @first_jump_2_1)
70
+ JoinJump.create!(:name => 'join-2-2', :destination => @destination2, :first_jump => @first_jump_2_2)
71
+ JoinJump.create!(:name => 'join-2-3', :destination => @destination2, :first_jump => @first_jump_2_3)
72
+ JoinJump.create!(:name => 'join-2-4', :destination => @destination3, :first_jump => @first_jump_2_4)
73
+ end
74
+
75
+ after(:all) do
76
+ ScopedSearch::RSpec::Database.drop_model(Source)
77
+ ScopedSearch::RSpec::Database.drop_model(FirstJump)
78
+ ScopedSearch::RSpec::Database.drop_model(JoinJump)
79
+ ScopedSearch::RSpec::Database.drop_model(Destination)
80
+ Object.send :remove_const, :Source
81
+ Object.send :remove_const, :FirstJump
82
+ Object.send :remove_const, :JoinJump
83
+ Object.send :remove_const, :Destination
84
+ end
85
+
86
+ it "allows searching on has many through has many" do
87
+ Source.search_for("join_jump.name = join-1-1").should == [@source1]
88
+ Source.search_for("join_jump.name = join-2-1").should == [@source2]
89
+ Source.search_for("join_jump.name ^ (join-1-1, join-2-1)").order(:id).should == [@source1, @source2]
90
+ end
91
+
92
+ it "allows searching on has many through has one through has many" do
93
+ Source.search_for("destination.name = dest-1").order(:id).should == [@source1, @source2]
94
+ Source.search_for("destination.name = dest-3").order(:id).should == [@source2]
95
+ Source.search_for("destination.name = dest-3 or destination.name = dest-2").order(:id).should == [@source1, @source2]
96
+ Source.search_for("destination.name = dest-3 and destination.name = dest-2").should == [@source2]
97
+ end
98
+ end
99
+ end
100
+ end
@@ -351,9 +351,10 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
351
351
 
352
352
  # Create some tables
353
353
  ActiveRecord::Migration.create_table(:taggables) { |t| t.integer :taggable_id; t.string :taggable_type; t.integer :tag_id }
354
- ActiveRecord::Migration.create_table(:dogs) { |t| t.string :related }
354
+ ActiveRecord::Migration.create_table(:dogs) { |t| t.string :related; t.integer :owner_id }
355
355
  ActiveRecord::Migration.create_table(:cats) { |t| t.string :related }
356
356
  ActiveRecord::Migration.create_table(:tags) { |t| t.string :foo }
357
+ ActiveRecord::Migration.create_table(:owners) { |t| t.string :name }
357
358
 
358
359
  # The related classes
359
360
  class Taggable < ActiveRecord::Base; belongs_to :tag; belongs_to :taggable, :polymorphic => true; end
@@ -369,6 +370,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
369
370
  class Dog < ActiveRecord::Base
370
371
  has_many :taggables, :as => :taggable
371
372
  has_many :tags, :through => :taggables
373
+ belongs_to :owner
372
374
 
373
375
  scoped_search :relation => :tags, :on => :foo
374
376
  end
@@ -378,6 +380,14 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
378
380
  has_many :tags, :through => :taggables
379
381
  end
380
382
 
383
+ class Owner < ActiveRecord::Base
384
+ has_many :dogs
385
+ has_many :taggables, :as => :taggable, :through => :dogs
386
+ has_many :tags, :through => :taggables
387
+
388
+ scoped_search :relation => :tags, :on => :foo
389
+ end
390
+
381
391
  @tag_1 = Tag.create!(:foo => 'foo')
382
392
  @tag_2 = Tag.create!(:foo => 'foo too')
383
393
  @tag_3 = Tag.create!(:foo => 'foo three')
@@ -386,6 +396,8 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
386
396
  @dog_2 = Dog.create(:related => 'baz too!')
387
397
  @cat_1 = Cat.create(:related => 'mitzi')
388
398
 
399
+ @owner_1 = Owner.create(:name => 'Fred', :dogs => [@dog_1])
400
+
389
401
  Taggable.create!(:tag => @tag_1, :taggable => @dog_1, :taggable_type => 'Dog' )
390
402
  Taggable.create!(:tag => @tag_1)
391
403
  Taggable.create!(:tag => @tag_2, :taggable => @dog_1 , :taggable_type => 'Dog')
@@ -400,12 +412,18 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
400
412
  ActiveRecord::Migration.drop_table(:taggables)
401
413
  ActiveRecord::Migration.drop_table(:tags)
402
414
  ActiveRecord::Migration.drop_table(:cats)
415
+ ActiveRecord::Migration.drop_table(:owners)
403
416
  end
404
417
 
405
418
  it "should find the two records that are related to a tag that contains foo record" do
406
419
  Dog.search_for('foo').length.should == 2
407
420
  end
408
421
 
422
+ it "should find the only record that is related to a tag" do
423
+ Owner.search_for('foo').length.should == 1
424
+ Owner.search_for('foo').to_sql.should =~ /taggable_type = 'Dog'/
425
+ end
426
+
409
427
  it "should find one records that is related to both tags" do
410
428
  Dog.search_for('foo=foo AND foo="foo too"').length.should == 1
411
429
  end
@@ -720,5 +738,49 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
720
738
  result.first.username.should == @usermat_1.username
721
739
  end
722
740
  end
741
+
742
+ context "querying on :has_many with primary key override" do
743
+
744
+ before do
745
+ ActiveRecord::Migration.create_table(:books) { |t| t.string :title; t.string :isbn }
746
+ ActiveRecord::Migration.create_table(:comments) { |t| t.string :comment; t.string :isbn }
747
+
748
+ class Book < ActiveRecord::Base
749
+ has_many :comments, foreign_key: 'isbn', primary_key: 'isbn'
750
+
751
+ scoped_search on: [:title]
752
+ scoped_search relation: :comments, on: [:comment]
753
+ end
754
+
755
+ class Comment < ActiveRecord::Base
756
+ belongs_to :book, foreign_key: 'isbn', primary_key: 'isbn'
757
+ end
758
+
759
+ @book1 = Book.create(:title => 'Eloquent Ruby', :isbn => '978-0321584106')
760
+ @book2 = Book.create(:title => 'The Well-Grounded Rubyist', :isbn => '978-1617295218')
761
+ Comment.create(:comment => 'Definitely worth a read', :isbn => @book1.isbn)
762
+ Comment.create(:comment => 'Wait what? I expected a book about gemstones', :isbn => @book1.isbn)
763
+ Comment.create(:comment => 'Cool book about ruby', :isbn => @book2.isbn)
764
+ end
765
+
766
+ after do
767
+ ActiveRecord::Migration.drop_table :comments
768
+ ActiveRecord::Migration.drop_table :books
769
+ end
770
+
771
+ it "correctly joins the tables" do
772
+ query = Book.search_for("test").to_sql
773
+ # On PostgreSQL and SQLite we use double quotes, on MySQL we use backticks
774
+ query.should =~ /LEFT OUTER JOIN ["`]comments["`] ON ["`]comments["`]\.["`]isbn["`] = ["`]books["`]\.["`]isbn["`]/
775
+ query.should =~ /["`]books["`]\.["`]isbn["`] IN \(SELECT ["`]isbn["`]/
776
+ end
777
+
778
+ it "finds the right results" do
779
+ Book.search_for('python').should == []
780
+ Book.search_for('comment ~ Wait').should == [@book1]
781
+ Book.search_for('comment ~ book').pluck(:id).sort.uniq.should == [@book1, @book2].map(&:id).sort
782
+ Book.search_for('comment ~ Cool').should == [@book2]
783
+ end
784
+ end
723
785
  end
724
786
  end
@@ -48,7 +48,7 @@ module ScopedSearch::RSpec::Database
48
48
  ActiveRecord::Migration.create_table(table_name) do |t|
49
49
  fields.each do |name, field_type|
50
50
  options = (field_type == :decimal) ? { :scale => 2, :precision => 10 } : {}
51
- t.send(field_type.to_s.gsub(/^unindexed_/, '').to_sym, name, options)
51
+ t.send(field_type.to_s.gsub(/^unindexed_/, '').to_sym, name, **options)
52
52
  end
53
53
  end
54
54
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scoped_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.8
4
+ version: 4.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amos Benari
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-04-15 00:00:00.000000000 Z
13
+ date: 2020-08-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -116,6 +116,7 @@ files:
116
116
  - spec/integration/auto_complete_spec.rb
117
117
  - spec/integration/ext_method_spec.rb
118
118
  - spec/integration/key_value_querying_spec.rb
119
+ - spec/integration/nested_has_many_through_querying_spec.rb
119
120
  - spec/integration/ordinal_querying_spec.rb
120
121
  - spec/integration/profile_querying_spec.rb
121
122
  - spec/integration/rails_helper_spec.rb
@@ -173,6 +174,7 @@ test_files:
173
174
  - spec/integration/auto_complete_spec.rb
174
175
  - spec/integration/ext_method_spec.rb
175
176
  - spec/integration/key_value_querying_spec.rb
177
+ - spec/integration/nested_has_many_through_querying_spec.rb
176
178
  - spec/integration/ordinal_querying_spec.rb
177
179
  - spec/integration/profile_querying_spec.rb
178
180
  - spec/integration/rails_helper_spec.rb