├── .document ├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── activerecord-postgres-hstore.gemspec ├── lib ├── activerecord-postgres-hstore.rb ├── activerecord-postgres-hstore │ ├── activerecord.rb │ ├── coder.rb │ └── railties.rb ├── tasks │ └── hstore.rake └── templates │ ├── setup_hstore.rb │ └── setup_hstore91.rb └── spec ├── activerecord-coders-hstore_spec.rb └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | Gemfile.lock 21 | 22 | ## PROJECT::SPECIFIC 23 | 24 | ## RVM 25 | .rvmrc 26 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color --format doc 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.6 4 | - 2.4.3 5 | - 2.5.0 6 | - rbx-2 7 | - ruby-head 8 | - jruby-head 9 | 10 | # jruby-head sometimes breaks stuff, but it's passing at the moment 11 | matrix: 12 | allow_failures: 13 | - rvm: jruby-head 14 | - rvm: rbx-2 15 | 16 | branches: 17 | only: 18 | - master 19 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.7.6 / 2013-03-26 2 | 3 | * Bug fixes 4 | 5 | * Fixed JRuby regressions 6 | 7 | 0.7.5 / 2013-02-14 8 | 9 | * Bug fixes 10 | 11 | * Fixed problem while dumping hstore fields to schema.rb 12 | 13 | 0.7.4 / 2013-02-11 14 | 15 | * Enhancements 16 | 17 | * Replaced Rails dependency with ActiveRecord 18 | 19 | 0.7.3 / 2013-02-07 20 | 21 | * Enhancements 22 | 23 | * Always use empty hash as default value 24 | 25 | 0.7.2 / 2013-02-07 26 | 27 | * Bug fixes 28 | 29 | * Properly quote/escape double quotes https://github.com/engageis/activerecord-postgres-hstore/issues/78 30 | 31 | * Enhancements 32 | 33 | * Delegate hstore dumping/loading to https://github.com/seamusabshere/pg-hstore 34 | * Add a CHANGELOG! 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # specify gem dependencies in activerecord-postgres-hstore.gemspec 4 | # except the platform-specific dependencies below 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Juan Maiz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED - Just use Rails JSON or HStore built-in support. 2 | 3 | If you are using Rails 4 you don't need this gem as ActiveRecord 4 provides HStore type support out of the box. ActiveRecord will see your HStore column and do all of the work for you. **Additional code is no longer needed.** 4 | 5 | You can test it with a migration like this: 6 | ```ruby 7 | class CreateTest < ActiveRecord::Migration 8 | def change 9 | create_table :tests do |t| 10 | t.hstore :data 11 | end 12 | end 13 | end 14 | ``` 15 | 16 | Its model: 17 | ```ruby 18 | class Test < ActiveRecord::Base 19 | # before Rails 4, we'd have to this here: 20 | # serialize :data, ActiveRecord::Coders::Hstore 21 | end 22 | ``` 23 | 24 | Then you can use the hash field straight away: 25 | ```ruby 26 | irb(main):003:0> t = Test.new data: {a: 1, b:2} 27 | => #"1", "b"=>"2"}> 28 | irb(main):004:0> t.save! 29 | (0.3ms) BEGIN 30 | SQL (2.3ms) INSERT INTO "tests" ("data") VALUES ($1) RETURNING "id" [["data", "\"a\"=>\"1\",\"b\"=>\"2\""]] 31 | (0.5ms) COMMIT 32 | => true 33 | irb(main):005:0> t 34 | => #"1", "b"=>"2"}> 35 | irb(main):006:0> t.data 36 | => {"a"=>"1", "b"=>"2"} 37 | irb(main):007:0> t.data['a'] 38 | => "1" 39 | ``` 40 | 41 | For more information take a look [here](http://jes.al/2013/11/using-postgres-hstore-rails4/) 42 | 43 | ## Common use cases 44 | 45 | Add settings to users, like in rails-settings or HasEasy. 46 | 47 | ```ruby 48 | class User < ActiveRecord::Base 49 | serialize :settings, ActiveRecord::Coders::Hstore 50 | end 51 | user = User.create settings: {theme: 'navy'} 52 | user.settings['theme'] 53 | ``` 54 | 55 | ## Requirements 56 | 57 | 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) 58 | On Ubuntu, this is easy: `sudo apt-get install postgresql-contrib-9.1` 59 | 60 | On Mac you have a couple of options: 61 | 62 | * [the binary package kindly provided by EnterpriseDB](http://www.enterprisedb.com/products-services-training/pgdownload#osx) 63 | * [Homebrew’s](https://github.com/mxcl/homebrew) Postgres installation also includes the contrib packages: `brew install postgres` 64 | * [Postgres.app](http://postgresapp.com/) 65 | 66 | ## Install 67 | 68 | 69 | Hstore is a PostgreSQL contrib type, [check it out first](http://www.postgresql.org/docs/9.2/static/hstore.html). 70 | 71 | Then, just add this to your Gemfile: 72 | 73 | `gem 'activerecord-postgres-hstore'` 74 | 75 | And run your bundler: 76 | 77 | `bundle install` 78 | 79 | Now you need to create a migration that adds hstore support for your 80 | PostgreSQL database: 81 | 82 | `rails g hstore:setup` 83 | 84 | Run it: 85 | 86 | `rake db:migrate` 87 | 88 | Finally you can create your own tables using hstore type. It’s easy: 89 | 90 | rails g model Person name:string data:hstore 91 | rake db:migrate 92 | 93 | You’re done. 94 | Well, not yet. Don’t forget to add indexes. Like this: 95 | 96 | ```sql 97 | CREATE INDEX people_gist_data ON people USING GIST(data); 98 | ``` 99 | or 100 | ```sql 101 | CREATE INDEX people_gin_data ON people USING GIN(data); 102 | ``` 103 | 104 | This gem provides some functions to generate this kind of index inside your migrations. 105 | For the model Person we could create an index (defaults to type GIST) over the data field with this migration: 106 | 107 | ```ruby 108 | class AddIndexToPeople < ActiveRecord::Migration 109 | def change 110 | add_hstore_index :people, :data 111 | end 112 | end 113 | ``` 114 | 115 | To understand the difference between the two types of indexes take a 116 | look at [PostgreSQL docs](http://www.postgresql.org/docs/9.2/static/textsearch-indexes.html). 117 | 118 | ## Usage 119 | 120 | This gem only provides a custom serialization coder. 121 | If you want to use it just put in your Gemfile: 122 | 123 | ```ruby 124 | gem 'activerecord-postgres-hstore' 125 | ``` 126 | 127 | Now add a line (for each hstore column) on the model you have your hstore columns. 128 | Assuming a model called **Person**, with a **data** field on it, the 129 | code should look like: 130 | 131 | ```ruby 132 | class Person < ActiveRecord::Base 133 | serialize :data, ActiveRecord::Coders::Hstore 134 | end 135 | ``` 136 | 137 | This way, you will automatically start with an empty hash that you can write attributes to. 138 | 139 | ```ruby 140 | irb(main):001:0> person = Person.new 141 | => # 142 | irb(main):002:0> person.data['favorite_color'] = 'blue' 143 | => "blue" 144 | ``` 145 | 146 | ### Querying the database 147 | 148 | Now you just need to learn a little bit of new 149 | sqls for selecting stuff (creating and updating is transparent). 150 | Find records that contains a key named 'foo’: 151 | 152 | ```ruby 153 | Person.where("data ? 'foo'") 154 | ``` 155 | 156 | Find records where 'foo’ is equal to 'bar’: 157 | 158 | ```ruby 159 | Person.where("data -> 'foo' = 'bar'") 160 | ``` 161 | 162 | This same sql is at least twice as fast (using indexes) if you do it 163 | that way: 164 | 165 | ```ruby 166 | Person.where("data @> 'foo=>bar'") 167 | ``` 168 | 169 | Find records where 'foo’ is not equal to 'bar’: 170 | 171 | ```ruby 172 | Person.where("data -> 'foo' <> 'bar'") 173 | ``` 174 | 175 | Find records where 'foo’ is like 'bar’: 176 | 177 | ```ruby 178 | Person.where("data -> 'foo' LIKE '%bar%'") 179 | ``` 180 | 181 | If you need to delete a key in a record, you can do it that way: 182 | 183 | ```ruby 184 | person.destroy_key(:data, :foo) 185 | ``` 186 | 187 | This way you’ll also save the record: 188 | 189 | ```ruby 190 | person.destroy_key!(:data, :foo) 191 | ``` 192 | 193 | The destroy\_key method returns 'self’, so you can chain it: 194 | 195 | ```ruby 196 | person.destroy_key(:data, :foo).destroy_key(:data, :bar).save 197 | ``` 198 | 199 | But there is a shortcuts for that: 200 | 201 | ```ruby 202 | person.destroy_keys(:data, :foo, :bar) 203 | ``` 204 | 205 | And finally, if you need to delete keys in many rows, you can: 206 | 207 | ```ruby 208 | Person.delete_key(:data, :foo) 209 | ``` 210 | 211 | and with many keys: 212 | 213 | ```ruby 214 | Person.delete_keys(:data, :foo, :bar) 215 | ``` 216 | 217 | ## Caveats 218 | 219 | hstore keys and values have to be strings. This means `true` will become `"true"` and `42` will become `"42"` after you save the record. Only `nil` values are preserved. 220 | 221 | It is also confusing when querying: 222 | 223 | ```ruby 224 | Person.where("data -> 'foo' = :value", value: true).to_sql 225 | #=> SELECT "people".* FROM "people" WHERE ("data -> 'foo' = 't'") # notice 't' 226 | ``` 227 | 228 | To avoid the above, make sure all named parameters are strings: 229 | 230 | ```ruby 231 | Person.where("data -> 'foo' = :value", value: some_var.to_s) 232 | ``` 233 | 234 | Have fun. 235 | 236 | ## Test Database 237 | 238 | To have hstore enabled when you load your database schema (as happens in rake db:test:prepare), you 239 | have two options. 240 | 241 | The first option is creating a template database with hstore installed and set the template option 242 | in database.yml to that database. If you use the template1 database for this you don't even need to 243 | set the template option, but the extension will be installed in all your databases from now on 244 | by default. To install the extension in your template1 database you could simply run: 245 | 246 | ```ruby 247 | psql -d template1 -c 'create extension hstore;' 248 | ``` 249 | 250 | The second option is to uncomment or add the following line in config/application.rb 251 | 252 | ```ruby 253 | config.active_record.schema_format = :sql 254 | ``` 255 | 256 | This will change your schema dumps from Ruby to SQL. If you're 257 | unsure about the implications of this change, we suggest reading this 258 | [Rails Guide](http://guides.rubyonrails.org/migrations.html#schema-dumping-and-you). 259 | 260 | ## Help 261 | 262 | You can use issues in github for that. Or else you can reach us at 263 | twitter: [@dbiazus](https://twitter.com/#!/dbiazus) or [@joaomilho](https://twitter.com/#!/joaomilho) 264 | 265 | ## Note on Patches/Pull Requests 266 | 267 | 268 | * Fork the project. 269 | * Make your feature addition or bug fix. 270 | * Add tests for it. This is important so I don’t break it in a future version unintentionally. 271 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 272 | * Send me a pull request. Bonus points for topic branches. 273 | 274 | ## Copyright 275 | 276 | Copyright © 2010 Juan Maiz. See LICENSE for details. 277 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.unshift File.expand_path("../lib", __FILE__) 3 | require 'bundler/gem_tasks' 4 | require 'rubygems' 5 | require 'rspec/core/rake_task' 6 | require 'rdoc/task' 7 | 8 | task :default => :spec 9 | 10 | begin 11 | Bundler.setup(:default, :development) 12 | rescue Bundler::BundlerError => e 13 | $stderr.puts e.message 14 | $stderr.puts "Run `bundle install` to install missing gems" 15 | exit e.status_code 16 | end 17 | 18 | RSpec::Core::RakeTask.new(:spec) 19 | 20 | RSpec::Core::RakeTask.new(:coverage) do |t| 21 | t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default. 22 | t.rcov = true 23 | t.rcov_opts = ['--exclude', 'spec'] 24 | end 25 | 26 | RDoc::Task.new do |rdoc| 27 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 28 | rdoc.rdoc_dir = 'rdoc' 29 | rdoc.title = "activerecord-postgres-hstore #{version}" 30 | rdoc.rdoc_files.include('README*') 31 | rdoc.rdoc_files.include('lib/**/*.rb') 32 | end 33 | 34 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.6 2 | -------------------------------------------------------------------------------- /activerecord-postgres-hstore.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib/', __FILE__) 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "activerecord-postgres-hstore" 7 | s.version = "0.7.8" 8 | 9 | s.platform = Gem::Platform::RUBY 10 | s.license = "MIT" 11 | s.authors = ["Juan Maiz", "Diogo Biazus"] 12 | s.email = "juanmaiz@gmail.com" 13 | s.homepage = "http://github.com/engageis/activerecord-postgres-hstore" 14 | s.summary = "Goodbye serialize, hello hstore" 15 | s.description = "This gem adds support for the postgres hstore type. It is the _just right_ alternative for storing hashes instead of using seralization or dynamic tables." 16 | s.required_ruby_version = ">= 1.8.7" 17 | s.required_rubygems_version = ">= 1.3.6" 18 | 19 | s.add_dependency "activerecord", ">= 3.1" 20 | s.add_dependency "rake" 21 | s.add_dependency 'pg-hstore', '>=1.1.5' 22 | s.add_development_dependency "bundler" 23 | s.add_development_dependency "rdoc" 24 | s.add_development_dependency "rspec", ">= 3.0" 25 | 26 | git_files = `git ls-files`.split("\n") rescue '' 27 | s.files = git_files 28 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 29 | s.executables = [] 30 | s.require_paths = %w(lib) 31 | end 32 | -------------------------------------------------------------------------------- /lib/activerecord-postgres-hstore.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | 3 | if defined? Rails 4 | require "activerecord-postgres-hstore/railties" 5 | else 6 | ActiveSupport.on_load :active_record do 7 | require "activerecord-postgres-hstore/activerecord" 8 | end 9 | end 10 | require "activerecord-postgres-hstore/coder" 11 | -------------------------------------------------------------------------------- /lib/activerecord-postgres-hstore/activerecord.rb: -------------------------------------------------------------------------------- 1 | # Extends AR to add Hstore functionality. 2 | module ActiveRecord 3 | 4 | # Adds methods for deleting keys in your hstore columns 5 | class Base 6 | # Deletes all keys from a specific column in a model. E.g. 7 | # Person.delete_key(:info, :father) 8 | # The SQL generated will be: 9 | # UPDATE "people" SET "info" = delete("info",'father'); 10 | def self.delete_key attribute, key 11 | raise "invalid attribute #{attribute}" unless column_names.include?(attribute.to_s) 12 | update_all([%(#{attribute} = delete("#{attribute}",?)),key]) 13 | end 14 | 15 | # Deletes many keys from a specific column in a model. E.g. 16 | # Person.delete_key(:info, :father, :mother) 17 | # The SQL generated will be: 18 | # UPDATE "people" SET "info" = delete(delete("info",'father'),'mother'); 19 | def self.delete_keys attribute, *keys 20 | raise "invalid attribute #{attribute}" unless column_names.include?(attribute.to_s) 21 | delete_str = "delete(#{attribute},?)" 22 | (keys.count-1).times{ delete_str = "delete(#{delete_str},?)" } 23 | update_all(["#{attribute} = #{delete_str}", *keys]) 24 | end 25 | 26 | # Deletes a key in a record. E.g. 27 | # witt = Person.find_by_name("Ludwig Wittgenstein") 28 | # witt.destroy_key(:info, :father) 29 | # It does not save the record, so you'll have to do it. 30 | def destroy_key attribute, key 31 | raise "invalid attribute #{attribute}" unless self.class.column_names.include?(attribute.to_s) 32 | new_value = send(attribute) 33 | new_value.delete(key.to_s) 34 | send("#{attribute}=", new_value) 35 | self 36 | end 37 | 38 | # Deletes a key in a record. E.g. 39 | # witt = Person.find_by_name("Ludwig Wittgenstein") 40 | # witt.destroy_key(:info, :father) 41 | # It does save the record. 42 | def destroy_key! attribute, key 43 | destroy_key(attribute, key).save 44 | end 45 | 46 | # Deletes many keys in a record. E.g. 47 | # witt = Person.find_by_name("Ludwig Wittgenstein") 48 | # witt.destroy_keys(:info, :father, :mother) 49 | # It does not save the record, so you'll have to do it. 50 | def destroy_keys attribute, *keys 51 | for key in keys 52 | new_value = send(attribute) 53 | new_value.delete(key.to_s) 54 | send("#{attribute}=", new_value) 55 | end 56 | self 57 | end 58 | 59 | # Deletes many keys in a record. E.g. 60 | # witt = Person.find_by_name("Ludwig Wittgenstein") 61 | # witt.destroy_keys!(:info, :father, :mother) 62 | # It does save the record. 63 | def destroy_keys! attribute, *keys 64 | raise "invalid attribute #{attribute}" unless self.class.column_names.include?(attribute.to_s) 65 | destroy_keys(attribute, *keys).save 66 | end 67 | end 68 | 69 | 70 | module ConnectionAdapters 71 | #I believe this change will break the ability to do a schema dump as per issue #83 72 | #https://github.com/engageis/activerecord-postgres-hstore/commit/ca34391c776949c13d561870067ddf581f0561b9#lib/activerecord-postgres-hstore/activerecord.rb 73 | if(RUBY_PLATFORM != 'java') 74 | class PostgreSQLColumn < Column 75 | # Adds the hstore type for the column. 76 | def simplified_type_with_hstore(field_type) 77 | field_type == 'hstore' ? :hstore : simplified_type_without_hstore(field_type) 78 | end 79 | 80 | alias_method_chain :simplified_type, :hstore 81 | end 82 | 83 | class PostgreSQLAdapter < AbstractAdapter 84 | def native_database_types_with_hstore 85 | native_database_types_without_hstore.merge({:hstore => { :name => "hstore" }}) 86 | end 87 | 88 | alias_method_chain :native_database_types, :hstore 89 | end 90 | else 91 | class PostgreSQLColumn 92 | # Adds the hstore type for the column. 93 | def simplified_type_with_hstore(field_type) 94 | field_type == 'hstore' ? :hstore : simplified_type_without_hstore(field_type) 95 | end 96 | 97 | alias_method_chain :simplified_type, :hstore 98 | end 99 | 100 | class PostgreSQLAdapter 101 | def native_database_types_with_hstore 102 | native_database_types_without_hstore.merge({:hstore => { :name => "hstore" }}) 103 | end 104 | 105 | alias_method_chain :native_database_types, :hstore 106 | end 107 | end 108 | 109 | module SchemaStatements 110 | 111 | # Installs hstore by creating the Postgres extension 112 | # if it does not exist 113 | # 114 | def install_hstore 115 | execute "CREATE EXTENSION IF NOT EXISTS hstore" 116 | end 117 | 118 | # Uninstalls hstore by dropping Postgres extension if it exists 119 | # 120 | def uninstall_hstore 121 | execute "DROP EXTENSION IF EXISTS hstore" 122 | end 123 | 124 | # Adds a GiST or GIN index to a table which has an hstore column. 125 | # 126 | # Example: 127 | # add_hstore_index :people, :info, :type => :gin 128 | # 129 | # Options: 130 | # :type = :gist (default) or :gin 131 | # 132 | # See http://www.postgresql.org/docs/9.2/static/textsearch-indexes.html for more information. 133 | # 134 | def add_hstore_index(table_name, column_name, options = {}) 135 | index_name, index_type, index_columns = add_index_options(table_name, column_name, options) 136 | index_type = index_type.present? ? index_type : 'gist' 137 | execute "CREATE INDEX #{index_name} ON #{table_name} USING #{index_type}(#{column_name})" 138 | end 139 | 140 | # Removes a GiST or GIN index of a table which has an hstore column. 141 | # 142 | # Example: 143 | # remove_hstore_index :people, :info 144 | # 145 | def remove_hstore_index(table_name, options = {}) 146 | index_name = index_name_for_remove(table_name, options) 147 | execute "DROP INDEX #{index_name}" 148 | end 149 | 150 | end 151 | 152 | class TableDefinition 153 | 154 | # Adds hstore type for migrations. So you can add columns to a table like: 155 | # create_table :people do |t| 156 | # ... 157 | # t.hstore :info 158 | # ... 159 | # end 160 | def hstore(*args) 161 | options = args.extract_options! 162 | column_names = args 163 | column_names.each { |name| column(name, 'hstore', options) } 164 | end 165 | 166 | end 167 | 168 | class Table 169 | 170 | # Adds hstore type for migrations. So you can add columns to a table like: 171 | # change_table :people do |t| 172 | # ... 173 | # t.hstore :info 174 | # ... 175 | # end 176 | def hstore(*args) 177 | options = args.extract_options! 178 | column_names = args 179 | column_names.each { |name| column(name, 'hstore', options) } 180 | end 181 | 182 | end 183 | end 184 | 185 | class Migration 186 | class CommandRecorder 187 | [:add_hstore_index, :remove_hstore_index].each do |method| 188 | class_eval <<-EOV, __FILE__, __LINE__ + 1 189 | def #{method}(*args) # def create_table(*args) 190 | record(:"#{method}", args) # record(:create_table, args) 191 | end # end 192 | EOV 193 | end 194 | 195 | private 196 | def invert_add_hstore_index(args) 197 | [:remove_hstore_index, args.first(2)] 198 | end 199 | end 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /lib/activerecord-postgres-hstore/coder.rb: -------------------------------------------------------------------------------- 1 | require 'pg_hstore' 2 | 3 | module ActiveRecord 4 | module Coders 5 | class Hstore 6 | def self.load(hstore) 7 | new({}).load(hstore) 8 | end 9 | 10 | def self.dump(hstore) 11 | new({}).dump(hstore) 12 | end 13 | 14 | def initialize(default=nil) 15 | @default=default 16 | end 17 | 18 | def dump(obj) 19 | obj.nil? ? (@default.nil? ? nil : to_hstore(@default)) : to_hstore(obj) 20 | end 21 | 22 | def load(hstore) 23 | hstore.nil? ? @default : from_hstore(hstore) 24 | end 25 | 26 | private 27 | 28 | def to_hstore obj 29 | PgHstore.dump obj, true 30 | end 31 | 32 | def from_hstore hstore 33 | PgHstore.load hstore, false 34 | end 35 | end 36 | end 37 | end 38 | 39 | -------------------------------------------------------------------------------- /lib/activerecord-postgres-hstore/railties.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | require 'rails/generators' 3 | require 'rails/generators/migration' 4 | 5 | # = Hstore Railtie 6 | # 7 | # Creates a new railtie for 2 reasons: 8 | # 9 | # * Initialize ActiveRecord properly 10 | # * Add hstore:setup generator 11 | class Hstore < Rails::Railtie 12 | rake_tasks do 13 | load "tasks/hstore.rake" 14 | end 15 | 16 | initializer 'activerecord-postgres-hstore' do 17 | ActiveSupport.on_load :active_record do 18 | require "activerecord-postgres-hstore/activerecord" 19 | end 20 | end 21 | 22 | # Creates the hstore:setup generator. This generator creates a migration that 23 | # adds hstore support for your database. If fact, it's just the sql from the 24 | # contrib inside a migration. But it' s handy, isn't it? 25 | # 26 | # To use your generator, simply run it in your project: 27 | # 28 | # rails g hstore:setup 29 | class Setup < Rails::Generators::Base 30 | include Rails::Generators::Migration 31 | 32 | def self.source_root 33 | @source_root ||= File.join(File.dirname(__FILE__), '../templates') 34 | end 35 | 36 | def self.next_migration_number(dirname) 37 | if ActiveRecord::Base.timestamped_migrations 38 | Time.now.utc.strftime("%Y%m%d%H%M%S") 39 | else 40 | "%.3d" % (current_migration_number(dirname) + 1) 41 | end 42 | end 43 | 44 | def create_migration_file 45 | pgversion = ActiveRecord::Base.connection.send(:postgresql_version) 46 | if pgversion < 90100 47 | migration_template 'setup_hstore.rb', 'db/migrate/setup_hstore.rb' 48 | else 49 | migration_template 'setup_hstore91.rb', 'db/migrate/setup_hstore.rb' 50 | end 51 | end 52 | 53 | end 54 | end 55 | 56 | -------------------------------------------------------------------------------- /lib/tasks/hstore.rake: -------------------------------------------------------------------------------- 1 | namespace :hstore do 2 | desc "Setup hstore extension into the database" 3 | task 'setup' do 4 | ActiveRecord::Base.connection.execute "CREATE EXTENSION IF NOT EXISTS hstore" 5 | end 6 | end 7 | Rake::Task["db:schema:load"].enhance(["hstore:setup"]) 8 | -------------------------------------------------------------------------------- /lib/templates/setup_hstore.rb: -------------------------------------------------------------------------------- 1 | class SetupHstore < ActiveRecord::Migration 2 | def self.up 3 | sql = <<-HSTORE_SQL 4 | /* $PostgreSQL: pgsql/contrib/hstore/hstore.sql.in,v 1.11 2009/06/11 18:30:03 tgl Exp $ */ 5 | 6 | -- Adjust this setting to control where the objects get created. 7 | SET search_path = public; 8 | 9 | CREATE TYPE hstore; 10 | 11 | CREATE OR REPLACE FUNCTION hstore_in(cstring) 12 | RETURNS hstore 13 | AS '$libdir/hstore' 14 | LANGUAGE C STRICT; 15 | 16 | CREATE OR REPLACE FUNCTION hstore_out(hstore) 17 | RETURNS cstring 18 | AS '$libdir/hstore' 19 | LANGUAGE C STRICT; 20 | 21 | CREATE TYPE hstore ( 22 | INTERNALLENGTH = -1, 23 | INPUT = hstore_in, 24 | OUTPUT = hstore_out, 25 | STORAGE = extended 26 | ); 27 | 28 | CREATE OR REPLACE FUNCTION fetchval(hstore,text) 29 | RETURNS text 30 | AS '$libdir/hstore' 31 | LANGUAGE C STRICT IMMUTABLE; 32 | 33 | CREATE OPERATOR -> ( 34 | LEFTARG = hstore, 35 | RIGHTARG = text, 36 | PROCEDURE = fetchval 37 | ); 38 | 39 | CREATE OR REPLACE FUNCTION isexists(hstore,text) 40 | RETURNS bool 41 | AS '$libdir/hstore','exists' 42 | LANGUAGE C STRICT IMMUTABLE; 43 | 44 | CREATE OR REPLACE FUNCTION exist(hstore,text) 45 | RETURNS bool 46 | AS '$libdir/hstore','exists' 47 | LANGUAGE C STRICT IMMUTABLE; 48 | 49 | CREATE OPERATOR ? ( 50 | LEFTARG = hstore, 51 | RIGHTARG = text, 52 | PROCEDURE = exist, 53 | RESTRICT = contsel, 54 | JOIN = contjoinsel 55 | ); 56 | 57 | CREATE OR REPLACE FUNCTION isdefined(hstore,text) 58 | RETURNS bool 59 | AS '$libdir/hstore','defined' 60 | LANGUAGE C STRICT IMMUTABLE; 61 | 62 | CREATE OR REPLACE FUNCTION defined(hstore,text) 63 | RETURNS bool 64 | AS '$libdir/hstore','defined' 65 | LANGUAGE C STRICT IMMUTABLE; 66 | 67 | CREATE OR REPLACE FUNCTION delete(hstore,text) 68 | RETURNS hstore 69 | AS '$libdir/hstore','delete' 70 | LANGUAGE C STRICT IMMUTABLE; 71 | 72 | CREATE OR REPLACE FUNCTION hs_concat(hstore,hstore) 73 | RETURNS hstore 74 | AS '$libdir/hstore' 75 | LANGUAGE C STRICT IMMUTABLE; 76 | 77 | CREATE OPERATOR || ( 78 | LEFTARG = hstore, 79 | RIGHTARG = hstore, 80 | PROCEDURE = hs_concat 81 | ); 82 | 83 | CREATE OR REPLACE FUNCTION hs_contains(hstore,hstore) 84 | RETURNS bool 85 | AS '$libdir/hstore' 86 | LANGUAGE C STRICT IMMUTABLE; 87 | 88 | CREATE OR REPLACE FUNCTION hs_contained(hstore,hstore) 89 | RETURNS bool 90 | AS '$libdir/hstore' 91 | LANGUAGE C STRICT IMMUTABLE; 92 | 93 | CREATE OPERATOR @> ( 94 | LEFTARG = hstore, 95 | RIGHTARG = hstore, 96 | PROCEDURE = hs_contains, 97 | COMMUTATOR = '<@', 98 | RESTRICT = contsel, 99 | JOIN = contjoinsel 100 | ); 101 | 102 | CREATE OPERATOR <@ ( 103 | LEFTARG = hstore, 104 | RIGHTARG = hstore, 105 | PROCEDURE = hs_contained, 106 | COMMUTATOR = '@>', 107 | RESTRICT = contsel, 108 | JOIN = contjoinsel 109 | ); 110 | 111 | -- obsolete: 112 | CREATE OPERATOR @ ( 113 | LEFTARG = hstore, 114 | RIGHTARG = hstore, 115 | PROCEDURE = hs_contains, 116 | COMMUTATOR = '~', 117 | RESTRICT = contsel, 118 | JOIN = contjoinsel 119 | ); 120 | 121 | CREATE OPERATOR ~ ( 122 | LEFTARG = hstore, 123 | RIGHTARG = hstore, 124 | PROCEDURE = hs_contained, 125 | COMMUTATOR = '@', 126 | RESTRICT = contsel, 127 | JOIN = contjoinsel 128 | ); 129 | 130 | CREATE OR REPLACE FUNCTION tconvert(text,text) 131 | RETURNS hstore 132 | AS '$libdir/hstore' 133 | LANGUAGE C IMMUTABLE; -- not STRICT 134 | 135 | CREATE OPERATOR => ( 136 | LEFTARG = text, 137 | RIGHTARG = text, 138 | PROCEDURE = tconvert 139 | ); 140 | 141 | CREATE OR REPLACE FUNCTION akeys(hstore) 142 | RETURNS _text 143 | AS '$libdir/hstore' 144 | LANGUAGE C STRICT IMMUTABLE; 145 | 146 | CREATE OR REPLACE FUNCTION avals(hstore) 147 | RETURNS _text 148 | AS '$libdir/hstore' 149 | LANGUAGE C STRICT IMMUTABLE; 150 | 151 | CREATE OR REPLACE FUNCTION skeys(hstore) 152 | RETURNS setof text 153 | AS '$libdir/hstore' 154 | LANGUAGE C STRICT IMMUTABLE; 155 | 156 | CREATE OR REPLACE FUNCTION svals(hstore) 157 | RETURNS setof text 158 | AS '$libdir/hstore' 159 | LANGUAGE C STRICT IMMUTABLE; 160 | 161 | CREATE OR REPLACE FUNCTION each(IN hs hstore, 162 | OUT key text, 163 | OUT value text) 164 | RETURNS SETOF record 165 | AS '$libdir/hstore' 166 | LANGUAGE C STRICT IMMUTABLE; 167 | 168 | 169 | 170 | -- define the GiST support methods 171 | 172 | CREATE TYPE ghstore; 173 | 174 | CREATE OR REPLACE FUNCTION ghstore_in(cstring) 175 | RETURNS ghstore 176 | AS '$libdir/hstore' 177 | LANGUAGE C STRICT; 178 | 179 | CREATE OR REPLACE FUNCTION ghstore_out(ghstore) 180 | RETURNS cstring 181 | AS '$libdir/hstore' 182 | LANGUAGE C STRICT; 183 | 184 | CREATE TYPE ghstore ( 185 | INTERNALLENGTH = -1, 186 | INPUT = ghstore_in, 187 | OUTPUT = ghstore_out 188 | ); 189 | 190 | CREATE OR REPLACE FUNCTION ghstore_compress(internal) 191 | RETURNS internal 192 | AS '$libdir/hstore' 193 | LANGUAGE C IMMUTABLE STRICT; 194 | 195 | CREATE OR REPLACE FUNCTION ghstore_decompress(internal) 196 | RETURNS internal 197 | AS '$libdir/hstore' 198 | LANGUAGE C IMMUTABLE STRICT; 199 | 200 | CREATE OR REPLACE FUNCTION ghstore_penalty(internal,internal,internal) 201 | RETURNS internal 202 | AS '$libdir/hstore' 203 | LANGUAGE C IMMUTABLE STRICT; 204 | 205 | CREATE OR REPLACE FUNCTION ghstore_picksplit(internal, internal) 206 | RETURNS internal 207 | AS '$libdir/hstore' 208 | LANGUAGE C IMMUTABLE STRICT; 209 | 210 | CREATE OR REPLACE FUNCTION ghstore_union(internal, internal) 211 | RETURNS internal 212 | AS '$libdir/hstore' 213 | LANGUAGE C IMMUTABLE STRICT; 214 | 215 | CREATE OR REPLACE FUNCTION ghstore_same(internal, internal, internal) 216 | RETURNS internal 217 | AS '$libdir/hstore' 218 | LANGUAGE C IMMUTABLE STRICT; 219 | 220 | CREATE OR REPLACE FUNCTION ghstore_consistent(internal,internal,int,oid,internal) 221 | RETURNS bool 222 | AS '$libdir/hstore' 223 | LANGUAGE C IMMUTABLE STRICT; 224 | 225 | -- register the opclass for indexing (not as default) 226 | CREATE OPERATOR CLASS gist_hstore_ops 227 | DEFAULT FOR TYPE hstore USING gist 228 | AS 229 | OPERATOR 7 @> , 230 | OPERATOR 9 ?(hstore,text) , 231 | --OPERATOR 8 <@ , 232 | OPERATOR 13 @ , 233 | --OPERATOR 14 ~ , 234 | FUNCTION 1 ghstore_consistent (internal, internal, int, oid, internal), 235 | FUNCTION 2 ghstore_union (internal, internal), 236 | FUNCTION 3 ghstore_compress (internal), 237 | FUNCTION 4 ghstore_decompress (internal), 238 | FUNCTION 5 ghstore_penalty (internal, internal, internal), 239 | FUNCTION 6 ghstore_picksplit (internal, internal), 240 | FUNCTION 7 ghstore_same (internal, internal, internal), 241 | STORAGE ghstore; 242 | 243 | -- define the GIN support methods 244 | 245 | CREATE OR REPLACE FUNCTION gin_extract_hstore(internal, internal) 246 | RETURNS internal 247 | AS '$libdir/hstore' 248 | LANGUAGE C IMMUTABLE STRICT; 249 | 250 | CREATE OR REPLACE FUNCTION gin_extract_hstore_query(internal, internal, int2, internal, internal) 251 | RETURNS internal 252 | AS '$libdir/hstore' 253 | LANGUAGE C IMMUTABLE STRICT; 254 | 255 | CREATE OR REPLACE FUNCTION gin_consistent_hstore(internal, int2, internal, int4, internal, internal) 256 | RETURNS bool 257 | AS '$libdir/hstore' 258 | LANGUAGE C IMMUTABLE STRICT; 259 | 260 | CREATE OPERATOR CLASS gin_hstore_ops 261 | DEFAULT FOR TYPE hstore USING gin 262 | AS 263 | OPERATOR 7 @> , 264 | OPERATOR 9 ?(hstore,text), 265 | FUNCTION 1 bttextcmp(text,text), 266 | FUNCTION 2 gin_extract_hstore(internal, internal), 267 | FUNCTION 3 gin_extract_hstore_query(internal, internal, int2, internal, internal), 268 | FUNCTION 4 gin_consistent_hstore(internal, int2, internal, int4, internal, internal), 269 | STORAGE text; 270 | HSTORE_SQL 271 | execute sql 272 | end 273 | 274 | def self.down 275 | # Kinda... hard. 276 | end 277 | end 278 | -------------------------------------------------------------------------------- /lib/templates/setup_hstore91.rb: -------------------------------------------------------------------------------- 1 | class SetupHstore < ActiveRecord::Migration 2 | def self.up 3 | execute "CREATE EXTENSION IF NOT EXISTS hstore" 4 | end 5 | 6 | def self.down 7 | execute "DROP EXTENSION IF EXISTS hstore" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/activerecord-coders-hstore_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe ActiveRecord::Coders::Hstore do 4 | describe "#load" do 5 | subject{ ActiveRecord::Coders::Hstore.new.load(value) } 6 | 7 | context 'when value is nil and we have a default in the constructor' do 8 | subject{ ActiveRecord::Coders::Hstore.new({'a'=>'a'}).load(nil) } 9 | it{ should eql({'a'=>'a'}) } 10 | end 11 | 12 | context 'when key and value have newline char' do 13 | let(:value){ "\"foo\nbar\"=>\"\nnewline\"" } 14 | it{ should eql({"foo\nbar" => "\nnewline"}) } 15 | end 16 | 17 | context 'when key and value are empty strings' do 18 | let(:value){ %q(""=>"") } 19 | it{ should eql({'' => ''}) } 20 | end 21 | 22 | context 'when value has single quotes' do 23 | let(:value){ %q("'a'"=>"'a'") } 24 | it{ should eql({"'a'" => "'a'"}) } 25 | end 26 | 27 | context 'when value is empty hash' do 28 | let(:value){ '' } 29 | it{ should eql({}) } 30 | end 31 | 32 | context 'when value is nil' do 33 | let(:value){ nil } 34 | it { should be_nil } 35 | end 36 | 37 | context 'when value is a hstore' do 38 | let(:value){ "a=>a" } 39 | it{ should eql({ 'a' => 'a' }) } 40 | end 41 | end 42 | 43 | describe "#dump" do 44 | subject{ ActiveRecord::Coders::Hstore.new.dump(value) } 45 | 46 | context 'when value is nil and we have a default in the constructor' do 47 | subject{ ActiveRecord::Coders::Hstore.new({'a'=>'a'}).dump(nil) } 48 | it{ should eql('"a"=>"a"') } 49 | end 50 | 51 | context 'when key and value have dollar sign char' do 52 | let(:value){ {"foo$bar" => "$ 5.00"} } 53 | it{ should eql("\"foo$bar\"=>\"$ 5.00\"") } 54 | end 55 | 56 | context 'when key and value have newline char' do 57 | let(:value){ {"foo\nbar" => "\nnewline"} } 58 | it{ should eql("\"foo\nbar\"=>\"\nnewline\"") } 59 | end 60 | 61 | context 'when key and value are empty strings' do 62 | let(:value){ {'' => ''} } 63 | it{ should eql(%q(""=>"")) } 64 | end 65 | 66 | context 'when value has single quotes' do 67 | let(:value){ {"'a'" => "'a'"} } 68 | it{ should eql(%q("'a'"=>"'a'")) } 69 | end 70 | 71 | context 'when value is empty hash' do 72 | let(:value){ {} } 73 | it{ should eql('') } 74 | end 75 | 76 | context 'when value is nil' do 77 | let(:value){ nil } 78 | it{ should be_nil } 79 | end 80 | 81 | context "when value is an hstore" do 82 | let(:value){ {'a' => 'a'} } 83 | it{ should eql('"a"=>"a"') } 84 | end 85 | 86 | context 'when value has double quotes' do 87 | let(:value){ {"a" => "\"a\""} } 88 | it{ should eql(%q("a"=>"\"a\"")) } 89 | end 90 | 91 | # @seamusabshere not sure about this test 92 | # context 'when value has double-escaped double quotes' do 93 | # let(:value){ {"a" => "\\\"a\\\""} } 94 | # it{ should eql(%q("a"=>"\"a\"")) } 95 | # end 96 | end 97 | 98 | describe ".load" do 99 | before do 100 | @parameter = 'b=>b' 101 | instance = double("coder instance") 102 | instance.should_receive(:load).with(@parameter) 103 | ActiveRecord::Coders::Hstore.should_receive(:new).and_return(instance) 104 | end 105 | 106 | it("should instantiate and call load") do 107 | ActiveRecord::Coders::Hstore.load(@parameter) 108 | end 109 | end 110 | 111 | describe ".dump" do 112 | before do 113 | @parameter = {'b' => 'b'} 114 | instance = double("coder instance") 115 | instance.should_receive(:dump).with(@parameter) 116 | ActiveRecord::Coders::Hstore.should_receive(:new).and_return(instance) 117 | end 118 | 119 | it("should instantiate and call dump") do 120 | ActiveRecord::Coders::Hstore.dump(@parameter) 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'activerecord-postgres-hstore' 4 | require 'rspec' 5 | require 'rspec/autorun' 6 | 7 | RSpec.configure do |config| 8 | 9 | end 10 | --------------------------------------------------------------------------------