active_record_extended 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -18
- data/lib/active_record_extended.rb +2 -1
- data/lib/active_record_extended/active_record.rb +2 -0
- data/lib/active_record_extended/active_record/relation_patch.rb +1 -1
- data/lib/active_record_extended/arel.rb +1 -0
- data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
- data/lib/active_record_extended/arel/nodes.rb +31 -41
- data/lib/active_record_extended/arel/predications.rb +4 -1
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +40 -1
- data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +4 -4
- data/lib/active_record_extended/patch/5_0/regex_match.rb +10 -0
- data/lib/active_record_extended/query_methods/any_of.rb +5 -4
- data/lib/active_record_extended/query_methods/inet.rb +1 -1
- data/lib/active_record_extended/query_methods/json.rb +152 -43
- data/lib/active_record_extended/query_methods/select.rb +117 -0
- data/lib/active_record_extended/query_methods/unionize.rb +4 -39
- data/lib/active_record_extended/query_methods/with_cte.rb +3 -3
- data/lib/active_record_extended/utilities/order_by.rb +96 -0
- data/lib/active_record_extended/utilities/support.rb +175 -0
- data/lib/active_record_extended/version.rb +1 -1
- data/spec/query_methods/any_of_spec.rb +40 -40
- data/spec/query_methods/array_query_spec.rb +14 -14
- data/spec/query_methods/either_spec.rb +14 -14
- data/spec/query_methods/hash_query_spec.rb +11 -11
- data/spec/query_methods/inet_query_spec.rb +33 -31
- data/spec/query_methods/json_spec.rb +40 -25
- data/spec/query_methods/select_spec.rb +115 -0
- data/spec/query_methods/unionize_spec.rb +54 -54
- data/spec/query_methods/with_cte_spec.rb +12 -12
- data/spec/sql_inspections/any_of_sql_spec.rb +11 -11
- data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
- data/spec/sql_inspections/arel/array_spec.rb +7 -7
- data/spec/sql_inspections/arel/inet_spec.rb +7 -7
- data/spec/sql_inspections/contains_sql_queries_spec.rb +14 -14
- data/spec/sql_inspections/either_sql_spec.rb +11 -11
- data/spec/sql_inspections/json_sql_spec.rb +38 -8
- data/spec/sql_inspections/unionize_sql_spec.rb +27 -27
- data/spec/sql_inspections/with_cte_sql_spec.rb +22 -22
- data/spec/support/models.rb +18 -4
- metadata +11 -3
- data/lib/active_record_extended/utilities.rb +0 -141
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module QueryMethods
|
5
|
+
module Select
|
6
|
+
class SelectHelper
|
7
|
+
include ::ActiveRecordExtended::Utilities::Support
|
8
|
+
include ::ActiveRecordExtended::Utilities::OrderBy
|
9
|
+
|
10
|
+
AGGREGATE_ONE_LINERS = /^(exists|sum|max|min|avg|count|jsonb?_agg|(bit|bool)_(and|or)|xmlagg|array_agg)$/.freeze
|
11
|
+
|
12
|
+
def initialize(scope)
|
13
|
+
@scope = scope
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_foster_select(*args)
|
17
|
+
flatten_safely(args).each do |select_arg|
|
18
|
+
case select_arg
|
19
|
+
when String, Symbol
|
20
|
+
select!(select_arg)
|
21
|
+
when Hash
|
22
|
+
select_arg.each_pair do |alias_name, options_or_column|
|
23
|
+
case options_or_column
|
24
|
+
when Array
|
25
|
+
process_array!(options_or_column, alias_name)
|
26
|
+
when Hash
|
27
|
+
process_hash!(options_or_column, alias_name)
|
28
|
+
else
|
29
|
+
select!(options_or_column, alias_name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Assumes that the first element in the array is the source/target column.
|
39
|
+
# Example
|
40
|
+
# process_array_options!([:col_name], :my_alias_name)
|
41
|
+
# #=> SELECT ([:col_name:]) AS "my_alias_name", ...
|
42
|
+
def process_array!(array_of_options, alias_name)
|
43
|
+
options = array_of_options.detect { |opts| opts.is_a?(Hash) }
|
44
|
+
query = { __select_statement: array_of_options.first }
|
45
|
+
query.merge!(options) unless options.nil?
|
46
|
+
process_hash!(query, alias_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Processes options that come in as Hash elements
|
50
|
+
# Examples:
|
51
|
+
# process_hash_options!({ memberships: :price, cast_with: :agg_array_distinct }, :past_purchases)
|
52
|
+
# #=> SELECT (ARRAY_AGG(DISTINCT members.price)) AS past_purchases, ...
|
53
|
+
def process_hash!(hash_of_options, alias_name)
|
54
|
+
enforced_options = {
|
55
|
+
cast_with: hash_of_options.delete(:cast_with),
|
56
|
+
order_by: hash_of_options.delete(:order_by),
|
57
|
+
distinct: !(!hash_of_options.delete(:distinct)),
|
58
|
+
}
|
59
|
+
query_statement = hash_to_dot_notation(hash_of_options.delete(:__select_statement) || hash_of_options.first)
|
60
|
+
select!(query_statement, alias_name, enforced_options)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Turn a hash chain into a query statement:
|
64
|
+
# Example: hash_to_dot_notation(table_name: :col_name) #=> "table_name.col_name"
|
65
|
+
def hash_to_dot_notation(column)
|
66
|
+
case column
|
67
|
+
when Hash, Array
|
68
|
+
column.to_a.flat_map(&method(:hash_to_dot_notation)).join(".")
|
69
|
+
when String, Symbol
|
70
|
+
/^([[:alpha:]]+)$/.match?(column.to_s) ? double_quote(column) : column
|
71
|
+
else
|
72
|
+
column
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Add's select statement values to the current relation, select statement lists
|
77
|
+
def select!(query, alias_name = nil, **options)
|
78
|
+
pipe_cte_with!(query)
|
79
|
+
@scope._select!(to_casted_query(query, alias_name, options))
|
80
|
+
end
|
81
|
+
|
82
|
+
# Wraps the query with the requested query method
|
83
|
+
# Example:
|
84
|
+
# to_casted_query("memberships.cost", :total_revenue, :sum)
|
85
|
+
# #=> SELECT (SUM(memberships.cost)) AS total_revenue
|
86
|
+
def to_casted_query(query, alias_name, **options)
|
87
|
+
cast_with = options.delete(:cast_with).to_s.downcase
|
88
|
+
order_expr = order_by_expression(options.delete(:order_by))
|
89
|
+
distinct = cast_with.chomp!("_distinct") || options.delete(:distinct) # account for [:agg_name:]_distinct
|
90
|
+
|
91
|
+
case cast_with
|
92
|
+
when "array", "true"
|
93
|
+
wrap_with_array(query, alias_name)
|
94
|
+
when AGGREGATE_ONE_LINERS
|
95
|
+
expr = to_sql_array(query, &method(:group_when_needed))
|
96
|
+
casted_query = ::Arel::Nodes::AggregateFunctionName.new(cast_with, expr, distinct).order_by(order_expr)
|
97
|
+
nested_alias_escape(casted_query, alias_name)
|
98
|
+
else
|
99
|
+
alias_name.presence ? nested_alias_escape(query, alias_name) : query
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def foster_select(*args)
|
105
|
+
raise ArgumentError, "Call `.forster_select' with at least one field" if args.empty?
|
106
|
+
spawn._foster_select!(*args)
|
107
|
+
end
|
108
|
+
|
109
|
+
def _foster_select!(*args)
|
110
|
+
SelectHelper.new(self).build_foster_select(*args)
|
111
|
+
self
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::Select)
|
@@ -7,7 +7,8 @@ module ActiveRecordExtended
|
|
7
7
|
UNIONIZE_METHODS = [:union, :union_all, :union_except, :union_intersect].freeze
|
8
8
|
|
9
9
|
class UnionChain
|
10
|
-
include ::ActiveRecordExtended::Utilities
|
10
|
+
include ::ActiveRecordExtended::Utilities::Support
|
11
|
+
include ::ActiveRecordExtended::Utilities::OrderBy
|
11
12
|
|
12
13
|
def initialize(scope)
|
13
14
|
@scope = scope
|
@@ -69,38 +70,6 @@ module ActiveRecordExtended
|
|
69
70
|
scope_count = 1 if scope_count <= 0 && @scope.union_values.size <= 1
|
70
71
|
@scope.union_operations += [union_type] * scope_count
|
71
72
|
end
|
72
|
-
|
73
|
-
# We'll need to preprocess these arguments for allowing `ActiveRecord::Relation#preprocess_order_args`,
|
74
|
-
# to check for sanitization issues and convert over to `Arel::Nodes::[Ascending/Descending]`.
|
75
|
-
# Without reflecting / prepending the parent's table name.
|
76
|
-
|
77
|
-
if ActiveRecord.gem_version < Gem::Version.new("5.1")
|
78
|
-
# TODO: Rails 5.0.x order logic will *always* append the parents name to the column when its an HASH obj
|
79
|
-
# We should really do this stuff better. Maybe even just ignore `preprocess_order_args` altogether?
|
80
|
-
# Maybe I'm just stupidly over paranoid on just the 'ORDER BY' for some odd reason.
|
81
|
-
def process_ordering_arguments!(ordering_args)
|
82
|
-
ordering_args.flatten!
|
83
|
-
ordering_args.compact!
|
84
|
-
ordering_args.map! do |arg|
|
85
|
-
next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol
|
86
|
-
arg.each_with_object([]) do |(field, dir), ordering_object|
|
87
|
-
ordering_object << to_arel_sql(field).send(dir.to_s.downcase)
|
88
|
-
end
|
89
|
-
end.flatten!
|
90
|
-
end
|
91
|
-
else
|
92
|
-
def process_ordering_arguments!(ordering_args)
|
93
|
-
ordering_args.flatten!
|
94
|
-
ordering_args.compact!
|
95
|
-
ordering_args.map! do |arg|
|
96
|
-
next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol
|
97
|
-
arg.each_with_object({}) do |(field, dir), ordering_obj|
|
98
|
-
# ActiveRecord will not reflect if the Hash keys are a `Arel::Nodes::SqlLiteral` klass
|
99
|
-
ordering_obj[to_arel_sql(field)] = dir.to_s.downcase
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
73
|
end
|
105
74
|
|
106
75
|
def unionize_storage
|
@@ -171,7 +140,7 @@ module ActiveRecordExtended
|
|
171
140
|
return unless union_values?
|
172
141
|
|
173
142
|
union_nodes = apply_union_ordering(build_union_nodes!)
|
174
|
-
table_name = Arel
|
143
|
+
table_name = Arel.sql(unionized_name)
|
175
144
|
table_alias = arel.create_table_alias(arel.grouping(union_nodes), table_name)
|
176
145
|
arel.from(table_alias)
|
177
146
|
end
|
@@ -246,11 +215,7 @@ module ActiveRecordExtended
|
|
246
215
|
#
|
247
216
|
def apply_union_ordering(union_nodes)
|
248
217
|
return union_nodes unless union_ordering_values?
|
249
|
-
|
250
|
-
# Sanitation check / resolver (ActiveRecord::Relation#preprocess_order_args)
|
251
|
-
preprocess_order_args(union_ordering_values)
|
252
|
-
union_ordering_values.uniq!
|
253
|
-
Arel::Nodes::InfixOperation.new("ORDER BY", union_nodes, union_ordering_values)
|
218
|
+
UnionChain.new(self).inline_order_by(union_nodes, union_ordering_values)
|
254
219
|
end
|
255
220
|
|
256
221
|
private
|
@@ -54,12 +54,12 @@ module ActiveRecordExtended
|
|
54
54
|
select =
|
55
55
|
case expression
|
56
56
|
when String
|
57
|
-
Arel
|
57
|
+
Arel.sql("(#{expression})")
|
58
58
|
when ActiveRecord::Relation, Arel::SelectManager
|
59
|
-
Arel
|
59
|
+
Arel.sql("(#{expression.to_sql})")
|
60
60
|
end
|
61
61
|
next if select.nil?
|
62
|
-
Arel::Nodes::As.new(Arel
|
62
|
+
Arel::Nodes::As.new(Arel.sql(PG::Connection.quote_ident(name.to_s)), select)
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
@@ -0,0 +1,96 @@
|
|
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(&method(:process_ordering_arguments!))
|
28
|
+
.tap(&method(:scope_preprocess_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
|
+
# We'll need to preprocess these arguments for allowing `ActiveRecord::Relation#preprocess_order_args`,
|
64
|
+
# to check for sanitization issues and convert over to `Arel::Nodes::[Ascending/Descending]`.
|
65
|
+
# Without reflecting / prepending the parent's table name.
|
66
|
+
#
|
67
|
+
if ActiveRecord.gem_version < Gem::Version.new("5.1")
|
68
|
+
# TODO: Rails 5.0.x order logic will *always* append the parents name to the column when its an HASH obj
|
69
|
+
# We should really do this stuff better. Maybe even just ignore `preprocess_order_args` altogether?
|
70
|
+
# Maybe I'm just stupidly over paranoid on just the 'ORDER BY' for some odd reason.
|
71
|
+
def process_ordering_arguments!(ordering_args)
|
72
|
+
ordering_args.flatten!
|
73
|
+
ordering_args.compact!
|
74
|
+
ordering_args.map! do |arg|
|
75
|
+
next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol
|
76
|
+
arg.each_with_object([]) do |(field, dir), ordering_object|
|
77
|
+
ordering_object << to_arel_sql(field).send(dir.to_s.downcase)
|
78
|
+
end
|
79
|
+
end.flatten!
|
80
|
+
end
|
81
|
+
else
|
82
|
+
def process_ordering_arguments!(ordering_args)
|
83
|
+
ordering_args.flatten!
|
84
|
+
ordering_args.compact!
|
85
|
+
ordering_args.map! do |arg|
|
86
|
+
next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol
|
87
|
+
arg.each_with_object({}) do |(field, dir), ordering_obj|
|
88
|
+
# ActiveRecord will not reflect if the Hash keys are a `Arel::Nodes::SqlLiteral` klass
|
89
|
+
ordering_obj[to_arel_sql(field)] = dir.to_s.downcase
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,175 @@
|
|
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_given?
|
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 = Arel::Nodes::Grouping.new(to_arel_sql(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
|
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
|
+
cte_ary = flatten_safely(subquery.with_values)
|
95
|
+
subquery.with_values = nil # Remove nested queries with values
|
96
|
+
|
97
|
+
# Add subquery's CTE's to the parents query stack. (READ THE SPECIAL NOTE ABOVE!)
|
98
|
+
if @scope.with_values?
|
99
|
+
# combine top-level and lower level queries `.with` values into 1 structure
|
100
|
+
with_hash = cte_ary.each_with_object(@scope.with_values.first) do |from_cte, hash|
|
101
|
+
hash.reverse_merge!(from_cte)
|
102
|
+
end
|
103
|
+
|
104
|
+
@scope.with_values = [with_hash]
|
105
|
+
else
|
106
|
+
# Top level has no with values
|
107
|
+
@scope.with!(*cte_ary)
|
108
|
+
end
|
109
|
+
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
# Ensures the given value is properly double quoted.
|
114
|
+
# This also ensures we don't have conflicts with reversed keywords.
|
115
|
+
#
|
116
|
+
# IE: `user` is a reserved keyword in PG. But `"user"` is allowed and works the same
|
117
|
+
# when used as an column/tbl alias.
|
118
|
+
def double_quote(value)
|
119
|
+
return if value.nil?
|
120
|
+
|
121
|
+
case value.to_s
|
122
|
+
# Ignore keys that contain double quotes or a Arel.star (*)[all columns]
|
123
|
+
# or if a table has already been explicitly declared (ex: users.id)
|
124
|
+
when "*", /((^".+"$)|(^[[:alpha:]]+\.[[:alnum:]]+))/
|
125
|
+
value
|
126
|
+
else
|
127
|
+
PG::Connection.quote_ident(value.to_s)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Ensures the key is properly single quoted and treated as a actual PG key reference.
|
132
|
+
def literal_key(key)
|
133
|
+
case key
|
134
|
+
when TrueClass then "'t'"
|
135
|
+
when FalseClass then "'f'"
|
136
|
+
when Numeric then key
|
137
|
+
else
|
138
|
+
key = key.to_s
|
139
|
+
key.start_with?("'") && key.end_with?("'") ? key : "'#{key}'"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Converts a potential subquery into a compatible Arel SQL node.
|
144
|
+
#
|
145
|
+
# Note:
|
146
|
+
# We convert relations to SQL to maintain compatibility with Rails 5.[0/1].
|
147
|
+
# Only Rails 5.2+ maintains bound attributes in Arel, so its better to be safe then sorry.
|
148
|
+
# When we drop support for Rails 5.[0/1], we then can then drop the '.to_sql' conversation
|
149
|
+
|
150
|
+
def to_arel_sql(value)
|
151
|
+
case value
|
152
|
+
when Arel::Node, Arel::Nodes::SqlLiteral, nil
|
153
|
+
value
|
154
|
+
when ActiveRecord::Relation
|
155
|
+
Arel.sql(value.spawn.to_sql)
|
156
|
+
else
|
157
|
+
Arel.sql(value.respond_to?(:to_sql) ? value.to_sql : value.to_s)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def group_when_needed(arel_or_rel_query)
|
162
|
+
return arel_or_rel_query unless needs_to_be_grouped?(arel_or_rel_query)
|
163
|
+
Arel::Nodes::Grouping.new(to_arel_sql(arel_or_rel_query))
|
164
|
+
end
|
165
|
+
|
166
|
+
def needs_to_be_grouped?(query)
|
167
|
+
query.respond_to?(:to_sql) || (query.is_a?(String) && /^SELECT.+/i.match?(query))
|
168
|
+
end
|
169
|
+
|
170
|
+
def key_generator
|
171
|
+
A_TO_Z_KEYS.sample
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|