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