postgres_ext 0.1.0 → 0.2.0
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.
- 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
|