active_record_extended 2.0.3 → 3.2.1

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 +7 -31
  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 +3 -3
  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 +70 -9
  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 +34 -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
@@ -5,23 +5,15 @@ module ActiveRecordExtended
5
5
  module AnyOf
6
6
  def any_of(*queries)
7
7
  queries = hash_map_queries(queries)
8
- build_query(queries) do |arel_query, binds|
9
- if binds.any?
10
- @scope.where(unprepared_query(arel_query.to_sql), *binds)
11
- else
12
- @scope.where(arel_query)
13
- end
8
+ build_query(queries) do |arel_query|
9
+ @scope.where(arel_query)
14
10
  end
15
11
  end
16
12
 
17
13
  def none_of(*queries)
18
14
  queries = hash_map_queries(queries)
19
- build_query(queries) do |arel_query, binds|
20
- if binds.any?
21
- @scope.where.not(unprepared_query(arel_query.to_sql), *binds)
22
- else
23
- @scope.where.not(arel_query)
24
- end
15
+ build_query(queries) do |arel_query|
16
+ @scope.where.not(arel_query)
25
17
  end
26
18
  end
27
19
 
@@ -29,7 +21,7 @@ module ActiveRecordExtended
29
21
 
30
22
  def hash_map_queries(queries)
31
23
  if queries.size == 1 && queries.first.is_a?(Hash)
32
- queries.first.each_pair.map { |attr, predicate| Hash[attr, predicate] }
24
+ queries.first.each_pair.map { |attr, predicate| { attr => predicate } }
33
25
  else
34
26
  queries
35
27
  end
@@ -37,7 +29,7 @@ module ActiveRecordExtended
37
29
 
38
30
  def build_query(queries)
39
31
  query_map = construct_query_mappings(queries)
40
- query = yield(query_map[:arel_query], query_map[:binds])
32
+ query = yield(query_map[:arel_query])
41
33
  query
42
34
  .joins(query_map[:joins].to_a)
43
35
  .includes(query_map[:includes].to_a)
@@ -51,29 +43,13 @@ module ActiveRecordExtended
51
43
  query_map[:joins] << translate_reference(query.joins_values) if query.joins_values.any?
52
44
  query_map[:includes] << translate_reference(query.includes_values) if query.includes_values.any?
53
45
  query_map[:references] << translate_reference(query.references_values) if query.references_values.any?
54
- query_map[:binds] += bind_attributes(query)
55
46
  query.arel.constraints.reduce(:and)
56
47
  end.reduce(:or)
57
48
  end
58
49
  end
59
50
 
60
- # Rails 5.1 fix
61
- # In Rails 5.2 the arel table maintains attribute binds
62
- def bind_attributes(query)
63
- return [] unless query.respond_to?(:bound_attributes)
64
-
65
- query.bound_attributes.map(&:value)
66
- end
67
-
68
- # Rails 5.1 fix
69
- def unprepared_query(query)
70
- query.gsub(/((?<!\\)'.*?(?<!\\)'|(?<!\\)".*?(?<!\\)")|(=\ \$\d+)/) do |match|
71
- Regexp.last_match(2)&.gsub(/=\ \$\d+/, "= ?") || match
72
- end
73
- end
74
-
75
51
  def translate_reference(reference)
76
- reference.map { |ref| ref.try(:to_sql) || ref }.compact
52
+ reference.filter_map { |ref| ref.try(:to_sql) || ref }
77
53
  end
78
54
 
79
55
  def generate_where_clause(query)
@@ -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
@@ -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,18 +155,23 @@ 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
- def with!(opts = :chain, *_rest)
123
- return WithChain.new(self) if :chain == opts
124
-
125
- tap do |scope|
126
- scope.cte ||= WithCTE.new(self)
127
- scope.cte.pipe_cte_with!(opts)
164
+ def with!(opts = :chain, *rest)
165
+ case opts
166
+ when :chain
167
+ WithChain.new(self)
168
+ when :recursive
169
+ WithChain.new(self).recursive(*rest)
170
+ else
171
+ tap do |scope|
172
+ scope.cte ||= WithCTE.new(self)
173
+ scope.cte.pipe_cte_with!(opts)
174
+ end
128
175
  end
129
176
  end
130
177
 
@@ -134,6 +181,8 @@ module ActiveRecordExtended
134
181
  cte_statements = cte.map do |name, expression|
135
182
  grouped_expression = cte.generate_grouping(expression)
136
183
  cte_name = cte.to_arel_sql(cte.double_quote(name.to_s))
184
+ grouped_expression = add_materialized_modifier(grouped_expression, cte, name)
185
+
137
186
  Arel::Nodes::As.new(cte_name, grouped_expression)
138
187
  end
139
188
 
@@ -143,6 +192,18 @@ module ActiveRecordExtended
143
192
  arel.with(cte_statements)
144
193
  end
145
194
  end
195
+
196
+ private
197
+
198
+ def add_materialized_modifier(expression, cte, name)
199
+ if cte.materialized_key?(name)
200
+ Arel.sql("MATERIALIZED #{expression.to_sql}")
201
+ elsif cte.not_materialized_key?(name)
202
+ Arel.sql("NOT MATERIALIZED #{expression.to_sql}")
203
+ else
204
+ expression
205
+ end
206
+ end
146
207
  end
147
208
  end
148
209
  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.2.1"
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