active_record_extended 1.1.0 → 1.2.0
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/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
|