superstore 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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