cequel 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Appraisals +3 -1
- data/CHANGELOG.md +6 -0
- data/Gemfile +5 -5
- data/Gemfile.lock +17 -68
- data/README.md +11 -3
- data/lib/cequel/record.rb +3 -3
- data/lib/cequel/record/finders.rb +102 -0
- data/lib/cequel/record/properties.rb +8 -0
- data/lib/cequel/record/record_set.rb +91 -29
- data/lib/cequel/version.rb +1 -1
- data/spec/examples/record/finders_spec.rb +177 -0
- data/spec/examples/record/record_set_spec.rb +98 -19
- data/spec/support/helpers.rb +2 -2
- metadata +24 -59
- data/lib/cequel/record/secondary_indexes.rb +0 -68
- data/spec/examples/record/secondary_index_spec.rb +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8151f255eac647140677b154e3d786f1ed817a26
|
4
|
+
data.tar.gz: e7b3214a86a25d8c6d98aa68e9d5e4a992731ab8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95d754925f448719477b3981b89f9be29d23ed6209708bc8675ad839f08da3283c98fc3c9cbdca2afc9eaf62019c46ca7783b0ded8d7da7d446e3fed5a95c87e
|
7
|
+
data.tar.gz: 60dca007fbbb8c678eb5f5219c90824b1bc80e1411a16a246d14f664498c693902eed64aef349cda3762a095399feba39ed5ee703ac41810005bf869388b7ba8
|
data/Appraisals
CHANGED
@@ -4,12 +4,14 @@ end
|
|
4
4
|
|
5
5
|
appraise "rails-3.2" do
|
6
6
|
gem "activesupport", "~> 3.2.0"
|
7
|
+
gem "tzinfo", "~> 0.3"
|
7
8
|
end
|
8
9
|
|
9
10
|
appraise "rails-3.1" do
|
10
11
|
gem "activesupport", "~> 3.1.0"
|
12
|
+
gem "tzinfo", "~> 0.3"
|
11
13
|
end
|
12
14
|
|
13
15
|
appraise "thrift" do
|
14
|
-
gem "cassandra-cql"
|
16
|
+
gem "cassandra-cql", "~> 1.2"
|
15
17
|
end
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
@@ -3,11 +3,11 @@ source 'https://rubygems.org'
|
|
3
3
|
gemspec
|
4
4
|
|
5
5
|
group :debug do
|
6
|
-
gem 'debugger', :platforms => :mri_19
|
7
|
-
gem 'byebug', :platforms => [:mri_20, :mri_21]
|
8
|
-
gem 'pry'
|
6
|
+
gem 'debugger', '~> 1.6', :platforms => :mri_19
|
7
|
+
gem 'byebug', '~> 2.7', :platforms => [:mri_20, :mri_21]
|
8
|
+
gem 'pry', '~> 0.9'
|
9
9
|
end
|
10
10
|
|
11
|
-
gem 'racc', :platforms => :rbx
|
11
|
+
gem 'racc', '~> 1.4', :platforms => :rbx
|
12
12
|
gem 'rubysl', '~> 2.0', :platforms => :rbx
|
13
|
-
gem 'psych', :platforms => :rbx
|
13
|
+
gem 'psych', '~> 2.0', :platforms => :rbx
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cequel (1.
|
5
|
-
activemodel (>= 3.1)
|
6
|
-
cql-rb
|
4
|
+
cequel (1.2.0)
|
5
|
+
activemodel (>= 3.1, < 5.0)
|
6
|
+
cql-rb (~> 1.2)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
@@ -17,14 +17,12 @@ GEM
|
|
17
17
|
multi_json (~> 1.3)
|
18
18
|
thread_safe (~> 0.1)
|
19
19
|
tzinfo (~> 0.3.37)
|
20
|
-
addressable (2.3.5)
|
21
20
|
appraisal (0.5.2)
|
22
21
|
bundler
|
23
22
|
rake
|
24
23
|
ast (1.1.0)
|
25
|
-
atomic (1.1.
|
26
|
-
atomic (1.1.
|
27
|
-
backports (3.6.0)
|
24
|
+
atomic (1.1.16)
|
25
|
+
atomic (1.1.16-java)
|
28
26
|
builder (3.1.4)
|
29
27
|
byebug (2.7.0)
|
30
28
|
columnize (~> 0.3)
|
@@ -39,40 +37,14 @@ GEM
|
|
39
37
|
debugger-linecache (1.2.0)
|
40
38
|
debugger-ruby_core_source (1.3.2)
|
41
39
|
diff-lcs (1.2.5)
|
42
|
-
ethon (0.6.3)
|
43
|
-
ffi (>= 1.3.0)
|
44
|
-
mime-types (~> 1.18)
|
45
|
-
faraday (0.8.9)
|
46
|
-
multipart-post (~> 1.2.0)
|
47
|
-
faraday_middleware (0.9.0)
|
48
|
-
faraday (>= 0.7.4, < 0.9)
|
49
|
-
ffi (1.9.3)
|
50
40
|
ffi (1.9.3-java)
|
51
41
|
ffi2-generators (0.1.1)
|
52
|
-
gh (0.13.2)
|
53
|
-
addressable
|
54
|
-
backports
|
55
|
-
faraday (~> 0.8)
|
56
|
-
multi_json (~> 1.0)
|
57
|
-
net-http-persistent (>= 2.7)
|
58
|
-
net-http-pipeline
|
59
|
-
hashr (0.0.22)
|
60
|
-
highline (1.6.21)
|
61
42
|
i18n (0.6.9)
|
62
43
|
json (1.8.1)
|
63
44
|
json (1.8.1-java)
|
64
|
-
launchy (2.4.2)
|
65
|
-
addressable (~> 2.3)
|
66
|
-
launchy (2.4.2-java)
|
67
|
-
addressable (~> 2.3)
|
68
|
-
spoon (~> 0.0.1)
|
69
45
|
method_source (0.8.2)
|
70
|
-
mime-types (1.25.1)
|
71
46
|
minitest (4.7.5)
|
72
|
-
multi_json (1.9.
|
73
|
-
multipart-post (1.2.0)
|
74
|
-
net-http-persistent (2.9.4)
|
75
|
-
net-http-pipeline (1.0.1)
|
47
|
+
multi_json (1.9.2)
|
76
48
|
parser (2.1.7)
|
77
49
|
ast (~> 1.1)
|
78
50
|
slop (~> 3.4, >= 3.4.5)
|
@@ -87,8 +59,6 @@ GEM
|
|
87
59
|
slop (~> 3.4)
|
88
60
|
spoon (~> 0.0)
|
89
61
|
psych (2.0.4)
|
90
|
-
pusher-client (0.4.0)
|
91
|
-
websocket (~> 1.0.0)
|
92
62
|
racc (1.4.11)
|
93
63
|
rainbow (2.0.0)
|
94
64
|
rake (10.1.1)
|
@@ -100,7 +70,7 @@ GEM
|
|
100
70
|
rspec-expectations (2.14.5)
|
101
71
|
diff-lcs (>= 1.1.3, < 2.0)
|
102
72
|
rspec-mocks (2.14.6)
|
103
|
-
rubocop (0.19.
|
73
|
+
rubocop (0.19.1)
|
104
74
|
json (>= 1.7.7, < 2)
|
105
75
|
parser (~> 2.1.7)
|
106
76
|
powerpack (~> 0.0.6)
|
@@ -309,33 +279,15 @@ GEM
|
|
309
279
|
rubysl-xmlrpc (2.0.0)
|
310
280
|
rubysl-yaml (2.0.4)
|
311
281
|
rubysl-zlib (2.0.1)
|
312
|
-
safe_yaml (0.9.7)
|
313
282
|
slop (3.5.0)
|
314
283
|
spoon (0.0.4)
|
315
284
|
ffi
|
316
|
-
thread_safe (0.
|
285
|
+
thread_safe (0.3.1)
|
317
286
|
atomic (>= 1.1.7, < 2)
|
318
|
-
thread_safe (0.
|
287
|
+
thread_safe (0.3.1-java)
|
319
288
|
atomic (>= 1.1.7, < 2)
|
320
|
-
travis (1.6.8)
|
321
|
-
addressable (~> 2.3)
|
322
|
-
backports
|
323
|
-
faraday (~> 0.8.7)
|
324
|
-
faraday_middleware (~> 0.9)
|
325
|
-
gh (~> 0.13)
|
326
|
-
highline (~> 1.6)
|
327
|
-
launchy (~> 2.1)
|
328
|
-
pry (~> 0.9)
|
329
|
-
pusher-client (~> 0.4)
|
330
|
-
typhoeus (~> 0.6)
|
331
|
-
travis-lint (1.8.0)
|
332
|
-
hashr (~> 0.0.22)
|
333
|
-
safe_yaml (~> 0.9.0)
|
334
|
-
typhoeus (0.6.7)
|
335
|
-
ethon (~> 0.6.2)
|
336
289
|
tzinfo (0.3.39)
|
337
|
-
|
338
|
-
yard (0.8.7.3)
|
290
|
+
yard (0.8.7.4)
|
339
291
|
|
340
292
|
PLATFORMS
|
341
293
|
java
|
@@ -343,17 +295,14 @@ PLATFORMS
|
|
343
295
|
|
344
296
|
DEPENDENCIES
|
345
297
|
appraisal (~> 0.5)
|
346
|
-
byebug
|
298
|
+
byebug (~> 2.7)
|
347
299
|
cequel!
|
348
|
-
debugger
|
349
|
-
pry
|
350
|
-
psych
|
351
|
-
racc
|
352
|
-
rake
|
300
|
+
debugger (~> 1.6)
|
301
|
+
pry (~> 0.9)
|
302
|
+
psych (~> 2.0)
|
303
|
+
racc (~> 1.4)
|
304
|
+
rake (~> 10.1)
|
353
305
|
rspec (~> 2.0)
|
354
|
-
rubocop
|
306
|
+
rubocop (~> 0.19)
|
355
307
|
rubysl (~> 2.0)
|
356
|
-
travis
|
357
|
-
travis-lint
|
358
|
-
tzinfo
|
359
308
|
yard (~> 0.6)
|
data/README.md
CHANGED
@@ -155,6 +155,17 @@ the `[]` operator, these methods operate on the first unscoped primary key:
|
|
155
155
|
Post['bigdata'].after(last_id) # scopes posts with blog_subdomain="bigdata" and id > last_id
|
156
156
|
```
|
157
157
|
|
158
|
+
You can also use `where` to scope to primary key columns, but a primary key
|
159
|
+
column can only be scoped if all the columns that come before it are also
|
160
|
+
scoped:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
Post.where(blog_subdomain: 'bigdata') # this is fine
|
164
|
+
Post.where(blog_subdomain: 'bigdata', permalink: 'cassandra') # also fine
|
165
|
+
Post.where(blog_subdomain: 'bigdata').where(permalink: 'cassandra') # also fine
|
166
|
+
Post.where(permalink: 'cassandra') # bad: can't use permalink without blog_subdomain
|
167
|
+
```
|
168
|
+
|
158
169
|
Note that record sets always load records in batches; Cassandra does not support
|
159
170
|
result sets of unbounded size. This process is transparent to you but you'll see
|
160
171
|
multiple queries in your logs if you're iterating over a huge result set.
|
@@ -308,9 +319,6 @@ You can also call the `where` method directly on record sets:
|
|
308
319
|
Post.where(:author_id, id)
|
309
320
|
```
|
310
321
|
|
311
|
-
Note that `where` is only for secondary indexed columns; use `[]` to scope
|
312
|
-
record sets by primary keys.
|
313
|
-
|
314
322
|
### Consistency tuning ###
|
315
323
|
|
316
324
|
Cassandra supports [tunable
|
data/lib/cequel/record.rb
CHANGED
@@ -13,7 +13,7 @@ require 'cequel/record/data_set_builder'
|
|
13
13
|
require 'cequel/record/bound'
|
14
14
|
require 'cequel/record/lazy_record_collection'
|
15
15
|
require 'cequel/record/scoped'
|
16
|
-
require 'cequel/record/
|
16
|
+
require 'cequel/record/finders'
|
17
17
|
require 'cequel/record/associations'
|
18
18
|
require 'cequel/record/association_collection'
|
19
19
|
require 'cequel/record/belongs_to_association'
|
@@ -64,11 +64,11 @@ module Cequel
|
|
64
64
|
#
|
65
65
|
# @see Properties Defining properties
|
66
66
|
# @see Collection Collection columns
|
67
|
-
# @see SecondaryIndexes Defining secondary indexes
|
68
67
|
# @see Associations Defining associations between records
|
69
68
|
# @see Persistence Creating, updating, and destroying records
|
70
69
|
# @see BulkWrites Updating and destroying records in bulk
|
71
70
|
# @see RecordSet Loading records from the database
|
71
|
+
# @see Finders Magic finder methods
|
72
72
|
# @see MassAssignment Mass-assignment protection and strong attributes
|
73
73
|
# @see Callbacks Lifecycle hooks
|
74
74
|
# @see Validations
|
@@ -84,7 +84,7 @@ module Cequel
|
|
84
84
|
include Persistence
|
85
85
|
include Associations
|
86
86
|
include Scoped
|
87
|
-
extend
|
87
|
+
extend Finders
|
88
88
|
include MassAssignment
|
89
89
|
include Callbacks
|
90
90
|
include Validations
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Cequel
|
2
|
+
module Record
|
3
|
+
#
|
4
|
+
# Cequel provides finder methods to construct scopes for looking up records
|
5
|
+
# by primary key or secondary indexed columns.
|
6
|
+
#
|
7
|
+
# @example Example model class
|
8
|
+
# class Post
|
9
|
+
# key :blog_subdomain, :text
|
10
|
+
# key :id, :timeuuid, auto: true
|
11
|
+
# column :title, :text
|
12
|
+
# column :body, :text
|
13
|
+
# column :author_id, :uuid, index: true # this column has a secondary
|
14
|
+
# # index
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Using some but not all primary key columns
|
18
|
+
# # return an Array of all posts with given subdomain (greedy load)
|
19
|
+
# Post.find_all_by_blog_subdomain(subdomain)
|
20
|
+
#
|
21
|
+
# # return a {RecordSet} of all posts with the given subdomain (lazy
|
22
|
+
# # load)
|
23
|
+
# Post.with_subdomain(subdomain)
|
24
|
+
#
|
25
|
+
# @example Using all primary key columns
|
26
|
+
# # return the first post with the given subdomain and id, or nil if none
|
27
|
+
# Post.find_by_blog_subdomain_and_id(subdomain, id)
|
28
|
+
#
|
29
|
+
# # return a record set to the post with the given subdomain and id
|
30
|
+
# # (one element array if exists, empty array otherwise)
|
31
|
+
# Post.with_blog_subdomain_and_id(subdomain, id)
|
32
|
+
#
|
33
|
+
# @example Chaining
|
34
|
+
# # return the first post with the given subdomain and id, or nil if none
|
35
|
+
# # Note that find_by_id can only be called on a scope that already has a
|
36
|
+
# # filter value for blog_subdomain
|
37
|
+
# Post.with_blog_subdomain(subdomain).find_by_id(id)
|
38
|
+
#
|
39
|
+
# @example Using a secondary index
|
40
|
+
# # return the first record with the author_id
|
41
|
+
# Post.find_by_author_id(id)
|
42
|
+
#
|
43
|
+
# # return an Array of all records with the author_id
|
44
|
+
# Post.find_all_by_author_id(id)
|
45
|
+
#
|
46
|
+
# # return a RecordSet scoped to the author_id
|
47
|
+
# Post.with_author_id(id)
|
48
|
+
#
|
49
|
+
# @since 1.2.0
|
50
|
+
#
|
51
|
+
module Finders
|
52
|
+
private
|
53
|
+
|
54
|
+
def key(*)
|
55
|
+
if key_columns.any?
|
56
|
+
def_key_finders('find_all_by', '.entries')
|
57
|
+
undef_key_finders('find_by')
|
58
|
+
end
|
59
|
+
super
|
60
|
+
def_key_finders('find_by', '.first')
|
61
|
+
def_key_finders('with')
|
62
|
+
end
|
63
|
+
|
64
|
+
def column(name, type, options = {})
|
65
|
+
super
|
66
|
+
if options[:index]
|
67
|
+
def_finder('with', [name])
|
68
|
+
def_finder('find_by', [name], '.first')
|
69
|
+
def_finder('find_all_by', [name], '.entries')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def def_key_finders(method_prefix, scope_operation = '')
|
74
|
+
def_finder(method_prefix, key_column_names, scope_operation)
|
75
|
+
def_finder(method_prefix, key_column_names.last(1), scope_operation)
|
76
|
+
end
|
77
|
+
|
78
|
+
def def_finder(method_prefix, column_names, scope_operation = '')
|
79
|
+
arg_names = column_names.join(', ')
|
80
|
+
method_suffix = finder_method_suffix(column_names)
|
81
|
+
column_filter_expr = column_names
|
82
|
+
.map { |name| "#{name}: #{name}" }.join(', ')
|
83
|
+
|
84
|
+
singleton_class.module_eval(<<-RUBY, __FILE__, __LINE__+1)
|
85
|
+
def #{method_prefix}_#{method_suffix}(#{arg_names})
|
86
|
+
where(#{column_filter_expr})#{scope_operation}
|
87
|
+
end
|
88
|
+
RUBY
|
89
|
+
end
|
90
|
+
|
91
|
+
def undef_key_finders(method_prefix)
|
92
|
+
method_suffix = finder_method_suffix(key_column_names)
|
93
|
+
method_name = "#{method_prefix}_#{method_suffix}"
|
94
|
+
singleton_class.module_eval { undef_method(method_name) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def finder_method_suffix(column_names)
|
98
|
+
column_names.join('_and_')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -116,8 +116,16 @@ module Cequel
|
|
116
116
|
# @param options [Options] options for the column
|
117
117
|
# @option options [Object,Proc] :default a default value for the
|
118
118
|
# column, or a proc that returns a default value for the column
|
119
|
+
# @option options [Boolean,Symbol] :index create a secondary index on
|
120
|
+
# this column
|
119
121
|
# @return [void]
|
120
122
|
#
|
123
|
+
# @note Secondary indexes are not nearly as flexible as primary keys:
|
124
|
+
# you cannot query for multiple values or for ranges of values. You
|
125
|
+
# also cannot combine a secondary index restriction with a primary
|
126
|
+
# key restriction in the same query, nor can you combine more than
|
127
|
+
# one secondary index restriction in the same query.
|
128
|
+
#
|
121
129
|
def column(name, type, options = {})
|
122
130
|
def_accessors(name)
|
123
131
|
set_attribute_default(name, options[:default])
|
@@ -181,38 +181,38 @@ module Cequel
|
|
181
181
|
# Filter the record set to records containing a given value in an indexed
|
182
182
|
# column
|
183
183
|
#
|
184
|
-
# @
|
185
|
-
#
|
186
|
-
#
|
187
|
-
#
|
188
|
-
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
#
|
192
|
-
#
|
193
|
-
#
|
184
|
+
# @overload where(column_name, value)
|
185
|
+
# @param column_name [Symbol] column for filter
|
186
|
+
# @param value value to match in given column
|
187
|
+
# @return [RecordSet] record set with filter applied
|
188
|
+
# @deprecated
|
189
|
+
#
|
190
|
+
# @overload where(column_values)
|
191
|
+
# @param column_values [Hash] map of key column names to values
|
192
|
+
# @return [RecordSet] record set with filter applied
|
193
|
+
#
|
194
|
+
# @raise [IllegalQuery] if applying filter would generate an impossible
|
195
|
+
# query
|
196
|
+
# @raise [ArgumentError] if the specified column is not a column that
|
197
|
+
# can be filtered on
|
198
|
+
#
|
199
|
+
# @note Filtering on a primary key requires also filtering on all prior
|
200
|
+
# primary keys
|
194
201
|
# @note Only one secondary index filter can be used in a given query
|
195
202
|
# @note Secondary index filters cannot be mixed with primary key filters
|
196
203
|
#
|
197
|
-
def where(
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
"No column #{column_name} configured for #{target_class.name}"
|
206
|
-
end
|
207
|
-
unless column.data_column?
|
208
|
-
fail ArgumentError,
|
209
|
-
"Use the `at` method to restrict scope by primary key"
|
210
|
-
end
|
211
|
-
unless column.indexed?
|
204
|
+
def where(*args)
|
205
|
+
if args.length == 1
|
206
|
+
column_filters = args.first.symbolize_keys
|
207
|
+
elsif args.length == 2
|
208
|
+
warn "where(column_name, value) is deprecated. Use " \
|
209
|
+
"where(column_name => value) instead"
|
210
|
+
column_filters = {args.first.to_sym => args.second}
|
211
|
+
else
|
212
212
|
fail ArgumentError,
|
213
|
-
"
|
213
|
+
"wrong number of arguments (#{args.length} for 1..2)"
|
214
214
|
end
|
215
|
-
|
215
|
+
filter_columns(column_filters)
|
216
216
|
end
|
217
217
|
|
218
218
|
#
|
@@ -639,6 +639,11 @@ module Cequel
|
|
639
639
|
entries == other.to_a
|
640
640
|
end
|
641
641
|
|
642
|
+
# @private
|
643
|
+
def to_ary
|
644
|
+
entries
|
645
|
+
end
|
646
|
+
|
642
647
|
protected
|
643
648
|
|
644
649
|
attr_reader :attributes
|
@@ -683,6 +688,57 @@ module Cequel
|
|
683
688
|
end
|
684
689
|
end
|
685
690
|
|
691
|
+
def filter_columns(column_values)
|
692
|
+
return self if column_values.empty?
|
693
|
+
|
694
|
+
if column_values.key?(next_unscoped_key_name)
|
695
|
+
filter_primary_key(column_values.delete(next_unscoped_key_name))
|
696
|
+
else
|
697
|
+
filter_secondary_index(*column_values.shift)
|
698
|
+
end.filter_columns(column_values)
|
699
|
+
end
|
700
|
+
|
701
|
+
def filter_primary_key(value)
|
702
|
+
if scoped_indexed_column
|
703
|
+
fail IllegalQuery,
|
704
|
+
"Can't filter by both primary key and secondary index"
|
705
|
+
end
|
706
|
+
if value.is_a?(Range)
|
707
|
+
self.in(value)
|
708
|
+
else
|
709
|
+
scoped { |attributes| attributes[:scoped_key_values] << value }
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
def filter_secondary_index(column_name, value)
|
714
|
+
column = target_class.reflect_on_column(column_name)
|
715
|
+
if column.nil?
|
716
|
+
fail ArgumentError,
|
717
|
+
"No column #{column_name} configured for #{target_class.name}"
|
718
|
+
end
|
719
|
+
if column.key?
|
720
|
+
missing_column_names = unscoped_key_names.take_while do |key_name|
|
721
|
+
key_name != column_name
|
722
|
+
end
|
723
|
+
fail IllegalQuery,
|
724
|
+
"Can't scope key column #{column_name} without also scoping " \
|
725
|
+
"#{missing_column_names.join(', ')}"
|
726
|
+
end
|
727
|
+
if scoped_key_values.any?
|
728
|
+
fail IllegalQuery,
|
729
|
+
"Can't filter by both primary key and secondary index"
|
730
|
+
end
|
731
|
+
if scoped_indexed_column
|
732
|
+
fail IllegalQuery,
|
733
|
+
"Can't scope by more than one indexed column in the same query"
|
734
|
+
end
|
735
|
+
unless column.indexed?
|
736
|
+
fail ArgumentError,
|
737
|
+
"Can't scope by non-indexed column #{column_name}"
|
738
|
+
end
|
739
|
+
scoped(scoped_indexed_column: {column_name => column.cast(value)})
|
740
|
+
end
|
741
|
+
|
686
742
|
def scoped_key_names
|
687
743
|
scoped_key_columns.map { |column| column.name }
|
688
744
|
end
|
@@ -707,6 +763,14 @@ module Cequel
|
|
707
763
|
range_key_column.name
|
708
764
|
end
|
709
765
|
|
766
|
+
def next_unscoped_key_column
|
767
|
+
unscoped_key_columns.first
|
768
|
+
end
|
769
|
+
|
770
|
+
def next_unscoped_key_name
|
771
|
+
next_unscoped_key_column.name
|
772
|
+
end
|
773
|
+
|
710
774
|
def next_range_key_column
|
711
775
|
unscoped_key_columns.second
|
712
776
|
end
|
@@ -736,8 +800,6 @@ module Cequel
|
|
736
800
|
end
|
737
801
|
|
738
802
|
def next_unscoped_key_column_valid_for_in_query?
|
739
|
-
next_unscoped_key_column = unscoped_key_columns.first
|
740
|
-
|
741
803
|
next_unscoped_key_column == partition_key_columns.last ||
|
742
804
|
next_unscoped_key_column == clustering_columns.last
|
743
805
|
end
|
data/lib/cequel/version.rb
CHANGED
@@ -0,0 +1,177 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Cequel::Record::Finders do
|
4
|
+
model :Blog do
|
5
|
+
key :subdomain, :text
|
6
|
+
column :name, :text
|
7
|
+
column :description, :text
|
8
|
+
column :owner_id, :uuid
|
9
|
+
end
|
10
|
+
|
11
|
+
model :User do
|
12
|
+
key :login, :text
|
13
|
+
column :name, :text
|
14
|
+
end
|
15
|
+
|
16
|
+
model :Post do
|
17
|
+
key :blog_subdomain, :text
|
18
|
+
key :permalink, :text
|
19
|
+
column :title, :text
|
20
|
+
column :body, :text
|
21
|
+
column :author_id, :uuid, index: true
|
22
|
+
end
|
23
|
+
|
24
|
+
let :blogs do
|
25
|
+
cequel.batch do
|
26
|
+
5.times.map do |i|
|
27
|
+
Blog.create!(subdomain: "cassandra#{i}", name: 'Cassandra')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:author_ids) { Array.new(2) { Cequel.uuid }}
|
33
|
+
|
34
|
+
let :cassandra_posts do
|
35
|
+
cequel.batch do
|
36
|
+
5.times.map do |i|
|
37
|
+
Post.create!(
|
38
|
+
blog_subdomain: 'cassandra',
|
39
|
+
permalink: "cassandra#{i}",
|
40
|
+
author_id: author_ids[i%2]
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
let :postgres_posts do
|
47
|
+
cequel.batch do
|
48
|
+
5.times.map do |i|
|
49
|
+
Post.create!(blog_subdomain: 'postgres', permalink: "postgres#{i}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
let(:posts) { cassandra_posts + postgres_posts }
|
55
|
+
|
56
|
+
context 'simple primary key' do
|
57
|
+
|
58
|
+
let!(:blog) { blogs.first }
|
59
|
+
|
60
|
+
describe '#find_by_*' do
|
61
|
+
it 'should return matching record' do
|
62
|
+
expect(Blog.find_by_subdomain('cassandra0')).to eq(blog)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should return nil if no record matches' do
|
66
|
+
expect(Blog.find_by_subdomain('bogus')).to be_nil
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should respond to method before it is called' do
|
70
|
+
expect(User).to be_respond_to(:find_by_login)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should raise error on wrong name' do
|
74
|
+
expect { Blog.find_by_bogus('bogus') }.to raise_error(NoMethodError)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should not respond to wrong name' do
|
78
|
+
expect(User).to_not be_respond_to(:find_by_bogus)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#find_all_by_*' do
|
83
|
+
it 'should raise error if called' do
|
84
|
+
expect { Blog.find_all_by_subdomain('outoftime') }
|
85
|
+
.to raise_error(NoMethodError)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should not respond' do
|
89
|
+
expect(User).not_to be_respond_to(:find_all_by_login)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'compound primary key' do
|
95
|
+
|
96
|
+
let!(:post) { posts.first }
|
97
|
+
|
98
|
+
describe '#find_all_by_*' do
|
99
|
+
it 'should return all records matching key prefix' do
|
100
|
+
expect(Post.find_all_by_blog_subdomain('cassandra'))
|
101
|
+
.to eq(cassandra_posts)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should greedily load records' do
|
105
|
+
records = Post.find_all_by_blog_subdomain('cassandra')
|
106
|
+
disallow_queries!
|
107
|
+
expect(records).to eq(cassandra_posts)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should return empty array if nothing matches' do
|
111
|
+
expect(Post.find_all_by_blog_subdomain('bogus')).to eq([])
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should not exist for all keys' do
|
115
|
+
expect { Post.find_all_by_blog_subdomain_and_permalink('f', 'b') }
|
116
|
+
.to raise_error(NoMethodError)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe '#find_by_*' do
|
121
|
+
it 'should return record matching all keys' do
|
122
|
+
expect(Post.find_by_blog_subdomain_and_permalink('cassandra',
|
123
|
+
'cassandra0'))
|
124
|
+
.to eq(cassandra_posts.first)
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should not exist for key prefix' do
|
128
|
+
expect { Post.find_by_blog_subdomain('foo') }
|
129
|
+
.to raise_error(NoMethodError)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'should allow lower-order key if chained' do
|
133
|
+
expect(Post.where(blog_subdomain: 'cassandra')
|
134
|
+
.find_by_permalink('cassandra0')).to eq(cassandra_posts.first)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe '#with_*' do
|
139
|
+
it 'should return record matching all keys' do
|
140
|
+
expect(Post.with_blog_subdomain_and_permalink('cassandra',
|
141
|
+
'cassandra0'))
|
142
|
+
.to eq(cassandra_posts.first(1))
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'should return all records matching key prefix' do
|
146
|
+
expect(Post.with_blog_subdomain('cassandra'))
|
147
|
+
.to eq(cassandra_posts)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'secondary index' do
|
153
|
+
before { cassandra_posts }
|
154
|
+
|
155
|
+
it 'should expose scope to query by secondary index' do
|
156
|
+
expect(Post.with_author_id(author_ids.first))
|
157
|
+
.to match_array(cassandra_posts.values_at(0, 2, 4))
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'should expose method to retrieve first result by secondary index' do
|
161
|
+
expect(Post.find_by_author_id(author_ids.first))
|
162
|
+
.to eq(cassandra_posts.first)
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should expose method to eagerly retrieve all results by secondary index' do
|
166
|
+
posts = Post.find_all_by_author_id(author_ids.first)
|
167
|
+
disallow_queries!
|
168
|
+
expect(posts).to match_array(cassandra_posts.values_at(0, 2, 4))
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should not expose methods for non-indexed columns' do
|
172
|
+
[:find_by_title, :find_all_by_title, :with_title].each do |method|
|
173
|
+
expect(Post).to_not respond_to(method)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -585,35 +585,114 @@ describe Cequel::Record::RecordSet do
|
|
585
585
|
end
|
586
586
|
|
587
587
|
describe '#where' do
|
588
|
-
|
589
|
-
|
590
|
-
|
588
|
+
context 'simple primary key' do
|
589
|
+
let(:records) { blogs }
|
590
|
+
|
591
|
+
it 'should correctly query for simple primary key with two arguments' do
|
592
|
+
expect(Blog.where(:subdomain, 'blog-0'))
|
593
|
+
.to eq(blogs.first(1))
|
594
|
+
end
|
595
|
+
|
596
|
+
it 'should correctly query for simple primary key with hash argument' do
|
597
|
+
expect(Blog.where(subdomain: 'blog-0'))
|
598
|
+
.to eq(blogs.first(1))
|
599
|
+
end
|
591
600
|
end
|
592
601
|
|
593
|
-
|
594
|
-
|
595
|
-
|
602
|
+
context 'compound primary key' do
|
603
|
+
it 'should correctly query for first primary key column' do
|
604
|
+
expect(Post.where(blog_subdomain: 'cassandra'))
|
605
|
+
.to eq(cassandra_posts)
|
606
|
+
end
|
607
|
+
|
608
|
+
it 'should perform IN query when passed multiple values' do
|
609
|
+
expect(Post.where(blog_subdomain: %w(cassandra postgres)))
|
610
|
+
.to match_array(cassandra_posts + postgres_posts)
|
611
|
+
end
|
612
|
+
|
613
|
+
it 'should correctly query for both primary key columns' do
|
614
|
+
expect(Post.where(blog_subdomain: 'cassandra', permalink: 'cequel0'))
|
615
|
+
.to eq(cassandra_posts.first(1))
|
616
|
+
end
|
617
|
+
|
618
|
+
it 'should correctly query for both primary key columns chained' do
|
619
|
+
expect(Post.where(blog_subdomain: 'cassandra')
|
620
|
+
.where(permalink: 'cequel0'))
|
621
|
+
.to eq(cassandra_posts.first(1))
|
622
|
+
end
|
623
|
+
|
624
|
+
it 'should perform range query when passed range' do
|
625
|
+
expect(Post.where(blog_subdomain: %w(cassandra),
|
626
|
+
permalink: 'cequel0'..'cequel2'))
|
627
|
+
.to eq(cassandra_posts.first(3))
|
628
|
+
end
|
629
|
+
|
630
|
+
it 'should raise error if lower-order primary key specified without higher' do
|
631
|
+
expect { Post.where(permalink: 'cequel0').first }
|
632
|
+
.to raise_error(Cequel::Record::IllegalQuery)
|
633
|
+
end
|
596
634
|
end
|
597
635
|
|
598
|
-
|
599
|
-
|
600
|
-
|
636
|
+
context 'secondary indexed column' do
|
637
|
+
it 'should query for secondary indexed columns with two arguments' do
|
638
|
+
Post.where(:author_id, uuids.first).map(&:permalink).
|
639
|
+
should == %w(cequel0 cequel2 cequel4)
|
640
|
+
end
|
641
|
+
|
642
|
+
it 'should query for secondary indexed columns with hash argument' do
|
643
|
+
Post.where(author_id: uuids.first).map(&:permalink).
|
644
|
+
should == %w(cequel0 cequel2 cequel4)
|
645
|
+
end
|
646
|
+
|
647
|
+
it 'should not allow multiple columns in the arguments' do
|
648
|
+
expect { Post.where(author_id: uuids.first, author_name: 'Mat Brown') }
|
649
|
+
.to raise_error(Cequel::Record::IllegalQuery)
|
650
|
+
end
|
651
|
+
|
652
|
+
it 'should not allow chaining of multiple columns' do
|
653
|
+
expect { Post.where(:author_id, uuids.first).
|
654
|
+
where(:author_name, 'Mat Brown') }.
|
655
|
+
to raise_error(Cequel::Record::IllegalQuery)
|
656
|
+
end
|
657
|
+
|
658
|
+
it 'should cast argument for column' do
|
659
|
+
Post.where(:author_id, uuids.first.to_s).map(&:permalink).
|
660
|
+
should == %w(cequel0 cequel2 cequel4)
|
661
|
+
end
|
601
662
|
end
|
602
663
|
|
603
|
-
|
604
|
-
|
605
|
-
|
664
|
+
context 'mixing keys and secondary-indexed columns' do
|
665
|
+
it 'should not allow mixture in hash argument' do
|
666
|
+
expect { Post.where(blog_subdomain: 'cassandra',
|
667
|
+
author_id: uuids.first) }
|
668
|
+
.to raise_error(Cequel::Record::IllegalQuery)
|
669
|
+
end
|
670
|
+
|
671
|
+
it 'should not allow mixture in chain with primary first' do
|
672
|
+
expect { Post.where(blog_subdomain: 'cassandra')
|
673
|
+
.where(author_id: uuids.first) }
|
674
|
+
.to raise_error(Cequel::Record::IllegalQuery)
|
675
|
+
end
|
676
|
+
|
677
|
+
it 'should not allow mixture in chain with secondary first' do
|
678
|
+
expect { Post.where(author_id: uuids.first)
|
679
|
+
.where(blog_subdomain: 'cassandra') }
|
680
|
+
.to raise_error(Cequel::Record::IllegalQuery)
|
681
|
+
end
|
606
682
|
end
|
607
683
|
|
608
|
-
|
609
|
-
|
610
|
-
|
684
|
+
context 'nonexistent column' do
|
685
|
+
it 'should raise ArgumentError if column is not recognized' do
|
686
|
+
expect { Post.where(:bogus, 'Business') }.
|
687
|
+
to raise_error(ArgumentError)
|
688
|
+
end
|
611
689
|
end
|
612
690
|
|
613
|
-
|
614
|
-
|
615
|
-
where(:
|
616
|
-
|
691
|
+
context 'non-indexed column' do
|
692
|
+
it 'should raise ArgumentError if column is not indexed' do
|
693
|
+
expect { Post.where(:title, 'Cequel 0') }.
|
694
|
+
to raise_error(ArgumentError)
|
695
|
+
end
|
617
696
|
end
|
618
697
|
end
|
619
698
|
|
data/spec/support/helpers.rb
CHANGED
@@ -108,11 +108,11 @@ module Cequel
|
|
108
108
|
end
|
109
109
|
|
110
110
|
def max_statements!(number)
|
111
|
-
cequel.should_receive(:execute).at_most(number).times.and_call_original
|
111
|
+
cequel.client.should_receive(:execute).at_most(number).times.and_call_original
|
112
112
|
end
|
113
113
|
|
114
114
|
def disallow_queries!
|
115
|
-
cequel.should_not_receive(:execute)
|
115
|
+
cequel.client.should_not_receive(:execute)
|
116
116
|
end
|
117
117
|
|
118
118
|
def expect_query_with_consistency(matcher, consistency)
|
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.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mat Brown
|
@@ -12,10 +12,11 @@ authors:
|
|
12
12
|
- Peter Williams
|
13
13
|
- Kenneth Hoffman
|
14
14
|
- Antti Tapio
|
15
|
+
- Ilya Bazylchuk
|
15
16
|
autorequire:
|
16
17
|
bindir: bin
|
17
18
|
cert_chain: []
|
18
|
-
date: 2014-03-
|
19
|
+
date: 2014-03-24 00:00:00.000000000 Z
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
21
22
|
name: activemodel
|
@@ -24,6 +25,9 @@ dependencies:
|
|
24
25
|
- - ">="
|
25
26
|
- !ruby/object:Gem::Version
|
26
27
|
version: '3.1'
|
28
|
+
- - "<"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '5.0'
|
27
31
|
type: :runtime
|
28
32
|
prerelease: false
|
29
33
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -31,20 +35,23 @@ dependencies:
|
|
31
35
|
- - ">="
|
32
36
|
- !ruby/object:Gem::Version
|
33
37
|
version: '3.1'
|
38
|
+
- - "<"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
34
41
|
- !ruby/object:Gem::Dependency
|
35
42
|
name: cql-rb
|
36
43
|
requirement: !ruby/object:Gem::Requirement
|
37
44
|
requirements:
|
38
|
-
- - "
|
45
|
+
- - "~>"
|
39
46
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
47
|
+
version: '1.2'
|
41
48
|
type: :runtime
|
42
49
|
prerelease: false
|
43
50
|
version_requirements: !ruby/object:Gem::Requirement
|
44
51
|
requirements:
|
45
|
-
- - "
|
52
|
+
- - "~>"
|
46
53
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
54
|
+
version: '1.2'
|
48
55
|
- !ruby/object:Gem::Dependency
|
49
56
|
name: appraisal
|
50
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -91,72 +98,30 @@ dependencies:
|
|
91
98
|
name: rake
|
92
99
|
requirement: !ruby/object:Gem::Requirement
|
93
100
|
requirements:
|
94
|
-
- - "
|
101
|
+
- - "~>"
|
95
102
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
103
|
+
version: '10.1'
|
97
104
|
type: :development
|
98
105
|
prerelease: false
|
99
106
|
version_requirements: !ruby/object:Gem::Requirement
|
100
107
|
requirements:
|
101
|
-
- - "
|
108
|
+
- - "~>"
|
102
109
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
110
|
+
version: '10.1'
|
104
111
|
- !ruby/object:Gem::Dependency
|
105
112
|
name: rubocop
|
106
113
|
requirement: !ruby/object:Gem::Requirement
|
107
114
|
requirements:
|
108
|
-
- - "
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
|
-
type: :development
|
112
|
-
prerelease: false
|
113
|
-
version_requirements: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
- !ruby/object:Gem::Dependency
|
119
|
-
name: travis
|
120
|
-
requirement: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
type: :development
|
126
|
-
prerelease: false
|
127
|
-
version_requirements: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - ">="
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '0'
|
132
|
-
- !ruby/object:Gem::Dependency
|
133
|
-
name: travis-lint
|
134
|
-
requirement: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - ">="
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '0'
|
139
|
-
type: :development
|
140
|
-
prerelease: false
|
141
|
-
version_requirements: !ruby/object:Gem::Requirement
|
142
|
-
requirements:
|
143
|
-
- - ">="
|
144
|
-
- !ruby/object:Gem::Version
|
145
|
-
version: '0'
|
146
|
-
- !ruby/object:Gem::Dependency
|
147
|
-
name: tzinfo
|
148
|
-
requirement: !ruby/object:Gem::Requirement
|
149
|
-
requirements:
|
150
|
-
- - ">="
|
115
|
+
- - "~>"
|
151
116
|
- !ruby/object:Gem::Version
|
152
|
-
version: '0'
|
117
|
+
version: '0.19'
|
153
118
|
type: :development
|
154
119
|
prerelease: false
|
155
120
|
version_requirements: !ruby/object:Gem::Requirement
|
156
121
|
requirements:
|
157
|
-
- - "
|
122
|
+
- - "~>"
|
158
123
|
- !ruby/object:Gem::Version
|
159
|
-
version: '0'
|
124
|
+
version: '0.19'
|
160
125
|
description: |
|
161
126
|
Cequel is an ActiveRecord-like domain model layer for Cassandra that exposes
|
162
127
|
the robust data modeling capabilities of CQL3, including parent-child
|
@@ -209,6 +174,7 @@ files:
|
|
209
174
|
- lib/cequel/record/data_set_builder.rb
|
210
175
|
- lib/cequel/record/dirty.rb
|
211
176
|
- lib/cequel/record/errors.rb
|
177
|
+
- lib/cequel/record/finders.rb
|
212
178
|
- lib/cequel/record/has_many_association.rb
|
213
179
|
- lib/cequel/record/lazy_record_collection.rb
|
214
180
|
- lib/cequel/record/mass_assignment.rb
|
@@ -219,7 +185,6 @@ files:
|
|
219
185
|
- lib/cequel/record/record_set.rb
|
220
186
|
- lib/cequel/record/schema.rb
|
221
187
|
- lib/cequel/record/scoped.rb
|
222
|
-
- lib/cequel/record/secondary_indexes.rb
|
223
188
|
- lib/cequel/record/tasks.rb
|
224
189
|
- lib/cequel/record/validations.rb
|
225
190
|
- lib/cequel/schema.rb
|
@@ -244,6 +209,7 @@ files:
|
|
244
209
|
- spec/examples/record/associations_spec.rb
|
245
210
|
- spec/examples/record/callbacks_spec.rb
|
246
211
|
- spec/examples/record/dirty_spec.rb
|
212
|
+
- spec/examples/record/finders_spec.rb
|
247
213
|
- spec/examples/record/list_spec.rb
|
248
214
|
- spec/examples/record/map_spec.rb
|
249
215
|
- spec/examples/record/mass_assignment_spec.rb
|
@@ -253,7 +219,6 @@ files:
|
|
253
219
|
- spec/examples/record/record_set_spec.rb
|
254
220
|
- spec/examples/record/schema_spec.rb
|
255
221
|
- spec/examples/record/scoped_spec.rb
|
256
|
-
- spec/examples/record/secondary_index_spec.rb
|
257
222
|
- spec/examples/record/serialization_spec.rb
|
258
223
|
- spec/examples/record/set_spec.rb
|
259
224
|
- spec/examples/record/spec_helper.rb
|
@@ -301,6 +266,7 @@ test_files:
|
|
301
266
|
- spec/examples/record/associations_spec.rb
|
302
267
|
- spec/examples/record/callbacks_spec.rb
|
303
268
|
- spec/examples/record/dirty_spec.rb
|
269
|
+
- spec/examples/record/finders_spec.rb
|
304
270
|
- spec/examples/record/list_spec.rb
|
305
271
|
- spec/examples/record/map_spec.rb
|
306
272
|
- spec/examples/record/mass_assignment_spec.rb
|
@@ -310,7 +276,6 @@ test_files:
|
|
310
276
|
- spec/examples/record/record_set_spec.rb
|
311
277
|
- spec/examples/record/schema_spec.rb
|
312
278
|
- spec/examples/record/scoped_spec.rb
|
313
|
-
- spec/examples/record/secondary_index_spec.rb
|
314
279
|
- spec/examples/record/serialization_spec.rb
|
315
280
|
- spec/examples/record/set_spec.rb
|
316
281
|
- spec/examples/record/spec_helper.rb
|
@@ -1,68 +0,0 @@
|
|
1
|
-
# -*- encoding : utf-8 -*-
|
2
|
-
module Cequel
|
3
|
-
module Record
|
4
|
-
#
|
5
|
-
# Data columns may be given secondary indexes. This allows you to query the
|
6
|
-
# table for records where the indexed column contains a single value.
|
7
|
-
# Secondary indexes are not nearly as flexible as primary keys: you cannot
|
8
|
-
# query for multiple values or for ranges of values. You also cannot
|
9
|
-
# combine a secondary index restriction with a primary key restriction in
|
10
|
-
# the same query, nor can you combine more than one secondary index
|
11
|
-
# restrictions in the same query.
|
12
|
-
#
|
13
|
-
# If a column is given a secondary index, magic finder methods `find_by_*`,
|
14
|
-
# `find_all_by_*`, and `with_*` are added to the class singleton. See below
|
15
|
-
# for an example.
|
16
|
-
#
|
17
|
-
# Secondary indexes are fairly expensive for Cassandra and should only be
|
18
|
-
# defined where needed.
|
19
|
-
#
|
20
|
-
# @example Defining a secondary index
|
21
|
-
# class Post
|
22
|
-
# belongs_to :blog
|
23
|
-
# key :id, :timeuuid, auto: true
|
24
|
-
# column :title, :text
|
25
|
-
# column :body, :text
|
26
|
-
# column :author_id, :uuid, index: true # this column has a secondary
|
27
|
-
# # index
|
28
|
-
# end
|
29
|
-
#
|
30
|
-
# @example Using a secondary index
|
31
|
-
# # return the first record with the author_id
|
32
|
-
# Post.find_by_author_id(id)
|
33
|
-
#
|
34
|
-
# # return an Array of all records with the author_id
|
35
|
-
# Post.find_all_by_author_id(id)
|
36
|
-
#
|
37
|
-
# # return a RecordSet scoped to the author_id
|
38
|
-
# Post.with_author_id(id)
|
39
|
-
#
|
40
|
-
# # same as with_author_id
|
41
|
-
# Post.where(:author_id, id)
|
42
|
-
#
|
43
|
-
# @since 1.0.0
|
44
|
-
#
|
45
|
-
module SecondaryIndexes
|
46
|
-
# @private
|
47
|
-
def column(name, type, options = {})
|
48
|
-
super
|
49
|
-
name = name.to_sym
|
50
|
-
if options[:index]
|
51
|
-
instance_eval <<-RUBY, __FILE__, __LINE__+1
|
52
|
-
def with_#{name}(value)
|
53
|
-
all.where(#{name.inspect}, value)
|
54
|
-
end
|
55
|
-
|
56
|
-
def find_by_#{name}(value)
|
57
|
-
with_#{name}(value).first
|
58
|
-
end
|
59
|
-
|
60
|
-
def find_all_by_#{name}(value)
|
61
|
-
with_#{name}(value).to_a
|
62
|
-
end
|
63
|
-
RUBY
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
@@ -1,46 +0,0 @@
|
|
1
|
-
# -*- encoding : utf-8 -*-
|
2
|
-
require_relative 'spec_helper'
|
3
|
-
|
4
|
-
describe Cequel::Record::SecondaryIndexes do
|
5
|
-
model :Post do
|
6
|
-
key :blog_subdomain, :text
|
7
|
-
key :permalink, :text
|
8
|
-
column :title, :text
|
9
|
-
column :author_id, :uuid, :index => true
|
10
|
-
end
|
11
|
-
|
12
|
-
let(:uuids) { Array.new(2) { Cequel.uuid }}
|
13
|
-
|
14
|
-
let!(:posts) do
|
15
|
-
3.times.map do |i|
|
16
|
-
Post.create! do |post|
|
17
|
-
post.blog_subdomain = 'bigdata'
|
18
|
-
post.permalink = "cequel#{i}"
|
19
|
-
post.title = "Cequel #{i}"
|
20
|
-
post.author_id = uuids[i%2]
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'should create secondary index in schema' do
|
26
|
-
cequel.schema.read_table(:posts).data_columns.
|
27
|
-
find { |column| column.name == :author_id }.index_name.
|
28
|
-
should be
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'should expose scope to query by secondary index' do
|
32
|
-
Post.with_author_id(uuids.first).map(&:permalink).
|
33
|
-
should == %w(cequel0 cequel2)
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'should expose method to retrieve first result by secondary index' do
|
37
|
-
Post.find_by_author_id(uuids.first).should == posts.first
|
38
|
-
end
|
39
|
-
|
40
|
-
it 'should expose method to eagerly retrieve all results by secondary index' do
|
41
|
-
posts = Post.find_all_by_author_id(uuids.first)
|
42
|
-
disallow_queries!
|
43
|
-
posts.map(&:permalink).should == %w(cequel0 cequel2)
|
44
|
-
end
|
45
|
-
|
46
|
-
end
|