active_record_extended_telescope 2.0.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 (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)