active_record_extended 1.1.0 → 2.0.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -15
  3. data/lib/active_record_extended.rb +2 -1
  4. data/lib/active_record_extended/active_record.rb +2 -9
  5. data/lib/active_record_extended/active_record/relation_patch.rb +21 -4
  6. data/lib/active_record_extended/arel.rb +2 -0
  7. data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
  8. data/lib/active_record_extended/arel/nodes.rb +32 -41
  9. data/lib/active_record_extended/arel/predications.rb +4 -1
  10. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  11. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +40 -1
  12. data/lib/active_record_extended/query_methods/any_of.rb +10 -8
  13. data/lib/active_record_extended/query_methods/either.rb +1 -1
  14. data/lib/active_record_extended/query_methods/inet.rb +7 -3
  15. data/lib/active_record_extended/query_methods/json.rb +156 -50
  16. data/lib/active_record_extended/query_methods/select.rb +118 -0
  17. data/lib/active_record_extended/query_methods/unionize.rb +14 -43
  18. data/lib/active_record_extended/query_methods/where_chain.rb +14 -6
  19. data/lib/active_record_extended/query_methods/window.rb +93 -0
  20. data/lib/active_record_extended/query_methods/with_cte.rb +102 -35
  21. data/lib/active_record_extended/utilities/order_by.rb +77 -0
  22. data/lib/active_record_extended/utilities/support.rb +178 -0
  23. data/lib/active_record_extended/version.rb +1 -1
  24. data/spec/query_methods/any_of_spec.rb +40 -40
  25. data/spec/query_methods/array_query_spec.rb +14 -14
  26. data/spec/query_methods/either_spec.rb +14 -14
  27. data/spec/query_methods/hash_query_spec.rb +11 -11
  28. data/spec/query_methods/inet_query_spec.rb +33 -31
  29. data/spec/query_methods/json_spec.rb +42 -27
  30. data/spec/query_methods/select_spec.rb +115 -0
  31. data/spec/query_methods/unionize_spec.rb +56 -56
  32. data/spec/query_methods/window_spec.rb +51 -0
  33. data/spec/query_methods/with_cte_spec.rb +22 -12
  34. data/spec/spec_helper.rb +1 -1
  35. data/spec/sql_inspections/any_of_sql_spec.rb +12 -12
  36. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
  37. data/spec/sql_inspections/arel/array_spec.rb +7 -7
  38. data/spec/sql_inspections/arel/inet_spec.rb +7 -7
  39. data/spec/sql_inspections/contains_sql_queries_spec.rb +14 -14
  40. data/spec/sql_inspections/either_sql_spec.rb +11 -11
  41. data/spec/sql_inspections/json_sql_spec.rb +44 -8
  42. data/spec/sql_inspections/unionize_sql_spec.rb +27 -27
  43. data/spec/sql_inspections/window_sql_spec.rb +98 -0
  44. data/spec/sql_inspections/with_cte_sql_spec.rb +52 -23
  45. data/spec/support/models.rb +24 -4
  46. metadata +31 -20
  47. data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +0 -87
  48. data/lib/active_record_extended/utilities.rb +0 -141
@@ -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
@@ -58,7 +59,7 @@ module ActiveRecordExtended
58
59
  protected
59
60
 
60
61
  def append_union_order!(union_type, args)
61
- args.each(&method(:pipe_cte_with!))
62
+ args.each { |arg| pipe_cte_with!(arg) }
62
63
  flatten_scopes = flatten_to_sql(args)
63
64
  @scope.union_values += flatten_scopes
64
65
  calculate_union_operation!(union_type, flatten_scopes.size)
@@ -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
@@ -112,7 +81,7 @@ module ActiveRecordExtended
112
81
  union_values: [],
113
82
  union_operations: [],
114
83
  union_ordering_values: [],
115
- unionized_name: nil,
84
+ unionized_name: nil
116
85
  }
117
86
  end
118
87
 
@@ -120,10 +89,11 @@ module ActiveRecordExtended
120
89
  union_values: Array,
121
90
  union_operations: Array,
122
91
  union_ordering_values: Array,
123
- unionized_name: lambda { |klass| klass.arel_table.name },
92
+ unionized_name: lambda { |klass| klass.arel_table.name }
124
93
  }.each_pair do |method_name, default|
125
94
  define_method(method_name) do
126
95
  return unionize_storage[method_name] if send("#{method_name}?")
96
+
127
97
  (default.is_a?(Proc) ? default.call(@klass) : default.new)
128
98
  end
129
99
 
@@ -138,11 +108,13 @@ module ActiveRecordExtended
138
108
 
139
109
  def union(opts = :chain, *args)
140
110
  return UnionChain.new(spawn) if opts == :chain
111
+
141
112
  opts.nil? ? self : spawn.union!(opts, *args, chain_method: __callee__)
142
113
  end
143
114
 
144
115
  (UNIONIZE_METHODS + UNION_RELATION_METHODS).each do |union_method|
145
116
  next if union_method == :union
117
+
146
118
  alias_method union_method, :union
147
119
  end
148
120
 
@@ -157,11 +129,13 @@ module ActiveRecordExtended
157
129
  # Will construct *Just* the union SQL statement that was been built thus far
158
130
  def to_union_sql
159
131
  return unless union_values?
132
+
160
133
  apply_union_ordering(build_union_nodes!(false)).to_sql
161
134
  end
162
135
 
163
136
  def to_nice_union_sql(color = true)
164
137
  return to_union_sql unless defined?(::Niceql)
138
+
165
139
  ::Niceql::Prettifier.prettify_sql(to_union_sql, color)
166
140
  end
167
141
 
@@ -171,7 +145,7 @@ module ActiveRecordExtended
171
145
  return unless union_values?
172
146
 
173
147
  union_nodes = apply_union_ordering(build_union_nodes!)
174
- table_name = Arel::Nodes::SqlLiteral.new(unionized_name)
148
+ table_name = Arel.sql(unionized_name)
175
149
  table_alias = arel.create_table_alias(arel.grouping(union_nodes), table_name)
176
150
  arel.from(table_alias)
177
151
  end
@@ -203,7 +177,7 @@ module ActiveRecordExtended
203
177
 
204
178
  def build_union_nodes!(raise_error = true)
205
179
  unionize_error_or_warn!(raise_error)
206
- union_values.each_with_index.inject(nil) do |union_node, (relation_node, index)|
180
+ union_values.each_with_index.reduce(nil) do |union_node, (relation_node, index)|
207
181
  next resolve_relation_node(relation_node) if union_node.nil?
208
182
 
209
183
  operation = union_operations.fetch(index - 1, :union)
@@ -247,17 +221,14 @@ module ActiveRecordExtended
247
221
  def apply_union_ordering(union_nodes)
248
222
  return union_nodes unless union_ordering_values?
249
223
 
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)
224
+ UnionChain.new(self).inline_order_by(union_nodes, union_ordering_values)
254
225
  end
255
226
 
256
227
  private
257
228
 
258
229
  def unionize_error_or_warn!(raise_error = true)
259
230
  if raise_error && union_values.size <= 1
260
- raise ArgumentError, "You are required to provide 2 or more unions to join!"
231
+ raise ArgumentError.new("You are required to provide 2 or more unions to join!")
261
232
  elsif !raise_error && union_values.size <= 1
262
233
  warn("Warning: You are required to provide 2 or more unions to join.")
263
234
  end
@@ -54,10 +54,10 @@ module ActiveRecordExtended
54
54
  elsif column.try(:array)
55
55
  Arel::Nodes::ContainsArray.new(arel.left, arel.right)
56
56
  else
57
- raise ArgumentError, "Invalid argument for .where.contains(), got #{arel.class}"
57
+ raise ArgumentError.new("Invalid argument for .where.contains(), got #{arel.class}")
58
58
  end
59
59
  else
60
- raise ArgumentError, "Invalid argument for .where.contains(), got #{arel.class}"
60
+ raise ArgumentError.new("Invalid argument for .where.contains(), got #{arel.class}")
61
61
  end
62
62
  end
63
63
  end
@@ -88,7 +88,7 @@ module ActiveRecordExtended
88
88
  when Arel::Nodes::Equality
89
89
  Arel::Nodes::Equality.new(arel.right, Arel::Nodes::NamedFunction.new(function_name, [arel.left]))
90
90
  else
91
- raise ArgumentError, "Invalid argument for .where.#{function_name.downcase}(), got #{arel.class}"
91
+ raise ArgumentError.new("Invalid argument for .where.#{function_name.downcase}(), got #{arel.class}")
92
92
  end
93
93
  end
94
94
  end
@@ -99,10 +99,18 @@ module ActiveRecordExtended
99
99
  when Arel::Nodes::In, Arel::Nodes::Equality
100
100
  arel_node_class.new(arel.left, arel.right)
101
101
  else
102
- raise ArgumentError, "Invalid argument for .where.#{method}(), got #{arel.class}"
102
+ raise ArgumentError.new("Invalid argument for .where.#{method}(), got #{arel.class}")
103
103
  end
104
104
  end
105
105
  end
106
+
107
+ def build_where_clause_for(scope, opts, rest)
108
+ if ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR == 1
109
+ scope.send(:build_where_clause, opts, rest)
110
+ else
111
+ scope.send(:where_clause_factory).build(opts, rest)
112
+ end
113
+ end
106
114
  end
107
115
  end
108
116
 
@@ -112,9 +120,9 @@ module ActiveRecord
112
120
  prepend ActiveRecordExtended::WhereChain
113
121
 
114
122
  def build_where_chain(opts, rest, &block)
115
- where_clause = @scope.send(:where_clause_factory).build(opts, rest)
123
+ where_clause = build_where_clause_for(@scope, opts, rest)
116
124
  @scope.tap do |scope|
117
- scope.references!(PredicateBuilder.references(opts)) if opts.is_a?(Hash)
125
+ scope.references!(PredicateBuilder.references(opts.stringify_keys)) if opts.is_a?(Hash)
118
126
  scope.where_clause += where_clause.modified_predicates(&block)
119
127
  end
120
128
  end
@@ -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)
@@ -3,78 +3,145 @@
3
3
  module ActiveRecordExtended
4
4
  module QueryMethods
5
5
  module WithCTE
6
- class WithChain
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
7
15
  def initialize(scope)
8
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)
9
34
  end
10
35
 
11
- def recursive(*args)
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)
12
73
  @scope.tap do |scope|
13
- scope.with_values += args
14
74
  scope.recursive_value = true
75
+ scope.cte.pipe_cte_with!(args)
15
76
  end
16
77
  end
17
78
  end
18
79
 
19
- def with_values
20
- @values[:with] || []
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
21
90
  end
22
91
 
92
+ # @return [Boolean]
23
93
  def with_values?
24
- !(@values[:with].nil? || @values[:with].empty?)
94
+ !(cte.nil? || cte.empty?)
25
95
  end
26
96
 
97
+ # @param [Hash, WithCTE] values
27
98
  def with_values=(values)
28
- @values[:with] = values
99
+ cte.with_values = values
29
100
  end
30
101
 
102
+ # @param [Boolean] value
31
103
  def recursive_value=(value)
32
104
  raise ImmutableRelation if @loaded
105
+
33
106
  @values[:recursive] = value
34
107
  end
35
108
 
36
- def recursive_value
37
- @values[:recursive]
109
+ # @return [Boolean]
110
+ def recursive_value?
111
+ !(!@values[:recursive])
38
112
  end
39
- alias recursive_value? recursive_value
40
113
 
114
+ # @param [Hash, WithCTE] opts
41
115
  def with(opts = :chain, *rest)
42
116
  return WithChain.new(spawn) if opts == :chain
117
+
43
118
  opts.blank? ? self : spawn.with!(opts, *rest)
44
119
  end
45
120
 
46
- def with!(opts = :chain, *rest)
121
+ # @param [Hash, WithCTE] opts
122
+ def with!(opts = :chain, *_rest)
47
123
  return WithChain.new(self) if opts == :chain
48
- self.with_values += [opts] + rest
49
- self
50
- end
51
124
 
52
- def build_with_hashed_value(with_value)
53
- with_value.map do |name, expression|
54
- select =
55
- case expression
56
- when String
57
- Arel::Nodes::SqlLiteral.new("(#{expression})")
58
- when ActiveRecord::Relation, Arel::SelectManager
59
- Arel::Nodes::SqlLiteral.new("(#{expression.to_sql})")
60
- end
61
- next if select.nil?
62
- Arel::Nodes::As.new(Arel::Nodes::SqlLiteral.new(PG::Connection.quote_ident(name.to_s)), select)
125
+ tap do |scope|
126
+ scope.cte ||= WithCTE.new(self)
127
+ scope.cte.pipe_cte_with!(opts)
63
128
  end
64
129
  end
65
130
 
66
131
  def build_with(arel)
67
- with_statements = with_values.flat_map do |with_value|
68
- case with_value
69
- when String, Arel::Nodes::As
70
- with_value
71
- when Hash
72
- build_with_hashed_value(with_value)
73
- end
74
- end.compact
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
75
139
 
76
- return if with_statements.empty?
77
- recursive_value? ? arel.with(:recursive, with_statements) : arel.with(with_statements)
140
+ if recursive_value?
141
+ arel.with(:recursive, cte_statements)
142
+ else
143
+ arel.with(cte_statements)
144
+ end
78
145
  end
79
146
  end
80
147
  end