scoped_search 4.1.8 → 4.1.9

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
  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