superstore 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.travis.yml +11 -0
  4. data/CHANGELOG +0 -0
  5. data/Gemfile +19 -0
  6. data/LICENSE +13 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +100 -0
  9. data/Rakefile +12 -0
  10. data/lib/superstore/adapters/abstract_adapter.rb +49 -0
  11. data/lib/superstore/adapters/cassandra_adapter.rb +181 -0
  12. data/lib/superstore/adapters/hstore_adapter.rb +163 -0
  13. data/lib/superstore/attribute_methods/definition.rb +22 -0
  14. data/lib/superstore/attribute_methods/dirty.rb +36 -0
  15. data/lib/superstore/attribute_methods/primary_key.rb +25 -0
  16. data/lib/superstore/attribute_methods/typecasting.rb +59 -0
  17. data/lib/superstore/attribute_methods.rb +96 -0
  18. data/lib/superstore/base.rb +33 -0
  19. data/lib/superstore/belongs_to/association.rb +48 -0
  20. data/lib/superstore/belongs_to/builder.rb +40 -0
  21. data/lib/superstore/belongs_to/reflection.rb +30 -0
  22. data/lib/superstore/belongs_to.rb +63 -0
  23. data/lib/superstore/callbacks.rb +29 -0
  24. data/lib/superstore/cassandra_schema/statements.rb +52 -0
  25. data/lib/superstore/cassandra_schema/tasks.rb +47 -0
  26. data/lib/superstore/cassandra_schema.rb +9 -0
  27. data/lib/superstore/connection.rb +39 -0
  28. data/lib/superstore/core.rb +59 -0
  29. data/lib/superstore/errors.rb +10 -0
  30. data/lib/superstore/identity.rb +26 -0
  31. data/lib/superstore/inspect.rb +25 -0
  32. data/lib/superstore/log_subscriber.rb +44 -0
  33. data/lib/superstore/model.rb +37 -0
  34. data/lib/superstore/persistence.rb +153 -0
  35. data/lib/superstore/railtie.rb +30 -0
  36. data/lib/superstore/railties/controller_runtime.rb +45 -0
  37. data/lib/superstore/schema.rb +20 -0
  38. data/lib/superstore/scope/batches.rb +32 -0
  39. data/lib/superstore/scope/finder_methods.rb +48 -0
  40. data/lib/superstore/scope/query_methods.rb +49 -0
  41. data/lib/superstore/scope.rb +49 -0
  42. data/lib/superstore/scoping.rb +27 -0
  43. data/lib/superstore/tasks/ks.rake +54 -0
  44. data/lib/superstore/timestamps.rb +19 -0
  45. data/lib/superstore/type.rb +16 -0
  46. data/lib/superstore/types/array_type.rb +20 -0
  47. data/lib/superstore/types/base_type.rb +26 -0
  48. data/lib/superstore/types/boolean_type.rb +20 -0
  49. data/lib/superstore/types/date_type.rb +22 -0
  50. data/lib/superstore/types/float_type.rb +16 -0
  51. data/lib/superstore/types/integer_type.rb +20 -0
  52. data/lib/superstore/types/json_type.rb +13 -0
  53. data/lib/superstore/types/string_type.rb +19 -0
  54. data/lib/superstore/types/time_type.rb +16 -0
  55. data/lib/superstore/types.rb +8 -0
  56. data/lib/superstore/validations.rb +44 -0
  57. data/lib/superstore.rb +69 -0
  58. data/superstore.gemspec +23 -0
  59. data/test/support/cassandra.rb +44 -0
  60. data/test/support/hstore.rb +40 -0
  61. data/test/support/issue.rb +10 -0
  62. data/test/test_helper.rb +42 -0
  63. data/test/unit/active_model_test.rb +18 -0
  64. data/test/unit/adapters/adapter_test.rb +6 -0
  65. data/test/unit/attribute_methods/definition_test.rb +13 -0
  66. data/test/unit/attribute_methods/dirty_test.rb +72 -0
  67. data/test/unit/attribute_methods/primary_key_test.rb +26 -0
  68. data/test/unit/attribute_methods/typecasting_test.rb +118 -0
  69. data/test/unit/attribute_methods_test.rb +51 -0
  70. data/test/unit/base_test.rb +20 -0
  71. data/test/unit/belongs_to/reflection_test.rb +12 -0
  72. data/test/unit/belongs_to_test.rb +62 -0
  73. data/test/unit/callbacks_test.rb +46 -0
  74. data/test/unit/cassandra_schema/statements_test.rb +47 -0
  75. data/test/unit/cassandra_schema/tasks_test.rb +31 -0
  76. data/test/unit/connection_test.rb +10 -0
  77. data/test/unit/core_test.rb +55 -0
  78. data/test/unit/identity_test.rb +26 -0
  79. data/test/unit/inspect_test.rb +26 -0
  80. data/test/unit/log_subscriber_test.rb +26 -0
  81. data/test/unit/persistence_test.rb +213 -0
  82. data/test/unit/railties/controller_runtime_test.rb +48 -0
  83. data/test/unit/schema_test.rb +27 -0
  84. data/test/unit/scope/batches_test.rb +30 -0
  85. data/test/unit/scope/finder_methods_test.rb +51 -0
  86. data/test/unit/scope/query_methods_test.rb +27 -0
  87. data/test/unit/scoping_test.rb +7 -0
  88. data/test/unit/serialization_test.rb +10 -0
  89. data/test/unit/timestamps_test.rb +27 -0
  90. data/test/unit/types/array_type_test.rb +21 -0
  91. data/test/unit/types/base_type_test.rb +19 -0
  92. data/test/unit/types/boolean_type_test.rb +24 -0
  93. data/test/unit/types/date_type_test.rb +15 -0
  94. data/test/unit/types/float_type_test.rb +17 -0
  95. data/test/unit/types/integer_type_test.rb +19 -0
  96. data/test/unit/types/json_type_test.rb +23 -0
  97. data/test/unit/types/string_type_test.rb +30 -0
  98. data/test/unit/types/time_type_test.rb +19 -0
  99. data/test/unit/validations_test.rb +27 -0
  100. metadata +170 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8332e4f8d43a68c737e31ae1a88a89c360782a88
4
+ data.tar.gz: 9cbbc5a3fa095a0af7ef48fb265460530a2a5191
5
+ SHA512:
6
+ metadata.gz: b09a471630f8a389b2cb250ab70b4d5f1ed48e67ebceb828d9fe1b13236d88bda6ca89c09b3153a70a41f225c6ef25861b7857c3cfeb9bf68bd37c7440e39dad
7
+ data.tar.gz: 6d68424187b262902591482f8f2db73a40cf7f0bcba670b6eea343538d0c7ca8cd09ff56d8fd11e1955c944a517353fa3243654600f431c21405a97c929aa161
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ *.gem
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ env: CQLSH=/usr/local/cassandra/bin/cqlsh
7
+ before_install:
8
+ - wget http://archive.apache.org/dist/cassandra/1.2.9/apache-cassandra-1.2.9-bin.tar.gz
9
+ - tar xfz apache-cassandra-1.2.9-bin.tar.gz
10
+ - sh -c "echo 'JVM_OPTS=\"\${JVM_OPTS} -Xss256k -Djava.net.preferIPv4Stack=false\"' >> apache-cassandra-1.2.9/conf/cassandra-env.sh"
11
+ - cd apache-cassandra-1.2.9 && sudo ./bin/cassandra 2>&1 >> cassandra.log &
data/CHANGELOG ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "http://rubygems.org"
2
+ gemspec
3
+
4
+ gem 'rake'
5
+ gem 'thin'
6
+
7
+ group :test do
8
+ gem 'rails'
9
+ gem 'mocha', require: false
10
+ end
11
+
12
+ group :cassandra do
13
+ gem 'cassandra-cql'
14
+ end
15
+
16
+ group :hstore do
17
+ gem 'activerecord'
18
+ gem 'pg'
19
+ end
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2009 Koziarski Software Ltd
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.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 [Michael Koziarski]
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.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Superstore
2
+ [![Build Status](https://secure.travis-ci.org/data-axle/superstore.png?rvm=2.0.0)](http://travis-ci.org/data-axle/superstore) [![Code Climate](https://codeclimate.com/github/data-axle/superstore.png)](https://codeclimate.com/github/data-axle/superstore)
3
+
4
+ Cassandra Object uses ActiveModel to mimic much of the behavior in ActiveRecord.
5
+
6
+ ## Installation
7
+
8
+ Add the following to your Gemfile:
9
+ ```ruby
10
+ gem 'superstore'
11
+ ```
12
+
13
+ Change the version of Cassandra accordingly. Recent versions have not been backward compatible.
14
+
15
+ ## Defining Models
16
+
17
+ ```ruby
18
+ class Widget < Superstore::Base
19
+ string :name
20
+ string :description
21
+ integer :price
22
+ array :colors, unique: true
23
+
24
+ validates :name, presence: :true
25
+
26
+ before_create do
27
+ self.description = "#{name} is the best product ever"
28
+ end
29
+ end
30
+ ```
31
+ ## Using with Cassandra
32
+
33
+ Add the cassandra-cql gem to Gemfile:
34
+
35
+ ```ruby
36
+ gem 'cassandra-cql'
37
+ ```
38
+
39
+ Add a config/superstore.yml:
40
+
41
+ ```yaml
42
+ development:
43
+ adapter: cassandra
44
+ keyspace: my_app_development
45
+ servers: 127.0.0.1:9160
46
+ thrift:
47
+ timeout: 20
48
+ retries: 2
49
+ ```
50
+
51
+ ## Using with Postgres HStore
52
+
53
+ Add the pg gem to your Gemfile:
54
+
55
+ ```ruby
56
+ gem 'pg'
57
+ ```
58
+
59
+ And a config/superstore.yml:
60
+
61
+ ```yaml
62
+ development:
63
+ adapter: hstore
64
+ ```
65
+
66
+ ## Creating and updating records
67
+
68
+ Cassandra Object has equivalent methods as ActiveRecord:
69
+
70
+ ```ruby
71
+ widget = Widget.new
72
+ widget.valid?
73
+ widget = Widget.create(name: 'Acme', price: 100)
74
+ widget.update_attribute(:price, 1200)
75
+ widget.update_attributes(price: 1200, name: 'Acme Corporation')
76
+ widget.attributes = {price: 300}
77
+ widget.price_was
78
+ widget.save
79
+ widget.save!
80
+ ```
81
+
82
+ ## Finding records
83
+
84
+ ```ruby
85
+ widget = Widget.find(uuid)
86
+ widget = Widget.first
87
+ widgets = Widget.all
88
+ Widget.find_each do |widget|
89
+ # Codez
90
+ end
91
+ ```
92
+
93
+ ## Scoping
94
+
95
+ Some lightweight scoping features are available:
96
+ ```ruby
97
+ Widget.where('color' => 'red')
98
+ Widget.select(['name', 'color'])
99
+ Widget.limit(10)
100
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'bundler/setup'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ task default: :test
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'lib'
9
+ t.libs << 'test'
10
+ t.pattern = 'test/unit/**/*_test.rb'
11
+ t.verbose = true
12
+ end
@@ -0,0 +1,49 @@
1
+ module Superstore
2
+ module Adapters
3
+ class AbstractAdapter
4
+ attr_reader :config
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+
9
+ # Read records from a instance of Superstore::Scope
10
+ def select(scope) # abstract
11
+ end
12
+
13
+ # Insert a new row
14
+ def insert(table, id, attributes) # abstract
15
+ end
16
+
17
+ # Update an existing row
18
+ def update(table, id, attributes) # abstract
19
+ end
20
+
21
+ # Delete rows by an array of ids
22
+ def delete(table, ids) # abstract
23
+ end
24
+
25
+ def execute_batch(statements) # abstract
26
+ end
27
+
28
+ def batching?
29
+ !@batch_statements.nil?
30
+ end
31
+
32
+ def batch
33
+ @batch_statements = []
34
+ yield
35
+ execute_batch(@batch_statements) if @batch_statements.any?
36
+ ensure
37
+ @batch_statements = nil
38
+ end
39
+
40
+ def execute_batchable(statement)
41
+ if @batch_statements
42
+ @batch_statements << statement
43
+ else
44
+ execute statement
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,181 @@
1
+ gem 'cassandra-cql'
2
+ require 'cassandra-cql'
3
+
4
+ module Superstore
5
+ module Adapters
6
+ class CassandraAdapter < AbstractAdapter
7
+ class QueryBuilder
8
+ def initialize(adapter, scope)
9
+ @adapter = adapter
10
+ @scope = scope
11
+ end
12
+
13
+ def to_query
14
+ [
15
+ "SELECT #{select_string} FROM #{@scope.klass.column_family}",
16
+ @adapter.write_option_string,
17
+ where_string,
18
+ limit_string
19
+ ].delete_if(&:blank?) * ' '
20
+ end
21
+
22
+ def select_string
23
+ if @scope.select_values.any?
24
+ (['KEY'] | @scope.select_values) * ','
25
+ else
26
+ '*'
27
+ end
28
+ end
29
+
30
+ def where_string
31
+ wheres = @scope.where_values.dup
32
+ if @scope.id_values.any?
33
+ wheres << @adapter.create_ids_where_clause(@scope.id_values)
34
+ end
35
+
36
+ if wheres.any?
37
+ "WHERE #{wheres * ' AND '}"
38
+ end
39
+ end
40
+
41
+ def limit_string
42
+ if @scope.limit_value
43
+ "LIMIT #{@scope.limit_value}"
44
+ else
45
+ ""
46
+ end
47
+ end
48
+ end
49
+
50
+ def primary_key_column
51
+ 'KEY'
52
+ end
53
+
54
+ def connection
55
+ @connection ||= begin
56
+ thrift_options = (config[:thrift] || {})
57
+ CassandraCQL::Database.new(servers, {keyspace: config[:keyspace]}, thrift_options)
58
+ end
59
+ end
60
+
61
+ def servers
62
+ Array.wrap(config[:servers] || "127.0.0.1:9160")
63
+ end
64
+
65
+ def execute(statement)
66
+ ActiveSupport::Notifications.instrument("cql.cassandra_object", cql: statement) do
67
+ connection.execute statement
68
+ end
69
+ end
70
+
71
+ def select(scope)
72
+ statement = QueryBuilder.new(self, scope).to_query
73
+
74
+ execute(statement).fetch do |cql_row|
75
+ attributes = cql_row.to_hash
76
+ key = attributes.delete(primary_key_column)
77
+ yield(key, attributes) unless attributes.empty?
78
+ end
79
+ end
80
+
81
+ def insert(table, id, attributes)
82
+ write(table, id, attributes)
83
+ end
84
+
85
+ def update(table, id, attributes)
86
+ write(table, id, attributes)
87
+ end
88
+
89
+ def write(table, id, attributes)
90
+ if (not_nil_attributes = attributes.reject { |key, value| value.nil? }).any?
91
+ insert_attributes = {primary_key_column => id}.update(not_nil_attributes)
92
+ statement = "INSERT INTO #{table} (#{quote_columns(insert_attributes.keys) * ','}) VALUES (#{Array.new(insert_attributes.size, '?') * ','})#{write_option_string}"
93
+ execute_batchable sanitize(statement, *insert_attributes.values)
94
+ end
95
+
96
+ if (nil_attributes = attributes.select { |key, value| value.nil? }).any?
97
+ execute_batchable sanitize("DELETE #{quote_columns(nil_attributes.keys) * ','} FROM #{table}#{write_option_string} WHERE #{primary_key_column} = ?", id)
98
+ end
99
+ end
100
+
101
+ def delete(table, ids)
102
+ statement = "DELETE FROM #{table}#{write_option_string} WHERE #{create_ids_where_clause(ids)}"
103
+
104
+ execute_batchable statement
105
+ end
106
+
107
+ def execute_batch(statements)
108
+ raise 'No can do' if statements.empty?
109
+
110
+ stmt = [
111
+ "BEGIN BATCH#{write_option_string(true)}",
112
+ statements * "\n",
113
+ 'APPLY BATCH'
114
+ ] * "\n"
115
+
116
+ execute stmt
117
+ end
118
+
119
+ # SCHEMA
120
+ def create_table(table_name, options = {})
121
+ stmt = "CREATE COLUMNFAMILY #{table_name} " +
122
+ "(KEY varchar PRIMARY KEY)"
123
+
124
+ schema_execute statement_with_options(stmt, options), config[:keyspace]
125
+ end
126
+
127
+ def drop_table(table_name)
128
+ schema_execute "DROP TABLE #{table_name}", config[:keyspace]
129
+ end
130
+
131
+ def schema_execute(cql, keyspace)
132
+ schema_db = CassandraCQL::Database.new(Superstore::Base.adapter.servers, {keyspace: keyspace}, {connect_timeout: 30, timeout: 30})
133
+ schema_db.execute cql
134
+ end
135
+ # /SCHEMA
136
+
137
+ def consistency
138
+ @consistency ||= config[:consistency]
139
+ end
140
+
141
+ def consistency=(val)
142
+ @consistency = val
143
+ end
144
+
145
+ def write_option_string(ignore_batching = false)
146
+ if (ignore_batching || !batching?) && consistency
147
+ " USING CONSISTENCY #{consistency}"
148
+ end
149
+ end
150
+
151
+ def statement_with_options(stmt, options)
152
+ if options.any?
153
+ with_stmt = options.map do |k,v|
154
+ "#{k} = #{CassandraCQL::Statement.quote(v)}"
155
+ end.join(' AND ')
156
+
157
+ "#{stmt} WITH #{with_stmt}"
158
+ else
159
+ stmt
160
+ end
161
+ end
162
+
163
+ def create_ids_where_clause(ids)
164
+ ids = ids.first if ids.is_a?(Array) && ids.one?
165
+ sql = ids.is_a?(Array) ? "#{primary_key_column} IN (?)" : "#{primary_key_column} = ?"
166
+ sanitize(sql, ids)
167
+ end
168
+
169
+ private
170
+
171
+ def sanitize(statement, *bind_vars)
172
+ CassandraCQL::Statement.sanitize(statement, bind_vars).force_encoding(Encoding::UTF_8)
173
+ end
174
+
175
+ def quote_columns(column_names)
176
+ column_names.map { |name| "'#{name}'" }
177
+ end
178
+
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,163 @@
1
+ gem 'pg'
2
+ require 'pg'
3
+
4
+ module Superstore
5
+ module Adapters
6
+ class HstoreAdapter < AbstractAdapter
7
+ class QueryBuilder
8
+ def initialize(adapter, scope)
9
+ @adapter = adapter
10
+ @scope = scope
11
+ end
12
+
13
+ def to_query
14
+ [
15
+ "SELECT #{select_string} FROM #{@scope.klass.column_family}",
16
+ where_string,
17
+ order_string,
18
+ limit_string,
19
+ ].delete_if(&:blank?) * ' '
20
+ end
21
+
22
+ def select_string
23
+ if @scope.select_values.any?
24
+ "id, slice(attribute_store, #{@adapter.fields_to_postgres_array(@scope.select_values)}) as attribute_store"
25
+ else
26
+ '*'
27
+ end
28
+ end
29
+
30
+ def where_string
31
+ wheres = @scope.where_values.dup
32
+ if @scope.id_values.any?
33
+ wheres << @adapter.create_ids_where_clause(@scope.id_values)
34
+ end
35
+
36
+ if wheres.any?
37
+ "WHERE #{wheres * ' AND '}"
38
+ end
39
+ end
40
+
41
+ def order_string
42
+ if @scope.id_values.many?
43
+ id_orders = @scope.id_values.map { |id| "ID=#{@adapter.quote(id)} DESC" }.join(',')
44
+ "ORDER BY #{id_orders}"
45
+ end
46
+ end
47
+
48
+ def limit_string
49
+ if @scope.limit_value
50
+ "LIMIT #{@scope.limit_value}"
51
+ end
52
+ end
53
+ end
54
+
55
+ def primary_key_column
56
+ 'id'
57
+ end
58
+
59
+ def connection
60
+ # conf = {:adapter=>"postgresql", :encoding=>"unicode", :database=>"axle_place_test", :pool=>5, :username=>"postgres"}
61
+ # @connection ||= ActiveRecord::Base.postgresql_connection(conf)
62
+ ActiveRecord::Base.connection
63
+ end
64
+
65
+ def execute(statement)
66
+ ActiveSupport::Notifications.instrument("cql.cassandra_object", cql: statement) do
67
+ connection.exec_query statement
68
+ end
69
+ end
70
+
71
+ def select(scope)
72
+ statement = QueryBuilder.new(self, scope).to_query
73
+
74
+ connection.execute(statement).each do |attributes|
75
+ yield attributes[primary_key_column], hstore_to_attributes(attributes['attribute_store'])
76
+ end
77
+ end
78
+
79
+ def insert(table, id, attributes)
80
+ not_nil_attributes = attributes.reject { |key, value| value.nil? }
81
+ statement = "INSERT INTO #{table} (#{primary_key_column}, attribute_store) VALUES (#{quote(id)}, #{attributes_to_hstore(not_nil_attributes)})"
82
+ execute_batchable statement
83
+ end
84
+
85
+ def update(table, id, attributes)
86
+ return if attributes.empty?
87
+
88
+ not_nil_attributes = attributes.reject { |key, value| value.nil? }
89
+ nil_attributes = attributes.select { |key, value| value.nil? }
90
+
91
+ if not_nil_attributes.any? && nil_attributes.any?
92
+ value_update = "(attribute_store - #{fields_to_postgres_array(nil_attributes.keys)}) || #{attributes_to_hstore(not_nil_attributes)}"
93
+ elsif not_nil_attributes.any?
94
+ value_update = "attribute_store || #{attributes_to_hstore(not_nil_attributes)}"
95
+ elsif nil_attributes.any?
96
+ value_update = "attribute_store - #{fields_to_postgres_array(nil_attributes.keys)}"
97
+ end
98
+
99
+ statement = "UPDATE #{table} SET attribute_store = #{value_update} WHERE #{primary_key_column} = #{quote(id)}"
100
+ execute_batchable statement
101
+ end
102
+
103
+ def delete(table, ids)
104
+ statement = "DELETE FROM #{table} WHERE #{create_ids_where_clause(ids)}"
105
+
106
+ execute_batchable statement
107
+ end
108
+
109
+ def execute_batch(statements)
110
+ stmt = [
111
+ "BEGIN",
112
+ statements * ";\n",
113
+ 'COMMIT'
114
+ ] * ";\n"
115
+
116
+ execute stmt
117
+ end
118
+
119
+ def create_table(table_name, options = {})
120
+ connection.execute 'CREATE EXTENSION IF NOT EXISTS hstore'
121
+ ActiveRecord::Migration.create_table table_name, id: false do |t|
122
+ t.string :id, null: false
123
+ t.hstore :attribute_store, null: false
124
+ end
125
+ connection.execute "ALTER TABLE \"#{table_name}\" ADD CONSTRAINT #{table_name}_pkey PRIMARY KEY (id)"
126
+ end
127
+
128
+ def drop_table(table_name)
129
+ ActiveRecord::Migration.drop_table table_name
130
+ end
131
+
132
+ def create_ids_where_clause(ids)
133
+ ids = ids.first if ids.is_a?(Array) && ids.one?
134
+
135
+ if ids.is_a?(Array)
136
+ id_list = ids.map { |id| quote(id) }.join(',')
137
+ "#{primary_key_column} IN (#{id_list})"
138
+ else
139
+ "#{primary_key_column} = #{quote(ids)}"
140
+ end
141
+ end
142
+
143
+ def quote(value)
144
+ connection.quote(value)
145
+ end
146
+
147
+ def fields_to_postgres_array(fields)
148
+ quoted_fields = fields.map { |field| "'#{field}'" }.join(',')
149
+ "ARRAY[#{quoted_fields}]"
150
+ end
151
+
152
+ private
153
+
154
+ def attributes_to_hstore(attributes)
155
+ quote ActiveRecord::ConnectionAdapters::PostgreSQLColumn.hstore_to_string(attributes)
156
+ end
157
+
158
+ def hstore_to_attributes(string)
159
+ ActiveRecord::ConnectionAdapters::PostgreSQLColumn.string_to_hstore(string)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,22 @@
1
+ module Superstore
2
+ module AttributeMethods
3
+ class Definition
4
+ attr_reader :name, :coder
5
+ def initialize(name, coder, options)
6
+ @name = name.to_s
7
+ @coder = coder.new(options)
8
+ end
9
+
10
+ def default
11
+ coder.default
12
+ end
13
+
14
+ def instantiate(record, value)
15
+ value = value.nil? ? coder.default : value
16
+ return if value.nil?
17
+
18
+ value.kind_of?(String) ? coder.decode(value) : coder.typecast(value)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ module Superstore
2
+ module AttributeMethods
3
+ module Dirty
4
+ extend ActiveSupport::Concern
5
+ include ActiveModel::Dirty
6
+
7
+ # Attempts to +save+ the record and clears changed attributes if successful.
8
+ def save(*) #:nodoc:
9
+ if status = super
10
+ @previously_changed = changes
11
+ @changed_attributes = {}
12
+ end
13
+ status
14
+ end
15
+
16
+ # <tt>reload</tt> the record and clears changed attributes.
17
+ def reload
18
+ super.tap do
19
+ @previously_changed.try :clear
20
+ @changed_attributes.try :clear
21
+ end
22
+ end
23
+
24
+ def write_attribute(name, value)
25
+ name = name.to_s
26
+ old = read_attribute(name)
27
+
28
+ super
29
+
30
+ unless attribute_changed?(name) || old == read_attribute(name)
31
+ changed_attributes[name] = old
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module Superstore
2
+ module AttributeMethods
3
+ module PrimaryKey
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def primary_key
8
+ 'id'
9
+ end
10
+ end
11
+
12
+ def id
13
+ @id ||= self.class._generate_key(self)
14
+ end
15
+
16
+ def id=(id)
17
+ @id = id
18
+ end
19
+
20
+ def attributes
21
+ super.update(self.class.primary_key => id)
22
+ end
23
+ end
24
+ end
25
+ end