corm 0.0.22 → 0.0.23

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.
@@ -0,0 +1,54 @@
1
+ # encoding: utf-8
2
+
3
+ module Corm
4
+ module Enhancements
5
+
6
+ def enhancements_logger(logger = nil)
7
+ Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } unless logger
8
+ end
9
+
10
+ RESCUED_CASSANDRA_EXCEPTIONS = [
11
+ ::Cassandra::Errors::ExecutionError,
12
+ ::Cassandra::Errors::IOError,
13
+ ::Cassandra::Errors::InternalError,
14
+ ::Cassandra::Errors::NoHostsAvailable,
15
+ ::Cassandra::Errors::ServerError,
16
+ ::Cassandra::Errors::TimeoutError
17
+ ]
18
+
19
+ # Trying to rescue from a Cassandra::Error
20
+ #
21
+ # The relevant documentation is here (version 2.1.3):
22
+ # https://datastax.github.io/ruby-driver/api/error/
23
+ #
24
+ # Saving from:
25
+ #
26
+ # - ::Cassandra::Errors::ExecutionError
27
+ # - ::Cassandra::Errors::IOError
28
+ # - ::Cassandra::Errors::InternalError
29
+ # - ::Cassandra::Errors::NoHostsAvailable
30
+ # - ::Cassandra::Errors::ServerError
31
+ # - ::Cassandra::Errors::TimeoutError
32
+ #
33
+ # Ignoring:
34
+ # - Errors::ClientError
35
+ # - Errors::DecodingError
36
+ # - Errors::EncodingError
37
+ # - Errors::ValidationError
38
+ #
39
+ # A possible and maybe-good refactoring could be refine for the
40
+ # network related issues.
41
+ def attempts_wrapper(attempts = 3, &block)
42
+ (1..attempts).each do |i|
43
+ begin
44
+ return block.call() if block_given?
45
+ rescue *RESCUED_CASSANDRA_EXCEPTIONS => e
46
+ sleep_for = i * Random.rand(1.5..2.5)
47
+ enhancements_logger.error { "(#{i}/#{attempts} attempts) Bad fail! Retry in #{sleep_for} seconds to recover #{e.class.name}: #{e.message}" }
48
+ sleep(sleep_for)
49
+ end
50
+ end
51
+ nil
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ module Corm
4
+ class TooManyKeysError < StandardError; end
5
+ class UnknownKey < StandardError; end
6
+ class MissingPartitionKey < StandardError; end
7
+ class MissingClusteringKey < StandardError; end
8
+ class UnknownClusteringKey < StandardError; end
9
+ end
@@ -1,25 +1,43 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'cassandra'
4
+ require 'corm/enhancements'
5
+ require 'corm/exceptions'
6
+ require 'corm/validations'
4
7
  require 'multi_json'
5
8
  require 'set'
9
+ require 'digest/md5'
6
10
 
7
11
  module Corm
8
12
  class Model
9
13
  include Enumerable
14
+ extend Enhancements
15
+ extend Validations
10
16
 
11
17
  @@cluster = nil
12
18
 
19
+ # Since the `Cassandra.cluster` method wants to connect, the configure will
20
+ # retry a couple of times (implemented in the Enhancements module) before
21
+ # give up...
22
+ # If it fails `@@cluster` was nil and nil remains.
13
23
  def self.configure(opts = {})
14
- @@cluster = Cassandra.cluster(opts)
24
+ attempts_wrapper do
25
+ @@cluster = Cassandra.cluster(opts)
26
+ end
15
27
  end
16
28
 
17
29
  def self.cluster
18
30
  @@cluster
19
31
  end
20
32
 
33
+ # Please, note the wrapper around the `session execute`.
34
+ # This wrapper (implemented in the Enhancements module) will recover some
35
+ # Cassandra::Error(s) retrying a couple of times.
36
+ #
37
+ # Note also that the <instance>#execute is just calling this Class#execute,
38
+ # as well as count, find, get, drop, etc...
21
39
  def self.execute(*args)
22
- session.execute(*args)
40
+ attempts_wrapper { session.execute(*args) }
23
41
  end
24
42
 
25
43
  def self.field(name, type, pkey = false)
@@ -139,20 +157,74 @@ module Corm
139
157
  end
140
158
 
141
159
  def self.drop!
142
- execute("DROP TABLE #{[keyspace, table].compact.join '.'};")
160
+ execute("DROP TABLE IF EXISTS #{[keyspace, table].compact.join('.')};")
143
161
  end
144
162
 
145
- def self.get(relations)
146
- if statements['get'].nil?
147
- fields = primary_key.flatten.map { |key| "#{key} = ?" }.join ' AND '
148
- statement = "SELECT * FROM #{keyspace}.#{table} WHERE #{fields} LIMIT 1;"
149
- statements['get'] = session.prepare statement
163
+ ##
164
+ # Find by keys.
165
+ # This `find` methods wants to be as flexible as possible.
166
+ #
167
+ # Unless a block is given, it returns an `Enumerator`, otherwise it yields
168
+ # to the block an instance of the found Cassandra entries.
169
+ #
170
+ # If no keys is passed as parameter, the methods returns (an Enumerator for)
171
+ # all the results in the table.
172
+ #
173
+ # The options hash support the ':limit' option to append at the statement;
174
+ # the default is no limit.
175
+ #
176
+ # The 'key_values' parameter is an Hash where the keys are the "column
177
+ # names" and the values... are the values.
178
+ #
179
+ # If the keys passed as parameter are more than the defined by the table the
180
+ # query is not valid, it cannot be executed and an error is raised.
181
+ # Other exceptions are raised when the keys doesn't include all the
182
+ # (required) partition_keys or the clustering keys doesn't are in the
183
+ # defined order.
184
+ def self.find(key_values = {}, query_options = {}, &block)
185
+
186
+ raise ArgumentError unless key_values.is_a?(Hash)
187
+ raise ArgumentError unless query_options.is_a?(Hash)
188
+
189
+ unless key_values.empty?
190
+ raise TooManyKeysError if there_are_too_many_keys_requested?(key_values)
191
+ raise MissingPartitionKey if a_partition_key_is_missing?(key_values)
192
+ raise UnknownClusteringKey if an_unknown_clustering_key_is_requested?(key_values)
193
+ # raise UnknownKey if an_unknown_key_is_requested?(key_values)
194
+ raise MissingClusteringKey if a_clustering_key_is_missing?(key_values)
150
195
  end
151
- values = primary_key.flatten.map do |key|
152
- relations[key.to_s] || relations[key.to_sym]
196
+
197
+ return to_enum(:find, key_values, query_options) unless block_given?
198
+
199
+ statement_find_key = Array(query_options.fetch(:statement_key, 'find')).flatten
200
+ field_names = []
201
+
202
+ key_values.each do |key, value|
203
+
204
+ statement_find_key << key.to_s
205
+ field_names << "#{key} = ?"
153
206
  end
154
- cassandra_record_ = execute(statements['get'], arguments: values).first
155
- cassandra_record_ ? new(_cassandra_record: cassandra_record_) : nil
207
+
208
+ statement_find_key = statement_find_key.join('_')
209
+ statement_find_key.concat("_limit#{query_options[:limit]}") if query_options[:limit]
210
+
211
+ if statements[statement_find_key].nil?
212
+ statement = self.the_select_statement_for(key_values, field_names, query_options[:limit])
213
+ statements[statement_find_key] = session.prepare(statement)
214
+ end
215
+
216
+ execute(statements[statement_find_key], arguments: key_values).each do |cassandra_record_|
217
+ block.call(new(_cassandra_record: cassandra_record_))
218
+ end
219
+ end
220
+
221
+ def self.get(relations)
222
+ query_options = {
223
+ limit: 1,
224
+ statement_key: 'get'
225
+ }
226
+ cassandra_record = self.find(relations, query_options).first
227
+ return cassandra_record ? cassandra_record : nil
156
228
  end
157
229
 
158
230
  def self.keyspace(name = nil)
@@ -160,14 +232,20 @@ module Corm
160
232
  class_variable_get(:@@keyspace)
161
233
  end
162
234
 
235
+ # Eventually set and return the session, taken from the connection to
236
+ # the cluster.
237
+ #
238
+ # This operation is wrapped by the retry policy (module Enhancements).
163
239
  def self.keyspace!(opts = {})
164
240
  replication = opts[:replication] ||
165
241
  "{'class': 'SimpleStrategy', 'replication_factor': '1'}"
166
242
  durable_writes = opts[:durable_writes].nil? ? true : opts[:durable_writes]
167
243
  if_not_exists = opts[:if_not_exists] ? 'IF NOT EXISTS' : ''
168
- cluster.connect.execute(
169
- "CREATE KEYSPACE #{if_not_exists} #{keyspace} WITH replication = #{replication} AND durable_writes = #{durable_writes};"
170
- )
244
+ attempts_wrapper do
245
+ cluster.connect.execute(
246
+ "CREATE KEYSPACE #{if_not_exists} #{keyspace} WITH replication = #{replication} AND durable_writes = #{durable_writes};"
247
+ )
248
+ end
171
249
  end
172
250
 
173
251
  def self.primary_key(partition_key = nil, *cols)
@@ -178,6 +256,18 @@ module Corm
178
256
  class_variable_get(:@@primary_key)
179
257
  end
180
258
 
259
+ def self.primary_key_count
260
+ self.primary_key.flatten.count
261
+ end
262
+
263
+ def self.partition_key
264
+ self.primary_key.first.map(&:to_sym)
265
+ end
266
+
267
+ def self.clustering_key
268
+ self.primary_key.count == 2 ? self.primary_key[1].flatten.map(&:to_sym) : []
269
+ end
270
+
181
271
  def self.properties(*args)
182
272
  class_variable_set(
183
273
  :@@properties,
@@ -186,11 +276,19 @@ module Corm
186
276
  class_variable_get(:@@properties)
187
277
  end
188
278
 
279
+ # Eventually set and return the session, taken from the connection to
280
+ # the cluster.
281
+ #
282
+ # This operation is wrapped by the retry policy (module Enhancements).
189
283
  def self.session
190
- class_variable_set(
191
- :@@session,
192
- cluster.connect(keyspace)
193
- ) unless class_variable_defined?(:@@session)
284
+ unless class_variable_defined?(:@@session)
285
+ attempts_wrapper do
286
+ class_variable_set(
287
+ :@@session,
288
+ cluster.connect(keyspace)
289
+ )
290
+ end
291
+ end
194
292
  class_variable_get :@@session
195
293
  end
196
294
 
@@ -295,5 +393,22 @@ module Corm
295
393
  def table
296
394
  self.class.table
297
395
  end
396
+
397
+ ##
398
+ # Create and return the proper query to find the C* entries, given an array
399
+ # of keys.
400
+ #
401
+ # @param key_values An array of key names; can be empty, cannot be, in size, greater than the length of the model keys.
402
+ # @param field_names The "column names" for the `WHERE` clause.
403
+ def self.the_select_statement_for(key_values, field_names, limit = nil)
404
+ limit = "LIMIT #{limit.to_i}" if (limit && limit > 0)
405
+ if key_values.empty?
406
+ return "SELECT * FROM #{keyspace}.#{table} #{limit} ;"
407
+ elsif key_values.count > self.primary_key_count
408
+ raise Corm::TooManyKeysError
409
+ else
410
+ return "SELECT * FROM #{keyspace}.#{table} WHERE #{field_names.join(' AND ')} #{limit} ;"
411
+ end
412
+ end
298
413
  end
299
414
  end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ module Corm
4
+ module Validations
5
+
6
+ def there_are_too_many_keys_requested?(key_values)
7
+ if key_values.keys.count > self.primary_key_count
8
+ return "You defined more find keys than the primary keys of the table!"
9
+ end
10
+ false
11
+ end
12
+
13
+ def an_unknown_key_is_requested?(key_values)
14
+ unless (key_values.keys - self.primary_key.flatten).empty?
15
+ return "You requested a key that it's not in the table primary key!"
16
+ end
17
+ false
18
+ end
19
+
20
+ def an_unknown_clustering_key_is_requested?(key_values)
21
+ unless ((key_values.keys - self.partition_key.flatten) - self.clustering_key).empty?
22
+ return "You requested some unsupported clustering keys!"
23
+ end
24
+ false
25
+ end
26
+
27
+ def a_partition_key_is_missing?(key_values)
28
+ self.partition_key.each do |part_key|
29
+ return "#{part_key} is required as partition key!" unless key_values.keys.include?(part_key.to_sym)
30
+ end
31
+ false
32
+ end
33
+
34
+ def a_clustering_key_is_missing?(key_values)
35
+
36
+ # This exception mimic the following...
37
+ # Class: <Cassandra::Errors::InvalidError>
38
+ # Message: <"PRIMARY KEY column 'still_another_uuid_field' cannot be
39
+ # restricted (preceding column 'another_uuid_field' is either not
40
+ # restricted or by a non-EQ relation)"
41
+ #
42
+ # ... and TBH leaving this check to the Cassandra driver is still an
43
+ # option.
44
+ return false if self.clustering_key.empty?
45
+ return false if no_clustering_key_requested?(key_values)
46
+ self.clustering_key.each do |clust_key|
47
+ return "#{clust_key} is required as clustering key! (Order matters)" unless key_values.include?(clust_key.to_sym)
48
+ end
49
+ false
50
+ end
51
+
52
+ def no_clustering_key_requested?(key_values)
53
+ (key_values.keys - self.partition_key.flatten).empty?
54
+ end
55
+ end
56
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: corm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.22
4
+ version: 0.0.23
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-06-11 00:00:00.000000000 Z
12
+ date: 2015-06-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cassandra-driver
@@ -71,6 +71,22 @@ dependencies:
71
71
  - - ! '>='
72
72
  - !ruby/object:Gem::Version
73
73
  version: 10.0.0
74
+ - !ruby/object:Gem::Dependency
75
+ name: pry
76
+ requirement: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: 0.10.1
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.10.1
74
90
  description: ''
75
91
  email:
76
92
  - stefano@gild.com
@@ -79,7 +95,10 @@ extensions: []
79
95
  extra_rdoc_files: []
80
96
  files:
81
97
  - lib/corm.rb
98
+ - lib/corm/enhancements.rb
99
+ - lib/corm/exceptions.rb
82
100
  - lib/corm/model.rb
101
+ - lib/corm/validations.rb
83
102
  homepage: https://github.com/stefanofontanelli/corm
84
103
  licenses: []
85
104
  post_install_message:
@@ -92,12 +111,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
111
  - - ! '>='
93
112
  - !ruby/object:Gem::Version
94
113
  version: '0'
114
+ segments:
115
+ - 0
116
+ hash: 1645116542226153215
95
117
  required_rubygems_version: !ruby/object:Gem::Requirement
96
118
  none: false
97
119
  requirements:
98
120
  - - ! '>='
99
121
  - !ruby/object:Gem::Version
100
122
  version: '0'
123
+ segments:
124
+ - 0
125
+ hash: 1645116542226153215
101
126
  requirements: []
102
127
  rubyforge_project:
103
128
  rubygems_version: 1.8.23.2