scoped_search 4.1.6 → 4.1.10

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
- SHA1:
3
- metadata.gz: c94c65fc191d2afdddd82f3177121d2ade7e6238
4
- data.tar.gz: 36dd864d06aafd40bd3015184a17222a92d5627f
2
+ SHA256:
3
+ metadata.gz: f0f6f98982cba23ec69f2b1adaf0c44832abb19adad7a347d4d78c4b0fbb63a8
4
+ data.tar.gz: 114ea022f87208dc89b92247687d64c2a0146119afa33f2106b69507271ba210
5
5
  SHA512:
6
- metadata.gz: 2a41edacae0b5140f56dc60bc921c8afb3349132b9ad3453313fc407abbda58b251c08d1da8d56706f9307520882e9e8006f18147833c91b7e3d0995962d843e
7
- data.tar.gz: 31932dcd317193df257a4d0fd0c46fea28866e7670a0cfbd1d840d4477ada8381906d5f83b9289b322de3bbc44048ec249d9bf6af67f605bfc76589be9ccc7e3
6
+ metadata.gz: 6a87832ba5990fd45b7116d374a9c2505c7f9fcf62b4364859986ae497b2aa6d3a0890b222b1ad6c07ea999d48b25fbc42303a63cdc8ce3f4ea3a23e1f049eeb
7
+ data.tar.gz: a5cd950d77712004cb649261f08a8e8e04169622b346de7c3b15b422dbae7343bf8211febbdb988cf390a43c5f4794c93e820b9bc440d4cc9130d7f3146464f9
@@ -0,0 +1,76 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on: [pull_request]
11
+
12
+ env:
13
+ TESTOPTS: --verbose
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ ruby_version:
22
+ - 2.7.1
23
+ - 3.0.0
24
+ - ruby-head
25
+ - jruby-head
26
+ gemfile:
27
+ - Gemfile.activerecord52
28
+ - Gemfile.activerecord52_with_activesupport52
29
+ - Gemfile.activerecord60
30
+ - Gemfile.activerecord60_with_activesupport60
31
+ - Gemfile.activerecord61
32
+ - Gemfile.activerecord61_with_activesupport61
33
+
34
+ exclude:
35
+ - ruby_version: 2.7.1
36
+ gemfile: Gemfile.activerecord52
37
+ - ruby_version: 2.7.1
38
+ gemfile: Gemfile.activerecord52_with_activesupport52
39
+ - ruby_version: 3.0.0
40
+ gemfile: Gemfile.activerecord52
41
+ - ruby_version: 3.0.0
42
+ gemfile: Gemfile.activerecord52_with_activesupport52
43
+ - ruby_version: ruby-head
44
+ gemfile: Gemfile.activerecord52
45
+ - ruby_version: ruby-head
46
+ gemfile: Gemfile.activerecord52_with_activesupport52
47
+ services:
48
+ postgres:
49
+ image: postgres:12.1
50
+ ports: ['5432:5432']
51
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
52
+ env:
53
+ POSTGRES_DB: scoped_search_test
54
+ mariadb:
55
+ image: mariadb:10
56
+ ports: ['3306:3306']
57
+ env:
58
+ MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
59
+ MYSQL_DATABASE: scoped_search_test
60
+
61
+ env:
62
+ BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
63
+
64
+ steps:
65
+ - uses: actions/checkout@v2
66
+ - name: Set up Ruby
67
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
68
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
69
+ uses: ruby/setup-ruby@v1
70
+ with:
71
+ ruby-version: ${{ matrix.ruby_version }}
72
+ bundler-cache: true
73
+ # - name: Install dependencies
74
+ # run: bundle install
75
+ - name: Run tests
76
+ run: bundle exec rake
data/.travis.yml CHANGED
@@ -1,6 +1,9 @@
1
1
  language: ruby
2
2
  cache: bundler
3
3
  sudo: false
4
+ services:
5
+ - postgresql
6
+ - mysql
4
7
 
5
8
  install:
6
9
  - bundle install
@@ -14,8 +17,12 @@ rvm:
14
17
  - "2.0"
15
18
  - "2.1"
16
19
  - "2.2.2"
17
- - "2.3.1"
20
+ - "2.3.7"
18
21
  - "2.4.0"
22
+ - "2.5.1"
23
+ - "2.6.0"
24
+ - "2.7.1"
25
+ - "3.0.2"
19
26
  - ruby-head
20
27
  - jruby-19mode
21
28
  - jruby-head
@@ -24,6 +31,12 @@ gemfile:
24
31
  - Gemfile.activerecord42
25
32
  - Gemfile.activerecord50
26
33
  - Gemfile.activerecord51
34
+ - Gemfile.activerecord52
35
+ - Gemfile.activerecord52_with_activesupport52
36
+ - Gemfile.activerecord60
37
+ - Gemfile.activerecord60_with_activesupport60
38
+ - Gemfile.activerecord61
39
+ - Gemfile.activerecord61_with_activesupport61
27
40
 
28
41
  matrix:
29
42
  allow_failures:
@@ -39,3 +52,71 @@ matrix:
39
52
  gemfile: Gemfile.activerecord51
40
53
  - rvm: "2.1"
41
54
  gemfile: Gemfile.activerecord51
55
+ - rvm: "2.0"
56
+ gemfile: Gemfile.activerecord52
57
+ - rvm: "2.1"
58
+ gemfile: Gemfile.activerecord52
59
+ - rvm: "2.0"
60
+ gemfile: Gemfile.activerecord52_with_activesupport52
61
+ - rvm: "2.1"
62
+ gemfile: Gemfile.activerecord52_with_activesupport52
63
+ - rvm: "2.0"
64
+ gemfile: Gemfile.activerecord60
65
+ - rvm: "2.1"
66
+ gemfile: Gemfile.activerecord60
67
+ - rvm: "2.2.2"
68
+ gemfile: Gemfile.activerecord60
69
+ - rvm: "2.3.7"
70
+ gemfile: Gemfile.activerecord60
71
+ - rvm: "2.4.0"
72
+ gemfile: Gemfile.activerecord60
73
+ - rvm: "2.0"
74
+ gemfile: Gemfile.activerecord60_with_activesupport60
75
+ - rvm: "2.1"
76
+ gemfile: Gemfile.activerecord60_with_activesupport60
77
+ - rvm: "2.2.2"
78
+ gemfile: Gemfile.activerecord60_with_activesupport60
79
+ - rvm: "2.3.7"
80
+ gemfile: Gemfile.activerecord60_with_activesupport60
81
+ - rvm: "2.4.0"
82
+ gemfile: Gemfile.activerecord60_with_activesupport60
83
+ - rvm: "2.0"
84
+ gemfile: Gemfile.activerecord61
85
+ - rvm: "2.1"
86
+ gemfile: Gemfile.activerecord61
87
+ - rvm: "2.2.2"
88
+ gemfile: Gemfile.activerecord61
89
+ - rvm: "2.3.7"
90
+ gemfile: Gemfile.activerecord61
91
+ - rvm: "2.4.0"
92
+ gemfile: Gemfile.activerecord61
93
+ - rvm: "2.0"
94
+ gemfile: Gemfile.activerecord61_with_activesupport61
95
+ - rvm: "2.1"
96
+ gemfile: Gemfile.activerecord61_with_activesupport61
97
+ - rvm: "2.2.2"
98
+ gemfile: Gemfile.activerecord61_with_activesupport61
99
+ - rvm: "2.3.7"
100
+ gemfile: Gemfile.activerecord61_with_activesupport61
101
+ - rvm: "2.4.0"
102
+ gemfile: Gemfile.activerecord61_with_activesupport61
103
+ - rvm: "2.7.1"
104
+ gemfile: Gemfile.activerecord42
105
+ - rvm: "2.7.1"
106
+ gemfile: Gemfile.activerecord50
107
+ - rvm: "2.7.1"
108
+ gemfile: Gemfile.activerecord51
109
+ - rvm: "2.7.1"
110
+ gemfile: Gemfile.activerecord52
111
+ - rvm: "2.7.1"
112
+ gemfile: Gemfile.activerecord52_with_activesupport52
113
+ - rvm: "3.0.2"
114
+ gemfile: Gemfile.activerecord42
115
+ - rvm: "3.0.2"
116
+ gemfile: Gemfile.activerecord50
117
+ - rvm: "3.0.2"
118
+ gemfile: Gemfile.activerecord51
119
+ - rvm: "3.0.2"
120
+ gemfile: Gemfile.activerecord52
121
+ - rvm: "3.0.2"
122
+ gemfile: Gemfile.activerecord52_with_activesupport52
data/CHANGELOG.rdoc CHANGED
@@ -6,7 +6,27 @@ Please add an entry to the "Unreleased changes" section in your pull requests.
6
6
 
7
7
  === Unreleased changes
8
8
 
9
- - Nothing yet
9
+ === Version 4.1.10
10
+
11
+ - Fix querying through associations in Rails 6.1 (undefined method join_keys) (#201)
12
+ - Fix query generation when going through STI associations (#211)
13
+ - Properly support Ruby 3
14
+ - Add support for postgres citext type
15
+
16
+ === Version 4.1.9
17
+ - Fix querying on nested has-many associations (#196)
18
+ - Abide "has_many ..., primary_key" overrides in relation searches
19
+
20
+ === Version 4.1.8
21
+
22
+ - Fix querying in associations by set, datetime or IN searches
23
+ - Add support for ActiveRecord 6.0 and Rails 6.1
24
+
25
+ === Version 4.1.7
26
+
27
+ - When Active Support is available, we parse time respecting current time zone
28
+ - Allow dots in rename definitions
29
+ - Accept extra value_filter in `complete_for`
10
30
 
11
31
  === Version 4.1.6
12
32
 
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'actionview', '~> 5.2.0'
5
+ gem 'activerecord', '~> 5.2.0'
6
+
7
+ platforms :jruby do
8
+ gem 'activerecord-jdbcsqlite3-adapter'
9
+ gem 'activerecord-jdbcmysql-adapter'
10
+ gem 'activerecord-jdbcpostgresql-adapter'
11
+ end
12
+
13
+ platforms :ruby do
14
+ gem 'sqlite3', '~> 1.3.6'
15
+ gem 'mysql2', '>= 0.3.18', '< 0.5'
16
+ gem 'pg', '~> 0.18'
17
+ end
@@ -0,0 +1,18 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'actionview', '~> 5.2.0'
5
+ gem 'activerecord', '~> 5.2.0'
6
+ gem 'activesupport', '~> 5.2.0'
7
+
8
+ platforms :jruby do
9
+ gem 'activerecord-jdbcsqlite3-adapter'
10
+ gem 'activerecord-jdbcmysql-adapter'
11
+ gem 'activerecord-jdbcpostgresql-adapter'
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem 'sqlite3', '~> 1.3.6'
16
+ gem 'mysql2', '>= 0.3.18', '< 0.5'
17
+ gem 'pg', '~> 0.18'
18
+ end
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'actionview', '~> 6.0.0'
5
+ gem 'activerecord', '~> 6.0.0'
6
+
7
+ platforms :jruby do
8
+ gem 'activerecord-jdbcsqlite3-adapter'
9
+ gem 'activerecord-jdbcmysql-adapter'
10
+ gem 'activerecord-jdbcpostgresql-adapter'
11
+ end
12
+
13
+ platforms :ruby do
14
+ gem 'sqlite3', '~> 1.4'
15
+ gem 'mysql2', '> 0.5'
16
+ gem 'pg', '>= 0.18', '< 2.0'
17
+ end
@@ -0,0 +1,18 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'actionview', '~> 6.0.0'
5
+ gem 'activerecord', '~> 6.0.0'
6
+ gem 'activesupport', '~> 6.0.0'
7
+
8
+ platforms :jruby do
9
+ gem 'activerecord-jdbcsqlite3-adapter'
10
+ gem 'activerecord-jdbcmysql-adapter'
11
+ gem 'activerecord-jdbcpostgresql-adapter'
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem 'sqlite3', '~> 1.4'
16
+ gem 'mysql2', '> 0.5'
17
+ gem 'pg', '>= 0.18', '< 2.0'
18
+ end
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'actionview', '~> 6.1.0'
5
+ gem 'activerecord', '~> 6.1.0'
6
+
7
+ platforms :jruby do
8
+ gem 'activerecord-jdbcsqlite3-adapter'
9
+ gem 'activerecord-jdbcmysql-adapter'
10
+ gem 'activerecord-jdbcpostgresql-adapter'
11
+ end
12
+
13
+ platforms :ruby do
14
+ gem 'sqlite3', '~> 1.4'
15
+ gem 'mysql2', '> 0.5'
16
+ gem 'pg', '>= 0.18', '< 2.0'
17
+ end
@@ -0,0 +1,18 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'actionview', '~> 6.1.0'
5
+ gem 'activerecord', '~> 6.1.0'
6
+ gem 'activesupport', '~> 6.1.0'
7
+
8
+ platforms :jruby do
9
+ gem 'activerecord-jdbcsqlite3-adapter'
10
+ gem 'activerecord-jdbcmysql-adapter'
11
+ gem 'activerecord-jdbcpostgresql-adapter'
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem 'sqlite3', '~> 1.4'
16
+ gem 'mysql2', '> 0.5'
17
+ gem 'pg', '>= 0.18', '< 2.0'
18
+ end
@@ -208,6 +208,7 @@ module ScopedSearch
208
208
  def complete_value_from_db(field, special_values, val)
209
209
  count = 20 - special_values.count
210
210
  completer_scope(field)
211
+ .where(@options[:value_filter])
211
212
  .where(value_conditions(field.quoted_field, val))
212
213
  .select(field.quoted_field)
213
214
  .limit(count)
@@ -219,8 +220,8 @@ module ScopedSearch
219
220
 
220
221
  def completer_scope(field)
221
222
  klass = field.klass
222
- scope = klass.respond_to?(:completer_scope) ? klass.completer_scope(@options) : klass
223
- scope.respond_to?(:reorder) ? scope.reorder(field.quoted_field) : scope.scoped(:order => field.quoted_field)
223
+ scope = klass.respond_to?(:completer_scope) ? klass.completer_scope(@options) : klass
224
+ scope.respond_to?(:reorder) ? scope.reorder(Arel.sql(field.quoted_field)) : scope.scoped(:order => field.quoted_field)
224
225
  end
225
226
 
226
227
  # set value completer
@@ -157,7 +157,7 @@ module ScopedSearch
157
157
 
158
158
  # Returns true if this is a textual column.
159
159
  def textual?
160
- [:string, :text].include?(type)
160
+ [:string, :text, :citext].include?(type)
161
161
  end
162
162
 
163
163
  def uuid?
@@ -172,7 +172,7 @@ module ScopedSearch
172
172
  # Returns the default search operator for this field.
173
173
  def default_operator
174
174
  @default_operator ||= case type
175
- when :string, :text then :like
175
+ when :string, :text, :citext then :like
176
176
  else :eq
177
177
  end
178
178
  end
@@ -238,6 +238,9 @@ module ScopedSearch
238
238
  if field.nil?
239
239
  dotted = name.to_s.split('.')[0]
240
240
  field = fields[dotted.to_sym] unless dotted.blank?
241
+ if field && field.key_relation.nil?
242
+ return nil
243
+ end
241
244
  end
242
245
  field
243
246
  end
@@ -263,7 +266,7 @@ module ScopedSearch
263
266
  def default_fields_for(value, operator = nil)
264
267
 
265
268
  column_types = [:virtual]
266
- column_types += [:string, :text] if [nil, :like, :unlike, :ne, :eq].include?(operator)
269
+ column_types += [:string, :text, :citext] if [nil, :like, :unlike, :ne, :eq].include?(operator)
267
270
  column_types += [:double, :float, :decimal] if value =~ NUMERICAL_REGXP
268
271
  column_types += [:integer] if value =~ INTEGER_REGXP
269
272
  column_types += [:uuid] if value =~ UUID_REGXP
@@ -275,6 +278,8 @@ module ScopedSearch
275
278
  # Try to parse a string as a datetime.
276
279
  # Supported formats are Today, Yesterday, Sunday, '1 day ago', '2 hours ago', '3 months ago', '4 weeks from now', 'Jan 23, 2004'
277
280
  # And many more formats that are documented in Ruby DateTime API Doc.
281
+ # In case Time responds to #zone, we know this is Rails environment and we can use Time.zone.parse. The benefit is that the
282
+ # current timezone is respected and does not have to be specified explicitly. That way even relative dates work as expected.
278
283
  def parse_temporal(value)
279
284
  return Date.current if value =~ /\btoday\b/i
280
285
  return 1.day.ago.to_date if value =~ /\byesterday\b/i
@@ -283,7 +288,12 @@ module ScopedSearch
283
288
  return (eval($1.strip.gsub(/\s+/,'.').downcase)).to_date if value =~ /\A\s*(\d+\s+\b(?:days?|weeks?|months?|years?)\b\s+\bago)\b\s*\z/i
284
289
  return (eval($1.strip.gsub(/from\s+now/i,'from_now').gsub(/\s+/,'.').downcase)).to_datetime if value =~ /\A\s*(\d+\s+\b(?:hours?|minutes?)\b\s+\bfrom\s+now)\b\s*\z/i
285
290
  return (eval($1.strip.gsub(/from\s+now/i,'from_now').gsub(/\s+/,'.').downcase)).to_date if value =~ /\A\s*(\d+\s+\b(?:days?|weeks?|months?|years?)\b\s+\bfrom\s+now)\b\s*\z/i
286
- DateTime.parse(value, true) rescue nil
291
+ if Time.respond_to?(:zone) && !Time.zone.nil?
292
+ parsed = Time.zone.parse(value) rescue nil
293
+ parsed && parsed.to_datetime
294
+ else
295
+ DateTime.parse(value, true) rescue nil
296
+ end
287
297
  end
288
298
 
289
299
  # Returns a list of fields that should be searched on by default.
@@ -295,8 +305,8 @@ module ScopedSearch
295
305
  end
296
306
 
297
307
  # Defines a new search field for this search definition.
298
- def define(*args)
299
- Field.new(self, *args)
308
+ def define(*args, **kwargs)
309
+ Field.new(self, *args, **kwargs)
300
310
  end
301
311
 
302
312
  # Returns a reflection for a given klass and name
@@ -316,11 +326,11 @@ module ScopedSearch
316
326
 
317
327
  search_scope = klass.all
318
328
  find_options = ScopedSearch::QueryBuilder.build_query(definition, query || '', options)
319
- search_scope = search_scope.where(find_options[:conditions]) if find_options[:conditions]
320
- search_scope = search_scope.includes(find_options[:include]) if find_options[:include]
321
- search_scope = search_scope.joins(find_options[:joins]) if find_options[:joins]
322
- search_scope = search_scope.reorder(find_options[:order]) if find_options[:order]
323
- search_scope = search_scope.references(find_options[:include]) if find_options[:include]
329
+ search_scope = search_scope.where(find_options[:conditions]) if find_options[:conditions]
330
+ search_scope = search_scope.includes(find_options[:include]) if find_options[:include]
331
+ search_scope = search_scope.joins(find_options[:joins]) if find_options[:joins]
332
+ search_scope = search_scope.reorder(Arel.sql(find_options[:order])) if find_options[:order]
333
+ search_scope = search_scope.references(find_options[:include]) if find_options[:include]
324
334
 
325
335
  search_scope
326
336
  end
@@ -38,7 +38,9 @@ module ScopedSearch
38
38
 
39
39
  # Initializes the instance by setting the relevant parameters
40
40
  def initialize(definition, ast, profile)
41
- @definition, @ast, @definition.profile = definition, ast, profile
41
+ @definition = definition
42
+ @ast = ast
43
+ @definition.profile = profile
42
44
  end
43
45
 
44
46
  # Actually builds the find parameters hash that should be used in the search_for
@@ -137,7 +139,7 @@ module ScopedSearch
137
139
 
138
140
  # Parse the value as a date/time and ignore invalid timestamps
139
141
  timestamp = definition.parse_temporal(value)
140
- return nil unless timestamp
142
+ return [] unless timestamp
141
143
 
142
144
  timestamp = timestamp.to_date if field.date?
143
145
  # Check for the case that a date-only value is given as search keyword,
@@ -149,11 +151,9 @@ module ScopedSearch
149
151
  if [:eq, :ne].include?(operator)
150
152
  # Instead of looking for an exact (non-)match, look for dates that
151
153
  # fall inside/outside the range of timestamps of that day.
152
- yield(:parameter, timestamp)
153
- yield(:parameter, timestamp + span)
154
154
  negate = (operator == :ne) ? 'NOT ' : ''
155
155
  field_sql = field.to_sql(operator, &block)
156
- return "#{negate}(#{field_sql} >= ? AND #{field_sql} < ?)"
156
+ return ["#{negate}(#{field_sql} >= ? AND #{field_sql} < ?)", timestamp, timestamp + span]
157
157
 
158
158
  elsif operator == :gt
159
159
  # Make sure timestamps on the given date are not included in the results
@@ -169,9 +169,8 @@ module ScopedSearch
169
169
  end
170
170
  end
171
171
 
172
- # Yield the timestamp and return the SQL test
173
- yield(:parameter, timestamp)
174
- "#{field.to_sql(operator, &block)} #{sql_operator(operator, field)} ?"
172
+ # return the SQL test
173
+ ["#{field.to_sql(operator, &block)} #{sql_operator(operator, field)} ?", timestamp]
175
174
  end
176
175
 
177
176
  # Validate the key name is in the set and translate the value to the set value.
@@ -205,8 +204,7 @@ module ScopedSearch
205
204
  set_value = false
206
205
  end
207
206
  end
208
- yield(:parameter, set_value)
209
- return "#{negate}(#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?)"
207
+ ["#{negate}(#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?)", set_value]
210
208
  end
211
209
 
212
210
  # Generates a simple SQL test expression, for a field and value using an operator.
@@ -222,41 +220,47 @@ module ScopedSearch
222
220
 
223
221
  yield(:keyparameter, lhs.sub(/^.*\./,'')) if field.key_field
224
222
 
225
- if [:like, :unlike].include?(operator)
226
- yield(:parameter, (value !~ /^\%|\*/ && value !~ /\%|\*$/) ? "%#{value}%" : value.tr_s('%*', '%'))
227
- return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
223
+ condition, *values = if field.temporal?
224
+ datetime_test(field, operator, value, &block)
225
+ elsif field.set?
226
+ set_test(field, operator, value, &block)
227
+ else
228
+ ["#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} #{value_placeholders(operator, value)}", value]
229
+ end
230
+ values.each { |value| preprocess_parameters(field, operator, value, &block) }
228
231
 
229
- elsif [:in, :notin].include?(operator)
230
- value.split(',').collect { |v| yield(:parameter, map_value(field, field.set? ? translate_value(field, v) : v.strip)) }
231
- value = value.split(',').collect { "?" }.join(",")
232
- return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} (#{value})"
233
-
234
- elsif field.temporal?
235
- return datetime_test(field, operator, value, &block)
232
+ if field.relation && definition.reflection_by_name(field.definition.klass, field.relation).macro == :has_many
233
+ connection = field.definition.klass.connection
234
+ reflection = definition.reflection_by_name(field.definition.klass, field.relation)
235
+ primary_key_col = reflection.options[:primary_key] || field.definition.klass.primary_key
236
+ primary_key = "#{connection.quote_table_name(field.definition.klass.table_name)}.#{connection.quote_column_name(primary_key_col)}"
237
+ key, join_table = if reflection.options.has_key?(:through)
238
+ [primary_key, has_many_through_join(field)]
239
+ else
240
+ [connection.quote_column_name(field.reflection_keys(reflection)[1]),
241
+ connection.quote_table_name(field.klass.table_name)]
242
+ end
243
+
244
+ condition = "#{primary_key} IN (SELECT #{key} FROM #{join_table} WHERE #{condition} )"
245
+ end
246
+ condition
247
+ end
236
248
 
237
- elsif field.set?
238
- return set_test(field, operator, value, &block)
249
+ def preprocess_parameters(field, operator, value, &block)
250
+ values = if [:in, :notin].include?(operator)
251
+ value.split(',').map { |v| map_value(field, field.set? ? translate_value(field, v) : v.strip) }
252
+ elsif [:like, :unlike].include?(operator)
253
+ [(value !~ /^\%|\*/ && value !~ /\%|\*$/) ? "%#{value}%" : value.tr_s('%*', '%')]
254
+ else
255
+ [map_value(field, field.offset ? value.to_i : value)]
256
+ end
257
+ values.each { |value| yield(:parameter, value) }
258
+ end
239
259
 
240
- elsif field.relation && definition.reflection_by_name(field.definition.klass, field.relation).macro == :has_many
241
- value = value.to_i if field.offset
242
- value = map_value(field, value)
243
- yield(:parameter, value)
244
- connection = field.definition.klass.connection
245
- primary_key = "#{connection.quote_table_name(field.definition.klass.table_name)}.#{connection.quote_column_name(field.definition.klass.primary_key)}"
246
- if definition.reflection_by_name(field.definition.klass, field.relation).options.has_key?(:through)
247
- join = has_many_through_join(field)
248
- return "#{primary_key} IN (SELECT #{primary_key} FROM #{join} WHERE #{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ? )"
249
- else
250
- foreign_key = connection.quote_column_name(field.reflection_keys(definition.reflection_by_name(field.definition.klass, field.relation))[1])
251
- return "#{primary_key} IN (SELECT #{foreign_key} FROM #{connection.quote_table_name(field.klass.table_name)} WHERE #{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ? )"
252
- end
260
+ def value_placeholders(operator, value)
261
+ return '?' unless [:in, :notin].include?(operator)
253
262
 
254
- else
255
- value = value.to_i if field.offset
256
- value = map_value(field, value)
257
- yield(:parameter, value)
258
- return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
259
- end
263
+ '(' + value.split(',').map { '?' }.join(',') + ')'
260
264
  end
261
265
 
262
266
  def find_has_many_through_association(field, through)
@@ -269,46 +273,51 @@ module ScopedSearch
269
273
  middle_table_association
270
274
  end
271
275
 
276
+ # Walk the chain of has-many-throughs, collecting all tables we will need to join
277
+ def nested_has_many(many_class, relation)
278
+ acc = [relation]
279
+ while (reflection = definition.reflection_by_name(many_class, relation))
280
+ break if reflection.nil? || reflection.options[:through].nil?
281
+ relation = reflection.options[:through]
282
+ acc.unshift(relation)
283
+ end
284
+ acc.map { |relation| definition.reflection_by_name(many_class, relation) }
285
+ end
286
+
272
287
  def has_many_through_join(field)
273
288
  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
289
  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
290
+ sql = connection.quote_table_name(many_class.table_name)
291
+ join_reflections = nested_has_many(many_class, field.relation)
292
+ table_names = [[many_class.table_name, many_class.sti_name.tableize]] + join_reflections.map(&:table_name)
293
+
294
+ join_reflections.zip(table_names.zip(join_reflections.drop(1))).reduce(sql) do |acc, (reflection, (previous_table, next_reflection))|
295
+ fk1, pk1 = if reflection.respond_to?(:join_keys)
296
+ klass = reflection.method(:join_keys).arity == 1 ? [reflection.klass] : [] # ActiveRecord <5.2 workaround
297
+ reflection.join_keys(*klass).values # We are joining the tables "in reverse", so the PK and FK are swapped
298
+ else
299
+ [reflection.join_primary_key, reflection.join_foreign_key] #ActiveRecord 6.1
300
+ end
301
+
302
+ previous_table, sti_name = previous_table
303
+ # primary and foreign keys + optional conditions for the joins
304
+ join_condition = if with_polymorphism?(reflection)
305
+ field.reflection_conditions(definition.reflection_by_name(next_reflection.klass, sti_name || previous_table))
306
+ else
307
+ ''
308
+ end
309
+
310
+ acc + <<-SQL
311
+ INNER JOIN #{connection.quote_table_name(reflection.table_name)}
312
+ 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}
313
+ SQL
314
+ end
305
315
  end
306
316
 
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]
317
+ def with_polymorphism?(reflection)
318
+ as = reflection.options[:as]
319
+ return unless as
320
+ definition.reflection_by_name(reflection.klass, as).options[:polymorphic]
312
321
  end
313
322
 
314
323
  # This module gets included into the Field class to add SQL generation.
@@ -1,3 +1,3 @@
1
1
  module ScopedSearch
2
- VERSION = "4.1.6"
2
+ VERSION = "4.1.10"
3
3
  end
data/lib/scoped_search.rb CHANGED
@@ -34,9 +34,9 @@ module ScopedSearch
34
34
 
35
35
  definitions.each do |definition|
36
36
  if definition[:on].kind_of?(Array)
37
- definition[:on].each { |field| self.scoped_search_definition.define(definition.merge(:on => field)) }
37
+ definition[:on].each { |field| self.scoped_search_definition.define(**definition.merge(:on => field)) }
38
38
  else
39
- self.scoped_search_definition.define(definition)
39
+ self.scoped_search_definition.define(**definition)
40
40
  end
41
41
  end
42
42
  return self.scoped_search_definition
@@ -4,7 +4,7 @@ sqlite:
4
4
 
5
5
  mysql:
6
6
  adapter: jdbcmysql
7
- host: localhost
7
+ host: 127.0.0.1
8
8
  username: root
9
9
  database: scoped_search_test
10
10
 
@@ -4,7 +4,7 @@ sqlite:
4
4
 
5
5
  mysql:
6
6
  adapter: mysql2
7
- host: localhost
7
+ host: 127.0.0.1
8
8
  port: 3306
9
9
  username: root
10
10
  database: scoped_search_test
@@ -17,6 +17,13 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
17
17
  t.string :other_c
18
18
  end
19
19
 
20
+ ActiveRecord::Migration.create_table(:quxes, :force => true) do |t|
21
+ t.string :related
22
+ t.string :other_a
23
+ t.string :other_b
24
+ t.string :other_c
25
+ end
26
+
20
27
  ActiveRecord::Migration.create_table(:foos, :force => true) do |t|
21
28
  t.string :string
22
29
  t.string :another
@@ -25,6 +32,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
25
32
  t.integer :int
26
33
  t.date :date
27
34
  t.integer :unindexed
35
+ t.integer :qux_id
28
36
 
29
37
  if on_postgresql?
30
38
  t.uuid :uuid
@@ -37,8 +45,13 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
37
45
  belongs_to :foo
38
46
  end
39
47
 
48
+ class ::Qux < ActiveRecord::Base
49
+ has_many :foos
50
+ end
51
+
40
52
  class ::Foo < ActiveRecord::Base
41
53
  has_many :bars
54
+ belongs_to :qux
42
55
  default_scope { order(:string) }
43
56
 
44
57
  scoped_search :on => [:string, :date, :uuid]
@@ -55,7 +68,10 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
55
68
  class ::Infoo < ::Foo
56
69
  end
57
70
 
58
- @foo_1 = Foo.create!(:string => 'foo', :another => 'temp 1', :explicit => 'baz', :int => 9 , :date => 'February 8, 2011' , :unindexed => 10)
71
+ @qux_1 = Qux.create!()
72
+ @qux_2 = Qux.create!()
73
+ @foo_1 = Foo.create!(:string => 'foo', :another => 'temp 1', :explicit => 'baz', :int => 9 , :date => 'February 8, 2011' , :unindexed => 10, :qux => @qux_1)
74
+ @foo_2 = Foo.create!(:string => 'foo', :another => 'temp 2', :explicit => 'baz', :int => 10 , :date => 'February 8, 2011' , :unindexed => 10, :qux => @qux_2)
59
75
  Foo.create!(:string => 'bar', :another => 'temp "2"', :explicit => 'baz', :int => 22 , :date => 'February 10, 2011', :unindexed => 10)
60
76
  Foo.create!(:string => 'baz', :another => nil, :explicit => nil , :int => nil, :date => nil , :unindexed => nil)
61
77
  20.times { Foo.create!(:explicit => "aaa") }
@@ -242,5 +258,14 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
242
258
  Foo.complete_for('int > 2').first.should match(/22/)
243
259
  end
244
260
  end
261
+
262
+ context 'autocompleting with value_filter' do
263
+ it 'should return filtered values' do
264
+ Foo.complete_for('int =', value_filter: { qux_id: @qux_1.id }).should == ['int = 9']
265
+ end
266
+ it 'should return filtered for invalid value' do
267
+ Foo.complete_for('int =', value_filter: { qux_id: 99 }).should == []
268
+ end
269
+ end
245
270
  end
246
271
  end
@@ -33,6 +33,7 @@ require "spec_helper"
33
33
  has_many :keys, :through => :facts
34
34
 
35
35
  scoped_search :relation => :facts, :on => :value, :rename => :facts, :in_key => :keys, :on_key => :name, :complete_value => true
36
+ scoped_search :relation => :facts, :on => :value, :rename => 'myfacts.value', :aliases => ['prefixed', 'prefixed.value']
36
37
  end
37
38
  class ::MyItem < ::Item
38
39
  end
@@ -102,6 +103,20 @@ require "spec_helper"
102
103
  it "should find all bars with a fact name color and fact value gold of descendant class" do
103
104
  MyItem.search_for('facts.color = gold').first.name.should eql('barbary')
104
105
  end
106
+
107
+ describe 'with prefixed aliases' do
108
+ it 'should search by the prefix' do
109
+ Item.search_for('prefixed = green').length.should == 1
110
+ end
111
+
112
+ it 'should search by the full length variant' do
113
+ Item.search_for('prefixed.value = green').length.should == 1
114
+ end
115
+
116
+ it 'should not search by just any key with common prefix' do
117
+ proc { Item.search_for('prefixed.something_which_is_not_defined = green') }.should raise_error(ScopedSearch::QueryNotSupported)
118
+ end
119
+ end
105
120
  end
106
121
  end
107
122
  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
@@ -13,15 +13,18 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
13
13
 
14
14
  @parent_class = ScopedSearch::RSpec::Database.create_model(int: :integer, type: :string, related_id: :integer) do |klass|
15
15
  klass.scoped_search on: :int
16
+ klass.belongs_to @related_class.table_name.to_sym, foreign_key: :related_id
16
17
  end
17
18
  @subclass1 = ScopedSearch::RSpec::Database.create_sti_model(@parent_class)
18
19
  @subclass2 = ScopedSearch::RSpec::Database.create_sti_model(@parent_class) do |klass|
19
- klass.belongs_to @related_class.table_name.to_sym, foreign_key: :related_id
20
20
  klass.scoped_search on: :int, rename: :other_int
21
21
  klass.scoped_search relation: @related_class.table_name, on: :int, rename: :related_int
22
22
  end
23
23
 
24
- @related_class.has_many @subclass1.table_name.to_sym
24
+ @related_class.has_many @subclass1.table_name.to_sym, :foreign_key => :related_id
25
+ @related_class.has_many @subclass2.table_name.to_sym, :foreign_key => :related_id
26
+ @related_class.scoped_search :relation => @subclass1.table_name.to_sym, :on => :int, :rename => 'subclass1.id'
27
+ @related_class.scoped_search :relation => @subclass2.table_name.to_sym, :on => :int, :rename => 'subclass2.id'
25
28
 
26
29
  @record1 = @subclass1.create!(int: 7)
27
30
  @related_record1 = @related_class.create!(int: 42)
@@ -79,5 +82,12 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
79
82
  @subclass2.search_for('related_int = 42').should eq([@record2])
80
83
  end
81
84
  end
85
+
86
+ context 'querying related records' do
87
+ it 'shuld find only relevant instances of STI subclasses' do
88
+ @related_class.search_for("subclass1.id ^ (#{@record1.int})").should eq([])
89
+ @related_class.search_for("subclass2.id ^ (#{@record1.int}, #{@record2.int})").should eq([@related_record1])
90
+ end
91
+ end
82
92
  end
83
93
  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
 
@@ -52,6 +52,8 @@ describe ScopedSearch::QueryBuilder do
52
52
  it "should validate value if validator selected" do
53
53
  field = double('field')
54
54
  field.stub(:virtual?).and_return(false)
55
+ field.stub(:temporal?).and_return(false)
56
+ field.stub(:relation).and_return(nil)
55
57
  field.stub(:only_explicit).and_return(true)
56
58
  field.stub(:field).and_return(:test_field)
57
59
  field.stub(:ext_method).and_return(nil)
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scoped_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.6
4
+ version: 4.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amos Benari
8
8
  - Willem van Bergen
9
9
  - Wes Hays
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-11-21 00:00:00.000000000 Z
13
+ date: 2021-11-29 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -77,6 +77,7 @@ extra_rdoc_files:
77
77
  - CONTRIBUTING.rdoc
78
78
  - LICENSE
79
79
  files:
80
+ - ".github/workflows/ruby.yml"
80
81
  - ".gitignore"
81
82
  - ".travis.yml"
82
83
  - CHANGELOG.rdoc
@@ -85,6 +86,12 @@ files:
85
86
  - Gemfile.activerecord42
86
87
  - Gemfile.activerecord50
87
88
  - Gemfile.activerecord51
89
+ - Gemfile.activerecord52
90
+ - Gemfile.activerecord52_with_activesupport52
91
+ - Gemfile.activerecord60
92
+ - Gemfile.activerecord60_with_activesupport60
93
+ - Gemfile.activerecord61
94
+ - Gemfile.activerecord61_with_activesupport61
88
95
  - LICENSE
89
96
  - README.rdoc
90
97
  - Rakefile
@@ -112,6 +119,7 @@ files:
112
119
  - spec/integration/auto_complete_spec.rb
113
120
  - spec/integration/ext_method_spec.rb
114
121
  - spec/integration/key_value_querying_spec.rb
122
+ - spec/integration/nested_has_many_through_querying_spec.rb
115
123
  - spec/integration/ordinal_querying_spec.rb
116
124
  - spec/integration/profile_querying_spec.rb
117
125
  - spec/integration/rails_helper_spec.rb
@@ -136,7 +144,7 @@ homepage: https://github.com/wvanbergen/scoped_search/wiki
136
144
  licenses:
137
145
  - MIT
138
146
  metadata: {}
139
- post_install_message:
147
+ post_install_message:
140
148
  rdoc_options:
141
149
  - "--title"
142
150
  - scoped_search
@@ -157,9 +165,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
165
  - !ruby/object:Gem::Version
158
166
  version: '0'
159
167
  requirements: []
160
- rubyforge_project:
161
- rubygems_version: 2.6.8
162
- signing_key:
168
+ rubygems_version: 3.1.2
169
+ signing_key:
163
170
  specification_version: 4
164
171
  summary: Easily search you ActiveRecord models with a simple query language using
165
172
  a named scope
@@ -170,6 +177,7 @@ test_files:
170
177
  - spec/integration/auto_complete_spec.rb
171
178
  - spec/integration/ext_method_spec.rb
172
179
  - spec/integration/key_value_querying_spec.rb
180
+ - spec/integration/nested_has_many_through_querying_spec.rb
173
181
  - spec/integration/ordinal_querying_spec.rb
174
182
  - spec/integration/profile_querying_spec.rb
175
183
  - spec/integration/rails_helper_spec.rb