postgres_ext 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +4 -0
- data/CHANGELOG.md +22 -0
- data/README.md +12 -240
- data/docs/indexes.md +19 -0
- data/docs/migrations.md +76 -0
- data/docs/querying.md +138 -0
- data/docs/type_casting.md +51 -0
- data/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb +10 -4
- data/lib/postgres_ext/active_record/relation/predicate_builder.rb +71 -0
- data/lib/postgres_ext/active_record/relation/query_methods.rb +84 -0
- data/lib/postgres_ext/active_record/relation.rb +2 -0
- data/lib/postgres_ext/active_record.rb +1 -0
- data/lib/postgres_ext/arel/nodes/contained_within.rb +12 -0
- data/lib/postgres_ext/arel/predications.rb +12 -0
- data/lib/postgres_ext/arel/visitors/to_sql.rb +15 -0
- data/lib/postgres_ext/arel/visitors/visitor.rb +14 -8
- data/lib/postgres_ext/arel/visitors.rb +1 -0
- data/lib/postgres_ext/version.rb +1 -1
- data/postgres_ext.gemspec +2 -2
- data/spec/arel/inet_spec.rb +25 -2
- data/spec/columns/array_spec.rb +39 -0
- data/spec/dummy/db/migrate/20120501163758_create_people.rb +3 -5
- data/spec/dummy/db/schema.rb +11 -7
- data/spec/migrations/array_spec.rb +23 -7
- data/spec/migrations/citext_spec.rb +27 -0
- data/spec/queries/array_queries_spec.rb +57 -0
- data/spec/queries/contains_querie_spec.rb +36 -0
- data/spec/queries/sanity_spec.rb +23 -0
- data/spec/schema_dumper/citext_spec.rb +17 -0
- metadata +26 -8
@@ -0,0 +1,51 @@
|
|
1
|
+
# Type Casting support
|
2
|
+
|
3
|
+
## INET and CIDR
|
4
|
+
INET and CIDR values are converted to
|
5
|
+
[IPAddr](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ipaddr/rdoc/IPAddr.html)
|
6
|
+
objects when retrieved from the database, or set as a string.
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
create_table :inet_examples do |t|
|
10
|
+
t.inet :ip_address
|
11
|
+
end
|
12
|
+
|
13
|
+
class InetExample < ActiveRecord::Base
|
14
|
+
end
|
15
|
+
|
16
|
+
inetExample = InetExample.new
|
17
|
+
inetExample.ip_address = '127.0.0.0/24'
|
18
|
+
inetExample.ip_address
|
19
|
+
# => #<IPAddr: IPv4:127.0.0.0/255.255.255.0>
|
20
|
+
inetExample.save
|
21
|
+
|
22
|
+
inet_2 = InetExample.first
|
23
|
+
inet_2.ip_address
|
24
|
+
# => #<IPAddr: IPv4:127.0.0.0/255.255.255.0>
|
25
|
+
```
|
26
|
+
|
27
|
+
## Arrays
|
28
|
+
Array values can be set with Array objects. Any array stored in the
|
29
|
+
database will be converted to a properly casted array of values on the
|
30
|
+
way out.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
create_table :people do |t|
|
34
|
+
t.integer :favorite_numbers, :array => true
|
35
|
+
end
|
36
|
+
|
37
|
+
class Person < ActiveRecord::Base
|
38
|
+
end
|
39
|
+
|
40
|
+
person = Person.new
|
41
|
+
person.favorite_numbers = [1,2,3]
|
42
|
+
person.favorite_numbers
|
43
|
+
# => [1,2,3]
|
44
|
+
person.save
|
45
|
+
|
46
|
+
person_2 = Person.first
|
47
|
+
person_2.favorite_numbers
|
48
|
+
# => [1,2,3]
|
49
|
+
person_2.favorite_numbers.first.class
|
50
|
+
# => Fixnum
|
51
|
+
```
|
@@ -56,7 +56,7 @@ module ActiveRecord
|
|
56
56
|
value
|
57
57
|
else
|
58
58
|
string_array = parse_pg_array value
|
59
|
-
if type == :string
|
59
|
+
if type == :string || type == :text
|
60
60
|
force_character_encoding(string_array)
|
61
61
|
else
|
62
62
|
type_cast_array(string_array)
|
@@ -108,6 +108,8 @@ module ActiveRecord
|
|
108
108
|
case field_type
|
109
109
|
when 'uuid'
|
110
110
|
:uuid
|
111
|
+
when 'citext'
|
112
|
+
:citext
|
111
113
|
when 'inet'
|
112
114
|
:inet
|
113
115
|
when 'cidr'
|
@@ -125,7 +127,7 @@ module ActiveRecord
|
|
125
127
|
class PostgreSQLAdapter
|
126
128
|
class UnsupportedFeature < Exception; end
|
127
129
|
|
128
|
-
EXTENDED_TYPES = {:inet => {:name => 'inet'}, :cidr => {:name => 'cidr'}, :macaddr => {:name => 'macaddr'}, :uuid => {:name => 'uuid'}}
|
130
|
+
EXTENDED_TYPES = {:inet => {:name => 'inet'}, :cidr => {:name => 'cidr'}, :macaddr => {:name => 'macaddr'}, :uuid => {:name => 'uuid'}, :citext => {:citext => 'citext'}}
|
129
131
|
|
130
132
|
class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
|
131
133
|
attr_accessor :array
|
@@ -256,7 +258,7 @@ module ActiveRecord
|
|
256
258
|
end
|
257
259
|
end
|
258
260
|
|
259
|
-
def
|
261
|
+
def type_cast_extended(value, column, part_array = false)
|
260
262
|
case value
|
261
263
|
when NilClass
|
262
264
|
if column.array && part_array
|
@@ -278,6 +280,10 @@ module ActiveRecord
|
|
278
280
|
type_cast_without_extended_types(value, column)
|
279
281
|
end
|
280
282
|
end
|
283
|
+
|
284
|
+
def type_cast_with_extended_types(value, column)
|
285
|
+
type_cast_extended(value, column)
|
286
|
+
end
|
281
287
|
alias_method_chain :type_cast, :extended_types
|
282
288
|
|
283
289
|
def quote_with_extended_types(value, column = nil)
|
@@ -366,7 +372,7 @@ module ActiveRecord
|
|
366
372
|
def item_to_string(value, column, encode_single_quotes = false)
|
367
373
|
return 'NULL' if value.nil?
|
368
374
|
|
369
|
-
casted_value =
|
375
|
+
casted_value = type_cast_extended(value, column, true)
|
370
376
|
|
371
377
|
if casted_value.is_a? String
|
372
378
|
casted_value = casted_value.dup
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'active_record/relation/predicate_builder'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class PredicateBuilder # :nodoc:
|
5
|
+
def self.build_from_hash(engine, attributes, default_table, allow_table_name = true)
|
6
|
+
predicates = attributes.map do |column, value|
|
7
|
+
table = default_table
|
8
|
+
|
9
|
+
if allow_table_name && value.is_a?(Hash)
|
10
|
+
table = Arel::Table.new(column, engine)
|
11
|
+
|
12
|
+
if value.empty?
|
13
|
+
'1 = 2'
|
14
|
+
else
|
15
|
+
build_from_hash(engine, value, table, false)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
column = column.to_s
|
19
|
+
|
20
|
+
if allow_table_name && column.include?('.')
|
21
|
+
table_name, column = column.split('.', 2)
|
22
|
+
table = Arel::Table.new(table_name, engine)
|
23
|
+
end
|
24
|
+
|
25
|
+
attribute = table[column.to_sym]
|
26
|
+
|
27
|
+
case value
|
28
|
+
when ActiveRecord::Relation
|
29
|
+
value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
|
30
|
+
attribute.in(value.arel.ast)
|
31
|
+
when Array, ActiveRecord::Associations::CollectionProxy
|
32
|
+
column_definition = engine.columns.find { |col| col.name == column }
|
33
|
+
|
34
|
+
if column_definition.respond_to?(:array) && column_definition.array
|
35
|
+
attribute.eq(value)
|
36
|
+
else
|
37
|
+
values = value.to_a.map {|x| x.is_a?(ActiveRecord::Base) ? x.id : x}
|
38
|
+
ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
|
39
|
+
|
40
|
+
array_predicates = ranges.map {|range| attribute.in(range)}
|
41
|
+
|
42
|
+
if values.include?(nil)
|
43
|
+
values = values.compact
|
44
|
+
if values.empty?
|
45
|
+
array_predicates << attribute.eq(nil)
|
46
|
+
else
|
47
|
+
array_predicates << attribute.in(values.compact).or(attribute.eq(nil))
|
48
|
+
end
|
49
|
+
else
|
50
|
+
array_predicates << attribute.in(values)
|
51
|
+
end
|
52
|
+
|
53
|
+
array_predicates.inject {|composite, predicate| composite.or(predicate)}
|
54
|
+
end
|
55
|
+
when Range, Arel::Relation
|
56
|
+
attribute.in(value)
|
57
|
+
when ActiveRecord::Base
|
58
|
+
attribute.eq(value.id)
|
59
|
+
when Class
|
60
|
+
# FIXME: I think we need to deprecate this behavior
|
61
|
+
attribute.eq(value.name)
|
62
|
+
else
|
63
|
+
attribute.eq(value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
predicates.flatten
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'active_record/relation/query_methods'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module QueryMethods
|
5
|
+
class WhereChain
|
6
|
+
def initialize(scope)
|
7
|
+
@scope = scope
|
8
|
+
end
|
9
|
+
|
10
|
+
def overlap(opts)
|
11
|
+
opts.each do |key, value|
|
12
|
+
@scope = @scope.where(arel_table[key].array_overlap(value))
|
13
|
+
end
|
14
|
+
@scope
|
15
|
+
end
|
16
|
+
|
17
|
+
def contained_within(opts)
|
18
|
+
opts.each do |key, value|
|
19
|
+
@scope = @scope.where(arel_table[key].contained_within(value))
|
20
|
+
end
|
21
|
+
|
22
|
+
@scope
|
23
|
+
end
|
24
|
+
|
25
|
+
def contained_within_or_equals(opts)
|
26
|
+
opts.each do |key, value|
|
27
|
+
@scope = @scope.where(arel_table[key].contained_within_or_equals(value))
|
28
|
+
end
|
29
|
+
|
30
|
+
@scope
|
31
|
+
end
|
32
|
+
|
33
|
+
def contains(opts)
|
34
|
+
opts.each do |key, value|
|
35
|
+
@scope = @scope.where(arel_table[key].contains(value))
|
36
|
+
end
|
37
|
+
|
38
|
+
@scope
|
39
|
+
end
|
40
|
+
|
41
|
+
def contains_or_equals(opts)
|
42
|
+
opts.each do |key, value|
|
43
|
+
@scope = @scope.where(arel_table[key].contains_or_equals(value))
|
44
|
+
end
|
45
|
+
|
46
|
+
@scope
|
47
|
+
end
|
48
|
+
|
49
|
+
def any(opts)
|
50
|
+
equality_to_function('ANY', opts)
|
51
|
+
end
|
52
|
+
|
53
|
+
def all(opts)
|
54
|
+
equality_to_function('ALL', opts)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def arel_table
|
60
|
+
@arel_table ||= @scope.engine.arel_table
|
61
|
+
end
|
62
|
+
|
63
|
+
def equality_to_function(function_name, opts)
|
64
|
+
opts.each do |key, value|
|
65
|
+
any_function = Arel::Nodes::NamedFunction.new(function_name, [arel_table[key]])
|
66
|
+
predicate = Arel::Nodes::Equality.new(value, any_function)
|
67
|
+
@scope = @scope.where(predicate)
|
68
|
+
end
|
69
|
+
|
70
|
+
@scope
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def where_with_chaining(opts = :chaining, *rest)
|
75
|
+
if opts == :chaining
|
76
|
+
WhereChain.new(self)
|
77
|
+
else
|
78
|
+
where_without_chaining(opts, *rest)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
alias_method_chain :where, :chaining
|
83
|
+
end
|
84
|
+
end
|
@@ -5,6 +5,18 @@ module Arel
|
|
5
5
|
def operator; :<< end
|
6
6
|
end
|
7
7
|
|
8
|
+
class ContainedWithinEquals < Arel::Nodes::Binary
|
9
|
+
def operator; '<<='.symbolize end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Contains < Arel::Nodes::Binary
|
13
|
+
def operator; :>> end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ContainsEquals < Arel::Nodes::Binary
|
17
|
+
def operator; '>>='.symbolize end
|
18
|
+
end
|
19
|
+
|
8
20
|
class ArrayOverlap < Arel::Nodes::Binary
|
9
21
|
def operator; '&&' end
|
10
22
|
end
|
@@ -6,6 +6,18 @@ module Arel
|
|
6
6
|
Nodes::ContainedWithin.new self, other
|
7
7
|
end
|
8
8
|
|
9
|
+
def contained_within_or_equals(other)
|
10
|
+
Nodes::ContainedWithinEquals.new self, other
|
11
|
+
end
|
12
|
+
|
13
|
+
def contains(other)
|
14
|
+
Nodes::Contains.new self, other
|
15
|
+
end
|
16
|
+
|
17
|
+
def contains_or_equals(other)
|
18
|
+
Nodes::ContainsEquals.new self, other
|
19
|
+
end
|
20
|
+
|
9
21
|
def array_overlap(other)
|
10
22
|
Nodes::ArrayOverlap.new self, other
|
11
23
|
end
|
@@ -8,10 +8,21 @@ module Arel
|
|
8
8
|
"#{visit o.left} << #{visit o.right}"
|
9
9
|
end
|
10
10
|
|
11
|
+
def visit_Arel_Nodes_ContainedWithinEquals o
|
12
|
+
"#{visit o.left} <<= #{visit o.right}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def visit_Arel_Nodes_Contains o
|
16
|
+
"#{visit o.left} >> #{visit o.right}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def visit_Arel_Nodes_ContainsEquals o
|
20
|
+
"#{visit o.left} >>= #{visit o.right}"
|
21
|
+
end
|
22
|
+
|
11
23
|
def visit_Arel_Nodes_ArrayOverlap o
|
12
|
-
if Array === o.right
|
13
|
-
|
14
|
-
"#{visit o.left} && '#{right}'"
|
24
|
+
if Array === o.right
|
25
|
+
"#{visit o.left} && #{visit o.right}"
|
15
26
|
else
|
16
27
|
"#{visit o.left} && #{visit o.right}"
|
17
28
|
end
|
@@ -20,11 +31,6 @@ module Arel
|
|
20
31
|
def visit_IPAddr value
|
21
32
|
"'#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}'"
|
22
33
|
end
|
23
|
-
|
24
|
-
def change_string value
|
25
|
-
return value unless value.is_a?(String)
|
26
|
-
value.gsub(/^\'/, '"').gsub(/\'$/, '"')
|
27
|
-
end
|
28
34
|
end
|
29
35
|
end
|
30
36
|
end
|
data/lib/postgres_ext/version.rb
CHANGED
data/postgres_ext.gemspec
CHANGED
@@ -19,8 +19,8 @@ Gem::Specification.new do |gem|
|
|
19
19
|
gem.add_dependency 'pg_array_parser', '~> 0.0.8'
|
20
20
|
|
21
21
|
gem.add_development_dependency 'rails', '~> 3.2.0'
|
22
|
-
gem.add_development_dependency 'rspec-rails', '~> 2.
|
23
|
-
gem.add_development_dependency 'bourne', '~> 1.
|
22
|
+
gem.add_development_dependency 'rspec-rails', '~> 2.12.0'
|
23
|
+
gem.add_development_dependency 'bourne', '~> 1.3.0'
|
24
24
|
if RUBY_PLATFORM =~ /java/
|
25
25
|
gem.add_development_dependency 'activerecord-jdbcpostgresql-adapter'
|
26
26
|
else
|
data/spec/arel/inet_spec.rb
CHANGED
@@ -23,11 +23,34 @@ describe 'INET related AREL functions' do
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
describe '
|
27
|
-
it 'converts Arel contained_within
|
26
|
+
describe 'contained with (<<) operator' do
|
27
|
+
it 'converts Arel contained_within statements to <<' do
|
28
28
|
arel_table = IpAddress.arel_table
|
29
29
|
|
30
30
|
arel_table.where(arel_table[:address].contained_within(IPAddr.new('127.0.0.1/24'))).to_sql.should match /<< '127.0.0.0\/24'/
|
31
31
|
end
|
32
32
|
end
|
33
|
+
|
34
|
+
describe 'contained within or equals (<<=) operator' do
|
35
|
+
it 'converts Arel contained_within_or_equals statements to <<=' do
|
36
|
+
arel_table = IpAddress.arel_table
|
37
|
+
|
38
|
+
arel_table.where(arel_table[:address].contained_within_or_equals(IPAddr.new('127.0.0.1/24'))).to_sql.should match /<<= '127.0.0.0\/24'/
|
39
|
+
end
|
40
|
+
end
|
41
|
+
describe 'contains (>>) operator' do
|
42
|
+
it 'converts Arel contains statements to >>' do
|
43
|
+
arel_table = IpAddress.arel_table
|
44
|
+
|
45
|
+
arel_table.where(arel_table[:address].contains(IPAddr.new('127.0.0.1/24'))).to_sql.should match />> '127.0.0.0\/24'/
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'contains or equals (>>=) operator' do
|
50
|
+
it 'converts Arel contains_or_equals statements to >>=' do
|
51
|
+
arel_table = IpAddress.arel_table
|
52
|
+
|
53
|
+
arel_table.where(arel_table[:address].contains_or_equals(IPAddr.new('127.0.0.1/24'))).to_sql.should match />>= '127.0.0.0\/24'/
|
54
|
+
end
|
55
|
+
end
|
33
56
|
end
|
data/spec/columns/array_spec.rb
CHANGED
@@ -5,6 +5,7 @@ require 'spec_helper'
|
|
5
5
|
describe 'Array column' do
|
6
6
|
let!(:integer_array_column) { ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new 'field', nil, 'integer[]'}
|
7
7
|
let!(:string_array_column) { ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new 'field', nil, 'character varying(255)[]'}
|
8
|
+
let!(:text_array_column) { ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new 'field', nil, 'text[]'}
|
8
9
|
let!(:adapter) { ActiveRecord::Base.connection }
|
9
10
|
|
10
11
|
context 'string array' do
|
@@ -45,6 +46,44 @@ describe 'Array column' do
|
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
49
|
+
context 'text array' do
|
50
|
+
describe '#type_class' do
|
51
|
+
context 'has null value' do
|
52
|
+
it 'converts the PostgreSQL value to an array with a nil value' do
|
53
|
+
text_array_column.type_cast('{1,NULL,"NULL"}').should eq ['1',nil,'NULL']
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'utf8' do
|
58
|
+
it "handles incoming UTF8 as UTF8" do
|
59
|
+
text_array_column.type_cast('{"Аркалио"}').should eq ['Аркалио']
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'corner cases, strings with commas and quotations' do
|
64
|
+
it 'converts the PostgreSQL value containing escaped " to an array' do
|
65
|
+
text_array_column.type_cast('{"has \" quote",another value}').should eq ['has " quote', 'another value']
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'converts the PostgreSQL value containing commas to an array' do
|
69
|
+
text_array_column.type_cast('{"has , comma",another value,"more, commas"}').should eq ['has , comma', 'another value', 'more, commas']
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'converts strings containing , to the proper value' do
|
73
|
+
adapter.type_cast(['c,'], text_array_column).should eq '{"c,"}'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "handles strings with double quotes" do
|
77
|
+
adapter.type_cast(['a"b'], text_array_column).should eq '{"a\\"b"}'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'converts arrays of strings containing nil to the proper value' do
|
81
|
+
adapter.type_cast(['one', nil, 'two'], text_array_column).should eq '{"one",NULL,"two"}'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
48
87
|
context 'integer array' do
|
49
88
|
describe '#type_class' do
|
50
89
|
context 'has null value' do
|