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.
- 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
|
+
[](http://travis-ci.org/data-axle/superstore) [](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
|