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 +4 -4
- data/.travis.yml +11 -0
- data/lib/scoped_search/query_builder.rb +40 -38
- data/lib/scoped_search/version.rb +1 -1
- data/spec/integration/nested_has_many_through_querying_spec.rb +100 -0
- data/spec/integration/relation_querying_spec.rb +63 -1
- data/spec/lib/database.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c1453ebb8fe2391f71ea35171f2115ed89bb7920a1a042d86a4393fe218888c
|
4
|
+
data.tar.gz: 61d2d0e948a952f7e955240747dae7e529103c5871ad3160cb2213574f0f155a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04e61c564c6117d60b6f82b970df5c6c7a68362d828d89d1cf5d2bf7f6b7c9ea777cf641dbb2a21be895905305c0c7305105c9840d391aa702cd69eab706fd4d
|
7
|
+
data.tar.gz: 842c31b1714558d8bd4578d47b87bca4fa4298617a16168b66a77e6ff06ee1f43406b30be3557a9c50222ba5effaa9c5eec27832839e75bb11faf085f95bb7bc
|
data/.travis.yml
CHANGED
@@ -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
|
-
|
233
|
-
|
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(
|
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
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
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?(
|
308
|
-
|
309
|
-
|
310
|
-
|
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.
|
@@ -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
|
data/spec/lib/database.rb
CHANGED
@@ -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.
|
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-
|
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
|