praxis 2.0.pre.16 → 2.0.pre.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fe1ba3515514e34ad80775b6b68b4fe046fd1806c40d2345c4fa945d34ed92a
4
- data.tar.gz: 11934130d0c527d1bc52020f393f2a5167ba3f88c876e81deaf7eaa05c518eff
3
+ metadata.gz: dafcffa2e0d146f5b601ed796cb482c5068482757fc2f88770783536da52ad7e
4
+ data.tar.gz: c6f8875641b0e418128dd8f8a923a3df639b1a409e65976472c24b6fac52f90e
5
5
  SHA512:
6
- metadata.gz: a32c98bd77159e59c5192390c2eb683b62930cb3f4b30cbe680b63db5b0c076199b0fb63e5a0a55abb772a92cadef19a8f05d7cedf7eb8218f585ce8b7a01acf
7
- data.tar.gz: 2a086210825ac166730d63b9ea7e5baa2420cdeb1772e434028b95de356ce69778d983fededb1125825f5b036bc0a0c13fc9b61536d77a31cc63cd87f6a48c02
6
+ metadata.gz: 4728bf36f52fd0bf1f73c9efe02190cde40308be2378232897570deab1aa9e5fcbd1ca2b4335b9ad168c4eedda18143a512512e6c132d851b6f6c9447e891d19
7
+ data.tar.gz: 1d689d714604b4841f4059b1ef9734f1ceae0a9afcc7e7d75286be28f67cfe2a51cb86fafdd6c66b2ac40acc72bd431c12ad1b33aa91990ceecd690abc9e01ad
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 2.0.pre.17
6
+ * Changed the Parameter Filtering to use left outer joins (and extra conditions), to allow for the proper results when OR clauses are involved in certain configurations.
7
+ * Built support for allowing filtering directly on associations using `!` and `!!` operators. This allows to filter results where
8
+ there are no associated rows (`!!`) or if there are some associated rows (`!`)
9
+ * Allow implicit definition of `filters_mapping` for filter names that match top-level associations of the model (i.e., like we do for the columns)
5
10
  ## 2.0.pre.16
6
11
 
7
12
  * Updated `Resource.property` signature to only accept known named arguments (`dependencies` and `though` at this time) to spare anyone else from going insane wondering why their `depednencies` aren't working.
@@ -49,9 +49,8 @@ module Praxis
49
49
  end
50
50
 
51
51
  def craft_filter_query(nodetree, for_model:)
52
- result = _compute_joins_and_conditions_data(nodetree, model: for_model)
52
+ result = _compute_joins_and_conditions_data(nodetree, model: for_model, parent_reflection: nil)
53
53
  return @initial_query if result[:conditions].empty?
54
-
55
54
 
56
55
  # Find the root group (usually an AND group) but can be an OR group, or nil if there's only 1 condition
57
56
  root_parent_group = result[:conditions].first[:node_object].parent_group || result[:conditions].first[:node_object]
@@ -60,13 +59,21 @@ module Praxis
60
59
  end
61
60
 
62
61
  # Process the joins
63
- query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.joins(result[:associations_hash])
62
+ query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.left_outer_joins(result[:associations_hash])
64
63
 
65
64
  # Proc to apply a single condition
66
65
  apply_single_condition = Proc.new do |condition, associated_query|
67
66
  colo = condition[:model].columns_hash[condition[:name].to_s]
68
67
  column_prefix = condition[:column_prefix]
69
-
68
+ association_key_column = \
69
+ if ref = condition[:parent_reflection]
70
+ # get the target model of the association(where the assoc pk is)
71
+ target_model = condition[:parent_reflection].klass
72
+ target_model.columns_hash[condition[:parent_reflection].association_primary_key]
73
+ else
74
+ nil
75
+ end
76
+
70
77
  # Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
71
78
  # If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
72
79
  # unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
@@ -79,7 +86,8 @@ module Praxis
79
86
  column_object: colo,
80
87
  op: condition[:op],
81
88
  value: condition[:value],
82
- fuzzy: condition[:fuzzy]
89
+ fuzzy: condition[:fuzzy],
90
+ association_key_column: association_key_column,
83
91
  )
84
92
  end
85
93
 
@@ -144,7 +152,8 @@ module Praxis
144
152
  def _mapped_filter(name)
145
153
  target = @filters_map[name]
146
154
  unless target
147
- if @model.attribute_names.include?(name.to_s)
155
+ filter_name = name.to_s
156
+ if (@model.attribute_names + @model.reflections.keys).include?(filter_name)
148
157
  # Cache it in the filters mapping (to avoid later lookups), and return it.
149
158
  @filters_map[name] = name
150
159
  target = name
@@ -182,31 +191,64 @@ module Praxis
182
191
  end
183
192
 
184
193
  # Calculate join tree and conditions array for the nodetree object and its children
185
- def _compute_joins_and_conditions_data(nodetree, model:)
194
+ def _compute_joins_and_conditions_data(nodetree, model:, parent_reflection:)
186
195
  h = {}
187
196
  conditions = []
188
197
  nodetree.children.each do |name, child|
189
- child_model = model.reflections[name.to_s].klass
190
- result = _compute_joins_and_conditions_data(child, model: child_model)
191
- h[name] = result[:associations_hash]
198
+ child_reflection = model.reflections[name.to_s]
199
+ result = _compute_joins_and_conditions_data(child, model: child_reflection.klass, parent_reflection: child_reflection)
200
+ h[name] = result[:associations_hash]
201
+
192
202
  conditions += result[:conditions]
193
203
  end
204
+
194
205
  column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
195
206
  nodetree.conditions.each do |condition|
196
- conditions += [condition.merge(column_prefix: column_prefix, model: model)]
207
+ # If it's a final ! or !! operation on an association from the parent, it means we need to add a condition
208
+ # on the existence (or lack of) of the whole associated table
209
+ ref = model.reflections[condition[:name].to_s]
210
+ if ref && ['!','!!'].include?(condition[:op])
211
+ cp = (nodetree.path + [condition[:name].to_s]).join(REFERENCES_STRING_SEPARATOR)
212
+ conditions += [condition.merge(column_prefix: cp, model: model, parent_reflection: ref)]
213
+ h[condition[:name]] = {}
214
+ else
215
+ # Save the parent reflection where the condition applies as well (used later to get assoc keys)
216
+ conditions += [condition.merge(column_prefix: column_prefix, model: model, parent_reflection: parent_reflection)]
217
+ end
218
+
197
219
  end
198
220
  {associations_hash: h, conditions: conditions}
199
221
  end
200
222
 
201
- def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:)
223
+ def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:, association_key_column:)
202
224
  likeval = get_like_value(value,fuzzy)
225
+
226
+ association_op = nil
203
227
  case op
204
228
  when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
205
229
  op = '!='
206
230
  value = nil # Enforce it is indeed nil (should be)
231
+ association_op = :not_null if association_key_column && !column_object
207
232
  when '!!'
208
233
  op = '='
209
234
  value = nil # Enforce it is indeed nil (should be)
235
+ association_op = :null if association_key_column && !column_object
236
+ end
237
+
238
+ if association_op
239
+ neg = association_op == :not_null ? true : false
240
+ qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: neg)
241
+ return query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: association_key_column)} #{qr}")
242
+ end
243
+
244
+ # Add an AND along with the condition, which ensures the left outter join 'exists' for it
245
+ # Normally this wouldn't be necessary as a condition on a given value mathing would imply the related row was there
246
+ # but this is not the case for NULL conditions, as the foreign column would match a NULL value, but not because the related column
247
+ # is NULL, but because the whole missing related row would appear with all fields null
248
+ # NOTE: we don't need to do it for conditions applying to the root of the tree (there isn't a join to it)
249
+ if association_key_column
250
+ qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: true)
251
+ query = query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: association_key_column)} #{qr}")
210
252
  end
211
253
 
212
254
  case op
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '2.0.pre.16'
2
+ VERSION = '2.0.pre.17'
3
3
  end
@@ -111,6 +111,48 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
111
111
  let(:filters_string) { 'tags.name=blue' }
112
112
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
113
113
  end
114
+
115
+ context 'by just an association filter condition' do
116
+ context 'for a belongs_to association with NO ROWS' do
117
+ let(:filters_string) { 'category!!'}
118
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:category)
119
+ end
120
+
121
+ context 'for a direct has_many association asking for missing rows' do
122
+ let(:filters_string) { 'primary_tags!!' }
123
+ it_behaves_like 'subject_equivalent_to',
124
+ ActiveBook.where.missing(:primary_tags)
125
+ end
126
+ context 'for a direct has_many association asking for non-missing rows' do
127
+ let(:filters_string) { 'primary_tags!' }
128
+ it_behaves_like 'subject_equivalent_to',
129
+ ActiveBook.left_outer_joins(:primary_tags).where.not('primary_tags.id' => nil)
130
+ end
131
+
132
+ context 'for a has_many through association with NO ROWS' do
133
+ let(:filters_string) { 'tags!!' }
134
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:tags)
135
+ end
136
+
137
+ context 'for a has_many through association with SOME ROWS' do
138
+ let(:filters_string) { 'tags!' }
139
+ it_behaves_like 'subject_equivalent_to', ActiveBook.left_outer_joins(:tags).where.not('tags.id' => nil)
140
+ end
141
+
142
+ context 'for a 3 levels deep has_many association with NO ROWS' do
143
+ let(:filters_string) { 'category.books.taggings!!' }
144
+ it_behaves_like 'subject_equivalent_to',
145
+ ActiveBook.left_outer_joins(category: { books: :taggings }).where('category.books.taggings.id' => nil)
146
+ end
147
+
148
+ context 'for a 3 levels deep has_many association WITH SIME ROWS' do
149
+ let(:filters_string) { 'category.books.taggings!' }
150
+ it_behaves_like 'subject_equivalent_to',
151
+ ActiveBook.left_outer_joins(category: { books: :taggings }).where.not('category.books.taggings.id' => nil)
152
+ end
153
+
154
+ end
155
+
114
156
  end
115
157
 
116
158
  # NOTE: apparently AR when conditions are build with strings in the where clauses (instead of names, etc)
@@ -120,77 +162,77 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
120
162
  PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
121
163
  COMMON_SQL_PREFIX = <<~SQL
122
164
  SELECT "active_books".* FROM "active_books"
123
- INNER JOIN
165
+ LEFT OUTER JOIN
124
166
  "active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
125
167
  SQL
126
168
  context '=' do
127
169
  let(:filters_string) { 'author.id=11'}
128
170
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
129
171
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
130
- WHERE ("#{PREF}/author"."id" = 11)
172
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" = 11)
131
173
  SQL
132
174
  end
133
175
  context '= (with array)' do
134
176
  let(:filters_string) { 'author.id=11,22'}
135
177
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
136
178
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
137
- WHERE ("#{PREF}/author"."id" IN (11,22))
179
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" IN (11,22))
138
180
  SQL
139
181
  end
140
182
  context '!=' do
141
183
  let(:filters_string) { 'author.id!=11'}
142
184
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
143
185
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
144
- WHERE ("#{PREF}/author"."id" <> 11)
186
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" <> 11)
145
187
  SQL
146
188
  end
147
189
  context '!= (with array)' do
148
190
  let(:filters_string) { 'author.id!=11,888'}
149
191
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
150
192
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
151
- WHERE ("#{PREF}/author"."id" NOT IN (11,888))
193
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" NOT IN (11,888))
152
194
  SQL
153
195
  end
154
196
  context '>' do
155
197
  let(:filters_string) { 'author.id>1'}
156
198
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
157
199
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
158
- WHERE ("#{PREF}/author"."id" > 1)
200
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" > 1)
159
201
  SQL
160
202
  end
161
203
  context '<' do
162
204
  let(:filters_string) { 'author.id<22'}
163
205
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
164
206
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
165
- WHERE ("#{PREF}/author"."id" < 22)
207
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" < 22)
166
208
  SQL
167
209
  end
168
210
  context '>=' do
169
211
  let(:filters_string) { 'author.id>=22'}
170
212
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
171
213
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
172
- WHERE ("#{PREF}/author"."id" >= 22)
214
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" >= 22)
173
215
  SQL
174
216
  end
175
217
  context '<=' do
176
218
  let(:filters_string) { 'author.id<=22'}
177
219
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
178
220
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
179
- WHERE ("#{PREF}/author"."id" <= 22)
221
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" <= 22)
180
222
  SQL
181
223
  end
182
224
  context '!' do
183
225
  let(:filters_string) { 'author.id!'}
184
226
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
185
227
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
186
- WHERE ("#{PREF}/author"."id" IS NOT NULL)
228
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" IS NOT NULL)
187
229
  SQL
188
230
  end
189
231
  context '!!' do
190
232
  let(:filters_string) { 'author.name!!'}
191
233
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
192
234
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
193
- WHERE ("#{PREF}/author"."name" IS NULL)
235
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" IS NULL)
194
236
  SQL
195
237
  end
196
238
  context 'including LIKE fuzzy queries' do
@@ -198,14 +240,14 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
198
240
  let(:filters_string) { 'author.name=author*'}
199
241
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
200
242
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
201
- WHERE ("#{PREF}/author"."name" LIKE 'author%')
243
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" LIKE 'author%')
202
244
  SQL
203
245
  end
204
246
  context 'NOT LIKE' do
205
247
  let(:filters_string) { 'author.name!=foobar*'}
206
248
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
207
249
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
208
- WHERE ("#{PREF}/author"."name" NOT LIKE 'foobar%')
250
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" NOT LIKE 'foobar%')
209
251
  SQL
210
252
  end
211
253
  end
@@ -227,10 +269,10 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
227
269
  ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
228
270
  it_behaves_like 'subject_matches_sql', <<~SQL
229
271
  SELECT "active_books".* FROM "active_books"
230
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
231
- INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
232
- INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
233
- WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
272
+ LEFT OUTER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
273
+ LEFT OUTER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
274
+ LEFT OUTER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
275
+ WHERE ("#{PREF}/category/books/taggings"."id" IS NOT NULL) AND ("#{PREF}/category/books/taggings"."tag_id" = 1)
234
276
  AND ("#{PREF}/category/books/taggings"."label" = 'primary')
235
277
  SQL
236
278
  end
@@ -260,14 +302,14 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
260
302
  .where('books_active_categories.simple_name': 'Book3')
261
303
  it_behaves_like 'subject_matches_sql', <<~SQL
262
304
  SELECT "active_books".* FROM "active_books"
263
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
264
- INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
305
+ LEFT OUTER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
306
+ LEFT OUTER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
265
307
  WHERE ("active_books"."simple_name" = 'Book1')
266
- AND ("#{PREF}/category/books"."simple_name" = 'Book3')
308
+ AND ("#{PREF}/category/books"."id" IS NOT NULL) AND ("#{PREF}/category/books"."simple_name" = 'Book3')
267
309
  SQL
268
310
  end
269
311
 
270
- context 'it qualifis them even if there are no joined tables/conditions at all' do
312
+ context 'it qualifies them even if there are no joined tables/conditions at all' do
271
313
  let(:filters_string) { 'id=11'}
272
314
  it_behaves_like 'subject_matches_sql', <<~SQL
273
315
  SELECT "active_books".* FROM "active_books"
@@ -310,20 +352,31 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
310
352
  .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
311
353
  it_behaves_like 'subject_matches_sql', <<~SQL
312
354
  SELECT "active_books".* FROM "active_books"
313
- INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
314
- WHERE ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
355
+ LEFT OUTER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
356
+ WHERE ("/taggings"."id" IS NOT NULL) AND ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
357
+ SQL
358
+ end
359
+
360
+ context 'works well with ORs at a parent table along with joined associations with no rows' do
361
+ let(:filters_string) { 'name=Book1005|category!!' }
362
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:category)
363
+ .or(ActiveBook.where.missing(:category).where(simple_name: "Book1005"))
364
+ it_behaves_like 'subject_matches_sql', <<~SQL
365
+ SELECT "active_books".* FROM "active_books"
366
+ LEFT OUTER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
367
+ WHERE ("active_books"."simple_name" = 'Book1005' OR "/category"."uuid" IS NULL)
315
368
  SQL
316
369
  end
317
370
 
318
371
  context '3-deep AND and OR conditions' do
319
372
  let(:filters_string) { '(category.name=cat2|(taggings.label=primary&tags.name=red))&category_uuid=deadbeef1' }
320
373
  it_behaves_like('subject_equivalent_to', Proc.new do
321
- base=ActiveBook.joins(:category,:taggings,:tags)
374
+ base=ActiveBook.left_outer_joins(:category,:taggings,:tags)
322
375
 
323
- and1_or1 = base.where('category.name': 'cat2')
376
+ and1_or1 = base.where('category.name': 'cat2').where.not('category.uuid': nil)
324
377
 
325
- and1_or2_and1 = base.where('taggings.label': 'primary')
326
- and1_or2_and2 = base.where('tags.name': 'red')
378
+ and1_or2_and1 = base.where('taggings.label': 'primary').where.not('taggings.id': nil)
379
+ and1_or2_and2 = base.where('tags.name': 'red').where.not('tags.id': nil)
327
380
  and1_or2 = and1_or2_and1.and(and1_or2_and2)
328
381
 
329
382
  and1 = and1_or1.or(and1_or2)
@@ -334,11 +387,16 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
334
387
 
335
388
  it_behaves_like 'subject_matches_sql', <<~SQL
336
389
  SELECT "active_books".* FROM "active_books"
337
- INNER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
338
- INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
339
- INNER JOIN "active_taggings" "taggings_active_books_join" ON "taggings_active_books_join"."book_id" = "active_books"."id"
340
- INNER JOIN "active_tags" "/tags" ON "/tags"."id" = "taggings_active_books_join"."tag_id"
341
- WHERE ("/category"."name" = 'cat2' OR ("/taggings"."label" = 'primary') AND ("/tags"."name" = 'red')) AND ("active_books"."category_uuid" = 'deadbeef1')
390
+ LEFT OUTER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
391
+ LEFT OUTER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
392
+ LEFT OUTER JOIN "active_tags" "/tags" ON "/tags"."id" = "/taggings"."tag_id"
393
+ WHERE (("/category"."uuid" IS NOT NULL)
394
+ AND ("/category"."name" = 'cat2')
395
+ OR ("/taggings"."id" IS NOT NULL)
396
+ AND ("/taggings"."label" = 'primary')
397
+ AND ("/tags"."id" IS NOT NULL)
398
+ AND ("/tags"."name" = 'red'))
399
+ AND ("active_books"."category_uuid" = 'deadbeef1')
342
400
  SQL
343
401
  end
344
402
  end
@@ -391,9 +449,9 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
391
449
  # This is slightly incorrect in AR 6.1+ (since the picked aliases for active_taggings tables vary)
392
450
  # it_behaves_like 'subject_matches_sql', <<~SQL
393
451
  # SELECT "active_books".* FROM "active_books"
394
- # INNER JOIN "active_taggings" ON "active_taggings"."label" = 'primary'
452
+ # LEFT OUTER JOIN "active_taggings" ON "active_taggings"."label" = 'primary'
395
453
  # AND "active_taggings"."book_id" = "active_books"."id"
396
- # INNER JOIN "active_tags" "/primary_tags" ON "/primary_tags"."id" = "active_taggings"."tag_id"
454
+ # LEFT OUTER JOIN "active_tags" "/primary_tags" ON "/primary_tags"."id" = "active_taggings"."tag_id"
397
455
  # WHERE ("/primary_tags"."name" = 'blue')
398
456
  # SQL
399
457
  end
@@ -120,6 +120,8 @@ class ActiveBookResource < ActiveBaseResource
120
120
  'category.books.name': 'category.books.simple_name',
121
121
  'category.books.taggings.tag_id': 'category.books.taggings.tag_id',
122
122
  'category.books.taggings.label': 'category.books.taggings.label',
123
+ 'primary_tags': 'primary_tags',
124
+ 'category.books.taggings': 'category.books.taggings',
123
125
  )
124
126
  # Forces to add an extra column (added_column)...and yet another (author_id) that will serve
125
127
  # to check that if that's already automatically added due to an association, it won't interfere or duplicate
data/spec/spec_helper.rb CHANGED
@@ -17,6 +17,7 @@ require 'simplecov'
17
17
  SimpleCov.start 'praxis'
18
18
 
19
19
  require 'pry'
20
+ require 'pry-byebug'
20
21
 
21
22
  require 'praxis'
22
23
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: praxis
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.pre.16
4
+ version: 2.0.pre.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-07-13 00:00:00.000000000 Z
12
+ date: 2021-08-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack