active_record_extended_telescope 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +870 -0
  3. data/lib/active_record_extended.rb +10 -0
  4. data/lib/active_record_extended/active_record.rb +25 -0
  5. data/lib/active_record_extended/active_record/relation_patch.rb +50 -0
  6. data/lib/active_record_extended/arel.rb +7 -0
  7. data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
  8. data/lib/active_record_extended/arel/nodes.rb +49 -0
  9. data/lib/active_record_extended/arel/predications.rb +50 -0
  10. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  11. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +122 -0
  12. data/lib/active_record_extended/patch/5_1/where_clause.rb +11 -0
  13. data/lib/active_record_extended/patch/5_2/where_clause.rb +11 -0
  14. data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +20 -0
  15. data/lib/active_record_extended/query_methods/any_of.rb +93 -0
  16. data/lib/active_record_extended/query_methods/either.rb +62 -0
  17. data/lib/active_record_extended/query_methods/inet.rb +88 -0
  18. data/lib/active_record_extended/query_methods/json.rb +329 -0
  19. data/lib/active_record_extended/query_methods/select.rb +118 -0
  20. data/lib/active_record_extended/query_methods/unionize.rb +249 -0
  21. data/lib/active_record_extended/query_methods/where_chain.rb +132 -0
  22. data/lib/active_record_extended/query_methods/window.rb +93 -0
  23. data/lib/active_record_extended/query_methods/with_cte.rb +150 -0
  24. data/lib/active_record_extended/utilities/order_by.rb +77 -0
  25. data/lib/active_record_extended/utilities/support.rb +178 -0
  26. data/lib/active_record_extended/version.rb +5 -0
  27. data/lib/active_record_extended_telescope.rb +4 -0
  28. data/spec/active_record_extended_spec.rb +7 -0
  29. data/spec/query_methods/any_of_spec.rb +131 -0
  30. data/spec/query_methods/array_query_spec.rb +64 -0
  31. data/spec/query_methods/either_spec.rb +59 -0
  32. data/spec/query_methods/hash_query_spec.rb +45 -0
  33. data/spec/query_methods/inet_query_spec.rb +112 -0
  34. data/spec/query_methods/json_spec.rb +157 -0
  35. data/spec/query_methods/select_spec.rb +115 -0
  36. data/spec/query_methods/unionize_spec.rb +165 -0
  37. data/spec/query_methods/window_spec.rb +51 -0
  38. data/spec/query_methods/with_cte_spec.rb +50 -0
  39. data/spec/spec_helper.rb +28 -0
  40. data/spec/sql_inspections/any_of_sql_spec.rb +41 -0
  41. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
  42. data/spec/sql_inspections/arel/array_spec.rb +63 -0
  43. data/spec/sql_inspections/arel/inet_spec.rb +66 -0
  44. data/spec/sql_inspections/contains_sql_queries_spec.rb +47 -0
  45. data/spec/sql_inspections/either_sql_spec.rb +55 -0
  46. data/spec/sql_inspections/json_sql_spec.rb +82 -0
  47. data/spec/sql_inspections/unionize_sql_spec.rb +124 -0
  48. data/spec/sql_inspections/window_sql_spec.rb +98 -0
  49. data/spec/sql_inspections/with_cte_sql_spec.rb +95 -0
  50. data/spec/support/database_cleaner.rb +15 -0
  51. data/spec/support/models.rb +68 -0
  52. metadata +245 -0
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
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"
8
+
9
+ module ActiveRecordExtended
10
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_record/relation"
5
+ require "active_record/relation/merger"
6
+ require "active_record/relation/query_methods"
7
+
8
+ require "active_record_extended/predicate_builder/array_handler_decorator"
9
+
10
+ require "active_record_extended/active_record/relation_patch"
11
+
12
+ require "active_record_extended/query_methods/where_chain"
13
+ require "active_record_extended/query_methods/with_cte"
14
+ require "active_record_extended/query_methods/unionize"
15
+ require "active_record_extended/query_methods/any_of"
16
+ require "active_record_extended/query_methods/either"
17
+ require "active_record_extended/query_methods/inet"
18
+ require "active_record_extended/query_methods/json"
19
+ require "active_record_extended/query_methods/select"
20
+
21
+ if Gem::Requirement.new("~> 5.1.0").satisfied_by?(ActiveRecord.gem_version)
22
+ require "active_record_extended/patch/5_1/where_clause"
23
+ else
24
+ require "active_record_extended/patch/5_2/where_clause"
25
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_extended/query_methods/window"
4
+ require "active_record_extended/query_methods/unionize"
5
+ require "active_record_extended/query_methods/json"
6
+
7
+ module ActiveRecordExtended
8
+ module RelationPatch
9
+ module QueryDelegation
10
+ delegate :with, :define_window, :select_window, :foster_select, to: :all
11
+ delegate(*::ActiveRecordExtended::QueryMethods::Unionize::UNIONIZE_METHODS, to: :all)
12
+ delegate(*::ActiveRecordExtended::QueryMethods::Json::JSON_QUERY_METHODS, to: :all)
13
+ end
14
+
15
+ module Merger
16
+ def normal_values
17
+ super + [:union, :define_window]
18
+ end
19
+
20
+ def merge
21
+ merge_ctes!
22
+ super
23
+ end
24
+
25
+ def merge_ctes!
26
+ return unless other.with_values?
27
+
28
+ if other.recursive_value? && !relation.recursive_value?
29
+ relation.with!(:chain).recursive(other.cte)
30
+ else
31
+ relation.with!(other.cte)
32
+ end
33
+ end
34
+ end
35
+
36
+ module ArelBuildPatch
37
+ def build_arel(*aliases)
38
+ super.tap do |arel|
39
+ build_windows(arel) if window_values?
40
+ build_unions(arel) if union_values?
41
+ build_with(arel) if with_values?
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ ActiveRecord::Relation.prepend(ActiveRecordExtended::RelationPatch::ArelBuildPatch)
49
+ ActiveRecord::Relation::Merger.prepend(ActiveRecordExtended::RelationPatch::Merger)
50
+ ActiveRecord::Querying.prepend(ActiveRecordExtended::RelationPatch::QueryDelegation)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_extended/arel/nodes"
4
+ require "active_record_extended/arel/sql_literal"
5
+ require "active_record_extended/arel/aggregate_function_name"
6
+ require "active_record_extended/arel/predications"
7
+ require "active_record_extended/arel/visitors/postgresql_decorator"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel
4
+ module Nodes
5
+ class AggregateFunctionName < ::Arel::Nodes::Node
6
+ include Arel::Predications
7
+ include Arel::WindowPredications
8
+ attr_accessor :name, :expressions, :distinct, :alias, :orderings
9
+
10
+ def initialize(name, expr, distinct = false)
11
+ super()
12
+ @name = name.to_s.upcase
13
+ @expressions = expr
14
+ @distinct = distinct
15
+ end
16
+
17
+ def order_by(expr)
18
+ @orderings = expr
19
+ self
20
+ end
21
+
22
+ def as(aliaz)
23
+ self.alias = SqlLiteral.new(aliaz)
24
+ self
25
+ end
26
+
27
+ def hash
28
+ [@name, @expressions, @distinct, @alias, @orderings].hash
29
+ end
30
+
31
+ def eql?(other)
32
+ self.class == other.class &&
33
+ expressions == other.expressions &&
34
+ orderings == other.orderings &&
35
+ distinct == other.distinct
36
+ end
37
+ alias == eql?
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "arel/nodes/binary"
4
+ require "arel/nodes/function"
5
+
6
+ module Arel
7
+ module Nodes
8
+ if Gem::Requirement.new("< 6.1").satisfied_by?(ActiveRecord.gem_version)
9
+ ["Contains", "Overlaps"].each { |binary_node_name| const_set(binary_node_name, Class.new(::Arel::Nodes::Binary)) }
10
+ end
11
+
12
+ [
13
+ "ContainsHStore",
14
+ "ContainsArray",
15
+ "ContainedInArray"
16
+ ].each { |binary_node_name| const_set(binary_node_name, Class.new(::Arel::Nodes::Binary)) }
17
+
18
+ [
19
+ "RowToJson",
20
+ "JsonBuildObject",
21
+ "JsonbBuildObject",
22
+ "ToJson",
23
+ "ToJsonb",
24
+ "Array",
25
+ "ArrayAgg"
26
+ ].each do |function_node_name|
27
+ func_klass = Class.new(::Arel::Nodes::Function) do
28
+ def initialize(*args)
29
+ super
30
+ return if @expressions.is_a?(::Array)
31
+
32
+ @expressions = @expressions.is_a?(::Arel::Nodes::Node) ? [@expressions] : [::Arel.sql(@expressions)]
33
+ end
34
+ end
35
+
36
+ const_set(function_node_name, func_klass)
37
+ end
38
+
39
+ module Inet
40
+ [
41
+ "Contains",
42
+ "ContainsEquals",
43
+ "ContainedWithin",
44
+ "ContainedWithinEquals",
45
+ "ContainsOrContainedWithin"
46
+ ].each { |binary_node_name| const_set(binary_node_name, Class.new(::Arel::Nodes::Binary)) }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "arel/predications"
4
+
5
+ module Arel
6
+ module Predications
7
+ def any(other)
8
+ any_tags_function = Arel::Nodes::NamedFunction.new("ANY", [self])
9
+ Arel::Nodes::Equality.new(Nodes.build_quoted(other, self), any_tags_function)
10
+ end
11
+
12
+ def all(other)
13
+ all_tags_function = Arel::Nodes::NamedFunction.new("ALL", [self])
14
+ Arel::Nodes::Equality.new(Nodes.build_quoted(other, self), all_tags_function)
15
+ end
16
+
17
+ def overlaps(other)
18
+ Nodes::Overlaps.new(self, Nodes.build_quoted(other, self))
19
+ end
20
+ alias overlap overlaps
21
+
22
+ def contains(other)
23
+ Nodes::Contains.new self, Nodes.build_quoted(other, self)
24
+ end
25
+
26
+ def contained_in_array(other)
27
+ Nodes::ContainedInArray.new self, Nodes.build_quoted(other, self)
28
+ end
29
+
30
+ def inet_contains(other)
31
+ Nodes::Inet::Contains.new self, Nodes.build_quoted(other, self)
32
+ end
33
+
34
+ def inet_contains_or_is_contained_within(other)
35
+ Nodes::Inet::ContainsOrContainedWithin.new self, Nodes.build_quoted(other, self)
36
+ end
37
+
38
+ def inet_contained_within(other)
39
+ Nodes::Inet::ContainedWithin.new self, Nodes.build_quoted(other, self)
40
+ end
41
+
42
+ def inet_contained_within_or_equals(other)
43
+ Nodes::Inet::ContainedWithinEquals.new self, Nodes.build_quoted(other, self)
44
+ end
45
+
46
+ def inet_contains_or_equals(other)
47
+ Nodes::Inet::ContainsEquals.new self, Nodes.build_quoted(other, self)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "arel/nodes/sql_literal"
4
+
5
+ # CTE alias fix for Rails 6.1
6
+ module Arel
7
+ module Nodes
8
+ module SqlLiteralDecorator
9
+ def name
10
+ self
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ Arel::Nodes::SqlLiteral.prepend(Arel::Nodes::SqlLiteralDecorator)
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "arel/visitors/postgresql"
4
+
5
+ module ActiveRecordExtended
6
+ module Visitors
7
+ module PostgreSQLDecorator
8
+ private
9
+
10
+ # rubocop:disable Naming/MethodName
11
+
12
+ def visit_Arel_Nodes_Overlaps(object, collector)
13
+ infix_value object, collector, " && "
14
+ end
15
+
16
+ def visit_Arel_Nodes_Contains(object, collector)
17
+ left_column = object.left.relation.name.classify.constantize.columns.detect do |col|
18
+ matchable_column?(col, object)
19
+ end
20
+
21
+ if [:hstore, :jsonb].include?(left_column&.type)
22
+ visit_Arel_Nodes_ContainsHStore(object, collector)
23
+ elsif left_column.try(:array)
24
+ visit_Arel_Nodes_ContainsArray(object, collector)
25
+ else
26
+ visit_Arel_Nodes_Inet_Contains(object, collector)
27
+ end
28
+ end
29
+
30
+ def visit_Arel_Nodes_ContainsArray(object, collector)
31
+ infix_value object, collector, " @> "
32
+ end
33
+
34
+ def visit_Arel_Nodes_ContainsHStore(object, collector)
35
+ infix_value object, collector, " @> "
36
+ end
37
+
38
+ def visit_Arel_Nodes_ContainedInHStore(object, collector)
39
+ infix_value object, collector, " <@ "
40
+ end
41
+
42
+ def visit_Arel_Nodes_ContainedInArray(object, collector)
43
+ infix_value object, collector, " <@ "
44
+ end
45
+
46
+ def visit_Arel_Nodes_Inet_ContainedWithin(object, collector)
47
+ infix_value object, collector, " << "
48
+ end
49
+
50
+ def visit_Arel_Nodes_RowToJson(object, collector)
51
+ aggregate "ROW_TO_JSON", object, collector
52
+ end
53
+
54
+ def visit_Arel_Nodes_JsonBuildObject(object, collector)
55
+ aggregate "JSON_BUILD_OBJECT", object, collector
56
+ end
57
+
58
+ def visit_Arel_Nodes_JsonbBuildObject(object, collector)
59
+ aggregate "JSONB_BUILD_OBJECT", object, collector
60
+ end
61
+
62
+ def visit_Arel_Nodes_ToJson(object, collector)
63
+ aggregate "TO_JSON", object, collector
64
+ end
65
+
66
+ def visit_Arel_Nodes_ToJsonb(object, collector)
67
+ aggregate "TO_JSONB", object, collector
68
+ end
69
+
70
+ def visit_Arel_Nodes_Array(object, collector)
71
+ aggregate "ARRAY", object, collector
72
+ end
73
+
74
+ def visit_Arel_Nodes_ArrayAgg(object, collector)
75
+ aggregate "ARRAY_AGG", object, collector
76
+ end
77
+
78
+ def visit_Arel_Nodes_AggregateFunctionName(object, collector)
79
+ collector << "#{object.name}("
80
+ collector << "DISTINCT " if object.distinct
81
+ collector = inject_join(object.expressions, collector, ", ")
82
+
83
+ if object.orderings
84
+ collector << " ORDER BY "
85
+ collector = inject_join(object.orderings, collector, ", ")
86
+ end
87
+ collector << ")"
88
+
89
+ if object.alias
90
+ collector << " AS "
91
+ visit object.alias, collector
92
+ else
93
+ collector
94
+ end
95
+ end
96
+
97
+ def visit_Arel_Nodes_Inet_Contains(object, collector)
98
+ infix_value object, collector, " >> "
99
+ end
100
+
101
+ def visit_Arel_Nodes_Inet_ContainedWithinEquals(object, collector)
102
+ infix_value object, collector, " <<= "
103
+ end
104
+
105
+ def visit_Arel_Nodes_Inet_ContainsEquals(object, collector)
106
+ infix_value object, collector, " >>= "
107
+ end
108
+
109
+ def visit_Arel_Nodes_Inet_ContainsOrContainedWithin(object, collector)
110
+ infix_value object, collector, " && "
111
+ end
112
+
113
+ def matchable_column?(col, object)
114
+ col.name == object.left.name.to_s || col.name == object.left.relation.name.to_s
115
+ end
116
+
117
+ # rubocop:enable Naming/MethodName
118
+ end
119
+ end
120
+ end
121
+
122
+ Arel::Visitors::PostgreSQL.prepend(ActiveRecordExtended::Visitors::PostgreSQLDecorator)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module WhereClause
5
+ def modified_predicates(&block)
6
+ ::ActiveRecord::Relation::WhereClause.new(predicates.map(&block), binds)
7
+ end
8
+ end
9
+ end
10
+
11
+ ActiveRecord::Relation::WhereClause.prepend(ActiveRecordExtended::WhereClause)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module WhereClause
5
+ def modified_predicates(&block)
6
+ ::ActiveRecord::Relation::WhereClause.new(predicates.map(&block))
7
+ end
8
+ end
9
+ end
10
+
11
+ ActiveRecord::Relation::WhereClause.prepend(ActiveRecordExtended::WhereClause)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/relation/predicate_builder"
4
+ require "active_record/relation/predicate_builder/array_handler"
5
+
6
+ module ActiveRecordExtended
7
+ module ArrayHandlerDecorator
8
+ def call(attribute, value)
9
+ cache = ActiveRecord::Base.connection.schema_cache
10
+ if cache.data_source_exists?(attribute.relation.name)
11
+ column = cache.columns(attribute.relation.name).detect { |col| col.name.to_s == attribute.name.to_s }
12
+ return attribute.eq(value) if column.try(:array)
13
+ end
14
+
15
+ super(attribute, value)
16
+ end
17
+ end
18
+ end
19
+
20
+ ActiveRecord::PredicateBuilder::ArrayHandler.prepend(ActiveRecordExtended::ArrayHandlerDecorator)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module QueryMethods
5
+ module AnyOf
6
+ def any_of(*queries)
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
14
+ end
15
+ end
16
+
17
+ def none_of(*queries)
18
+ 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
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def hash_map_queries(queries)
31
+ if queries.size == 1 && queries.first.is_a?(Hash)
32
+ queries.first.each_pair.map { |attr, predicate| Hash[attr, predicate] }
33
+ else
34
+ queries
35
+ end
36
+ end
37
+
38
+ def build_query(queries)
39
+ query_map = construct_query_mappings(queries)
40
+ query = yield(query_map[:arel_query], query_map[:binds])
41
+ query
42
+ .joins(query_map[:joins].to_a)
43
+ .includes(query_map[:includes].to_a)
44
+ .references(query_map[:references].to_a)
45
+ end
46
+
47
+ def construct_query_mappings(queries) # rubocop:disable Metrics/AbcSize
48
+ { joins: Set.new, references: Set.new, includes: Set.new, arel_query: nil, binds: [] }.tap do |query_map|
49
+ query_map[:arel_query] = queries.map do |raw_query|
50
+ query = generate_where_clause(raw_query)
51
+ query_map[:joins] << translate_reference(query.joins_values) if query.joins_values.any?
52
+ query_map[:includes] << translate_reference(query.includes_values) if query.includes_values.any?
53
+ query_map[:references] << translate_reference(query.references_values) if query.references_values.any?
54
+ query_map[:binds] += bind_attributes(query)
55
+ query.arel.constraints.reduce(:and)
56
+ end.reduce(:or)
57
+ end
58
+ end
59
+
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
+ def translate_reference(reference)
76
+ reference.map { |ref| ref.try(:to_sql) || ref }.compact
77
+ end
78
+
79
+ def generate_where_clause(query)
80
+ case query
81
+ when String, Hash
82
+ @scope.unscoped.where(query)
83
+ when Array
84
+ @scope.unscoped.where(*query)
85
+ else
86
+ query
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ ActiveRecord::QueryMethods::WhereChain.prepend(ActiveRecordExtended::QueryMethods::AnyOf)