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.
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activemodel", "~> 6.0"
6
+
7
+ group :development, :test do
8
+ gem "rake"
9
+ gem "rspec"
10
+ gem "appraisal"
11
+ gem "standard", "~>0.8.1"
12
+ end
13
+
14
+ gemspec path: "../"
@@ -1,41 +1,45 @@
1
- require 'cassandra'
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
- def logger=(value)
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(:logger => logger) if logger
101
+ cluster_config = cluster_config.merge(logger: logger) if logger
99
102
  cluster = Cassandra.cluster(cluster_config)
100
- logger.info("Cassie.connect with #{config.sanitized_cluster} in #{((Time.now - start_time) * 1000).round}ms") if 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.info("Cassie.disconnect from #{config.sanitized_cluster}") if logger
112
+ logger&.info("Cassie.disconnect from #{config.sanitized_cluster}")
110
113
  @monitor.synchronize do
111
- @session.close if @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
- if !value.nil?
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(', ')}) VALUES (#{question_marks(columns.size)})"
216
-
217
- ttl = options[:ttl] if options
218
- if ttl
219
- cql << " USING TTL ?"
220
- values << ttl
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
- ttl = options[:ttl] if options
248
- if ttl
249
- cql << " USING TTL ?"
250
- values.unshift(ttl)
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
- cql << " SET #{update_cql.join(', ')} WHERE #{key_cql}"
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) && values.present?
274
- statement = prepare(cql)
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
- statement = cql
291
+ cql
277
292
  end
278
-
293
+
279
294
  if values.present?
280
295
  values = Array(values)
281
- options = (options ? options.merge(:arguments => values) : {:arguments => values})
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
- default_consistency = Thread.current[:cassie_consistency]
286
- if default_consistency
287
- options = (options ? options.reverse_merge(:consistency => default_consistency) : {:consistency => default_consistency})
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
- unless subscribers.empty?
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
- q = '?'
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(' AND '), values]
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
@@ -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
@@ -1,5 +1,7 @@
1
- require 'active_model'
2
- require 'active_support/hash_with_indifferent_access'
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, :instance_reader => false, :instance_writer => false
44
- class_attribute :_keyspace, :instance_reader => false, :instance_writer => false
45
- class_attribute :_primary_key, :instance_reader => false, :instance_writer => false
46
- class_attribute :_columns, :instance_reader => false, :instance_writer => false
47
- class_attribute :_column_aliases, :instance_reader => false, :instance_writer => false
48
- class_attribute :_ordering_keys, :instance_reader => false, :instance_writer => false
49
- class_attribute :_counter_table, :instance_reader => false, :instance_writer => false
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
- module ClassMethods
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::#{type.to_s.downcase.classify}".constantize
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 = self._column_aliases.merge(name => name)
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 = self._column_aliases.merge(as => name)
119
+ self._column_aliases = _column_aliases.merge(as => name)
90
120
  end
91
121
 
92
- if type.to_s == "counter".freeze
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
- name = _column_aliases[name_or_alias] || name_or_alias
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
- columns = (select ? Array(select).collect{|c| column_name(c)} : column_names)
190
- cql = "SELECT #{columns.join(', ')} FROM #{full_table_name}"
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 << " WHERE #{where_clause}" if where_clause
200
-
230
+ cql += " WHERE #{where_clause}" if where_clause
231
+
201
232
  if order
202
- cql << " ORDER BY #{order}"
233
+ cql += " ORDER BY #{order}"
203
234
  end
204
-
235
+
205
236
  if limit
206
- cql << " LIMIT ?"
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 #{self.full_table_name}"
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 << " WHERE #{where_clause}"
299
+ cql += " WHERE #{where_clause}"
261
300
  else
262
- where = connection.prepare(cql)
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
- def batch
298
- connection.batch do
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(:select => [ordering_key], :where => conditions, :limit => limit, :order => order_cql)
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(' AND '), values]
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, :ttl => (ttl || persistence_ttl))
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, :ttl => (ttl || persistence_ttl))
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] = self.send(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
- self.class.connection.update(self.class.full_table_name, adjustment, key_hash, :ttl => (ttl || persistence_ttl))
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