wvanbergen-scoped_search 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/README.rdoc +48 -32
  2. data/Rakefile +1 -3
  3. data/lib/scoped_search.rb +45 -95
  4. data/lib/scoped_search/adapters.rb +41 -0
  5. data/lib/scoped_search/definition.rb +122 -0
  6. data/lib/scoped_search/query_builder.rb +213 -0
  7. data/lib/scoped_search/query_language.rb +30 -0
  8. data/lib/scoped_search/query_language/ast.rb +141 -0
  9. data/lib/scoped_search/query_language/parser.rb +115 -0
  10. data/lib/scoped_search/query_language/tokenizer.rb +62 -0
  11. data/{test → spec}/database.yml +0 -0
  12. data/spec/integration/api_spec.rb +82 -0
  13. data/spec/integration/ordinal_querying_spec.rb +153 -0
  14. data/spec/integration/relation_querying_spec.rb +258 -0
  15. data/spec/integration/string_querying_spec.rb +187 -0
  16. data/spec/lib/database.rb +44 -0
  17. data/spec/lib/matchers.rb +40 -0
  18. data/spec/lib/mocks.rb +19 -0
  19. data/spec/spec_helper.rb +21 -0
  20. data/spec/unit/ast_spec.rb +197 -0
  21. data/spec/unit/definition_spec.rb +24 -0
  22. data/spec/unit/parser_spec.rb +105 -0
  23. data/spec/unit/query_builder_spec.rb +5 -0
  24. data/spec/unit/tokenizer_spec.rb +97 -0
  25. data/tasks/database_tests.rake +5 -5
  26. data/tasks/github-gem.rake +8 -3
  27. metadata +39 -23
  28. data/lib/scoped_search/query_conditions_builder.rb +0 -209
  29. data/lib/scoped_search/query_language_parser.rb +0 -117
  30. data/lib/scoped_search/reg_tokens.rb +0 -51
  31. data/tasks/documentation.rake +0 -33
  32. data/test/integration/api_test.rb +0 -53
  33. data/test/lib/test_models.rb +0 -148
  34. data/test/lib/test_schema.rb +0 -68
  35. data/test/test_helper.rb +0 -44
  36. data/test/unit/query_conditions_builder_test.rb +0 -410
  37. data/test/unit/query_language_test.rb +0 -155
  38. data/test/unit/search_for_test.rb +0 -124
@@ -0,0 +1,258 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe ScopedSearch do
4
+
5
+ before(:all) do
6
+ ScopedSearch::Spec::Database.establish_connection
7
+ end
8
+
9
+ after(:all) do
10
+ ScopedSearch::Spec::Database.close_connection
11
+ end
12
+
13
+ context 'querying a :belongs_to relation' do
14
+
15
+ before(:all) do
16
+
17
+ # The related class
18
+ ActiveRecord::Migration.create_table(:bars) { |t| t.string :related }
19
+ class Bar < ActiveRecord::Base; has_many :foos; end
20
+
21
+ # The class on which to call search_for
22
+ Foo = ScopedSearch::Spec::Database.create_model(:foo => :string, :bar_id => :integer) do |klass|
23
+ klass.belongs_to :bar
24
+ klass.scoped_search :in => :bar, :on => :related
25
+ end
26
+
27
+ @bar_record = Bar.create!(:related => 'bar')
28
+
29
+ Foo.create!(:foo => 'foo', :bar => @bar_record)
30
+ Foo.create!(:foo => 'foo too', :bar => @bar_record)
31
+ Foo.create!(:foo => 'foo three', :bar => Bar.create!(:related => 'another bar'))
32
+ Foo.create!(:foo => 'foo four')
33
+ end
34
+
35
+ after(:all) do
36
+ ScopedSearch::Spec::Database.drop_model(Bar)
37
+ ScopedSearch::Spec::Database.drop_model(Foo)
38
+ Object.send :remove_const, :Foo
39
+ Object.send :remove_const, :Bar
40
+ end
41
+
42
+ it "should find all records with a related bar record containing bar" do
43
+ Foo.search_for('bar').should have(3).items
44
+ end
45
+
46
+ it "should find all records with a related bar record having an exact value of bar" do
47
+ Foo.search_for('= bar').should have(2).items
48
+ end
49
+
50
+ it "should find all records with a related bar record having an exact value of bar with an explicit field" do
51
+ Foo.search_for('related = bar').should have(2).items
52
+ end
53
+
54
+ it "should find records for which the bar relation is not set using null?" do
55
+ Foo.search_for('null? related').should have(1).items
56
+ end
57
+ end
58
+
59
+ context 'querying a :has_many relation' do
60
+
61
+ before(:all) do
62
+
63
+ # The related class
64
+ ActiveRecord::Migration.create_table(:bars) { |t| t.string :related; t.integer :foo_id }
65
+ class Bar < ActiveRecord::Base; belongs_to :foo; end
66
+
67
+ # The class on which to call search_for
68
+ Foo = ScopedSearch::Spec::Database.create_model(:foo => :string, :bar_id => :integer) do |klass|
69
+ klass.has_many :bars
70
+ klass.scoped_search :in => :bars, :on => :related
71
+ end
72
+
73
+ @foo_1 = Foo.create!(:foo => 'foo')
74
+ @foo_2 = Foo.create!(:foo => 'foo too')
75
+ @foo_3 = Foo.create!(:foo => 'foo three')
76
+
77
+ Bar.create!(:related => 'bar', :foo => @foo_1)
78
+ Bar.create!(:related => 'another bar', :foo => @foo_1)
79
+ Bar.create!(:related => 'other bar', :foo => @foo_2)
80
+ end
81
+
82
+ after(:all) do
83
+ ScopedSearch::Spec::Database.drop_model(Bar)
84
+ ScopedSearch::Spec::Database.drop_model(Foo)
85
+ Object.send :remove_const, :Foo
86
+ Object.send :remove_const, :Bar
87
+ end
88
+
89
+ it "should find all records with at least one bar record containing 'bar'" do
90
+ Foo.search_for('bar').should have(2).items
91
+ end
92
+
93
+ it "should find the only record with at least one bar record having the exact value 'bar'" do
94
+ Foo.search_for('= bar').should have(1).item
95
+ end
96
+
97
+ it "should find all records for which at least one related bar record exists" do
98
+ Foo.search_for('set? related').should have(2).items
99
+ end
100
+
101
+ it "should find all records for which none related bar records exist" do
102
+ Foo.search_for('null? related').should have(1).items
103
+ end
104
+
105
+ end
106
+
107
+ context 'querying a :has_one relation' do
108
+
109
+ before(:all) do
110
+
111
+ # The related class
112
+ ActiveRecord::Migration.create_table(:bars) { |t| t.string :related; t.integer :foo_id }
113
+ class Bar < ActiveRecord::Base; belongs_to :foo; end
114
+
115
+ # The class on which to call search_for
116
+ Foo = ScopedSearch::Spec::Database.create_model(:foo => :string) do |klass|
117
+ klass.has_one :bar
118
+ klass.scoped_search :in => :bar, :on => :related
119
+ end
120
+
121
+ @foo_1 = Foo.create!(:foo => 'foo')
122
+ @foo_2 = Foo.create!(:foo => 'foo too')
123
+ @foo_3 = Foo.create!(:foo => 'foo three')
124
+
125
+ Bar.create!(:related => 'bar', :foo => @foo_1)
126
+ Bar.create!(:related => 'other bar', :foo => @foo_2)
127
+ end
128
+
129
+ after(:all) do
130
+ ScopedSearch::Spec::Database.drop_model(Bar)
131
+ ScopedSearch::Spec::Database.drop_model(Foo)
132
+ Object.send :remove_const, :Foo
133
+ Object.send :remove_const, :Bar
134
+ end
135
+
136
+ it "should find all records with a bar record containing 'bar" do
137
+ Foo.search_for('bar').should have(2).items
138
+ end
139
+
140
+ it "should find the only record with the bar record has the exact value 'bar" do
141
+ Foo.search_for('= bar').should have(1).item
142
+ end
143
+
144
+ it "should find all records for which the related bar record exists" do
145
+ Foo.search_for('set? related').should have(2).items
146
+ end
147
+
148
+ it "should find all records for which the related bar record does not exist" do
149
+ Foo.search_for('null? related').should have(1).items
150
+ end
151
+ end
152
+
153
+ context 'querying a :has_and_belongs_to_many relation' do
154
+
155
+ before(:all) do
156
+
157
+ # Create some tables
158
+ ActiveRecord::Migration.create_table(:bars) { |t| t.string :related }
159
+ ActiveRecord::Migration.create_table(:bars_foos, :id => false) { |t| t.integer :foo_id; t.integer :bar_id }
160
+ ActiveRecord::Migration.create_table(:foos) { |t| t.string :foo }
161
+
162
+ # The related class
163
+ class Bar < ActiveRecord::Base; end
164
+
165
+ # The class on which to call search_for
166
+ class Foo < ActiveRecord::Base
167
+ has_and_belongs_to_many :bars
168
+ scoped_search :in => :bars, :on => :related
169
+ end
170
+
171
+ @foo_1 = Foo.create!(:foo => 'foo')
172
+ @foo_2 = Foo.create!(:foo => 'foo too')
173
+ @foo_3 = Foo.create!(:foo => 'foo three')
174
+
175
+ @bar_1 = Bar.create!(:related => 'bar')
176
+ @bar_2 = Bar.create!(:related => 'other bar')
177
+ @bar_3 = Bar.create!(:related => 'last bar')
178
+
179
+ @foo_1.bars << @bar_1 << @bar_2
180
+ @foo_2.bars << @bar_2 << @bar_3
181
+ end
182
+
183
+ after(:all) do
184
+ ActiveRecord::Migration.drop_table(:bars_foos)
185
+ ActiveRecord::Migration.drop_table(:bars)
186
+ ActiveRecord::Migration.drop_table(:foos)
187
+ Object.send :remove_const, :Foo
188
+ Object.send :remove_const, :Bar
189
+ end
190
+
191
+ it "should find all records with at least one associated bar record containing 'bar'" do
192
+ Foo.search_for('bar').should have(2).items
193
+ end
194
+
195
+ it "should find record which is related to @bar_1" do
196
+ Foo.search_for('= bar').should have(1).items
197
+ end
198
+
199
+ it "should find the only record related to @bar_3" do
200
+ Foo.search_for('last').should have(1).items
201
+ end
202
+
203
+ it "should find all records that are related to @bar_2" do
204
+ Foo.search_for('other').should have(2).items
205
+ end
206
+ end
207
+
208
+ context 'querying a :has_many => :through relation' do
209
+
210
+ before(:all) do
211
+
212
+ # Create some tables
213
+ ActiveRecord::Migration.create_table(:bars) { |t| t.integer :foo_id; t.integer :baz_id }
214
+ ActiveRecord::Migration.create_table(:bazs) { |t| t.string :related }
215
+ ActiveRecord::Migration.create_table(:foos) { |t| t.string :foo }
216
+
217
+ # The related classes
218
+ class Bar < ActiveRecord::Base; belongs_to :baz; belongs_to :foo; end
219
+ class Baz < ActiveRecord::Base; has_many :bars; end
220
+
221
+ # The class on which to call search_for
222
+ class Foo < ActiveRecord::Base
223
+ has_many :bars
224
+ has_many :bazs, :through => :bars
225
+
226
+ scoped_search :in => :bazs, :on => :related
227
+ end
228
+
229
+ @foo_1 = Foo.create!(:foo => 'foo')
230
+ @foo_2 = Foo.create!(:foo => 'foo too')
231
+ @foo_3 = Foo.create!(:foo => 'foo three')
232
+
233
+ @baz_1 = Baz.create(:related => 'baz')
234
+ @baz_2 = Baz.create(:related => 'baz too!')
235
+
236
+ @bar_1 = Bar.create!(:foo => @foo_1, :baz => @baz_1)
237
+ @bar_2 = Bar.create!(:foo => @foo_1)
238
+ @bar_3 = Bar.create!(:foo => @foo_2, :baz => @baz_1)
239
+ @bar_3 = Bar.create!(:foo => @foo_2, :baz => @baz_2)
240
+ @bar_3 = Bar.create!(:foo => @foo_2, :baz => @baz_2)
241
+ @bar_4 = Bar.create!(:foo => @foo_3)
242
+ end
243
+
244
+ after(:all) do
245
+ ActiveRecord::Migration.drop_table(:bazs)
246
+ ActiveRecord::Migration.drop_table(:bars)
247
+ ActiveRecord::Migration.drop_table(:foos)
248
+ Object.send :remove_const, :Foo
249
+ Object.send :remove_const, :Bar
250
+ Object.send :remove_const, :Baz
251
+ end
252
+
253
+ it "should find the two records that are related to a baz record" do
254
+ Foo.search_for('baz').should have(2).items
255
+ end
256
+ end
257
+
258
+ end
@@ -0,0 +1,187 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe ScopedSearch, :search_for do
4
+
5
+ before(:all) do
6
+ ScopedSearch::Spec::Database.establish_connection
7
+ @class = ScopedSearch::Spec::Database.create_model(:string => :string, :another => :string, :explicit => :string) do |klass|
8
+ klass.scoped_search :on => :string
9
+ klass.scoped_search :on => :another, :default_operator => :eq, :alias => :alias
10
+ klass.scoped_search :on => :explicit, :only_explicit => true
11
+ end
12
+
13
+ @class.create!(:string => 'foo', :another => 'temp 1', :explicit => 'baz')
14
+ @class.create!(:string => 'bar', :another => 'temp 2', :explicit => 'baz')
15
+ @class.create!(:string => 'baz', :another => 'temp 3', :explicit => nil)
16
+ end
17
+
18
+ after(:all) do
19
+ ScopedSearch::Spec::Database.drop_model(@class)
20
+ ScopedSearch::Spec::Database.close_connection
21
+ end
22
+
23
+ context 'in an implicit string field' do
24
+ it "should find the record with an exact string match" do
25
+ @class.search_for('foo').should have(1).item
26
+ end
27
+
28
+ it "should find the opther two records using NOT with an exact string match" do
29
+ @class.search_for('-foo').should have(2).item
30
+ end
31
+
32
+ it "should find the record with an exact string match and an explicit field operator" do
33
+ @class.search_for('string = foo').should have(1).item
34
+ end
35
+
36
+ it "should find the record with an exact string match and an explicit field operator" do
37
+ @class.search_for('another = foo').should have(0).items
38
+ end
39
+
40
+ it "should find the record with an partial string match" do
41
+ @class.search_for('fo').should have(1).item
42
+ end
43
+
44
+ it "should find the other two records using NOT with an partial string match" do
45
+ @class.search_for('-fo').should have(2).item
46
+ end
47
+
48
+ it "should not find the record with an explicit equals operator and a partial match" do
49
+ @class.search_for('= fo').should have(0).items
50
+ end
51
+
52
+ it "should find the record with an explicit LIKE operator and a partial match" do
53
+ @class.search_for('~ fo').should have(1).items
54
+ end
55
+
56
+ it "should find the all other record with an explicit NOT LIKE operator and a partial match" do
57
+ @class.search_for('string !~ fo').should have(@class.count - 1).items
58
+ end
59
+
60
+ it "should not find a record with a non-match" do
61
+ @class.search_for('nonsense').should have(0).items
62
+ end
63
+
64
+ it "should find two records if it partially matches them" do
65
+ @class.search_for('ba').should have(2).item
66
+ end
67
+
68
+ it "should find no records starting with an a" do
69
+ @class.search_for('a%').should have(0).item
70
+ end
71
+
72
+ it "should find one records ending with an oo" do
73
+ @class.search_for('%oo').should have(1).item
74
+ end
75
+
76
+ it "should find records without case sensitivity when using the LIKE operator" do
77
+ @class.search_for('string ~ FOO').should have(1).item
78
+ end
79
+
80
+ it "should not find records without case sensitivity when using the = operator" do
81
+ @class.search_for('string = FOO').should have(0).items
82
+ end
83
+
84
+ it "should find records without case sensitivity when using the != operator" do
85
+ @class.search_for('string != FOO').should have(3).items
86
+ end
87
+
88
+ it "should find records without case sensitivity when using the NOT LIKE operator" do
89
+ @class.search_for('string !~ FOO').should have(2).items
90
+ end
91
+
92
+ it "should find the record if one of the query words match using OR" do
93
+ @class.search_for('foo OR nonsense').should have(1).item
94
+ end
95
+
96
+ it "should find no records in one of the AND conditions isn't met" do
97
+ @class.search_for('foo AND nonsense').should have(0).item
98
+ end
99
+
100
+ it "should find two records every single OR conditions matches one single record" do
101
+ @class.search_for('foo OR baz').should have(2).item
102
+ end
103
+
104
+ it "should find two records every single AND conditions matches one single record" do
105
+ @class.search_for('foo AND baz').should have(0).item
106
+ end
107
+ end
108
+
109
+ context 'in a field with a different default operator' do
110
+ it "should find an exact match" do
111
+ @class.search_for('"temp 1"').should have(1).item
112
+ end
113
+
114
+ it "should find the orther records using NOT and an exact match" do
115
+ @class.search_for('-"temp 1"').should have(2).item
116
+ end
117
+
118
+ it "should find an explicit match" do
119
+ @class.search_for('another = "temp 1"').should have(1).item
120
+ end
121
+
122
+ it "should not find a partial match" do
123
+ @class.search_for('temp').should have(0).item
124
+ end
125
+
126
+ it "should find all records using a NOT with a partial match on all records" do
127
+ @class.search_for('-temp"').should have(3).item
128
+ end
129
+
130
+ it "should find a partial match when the like operator is given" do
131
+ @class.search_for('~ temp').should have(3).item
132
+ end
133
+
134
+ it "should find a partial match when the like operator and the field name is given" do
135
+ @class.search_for('another ~ temp').should have(3).item
136
+ end
137
+ end
138
+
139
+ context 'using an aliased field' do
140
+ it "should find an explicit match using its alias" do
141
+ @class.search_for('alias = "temp 1"').should have(1).item
142
+ end
143
+ end
144
+
145
+ context 'in an explicit string field' do
146
+
147
+ it "should not find the records if the explicit field is not given in the query" do
148
+ @class.search_for('= baz').should have(1).item
149
+ end
150
+
151
+ it "should find all records when searching on the explicit field" do
152
+ @class.search_for('explicit = baz').should have(2).items
153
+ end
154
+
155
+ it "should find no records if the value in the explicit field is not an exact match" do
156
+ @class.search_for('explicit = ba').should have(0).item
157
+ end
158
+
159
+ it "should find all records when searching on the explicit field" do
160
+ @class.search_for('explicit ~ ba').should have(2).items
161
+ end
162
+
163
+ it "should only find the record with string = foo and explicit = baz" do
164
+ @class.search_for('foo, explicit = baz').should have(1).item
165
+ end
166
+ end
167
+
168
+ context 'using null? and set? queries' do
169
+
170
+ it "should return all records if the string field is being checked with set?" do
171
+ @class.search_for('set? string').should have(3).items
172
+ end
173
+
174
+ it "should return no records if the string field is being checked with null?" do
175
+ @class.search_for('null? string').should have(0).items
176
+ end
177
+
178
+ it "should return all records with a value if the string field is being checked with set?" do
179
+ @class.search_for('set? explicit').should have(2).items
180
+ end
181
+
182
+ it "should return all records without a value if the string field is being checked with null?" do
183
+ @class.search_for('null? explicit').should have(1).items
184
+ end
185
+
186
+ end
187
+ end
@@ -0,0 +1,44 @@
1
+ ActiveRecord::Migration.verbose = false unless ENV.has_key?('DEBUG')
2
+
3
+ module ScopedSearch::Spec::Database
4
+
5
+ def self.establish_connection
6
+ if ENV['DATABASE']
7
+ self.establish_named_connection(ENV['DATABASE'])
8
+ else
9
+ self.establish_default_connection
10
+ end
11
+ end
12
+
13
+ def self.establish_named_connection(name)
14
+ @database_connections ||= YAML.load(File.read("#{File.dirname(__FILE__)}/../database.yml"))
15
+ raise "#{name} database not configured" if @database_connections[name.to_s].nil?
16
+ ActiveRecord::Base.establish_connection(@database_connections[name.to_s])
17
+ end
18
+
19
+ def self.establish_default_connection
20
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
21
+ end
22
+
23
+ def self.close_connection
24
+ ActiveRecord::Base.remove_connection
25
+ end
26
+
27
+ def self.create_model(fields)
28
+ table_name = "model_#{rand}".gsub(/\./, '')
29
+ ActiveRecord::Migration.create_table(table_name) do |t|
30
+ fields.each do |name, field_type|
31
+ t.send(field_type.to_s.gsub(/^unindexed_/, '').to_sym, name)
32
+ end
33
+ end
34
+
35
+ klass = Class.new(ActiveRecord::Base)
36
+ klass.set_table_name(table_name)
37
+ yield(klass) if block_given?
38
+ return klass
39
+ end
40
+
41
+ def self.drop_model(klass)
42
+ ActiveRecord::Migration.drop_table(klass.table_name)
43
+ end
44
+ end