active_record_extended 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|