Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #67 from jagregory/array_contains

Array contains operator support
  • Loading branch information...
commit dae3222391f0c5e72acc69ee4e28cfa750a397ca 2 parents cf1053e + 8203c97
@danmcclain danmcclain authored
View
32 docs/querying.md
@@ -42,6 +42,38 @@ User.where(user_arel[:tags].array_overlap(['one','two']))
# => SELECT \"users\".* FROM \"users\" WHERE \"users\".\"tags\" && '{one,two}'
```
+### @> - Array Contains operator
+
+PostgreSQL has a contains (`@>`) operator for querying whether all the
+elements of an array are within another.
+
+```sql
+ARRAY[1,2,3] @> ARRAY[3,4]
+-- f
+
+ARRAY[1,2,3] @> ARRAY[2,3]
+-- t
+```
+
+Postgres\_ext extends the `ActiveRecord::Relation.where` method by
+adding a `contains` method. To make a contains query, you can do:
+
+```ruby
+User.where.contains(:nick_names => ['Bob', 'Fred'])
+```
+
+Postgres\_ext defines `array_contains`, an [Arel](https://github.com/rails/arel)
+predicate for the `@>` operator. This is utilized by the
+`where.array_contains` call above.
+
+```ruby
+user_arel = User.arel_table
+
+# Execute the query
+User.where(user_arel[:tags].array_contains(['one','two']))
+# => SELECT "users".* FROM "users" WHERE "users"."tags" @> '{"one","two"}'
+```
+
### ANY or ALL functions
When querying array columns, you have the ability to see if a predicate
View
8 lib/postgres_ext/active_record/relation/query_methods.rb
@@ -32,7 +32,13 @@ def contained_within_or_equals(opts)
def contains(opts)
opts.each do |key, value|
- @scope = @scope.where(arel_table[key].contains(value))
+ column_definition = @scope.engine.columns.find { |col| col.name == key.to_s }
+
+ if column_definition.respond_to?(:array) && column_definition.array
+ @scope = @scope.where(arel_table[key].array_contains(value))
+ else
+ @scope = @scope.where(arel_table[key].contains(value))
+ end
end
@scope
View
1  lib/postgres_ext/arel/nodes.rb
@@ -1 +1,2 @@
+require 'postgres_ext/arel/nodes/array_nodes'
require 'postgres_ext/arel/nodes/contained_within'
View
13 lib/postgres_ext/arel/nodes/array_nodes.rb
@@ -0,0 +1,13 @@
+require 'arel/nodes/binary'
+
+module Arel
+ module Nodes
+ class ArrayOverlap < Arel::Nodes::Binary
+ def operator; '&&' end
+ end
+
+ class ArrayContains < Arel::Nodes::Binary
+ def operator; '@>' end
+ end
+ end
+end
View
4 lib/postgres_ext/arel/nodes/contained_within.rb
@@ -16,9 +16,5 @@ def operator; :>> end
class ContainsEquals < Arel::Nodes::Binary
def operator; '>>='.symbolize end
end
-
- class ArrayOverlap < Arel::Nodes::Binary
- def operator; '&&' end
- end
end
end
View
4 lib/postgres_ext/arel/predications.rb
@@ -21,5 +21,9 @@ def contains_or_equals(other)
def array_overlap(other)
Nodes::ArrayOverlap.new self, other
end
+
+ def array_contains(other)
+ Nodes::ArrayContains.new self, other
+ end
end
end
View
4 lib/postgres_ext/arel/visitors/visitor.rb
@@ -28,6 +28,10 @@ def visit_Arel_Nodes_ArrayOverlap o
end
end
+ def visit_Arel_Nodes_ArrayContains o
+ "#{visit o.left} @> #{visit o.right}"
+ end
+
def visit_IPAddr value
"'#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}'"
end
View
38 spec/arel/array_spec.rb
@@ -54,4 +54,42 @@ class ArelArray < ActiveRecord::Base
ArelArray.find_by_sql(query.to_sql).should include(one)
end
end
+
+ describe 'Array Contains' do
+ it 'converts Arel array_contains statement and escapes strings' do
+ arel_table = ArelArray.arel_table
+
+ arel_table.where(arel_table[:tags].array_contains(['tag','tag 2'])).to_sql.should match /@> '\{"tag","tag 2"\}'/
+ end
+
+ it 'converts Arel array_contains statement with numbers' do
+ arel_table = ArelArray.arel_table
+
+ arel_table.where(arel_table[:tag_ids].array_contains([1,2])).to_sql.should match /@> '\{1,2\}'/
+ end
+
+ it 'works with count (and other predicates)' do
+ arel_table = ArelArray.arel_table
+
+ ArelArray.where(arel_table[:tag_ids].array_contains([1,2])).count.should eq 0
+ end
+
+ it 'returns matched records' do
+ one = ArelArray.create!(:tags => ['one', 'two', 'three'])
+ two = ArelArray.create!(:tags => ['one', 'three'])
+ arel_table = ArelArray.arel_table
+
+ query = arel_table.where(arel_table[:tags].array_contains(['one', 'two'])).project(Arel.sql('*'))
+ ArelArray.find_by_sql(query.to_sql).should include one
+ ArelArray.find_by_sql(query.to_sql).should_not include two
+
+ query = arel_table.where(arel_table[:tags].array_contains(['one', 'three'])).project(Arel.sql('*'))
+ ArelArray.find_by_sql(query.to_sql).should include one
+ ArelArray.find_by_sql(query.to_sql).should include two
+
+ query = arel_table.where(arel_table[:tags].array_contains(['two'])).project(Arel.sql('*'))
+ ArelArray.find_by_sql(query.to_sql).should include one
+ ArelArray.find_by_sql(query.to_sql).should_not include two
+ end
+ end
end
View
15 spec/queries/array_queries_spec.rb
@@ -3,6 +3,7 @@
describe 'Array queries' do
let(:equality_regex) { %r{\"people\"\.\"tags\" = '\{\"working\"\}'} }
let(:overlap_regex) { %r{\"people\"\.\"tag_ids\" && '\{1,2\}'} }
+ let(:contains_regex) { %r{\"people\"\.\"tag_ids\" @> '\{1,2\}'} }
let(:any_regex) { %r{2 = ANY\(\"people\"\.\"tag_ids\"\)} }
let(:all_regex) { %r{2 = ALL\(\"people\"\.\"tag_ids\"\)} }
@@ -27,6 +28,20 @@
end
end
+ describe '.where.contains(:column => value)' do
+ it 'generates the appropriate where clause' do
+ query = Person.where.contains(:tag_ids => [1,2])
+ query.to_sql.should match contains_regex
+ end
+
+ it 'allows chaining' do
+ query = Person.where.contains(:tag_ids => [1,2]).where(:tags => ['working']).to_sql
+
+ query.should match contains_regex
+ query.should match equality_regex
+ end
+ end
+
describe '.where.any(:column => value)' do
it 'generates the appropriate where clause' do
query = Person.where.any(:tag_ids => 2)
Please sign in to comment.
Something went wrong with that request. Please try again.