postgres_ext 0.3.1 → 0.4.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.
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