cassandra_object_rails 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +3 -0
- data/.travis.yml +7 -0
- data/CHANGELOG +5 -0
- data/Gemfile +8 -0
- data/LICENSE +13 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +97 -0
- data/Rakefile +12 -0
- data/cassandra_object_rails.gemspec +26 -0
- data/lib/cassandra_object/attribute_methods.rb +87 -0
- data/lib/cassandra_object/attribute_methods/definition.rb +19 -0
- data/lib/cassandra_object/attribute_methods/dirty.rb +44 -0
- data/lib/cassandra_object/attribute_methods/primary_key.rb +25 -0
- data/lib/cassandra_object/attribute_methods/typecasting.rb +59 -0
- data/lib/cassandra_object/base.rb +69 -0
- data/lib/cassandra_object/belongs_to.rb +63 -0
- data/lib/cassandra_object/belongs_to/association.rb +48 -0
- data/lib/cassandra_object/belongs_to/builder.rb +40 -0
- data/lib/cassandra_object/belongs_to/reflection.rb +30 -0
- data/lib/cassandra_object/callbacks.rb +29 -0
- data/lib/cassandra_object/config.rb +15 -0
- data/lib/cassandra_object/connection.rb +36 -0
- data/lib/cassandra_object/consistency.rb +18 -0
- data/lib/cassandra_object/core.rb +59 -0
- data/lib/cassandra_object/errors.rb +6 -0
- data/lib/cassandra_object/identity.rb +24 -0
- data/lib/cassandra_object/inspect.rb +25 -0
- data/lib/cassandra_object/log_subscriber.rb +29 -0
- data/lib/cassandra_object/persistence.rb +169 -0
- data/lib/cassandra_object/rails_initializer.rb +19 -0
- data/lib/cassandra_object/railtie.rb +11 -0
- data/lib/cassandra_object/savepoints.rb +79 -0
- data/lib/cassandra_object/schema.rb +78 -0
- data/lib/cassandra_object/schema/tasks.rb +48 -0
- data/lib/cassandra_object/scope.rb +48 -0
- data/lib/cassandra_object/scope/batches.rb +32 -0
- data/lib/cassandra_object/scope/finder_methods.rb +47 -0
- data/lib/cassandra_object/scope/query_methods.rb +111 -0
- data/lib/cassandra_object/scoping.rb +19 -0
- data/lib/cassandra_object/serialization.rb +6 -0
- data/lib/cassandra_object/tasks/cassandra.rake +53 -0
- data/lib/cassandra_object/timestamps.rb +19 -0
- data/lib/cassandra_object/type.rb +16 -0
- data/lib/cassandra_object/types.rb +8 -0
- data/lib/cassandra_object/types/array_type.rb +76 -0
- data/lib/cassandra_object/types/base_type.rb +26 -0
- data/lib/cassandra_object/types/boolean_type.rb +20 -0
- data/lib/cassandra_object/types/date_type.rb +17 -0
- data/lib/cassandra_object/types/float_type.rb +16 -0
- data/lib/cassandra_object/types/integer_type.rb +16 -0
- data/lib/cassandra_object/types/json_type.rb +52 -0
- data/lib/cassandra_object/types/string_type.rb +15 -0
- data/lib/cassandra_object/types/time_type.rb +16 -0
- data/lib/cassandra_object/validations.rb +44 -0
- data/lib/cassandra_object_rails.rb +64 -0
- data/test/support/connect.rb +17 -0
- data/test/support/issue.rb +5 -0
- data/test/support/teardown.rb +24 -0
- data/test/test_helper.rb +34 -0
- data/test/unit/active_model_test.rb +18 -0
- data/test/unit/attribute_methods/definition_test.rb +13 -0
- data/test/unit/attribute_methods/dirty_test.rb +71 -0
- data/test/unit/attribute_methods/primary_key_test.rb +26 -0
- data/test/unit/attribute_methods/typecasting_test.rb +112 -0
- data/test/unit/attribute_methods_test.rb +39 -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/config_test.rb +23 -0
- data/test/unit/connection_test.rb +10 -0
- data/test/unit/consistency_test.rb +13 -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 +22 -0
- data/test/unit/persistence_test.rb +187 -0
- data/test/unit/savepoints_test.rb +35 -0
- data/test/unit/schema/tasks_test.rb +29 -0
- data/test/unit/schema_test.rb +47 -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 +26 -0
- data/test/unit/scoping_test.rb +7 -0
- data/test/unit/timestamps_test.rb +27 -0
- data/test/unit/types/array_type_test.rb +71 -0
- data/test/unit/types/base_type_test.rb +24 -0
- data/test/unit/types/boolean_type_test.rb +24 -0
- data/test/unit/types/date_type_test.rb +11 -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 +77 -0
- data/test/unit/types/string_type_test.rb +32 -0
- data/test/unit/types/time_type_test.rb +14 -0
- data/test/unit/validations_test.rb +27 -0
- metadata +208 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
module Inspect
|
3
|
+
def inspect
|
4
|
+
inspection = ["#{self.class.primary_key}: #{id.inspect}"]
|
5
|
+
|
6
|
+
@attributes.each do |attribute, value|
|
7
|
+
if value.present?
|
8
|
+
inspection << "#{attribute}: #{attribute_for_inspect(value)}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
"#<#{self.class} #{inspection * ', '}>"
|
13
|
+
end
|
14
|
+
|
15
|
+
def attribute_for_inspect(value)
|
16
|
+
if value.is_a?(String) && value.length > 50
|
17
|
+
"#{value[0..50]}...".inspect
|
18
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
19
|
+
%("#{value.to_s(:db)}")
|
20
|
+
else
|
21
|
+
value.inspect
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
3
|
+
def initialize
|
4
|
+
super
|
5
|
+
@odd_or_even = false
|
6
|
+
end
|
7
|
+
|
8
|
+
def cql(event)
|
9
|
+
payload = event.payload
|
10
|
+
name = '%s (%.1fms)' % [payload[:name], event.duration]
|
11
|
+
cql = payload[:cql].squeeze(' ')
|
12
|
+
|
13
|
+
if odd?
|
14
|
+
name = color(name, CYAN, true)
|
15
|
+
cql = color(cql, nil, true)
|
16
|
+
else
|
17
|
+
name = color(name, MAGENTA, true)
|
18
|
+
end
|
19
|
+
|
20
|
+
debug " #{name} #{cql}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def odd?
|
24
|
+
@odd_or_even = !@odd_or_even
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
CassandraObject::LogSubscriber.attach_to :cassandra_object
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
module Persistence
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :batch_statements
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def remove(id)
|
11
|
+
execute_batchable_cql "DELETE FROM #{column_family}#{write_option_string} WHERE KEY = ?", id
|
12
|
+
end
|
13
|
+
|
14
|
+
def delete_all
|
15
|
+
execute_cql "TRUNCATE #{column_family}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def create(attributes = {}, &block)
|
19
|
+
new(attributes, &block).tap do |object|
|
20
|
+
object.save
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(id, attributes)
|
25
|
+
if (encoded = encode_attributes(attributes)).any?
|
26
|
+
insert_attributes = {'KEY' => id}.update encode_attributes(attributes)
|
27
|
+
statement = "INSERT INTO #{column_family} (#{quote_columns(insert_attributes.keys) * ','}) VALUES (#{Array.new(insert_attributes.size, '?') * ','})#{write_option_string}"
|
28
|
+
execute_batchable_cql statement, *insert_attributes.values
|
29
|
+
end
|
30
|
+
|
31
|
+
if (nil_attributes = attributes.select { |key, value| value.nil? }).any?
|
32
|
+
execute_batchable_cql "DELETE #{quote_columns(nil_attributes.keys) * ','} FROM #{column_family}#{write_option_string} WHERE KEY = ?", id
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def batching?
|
37
|
+
!batch_statements.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
def batch
|
41
|
+
self.batch_statements = []
|
42
|
+
yield
|
43
|
+
execute_cql(batch_statement) if batch_statements.any?
|
44
|
+
ensure
|
45
|
+
self.batch_statements = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def instantiate(id, attributes)
|
49
|
+
allocate.tap do |object|
|
50
|
+
object.instance_variable_set("@id", id) if id
|
51
|
+
object.instance_variable_set("@new_record", false)
|
52
|
+
object.instance_variable_set("@destroyed", false)
|
53
|
+
object.instance_variable_set("@attributes", typecast_attributes(object, attributes))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def encode_attributes(attributes)
|
58
|
+
encoded = {}
|
59
|
+
attributes.each do |column_name, value|
|
60
|
+
unless value.nil?
|
61
|
+
encoded[column_name.to_s] = attribute_definitions[column_name.to_sym].coder.encode(value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
encoded
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def quote_columns(column_names)
|
70
|
+
column_names.map { |name| "'#{name}'" }
|
71
|
+
end
|
72
|
+
|
73
|
+
def batch_statement
|
74
|
+
return nil unless batch_statements.any?
|
75
|
+
|
76
|
+
[
|
77
|
+
"BEGIN BATCH#{write_option_string(true)}",
|
78
|
+
batch_statements * "\n",
|
79
|
+
'APPLY BATCH'
|
80
|
+
] * "\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
def execute_batchable_cql(cql_string, *bind_vars)
|
84
|
+
if batch_statements
|
85
|
+
batch_statements << CassandraCQL::Statement.sanitize(cql_string, bind_vars)
|
86
|
+
else
|
87
|
+
execute_cql cql_string, *bind_vars
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def typecast_attributes(object, attributes)
|
92
|
+
attributes = attributes.symbolize_keys
|
93
|
+
Hash[attribute_definitions.map { |k, attribute_definition| [k.to_s, attribute_definition.instantiate(object, attributes[k])] }]
|
94
|
+
end
|
95
|
+
|
96
|
+
def write_option_string(ignore_batching = false)
|
97
|
+
if (ignore_batching || !batching?) && base_class.default_consistency
|
98
|
+
" USING CONSISTENCY #{base_class.default_consistency}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def new_record?
|
104
|
+
@new_record
|
105
|
+
end
|
106
|
+
|
107
|
+
def destroyed?
|
108
|
+
@destroyed
|
109
|
+
end
|
110
|
+
|
111
|
+
def persisted?
|
112
|
+
!(new_record? || destroyed?)
|
113
|
+
end
|
114
|
+
|
115
|
+
def save(*)
|
116
|
+
new_record? ? create : update
|
117
|
+
end
|
118
|
+
|
119
|
+
def destroy
|
120
|
+
self.class.remove(id)
|
121
|
+
@destroyed = true
|
122
|
+
end
|
123
|
+
|
124
|
+
def update_attribute(name, value)
|
125
|
+
name = name.to_s
|
126
|
+
send("#{name}=", value)
|
127
|
+
save(validate: false)
|
128
|
+
end
|
129
|
+
|
130
|
+
def update_attributes(attributes)
|
131
|
+
self.attributes = attributes
|
132
|
+
save
|
133
|
+
end
|
134
|
+
|
135
|
+
def update_attributes!(attributes)
|
136
|
+
self.attributes = attributes
|
137
|
+
save!
|
138
|
+
end
|
139
|
+
|
140
|
+
def becomes(klass)
|
141
|
+
became = klass.new
|
142
|
+
became.instance_variable_set("@attributes", @attributes)
|
143
|
+
became.instance_variable_set("@new_record", new_record?)
|
144
|
+
became.instance_variable_set("@destroyed", destroyed?)
|
145
|
+
became
|
146
|
+
end
|
147
|
+
|
148
|
+
def reload
|
149
|
+
clear_belongs_to_cache
|
150
|
+
@attributes.update(self.class.find(id).instance_variable_get('@attributes'))
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
def update
|
156
|
+
write
|
157
|
+
end
|
158
|
+
|
159
|
+
def create
|
160
|
+
@new_record = false
|
161
|
+
write
|
162
|
+
end
|
163
|
+
|
164
|
+
def write
|
165
|
+
changed_attributes = Hash[changed.map { |attr| [attr, read_attribute(attr)] }]
|
166
|
+
self.class.write(id, changed_attributes)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
|
3
|
+
class RailsInitializer
|
4
|
+
|
5
|
+
def self.configure!
|
6
|
+
self.new.configure!
|
7
|
+
end
|
8
|
+
|
9
|
+
def configure!
|
10
|
+
return if cassandra_configs.nil?
|
11
|
+
CassandraObject::Base.config = cassandra_configs[Rails.env || 'development']
|
12
|
+
end
|
13
|
+
|
14
|
+
def cassandra_configs
|
15
|
+
@config ||= YAML.load_file(Rails.root.join('config', 'cassandra.yml'))
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
module Savepoints
|
3
|
+
class Rollback
|
4
|
+
attr_accessor :action, :record, :rollback_attributes
|
5
|
+
def initialize(action, record)
|
6
|
+
@action = action
|
7
|
+
@record = record
|
8
|
+
if action == :update
|
9
|
+
@rollback_attributes = record.changed_attributes.deep_dup
|
10
|
+
elsif action == :create
|
11
|
+
@rollback_attributes = record.attributes.deep_dup
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def run!
|
16
|
+
case action
|
17
|
+
when :destroy
|
18
|
+
record.class.remove(record.id)
|
19
|
+
when :create, :update
|
20
|
+
record.class.write(record.id, rollback_attributes)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Savepoint
|
26
|
+
attr_accessor :rollbacks
|
27
|
+
def initialize
|
28
|
+
self.rollbacks = []
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_rollback(action, record)
|
32
|
+
states << Rollback.new(action, record)
|
33
|
+
end
|
34
|
+
|
35
|
+
def rollback!
|
36
|
+
rollbacks.reverse_each(&:run!)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
extend ActiveSupport::Concern
|
41
|
+
|
42
|
+
included do
|
43
|
+
class_attribute :savepoints
|
44
|
+
self.savepoints = []
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def savepoint
|
49
|
+
self.savepoints.push Savepoint.new
|
50
|
+
yield
|
51
|
+
savepoints.pop
|
52
|
+
rescue => e
|
53
|
+
savepoints.pop.rollback!
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_savepoint_rollback(action, record)
|
57
|
+
unless savepoints.empty?
|
58
|
+
savepoints.last.add_rollback(action, record)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def destroy
|
64
|
+
self.class.add_savepoint_rollback(:create, self)
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def create
|
70
|
+
self.class.add_savepoint_rollback(:destroy, self)
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
def update
|
75
|
+
self.class.add_savepoint_rollback(:update, self)
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'cassandra_object/schema/tasks'
|
2
|
+
|
3
|
+
module CassandraObject
|
4
|
+
class Schema
|
5
|
+
extend Tasks
|
6
|
+
|
7
|
+
class << self
|
8
|
+
DEFAULT_CREATE_KEYSPACE = {
|
9
|
+
'strategy_class' => 'SimpleStrategy',
|
10
|
+
'strategy_options:replication_factor' => 1
|
11
|
+
}
|
12
|
+
|
13
|
+
def create_keyspace(keyspace, options = {})
|
14
|
+
stmt = "CREATE KEYSPACE #{keyspace}"
|
15
|
+
|
16
|
+
if options.empty?
|
17
|
+
options = DEFAULT_CREATE_KEYSPACE
|
18
|
+
end
|
19
|
+
|
20
|
+
system_execute statement_with_options(stmt, options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def drop_keyspace(keyspace)
|
24
|
+
system_execute "DROP KEYSPACE #{keyspace}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_column_family(column_family, options = {})
|
28
|
+
stmt = "CREATE COLUMNFAMILY #{column_family} " +
|
29
|
+
"(KEY varchar PRIMARY KEY)"
|
30
|
+
|
31
|
+
execute statement_with_options(stmt, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def alter_column_family(column_family, instruction, options = {})
|
35
|
+
stmt = "ALTER TABLE #{column_family} #{instruction}"
|
36
|
+
execute statement_with_options(stmt, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def drop_column_family(column_family)
|
40
|
+
stmt = "DROP TABLE #{column_family}"
|
41
|
+
execute stmt
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_index(column_family, column, index_name = nil)
|
45
|
+
stmt = "CREATE INDEX #{index_name.nil? ? '' : index_name} ON #{column_family} (#{column})"
|
46
|
+
execute stmt
|
47
|
+
end
|
48
|
+
|
49
|
+
def drop_index(index_name)
|
50
|
+
# If the index was not given a name during creation, the index name is <columnfamily_name>_<column_name>_idx.
|
51
|
+
stmt = "DROP INDEX #{index_name}"
|
52
|
+
execute stmt
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def statement_with_options(stmt, options)
|
57
|
+
if options.any?
|
58
|
+
with_stmt = options.map do |k,v|
|
59
|
+
"#{k} = #{CassandraCQL::Statement.quote(v)}"
|
60
|
+
end.join(' AND ')
|
61
|
+
|
62
|
+
stmt << " WITH #{with_stmt}"
|
63
|
+
end
|
64
|
+
|
65
|
+
stmt
|
66
|
+
end
|
67
|
+
|
68
|
+
def execute(cql)
|
69
|
+
CassandraObject::Base.execute_cql cql
|
70
|
+
end
|
71
|
+
|
72
|
+
def system_execute(cql)
|
73
|
+
@system_cql ||= CassandraCQL::Database.new(CassandraObject::Base.config.servers, keyspace: 'system')
|
74
|
+
@system_cql.execute cql
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class Schema
|
3
|
+
module Tasks
|
4
|
+
def dump(io)
|
5
|
+
column_families.each do |column_family|
|
6
|
+
io.puts run_command("DESCRIBE COLUMNFAMILY #{column_family}")
|
7
|
+
io.puts
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def load(io)
|
12
|
+
current_cql = ''
|
13
|
+
|
14
|
+
io.each_line do |line|
|
15
|
+
next if line.blank?
|
16
|
+
|
17
|
+
current_cql << line.rstrip
|
18
|
+
|
19
|
+
if current_cql =~ /;$/
|
20
|
+
CassandraObject::Base.execute_cql current_cql
|
21
|
+
current_cql = ''
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def column_families
|
27
|
+
run_command('DESCRIBE COLUMNFAMILIES').split.sort
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def run_command(command)
|
32
|
+
`echo "#{command};" | #{cqlsh} -k #{keyspace} #{server}`.sub(/^(.*)$/, '').strip
|
33
|
+
end
|
34
|
+
|
35
|
+
def cqlsh
|
36
|
+
ENV['CQLSH'] || 'cqlsh'
|
37
|
+
end
|
38
|
+
|
39
|
+
def keyspace
|
40
|
+
CassandraObject::Base.config.keyspace
|
41
|
+
end
|
42
|
+
|
43
|
+
def server
|
44
|
+
CassandraObject::Base.config.servers.first.gsub(/:.*/, '')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|