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.
@@ -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