active_record_extended 0.1.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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.rubocop.yml +85 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +35 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +92 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +102 -0
- data/active_record_extended.gemspec +32 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/gemfiles/activerecord-51.gemfile +8 -0
- data/gemfiles/activerecord-52+.gemfile +8 -0
- data/gemfiles/activerecord-52.gemfile +8 -0
- data/lib/active_record_extended/active_record.rb +15 -0
- data/lib/active_record_extended/arel/nodes.rb +61 -0
- data/lib/active_record_extended/arel/predications.rb +41 -0
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +73 -0
- data/lib/active_record_extended/arel.rb +6 -0
- data/lib/active_record_extended/patch/5_1/where_clause.rb +11 -0
- data/lib/active_record_extended/patch/5_2/where_clause.rb +11 -0
- data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +20 -0
- data/lib/active_record_extended/query_methods/either.rb +60 -0
- data/lib/active_record_extended/query_methods/where_chain.rb +106 -0
- data/lib/active_record_extended/version.rb +5 -0
- data/lib/active_record_extended.rb +9 -0
- data/spec/active_record_extended_spec.rb +7 -0
- data/spec/query_methods/array_query_spec.rb +64 -0
- data/spec/query_methods/either_spec.rb +36 -0
- data/spec/query_methods/hash_query_spec.rb +45 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/sql_inspections/arel/array_spec.rb +63 -0
- data/spec/sql_inspections/contains_sql_queries_spec.rb +47 -0
- data/spec/sql_inspections/either_sql_spec.rb +55 -0
- data/spec/support/database_cleaner.rb +15 -0
- data/spec/support/models.rb +20 -0
- metadata +203 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "arel/nodes/binary"
|
4
|
+
|
5
|
+
module Arel
|
6
|
+
module Nodes
|
7
|
+
class Overlap < Arel::Nodes::Binary
|
8
|
+
def operator
|
9
|
+
:"&&"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Contains < Arel::Nodes::Binary
|
14
|
+
def operator
|
15
|
+
:>>
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ContainsHStore < Arel::Nodes::Binary
|
20
|
+
def operator
|
21
|
+
:"@>"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class ContainsArray < Arel::Nodes::Binary
|
26
|
+
def operator
|
27
|
+
:"@>"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ContainedInArray < Arel::Nodes::Binary
|
32
|
+
def operator
|
33
|
+
:"<@"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class ContainsEquals < Arel::Nodes::Binary
|
38
|
+
def operator
|
39
|
+
:">>="
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ContainedWithin < Arel::Nodes::Binary
|
44
|
+
def operator
|
45
|
+
:<<
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class ContainedWithinEquals < Arel::Nodes::Binary
|
50
|
+
def operator
|
51
|
+
:"<<="
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Node
|
56
|
+
def group_or(right)
|
57
|
+
Arel::Nodes::Or.new self, Arel::Nodes::Grouping.new(right)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "arel/predications"
|
4
|
+
|
5
|
+
module Arel
|
6
|
+
module Predications
|
7
|
+
def overlap(other)
|
8
|
+
Nodes::Overlap.new(self, Nodes.build_quoted(other, self))
|
9
|
+
end
|
10
|
+
|
11
|
+
def contains(other)
|
12
|
+
Nodes::Contains.new self, Nodes.build_quoted(other, self)
|
13
|
+
end
|
14
|
+
|
15
|
+
def contained_within(other)
|
16
|
+
Nodes::ContainedWithin.new self, Nodes.build_quoted(other, self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def contained_within_or_equals(other)
|
20
|
+
Nodes::ContainedWithinEquals.new self, Nodes.build_quoted(other, self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def contained_in_array(other)
|
24
|
+
Nodes::ContainedInArray.new self, Nodes.build_quoted(other, self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def contains_or_equals(other)
|
28
|
+
Nodes::ContainsEquals.new self, Nodes.build_quoted(other, self)
|
29
|
+
end
|
30
|
+
|
31
|
+
def any(other)
|
32
|
+
any_tags_function = Arel::Nodes::NamedFunction.new("ANY", [self])
|
33
|
+
Arel::Nodes::Equality.new(Nodes.build_quoted(other, self), any_tags_function)
|
34
|
+
end
|
35
|
+
|
36
|
+
def all(other)
|
37
|
+
any_tags_function = Arel::Nodes::NamedFunction.new("ALL", [self])
|
38
|
+
Arel::Nodes::Equality.new(Nodes.build_quoted(other, self), any_tags_function)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,73 @@
|
|
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_Overlap(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 %i[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
|
+
infix_value 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_ContainedWithin(object, collector)
|
39
|
+
infix_value object, collector, " << "
|
40
|
+
end
|
41
|
+
|
42
|
+
def visit_Arel_Nodes_ContainedWithinEquals(object, collector)
|
43
|
+
infix_value object, collector, " <<= "
|
44
|
+
end
|
45
|
+
|
46
|
+
def visit_Arel_Nodes_ContainedInHStore(object, collector)
|
47
|
+
infix_value object, collector, " <@ "
|
48
|
+
end
|
49
|
+
|
50
|
+
def visit_Arel_Nodes_ContainedInArray(object, collector)
|
51
|
+
infix_value object, collector, " <@ "
|
52
|
+
end
|
53
|
+
|
54
|
+
def visit_Arel_Nodes_ContainsEquals(object, collector)
|
55
|
+
infix_value object, collector, " >>= "
|
56
|
+
end
|
57
|
+
|
58
|
+
def visit_Arel_Nodes_AnyOf(object, collector)
|
59
|
+
pp object
|
60
|
+
pp collector
|
61
|
+
collector
|
62
|
+
end
|
63
|
+
|
64
|
+
def matchable_column?(col, object)
|
65
|
+
col.name == object.left.name.to_s || col.name == object.left.relation.name.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
# rubocop:enable Naming/MethodName
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
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,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ar_outer_joins"
|
4
|
+
|
5
|
+
module ActiveRecordExtended
|
6
|
+
module QueryMethods
|
7
|
+
module Either
|
8
|
+
XOR_FIELD_SQL = "(CASE WHEN %<t1>s.%<c1>s IS NULL THEN %<t2>s.%<c2>s ELSE %<t1>s.%<c1>s END) "
|
9
|
+
XOR_FIELD_KEYS = %i[t1 c1 t2 c2].freeze
|
10
|
+
|
11
|
+
def either_join(initial_association, fallback_association)
|
12
|
+
associations = [initial_association, fallback_association]
|
13
|
+
association_options = xor_field_options_for_associations(associations)
|
14
|
+
condition__query = xor_field_sql(association_options) + "= #{table_name}.#{primary_key}"
|
15
|
+
outer_joins(associations).where(Arel.sql(condition__query))
|
16
|
+
end
|
17
|
+
|
18
|
+
def either_order(direction, **associations_and_columns)
|
19
|
+
reflected_columns = map_columns_to_tables(associations_and_columns)
|
20
|
+
conditional_query = xor_field_sql(reflected_columns) + sort_order_sql(direction)
|
21
|
+
outer_joins(associations_and_columns.keys).order(Arel.sql(conditional_query))
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def xor_field_sql(options)
|
27
|
+
XOR_FIELD_SQL % Hash[xor_field_options(options)]
|
28
|
+
end
|
29
|
+
|
30
|
+
def sort_order_sql(dir)
|
31
|
+
%w[asc desc].include?(dir.to_s) ? dir.to_s : "asc"
|
32
|
+
end
|
33
|
+
|
34
|
+
def xor_field_options(options)
|
35
|
+
str_args = options.flatten.take(XOR_FIELD_KEYS.size).map(&:to_s)
|
36
|
+
Hash[XOR_FIELD_KEYS.zip(str_args)]
|
37
|
+
end
|
38
|
+
|
39
|
+
def map_columns_to_tables(associations_and_columns)
|
40
|
+
if associations_and_columns.respond_to?(:transform_keys)
|
41
|
+
associations_and_columns.transform_keys { |assc| reflect_on_association(assc).table_name }
|
42
|
+
else
|
43
|
+
associations_and_columns.each_with_object({}) do |(assc, value), key_table|
|
44
|
+
reflect_table = reflect_on_association(assc).table_name
|
45
|
+
key_table[reflect_table] = value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def xor_field_options_for_associations(associations)
|
51
|
+
associations.each_with_object({}) do |association_name, options|
|
52
|
+
reflection = reflect_on_association(association_name)
|
53
|
+
options[reflection.table_name] = reflection.foreign_key
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
ActiveRecord::Base.extend(ActiveRecordExtended::QueryMethods::Either)
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module WhereChain
|
5
|
+
def overlap(opts, *rest)
|
6
|
+
substitute_comparisons(opts, rest, Arel::Nodes::Overlap, "overlap")
|
7
|
+
end
|
8
|
+
|
9
|
+
def contained_within(opts, *rest)
|
10
|
+
substitute_comparisons(opts, rest, Arel::Nodes::ContainedWithin, "contained_within")
|
11
|
+
end
|
12
|
+
|
13
|
+
def contained_within_or_equals(opts, *rest)
|
14
|
+
substitute_comparisons(opts, rest, Arel::Nodes::ContainedWithinEquals, "contained_within_or_equals")
|
15
|
+
end
|
16
|
+
|
17
|
+
def contains_or_equals(opts, *rest)
|
18
|
+
substitute_comparisons(opts, rest, Arel::Nodes::ContainsEquals, "contains_or_equals")
|
19
|
+
end
|
20
|
+
|
21
|
+
def any(opts, *rest)
|
22
|
+
equality_to_function("ANY", opts, rest)
|
23
|
+
end
|
24
|
+
|
25
|
+
def all(opts, *rest)
|
26
|
+
equality_to_function("ALL", opts, rest)
|
27
|
+
end
|
28
|
+
|
29
|
+
def contains(opts, *rest)
|
30
|
+
build_where_chain(opts, rest) do |arel|
|
31
|
+
case arel
|
32
|
+
when Arel::Nodes::In, Arel::Nodes::Equality
|
33
|
+
column = left_column(arel) || column_from_association(arel)
|
34
|
+
|
35
|
+
if %i[hstore jsonb].include?(column.type)
|
36
|
+
Arel::Nodes::ContainsHStore.new(arel.left, arel.right)
|
37
|
+
elsif column.try(:array)
|
38
|
+
Arel::Nodes::ContainsArray.new(arel.left, arel.right)
|
39
|
+
else
|
40
|
+
raise ArgumentError, "Invalid argument for .where.contains(), got #{arel.class}"
|
41
|
+
end
|
42
|
+
else
|
43
|
+
raise ArgumentError, "Invalid argument for .where.contains(), got #{arel.class}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def matchable_column?(col, arel)
|
51
|
+
col.name == arel.left.name.to_s || col.name == arel.left.relation.name.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def column_from_association(arel)
|
55
|
+
assoc = assoc_from_related_table(arel)
|
56
|
+
assoc.klass.columns.detect { |col| matchable_column?(col, arel) } if assoc
|
57
|
+
end
|
58
|
+
|
59
|
+
def assoc_from_related_table(arel)
|
60
|
+
@scope.klass.reflect_on_association(arel.left.relation.name.to_sym) ||
|
61
|
+
@scope.klass.reflect_on_association(arel.left.relation.name.singularize.to_sym)
|
62
|
+
end
|
63
|
+
|
64
|
+
def left_column(arel)
|
65
|
+
@scope.klass.columns_hash[arel.left.name] || @scope.klass.columns_hash[arel.left.relation.name]
|
66
|
+
end
|
67
|
+
|
68
|
+
def equality_to_function(function_name, opts, rest)
|
69
|
+
build_where_chain(opts, rest) do |arel|
|
70
|
+
case arel
|
71
|
+
when Arel::Nodes::Equality
|
72
|
+
Arel::Nodes::Equality.new(arel.right, Arel::Nodes::NamedFunction.new(function_name, [arel.left]))
|
73
|
+
else
|
74
|
+
raise ArgumentError, "Invalid argument for .where.#{function_name.downcase}(), got #{arel.class}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def substitute_comparisons(opts, rest, arel_node_class, method)
|
80
|
+
build_where_chain(opts, rest) do |arel|
|
81
|
+
case arel
|
82
|
+
when Arel::Nodes::In, Arel::Nodes::Equality
|
83
|
+
arel_node_class.new(arel.left, arel.right)
|
84
|
+
else
|
85
|
+
raise ArgumentError, "Invalid argument for .where.#{method}(), got #{arel.class}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module ActiveRecord
|
93
|
+
module QueryMethods
|
94
|
+
class WhereChain
|
95
|
+
prepend ActiveRecordExtended::WhereChain
|
96
|
+
|
97
|
+
def build_where_chain(opts, rest, &block)
|
98
|
+
where_clause = @scope.send(:where_clause_factory).build(opts, rest)
|
99
|
+
@scope.tap do |scope|
|
100
|
+
scope.references!(PredicateBuilder.references(opts)) if opts.is_a?(Hash)
|
101
|
+
scope.where_clause += where_clause.modified_predicates(&block)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Active Record Array Query Methods" do
|
6
|
+
let!(:one) { Person.create!(tags: [1, 2, 3], personal_id: 33) }
|
7
|
+
let!(:two) { Person.create!(tags: [3, 1, 5], personal_id: 88) }
|
8
|
+
let!(:three) { Person.create!(tags: [2, 8, 20], personal_id: 33) }
|
9
|
+
|
10
|
+
describe "#overlap" do
|
11
|
+
it "Should return matched records" do
|
12
|
+
query = Person.where.overlap(tags: [1])
|
13
|
+
expect(query).to include(one, two)
|
14
|
+
expect(query).to_not include(three)
|
15
|
+
|
16
|
+
query = Person.where.overlap(tags: [2, 3])
|
17
|
+
expect(query).to include(one, two, three)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#contains" do
|
22
|
+
it "returns records that contain elements in an array" do
|
23
|
+
query = Person.where.contains(tags: [1, 3])
|
24
|
+
expect(query).to include(one, two)
|
25
|
+
expect(query).to_not include(three)
|
26
|
+
|
27
|
+
query = Person.where.overlap(tags: [8, 2])
|
28
|
+
expect(query).to include(one, three)
|
29
|
+
expect(query).to_not include(two)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#any" do
|
34
|
+
it "should return any records that match" do
|
35
|
+
query = Person.where.any(tags: 3)
|
36
|
+
expect(query).to include(one, two)
|
37
|
+
expect(query).to_not include(three)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "allows chaining" do
|
41
|
+
query = Person.where.any(tags: 3).where(personal_id: 33)
|
42
|
+
expect(query).to include(one)
|
43
|
+
expect(query).to_not include(two, three)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#all" do
|
48
|
+
let!(:contains_all) { Person.create!(tags: [1], personal_id: 1) }
|
49
|
+
let!(:contains_all_two) { Person.create!(tags: [1], personal_id: 2) }
|
50
|
+
let!(:contains_some) { Person.create!(tags: [1, 2], personal_id: 2) }
|
51
|
+
|
52
|
+
it "should return any records that match" do
|
53
|
+
query = Person.where.all(tags: 1)
|
54
|
+
expect(query).to include(contains_all, contains_all_two)
|
55
|
+
expect(query).to_not include(contains_some)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "allows chaining" do
|
59
|
+
query = Person.where.all(tags: 1).where(personal_id: 1)
|
60
|
+
expect(query).to include(contains_all)
|
61
|
+
expect(query).to_not include(contains_all_two, contains_some)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Active Record Either Methods" do
|
6
|
+
let!(:one) { Person.create! }
|
7
|
+
let!(:two) { Person.create! }
|
8
|
+
let!(:three) { Person.create! }
|
9
|
+
let!(:profile_l) { ProfileL.create!(person_id: one.id, likes: 100) }
|
10
|
+
let!(:profile_r) { ProfileR.create!(person_id: two.id, dislikes: 50) }
|
11
|
+
|
12
|
+
describe ".either_join/2" do
|
13
|
+
it "Should only only return records that belong to profile L or profile R" do
|
14
|
+
query = Person.either_join(:profile_l, :profile_r)
|
15
|
+
expect(query).to include(one, two)
|
16
|
+
expect(query).to_not include(three)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe ".either_order/2" do
|
21
|
+
it "Should not exclude anyone who does not have a relationship" do
|
22
|
+
query = Person.either_order(:asc, profile_l: :likes, profile_r: :dislikes)
|
23
|
+
expect(query).to include(one, two, three)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "Should order people based on their likes and dislikes in ascended order" do
|
27
|
+
query = Person.either_order(:asc, profile_l: :likes, profile_r: :dislikes).where(id: [one.id, two.id])
|
28
|
+
expect(query).to match_array([two, one])
|
29
|
+
end
|
30
|
+
|
31
|
+
it "Should order people based on their likes and dislikes in descending order" do
|
32
|
+
query = Person.either_order(:desc, profile_l: :likes, profile_r: :dislikes).where(id: [one.id, two.id])
|
33
|
+
expect(query).to match_array([one, two])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Active Record Hash Related Query Methods" do
|
6
|
+
let!(:one) { Person.create!(data: { nickname: "george" }, jsonb_data: { payment: "zip" }) }
|
7
|
+
let!(:two) { Person.create!(data: { nickname: "dan" }, jsonb_data: { payment: "zipper" }) }
|
8
|
+
let!(:three) { Person.create!(data: { nickname: "georgey" }) }
|
9
|
+
|
10
|
+
describe "#contains" do
|
11
|
+
context "HStore Column Type" do
|
12
|
+
it "returns records that contain hash elements in joined tables" do
|
13
|
+
tag_one = Tag.create!(person_id: one.id)
|
14
|
+
tag_two = Tag.create!(person_id: two.id)
|
15
|
+
|
16
|
+
query = Tag.joins(:person).where.contains(people: { data: { nickname: "george" } })
|
17
|
+
expect(query).to include(tag_one)
|
18
|
+
expect(query).to_not include(tag_two)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns records that contain hash value" do
|
22
|
+
query = Person.where.contains(data: { nickname: "george" })
|
23
|
+
expect(query).to include(one)
|
24
|
+
expect(query).to_not include(two, three)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "JSONB Column Type" do
|
29
|
+
it "returns records that contains a json hashed value" do
|
30
|
+
query = Person.where.contains(jsonb_data: { payment: "zip" })
|
31
|
+
expect(query).to include(one)
|
32
|
+
expect(query).to_not include(two, three)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns records that contain jsonb elements in joined tables" do
|
36
|
+
tag_one = Tag.create!(person_id: one.id)
|
37
|
+
tag_two = Tag.create!(person_id: two.id)
|
38
|
+
|
39
|
+
query = Tag.joins(:person).where.contains(people: { jsonb_data: { payment: "zip" } })
|
40
|
+
expect(query).to include(tag_one)
|
41
|
+
expect(query).to_not include(tag_two)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
unless ENV["DATABASE_URL"]
|
6
|
+
require "dotenv"
|
7
|
+
Dotenv.load
|
8
|
+
end
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection(ENV["DATABASE_URL"])
|
11
|
+
|
12
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require File.expand_path(f) }
|
13
|
+
Dir["#{File.dirname(__FILE__)}/**/*examples.rb"].each { |f| require f }
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
# Enable flags like --only-failures and --next-failure
|
17
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
18
|
+
|
19
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
20
|
+
config.disable_monkey_patching!
|
21
|
+
|
22
|
+
config.expect_with :rspec do |c|
|
23
|
+
c.syntax = :expect
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Gem files must be loaded last
|
28
|
+
require "active_record_extended"
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Array Column Predicates" do
|
6
|
+
let(:arel_table) { Person.arel_table }
|
7
|
+
|
8
|
+
describe "Array Overlap" do
|
9
|
+
it "converts Arel overlap statement" do
|
10
|
+
query = arel_table.where(arel_table[:tags].overlap(["tag", "tag 2"])).to_sql
|
11
|
+
expect(query).to match_regex(/&& '\{"?tag"?,"tag 2"\}'/)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "converts Arel overlap statement" do
|
15
|
+
query = arel_table.where(arel_table[:tag_ids].overlap([1, 2])).to_sql
|
16
|
+
expect(query).to match_regex(/&& '\{1,2\}'/)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "works with count (and other predicates)" do
|
20
|
+
expect(Person.where(arel_table[:tag_ids].overlap([1, 2])).count).to eq 0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "Array Contains" do
|
25
|
+
it "converts Arel contains statement and escapes strings" do
|
26
|
+
query = arel_table.where(arel_table[:tags].contains(["tag", "tag 2"])).to_sql
|
27
|
+
expect(query).to match_regex(/@> '\{"?tag"?,"tag 2"\}'/)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "converts Arel contains statement with numbers" do
|
31
|
+
query = arel_table.where(arel_table[:tag_ids].contains([1, 2])).to_sql
|
32
|
+
expect(query).to match_regex(/@> '\{1,2\}'/)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "works with count (and other predicates)" do
|
36
|
+
expect(Person.where(arel_table[:tag_ids].contains([1, 2])).count).to eq 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "Any Array Element" do
|
41
|
+
it "creates any predicates that contain a string value" do
|
42
|
+
query = arel_table.where(arel_table[:tags].any("tag")).to_sql
|
43
|
+
expect(query).to match_regex(/'tag' = ANY\("people"\."tags"\)/)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "creates any predicates that contain a integer value" do
|
47
|
+
query = arel_table.where(arel_table[:tags].any(2)).to_sql
|
48
|
+
expect(query).to match_regex(/2 = ANY\("people"\."tags"\)/)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "All Array Elements" do
|
53
|
+
it "create all predicates that contain a string value" do
|
54
|
+
query = arel_table.where(arel_table[:tags].all("tag")).to_sql
|
55
|
+
expect(query).to match_regex(/'tag' = ALL\("people"\."tags"\)/)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "create all predicates that contain a interger value" do
|
59
|
+
query = arel_table.where(arel_table[:tags].all(2)).to_sql
|
60
|
+
expect(query).to match_regex(/2 = ALL\("people"\."tags"\)/)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|