cequel 1.0.4 → 1.1.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.
- checksums.yaml +4 -4
- data/Appraisals +3 -0
- data/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +5 -4
- data/Gemfile.lock +215 -22
- data/README.md +19 -6
- data/Vagrantfile +6 -1
- data/lib/cequel.rb +3 -2
- data/lib/cequel/metal/batch.rb +16 -1
- data/lib/cequel/metal/data_set.rb +63 -39
- data/lib/cequel/metal/deleter.rb +2 -2
- data/lib/cequel/metal/incrementer.rb +2 -2
- data/lib/cequel/metal/inserter.rb +8 -6
- data/lib/cequel/metal/keyspace.rb +127 -34
- data/lib/cequel/metal/logger.rb +1 -1
- data/lib/cequel/metal/row.rb +3 -4
- data/lib/cequel/metal/updater.rb +2 -2
- data/lib/cequel/metal/writer.rb +18 -12
- data/lib/cequel/record/associations.rb +7 -1
- data/lib/cequel/record/bound.rb +1 -1
- data/lib/cequel/record/callbacks.rb +10 -6
- data/lib/cequel/record/data_set_builder.rb +13 -2
- data/lib/cequel/record/persistence.rb +14 -12
- data/lib/cequel/record/properties.rb +3 -3
- data/lib/cequel/record/railtie.rb +1 -1
- data/lib/cequel/record/record_set.rb +12 -12
- data/lib/cequel/record/schema.rb +5 -1
- data/lib/cequel/schema/keyspace.rb +1 -1
- data/lib/cequel/schema/table_property.rb +1 -1
- data/lib/cequel/type.rb +44 -10
- data/lib/cequel/uuids.rb +46 -0
- data/lib/cequel/version.rb +1 -1
- data/spec/examples/metal/data_set_spec.rb +93 -0
- data/spec/examples/metal/keyspace_spec.rb +74 -0
- data/spec/examples/record/associations_spec.rb +84 -0
- data/spec/examples/record/persistence_spec.rb +23 -0
- data/spec/examples/record/properties_spec.rb +1 -1
- data/spec/examples/record/record_set_spec.rb +12 -3
- data/spec/examples/record/schema_spec.rb +1 -1
- data/spec/examples/record/secondary_index_spec.rb +1 -1
- data/spec/examples/schema/table_reader_spec.rb +2 -3
- data/spec/examples/spec_helper.rb +8 -0
- data/spec/examples/type_spec.rb +7 -5
- data/spec/examples/uuids_spec.rb +22 -0
- data/spec/support/helpers.rb +25 -19
- data/templates/config/cequel.yml +5 -5
- metadata +40 -33
data/lib/cequel/uuids.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Cequel
|
3
|
+
#
|
4
|
+
# This module adds some utility methods for generating and type-checking UUID
|
5
|
+
# objects for use with Cequel. These methods are provided because the actual
|
6
|
+
# UUID implementation is an artifact of the underlying driver;
|
7
|
+
# initializing/typechecking those driver classes directly is potentially
|
8
|
+
# breaking.
|
9
|
+
#
|
10
|
+
module Uuids
|
11
|
+
#
|
12
|
+
# Create a UUID
|
13
|
+
#
|
14
|
+
# @param value [Time,String,Integer] timestamp to assign to the UUID, or
|
15
|
+
# numeric or string representation of the UUID
|
16
|
+
# @return a UUID appropriate for use with Cequel
|
17
|
+
#
|
18
|
+
def uuid(value = nil)
|
19
|
+
if value.nil?
|
20
|
+
timeuuid_generator.next
|
21
|
+
elsif value.is_a?(Time)
|
22
|
+
timeuuid_generator.from_time(value)
|
23
|
+
elsif value.is_a?(DateTime)
|
24
|
+
timeuuid_generator.from_time(Time.at(value.to_f))
|
25
|
+
else
|
26
|
+
Type::Timeuuid.instance.cast(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Determine if an object is a UUID
|
32
|
+
#
|
33
|
+
# @param object an object to check
|
34
|
+
# @return [Boolean] true if the object is recognized by Cequel as a UUID
|
35
|
+
#
|
36
|
+
def uuid?(object)
|
37
|
+
object.is_a?(Cql::Uuid)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def timeuuid_generator
|
43
|
+
@timeuuid_generator ||= Cql::TimeUuid::Generator.new
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/cequel/version.rb
CHANGED
@@ -84,6 +84,12 @@ describe Cequel::Metal::DataSet do
|
|
84
84
|
first.writetime(:title).should == (time.to_f * 1_000_000).to_i
|
85
85
|
end
|
86
86
|
|
87
|
+
it 'should insert row with given consistency' do
|
88
|
+
expect_query_with_consistency(/INSERT/, :one) do
|
89
|
+
cequel[:posts].insert(row, consistency: :one)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
87
93
|
it 'should include multiple arguments joined by AND' do
|
88
94
|
cequel.schema.truncate_table(:posts)
|
89
95
|
time = 1.day.ago
|
@@ -118,6 +124,13 @@ describe Cequel::Metal::DataSet do
|
|
118
124
|
row.writetime(:title).should == (time.to_f * 1_000_000).to_i
|
119
125
|
end
|
120
126
|
|
127
|
+
it 'should send update statement with given consistency' do
|
128
|
+
expect_query_with_consistency(/UPDATE/, :one) do
|
129
|
+
cequel[:posts].where(row_keys).update(
|
130
|
+
{title: 'Marshmallows'}, consistency: :one)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
121
134
|
it 'should overwrite list column' do
|
122
135
|
cequel[:posts].where(row_keys).
|
123
136
|
update(categories: ['Big Data', 'Cassandra'])
|
@@ -357,6 +370,12 @@ describe Cequel::Metal::DataSet do
|
|
357
370
|
# This means timestamp is working, since the earlier timestamp would cause
|
358
371
|
# Cassandra to ignore the deletion
|
359
372
|
end
|
373
|
+
|
374
|
+
it 'should send delete with specified consistency' do
|
375
|
+
expect_query_with_consistency(/DELETE/, :one) do
|
376
|
+
cequel[:posts].where(row_keys).delete(:body, :consistency => :one)
|
377
|
+
end
|
378
|
+
end
|
360
379
|
end
|
361
380
|
|
362
381
|
describe '#list_remove_at' do
|
@@ -554,6 +573,80 @@ describe Cequel::Metal::DataSet do
|
|
554
573
|
end
|
555
574
|
end
|
556
575
|
|
576
|
+
describe '#consistency' do
|
577
|
+
let(:data_set) { cequel[:posts].consistency(:one) }
|
578
|
+
|
579
|
+
it 'should issue SELECT with scoped consistency' do
|
580
|
+
expect_query_with_consistency(/SELECT/, :one) { data_set.to_a }
|
581
|
+
end
|
582
|
+
|
583
|
+
it 'should issue COUNT with scoped consistency' do
|
584
|
+
expect_query_with_consistency(/SELECT.*COUNT/, :one) { data_set.count }
|
585
|
+
end
|
586
|
+
|
587
|
+
it 'should issue INSERT with scoped consistency' do
|
588
|
+
expect_query_with_consistency(/INSERT/, :one) do
|
589
|
+
data_set.insert(row_keys)
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
it 'should issue UPDATE with scoped consistency' do
|
594
|
+
expect_query_with_consistency(/UPDATE/, :one) do
|
595
|
+
data_set.where(row_keys).update(title: 'Marshmallows')
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
it 'should issue DELETE with scoped consistency' do
|
600
|
+
expect_query_with_consistency(/DELETE/, :one) do
|
601
|
+
data_set.where(row_keys).delete
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
it 'should issue DELETE column with scoped consistency' do
|
606
|
+
expect_query_with_consistency(/DELETE/, :one) do
|
607
|
+
data_set.where(row_keys).delete(:title)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
describe 'default consistency' do
|
613
|
+
before(:all) { cequel.default_consistency = :all }
|
614
|
+
after(:all) { cequel.default_consistency = nil }
|
615
|
+
let(:data_set) { cequel[:posts] }
|
616
|
+
|
617
|
+
it 'should issue SELECT with default consistency' do
|
618
|
+
expect_query_with_consistency(/SELECT/, :all) { data_set.to_a }
|
619
|
+
end
|
620
|
+
|
621
|
+
it 'should issue COUNT with default consistency' do
|
622
|
+
expect_query_with_consistency(/SELECT.*COUNT/, :all) { data_set.count }
|
623
|
+
end
|
624
|
+
|
625
|
+
it 'should issue INSERT with default consistency' do
|
626
|
+
expect_query_with_consistency(/INSERT/, :all) do
|
627
|
+
data_set.insert(row_keys)
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
it 'should issue UPDATE with default consistency' do
|
632
|
+
expect_query_with_consistency(/UPDATE/, :all) do
|
633
|
+
data_set.where(row_keys).update(title: 'Marshmallows')
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
it 'should issue DELETE with default consistency' do
|
638
|
+
expect_query_with_consistency(/DELETE/, :all) do
|
639
|
+
data_set.where(row_keys).delete
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
it 'should issue DELETE column with default consistency' do
|
644
|
+
expect_query_with_consistency(/DELETE/, :all) do
|
645
|
+
data_set.where(row_keys).delete(:title)
|
646
|
+
end
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
557
650
|
describe 'result enumeration' do
|
558
651
|
let(:row) { row_keys.merge(:title => 'Big Data') }
|
559
652
|
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require_relative '../spec_helper'
|
3
|
+
|
4
|
+
describe Cequel::Metal::Keyspace do
|
5
|
+
before :all do
|
6
|
+
cequel.schema.create_table(:posts) do
|
7
|
+
key :id, :int
|
8
|
+
column :title, :text
|
9
|
+
column :body, :text
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
after :each do
|
14
|
+
ids = cequel[:posts].select(:id).map { |row| row[:id] }
|
15
|
+
cequel[:posts].where(id: ids).delete if ids.any?
|
16
|
+
end
|
17
|
+
|
18
|
+
after :all do
|
19
|
+
cequel.schema.drop_table(:posts)
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '::batch' do
|
23
|
+
it 'should send enclosed write statements in bulk' do
|
24
|
+
expect(cequel).to receive(:execute).once.and_call_original
|
25
|
+
cequel.batch do
|
26
|
+
cequel[:posts].insert(id: 1, title: 'Hey')
|
27
|
+
cequel[:posts].where(id: 1).update(body: 'Body')
|
28
|
+
cequel[:posts].where(id: 1).delete(:title)
|
29
|
+
end
|
30
|
+
RSpec::Mocks.proxy_for(cequel).reset
|
31
|
+
expect(cequel[:posts].first).to eq({id: 1, title: nil, body: 'Body'}
|
32
|
+
.with_indifferent_access)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should auto-apply if option given' do
|
36
|
+
cequel.batch(auto_apply: 2) do
|
37
|
+
cequel[:posts].insert(id: 1, title: 'One')
|
38
|
+
expect(cequel[:posts].count).to be_zero
|
39
|
+
cequel[:posts].insert(id: 2, title: 'Two')
|
40
|
+
expect(cequel[:posts].count).to be(2)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should do nothing if no statements executed in batch' do
|
45
|
+
expect { cequel.batch {} }.to_not raise_error
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should execute unlogged batch if specified' do
|
49
|
+
expect_query_with_consistency(/BEGIN UNLOGGED BATCH/, anything) do
|
50
|
+
cequel.batch(unlogged: true) do
|
51
|
+
cequel[:posts].insert(id: 1, title: 'One')
|
52
|
+
cequel[:posts].insert(id: 2, title: 'Two')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should execute batch with given consistency' do
|
58
|
+
expect_query_with_consistency(/BEGIN BATCH/, :one) do
|
59
|
+
cequel.batch(consistency: :one) do
|
60
|
+
cequel[:posts].insert(id: 1, title: 'One')
|
61
|
+
cequel[:posts].insert(id: 2, title: 'Two')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should raise error if consistency specified in individual query in batch' do
|
67
|
+
expect {
|
68
|
+
cequel.batch(consistency: :one) do
|
69
|
+
cequel[:posts].consistency(:quorum).insert(id: 1, title: 'One')
|
70
|
+
end
|
71
|
+
}.to raise_error(ArgumentError)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -44,6 +44,12 @@ describe Cequel::Record::Associations do
|
|
44
44
|
before_destroy { self.class.callback_count += 1 }
|
45
45
|
end
|
46
46
|
|
47
|
+
model :Photo do
|
48
|
+
belongs_to :post, partition: true
|
49
|
+
key :id, :uuid, auto: true
|
50
|
+
column :caption, :text
|
51
|
+
end
|
52
|
+
|
47
53
|
describe '::belongs_to' do
|
48
54
|
let(:blog) { Blog.new { |blog| blog.subdomain = 'big-data' }}
|
49
55
|
let(:post) { Post.new }
|
@@ -52,6 +58,18 @@ describe Cequel::Record::Associations do
|
|
52
58
|
Post.key_column_names.first.should == :blog_subdomain
|
53
59
|
end
|
54
60
|
|
61
|
+
it 'should add parent key as the partition key' do
|
62
|
+
Post.partition_key_column_names.should == [:blog_subdomain]
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should add parent's keys as first keys" do
|
66
|
+
Comment.key_column_names.first(2).should == [:post_blog_subdomain, :post_id]
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should add parent's first key as partition key" do
|
70
|
+
Comment.partition_key_column_names.should == [:post_blog_subdomain]
|
71
|
+
end
|
72
|
+
|
55
73
|
it 'should provide accessors for association object' do
|
56
74
|
post.blog = blog
|
57
75
|
post.blog.should == blog
|
@@ -102,6 +120,72 @@ describe Cequel::Record::Associations do
|
|
102
120
|
end.to raise_error(Cequel::Record::InvalidRecordConfiguration)
|
103
121
|
end
|
104
122
|
|
123
|
+
context "with partition => true" do
|
124
|
+
let(:post) { Post.new { |post| post.blog_subdomain = 'big-data' }}
|
125
|
+
let(:photo) { Photo.new }
|
126
|
+
|
127
|
+
it "should add parent's keys as first keys" do
|
128
|
+
Photo.key_column_names.first(2).should == [:post_blog_subdomain, :post_id]
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should add parent's keys as partition keys" do
|
132
|
+
Photo.partition_key_column_names.should == [:post_blog_subdomain, :post_id]
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should provide accessors for association object' do
|
136
|
+
photo.post = post
|
137
|
+
photo.post.should == post
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'should set parent key(s) when setting association object' do
|
141
|
+
photo.post = post
|
142
|
+
photo.post_blog_subdomain.should == 'big-data'
|
143
|
+
photo.post_id.should == post.id
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should raise ArgumentError when parent is set without a key' do
|
147
|
+
post.blog_subdomain = nil
|
148
|
+
expect { photo.post = post }.to raise_error(ArgumentError)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should raise ArgumentError when parent is set to wrong class' do
|
152
|
+
expect { photo.post = photo }.to raise_error(ArgumentError)
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'should return Photo instance when parent keys are set directly' do
|
156
|
+
photo.post_blog_subdomain = 'big-data'
|
157
|
+
photo.post_id = post.id
|
158
|
+
photo.post.should == post
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should not hydrate parent instance when creating from keys' do
|
162
|
+
photo.post_blog_subdomain = 'big-data'
|
163
|
+
photo.post_id = post.id
|
164
|
+
disallow_queries!
|
165
|
+
photo.post.should_not be_loaded
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should not allow declaring belongs_to after key' do
|
169
|
+
expect do
|
170
|
+
Class.new do
|
171
|
+
include Cequel::Record
|
172
|
+
key :permalink, :text
|
173
|
+
belongs_to :post, partition: true
|
174
|
+
end
|
175
|
+
end.to raise_error(Cequel::Record::InvalidRecordConfiguration)
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should not allow declaring belongs_to more than once' do
|
179
|
+
expect do
|
180
|
+
Class.new do
|
181
|
+
include Cequel::Record
|
182
|
+
belongs_to :post, partition: true
|
183
|
+
belongs_to :user
|
184
|
+
end
|
185
|
+
end.to raise_error(Cequel::Record::InvalidRecordConfiguration)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
105
189
|
end
|
106
190
|
|
107
191
|
describe '::has_many' do
|
@@ -48,6 +48,15 @@ describe Cequel::Record::Persistence do
|
|
48
48
|
Blog.new.save
|
49
49
|
}.to raise_error(Cequel::Record::MissingKeyError)
|
50
50
|
end
|
51
|
+
|
52
|
+
it 'should save with specified consistency' do
|
53
|
+
expect_query_with_consistency(/INSERT/, :one) do
|
54
|
+
Blog.new do |blog|
|
55
|
+
blog.subdomain = 'cequel'
|
56
|
+
blog.name = 'Cequel'
|
57
|
+
end.save(consistency: :one)
|
58
|
+
end
|
59
|
+
end
|
51
60
|
end
|
52
61
|
|
53
62
|
context 'on update' do
|
@@ -78,6 +87,13 @@ describe Cequel::Record::Persistence do
|
|
78
87
|
blog.save
|
79
88
|
}.to raise_error(ArgumentError)
|
80
89
|
end
|
90
|
+
|
91
|
+
it 'should save with specified consistency' do
|
92
|
+
expect_query_with_consistency(/UPDATE/, :one) do
|
93
|
+
blog.name = 'Cequel'
|
94
|
+
blog.save(consistency: :one)
|
95
|
+
end
|
96
|
+
end
|
81
97
|
end
|
82
98
|
end
|
83
99
|
|
@@ -161,6 +177,13 @@ describe Cequel::Record::Persistence do
|
|
161
177
|
it 'should mark record transient' do
|
162
178
|
blog.should be_transient
|
163
179
|
end
|
180
|
+
|
181
|
+
it 'should destroy with specified consistency' do
|
182
|
+
blog = Blog.create(:subdomain => 'big-data', :name => 'Big Data')
|
183
|
+
expect_query_with_consistency(/DELETE/, :one) do
|
184
|
+
blog.destroy(consistency: :one)
|
185
|
+
end
|
186
|
+
end
|
164
187
|
end
|
165
188
|
end
|
166
189
|
|
@@ -147,7 +147,7 @@ describe Cequel::Record::Properties do
|
|
147
147
|
end
|
148
148
|
|
149
149
|
it 'should auto-generate UUID key' do
|
150
|
-
Post.new.id.should
|
150
|
+
Cequel.uuid?(Post.new.id).should be_true
|
151
151
|
end
|
152
152
|
|
153
153
|
it 'should raise ArgumentError if auto specified for non-UUID' do
|
@@ -38,7 +38,7 @@ describe Cequel::Record::RecordSet do
|
|
38
38
|
end
|
39
39
|
|
40
40
|
let(:subdomains) { blogs.map(&:subdomain) }
|
41
|
-
let(:uuids) { Array.new(2) {
|
41
|
+
let(:uuids) { Array.new(2) { Cequel.uuid }}
|
42
42
|
let(:now) { Time.at(Time.now.to_i) }
|
43
43
|
|
44
44
|
let(:blogs) do
|
@@ -110,7 +110,7 @@ describe Cequel::Record::RecordSet do
|
|
110
110
|
Comment.new(
|
111
111
|
:blog_subdomain => 'cassandra',
|
112
112
|
:permalink => 'cequel0',
|
113
|
-
:id =>
|
113
|
+
:id => Cequel.uuid(Time.now - 5 + i),
|
114
114
|
:body => "Comment #{i}"
|
115
115
|
)
|
116
116
|
end
|
@@ -353,10 +353,11 @@ describe Cequel::Record::RecordSet do
|
|
353
353
|
let(:records) { [posts, blogs, mongo_posts] }
|
354
354
|
|
355
355
|
it 'should respect :batch_size argument' do
|
356
|
-
cequel.should_receive(:
|
356
|
+
cequel.should_receive(:execute_with_consistency).twice.and_call_original
|
357
357
|
Blog.find_each(:batch_size => 2).map(&:subdomain).
|
358
358
|
should =~ subdomains
|
359
359
|
end
|
360
|
+
|
360
361
|
it 'should iterate over all keys' do
|
361
362
|
Post.find_each(:batch_size => 2).map(&:title).should =~
|
362
363
|
(0...5).flat_map { |i| ["Cequel #{i}", "Sequel #{i}", "Mongoid #{i}"] }
|
@@ -616,6 +617,14 @@ describe Cequel::Record::RecordSet do
|
|
616
617
|
end
|
617
618
|
end
|
618
619
|
|
620
|
+
describe '#consistency' do
|
621
|
+
it 'should perform query with specified consistency' do
|
622
|
+
expect_query_with_consistency(/SELECT/, :one) do
|
623
|
+
Post.consistency(:one).to_a
|
624
|
+
end
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
619
628
|
describe '#count' do
|
620
629
|
let(:records) { blogs }
|
621
630
|
|