whi-cassie 1.0.5 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/continuous_integration.yml +63 -0
- data/.gitignore +4 -15
- data/.standard.yml +11 -0
- data/Appraisals +13 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +89 -0
- data/HISTORY.md +49 -0
- data/README.md +30 -11
- data/Rakefile +13 -12
- data/VERSION +1 -1
- data/gemfiles/activemodel_4.gemfile +14 -0
- data/gemfiles/activemodel_5.gemfile +14 -0
- data/gemfiles/activemodel_6.gemfile +14 -0
- data/lib/cassie.rb +84 -62
- data/lib/cassie/config.rb +8 -7
- data/lib/cassie/model.rb +162 -107
- data/lib/cassie/railtie.rb +7 -5
- data/lib/cassie/schema.rb +35 -28
- data/lib/cassie/subscribers.rb +51 -0
- data/lib/cassie/testing.rb +22 -20
- data/lib/whi-cassie.rb +2 -0
- data/spec/cassie/config_spec.rb +20 -22
- data/spec/cassie/model_spec.rb +342 -275
- data/spec/cassie/subscribers_spec.rb +61 -0
- data/spec/cassie_spec.rb +116 -71
- data/spec/models/thing.rb +13 -11
- data/spec/models/type_tester.rb +7 -7
- data/spec/spec_helper.rb +35 -10
- data/whi-cassie.gemspec +16 -19
- metadata +21 -38
- data/HISTORY.txt +0 -25
data/lib/cassie.rb
CHANGED
@@ -1,41 +1,45 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cassandra"
|
2
4
|
|
3
5
|
# This class provides a lightweight wrapper around the Cassandra driver. It provides
|
4
6
|
# a foundation for maintaining a connection and constructing CQL statements.
|
5
7
|
class Cassie
|
6
8
|
require File.expand_path("../cassie/config.rb", __FILE__)
|
9
|
+
require File.expand_path("../cassie/subscribers.rb", __FILE__)
|
7
10
|
require File.expand_path("../cassie/model.rb", __FILE__)
|
8
11
|
require File.expand_path("../cassie/schema.rb", __FILE__)
|
9
12
|
require File.expand_path("../cassie/testing.rb", __FILE__)
|
10
13
|
require File.expand_path("../cassie/railtie.rb", __FILE__) if defined?(Rails)
|
11
|
-
|
14
|
+
|
12
15
|
class RecordNotFound < StandardError
|
13
16
|
end
|
14
|
-
|
17
|
+
|
15
18
|
class RecordInvalid < StandardError
|
16
19
|
attr_reader :record
|
17
|
-
|
20
|
+
|
18
21
|
def initialize(record)
|
19
22
|
super("Errors on #{record.class.name}: #{record.errors.to_hash.inspect}")
|
20
23
|
@record = record
|
21
24
|
end
|
22
25
|
end
|
23
|
-
|
26
|
+
|
24
27
|
# Message passed to subscribers with the statement, options, and time for each statement
|
25
28
|
# to execute. Note that if statements are batched they will be packed into one message
|
26
29
|
# with a Cassandra::Statements::Batch statement and empty options.
|
27
30
|
class Message
|
28
31
|
attr_reader :statement, :options, :elapsed_time
|
29
|
-
|
32
|
+
|
30
33
|
def initialize(statement, options, elapsed_time)
|
31
34
|
@statement = statement
|
32
35
|
@options = options
|
33
36
|
@elapsed_time = elapsed_time
|
34
37
|
end
|
35
38
|
end
|
36
|
-
|
39
|
+
|
37
40
|
attr_reader :config, :subscribers
|
38
|
-
|
41
|
+
attr_accessor :consistency
|
42
|
+
|
39
43
|
class << self
|
40
44
|
# A singleton instance that can be shared to communicate with a Cassandra cluster.
|
41
45
|
def instance
|
@@ -45,7 +49,7 @@ class Cassie
|
|
45
49
|
end
|
46
50
|
@instance
|
47
51
|
end
|
48
|
-
|
52
|
+
|
49
53
|
# Call this method to load the Cassie::Config from the specified file for the
|
50
54
|
# specified environment.
|
51
55
|
def configure!(options)
|
@@ -56,7 +60,7 @@ class Cassie
|
|
56
60
|
end
|
57
61
|
@config = Cassie::Config.new(options)
|
58
62
|
end
|
59
|
-
|
63
|
+
|
60
64
|
# This method can be used to set a consistency level for all Cassandra queries
|
61
65
|
# within a block that don't explicitly define them. It can be used where consistency
|
62
66
|
# is important (i.e. on validation queries) but where a higher level method
|
@@ -70,34 +74,33 @@ class Cassie
|
|
70
74
|
Thread.current[:cassie_consistency] = save_val
|
71
75
|
end
|
72
76
|
end
|
73
|
-
|
77
|
+
|
74
78
|
# Get a Logger compatible object if it has been set.
|
75
79
|
def logger
|
76
80
|
@logger if defined?(@logger)
|
77
81
|
end
|
78
|
-
|
82
|
+
|
79
83
|
# Set a logger with a Logger compatible object.
|
80
|
-
|
81
|
-
@logger = value
|
82
|
-
end
|
84
|
+
attr_writer :logger
|
83
85
|
end
|
84
|
-
|
86
|
+
|
85
87
|
def initialize(config)
|
86
88
|
@config = config
|
87
89
|
@monitor = Monitor.new
|
88
90
|
@session = nil
|
89
91
|
@prepared_statements = {}
|
90
92
|
@last_prepare_warning = Time.now
|
91
|
-
@subscribers =
|
93
|
+
@subscribers = Subscribers.new
|
94
|
+
@consistency = ((config.cluster || {})[:consistency] || :local_one)
|
92
95
|
end
|
93
|
-
|
96
|
+
|
94
97
|
# Open a connection to the Cassandra cluster.
|
95
98
|
def connect
|
96
99
|
start_time = Time.now
|
97
100
|
cluster_config = config.cluster
|
98
|
-
cluster_config = cluster_config.merge(:
|
101
|
+
cluster_config = cluster_config.merge(logger: logger) if logger
|
99
102
|
cluster = Cassandra.cluster(cluster_config)
|
100
|
-
logger
|
103
|
+
logger&.info("Cassie.connect with #{config.sanitized_cluster} in #{((Time.now - start_time) * 1000).round}ms")
|
101
104
|
@monitor.synchronize do
|
102
105
|
@session = cluster.connect(config.default_keyspace)
|
103
106
|
@prepared_statements = {}
|
@@ -106,19 +109,19 @@ class Cassie
|
|
106
109
|
|
107
110
|
# Close the connections to the Cassandra cluster.
|
108
111
|
def disconnect
|
109
|
-
logger
|
112
|
+
logger&.info("Cassie.disconnect from #{config.sanitized_cluster}")
|
110
113
|
@monitor.synchronize do
|
111
|
-
@session
|
114
|
+
@session&.close
|
112
115
|
@session = nil
|
113
116
|
@prepared_statements = {}
|
114
117
|
end
|
115
118
|
end
|
116
|
-
|
119
|
+
|
117
120
|
# Return true if the connection to the Cassandra cluster has been established.
|
118
121
|
def connected?
|
119
122
|
!!@session
|
120
123
|
end
|
121
|
-
|
124
|
+
|
122
125
|
# Force reconnection. If you're using this code in conjunction in a forking server environment
|
123
126
|
# like passenger or unicorn you should call this method after forking.
|
124
127
|
def reconnect
|
@@ -147,13 +150,13 @@ class Cassie
|
|
147
150
|
end
|
148
151
|
end
|
149
152
|
end
|
150
|
-
|
153
|
+
|
151
154
|
if cache_filled_up && logger && Time.now > @last_prepare_warning + 10
|
152
155
|
# Set a throttle on how often this message is logged so we don't kill performance enven more.
|
153
156
|
@last_prepare_warning = Time.now
|
154
157
|
logger.warn("Cassie.prepare cache filled up. Consider increasing the size from #{config.max_prepared_statements}.")
|
155
158
|
end
|
156
|
-
|
159
|
+
|
157
160
|
statement
|
158
161
|
end
|
159
162
|
|
@@ -179,7 +182,7 @@ class Cassie
|
|
179
182
|
batch_statement.add(statement)
|
180
183
|
end
|
181
184
|
end
|
182
|
-
execute(batch_statement)
|
185
|
+
execute(batch_statement, nil, options)
|
183
186
|
end
|
184
187
|
ensure
|
185
188
|
Thread.current[:cassie_batch] = nil
|
@@ -207,19 +210,22 @@ class Cassie
|
|
207
210
|
columns = []
|
208
211
|
values = []
|
209
212
|
values_hash.each do |column, value|
|
210
|
-
|
213
|
+
unless value.nil?
|
211
214
|
columns << column
|
212
215
|
values << value
|
213
216
|
end
|
214
217
|
end
|
215
|
-
cql = "INSERT INTO #{table} (#{columns.join(
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
218
|
+
cql = "INSERT INTO #{table} (#{columns.join(", ")}) VALUES (#{question_marks(columns.size)})"
|
219
|
+
|
220
|
+
if options&.include?(:ttl)
|
221
|
+
options = options.dup
|
222
|
+
ttl = options.delete(:ttl)
|
223
|
+
if ttl
|
224
|
+
cql += " USING TTL ?"
|
225
|
+
values << Integer(ttl)
|
226
|
+
end
|
221
227
|
end
|
222
|
-
|
228
|
+
|
223
229
|
batch_or_execute(cql, values, options)
|
224
230
|
end
|
225
231
|
|
@@ -242,15 +248,20 @@ class Cassie
|
|
242
248
|
end
|
243
249
|
end
|
244
250
|
values = update_values + key_values
|
245
|
-
|
251
|
+
|
246
252
|
cql = "UPDATE #{table}"
|
247
|
-
|
248
|
-
if ttl
|
249
|
-
|
250
|
-
|
253
|
+
|
254
|
+
if options&.include?(:ttl)
|
255
|
+
options = options.dup
|
256
|
+
ttl = options.delete(:ttl)
|
257
|
+
if ttl
|
258
|
+
cql += " USING TTL ?"
|
259
|
+
values.unshift(Integer(ttl))
|
260
|
+
end
|
251
261
|
end
|
252
|
-
|
253
|
-
|
262
|
+
|
263
|
+
cql += " SET #{update_cql.join(", ")} WHERE #{key_cql}"
|
264
|
+
|
254
265
|
batch_or_execute(cql, values, options)
|
255
266
|
end
|
256
267
|
|
@@ -270,37 +281,50 @@ class Cassie
|
|
270
281
|
start_time = Time.now
|
271
282
|
begin
|
272
283
|
statement = nil
|
273
|
-
if cql.is_a?(String)
|
274
|
-
|
284
|
+
statement = if cql.is_a?(String)
|
285
|
+
if values.present?
|
286
|
+
prepare(cql)
|
287
|
+
else
|
288
|
+
Cassandra::Statements::Simple.new(cql)
|
289
|
+
end
|
275
290
|
else
|
276
|
-
|
291
|
+
cql
|
277
292
|
end
|
278
|
-
|
293
|
+
|
279
294
|
if values.present?
|
280
295
|
values = Array(values)
|
281
|
-
options = (options ? options.merge(:
|
296
|
+
options = (options ? options.merge(arguments: values) : {arguments: values})
|
282
297
|
end
|
283
|
-
|
298
|
+
|
284
299
|
# Set a default consistency from a block context if it isn't explicitly set.
|
285
|
-
|
286
|
-
if
|
287
|
-
|
300
|
+
statement_consistency = current_consistency
|
301
|
+
if statement_consistency
|
302
|
+
if options
|
303
|
+
options = options.merge(consistency: statement_consistency) if options[:consistency].nil?
|
304
|
+
else
|
305
|
+
options = {consistency: statement_consistency}
|
306
|
+
end
|
288
307
|
end
|
289
|
-
|
308
|
+
|
290
309
|
session.execute(statement, options || {})
|
291
310
|
rescue Cassandra::Errors::IOError => e
|
292
311
|
disconnect
|
293
312
|
raise e
|
294
313
|
ensure
|
295
|
-
|
314
|
+
if statement.is_a?(Cassandra::Statement) && !subscribers.empty?
|
296
315
|
payload = Message.new(statement, options, Time.now - start_time)
|
297
|
-
subscribers.each{|subscriber| subscriber.call(payload)}
|
316
|
+
subscribers.each { |subscriber| subscriber.call(payload) }
|
298
317
|
end
|
299
318
|
end
|
300
319
|
end
|
301
320
|
|
321
|
+
# Return the current consistency level that has been set for statements.
|
322
|
+
def current_consistency
|
323
|
+
Thread.current[:cassie_consistency] || consistency
|
324
|
+
end
|
325
|
+
|
302
326
|
private
|
303
|
-
|
327
|
+
|
304
328
|
def logger
|
305
329
|
self.class.logger
|
306
330
|
end
|
@@ -309,7 +333,7 @@ class Cassie
|
|
309
333
|
connect unless connected?
|
310
334
|
@session
|
311
335
|
end
|
312
|
-
|
336
|
+
|
313
337
|
def batch_or_execute(cql, values, options = nil)
|
314
338
|
batch = Thread.current[:cassie_batch]
|
315
339
|
if batch
|
@@ -321,9 +345,7 @@ class Cassie
|
|
321
345
|
end
|
322
346
|
|
323
347
|
def question_marks(size)
|
324
|
-
|
325
|
-
(size - 1).times{ q << ',?' }
|
326
|
-
q
|
348
|
+
"?#{",?" * (size - 1)}"
|
327
349
|
end
|
328
350
|
|
329
351
|
def key_clause(key_hash)
|
@@ -333,9 +355,9 @@ class Cassie
|
|
333
355
|
cql << "#{key} = ?"
|
334
356
|
values << value
|
335
357
|
end
|
336
|
-
[cql.join(
|
358
|
+
[cql.join(" AND "), values]
|
337
359
|
end
|
338
|
-
|
360
|
+
|
339
361
|
# Extract the CQL from a statement
|
340
362
|
def statement_cql(statement, previous = nil)
|
341
363
|
cql = nil
|
@@ -344,7 +366,7 @@ class Cassie
|
|
344
366
|
elsif statement.respond_to?(:statements) && (previous.nil? || !previous.include?(statement))
|
345
367
|
previous ||= []
|
346
368
|
previous << statement
|
347
|
-
cql = statement.statements.collect{|s| statement_cql(s, previous)}.join(
|
369
|
+
cql = statement.statements.collect { |s| statement_cql(s, previous) }.join("; ")
|
348
370
|
end
|
349
371
|
cql
|
350
372
|
end
|
data/lib/cassie/config.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Simple configuration for connecting to Cassandra.
|
2
4
|
#
|
3
5
|
# :cluster should be a Hash of the options to initialize the Cassandra cluster.
|
@@ -19,7 +21,7 @@
|
|
19
21
|
class Cassie::Config
|
20
22
|
attr_reader :cluster
|
21
23
|
attr_accessor :max_prepared_statements, :schema_directory, :default_keyspace
|
22
|
-
|
24
|
+
|
23
25
|
def initialize(options = {})
|
24
26
|
options = options.symbolize_keys
|
25
27
|
@cluster = (options[:cluster] || {}).symbolize_keys
|
@@ -28,27 +30,27 @@ class Cassie::Config
|
|
28
30
|
@schema_directory = options[:schema_directory]
|
29
31
|
@default_keyspace = options[:default_keyspace]
|
30
32
|
end
|
31
|
-
|
33
|
+
|
32
34
|
# Get the actual keyspace mapped to the abstract name.
|
33
35
|
def keyspace(name)
|
34
36
|
@keyspaces[name.to_s] || name.to_s
|
35
37
|
end
|
36
|
-
|
38
|
+
|
37
39
|
# Get the list of keyspaces defined for the cluster.
|
38
40
|
def keyspaces
|
39
41
|
@keyspaces.values
|
40
42
|
end
|
41
|
-
|
43
|
+
|
42
44
|
# Get the list of abstract keyspace names.
|
43
45
|
def keyspace_names
|
44
46
|
@keyspaces.keys
|
45
47
|
end
|
46
|
-
|
48
|
+
|
47
49
|
# Add a mapping of a name to a keyspace.
|
48
50
|
def add_keyspace(name, value)
|
49
51
|
@keyspaces[name.to_s] = value
|
50
52
|
end
|
51
|
-
|
53
|
+
|
52
54
|
# Return the cluster options without passwords or tokens. Used for logging.
|
53
55
|
def sanitized_cluster
|
54
56
|
options = cluster.dup
|
@@ -59,5 +61,4 @@ class Cassie::Config
|
|
59
61
|
options[:logger] = options[:logger].class.name if options.include?(:logger)
|
60
62
|
options
|
61
63
|
end
|
62
|
-
|
63
64
|
end
|
data/lib/cassie/model.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model"
|
4
|
+
require "active_support/hash_with_indifferent_access"
|
3
5
|
|
4
6
|
# This module provides a simple interface for models backed by Cassandra tables.
|
5
7
|
#
|
@@ -15,19 +17,19 @@ require 'active_support/hash_with_indifferent_access'
|
|
15
17
|
#
|
16
18
|
# class Thing
|
17
19
|
# include Cassie::Model
|
18
|
-
#
|
20
|
+
#
|
19
21
|
# self.table_name = "things"
|
20
22
|
# self.keyspace = "test"
|
21
23
|
# self.primary_key = [:owner, :id]
|
22
|
-
#
|
24
|
+
#
|
23
25
|
# column :owner, :int
|
24
26
|
# column :id, :int, :as => :identifier
|
25
27
|
# column :val, :varchar, :as => :value
|
26
|
-
#
|
28
|
+
#
|
27
29
|
# ordering_key :id, :desc
|
28
|
-
#
|
30
|
+
#
|
29
31
|
# validates_presence_of :id, :value
|
30
|
-
#
|
32
|
+
#
|
31
33
|
# before_save do
|
32
34
|
# ...
|
33
35
|
# end
|
@@ -38,22 +40,47 @@ module Cassie::Model
|
|
38
40
|
include ActiveModel::Validations
|
39
41
|
include ActiveModel::Validations::Callbacks
|
40
42
|
extend ActiveModel::Callbacks
|
41
|
-
|
43
|
+
|
42
44
|
included do |base|
|
43
|
-
class_attribute :table_name, :
|
44
|
-
class_attribute :_keyspace, :
|
45
|
-
class_attribute :_primary_key, :
|
46
|
-
class_attribute :_columns, :
|
47
|
-
class_attribute :_column_aliases, :
|
48
|
-
class_attribute :_ordering_keys, :
|
49
|
-
class_attribute :_counter_table, :
|
45
|
+
class_attribute :table_name, instance_reader: false, instance_writer: false
|
46
|
+
class_attribute :_keyspace, instance_reader: false, instance_writer: false
|
47
|
+
class_attribute :_primary_key, instance_reader: false, instance_writer: false
|
48
|
+
class_attribute :_columns, instance_reader: false, instance_writer: false
|
49
|
+
class_attribute :_column_aliases, instance_reader: false, instance_writer: false
|
50
|
+
class_attribute :_ordering_keys, instance_reader: false, instance_writer: false
|
51
|
+
class_attribute :_counter_table, instance_reader: false, instance_writer: false
|
52
|
+
class_attribute :find_subscribers, instance_reader: false, instance_writer: false
|
53
|
+
class_attribute :read_consistency, instance_reader: false, instance_writer: false
|
54
|
+
class_attribute :write_consistency
|
50
55
|
define_model_callbacks :create, :update, :save, :destroy
|
51
56
|
self._columns = {}
|
52
57
|
self._column_aliases = HashWithIndifferentAccess.new
|
53
58
|
self._ordering_keys = {}
|
59
|
+
self.find_subscribers = Cassie::Subscribers.new(Cassie::Model.find_subscribers)
|
60
|
+
end
|
61
|
+
|
62
|
+
class << self
|
63
|
+
@@find_subscribers = Cassie::Subscribers.new
|
64
|
+
|
65
|
+
def find_subscribers
|
66
|
+
@@find_subscribers
|
67
|
+
end
|
54
68
|
end
|
55
|
-
|
56
|
-
|
69
|
+
|
70
|
+
# Message sent to find subscribers for instrumenting find operations.
|
71
|
+
class FindMessage
|
72
|
+
attr_reader :cql, :args, :options, :elapsed_time, :rows
|
73
|
+
|
74
|
+
def initialize(cql, args, options, elapsed_time, rows)
|
75
|
+
@cql = cql
|
76
|
+
@args = args
|
77
|
+
@options = options
|
78
|
+
@elapsed_time = elapsed_time
|
79
|
+
@rows = rows
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module ClassMethods
|
57
84
|
# Define a column name and type from the table. Columns must be defined in order
|
58
85
|
# to be used. This method will handle defining the getter and setter methods as well.
|
59
86
|
#
|
@@ -75,54 +102,57 @@ module Cassie::Model
|
|
75
102
|
def column(name, type, as: nil)
|
76
103
|
name = name.to_sym
|
77
104
|
type_class = nil
|
105
|
+
type_name = type.to_s.downcase.classify
|
106
|
+
# Backward compatibility with older driver versions.
|
107
|
+
type_name = "Text" if type_name == "Varchar"
|
78
108
|
begin
|
79
|
-
type_class = "Cassandra::Types::#{
|
109
|
+
type_class = "Cassandra::Types::#{type_name}".constantize
|
80
110
|
rescue NameError
|
81
111
|
raise ArgumentError.new("#{type.inspect} is not an allowed Cassandra type")
|
82
112
|
end
|
83
|
-
|
113
|
+
|
84
114
|
self._columns = _columns.merge(name => type_class)
|
85
|
-
self._column_aliases =
|
115
|
+
self._column_aliases = _column_aliases.merge(name => name)
|
86
116
|
|
87
117
|
aliased = (as && as.to_s != name.to_s)
|
88
118
|
if aliased
|
89
|
-
self._column_aliases =
|
119
|
+
self._column_aliases = _column_aliases.merge(as => name)
|
90
120
|
end
|
91
121
|
|
92
|
-
if type.to_s == "counter"
|
122
|
+
if type.to_s == "counter"
|
93
123
|
self._counter_table = true
|
94
|
-
|
95
|
-
define_method(name){ instance_variable_get(:"@#{name}") || 0 }
|
96
|
-
define_method("#{name}="){ |value| instance_variable_set(:"@#{name}", value.to_i) }
|
97
|
-
|
98
|
-
define_method("increment_#{name}!"){ |amount=1, ttl: nil| send(:adjust_counter!, name, amount, ttl: ttl) }
|
99
|
-
define_method("decrement_#{name}!"){ |amount=1, ttl: nil| send(:adjust_counter!, name, -amount, ttl: ttl) }
|
124
|
+
|
125
|
+
define_method(name) { instance_variable_get(:"@#{name}") || 0 }
|
126
|
+
define_method("#{name}=") { |value| instance_variable_set(:"@#{name}", value.to_i) }
|
127
|
+
|
128
|
+
define_method("increment_#{name}!") { |amount = 1, ttl: nil| send(:adjust_counter!, name, amount, ttl: ttl) }
|
129
|
+
define_method("decrement_#{name}!") { |amount = 1, ttl: nil| send(:adjust_counter!, name, -amount, ttl: ttl) }
|
100
130
|
if aliased
|
101
|
-
define_method(as){ send(name) }
|
102
|
-
define_method("increment_#{as}!"){ |amount=1, ttl: nil| send("increment_#{name}!", amount, ttl: ttl) }
|
103
|
-
define_method("decrement_#{as}!"){ |amount=1, ttl: nil| send("increment_#{name}!", amount, ttl: ttl) }
|
131
|
+
define_method(as) { send(name) }
|
132
|
+
define_method("increment_#{as}!") { |amount = 1, ttl: nil| send("increment_#{name}!", amount, ttl: ttl) }
|
133
|
+
define_method("decrement_#{as}!") { |amount = 1, ttl: nil| send("increment_#{name}!", amount, ttl: ttl) }
|
104
134
|
end
|
105
135
|
else
|
106
136
|
attr_reader name
|
107
|
-
define_method("#{name}="){ |value| instance_variable_set(:"@#{name}", self.class.send(:coerce, value, type_class)) }
|
137
|
+
define_method("#{name}=") { |value| instance_variable_set(:"@#{name}", self.class.send(:coerce, value, type_class)) }
|
108
138
|
attr_reader name
|
109
139
|
if aliased
|
110
|
-
define_method(as){ send(name) }
|
111
|
-
define_method("#{as}="){|value| send("#{name}=", value) }
|
140
|
+
define_method(as) { send(name) }
|
141
|
+
define_method("#{as}=") { |value| send("#{name}=", value) }
|
112
142
|
end
|
113
143
|
end
|
114
144
|
end
|
115
|
-
|
145
|
+
|
116
146
|
# Returns an array of the defined column names as symbols.
|
117
147
|
def column_names
|
118
148
|
_columns.keys
|
119
149
|
end
|
120
|
-
|
150
|
+
|
121
151
|
# Returns the internal column name after resolving any aliases.
|
122
152
|
def column_name(name_or_alias)
|
123
|
-
|
153
|
+
_column_aliases[name_or_alias] || name_or_alias
|
124
154
|
end
|
125
|
-
|
155
|
+
|
126
156
|
# Set the primary key for the table. The value should be set as an array with the
|
127
157
|
# clustering key first.
|
128
158
|
def primary_key=(value)
|
@@ -134,31 +164,31 @@ module Cassie::Model
|
|
134
164
|
end
|
135
165
|
}.flatten
|
136
166
|
end
|
137
|
-
|
167
|
+
|
138
168
|
# Return an array of column names for the table primary key.
|
139
169
|
def primary_key
|
140
170
|
_primary_key
|
141
171
|
end
|
142
|
-
|
172
|
+
|
143
173
|
# Define and ordering key for the table. The order attribute should be either :asc or :desc
|
144
174
|
def ordering_key(name, order)
|
145
175
|
order = order.to_sym
|
146
176
|
raise ArgumentError.new("order must be either :asc or :desc") unless order == :asc || order == :desc
|
147
177
|
_ordering_keys[name.to_sym] = order
|
148
178
|
end
|
149
|
-
|
179
|
+
|
150
180
|
# Set the keyspace for the table. The name should be an abstract keyspace name
|
151
181
|
# that is mapped to an actual keyspace name in the configuration. If the name
|
152
182
|
# provided is not mapped in the configuration, then the raw value will be used.
|
153
183
|
def keyspace=(name)
|
154
184
|
self._keyspace = name.to_s
|
155
185
|
end
|
156
|
-
|
186
|
+
|
157
187
|
# Return the keyspace name where the table is located.
|
158
188
|
def keyspace
|
159
189
|
connection.config.keyspace(_keyspace)
|
160
190
|
end
|
161
|
-
|
191
|
+
|
162
192
|
# Return the full table name including the keyspace.
|
163
193
|
def full_table_name
|
164
194
|
if _keyspace
|
@@ -167,7 +197,7 @@ module Cassie::Model
|
|
167
197
|
table_name
|
168
198
|
end
|
169
199
|
end
|
170
|
-
|
200
|
+
|
171
201
|
# Find all records.
|
172
202
|
#
|
173
203
|
# The +where+ argument can be a Hash, Array, or String WHERE clause to
|
@@ -186,30 +216,33 @@ module Cassie::Model
|
|
186
216
|
# You can provide a block to this method in which case it will yield each
|
187
217
|
# record as it is foundto the block instead of returning them.
|
188
218
|
def find_all(where:, select: nil, order: nil, limit: nil, options: nil)
|
189
|
-
|
190
|
-
|
219
|
+
start_time = Time.now
|
220
|
+
columns = (select ? Array(select).collect { |c| column_name(c) } : column_names)
|
221
|
+
cql = "SELECT #{columns.join(", ")} FROM #{full_table_name}"
|
191
222
|
values = nil
|
192
|
-
|
223
|
+
|
193
224
|
raise ArgumentError.new("Where clause cannot be blank. Pass :all to find all records.") if where.blank?
|
194
225
|
if where && where != :all
|
195
226
|
where_clause, values = cql_where_clause(where)
|
196
227
|
else
|
197
228
|
values = []
|
198
229
|
end
|
199
|
-
cql
|
200
|
-
|
230
|
+
cql += " WHERE #{where_clause}" if where_clause
|
231
|
+
|
201
232
|
if order
|
202
|
-
cql
|
233
|
+
cql += " ORDER BY #{order}"
|
203
234
|
end
|
204
|
-
|
235
|
+
|
205
236
|
if limit
|
206
|
-
cql
|
237
|
+
cql += " LIMIT ?"
|
207
238
|
values << Integer(limit)
|
208
239
|
end
|
209
|
-
|
210
|
-
results = connection.find(cql, values, options)
|
240
|
+
|
241
|
+
results = connection.find(cql, values, consistency_options(read_consistency, options))
|
211
242
|
records = [] unless block_given?
|
243
|
+
row_count = 0
|
212
244
|
loop do
|
245
|
+
row_count += results.size
|
213
246
|
results.each do |row|
|
214
247
|
record = new(row)
|
215
248
|
record.instance_variable_set(:@persisted, true)
|
@@ -222,9 +255,15 @@ module Cassie::Model
|
|
222
255
|
break if results.last_page?
|
223
256
|
results = results.next_page
|
224
257
|
end
|
258
|
+
|
259
|
+
if find_subscribers && !find_subscribers.empty?
|
260
|
+
payload = FindMessage.new(cql, values, options, Time.now - start_time, row_count)
|
261
|
+
find_subscribers.each { |subscriber| subscriber.call(payload) }
|
262
|
+
end
|
263
|
+
|
225
264
|
records
|
226
265
|
end
|
227
|
-
|
266
|
+
|
228
267
|
# Find a single record that matches the +where+ argument.
|
229
268
|
def find(where)
|
230
269
|
options = nil
|
@@ -234,7 +273,7 @@ module Cassie::Model
|
|
234
273
|
end
|
235
274
|
find_all(where: where, limit: 1, options: options).first
|
236
275
|
end
|
237
|
-
|
276
|
+
|
238
277
|
# Find a single record that matches the +where+ argument or raise an
|
239
278
|
# ActiveRecord::RecordNotFound error if none is found.
|
240
279
|
def find!(where)
|
@@ -242,7 +281,7 @@ module Cassie::Model
|
|
242
281
|
raise Cassie::RecordNotFound unless record
|
243
282
|
record
|
244
283
|
end
|
245
|
-
|
284
|
+
|
246
285
|
# Return the count of rows in the table. If the +where+ argument is specified
|
247
286
|
# then it will be added as the WHERE clause.
|
248
287
|
def count(where = nil)
|
@@ -251,21 +290,21 @@ module Cassie::Model
|
|
251
290
|
where = where.dup
|
252
291
|
options = where.delete(:options)
|
253
292
|
end
|
254
|
-
|
255
|
-
cql = "SELECT COUNT(*) FROM #{
|
293
|
+
|
294
|
+
cql = "SELECT COUNT(*) FROM #{full_table_name}"
|
256
295
|
values = nil
|
257
|
-
|
296
|
+
|
258
297
|
if where
|
259
298
|
where_clause, values = cql_where_clause(where)
|
260
|
-
cql
|
299
|
+
cql += " WHERE #{where_clause}"
|
261
300
|
else
|
262
|
-
|
301
|
+
connection.prepare(cql)
|
263
302
|
end
|
264
|
-
|
265
|
-
results = connection.find(cql, values, options)
|
303
|
+
|
304
|
+
results = connection.find(cql, values, consistency_options(read_consistency, options))
|
266
305
|
results.rows.first["count"]
|
267
306
|
end
|
268
|
-
|
307
|
+
|
269
308
|
# Returns a newly created record. If the record is not valid then it won't be
|
270
309
|
# persisted.
|
271
310
|
def create(attributes)
|
@@ -273,7 +312,7 @@ module Cassie::Model
|
|
273
312
|
record.save
|
274
313
|
record
|
275
314
|
end
|
276
|
-
|
315
|
+
|
277
316
|
# Returns a newly created record or raises an ActiveRecord::RecordInvalid error
|
278
317
|
# if the record is not valid.
|
279
318
|
def create!(attributes)
|
@@ -281,7 +320,7 @@ module Cassie::Model
|
|
281
320
|
record.save!
|
282
321
|
record
|
283
322
|
end
|
284
|
-
|
323
|
+
|
285
324
|
# Delete all rows from the table that match the key hash. This method bypasses
|
286
325
|
# any destroy callbacks defined on the model.
|
287
326
|
def delete_all(key_hash)
|
@@ -289,22 +328,24 @@ module Cassie::Model
|
|
289
328
|
key_hash.each do |name, value|
|
290
329
|
cleanup_up_hash[column_name(name)] = value
|
291
330
|
end
|
292
|
-
connection.delete(full_table_name, cleanup_up_hash)
|
331
|
+
connection.delete(full_table_name, cleanup_up_hash, consistency: write_consistency)
|
293
332
|
end
|
294
|
-
|
333
|
+
|
295
334
|
# All insert, update, and delete calls within the block will be sent as a single
|
296
|
-
# batch to Cassandra.
|
297
|
-
|
298
|
-
|
335
|
+
# batch to Cassandra. The consistency level will default to the write consistency
|
336
|
+
# level if it's been set.
|
337
|
+
def batch(options = nil)
|
338
|
+
options = consistency_options(write_consistency, options)
|
339
|
+
connection.batch(options) do
|
299
340
|
yield
|
300
341
|
end
|
301
342
|
end
|
302
|
-
|
343
|
+
|
303
344
|
# Returns the Cassie instance used to communicate with Cassandra.
|
304
345
|
def connection
|
305
346
|
Cassie.instance
|
306
347
|
end
|
307
|
-
|
348
|
+
|
308
349
|
# Since Cassandra doesn't support offset we need to find the order key of record
|
309
350
|
# at the specified the offset.
|
310
351
|
#
|
@@ -321,7 +362,7 @@ module Cassie::Model
|
|
321
362
|
cluster_order = _ordering_keys[ordering_key] || :asc
|
322
363
|
order ||= cluster_order
|
323
364
|
order_cql = "#{ordering_key} #{order}" unless order == cluster_order
|
324
|
-
|
365
|
+
|
325
366
|
from = (order == :desc ? max : min)
|
326
367
|
to = (order == :desc ? min : max)
|
327
368
|
loop do
|
@@ -329,11 +370,11 @@ module Cassie::Model
|
|
329
370
|
conditions_cql = []
|
330
371
|
conditions = []
|
331
372
|
if from
|
332
|
-
conditions_cql << "#{ordering_key} #{order == :desc ?
|
373
|
+
conditions_cql << "#{ordering_key} #{order == :desc ? "<" : ">"} ?"
|
333
374
|
conditions << from
|
334
375
|
end
|
335
376
|
if to
|
336
|
-
conditions_cql << "#{ordering_key} #{order == :desc ?
|
377
|
+
conditions_cql << "#{ordering_key} #{order == :desc ? ">" : "<"} ?"
|
337
378
|
conditions << to
|
338
379
|
end
|
339
380
|
key.each do |name, value|
|
@@ -342,10 +383,10 @@ module Cassie::Model
|
|
342
383
|
end
|
343
384
|
conditions.unshift(conditions_cql.join(" AND "))
|
344
385
|
|
345
|
-
results = find_all(:
|
386
|
+
results = find_all(select: [ordering_key], where: conditions, limit: limit, order: order_cql)
|
346
387
|
last_row = results.last if results.size == limit
|
347
388
|
last_id = last_row.send(ordering_key) if last_row
|
348
|
-
|
389
|
+
|
349
390
|
if last_id.nil?
|
350
391
|
return nil
|
351
392
|
elsif limit >= offset
|
@@ -356,9 +397,9 @@ module Cassie::Model
|
|
356
397
|
end
|
357
398
|
end
|
358
399
|
end
|
359
|
-
|
400
|
+
|
360
401
|
private
|
361
|
-
|
402
|
+
|
362
403
|
# Turn a hash of column value, array of [cql, value] or a CQL string into
|
363
404
|
# a CQL where clause. Returns the values pulled out in an array for making
|
364
405
|
# a prepared statement.
|
@@ -370,8 +411,7 @@ module Cassie::Model
|
|
370
411
|
where.each do |column, value|
|
371
412
|
col_name = column_name(column)
|
372
413
|
if value.is_a?(Array)
|
373
|
-
q =
|
374
|
-
(value.size - 1).times{ q << ',?' }
|
414
|
+
q = "?#{",?" * (value.size - 1)}"
|
375
415
|
cql << "#{col_name} IN (#{q})"
|
376
416
|
values.concat(value)
|
377
417
|
else
|
@@ -379,7 +419,7 @@ module Cassie::Model
|
|
379
419
|
values << coerce(value, _columns[col_name])
|
380
420
|
end
|
381
421
|
end
|
382
|
-
[cql.join(
|
422
|
+
[cql.join(" AND "), values]
|
383
423
|
when Array
|
384
424
|
[where.first, where[1, where.size]]
|
385
425
|
when String
|
@@ -388,7 +428,7 @@ module Cassie::Model
|
|
388
428
|
raise ArgumentError.new("invalid CQL where clause #{where}")
|
389
429
|
end
|
390
430
|
end
|
391
|
-
|
431
|
+
|
392
432
|
# Force a value to be the correct Cassandra data type.
|
393
433
|
def coerce(value, type_class)
|
394
434
|
if value.nil?
|
@@ -416,23 +456,36 @@ module Cassie::Model
|
|
416
456
|
type_class.new(value)
|
417
457
|
end
|
418
458
|
end
|
459
|
+
|
460
|
+
def consistency_options(consistency, options)
|
461
|
+
if consistency
|
462
|
+
if options
|
463
|
+
options = options.merge(consistency: consistency) if options[:consistency].nil?
|
464
|
+
options
|
465
|
+
else
|
466
|
+
{consistency: consistency}
|
467
|
+
end
|
468
|
+
else
|
469
|
+
options
|
470
|
+
end
|
471
|
+
end
|
419
472
|
end
|
420
|
-
|
473
|
+
|
421
474
|
def initialize(attributes = {})
|
422
475
|
super
|
423
476
|
@persisted = false
|
424
477
|
end
|
425
|
-
|
478
|
+
|
426
479
|
# Return true if the record has been persisted to Cassandra.
|
427
480
|
def persisted?
|
428
481
|
@persisted
|
429
482
|
end
|
430
|
-
|
483
|
+
|
431
484
|
# Return true if the table is used for a counter.
|
432
485
|
def counter_table?
|
433
486
|
!!self.class._counter_table
|
434
487
|
end
|
435
|
-
|
488
|
+
|
436
489
|
# Save a record. Returns true if the record was persisted and false if it was invalid.
|
437
490
|
# This method will run the save callbacks as well as either the update or create
|
438
491
|
# callbacks as necessary.
|
@@ -441,13 +494,14 @@ module Cassie::Model
|
|
441
494
|
valid_record = (validate ? valid? : true)
|
442
495
|
if valid_record
|
443
496
|
run_callbacks(:save) do
|
497
|
+
options = {consistency: write_consistency, ttl: (ttl || persistence_ttl)}
|
444
498
|
if persisted?
|
445
499
|
run_callbacks(:update) do
|
446
|
-
self.class.connection.update(self.class.full_table_name, values_hash, key_hash,
|
500
|
+
self.class.connection.update(self.class.full_table_name, values_hash, key_hash, options)
|
447
501
|
end
|
448
502
|
else
|
449
503
|
run_callbacks(:create) do
|
450
|
-
self.class.connection.insert(self.class.full_table_name, attributes,
|
504
|
+
self.class.connection.insert(self.class.full_table_name, attributes, options)
|
451
505
|
@persisted = true
|
452
506
|
end
|
453
507
|
end
|
@@ -457,26 +511,26 @@ module Cassie::Model
|
|
457
511
|
false
|
458
512
|
end
|
459
513
|
end
|
460
|
-
|
514
|
+
|
461
515
|
# Save a record. Returns true if the record was saved and raises an ActiveRecord::RecordInvalid
|
462
516
|
# error if the record is invalid.
|
463
|
-
def save!
|
464
|
-
if save
|
517
|
+
def save!(ttl: nil)
|
518
|
+
if save(ttl: ttl)
|
465
519
|
true
|
466
520
|
else
|
467
521
|
raise Cassie::RecordInvalid.new(self)
|
468
522
|
end
|
469
523
|
end
|
470
|
-
|
524
|
+
|
471
525
|
# Delete a record and call the destroy callbacks.
|
472
526
|
def destroy
|
473
527
|
run_callbacks(:destroy) do
|
474
|
-
self.class.connection.delete(self.class.full_table_name, key_hash)
|
528
|
+
self.class.connection.delete(self.class.full_table_name, key_hash, consistency: write_consistency)
|
475
529
|
@persisted = false
|
476
530
|
true
|
477
531
|
end
|
478
532
|
end
|
479
|
-
|
533
|
+
|
480
534
|
# Returns a hash of column to values. Column names will be symbols.
|
481
535
|
def attributes
|
482
536
|
hash = {}
|
@@ -485,51 +539,52 @@ module Cassie::Model
|
|
485
539
|
end
|
486
540
|
hash
|
487
541
|
end
|
488
|
-
|
542
|
+
|
489
543
|
# Subclasses can override this method to provide a TTL on the persisted record.
|
490
544
|
def persistence_ttl
|
491
545
|
nil
|
492
546
|
end
|
493
|
-
|
547
|
+
|
494
548
|
def eql?(other)
|
495
549
|
other.is_a?(self.class) && other.key_hash == key_hash
|
496
550
|
end
|
497
|
-
|
551
|
+
|
498
552
|
def ==(other)
|
499
553
|
eql?(other)
|
500
554
|
end
|
501
|
-
|
555
|
+
|
502
556
|
# Returns the primary key as a hash
|
503
557
|
def key_hash
|
504
558
|
hash = {}
|
505
559
|
self.class.primary_key.each do |key|
|
506
|
-
hash[key] =
|
560
|
+
hash[key] = send(key)
|
507
561
|
end
|
508
562
|
hash
|
509
563
|
end
|
510
|
-
|
564
|
+
|
511
565
|
private
|
512
|
-
|
566
|
+
|
513
567
|
# Used for updating counter columns.
|
514
568
|
def adjust_counter!(name, amount, ttl: nil)
|
515
569
|
amount = amount.to_i
|
516
570
|
if amount != 0
|
517
571
|
run_callbacks(:update) do
|
518
572
|
adjustment = (amount < 0 ? "#{name} = #{name} - #{amount.abs}" : "#{name} = #{name} + #{amount}")
|
519
|
-
|
573
|
+
options = {consistency: write_consistency, ttl: (ttl || persistence_ttl)}
|
574
|
+
self.class.connection.update(self.class.full_table_name, adjustment, key_hash, options)
|
520
575
|
end
|
521
576
|
end
|
522
577
|
record = self.class.find(key_hash)
|
523
578
|
value = (record ? record.send(name) : send(name) + amount)
|
524
579
|
send("#{name}=", value)
|
525
580
|
end
|
526
|
-
|
581
|
+
|
527
582
|
# Returns a hash of value except for the ones that constitute the primary key
|
528
583
|
def values_hash
|
529
584
|
pk = self.class.primary_key
|
530
585
|
hash = {}
|
531
586
|
self.class.column_names.each do |name|
|
532
|
-
hash[name] = send(name) unless pk.include?(name)
|
587
|
+
hash[name] = send(name) unless pk.include?(name)
|
533
588
|
end
|
534
589
|
hash
|
535
590
|
end
|