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 +4 -4
- data/CHANGELOG.md +5 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +54 -12
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +92 -34
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +2 -0
- data/spec/spec_helper.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dafcffa2e0d146f5b601ed796cb482c5068482757fc2f88770783536da52ad7e
|
4
|
+
data.tar.gz: c6f8875641b0e418128dd8f8a923a3df639b1a409e65976472c24b6fac52f90e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
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
|
-
|
190
|
-
result = _compute_joins_and_conditions_data(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
|
-
|
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
|
data/lib/praxis/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
-
|
264
|
-
|
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
|
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
|
-
|
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.
|
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
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
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
|
-
#
|
452
|
+
# LEFT OUTER JOIN "active_taggings" ON "active_taggings"."label" = 'primary'
|
395
453
|
# AND "active_taggings"."book_id" = "active_books"."id"
|
396
|
-
#
|
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
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.
|
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-
|
12
|
+
date: 2021-08-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|