gotime-cassandra_object 0.6.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.
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