active_record_extended_telescope 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +870 -0
  3. data/lib/active_record_extended.rb +10 -0
  4. data/lib/active_record_extended/active_record.rb +25 -0
  5. data/lib/active_record_extended/active_record/relation_patch.rb +50 -0
  6. data/lib/active_record_extended/arel.rb +7 -0
  7. data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
  8. data/lib/active_record_extended/arel/nodes.rb +49 -0
  9. data/lib/active_record_extended/arel/predications.rb +50 -0
  10. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  11. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +122 -0
  12. data/lib/active_record_extended/patch/5_1/where_clause.rb +11 -0
  13. data/lib/active_record_extended/patch/5_2/where_clause.rb +11 -0
  14. data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +20 -0
  15. data/lib/active_record_extended/query_methods/any_of.rb +93 -0
  16. data/lib/active_record_extended/query_methods/either.rb +62 -0
  17. data/lib/active_record_extended/query_methods/inet.rb +88 -0
  18. data/lib/active_record_extended/query_methods/json.rb +329 -0
  19. data/lib/active_record_extended/query_methods/select.rb +118 -0
  20. data/lib/active_record_extended/query_methods/unionize.rb +249 -0
  21. data/lib/active_record_extended/query_methods/where_chain.rb +132 -0
  22. data/lib/active_record_extended/query_methods/window.rb +93 -0
  23. data/lib/active_record_extended/query_methods/with_cte.rb +150 -0
  24. data/lib/active_record_extended/utilities/order_by.rb +77 -0
  25. data/lib/active_record_extended/utilities/support.rb +178 -0
  26. data/lib/active_record_extended/version.rb +5 -0
  27. data/lib/active_record_extended_telescope.rb +4 -0
  28. data/spec/active_record_extended_spec.rb +7 -0
  29. data/spec/query_methods/any_of_spec.rb +131 -0
  30. data/spec/query_methods/array_query_spec.rb +64 -0
  31. data/spec/query_methods/either_spec.rb +59 -0
  32. data/spec/query_methods/hash_query_spec.rb +45 -0
  33. data/spec/query_methods/inet_query_spec.rb +112 -0
  34. data/spec/query_methods/json_spec.rb +157 -0
  35. data/spec/query_methods/select_spec.rb +115 -0
  36. data/spec/query_methods/unionize_spec.rb +165 -0
  37. data/spec/query_methods/window_spec.rb +51 -0
  38. data/spec/query_methods/with_cte_spec.rb +50 -0
  39. data/spec/spec_helper.rb +28 -0
  40. data/spec/sql_inspections/any_of_sql_spec.rb +41 -0
  41. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
  42. data/spec/sql_inspections/arel/array_spec.rb +63 -0
  43. data/spec/sql_inspections/arel/inet_spec.rb +66 -0
  44. data/spec/sql_inspections/contains_sql_queries_spec.rb +47 -0
  45. data/spec/sql_inspections/either_sql_spec.rb +55 -0
  46. data/spec/sql_inspections/json_sql_spec.rb +82 -0
  47. data/spec/sql_inspections/unionize_sql_spec.rb +124 -0
  48. data/spec/sql_inspections/window_sql_spec.rb +98 -0
  49. data/spec/sql_inspections/with_cte_sql_spec.rb +95 -0
  50. data/spec/support/database_cleaner.rb +15 -0
  51. data/spec/support/models.rb +68 -0
  52. metadata +245 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ar_outer_joins"
4
+
5
+ module ActiveRecordExtended
6
+ module QueryMethods
7
+ module Either
8
+ XOR_FIELD_SQL = "(CASE WHEN %<t1>s.%<c1>s IS NULL THEN %<t2>s.%<c2>s ELSE %<t1>s.%<c1>s END) "
9
+ XOR_FIELD_KEYS = [:t1, :c1, :t2, :c2].freeze
10
+
11
+ def either_join(initial_association, fallback_association)
12
+ associations = [initial_association, fallback_association]
13
+ association_options = xor_field_options_for_associations(associations)
14
+ condition__query = xor_field_sql(association_options) + "= #{table_name}.#{primary_key}"
15
+ outer_joins(associations).where(Arel.sql(condition__query))
16
+ end
17
+ alias either_joins either_join
18
+
19
+ def either_order(direction, **associations_and_columns)
20
+ reflected_columns = map_columns_to_tables(associations_and_columns)
21
+ conditional_query = xor_field_sql(reflected_columns) + sort_order_sql(direction)
22
+ outer_joins(associations_and_columns.keys).order(Arel.sql(conditional_query))
23
+ end
24
+ alias either_orders either_order
25
+
26
+ private
27
+
28
+ def xor_field_sql(options)
29
+ XOR_FIELD_SQL % Hash[xor_field_options(options)]
30
+ end
31
+
32
+ def sort_order_sql(dir)
33
+ ["asc", "desc"].include?(dir.to_s) ? dir.to_s : "asc"
34
+ end
35
+
36
+ def xor_field_options(options)
37
+ str_args = options.flatten.take(XOR_FIELD_KEYS.size).map(&:to_s)
38
+ Hash[XOR_FIELD_KEYS.zip(str_args)]
39
+ end
40
+
41
+ def map_columns_to_tables(associations_and_columns)
42
+ if associations_and_columns.respond_to?(:transform_keys)
43
+ associations_and_columns.transform_keys { |assc| reflect_on_association(assc).table_name }
44
+ else
45
+ associations_and_columns.each_with_object({}) do |(assc, value), key_table|
46
+ reflect_table = reflect_on_association(assc).table_name
47
+ key_table[reflect_table] = value
48
+ end
49
+ end
50
+ end
51
+
52
+ def xor_field_options_for_associations(associations)
53
+ associations.each_with_object({}) do |association_name, options|
54
+ reflection = reflect_on_association(association_name)
55
+ options[reflection.table_name] = reflection.foreign_key
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ ActiveRecord::Base.extend(ActiveRecordExtended::QueryMethods::Either)
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module QueryMethods
5
+ module Inet
6
+ # Finds matching inet column records that fall within a given submasked IP range
7
+ #
8
+ # Column(inet) << "127.0.0.1/24"
9
+ #
10
+ # User.where.inet_contained_within(ip: "127.0.0.1/16")
11
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" << '127.0.0.1/16'"
12
+ #
13
+ # User.where.inet_contained_within(ip: IPAddr.new("192.168.2.0/24"))
14
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" << '192.168.2.0/24'"
15
+ #
16
+ def inet_contained_within(opts, *rest)
17
+ substitute_comparisons(opts, rest, Arel::Nodes::Inet::ContainedWithin, "inet_contained_within")
18
+ end
19
+
20
+ # Finds matching inet column records that fall within a given submasked IP range and also finds records that also
21
+ # contain a submasking field that fall within range too.
22
+ #
23
+ # Column(inet) <<= "127.0.0.1/24"
24
+ #
25
+ # User.where.inet_contained_within_or_equals(ip: "127.0.0.1/16")
26
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" <<= '127.0.0.44/32'"
27
+ #
28
+ # User.where.inet_contained_within_or_equals(ip: IPAddr.new("192.168.2.0/24"))
29
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" <<= '192.168.2.0/24'"
30
+ #
31
+ def inet_contained_within_or_equals(opts, *rest)
32
+ substitute_comparisons(opts, rest, Arel::Nodes::Inet::ContainedWithinEquals, "inet_contained_within_or_equals")
33
+ end
34
+
35
+ # Finds records that contain a submask and the supplied IP address falls within its range.
36
+ #
37
+ # Column(inet) >>= "127.0.0.1/24"
38
+ #
39
+ # User.where.inet_contained_within_or_equals(ip: "127.0.255.255")
40
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >>= '127.0.255.255'"
41
+ #
42
+ # User.where.inet_contained_within_or_equals(ip: IPAddr.new("127.0.0.255"))
43
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >>= '127.0.0.255/32'"
44
+ #
45
+ def inet_contains_or_equals(opts, *rest)
46
+ substitute_comparisons(opts, rest, Arel::Nodes::Inet::ContainsEquals, "inet_contains_or_equals")
47
+ end
48
+
49
+ # Strictly finds records that contain a submask and the supplied IP address falls within its range.
50
+ #
51
+ # Column(inet) >>= "127.0.0.1"
52
+ #
53
+ # User.where.inet_contained_within_or_equals(ip: "127.0.255.255")
54
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >> '127.0.255.255'"
55
+ #
56
+ # User.where.inet_contained_within_or_equals(ip: IPAddr.new("127.0.0.255"))
57
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >> '127.0.0.255/32'"
58
+ #
59
+ def inet_contains(opts, *rest)
60
+ substitute_comparisons(opts, rest, Arel::Nodes::Inet::Contains, "inet_contains")
61
+ end
62
+
63
+ # This method is a combination of `inet_contains` and `inet_contained_within`
64
+ #
65
+ # Finds records that are contained within a given submask. And will also find records where their submask is also
66
+ # contains a given IP or IP submask.
67
+ #
68
+ # Column(inet) && "127.0.0.1/28"
69
+ #
70
+ # User.where.inet_contains_or_is_contained_by(ip: "127.0.255.255/28")
71
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" && '127.0.255.255/28'"
72
+ #
73
+ # User.where.inet_contains_or_is_contained_by(ip: IPAddr.new("127.0.0.255"))
74
+ # #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" && '127.0.0.255/32'"
75
+ #
76
+ def inet_contains_or_is_contained_within(opts, *rest)
77
+ substitute_comparisons(
78
+ opts,
79
+ rest,
80
+ Arel::Nodes::Inet::ContainsOrContainedWithin,
81
+ "inet_contains_or_is_contained_within"
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ ActiveRecord::QueryMethods::WhereChain.prepend(ActiveRecordExtended::QueryMethods::Inet)
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module QueryMethods
5
+ module Json
6
+ JSON_QUERY_METHODS = [
7
+ :select_row_to_json,
8
+ :json_build_object,
9
+ :jsonb_build_object,
10
+ :json_build_literal,
11
+ :jsonb_build_literal
12
+ ].freeze
13
+
14
+ class JsonChain
15
+ include ::ActiveRecordExtended::Utilities::Support
16
+ include ::ActiveRecordExtended::Utilities::OrderBy
17
+
18
+ DEFAULT_ALIAS = '"results"'
19
+ TO_JSONB_OPTIONS = [:array_agg, :distinct, :to_jsonb].to_set.freeze
20
+ ARRAY_OPTIONS = [:array, true].freeze
21
+
22
+ def initialize(scope)
23
+ @scope = scope
24
+ end
25
+
26
+ def row_to_json!(**args, &block)
27
+ options = json_object_options(args, except: [:values, :value])
28
+ build_row_to_json(**options, &block)
29
+ end
30
+
31
+ def json_build_object!(*args)
32
+ options = json_object_options(args, except: [:values, :cast_with, :order_by])
33
+ build_json_object(Arel::Nodes::JsonBuildObject, **options)
34
+ end
35
+
36
+ def jsonb_build_object!(*args)
37
+ options = json_object_options(args, except: [:values, :cast_with, :order_by])
38
+ build_json_object(Arel::Nodes::JsonbBuildObject, **options)
39
+ end
40
+
41
+ def json_build_literal!(*args)
42
+ options = json_object_options(args, only: [:values, :col_alias])
43
+ build_json_literal(Arel::Nodes::JsonBuildObject, **options)
44
+ end
45
+
46
+ def jsonb_build_literal!(*args)
47
+ options = json_object_options(args, only: [:values, :col_alias])
48
+ build_json_literal(Arel::Nodes::JsonbBuildObject, **options)
49
+ end
50
+
51
+ private
52
+
53
+ def build_json_literal(arel_klass, values:, col_alias: DEFAULT_ALIAS)
54
+ json_values = flatten_to_sql(values.to_a) { |value| literal_key(value) }
55
+ col_alias = double_quote(col_alias)
56
+ json_build_obj = arel_klass.new(json_values)
57
+ @scope.select(nested_alias_escape(json_build_obj, col_alias))
58
+ end
59
+
60
+ def build_json_object(arel_klass, from:, key: key_generator, value: nil, col_alias: DEFAULT_ALIAS)
61
+ tbl_alias = double_quote(key)
62
+ col_alias = double_quote(col_alias)
63
+ col_key = literal_key(key)
64
+ col_value = to_arel_sql(value.presence || tbl_alias)
65
+ json_build_object = arel_klass.new(to_sql_array(col_key, col_value))
66
+
67
+ unless /".+"/.match?(col_value)
68
+ warn("`#{col_value}`: the `value` argument should contain a double quoted key reference for safety")
69
+ end
70
+
71
+ @scope.select(nested_alias_escape(json_build_object, col_alias)).from(nested_alias_escape(from, tbl_alias))
72
+ end
73
+
74
+ def build_row_to_json(from:, **options, &block)
75
+ key = options[:key]
76
+ row_to_json = ::Arel::Nodes::RowToJson.new(double_quote(key))
77
+ row_to_json = ::Arel::Nodes::ToJsonb.new(row_to_json) if options.dig(:cast_with, :to_jsonb)
78
+
79
+ dummy_table = from_clause_constructor(from, key).select(row_to_json)
80
+ dummy_table = dummy_table.instance_eval(&block) if block
81
+ return dummy_table if options[:col_alias].blank?
82
+
83
+ query = wrap_row_to_json(dummy_table, options)
84
+ @scope.select(query)
85
+ end
86
+
87
+ def wrap_row_to_json(dummy_table, options)
88
+ cast_opts = options[:cast_with]
89
+ col_alias = options[:col_alias]
90
+ order_by = options[:order_by]
91
+
92
+ if cast_opts[:array_agg] || cast_opts[:distinct]
93
+ wrap_with_agg_array(dummy_table, col_alias, order_by: order_by, distinct: cast_opts[:distinct])
94
+ elsif cast_opts[:array]
95
+ wrap_with_array(dummy_table, col_alias, order_by: order_by)
96
+ else
97
+ nested_alias_escape(dummy_table, col_alias)
98
+ end
99
+ end
100
+
101
+ def json_object_options(args, except: [], only: []) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
102
+ options = {}
103
+ lean_opts = lambda do |key, &block|
104
+ if only.present?
105
+ options[key] ||= block.call if only.include?(key)
106
+ elsif !except.include?(key)
107
+ options[key] ||= block.call
108
+ end
109
+ end
110
+
111
+ flatten_safely(args) do |arg|
112
+ next if arg.nil?
113
+
114
+ if arg.is_a?(Hash)
115
+ lean_opts.call(:key) { arg.fetch(:key, key_generator) }
116
+ lean_opts.call(:value) { arg[:value].presence }
117
+ lean_opts.call(:col_alias) { arg[:as] }
118
+ lean_opts.call(:order_by) { order_by_expression(arg[:order_by]) }
119
+ lean_opts.call(:from) { arg[:from].tap { |from_clause| pipe_cte_with!(from_clause) } }
120
+ lean_opts.call(:cast_with) { casting_options(arg[:cast_with]) }
121
+ end
122
+
123
+ unless except.include?(:values)
124
+ options[:values] ||= []
125
+ options[:values] << (arg.respond_to?(:to_a) ? arg.to_a : arg)
126
+ end
127
+ end
128
+
129
+ options.tap(&:compact!)
130
+ end
131
+
132
+ def casting_options(cast_with)
133
+ return {} if cast_with.blank?
134
+
135
+ skip_convert = [Symbol, TrueClass, FalseClass]
136
+ Array(cast_with).compact.each_with_object({}) do |arg, options|
137
+ arg = arg.to_sym unless skip_convert.include?(arg.class)
138
+ options[:to_jsonb] |= TO_JSONB_OPTIONS.include?(arg)
139
+ options[:array] |= ARRAY_OPTIONS.include?(arg)
140
+ options[:array_agg] |= arg == :array_agg
141
+ options[:distinct] |= arg == :distinct
142
+ end
143
+ end
144
+ end
145
+
146
+ # Appends a select statement that contains a subquery that is converted to a json response
147
+ #
148
+ # Arguments:
149
+ # - from: [String, Arel, or ActiveRecord::Relation] A subquery that can be nested into a ROW_TO_JSON clause
150
+ #
151
+ # Options:
152
+ # - as: [Symbol or String] (default="results"): What the column will be aliased to
153
+ #
154
+ # - key: [Symbol or String] (default=[random letter]) What the row clause will be set as.
155
+ # - This is useful if you would like to add additional mid-level clauses (see mid-level scope example)
156
+ #
157
+ # - cast_with [Symbol or Array of symbols]: Actions to transform your query
158
+ # * :to_jsonb
159
+ # * :array
160
+ # * :array_agg (including just :array with this option will favor :array_agg)
161
+ # * :distinct (auto applies :array_agg & :to_jsonb)
162
+ #
163
+ # - order_by [Symbol or hash]: Applies an ordering operation (similar to ActiveRecord #order)
164
+ # - NOTE: this option will be ignored if you need to order a DISTINCT Aggregated Array,
165
+ # since postgres will thrown an error.
166
+ #
167
+ #
168
+ #
169
+ # Examples:
170
+ # subquery = Group.select(:name, :category_id).where("user_id = users.id")
171
+ # User.select(:name, email).select_row_to_json(subquery, as: :users_groups, cast_with: :array)
172
+ # #=> [<#User name:.., email:.., users_groups: [{ name: .., category_id: .. }, ..]]
173
+ #
174
+ # - Adding mid-level scopes:
175
+ #
176
+ # subquery = Group.select(:name, :category_id)
177
+ # User.select_row_to_json(subquery, key: :group, cast_with: :array) do |scope|
178
+ # scope.where(group: { name: "Nerd Core" })
179
+ # end
180
+ # #=> ```sql
181
+ # SELECT ARRAY(
182
+ # SELECT ROW_TO_JSON("group")
183
+ # FROM(SELECT name, category_id FROM groups) AS group
184
+ # WHERE group.name = 'Nerd Core'
185
+ # )
186
+ # ```
187
+ #
188
+ #
189
+ # - Array of JSONB objects
190
+ #
191
+ # subquery = Group.select(:name, :category_id)
192
+ # User.select_row_to_json(subquery, key: :group, cast_with: [:array, :to_jsonb]) do |scope|
193
+ # scope.where(group: { name: "Nerd Core" })
194
+ # end
195
+ # #=> ```sql
196
+ # SELECT ARRAY(
197
+ # SELECT TO_JSONB(ROW_TO_JSON("group"))
198
+ # FROM(SELECT name, category_id FROM groups) AS group
199
+ # WHERE group.name = 'Nerd Core'
200
+ # )
201
+ # ```
202
+ #
203
+ # - Distinct Aggregated Array
204
+ #
205
+ # subquery = Group.select(:name, :category_id)
206
+ # User.select_row_to_json(subquery, key: :group, cast_with: [:array_agg, :distinct]) do |scope|
207
+ # scope.where(group: { name: "Nerd Core" })
208
+ # end
209
+ # #=> ```sql
210
+ # SELECT ARRAY_AGG(DISTINCT (
211
+ # SELECT TO_JSONB(ROW_TO_JSON("group"))
212
+ # FROM(SELECT name, category_id FROM groups) AS group
213
+ # WHERE group.name = 'Nerd Core'
214
+ # ))
215
+ # ```
216
+ #
217
+ # - Ordering a Non-aggregated Array
218
+ #
219
+ # subquery = Group.select(:name, :category_id)
220
+ # User.select_row_to_json(subquery, key: :group, cast_with: :array, order_by: { group: { name: :desc } })
221
+ # #=> ```sql
222
+ # SELECT ARRAY(
223
+ # SELECT ROW_TO_JSON("group")
224
+ # FROM(SELECT name, category_id FROM groups) AS group
225
+ # ORDER BY group.name DESC
226
+ # )
227
+ # ```
228
+ #
229
+ # - Ordering an Aggregated Array
230
+ #
231
+ # Subquery = Group.select(:name, :category_id)
232
+ # User
233
+ # .joins(:people_groups)
234
+ # .select_row_to_json(
235
+ # subquery,
236
+ # key: :group,
237
+ # cast_with: :array_agg,
238
+ # order_by: { people_groups: :category_id }
239
+ # )
240
+ # #=> ```sql
241
+ # SELECT ARRAY_AGG((
242
+ # SELECT ROW_TO_JSON("group")
243
+ # FROM(SELECT name, category_id FROM groups) AS group
244
+ # ORDER BY group.name DESC
245
+ # ) ORDER BY people_groups.category_id ASC)
246
+ # ```
247
+ #
248
+ def select_row_to_json(from = nil, **options, &block)
249
+ from.is_a?(Hash) ? options.merge!(from) : options.reverse_merge!(from: from)
250
+ options.compact!
251
+ raise ArgumentError.new("Required to provide a non-nilled from clause") unless options.key?(:from)
252
+
253
+ JsonChain.new(spawn).row_to_json!(**options, &block)
254
+ end
255
+
256
+ # Creates a json response object that will convert all subquery results into a json compatible response
257
+ #
258
+ # Arguments:
259
+ # key: [Symbol or String]: What should this response return as
260
+ # from: [String, Arel, or ActiveRecord::Relation] : A subquery that can be nested into the top-level from clause
261
+ #
262
+ # Options:
263
+ # - as: [Symbol or String] (default="results"): What the column will be aliased to
264
+ #
265
+ #
266
+ # - value: [Symbol or String] (defaults=[key]): How the response should handel the json value return
267
+ #
268
+ # Example:
269
+ #
270
+ # - Generic example:
271
+ #
272
+ # subquery = Group.select(:name, :category_id).where("user_id = users.id")
273
+ # User.select(:name, email).select_row_to_json(subquery, as: :users_groups, cast_with: :array)
274
+ # #=> [<#User name:.., email:.., users_groups: [{ name: .., category_id: .. }, ..]]
275
+ #
276
+ # - Setting a custom value:
277
+ #
278
+ # Before:
279
+ # subquery = User.select(:name).where(id: 100..110).group(:name)
280
+ # User.build_json_object(:gang_members, subquery).take.results["gang_members"] #=> nil
281
+ #
282
+ # After:
283
+ # User.build_json_object(:gang_members, subquery, value: "COALESCE(array_agg(\"gang_members\"), 'BANG!')")
284
+ # .take
285
+ # .results["gang_members"] #=> "BANG!"
286
+ #
287
+ def json_build_object(key, from, **options)
288
+ options[:key] = key
289
+ options[:from] = from
290
+ JsonChain.new(spawn).json_build_object!(options)
291
+ end
292
+
293
+ def jsonb_build_object(key, from, **options)
294
+ options[:key] = key
295
+ options[:from] = from
296
+ JsonChain.new(spawn).jsonb_build_object!(options)
297
+ end
298
+
299
+ # Appends a hash literal to the calling relations response
300
+ #
301
+ # Arguments: Requires an Array or Hash set of values
302
+ #
303
+ # Options:
304
+ #
305
+ # - as: [Symbol or String] (default="results"): What the column will be aliased to
306
+ #
307
+ # Example:
308
+ # - Supplying inputs as a Hash
309
+ # query = User.json_build_literal(number: 1, last_name: "json", pi: 3.14)
310
+ # query.take.results #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
311
+ #
312
+ # - Supplying inputs as an Array
313
+ #
314
+ # query = User.json_build_literal(:number, 1, :last_name, "json", :pi, 3.14)
315
+ # query.take.results #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
316
+ #
317
+
318
+ def json_build_literal(*args)
319
+ JsonChain.new(spawn).json_build_literal!(args)
320
+ end
321
+
322
+ def jsonb_build_literal(*args)
323
+ JsonChain.new(spawn).jsonb_build_literal!(args)
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::Json)