cequel 0.0.0 → 0.4.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.
Files changed (71) hide show
  1. data/lib/cequel.rb +16 -0
  2. data/lib/cequel/batch.rb +58 -0
  3. data/lib/cequel/cql_row_specification.rb +22 -0
  4. data/lib/cequel/data_set.rb +346 -0
  5. data/lib/cequel/errors.rb +4 -0
  6. data/lib/cequel/keyspace.rb +106 -0
  7. data/lib/cequel/model.rb +95 -0
  8. data/lib/cequel/model/associations.rb +120 -0
  9. data/lib/cequel/model/callbacks.rb +32 -0
  10. data/lib/cequel/model/class_internals.rb +48 -0
  11. data/lib/cequel/model/column.rb +20 -0
  12. data/lib/cequel/model/dictionary.rb +202 -0
  13. data/lib/cequel/model/dirty.rb +53 -0
  14. data/lib/cequel/model/dynamic.rb +31 -0
  15. data/lib/cequel/model/errors.rb +13 -0
  16. data/lib/cequel/model/inheritable.rb +48 -0
  17. data/lib/cequel/model/instance_internals.rb +23 -0
  18. data/lib/cequel/model/local_association.rb +42 -0
  19. data/lib/cequel/model/magic.rb +79 -0
  20. data/lib/cequel/model/mass_assignment_security.rb +21 -0
  21. data/lib/cequel/model/naming.rb +17 -0
  22. data/lib/cequel/model/observer.rb +42 -0
  23. data/lib/cequel/model/persistence.rb +173 -0
  24. data/lib/cequel/model/properties.rb +143 -0
  25. data/lib/cequel/model/railtie.rb +33 -0
  26. data/lib/cequel/model/remote_association.rb +40 -0
  27. data/lib/cequel/model/scope.rb +362 -0
  28. data/lib/cequel/model/scoped.rb +50 -0
  29. data/lib/cequel/model/subclass_internals.rb +45 -0
  30. data/lib/cequel/model/timestamps.rb +52 -0
  31. data/lib/cequel/model/translation.rb +17 -0
  32. data/lib/cequel/model/validations.rb +50 -0
  33. data/lib/cequel/new_relic_instrumentation.rb +22 -0
  34. data/lib/cequel/row_specification.rb +63 -0
  35. data/lib/cequel/statement.rb +23 -0
  36. data/lib/cequel/version.rb +3 -0
  37. data/spec/environment.rb +3 -0
  38. data/spec/examples/data_set_spec.rb +382 -0
  39. data/spec/examples/keyspace_spec.rb +63 -0
  40. data/spec/examples/model/associations_spec.rb +109 -0
  41. data/spec/examples/model/callbacks_spec.rb +79 -0
  42. data/spec/examples/model/dictionary_spec.rb +413 -0
  43. data/spec/examples/model/dirty_spec.rb +39 -0
  44. data/spec/examples/model/dynamic_spec.rb +41 -0
  45. data/spec/examples/model/inheritable_spec.rb +45 -0
  46. data/spec/examples/model/magic_spec.rb +199 -0
  47. data/spec/examples/model/mass_assignment_security_spec.rb +13 -0
  48. data/spec/examples/model/naming_spec.rb +9 -0
  49. data/spec/examples/model/observer_spec.rb +86 -0
  50. data/spec/examples/model/persistence_spec.rb +201 -0
  51. data/spec/examples/model/properties_spec.rb +81 -0
  52. data/spec/examples/model/scope_spec.rb +677 -0
  53. data/spec/examples/model/serialization_spec.rb +20 -0
  54. data/spec/examples/model/spec_helper.rb +12 -0
  55. data/spec/examples/model/timestamps_spec.rb +52 -0
  56. data/spec/examples/model/translation_spec.rb +23 -0
  57. data/spec/examples/model/validations_spec.rb +86 -0
  58. data/spec/examples/spec_helper.rb +9 -0
  59. data/spec/models/asset.rb +21 -0
  60. data/spec/models/asset_observer.rb +5 -0
  61. data/spec/models/blog.rb +14 -0
  62. data/spec/models/blog_posts.rb +6 -0
  63. data/spec/models/category.rb +9 -0
  64. data/spec/models/comment.rb +12 -0
  65. data/spec/models/photo.rb +5 -0
  66. data/spec/models/post.rb +88 -0
  67. data/spec/models/post_comments.rb +14 -0
  68. data/spec/models/post_observer.rb +43 -0
  69. data/spec/support/helpers.rb +26 -0
  70. data/spec/support/result_stub.rb +27 -0
  71. metadata +125 -23
@@ -0,0 +1,50 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Scoped
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ delegate :consistency, :count, :first, :limit, :select, :where,
11
+ :find_in_batches, :find_each, :find_rows_in_batches, :find_each_row,
12
+ :to => :all
13
+
14
+ def default_scope(scope)
15
+ @_cequel.default_scope = scope
16
+ end
17
+
18
+ def all
19
+ @_cequel.current_scope || @_cequel.default_scope || empty_scope
20
+ end
21
+
22
+ def select(*rows)
23
+ all.select(*rows)
24
+ end
25
+
26
+ def with_scope(scope)
27
+ @_cequel.synchronize do
28
+ old_scope = @_cequel.current_scope
29
+ begin
30
+ @_cequel.current_scope = scope
31
+ yield
32
+ ensure
33
+ @_cequel.current_scope = old_scope
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def empty_scope
41
+ Scope.new(self, [column_family])
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,45 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ class SubclassInternals < ClassInternals
6
+
7
+ def initialize(clazz, super_internals)
8
+ super(clazz)
9
+ @super = super_internals
10
+ @columns = {}
11
+ end
12
+
13
+ def index_preference
14
+ @super.index_preference + @index_preference
15
+ end
16
+
17
+ def key
18
+ @super.key
19
+ end
20
+
21
+ def columns
22
+ @super.columns.merge(@columns)
23
+ end
24
+
25
+ def type_column
26
+ @super.type_column
27
+ end
28
+
29
+ def column_family_name
30
+ @super.column_family_name
31
+ end
32
+
33
+ def base_class
34
+ @super.base_class
35
+ end
36
+
37
+ def associations
38
+ @super.associations.merge(super)
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,52 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Timestamps
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include CreatedAt
11
+ include UpdatedAt
12
+ end
13
+
14
+ module CreatedAt
15
+
16
+ extend ActiveSupport::Concern
17
+
18
+ included do
19
+ column :created_at, :timestamp
20
+ before_create :_set_created_at
21
+ end
22
+
23
+ private
24
+
25
+ def _set_created_at
26
+ self.created_at = Time.now
27
+ end
28
+
29
+ end
30
+
31
+ module UpdatedAt
32
+
33
+ extend ActiveSupport::Concern
34
+
35
+ included do
36
+ column :updated_at, :timestamp
37
+ before_save :_set_updated_at
38
+ end
39
+
40
+ private
41
+
42
+ def _set_updated_at
43
+ self.updated_at = Time.now if transient? || changed?
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,17 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Translation
6
+
7
+ extend ActiveModel::Translation
8
+
9
+ def i18n_scope
10
+ 'cequel'
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,50 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Validations
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include ActiveModel::Validations
11
+ alias_method_chain :valid?, :callbacks # XXX is there no better way?
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def create!(attributes = {}, &block)
17
+ instance = new(attributes, &block)
18
+ instance.save!
19
+ end
20
+
21
+ end
22
+
23
+ def save(*args)
24
+ if valid?
25
+ super
26
+ true
27
+ else
28
+ false
29
+ end
30
+ end
31
+
32
+ def save!(*args)
33
+ raise RecordInvalid, errors.full_messages.join("; ") unless save
34
+ self
35
+ end
36
+
37
+ def update_attributes!(*args)
38
+ raise RecordInvalid, errors.full_messages.join("; ") unless update_attributes(*args)
39
+ self
40
+ end
41
+
42
+ def valid_with_callbacks?
43
+ run_callbacks(:validation) { valid_without_callbacks? }
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'new_relic/agent/method_tracer'
3
+ rescue LoadError => e
4
+ raise LoadError, "Can't use NewRelic instrumentation without NewRelic gem"
5
+ end
6
+
7
+ module Cequel
8
+
9
+ module NewRelicInstrumentation
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ include NewRelic::Agent::MethodTracer
15
+ add_method_tracer :execute, 'Database/Cassandra/#{args[0][/^[A-Z ]*[A-Z]/].sub(/ FROM$/, \'\')}'
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ Cequel::Keyspace.module_eval { include Cequel::NewRelicInstrumentation }
@@ -0,0 +1,63 @@
1
+ module Cequel
2
+
3
+ #
4
+ # @private
5
+ #
6
+ class RowSpecification
7
+
8
+ def self.build(column_values)
9
+ column_values.map { |column, value| new(column, value) }
10
+ end
11
+
12
+ attr_reader :column, :value
13
+
14
+ def initialize(column, value)
15
+ @column, @value = column, value
16
+ end
17
+
18
+ def cql
19
+ case @value
20
+ when DataSet
21
+ subquery_cql
22
+ when Array
23
+ if @value.length == 1
24
+ ["? = ?", @column, @value.first]
25
+ else
26
+ [
27
+ "? IN (?)",
28
+ @column, @value
29
+ ]
30
+ end
31
+ else
32
+ ["? = ?", @column, @value]
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def subquery_cql
39
+ values = values_from_subquery
40
+ case values.length
41
+ when 0
42
+ raise EmptySubquery,
43
+ "Unable to generate CQL row specification: subquery (#{@value.cql}) returned no results."
44
+ when 1
45
+ RowSpecification.new(@column, values.first).cql
46
+ else
47
+ RowSpecification.new(@column, values).cql
48
+ end
49
+ end
50
+
51
+ def values_from_subquery
52
+ results = @value.map do |row|
53
+ if row.length > 1
54
+ raise ArgumentError,
55
+ "Subqueries must return a single row per column"
56
+ end
57
+ row.values.first
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,23 @@
1
+ module Cequel
2
+ class Statement
3
+ attr_reader :bind_vars
4
+
5
+ def initialize
6
+ @cql, @bind_vars = StringIO.new, []
7
+ end
8
+
9
+ def cql
10
+ @cql.string
11
+ end
12
+
13
+ def append(cql, *bind_vars)
14
+ @cql << cql
15
+ @bind_vars.concat(bind_vars)
16
+ self
17
+ end
18
+
19
+ def args
20
+ [cql, *bind_vars]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Cequel
2
+ VERSION = '0.4.0'
3
+ end
@@ -0,0 +1,3 @@
1
+ require 'bundler'
2
+
3
+ Bundler.require(:default, :test)
@@ -0,0 +1,382 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Cequel::DataSet do
4
+ describe '#insert' do
5
+ it 'should insert a row' do
6
+ connection.should_receive(:execute).
7
+ with "INSERT INTO posts (?) VALUES (?)", [:id, :title], [1, 'Fun times']
8
+
9
+ cequel[:posts].insert(:id => 1, :title => 'Fun times')
10
+ end
11
+
12
+ it 'should include consistency argument' do
13
+ connection.should_receive(:execute).
14
+ with "INSERT INTO posts (?) VALUES (?) USING CONSISTENCY QUORUM", [:id, :title], [1, 'Fun times']
15
+
16
+ cequel[:posts].insert(
17
+ {:id => 1, :title => 'Fun times'},
18
+ :consistency => :quorum
19
+ )
20
+ end
21
+
22
+ it 'should include ttl argument' do
23
+ connection.should_receive(:execute).
24
+ with "INSERT INTO posts (?) VALUES (?) USING TTL 600", [:id, :title], [1, 'Fun times']
25
+
26
+ cequel[:posts].insert(
27
+ {:id => 1, :title => 'Fun times'},
28
+ :ttl => 10.minutes
29
+ )
30
+ end
31
+
32
+ it 'should include timestamp argument' do
33
+ time = Time.now - 10.minutes
34
+ connection.should_receive(:execute).
35
+ with "INSERT INTO posts (?) VALUES (?) USING TIMESTAMP #{time.to_i}", [:id, :title], [1, 'Fun times']
36
+
37
+ cequel[:posts].insert(
38
+ {:id => 1, :title => 'Fun times'},
39
+ :timestamp => time
40
+ )
41
+ end
42
+
43
+ it 'should include multiple arguments joined by AND' do
44
+ time = Time.now - 10.minutes
45
+ connection.should_receive(:execute).
46
+ with "INSERT INTO posts (?) VALUES (?) USING CONSISTENCY QUORUM AND TTL 600 AND TIMESTAMP #{time.to_i}",
47
+ [:id, :title], [1, 'Fun times']
48
+
49
+ cequel[:posts].insert(
50
+ {:id => 1, :title => 'Fun times'},
51
+ :consistency => :quorum,
52
+ :ttl => 600,
53
+ :timestamp => time
54
+ )
55
+ end
56
+ end
57
+
58
+ describe '#update' do
59
+ it 'should send basic update statement' do
60
+ connection.should_receive(:execute).
61
+ with "UPDATE posts SET ? = ?, ? = ?", :title, 'Fun times', :body, 'Fun'
62
+
63
+ cequel[:posts].update(:title => 'Fun times', :body => 'Fun')
64
+ end
65
+
66
+ it 'should send update statement with options' do
67
+ time = Time.now - 10.minutes
68
+
69
+ connection.should_receive(:execute).
70
+ with "UPDATE posts USING CONSISTENCY QUORUM AND TTL 600 AND TIMESTAMP #{time.to_i} SET ? = ?, ? = ?", :title, 'Fun times', :body, 'Fun'
71
+
72
+ cequel[:posts].update(
73
+ {:title => 'Fun times', :body => 'Fun'},
74
+ :consistency => :quorum, :ttl => 600, :timestamp => time
75
+ )
76
+ end
77
+
78
+ it 'should send update statement scoped to current row specifications' do
79
+ connection.should_receive(:execute).
80
+ with "UPDATE posts SET ? = ? WHERE ? = ?", :title, 'Fun', :id, 4
81
+
82
+ cequel[:posts].where(:id => 4).update(:title => 'Fun')
83
+ end
84
+
85
+ it 'should do nothing if row specification contains empty subquery' do
86
+ connection.stub(:execute).with("SELECT ? FROM posts", [:blog_id]).
87
+ and_return result_stub
88
+
89
+ expect do
90
+ cequel[:blogs].where(:id => cequel[:posts].select(:blog_id)).
91
+ update(:title => 'Fun')
92
+ end.to_not raise_error
93
+ end
94
+ end
95
+
96
+ describe '#delete' do
97
+ it 'should send basic delete statement' do
98
+ connection.should_receive(:execute).
99
+ with 'DELETE FROM posts'
100
+
101
+ cequel[:posts].delete
102
+ end
103
+
104
+ it 'should send delete statement for specified columns' do
105
+ connection.should_receive(:execute).
106
+ with 'DELETE ? FROM posts', [:title, :body]
107
+
108
+ cequel[:posts].delete(:title, :body)
109
+ end
110
+
111
+ it 'should send delete statement with persistence options' do
112
+ time = Time.now - 10.minutes
113
+
114
+ connection.should_receive(:execute).
115
+ with "DELETE ? FROM posts USING CONSISTENCY QUORUM AND TIMESTAMP #{time.to_i}", [:title, :body]
116
+
117
+ cequel[:posts].delete(
118
+ :title, :body,
119
+ :consistency => :quorum, :timestamp => time
120
+ )
121
+ end
122
+
123
+ it 'should send delete statement with scoped row specifications' do
124
+ connection.should_receive(:execute).
125
+ with "DELETE FROM posts WHERE ? = ?", :id, 4
126
+
127
+ cequel[:posts].where(:id => 4).delete
128
+ end
129
+
130
+ it 'should not do anything if scoped to empty subquery' do
131
+ connection.stub(:execute).with("SELECT ? FROM posts", [:blog_id]).
132
+ and_return result_stub
133
+
134
+ expect do
135
+ cequel[:blogs].where(:id => cequel[:posts].select(:blog_id)).
136
+ delete
137
+ end.to_not raise_error
138
+ end
139
+ end
140
+
141
+ describe '#truncate' do
142
+ it 'should send a TRUNCATE statement' do
143
+ connection.should_receive(:execute).with("TRUNCATE posts")
144
+
145
+ cequel[:posts].truncate
146
+ end
147
+ end
148
+
149
+ describe '#cql' do
150
+ it 'should generate select statement with all columns' do
151
+ cequel[:posts].cql.should == ['SELECT * FROM posts']
152
+ end
153
+ end
154
+
155
+ describe '#select' do
156
+ it 'should generate select statement with given columns' do
157
+ cequel[:posts].select(:id, :title).cql.
158
+ should == ['SELECT ? FROM posts', [:id, :title]]
159
+ end
160
+
161
+ it 'should accept array argument' do
162
+ cequel[:posts].select([:id, :title]).cql.
163
+ should == ['SELECT ? FROM posts', [:id, :title]]
164
+ end
165
+
166
+ it 'should combine multiple selects' do
167
+ cequel[:posts].select(:id).select(:title).cql.
168
+ should == ['SELECT ? FROM posts', [:id, :title]]
169
+ end
170
+
171
+ it 'should accept :first option' do
172
+ cequel[:posts].select(:first => 100).cql.
173
+ should == ['SELECT FIRST 100 * FROM posts']
174
+ end
175
+
176
+ it 'should accept :last option' do
177
+ cequel[:posts].select(:last => 100).cql.
178
+ should == ['SELECT FIRST 100 REVERSED * FROM posts']
179
+ end
180
+
181
+ it 'should accept column range' do
182
+ cequel[:posts].select(1..10).cql.
183
+ should == ['SELECT ?..? FROM posts', 1, 10]
184
+ end
185
+
186
+ it 'should accept :from option' do
187
+ cequel[:posts].select(:from => 10).cql.
188
+ should == ['SELECT ?..? FROM posts', 10, '']
189
+ end
190
+
191
+ it 'should combine range and column limit options' do
192
+ cequel[:posts].select(:first => 100, :from => 10).cql.
193
+ should == ['SELECT FIRST 100 ?..? FROM posts', 10, '']
194
+ end
195
+
196
+ it 'should chain select options' do
197
+ cequel[:posts].select(:first => 100).select(:from => 10).cql.
198
+ should == ['SELECT FIRST 100 ?..? FROM posts', 10, '']
199
+ end
200
+ end
201
+
202
+ describe '#select!' do
203
+ it 'should generate select statement with given columns' do
204
+ cequel[:posts].select(:id, :title).select!(:published).cql.
205
+ should == ['SELECT ? FROM posts', [:published]]
206
+ end
207
+ end
208
+
209
+ describe '#where' do
210
+ it 'should build WHERE statement from hash' do
211
+ cequel[:posts].where(:title => 'Hey').cql.
212
+ should == ["SELECT * FROM posts WHERE ? = ?", :title, 'Hey']
213
+ end
214
+
215
+ it 'should build WHERE statement from multi-element hash' do
216
+ cequel[:posts].where(:title => 'Hey', :body => 'Guy').cql.
217
+ should == ["SELECT * FROM posts WHERE ? = ? AND ? = ?", :title, 'Hey', :body, 'Guy']
218
+ end
219
+
220
+ it 'should build WHERE statement with IN' do
221
+ cequel[:posts].where(:id => [1, 2, 3, 4]).cql.
222
+ should == ['SELECT * FROM posts WHERE ? IN (?)', :id, [1, 2, 3, 4]]
223
+ end
224
+
225
+ it 'should use = if provided one-element array' do
226
+ cequel[:posts].where(:id => [1]).cql.
227
+ should == ['SELECT * FROM posts WHERE ? = ?', :id, 1]
228
+ end
229
+
230
+ it 'should build WHERE statement from CQL string' do
231
+ cequel[:posts].where("title = ?", 'Hey').cql.
232
+ should == ["SELECT * FROM posts WHERE title = ?", 'Hey']
233
+ end
234
+
235
+ it 'should build WHERE statement from CQL string with bind variables' do
236
+ cequel[:posts].where("title = ?", 'Hey').cql.
237
+ should == ["SELECT * FROM posts WHERE title = ?", 'Hey']
238
+ end
239
+
240
+ it 'should aggregate multiple WHERE statements' do
241
+ cequel[:posts].where(:title => 'Hey').where('body = ?', 'Sup').cql.
242
+ should == ["SELECT * FROM posts WHERE ? = ? AND body = ?", :title, 'Hey', 'Sup']
243
+ end
244
+
245
+ it 'should take a data set as a condition and perform an IN statement' do
246
+ connection.stub(:execute).
247
+ with("SELECT ? FROM posts WHERE ? = ?", [:blog_id], :title, 'Blog').
248
+ and_return result_stub(
249
+ {:blog_id => 1},
250
+ {:blog_id => 3}
251
+ )
252
+
253
+ cequel[:blogs].where(
254
+ :id => cequel[:posts].select(:blog_id).where(:title => 'Blog')
255
+ ).cql.
256
+ should == ['SELECT * FROM blogs WHERE ? IN (?)', :id, [1, 3]]
257
+ end
258
+
259
+ it 'should raise EmptySubquery if inner data set has no results' do
260
+ connection.stub(:execute).
261
+ with("SELECT ? FROM posts WHERE ? = ?", [:blog_id], :title, 'Blog').
262
+ and_return result_stub
263
+
264
+ expect do
265
+ cequel[:blogs].where(
266
+ :id => cequel[:posts].select(:blog_id).where(:title => 'Blog')
267
+ ).cql
268
+ end.to raise_error(Cequel::EmptySubquery)
269
+ end
270
+
271
+ end
272
+
273
+ describe '#where!' do
274
+ it 'should override chained conditions' do
275
+ cequel[:posts].where(:title => 'Hey').where!(:title => 'Cequel').cql.
276
+ should == ["SELECT * FROM posts WHERE ? = ?", :title, 'Cequel']
277
+ end
278
+ end
279
+
280
+ describe '#consistency' do
281
+ it 'should add USING CONSISTENCY to select' do
282
+ cequel[:posts].consistency(:quorum).cql.
283
+ should == ["SELECT * FROM posts USING CONSISTENCY QUORUM"]
284
+ end
285
+ end
286
+
287
+ describe '#limit' do
288
+ it 'should add LIMIT' do
289
+ cequel[:posts].limit(2).cql.
290
+ should == ['SELECT * FROM posts LIMIT 2']
291
+ end
292
+ end
293
+
294
+ describe 'chaining scopes' do
295
+ it 'should aggregate all scope options' do
296
+ cequel[:posts].
297
+ select(:id, :title).
298
+ consistency(:quorum).
299
+ where(:title => 'Hey').
300
+ limit(3).cql.
301
+ should == ["SELECT ? FROM posts USING CONSISTENCY QUORUM WHERE ? = ? LIMIT 3", [:id, :title], :title, 'Hey']
302
+ end
303
+ end
304
+
305
+ describe 'result enumeration' do
306
+ it 'should enumerate over results' do
307
+ connection.stub(:execute).with("SELECT * FROM posts").
308
+ and_return result_stub('id' => 1, 'title' => 'Hey')
309
+
310
+ cequel[:posts].to_a.should == [{'id' => 1, 'title' => 'Hey'}]
311
+ end
312
+
313
+ it 'should provide results with indifferent access' do
314
+ connection.stub(:execute).with("SELECT * FROM posts").
315
+ and_return result_stub('id' => 1, 'title' => 'Hey')
316
+
317
+ cequel[:posts].to_a.first[:id].should == 1
318
+ end
319
+
320
+ it 'should not run query if no block given to #each' do
321
+ expect { cequel[:posts].each }.to_not raise_error
322
+ end
323
+
324
+ it 'should return Enumerator if no block given to #each' do
325
+ connection.stub(:execute).with("SELECT * FROM posts").
326
+ and_return result_stub('id' => 1, 'title' => 'Hey')
327
+
328
+ cequel[:posts].each.each_with_index.map { |row, i| [row[:id], i] }.
329
+ should == [[1, 0]]
330
+ end
331
+
332
+ it 'should return no results if subquery is empty' do
333
+ connection.stub(:execute).with("SELECT ? FROM posts", [:blog_id]).
334
+ and_return result_stub
335
+
336
+ cequel[:blogs].where(:id => cequel[:posts].select(:blog_id)).to_a.
337
+ should == []
338
+ end
339
+ end
340
+
341
+ describe '#first' do
342
+ it 'should run a query with LIMIT 1 and return first row' do
343
+ connection.stub(:execute).with("SELECT * FROM posts LIMIT 1").
344
+ and_return result_stub('id' => 1, 'title' => 'Hey')
345
+
346
+ cequel[:posts].first.should == {'id' => 1, 'title' => 'Hey'}
347
+ end
348
+
349
+ it 'should return nil if subquery returns empty results' do
350
+ connection.stub(:execute).with("SELECT ? FROM posts", [:blog_id]).
351
+ and_return result_stub
352
+
353
+ cequel[:blogs].where(:id => cequel[:posts].select(:blog_id)).first.
354
+ should be_nil
355
+ end
356
+ end
357
+
358
+ describe '#count' do
359
+ it 'should run a count query and return count' do
360
+ connection.stub(:execute).with("SELECT COUNT(*) FROM posts").
361
+ and_return result_stub('count' => 4)
362
+
363
+ cequel[:posts].count.should == 4
364
+ end
365
+
366
+ it 'should return 0 if subquery returns no results' do
367
+ connection.stub(:execute).with("SELECT ? FROM posts", [:blog_id]).
368
+ and_return result_stub
369
+
370
+ cequel[:blogs].where(:id => cequel[:posts].select(:blog_id)).count.
371
+ should == 0
372
+ end
373
+
374
+ it 'should use limit if specified' do
375
+ connection.stub(:execute).with("SELECT COUNT(*) FROM posts LIMIT 100000").
376
+ and_return result_stub('count' => 4)
377
+
378
+ cequel[:posts].limit(100_000).count.should == 4
379
+ end
380
+ end
381
+
382
+ end