cassandra_object_rails 0.0.1
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 +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
|