cassandra_store 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ class CassandraStore::Migration
2
+ def self.migration_file(path, version)
3
+ Dir[File.join(path, "#{version}_*.rb")].first
4
+ end
5
+
6
+ def self.migration_class(path, version)
7
+ require migration_file(path, version)
8
+
9
+ File.basename(migration_file(path, version), ".rb").gsub(/\A[0-9]+_/, "").camelcase.constantize
10
+ end
11
+
12
+ def self.up(path, version)
13
+ migration_class(path, version).new.up
14
+
15
+ CassandraStore::SchemaMigration.create!(version: version.to_s)
16
+ end
17
+
18
+ def self.down(path, version)
19
+ migration_class(path, version).new.down
20
+
21
+ CassandraStore::SchemaMigration.where(version: version.to_s).delete_all
22
+ end
23
+
24
+ def self.migrate(path)
25
+ migrated = CassandraStore::SchemaMigration.all.to_a.map(&:version).to_set
26
+ all = Dir[File.join(path, "*.rb")].map { |file| File.basename(file) }
27
+ todo = all.select { |file| file =~ /\A[0-9]+_/ && !migrated.include?(file.to_i.to_s) }.sort_by(&:to_i)
28
+
29
+ todo.each do |file|
30
+ up path, file.to_i.to_s
31
+ end
32
+ end
33
+
34
+ def execute(*args)
35
+ CassandraStore::Base.execute(*args)
36
+ end
37
+
38
+ def up; end
39
+
40
+ def down; end
41
+ end
@@ -0,0 +1,7 @@
1
+ module CassandraStore
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load "cassandra_store/tasks/cassandra.rake"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,187 @@
1
+ class CassandraStore::Relation
2
+ attr_accessor :target, :where_values, :where_cql_values, :order_values, :limit_value, :distinct_value, :select_values
3
+
4
+ def initialize(target:)
5
+ self.target = target
6
+ end
7
+
8
+ def all
9
+ fresh
10
+ end
11
+
12
+ def where(hash = {})
13
+ fresh.tap do |relation|
14
+ relation.where_values = (relation.where_values || []) + [hash]
15
+ end
16
+ end
17
+
18
+ def where_cql(string, args = {})
19
+ fresh.tap do |relation|
20
+ str = string
21
+
22
+ args.each do |key, value|
23
+ str.gsub!(":#{key}", target.quote_value(value))
24
+ end
25
+
26
+ relation.where_cql_values = (relation.where_cql_values || []) + [str]
27
+ end
28
+ end
29
+
30
+ def update_all(string_or_hash)
31
+ if string_or_hash.is_a?(Hash)
32
+ target.execute("UPDATE #{target.quote_table_name target.table_name} SET #{string_or_hash.map { |column, value| "#{target.quote_column_name column} = #{target.quote_value value}" }.join(", ")} #{where_clause}")
33
+ else
34
+ target.execute("UPDATE #{target.quote_table_name target.table_name} SET #{string_or_hash} #{where_clause}")
35
+ end
36
+
37
+ true
38
+ end
39
+
40
+ def order(hash = {})
41
+ fresh.tap do |relation|
42
+ relation.order_values = (relation.order_values || {}).merge(hash)
43
+ end
44
+ end
45
+
46
+ def limit(n)
47
+ fresh.tap do |relation|
48
+ relation.limit_value = n
49
+ end
50
+ end
51
+
52
+ def first(n = 1)
53
+ result = limit(n).to_a
54
+
55
+ return result.first if n == 1
56
+
57
+ result
58
+ end
59
+
60
+ def distinct
61
+ fresh.tap do |relation|
62
+ relation.distinct_value = true
63
+ end
64
+ end
65
+
66
+ def select(*columns)
67
+ fresh.tap do |relation|
68
+ relation.select_values = (relation.select_values || []) + columns
69
+ end
70
+ end
71
+
72
+ def find_each(options = {})
73
+ return enum_for(:find_each, options) unless block_given?
74
+
75
+ find_in_batches options do |batch|
76
+ batch.each do |record|
77
+ yield record
78
+ end
79
+ end
80
+ end
81
+
82
+ def find_in_batches(batch_size: 1_000)
83
+ return enum_for(:find_in_batches, batch_size: batch_size) unless block_given?
84
+
85
+ each_page "SELECT #{select_clause} FROM #{target.quote_table_name target.table_name} #{where_clause} #{order_clause} #{limit_clause}", page_size: batch_size do |result|
86
+ records = []
87
+
88
+ result.each do |row|
89
+ records << if select_values.present?
90
+ row
91
+ else
92
+ load_record(row)
93
+ end
94
+ end
95
+
96
+ yield(records) unless records.empty?
97
+ end
98
+ end
99
+
100
+ def delete_all
101
+ target.execute("DELETE FROM #{target.quote_table_name target.table_name} #{where_clause}")
102
+
103
+ true
104
+ end
105
+
106
+ def delete_in_batches
107
+ find_in_batches do |records|
108
+ records.each do |record|
109
+ where_clause = target.key_columns.map { |column, _| "#{target.quote_column_name column} = #{target.quote_value record.read_raw_attribute(column)}" }.join(" AND ")
110
+
111
+ target.execute "DELETE FROM #{target.quote_table_name target.table_name} WHERE #{where_clause}"
112
+ end
113
+ end
114
+
115
+ true
116
+ end
117
+
118
+ def count
119
+ cql = "SELECT COUNT(*) FROM #{target.quote_table_name target.table_name} #{where_clause}"
120
+
121
+ target.execute(cql).first["count"]
122
+ end
123
+
124
+ def to_a
125
+ @records ||= find_each.to_a
126
+ end
127
+
128
+ private
129
+
130
+ def load_record(row)
131
+ target.new.tap do |record|
132
+ record.persisted!
133
+
134
+ row.each do |key, value|
135
+ record.write_raw_attribute(key, value)
136
+ end
137
+ end
138
+ end
139
+
140
+ def fresh
141
+ dup.tap do |relation|
142
+ relation.instance_variable_set(:@records, nil)
143
+ end
144
+ end
145
+
146
+ def each_page(cql, page_size:)
147
+ result = target.execute(cql, page_size: page_size)
148
+
149
+ while result
150
+ yield result
151
+
152
+ result = result.next_page
153
+ end
154
+ end
155
+
156
+ def select_clause
157
+ "#{distinct_value ? "DISTINCT" : ""} #{select_values.presence ? select_values.join(", ") : "*"}"
158
+ end
159
+
160
+ def where_clause
161
+ return if where_values.blank? && where_cql_values.blank?
162
+
163
+ constraints = []
164
+
165
+ Array(where_values).each do |hash|
166
+ hash.each do |column, value|
167
+ constraints << if value.is_a?(Array) || value.is_a?(Range)
168
+ "#{target.quote_column_name column} IN (#{value.to_a.map { |v| target.quote_value v }.join(", ")})"
169
+ else
170
+ "#{target.quote_column_name column} = #{target.quote_value value}"
171
+ end
172
+ end
173
+ end
174
+
175
+ constraints += Array(where_cql_values)
176
+
177
+ "WHERE #{constraints.join(" AND ")}"
178
+ end
179
+
180
+ def order_clause
181
+ (order_values.presence ? "ORDER BY #{order_values.map { |column, value| "#{target.quote_column_name column} #{value}" }.join(", ")}" : "").to_s
182
+ end
183
+
184
+ def limit_clause
185
+ (limit_value ? "LIMIT #{limit_value.to_i}" : "").to_s
186
+ end
187
+ end
@@ -0,0 +1,11 @@
1
+ class CassandraStore::SchemaMigration < CassandraStore::Base
2
+ def self.table_name
3
+ "schema_migrations"
4
+ end
5
+
6
+ def self.create_table(if_not_exists: false)
7
+ execute "CREATE TABLE #{"IF NOT EXISTS" if if_not_exists} schema_migrations(version TEXT PRIMARY KEY)"
8
+ end
9
+
10
+ column :version, :text, partition_key: true
11
+ end
@@ -0,0 +1,45 @@
1
+ namespace :cassandra do
2
+ namespace :keyspace do
3
+ desc "Drop the keyspace"
4
+ task drop: :environment do
5
+ CassandraStore::Base.logger.level = Logger::DEBUG
6
+ CassandraStore::Base.drop_keyspace(if_exists: true)
7
+ end
8
+
9
+ desc "Create the keyspace"
10
+ task create: :environment do
11
+ CassandraStore::Base.logger.level = Logger::DEBUG
12
+ CassandraStore::Base.create_keyspace(if_not_exists: true)
13
+ end
14
+ end
15
+
16
+ namespace :migrate do
17
+ desc "Run a specific up-migration"
18
+ task up: :environment do
19
+ raise "No VERSION specified" unless ENV["VERSION"]
20
+
21
+ CassandraStore::Base.logger.level = Logger::DEBUG
22
+
23
+ CassandraStore::SchemaMigration.create_table(if_not_exists: true)
24
+ CassandraStore::Migration.up Rails.root.join("cassandra/migrate"), ENV["VERSION"]
25
+ end
26
+
27
+ desc "Run a specific down-migration"
28
+ task down: :environment do
29
+ raise "No VERSION specified" unless ENV["VERSION"]
30
+
31
+ CassandraStore::Base.logger.level = Logger::DEBUG
32
+
33
+ CassandraStore::SchemaMigration.create_table(if_not_exists: true)
34
+ CassandraStore::Migration.down Rails.root.join("cassandra/migrate"), ENV["VERSION"]
35
+ end
36
+ end
37
+
38
+ desc "Run pending migrations"
39
+ task migrate: :environment do
40
+ CassandraStore::Base.logger.level = Logger::DEBUG
41
+
42
+ CassandraStore::SchemaMigration.create_table(if_not_exists: true)
43
+ CassandraStore::Migration.migrate Rails.root.join("cassandra/migrate")
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module CassandraStore
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,691 @@
1
+ require File.expand_path("../spec_helper", __dir__)
2
+
3
+ class TestRecord < CassandraStore::Base
4
+ column :text, :text
5
+ column :boolean, :boolean
6
+ column :int, :int
7
+ column :bigint, :bigint
8
+ column :date, :date
9
+ column :timestamp, :timestamp
10
+ column :timeuuid, :timeuuid
11
+ column :uuid, :uuid
12
+ end
13
+
14
+ RSpec.describe CassandraStore::Base do
15
+ describe ".new" do
16
+ it "assigns the specified attributes" do
17
+ test_log = TestLog.new(timestamp: "2016-11-01 12:00:00", username: "username")
18
+
19
+ expect(Time.parse("2016-11-01 12:00:00").utc.round(3)).to eq(test_log.timestamp)
20
+ expect(test_log.username).to eq("username")
21
+ end
22
+ end
23
+
24
+ describe ".drop_keyspace" do
25
+ # Already tested
26
+ end
27
+
28
+ describe ".create_keyspace" do
29
+ # Already tested
30
+ end
31
+
32
+ describe ".quote_keyspace_name" do
33
+ it "delegates to quote_column_name" do
34
+ allow(described_class).to receive(:quote_column_name)
35
+
36
+ described_class.quote_keyspace_name("keyspace_name")
37
+
38
+ expect(described_class).to have_received(:quote_column_name).with("keyspace_name")
39
+ end
40
+ end
41
+
42
+ describe ".quote_table_name" do
43
+ it "delegates to quote_column_name" do
44
+ allow(described_class).to receive(:quote_column_name)
45
+
46
+ described_class.quote_table_name("table_name")
47
+
48
+ expect(described_class).to have_received(:quote_column_name).with("table_name")
49
+ end
50
+ end
51
+
52
+ describe ".quote_column_name" do
53
+ it "quotes the value" do
54
+ expect(described_class.quote_column_name("column_name")).to eq("\"column_name\"")
55
+ end
56
+
57
+ it "raises an ArgumentError if the value includes quotes" do
58
+ expect { described_class.quote_column_name("column\"name") }.to raise_error(ArgumentError)
59
+ end
60
+ end
61
+
62
+ describe ".quote_value" do
63
+ it "converts timestamps" do
64
+ expect(described_class.quote_value(Time.parse("2020-05-21 12:00:00 UTC"))).to eq("1590062400000")
65
+ end
66
+
67
+ it "quotes datetimes" do
68
+ expect(described_class.quote_value(DateTime.new(2020, 5, 21, 12, 0, 0))).to eq("1590062400000")
69
+ end
70
+
71
+ it "quotes dates" do
72
+ expect(described_class.quote_value(Date.new(2020, 5, 21))).to eq("'2020-05-21'")
73
+ end
74
+
75
+ it "does not quote numerics" do
76
+ expect(described_class.quote_value(19)).to eq("19")
77
+ expect(described_class.quote_value(19.5)).to eq("19.5")
78
+ end
79
+
80
+ it "does not quote booleans" do
81
+ expect(described_class.quote_value(true)).to eq("true")
82
+ expect(described_class.quote_value(false)).to eq("false")
83
+ end
84
+
85
+ it "does not quote cassandra uuids" do
86
+ expect(described_class.quote_value(Cassandra::Uuid.new("50554d6e-29bb-11e5-b345-feff819cdc9f"))).to eq("50554d6e-29bb-11e5-b345-feff819cdc9f")
87
+ expect(described_class.quote_value(Cassandra::TimeUuid.new("e3341564-9b5f-11ea-8fa9-315018f39af9"))).to eq("e3341564-9b5f-11ea-8fa9-315018f39af9")
88
+ end
89
+
90
+ it "quotes strings" do
91
+ expect(described_class.quote_value("some value")).to eq("'some value'")
92
+ expect(described_class.quote_value("some'value")).to eq("'some''value'")
93
+ end
94
+ end
95
+
96
+ describe "#assign" do
97
+ it "assigns the specified attributes" do
98
+ test_log = TestLog.new
99
+ test_log.assign(timestamp: "2016-11-01 12:00:00", username: "username")
100
+
101
+ expect(Time.parse("2016-11-01 12:00:00").utc.round(3)).to eq(test_log.timestamp)
102
+ expect(test_log.username).to eq("username")
103
+ end
104
+
105
+ it "raises an ArgumentError when re-assigning a key attribute" do
106
+ test_log = TestLog.create!(timestamp: Time.parse("2016-11-01 12:00:00"))
107
+
108
+ expect(test_log.persisted?).to eq(true)
109
+ expect { test_log.assign(date: Date.parse("2016-11-02")) }.to raise_error(ArgumentError)
110
+ end
111
+ end
112
+
113
+ describe "#attributes" do
114
+ it "returns the attributes as a hash" do
115
+ test_log = TestLog.new(timestamp: "2016-11-01 12:00:00", username: "username")
116
+
117
+ expect(test_log.attributes).to eq(
118
+ date: nil,
119
+ bucket: nil,
120
+ id: nil,
121
+ query: nil,
122
+ username: "username",
123
+ timestamp: Time.parse("2016-11-01 12:00:00").utc.round(3)
124
+ )
125
+ end
126
+ end
127
+
128
+ describe ".cast_value" do
129
+ it "casts string attributes" do
130
+ expect(TestRecord.new(text: "text").text).to eq("text")
131
+ expect(TestRecord.new(text: 1).text).to eq("1")
132
+ end
133
+
134
+ it "casts boolean attributes" do
135
+ expect(TestRecord.new(boolean: true).boolean).to eq(true)
136
+ expect(TestRecord.new(boolean: false).boolean).to eq(false)
137
+ expect(TestRecord.new(boolean: 1).boolean).to eq(true)
138
+ expect(TestRecord.new(boolean: 0).boolean).to eq(false)
139
+ expect(TestRecord.new(boolean: "1").boolean).to eq(true)
140
+ expect(TestRecord.new(boolean: "0").boolean).to eq(false)
141
+ expect(TestRecord.new(boolean: "true").boolean).to eq(true)
142
+ expect(TestRecord.new(boolean: "false").boolean).to eq(false)
143
+ expect { TestRecord.new(boolean: :other).boolean }.to raise_error(ArgumentError)
144
+ end
145
+
146
+ it "casts int attributes" do
147
+ expect(TestRecord.new(int: 1).int).to eq(1)
148
+ expect(TestRecord.new(int: "1").int).to eq(1)
149
+ expect(TestRecord.new(int: 1.0).int).to eq(1)
150
+ expect { TestRecord.new(int: :other).int }.to raise_error(TypeError)
151
+ end
152
+
153
+ it "casts bigint attributes" do
154
+ expect(TestRecord.new(bigint: 1).bigint).to eq(1)
155
+ expect(TestRecord.new(bigint: "1").bigint).to eq(1)
156
+ expect(TestRecord.new(bigint: 1.0).bigint).to eq(1)
157
+ expect { TestRecord.new(bigint: :other).int }.to raise_error(TypeError)
158
+ end
159
+
160
+ it "casts date attributes" do
161
+ expect(TestRecord.new(date: Date.new(2016, 11, 1)).date).to eq(Date.new(2016, 11, 1))
162
+ expect(TestRecord.new(date: "2016-11-01").date).to eq(Date.new(2016, 11, 1))
163
+ expect { TestRecord.new(date: :other).date }.to raise_error(ArgumentError)
164
+ end
165
+
166
+ it "casts timestamp attributes" do
167
+ expect(TestRecord.new(timestamp: Time.parse("2016-11-01 12:00:00")).timestamp).to eq(Time.parse("2016-11-01 12:00:00").utc.round(3))
168
+ expect(TestRecord.new(timestamp: "2016-11-01 12:00:00").timestamp).to eq(Time.parse("2016-11-01 12:00:00").utc.round(3))
169
+ expect(TestRecord.new(timestamp: Time.parse("2016-11-01 12:00:00").to_i).timestamp).to eq(Time.parse("2016-11-01 12:00:00").utc.round(3))
170
+ expect { TestRecord.new(timestamp: :other).timestamp }.to raise_error(ArgumentError)
171
+ end
172
+
173
+ it "casts timeuuid attributes" do
174
+ expect(TestRecord.new(timeuuid: Cassandra::TimeUuid.new("1ce29e82-b2ea-11e6-88fa-2971245f69e1")).timeuuid).to eq(Cassandra::TimeUuid.new("1ce29e82-b2ea-11e6-88fa-2971245f69e1"))
175
+ expect(TestRecord.new(timeuuid: "1ce29e82-b2ea-11e6-88fa-2971245f69e2").timeuuid).to eq(Cassandra::TimeUuid.new("1ce29e82-b2ea-11e6-88fa-2971245f69e2"))
176
+ expect(TestRecord.new(timeuuid: 38_395_057_947_756_324_226_486_198_980_982_041_059).timeuuid).to eq(Cassandra::TimeUuid.new(38_395_057_947_756_324_226_486_198_980_982_041_059))
177
+ expect { TestRecord.new(timeuuid: :other).timeuuid }.to raise_error(ArgumentError)
178
+ end
179
+
180
+ it "casts uuid attributes" do
181
+ expect(TestRecord.new(uuid: Cassandra::Uuid.new("b9af7b9b-9317-43b3-922e-fe303f5942c1")).uuid).to eq(Cassandra::Uuid.new("b9af7b9b-9317-43b3-922e-fe303f5942c1"))
182
+ expect(TestRecord.new(uuid: "b9af7b9b-9317-43b3-922e-fe303f5942c1").uuid).to eq(Cassandra::Uuid.new("b9af7b9b-9317-43b3-922e-fe303f5942c1"))
183
+ expect(TestRecord.new(uuid: 13_466_612_472_233_423_808_722_080_080_896_418_394).uuid).to eq(Cassandra::Uuid.new(13_466_612_472_233_423_808_722_080_080_896_418_394))
184
+ expect { TestRecord.new(uuid: :other).uuid }.to raise_error(ArgumentError)
185
+ end
186
+ end
187
+
188
+ describe "#save" do
189
+ it "returns false when validation fails" do
190
+ test_log = TestLog.new
191
+
192
+ expect(test_log.save).to eq(false)
193
+ end
194
+
195
+ it "does not persist the record when validation fails" do
196
+ test_log = TestLog.new
197
+
198
+ expect { test_log.save }.not_to(change { TestLog.count })
199
+ end
200
+
201
+ it "adds the errors when validation fails" do
202
+ test_log = TestLog.new
203
+ test_log.save
204
+
205
+ expect(test_log.errors[:timestamp]).to include("can't be blank")
206
+ end
207
+
208
+ it "persists the record" do
209
+ test_log = TestLog.new(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
210
+
211
+ expect { test_log.save }.to change { TestLog.count }.by(1)
212
+ expect(test_log.persisted?).to eq(true)
213
+
214
+ reloaded_test_log = TestLog.where(date: test_log.date, bucket: test_log.bucket, id: test_log.id).first
215
+
216
+ expect(reloaded_test_log.attributes).to eq(test_log.attributes)
217
+ end
218
+
219
+ it "executes the hooks" do
220
+ test_log = TestLog.new(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
221
+ test_log.save
222
+
223
+ expect(test_log.date).to eq(Date.parse("2016-11-01"))
224
+ expect(test_log.username).to eq("username")
225
+ expect(test_log.bucket).to be_present
226
+ expect(test_log.id).to be_present
227
+ end
228
+ end
229
+
230
+ describe "#save!" do
231
+ it "raises an error if validation fails" do
232
+ test_log = TestLog.new
233
+
234
+ expect { test_log.save! }.to raise_error(CassandraStore::RecordInvalid)
235
+ end
236
+
237
+ it "does not persist the record if validation fails" do
238
+ test_log = TestLog.new
239
+
240
+ block = proc do
241
+ begin
242
+ test_log.save!
243
+ rescue StandardError
244
+ nil
245
+ end
246
+ end
247
+
248
+ expect(&block).not_to(change { TestLog.count })
249
+ end
250
+
251
+ it "returns true when the record can be persisted" do
252
+ test_log = TestLog.new(timestamp: Time.parse("2016-11-01 12:00:00"))
253
+
254
+ block = proc do
255
+ begin
256
+ test_log.save!
257
+ rescue StandardError
258
+ nil
259
+ end
260
+ end
261
+
262
+ expect(&block).to(change { TestLog.count }.by(1))
263
+ end
264
+
265
+ it "persists the record" do
266
+ test_log = TestLog.new(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
267
+
268
+ expect { test_log.save! }.to change { TestLog.count }.by(1)
269
+ expect(test_log.persisted?).to eq(true)
270
+
271
+ reloaded_test_log = TestLog.where(date: test_log.date, bucket: test_log.bucket, id: test_log.id).first
272
+
273
+ expect(reloaded_test_log.attributes).to eq(test_log.attributes)
274
+ end
275
+
276
+ it "executes the hooks" do
277
+ test_log = TestLog.new(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
278
+ test_log.save!
279
+
280
+ expect(test_log.date).to eq(Date.parse("2016-11-01"))
281
+ expect(test_log.username).to eq("username")
282
+ expect(test_log.bucket).to be_present
283
+ expect(test_log.id).to be_present
284
+ end
285
+ end
286
+
287
+ describe "#valid?" do
288
+ it "respects the validation context for create" do
289
+ test_log = TestLogWithContext.new
290
+ test_log.valid?
291
+
292
+ expect(test_log.errors[:username]).to include("can't be blank")
293
+ end
294
+
295
+ it "respects the validation context for update" do
296
+ test_log = TestLogWithContext.create!(username: "username", timestamp: Time.now)
297
+ test_log.username = nil
298
+ test_log.valid?
299
+
300
+ expect(test_log.errors[:username]).not_to include("can't be blank")
301
+ expect(test_log.errors[:query]).to include("can't be blank")
302
+ end
303
+ end
304
+
305
+ describe ".create" do
306
+ it "assigns the attributes" do
307
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
308
+
309
+ expect(test_log.timestamp).to eq(Time.parse("2016-11-01 12:00:00").utc.round(3))
310
+ expect(test_log.username).to eq("username")
311
+ end
312
+
313
+ it "delegates to save" do
314
+ allow_any_instance_of(TestLog).to receive(:save).and_raise("delegated")
315
+
316
+ expect { TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username") }.to raise_error("delegated")
317
+ end
318
+ end
319
+
320
+ describe ".create!" do
321
+ it "assigns the attributes" do
322
+ test_log = TestLog.create!(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
323
+
324
+ expect(test_log.timestamp).to eq(Time.parse("2016-11-01 12:00:00").utc.round(3))
325
+ expect(test_log.username).to eq("username")
326
+ end
327
+
328
+ it "delegates to save!" do
329
+ allow_any_instance_of(TestLog).to receive(:save!).and_raise("delegated")
330
+
331
+ expect { TestLog.create!(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username") }.to raise_error("delegated")
332
+ end
333
+ end
334
+
335
+ describe "#update" do
336
+ it "assigns the attributes" do
337
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
338
+ test_log.update(username: "new username", timestamp: Time.parse("2016-11-02 12:00:00"))
339
+
340
+ expect(test_log.username).to eq("new username")
341
+ expect(test_log.timestamp).to eq(Time.parse("2016-11-02 12:00:00").utc.round(3))
342
+ end
343
+
344
+ it "delegates to save" do
345
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
346
+
347
+ allow(test_log).to receive(:save)
348
+
349
+ test_log.update(username: "new username", timestamp: Time.parse("2016-11-02 12:00:00"))
350
+
351
+ expect(test_log).to have_received(:save)
352
+ end
353
+
354
+ it "returns true when the update is successfull" do
355
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
356
+
357
+ expect(test_log.update(username: "new username", timestamp: Time.parse("2016-11-02 12:00:00"))).to eq(true)
358
+ end
359
+
360
+ it "returns false when the update fails" do
361
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
362
+
363
+ expect(test_log.update(timestamp: nil)).to eq(false)
364
+ end
365
+ end
366
+
367
+ describe "#update!" do
368
+ it "assigns the attributes" do
369
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
370
+ test_log.update!(username: "new username", timestamp: Time.parse("2016-11-02 12:00:00"))
371
+
372
+ expect(test_log.username).to eq("new username")
373
+ expect(test_log.timestamp).to eq(Time.parse("2016-11-02 12:00:00").utc.round(3))
374
+ end
375
+
376
+ it "delegates to save!" do
377
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
378
+
379
+ allow(test_log).to receive(:save!)
380
+
381
+ test_log.update!(username: "new username", timestamp: Time.parse("2016-11-02 12:00:00"))
382
+
383
+ expect(test_log).to have_received(:save!)
384
+ end
385
+
386
+ it "returns true when the update is successfull" do
387
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"), username: "username")
388
+
389
+ expect(test_log.update!(username: "new username", timestamp: Time.parse("2016-11-02 12:00:00"))).to eq(true)
390
+ end
391
+ end
392
+
393
+ describe "#persisted" do
394
+ it "returns false if the record is not persisted" do
395
+ test_log = TestLog.new(timestamp: Time.parse("2016-11-01 12:00:00"))
396
+
397
+ expect(test_log.persisted?).to eq(false)
398
+ end
399
+
400
+ it "returns true if the record is persisted" do
401
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"))
402
+
403
+ expect(test_log.persisted?).to eq(true)
404
+ end
405
+ end
406
+
407
+ describe "#new_record?" do
408
+ it "returns true if the record is not yet persisted" do
409
+ test_log = TestLog.new(timestamp: Time.parse("2016-11-01 12:00:00"))
410
+
411
+ expect(test_log.new_record?).to eq(true)
412
+ end
413
+
414
+ it "returns false if the record is persisted" do
415
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"))
416
+
417
+ expect(test_log.new_record?).to eq(false)
418
+ end
419
+ end
420
+
421
+ describe "#delete" do
422
+ it "deletes the record" do
423
+ test_log1 = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"))
424
+ test_log2 = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"))
425
+
426
+ test_log1.delete
427
+
428
+ expect(TestLog.all.to_a).to eq([test_log2])
429
+ end
430
+ end
431
+
432
+ describe "#destroy" do
433
+ it "deletes the record and updates the destroyed info" do
434
+ test_log1 = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"))
435
+ test_log2 = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"))
436
+
437
+ test_log1.destroy
438
+
439
+ expect(TestLog.all.to_a).to eq([test_log2])
440
+ expect(test_log1.destroyed?).to eq(true)
441
+ end
442
+ end
443
+
444
+ describe "#destroyed?" do
445
+ it "returns false when the record was not yet destroyed" do
446
+ test_log = TestLog.new(timestamp: Time.parse("2016-11-01 12:00:00"))
447
+
448
+ expect(test_log.destroyed?).to eq(false)
449
+ end
450
+
451
+ it "returns true when the record was destroyed" do
452
+ test_log = TestLog.create(timestamp: Time.parse("2016-11-01 12:00:00"))
453
+ test_log.destroy
454
+
455
+ expect(test_log.destroyed?).to eq(true)
456
+ end
457
+ end
458
+
459
+ describe ".table_name" do
460
+ it "returns the table name" do
461
+ expect(TestLog.table_name).to eq("test_logs")
462
+ end
463
+ end
464
+
465
+ describe ".truncate_table" do
466
+ it "deletes all records" do
467
+ TestLog.create!(timestamp: Time.parse("2016-11-01 12:00:00"))
468
+ TestLog.create!(timestamp: Time.parse("2016-11-02 12:00:00"))
469
+
470
+ expect { TestLog.truncate_table }.to change { TestLog.count }.by(-2)
471
+ end
472
+ end
473
+
474
+ describe ".statement" do
475
+ it "inserts the specified placeholders and quotes the values" do
476
+ statement = TestLog.statement(
477
+ "SELECT * FROM table WHERE date = :date AND id = :id AND message = :message",
478
+ date: Date.parse("2016-12-06"),
479
+ id: 1,
480
+ message: "some'value"
481
+ )
482
+
483
+ expect(statement).to eq("SELECT * FROM table WHERE date = '2016-12-06' AND id = 1 AND message = 'some''value'")
484
+ end
485
+ end
486
+
487
+ describe ".cluster_execute" do
488
+ it "raises when a table is accessed without keyspace" do
489
+ expect { CassandraStore::Base.cluster_execute("SELECT * FROM test_logs") }.to raise_error(/No keyspace has been specified/)
490
+ end
491
+
492
+ it "executes the statement and returns the result" do
493
+ records = [
494
+ TestLog.create!(timestamp: Time.parse("2016-11-01 12:00:00")),
495
+ TestLog.create!(timestamp: Time.parse("2016-11-02 12:00:00"))
496
+ ]
497
+
498
+ expect(CassandraStore::Base.execute("SELECT * FROM cassandra_store.test_logs", consistency: :all).map { |row| row["id"] }.to_set).to eq(records.map(&:id).to_set)
499
+ end
500
+ end
501
+
502
+ describe ".execute" do
503
+ it "executes the statement and returns the result" do
504
+ records = [
505
+ TestLog.create!(timestamp: Time.parse("2016-11-01 12:00:00")),
506
+ TestLog.create!(timestamp: Time.parse("2016-11-02 12:00:00"))
507
+ ]
508
+
509
+ expect(TestLog.execute("SELECT * FROM test_logs", consistency: :all).map { |row| row["id"] }.to_set).to eq(records.map(&:id).to_set)
510
+ end
511
+ end
512
+
513
+ describe ".execute_batch" do
514
+ it "executes the statements" do
515
+ records = [
516
+ TestLog.create!(timestamp: Time.parse("2016-11-01 12:00:00")),
517
+ TestLog.create!(timestamp: Time.parse("2016-11-02 12:00:00"))
518
+ ]
519
+
520
+ batch = [
521
+ "DELETE FROM test_logs WHERE date = '#{records[0].date.strftime("%F")}' AND bucket = #{records[0].bucket} AND id = #{records[0].id}",
522
+ "DELETE FROM test_logs WHERE date = '#{records[1].date.strftime("%F")}' AND bucket = #{records[1].bucket} AND id = #{records[1].id}"
523
+ ]
524
+
525
+ expect { TestLog.execute_batch(batch, consistency: :all) }.to change { TestLog.count }.by(-2)
526
+ end
527
+ end
528
+
529
+ describe "#callbacks" do
530
+ let(:temp_log) do
531
+ Class.new(TestLog) do
532
+ def self.table_name
533
+ "test_logs"
534
+ end
535
+
536
+ def called_callbacks
537
+ @called_callbacks ||= []
538
+ end
539
+
540
+ def reset_called_callbacks
541
+ @called_callbacks = []
542
+ end
543
+
544
+ before_validation { called_callbacks << :before_validation }
545
+ after_validation { called_callbacks << :after_validation }
546
+ before_save { called_callbacks << :before_save }
547
+ after_save { called_callbacks << :after_save }
548
+ before_create { called_callbacks << :before_create }
549
+ after_create { called_callbacks << :after_create }
550
+ before_update { called_callbacks << :before_update }
551
+ after_update { called_callbacks << :after_update }
552
+ before_destroy { called_callbacks << :before_destroy }
553
+ after_destroy { called_callbacks << :after_destroy }
554
+ end
555
+ end
556
+
557
+ it "executes the correct callbacks in the correct order on create" do
558
+ record = temp_log.create!(timestamp: Time.now)
559
+
560
+ expect(record.called_callbacks).to eq([:before_validation, :after_validation, :before_save, :before_create, :after_create, :after_save])
561
+ end
562
+
563
+ it "executes the correct callbacks in the correct order on update" do
564
+ record = temp_log.create!(timestamp: Time.now)
565
+ record.reset_called_callbacks
566
+ record.save
567
+
568
+ expect(record.called_callbacks).to eq([:before_validation, :after_validation, :before_save, :before_update, :after_update, :after_save])
569
+ end
570
+
571
+ it "executes the correct callbacks in the correct order on destroy" do
572
+ record = temp_log.create!(timestamp: Time.now)
573
+ record.reset_called_callbacks
574
+ record.destroy
575
+
576
+ expect(record.called_callbacks).to eq([:before_destroy, :after_destroy])
577
+ end
578
+ end
579
+
580
+ describe "#validate!" do
581
+ it "raises CassandraStore::RecordInvalid if validation fails" do
582
+ TestLog.new(timestamp: Time.now).validate!
583
+
584
+ expect { TestLog.new.validate! }.to raise_error(CassandraStore::RecordInvalid)
585
+ end
586
+ end
587
+
588
+ describe "dirty attributes" do
589
+ let(:timestamp) { Time.now }
590
+
591
+ it "returns true for dirty attributes" do
592
+ test_log = TestLog.new(timestamp: timestamp, username: "username")
593
+
594
+ expect(test_log.timestamp_changed?).to eq(true)
595
+ expect(test_log.username_changed?).to eq(true)
596
+
597
+ expect(test_log.changes).to eq("timestamp" => [nil, timestamp.utc.round(3)], "username" => [nil, "username"])
598
+ end
599
+
600
+ it "resets the dirty attributes after save" do
601
+ test_log = TestLog.new(timestamp: timestamp, username: "username")
602
+ test_log.save!
603
+
604
+ expect(test_log.timestamp_changed?).to eq(false)
605
+ expect(test_log.username_changed?).to eq(false)
606
+
607
+ expect(test_log.changes).to be_blank
608
+ end
609
+ end
610
+
611
+ describe ".key_columns" do
612
+ it "returns the key columns" do
613
+ expect(TestLog.key_columns).to eq(
614
+ date: { type: :date, partition_key: true, clustering_key: false },
615
+ bucket: { type: :int, partition_key: true, clustering_key: false },
616
+ id: { type: :timeuuid, partition_key: false, clustering_key: true }
617
+ )
618
+ end
619
+ end
620
+
621
+ describe ".clustering_key_columns" do
622
+ it "returns the clustering key columns" do
623
+ expect(TestLog.clustering_key_columns).to eq(id: { type: :timeuuid, partition_key: false, clustering_key: true })
624
+ end
625
+ end
626
+
627
+ describe ".parition_key_columns" do
628
+ it "returns the partition key columns" do
629
+ expect(TestLog.partition_key_columns).to eq(
630
+ date: { type: :date, partition_key: true, clustering_key: false },
631
+ bucket: { type: :int, partition_key: true, clustering_key: false }
632
+ )
633
+ end
634
+ end
635
+
636
+ describe "#key_values" do
637
+ it "returns the values of the keys" do
638
+ date = Date.today
639
+ bucket = 1
640
+ id = Cassandra::TimeUuid::Generator.new.at(Time.now)
641
+
642
+ expect(TestLog.new(date: date, bucket: bucket, id: id).key_values).to eq([date, bucket, id])
643
+ end
644
+ end
645
+
646
+ describe "equality" do
647
+ let(:generator) { Cassandra::TimeUuid::Generator.new }
648
+ let(:id) { generator.at(Time.parse("2017-01-01 12:00:00")) }
649
+
650
+ it "returns true if the records have the same key values" do
651
+ record1 = TestLog.new(date: Date.parse("2017-01-01"), bucket: 1, id: id, username: "username1")
652
+ record2 = TestLog.new(date: Date.parse("2017-01-01"), bucket: 1, id: id, username: "username2")
653
+
654
+ expect(record1).to eq(record2)
655
+ end
656
+
657
+ it "returns false if auto generated keys are not the same" do
658
+ record1 = TestLog.new(date: Date.parse("2017-01-01"), bucket: 1, id: generator.at(Time.parse("2017-01-01 12:00:00")))
659
+ record2 = TestLog.new(date: Date.parse("2017-01-01"), bucket: 1, id: generator.at(Time.parse("2017-01-01 12:00:00")))
660
+
661
+ expect(record1).not_to eq(record2)
662
+ end
663
+
664
+ it "returns false if key values are not the same" do
665
+ record1 = TestLog.new(date: Date.parse("2017-01-01"), bucket: 1, id: id)
666
+ record2 = TestLog.new(date: Date.parse("2017-01-01"), bucket: 2, id: id)
667
+
668
+ expect(record1).not_to eq(record2)
669
+ end
670
+ end
671
+
672
+ describe ".generate_uuid" do
673
+ it "generates a uuid" do
674
+ expect(TestLog.new.send(:generate_uuid)).to be_instance_of(Cassandra::Uuid)
675
+ expect(TestLog.new.send(:generate_uuid).to_s).to match(/\A[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+\z/)
676
+ end
677
+ end
678
+
679
+ describe ".generate_timeuuid" do
680
+ it "generates a timeuuid" do
681
+ expect(TestLog.new.send(:generate_timeuuid)).to be_instance_of(Cassandra::TimeUuid)
682
+ expect(TestLog.new.send(:generate_timeuuid).to_s).to match(/\A[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+\z/)
683
+ end
684
+
685
+ it "respects a passed a timestamp" do
686
+ timestamp = Time.parse("2020-05-20 12:00:00")
687
+
688
+ expect(TestLog.new.send(:generate_timeuuid, timestamp).to_time.utc.round).to eq(timestamp.utc.round)
689
+ end
690
+ end
691
+ end