activerecord-postgres-array 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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: