superstore 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.travis.yml +11 -0
- data/CHANGELOG +0 -0
- data/Gemfile +19 -0
- data/LICENSE +13 -0
- data/MIT-LICENSE +20 -0
- data/README.md +100 -0
- data/Rakefile +12 -0
- data/lib/superstore/adapters/abstract_adapter.rb +49 -0
- data/lib/superstore/adapters/cassandra_adapter.rb +181 -0
- data/lib/superstore/adapters/hstore_adapter.rb +163 -0
- data/lib/superstore/attribute_methods/definition.rb +22 -0
- data/lib/superstore/attribute_methods/dirty.rb +36 -0
- data/lib/superstore/attribute_methods/primary_key.rb +25 -0
- data/lib/superstore/attribute_methods/typecasting.rb +59 -0
- data/lib/superstore/attribute_methods.rb +96 -0
- data/lib/superstore/base.rb +33 -0
- data/lib/superstore/belongs_to/association.rb +48 -0
- data/lib/superstore/belongs_to/builder.rb +40 -0
- data/lib/superstore/belongs_to/reflection.rb +30 -0
- data/lib/superstore/belongs_to.rb +63 -0
- data/lib/superstore/callbacks.rb +29 -0
- data/lib/superstore/cassandra_schema/statements.rb +52 -0
- data/lib/superstore/cassandra_schema/tasks.rb +47 -0
- data/lib/superstore/cassandra_schema.rb +9 -0
- data/lib/superstore/connection.rb +39 -0
- data/lib/superstore/core.rb +59 -0
- data/lib/superstore/errors.rb +10 -0
- data/lib/superstore/identity.rb +26 -0
- data/lib/superstore/inspect.rb +25 -0
- data/lib/superstore/log_subscriber.rb +44 -0
- data/lib/superstore/model.rb +37 -0
- data/lib/superstore/persistence.rb +153 -0
- data/lib/superstore/railtie.rb +30 -0
- data/lib/superstore/railties/controller_runtime.rb +45 -0
- data/lib/superstore/schema.rb +20 -0
- data/lib/superstore/scope/batches.rb +32 -0
- data/lib/superstore/scope/finder_methods.rb +48 -0
- data/lib/superstore/scope/query_methods.rb +49 -0
- data/lib/superstore/scope.rb +49 -0
- data/lib/superstore/scoping.rb +27 -0
- data/lib/superstore/tasks/ks.rake +54 -0
- data/lib/superstore/timestamps.rb +19 -0
- data/lib/superstore/type.rb +16 -0
- data/lib/superstore/types/array_type.rb +20 -0
- data/lib/superstore/types/base_type.rb +26 -0
- data/lib/superstore/types/boolean_type.rb +20 -0
- data/lib/superstore/types/date_type.rb +22 -0
- data/lib/superstore/types/float_type.rb +16 -0
- data/lib/superstore/types/integer_type.rb +20 -0
- data/lib/superstore/types/json_type.rb +13 -0
- data/lib/superstore/types/string_type.rb +19 -0
- data/lib/superstore/types/time_type.rb +16 -0
- data/lib/superstore/types.rb +8 -0
- data/lib/superstore/validations.rb +44 -0
- data/lib/superstore.rb +69 -0
- data/superstore.gemspec +23 -0
- data/test/support/cassandra.rb +44 -0
- data/test/support/hstore.rb +40 -0
- data/test/support/issue.rb +10 -0
- data/test/test_helper.rb +42 -0
- data/test/unit/active_model_test.rb +18 -0
- data/test/unit/adapters/adapter_test.rb +6 -0
- data/test/unit/attribute_methods/definition_test.rb +13 -0
- data/test/unit/attribute_methods/dirty_test.rb +72 -0
- data/test/unit/attribute_methods/primary_key_test.rb +26 -0
- data/test/unit/attribute_methods/typecasting_test.rb +118 -0
- data/test/unit/attribute_methods_test.rb +51 -0
- data/test/unit/base_test.rb +20 -0
- data/test/unit/belongs_to/reflection_test.rb +12 -0
- data/test/unit/belongs_to_test.rb +62 -0
- data/test/unit/callbacks_test.rb +46 -0
- data/test/unit/cassandra_schema/statements_test.rb +47 -0
- data/test/unit/cassandra_schema/tasks_test.rb +31 -0
- data/test/unit/connection_test.rb +10 -0
- data/test/unit/core_test.rb +55 -0
- data/test/unit/identity_test.rb +26 -0
- data/test/unit/inspect_test.rb +26 -0
- data/test/unit/log_subscriber_test.rb +26 -0
- data/test/unit/persistence_test.rb +213 -0
- data/test/unit/railties/controller_runtime_test.rb +48 -0
- data/test/unit/schema_test.rb +27 -0
- data/test/unit/scope/batches_test.rb +30 -0
- data/test/unit/scope/finder_methods_test.rb +51 -0
- data/test/unit/scope/query_methods_test.rb +27 -0
- data/test/unit/scoping_test.rb +7 -0
- data/test/unit/serialization_test.rb +10 -0
- data/test/unit/timestamps_test.rb +27 -0
- data/test/unit/types/array_type_test.rb +21 -0
- data/test/unit/types/base_type_test.rb +19 -0
- data/test/unit/types/boolean_type_test.rb +24 -0
- data/test/unit/types/date_type_test.rb +15 -0
- data/test/unit/types/float_type_test.rb +17 -0
- data/test/unit/types/integer_type_test.rb +19 -0
- data/test/unit/types/json_type_test.rb +23 -0
- data/test/unit/types/string_type_test.rb +30 -0
- data/test/unit/types/time_type_test.rb +19 -0
- data/test/unit/validations_test.rb +27 -0
- 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
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,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
|