gotime-cassandra_object 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +3 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +13 -0
- data/README.markdown +79 -0
- data/Rakefile +74 -0
- data/TODO +2 -0
- data/VERSION +1 -0
- data/gotime-cassandra_object.gemspec +134 -0
- data/lib/cassandra_object.rb +13 -0
- data/lib/cassandra_object/associations.rb +35 -0
- data/lib/cassandra_object/associations/one_to_many.rb +136 -0
- data/lib/cassandra_object/associations/one_to_one.rb +77 -0
- data/lib/cassandra_object/attributes.rb +93 -0
- data/lib/cassandra_object/base.rb +97 -0
- data/lib/cassandra_object/callbacks.rb +10 -0
- data/lib/cassandra_object/collection.rb +8 -0
- data/lib/cassandra_object/cursor.rb +86 -0
- data/lib/cassandra_object/dirty.rb +27 -0
- data/lib/cassandra_object/identity.rb +61 -0
- data/lib/cassandra_object/identity/abstract_key_factory.rb +36 -0
- data/lib/cassandra_object/identity/key.rb +20 -0
- data/lib/cassandra_object/identity/natural_key_factory.rb +51 -0
- data/lib/cassandra_object/identity/uuid_key_factory.rb +37 -0
- data/lib/cassandra_object/indexes.rb +129 -0
- data/lib/cassandra_object/log_subscriber.rb +17 -0
- data/lib/cassandra_object/migrations.rb +72 -0
- data/lib/cassandra_object/mocking.rb +15 -0
- data/lib/cassandra_object/persistence.rb +195 -0
- data/lib/cassandra_object/serialization.rb +6 -0
- data/lib/cassandra_object/type_registration.rb +7 -0
- data/lib/cassandra_object/types.rb +128 -0
- data/lib/cassandra_object/validation.rb +49 -0
- data/test/basic_scenarios_test.rb +243 -0
- data/test/callbacks_test.rb +19 -0
- data/test/config/cassandra.in.sh +53 -0
- data/test/config/log4j.properties +38 -0
- data/test/config/storage-conf.xml +221 -0
- data/test/connection.rb +25 -0
- data/test/cursor_test.rb +66 -0
- data/test/dirty_test.rb +34 -0
- data/test/fixture_models.rb +90 -0
- data/test/identity/natural_key_factory_test.rb +94 -0
- data/test/index_test.rb +69 -0
- data/test/legacy/test_helper.rb +18 -0
- data/test/migration_test.rb +21 -0
- data/test/one_to_many_associations_test.rb +163 -0
- data/test/test_case.rb +28 -0
- data/test/test_helper.rb +16 -0
- data/test/time_test.rb +32 -0
- data/test/types_test.rb +252 -0
- data/test/validation_test.rb +25 -0
- data/test/z_mock_test.rb +36 -0
- metadata +243 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class OneToOneAssociation
|
3
|
+
def initialize(association_name, owner_class, options)
|
4
|
+
@association_name = association_name.to_s
|
5
|
+
@owner_class = owner_class
|
6
|
+
@target_class_name = options[:class_name] || association_name.to_s.camelize
|
7
|
+
@options = options
|
8
|
+
|
9
|
+
define_methods!
|
10
|
+
end
|
11
|
+
|
12
|
+
def define_methods!
|
13
|
+
@owner_class.class_eval <<-eos
|
14
|
+
def #{@association_name}
|
15
|
+
@_#{@association_name} ||= self.class.associations[:#{@association_name}].find(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def #{@association_name}=(record)
|
19
|
+
@_#{@association_name} = record
|
20
|
+
self.class.associations[:#{@association_name}].set(self, record)
|
21
|
+
end
|
22
|
+
eos
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear(owner)
|
26
|
+
connection.remove(column_family, owner.key.to_s, @association_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def find(owner)
|
30
|
+
if key = connection.get(column_family, owner.key.to_s, @association_name.to_s, :count=>1).values.first
|
31
|
+
target_class.get(key)
|
32
|
+
else
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def set(owner, record, set_inverse = true)
|
38
|
+
clear(owner)
|
39
|
+
connection.insert(column_family, owner.key.to_s, {@association_name=>{new_key => record.key.to_s}})
|
40
|
+
if has_inverse? && set_inverse
|
41
|
+
inverse.set_inverse(record, owner)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def new_key
|
46
|
+
SimpleUUID::UUID.new
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_inverse(owner, record)
|
50
|
+
set(owner, record, false)
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_inverse?
|
54
|
+
@options[:inverse_of]
|
55
|
+
end
|
56
|
+
|
57
|
+
def inverse
|
58
|
+
has_inverse? && target_class.associations[@options[:inverse_of]]
|
59
|
+
end
|
60
|
+
|
61
|
+
def column_family
|
62
|
+
@owner_class.to_s + "Relationships"
|
63
|
+
end
|
64
|
+
|
65
|
+
def connection
|
66
|
+
@owner_class.connection
|
67
|
+
end
|
68
|
+
|
69
|
+
def target_class
|
70
|
+
@target_class ||= @target_class_name.constantize
|
71
|
+
end
|
72
|
+
|
73
|
+
def new_proxy(owner)
|
74
|
+
# OneToManyAssociationProxy.new(self, owner)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class Attribute
|
3
|
+
|
4
|
+
attr_reader :name, :converter, :expected_type
|
5
|
+
def initialize(name, owner_class, converter, expected_type, options)
|
6
|
+
@name = name.to_s
|
7
|
+
@owner_class = owner_class
|
8
|
+
@converter = converter
|
9
|
+
@expected_type = expected_type
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def check_value!(value)
|
14
|
+
converter.encode(value) unless value.nil? && @options[:allow_nil]
|
15
|
+
value
|
16
|
+
end
|
17
|
+
|
18
|
+
def define_methods!
|
19
|
+
@owner_class.define_attribute_methods(true)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Attributes
|
24
|
+
extend ActiveSupport::Concern
|
25
|
+
include ActiveModel::AttributeMethods
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def attribute(name, options)
|
29
|
+
|
30
|
+
unless type_mapping = attribute_types[options[:type]]
|
31
|
+
type_mapping = { :expected_type => options[:type],
|
32
|
+
:converter => options[:converter] }.with_indifferent_access
|
33
|
+
end
|
34
|
+
|
35
|
+
new_attr = Attribute.new(name, self, type_mapping[:converter], type_mapping[:expected_type], options)
|
36
|
+
write_inheritable_hash(:model_attributes, {name => new_attr}.with_indifferent_access)
|
37
|
+
new_attr.define_methods!
|
38
|
+
end
|
39
|
+
|
40
|
+
def define_attribute_methods(force = false)
|
41
|
+
return unless model_attributes
|
42
|
+
undefine_attribute_methods if force
|
43
|
+
super(model_attributes.keys)
|
44
|
+
end
|
45
|
+
|
46
|
+
def register_attribute_type(name, expected_type, converter)
|
47
|
+
attribute_types[name] = { :expected_type => expected_type, :converter => converter }.with_indifferent_access
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
included do
|
52
|
+
class_inheritable_hash :model_attributes
|
53
|
+
attribute_method_suffix("", "=")
|
54
|
+
|
55
|
+
cattr_accessor :attribute_types
|
56
|
+
self.attribute_types = {}.with_indifferent_access
|
57
|
+
end
|
58
|
+
|
59
|
+
module InstanceMethods
|
60
|
+
def write_attribute(name, value)
|
61
|
+
if ma = self.class.model_attributes[name]
|
62
|
+
@attributes[name.to_s] = ma.check_value!(value)
|
63
|
+
else
|
64
|
+
raise NoMethodError, "Unknown attribute #{name.inspect}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def read_attribute(name)
|
69
|
+
@attributes[name.to_s]
|
70
|
+
end
|
71
|
+
|
72
|
+
def attributes=(attributes)
|
73
|
+
attributes.each do |(name, value)|
|
74
|
+
send("#{name}=", value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
def attribute_method?(name)
|
80
|
+
!!model_attributes[name.to_sym]
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def attribute(name)
|
85
|
+
read_attribute(name.to_sym)
|
86
|
+
end
|
87
|
+
|
88
|
+
def attribute=(name, value)
|
89
|
+
write_attribute(name.to_sym, value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'cassandra'
|
2
|
+
require 'set'
|
3
|
+
require 'cassandra_object/attributes'
|
4
|
+
require 'cassandra_object/dirty'
|
5
|
+
require 'cassandra_object/persistence'
|
6
|
+
|
7
|
+
require 'cassandra_object/callbacks'
|
8
|
+
|
9
|
+
require 'cassandra_object/validation'
|
10
|
+
require 'cassandra_object/identity'
|
11
|
+
require 'cassandra_object/indexes'
|
12
|
+
require 'cassandra_object/serialization'
|
13
|
+
require 'cassandra_object/associations'
|
14
|
+
#require 'cassandra_object/sets'
|
15
|
+
require 'cassandra_object/migrations'
|
16
|
+
require 'cassandra_object/cursor'
|
17
|
+
require 'cassandra_object/collection'
|
18
|
+
require 'cassandra_object/types'
|
19
|
+
require 'cassandra_object/mocking'
|
20
|
+
|
21
|
+
require 'cassandra_object/log_subscriber'
|
22
|
+
|
23
|
+
module CassandraObject
|
24
|
+
class Base
|
25
|
+
class_inheritable_accessor :connection
|
26
|
+
class_inheritable_writer :connection_class
|
27
|
+
|
28
|
+
def self.connection_class
|
29
|
+
read_inheritable_attribute(:connection_class) || Cassandra
|
30
|
+
end
|
31
|
+
|
32
|
+
module ConnectionManagement
|
33
|
+
def establish_connection(*args)
|
34
|
+
self.connection = connection_class.new(*args)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
extend ConnectionManagement
|
38
|
+
|
39
|
+
module Naming
|
40
|
+
def column_family=(column_family)
|
41
|
+
@column_family = column_family
|
42
|
+
end
|
43
|
+
|
44
|
+
def column_family
|
45
|
+
@column_family || name.pluralize
|
46
|
+
end
|
47
|
+
end
|
48
|
+
extend Naming
|
49
|
+
|
50
|
+
extend ActiveModel::Naming
|
51
|
+
|
52
|
+
module ConfigurationDumper
|
53
|
+
def storage_config_xml
|
54
|
+
subclasses.map(&:constantize).map(&:column_family_configuration).flatten.map do |config|
|
55
|
+
config_to_xml(config)
|
56
|
+
end.join("\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
def config_to_xml(config)
|
60
|
+
xml = "<ColumnFamily "
|
61
|
+
config.each do |(attr_name, attr_value)|
|
62
|
+
xml << " #{attr_name}=\"#{attr_value}\""
|
63
|
+
end
|
64
|
+
xml << " />"
|
65
|
+
xml
|
66
|
+
end
|
67
|
+
end
|
68
|
+
extend ConfigurationDumper
|
69
|
+
|
70
|
+
include Callbacks
|
71
|
+
include Identity
|
72
|
+
include Attributes
|
73
|
+
include Persistence
|
74
|
+
include Indexes
|
75
|
+
include Dirty
|
76
|
+
|
77
|
+
include Validation
|
78
|
+
include Associations
|
79
|
+
|
80
|
+
attr_reader :attributes
|
81
|
+
attr_accessor :key
|
82
|
+
|
83
|
+
include Serialization
|
84
|
+
include Migrations
|
85
|
+
include Mocking
|
86
|
+
|
87
|
+
def initialize(attributes={})
|
88
|
+
@key = attributes.delete(:key)
|
89
|
+
@new_record = true
|
90
|
+
@attributes = {}.with_indifferent_access
|
91
|
+
self.attributes = attributes
|
92
|
+
@schema_version = self.class.current_schema_version
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
require 'cassandra_object/type_registration'
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
class Cursor
|
3
|
+
def initialize(target_class, column_family, key, super_column, options={})
|
4
|
+
@target_class = target_class
|
5
|
+
@column_family = column_family
|
6
|
+
@key = key.to_s
|
7
|
+
@super_column = super_column
|
8
|
+
@options = options
|
9
|
+
@validators = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def find(number_to_find)
|
13
|
+
limit = number_to_find
|
14
|
+
objects = CassandraObject::Collection.new
|
15
|
+
out_of_keys = false
|
16
|
+
|
17
|
+
if start_with = @options[:start_after]
|
18
|
+
limit += 1
|
19
|
+
else
|
20
|
+
start_with = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
while objects.size < number_to_find && !out_of_keys
|
24
|
+
index_results = connection.get(@column_family, @key, @super_column, :count=>limit,
|
25
|
+
:start=>start_with,
|
26
|
+
:reversed=>@options[:reversed])
|
27
|
+
|
28
|
+
out_of_keys = index_results.size < limit
|
29
|
+
|
30
|
+
if !start_with.blank?
|
31
|
+
index_results.delete(start_with)
|
32
|
+
end
|
33
|
+
|
34
|
+
keys = index_results.keys
|
35
|
+
values = index_results.values
|
36
|
+
|
37
|
+
missing_keys = []
|
38
|
+
|
39
|
+
results = values.empty? ? {} : @target_class.multi_get(values)
|
40
|
+
results.each do |(key, result)|
|
41
|
+
if result.nil?
|
42
|
+
missing_keys << key
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
unless missing_keys.empty?
|
47
|
+
@target_class.multi_get(missing_keys, :quorum=>true).each do |(key, result)|
|
48
|
+
index_key = index_results.index(key)
|
49
|
+
if result.nil?
|
50
|
+
remove(index_key)
|
51
|
+
results.delete(key)
|
52
|
+
else
|
53
|
+
results[key] = result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
results.values.each do |o|
|
59
|
+
if @validators.all? {|v| v.call(o) }
|
60
|
+
objects << o
|
61
|
+
else
|
62
|
+
remove(index_results.index(o.key))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
start_with = objects.last_column_name = keys.last
|
67
|
+
limit = (number_to_find - results.size) + 1
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
return objects
|
72
|
+
end
|
73
|
+
|
74
|
+
def connection
|
75
|
+
@target_class.connection
|
76
|
+
end
|
77
|
+
|
78
|
+
def remove(index_key)
|
79
|
+
connection.remove(@column_family, @key, @super_column, index_key)
|
80
|
+
end
|
81
|
+
|
82
|
+
def validator(&validator)
|
83
|
+
@validators << validator
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module CassandraObject
|
2
|
+
module Dirty
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include ActiveModel::Dirty
|
5
|
+
|
6
|
+
module InstanceMethods
|
7
|
+
def attributes_changed!(attributes)
|
8
|
+
attributes.each do |attr_name|
|
9
|
+
attribute_will_change!(attr_name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def save
|
14
|
+
super.tap { changed_attributes.clear }
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_attribute(name, value)
|
18
|
+
name = name.to_s
|
19
|
+
unless attribute_changed?(name)
|
20
|
+
old = read_attribute(name)
|
21
|
+
changed_attributes[name] = old if old != value
|
22
|
+
end
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'cassandra_object/identity/abstract_key_factory'
|
2
|
+
require 'cassandra_object/identity/key'
|
3
|
+
require 'cassandra_object/identity/uuid_key_factory'
|
4
|
+
require 'cassandra_object/identity/natural_key_factory'
|
5
|
+
|
6
|
+
module CassandraObject
|
7
|
+
# Some docs will be needed here but the gist of this is simple. Instead of returning a string, Base#key now returns a key object.
|
8
|
+
# There are corresponding key factories which generate them
|
9
|
+
module Identity
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
module ClassMethods
|
12
|
+
# Indicate what kind of key the model will have: uuid or natural
|
13
|
+
#
|
14
|
+
# @param [:uuid, :natural] the type of key
|
15
|
+
# @param the options you want to pass along to the key factory (like :attributes => :name, for a natural key).
|
16
|
+
#
|
17
|
+
def key(name_or_factory = :uuid, *options)
|
18
|
+
@key_factory = case name_or_factory
|
19
|
+
when :uuid
|
20
|
+
UUIDKeyFactory.new
|
21
|
+
when :natural
|
22
|
+
NaturalKeyFactory.new(*options)
|
23
|
+
else
|
24
|
+
name_or_factory
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def next_key(object = nil)
|
29
|
+
@key_factory.next_key(object).tap do |key|
|
30
|
+
raise "Keys may not be nil" if key.nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_key(string)
|
35
|
+
@key_factory.parse(string)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module InstanceMethods
|
40
|
+
|
41
|
+
def ==(comparison_object)
|
42
|
+
comparison_object.equal?(self) ||
|
43
|
+
(comparison_object.instance_of?(self.class) &&
|
44
|
+
comparison_object.key == key &&
|
45
|
+
!comparison_object.new_record?)
|
46
|
+
end
|
47
|
+
|
48
|
+
def eql?(comparison_object)
|
49
|
+
self == (comparison_object)
|
50
|
+
end
|
51
|
+
|
52
|
+
def hash
|
53
|
+
key.to_s.hash
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_param
|
57
|
+
key.to_param
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|