whi-cassie 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,483 @@
1
+ require 'active_model'
2
+ require 'active_support/hash_with_indifferent_access'
3
+
4
+ # This module provides a simple interface for models backed by Cassandra tables.
5
+ #
6
+ # Cassandra is very limited in how data can be accessed efficiently so this code
7
+ # is intentionally not designed as a full fledged DSL with all the nifty features
8
+ # of ActiveRecord. Doing so will only get you into trouble when you run into the
9
+ # limits of Cassandra data structures.
10
+ #
11
+ # It implements ActiveModel::Model and supports ActiveModel callbacks on :create,
12
+ # :update, :save, and :destroy as well as ActiveModel validations.
13
+ #
14
+ # Example:
15
+ #
16
+ # class Thing
17
+ # include Cassie::Model
18
+ #
19
+ # self.table_name = "things"
20
+ # self.keyspace = "test"
21
+ # self.primary_key = [:owner, :id]
22
+ #
23
+ # column :owner, :int
24
+ # column :id, :int, :as => :identifier
25
+ # column :val, :varchar, :as => :value
26
+ #
27
+ # ordering_key :id, :desc
28
+ #
29
+ # validates_presence_of :id, :value
30
+ #
31
+ # before_save do
32
+ # ...
33
+ # end
34
+ # end
35
+ module Cassie::Model
36
+ extend ActiveSupport::Concern
37
+ include ActiveModel::Model
38
+ include ActiveModel::Validations
39
+ include ActiveModel::Validations::Callbacks
40
+ extend ActiveModel::Callbacks
41
+
42
+ 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
+ define_model_callbacks :create, :update, :save, :destroy
50
+ self._columns = {}
51
+ self._column_aliases = HashWithIndifferentAccess.new
52
+ self._ordering_keys = {}
53
+ end
54
+
55
+ module ClassMethods
56
+ # Define a column name and type from the table. Columns must be defined in order
57
+ # to be used. This method will handle defining the getter and setter methods as well.
58
+ #
59
+ # The type specified must be a valid CQL data type.
60
+ #
61
+ # Because Cassandra stores column names with each row it is beneficial to use very short
62
+ # column names. You can specify the :as option to define a more human readable version.
63
+ # This will add the appropriate getter and setter methods as well as allow you to use
64
+ # the alias name in the methods that take an attributes hash.
65
+ #
66
+ # Defining a column will also define getter and setter methods for both the column name
67
+ # and the alias name (if specified). So `column :i, :int, as: :id` will define the methods
68
+ # `i`, `i=`, `id`, and `id=`.
69
+ def column(name, type, as: nil)
70
+ name = name.to_sym
71
+ type_class = nil
72
+ begin
73
+ type_class = "Cassandra::Types::#{type.to_s.downcase.classify}".constantize
74
+ rescue NameError
75
+ raise ArgumentError.new("#{type.inspect} is not an allowed Cassandra type")
76
+ end
77
+
78
+ self._columns = _columns.merge(name => type_class)
79
+ self._column_aliases = self._column_aliases.merge(name => name)
80
+
81
+ define_method("#{name}="){ |value| instance_variable_set(:"@#{name}", self.class.send(:coerce, value, type_class)) }
82
+ attr_reader name
83
+ if as && as.to_s != name.to_s
84
+ self._column_aliases = self._column_aliases.merge(as => name)
85
+ define_method(as){ send(name) }
86
+ define_method("#{as}="){|value| send("#{name}=", value) }
87
+ end
88
+ end
89
+
90
+ # Returns an array of the defined column names as symbols.
91
+ def column_names
92
+ _columns.keys
93
+ end
94
+
95
+ # Returns the internal column name after resolving any aliases.
96
+ def column_name(name_or_alias)
97
+ name = _column_aliases[name_or_alias] || name_or_alias
98
+ end
99
+
100
+ # Set the primary key for the table. The value should be set as an array with the
101
+ # clustering key first.
102
+ def primary_key=(value)
103
+ self._primary_key = Array(value).map { |column|
104
+ if column.is_a?(Array)
105
+ column.map(&:to_sym)
106
+ else
107
+ column.to_sym
108
+ end
109
+ }.flatten
110
+ end
111
+
112
+ # Return an array of column names for the table primary key.
113
+ def primary_key
114
+ _primary_key
115
+ end
116
+
117
+ # Define and ordering key for the table. The order attribute should be either :asc or :desc
118
+ def ordering_key(name, order)
119
+ order = order.to_sym
120
+ raise ArgumentError.new("order must be either :asc or :desc") unless order == :asc || order == :desc
121
+ _ordering_keys[name.to_sym] = order
122
+ end
123
+
124
+ # Set the keyspace for the table. The name should be an abstract keyspace name
125
+ # that is mapped to an actual keyspace name in the configuration. If the name
126
+ # provided is not mapped in the configuration, then the raw value will be used.
127
+ def keyspace=(name)
128
+ self._keyspace = name.to_s
129
+ end
130
+
131
+ # Return the keyspace name where the table is located.
132
+ def keyspace
133
+ connection.config.keyspace(_keyspace)
134
+ end
135
+
136
+ # Return the full table name including the keyspace.
137
+ def full_table_name
138
+ if _keyspace
139
+ "#{keyspace}.#{table_name}"
140
+ else
141
+ table_name
142
+ end
143
+ end
144
+
145
+ # Find all records.
146
+ #
147
+ # The +where+ argument can be a Hash, Array, or String WHERE clause to
148
+ # filter the rows returned. It is required so that you don't accidentally
149
+ # release code that returns all rows. If you really want to select all
150
+ # rows from a table you can specify the value :all.
151
+ #
152
+ # The +select+ argument can be used to limit which columns are returned and
153
+ # should be passed as an array of column names which can include aliases.
154
+ #
155
+ # The +order+ argument is a CQL fragment indicating the order. Note that
156
+ # Cassandra will only allow ordering by rows in the primary key.
157
+ #
158
+ # The +limit+ argument specifies how many rows to return.
159
+ #
160
+ # You can provide a block to this method in which case it will yield each
161
+ # record as it is foundto the block instead of returning them.
162
+ def find_all(where:, select: nil, order: nil, limit: nil, options: nil)
163
+ columns = (select ? Array(select).collect{|c| column_name(c)} : column_names)
164
+ cql = "SELECT #{columns.join(', ')} FROM #{full_table_name}"
165
+ values = nil
166
+
167
+ raise ArgumentError.new("Where clause cannot be blank. Pass :all to find all records.") if where.blank?
168
+ if where && where != :all
169
+ where_clause, values = cql_where_clause(where)
170
+ else
171
+ values = []
172
+ end
173
+ cql << " WHERE #{where_clause}" if where_clause
174
+
175
+ if order
176
+ cql << " ORDER BY #{order}"
177
+ end
178
+
179
+ if limit
180
+ cql << " LIMIT ?"
181
+ values << Integer(limit)
182
+ end
183
+
184
+ results = connection.find(cql, values, options)
185
+ records = [] unless block_given?
186
+ loop do
187
+ results.each do |row|
188
+ record = new(row)
189
+ record.instance_variable_set(:@persisted, true)
190
+ if block_given?
191
+ yield record
192
+ else
193
+ records << record
194
+ end
195
+ end
196
+ break if results.last_page?
197
+ results = results.next_page
198
+ end
199
+ records
200
+ end
201
+
202
+ # Find a single record that matches the +where+ argument.
203
+ def find(where)
204
+ options = nil
205
+ if where.is_a?(Hash) && where.include?(:options)
206
+ where = where.dup
207
+ options = where.delete(:options)
208
+ end
209
+ find_all(where: where, limit: 1, options: options).first
210
+ end
211
+
212
+ # Find a single record that matches the +where+ argument or raise an
213
+ # ActiveRecord::RecordNotFound error if none is found.
214
+ def find!(where)
215
+ record = find(where)
216
+ raise Cassie::RecordNotFound unless record
217
+ record
218
+ end
219
+
220
+ # Return the count of rows in the table. If the +where+ argument is specified
221
+ # then it will be added as the WHERE clause.
222
+ def count(where = nil)
223
+ options = nil
224
+ if where.is_a?(Hash) && where.include?(:options)
225
+ where = where.dup
226
+ options = where.delete(:options)
227
+ end
228
+
229
+ cql = "SELECT COUNT(*) FROM #{self.full_table_name}"
230
+ values = nil
231
+
232
+ if where
233
+ where_clause, values = cql_where_clause(where)
234
+ cql << " WHERE #{where_clause}"
235
+ else
236
+ where = connection.prepare(cql)
237
+ end
238
+
239
+ results = connection.find(cql, values, options)
240
+ results.rows.first["count"]
241
+ end
242
+
243
+ # Returns a newly created record. If the record is not valid then it won't be
244
+ # persisted.
245
+ def create(attributes)
246
+ record = new(attributes)
247
+ record.save
248
+ record
249
+ end
250
+
251
+ # Returns a newly created record or raises an ActiveRecord::RecordInvalid error
252
+ # if the record is not valid.
253
+ def create!(attributes)
254
+ record = new(attributes)
255
+ record.save!
256
+ record
257
+ end
258
+
259
+ # Delete all rows from the table that match the key hash. This method bypasses
260
+ # any destroy callbacks defined on the model.
261
+ def delete_all(key_hash)
262
+ cleanup_up_hash = {}
263
+ key_hash.each do |name, value|
264
+ cleanup_up_hash[column_name(name)] = value
265
+ end
266
+ connection.delete(full_table_name, cleanup_up_hash)
267
+ end
268
+
269
+ # All insert, update, and delete calls within the block will be sent as a single
270
+ # batch to Cassandra.
271
+ def batch
272
+ connection.batch do
273
+ yield
274
+ end
275
+ end
276
+
277
+ # Returns the Cassie instance used to communicate with Cassandra.
278
+ def connection
279
+ Cassie.instance
280
+ end
281
+
282
+ # Since Cassandra doesn't support offset we need to find the order key of record
283
+ # at the specified the offset.
284
+ #
285
+ # The key is a Hash describing the primary keys to search minus the last column defined
286
+ # for the primary key. This column is assumed to be an ordering key. If it isn't, this
287
+ # method will fail.
288
+ #
289
+ # The order argument can be used to specify an order for the ordering key (:asc or :desc).
290
+ # It will default to the natural order of the last ordering key as defined by the ordering_key method.
291
+ def offset_to_id(key, offset, order: nil, batch_size: 1000)
292
+ ordering_key = primary_key.last
293
+ cluster_order = _ordering_keys[ordering_key] || :asc
294
+ order ||= cluster_order
295
+ order_cql = "#{ordering_key} #{order}" unless order == cluster_order
296
+
297
+ from = nil
298
+ loop do
299
+ limit = (offset > batch_size ? batch_size : offset + 1)
300
+ conditions_cql = []
301
+ conditions = []
302
+ if from
303
+ conditions_cql << "#{ordering_key} #{order == :desc ? '<' : '>'} ?"
304
+ conditions << from
305
+ end
306
+ key.each do |name, value|
307
+ conditions_cql << "#{column_name(name)} = ?"
308
+ conditions << value
309
+ end
310
+ conditions.unshift(conditions_cql.join(" AND "))
311
+
312
+ results = find_all(:select => [ordering_key], :where => conditions, :limit => limit, :order => order_cql)
313
+ last_row = results.last if results.size == limit
314
+ last_id = last_row.send(ordering_key) if last_row
315
+
316
+ if last_id.nil?
317
+ return nil
318
+ elsif limit >= offset
319
+ return last_id
320
+ else
321
+ offset -= results.size
322
+ from = last_id
323
+ end
324
+ end
325
+ end
326
+
327
+ private
328
+
329
+ # Turn a hash of column value, array of [cql, value] or a CQL string into
330
+ # a CQL where clause. Returns the values pulled out in an array for making
331
+ # a prepared statement.
332
+ def cql_where_clause(where)
333
+ case where
334
+ when Hash
335
+ cql = []
336
+ values = []
337
+ where.each do |column, value|
338
+ col_name = column_name(column)
339
+ if value.is_a?(Array)
340
+ q = '?'
341
+ (value.size - 1).times{ q << ',?' }
342
+ cql << "#{col_name} IN (#{q})"
343
+ values.concat(value)
344
+ else
345
+ cql << "#{col_name} = ?"
346
+ values << coerce(value, _columns[col_name])
347
+ end
348
+ end
349
+ [cql.join(' AND '), values]
350
+ when Array
351
+ [where.first, where[1, where.size]]
352
+ when String
353
+ [where, []]
354
+ else
355
+ raise ArgumentError.new("invalid CQL where clause #{where}")
356
+ end
357
+ end
358
+
359
+ # Force a value to be the correct Cassandra data type.
360
+ def coerce(value, type_class)
361
+ if value.nil?
362
+ nil
363
+ elsif type_class == Cassandra::Types::Timeuuid && value.is_a?(Cassandra::TimeUuid)
364
+ value
365
+ elsif type_class == Cassandra::Types::Uuid
366
+ # Work around for bug in cassandra-driver 2.1.3
367
+ if value.is_a?(Cassandra::Uuid)
368
+ value
369
+ else
370
+ Cassandra::Uuid.new(value)
371
+ end
372
+ elsif type_class == Cassandra::Types::Timestamp && value.is_a?(String)
373
+ Time.parse(value)
374
+ elsif type_class == Cassandra::Types::Inet && value.is_a?(::IPAddr)
375
+ value
376
+ elsif type_class == Cassandra::Types::List
377
+ Array.new(value)
378
+ elsif type_class == Cassandra::Types::Set
379
+ Array.new(value).to_set
380
+ elsif type_class == Cassandra::Types::Map
381
+ Hash[value]
382
+ else
383
+ type_class.new(value)
384
+ end
385
+ end
386
+ end
387
+
388
+ def initialize(attributes = {})
389
+ super
390
+ @persisted = false
391
+ end
392
+
393
+ # Return true if the record has been persisted to Cassandra.
394
+ def persisted?
395
+ @persisted
396
+ end
397
+
398
+ # Save a record. Returns true if the record was persisted and false if it was invalid.
399
+ # This method will run the save callbacks as well as either the update or create
400
+ # callbacks as necessary.
401
+ def save(validate: true, ttl: nil)
402
+ valid_record = (validate ? valid? : true)
403
+ if valid_record
404
+ run_callbacks(:save) do
405
+ if persisted?
406
+ run_callbacks(:update) do
407
+ self.class.connection.update(self.class.full_table_name, values_hash, key_hash, :ttl => persistence_ttl || ttl)
408
+ end
409
+ else
410
+ run_callbacks(:create) do
411
+ self.class.connection.insert(self.class.full_table_name, attributes, :ttl => persistence_ttl || ttl)
412
+ @persisted = true
413
+ end
414
+ end
415
+ end
416
+ true
417
+ else
418
+ false
419
+ end
420
+ end
421
+
422
+ # Save a record. Returns true if the record was saved and raises an ActiveRecord::RecordInvalid
423
+ # error if the record is invalid.
424
+ def save!
425
+ if save
426
+ true
427
+ else
428
+ raise Cassie::RecordInvalid.new(self)
429
+ end
430
+ end
431
+
432
+ # Delete a record and call the destroy callbacks.
433
+ def destroy
434
+ run_callbacks(:destroy) do
435
+ self.class.connection.delete(self.class.full_table_name, key_hash)
436
+ @persisted = false
437
+ true
438
+ end
439
+ end
440
+
441
+ # Returns a hash of column to values. Column names will be symbols.
442
+ def attributes
443
+ hash = {}
444
+ self.class.column_names.each do |name|
445
+ hash[name] = send(name)
446
+ end
447
+ hash
448
+ end
449
+
450
+ # Subclasses can override this method to provide a TTL on the persisted record.
451
+ def persistence_ttl
452
+ nil
453
+ end
454
+
455
+ def eql?(other)
456
+ other.is_a?(self.class) && other.key_hash == key_hash
457
+ end
458
+
459
+ def ==(other)
460
+ eql?(other)
461
+ end
462
+
463
+ # Returns the primary key as a hash
464
+ def key_hash
465
+ hash = {}
466
+ self.class.primary_key.each do |key|
467
+ hash[key] = self.send(key)
468
+ end
469
+ hash
470
+ end
471
+
472
+ private
473
+
474
+ # Returns a hash of value except for the ones that constitute the primary key
475
+ def values_hash
476
+ pk = self.class.primary_key
477
+ hash = {}
478
+ self.class.column_names.each do |name|
479
+ hash[name] = send(name) unless pk.include?(name)
480
+ end
481
+ hash
482
+ end
483
+ end
@@ -0,0 +1,20 @@
1
+ # Initialize Cassie instance with default behaviors for a Rails environment.
2
+ #
3
+ # Configuration will be gotten from config/cassie.yml.
4
+ #
5
+ # Schema location will be set to db/cassandra for development and test environments.
6
+ class Cassie::Railtie < Rails::Railtie
7
+ initializer "cassie.initialization" do
8
+ Cassie.logger = Rails.logger
9
+
10
+ config_file = Rails.root + 'config' + 'cassie.yml'
11
+ if config_file.exist?
12
+ options = YAML::load(ERB.new(config_file.read).result)[Rails.env]
13
+ if Rails.env.development? || Rails.env.test?
14
+ schema_dir = Rails.root + 'db' + 'cassandra'
15
+ options['schema_directory'] = schema_dir.to_s if schema_dir.exist?
16
+ end
17
+ Cassie.configure!(options)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,129 @@
1
+ # This class can be used to create, drop, or get information about the cassandra schemas. This class
2
+ # is intended only to provide support for creating schemas in development and test environments. You
3
+ # should not use this class with your production environment since some of the methods can be destructive.
4
+ #
5
+ # The schemas are organized by keyspace.
6
+ #
7
+ # To load schemas for test and development environments you should specify a directory where the schema
8
+ # definition files live. The files should be named "#{abstract_keyspace}.cql". The actual keyspace name will
9
+ # be looked from the keyspace mapping in the configuration.
10
+ class Cassie::Schema
11
+ TABLES_CQL = "SELECT columnfamily_name FROM system.schema_columnfamilies WHERE keyspace_name = ?".freeze
12
+
13
+ CREATE_MATCHER = /\A(?<create>CREATE (TABLE|((CUSTOM )?INDEX)|TYPE|TRIGGER))(?<exist>( IF NOT EXISTS)?) (?<object>[a-z0-9_.]+)/i.freeze
14
+ DROP_MATCHER = /\A(?<drop>DROP (TABLE|INDEX|TYPE|TRIGGER))(?<exist>( IF EXISTS)?) (?<object>[a-z0-9_.]+)/i.freeze
15
+
16
+ attr_reader :keyspace
17
+
18
+ class << self
19
+ # Get all the defined schemas.
20
+ def all
21
+ schemas.values
22
+ end
23
+
24
+ # Find the schema for a keyspace using the abstract name.
25
+ def find(keyspace)
26
+ schemas[keyspace]
27
+ end
28
+
29
+ # Throw out the cached schemas so they can be reloaded from the configuration.
30
+ def reset!
31
+ @schemas = nil
32
+ end
33
+
34
+ # Drop a specified keyspace by abstract name. The actual keyspace name will be looked up
35
+ # from the keyspaces in the configuration.
36
+ def drop!(keyspace_name)
37
+ keyspace = Cassie.instance.config.keyspace(keyspace_name)
38
+ raise ArgumentError.new("#{keyspace_name} is not defined as keyspace in the configuration") unless keyspace
39
+
40
+ drop_keyspace_cql = "DROP KEYSPACE IF EXISTS #{keyspace}"
41
+ Cassie.instance.execute(drop_keyspace_cql)
42
+ end
43
+
44
+ # Load a specified keyspace by abstract name. The actual keyspace name will be looked up
45
+ # from the keyspaces in the configuration.
46
+ def load!(keyspace_name)
47
+ keyspace = Cassie.instance.config.keyspace(keyspace_name)
48
+ raise ArgumentError.new("#{keyspace_name} is not defined as keyspace in the configuration") unless keyspace
49
+
50
+ schema_file = File.join(Cassie.instance.config.schema_directory, "#{keyspace_name}.cql")
51
+ 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
+
54
+ create_keyspace_cql = "CREATE KEYSPACE IF NOT EXISTS #{keyspace} WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1}"
55
+ Cassie.instance.execute(create_keyspace_cql)
56
+
57
+ schema_statements.each do |statement|
58
+ statement = statement.gsub(/#(.*)$/, '').gsub(/\s+/, ' ').strip
59
+ create_match = statement.match(CREATE_MATCHER)
60
+ if create_match
61
+ 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}")
64
+ else
65
+ drop_match = statement.match(DROP_MATCHER)
66
+ if drop_match
67
+ 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}")
70
+ end
71
+ end
72
+ unless statement.blank?
73
+ Cassie.instance.execute(statement)
74
+ end
75
+ end
76
+ nil
77
+ end
78
+
79
+ # Drop all keyspaces defined in the configuration.
80
+ def drop_all!
81
+ Cassie.instance.config.keyspace_names.each do |keyspace|
82
+ drop!(keyspace)
83
+ end
84
+ end
85
+
86
+ # Drop all keyspaces defined in the configuration.
87
+ def load_all!
88
+ Cassie.instance.config.keyspace_names.each do |keyspace|
89
+ load!(keyspace)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def schemas
96
+ unless defined?(@schemas) && @schemas
97
+ schemas = {}
98
+ Cassie.instance.config.keyspaces.each do |keyspace|
99
+ schemas[keyspace] = new(keyspace)
100
+ end
101
+ @schemas = schemas
102
+ end
103
+ @schemas
104
+ end
105
+ end
106
+
107
+ def initialize(keyspace)
108
+ @keyspace = keyspace
109
+ end
110
+
111
+ # Returns a list of tables defined for the schema.
112
+ def tables
113
+ unless defined?(@tables) && @tables
114
+ tables = []
115
+ results = Cassie.instance.execute(TABLES_CQL, keyspace)
116
+ results.each do |row|
117
+ tables << row['columnfamily_name']
118
+ end
119
+ @tables = tables
120
+ end
121
+ @tables
122
+ end
123
+
124
+ # Truncate the data from a table.
125
+ def truncate!(table)
126
+ statement = Cassie.instance.prepare("TRUNCATE #{keyspace}.#{table}")
127
+ Cassie.instance.execute(statement)
128
+ end
129
+ end
@@ -0,0 +1,46 @@
1
+ # This class provides helper methods for testing.
2
+ module Cassie::Testing
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method_chain :insert, :testing
7
+ end
8
+
9
+ class << self
10
+ # Prepare the test environment. This method must be called before running the test suite.
11
+ def prepare!
12
+ Cassie.send(:include, Cassie::Testing) unless Cassie.include?(Cassie::Testing)
13
+ Cassie::Schema.all.each do |schema|
14
+ schema.tables.each do |table|
15
+ schema.truncate!(table)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Wrap test cases as a block in this method. After the test case finishes, all tables
21
+ # that had data inserted into them will be truncated so that the data state will be clean
22
+ # for the next test case.
23
+ 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
34
+ end
35
+ Thread.current[:cassie_inserted] = nil
36
+ end
37
+ end
38
+ end
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)
45
+ end
46
+ end