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 +5 -5
- data/.github/workflows/ruby.yml +76 -0
- data/.travis.yml +82 -1
- data/CHANGELOG.rdoc +21 -1
- data/Gemfile.activerecord52 +17 -0
- data/Gemfile.activerecord52_with_activesupport52 +18 -0
- data/Gemfile.activerecord60 +17 -0
- data/Gemfile.activerecord60_with_activesupport60 +18 -0
- data/Gemfile.activerecord61 +17 -0
- data/Gemfile.activerecord61_with_activesupport61 +18 -0
- data/lib/scoped_search/auto_complete_builder.rb +3 -2
- data/lib/scoped_search/definition.rb +21 -11
- data/lib/scoped_search/query_builder.rb +85 -76
- data/lib/scoped_search/version.rb +1 -1
- data/lib/scoped_search.rb +2 -2
- data/spec/database.jruby.yml +1 -1
- data/spec/database.ruby.yml +1 -1
- data/spec/integration/auto_complete_spec.rb +26 -1
- data/spec/integration/key_value_querying_spec.rb +15 -0
- data/spec/integration/nested_has_many_through_querying_spec.rb +100 -0
- data/spec/integration/relation_querying_spec.rb +63 -1
- data/spec/integration/sti_querying_spec.rb +12 -2
- data/spec/lib/database.rb +1 -1
- data/spec/unit/query_builder_spec.rb +2 -0
- metadata +15 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f0f6f98982cba23ec69f2b1adaf0c44832abb19adad7a347d4d78c4b0fbb63a8
|
4
|
+
data.tar.gz: 114ea022f87208dc89b92247687d64c2a0146119afa33f2106b69507271ba210
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
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 =
|
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]
|
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
|
-
|
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])
|
320
|
-
search_scope = search_scope.includes(find_options[:include])
|
321
|
-
search_scope = search_scope.joins(find_options[:joins])
|
322
|
-
search_scope = search_scope.reorder(find_options[:order])
|
323
|
-
search_scope = search_scope.references(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
|
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
|
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
|
-
#
|
173
|
-
|
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
|
-
|
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
|
226
|
-
|
227
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
238
|
-
|
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
|
-
|
241
|
-
|
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
|
-
|
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
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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?(
|
308
|
-
|
309
|
-
|
310
|
-
|
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.
|
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
|
data/spec/database.jruby.yml
CHANGED
data/spec/database.ruby.yml
CHANGED
@@ -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
|
-
@
|
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.
|
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:
|
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
|
-
|
161
|
-
|
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
|