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

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 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