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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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