activerecord-postgres-array 0.0.2 → 0.0.3

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/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ Copyright (c) 2011 Tim Connor <tlconnor@gmail.com>
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
data/README.textile ADDED
@@ -0,0 +1,36 @@
1
+ h2. Postgres array support for activerecord
2
+
3
+ Add basic support for postgres arrays to activerecord, with special attention to getting rails migrations / schema dumps working nicely.
4
+
5
+
6
+ h2. Installation
7
+
8
+ <pre><code>gem install activerecord-postgres-array</code></pre>
9
+
10
+ or if you use bundler
11
+ <pre><code>gem 'activerecord-postgres-array'</code></pre>
12
+
13
+ h2. Usage
14
+
15
+ * In your migrations you can define postgres array fields such as:
16
+ <pre><code>create_table :people do |t|
17
+ ...
18
+ t.string_array :real_energy
19
+ t.decimal_array :real_energy, :precision => 18, :scale => 6
20
+ ...
21
+ end
22
+ </code></pre>
23
+
24
+ * When queried, the postgres arrays will be returned as ruby arrays, and vice versa.
25
+
26
+
27
+ h2. Current limitations
28
+
29
+ * Validation of serialised postgres array strings is currently not implemented.
30
+ * Parsing of multi-dimensional postgres array strings is currently not implemented.
31
+ * String and Decimal arrays have been tested, but other array types have not been. Type casting will need to be implemented for booleans, dates, etc
32
+
33
+ h3. Future enhancements
34
+
35
+ * Arel like querying of values within arrays
36
+ * Arel like aggregate functions
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ require 'rspec/core/rake_task'
11
+ RSpec::Core::RakeTask.new
12
+
13
+ task :default => :spec
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "activerecord-postgres-array"
3
+ s.version = "0.0.3"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Tim Connor"]
7
+ s.date = %q{2012-02-08}
8
+ s.description = "Adds support for postgres arrays to ActiveRecord"
9
+ s.email = "tlconnor@gmail.com"
10
+ s.homepage = "https://github.com/tlconnor/activerecord-postgres-array"
11
+ s.files = ["Gemfile", "LICENSE", "Rakefile", "README.textile", "activerecord-postgres-array.gemspec"] + Dir['**/*.rb']
12
+ s.require_paths = ["lib"]
13
+ s.rubygems_version = %q{1.3.7}
14
+ s.summary = s.description
15
+
16
+ s.add_dependency "rails"
17
+ s.add_development_dependency 'rake'
18
+ s.add_development_dependency 'rspec', '~> 2.0'
19
+ s.add_development_dependency 'pg'
20
+ s.add_development_dependency 'combustion', '~> 0.3.1'
21
+ end
@@ -1,21 +1,46 @@
1
+ require 'active_record/connection_adapters/postgresql_adapter'
2
+
1
3
  module ActiveRecord
2
4
  class ArrayTypeMismatch < ActiveRecord::ActiveRecordError
3
5
  end
4
6
 
7
+ class Base
8
+ def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
9
+ attrs = {}
10
+ klass = self.class
11
+ arel_table = klass.arel_table
12
+
13
+ attribute_names.each do |name|
14
+ if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
15
+ if include_readonly_attributes || !self.class.readonly_attributes.include?(name)
16
+ value = read_attribute(name)
17
+ if column.type.to_s =~ /_array$/ && value && value.is_a?(Array)
18
+ value = value.to_postgres_array(new_record?)
19
+ elsif klass.serialized_attributes.include?(name)
20
+ value = @attributes[name].serialized_value
21
+ end
22
+ attrs[arel_table[name]] = value
23
+ end
24
+ end
25
+ end
26
+
27
+ attrs
28
+ end
29
+ end
30
+
5
31
  module ConnectionAdapters
6
32
  class PostgreSQLAdapter < AbstractAdapter
7
33
  POSTGRES_ARRAY_TYPES = %w( string text integer float decimal datetime timestamp time date binary boolean )
8
34
 
9
35
  def native_database_types_with_array(*args)
10
- native_database_types_without_array.merge(POSTGRES_ARRAY_TYPES.inject(Hash.new) {|h, t| h.update("#{t}_array".to_sym => {:name => "#{t}_array"})})
36
+ native_database_types_without_array.merge(POSTGRES_ARRAY_TYPES.inject(Hash.new) {|h, t| h.update("#{t}_array".to_sym => {:name => "#{native_database_types_without_array[t.to_sym][:name]}[]"})})
11
37
  end
12
38
  alias_method_chain :native_database_types, :array
13
39
 
14
-
15
40
  # Quotes a value for use in an SQL statement
16
41
  def quote_with_array(value, column = nil)
17
42
  if value && column && column.sql_type =~ /\[\]$/
18
- raise ArrayTypeMismatch, "#{column.name} must have a Hash or a valid array value (#{value})" unless value.kind_of?(Array) || value.valid_postgres_array?
43
+ raise ArrayTypeMismatch, "#{column.name} must be an Array or have a valid array value (#{value})" unless value.kind_of?(Array) || value.valid_postgres_array?
19
44
  return value.to_postgres_array
20
45
  end
21
46
  quote_without_array(value,column)
@@ -53,13 +78,14 @@ module ActiveRecord
53
78
  end
54
79
  alias_method_chain :type_cast_code, :array
55
80
 
56
-
57
81
  # Adds the array type for the column.
58
82
  def simplified_type_with_array(field_type)
59
83
  if field_type =~ /^numeric.+\[\]$/
60
84
  :decimal_array
85
+ elsif field_type =~ /character varying.*\[\]/
86
+ :string_array
61
87
  elsif field_type =~ /\[\]$/
62
- field_type.gsub(/\[\]/, '_array')
88
+ field_type.gsub(/\[\]/, '_array').to_sym
63
89
  else
64
90
  simplified_type_without_array(field_type)
65
91
  end
@@ -1,14 +1,23 @@
1
1
  class Array
2
-
3
2
  # Generates a single quoted postgres array string format. This is the format used
4
3
  # to insert or update stuff in the database.
5
4
  def to_postgres_array(omit_quotes = false)
6
5
  result = "#{omit_quotes ? '' : "'" }{"
7
-
6
+
8
7
  result << collect do |value|
9
- value.is_a?(Array) ? value.to_postgres_array(true) : value
10
- end.join(", ")
11
-
8
+ if value.is_a?(Array)
9
+ value.to_postgres_array(true)
10
+ elsif value.is_a?(String)
11
+ value = value.gsub(/\\/, '\&\&')
12
+ value = value.gsub(/'/, "''")
13
+ value = value.gsub(/"/, '\"')
14
+ value = "\"#{ value }\""
15
+ value
16
+ else
17
+ value
18
+ end
19
+ end.join(",")
20
+
12
21
  result << "}#{omit_quotes ? '' : "'" }"
13
22
  end
14
23
 
@@ -16,5 +25,4 @@ class Array
16
25
  def from_postgres_array(base_type = :string)
17
26
  self
18
27
  end
19
-
20
28
  end
@@ -1,5 +1,4 @@
1
1
  class String
2
-
3
2
  def to_postgres_array
4
3
  self
5
4
  end
@@ -9,22 +8,28 @@ class String
9
8
  # * A string like '{10000, 10000, 10000, 10000}'
10
9
  # * TODO A multi dimensional array string like '{{"meeting", "lunch"}, {"training", "presentation"}}'
11
10
  def valid_postgres_array?
12
- # TODO validate formats above
13
- true
11
+ string_regexp = /[^",\\]+/
12
+ quoted_string_regexp = /"[^"\\]*(?:\\.[^"\\]*)*"/
13
+ number_regexp = /[-+]?[0-9]*\.?[0-9]+/
14
+ validation_regexp = /\{\s*((#{number_regexp}|#{quoted_string_regexp}|#{string_regexp})(\s*\,\s*(#{number_regexp}|#{quoted_string_regexp}|#{string_regexp}))*)?\}/
15
+ !!match(/^\s*('#{validation_regexp}'|#{validation_regexp})?\s*$/)
14
16
  end
15
17
 
16
18
  # Creates an array from a postgres array string that postgresql spits out.
17
19
  def from_postgres_array(base_type = :string)
18
20
  if empty?
19
- return []
21
+ []
20
22
  else
21
- elements = match(/^\{(.+)\}$/).captures.first.split(",").collect(&:strip)
22
-
23
+ elements = match(/\{(.*)\}/).captures.first.gsub(/\\"/, '$ESCAPED_DOUBLE_QUOTE$').split(/(,)(?=(?:[^"]|"[^"]*")*$)/).reject {|e| e == ',' }
24
+ elements = elements.map do |e|
25
+ e.gsub('$ESCAPED_DOUBLE_QUOTE$', '"').gsub("\\\\", "\\").gsub(/^"/, '').gsub(/"$/, '').gsub("''", "'").strip
26
+ end
27
+
23
28
  if base_type == :decimal
24
- return elements.collect(&:to_d)
29
+ elements.collect(&:to_d)
25
30
  else
26
- return elements
31
+ elements
27
32
  end
28
33
  end
29
34
  end
30
- end
35
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'activerecord-postgres-array/array'
3
+
4
+ describe "Array" do
5
+ describe "#to_postgres_array" do
6
+ it "returns '{}' if used on an empty array" do
7
+ [].to_postgres_array.should == "'{}'"
8
+ end
9
+
10
+ it "returns a correct array if used on a numerical array" do
11
+ [1,2,3].to_postgres_array.should == "'{1,2,3}'"
12
+ end
13
+
14
+ it "returns a correct array if used on a string array" do
15
+ ["Ruby","on","Rails"].to_postgres_array.should == "'{\"Ruby\",\"on\",\"Rails\"}'"
16
+ end
17
+
18
+ it "escapes double quotes correctly" do
19
+ ["Ruby","on","Ra\"ils"].to_postgres_array.should == "'{\"Ruby\",\"on\",\"Ra\\\"ils\"}'"
20
+ end
21
+
22
+ it "escapes backslashes correctly" do
23
+ ["\\","\""].to_postgres_array.should == '\'{"\\\\","\\""}\''
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ describe Article do
4
+ describe ".create" do
5
+ it "builds valid arrays" do
6
+ article = Article.create(:languages => ["English", "German"])
7
+ article.reload
8
+ article.languages_before_type_cast.should == "{English,German}"
9
+ article.languages.should == ["English", "German"]
10
+ end
11
+
12
+ it "escapes single quotes correctly" do
13
+ article = Article.create(:languages => ["English", "Ger'man"])
14
+ article.reload
15
+ article.languages_before_type_cast.should == "{English,Ger''man}"
16
+ article.languages.should == ["English", "Ger'man"]
17
+ end
18
+
19
+ it "escapes double quotes correctly" do
20
+ article = Article.create(:languages => ["English", "Ger\"man"])
21
+ article.reload
22
+ article.languages_before_type_cast.should == "{English,\"Ger\\\"man\"}"
23
+ article.languages.should == ["English", "Ger\"man"]
24
+ end
25
+
26
+ it "handles commas correctly" do
27
+ article = Article.create(:languages => ["English", "Ger,man"])
28
+ article.reload
29
+ article.languages_before_type_cast.should == "{English,\"Ger,man\"}"
30
+ article.languages.should == ["English", "Ger,man"]
31
+ end
32
+
33
+ it "handles backslashes correctly" do
34
+ article = Article.create(:languages => ["\\","\""])
35
+ article.reload
36
+ article.languages_before_type_cast.should == '{"\\\\","\\""}'
37
+ article.languages.should == ["\\","\""]
38
+ end
39
+ end
40
+
41
+ describe ".update" do
42
+ before(:each) do
43
+ @article = Article.create
44
+ end
45
+
46
+ it "builds valid arrays" do
47
+ @article.languages = ["English", "German"]
48
+ @article.save
49
+ @article.reload
50
+ @article.languages_before_type_cast.should == "{English,German}"
51
+ end
52
+
53
+ it "escapes single quotes correctly" do
54
+ @article.languages = ["English", "Ger'man"]
55
+ @article.save
56
+ @article.reload
57
+ @article.languages_before_type_cast.should == "{English,Ger'man}"
58
+ @article.languages.should == ["English", "Ger'man"]
59
+ end
60
+
61
+ it "escapes double quotes correctly" do
62
+ @article.languages = ["English", "Ger\"man"]
63
+ @article.save
64
+ @article.reload
65
+ @article.languages_before_type_cast.should == "{English,\"Ger\\\"man\"}"
66
+ @article.languages.should == ["English", "Ger\"man"]
67
+ end
68
+
69
+ it "handles commas correctly" do
70
+ @article.languages = ["English", "Ger,man"]
71
+ @article.save
72
+ @article.reload
73
+ @article.languages_before_type_cast.should == "{English,\"Ger,man\"}"
74
+ @article.languages.should == ["English", "Ger,man"]
75
+ end
76
+
77
+ it "handles backslashes correctly" do
78
+ @article.languages = ["\\","\""]
79
+ @article.save
80
+ @article.reload
81
+ @article.languages_before_type_cast.should == '{"\\\\","\\""}'
82
+ @article.languages.should == ["\\","\""]
83
+ end
84
+ end
85
+
86
+ end
@@ -0,0 +1,2 @@
1
+ class Article < ActiveRecord::Base
2
+ end
@@ -0,0 +1,6 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table(:articles, :force => true) do |t|
3
+ t.string :name
4
+ t.string_array :languages
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize! :active_record
7
+
8
+ RSpec.configure do |config|
9
+ config.treat_symbols_as_metadata_keys_with_true_values = true
10
+ config.run_all_when_everything_filtered = true
11
+ config.filter_run :focus
12
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+ require 'activerecord-postgres-array/string'
3
+
4
+ describe "String" do
5
+ describe "#valid_postgres_array?" do
6
+ it 'returns true for an empty string' do
7
+ "".should be_valid_postgres_array
8
+ end
9
+
10
+ it 'returns true for a string consisting only of whitespace' do
11
+ " ".should be_valid_postgres_array
12
+ end
13
+
14
+ it 'returns true for a valid postgres integer array' do
15
+ "{10000, 10000, 10000, 10000}".should be_valid_postgres_array
16
+ end
17
+
18
+ it 'returns true for a valid postgres float array' do
19
+ "{10000.2, .5, 10000, 10000.9}".should be_valid_postgres_array
20
+ end
21
+
22
+ it 'returns true for a valid postgres numerical array with irregular whitespace' do
23
+ "{ 10000, 10000 , 10000,10000}".should be_valid_postgres_array
24
+ end
25
+
26
+ it 'returns false for an array with invalid commas' do
27
+ "{213,}".should_not be_valid_postgres_array
28
+ end
29
+
30
+ it 'allows enclosing single quotes' do
31
+ '\'{"ruby", "on", "rails"}\''.should be_valid_postgres_array
32
+ end
33
+
34
+ it 'returns false for an array without enclosing curly brackets' do
35
+ "213, 1234".should_not be_valid_postgres_array
36
+ end
37
+
38
+ it 'returns true for a valid postgres string array' do
39
+ '{"ruby", "on", "rails"}'.should be_valid_postgres_array
40
+ end
41
+
42
+ it 'returns true for a postgres string array with escaped double quote' do
43
+ '{"ruby", "on", "ra\"ils"}'.should be_valid_postgres_array
44
+ end
45
+
46
+ it 'returns false for a postgres string array with wrong quotation' do
47
+ '{"ruby", "on", "ra"ils"}'.should_not be_valid_postgres_array
48
+ end
49
+
50
+ it 'returns true for string array without quotes' do
51
+ "{ruby, on, rails}".should be_valid_postgres_array
52
+ end
53
+
54
+ it 'returns false for array consisting of commas' do
55
+ "{,,}".should_not be_valid_postgres_array
56
+ end
57
+
58
+ it 'returns false for concatenated strings' do
59
+ '{"ruby""on""rails"}'.should_not be_valid_postgres_array
60
+ end
61
+
62
+ it "returns false if single quotes are not closed" do
63
+ '\'{"ruby", "on", "rails"}'.should_not be_valid_postgres_array
64
+ end
65
+
66
+ it "returns true for an empty postgres array" do
67
+ "{}".should be_valid_postgres_array
68
+ end
69
+
70
+ it "returns false for postgres array beginning with ," do
71
+ "{,ruby,on,rails}".should_not be_valid_postgres_array
72
+ end
73
+
74
+ end
75
+
76
+ describe "#from_postgres_array" do
77
+ it 'returns an empty array if string is empty' do
78
+ "".from_postgres_array.should == []
79
+ end
80
+
81
+ it 'returns an empty array if empty postgres array is given' do
82
+ "{}".from_postgres_array.should == []
83
+ end
84
+
85
+ it 'returns an correct array if a valid postgres array is given' do
86
+ "{Ruby,on,Rails}".from_postgres_array.should == ["Ruby", "on", "Rails"]
87
+ end
88
+
89
+ it 'correctly handles commas' do
90
+ '{Ruby,on,"Rails,"}'.from_postgres_array.should == ["Ruby", "on", "Rails,"]
91
+ end
92
+
93
+ it 'correctly handles single quotes' do
94
+ "{Ruby,on,Ra'ils}".from_postgres_array.should == ["Ruby", "on", "Ra'ils"]
95
+ end
96
+
97
+ it 'correctly handles double quotes' do
98
+ "{Ruby,on,\"Ra\\\"ils\"}".from_postgres_array.should == ["Ruby", "on", 'Ra"ils']
99
+ end
100
+
101
+ it 'correctly handles backslashes' do
102
+ '\'{"\\\\","\\""}\''.from_postgres_array.should == ["\\","\""]
103
+ end
104
+ end
105
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-postgres-array
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 2
10
- version: 0.0.2
9
+ - 3
10
+ version: 0.0.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Tim Connor
@@ -15,12 +15,84 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-04-15 00:00:00 +12:00
18
+ date: 2012-02-08 00:00:00 +13:00
19
19
  default_executable:
20
- dependencies: []
21
-
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rails
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rake
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 2
60
+ - 0
61
+ version: "2.0"
62
+ type: :development
63
+ version_requirements: *id003
64
+ - !ruby/object:Gem::Dependency
65
+ name: pg
66
+ prerelease: false
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ hash: 3
73
+ segments:
74
+ - 0
75
+ version: "0"
76
+ type: :development
77
+ version_requirements: *id004
78
+ - !ruby/object:Gem::Dependency
79
+ name: combustion
80
+ prerelease: false
81
+ requirement: &id005 !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ~>
85
+ - !ruby/object:Gem::Version
86
+ hash: 17
87
+ segments:
88
+ - 0
89
+ - 3
90
+ - 1
91
+ version: 0.3.1
92
+ type: :development
93
+ version_requirements: *id005
22
94
  description: Adds support for postgres arrays to ActiveRecord
23
- email: tim@youdo.co.nz
95
+ email: tlconnor@gmail.com
24
96
  executables: []
25
97
 
26
98
  extensions: []
@@ -28,12 +100,23 @@ extensions: []
28
100
  extra_rdoc_files: []
29
101
 
30
102
  files:
31
- - lib/activerecord-postgres-array.rb
103
+ - Gemfile
104
+ - LICENSE
105
+ - Rakefile
106
+ - README.textile
107
+ - activerecord-postgres-array.gemspec
32
108
  - lib/activerecord-postgres-array/activerecord.rb
33
109
  - lib/activerecord-postgres-array/array.rb
34
110
  - lib/activerecord-postgres-array/string.rb
111
+ - lib/activerecord-postgres-array.rb
112
+ - spec/array_ext_spec.rb
113
+ - spec/integration_spec.rb
114
+ - spec/internal/app/models/article.rb
115
+ - spec/internal/db/schema.rb
116
+ - spec/spec_helper.rb
117
+ - spec/string_ext_spec.rb
35
118
  has_rdoc: true
36
- homepage:
119
+ homepage: https://github.com/tlconnor/activerecord-postgres-array
37
120
  licenses: []
38
121
 
39
122
  post_install_message: