active_record_extended_telescope 2.0.1
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 +7 -0
- data/README.md +870 -0
- data/lib/active_record_extended.rb +10 -0
- data/lib/active_record_extended/active_record.rb +25 -0
- data/lib/active_record_extended/active_record/relation_patch.rb +50 -0
- data/lib/active_record_extended/arel.rb +7 -0
- data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
- data/lib/active_record_extended/arel/nodes.rb +49 -0
- data/lib/active_record_extended/arel/predications.rb +50 -0
- data/lib/active_record_extended/arel/sql_literal.rb +16 -0
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +122 -0
- data/lib/active_record_extended/patch/5_1/where_clause.rb +11 -0
- data/lib/active_record_extended/patch/5_2/where_clause.rb +11 -0
- data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +20 -0
- data/lib/active_record_extended/query_methods/any_of.rb +93 -0
- data/lib/active_record_extended/query_methods/either.rb +62 -0
- data/lib/active_record_extended/query_methods/inet.rb +88 -0
- data/lib/active_record_extended/query_methods/json.rb +329 -0
- data/lib/active_record_extended/query_methods/select.rb +118 -0
- data/lib/active_record_extended/query_methods/unionize.rb +249 -0
- data/lib/active_record_extended/query_methods/where_chain.rb +132 -0
- data/lib/active_record_extended/query_methods/window.rb +93 -0
- data/lib/active_record_extended/query_methods/with_cte.rb +150 -0
- data/lib/active_record_extended/utilities/order_by.rb +77 -0
- data/lib/active_record_extended/utilities/support.rb +178 -0
- data/lib/active_record_extended/version.rb +5 -0
- data/lib/active_record_extended_telescope.rb +4 -0
- data/spec/active_record_extended_spec.rb +7 -0
- data/spec/query_methods/any_of_spec.rb +131 -0
- data/spec/query_methods/array_query_spec.rb +64 -0
- data/spec/query_methods/either_spec.rb +59 -0
- data/spec/query_methods/hash_query_spec.rb +45 -0
- data/spec/query_methods/inet_query_spec.rb +112 -0
- data/spec/query_methods/json_spec.rb +157 -0
- data/spec/query_methods/select_spec.rb +115 -0
- data/spec/query_methods/unionize_spec.rb +165 -0
- data/spec/query_methods/window_spec.rb +51 -0
- data/spec/query_methods/with_cte_spec.rb +50 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/sql_inspections/any_of_sql_spec.rb +41 -0
- data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
- data/spec/sql_inspections/arel/array_spec.rb +63 -0
- data/spec/sql_inspections/arel/inet_spec.rb +66 -0
- data/spec/sql_inspections/contains_sql_queries_spec.rb +47 -0
- data/spec/sql_inspections/either_sql_spec.rb +55 -0
- data/spec/sql_inspections/json_sql_spec.rb +82 -0
- data/spec/sql_inspections/unionize_sql_spec.rb +124 -0
- data/spec/sql_inspections/window_sql_spec.rb +98 -0
- data/spec/sql_inspections/with_cte_sql_spec.rb +95 -0
- data/spec/support/database_cleaner.rb +15 -0
- data/spec/support/models.rb +68 -0
- metadata +245 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module QueryMethods
|
5
|
+
module Window
|
6
|
+
class DefineWindowChain
|
7
|
+
include ::ActiveRecordExtended::Utilities::Support
|
8
|
+
include ::ActiveRecordExtended::Utilities::OrderBy
|
9
|
+
|
10
|
+
def initialize(scope, window_name)
|
11
|
+
@scope = scope
|
12
|
+
@window_name = window_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def partition_by(*partitions, order_by: nil)
|
16
|
+
@scope.window_values! << {
|
17
|
+
window_name: to_arel_sql(@window_name),
|
18
|
+
partition_by: flatten_to_sql(partitions),
|
19
|
+
order_by: order_by_expression(order_by)
|
20
|
+
}
|
21
|
+
|
22
|
+
@scope
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class WindowSelectBuilder
|
27
|
+
include ::ActiveRecordExtended::Utilities::Support
|
28
|
+
|
29
|
+
def initialize(window_function, args, window_name)
|
30
|
+
@window_function = window_function
|
31
|
+
@win_args = to_sql_array(args)
|
32
|
+
@over = to_arel_sql(window_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_select(alias_name = nil)
|
36
|
+
window_arel = generate_named_function(@window_function, *@win_args).over(@over)
|
37
|
+
|
38
|
+
if alias_name.nil?
|
39
|
+
window_arel
|
40
|
+
else
|
41
|
+
nested_alias_escape(window_arel, alias_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def window_values
|
47
|
+
@values.fetch(:window, [])
|
48
|
+
end
|
49
|
+
|
50
|
+
def window_values!
|
51
|
+
@values[:window] ||= []
|
52
|
+
end
|
53
|
+
|
54
|
+
def window_values?
|
55
|
+
!window_values.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def window_values=(*values)
|
59
|
+
@values[:window] = values.flatten(1)
|
60
|
+
end
|
61
|
+
|
62
|
+
def define_window(name)
|
63
|
+
spawn.define_window!(name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def define_window!(name)
|
67
|
+
DefineWindowChain.new(self, name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def select_window(window_function, *args, over:, as: nil)
|
71
|
+
spawn.select_window!(window_function, args, over: over, as: as)
|
72
|
+
end
|
73
|
+
|
74
|
+
def select_window!(window_function, *args, over:, as: nil)
|
75
|
+
args.flatten!
|
76
|
+
args.compact!
|
77
|
+
|
78
|
+
select_statement = WindowSelectBuilder.new(window_function, args, over).build_select(as)
|
79
|
+
_select!(select_statement)
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_windows(arel)
|
83
|
+
window_values.each do |window_value|
|
84
|
+
window = arel.window(window_value[:window_name])
|
85
|
+
window = window.partition(window_value[:partition_by]) if window_value[:partition_by].present?
|
86
|
+
window.order(window_value[:order_by]) if window_value[:order_by]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::Window)
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module QueryMethods
|
5
|
+
module WithCTE
|
6
|
+
class WithCTE
|
7
|
+
include ::ActiveRecordExtended::Utilities::Support
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :@with_values, :empty?, :blank?, :present?
|
12
|
+
attr_reader :with_values, :with_keys
|
13
|
+
|
14
|
+
# @param [ActiveRecord::Relation] scope
|
15
|
+
def initialize(scope)
|
16
|
+
@scope = scope
|
17
|
+
reset!
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Enumerable] Returns the order for which CTE's were imported as.
|
21
|
+
def each
|
22
|
+
return to_enum(:each) unless block_given?
|
23
|
+
|
24
|
+
with_keys.each do |key|
|
25
|
+
yield(key, with_values[key])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
alias each_pair each
|
29
|
+
|
30
|
+
# @param [Hash, WithCTE] value
|
31
|
+
def with_values=(value)
|
32
|
+
reset!
|
33
|
+
pipe_cte_with!(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [Hash, WithCTE] value
|
37
|
+
def pipe_cte_with!(value)
|
38
|
+
return if value.nil? || value.empty?
|
39
|
+
|
40
|
+
value.each_pair do |name, expression|
|
41
|
+
sym_name = name.to_sym
|
42
|
+
next if with_values.key?(sym_name)
|
43
|
+
|
44
|
+
# Ensure we follow FIFO pattern.
|
45
|
+
# If the parent has similar CTE alias keys, we want to favor the parent's expressions over its children's.
|
46
|
+
if expression.is_a?(ActiveRecord::Relation) && expression.with_values?
|
47
|
+
pipe_cte_with!(expression.cte)
|
48
|
+
expression.cte.reset!
|
49
|
+
end
|
50
|
+
|
51
|
+
@with_keys |= [sym_name]
|
52
|
+
@with_values[sym_name] = expression
|
53
|
+
end
|
54
|
+
|
55
|
+
value.reset! if value.is_a?(WithCTE)
|
56
|
+
end
|
57
|
+
|
58
|
+
def reset!
|
59
|
+
@with_keys = []
|
60
|
+
@with_values = {}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class WithChain
|
65
|
+
# @param [ActiveRecord::Relation] scope
|
66
|
+
def initialize(scope)
|
67
|
+
@scope = scope
|
68
|
+
@scope.cte ||= WithCTE.new(scope)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param [Hash, WithCTE] args
|
72
|
+
def recursive(args)
|
73
|
+
@scope.tap do |scope|
|
74
|
+
scope.recursive_value = true
|
75
|
+
scope.cte.pipe_cte_with!(args)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [WithCTE]
|
81
|
+
def cte
|
82
|
+
@values[:cte]
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [WithCTE] cte
|
86
|
+
def cte=(cte)
|
87
|
+
raise TypeError.new("Must be a WithCTE class type") unless cte.is_a?(WithCTE)
|
88
|
+
|
89
|
+
@values[:cte] = cte
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Boolean]
|
93
|
+
def with_values?
|
94
|
+
!(cte.nil? || cte.empty?)
|
95
|
+
end
|
96
|
+
|
97
|
+
# @param [Hash, WithCTE] values
|
98
|
+
def with_values=(values)
|
99
|
+
cte.with_values = values
|
100
|
+
end
|
101
|
+
|
102
|
+
# @param [Boolean] value
|
103
|
+
def recursive_value=(value)
|
104
|
+
raise ImmutableRelation if @loaded
|
105
|
+
|
106
|
+
@values[:recursive] = value
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Boolean]
|
110
|
+
def recursive_value?
|
111
|
+
!(!@values[:recursive])
|
112
|
+
end
|
113
|
+
|
114
|
+
# @param [Hash, WithCTE] opts
|
115
|
+
def with(opts = :chain, *rest)
|
116
|
+
return WithChain.new(spawn) if opts == :chain
|
117
|
+
|
118
|
+
opts.blank? ? self : spawn.with!(opts, *rest)
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param [Hash, WithCTE] opts
|
122
|
+
def with!(opts = :chain, *_rest)
|
123
|
+
return WithChain.new(self) if opts == :chain
|
124
|
+
|
125
|
+
tap do |scope|
|
126
|
+
scope.cte ||= WithCTE.new(self)
|
127
|
+
scope.cte.pipe_cte_with!(opts)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def build_with(arel)
|
132
|
+
return unless with_values?
|
133
|
+
|
134
|
+
cte_statements = cte.map do |name, expression|
|
135
|
+
grouped_expression = cte.generate_grouping(expression)
|
136
|
+
cte_name = cte.to_arel_sql(cte.double_quote(name.to_s))
|
137
|
+
Arel::Nodes::As.new(cte_name, grouped_expression)
|
138
|
+
end
|
139
|
+
|
140
|
+
if recursive_value?
|
141
|
+
arel.with(:recursive, cte_statements)
|
142
|
+
else
|
143
|
+
arel.with(cte_statements)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::WithCTE)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
module ActiveRecordExtended
|
6
|
+
module Utilities
|
7
|
+
module OrderBy
|
8
|
+
def inline_order_by(arel_node, ordering_args)
|
9
|
+
return arel_node unless scope_preprocess_order_args(ordering_args)
|
10
|
+
|
11
|
+
Arel::Nodes::InfixOperation.new("ORDER BY", arel_node, ordering_args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def scope_preprocess_order_args(ordering_args)
|
15
|
+
return false if ordering_args.blank? || !@scope.respond_to?(:preprocess_order_args, true)
|
16
|
+
|
17
|
+
# Sanitation check / resolver (ActiveRecord::Relation#preprocess_order_args)
|
18
|
+
@scope.send(:preprocess_order_args, ordering_args)
|
19
|
+
ordering_args
|
20
|
+
end
|
21
|
+
|
22
|
+
# Processes "ORDER BY" expressions for supported aggregate functions
|
23
|
+
def order_by_expression(order_by)
|
24
|
+
return false unless order_by && order_by.presence.present?
|
25
|
+
|
26
|
+
to_ordered_table_path(order_by)
|
27
|
+
.tap { |order_args| process_ordering_arguments!(order_args) }
|
28
|
+
.tap { |order_args| scope_preprocess_order_args(order_args) }
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Turns a hash into a dot notation path.
|
33
|
+
#
|
34
|
+
# Example:
|
35
|
+
# - Using pre-set directions:
|
36
|
+
# [{ products: { position: :asc, id: :desc } }]
|
37
|
+
# #=> [{ "products.position" => :asc, "products.id" => :desc }]
|
38
|
+
#
|
39
|
+
# - Using fallback directions:
|
40
|
+
# [{products: :position}]
|
41
|
+
# #=> [{"products.position" => :asc}]
|
42
|
+
#
|
43
|
+
def to_ordered_table_path(args)
|
44
|
+
flatten_safely(Array.wrap(args)) do |arg|
|
45
|
+
next arg unless arg.is_a?(Hash)
|
46
|
+
|
47
|
+
arg.each_with_object({}) do |(tbl_or_col, obj), new_hash|
|
48
|
+
if obj.is_a?(Hash)
|
49
|
+
obj.each_pair do |o_key, o_value|
|
50
|
+
new_hash["#{tbl_or_col}.#{o_key}"] = o_value
|
51
|
+
end
|
52
|
+
elsif ::ActiveRecord::QueryMethods::VALID_DIRECTIONS.include?(obj)
|
53
|
+
new_hash[tbl_or_col] = obj
|
54
|
+
elsif obj.nil?
|
55
|
+
new_hash[tbl_or_col.to_s] = :asc
|
56
|
+
else
|
57
|
+
new_hash["#{tbl_or_col}.#{obj}"] = :asc
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def process_ordering_arguments!(ordering_args)
|
64
|
+
ordering_args.flatten!
|
65
|
+
ordering_args.compact!
|
66
|
+
ordering_args.map! do |arg|
|
67
|
+
next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol
|
68
|
+
|
69
|
+
arg.each_with_object({}) do |(field, dir), ordering_obj|
|
70
|
+
# ActiveRecord will not reflect if the Hash keys are a `Arel::Nodes::SqlLiteral` klass
|
71
|
+
ordering_obj[to_arel_sql(field)] = dir.to_s.downcase
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module Utilities
|
5
|
+
module Support
|
6
|
+
A_TO_Z_KEYS = ("a".."z").to_a.freeze
|
7
|
+
|
8
|
+
# We need to ensure we can flatten nested ActiveRecord::Relations
|
9
|
+
# that might have been nested due to the (splat)*args parameters
|
10
|
+
#
|
11
|
+
# Note: calling `Array.flatten[!]/1` will actually remove all AR relations from the array.
|
12
|
+
#
|
13
|
+
def flatten_to_sql(*values)
|
14
|
+
flatten_safely(values) do |value|
|
15
|
+
value = yield value if block_given?
|
16
|
+
to_arel_sql(value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
alias to_sql_array flatten_to_sql
|
20
|
+
|
21
|
+
def flatten_safely(values, &block)
|
22
|
+
unless values.is_a?(Array)
|
23
|
+
values = yield values if block
|
24
|
+
return [values]
|
25
|
+
end
|
26
|
+
|
27
|
+
values.map { |value| flatten_safely(value, &block) }.reduce(:+)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Applies aliases to the given query
|
31
|
+
# Ex: `SELECT * FROM users` => `(SELECT * FROM users) AS "members"`
|
32
|
+
def nested_alias_escape(query, alias_name)
|
33
|
+
sql_query = generate_grouping(query)
|
34
|
+
Arel::Nodes::As.new(sql_query, to_arel_sql(double_quote(alias_name)))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Wraps subquery into an Aliased ARRAY
|
38
|
+
# Ex: `SELECT * FROM users` => (ARRAY(SELECT * FROM users)) AS "members"
|
39
|
+
def wrap_with_array(arel_or_rel_query, alias_name, order_by: false)
|
40
|
+
if order_by && arel_or_rel_query.is_a?(ActiveRecord::Relation)
|
41
|
+
arel_or_rel_query = arel_or_rel_query.order(order_by)
|
42
|
+
end
|
43
|
+
|
44
|
+
query = Arel::Nodes::Array.new(to_sql_array(arel_or_rel_query))
|
45
|
+
nested_alias_escape(query, alias_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Wraps query into an aggregated array
|
49
|
+
# EX: `(ARRAY_AGG((SELECT * FROM users)) AS "members"`
|
50
|
+
# `(ARRAY_AGG(DISTINCT (SELECT * FROM users)) AS "members"`
|
51
|
+
# `SELECT ARRAY_AGG((id)) AS "ids" FROM users`
|
52
|
+
# `SELECT ARRAY_AGG(DISTINCT (id)) AS "ids" FROM users`
|
53
|
+
def wrap_with_agg_array(arel_or_rel_query, alias_name, order_by: false, distinct: false)
|
54
|
+
distinct = !(!distinct)
|
55
|
+
order_exp = distinct ? nil : order_by # Can't order a distinct agg
|
56
|
+
query = group_when_needed(arel_or_rel_query)
|
57
|
+
query =
|
58
|
+
Arel::Nodes::AggregateFunctionName
|
59
|
+
.new("ARRAY_AGG", to_sql_array(query), distinct)
|
60
|
+
.order_by(order_exp)
|
61
|
+
|
62
|
+
nested_alias_escape(query, alias_name)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Will attempt to digest and resolve the from clause
|
66
|
+
#
|
67
|
+
# If the from clause is a String, it will check to see if a table reference key has been assigned.
|
68
|
+
# - If one cannot be detected, one will be appended.
|
69
|
+
# - Rails does not allow assigning table references using the `.from/2` method, when its a string / sym type.
|
70
|
+
#
|
71
|
+
# If the from clause is an AR relation; it will duplicate the object.
|
72
|
+
# - Ensures any memorizers are reset (ex: `.to_sql` sets a memorizer on the instance)
|
73
|
+
# - Key's can be assigned using the `.from/2` method.
|
74
|
+
#
|
75
|
+
def from_clause_constructor(from, reference_key)
|
76
|
+
case from
|
77
|
+
when /\s.?#{reference_key}.?$/ # The from clause is a string and has the tbl reference key
|
78
|
+
@scope.unscoped.from(from)
|
79
|
+
when String, Symbol
|
80
|
+
@scope.unscoped.from("#{from} #{reference_key}")
|
81
|
+
else
|
82
|
+
replicate_klass = from.respond_to?(:unscoped) ? from.unscoped : @scope.unscoped
|
83
|
+
replicate_klass.from(from.dup, reference_key)
|
84
|
+
end.unscope(:where)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Will carry defined CTE tables from the nested sub-query and gradually pushes it up to the parents query stack
|
88
|
+
# I.E: It pushes `WITH [:cte_name:] AS(...), ..` to the top of the query structure tree
|
89
|
+
#
|
90
|
+
# SPECIAL GOTCHA NOTE: (if duplicate keys are found) This will favor the parents query `with's` over nested ones!
|
91
|
+
def pipe_cte_with!(subquery)
|
92
|
+
return self unless subquery.try(:with_values?)
|
93
|
+
|
94
|
+
# Add subquery CTE's to the parents query stack. (READ THE SPECIAL NOTE ABOVE!)
|
95
|
+
if @scope.with_values?
|
96
|
+
@scope.cte.pipe_cte_with!(subquery.cte)
|
97
|
+
else
|
98
|
+
# Top level has no with values
|
99
|
+
@scope.with!(subquery.cte)
|
100
|
+
end
|
101
|
+
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
# Ensures the given value is properly double quoted.
|
106
|
+
# This also ensures we don't have conflicts with reversed keywords.
|
107
|
+
#
|
108
|
+
# IE: `user` is a reserved keyword in PG. But `"user"` is allowed and works the same
|
109
|
+
# when used as an column/tbl alias.
|
110
|
+
def double_quote(value)
|
111
|
+
return if value.nil?
|
112
|
+
|
113
|
+
case value.to_s
|
114
|
+
# Ignore keys that contain double quotes or a Arel.star (*)[all columns]
|
115
|
+
# or if a table has already been explicitly declared (ex: users.id)
|
116
|
+
when "*", /((^".+"$)|(^[[:alpha:]]+\.[[:alnum:]]+))/
|
117
|
+
value
|
118
|
+
else
|
119
|
+
PG::Connection.quote_ident(value.to_s)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Ensures the key is properly single quoted and treated as a actual PG key reference.
|
124
|
+
def literal_key(key)
|
125
|
+
case key
|
126
|
+
when TrueClass then "'t'"
|
127
|
+
when FalseClass then "'f'"
|
128
|
+
when Numeric then key
|
129
|
+
else
|
130
|
+
key = key.to_s
|
131
|
+
key.start_with?("'") && key.end_with?("'") ? key : "'#{key}'"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Converts a potential subquery into a compatible Arel SQL node.
|
136
|
+
#
|
137
|
+
# Note:
|
138
|
+
# We convert relations to SQL to maintain compatibility with Rails 5.1.
|
139
|
+
# Only Rails 5.2+ maintains bound attributes in Arel, so its better to be safe then sorry.
|
140
|
+
# When we drop support for Rails 5.1, we then can then drop the '.to_sql' conversation
|
141
|
+
|
142
|
+
def to_arel_sql(value)
|
143
|
+
case value
|
144
|
+
when Arel::Nodes::Node, Arel::Nodes::SqlLiteral, nil
|
145
|
+
value
|
146
|
+
when ActiveRecord::Relation
|
147
|
+
Arel.sql(value.spawn.to_sql)
|
148
|
+
else
|
149
|
+
Arel.sql(value.respond_to?(:to_sql) ? value.to_sql : value.to_s)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def group_when_needed(arel_or_rel_query)
|
154
|
+
return arel_or_rel_query unless needs_to_be_grouped?(arel_or_rel_query)
|
155
|
+
|
156
|
+
generate_grouping(arel_or_rel_query)
|
157
|
+
end
|
158
|
+
|
159
|
+
def needs_to_be_grouped?(query)
|
160
|
+
query.respond_to?(:to_sql) || (query.is_a?(String) && /^SELECT.+/i.match?(query))
|
161
|
+
end
|
162
|
+
|
163
|
+
def generate_grouping(expr)
|
164
|
+
::Arel::Nodes::Grouping.new(to_arel_sql(expr))
|
165
|
+
end
|
166
|
+
|
167
|
+
def generate_named_function(function_name, *args)
|
168
|
+
args.map! { |arg| to_arel_sql(arg) }
|
169
|
+
function_name = function_name.to_s.upcase
|
170
|
+
::Arel::Nodes::NamedFunction.new(to_arel_sql(function_name), args)
|
171
|
+
end
|
172
|
+
|
173
|
+
def key_generator
|
174
|
+
A_TO_Z_KEYS.sample
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|