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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32765637991b209fa0eda38cae999ce92e03d9de
4
- data.tar.gz: 5b0f247d96c436fa9947d14162e91b648cc29929
3
+ metadata.gz: 8151f255eac647140677b154e3d786f1ed817a26
4
+ data.tar.gz: e7b3214a86a25d8c6d98aa68e9d5e4a992731ab8
5
5
  SHA512:
6
- metadata.gz: 7739409b3a223c117e17673d769b04b8382de1638c40262e72a68527bf75b87fd66c850681401fd652f998e59ab272d430379d611da2069ac316e9c7a0eb00b0
7
- data.tar.gz: 1dc7c4ff48b96d2b6f4716fce64faad2569580a73c54446499809eb2f8947c3bf304da5a4082ae8d14f65da331f0b9ba8266daee99dcadc783bc3b47130a4c58
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
@@ -1,3 +1,9 @@
1
+ ## 1.2.0
2
+
3
+ * `where` can now be used to scope primary keys
4
+ * Magic finders for primary keys
5
+ * Pessimistic versioning for all dependencies
6
+
1
7
  ## 1.1.2
2
8
 
3
9
  * Simplify logging implementation
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
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cequel (1.1.2)
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.15)
26
- atomic (1.1.15-java)
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.0)
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.0)
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.2.0)
285
+ thread_safe (0.3.1)
317
286
  atomic (>= 1.1.7, < 2)
318
- thread_safe (0.2.0-java)
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
- websocket (1.0.7)
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
@@ -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/secondary_indexes'
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 SecondaryIndexes
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
- # @param column_name [Symbol] column for filter
185
- # @param value value to match in given column
186
- # @return [RecordSet] record set with filter applied
187
- # @raise [IllegalQuery] if this record set is already filtered by an
188
- # indexed column
189
- # @raise [ArgumentError] if the specified column is not an data column
190
- # with a secondary index
191
- #
192
- # @note This should only be used with data columns that have secondary
193
- # indexes. To filter a record set using a primary key, use {#[]}
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(column_name, value)
198
- column = target_class.reflect_on_column(column_name)
199
- if scoped_indexed_column
200
- fail IllegalQuery,
201
- "Can't scope by more than one indexed column in the same query"
202
- end
203
- unless column
204
- fail ArgumentError,
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
- "Can't scope by non-indexed column #{column_name}"
213
+ "wrong number of arguments (#{args.length} for 1..2)"
214
214
  end
215
- scoped(scoped_indexed_column: {column_name => column.cast(value)})
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
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module Cequel
3
3
  # The current version of the library
4
- VERSION = '1.1.2'
4
+ VERSION = '1.2.0'
5
5
  end
@@ -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
- it 'should correctly query for secondary indexed columns' do
589
- Post.where(:author_id, uuids.first).map(&:permalink).
590
- should == %w(cequel0 cequel2 cequel4)
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
- it 'should cast argument for column' do
594
- Post.where(:author_id, uuids.first.to_s).map(&:permalink).
595
- should == %w(cequel0 cequel2 cequel4)
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
- it 'should raise ArgumentError if column is not recognized' do
599
- expect { Post.where(:bogus, 'Business') }.
600
- to raise_error(ArgumentError)
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
- it 'should raise ArgumentError if column is not indexed' do
604
- expect { Post.where(:title, 'Cequel 0') }.
605
- to raise_error(ArgumentError)
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
- it 'should raise ArgumentError if column is a key' do
609
- expect { Post.where(:permalink, 'cequel0') }.
610
- to raise_error(ArgumentError)
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
- it 'should raise IllegalQuery if applied twice' do
614
- expect { Post.where(:author_id, uuids.first).
615
- where(:author_name, 'Mat Brown') }.
616
- to raise_error(Cequel::Record::IllegalQuery)
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
 
@@ -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.1.2
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-18 00:00:00.000000000 Z
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: '0'
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: '0'
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: '0'
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: '0'
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