postgres_ext 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +15 -5
  3. data/CHANGELOG.md +3 -0
  4. data/Gemfile +2 -2
  5. data/README.md +4 -0
  6. data/docs/type_casting.md +19 -0
  7. data/lib/postgres_ext/active_record/connection_adapters/postgres_adapter.rb +149 -3
  8. data/lib/postgres_ext/active_record/relation/predicate_builder.rb +1 -1
  9. data/lib/postgres_ext/version.rb +1 -1
  10. data/spec/columns/array_spec.rb +3 -4
  11. data/spec/columns/ranges/daterange_spec.rb +37 -0
  12. data/spec/columns/ranges/int4range_spec.rb +38 -0
  13. data/spec/columns/ranges/int8range_spec.rb +38 -0
  14. data/spec/columns/ranges/numrange_spec.rb +37 -0
  15. data/spec/columns/ranges/tsrange_spec.rb +37 -0
  16. data/spec/dummy/app/models/person.rb +1 -1
  17. data/spec/dummy/config/application.rb +1 -1
  18. data/spec/dummy/db/migrate/20120501163758_create_people.rb +1 -0
  19. data/spec/dummy/db/schema.rb +9 -8
  20. data/spec/migrations/array_spec.rb +20 -0
  21. data/spec/migrations/ranges/daterange_spec.rb +27 -0
  22. data/spec/migrations/ranges/int4range_spec.rb +27 -0
  23. data/spec/migrations/ranges/int8range_spec.rb +27 -0
  24. data/spec/migrations/ranges/numrange_spec.rb +27 -0
  25. data/spec/migrations/ranges/tsrange_spec.rb +27 -0
  26. data/spec/migrations/ranges/tstzrange_spec.rb +27 -0
  27. data/spec/models/ranges/daterange_spec.rb +88 -0
  28. data/spec/models/ranges/int4range_spec.rb +85 -0
  29. data/spec/models/ranges/int8range_spec.rb +85 -0
  30. data/spec/models/ranges/numrange_spec.rb +85 -0
  31. data/spec/models/ranges/tsrange_spec.rb +89 -0
  32. data/spec/models/ranges/tstzrange_spec.rb +89 -0
  33. data/spec/queries/sanity_spec.rb +1 -0
  34. data/spec/schema_dumper/array_spec.rb +1 -1
  35. data/spec/schema_dumper/ranges/daterange_spec.rb +18 -0
  36. data/spec/schema_dumper/ranges/int4range_spec.rb +18 -0
  37. data/spec/schema_dumper/ranges/int8range_spec.rb +18 -0
  38. data/spec/schema_dumper/ranges/numrange_spec.rb +18 -0
  39. data/spec/schema_dumper/ranges/tsrange_spec.rb +18 -0
  40. data/spec/schema_dumper/ranges/tstzrange_spec.rb +17 -0
  41. metadata +53 -27
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7e8eff00c75c70dcf28c108a470e187fb6c62d01
4
+ data.tar.gz: 41af271c607addeee1883185aa52602f37227767
5
+ SHA512:
6
+ metadata.gz: 17fa65379b957864bb359d6ccccd85672422361b6f5126c78f113d8c05ea0c39595fa790d991c3d79d6778b1ce790f133fdb65458f9da8f99f395c2f5fe19bdf
7
+ data.tar.gz: 80a96500066d32ac9547f5960b3bced0a27a47a20a3c233f7a4c0d78ae06b7febd03be367b901b9ef62747d3f6258da5839ff9ed5d464c8055c6b7a9fe122c1c
data/.travis.yml CHANGED
@@ -1,12 +1,22 @@
1
1
  rvm:
2
- - 1.8.7
3
- - 1.9.2
4
2
  - 1.9.3
5
- - jruby-18mode
3
+ - 2.0.0
6
4
  - jruby-19mode
7
5
 
8
6
  before_script:
9
- - psql -c 'create database postgres_ext_test;' -U postgres
7
+ - sudo /etc/init.d/postgresql stop
8
+ - sudo cp /etc/postgresql/9.1/main/pg_hba.conf ./
9
+ - sudo apt-get remove postgresql postgresql-9.1 -qq --purge
10
+ - source /etc/lsb-release
11
+ - echo "deb http://apt.postgresql.org/pub/repos/apt/ $DISTRIB_CODENAME-pgdg main" > pgdg.list
12
+ - sudo mv pgdg.list /etc/apt/sources.list.d/
13
+ - wget --quiet -O - http://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | sudo apt-key add -
14
+ - sudo apt-get update -qq
15
+ - sudo apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" install postgresql-9.2 postgresql-contrib-9.2 -qq
16
+ - sudo /etc/init.d/postgresql stop
17
+ - sudo cp ./pg_hba.conf /etc/postgresql/9.2/main
18
+ - sudo /etc/init.d/postgresql start
19
+ - psql -c 'create database postgres_ext_test;' -U postgres -h localhost
10
20
  - cp spec/dummy/config/database.yml.example spec/dummy/config/database.yml
11
21
  - RAILS_ENV=test rake db:migrate
12
22
 
@@ -15,4 +25,4 @@ notifications:
15
25
  - git@danmcclain.net
16
26
  campfire:
17
27
  rooms:
18
- - secure: "yVESPleawl+fzvnzXw/W7rULyCjMEq3gPc3cEqcqM2SBBtEIDNXto2zoTAoR\nC5yqhijr+UtmVMsI7CxVK3XvfkmCJZN9P4DP0uas8XYx5DsSabCdPN0h3pka\nbaDCMCInU5QF4WswL2iuyLsOJeKDRwxh09adsHi1HpMgf0nTKPA="
28
+ - secure: "dNVxAfeOqRuA7k4Wu3H63deqV8Z1mmpVBdPEtkK2ry+mp+51RFHcO0cUJ/fI\nN4PXZu2wNWQlvz5LCMRPe+hxio/w8hTvgQxzBVvi0kuOh/22wXumdy7LR/RJ\nvyrrbP3+1hSxhzufyvIe/fOU11v31d3WRA1/q80ls9EkwzoDTNI="
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 0.4.0
2
+ * Adds support for (limited) support for PostgreSQL ranges - Dan McClain
3
+
1
4
  ## 0.3.1
2
5
 
3
6
  * Fixes issue with array -> string code - Dan McClain
data/Gemfile CHANGED
@@ -5,8 +5,8 @@ gemspec
5
5
  unless ENV['CI']
6
6
  if RUBY_PLATFORM =~ /java/
7
7
  gem 'ruby-debug'
8
- elsif RUBY_VERSION == '1.9.3' || RUBY_VERSION == '2.0.0'
9
- gem 'debugger'
8
+ elsif RUBY_VERSION == '2.0.0'
9
+ gem 'byebug'
10
10
  end
11
11
  end
12
12
  gem 'fivemat'
data/README.md CHANGED
@@ -13,6 +13,10 @@ the gem please ask the question on
13
13
  [Stack Overflow](http://stackoverflow.com). Be sure to tag the
14
14
  question with `DockYard` so we can find it.
15
15
 
16
+ ## Note ##
17
+ PostgresExt is dropping support for Ruby 1.8.7 with the next minor
18
+ release.
19
+
16
20
  ## Installation
17
21
 
18
22
  Add this line to your application's Gemfile:
data/docs/type_casting.md CHANGED
@@ -49,3 +49,22 @@ person_2.favorite_numbers
49
49
  person_2.favorite_numbers.first.class
50
50
  # => Fixnum
51
51
  ```
52
+
53
+ ## Ranges
54
+ Like array objects, postgres\_ext supports range types as well.
55
+ Numrange, in4range, int8range, daterange, tsrange, and tstzrange are all
56
+ supported, but there are some notable caveats.
57
+
58
+ ### Int and Date ranges
59
+ As integers and days are discrete measurements, PostgreSQL will
60
+ normalize these ranges as they are store in the database. PostgreSQL
61
+ will convert end-inclusive ranges to end-exclusive, meaning that `0..4`
62
+ becomes `0...5`. Developers should be aware of this when using integer
63
+ and date ranges, since ruby will treat these ranges differently from
64
+ PostgreSQL.
65
+
66
+ ### Timestamp with and without timezone
67
+ Ruby/Rails 3.2.x does not support datetime ranges that begin or end with
68
+ infinity. Rails 4 has patched datetime and time so that infinity
69
+ terminated ranges work, but currently postgres\_ext has not patched the
70
+ required methods.
@@ -43,7 +43,10 @@ module ActiveRecord
43
43
  value
44
44
  else
45
45
  case type
46
- when :inet, :cidr then klass.string_to_cidr_address(value)
46
+ when :inet, :cidr then klass.string_to_cidr_address(value)
47
+ when :numrange,:int4range,:int8range then klass.string_to_numeric_range(value,type)
48
+ when :daterange then klass.string_to_date_range(value)
49
+ when :tsrange,:tstzrange then klass.string_to_datetime_range(value)
47
50
  else
48
51
  type_cast_without_extended_types(value)
49
52
  end
@@ -81,7 +84,10 @@ module ActiveRecord
81
84
  "#{klass}.new('#{self.name}', #{self.default.nil? ? 'nil' : "'#{self.default}'"}, '#{self.sql_type}').string_to_array(#{var_name})"
82
85
  else
83
86
  case type
84
- when :inet, :cidr then "#{klass}.string_to_cidr_address(#{var_name})"
87
+ when :inet, :cidr then "#{klass}.string_to_cidr_address(#{var_name})"
88
+ when :numrange,:int4range,:int8range then "#{klass}.string_to_numeric_range(#{var_name},#{type.inspect})"
89
+ when :daterange then "#{klass}.string_to_date_range(#{var_name})"
90
+ when :tsrange,:tstzrange then "#{klass}.string_to_datetime_range(#{var_name})"
85
91
  else
86
92
  type_cast_code_without_extended_types(var_name)
87
93
  end
@@ -89,7 +95,33 @@ module ActiveRecord
89
95
  end
90
96
  alias_method_chain :type_cast_code, :extended_types
91
97
 
98
+ if RUBY_PLATFORM =~ /java/
99
+ def default_value_with_extended_types(default)
100
+ case default
101
+ when /\A'(.*)'::(?:(num|int[48]|date|ts(tz)?)range)\z/
102
+ $1
103
+ else
104
+ default_value_without_extended_types(default)
105
+ end
106
+
107
+ end
108
+ alias_method_chain :default_value, :extended_types
109
+ end
110
+
92
111
  class << self
112
+ unless RUBY_PLATFORM =~ /java/
113
+ def extract_value_from_default_with_extended_types(default)
114
+ case default
115
+ when /\A'(.*)'::(?:(num|int[48]|date|ts(tz)?)range)\z/
116
+ $1
117
+ else
118
+ extract_value_from_default_without_extended_types(default)
119
+ end
120
+
121
+ end
122
+ alias_method_chain :extract_value_from_default, :extended_types
123
+ end
124
+
93
125
  def string_to_cidr_address(string)
94
126
  return string unless String === string
95
127
 
@@ -97,6 +129,51 @@ module ActiveRecord
97
129
  IPAddr.new(string)
98
130
  end
99
131
  end
132
+
133
+ def string_to_numeric_range(value, type)
134
+ if type == :numrange
135
+ extract_range(value) do |end_value|
136
+ end_value.to_f
137
+ end
138
+ else
139
+ extract_range(value) do |end_value|
140
+ end_value.to_i
141
+ end
142
+ end
143
+ end
144
+
145
+ def string_to_date_range(value)
146
+ extract_range(value) do |end_value|
147
+ Date.parse(end_value)
148
+ end
149
+ end
150
+
151
+ def string_to_datetime_range(value)
152
+ extract_range(value) do |end_value|
153
+ Time.parse(end_value)
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def extract_range(value, &conversion)
160
+ if Range === value
161
+ value
162
+ else
163
+ # Until 1.8.7 support is dropped, must use group numbers instead of named groups
164
+ #range_regex = /\A(?<open>\[|\()(?<start>.*?),(?<end>.*?)(?<close>\]|\))\z/
165
+ range_regex = /\A(\[|\()(.*?),(.*?)(\]|\))\z/
166
+ if match = value.match(range_regex)
167
+ if match = value.match(range_regex)
168
+ start_value = match[2].empty? ? -(1.0/0.0) : conversion.call(match[2])
169
+ end_value = match[3].empty? ? (1.0/0.0) : conversion.call(match[3])
170
+
171
+ end_exclusive = end_value != (1.0/0.0) && match[4] == ')'
172
+ Range.new start_value, end_value, end_exclusive
173
+ end
174
+ end
175
+ end
176
+ end
100
177
  end
101
178
 
102
179
  private
@@ -121,6 +198,18 @@ module ActiveRecord
121
198
  :macaddr
122
199
  when 'ean13'
123
200
  :ean13
201
+ when 'int4range'
202
+ :int4range
203
+ when 'int8range'
204
+ :int8range
205
+ when 'numrange'
206
+ :numrange
207
+ when 'daterange'
208
+ :daterange
209
+ when 'tsrange'
210
+ :tsrange
211
+ when 'tstzrange'
212
+ :tstzrange
124
213
  else
125
214
  simplified_type_without_extended_types field_type
126
215
  end
@@ -133,7 +222,8 @@ module ActiveRecord
133
222
  class UnsupportedFeature < Exception; end
134
223
 
135
224
  EXTENDED_TYPES = { :inet => {:name => 'inet'}, :cidr => {:name => 'cidr'}, :macaddr => {:name => 'macaddr'},
136
- :uuid => {:name => 'uuid'}, :citext => {:name => 'citext'}, :ean13 => {:name => 'ean13'} }
225
+ :uuid => {:name => 'uuid'}, :citext => {:name => 'citext'}, :ean13 => {:name => 'ean13'}, :numrange => {:name => 'numrange'},
226
+ :daterange => {:name => 'daterange'}, :int4range => {:name => 'int4range'}, :int8range => {:name => 'int8range'}, :tsrange => {:name => 'tsrange'}, :tstzrange => {:name => 'tstzrange'} }
137
227
 
138
228
  class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
139
229
  attr_accessor :array
@@ -203,6 +293,8 @@ module ActiveRecord
203
293
  @database_encoding ||= case ActiveRecord::Base.connection.encoding
204
294
  when 'UTF8'
205
295
  'UTF-8'
296
+ when 'SQL_ASCII'
297
+ 'ASCII'
206
298
  else
207
299
  ActiveRecord::Base.connection.encoding
208
300
  end
@@ -279,6 +371,22 @@ module ActiveRecord
279
371
  else
280
372
  type_cast_without_extended_types(value, column)
281
373
  end
374
+ when Float
375
+ if [:numrange,:int4range,:int8range,:daterange].include?(column.type)&& value.abs == (1.0/0.0)
376
+ ''
377
+ else
378
+ type_cast_without_extended_types(value, column)
379
+ end
380
+ when Date, DateTime, Time
381
+ if column.type == :tstzrange
382
+ quoted_date(value)
383
+ elsif column.type == :tsrange
384
+ value.to_s(:db)
385
+ else
386
+ type_cast_without_extended_types(value, column)
387
+ end
388
+ when Range
389
+ range_to_string(value, column)
282
390
  when Array
283
391
  if column.array
284
392
  array_to_string(value, column)
@@ -304,6 +412,8 @@ module ActiveRecord
304
412
  "'#{array_to_string(value, column, true)}'"
305
413
  elsif column.respond_to?(:array) && column.array && value =~ /^\{.*\}$/
306
414
  "'#{value}'"
415
+ elsif value.is_a? Range
416
+ "'#{type_cast(value, column)}'"
307
417
  else
308
418
  quote_without_extended_types(value, column)
309
419
  end
@@ -370,6 +480,22 @@ module ActiveRecord
370
480
  select_rows('select extname from pg_extension', 'extensions').map { |row| row[0] }.delete_if {|name| name == 'plpgsql'}
371
481
  end
372
482
 
483
+ def change_column_with_extended_types(table_name, column_name, type, options = {})
484
+ if options[:array]
485
+ clear_cache!
486
+ quoted_table_name = quote_table_name(table_name)
487
+
488
+ execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}[]"
489
+
490
+ change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
491
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
492
+ else
493
+ change_column_without_extended_types(table_name, column_name, type, options)
494
+ end
495
+ end
496
+
497
+ alias_method_chain :change_column, :extended_types
498
+
373
499
  private
374
500
 
375
501
  def ipaddr_to_string(value)
@@ -380,6 +506,26 @@ module ActiveRecord
380
506
  "{#{value.map { |val| item_to_string(val, column, encode_single_quotes) }.join(',')}}"
381
507
  end
382
508
 
509
+ def range_to_string(value, column)
510
+ "#{range_lower_bound_character value}#{type_cast value.begin, column},#{type_cast value.end, column}#{range_upper_bound_character value}"
511
+ end
512
+
513
+ def range_lower_bound_character(value)
514
+ if value.begin == -(1.0/0.0)
515
+ '('
516
+ else
517
+ '['
518
+ end
519
+ end
520
+
521
+ def range_upper_bound_character(value)
522
+ if value.end == (1.0/0.0) || value.exclude_end?
523
+ ')'
524
+ else
525
+ ']'
526
+ end
527
+ end
528
+
383
529
  def item_to_string(value, column, encode_single_quotes = false)
384
530
  return 'NULL' if value.nil?
385
531
 
@@ -29,7 +29,7 @@ module ActiveRecord
29
29
  value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
30
30
  attribute.in(value.arel.ast)
31
31
  when Array, ActiveRecord::Associations::CollectionProxy
32
- column_definition = engine.columns.find { |col| col.name == column }
32
+ column_definition = engine.connection.columns(table.name).find { |col| col.name == column }
33
33
 
34
34
  if column_definition.respond_to?(:array) && column_definition.array
35
35
  attribute.eq(value)
@@ -1,3 +1,3 @@
1
1
  module PostgresExt
2
- VERSION = '0.3.1'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -9,7 +9,7 @@ describe 'Array column' do
9
9
  let!(:adapter) { ActiveRecord::Base.connection }
10
10
 
11
11
  context 'string array' do
12
- describe '#type_class' do
12
+ describe '#type_cast' do
13
13
  context 'has null value' do
14
14
  it 'converts the PostgreSQL value to an array with a nil value' do
15
15
  string_array_column.type_cast('{1,NULL,"NULL"}').should eq ['1',nil,'NULL']
@@ -47,7 +47,7 @@ describe 'Array column' do
47
47
  end
48
48
 
49
49
  context 'text array' do
50
- describe '#type_class' do
50
+ describe '#type_cast' do
51
51
  context 'has null value' do
52
52
  it 'converts the PostgreSQL value to an array with a nil value' do
53
53
  text_array_column.type_cast('{1,NULL,"NULL"}').should eq ['1',nil,'NULL']
@@ -88,7 +88,7 @@ describe 'Array column' do
88
88
  end
89
89
 
90
90
  context 'integer array' do
91
- describe '#type_class' do
91
+ describe '#type_cast' do
92
92
  context 'has null value' do
93
93
  it 'converts a Postgres Array with nulls in it' do
94
94
  integer_array_column.type_cast('{1,NULL,2}').should eq [1,nil,2]
@@ -116,5 +116,4 @@ describe 'Array column' do
116
116
  end
117
117
  end
118
118
  end
119
-
120
119
  end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'daterange column' do
4
+ let!(:adapter) { ActiveRecord::Base.connection }
5
+ let!(:date_range_column) { ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new 'field', nil, 'daterange'}
6
+
7
+ describe '#type_class' do
8
+ it 'converts an end-inclusive PostgreSQL integer range to a Ruby range' do
9
+ date_range_column.type_cast('[2011-01-01,2012-01-31]').should eq Date.new(2011,01,01)..Date.new(2012,01,31)
10
+ end
11
+
12
+ it 'converts an end-exclusive PostgreSQL integer range to a Ruby range' do
13
+ date_range_column.type_cast('[2011-01-01,2012-01-31)').should eq Date.new(2011,01,01)...Date.new(2012,01,31)
14
+ end
15
+
16
+ it 'converts an infinite PostgreSQL integer range to a Ruby range' do
17
+ # Cannot have a range from -Infinity to a date
18
+ # date_range_column.type_cast('(,2011-01-01)').should eq -(1.0/0.0)...4
19
+ date_range_column.type_cast('[2011-01-01,)').should eq Date.new(2011,01,01)..(1.0/0.0)
20
+ end
21
+ end
22
+
23
+ describe 'date range to SQL statment conversion' do
24
+ it 'returns an end-inclusive PostgreSQL range' do
25
+ value = date_range_column.type_cast('[2011-01-01,2012-01-31]')
26
+ adapter.type_cast(value, date_range_column).should eq '[2011-01-01,2012-01-31]'
27
+ end
28
+ it 'returns an end-exclusive PostgreSQL range' do
29
+ value = date_range_column.type_cast('[2011-01-01,2012-01-31)')
30
+ adapter.type_cast(value, date_range_column).should eq '[2011-01-01,2012-01-31)'
31
+ end
32
+ it 'converts an infinite PostgreSQL integer range to a Ruby range' do
33
+ value = date_range_column.type_cast('[2011-01-01,)')
34
+ adapter.type_cast(value, date_range_column).should eq '[2011-01-01,)'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'int4range column' do
4
+ let!(:adapter) { ActiveRecord::Base.connection }
5
+ let!(:int4_range_column) { ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new 'field', nil, 'int4range'}
6
+
7
+ describe '#type_class' do
8
+ it 'converts an end-inclusive PostgreSQL integer range to a Ruby range' do
9
+ int4_range_column.type_cast('[0,4]').should eq 0..4
10
+ end
11
+
12
+ it 'converts an end-exclusive PostgreSQL integer range to a Ruby range' do
13
+ int4_range_column.type_cast('[0,4)').should eq 0...4
14
+ end
15
+
16
+ it 'converts an infinite PostgreSQL integer range to a Ruby range' do
17
+ int4_range_column.type_cast('(,4)').should eq -(1.0/0.0)...4
18
+ int4_range_column.type_cast('[0,)').should eq 0..(1.0/0.0)
19
+ end
20
+ end
21
+
22
+ describe 'int4 range to SQL statment conversion' do
23
+ it 'returns an end-inclusive PostgreSQL range' do
24
+ value = int4_range_column.type_cast('[0,4]')
25
+ adapter.type_cast(value, int4_range_column).should eq '[0,4]'
26
+ end
27
+ it 'returns an end-exclusive PostgreSQL range' do
28
+ value = int4_range_column.type_cast('[0,4)')
29
+ adapter.type_cast(value, int4_range_column).should eq '[0,4)'
30
+ end
31
+ it 'converts an infinite PostgreSQL integer range to a Ruby range' do
32
+ value = int4_range_column.type_cast('(,4)')
33
+ adapter.type_cast(value, int4_range_column).should eq '(,4)'
34
+ value = int4_range_column.type_cast('[0,)')
35
+ adapter.type_cast(value, int4_range_column).should eq '[0,)'
36
+ end
37
+ end
38
+ end