active_record_extended 2.0.3 → 3.1.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +140 -77
  3. data/lib/active_record_extended/arel/nodes.rb +1 -1
  4. data/lib/active_record_extended/arel/{sql_literal.rb → sql_literal_patch.rb} +2 -2
  5. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +16 -12
  6. data/lib/active_record_extended/arel.rb +1 -1
  7. data/lib/active_record_extended/patch/array_handler_patch.rb +22 -0
  8. data/lib/active_record_extended/patch/relation_patch.rb +82 -0
  9. data/lib/active_record_extended/patch/where_clause_patch.rb +13 -0
  10. data/lib/active_record_extended/query_methods/any_of.rb +1 -1
  11. data/lib/active_record_extended/query_methods/either.rb +5 -7
  12. data/lib/active_record_extended/query_methods/{select.rb → foster_select.rb} +4 -4
  13. data/lib/active_record_extended/query_methods/json.rb +2 -2
  14. data/lib/active_record_extended/query_methods/unionize.rb +5 -5
  15. data/lib/active_record_extended/query_methods/where_chain.rb +96 -90
  16. data/lib/active_record_extended/query_methods/window.rb +3 -3
  17. data/lib/active_record_extended/query_methods/with_cte.rb +61 -4
  18. data/lib/active_record_extended/utilities/order_by.rb +1 -1
  19. data/lib/active_record_extended/utilities/support.rb +1 -1
  20. data/lib/active_record_extended/version.rb +1 -1
  21. data/lib/active_record_extended.rb +55 -4
  22. metadata +20 -83
  23. data/lib/active_record_extended/active_record/relation_patch.rb +0 -50
  24. data/lib/active_record_extended/active_record.rb +0 -25
  25. data/lib/active_record_extended/patch/5_1/where_clause.rb +0 -11
  26. data/lib/active_record_extended/patch/5_2/where_clause.rb +0 -11
  27. data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +0 -20
  28. data/spec/active_record_extended_spec.rb +0 -7
  29. data/spec/query_methods/any_of_spec.rb +0 -131
  30. data/spec/query_methods/array_query_spec.rb +0 -64
  31. data/spec/query_methods/either_spec.rb +0 -70
  32. data/spec/query_methods/hash_query_spec.rb +0 -45
  33. data/spec/query_methods/inet_query_spec.rb +0 -112
  34. data/spec/query_methods/json_spec.rb +0 -157
  35. data/spec/query_methods/select_spec.rb +0 -115
  36. data/spec/query_methods/unionize_spec.rb +0 -165
  37. data/spec/query_methods/window_spec.rb +0 -51
  38. data/spec/query_methods/with_cte_spec.rb +0 -50
  39. data/spec/spec_helper.rb +0 -28
  40. data/spec/sql_inspections/any_of_sql_spec.rb +0 -41
  41. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +0 -41
  42. data/spec/sql_inspections/arel/array_spec.rb +0 -63
  43. data/spec/sql_inspections/arel/inet_spec.rb +0 -66
  44. data/spec/sql_inspections/contains_sql_queries_spec.rb +0 -47
  45. data/spec/sql_inspections/either_sql_spec.rb +0 -71
  46. data/spec/sql_inspections/json_sql_spec.rb +0 -82
  47. data/spec/sql_inspections/unionize_sql_spec.rb +0 -124
  48. data/spec/sql_inspections/window_sql_spec.rb +0 -98
  49. data/spec/sql_inspections/with_cte_sql_spec.rb +0 -95
  50. data/spec/support/database_cleaner.rb +0 -15
  51. data/spec/support/models.rb +0 -80
@@ -29,7 +29,7 @@ module ActiveRecordExtended
29
29
 
30
30
  def hash_map_queries(queries)
31
31
  if queries.size == 1 && queries.first.is_a?(Hash)
32
- queries.first.each_pair.map { |attr, predicate| Hash[attr, predicate] }
32
+ queries.first.each_pair.map { |attr, predicate| { attr => predicate } }
33
33
  else
34
34
  queries
35
35
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ar_outer_joins"
4
-
5
3
  module ActiveRecordExtended
6
4
  module QueryMethods
7
5
  module Either
@@ -12,21 +10,21 @@ module ActiveRecordExtended
12
10
  associations = [initial_association, fallback_association]
13
11
  association_options = xor_field_options_for_associations(associations)
14
12
  condition__query = xor_field_sql(association_options) + "= #{table_name}.#{primary_key}"
15
- outer_joins(associations).where(Arel.sql(condition__query))
13
+ left_outer_joins(associations).where(Arel.sql(condition__query))
16
14
  end
17
15
  alias either_joins either_join
18
16
 
19
17
  def either_order(direction, **associations_and_columns)
20
18
  reflected_columns = map_columns_to_tables(associations_and_columns)
21
19
  conditional_query = xor_field_sql(reflected_columns) + sort_order_sql(direction)
22
- outer_joins(associations_and_columns.keys).order(Arel.sql(conditional_query))
20
+ left_outer_joins(associations_and_columns.keys).order(Arel.sql(conditional_query))
23
21
  end
24
22
  alias either_orders either_order
25
23
 
26
24
  private
27
25
 
28
26
  def xor_field_sql(options)
29
- XOR_FIELD_SQL % Hash[xor_field_options(options)]
27
+ XOR_FIELD_SQL % xor_field_options(options).to_h
30
28
  end
31
29
 
32
30
  def sort_order_sql(dir)
@@ -35,7 +33,7 @@ module ActiveRecordExtended
35
33
 
36
34
  def xor_field_options(options)
37
35
  str_args = options.flatten.take(XOR_FIELD_KEYS.size).map(&:to_s)
38
- Hash[XOR_FIELD_KEYS.zip(str_args)]
36
+ XOR_FIELD_KEYS.zip(str_args).to_h
39
37
  end
40
38
 
41
39
  def map_columns_to_tables(associations_and_columns)
@@ -60,4 +58,4 @@ module ActiveRecordExtended
60
58
  end
61
59
  end
62
60
 
63
- ActiveRecord::Base.extend(ActiveRecordExtended::QueryMethods::Either)
61
+ ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::Either)
@@ -2,10 +2,10 @@
2
2
 
3
3
  module ActiveRecordExtended
4
4
  module QueryMethods
5
- module Select
5
+ module FosterSelect
6
6
  class SelectHelper
7
- include ::ActiveRecordExtended::Utilities::Support
8
- include ::ActiveRecordExtended::Utilities::OrderBy
7
+ include ActiveRecordExtended::Utilities::Support
8
+ include ActiveRecordExtended::Utilities::OrderBy
9
9
 
10
10
  AGGREGATE_ONE_LINERS = /^(exists|sum|max|min|avg|count|jsonb?_agg|(bit|bool)_(and|or)|xmlagg|array_agg)$/.freeze
11
11
 
@@ -115,4 +115,4 @@ module ActiveRecordExtended
115
115
  end
116
116
  end
117
117
 
118
- ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::Select)
118
+ ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::FosterSelect)
@@ -12,8 +12,8 @@ module ActiveRecordExtended
12
12
  ].freeze
13
13
 
14
14
  class JsonChain
15
- include ::ActiveRecordExtended::Utilities::Support
16
- include ::ActiveRecordExtended::Utilities::OrderBy
15
+ include ActiveRecordExtended::Utilities::Support
16
+ include ActiveRecordExtended::Utilities::OrderBy
17
17
 
18
18
  DEFAULT_ALIAS = '"results"'
19
19
  TO_JSONB_OPTIONS = [:array_agg, :distinct, :to_jsonb].to_set.freeze
@@ -7,8 +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::Support
11
- include ::ActiveRecordExtended::Utilities::OrderBy
10
+ include ActiveRecordExtended::Utilities::Support
11
+ include ActiveRecordExtended::Utilities::OrderBy
12
12
 
13
13
  def initialize(scope)
14
14
  @scope = scope
@@ -107,7 +107,7 @@ module ActiveRecordExtended
107
107
  end
108
108
 
109
109
  def union(opts = :chain, *args)
110
- return UnionChain.new(spawn) if :chain == opts
110
+ return UnionChain.new(spawn) if opts == :chain
111
111
 
112
112
  opts.nil? ? self : spawn.union!(opts, *args, chain_method: __callee__)
113
113
  end
@@ -121,7 +121,7 @@ module ActiveRecordExtended
121
121
  def union!(opts = :chain, *args, chain_method: :union)
122
122
  union_chain = UnionChain.new(self)
123
123
  chain_method ||= :union
124
- return union_chain if :chain == opts
124
+ return union_chain if opts == :chain
125
125
 
126
126
  union_chain.public_send(chain_method, *([opts] + args))
127
127
  end
@@ -178,7 +178,7 @@ module ActiveRecordExtended
178
178
  def build_union_nodes!(raise_error = true)
179
179
  unionize_error_or_warn!(raise_error)
180
180
  union_values.each_with_index.reduce(nil) do |union_node, (relation_node, index)|
181
- next resolve_relation_node(relation_node) if union_node.nil?
181
+ next resolve_relation_node(relation_node) if union_node.nil? # rubocop:disable Lint/UnmodifiedReduceAccumulator
182
182
 
183
183
  operation = union_operations.fetch(index - 1, :union)
184
184
  left = union_node
@@ -1,115 +1,121 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordExtended
4
- module WhereChain
5
- # Finds Records that have an array column that contain any a set of values
6
- # User.where.overlap(tags: [1,2])
7
- # # SELECT * FROM users WHERE tags && {1,2}
8
- def overlaps(opts, *rest)
9
- substitute_comparisons(opts, rest, Arel::Nodes::Overlaps, "overlap")
10
- end
11
- alias overlap overlaps
4
+ module QueryMethods
5
+ module WhereChain
6
+ # Finds Records that have an array column that contain any a set of values
7
+ # User.where.overlap(tags: [1,2])
8
+ # # SELECT * FROM users WHERE tags && {1,2}
9
+ def overlaps(opts, *rest)
10
+ substitute_comparisons(opts, rest, Arel::Nodes::Overlaps, "overlap")
11
+ end
12
+ alias overlap overlaps
12
13
 
13
- # Finds Records that contain an element in an array column
14
- # User.where.any(tags: 3)
15
- # # SELECT user.* FROM user WHERE 3 = ANY(user.tags)
16
- def any(opts, *rest)
17
- equality_to_function("ANY", opts, rest)
18
- end
14
+ # Finds Records that contain an element in an array column
15
+ # User.where.any(tags: 3)
16
+ # # SELECT user.* FROM user WHERE 3 = ANY(user.tags)
17
+ def any(opts, *rest)
18
+ equality_to_function("ANY", opts, rest)
19
+ end
19
20
 
20
- # Finds Records that contain a single matchable array element
21
- # User.where.all(tags: 3)
22
- # # SELECT user.* FROM user WHERE 3 = ALL(user.tags)
23
- def all(opts, *rest)
24
- equality_to_function("ALL", opts, rest)
25
- end
21
+ # Finds Records that contain a single matchable array element
22
+ # User.where.all(tags: 3)
23
+ # # SELECT user.* FROM user WHERE 3 = ALL(user.tags)
24
+ def all(opts, *rest)
25
+ equality_to_function("ALL", opts, rest)
26
+ end
26
27
 
27
- # Finds Records that contains a nested set elements
28
- #
29
- # Array Column Type:
30
- # User.where.contains(tags: [1, 3])
31
- # # SELECT user.* FROM user WHERE user.tags @> {1,3}
32
- #
33
- # HStore Column Type:
34
- # User.where.contains(data: { nickname: 'chainer' })
35
- # # SELECT user.* FROM user WHERE user.data @> 'nickname' => 'chainer'
36
- #
37
- # JSONB Column Type:
38
- # User.where.contains(data: { nickname: 'chainer' })
39
- # # SELECT user.* FROM user WHERE user.data @> {'nickname': 'chainer'}
40
- #
41
- # This can also be used along side joined tables
42
- #
43
- # JSONB Column Type Example:
44
- # Tag.joins(:user).where.contains(user: { data: { nickname: 'chainer' } })
45
- # # SELECT tags.* FROM tags INNER JOIN user on user.id = tags.user_id WHERE user.data @> { nickname: 'chainer' }
46
- #
47
- def contains(opts, *rest)
48
- build_where_chain(opts, rest) do |arel|
49
- case arel
50
- when Arel::Nodes::In, Arel::Nodes::Equality
51
- column = left_column(arel) || column_from_association(arel)
52
-
53
- if [:hstore, :jsonb].include?(column.type)
54
- Arel::Nodes::ContainsHStore.new(arel.left, arel.right)
55
- elsif column.try(:array)
56
- Arel::Nodes::ContainsArray.new(arel.left, arel.right)
28
+ # Finds Records that contains a nested set elements
29
+ #
30
+ # Array Column Type:
31
+ # User.where.contains(tags: [1, 3])
32
+ # # SELECT user.* FROM user WHERE user.tags @> {1,3}
33
+ #
34
+ # HStore Column Type:
35
+ # User.where.contains(data: { nickname: 'chainer' })
36
+ # # SELECT user.* FROM user WHERE user.data @> 'nickname' => 'chainer'
37
+ #
38
+ # JSONB Column Type:
39
+ # User.where.contains(data: { nickname: 'chainer' })
40
+ # # SELECT user.* FROM user WHERE user.data @> {'nickname': 'chainer'}
41
+ #
42
+ # This can also be used along side joined tables
43
+ #
44
+ # JSONB Column Type Example:
45
+ # Tag.joins(:user).where.contains(user: { data: { nickname: 'chainer' } })
46
+ # # SELECT tags.* FROM tags INNER JOIN user on user.id = tags.user_id WHERE user.data @> { nickname: 'chainer' }
47
+ #
48
+ def contains(opts, *rest)
49
+ if ActiveRecordExtended::AR_VERSION_GTE_6_1
50
+ return substitute_comparisons(opts, rest, Arel::Nodes::Contains, "contains")
51
+ end
52
+
53
+ build_where_chain(opts, rest) do |arel|
54
+ case arel
55
+ when Arel::Nodes::In, Arel::Nodes::Equality
56
+ column = left_column(arel) || column_from_association(arel)
57
+
58
+ if [:hstore, :jsonb].include?(column.type)
59
+ Arel::Nodes::ContainsHStore.new(arel.left, arel.right)
60
+ elsif column.try(:array)
61
+ Arel::Nodes::ContainsArray.new(arel.left, arel.right)
62
+ else
63
+ raise ArgumentError.new("Invalid argument for .where.contains(), got #{arel.class}")
64
+ end
57
65
  else
58
66
  raise ArgumentError.new("Invalid argument for .where.contains(), got #{arel.class}")
59
67
  end
60
- else
61
- raise ArgumentError.new("Invalid argument for .where.contains(), got #{arel.class}")
62
68
  end
63
69
  end
64
- end
65
70
 
66
- private
71
+ private
67
72
 
68
- def matchable_column?(col, arel)
69
- col.name == arel.left.name.to_s || col.name == arel.left.relation.name.to_s
70
- end
73
+ def matchable_column?(col, arel)
74
+ col.name == arel.left.name.to_s || col.name == arel.left.relation.name.to_s
75
+ end
71
76
 
72
- def column_from_association(arel)
73
- assoc = assoc_from_related_table(arel)
74
- assoc.klass.columns.detect { |col| matchable_column?(col, arel) } if assoc
75
- end
77
+ def column_from_association(arel)
78
+ assoc = assoc_from_related_table(arel)
79
+ assoc.klass.columns.detect { |col| matchable_column?(col, arel) } if assoc
80
+ end
76
81
 
77
- def assoc_from_related_table(arel)
78
- @scope.klass.reflect_on_association(arel.left.relation.name.to_sym) ||
79
- @scope.klass.reflect_on_association(arel.left.relation.name.singularize.to_sym)
80
- end
82
+ def assoc_from_related_table(arel)
83
+ @scope.klass.reflect_on_association(arel.left.relation.name.to_sym) ||
84
+ @scope.klass.reflect_on_association(arel.left.relation.name.singularize.to_sym)
85
+ end
81
86
 
82
- def left_column(arel)
83
- @scope.klass.columns_hash[arel.left.name] || @scope.klass.columns_hash[arel.left.relation.name]
84
- end
87
+ def left_column(arel)
88
+ @scope.klass.columns_hash[arel.left.name] || @scope.klass.columns_hash[arel.left.relation.name]
89
+ end
85
90
 
86
- def equality_to_function(function_name, opts, rest)
87
- build_where_chain(opts, rest) do |arel|
88
- case arel
89
- when Arel::Nodes::Equality
90
- Arel::Nodes::Equality.new(arel.right, Arel::Nodes::NamedFunction.new(function_name, [arel.left]))
91
- else
92
- raise ArgumentError.new("Invalid argument for .where.#{function_name.downcase}(), got #{arel.class}")
91
+ def equality_to_function(function_name, opts, rest)
92
+ build_where_chain(opts, rest) do |arel|
93
+ case arel
94
+ when Arel::Nodes::Equality
95
+ Arel::Nodes::Equality.new(arel.right, Arel::Nodes::NamedFunction.new(function_name, [arel.left]))
96
+ else
97
+ raise ArgumentError.new("Invalid argument for .where.#{function_name.downcase}(), got #{arel.class}")
98
+ end
93
99
  end
94
100
  end
95
- end
96
101
 
97
- def substitute_comparisons(opts, rest, arel_node_class, method)
98
- build_where_chain(opts, rest) do |arel|
99
- case arel
100
- when Arel::Nodes::In, Arel::Nodes::Equality
101
- arel_node_class.new(arel.left, arel.right)
102
- else
103
- raise ArgumentError.new("Invalid argument for .where.#{method}(), got #{arel.class}")
102
+ def substitute_comparisons(opts, rest, arel_node_class, method)
103
+ build_where_chain(opts, rest) do |arel|
104
+ case arel
105
+ when Arel::Nodes::In, Arel::Nodes::Equality
106
+ arel_node_class.new(arel.left, arel.right)
107
+ else
108
+ raise ArgumentError.new("Invalid argument for .where.#{method}(), got #{arel.class}")
109
+ end
104
110
  end
105
111
  end
106
- end
107
112
 
108
- def build_where_clause_for(scope, opts, rest)
109
- if ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR == 1
110
- scope.send(:build_where_clause, opts, rest)
111
- else
112
- scope.send(:where_clause_factory).build(opts, rest)
113
+ def build_where_clause_for(scope, opts, rest)
114
+ if ActiveRecordExtended::AR_VERSION_GTE_6_1
115
+ scope.send(:build_where_clause, opts, rest)
116
+ else
117
+ scope.send(:where_clause_factory).build(opts, rest)
118
+ end
113
119
  end
114
120
  end
115
121
  end
@@ -118,7 +124,7 @@ end
118
124
  module ActiveRecord
119
125
  module QueryMethods
120
126
  class WhereChain
121
- prepend ActiveRecordExtended::WhereChain
127
+ prepend ActiveRecordExtended::QueryMethods::WhereChain
122
128
 
123
129
  def build_where_chain(opts, rest, &block)
124
130
  where_clause = build_where_clause_for(@scope, opts, rest)
@@ -4,8 +4,8 @@ module ActiveRecordExtended
4
4
  module QueryMethods
5
5
  module Window
6
6
  class DefineWindowChain
7
- include ::ActiveRecordExtended::Utilities::Support
8
- include ::ActiveRecordExtended::Utilities::OrderBy
7
+ include ActiveRecordExtended::Utilities::Support
8
+ include ActiveRecordExtended::Utilities::OrderBy
9
9
 
10
10
  def initialize(scope, window_name)
11
11
  @scope = scope
@@ -24,7 +24,7 @@ module ActiveRecordExtended
24
24
  end
25
25
 
26
26
  class WindowSelectBuilder
27
- include ::ActiveRecordExtended::Utilities::Support
27
+ include ActiveRecordExtended::Utilities::Support
28
28
 
29
29
  def initialize(window_function, args, window_name)
30
30
  @window_function = window_function
@@ -4,12 +4,12 @@ module ActiveRecordExtended
4
4
  module QueryMethods
5
5
  module WithCTE
6
6
  class WithCTE
7
- include ::ActiveRecordExtended::Utilities::Support
7
+ include ActiveRecordExtended::Utilities::Support
8
8
  include Enumerable
9
9
  extend Forwardable
10
10
 
11
11
  def_delegators :@with_values, :empty?, :blank?, :present?
12
- attr_reader :with_values, :with_keys
12
+ attr_reader :with_values, :with_keys, :materialized_keys, :not_materialized_keys
13
13
 
14
14
  # @param [ActiveRecord::Relation] scope
15
15
  def initialize(scope)
@@ -33,6 +33,16 @@ module ActiveRecordExtended
33
33
  pipe_cte_with!(value)
34
34
  end
35
35
 
36
+ # @return [Boolean]
37
+ def materialized_key?(key)
38
+ materialized_keys.include?(key.to_sym)
39
+ end
40
+
41
+ # @return [Boolean]
42
+ def not_materialized_key?(key)
43
+ not_materialized_keys.include?(key.to_sym)
44
+ end
45
+
36
46
  # @param [Hash, WithCTE] value
37
47
  def pipe_cte_with!(value)
38
48
  return if value.nil? || value.empty?
@@ -44,6 +54,10 @@ module ActiveRecordExtended
44
54
  # Ensure we follow FIFO pattern.
45
55
  # If the parent has similar CTE alias keys, we want to favor the parent's expressions over its children's.
46
56
  if expression.is_a?(ActiveRecord::Relation) && expression.with_values?
57
+ # Add child's materialized keys to the parent
58
+ @materialized_keys += expression.cte.materialized_keys
59
+ @not_materialized_keys += expression.cte.not_materialized_keys
60
+
47
61
  pipe_cte_with!(expression.cte)
48
62
  expression.cte.reset!
49
63
  end
@@ -58,6 +72,8 @@ module ActiveRecordExtended
58
72
  def reset!
59
73
  @with_keys = []
60
74
  @with_values = {}
75
+ @materialized_keys = Set.new
76
+ @not_materialized_keys = Set.new
61
77
  end
62
78
  end
63
79
 
@@ -75,6 +91,32 @@ module ActiveRecordExtended
75
91
  scope.cte.pipe_cte_with!(args)
76
92
  end
77
93
  end
94
+
95
+ # @param [Hash, WithCTE] args
96
+ def materialized(args)
97
+ @scope.tap do |scope|
98
+ args.each_pair do |name, _expression|
99
+ sym_name = name.to_sym
100
+ raise ArgumentError.new("CTE already set as not_materialized") if scope.cte.not_materialized_key?(sym_name)
101
+
102
+ scope.cte.materialized_keys << sym_name
103
+ end
104
+ scope.cte.pipe_cte_with!(args)
105
+ end
106
+ end
107
+
108
+ # @param [Hash, WithCTE] args
109
+ def not_materialized(args)
110
+ @scope.tap do |scope|
111
+ args.each_pair do |name, _expression|
112
+ sym_name = name.to_sym
113
+ raise ArgumentError.new("CTE already set as materialized") if scope.cte.materialized_key?(sym_name)
114
+
115
+ scope.cte.not_materialized_keys << sym_name
116
+ end
117
+ scope.cte.pipe_cte_with!(args)
118
+ end
119
+ end
78
120
  end
79
121
 
80
122
  # @return [WithCTE]
@@ -113,14 +155,14 @@ module ActiveRecordExtended
113
155
 
114
156
  # @param [Hash, WithCTE] opts
115
157
  def with(opts = :chain, *rest)
116
- return WithChain.new(spawn) if :chain == opts
158
+ return WithChain.new(spawn) if opts == :chain
117
159
 
118
160
  opts.blank? ? self : spawn.with!(opts, *rest)
119
161
  end
120
162
 
121
163
  # @param [Hash, WithCTE] opts
122
164
  def with!(opts = :chain, *_rest)
123
- return WithChain.new(self) if :chain == opts
165
+ return WithChain.new(self) if opts == :chain
124
166
 
125
167
  tap do |scope|
126
168
  scope.cte ||= WithCTE.new(self)
@@ -134,6 +176,9 @@ module ActiveRecordExtended
134
176
  cte_statements = cte.map do |name, expression|
135
177
  grouped_expression = cte.generate_grouping(expression)
136
178
  cte_name = cte.to_arel_sql(cte.double_quote(name.to_s))
179
+
180
+ grouped_expression = add_materialized_modifier(grouped_expression, cte, name)
181
+
137
182
  Arel::Nodes::As.new(cte_name, grouped_expression)
138
183
  end
139
184
 
@@ -143,6 +188,18 @@ module ActiveRecordExtended
143
188
  arel.with(cte_statements)
144
189
  end
145
190
  end
191
+
192
+ private
193
+
194
+ def add_materialized_modifier(expression, cte, name)
195
+ if cte.materialized_key?(name)
196
+ Arel::Nodes::SqlLiteral.new("MATERIALIZED #{expression.to_sql}")
197
+ elsif cte.not_materialized_key?(name)
198
+ Arel::Nodes::SqlLiteral.new("NOT MATERIALIZED #{expression.to_sql}")
199
+ else
200
+ expression
201
+ end
202
+ end
146
203
  end
147
204
  end
148
205
  end
@@ -49,7 +49,7 @@ module ActiveRecordExtended
49
49
  obj.each_pair do |o_key, o_value|
50
50
  new_hash["#{tbl_or_col}.#{o_key}"] = o_value
51
51
  end
52
- elsif ::ActiveRecord::QueryMethods::VALID_DIRECTIONS.include?(obj)
52
+ elsif ActiveRecord::QueryMethods::VALID_DIRECTIONS.include?(obj)
53
53
  new_hash[tbl_or_col] = obj
54
54
  elsif obj.nil?
55
55
  new_hash[tbl_or_col.to_s] = :asc
@@ -113,7 +113,7 @@ module ActiveRecordExtended
113
113
  case value.to_s
114
114
  # Ignore keys that contain double quotes or a Arel.star (*)[all columns]
115
115
  # or if a table has already been explicitly declared (ex: users.id)
116
- when "*", /((^".+"$)|(^[[:alpha:]]+\.[[:alnum:]]+))/
116
+ when "*", /((^".+"$)|(^[[:alpha:]]+\.[[:alnum:]]+)|\(.+\))/
117
117
  value
118
118
  else
119
119
  PG::Connection.quote_ident(value.to_s)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordExtended
4
- VERSION = "2.0.3"
4
+ VERSION = "3.1.0"
5
5
  end
@@ -1,10 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record_extended/version"
4
- require "active_record_extended/utilities/support"
5
- require "active_record_extended/utilities/order_by"
6
- require "active_record_extended/active_record"
7
- require "active_record_extended/arel"
4
+
5
+ require "active_record"
6
+ require "active_record/relation"
7
+ require "active_record/relation/merger"
8
+ require "active_record/relation/query_methods"
8
9
 
9
10
  module ActiveRecordExtended
11
+ extend ActiveSupport::Autoload
12
+
13
+ AR_VERSION_GTE_6_1 = Gem::Requirement.new(">= 6.1").satisfied_by?(ActiveRecord.gem_version)
14
+
15
+ module Utilities
16
+ extend ActiveSupport::Autoload
17
+
18
+ eager_autoload do
19
+ autoload :OrderBy
20
+ autoload :Support
21
+ end
22
+ end
23
+
24
+ module Patch
25
+ extend ActiveSupport::Autoload
26
+
27
+ eager_autoload do
28
+ autoload :ArrayHandlerPatch
29
+ autoload :RelationPatch
30
+ autoload :WhereClausePatch
31
+ end
32
+ end
33
+
34
+ module QueryMethods
35
+ extend ActiveSupport::Autoload
36
+
37
+ eager_autoload do
38
+ autoload :AnyOf
39
+ autoload :Either
40
+ autoload :FosterSelect
41
+ autoload :Inet
42
+ autoload :Json
43
+ autoload :Unionize
44
+ autoload :WhereChain
45
+ autoload :Window
46
+ autoload :WithCTE
47
+ end
48
+ end
49
+
50
+ def self.eager_load!
51
+ super
52
+ ActiveRecordExtended::Utilities.eager_load!
53
+ ActiveRecordExtended::Patch.eager_load!
54
+ ActiveRecordExtended::QueryMethods.eager_load!
55
+ end
56
+ end
57
+
58
+ ActiveSupport.on_load(:active_record) do
59
+ require "active_record_extended/arel"
60
+ ActiveRecordExtended.eager_load!
10
61
  end