cequel 0.0.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/lib/cequel.rb +16 -0
  2. data/lib/cequel/batch.rb +58 -0
  3. data/lib/cequel/cql_row_specification.rb +22 -0
  4. data/lib/cequel/data_set.rb +346 -0
  5. data/lib/cequel/errors.rb +4 -0
  6. data/lib/cequel/keyspace.rb +106 -0
  7. data/lib/cequel/model.rb +95 -0
  8. data/lib/cequel/model/associations.rb +120 -0
  9. data/lib/cequel/model/callbacks.rb +32 -0
  10. data/lib/cequel/model/class_internals.rb +48 -0
  11. data/lib/cequel/model/column.rb +20 -0
  12. data/lib/cequel/model/dictionary.rb +202 -0
  13. data/lib/cequel/model/dirty.rb +53 -0
  14. data/lib/cequel/model/dynamic.rb +31 -0
  15. data/lib/cequel/model/errors.rb +13 -0
  16. data/lib/cequel/model/inheritable.rb +48 -0
  17. data/lib/cequel/model/instance_internals.rb +23 -0
  18. data/lib/cequel/model/local_association.rb +42 -0
  19. data/lib/cequel/model/magic.rb +79 -0
  20. data/lib/cequel/model/mass_assignment_security.rb +21 -0
  21. data/lib/cequel/model/naming.rb +17 -0
  22. data/lib/cequel/model/observer.rb +42 -0
  23. data/lib/cequel/model/persistence.rb +173 -0
  24. data/lib/cequel/model/properties.rb +143 -0
  25. data/lib/cequel/model/railtie.rb +33 -0
  26. data/lib/cequel/model/remote_association.rb +40 -0
  27. data/lib/cequel/model/scope.rb +362 -0
  28. data/lib/cequel/model/scoped.rb +50 -0
  29. data/lib/cequel/model/subclass_internals.rb +45 -0
  30. data/lib/cequel/model/timestamps.rb +52 -0
  31. data/lib/cequel/model/translation.rb +17 -0
  32. data/lib/cequel/model/validations.rb +50 -0
  33. data/lib/cequel/new_relic_instrumentation.rb +22 -0
  34. data/lib/cequel/row_specification.rb +63 -0
  35. data/lib/cequel/statement.rb +23 -0
  36. data/lib/cequel/version.rb +3 -0
  37. data/spec/environment.rb +3 -0
  38. data/spec/examples/data_set_spec.rb +382 -0
  39. data/spec/examples/keyspace_spec.rb +63 -0
  40. data/spec/examples/model/associations_spec.rb +109 -0
  41. data/spec/examples/model/callbacks_spec.rb +79 -0
  42. data/spec/examples/model/dictionary_spec.rb +413 -0
  43. data/spec/examples/model/dirty_spec.rb +39 -0
  44. data/spec/examples/model/dynamic_spec.rb +41 -0
  45. data/spec/examples/model/inheritable_spec.rb +45 -0
  46. data/spec/examples/model/magic_spec.rb +199 -0
  47. data/spec/examples/model/mass_assignment_security_spec.rb +13 -0
  48. data/spec/examples/model/naming_spec.rb +9 -0
  49. data/spec/examples/model/observer_spec.rb +86 -0
  50. data/spec/examples/model/persistence_spec.rb +201 -0
  51. data/spec/examples/model/properties_spec.rb +81 -0
  52. data/spec/examples/model/scope_spec.rb +677 -0
  53. data/spec/examples/model/serialization_spec.rb +20 -0
  54. data/spec/examples/model/spec_helper.rb +12 -0
  55. data/spec/examples/model/timestamps_spec.rb +52 -0
  56. data/spec/examples/model/translation_spec.rb +23 -0
  57. data/spec/examples/model/validations_spec.rb +86 -0
  58. data/spec/examples/spec_helper.rb +9 -0
  59. data/spec/models/asset.rb +21 -0
  60. data/spec/models/asset_observer.rb +5 -0
  61. data/spec/models/blog.rb +14 -0
  62. data/spec/models/blog_posts.rb +6 -0
  63. data/spec/models/category.rb +9 -0
  64. data/spec/models/comment.rb +12 -0
  65. data/spec/models/photo.rb +5 -0
  66. data/spec/models/post.rb +88 -0
  67. data/spec/models/post_comments.rb +14 -0
  68. data/spec/models/post_observer.rb +43 -0
  69. data/spec/support/helpers.rb +26 -0
  70. data/spec/support/result_stub.rb +27 -0
  71. metadata +125 -23
@@ -0,0 +1,106 @@
1
+ module Cequel
2
+
3
+ #
4
+ # Handle to a Cassandra keyspace.
5
+ #
6
+ class Keyspace
7
+
8
+ #
9
+ # Set a logger for logging queries. Queries logged at INFO level
10
+ #
11
+ attr_writer :logger, :slowlog, :slowlog_threshold, :connection
12
+
13
+ #
14
+ # @api private
15
+ # @see Cequel.connect
16
+ #
17
+ def initialize(configuration = {})
18
+ @name = configuration[:keyspace]
19
+ @hosts = configuration[:host] || configuration[:hosts]
20
+ @thrift_options = configuration[:thrift].try(:symbolize_keys)
21
+ end
22
+
23
+ def connection
24
+ @connection ||= CassandraCQL::Database.new(
25
+ @hosts, {:keyspace => @name}, @thrift_options
26
+ )
27
+ end
28
+
29
+ #
30
+ # Get DataSet encapsulating a column family in this keyspace
31
+ #
32
+ # @param column_family_name [Symbol] the name of the column family
33
+ # @return [DataSet] a column family
34
+ #
35
+ def [](column_family_name)
36
+ DataSet.new(column_family_name.to_sym, self)
37
+ end
38
+
39
+ #
40
+ # Execute a CQL query in this keyspace.
41
+ #
42
+ # @param statement [String] CQL string
43
+ # @param *bind_vars [Object] values for bind variables
44
+ #
45
+ def execute(statement, *bind_vars)
46
+ log('CQL', statement, *bind_vars) do
47
+ connection.execute(statement, *bind_vars)
48
+ end
49
+ end
50
+
51
+ #
52
+ # Write data to this keyspace using a CQL query. Will be included the
53
+ # current batch operation if one is present.
54
+ #
55
+ # @param (see #execute)
56
+ #
57
+ def write(statement, *bind_vars)
58
+ if @batch
59
+ @batch.execute(statement, *bind_vars)
60
+ else
61
+ execute(statement, *bind_vars)
62
+ end
63
+ end
64
+
65
+ #
66
+ # Execute write operations in a batch. Any inserts, updates, and deletes
67
+ # inside this method's block will be executed inside a CQL BATCH operation.
68
+ #
69
+ # @param options [Hash]
70
+ # @option options [Fixnum] :auto_apply Automatically send batch to Cassandra after this many statements
71
+ #
72
+ # @example Perform inserts in a batch
73
+ # DB.batch do
74
+ # DB[:posts].insert(:id => 1, :title => 'One')
75
+ # DB[:posts].insert(:id => 2, :title => 'Two')
76
+ # end
77
+ #
78
+ def batch(options = {})
79
+ old_batch, @batch = @batch, Batch.new(self, options)
80
+ yield
81
+ @batch.apply
82
+ ensure
83
+ @batch = old_batch
84
+ end
85
+
86
+ private
87
+
88
+ def log(label, statement, *bind_vars)
89
+ return yield unless @logger || @slowlog
90
+ response = nil
91
+ time = Benchmark.ms { response = yield }
92
+ generate_message = proc do
93
+ sprintf(
94
+ '%s (%dms) %s', label, time.to_i,
95
+ CassandraCQL::Statement.sanitize(statement, bind_vars)
96
+ )
97
+ end
98
+ @logger.debug(&generate_message) if @logger
99
+ threshold = @slowlog_threshold || 2000
100
+ @slowlog.warn(&generate_message) if @slowlog && time >= threshold
101
+ response
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,95 @@
1
+ require 'active_model'
2
+
3
+ require 'cequel'
4
+ require 'cequel/model/associations'
5
+ require 'cequel/model/callbacks'
6
+ require 'cequel/model/class_internals'
7
+ require 'cequel/model/column'
8
+ require 'cequel/model/dictionary'
9
+ require 'cequel/model/dirty'
10
+ require 'cequel/model/dynamic'
11
+ require 'cequel/model/errors'
12
+ require 'cequel/model/inheritable'
13
+ require 'cequel/model/instance_internals'
14
+ require 'cequel/model/local_association'
15
+ require 'cequel/model/mass_assignment_security'
16
+ require 'cequel/model/magic'
17
+ require 'cequel/model/naming'
18
+ require 'cequel/model/observer'
19
+ require 'cequel/model/persistence'
20
+ require 'cequel/model/properties'
21
+ require 'cequel/model/remote_association'
22
+ require 'cequel/model/scope'
23
+ require 'cequel/model/scoped'
24
+ require 'cequel/model/subclass_internals'
25
+ require 'cequel/model/timestamps'
26
+ require 'cequel/model/translation'
27
+ require 'cequel/model/validations'
28
+
29
+ if defined? Rails
30
+ require 'cequel/model/railtie'
31
+ end
32
+
33
+ module Cequel
34
+
35
+ #
36
+ # This module adds Cassandra persistence to a class using Cequel.
37
+ #
38
+ module Model
39
+
40
+ extend ActiveSupport::Concern
41
+ extend ActiveModel::Observing::ClassMethods
42
+
43
+ included do
44
+ @_cequel = ClassInternals.new(self)
45
+
46
+ include Properties
47
+ include Persistence
48
+ include Scoped
49
+ include Naming
50
+ include Callbacks
51
+ include Validations
52
+ include ActiveModel::Observing
53
+ include Dirty
54
+ include MassAssignmentSecurity
55
+ include Associations
56
+ extend Inheritable
57
+ extend Magic
58
+
59
+ include ActiveModel::Serializers::JSON
60
+ include ActiveModel::Serializers::Xml
61
+
62
+ extend Translation
63
+ end
64
+
65
+ def self.keyspace
66
+ @keyspace ||= Cequel.connect(@configuration).tap do |keyspace|
67
+ keyspace.logger = @logger if @logger
68
+ keyspace.slowlog = @slowlog if @slowlog
69
+ keyspace.slowlog_threshold = @slowlog_threshold if @slowlog_threshold
70
+ end
71
+ end
72
+
73
+ def self.configure(configuration)
74
+ @configuration = configuration
75
+ end
76
+
77
+ def self.logger=(logger)
78
+ @logger = logger
79
+ end
80
+
81
+ def self.slowlog=(slowlog)
82
+ @slowlog = slowlog
83
+ end
84
+
85
+ def self.slowlog_threshold=(slowlog_threshold)
86
+ @slowlog_threshold = slowlog_threshold
87
+ end
88
+
89
+ def initialize
90
+ @_cequel = InstanceInternals.new(self)
91
+ end
92
+
93
+ end
94
+
95
+ end
@@ -0,0 +1,120 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Associations
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+
11
+ def belongs_to(name, options = {})
12
+ name = name.to_sym
13
+ association = LocalAssociation.new(name, self, options.symbolize_keys)
14
+ @_cequel.associations[name] = association
15
+ column(association.foreign_key_name, association.primary_key.type)
16
+
17
+ module_eval <<-RUBY, __FILE__, __LINE__+1
18
+ def #{name}
19
+ if @_cequel.associations.key?(#{name.inspect})
20
+ return @_cequel.associations[#{name.inspect}]
21
+ end
22
+ key = __send__(:#{name}_id)
23
+ if key
24
+ @_cequel.associations[#{name.inspect}] =
25
+ self.class.reflect_on_association(#{name.inspect}).
26
+ scope(self).first
27
+ else
28
+ @_cequel.associations[#{name.inspect}] = nil
29
+ end
30
+ end
31
+
32
+ def #{name}=(instance)
33
+ @_cequel.associations[#{name.inspect}] = instance
34
+ if instance.nil?
35
+ key = nil
36
+ else
37
+ key = instance.__send__(instance.class.key_alias)
38
+ end
39
+ write_attribute(#{association.foreign_key_name.inspect}, key)
40
+ end
41
+
42
+ def #{association.foreign_key_name}=(key)
43
+ @_cequel.associations.delete(#{name.inspect})
44
+ write_attribute(#{association.foreign_key_name.inspect}, key)
45
+ end
46
+ RUBY
47
+ end
48
+
49
+ def has_many(name, options = {})
50
+ name = name.to_sym
51
+ @_cequel.associations[name] =
52
+ RemoteAssociation.new(name, self, options.symbolize_keys)
53
+
54
+ module_eval <<-RUBY, __FILE__, __LINE__+1
55
+ def #{name}
56
+ self.class.reflect_on_association(#{name.inspect}).scope(self)
57
+ end
58
+ RUBY
59
+ end
60
+
61
+ def has_one(name, options = {})
62
+ name = name.to_sym
63
+ @_cequel.associations[name] =
64
+ RemoteAssociation.new(name, self, options.symbolize_keys)
65
+
66
+ module_eval <<-RUBY, __FILE__, __LINE__+1
67
+ def #{name}
68
+ self.class.reflect_on_association(#{name.inspect}).scope(self).first
69
+ end
70
+ RUBY
71
+ end
72
+
73
+ def reflect_on_association(name)
74
+ @_cequel.association(name.to_sym)
75
+ end
76
+
77
+ def reflect_on_associations
78
+ @_cequel.associations.values
79
+ end
80
+
81
+ end
82
+
83
+ def save(*args)
84
+ save_transient_associated
85
+ super
86
+ end
87
+
88
+ def destroy(*args)
89
+ destroy_associated
90
+ super
91
+ end
92
+
93
+ private
94
+
95
+ def save_transient_associated
96
+ self.class.reflect_on_associations.each do |association|
97
+ if LocalAssociation === association
98
+ associated = @_cequel.associations[association.name]
99
+ if associated && associated.transient?
100
+ associated.save
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def destroy_associated
107
+ self.class.reflect_on_associations.each do |association|
108
+ if association.dependent == :destroy
109
+ association.scope(self).each do |associated|
110
+ associated.destroy
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end
@@ -0,0 +1,32 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Callbacks
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ HOOKS = [:save, :create, :update, :destroy, :validation]
10
+ CALLBACKS = HOOKS.map { |hook| [:"before_#{hook}", :"after_#{hook}"] }.
11
+ flatten
12
+
13
+ included do
14
+ extend ActiveModel::Callbacks
15
+ define_model_callbacks *HOOKS
16
+ end
17
+
18
+ def save(*args)
19
+ run_callbacks(:save) do
20
+ run_callbacks(persisted? ? :update : :create) { super }
21
+ end
22
+ end
23
+
24
+ def destroy(*args)
25
+ run_callbacks(:destroy) { super }
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,48 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ #
6
+ # @private
7
+ #
8
+ class ClassInternals
9
+
10
+ attr_accessor :key, :current_scope, :default_scope
11
+ attr_reader :columns, :associations, :index_preference
12
+
13
+ def initialize(clazz)
14
+ @clazz = clazz
15
+ @columns, @associations = {}, {}
16
+ @index_preference = []
17
+ @lock = Monitor.new
18
+ end
19
+
20
+ def add_column(name, type, options = {})
21
+ @columns[name] = Column.new(name, type, options)
22
+ end
23
+
24
+ def type_column
25
+ @columns[:class_name]
26
+ end
27
+
28
+ def column_family_name
29
+ @column_family_name ||= @clazz.name.tableize
30
+ end
31
+
32
+ def base_class
33
+ @clazz
34
+ end
35
+
36
+ def association(name)
37
+ associations[name]
38
+ end
39
+
40
+ def synchronize(&block)
41
+ @lock.synchronize(&block)
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,20 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ #
6
+ # Encapsulates information about a column in a model's column family
7
+ #
8
+ class Column
9
+ attr_reader :name, :type, :default
10
+
11
+ def initialize(name, type, options = {})
12
+ @name, @type = name, type
13
+ @default = options[:default]
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,202 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ class Dictionary
6
+
7
+ class <<self
8
+
9
+ attr_writer :column_family, :default_batch_size
10
+
11
+ def key_alias
12
+ @key_alias ||= :KEY
13
+ end
14
+
15
+ def key_type
16
+ @key_type ||= :text
17
+ end
18
+
19
+ def comparator
20
+ @comparator ||= :text
21
+ end
22
+
23
+ def validation
24
+ @validation ||= :text
25
+ end
26
+
27
+ def key(key_alias, type)
28
+ @key_alias, @key_type = key_alias, type
29
+
30
+ module_eval(<<-RUBY)
31
+ def #{key_alias.downcase}
32
+ @key
33
+ end
34
+ RUBY
35
+ end
36
+
37
+ def maps(options)
38
+ @comparator, @validation = *options.first
39
+ end
40
+
41
+ def column_family
42
+ return @column_family if @column_family
43
+ self.column_family_name = name.underscore.to_sym
44
+ @column_family
45
+ end
46
+
47
+ def column_family_name=(column_family_name)
48
+ self.column_family = Cequel::Model.keyspace[column_family_name]
49
+ end
50
+
51
+ def default_batch_size
52
+ @default_batch_size || 1000
53
+ end
54
+
55
+ def [](key)
56
+ new(key)
57
+ end
58
+ private :new
59
+ end
60
+
61
+ include Enumerable
62
+
63
+ def initialize(key)
64
+ @key = key
65
+ setup
66
+ end
67
+
68
+ def []=(column, value)
69
+ if value.nil?
70
+ @deleted_columns << column
71
+ @changed_columns.delete(column)
72
+ else
73
+ @changed_columns << column
74
+ @deleted_columns.delete(column)
75
+ end
76
+ @row[column] = value
77
+ end
78
+
79
+ def [](column)
80
+ if @loaded || @changed_columns.include?(column)
81
+ @row[column]
82
+ elsif !@deleted_columns.include?(column)
83
+ value = scope.select(column).first[column]
84
+ deserialize_value(column, value) if value
85
+ end
86
+ end
87
+
88
+ def keys
89
+ @loaded ? @row.keys : each_pair.map { |key, value| key }
90
+ end
91
+
92
+ def values
93
+ @loaded ? @row.values : each_pair.map { |key, value| value }
94
+ end
95
+
96
+ def slice(*columns)
97
+ if @loaded
98
+ @row.slice(*columns)
99
+ else
100
+ {}.tap do |slice|
101
+ row = scope.select(*columns).first.except(self.class.key_alias)
102
+ row.each { |col, value| slice[col] = deserialize_value(col, value) }
103
+ slice.merge!(@row.slice(*columns))
104
+ @deleted_columns.each { |column| slice.delete(column) }
105
+ end
106
+ end
107
+ end
108
+
109
+ def destroy
110
+ scope.delete
111
+ setup
112
+ end
113
+
114
+ def save
115
+ updates = {}
116
+ @changed_columns.each do |column|
117
+ updates[column] = serialize_value(@row[column])
118
+ end
119
+ scope.update(updates) if updates.any?
120
+ scope.delete(*@deleted_columns.to_a) if @deleted_columns.any?
121
+ @changed_columns.clear
122
+ @deleted_columns.clear
123
+ self
124
+ end
125
+
126
+ def each_pair(options = {}, &block)
127
+ return Enumerator.new(self, :each_pair, options) unless block
128
+ return @row.each_pair(&block) if @loaded
129
+ batch_size = options[:batch_size] || self.class.default_batch_size
130
+ batch_scope = scope.select(:first => batch_size)
131
+ key_alias = self.class.key_alias
132
+ last_key = nil
133
+ new_columns = @changed_columns.dup
134
+ begin
135
+ batch_results = batch_scope.first
136
+ batch_results.delete(key_alias)
137
+ result_length = batch_results.length
138
+ batch_results.delete(last_key) unless last_key.nil?
139
+ batch_results.each_pair do |key, value|
140
+ if @changed_columns.include?(key)
141
+ new_columns.delete(key)
142
+ yield key, @row[key]
143
+ elsif !@deleted_columns.include?(key)
144
+ yield key, deserialize_value(key, value)
145
+ end
146
+ end
147
+ last_key = batch_results.keys.last
148
+ batch_scope = batch_scope.select(:from => last_key)
149
+ end while result_length == batch_size
150
+ new_columns.each do |key|
151
+ yield key, @row[key]
152
+ end
153
+ end
154
+
155
+ def each(&block)
156
+ each_pair(&block)
157
+ end
158
+
159
+ def load
160
+ @row = {}
161
+ each_pair { |column, value| @row[column] = value }
162
+ @loaded = true
163
+ self
164
+ end
165
+
166
+ def loaded?
167
+ !!@loaded
168
+ end
169
+
170
+ private
171
+
172
+ def setup
173
+ @row = {}
174
+ @changed_columns = Set[]
175
+ @deleted_columns = Set[]
176
+ end
177
+
178
+ def scope
179
+ self.class.column_family.where(self.class.key_alias => @key)
180
+ end
181
+
182
+ #
183
+ # Subclasses may override this method to implement custom serialization
184
+ # strategies
185
+ #
186
+ def serialize_value(value)
187
+ value
188
+ end
189
+
190
+ #
191
+ # Subclasses may override this method to implement custom deserialization
192
+ # strategies
193
+ #
194
+ def deserialize_value(column, value)
195
+ value
196
+ end
197
+
198
+ end
199
+
200
+ end
201
+
202
+ end