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,48 @@
|
|
1
|
+
require 'cassandra_object/scope/batches'
|
2
|
+
require 'cassandra_object/scope/finder_methods'
|
3
|
+
require 'cassandra_object/scope/query_methods'
|
4
|
+
|
5
|
+
module CassandraObject
|
6
|
+
class Scope
|
7
|
+
include Batches, FinderMethods, QueryMethods
|
8
|
+
|
9
|
+
attr_accessor :klass
|
10
|
+
attr_accessor :limit_value, :select_values, :where_values
|
11
|
+
|
12
|
+
def initialize(klass)
|
13
|
+
@klass = klass
|
14
|
+
|
15
|
+
@limit_value = nil
|
16
|
+
@select_values = []
|
17
|
+
@where_values = []
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def method_missing(method_name, *args, &block)
|
22
|
+
if klass.respond_to?(method_name)
|
23
|
+
klass.send(method_name, *args, &block)
|
24
|
+
elsif Array.method_defined?(method_name)
|
25
|
+
to_a.send(method_name, *args, &block)
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def instantiate_from_cql(cql_string, *args)
|
32
|
+
results = []
|
33
|
+
klass.execute_cql(cql_string, *args).fetch do |cql_row|
|
34
|
+
results << instantiate_cql_row(cql_row)
|
35
|
+
end
|
36
|
+
results.compact!
|
37
|
+
results
|
38
|
+
end
|
39
|
+
|
40
|
+
def instantiate_cql_row(cql_row)
|
41
|
+
attributes = cql_row.to_hash
|
42
|
+
key = attributes.delete('KEY')
|
43
|
+
if attributes.any?
|
44
|
+
klass.instantiate(key, attributes)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class Scope
|
3
|
+
module Batches
|
4
|
+
def find_each(options = {})
|
5
|
+
find_in_batches(options) do |records|
|
6
|
+
records.each { |record| yield record }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def find_in_batches(options = {})
|
11
|
+
batch_size = options.delete(:batch_size) || 1000
|
12
|
+
start_key = nil
|
13
|
+
|
14
|
+
scope = limit(batch_size + 1)
|
15
|
+
records = scope.to_a
|
16
|
+
|
17
|
+
while records.any?
|
18
|
+
if records.size > batch_size
|
19
|
+
next_record = records.pop
|
20
|
+
else
|
21
|
+
next_record = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
yield records
|
25
|
+
break if next_record.nil?
|
26
|
+
|
27
|
+
records = scope.where("KEY >= '#{next_record.id}'").to_a
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class Scope
|
3
|
+
module FinderMethods
|
4
|
+
def find(ids)
|
5
|
+
if ids.is_a?(Array)
|
6
|
+
find_some(ids)
|
7
|
+
else
|
8
|
+
find_one(ids)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_by_id(ids)
|
13
|
+
find(ids)
|
14
|
+
rescue CassandraObject::RecordNotFound
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
to_a
|
20
|
+
end
|
21
|
+
|
22
|
+
def first
|
23
|
+
limit(1).to_a.first
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def find_one(id)
|
28
|
+
if id.blank?
|
29
|
+
raise CassandraObject::RecordNotFound, "Couldn't find #{self.name} with key #{id.inspect}"
|
30
|
+
elsif record = where('KEY' => id).first
|
31
|
+
record
|
32
|
+
else
|
33
|
+
raise CassandraObject::RecordNotFound
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_some(ids)
|
38
|
+
ids = ids.flatten
|
39
|
+
return [] if ids.empty?
|
40
|
+
|
41
|
+
ids = ids.compact.map(&:to_s).uniq
|
42
|
+
|
43
|
+
where("KEY" => ids).to_a
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class Scope
|
3
|
+
module QueryMethods
|
4
|
+
def select!(*values)
|
5
|
+
self.select_values += values.flatten
|
6
|
+
self
|
7
|
+
end
|
8
|
+
|
9
|
+
def select(*values, &block)
|
10
|
+
if block_given?
|
11
|
+
to_a.select(&block)
|
12
|
+
else
|
13
|
+
clone.select! *values
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def where!(*values)
|
18
|
+
self.where_values += values.flatten
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def where(*values)
|
23
|
+
clone.where! values
|
24
|
+
end
|
25
|
+
|
26
|
+
def limit!(value)
|
27
|
+
self.limit_value = value
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def limit(value)
|
32
|
+
clone.limit! value
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_cql
|
36
|
+
[
|
37
|
+
"SELECT #{select_string} FROM #{klass.column_family}",
|
38
|
+
consistency_string,
|
39
|
+
where_string,
|
40
|
+
limit_string
|
41
|
+
].delete_if(&:blank?) * ' '
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_a
|
45
|
+
instantiate_from_cql(to_cql)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def select_string
|
50
|
+
if select_values.any?
|
51
|
+
(['KEY'] | select_values) * ','
|
52
|
+
else
|
53
|
+
'*'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def where_string
|
58
|
+
if where_values.any?
|
59
|
+
wheres = []
|
60
|
+
|
61
|
+
where_values.map do |where_value|
|
62
|
+
wheres.concat format_where_statement(where_value)
|
63
|
+
end
|
64
|
+
|
65
|
+
"WHERE #{wheres * ' AND '}"
|
66
|
+
else
|
67
|
+
''
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def format_where_statement(where_value)
|
72
|
+
if where_value.is_a?(String)
|
73
|
+
[where_value]
|
74
|
+
elsif where_value.is_a?(Hash)
|
75
|
+
where_value.map do |column, value|
|
76
|
+
if value.is_a?(Array)
|
77
|
+
"#{column} IN (#{escape_where_value(value)})"
|
78
|
+
else
|
79
|
+
"#{column} = #{escape_where_value(value)}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def escape_where_value(value)
|
86
|
+
if value.is_a?(Array)
|
87
|
+
value.map { |v| escape_where_value(v) }.join(",")
|
88
|
+
elsif value.is_a?(String)
|
89
|
+
value = value.gsub("'", "''")
|
90
|
+
"'#{value}'"
|
91
|
+
else
|
92
|
+
value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def limit_string
|
97
|
+
if limit_value
|
98
|
+
"LIMIT #{limit_value}"
|
99
|
+
else
|
100
|
+
""
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def consistency_string
|
105
|
+
if klass.default_consistency
|
106
|
+
"USING CONSISTENCY #{klass.default_consistency}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
module Scoping
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
singleton_class.class_eval do
|
7
|
+
delegate :find, :find_by_id, :first, :all, to: :scope
|
8
|
+
delegate :find_each, :find_in_batches, to: :scope
|
9
|
+
delegate :select, :where, to: :scope
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def scope
|
15
|
+
Scope.new(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
namespace :cassandra do
|
2
|
+
|
3
|
+
desc 'Creates the keyspace in config/cassandra.yml for the current environment'
|
4
|
+
task create: :environment do
|
5
|
+
begin
|
6
|
+
CassandraObject::Schema.create_keyspace cassandra_config.keyspace, cassandra_config.keyspace_options
|
7
|
+
rescue Exception => e
|
8
|
+
if e.message =~ /conflicts/
|
9
|
+
p "Keyspace #{cassandra_config.keyspace} already exists"
|
10
|
+
else
|
11
|
+
raise e
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Drops the keyspace in config/cassandra.yml for the current environment'
|
17
|
+
task drop: :environment do
|
18
|
+
begin
|
19
|
+
CassandraObject::Schema.drop_keyspace cassandra_config.keyspace
|
20
|
+
rescue Exception => e
|
21
|
+
if e.message =~ /non existing keyspace/
|
22
|
+
p "Keyspace #{cassandra_config.keyspace} does not exist"
|
23
|
+
else
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
task reset: [:drop, :setup]
|
30
|
+
task setup: [:create, :load]
|
31
|
+
|
32
|
+
task dump: :environment do
|
33
|
+
filename = ENV['SCHEMA'] || "#{Rails.root}/db/cassandra/structure.cql"
|
34
|
+
File.open(filename, "w:utf-8") do |file|
|
35
|
+
CassandraObject::Schema.dump(file)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
task load: :environment do
|
40
|
+
filename = ENV['SCHEMA'] || "#{Rails.root}/db/cassandra/structure.cql"
|
41
|
+
File.open(filename) do |file|
|
42
|
+
CassandraObject::Schema.load(file)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def cassandra_config
|
48
|
+
@cassandra_config ||= begin
|
49
|
+
cassandra_configs = YAML.load_file(Rails.root.join('config', 'cassandra.yml'))
|
50
|
+
CassandraObject::Config.new cassandra_configs[Rails.env || 'development']
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
module Timestamps
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
attribute :created_at, type: :time
|
7
|
+
attribute :updated_at, type: :time
|
8
|
+
|
9
|
+
before_create do
|
10
|
+
self.created_at ||= Time.current
|
11
|
+
self.updated_at ||= Time.current
|
12
|
+
end
|
13
|
+
|
14
|
+
before_update if: :changed? do
|
15
|
+
self.updated_at = Time.current
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class Type
|
3
|
+
cattr_accessor :attribute_types
|
4
|
+
self.attribute_types = {}.with_indifferent_access
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def register(name, coder)
|
8
|
+
attribute_types[name] = coder
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_coder(name)
|
12
|
+
attribute_types[name]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
CassandraObject::Type.register(:array, CassandraObject::Types::ArrayType)
|
2
|
+
CassandraObject::Type.register(:boolean, CassandraObject::Types::BooleanType)
|
3
|
+
CassandraObject::Type.register(:date, CassandraObject::Types::DateType)
|
4
|
+
CassandraObject::Type.register(:float, CassandraObject::Types::FloatType)
|
5
|
+
CassandraObject::Type.register(:integer, CassandraObject::Types::IntegerType)
|
6
|
+
CassandraObject::Type.register(:json, CassandraObject::Types::JsonType)
|
7
|
+
CassandraObject::Type.register(:time, CassandraObject::Types::TimeType)
|
8
|
+
CassandraObject::Type.register(:string, CassandraObject::Types::StringType)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
module Types
|
3
|
+
class ArrayType < BaseType
|
4
|
+
class DirtyArray < Array
|
5
|
+
attr_accessor :record, :name, :options
|
6
|
+
def initialize(record, name, array, options)
|
7
|
+
@record = record
|
8
|
+
@name = name.to_s
|
9
|
+
@options = options
|
10
|
+
|
11
|
+
super(array)
|
12
|
+
setify!
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(obj)
|
16
|
+
modifying do
|
17
|
+
super
|
18
|
+
setify!
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete(obj)
|
23
|
+
modifying do
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def setify!
|
30
|
+
if options[:unique]
|
31
|
+
reject!(&:blank?)
|
32
|
+
uniq!
|
33
|
+
begin sort! rescue ArgumentError end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def modifying
|
38
|
+
unless record.changed_attributes.include?(name)
|
39
|
+
original = dup
|
40
|
+
end
|
41
|
+
|
42
|
+
result = yield
|
43
|
+
|
44
|
+
if !record.changed_attributes.key?(name) && original != self
|
45
|
+
record.changed_attributes[name] = original
|
46
|
+
end
|
47
|
+
|
48
|
+
record.send("#{name}=", self)
|
49
|
+
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def default
|
55
|
+
[]
|
56
|
+
end
|
57
|
+
|
58
|
+
def encode(array)
|
59
|
+
raise ArgumentError.new("#{array.inspect} is not an Array") unless array.kind_of?(Array)
|
60
|
+
array.to_a.to_json
|
61
|
+
end
|
62
|
+
|
63
|
+
def decode(str)
|
64
|
+
return [] if str.blank?
|
65
|
+
|
66
|
+
ActiveSupport::JSON.decode(str).tap do |array|
|
67
|
+
array.uniq! if options[:unique]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def wrap(record, name, value)
|
72
|
+
DirtyArray.new(record, name, Array(value), options)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|