gotime-cassandra_object 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/CHANGELOG +3 -0
  2. data/Gemfile +14 -0
  3. data/Gemfile.lock +42 -0
  4. data/LICENSE +13 -0
  5. data/README.markdown +79 -0
  6. data/Rakefile +74 -0
  7. data/TODO +2 -0
  8. data/VERSION +1 -0
  9. data/gotime-cassandra_object.gemspec +134 -0
  10. data/lib/cassandra_object.rb +13 -0
  11. data/lib/cassandra_object/associations.rb +35 -0
  12. data/lib/cassandra_object/associations/one_to_many.rb +136 -0
  13. data/lib/cassandra_object/associations/one_to_one.rb +77 -0
  14. data/lib/cassandra_object/attributes.rb +93 -0
  15. data/lib/cassandra_object/base.rb +97 -0
  16. data/lib/cassandra_object/callbacks.rb +10 -0
  17. data/lib/cassandra_object/collection.rb +8 -0
  18. data/lib/cassandra_object/cursor.rb +86 -0
  19. data/lib/cassandra_object/dirty.rb +27 -0
  20. data/lib/cassandra_object/identity.rb +61 -0
  21. data/lib/cassandra_object/identity/abstract_key_factory.rb +36 -0
  22. data/lib/cassandra_object/identity/key.rb +20 -0
  23. data/lib/cassandra_object/identity/natural_key_factory.rb +51 -0
  24. data/lib/cassandra_object/identity/uuid_key_factory.rb +37 -0
  25. data/lib/cassandra_object/indexes.rb +129 -0
  26. data/lib/cassandra_object/log_subscriber.rb +17 -0
  27. data/lib/cassandra_object/migrations.rb +72 -0
  28. data/lib/cassandra_object/mocking.rb +15 -0
  29. data/lib/cassandra_object/persistence.rb +195 -0
  30. data/lib/cassandra_object/serialization.rb +6 -0
  31. data/lib/cassandra_object/type_registration.rb +7 -0
  32. data/lib/cassandra_object/types.rb +128 -0
  33. data/lib/cassandra_object/validation.rb +49 -0
  34. data/test/basic_scenarios_test.rb +243 -0
  35. data/test/callbacks_test.rb +19 -0
  36. data/test/config/cassandra.in.sh +53 -0
  37. data/test/config/log4j.properties +38 -0
  38. data/test/config/storage-conf.xml +221 -0
  39. data/test/connection.rb +25 -0
  40. data/test/cursor_test.rb +66 -0
  41. data/test/dirty_test.rb +34 -0
  42. data/test/fixture_models.rb +90 -0
  43. data/test/identity/natural_key_factory_test.rb +94 -0
  44. data/test/index_test.rb +69 -0
  45. data/test/legacy/test_helper.rb +18 -0
  46. data/test/migration_test.rb +21 -0
  47. data/test/one_to_many_associations_test.rb +163 -0
  48. data/test/test_case.rb +28 -0
  49. data/test/test_helper.rb +16 -0
  50. data/test/time_test.rb +32 -0
  51. data/test/types_test.rb +252 -0
  52. data/test/validation_test.rb +25 -0
  53. data/test/z_mock_test.rb +36 -0
  54. metadata +243 -0
@@ -0,0 +1,36 @@
1
+ module CassandraObject
2
+ module Identity
3
+ # Key factories need to support 3 operations
4
+ class AbstractKeyFactory
5
+ # Next key takes an object and returns the key object it should use.
6
+ # object will be ignored with synthetic keys but could be useful with natural ones
7
+ #
8
+ # @param [CassandraObject::Base] the object that needs a new key
9
+ # @return [CassandraObject::Identity::Key] the key
10
+ #
11
+ def next_key(object)
12
+ raise NotImplementedError, "#{self.class.name}#next_key isn't implemented."
13
+ end
14
+
15
+ # Parse should create a new key object from the 'to_param' format
16
+ #
17
+ # @param [String] the result of calling key.to_param
18
+ # @return [CassandraObject::Identity::Key] the parsed key
19
+ #
20
+ def parse(string)
21
+ raise NotImplementedError, "#{self.class.name}#parse isn't implemented."
22
+ end
23
+
24
+
25
+ # create should create a new key object from the cassandra format.
26
+ #
27
+ # @param [String] the result of calling key.to_s
28
+ # @return [CassandraObject::Identity::Key] the key
29
+ #
30
+ def create(string)
31
+ raise NotImplementedError, "#{self.class.name}#create isn't implemented."
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,20 @@
1
+ module CassandraObject
2
+ module Identity
3
+ # An "interface" that keys need to implement
4
+ #
5
+ # You don't have to include this. But, there's no reason I can think of not to.
6
+ #
7
+ module Key
8
+ # to_param should return a nice-readable representation of the key suitable to chuck into URLs
9
+ #
10
+ # @return [String] a nice readable representation of the key suitable for URLs
11
+ def to_param; end
12
+
13
+ # to_s should return the bytes which will be written to cassandra both as keys and values for associations.
14
+ #
15
+ # @return [String] the bytes which will be written to cassandra as keys
16
+ def to_s; end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,51 @@
1
+ module CassandraObject
2
+ module Identity
3
+ class NaturalKeyFactory < AbstractKeyFactory
4
+ class NaturalKey
5
+ include Key
6
+
7
+ attr_reader :value
8
+
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ def to_s
14
+ value
15
+ end
16
+
17
+ def to_param
18
+ value
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(NaturalKey) && other.value == value
23
+ end
24
+
25
+ def eql?(other)
26
+ other == self
27
+ end
28
+ end
29
+
30
+ attr_reader :attributes, :separator
31
+
32
+ def initialize(options)
33
+ @attributes = [*options[:attributes]]
34
+ @separator = options[:separator] || "-"
35
+ end
36
+
37
+ def next_key(object)
38
+ NaturalKey.new(attributes.map { |a| object.attributes[a.to_s] }.join(separator))
39
+ end
40
+
41
+ def parse(paramized_key)
42
+ NaturalKey.new(paramized_key)
43
+ end
44
+
45
+ def create(paramized_key)
46
+ NaturalKey.new(paramized_key)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,37 @@
1
+ module CassandraObject
2
+ module Identity
3
+ # Key factories need to support 3 operations
4
+ class UUIDKeyFactory < AbstractKeyFactory
5
+ class UUID < SimpleUUID::UUID
6
+ include Key
7
+
8
+ def to_param
9
+ to_guid
10
+ end
11
+
12
+ def to_s
13
+ # FIXME - this should probably write the raw bytes
14
+ # but it's very hard to debug without this for now.
15
+ to_guid
16
+ end
17
+ end
18
+
19
+ # Next key takes an object and returns the key object it should use.
20
+ # object will be ignored with synthetic keys but could be useful with natural ones
21
+ def next_key(object)
22
+ UUID.new
23
+ end
24
+
25
+ # Parse should create a new key object from the 'to_param' format
26
+ def parse(string)
27
+ UUID.new(string)
28
+ end
29
+
30
+ # create should create a new key object from the cassandra format.
31
+ def create(string)
32
+ UUID.new(string)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,129 @@
1
+ module CassandraObject
2
+ module Indexes
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_inheritable_accessor :indexes
7
+ end
8
+
9
+ class UniqueIndex
10
+ def initialize(attribute_name, model_class, options)
11
+ @attribute_name = attribute_name
12
+ @model_class = model_class
13
+ end
14
+
15
+ def find(attribute_value)
16
+ # first find the key value
17
+ key = @model_class.connection.get(column_family, attribute_value.to_s, 'key')
18
+ # then pass to get
19
+ if key
20
+ @model_class.get(key.to_s)
21
+ else
22
+ @model_class.connection.remove(column_family, attribute_value.to_s)
23
+ nil
24
+ end
25
+ end
26
+
27
+ def write(record)
28
+ @model_class.connection.insert(column_family, record.send(@attribute_name).to_s, {'key'=>record.key.to_s})
29
+ end
30
+
31
+ def remove(record)
32
+ @model_class.connection.remove(column_family, record.send(@attribute_name).to_s)
33
+ end
34
+
35
+ def column_family
36
+ @model_class.column_family + "By" + @attribute_name.to_s.camelize
37
+ end
38
+
39
+ def column_family_configuration
40
+ {:Name=>column_family, :CompareWith=>"UTF8Type"}
41
+ end
42
+ end
43
+
44
+ class Index
45
+ def initialize(attribute_name, model_class, options)
46
+ @attribute_name = attribute_name
47
+ @model_class = model_class
48
+ @reversed = options[:reversed]
49
+ end
50
+
51
+ def find(attribute_value, options = {})
52
+ cursor = CassandraObject::Cursor.new(@model_class, column_family, attribute_value.to_s, @attribute_name.to_s, :start_after=>options[:start_after], :reversed=>@reversed)
53
+ cursor.validator do |object|
54
+ object.send(@attribute_name) == attribute_value
55
+ end
56
+ cursor.find(options[:limit] || 100)
57
+ end
58
+
59
+ def write(record)
60
+ @model_class.connection.insert(column_family, record.send(@attribute_name).to_s, {@attribute_name.to_s=>{new_key=>record.key.to_s}})
61
+ end
62
+
63
+ def remove(record)
64
+ end
65
+
66
+ def column_family
67
+ @model_class.column_family + "By" + @attribute_name.to_s.camelize
68
+ end
69
+
70
+ def new_key
71
+ SimpleUUID::UUID.new
72
+ end
73
+
74
+ def column_family_configuration
75
+ {:Name=>column_family, :CompareWith=>"UTF8Type", :ColumnType=>"Super", :CompareSubcolumnsWith=>"TimeUUIDType"}
76
+ end
77
+
78
+ end
79
+
80
+ module ClassMethods
81
+ def column_family_configuration
82
+ if indexes
83
+ super + indexes.values.map(&:column_family_configuration)
84
+ else
85
+ super
86
+ end
87
+ end
88
+
89
+ def index(attribute_name, options = {})
90
+ self.indexes ||= {}.with_indifferent_access
91
+ if options.delete(:unique)
92
+ self.indexes[attribute_name] = UniqueIndex.new(attribute_name, self, options)
93
+ class_eval <<-eom
94
+ def self.find_by_#{attribute_name}(value)
95
+ indexes[:#{attribute_name}].find(value)
96
+ end
97
+
98
+ after_save do |record|
99
+ self.indexes[:#{attribute_name}].write(record)
100
+ true
101
+ end
102
+
103
+ after_destroy do |record|
104
+ record.class.indexes[:#{attribute_name}].remove(record)
105
+ true
106
+ end
107
+ eom
108
+ else
109
+ self.indexes[attribute_name] = Index.new(attribute_name, self, options)
110
+ class_eval <<-eom
111
+ def self.find_all_by_#{attribute_name}(value, options = {})
112
+ self.indexes[:#{attribute_name}].find(value, options)
113
+ end
114
+
115
+ after_save do |record|
116
+ record.class.indexes[:#{attribute_name}].write(record)
117
+ true
118
+ end
119
+
120
+ after_destroy do |record|
121
+ record.class.indexes[:#{attribute_name}].remove(record)
122
+ true
123
+ end
124
+ eom
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,17 @@
1
+ module CassandraObject
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def multi_get(event)
4
+ name = 'CassandraObject multi_get (%.1fms)'
5
+ keys = event.payload[:keys].join(" ")
6
+
7
+ debug " #{name} (#{keys.size}) #{keys}"
8
+ end
9
+
10
+ def remove(event)
11
+ end
12
+
13
+ def insert(event)
14
+ end
15
+ end
16
+ end
17
+ CassandraObject::LogSubscriber.attach_to :cassandra_object
@@ -0,0 +1,72 @@
1
+ module CassandraObject
2
+ module Migrations
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ class_inheritable_array :migrations
6
+ class_inheritable_accessor :current_schema_version
7
+ self.current_schema_version = 0
8
+ end
9
+
10
+ class Migration
11
+ attr_reader :version
12
+ def initialize(version, block)
13
+ @version = version
14
+ @block = block
15
+ end
16
+
17
+ def run(attrs)
18
+ @block.call(attrs)
19
+ end
20
+ end
21
+
22
+ class MigrationNotFoundError < StandardError
23
+ def initialize(record_version, migrations)
24
+ super("Cannot migrate a record from #{record_version.inspect}. Migrations exist for #{migrations.map(&:version)}")
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ def schema_version
30
+ Integer(@schema_version || self.class.current_schema_version)
31
+ end
32
+ end
33
+
34
+ module ClassMethods
35
+ def migrate(version, &blk)
36
+ write_inheritable_array(:migrations, [Migration.new(version, blk)])
37
+
38
+ if version > self.current_schema_version
39
+ self.current_schema_version = version
40
+ end
41
+ end
42
+
43
+ def instantiate(key, attributes)
44
+ version = attributes.delete('schema_version')
45
+ original_attributes = attributes.dup
46
+ if version == current_schema_version
47
+ return super(key, attributes)
48
+ end
49
+
50
+ versions_to_migrate = ((version.to_i + 1)..current_schema_version)
51
+
52
+ migrations_to_run = versions_to_migrate.map do |v|
53
+ migrations.find {|m| m.version == v}
54
+ end
55
+
56
+ if migrations_to_run.any?(&:nil?)
57
+ raise MigrationNotFoundError.new(version, migrations)
58
+ end
59
+
60
+ migrations_to_run.inject(attributes) do |attrs, migration|
61
+ migration.run(attrs)
62
+ @schema_version = migration.version.to_s
63
+ attrs
64
+ end
65
+
66
+ super(key, attributes).tap do |record|
67
+ record.attributes_changed!(original_attributes.diff(attributes).keys)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,15 @@
1
+ require 'cassandra/mock'
2
+ module CassandraObject
3
+ module Mocking
4
+ extend ActiveSupport::Concern
5
+ module ClassMethods
6
+ def use_mock!(really=true)
7
+ if really
8
+ self.connection_class = Cassandra::Mock
9
+ else
10
+ self.connection_class = Cassandra
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,195 @@
1
+ module CassandraObject
2
+ module Persistence
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ class_inheritable_writer :write_consistency
6
+ class_inheritable_writer :read_consistency
7
+ end
8
+
9
+ VALID_READ_CONSISTENCY_LEVELS = [:one, :quorum, :all]
10
+ VALID_WRITE_CONSISTENCY_LEVELS = VALID_READ_CONSISTENCY_LEVELS + [:zero]
11
+
12
+ module ClassMethods
13
+ def consistency_levels(levels)
14
+ if levels.has_key?(:write)
15
+ unless valid_write_consistency_level?(levels[:write])
16
+ raise ArgumentError, "Invalid write consistency level. Valid levels are: #{VALID_WRITE_CONSISTENCY_LEVELS.inspect}. You gave me #{levels[:write].inspect}"
17
+ end
18
+ self.write_consistency = levels[:write]
19
+ end
20
+
21
+ if levels.has_key?(:read)
22
+ unless valid_read_consistency_level?(levels[:read])
23
+ raise ArgumentError, "Invalid read consistency level. Valid levels are #{VALID_READ_CONSISTENCY_LEVELS.inspect}. You gave me #{levels[:write].inspect}"
24
+ end
25
+ self.read_consistency = levels[:read]
26
+ end
27
+ end
28
+
29
+ def write_consistency
30
+ read_inheritable_attribute(:write_consistency) || :quorum
31
+ end
32
+
33
+ def read_consistency
34
+ read_inheritable_attribute(:read_consistency) || :quorum
35
+ end
36
+
37
+ def get(key, options = {})
38
+ multi_get([key], options).values.first
39
+ end
40
+
41
+ def multi_get(keys, options = {})
42
+ options = {:consistency => self.read_consistency, :limit => 100}.merge(options)
43
+ unless valid_read_consistency_level?(options[:consistency])
44
+ raise ArgumentError, "Invalid read consistency level: '#{options[:consistency]}'. Valid options are [:quorum, :one]"
45
+ end
46
+
47
+ attribute_results = ActiveSupport::Notifications.instrument("multi_get.cassandra_object", :keys => keys) do
48
+ connection.multi_get(column_family, keys.map(&:to_s), :count=>options[:limit], :consistency=>consistency_for_thrift(options[:consistency]))
49
+ end
50
+
51
+ attribute_results.inject(ActiveSupport::OrderedHash.new) do |memo, (key, attributes)|
52
+ if attributes.empty?
53
+ memo[key] = nil
54
+ else
55
+ memo[parse_key(key)] = instantiate(key, attributes)
56
+ end
57
+ memo
58
+ end
59
+ end
60
+
61
+ def remove(key)
62
+ connection.remove(column_family, key.to_s, :consistency => write_consistency_for_thrift)
63
+ end
64
+
65
+ def all(keyrange = ''..'', options = {})
66
+ results = connection.get_range(column_family, :start => keyrange.first, :finish => keyrange.last, :count=>(options[:limit] || 100))
67
+ keys = results.map(&:key)
68
+ keys.map {|key| get(key) }
69
+ end
70
+
71
+ def first(keyrange = ''..'', options = {})
72
+ all(keyrange, options.merge(:limit=>1)).first
73
+ end
74
+
75
+ def create(attributes)
76
+ new(attributes).tap do |object|
77
+ object.save
78
+ end
79
+ end
80
+
81
+ def write(key, attributes, schema_version)
82
+ key.tap do |key|
83
+ connection.insert(column_family, key.to_s, encode_columns_hash(attributes, schema_version), :consistency => write_consistency_for_thrift)
84
+ end
85
+ end
86
+
87
+ def instantiate(key, attributes)
88
+ # remove any attributes we don't know about. we would do this earlier, but we want to make such
89
+ # attributes available to migrations
90
+ attributes.delete_if{|k,_| !model_attributes.keys.include?(k)}
91
+ allocate.tap do |object|
92
+ object.instance_variable_set("@schema_version", attributes.delete('schema_version'))
93
+ object.instance_variable_set("@key", parse_key(key))
94
+ object.instance_variable_set("@attributes", decode_columns_hash(attributes).with_indifferent_access)
95
+ end
96
+ end
97
+
98
+ def encode_columns_hash(attributes, schema_version)
99
+ attributes.inject(Hash.new) do |memo, (column_name, value)|
100
+ memo[column_name.to_s] = model_attributes[column_name].converter.encode(value)
101
+ memo
102
+ end.merge({"schema_version" => schema_version.to_s})
103
+ end
104
+
105
+ def decode_columns_hash(attributes)
106
+ attributes.inject(Hash.new) do |memo, (column_name, value)|
107
+ memo[column_name.to_s] = model_attributes[column_name].converter.decode(value)
108
+ memo
109
+ end
110
+ end
111
+
112
+ def column_family_configuration
113
+ [{:Name=>column_family, :CompareWith=>"UTF8Type"}]
114
+ end
115
+
116
+ protected
117
+ def valid_read_consistency_level?(level)
118
+ !!VALID_READ_CONSISTENCY_LEVELS.include?(level)
119
+ end
120
+
121
+ def valid_write_consistency_level?(level)
122
+ !!VALID_WRITE_CONSISTENCY_LEVELS.include?(level)
123
+ end
124
+
125
+ def write_consistency_for_thrift
126
+ consistency_for_thrift(write_consistency)
127
+ end
128
+
129
+ def read_consistency_for_thrift
130
+ consistency_for_thrift(read_consistency)
131
+ end
132
+
133
+ def consistency_for_thrift(consistency)
134
+ {
135
+ :zero => Cassandra::Consistency::ZERO,
136
+ :one => Cassandra::Consistency::ONE,
137
+ :quorum => Cassandra::Consistency::QUORUM,
138
+ :all => Cassandra::Consistency::ALL
139
+ }[consistency]
140
+ end
141
+ end
142
+
143
+ module InstanceMethods
144
+ def save
145
+ run_callbacks :save do
146
+ create_or_update
147
+ end
148
+ end
149
+
150
+ def create_or_update
151
+ if new_record?
152
+ create
153
+ else
154
+ update
155
+ end
156
+ true
157
+ end
158
+
159
+ def create
160
+ run_callbacks :create do
161
+ @key ||= self.class.next_key(self)
162
+ _write
163
+ @new_record = false
164
+ true
165
+ end
166
+ end
167
+
168
+ def update
169
+ run_callbacks :update do
170
+ _write
171
+ end
172
+ end
173
+
174
+ def _write
175
+ changed_attributes = changed.inject({}) { |h, n| h[n] = read_attribute(n); h }
176
+ self.class.write(key, changed_attributes, schema_version)
177
+ end
178
+
179
+ def new_record?
180
+ @new_record || false
181
+ end
182
+
183
+ def destroy
184
+ run_callbacks :destroy do
185
+ self.class.remove(key)
186
+ end
187
+ end
188
+
189
+ def reload
190
+ self.class.get(self.key)
191
+ end
192
+
193
+ end
194
+ end
195
+ end