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

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.
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