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.
@@ -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 type_cast_with_extended_types(value, column, part_array = false)
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 = type_cast(value, column, true)
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
@@ -0,0 +1,2 @@
1
+ require 'postgres_ext/active_record/relation/query_methods'
2
+ require 'postgres_ext/active_record/relation/predicate_builder'
@@ -1,3 +1,4 @@
1
1
  require 'postgres_ext/active_record/sanitization'
2
2
  require 'postgres_ext/active_record/connection_adapters'
3
3
  require 'postgres_ext/active_record/schema_dumper'
4
+ require 'postgres_ext/active_record/relation'
@@ -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
@@ -0,0 +1,15 @@
1
+ require 'arel/visitors/to_sql'
2
+
3
+ module Arel
4
+ module Visitors
5
+ class ToSql
6
+ def visit_Array o
7
+ if last_column.respond_to?(:array) && last_column.array
8
+ quoted o
9
+ else
10
+ o.map { |x| visit x }.join(', ')
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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
- right = "{#{o.right.map{|v| change_string(visit(v))}.join(',')}}"
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
@@ -1 +1,2 @@
1
1
  require 'postgres_ext/arel/visitors/visitor'
2
+ require 'postgres_ext/arel/visitors/to_sql'
@@ -1,3 +1,3 @@
1
1
  module PostgresExt
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
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.9.0'
23
- gem.add_development_dependency 'bourne', '~> 1.1.2'
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
@@ -23,11 +23,34 @@ describe 'INET related AREL functions' do
23
23
  end
24
24
  end
25
25
 
26
- describe 'cotained with (<<) operator' do
27
- it 'converts Arel contained_within statemnts to <<' do
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
@@ -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