praxis 2.0.pre.11 → 2.0.pre.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '2.0.pre.11'
2
+ VERSION = '2.0.pre.12'
3
3
  end
@@ -14,6 +14,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
14
14
  shared_examples 'subject_equivalent_to' do |expected_result|
15
15
  it do
16
16
  loaded_ids = subject.all.map(&:id).sort
17
+ expected_result = expected_result.call if expected_result.is_a?(Proc)
17
18
  expected_ids = expected_result.all.map(&:id).sort
18
19
  expect(loaded_ids).to_not be_empty
19
20
  expect(loaded_ids).to eq(expected_ids)
@@ -23,12 +24,11 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
23
24
  # Poorman's way to compare SQL queries...
24
25
  shared_examples 'subject_matches_sql' do |expected_sql|
25
26
  it do
26
- # Remove parenthesis as our queries have WHERE clauses using them...
27
- gen_sql = subject.all.to_sql.gsub(/[()]/,'')
27
+ gen_sql = subject.all.to_sql
28
28
  # Strip blank at the beggining (and end) of every line
29
29
  # ...and recompose it by adding an extra space at the beginning of each one instead
30
30
  exp = expected_sql.split(/\n/).map do |line|
31
- " " + line.strip.gsub(/[()]/,'')
31
+ " " + line.strip
32
32
  end.join.strip
33
33
  expect(gen_sql).to eq(exp)
34
34
  end
@@ -37,7 +37,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
37
37
  context 'initialize' do
38
38
  it 'sets the right things to the instance' do
39
39
  instance
40
- expect(instance.query).to eq(base_query)
40
+ expect(instance.instance_variable_get(:@initial_query)).to eq(base_query)
41
41
  expect(instance.model).to eq(base_model)
42
42
  expect(instance.filters_map).to eq(filters_map)
43
43
  end
@@ -52,208 +52,278 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
52
52
  expect(subject).to be(base_query)
53
53
  end
54
54
  end
55
- context 'by a simple field' do
56
- context 'that maps to the same name' do
57
- let(:filters_string) { 'category_uuid=deadbeef1' }
58
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
59
- end
60
- context 'same-name filter mapping works' do
61
- context 'even if ther was not a filter explicitly defined for it' do
55
+ context 'with flat AND conditions' do
56
+ context 'by a simple field' do
57
+ context 'that maps to the same name' do
62
58
  let(:filters_string) { 'category_uuid=deadbeef1' }
63
59
  it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
64
60
  end
61
+ context 'same-name filter mapping works' do
62
+ context 'even if ther was not a filter explicitly defined for it' do
63
+ let(:filters_string) { 'category_uuid=deadbeef1' }
64
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
65
+ end
65
66
 
66
- context 'but if it is a field that does not exist in the model' do
67
- let(:filters_string) { 'nonexisting=valuehere' }
68
- it 'it blows up with the right error' do
69
- expect{subject}.to raise_error(/Filtering by nonexisting is not allowed/)
67
+ context 'but if it is a field that does not exist in the model' do
68
+ let(:filters_string) { 'nonexisting=valuehere' }
69
+ it 'it blows up with the right error' do
70
+ expect{subject}.to raise_error(/Filtering by nonexisting is not allowed/)
71
+ end
70
72
  end
71
73
  end
74
+ context 'that maps to a different name' do
75
+ let(:filters_string) { 'name=Book1'}
76
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
77
+ end
78
+ context 'that is mapped as a nested struct' do
79
+ let(:filters_string) { 'fake_nested.name=Book1'}
80
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
81
+ end
72
82
  end
73
- context 'that maps to a different name' do
74
- let(:filters_string) { 'name=Book1'}
75
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
76
- end
77
- context 'that is mapped as a nested struct' do
78
- let(:filters_string) { 'fake_nested.name=Book1'}
79
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
80
- end
81
- end
82
83
 
83
- context 'by a field or a related model' do
84
- context 'for a belongs_to association' do
85
- let(:filters_string) { 'author.name=author2'}
86
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name' => 'author2')
87
- end
88
- context 'for a has_many association' do
89
- let(:filters_string) { 'taggings.label=primary' }
90
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
91
- end
92
- context 'for a has_many through association' do
93
- let(:filters_string) { 'tags.name=blue' }
94
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
84
+ context 'by a field or a related model' do
85
+ context 'for a belongs_to association' do
86
+ let(:filters_string) { 'author.name=author2'}
87
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name' => 'author2')
88
+ end
89
+ context 'for a has_many association' do
90
+ let(:filters_string) { 'taggings.label=primary' }
91
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
92
+ end
93
+ context 'for a has_many through association' do
94
+ let(:filters_string) { 'tags.name=blue' }
95
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
96
+ end
95
97
  end
96
- end
97
98
 
98
- context 'by using all supported operators' do
99
- PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
100
- COMMON_SQL_PREFIX = <<~SQL
101
- SELECT "active_books".* FROM "active_books"
102
- INNER JOIN
103
- "active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
104
- SQL
105
- context '=' do
106
- let(:filters_string) { 'author.id=11'}
107
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
108
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
109
- WHERE "#{PREF}/author"."id" = 11
110
- SQL
111
- end
112
- context '= (with array)' do
113
- let(:filters_string) { 'author.id=11,22'}
114
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
115
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
116
- WHERE "#{PREF}/author"."id" IN (11,22)
117
- SQL
118
- end
119
- context '!=' do
120
- let(:filters_string) { 'author.id!=11'}
121
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
122
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
123
- WHERE "#{PREF}/author"."id" <> 11
124
- SQL
125
- end
126
- context '!= (with array)' do
127
- let(:filters_string) { 'author.id!=11,888'}
128
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
129
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
130
- WHERE "#{PREF}/author"."id" NOT IN (11,888)
131
- SQL
132
- end
133
- context '>' do
134
- let(:filters_string) { 'author.id>1'}
135
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
136
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
137
- WHERE "#{PREF}/author"."id" > 1
138
- SQL
139
- end
140
- context '<' do
141
- let(:filters_string) { 'author.id<22'}
142
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
143
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
144
- WHERE "#{PREF}/author"."id" < 22
145
- SQL
146
- end
147
- context '>=' do
148
- let(:filters_string) { 'author.id>=22'}
149
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
150
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
151
- WHERE "#{PREF}/author"."id" >= 22
152
- SQL
153
- end
154
- context '<=' do
155
- let(:filters_string) { 'author.id<=22'}
156
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
157
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
158
- WHERE "#{PREF}/author"."id" <= 22
159
- SQL
160
- end
161
- context '!' do
162
- let(:filters_string) { 'author.id!'}
163
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
164
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
165
- WHERE "#{PREF}/author"."id" IS NOT NULL
166
- SQL
167
- end
168
- context '!!' do
169
- let(:filters_string) { 'author.name!!'}
170
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
171
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
172
- WHERE "#{PREF}/author"."name" IS NULL
173
- SQL
174
- end
175
- context 'including LIKE fuzzy queries' do
176
- context 'LIKE' do
177
- let(:filters_string) { 'author.name=author*'}
178
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
99
+ # NOTE: apparently AR when conditions are build with strings in the where clauses (instead of names, etc)
100
+ # it decides to parenthesize them, even when there's only 1 condition. Hence the silly parentization of
101
+ # these SQL fragments here (and others)
102
+ context 'by using all supported operators' do
103
+ PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
104
+ COMMON_SQL_PREFIX = <<~SQL
105
+ SELECT "active_books".* FROM "active_books"
106
+ INNER JOIN
107
+ "active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
108
+ SQL
109
+ context '=' do
110
+ let(:filters_string) { 'author.id=11'}
111
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
112
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
113
+ WHERE ("#{PREF}/author"."id" = 11)
114
+ SQL
115
+ end
116
+ context '= (with array)' do
117
+ let(:filters_string) { 'author.id=11,22'}
118
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
119
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
120
+ WHERE ("#{PREF}/author"."id" IN (11,22))
121
+ SQL
122
+ end
123
+ context '!=' do
124
+ let(:filters_string) { 'author.id!=11'}
125
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
126
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
127
+ WHERE ("#{PREF}/author"."id" <> 11)
128
+ SQL
129
+ end
130
+ context '!= (with array)' do
131
+ let(:filters_string) { 'author.id!=11,888'}
132
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
133
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
134
+ WHERE ("#{PREF}/author"."id" NOT IN (11,888))
135
+ SQL
136
+ end
137
+ context '>' do
138
+ let(:filters_string) { 'author.id>1'}
139
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
140
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
141
+ WHERE ("#{PREF}/author"."id" > 1)
142
+ SQL
143
+ end
144
+ context '<' do
145
+ let(:filters_string) { 'author.id<22'}
146
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
147
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
148
+ WHERE ("#{PREF}/author"."id" < 22)
149
+ SQL
150
+ end
151
+ context '>=' do
152
+ let(:filters_string) { 'author.id>=22'}
153
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
154
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
155
+ WHERE ("#{PREF}/author"."id" >= 22)
156
+ SQL
157
+ end
158
+ context '<=' do
159
+ let(:filters_string) { 'author.id<=22'}
160
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
161
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
162
+ WHERE ("#{PREF}/author"."id" <= 22)
163
+ SQL
164
+ end
165
+ context '!' do
166
+ let(:filters_string) { 'author.id!'}
167
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
179
168
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
180
- WHERE "#{PREF}/author"."name" LIKE 'author%'
181
- SQL
169
+ WHERE ("#{PREF}/author"."id" IS NOT NULL)
170
+ SQL
182
171
  end
183
- context 'NOT LIKE' do
184
- let(:filters_string) { 'author.name!=foobar*'}
185
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
172
+ context '!!' do
173
+ let(:filters_string) { 'author.name!!'}
174
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
186
175
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
187
- WHERE "#{PREF}/author"."name" NOT LIKE 'foobar%'
188
- SQL
176
+ WHERE ("#{PREF}/author"."name" IS NULL)
177
+ SQL
178
+ end
179
+ context 'including LIKE fuzzy queries' do
180
+ context 'LIKE' do
181
+ let(:filters_string) { 'author.name=author*'}
182
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
183
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
184
+ WHERE ("#{PREF}/author"."name" LIKE 'author%')
185
+ SQL
186
+ end
187
+ context 'NOT LIKE' do
188
+ let(:filters_string) { 'author.name!=foobar*'}
189
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
190
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
191
+ WHERE ("#{PREF}/author"."name" NOT LIKE 'foobar%')
192
+ SQL
193
+ end
189
194
  end
190
195
  end
191
- end
192
196
 
193
- context 'with a field mapping using a proc' do
194
- let(:filters_string) { 'name_is_not=Book1' }
195
- it_behaves_like 'subject_equivalent_to', ActiveBook.where.not(simple_name: 'Book1')
196
- end
197
+ context 'with a field mapping using a proc' do
198
+ let(:filters_string) { 'name_is_not=Book1' }
199
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where.not(simple_name: 'Book1')
200
+ end
197
201
 
198
- context 'with a deeply nested chains' do
199
- context 'of depth 2' do
200
- let(:filters_string) { 'category.books.name=Book2' }
201
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books).where('books_active_categories.simple_name': 'Book2')
202
+ context 'with a deeply nested chains' do
203
+ context 'of depth 2' do
204
+ let(:filters_string) { 'category.books.name=Book2' }
205
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books).where('books_active_categories.simple_name': 'Book2')
206
+ end
207
+ context 'multiple conditions on a nested relationship' do
208
+ let(:filters_string) { 'category.books.taggings.tag_id=1&category.books.taggings.label=primary' }
209
+ it_behaves_like 'subject_equivalent_to',
210
+ ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
211
+ it_behaves_like 'subject_matches_sql', <<~SQL
212
+ SELECT "active_books".* FROM "active_books"
213
+ INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
214
+ INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
215
+ INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
216
+ WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
217
+ AND ("#{PREF}/category/books/taggings"."label" = 'primary')
218
+ SQL
219
+ end
220
+ context 'that contain multiple joins to the same table' do
221
+ let(:filters_string) { 'taggings.tag.taggings.tag_id=1' }
222
+ it_behaves_like 'subject_equivalent_to',
223
+ ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1')
224
+ end
202
225
  end
203
- context 'multiple conditions on a nested relationship' do
204
- let(:filters_string) { 'category.books.taggings.tag_id=1&category.books.taggings.label=primary' }
205
- it_behaves_like 'subject_equivalent_to',
206
- ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
207
- it_behaves_like 'subject_matches_sql', <<~SQL
208
- SELECT "active_books".* FROM "active_books"
209
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
210
- INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
211
- INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
212
- WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
213
- AND ("#{PREF}/category/books/taggings"."label" = 'primary')
214
- SQL
226
+
227
+ context 'by multiple fields' do
228
+ context 'adds the where clauses for the top model if fields belong to it' do
229
+ let(:filters_string) { 'category_uuid=deadbeef1&name=Book1' }
230
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
231
+ end
232
+ context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
233
+ let(:filters_string) { 'taggings.label=primary&taggings.tag_id=2' }
234
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary', 'active_taggings.tag_id' => 2)
235
+ end
215
236
  end
216
- context 'that contain multiple joins to the same table' do
217
- let(:filters_string) { 'taggings.tag.taggings.tag_id=1' }
218
- it_behaves_like 'subject_equivalent_to',
219
- ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1')
237
+
238
+ context 'uses fully qualified names for conditions (disambiguate fields)' do
239
+ context 'when we have a join table condition that has the same field' do
240
+ let(:filters_string) { 'name=Book1&category.books.name=Book3' }
241
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
242
+ .where('simple_name': 'Book1')
243
+ .where('books_active_categories.simple_name': 'Book3')
244
+ it_behaves_like 'subject_matches_sql', <<~SQL
245
+ SELECT "active_books".* FROM "active_books"
246
+ INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
247
+ INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
248
+ WHERE ("active_books"."simple_name" = 'Book1')
249
+ AND ("#{PREF}/category/books"."simple_name" = 'Book3')
250
+ SQL
251
+ end
252
+
253
+ context 'it qualifis them even if there are no joined tables/conditions at all' do
254
+ let(:filters_string) { 'id=11'}
255
+ it_behaves_like 'subject_matches_sql', <<~SQL
256
+ SELECT "active_books".* FROM "active_books"
257
+ WHERE ("active_books"."id" = 11)
258
+ SQL
259
+ end
260
+
220
261
  end
221
262
  end
222
263
 
223
- context 'by multiple fields' do
264
+ context 'with simple OR conditions' do
224
265
  context 'adds the where clauses for the top model if fields belong to it' do
225
- let(:filters_string) { 'category_uuid=deadbeef1&name=Book1' }
226
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
266
+ let(:filters_string) { 'category_uuid=deadbeef1|name=Book1' }
267
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(simple_name: 'Book1'))
268
+ end
269
+ context 'supports top level parenthesis' do
270
+ let(:filters_string) { '(category_uuid=deadbeef1|name=Book1)' }
271
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(simple_name: 'Book1'))
227
272
  end
228
273
  context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
229
- let(:filters_string) { 'taggings.label=primary&taggings.tag_id=2' }
230
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary', 'active_taggings.tag_id' => 2)
274
+ let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
275
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
276
+ .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
231
277
  end
232
278
  end
233
279
 
234
- context 'uses fully qualified names for conditions (disambiguate fields)' do
235
- context 'when we have a join table condition that has the same field' do
236
- let(:filters_string) { 'name=Book1&category.books.name=Book3' }
237
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
238
- .where('simple_name': 'Book1')
239
- .where('books_active_categories.simple_name': 'Book3')
240
- it_behaves_like 'subject_matches_sql', <<~SQL
241
- SELECT "active_books".* FROM "active_books"
242
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
243
- INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
244
- WHERE ("#{PREF}/category/books"."simple_name" = 'Book3')
245
- AND ("active_books"."simple_name" = 'Book1')
246
- SQL
247
- end
280
+ context 'with combined AND and OR conditions' do
281
+ let(:filters_string) { '(category_uuid=deadbeef1|category_uuid=deadbeef2)&(name=Book1|name=Book2)' }
282
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(category_uuid: 'deadbeef2'))
283
+ .and(ActiveBook.where(simple_name: 'Book1').or(ActiveBook.where(simple_name: 'Book2')))
284
+ it_behaves_like 'subject_matches_sql', <<~SQL
285
+ SELECT "active_books".* FROM "active_books"
286
+ WHERE ("active_books"."category_uuid" = 'deadbeef1' OR "active_books"."category_uuid" = 'deadbeef2')
287
+ AND ("active_books"."simple_name" = 'Book1' OR "active_books"."simple_name" = 'Book2')
288
+ SQL
248
289
 
249
- context 'it qualifis them even if there are no joined tables/conditions at all' do
250
- let(:filters_string) { 'id=11'}
290
+ context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
291
+ let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
292
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
293
+ .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
251
294
  it_behaves_like 'subject_matches_sql', <<~SQL
252
295
  SELECT "active_books".* FROM "active_books"
253
- WHERE "active_books"."id" = 11
254
- SQL
296
+ INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
297
+ WHERE ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
298
+ SQL
255
299
  end
256
300
 
301
+ context '3-deep AND and OR conditions' do
302
+ let(:filters_string) { '(category.name=cat2|(taggings.label=primary&tags.name=red))&category_uuid=deadbeef1' }
303
+ it_behaves_like('subject_equivalent_to', Proc.new do
304
+ base=ActiveBook.joins(:category,:taggings,:tags)
305
+
306
+ and1_or1 = base.where('category.name': 'cat2')
307
+
308
+ and1_or2_and1 = base.where('taggings.label': 'primary')
309
+ and1_or2_and2 = base.where('tags.name': 'red')
310
+ and1_or2 = and1_or2_and1.and(and1_or2_and2)
311
+
312
+ and1 = and1_or1.or(and1_or2)
313
+ and2=base.where(category_uuid: 'deadbeef1')
314
+
315
+ query = and1.and(and2)
316
+ end)
317
+
318
+ it_behaves_like 'subject_matches_sql', <<~SQL
319
+ SELECT "active_books".* FROM "active_books"
320
+ INNER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
321
+ INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
322
+ INNER JOIN "active_taggings" "taggings_active_books_join" ON "taggings_active_books_join"."book_id" = "active_books"."id"
323
+ INNER JOIN "active_tags" "/tags" ON "/tags"."id" = "taggings_active_books_join"."tag_id"
324
+ WHERE ("/category"."name" = 'cat2' OR ("/taggings"."label" = 'primary') AND ("/tags"."name" = 'red')) AND ("active_books"."category_uuid" = 'deadbeef1')
325
+ SQL
326
+ end
257
327
  end
258
328
 
259
329
  context 'ActiveRecord continues to work as expected (with our patches)' do