Module: ActiveRecordExtended::Utilities::Support

Constant Summary collapse

A_TO_Z_KEYS =
("a".."z").to_a.freeze

Instance Method Summary collapse

Instance Method Details

#double_quote(value) ⇒ Object

Ensures the given value is properly double quoted. This also ensures we don’t have conflicts with reversed keywords.

IE: ‘user` is a reserved keyword in PG. But `“user”` is allowed and works the same

when used as an column/tbl alias.


110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/active_record_extended/utilities/support.rb', line 110

def double_quote(value)
  return if value.nil?

  case value.to_s
    # Ignore keys that contain double quotes or a Arel.star (*)[all columns]
    # or if a table has already been explicitly declared (ex: users.id)
  when "*", /((^".+"$)|(^[[:alpha:]]+\.[[:alnum:]]+)|\(.+\))/
    value
  else
    PG::Connection.quote_ident(value.to_s)
  end
end

#flatten_safely(values, &block) ⇒ Object



21
22
23
24
25
26
27
28
# File 'lib/active_record_extended/utilities/support.rb', line 21

def flatten_safely(values, &block)
  unless values.is_a?(Array)
    values = yield values if block
    return [values]
  end

  values.map { |value| flatten_safely(value, &block) }.reduce(:+)
end

#flatten_to_sql(*values) ⇒ Object Also known as: to_sql_array

We need to ensure we can flatten nested ActiveRecord::Relations that might have been nested due to the (splat)*args parameters

Note: calling ‘Array.flatten/1` will actually remove all AR relations from the array.



13
14
15
16
17
18
# File 'lib/active_record_extended/utilities/support.rb', line 13

def flatten_to_sql(*values)
  flatten_safely(values) do |value|
    value = yield value if block_given?
    to_arel_sql(value)
  end
end

#from_clause_constructor(from, reference_key) ⇒ Object

Will attempt to digest and resolve the from clause

If the from clause is a String, it will check to see if a table reference key has been assigned.

- If one cannot be detected, one will be appended.
- Rails does not allow assigning table references using the `.from/2` method, when its a string / sym type.

If the from clause is an AR relation; it will duplicate the object.

- Ensures any memorizers are reset (ex: `.to_sql` sets a memorizer on the instance)
- Key's can be assigned using the `.from/2` method.


75
76
77
78
79
80
81
82
83
84
85
# File 'lib/active_record_extended/utilities/support.rb', line 75

def from_clause_constructor(from, reference_key)
  case from
  when /\s.?#{reference_key}.?$/ # The from clause is a string and has the tbl reference key
    @scope.unscoped.from(from)
  when String, Symbol
    @scope.unscoped.from("#{from} #{reference_key}")
  else
    replicate_klass = from.respond_to?(:unscoped) ? from.unscoped : @scope.unscoped
    replicate_klass.from(from.dup, reference_key)
  end.unscope(:where)
end

#generate_grouping(expr) ⇒ Object



163
164
165
# File 'lib/active_record_extended/utilities/support.rb', line 163

def generate_grouping(expr)
  ::Arel::Nodes::Grouping.new(to_arel_sql(expr))
end

#generate_named_function(function_name, *args) ⇒ Object



167
168
169
170
171
# File 'lib/active_record_extended/utilities/support.rb', line 167

def generate_named_function(function_name, *args)
  args.map! { |arg| to_arel_sql(arg) }
  function_name = function_name.to_s.upcase
  ::Arel::Nodes::NamedFunction.new(to_arel_sql(function_name), args)
end

#group_when_needed(arel_or_rel_query) ⇒ Object



153
154
155
156
157
# File 'lib/active_record_extended/utilities/support.rb', line 153

def group_when_needed(arel_or_rel_query)
  return arel_or_rel_query unless needs_to_be_grouped?(arel_or_rel_query)

  generate_grouping(arel_or_rel_query)
end

#key_generatorObject



173
174
175
# File 'lib/active_record_extended/utilities/support.rb', line 173

def key_generator
  A_TO_Z_KEYS.sample
end

#literal_key(key) ⇒ Object

Ensures the key is properly single quoted and treated as a actual PG key reference.



124
125
126
127
128
129
130
131
132
133
# File 'lib/active_record_extended/utilities/support.rb', line 124

def literal_key(key)
  case key
  when TrueClass  then "'t'"
  when FalseClass then "'f'"
  when Numeric    then key
  else
    key = key.to_s
    key.start_with?("'") && key.end_with?("'") ? key : "'#{key}'"
  end
end

#needs_to_be_grouped?(query) ⇒ Boolean

Returns:

  • (Boolean)


159
160
161
# File 'lib/active_record_extended/utilities/support.rb', line 159

def needs_to_be_grouped?(query)
  query.respond_to?(:to_sql) || (query.is_a?(String) && /^SELECT.+/i.match?(query))
end

#nested_alias_escape(query, alias_name) ⇒ Object

Applies aliases to the given query Ex: ‘SELECT * FROM users` => `(SELECT * FROM users) AS “members”`



32
33
34
35
# File 'lib/active_record_extended/utilities/support.rb', line 32

def nested_alias_escape(query, alias_name)
  sql_query = generate_grouping(query)
  Arel::Nodes::As.new(sql_query, to_arel_sql(double_quote(alias_name)))
end

#pipe_cte_with!(subquery) ⇒ Object

Will carry defined CTE tables from the nested sub-query and gradually pushes it up to the parents query stack I.E: It pushes ‘WITH [:cte_name:] AS(…), ..` to the top of the query structure tree

SPECIAL GOTCHA NOTE: (if duplicate keys are found) This will favor the parents query ‘with’s` over nested ones!



91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/active_record_extended/utilities/support.rb', line 91

def pipe_cte_with!(subquery)
  return self unless subquery.try(:with_values?)

  # Add subquery CTE's to the parents query stack. (READ THE SPECIAL NOTE ABOVE!)
  if @scope.with_values?
    @scope.cte.pipe_cte_with!(subquery.cte)
  else
    # Top level has no with values
    @scope.with!(subquery.cte)
  end

  self
end

#to_arel_sql(value) ⇒ Object

Converts a potential subquery into a compatible Arel SQL node.

Note: We convert relations to SQL to maintain compatibility with Rails 5.1. Only Rails 5.2+ maintains bound attributes in Arel, so its better to be safe then sorry. When we drop support for Rails 5.1, we then can then drop the ‘.to_sql’ conversation



142
143
144
145
146
147
148
149
150
151
# File 'lib/active_record_extended/utilities/support.rb', line 142

def to_arel_sql(value)
  case value
  when Arel::Nodes::Node, Arel::Nodes::SqlLiteral, nil
    value
  when ActiveRecord::Relation
    Arel.sql(value.spawn.to_sql)
  else
    Arel.sql(value.respond_to?(:to_sql) ? value.to_sql : value.to_s)
  end
end

#wrap_with_agg_array(arel_or_rel_query, alias_name, order_by: false, distinct: false) ⇒ Object

Wraps query into an aggregated array EX: ‘(ARRAY_AGG((SELECT * FROM users)) AS “members”`

`(ARRAY_AGG(DISTINCT (SELECT * FROM users)) AS "members"`
`SELECT ARRAY_AGG((id)) AS "ids" FROM users`
`SELECT ARRAY_AGG(DISTINCT (id)) AS "ids" FROM users`


53
54
55
56
57
58
59
60
61
62
63
# File 'lib/active_record_extended/utilities/support.rb', line 53

def wrap_with_agg_array(arel_or_rel_query, alias_name, order_by: false, distinct: false)
  distinct       = !(!distinct)
  order_exp      = distinct ? nil : order_by # Can't order a distinct agg
  query          = group_when_needed(arel_or_rel_query)
  query          =
    Arel::Nodes::AggregateFunctionName
    .new("ARRAY_AGG", to_sql_array(query), distinct)
    .order_by(order_exp)

  nested_alias_escape(query, alias_name)
end

#wrap_with_array(arel_or_rel_query, alias_name, order_by: false) ⇒ Object

Wraps subquery into an Aliased ARRAY Ex: ‘SELECT * FROM users` => (ARRAY(SELECT * FROM users)) AS “members”



39
40
41
42
43
44
45
46
# File 'lib/active_record_extended/utilities/support.rb', line 39

def wrap_with_array(arel_or_rel_query, alias_name, order_by: false)
  if order_by && arel_or_rel_query.is_a?(ActiveRecord::Relation)
    arel_or_rel_query = arel_or_rel_query.order(order_by)
  end

  query = Arel::Nodes::Array.new(to_sql_array(arel_or_rel_query))
  nested_alias_escape(query, alias_name)
end