praxis 2.0.pre.11 → 2.0.pre.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +22 -0
  4. data/bin/praxis +6 -0
  5. data/lib/praxis/api_definition.rb +8 -4
  6. data/lib/praxis/collection.rb +11 -0
  7. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  8. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  9. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +154 -63
  10. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  11. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +46 -43
  12. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
  13. data/lib/praxis/mapper/resource.rb +2 -2
  14. data/lib/praxis/media_type_identifier.rb +11 -1
  15. data/lib/praxis/response_definition.rb +46 -66
  16. data/lib/praxis/responses/http.rb +3 -1
  17. data/lib/praxis/tasks/routes.rb +6 -6
  18. data/lib/praxis/version.rb +1 -1
  19. data/spec/praxis/action_definition_spec.rb +3 -1
  20. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +259 -172
  21. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  22. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +117 -19
  23. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
  24. data/spec/praxis/mapper/resource_spec.rb +3 -3
  25. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  26. data/spec/praxis/response_definition_spec.rb +37 -129
  27. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  28. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
  29. data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
  30. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
  31. metadata +9 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7bbd374311046cf8d12c68d564382de6cbfd0c1032c45bbc8c1ac00d7a02e68a
4
- data.tar.gz: e33676f45266facdbcae595c2c05ca94ae8dfa34134aa9e4113e5a5d9d729052
3
+ metadata.gz: 2fe1ba3515514e34ad80775b6b68b4fe046fd1806c40d2345c4fa945d34ed92a
4
+ data.tar.gz: 11934130d0c527d1bc52020f393f2a5167ba3f88c876e81deaf7eaa05c518eff
5
5
  SHA512:
6
- metadata.gz: 5db86e95bd0b723560036435ded99b893fd92bd55f20dc4b49e326d96e7c9422549124ba986eb3248ff746d6bf928a60e6677b854c8761a34c026c7430a578bb
7
- data.tar.gz: 39677f252617f79d3d054fd2286562d3355310d8161096c7c73294d2b242cee4ebdf749b462d7f1c5261fc487e80ba473dcd258dcb77a510c47db7e698275cad
6
+ metadata.gz: a32c98bd77159e59c5192390c2eb683b62930cb3f4b30cbe680b63db5b0c076199b0fb63e5a0a55abb772a92cadef19a8f05d7cedf7eb8218f585ce8b7a01acf
7
+ data.tar.gz: 2a086210825ac166730d63b9ea7e5baa2420cdeb1772e434028b95de356ce69778d983fededb1125825f5b036bc0a0c13fc9b61536d77a31cc63cd87f6a48c02
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.3
1
+ 2.7.1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 2.0.pre.16
6
+
7
+ * 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.
8
+ * Fixed issue with Filtering Params, that occurred with using the ! or !! operators on String-typed fields.
9
+
10
+ ## 2.0.pre.14
11
+
12
+ * More encoding/decoding robustness for filters.
13
+ * Specs for how to encode filters are now properly defined by:
14
+ * The "value" of the filters query string needs to be URI encoded (like any other query string value). This encoding is subject to the normal rules, and therefore "could" leave some of the URI unreserved characters (i.e., 'markers') unencoded depending on the client (Section 2.2 of https://tools.ietf.org/html/rfc2396).
15
+ * The "values" for any of the conditions in the contents of the filters, however, will need to be properly "escaped" as well (prior to URL-encoding the whole syntax string itself like described above). This means that any match value needs to ensure that it has (at least) "(",")","|","&" and "," escaped as they are reserved characters for the filter expression syntax. For example, if I want to search for a name with value "Rocket&(Pants)", I need to first compose the syntax by: "name=<escaped Rocket&(Pants)>, which is "name=Rocket%26%28Pants%29" and then, just URI encode that query string value for the filters parameter in the URL like any other. For example: "filters=name%3DRocket%2526%2528Pants%2529"
16
+ * When using a multi-match (csv-separated) list of values, you need to escape each of the values as well, leaving the 'comma' unescape, as that's part of the syntax. Then uri-encode it all for the filters query string parameter value like above.
17
+ * Now, one can properly differentiate between fuzzy query prefix/postfix, and the literal data to search for (which can be or include '*'). Report that multi-matches (i.e., csv separated values for a single field, which translate into "IN" clauses) is not allowed if fuzzy matches are received (need to use multiple OR clauses for it).
18
+
19
+ ## 2.0.pre.13
20
+
21
+ * Fix filters parser regression, which would incorrectly decode url-encoded values
22
+
23
+ ## 2.0.pre.12
24
+
25
+ * Rebuilt API filters to support a much richer syntax. One can now use ANDs and ORs (with ANDs having order precedence), as well as group them with parenthesis. The same individual filter operands are supported. For example: 'email=*@gmail.com&(friends.first_name=Joe*,Patty|friends.last_name=Smith)
26
+
5
27
  ## 2.0.pre.11
6
28
 
7
29
  - Remove MapperPlugin's `set_selectors` (made `selector_generator` lazy instead), and ensure it includes the rendering extensions to the Controllers. Less things to configure if you opt into the Mapper way.
data/bin/praxis CHANGED
@@ -9,6 +9,12 @@ rescue Bundler::GemfileNotFound
9
9
  # no-op: we might be installed as a system gem
10
10
  end
11
11
 
12
+ if ARGV[0] == "version"
13
+ require 'praxis/version'
14
+ puts "Praxis version #{Praxis::VERSION}"
15
+ exit 0
16
+ end
17
+
12
18
  if ["routes","docs","console"].include? ARGV[0]
13
19
  require 'rake'
14
20
  require 'praxis'
@@ -97,8 +97,10 @@ module Praxis
97
97
  description( description || 'Standard response for successful HTTP requests.' )
98
98
 
99
99
  media_type media_type
100
- location location
101
- headers headers if headers
100
+ location if location
101
+ headers&.each do |(name, value)|
102
+ header(name: name, value: value)
103
+ end
102
104
  end
103
105
 
104
106
  api.response_template :created do |media_type: nil, location: nil, headers: nil, description: nil|
@@ -106,8 +108,10 @@ module Praxis
106
108
  description( description || 'The request has been fulfilled and resulted in a new resource being created.' )
107
109
 
108
110
  media_type media_type if media_type
109
- location location
110
- headers headers if headers
111
+ location if location
112
+ headers&.each do |(name, value)|
113
+ header(name: name, value: value)
114
+ end
111
115
  end
112
116
  end
113
117
 
@@ -32,5 +32,16 @@ module Praxis
32
32
  @member_type.domain_model
33
33
  end
34
34
 
35
+ def self.json_schema_type
36
+ :array
37
+ end
38
+
39
+ def self.as_json_schema(**args)
40
+ the_type = @attribute && @attribute.type || member_type
41
+ {
42
+ type: json_schema_type,
43
+ items: { '$ref': "#/components/schemas/#{the_type.id}" }
44
+ }
45
+ end
35
46
  end
36
47
  end
@@ -16,12 +16,27 @@ module Praxis
16
16
 
17
17
  def dump_response_headers_object( headers )
18
18
  headers.each_with_object({}) do |(name,data),accum|
19
- # data is a hash with :value and :type keys
20
- # How did we say in that must match a value in json schema again??
21
- accum[name] = {
22
- schema: SchemaObject.new(info: data[:type])
23
- # allowed values: [ data[:value] ] ??? is this the right json schema way?
24
- }
19
+ # each header comes from Praxis::ResponseDefinition
20
+ # the keys are the header names, and value can be:
21
+ # "true" => means it only needs to exist
22
+ # String => which means that it has to fully match
23
+ # Regex => which means it has to regexp match it
24
+
25
+ # Get the schema from the type (defaulting to string in case the type doesn't have the as_json_schema defined)
26
+ schema = data[:attribute].type.as_json_schema rescue { type: :string }
27
+ hash = { description: data[:description] || '', schema: schema }
28
+ # Note, our Headers in response definition are not full types...they're basically only
29
+ # strings, which can either match anything, match the exact word or match a regex
30
+ # they don't even have a description...
31
+ data_value = data[:value]
32
+ if data_value.is_a? String
33
+ hash[:pattern] = "^#{data_value}$" # Exact String match
34
+ elsif data_value.is_a? Regexp
35
+ sanitized_pattern = data_value.inspect[1..-2] #inspect returns enclosing '/' characters
36
+ hash[:pattern] = sanitized_pattern
37
+ end
38
+
39
+ accum[name] = hash
25
40
  end
26
41
  end
27
42
 
@@ -1,2 +1,15 @@
1
1
  require 'praxis/extensions/attribute_filtering/filtering_params'
2
- require 'praxis/extensions/attribute_filtering/filter_tree_node'
2
+ require 'praxis/extensions/attribute_filtering/filter_tree_node'
3
+ module Praxis
4
+ module Extensions
5
+ module AttributeFiltering
6
+ class MultiMatchWithFuzzyNotAllowedByAdapter < StandardError
7
+ def initialize
8
+ msg = 'Matching multiple, comma-separated values with fuzzy matches for a single field is not allowed by this DB adapter'\
9
+ 'Please use multiple OR clauses instead.'
10
+ super(msg)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -23,14 +23,17 @@ module Praxis
23
23
  end
24
24
 
25
25
  class ActiveRecordFilterQueryBuilder
26
- attr_reader :query, :model, :filters_map
26
+ REFERENCES_STRING_SEPARATOR = '/'
27
+ attr_reader :model, :filters_map
27
28
 
28
29
  # Base query to build upon
29
30
  def initialize(query: , model:, filters_map:, debug: false)
30
- @query = query
31
+ # Note: Do not make the initial_query an attr reader to make sure we don't count/leak on modifying it. Easier to mostly use class methods
32
+ @initial_query = query
31
33
  @model = model
32
34
  @filters_map = filters_map
33
35
  @logger = debug ? Logger.new(STDOUT) : nil
36
+ @active_record_version_maj = ActiveRecord.gem_version.segments[0]
34
37
  end
35
38
 
36
39
  def debug_query(msg, query)
@@ -40,26 +43,103 @@ module Praxis
40
43
  def generate(filters)
41
44
  # Resolve the names and values first, based on filters_map
42
45
  root_node = _convert_to_treenode(filters)
43
- craft_filter_query(root_node, for_model: @model)
44
- debug_query("SQL due to filters: ", @query.all)
45
- @query
46
+ crafted = craft_filter_query(root_node, for_model: @model)
47
+ debug_query("SQL due to filters: ", crafted.all)
48
+ crafted
46
49
  end
47
50
 
48
51
  def craft_filter_query(nodetree, for_model:)
49
52
  result = _compute_joins_and_conditions_data(nodetree, model: for_model)
50
- @query = query.joins(result[:associations_hash]) unless result[:associations_hash].empty?
53
+ return @initial_query if result[:conditions].empty?
51
54
 
52
- result[:conditions].each do |condition|
53
- filter_name = condition[:name]
54
- filter_value = condition[:value]
55
+
56
+ # Find the root group (usually an AND group) but can be an OR group, or nil if there's only 1 condition
57
+ root_parent_group = result[:conditions].first[:node_object].parent_group || result[:conditions].first[:node_object]
58
+ while root_parent_group.parent_group != nil
59
+ root_parent_group = root_parent_group.parent_group
60
+ end
61
+
62
+ # Process the joins
63
+ query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.joins(result[:associations_hash])
64
+
65
+ # Proc to apply a single condition
66
+ apply_single_condition = Proc.new do |condition, associated_query|
67
+ colo = condition[:model].columns_hash[condition[:name].to_s]
55
68
  column_prefix = condition[:column_prefix]
69
+
70
+ # Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
71
+ # 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
+ # unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
73
+ unless for_model.table_name == column_prefix
74
+ associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
75
+ end
76
+ self.class.add_clause(
77
+ query: associated_query,
78
+ column_prefix: column_prefix,
79
+ column_object: colo,
80
+ op: condition[:op],
81
+ value: condition[:value],
82
+ fuzzy: condition[:fuzzy]
83
+ )
84
+ end
56
85
 
57
- colo = condition[:model].columns_hash[filter_name.to_s]
58
- add_clause(column_prefix: column_prefix, column_object: colo, op: condition[:op], value: filter_value)
86
+ if @active_record_version_maj < 6
87
+ # ActiveRecord < 6 does not support '.and' so no nested things can be done
88
+ # But we can still support the case of 1+ flat conditions of the same AND/OR type
89
+ if root_parent_group.is_a?(FilteringParams::Condition)
90
+ # A Single condition it is easy to handle
91
+ apply_single_condition.call(result[:conditions].first, query_with_joins)
92
+ elsif root_parent_group.items.all?{|i| i.is_a?(FilteringParams::Condition)}
93
+ # Only 1 top level root, with only with simple condition items
94
+ if root_parent_group.type == :and
95
+ result[:conditions].reverse.inject(query_with_joins) do |accum, condition|
96
+ apply_single_condition.call(condition, accum)
97
+ end
98
+ else
99
+ # To do a flat OR, we need to apply the first condition to the incoming query
100
+ # and then apply any extra ORs to it. Otherwise Book.or(X).or(X) still matches all books
101
+ cond1, *rest = result[:conditions].reverse
102
+ start_query = apply_single_condition.call(cond1, query_with_joins)
103
+ rest.inject(start_query) do |accum, condition|
104
+ accum.or(apply_single_condition.call(condition, query_with_joins))
105
+ end
106
+ end
107
+ else
108
+ raise "Mixing AND and OR conditions is not supported for ActiveRecord <6."
109
+ end
110
+ else # ActiveRecord 6+
111
+ # Process the conditions in a depth-first order, and return the resulting query
112
+ _depth_first_traversal(
113
+ root_query: query_with_joins,
114
+ root_node: root_parent_group,
115
+ conditions: result[:conditions],
116
+ &apply_single_condition
117
+ )
59
118
  end
60
119
  end
61
120
 
62
121
  private
122
+ def _depth_first_traversal(root_query:, root_node:, conditions:, &block)
123
+ # Save the associated query for non-leaves
124
+ root_node.associated_query = root_query if root_node.is_a?(FilteringParams::ConditionGroup)
125
+
126
+ if root_node.is_a?(FilteringParams::Condition)
127
+ matching_condition = conditions.find {|cond| cond[:node_object] == root_node }
128
+
129
+ # The simplified case of a single top level condition (without a wrapping group)
130
+ # will need to pass the root query itself
131
+ associated_query = root_node.parent_group ? root_node.parent_group.associated_query : root_query
132
+ return yield matching_condition, associated_query
133
+ else
134
+ first_query, *rest_queries = root_node.items.map do |child|
135
+ _depth_first_traversal(root_query: root_query, root_node: child, conditions: conditions, &block)
136
+ end
137
+
138
+ rest_queries.each.inject(first_query) do |q, a_query|
139
+ root_node.type == :and ? q.and(a_query) : q.or(a_query)
140
+ end
141
+ end
142
+ end
63
143
 
64
144
  def _mapped_filter(name)
65
145
  target = @filters_map[name]
@@ -89,7 +169,9 @@ module Praxis
89
169
  if mapped_value.is_a?(Proc)
90
170
  result = mapped_value.call(filter)
91
171
  # Result could be an array of hashes (each hash has name/op/value to identify a condition)
92
- result.is_a?(Array) ? result : [result]
172
+ result_from_proc = result.is_a?(Array) ? result : [result]
173
+ # Make sure we tack on the node object associated with the filter
174
+ result_from_proc.map{|hash| hash.merge(node_object: filter[:node_object])}
93
175
  else
94
176
  # For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
95
177
  [filter.merge( name: mapped_value)]
@@ -109,59 +191,58 @@ module Praxis
109
191
  h[name] = result[:associations_hash]
110
192
  conditions += result[:conditions]
111
193
  end
112
- column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join('/')
113
- #column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? nil : nodetree.path.join('/')
194
+ column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
114
195
  nodetree.conditions.each do |condition|
115
196
  conditions += [condition.merge(column_prefix: column_prefix, model: model)]
116
197
  end
117
198
  {associations_hash: h, conditions: conditions}
118
199
  end
119
200
 
120
- def add_clause(column_prefix:, column_object:, op:, value:)
121
- @query = @query.references(build_reference_value(column_prefix)) #Mark where clause referencing the appropriate alias
122
- likeval = get_like_value(value)
201
+ def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:)
202
+ likeval = get_like_value(value,fuzzy)
123
203
  case op
124
- when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
125
- op = '!='
126
- value = nil # Enforce it is indeed nil (should be)
127
- when '!!'
128
- op = '='
129
- value = nil # Enforce it is indeed nil (should be)
204
+ when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
205
+ op = '!='
206
+ value = nil # Enforce it is indeed nil (should be)
207
+ when '!!'
208
+ op = '='
209
+ value = nil # Enforce it is indeed nil (should be)
210
+ end
211
+
212
+ case op
213
+ when '='
214
+ if likeval
215
+ add_safe_where(query: query, tab: column_prefix, col: column_object, op: 'LIKE', value: likeval)
216
+ else
217
+ quoted_right = quote_right_part(query: query, value: value, column_object: column_object, negative: false)
218
+ query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: column_object)} #{quoted_right}")
219
+ end
220
+ when '!='
221
+ if likeval
222
+ add_safe_where(query: query, tab: column_prefix, col: column_object, op: 'NOT LIKE', value: likeval)
223
+ else
224
+ quoted_right = quote_right_part(query: query, value: value, column_object: column_object, negative: true)
225
+ query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: column_object)} #{quoted_right}")
130
226
  end
131
- @query = case op
132
- when '='
133
- if likeval
134
- add_safe_where(tab: column_prefix, col: column_object, op: 'LIKE', value: likeval)
135
- else
136
- quoted_right = quote_right_part(value: value, column_object: column_object, negative: false)
137
- query.where("#{quote_column_path(column_prefix, column_object)} #{quoted_right}")
138
- end
139
- when '!='
140
- if likeval
141
- add_safe_where(tab: column_prefix, col: column_object, op: 'NOT LIKE', value: likeval)
142
- else
143
- quoted_right = quote_right_part(value: value, column_object: column_object, negative: true)
144
- query.where("#{quote_column_path(column_prefix, column_object)} #{quoted_right}")
145
- end
146
- when '>'
147
- add_safe_where(tab: column_prefix, col: column_object, op: '>', value: value)
148
- when '<'
149
- add_safe_where(tab: column_prefix, col: column_object, op: '<', value: value)
150
- when '>='
151
- add_safe_where(tab: column_prefix, col: column_object, op: '>=', value: value)
152
- when '<='
153
- add_safe_where(tab: column_prefix, col: column_object, op: '<=', value: value)
154
- else
155
- raise "Unsupported Operator!!! #{op}"
156
- end
157
- end
158
-
159
- def add_safe_where(tab:, col:, op:, value:)
227
+ when '>'
228
+ add_safe_where(query: query, tab: column_prefix, col: column_object, op: '>', value: value)
229
+ when '<'
230
+ add_safe_where(query: query, tab: column_prefix, col: column_object, op: '<', value: value)
231
+ when '>='
232
+ add_safe_where(query: query, tab: column_prefix, col: column_object, op: '>=', value: value)
233
+ when '<='
234
+ add_safe_where(query: query, tab: column_prefix, col: column_object, op: '<=', value: value)
235
+ else
236
+ raise "Unsupported Operator!!! #{op}"
237
+ end
238
+ end
239
+
240
+ def self.add_safe_where(query:, tab:, col:, op:, value:)
160
241
  quoted_value = query.connection.quote_default_expression(value,col)
161
- query.where("#{quote_column_path(tab, col)} #{op} #{quoted_value}")
242
+ query.where("#{self.quote_column_path(query: query, prefix: tab, column_object: col)} #{op} #{quoted_value}")
162
243
  end
163
244
 
164
- def quote_column_path(prefix, column_object)
245
+ def self.quote_column_path(query:, prefix:, column_object:)
165
246
  c = query.connection
166
247
  quoted_column = c.quote_column_name(column_object.name)
167
248
  if prefix
@@ -172,7 +253,7 @@ module Praxis
172
253
  end
173
254
  end
174
255
 
175
- def quote_right_part(value:, column_object:, negative:)
256
+ def self.quote_right_part(query:, value:, column_object:, negative:)
176
257
  conn = query.connection
177
258
  if value.nil?
178
259
  no = negative ? ' NOT' : ''
@@ -190,12 +271,22 @@ module Praxis
190
271
  end
191
272
 
192
273
  # Returns nil if the value was not a fuzzzy pattern
193
- def get_like_value(value)
194
- if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
195
- likeval = value.dup
196
- likeval[-1] = '%' if value[-1] == '*'
197
- likeval[0] = '%' if value[0] == '*'
198
- likeval
274
+ def self.get_like_value(value,fuzzy)
275
+ is_fuzzy = fuzzy.is_a?(Array) ? !fuzzy.compact.empty? : fuzzy
276
+ if is_fuzzy
277
+ unless value.is_a?(String)
278
+ raise MultiMatchWithFuzzyNotAllowedByAdapter.new
279
+ end
280
+ case fuzzy
281
+ when :start_end
282
+ '%'+value+'%'
283
+ when :start
284
+ '%'+value
285
+ when :end
286
+ value+'%'
287
+ end
288
+ else
289
+ nil
199
290
  end
200
291
  end
201
292
 
@@ -203,14 +294,14 @@ module Praxis
203
294
  maj, min, _ = ActiveRecord.gem_version.segments
204
295
  if maj == 5 || (maj == 6 && min == 0)
205
296
  # In AR 6 (and 6.0) the references are simple strings
206
- def build_reference_value(column_prefix)
297
+ def build_reference_value(column_prefix, query: nil)
207
298
  column_prefix
208
299
  end
209
300
  else
210
301
  # The latest AR versions discard passing references to joins when they're not SqlLiterals ... so let's wrap it
211
302
  # with our class, so that it is a literal (already quoted), but that can still provide the expected "symbol" without quotes
212
303
  # so that our aliasing code can match it.
213
- def build_reference_value(column_prefix)
304
+ def build_reference_value(column_prefix, query:)
214
305
  QuasiSqlLiteral.new(quoted: query.connection.quote_table_name(column_prefix), symbolized: column_prefix.to_sym)
215
306
  end
216
307
  end