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.
- data/lib/corm/enhancements.rb +54 -0
- data/lib/corm/exceptions.rb +9 -0
- data/lib/corm/model.rb +134 -19
- data/lib/corm/validations.rb +56 -0
- metadata +27 -2
@@ -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
|
data/lib/corm/model.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
152
|
-
|
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
|
-
|
155
|
-
|
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
|
-
|
169
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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.
|
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-
|
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
|