cequel 0.0.0 → 0.4.0

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 (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