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.
- data/lib/cequel.rb +16 -0
- data/lib/cequel/batch.rb +58 -0
- data/lib/cequel/cql_row_specification.rb +22 -0
- data/lib/cequel/data_set.rb +346 -0
- data/lib/cequel/errors.rb +4 -0
- data/lib/cequel/keyspace.rb +106 -0
- data/lib/cequel/model.rb +95 -0
- data/lib/cequel/model/associations.rb +120 -0
- data/lib/cequel/model/callbacks.rb +32 -0
- data/lib/cequel/model/class_internals.rb +48 -0
- data/lib/cequel/model/column.rb +20 -0
- data/lib/cequel/model/dictionary.rb +202 -0
- data/lib/cequel/model/dirty.rb +53 -0
- data/lib/cequel/model/dynamic.rb +31 -0
- data/lib/cequel/model/errors.rb +13 -0
- data/lib/cequel/model/inheritable.rb +48 -0
- data/lib/cequel/model/instance_internals.rb +23 -0
- data/lib/cequel/model/local_association.rb +42 -0
- data/lib/cequel/model/magic.rb +79 -0
- data/lib/cequel/model/mass_assignment_security.rb +21 -0
- data/lib/cequel/model/naming.rb +17 -0
- data/lib/cequel/model/observer.rb +42 -0
- data/lib/cequel/model/persistence.rb +173 -0
- data/lib/cequel/model/properties.rb +143 -0
- data/lib/cequel/model/railtie.rb +33 -0
- data/lib/cequel/model/remote_association.rb +40 -0
- data/lib/cequel/model/scope.rb +362 -0
- data/lib/cequel/model/scoped.rb +50 -0
- data/lib/cequel/model/subclass_internals.rb +45 -0
- data/lib/cequel/model/timestamps.rb +52 -0
- data/lib/cequel/model/translation.rb +17 -0
- data/lib/cequel/model/validations.rb +50 -0
- data/lib/cequel/new_relic_instrumentation.rb +22 -0
- data/lib/cequel/row_specification.rb +63 -0
- data/lib/cequel/statement.rb +23 -0
- data/lib/cequel/version.rb +3 -0
- data/spec/environment.rb +3 -0
- data/spec/examples/data_set_spec.rb +382 -0
- data/spec/examples/keyspace_spec.rb +63 -0
- data/spec/examples/model/associations_spec.rb +109 -0
- data/spec/examples/model/callbacks_spec.rb +79 -0
- data/spec/examples/model/dictionary_spec.rb +413 -0
- data/spec/examples/model/dirty_spec.rb +39 -0
- data/spec/examples/model/dynamic_spec.rb +41 -0
- data/spec/examples/model/inheritable_spec.rb +45 -0
- data/spec/examples/model/magic_spec.rb +199 -0
- data/spec/examples/model/mass_assignment_security_spec.rb +13 -0
- data/spec/examples/model/naming_spec.rb +9 -0
- data/spec/examples/model/observer_spec.rb +86 -0
- data/spec/examples/model/persistence_spec.rb +201 -0
- data/spec/examples/model/properties_spec.rb +81 -0
- data/spec/examples/model/scope_spec.rb +677 -0
- data/spec/examples/model/serialization_spec.rb +20 -0
- data/spec/examples/model/spec_helper.rb +12 -0
- data/spec/examples/model/timestamps_spec.rb +52 -0
- data/spec/examples/model/translation_spec.rb +23 -0
- data/spec/examples/model/validations_spec.rb +86 -0
- data/spec/examples/spec_helper.rb +9 -0
- data/spec/models/asset.rb +21 -0
- data/spec/models/asset_observer.rb +5 -0
- data/spec/models/blog.rb +14 -0
- data/spec/models/blog_posts.rb +6 -0
- data/spec/models/category.rb +9 -0
- data/spec/models/comment.rb +12 -0
- data/spec/models/photo.rb +5 -0
- data/spec/models/post.rb +88 -0
- data/spec/models/post_comments.rb +14 -0
- data/spec/models/post_observer.rb +43 -0
- data/spec/support/helpers.rb +26 -0
- data/spec/support/result_stub.rb +27 -0
- 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,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
|
data/spec/environment.rb
ADDED
@@ -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
|