cequel 1.0.0.pre.5 → 1.0.0.pre.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f830646acc29101f50ff14afb06933e15fa88e35
4
- data.tar.gz: c3d9d72fbf56847758e5de1191e7f48200b03423
3
+ metadata.gz: 720dcb8eb0924dd3f972efba9e93c2d96ae046ab
4
+ data.tar.gz: 36b08070aa03e28e6f0748e1c6a375e2ca57822f
5
5
  SHA512:
6
- metadata.gz: 981a73d5f5f4a6b5a07374df3f96ccdd599642927ea29aa652a13249234ffd18a40921aaa675345355de052072b94ecd8e8f2b63d47e16c2b1bdabbb7c7150e1
7
- data.tar.gz: dc9035f9e642cdffac8f3b83ab9ec2abacb3498580cdfe4e3b6e5b370ec8a70fe22ab4e5f20a9f702fc5d8eacda436098a73fb04592d7858a73f64bd1720e1c6
6
+ metadata.gz: c51e2ab0b17869ca3f6720e1c74b78c6f116ac705cce8aac49925750e4204012a7701ea1ed6ce4b51702a9f0a88ed2aad3d39609e5b57080ca9d2be1b0913fa6
7
+ data.tar.gz: 1dae89c0e31809ee91f54a137ca22396d8c591b5ebdf85efe9091ef53143674a71d9873b3b84b562c2deeb5eacad8ffd8dfe25d4fcb4706489656c5307e7ec81
@@ -43,7 +43,10 @@ module Cequel
43
43
  #
44
44
  def apply
45
45
  return if @statement_count.zero?
46
- @statement.append("APPLY BATCH\n")
46
+ if @statement_count > 1
47
+ @statement.prepend("BEGIN BATCH\n")
48
+ @statement.append("APPLY BATCH\n")
49
+ end
47
50
  @keyspace.execute(*@statement.args)
48
51
  end
49
52
 
@@ -51,7 +54,6 @@ module Cequel
51
54
 
52
55
  def reset
53
56
  @statement = Statement.new
54
- @statement.append("BEGIN BATCH\n")
55
57
  @statement_count = 0
56
58
  end
57
59
 
@@ -144,13 +144,14 @@ module Cequel
144
144
  # end
145
145
  #
146
146
  def batch(options = {})
147
- old_batch = get_batch
148
- new_batch = Batch.new(self, options)
149
- set_batch(new_batch)
150
- yield
151
- new_batch.apply
152
- ensure
153
- set_batch(old_batch)
147
+ return yield if get_batch
148
+ begin
149
+ new_batch = Batch.new(self, options)
150
+ set_batch(new_batch)
151
+ yield.tap { new_batch.apply }
152
+ ensure
153
+ set_batch(nil)
154
+ end
154
155
  end
155
156
 
156
157
  private
@@ -3,14 +3,19 @@ module Cequel
3
3
  module Metal
4
4
 
5
5
  class Statement
6
- attr_reader :bind_vars
6
+ attr_reader :bind_vars, :length
7
7
 
8
8
  def initialize
9
- @cql, @bind_vars = StringIO.new, []
9
+ @cql, @bind_vars = [], []
10
10
  end
11
11
 
12
12
  def cql
13
- @cql.string
13
+ @cql.join
14
+ end
15
+
16
+ def prepend(cql, *bind_vars)
17
+ @cql.unshift(cql)
18
+ @bind_vars.unshift(*bind_vars)
14
19
  end
15
20
 
16
21
  def append(cql, *bind_vars)
data/lib/cequel/record.rb CHANGED
@@ -8,6 +8,7 @@ require 'cequel/record/collection'
8
8
  require 'cequel/record/persistence'
9
9
  require 'cequel/record/record_set'
10
10
  require 'cequel/record/bound'
11
+ require 'cequel/record/lazy_record_collection'
11
12
  require 'cequel/record/scoped'
12
13
  require 'cequel/record/secondary_indexes'
13
14
  require 'cequel/record/associations'
@@ -12,11 +12,11 @@ module Cequel
12
12
  end
13
13
 
14
14
  def save(options = {})
15
- run_callbacks(:save) { super }
15
+ connection.batch { run_callbacks(:save) { super }}
16
16
  end
17
17
 
18
18
  def destroy
19
- run_callbacks(:destroy) { super }
19
+ connection.batch { run_callbacks(:destroy) { super }}
20
20
  end
21
21
 
22
22
  protected
@@ -0,0 +1,44 @@
1
+ module Cequel
2
+
3
+ module Record
4
+
5
+ class LazyRecordCollection < DelegateClass(Array)
6
+
7
+ def initialize(record_set)
8
+ raise ArgumentError if record_set.nil?
9
+
10
+ exploded_key_attributes = [{}].tap do |all_key_attributes|
11
+ record_set.key_columns.zip(record_set.scoped_key_attributes.values) do |column, values|
12
+ all_key_attributes.replace(Array(values).flat_map do |value|
13
+ all_key_attributes.map do |key_attributes|
14
+ key_attributes.merge(column.name => value)
15
+ end
16
+ end)
17
+ end
18
+ end
19
+
20
+ unloaded_records = exploded_key_attributes.map do |key_attributes|
21
+ record_set.target_class.new_empty(key_attributes, self)
22
+ end
23
+
24
+ super(unloaded_records)
25
+ @record_set = record_set
26
+ end
27
+
28
+ def load!
29
+ records_by_identity = index_by { |record| record.key_values }
30
+
31
+ record_set.find_each_row do |row|
32
+ identity = row.values_at(*record_set.key_column_names)
33
+ records_by_identity[identity].hydrate(row)
34
+ end
35
+ end
36
+
37
+ private
38
+ attr_reader :record_set
39
+
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -17,7 +17,7 @@ module Cequel
17
17
  end
18
18
 
19
19
  def hydrate(row)
20
- new_empty { hydrate(row) }
20
+ new_empty(row).__send__(:hydrated!)
21
21
  end
22
22
 
23
23
  end
@@ -42,11 +42,7 @@ module Cequel
42
42
 
43
43
  def load
44
44
  assert_keys_present!
45
- unless loaded?
46
- row = metal_scope.first
47
- hydrate(row) unless row.nil?
48
- collection_proxies.each_value { |collection| collection.loaded! }
49
- end
45
+ record_collection.load! unless loaded?
50
46
  self
51
47
  end
52
48
 
@@ -96,14 +92,22 @@ module Cequel
96
92
  !persisted?
97
93
  end
98
94
 
95
+ def hydrate(row)
96
+ @attributes = row
97
+ hydrated!
98
+ self
99
+ end
100
+
99
101
  protected
100
102
 
101
103
  def persisted!
102
104
  @persisted = true
105
+ self
103
106
  end
104
107
 
105
108
  def transient!
106
109
  @persisted = false
110
+ self
107
111
  end
108
112
 
109
113
  def create
@@ -159,8 +163,13 @@ module Cequel
159
163
  end
160
164
  end
161
165
 
162
- def hydrate(row)
163
- @attributes = row
166
+ def record_collection
167
+ @record_collection ||=
168
+ LazyRecordCollection.new(self.class.at(*key_values)).
169
+ tap { |set| set.__setobj__([self]) }
170
+ end
171
+
172
+ def hydrated!
164
173
  loaded!
165
174
  persisted!
166
175
  self
@@ -169,11 +178,11 @@ module Cequel
169
178
  def loaded!
170
179
  @loaded = true
171
180
  collection_proxies.each_value { |collection| collection.loaded! }
181
+ self
172
182
  end
173
183
 
174
184
  def metal_scope
175
- connection[table_name].
176
- where(key_attributes)
185
+ connection[table_name].where(key_attributes)
177
186
  end
178
187
 
179
188
  def attributes_for_create
@@ -111,9 +111,10 @@ module Cequel
111
111
 
112
112
  end
113
113
 
114
- def initialize(&block)
115
- @attributes, @collection_proxies = {}, {}
116
- instance_eval(&block) if block
114
+ # FIXME this isn't empty anymore! Rethink.
115
+ def initialize(attributes = {}, record_collection = nil)
116
+ @attributes, @record_collection = attributes, record_collection
117
+ @collection_proxies = {}
117
118
  end
118
119
 
119
120
  def attribute_names
@@ -12,10 +12,13 @@ module Cequel
12
12
  {:scoped_key_values => [], :select_columns => []}
13
13
  end
14
14
 
15
- def initialize(clazz, attributes = {})
15
+ attr_reader :target_class
16
+ attr_writer :unloaded_records
17
+
18
+ def initialize(target_class, attributes = {})
16
19
  attributes = self.class.default_attributes.merge!(attributes)
17
- @clazz, @attributes = clazz, attributes
18
- super(clazz)
20
+ @target_class, @attributes = target_class, attributes
21
+ super(target_class)
19
22
  end
20
23
 
21
24
  def all
@@ -32,11 +35,11 @@ module Cequel
32
35
  end
33
36
 
34
37
  def where(column_name, value)
35
- column = clazz.reflect_on_column(column_name)
38
+ column = target_class.reflect_on_column(column_name)
36
39
  raise IllegalQuery,
37
40
  "Can't scope by more than one indexed column in the same query" if scoped_indexed_column
38
41
  raise ArgumentError,
39
- "No column #{column_name} configured for #{clazz.name}" unless column
42
+ "No column #{column_name} configured for #{target_class.name}" unless column
40
43
  raise ArgumentError,
41
44
  "Use the `at` method to restrict scope by primary key" unless column.data_column?
42
45
  raise ArgumentError,
@@ -45,26 +48,20 @@ module Cequel
45
48
  end
46
49
 
47
50
  def at(*scoped_key_values)
48
- scoped do |attributes|
49
- type_cast_key_values = scoped_key_values.zip(unscoped_key_columns).
50
- map { |value, column| column.cast(value) }
51
- attributes[:scoped_key_values].concat(type_cast_key_values)
52
- end
51
+ warn "`at` is deprecated. Use `[]` instead"
52
+ scoped_key_values.
53
+ inject(self) { |record_set, key_value| record_set[key_value] }
53
54
  end
54
55
 
55
- def [](scoped_key_value)
56
- scoped_key_value = cast_range_key(scoped_key_value)
56
+ def [](*new_scoped_key_values)
57
+ new_scoped_key_values =
58
+ new_scoped_key_values.map(&method(:cast_range_key))
57
59
 
58
- if next_range_key_column
59
- at(scoped_key_value)
60
- else
61
- attributes = {}
62
- key_values = [*scoped_key_values, scoped_key_value]
63
- clazz.key_column_names.zip(key_values) do |key_name, key_value|
64
- attributes[key_name] = key_value
65
- end
66
- clazz.new_empty { @attributes = attributes }
67
- end
60
+ new_scoped_key_values =
61
+ new_scoped_key_values.first if new_scoped_key_values.one?
62
+
63
+ scoped { |attributes| attributes[:scoped_key_values] <<
64
+ new_scoped_key_values }.resolve_if_fully_specified
68
65
  end
69
66
 
70
67
  def find(*scoped_key_values)
@@ -72,7 +69,7 @@ module Cequel
72
69
  end
73
70
 
74
71
  def /(scoped_key_value)
75
- at(scoped_key_value)
72
+ self[scoped_key_value]
76
73
  end
77
74
 
78
75
  def after(start_key)
@@ -91,7 +88,7 @@ module Cequel
91
88
  end
92
89
 
93
90
  def from(start_key)
94
- unless single_partition?
91
+ unless partition_specified?
95
92
  raise IllegalQuery,
96
93
  "Can't construct exclusive range on partition key #{range_key_name}"
97
94
  end
@@ -99,7 +96,7 @@ module Cequel
99
96
  end
100
97
 
101
98
  def upto(end_key)
102
- unless single_partition?
99
+ unless partition_specified?
103
100
  raise IllegalQuery,
104
101
  "Can't construct exclusive range on partition key #{range_key_name}"
105
102
  end
@@ -107,7 +104,7 @@ module Cequel
107
104
  end
108
105
 
109
106
  def reverse
110
- unless single_partition?
107
+ unless partition_specified?
111
108
  raise IllegalQuery,
112
109
  "Can't reverse without scoping to partition key #{range_key_name}"
113
110
  end
@@ -134,7 +131,7 @@ module Cequel
134
131
 
135
132
  def find_each(options = {})
136
133
  return enum_for(:find_each, options) unless block_given?
137
- find_each_row(options) { |row| yield clazz.hydrate(row) }
134
+ find_each_row(options) { |row| yield target_class.hydrate(row) }
138
135
  end
139
136
 
140
137
  def find_each_row(options = {}, &block)
@@ -211,11 +208,11 @@ module Cequel
211
208
  end
212
209
 
213
210
  def scoped_key_columns
214
- clazz.key_columns.first(scoped_key_values.length)
211
+ target_class.key_columns.first(scoped_key_values.length)
215
212
  end
216
213
 
217
214
  def unscoped_key_columns
218
- clazz.key_columns.drop(scoped_key_values.length)
215
+ target_class.key_columns.drop(scoped_key_values.length)
219
216
  end
220
217
 
221
218
  def unscoped_key_names
@@ -238,27 +235,68 @@ module Cequel
238
235
  next_range_key_column.try(:name)
239
236
  end
240
237
 
241
- def single_partition?
242
- scoped_key_values.length >= clazz.partition_key_columns.length
238
+ def fully_specified?
239
+ scoped_key_values.length == target_class.key_columns.length
240
+ end
241
+
242
+ def partition_specified?
243
+ scoped_key_values.length >= target_class.partition_key_columns.length
244
+ end
245
+
246
+ def multiple_records_specified?
247
+ scoped_key_values.any? { |values| values.is_a?(Array) }
248
+ end
249
+
250
+ def resolve_if_fully_specified
251
+ if fully_specified?
252
+ if multiple_records_specified?
253
+ LazyRecordCollection.new(select_non_collection_columns!)
254
+ else
255
+ LazyRecordCollection.new(self).first
256
+ end
257
+ else
258
+ self
259
+ end
243
260
  end
244
261
 
245
262
  # Try to order results by the first clustering column. Fall back to partition key if none exist.
246
263
  def order_by_column
247
- clazz.clustering_columns.first.name if clazz.clustering_columns.any?
264
+ target_class.clustering_columns.first.name if target_class.clustering_columns.any?
265
+ end
266
+
267
+ def selects_collection_columns?
268
+ select_columns.any? do |column_name|
269
+ target_class.reflect_on_column(column_name).
270
+ is_a?(Cequel::Schema::CollectionColumn)
271
+ end
272
+ end
273
+
274
+ def select_non_collection_columns!
275
+ if selects_collection_columns?
276
+ raise ArgumentError,
277
+ "Can't scope by multiple keys when selecting a collection column."
278
+ end
279
+ if select_columns.empty?
280
+ non_collection_columns = target_class.columns.
281
+ reject { |column| column.is_a?(Cequel::Schema::CollectionColumn) }.
282
+ map { |column| column.name }
283
+ select(*non_collection_columns)
284
+ else
285
+ self
286
+ end
248
287
  end
249
288
 
250
289
  private
251
- attr_reader :clazz
252
- def_delegators :clazz, :connection
290
+ def_delegators :target_class, :connection
253
291
  def_delegator :range_key_column, :cast, :cast_range_key
254
292
  private :connection, :cast_range_key
255
293
 
256
294
  def method_missing(method, *args, &block)
257
- clazz.with_scope(self) { super }
295
+ target_class.with_scope(self) { super }
258
296
  end
259
297
 
260
298
  def construct_data_set
261
- data_set = connection[clazz.table_name]
299
+ data_set = connection[target_class.table_name]
262
300
  data_set = data_set.limit(row_limit) if row_limit
263
301
  data_set = data_set.select(*select_columns) if select_columns
264
302
  if scoped_key_values
@@ -292,7 +330,7 @@ module Cequel
292
330
  attributes_copy = Marshal.load(Marshal.dump(attributes))
293
331
  attributes_copy.merge!(new_attributes)
294
332
  attributes_copy.tap(&block) if block
295
- RecordSet.new(clazz, attributes_copy)
333
+ RecordSet.new(target_class, attributes_copy)
296
334
  end
297
335
 
298
336
  end
@@ -15,7 +15,7 @@ module Cequel
15
15
  module ClassMethods
16
16
  extend Forwardable
17
17
 
18
- def_delegators :table_schema, :key_columns, :key_column_names,
18
+ def_delegators :table_schema, :columns, :key_columns, :key_column_names,
19
19
  :partition_key_columns, :clustering_columns, :compact_storage?
20
20
  def_delegator :table_schema, :column, :reflect_on_column
21
21
 
@@ -1,3 +1,3 @@
1
1
  module Cequel
2
- VERSION = '1.0.0.pre.5'
2
+ VERSION = '1.0.0.pre.6'
3
3
  end
@@ -24,6 +24,28 @@ describe Cequel::Record::Callbacks do
24
24
 
25
25
  end
26
26
 
27
+ model :Comment do
28
+ belongs_to :post
29
+ key :id, :timeuuid, :auto => true
30
+ column :body, :text
31
+
32
+ before_save :create_post
33
+ after_save :run_instance_after_save
34
+
35
+ attr_writer :instance_after_save
36
+
37
+ private
38
+
39
+ def create_post
40
+ post = Post.create!(permalink: 'autopost', title: 'Auto Post')
41
+ self.post = post
42
+ end
43
+
44
+ def run_instance_after_save
45
+ @instance_after_save.call
46
+ end
47
+ end
48
+
27
49
  let(:new_post) do
28
50
  Post.new do |post|
29
51
  post.permalink = 'new-post'
@@ -83,4 +105,15 @@ describe Cequel::Record::Callbacks do
83
105
  it { should include(:before_destroy) }
84
106
  it { should include(:after_destroy) }
85
107
  end
108
+
109
+ describe 'atomic writes' do
110
+ it 'should run callbacks in a logged batch' do
111
+ comment = Comment.new(:body => 'Great web site!')
112
+ comment.instance_after_save =
113
+ -> { expect { Post.find('autopost') }.
114
+ to raise_error(Cequel::Record::RecordNotFound) }
115
+ comment.save!
116
+ Post.find('autopost').title.should == 'Auto Post'
117
+ end
118
+ end
86
119
  end
@@ -36,53 +36,92 @@ describe Cequel::Record::RecordSet do
36
36
  column :permalink, :ascii, index: true
37
37
  end
38
38
 
39
- let(:subdomains) { [] }
39
+ let(:subdomains) { blogs.map(&:subdomain) }
40
40
  let(:uuids) { Array.new(2) { CassandraCQL::UUID.new }}
41
41
  let(:now) { Time.at(Time.now.to_i) }
42
42
 
43
- before do
44
- cequel.batch do
45
- 3.times do |i|
46
- Blog.new do |blog|
47
- subdomains << blog.subdomain = "blog-#{i}"
48
- blog.name = "Blog #{i}"
49
- blog.description = "This is Blog number #{i}"
50
- end.save
43
+ let(:blogs) do
44
+ 3.times.map do |i|
45
+ Blog.new do |blog|
46
+ blog.subdomain = "blog-#{i}"
47
+ blog.name = "Blog #{i}"
48
+ blog.description = "This is Blog number #{i}"
51
49
  end
50
+ end
51
+ end
52
52
 
53
- 5.times do |i|
54
- cequel[:posts].insert(
55
- :blog_subdomain => 'cassandra',
56
- :permalink => "cequel#{i}",
57
- :title => "Cequel #{i}",
58
- :body => "Post number #{i}",
59
- :author_id => uuids[i%2]
60
- )
61
- cequel[:published_posts].insert(
62
- :blog_subdomain => 'cassandra',
63
- :published_at => max_uuid(now + (i - 4).minutes),
64
- :permalink => "cequel#{i}"
65
- )
66
- cequel[:posts].insert(
67
- :blog_subdomain => 'postgres',
68
- :permalink => "sequel#{i}",
69
- :title => "Sequel #{i}"
70
- )
71
- end
53
+ let(:cassandra_posts) do
54
+ 5.times.map do |i|
55
+ Post.new(
56
+ :blog_subdomain => 'cassandra',
57
+ :permalink => "cequel#{i}",
58
+ :title => "Cequel #{i}",
59
+ :body => "Post number #{i}",
60
+ :author_id => uuids[i%2]
61
+ )
62
+ end
63
+ end
72
64
 
73
- 5.times do |i|
74
- cequel[:comments].insert(
75
- :blog_subdomain => 'cassandra',
76
- :permalink => 'cequel0',
77
- :id => CassandraCQL::UUID.new(Time.now - 5 + i),
78
- :body => "Comment #{i}"
79
- )
80
- end
65
+ let(:published_posts) do
66
+ 5.times.map do |i|
67
+ PublishedPost.new(
68
+ :blog_subdomain => 'cassandra',
69
+ :published_at => max_uuid(now + (i - 4).minutes),
70
+ :permalink => "cequel#{i}"
71
+ )
72
+ end
73
+ end
74
+
75
+ let(:postgres_posts) do
76
+ 5.times.map do |i|
77
+ Post.new(
78
+ :blog_subdomain => 'postgres',
79
+ :permalink => "sequel#{i}",
80
+ :title => "Sequel #{i}"
81
+ )
82
+ end
83
+ end
84
+
85
+ let(:mongo_posts) do
86
+ 5.times.map do |i|
87
+ Post.new(
88
+ :blog_subdomain => 'mongo',
89
+ :permalink => "mongoid#{i}",
90
+ :title => "Mongoid #{i}"
91
+ )
92
+ end
93
+ end
94
+
95
+ let(:orm_posts) do
96
+ 5.times.map do |i|
97
+ Post.new(
98
+ :blog_subdomain => 'orms',
99
+ :permalink => "cequel#{i}",
100
+ :title => "Cequel ORM #{i}"
101
+ )
81
102
  end
82
103
  end
83
104
 
105
+ let(:posts) { [cassandra_posts, postgres_posts] }
106
+
107
+ let(:comments) do
108
+ 5.times.map do |i|
109
+ Comment.new(
110
+ :blog_subdomain => 'cassandra',
111
+ :permalink => 'cequel0',
112
+ :id => CassandraCQL::UUID.new(Time.now - 5 + i),
113
+ :body => "Comment #{i}"
114
+ )
115
+ end
116
+ end
117
+
118
+ let(:records) { posts }
119
+
120
+ before { cequel.batch { records.flatten.each { |record| record.save! }}}
121
+
84
122
  describe '::find' do
85
123
  context 'simple primary key' do
124
+ let(:records) { blogs }
86
125
  subject { Blog.find('blog-0') }
87
126
 
88
127
  its(:subdomain) { should == 'blog-0' }
@@ -104,6 +143,7 @@ describe Cequel::Record::RecordSet do
104
143
  end
105
144
 
106
145
  context 'compound primary key' do
146
+ let(:records) { cassandra_posts }
107
147
  subject { Post['cassandra'].find('cequel0') }
108
148
 
109
149
  its(:blog_subdomain) { should == 'cassandra' }
@@ -128,7 +168,8 @@ describe Cequel::Record::RecordSet do
128
168
  end
129
169
 
130
170
  describe '::[]' do
131
- context 'simple primary key' do
171
+ context 'fully specified simple primary key' do
172
+ let(:records) { blogs }
132
173
  subject { Blog['blog-0'] }
133
174
 
134
175
  it 'should not query the database' do
@@ -151,7 +192,32 @@ describe Cequel::Record::RecordSet do
151
192
  end
152
193
  end
153
194
 
154
- context 'compound primary key' do
195
+ context 'multiple simple primary keys' do
196
+ let(:records) { blogs }
197
+ subject { Blog['blog-0', 'blog-1'] }
198
+
199
+ it 'should return both specified records' do
200
+ subject.map(&:subdomain).should =~ %w(blog-0 blog-1)
201
+ end
202
+
203
+ it 'should not query the database' do
204
+ disallow_queries!
205
+ subject.map(&:subdomain)
206
+ end
207
+
208
+ it 'should load value lazily' do
209
+ subject.first.name.should == 'Blog 0'
210
+ end
211
+
212
+ it 'should load values for all referenced records on first access' do
213
+ max_statements! 1
214
+ subject.first.name.should == 'Blog 0'
215
+ subject.last.name.should == 'Blog 1'
216
+ end
217
+ end
218
+
219
+ context 'fully specified compound primary key' do
220
+ let(:records) { posts }
155
221
  subject { Post['cassandra']['cequel0'] }
156
222
 
157
223
  it 'should not query the database' do
@@ -175,61 +241,118 @@ describe Cequel::Record::RecordSet do
175
241
  subject.body.should == 'Post number 0'
176
242
  end
177
243
  end
244
+
245
+ context 'fully specified compound primary key with multiple clustering columns' do
246
+ let(:records) { posts }
247
+ subject { Post['cassandra']['cequel0', 'cequel1'] }
248
+
249
+ it 'should combine partition key with each clustering column' do
250
+ disallow_queries!
251
+ subject.map(&:key_values).
252
+ should == [['cassandra', 'cequel0'], ['cassandra', 'cequel1']]
253
+ end
254
+
255
+ it 'should lazily load all records when one record accessed' do
256
+ max_statements! 1
257
+ subject.first.title.should == 'Cequel 0'
258
+ subject.second.title.should == 'Cequel 1'
259
+ end
260
+
261
+ it 'should not allow collection columns to be selected' do
262
+ expect { Post.select(:tags)['cassandra']['cequel0', 'cequel1'] }.
263
+ to raise_error(ArgumentError)
264
+ end
265
+ end
266
+
267
+ context 'partially specified compound primary key' do
268
+ let(:records) { posts }
269
+ it 'should create partial collection if not all keys specified' do
270
+ Post['cassandra'].find_each(:batch_size => 2).map(&:title).
271
+ should == (0...5).map { |i| "Cequel #{i}" }
272
+ end
273
+ end
274
+
275
+ context 'partially specified compound primary key with multiple partition keys' do
276
+ let(:records) { posts }
277
+ subject { Post['cassandra', 'postgres'] }
278
+
279
+ it 'should return scope to keys' do
280
+ subject.map { |post| post.title }.should =~ (0...5).
281
+ map { |i| ["Cequel #{i}", "Sequel #{i}"] }.flatten
282
+ end
283
+ end
284
+
285
+ context 'fully specified compound primary key with multiple partition keys' do
286
+ let(:records) { [posts, orm_posts] }
287
+
288
+ subject { Post['cassandra', 'orms']['cequel0'] }
289
+
290
+ it 'should return collection of unloaded models' do
291
+ disallow_queries!
292
+ subject.map(&:key_values).
293
+ should == [['cassandra', 'cequel0'], ['orms', 'cequel0']]
294
+ end
295
+
296
+ it 'should lazy-load all records when properties of one accessed' do
297
+ max_statements! 1
298
+ subject.first.title.should == 'Cequel 0'
299
+ subject.second.title.should == 'Cequel ORM 0'
300
+ end
301
+ end
178
302
  end
179
303
 
180
304
  describe '#all' do
305
+ let(:records) { blogs }
306
+
181
307
  it 'should return all the records' do
182
308
  Blog.all.map(&:subdomain).should =~ subdomains
183
309
  end
184
310
  end
185
311
 
186
312
  describe '#find_each' do
313
+ let(:records) { [posts, blogs, mongo_posts] }
314
+
187
315
  it 'should respect :batch_size argument' do
188
316
  cequel.should_receive(:execute).twice.and_call_original
189
317
  Blog.find_each(:batch_size => 2).map(&:subdomain).
190
318
  should =~ subdomains
191
319
  end
192
320
  it 'should iterate over all keys' do
193
- Post.find_each(:batch_size => 2).map(&:title).
194
- should =~ (0...5).flat_map { |i| ["Cequel #{i}", "Sequel #{i}"] }
321
+ Post.find_each(:batch_size => 2).map(&:title).should =~
322
+ (0...5).flat_map { |i| ["Cequel #{i}", "Sequel #{i}", "Mongoid #{i}"] }
195
323
  end
196
324
  end
197
325
 
198
- describe '#at' do
326
+ describe '#[]' do
199
327
  it 'should return partial collection' do
200
- Post.at('cassandra').find_each(:batch_size => 2).map(&:title).
328
+ Post['cassandra'].find_each(:batch_size => 2).map(&:title).
201
329
  should == (0...5).map { |i| "Cequel #{i}" }
202
330
  end
203
331
 
204
332
  it 'should cast arguments correctly' do
205
- Post.at('cassandra'.force_encoding('ASCII-8BIT')).
333
+ Post['cassandra'.force_encoding('ASCII-8BIT')].
206
334
  find_each(:batch_size => 2).map(&:title).
207
335
  should == (0...5).map { |i| "Cequel #{i}" }
208
336
  end
209
337
  end
210
338
 
211
- describe '#[]' do
212
- it 'should create partial collection if not all keys specified' do
213
- Post['cassandra'].find_each(:batch_size => 2).map(&:title).
214
- should == (0...5).map { |i| "Cequel #{i}" }
215
- end
216
- end
217
-
218
339
  describe '#/' do
219
- it 'should behave like #at' do
340
+ it 'should behave like #[]' do
220
341
  (Post / 'cassandra').find_each(:batch_size => 2).map(&:title).
221
342
  should == (0...5).map { |i| "Cequel #{i}" }
222
343
  end
223
344
  end
224
345
 
225
346
  describe '#after' do
347
+ let(:records) { [posts, published_posts] }
348
+
226
349
  it 'should return collection after given key' do
227
- Post.at('cassandra').after('cequel1').map(&:title).
350
+ Post['cassandra'].after('cequel1').map(&:title).
228
351
  should == (2...5).map { |i| "Cequel #{i}" }
229
352
  end
230
353
 
231
354
  it 'should cast argument' do
232
- Post.at('cassandra').after('cequel1'.force_encoding('ASCII-8BIT')).
355
+ Post['cassandra'].after('cequel1'.force_encoding('ASCII-8BIT')).
233
356
  map(&:title).should == (2...5).map { |i| "Cequel #{i}" }
234
357
  end
235
358
 
@@ -240,13 +363,15 @@ describe Cequel::Record::RecordSet do
240
363
  end
241
364
 
242
365
  describe '#from' do
366
+ let(:records) { [posts, published_posts] }
367
+
243
368
  it 'should return collection starting with given key' do
244
- Post.at('cassandra').from('cequel1').map(&:title).
369
+ Post['cassandra'].from('cequel1').map(&:title).
245
370
  should == (1...5).map { |i| "Cequel #{i}" }
246
371
  end
247
372
 
248
373
  it 'should cast argument' do
249
- Post.at('cassandra').from('cequel1'.force_encoding('ASCII-8BIT')).
374
+ Post['cassandra'].from('cequel1'.force_encoding('ASCII-8BIT')).
250
375
  map(&:title).should == (1...5).map { |i| "Cequel #{i}" }
251
376
  end
252
377
 
@@ -262,8 +387,10 @@ describe Cequel::Record::RecordSet do
262
387
  end
263
388
 
264
389
  describe '#before' do
390
+ let(:records) { [posts, published_posts] }
391
+
265
392
  it 'should return collection before given key' do
266
- Post.at('cassandra').before('cequel3').map(&:title).
393
+ Post['cassandra'].before('cequel3').map(&:title).
267
394
  should == (0...3).map { |i| "Cequel #{i}" }
268
395
  end
269
396
 
@@ -273,19 +400,21 @@ describe Cequel::Record::RecordSet do
273
400
  end
274
401
 
275
402
  it 'should cast argument' do
276
- Post.at('cassandra').before('cequel3'.force_encoding('ASCII-8BIT')).
403
+ Post['cassandra'].before('cequel3'.force_encoding('ASCII-8BIT')).
277
404
  map(&:title).should == (0...3).map { |i| "Cequel #{i}" }
278
405
  end
279
406
  end
280
407
 
281
408
  describe '#upto' do
409
+ let(:records) { [posts, published_posts] }
410
+
282
411
  it 'should return collection up to given key' do
283
- Post.at('cassandra').upto('cequel3').map(&:title).
412
+ Post['cassandra'].upto('cequel3').map(&:title).
284
413
  should == (0..3).map { |i| "Cequel #{i}" }
285
414
  end
286
415
 
287
416
  it 'should cast argument' do
288
- Post.at('cassandra').upto('cequel3'.force_encoding('ASCII-8BIT')).
417
+ Post['cassandra'].upto('cequel3'.force_encoding('ASCII-8BIT')).
289
418
  map(&:title).should == (0..3).map { |i| "Cequel #{i}" }
290
419
  end
291
420
 
@@ -296,19 +425,21 @@ describe Cequel::Record::RecordSet do
296
425
  end
297
426
 
298
427
  describe '#in' do
428
+ let(:records) { [posts, published_posts] }
429
+
299
430
  it 'should return collection with inclusive upper bound' do
300
- Post.at('cassandra').in('cequel1'..'cequel3').map(&:title).
431
+ Post['cassandra'].in('cequel1'..'cequel3').map(&:title).
301
432
  should == (1..3).map { |i| "Cequel #{i}" }
302
433
  end
303
434
 
304
435
  it 'should cast arguments' do
305
- Post.at('cassandra').in('cequel1'.force_encoding('ASCII-8BIT')..
436
+ Post['cassandra'].in('cequel1'.force_encoding('ASCII-8BIT')..
306
437
  'cequel3'.force_encoding('ASCII-8BIT')).
307
438
  map(&:title).should == (1..3).map { |i| "Cequel #{i}" }
308
439
  end
309
440
 
310
441
  it 'should return collection with exclusive upper bound' do
311
- Post.at('cassandra').in('cequel1'...'cequel3').map(&:title).
442
+ Post['cassandra'].in('cequel1'...'cequel3').map(&:title).
312
443
  should == (1...3).map { |i| "Cequel #{i}" }
313
444
  end
314
445
 
@@ -324,18 +455,20 @@ describe Cequel::Record::RecordSet do
324
455
  end
325
456
 
326
457
  describe '#reverse' do
458
+ let(:records) { [posts, comments] }
459
+
327
460
  it 'should not call the database' do
328
461
  disallow_queries!
329
- Post.at('cassandra').reverse
462
+ Post['cassandra'].reverse
330
463
  end
331
464
 
332
465
  it 'should return collection in reverse' do
333
- Post.at('cassandra').reverse.map(&:title).
466
+ Post['cassandra'].reverse.map(&:title).
334
467
  should == (0...5).map { |i| "Cequel #{i}" }.reverse
335
468
  end
336
469
 
337
470
  it 'should batch iterate over collection in reverse' do
338
- Post.at('cassandra').reverse.find_each(:batch_size => 2).map(&:title).
471
+ Post['cassandra'].reverse.find_each(:batch_size => 2).map(&:title).
339
472
  should == (0...5).map { |i| "Cequel #{i}" }.reverse
340
473
  end
341
474
 
@@ -351,16 +484,18 @@ describe Cequel::Record::RecordSet do
351
484
 
352
485
  describe 'last' do
353
486
  it 'should return the last instance' do
354
- Post.at('cassandra').last.title.should == "Cequel 4"
487
+ Post['cassandra'].last.title.should == "Cequel 4"
355
488
  end
356
489
 
357
490
  it 'should return the last N instances if specified' do
358
- Post.at('cassandra').last(3).map(&:title).
491
+ Post['cassandra'].last(3).map(&:title).
359
492
  should == ["Cequel 2", "Cequel 3", "Cequel 4"]
360
493
  end
361
494
  end
362
495
 
363
496
  describe '#first' do
497
+ let(:records) { blogs }
498
+
364
499
  context 'with no arguments' do
365
500
  it 'should return an arbitrary record' do
366
501
  subdomains.should include(Blog.first.subdomain)
@@ -377,12 +512,16 @@ describe Cequel::Record::RecordSet do
377
512
  end
378
513
 
379
514
  describe '#limit' do
380
- it 'should return the number of blogs requested' do
515
+ let(:records) { blogs }
516
+
517
+ it 'should return the number of records requested' do
381
518
  Blog.limit(2).should have(2).entries
382
519
  end
383
520
  end
384
521
 
385
522
  describe '#select' do
523
+ let(:records) { blogs }
524
+
386
525
  context 'with no block' do
387
526
  subject { Blog.select(:subdomain, :name).first }
388
527
 
@@ -435,6 +574,8 @@ describe Cequel::Record::RecordSet do
435
574
  end
436
575
 
437
576
  describe '#count' do
577
+ let(:records) { blogs }
578
+
438
579
  it 'should count records' do
439
580
  Blog.count.should == 3
440
581
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.5
4
+ version: 1.0.0.pre.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mat Brown
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2013-11-08 00:00:00.000000000 Z
15
+ date: 2013-11-09 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: activesupport
@@ -62,14 +62,14 @@ dependencies:
62
62
  requirements:
63
63
  - - ~>
64
64
  - !ruby/object:Gem::Version
65
- version: 0.9.2
65
+ version: '1.1'
66
66
  type: :runtime
67
67
  prerelease: false
68
68
  version_requirements: !ruby/object:Gem::Requirement
69
69
  requirements:
70
70
  - - ~>
71
71
  - !ruby/object:Gem::Version
72
- version: 0.9.2
72
+ version: '1.1'
73
73
  - !ruby/object:Gem::Dependency
74
74
  name: i18n
75
75
  requirement: !ruby/object:Gem::Requirement
@@ -160,6 +160,7 @@ files:
160
160
  - lib/cequel/record/dirty.rb
161
161
  - lib/cequel/record/errors.rb
162
162
  - lib/cequel/record/has_many_association.rb
163
+ - lib/cequel/record/lazy_record_collection.rb
163
164
  - lib/cequel/record/mass_assignment.rb
164
165
  - lib/cequel/record/persistence.rb
165
166
  - lib/cequel/record/properties.rb