Permalink
Browse files

removed hashgit^Cstring and some ActiveRecord monkey patching, trying…

… to rely only in the custom serializer to store and retrieve values from the database
  • Loading branch information...
1 parent fb59b1c commit 7284b8b063293428292666c7290592ea922ca598 @diogob committed Feb 4, 2013
View
@@ -3,8 +3,3 @@ source "http://rubygems.org"
# specify gem dependencies in activerecord-postgres-hstore.gemspec
# except the platform-specific dependencies below
gemspec
-
-group :development, :test do
- gem 'activerecord-jdbcpostgresql-adapter', :platforms => :jruby
- gem 'pg', :platforms => :ruby
-end
View
119 README.md
@@ -9,11 +9,18 @@ You need dynamic columns in your tables. What do you do?
* Use a noSQL database just for this issue. Good luck.
* Create a serialized column. Nice, insertion will be fine, and reading data from a record too. But, what if you have a condition in your select that includes serialized data? Yeah, regular expressions.
+Note about 0.7
+--------------
+I have decided to clean up the old code and provide only a custom serializer in this new version.
+In order to acomplish this I had to drop support for older versions of Rails (3.0 and earlier) and also
+remove some monkey patches that added functionality to the Hash, String, and some ActiveRecord objects.
+This monkey patches provided methods such as Hash\#to\_hstore and String\#from\_hstore.
+If you rely on this feature please stick to 0.6 version and there is still a branch named 0.6 to which you can submit your pull requests.
+
Requirements
------------
-Postgresql 8.4+ with contrib and Rails 3+. (It
-might work on 2.3.x with minor patches…)
+Postgresql 8.4+ with contrib and Rails 3.1+ (If you want to try on older rails versions I recommend the 0.6 and ealier versions of this gem)
On Ubuntu, this is easy: `sudo apt-get install postgresql-contrib-9.1`
On Mac you have a couple of options:
@@ -22,42 +29,6 @@ On Mac you have a couple of options:
* [Homebrew’s](https://github.com/mxcl/homebrew) Postgres installation also includes the contrib packages: `brew install postgres`
* [Postgres.app](http://postgresapp.com/)
-Notes for Rails 3.1 and above
------------------------------
-
-The master branch already support a custom serialization coder.
-If you want to use it just put in your Gemfile:
-
- gem 'activerecord-postgres-hstore', github: 'engageis/activerecord-postgres-hstore'
-
-If you install them gem from the master branch you also have to insert a
-line in each model that uses hstore.
-Assuming a model called **Person**, with a **data** field on it, the
-code should look like:
-
- class Person < ActiveRecord::Base
- serialize :data, ActiveRecord::Coders::Hstore
- end
-
-If you want a default value (say, an empty hash) you should pass it as an argument when you
-initialize the serializer, like so:
-
- class Person < ActiveRecord::Base
- serialize :data, ActiveRecord::Coders::Hstore.new({})
- end
-
-This way, you will automatically start with an empty hash that you can write attributes to.
-
- irb(main):001:0> person = Person.new
- => #<Person id: nil, name: nil, data: {}, created_at: nil, updated_at: nil>
- irb(main):002:0> person.data['favorite_color'] = 'blue'
- => "blue"
-
-If you skip this step, you will have to manually initialize the value to an empty hash before
-writing to the attribute, or else you will get an error:
-
- NoMethodError: undefined method `[]=' for nil:NilClass
-
Install
-------
@@ -71,23 +42,6 @@ And run your bundler:
`bundle install`
-Make sure that you have the desired database, if not create it as the
-desired user:
-
-`createdb hstorage_dev`
-
-Add the parameters to your database.yml (these are system dependant),
-e.g.:
-
- development:
- adapter: postgresql
- host: 127.0.0.1
- database: hstorage_dev
- encoding: unicode
- username: postgres
- password:
- pool: 5
-
Now you need to create a migration that adds hstore support for your
PostgreSQL database:
@@ -105,17 +59,66 @@ Finally you can create your own tables using hstore type. It’s easy:
You’re done.
Well, not yet. Don’t forget to add indexes. Like this:
-`CREATE INDEX people_gist_data ON people USING GIST(data);`
+```sql CREATE INDEX people_gist_data ON people USING GIST(data);```
or
-`CREATE INDEX people_gin_data ON people USING GIN(data);`
+```sql CREATE INDEX people_gin_data ON people USING GIN(data);```
+
+This gem provides some functions to generate this kind of index inside your migrations.
+For the model Person we could create an index (defaults to type GIST) over the data field with this migration:
+
+```ruby
+class AddIndexToPeople < ActiveRecord::Migration
+ def change
+ add_hstore_index :people, :data
+ end
+end
+```
To understand the difference between the two types of indexes take a
look at [PostgreSQL docs](http://www.postgresql.org/docs/9.2/static/textsearch-indexes.html).
Usage
-----
-Once you have it installed, you just need to learn a little bit of new
+This gem only provides a custom serialization coder.
+If you want to use it just put in your Gemfile:
+
+ gem 'activerecord-postgres-hstore'
+
+Now add a line (for each hstore column) on the model you have your hstore columns.
+Assuming a model called **Person**, with a **data** field on it, the
+code should look like:
+
+```ruby
+class Person < ActiveRecord::Base
+ serialize :data, ActiveRecord::Coders::Hstore
+end
+```
+
+If you want a default value (say, an empty hash) you should pass it as an argument when you
+initialize the serializer, like so:
+
+```ruby
+class Person < ActiveRecord::Base
+ serialize :data, ActiveRecord::Coders::Hstore.new({})
+end
+```
+
+This way, you will automatically start with an empty hash that you can write attributes to.
+
+ irb(main):001:0> person = Person.new
+ => #<Person id: nil, name: nil, data: {}, created_at: nil, updated_at: nil>
+ irb(main):002:0> person.data['favorite_color'] = 'blue'
+ => "blue"
+
+If you skip this step, you will have to manually initialize the value to an empty hash before
+writing to the attribute, or else you will get an error:
+
+ NoMethodError: undefined method `[]=' for nil:NilClass
+
+Querying the database
+---------------------
+Now you just need to learn a little bit of new
sqls for selecting stuff (creating and updating is transparent).
Find records that contains a key named 'foo’:
@@ -4,7 +4,7 @@ $:.unshift lib unless $:.include?(lib)
Gem::Specification.new do |s|
s.name = "activerecord-postgres-hstore"
- s.version = "0.6.0"
+ s.version = "0.7.0"
s.platform = Gem::Platform::RUBY
s.license = "MIT"
@@ -19,7 +19,6 @@ Gem::Specification.new do |s|
s.add_dependency "rails"
s.add_dependency "rake"
s.add_development_dependency "bundler"
- s.add_development_dependency "shoulda"
s.add_development_dependency "rdoc"
s.add_development_dependency "rspec", "~> 2.11"
@@ -1,18 +1,10 @@
require 'active_support'
-if RUBY_PLATFORM == "jruby"
- require 'activerecord-jdbcpostgresql-adapter'
-else
- require 'pg'
-end
-
if defined? Rails
require "activerecord-postgres-hstore/railties"
else
ActiveSupport.on_load :active_record do
require "activerecord-postgres-hstore/activerecord"
end
end
-require "activerecord-postgres-hstore/string"
-require "activerecord-postgres-hstore/hash"
require "activerecord-postgres-hstore/coder"
@@ -3,7 +3,6 @@ module ActiveRecord
# Adds methods for deleting keys in your hstore columns
class Base
-
# Deletes all keys from a specific column in a model. E.g.
# Person.delete_key(:info, :father)
# The SQL generated will be:
@@ -65,42 +64,9 @@ def destroy_keys! attribute, *keys
raise "invalid attribute #{attribute}" unless self.class.column_names.include?(attribute.to_s)
destroy_keys(attribute, *keys).save
end
-
- if defined? Rails and Rails.version < '3.1.0'
- # This method is replaced for Rails 3 compatibility.
- # All I do is add the condition when the field is a hash that converts the value
- # to hstore format.
- # IMHO this should be delegated to the column, so it won't be necessary to rewrite all
- # this method.
- def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
- attrs = {}
- attribute_names.each do |name|
- if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
- if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name))
- value = read_attribute(name)
- if self.class.columns_hash[name].type == :hstore && value && value.is_a?(Hash)
- value = value.to_hstore # Done!
- elsif value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time) || value.is_a?(Hash) || value.is_a?(Array))
- value = value.to_yaml
- end
- attrs[self.class.arel_table[name]] = value
- end
- end
- end
- attrs
- end
- end
-
- end
-
- # This erro class is used when the user passes a wrong value to a hstore column.
- # Hstore columns accepts hashes or hstore valid strings. It is validated with
- # String#valid_hstore? method.
- class HstoreTypeMismatch < ActiveRecord::ActiveRecordError
end
module ConnectionAdapters
-
module SchemaStatements
# Installs hstore by creating the Postgres extension
@@ -175,38 +141,5 @@ def hstore(*args)
end
end
-
- class PostgreSQLColumn < Column
- # Does the type casting from hstore columns using String#from_hstore or Hash#from_hstore.
- def type_cast_code_with_hstore(var_name)
- type == :hstore ? "#{var_name}.from_hstore" : type_cast_code_without_hstore(var_name)
- end
-
- # Adds the hstore type for the column.
- def simplified_type_with_hstore(field_type)
- field_type == 'hstore' ? :hstore : simplified_type_without_hstore(field_type)
- end
-
- alias_method_chain :type_cast_code, :hstore
- alias_method_chain :simplified_type, :hstore
- end
-
- class PostgreSQLAdapter < AbstractAdapter
- def native_database_types_with_hstore
- native_database_types_without_hstore.merge({:hstore => { :name => "hstore" }})
- end
-
- # Quotes correctly a hstore column value.
- def quote_with_hstore(value, column = nil)
- if value && column && column.sql_type == 'hstore'
- raise HstoreTypeMismatch, "#{column.name} must have a Hash or a valid hstore value (#{value})" unless value.kind_of?(Hash) || value.valid_hstore?
- return quote_without_hstore(value.to_hstore, column)
- end
- quote_without_hstore(value,column)
- end
-
- alias_method_chain :quote, :hstore
- alias_method_chain :native_database_types, :hstore
- end
end
end
@@ -14,11 +14,47 @@ def initialize(default=nil)
end
def dump(obj)
- obj.nil? ? (@default.nil? ? nil : @default.to_hstore) : obj.to_hstore
+ obj.nil? ? (@default.nil? ? nil : to_hstore(@default)) : to_hstore(obj)
end
def load(hstore)
- hstore.nil? ? @default : hstore.from_hstore
+ hstore.nil? ? @default : from_hstore(hstore)
+ end
+
+ private
+ # Escapes values such that they will work in an hstore string
+ def hstore_escape(str)
+ return 'NULL' if str.nil?
+ return str if str =~ /^".*"$/
+ '"%s"' % str
+ end
+
+ def to_hstore obj
+ return "" if obj.empty?
+ obj.map do |idx, val|
+ "%s=>%s" % [hstore_escape(idx), hstore_escape(val)]
+ end * ","
+ end
+
+ def hstore_pair
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /[^\s=,][^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ string = /(#{quoted_string}|#{unquoted_string})/
+ /#{string}\s*=>\s*#{string}/
+ end
+
+ def from_hstore hstore
+ token_pairs = (hstore.scan(hstore_pair)).map { |k,v| [k,v =~ /^NULL$/i ? nil : v] }
+ token_pairs = token_pairs.map { |k,v|
+ [k,v].map { |t|
+ case t
+ when nil then t
+ when /\A"(.*)"\Z/m then $1.gsub(/\\(.)/, '\1')
+ else t.gsub(/\\(.)/, '\1')
+ end
+ }
+ }
+ Hash[ token_pairs ]
end
end
end
@@ -1,39 +0,0 @@
-class Hash
- HSTORE_ESCAPED = /[,\s=>\\]/
-
- # Escapes values such that they will work in an hstore string
- def hstore_escape(str)
- if str.nil?
- return 'NULL'
- end
-
- str = str.to_s.dup
- # backslash is an escape character for strings, and an escape character for gsub, so you need 6 backslashes to get 2 in the output.
- # see http://stackoverflow.com/questions/1542214/weird-backslash-substitution-in-ruby for the gory details
- str.gsub!(/\\/, '\\\\\\')
- # escape backslashes before injecting more backslashes
- str.gsub!(/"/, '\"')
-
- if str =~ HSTORE_ESCAPED or str.empty?
- str = '"%s"' % str
- end
-
- return str
- end
-
- # Generates an hstore string format. This is the format used
- # to insert or update stuff in the database.
- def to_hstore
- return "" if empty?
-
- map do |idx, val|
- "%s=>%s" % [hstore_escape(idx), hstore_escape(val)]
- end * ","
- end
-
- # If the method from_hstore is called in a Hash, it just returns self.
- def from_hstore
- self
- end
-
-end
Oops, something went wrong.

0 comments on commit 7284b8b

Please sign in to comment.