whi-cassie 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cassie/schema.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This class can be used to create, drop, or get information about the cassandra schemas. This class
2
4
  # is intended only to provide support for creating schemas in development and test environments. You
3
5
  # should not use this class with your production environment since some of the methods can be destructive.
@@ -8,39 +10,42 @@
8
10
  # definition files live. The files should be named "#{abstract_keyspace}.cql". The actual keyspace name will
9
11
  # be looked from the keyspace mapping in the configuration.
10
12
  class Cassie::Schema
11
- TABLES_CQL = "SELECT columnfamily_name FROM system.schema_columnfamilies WHERE keyspace_name = ?".freeze
12
-
13
+ TABLES_CQL = "SELECT table_name FROM system_schema.tables WHERE keyspace_name = ?"
14
+ VERSION_2_TABLES_CQL = "SELECT columnfamily_name FROM system.schema_columnfamilies WHERE keyspace_name = ?"
15
+
16
+ # rubocop:disable Lint/MixedRegexpCaptureTypes
13
17
  CREATE_MATCHER = /\A(?<create>CREATE (TABLE|((CUSTOM )?INDEX)|TYPE|TRIGGER))(?<exist>( IF NOT EXISTS)?) (?<object>[a-z0-9_.]+)/i.freeze
14
18
  DROP_MATCHER = /\A(?<drop>DROP (TABLE|INDEX|TYPE|TRIGGER))(?<exist>( IF EXISTS)?) (?<object>[a-z0-9_.]+)/i.freeze
15
-
19
+ # rubocop:enable Lint/MixedRegexpCaptureTypes
20
+
16
21
  attr_reader :keyspace
17
-
22
+
18
23
  class << self
19
24
  # Get all the defined schemas.
20
25
  def all
21
26
  schemas.values
22
27
  end
23
-
28
+
24
29
  # Find the schema for a keyspace using the abstract name.
25
30
  def find(keyspace)
26
31
  schemas[keyspace]
27
32
  end
28
-
33
+
29
34
  # Throw out the cached schemas so they can be reloaded from the configuration.
30
35
  def reset!
31
36
  @schemas = nil
32
37
  end
33
-
38
+
34
39
  # Drop a specified keyspace by abstract name. The actual keyspace name will be looked up
35
40
  # from the keyspaces in the configuration.
36
41
  def drop!(keyspace_name)
37
42
  keyspace = Cassie.instance.config.keyspace(keyspace_name)
38
43
  raise ArgumentError.new("#{keyspace_name} is not defined as keyspace in the configuration") unless keyspace
39
-
44
+
40
45
  drop_keyspace_cql = "DROP KEYSPACE IF EXISTS #{keyspace}"
41
46
  Cassie.instance.execute(drop_keyspace_cql)
42
47
  end
43
-
48
+
44
49
  # Load a specified keyspace by abstract name. The actual keyspace name will be looked up
45
50
  # from the keyspaces in the configuration.
46
51
  def load!(keyspace_name)
@@ -49,24 +54,24 @@ class Cassie::Schema
49
54
 
50
55
  schema_file = File.join(Cassie.instance.config.schema_directory, "#{keyspace_name}.cql")
51
56
  raise ArgumentError.new("#{keyspace_name} schema file does not exist at #{schema_file}") unless File.exist?(schema_file)
52
- schema_statements = File.read(schema_file).split(';').collect{|s| s.strip.chomp(';')}
53
-
57
+ schema_statements = File.read(schema_file).split(";").collect { |s| s.strip.chomp(";") }
58
+
54
59
  create_keyspace_cql = "CREATE KEYSPACE IF NOT EXISTS #{keyspace} WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1}"
55
60
  Cassie.instance.execute(create_keyspace_cql)
56
-
61
+
57
62
  schema_statements.each do |statement|
58
- statement = statement.gsub(/#(.*)$/, '').gsub(/\s+/, ' ').strip
63
+ statement = statement.gsub(/#(.*)$/, "").gsub(/\s+/, " ").strip
59
64
  create_match = statement.match(CREATE_MATCHER)
60
65
  if create_match
61
66
  object = create_match["object"]
62
- object = "#{keyspace}.#{object}" unless object.include?('.')
63
- statement = statement.sub(create_match.to_s, "#{create_match['create']} IF NOT EXISTS #{object}")
67
+ object = "#{keyspace}.#{object}" unless object.include?(".")
68
+ statement = statement.sub(create_match.to_s, "#{create_match["create"]} IF NOT EXISTS #{object}")
64
69
  else
65
70
  drop_match = statement.match(DROP_MATCHER)
66
71
  if drop_match
67
72
  object = drop_match["object"]
68
- object = "#{keyspace}.#{object}" unless object.include?('.')
69
- statement = statement.sub(drop_match.to_s, "#{drop_match['drop']} IF EXISTS #{object}")
73
+ object = "#{keyspace}.#{object}" unless object.include?(".")
74
+ statement = statement.sub(drop_match.to_s, "#{drop_match["drop"]} IF EXISTS #{object}")
70
75
  end
71
76
  end
72
77
  unless statement.blank?
@@ -75,23 +80,23 @@ class Cassie::Schema
75
80
  end
76
81
  nil
77
82
  end
78
-
83
+
79
84
  # Drop all keyspaces defined in the configuration.
80
85
  def drop_all!
81
86
  Cassie.instance.config.keyspace_names.each do |keyspace|
82
87
  drop!(keyspace)
83
88
  end
84
89
  end
85
-
90
+
86
91
  # Drop all keyspaces defined in the configuration.
87
92
  def load_all!
88
93
  Cassie.instance.config.keyspace_names.each do |keyspace|
89
94
  load!(keyspace)
90
95
  end
91
96
  end
92
-
97
+
93
98
  private
94
-
99
+
95
100
  def schemas
96
101
  unless defined?(@schemas) && @schemas
97
102
  schemas = {}
@@ -103,24 +108,26 @@ class Cassie::Schema
103
108
  @schemas
104
109
  end
105
110
  end
106
-
111
+
107
112
  def initialize(keyspace)
108
113
  @keyspace = keyspace
109
114
  end
110
-
115
+
111
116
  # Returns a list of tables defined for the schema.
112
117
  def tables
113
118
  unless defined?(@tables) && @tables
114
- tables = []
115
- results = Cassie.instance.execute(TABLES_CQL, keyspace)
116
- results.each do |row|
117
- tables << row['columnfamily_name']
119
+ results = nil
120
+ begin
121
+ results = Cassie.instance.execute(TABLES_CQL, keyspace)
122
+ rescue Cassandra::Errors::InvalidError
123
+ results = Cassie.instance.execute(VERSION_2_TABLES_CQL, keyspace)
118
124
  end
125
+ tables = results.collect { |row| row.values.first }
119
126
  @tables = tables
120
127
  end
121
128
  @tables
122
129
  end
123
-
130
+
124
131
  # Truncate the data from a table.
125
132
  def truncate!(table)
126
133
  statement = Cassie.instance.prepare("TRUNCATE #{keyspace}.#{table}")
@@ -1,12 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Thread safe list of subscribers. Each subscriber must respond to the :call method.
2
4
  class Cassie::Subscribers
3
-
4
5
  def initialize(parent_subscribers = nil)
5
6
  @array = [].freeze
6
7
  @lock = Mutex.new
7
8
  @parent_subscribers = parent_subscribers
8
9
  end
9
-
10
+
10
11
  def add(subscriber)
11
12
  @lock.synchronize do
12
13
  new_array = @array.dup
@@ -15,7 +16,7 @@ class Cassie::Subscribers
15
16
  end
16
17
  end
17
18
  alias_method :<<, :add
18
-
19
+
19
20
  def remove(subscriber)
20
21
  removed = nil
21
22
  @lock.synchronize do
@@ -26,28 +27,25 @@ class Cassie::Subscribers
26
27
  removed
27
28
  end
28
29
  alias_method :delete, :remove
29
-
30
+
30
31
  def clear
31
32
  @array = []
32
33
  end
33
-
34
+
34
35
  def size
35
36
  @array.size + (@parent_subscribers ? @parent_subscribers.size : 0)
36
37
  end
37
-
38
+
38
39
  def empty?
39
40
  size == 0
40
41
  end
41
-
42
+
42
43
  def each(&block)
43
44
  @array.each(&block)
44
- if @parent_subscribers
45
- @parent_subscribers.each(&block)
46
- end
45
+ @parent_subscribers&.each(&block)
47
46
  end
48
-
47
+
49
48
  def include?(subscriber)
50
49
  @array.include?(subscriber)
51
50
  end
52
-
53
51
  end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This class provides helper methods for testing.
2
4
  module Cassie::Testing
3
5
  extend ActiveSupport::Concern
4
-
6
+
5
7
  included do
6
- alias_method_chain :insert, :testing
8
+ prepend OverrideMethods
7
9
  end
8
-
10
+
9
11
  class << self
10
12
  # Prepare the test environment. This method must be called before running the test suite.
11
13
  def prepare!
@@ -16,31 +18,31 @@ module Cassie::Testing
16
18
  end
17
19
  end
18
20
  end
19
-
21
+
20
22
  # Wrap test cases as a block in this method. After the test case finishes, all tables
21
23
  # that had data inserted into them will be truncated so that the data state will be clean
22
24
  # for the next test case.
23
25
  def cleanup!
24
- begin
25
- yield
26
- ensure
27
- if Thread.current[:cassie_inserted].present?
28
- Cassie.instance.batch do
29
- Thread.current[:cassie_inserted].each do |table|
30
- keyspace, table = table.split('.', 2)
31
- schema = Cassie::Schema.find(keyspace)
32
- schema.truncate!(table) if schema
33
- end
26
+ yield
27
+ ensure
28
+ if Thread.current[:cassie_inserted].present?
29
+ Cassie.instance.batch do
30
+ Thread.current[:cassie_inserted].each do |table|
31
+ keyspace, table = table.split(".", 2)
32
+ schema = Cassie::Schema.find(keyspace)
33
+ schema&.truncate!(table)
34
34
  end
35
- Thread.current[:cassie_inserted] = nil
36
35
  end
36
+ Thread.current[:cassie_inserted] = nil
37
37
  end
38
38
  end
39
39
  end
40
-
41
- def insert_with_testing(table, *args)
42
- Thread.current[:cassie_inserted] ||= Set.new
43
- Thread.current[:cassie_inserted] << table
44
- insert_without_testing(table, *args)
40
+
41
+ module OverrideMethods
42
+ def insert(table, *args)
43
+ Thread.current[:cassie_inserted] ||= Set.new
44
+ Thread.current[:cassie_inserted] << table
45
+ super(table, *args)
46
+ end
45
47
  end
46
48
  end
data/lib/cassie.rb CHANGED
@@ -1,4 +1,6 @@
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.
@@ -9,35 +11,35 @@ class Cassie
9
11
  require File.expand_path("../cassie/schema.rb", __FILE__)
10
12
  require File.expand_path("../cassie/testing.rb", __FILE__)
11
13
  require File.expand_path("../cassie/railtie.rb", __FILE__) if defined?(Rails)
12
-
14
+
13
15
  class RecordNotFound < StandardError
14
16
  end
15
-
17
+
16
18
  class RecordInvalid < StandardError
17
19
  attr_reader :record
18
-
20
+
19
21
  def initialize(record)
20
22
  super("Errors on #{record.class.name}: #{record.errors.to_hash.inspect}")
21
23
  @record = record
22
24
  end
23
25
  end
24
-
26
+
25
27
  # Message passed to subscribers with the statement, options, and time for each statement
26
28
  # to execute. Note that if statements are batched they will be packed into one message
27
29
  # with a Cassandra::Statements::Batch statement and empty options.
28
30
  class Message
29
31
  attr_reader :statement, :options, :elapsed_time
30
-
32
+
31
33
  def initialize(statement, options, elapsed_time)
32
34
  @statement = statement
33
35
  @options = options
34
36
  @elapsed_time = elapsed_time
35
37
  end
36
38
  end
37
-
39
+
38
40
  attr_reader :config, :subscribers
39
41
  attr_accessor :consistency
40
-
42
+
41
43
  class << self
42
44
  # A singleton instance that can be shared to communicate with a Cassandra cluster.
43
45
  def instance
@@ -47,7 +49,7 @@ class Cassie
47
49
  end
48
50
  @instance
49
51
  end
50
-
52
+
51
53
  # Call this method to load the Cassie::Config from the specified file for the
52
54
  # specified environment.
53
55
  def configure!(options)
@@ -58,7 +60,7 @@ class Cassie
58
60
  end
59
61
  @config = Cassie::Config.new(options)
60
62
  end
61
-
63
+
62
64
  # This method can be used to set a consistency level for all Cassandra queries
63
65
  # within a block that don't explicitly define them. It can be used where consistency
64
66
  # is important (i.e. on validation queries) but where a higher level method
@@ -72,18 +74,16 @@ class Cassie
72
74
  Thread.current[:cassie_consistency] = save_val
73
75
  end
74
76
  end
75
-
77
+
76
78
  # Get a Logger compatible object if it has been set.
77
79
  def logger
78
80
  @logger if defined?(@logger)
79
81
  end
80
-
82
+
81
83
  # Set a logger with a Logger compatible object.
82
- def logger=(value)
83
- @logger = value
84
- end
84
+ attr_writer :logger
85
85
  end
86
-
86
+
87
87
  def initialize(config)
88
88
  @config = config
89
89
  @monitor = Monitor.new
@@ -93,14 +93,14 @@ class Cassie
93
93
  @subscribers = Subscribers.new
94
94
  @consistency = ((config.cluster || {})[:consistency] || :local_one)
95
95
  end
96
-
96
+
97
97
  # Open a connection to the Cassandra cluster.
98
98
  def connect
99
99
  start_time = Time.now
100
100
  cluster_config = config.cluster
101
- cluster_config = cluster_config.merge(:logger => logger) if logger
101
+ cluster_config = cluster_config.merge(logger: logger) if logger
102
102
  cluster = Cassandra.cluster(cluster_config)
103
- 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")
104
104
  @monitor.synchronize do
105
105
  @session = cluster.connect(config.default_keyspace)
106
106
  @prepared_statements = {}
@@ -109,19 +109,19 @@ class Cassie
109
109
 
110
110
  # Close the connections to the Cassandra cluster.
111
111
  def disconnect
112
- logger.info("Cassie.disconnect from #{config.sanitized_cluster}") if logger
112
+ logger&.info("Cassie.disconnect from #{config.sanitized_cluster}")
113
113
  @monitor.synchronize do
114
- @session.close if @session
114
+ @session&.close
115
115
  @session = nil
116
116
  @prepared_statements = {}
117
117
  end
118
118
  end
119
-
119
+
120
120
  # Return true if the connection to the Cassandra cluster has been established.
121
121
  def connected?
122
122
  !!@session
123
123
  end
124
-
124
+
125
125
  # Force reconnection. If you're using this code in conjunction in a forking server environment
126
126
  # like passenger or unicorn you should call this method after forking.
127
127
  def reconnect
@@ -150,13 +150,13 @@ class Cassie
150
150
  end
151
151
  end
152
152
  end
153
-
153
+
154
154
  if cache_filled_up && logger && Time.now > @last_prepare_warning + 10
155
155
  # Set a throttle on how often this message is logged so we don't kill performance enven more.
156
156
  @last_prepare_warning = Time.now
157
157
  logger.warn("Cassie.prepare cache filled up. Consider increasing the size from #{config.max_prepared_statements}.")
158
158
  end
159
-
159
+
160
160
  statement
161
161
  end
162
162
 
@@ -210,22 +210,22 @@ class Cassie
210
210
  columns = []
211
211
  values = []
212
212
  values_hash.each do |column, value|
213
- if !value.nil?
213
+ unless value.nil?
214
214
  columns << column
215
215
  values << value
216
216
  end
217
217
  end
218
- cql = "INSERT INTO #{table} (#{columns.join(', ')}) VALUES (#{question_marks(columns.size)})"
219
-
220
- if options && options.include?(:ttl)
218
+ cql = "INSERT INTO #{table} (#{columns.join(", ")}) VALUES (#{question_marks(columns.size)})"
219
+
220
+ if options&.include?(:ttl)
221
221
  options = options.dup
222
222
  ttl = options.delete(:ttl)
223
223
  if ttl
224
- cql << " USING TTL ?"
224
+ cql += " USING TTL ?"
225
225
  values << Integer(ttl)
226
226
  end
227
227
  end
228
-
228
+
229
229
  batch_or_execute(cql, values, options)
230
230
  end
231
231
 
@@ -248,20 +248,20 @@ class Cassie
248
248
  end
249
249
  end
250
250
  values = update_values + key_values
251
-
251
+
252
252
  cql = "UPDATE #{table}"
253
-
254
- if options && options.include?(:ttl)
253
+
254
+ if options&.include?(:ttl)
255
255
  options = options.dup
256
256
  ttl = options.delete(:ttl)
257
257
  if ttl
258
- cql << " USING TTL ?"
258
+ cql += " USING TTL ?"
259
259
  values.unshift(Integer(ttl))
260
260
  end
261
261
  end
262
262
 
263
- cql << " SET #{update_cql.join(', ')} WHERE #{key_cql}"
264
-
263
+ cql += " SET #{update_cql.join(", ")} WHERE #{key_cql}"
264
+
265
265
  batch_or_execute(cql, values, options)
266
266
  end
267
267
 
@@ -281,31 +281,31 @@ class Cassie
281
281
  start_time = Time.now
282
282
  begin
283
283
  statement = nil
284
- if cql.is_a?(String)
284
+ statement = if cql.is_a?(String)
285
285
  if values.present?
286
- statement = prepare(cql)
286
+ prepare(cql)
287
287
  else
288
- statement = Cassandra::Statements::Simple.new(cql)
288
+ Cassandra::Statements::Simple.new(cql)
289
289
  end
290
290
  else
291
- statement = cql
291
+ cql
292
292
  end
293
-
293
+
294
294
  if values.present?
295
295
  values = Array(values)
296
- options = (options ? options.merge(:arguments => values) : {:arguments => values})
296
+ options = (options ? options.merge(arguments: values) : {arguments: values})
297
297
  end
298
-
298
+
299
299
  # Set a default consistency from a block context if it isn't explicitly set.
300
300
  statement_consistency = current_consistency
301
301
  if statement_consistency
302
302
  if options
303
- options = options.merge(:consistency => statement_consistency) if options[:consistency].nil?
303
+ options = options.merge(consistency: statement_consistency) if options[:consistency].nil?
304
304
  else
305
- options = {:consistency => statement_consistency}
305
+ options = {consistency: statement_consistency}
306
306
  end
307
307
  end
308
-
308
+
309
309
  session.execute(statement, options || {})
310
310
  rescue Cassandra::Errors::IOError => e
311
311
  disconnect
@@ -313,18 +313,18 @@ class Cassie
313
313
  ensure
314
314
  if statement.is_a?(Cassandra::Statement) && !subscribers.empty?
315
315
  payload = Message.new(statement, options, Time.now - start_time)
316
- subscribers.each{|subscriber| subscriber.call(payload)}
316
+ subscribers.each { |subscriber| subscriber.call(payload) }
317
317
  end
318
318
  end
319
319
  end
320
-
320
+
321
321
  # Return the current consistency level that has been set for statements.
322
322
  def current_consistency
323
323
  Thread.current[:cassie_consistency] || consistency
324
324
  end
325
325
 
326
326
  private
327
-
327
+
328
328
  def logger
329
329
  self.class.logger
330
330
  end
@@ -333,7 +333,7 @@ class Cassie
333
333
  connect unless connected?
334
334
  @session
335
335
  end
336
-
336
+
337
337
  def batch_or_execute(cql, values, options = nil)
338
338
  batch = Thread.current[:cassie_batch]
339
339
  if batch
@@ -345,9 +345,7 @@ class Cassie
345
345
  end
346
346
 
347
347
  def question_marks(size)
348
- q = '?'
349
- (size - 1).times{ q << ',?' }
350
- q
348
+ "?#{",?" * (size - 1)}"
351
349
  end
352
350
 
353
351
  def key_clause(key_hash)
@@ -357,9 +355,9 @@ class Cassie
357
355
  cql << "#{key} = ?"
358
356
  values << value
359
357
  end
360
- [cql.join(' AND '), values]
358
+ [cql.join(" AND "), values]
361
359
  end
362
-
360
+
363
361
  # Extract the CQL from a statement
364
362
  def statement_cql(statement, previous = nil)
365
363
  cql = nil
@@ -368,7 +366,7 @@ class Cassie
368
366
  elsif statement.respond_to?(:statements) && (previous.nil? || !previous.include?(statement))
369
367
  previous ||= []
370
368
  previous << statement
371
- cql = statement.statements.collect{|s| statement_cql(s, previous)}.join('; ')
369
+ cql = statement.statements.collect { |s| statement_cql(s, previous) }.join("; ")
372
370
  end
373
371
  cql
374
372
  end
data/lib/whi-cassie.rb CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path("../cassie.rb", __FILE__)
@@ -1,7 +1,6 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Cassie::Config do
4
-
5
4
  let(:options) do
6
5
  {
7
6
  "cluster" => {
@@ -14,43 +13,42 @@ describe Cassie::Config do
14
13
  "default_keyspace" => "another"
15
14
  }
16
15
  end
17
-
16
+
18
17
  it "should handle empty options" do
19
18
  config = Cassie::Config.new({})
20
- config.cluster.should == {}
21
- config.keyspace_names.should == []
22
- config.default_keyspace.should == nil
23
- config.schema_directory.should == nil
24
- config.max_prepared_statements.should == 1000
19
+ expect(config.cluster).to eq({})
20
+ expect(config.keyspace_names).to eq([])
21
+ expect(config.default_keyspace).to eq(nil)
22
+ expect(config.schema_directory).to eq(nil)
23
+ expect(config.max_prepared_statements).to eq(1000)
25
24
  end
26
-
25
+
27
26
  it "should have cluster options" do
28
27
  config = Cassie::Config.new(options)
29
- config.cluster.should == {:consistency => :one, :timeout => 15}
28
+ expect(config.cluster).to eq({consistency: :one, timeout: 15})
30
29
  end
31
-
30
+
32
31
  it "should have keyspaces" do
33
32
  config = Cassie::Config.new(options)
34
- config.keyspace(:default).should start_with("test_default")
35
- config.keyspace("other").should start_with("test_other")
36
- config.keyspace_names.should =~ ["default", "other"]
33
+ expect(config.keyspace(:default)).to start_with("test_default")
34
+ expect(config.keyspace("other")).to start_with("test_other")
35
+ expect(config.keyspace_names).to match_array(["default", "other"])
37
36
  end
38
-
37
+
39
38
  it "should have a default_keyspace" do
40
39
  config = Cassie::Config.new(options)
41
- config.default_keyspace.should == "another"
40
+ expect(config.default_keyspace).to eq("another")
42
41
  end
43
-
42
+
44
43
  it "should get the schema_directory" do
45
44
  config = Cassie::Config.new(options)
46
- config.schema_directory.should == "/tmp"
47
- Cassie::Config.new({}).schema_directory.should == nil
45
+ expect(config.schema_directory).to eq("/tmp")
46
+ expect(Cassie::Config.new({}).schema_directory).to eq(nil)
48
47
  end
49
-
48
+
50
49
  it "should get the max_prepared_statements" do
51
50
  config = Cassie::Config.new(options)
52
- config.max_prepared_statements.should == 100
53
- Cassie::Config.new({}).max_prepared_statements.should == 1000
51
+ expect(config.max_prepared_statements).to eq(100)
52
+ expect(Cassie::Config.new({}).max_prepared_statements).to eq(1000)
54
53
  end
55
-
56
54
  end