Skip to content
This repository has been archived by the owner on Sep 21, 2020. It is now read-only.

Commit

Permalink
removed hashgit^Cstring and some ActiveRecord monkey patching, trying…
Browse files Browse the repository at this point in the history
… to rely only in the custom serializer to store and retrieve values from the database
  • Loading branch information
diogob committed Feb 4, 2013
1 parent fb59b1c commit 7284b8b
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 331 deletions.
5 changes: 0 additions & 5 deletions Gemfile
Expand Up @@ -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
119 changes: 61 additions & 58 deletions README.md
Expand Up @@ -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:
Expand All @@ -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
-------

Expand All @@ -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:

Expand All @@ -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’:

Expand Down
3 changes: 1 addition & 2 deletions activerecord-postgres-hstore.gemspec
Expand Up @@ -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"
Expand All @@ -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"

Expand Down
8 changes: 0 additions & 8 deletions lib/activerecord-postgres-hstore.rb
@@ -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"
67 changes: 0 additions & 67 deletions lib/activerecord-postgres-hstore/activerecord.rb
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
40 changes: 38 additions & 2 deletions lib/activerecord-postgres-hstore/coder.rb
Expand Up @@ -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
Expand Down
39 changes: 0 additions & 39 deletions lib/activerecord-postgres-hstore/hash.rb

This file was deleted.

0 comments on commit 7284b8b

Please sign in to comment.