cequel 1.0.0.pre.1 → 1.0.0.pre.2

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: 1b840b678aaff8377efff196f7e011a37939fbbc
4
- data.tar.gz: b0726b2298165f250f382cf13177c425dd819fcd
3
+ metadata.gz: 7888f93ffd3d74bdfef143a9c8d18c6c9f2f4ba1
4
+ data.tar.gz: 831e5a90a03cf1c22ddf08421284107254aa59a6
5
5
  SHA512:
6
- metadata.gz: 5606d970c9051fa1e45168b0f249c1e2ebe0be2009fb8ab0270af1d75151c96ee990978c73affe468d85394190dfc2d1abf3516c57c86a4edf20266882e4b47f
7
- data.tar.gz: 98d44471f0dfc64ee16bd31168d76793647a22afb6c17d7fe91b85e38bf65ea88f7017486a91d04a5d8b6804153fe6dd7cda133943eb4514e894b046c98e26bb
6
+ metadata.gz: 41a8585f9c78171c34944551914e4e06b44fabe615b9192901782b6653be345314c63ad4e4b5b048818bb10e1d4085aad24006285a9bd573489153b24719ccc3
7
+ data.tar.gz: 90297a4b794c2eb265ffdc7659f80c1b90ef8870c1d04ff910d4d015e425a47395b8151fa72c8cf85266538e5e7fe3c495b9433751776eb727ee5983f986314c
@@ -6,6 +6,8 @@ require 'cequel/errors'
6
6
  require 'cequel/metal'
7
7
  require 'cequel/schema'
8
8
  require 'cequel/type'
9
+ require 'cequel/util'
10
+ require 'cequel/model'
9
11
 
10
12
  module Cequel
11
13
  def self.connect(configuration = nil)
@@ -8,6 +8,7 @@ require 'cequel/model/collection'
8
8
  require 'cequel/model/persistence'
9
9
  require 'cequel/model/record_set'
10
10
  require 'cequel/model/scoped'
11
+ require 'cequel/model/secondary_indexes'
11
12
  require 'cequel/model/associations'
12
13
  require 'cequel/model/association_collection'
13
14
  require 'cequel/model/belongs_to_association'
@@ -15,6 +16,7 @@ require 'cequel/model/has_many_association'
15
16
  require 'cequel/model/mass_assignment'
16
17
  require 'cequel/model/callbacks'
17
18
  require 'cequel/model/validations'
19
+ require 'cequel/model/dirty'
18
20
 
19
21
  require 'cequel/model/base'
20
22
 
@@ -9,9 +9,11 @@ module Cequel
9
9
  include Cequel::Model::Persistence
10
10
  include Cequel::Model::Associations
11
11
  extend Cequel::Model::Scoped
12
+ extend Cequel::Model::SecondaryIndexes
12
13
  include Cequel::Model::MassAssignment
13
14
  include Cequel::Model::Callbacks
14
15
  include Cequel::Model::Validations
16
+ include Cequel::Model::Dirty
15
17
  extend ActiveModel::Naming
16
18
  include ActiveModel::Serializers::JSON
17
19
  include ActiveModel::Serializers::Xml
@@ -41,6 +43,14 @@ module Cequel
41
43
  instance_eval(&block) if block
42
44
  end
43
45
 
46
+ def ==(other)
47
+ if key_values.any? { |value| value.nil? }
48
+ super
49
+ else
50
+ self.class == other.class && key_values == other.key_values
51
+ end
52
+ end
53
+
44
54
  def inspect
45
55
  inspected_attributes = attributes.each_pair.map do |attr, value|
46
56
  inspected_value = value.is_a?(CassandraCQL::UUID) ?
@@ -10,6 +10,7 @@ module Cequel
10
10
  extend Forwardable
11
11
 
12
12
  def_delegators :@model, :loaded?, :updater, :deleter
13
+ def_delegators :__getobj__, :clone, :dup
13
14
 
14
15
  attr_reader :column_name
15
16
 
@@ -50,7 +51,9 @@ module Cequel
50
51
  private
51
52
 
52
53
  def to_modify(&block)
53
- if loaded? then block.()
54
+ if loaded?
55
+ @model.__send__("#{@column_name}_will_change!")
56
+ block.()
54
57
  else modifications << block
55
58
  end
56
59
  self
@@ -175,6 +178,7 @@ module Cequel
175
178
  updater.set_add(column_name, object)
176
179
  to_modify { super }
177
180
  end
181
+ alias_method :<<, :add
178
182
 
179
183
  def clear
180
184
  deleter.delete_columns(column_name)
@@ -0,0 +1,62 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module Dirty
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included { include ActiveModel::Dirty }
10
+
11
+ module ClassMethods
12
+
13
+ def key(name, *)
14
+ define_attribute_method(name)
15
+ super
16
+ end
17
+
18
+ def column(name, *)
19
+ define_attribute_method(name)
20
+ super
21
+ end
22
+
23
+ def set(name, *)
24
+ define_attribute_method(name)
25
+ super
26
+ end
27
+
28
+ def list(name, *)
29
+ define_attribute_method(name)
30
+ super
31
+ end
32
+
33
+ def map(name, *)
34
+ define_attribute_method(name)
35
+ super
36
+ end
37
+
38
+ end
39
+
40
+ def save(options = {})
41
+ super.tap do |success|
42
+ if success
43
+ @previously_changed = changes
44
+ @changed_attributes.clear
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def write_attribute(name, value)
52
+ if loaded? && value != read_attribute(name)
53
+ __send__("#{name}_will_change!")
54
+ end
55
+ super
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -7,6 +7,7 @@ module Cequel
7
7
  RecordNotFound = Class.new(StandardError)
8
8
  InvalidRecordConfiguration = Class.new(StandardError)
9
9
  RecordInvalid = Class.new(StandardError)
10
+ IllegalQuery = Class.new(StandardError)
10
11
 
11
12
  end
12
13
 
@@ -5,14 +5,26 @@ module Cequel
5
5
  class RecordSet
6
6
 
7
7
  extend Forwardable
8
+ extend Cequel::Util::HashAccessors
8
9
  include Enumerable
9
10
 
10
11
  Bound = Struct.new(:value, :inclusive)
11
12
 
12
- def initialize(clazz)
13
- @clazz = clazz
14
- @select_columns = []
15
- @scoped_key_values = []
13
+ def self.default_attributes
14
+ {:scoped_key_values => [], :select_columns => []}
15
+ end
16
+
17
+ def self.create(clazz, attributes = {})
18
+ attributes = default_attributes.merge!(attributes)
19
+ if attributes[:scoped_key_values].length >= clazz.partition_key_columns.length
20
+ SinglePartitionRecordSet.new(clazz, attributes)
21
+ else
22
+ RecordSet.new(clazz, attributes)
23
+ end
24
+ end
25
+
26
+ def initialize(clazz, attributes)
27
+ @clazz, @attributes = clazz, attributes
16
28
  end
17
29
 
18
30
  def all
@@ -21,18 +33,29 @@ module Cequel
21
33
 
22
34
  def select(*columns)
23
35
  return super if block_given?
24
- scoped { |record_set| record_set.select_columns.concat(columns) }
36
+ scoped { |attributes| attributes[:select_columns].concat(columns) }
25
37
  end
26
38
 
27
39
  def limit(count)
28
- scoped { |record_set| record_set.row_limit = count }
40
+ scoped(:row_limit => count)
41
+ end
42
+
43
+ def where(column_name, value)
44
+ column = clazz.table_schema.column(column_name)
45
+ raise IllegalQuery,
46
+ "Can't scope by more than one indexed column in the same query" if scoped_indexed_column
47
+ raise ArgumentError,
48
+ "No column #{column_name} configured for #{clazz.name}" unless column
49
+ raise ArgumentError,
50
+ "Use the `at` method to restrict scope by primary key" unless column.data_column?
51
+ raise ArgumentError,
52
+ "Can't scope by non-indexed column #{column_name}" unless column.indexed?
53
+ scoped(scoped_indexed_column: {column_name => value})
29
54
  end
30
55
 
31
56
  def at(*scoped_key_values)
32
- record_set_class = next_key_column.partition_key? ?
33
- RecordSet : SortableRecordSet
34
- scoped(record_set_class) do |record_set|
35
- record_set.scoped_key_values.concat(scoped_key_values)
57
+ scoped do |attributes|
58
+ attributes[:scoped_key_values].concat(scoped_key_values)
36
59
  end
37
60
  end
38
61
 
@@ -58,22 +81,18 @@ module Cequel
58
81
  end
59
82
 
60
83
  def after(start_key)
61
- scoped do |record_set|
62
- record_set.lower_bound = Bound.new(start_key, false)
63
- end
84
+ scoped(lower_bound: Bound.new(start_key, false))
64
85
  end
65
86
 
66
87
  def before(end_key)
67
- scoped do |record_set|
68
- record_set.upper_bound = Bound.new(end_key, false)
69
- end
88
+ scoped(upper_bound: Bound.new(end_key, false))
70
89
  end
71
90
 
72
91
  def in(range)
73
- scoped do |record_set|
74
- record_set.lower_bound = Bound.new(range.first, true)
75
- record_set.upper_bound = Bound.new(range.last, !range.exclude_end?)
76
- end
92
+ scoped(
93
+ lower_bound: Bound.new(range.first, true),
94
+ upper_bound: Bound.new(range.last, !range.exclude_end?)
95
+ )
77
96
  end
78
97
 
79
98
  def first(count = nil)
@@ -116,29 +135,17 @@ module Cequel
116
135
  end
117
136
  end
118
137
 
119
- protected
120
- attr_accessor :row_limit
121
- attr_reader :select_columns, :scoped_key_values,
122
- :lower_bound, :upper_bound
123
-
124
- def reversed?
125
- false
126
- end
127
-
128
- def lower_bound=(bound)
129
- @lower_bound = bound
130
- end
131
-
132
- def upper_bound=(bound)
133
- @upper_bound = bound
134
- end
135
-
136
138
  def data_set
137
139
  @data_set ||= construct_data_set
138
140
  end
139
141
 
142
+ protected
143
+ attr_reader :attributes
144
+ hattr_reader :attributes, :select_columns, :scoped_key_values, :row_limit,
145
+ :lower_bound, :upper_bound, :scoped_indexed_column
146
+
140
147
  def next_batch_from(row)
141
- reversed? ? before(row[range_key_name]) : after(row[range_key_name])
148
+ after(row[range_key_name])
142
149
  end
143
150
 
144
151
  def find_nested_batches_from(row, options, &block)
@@ -176,23 +183,10 @@ module Cequel
176
183
  scoped_key_columns.map { |column| column.name }
177
184
  end
178
185
 
179
- def chain_from(collection)
180
- @select_columns = collection.select_columns.dup
181
- @scoped_key_values = collection.scoped_key_values.dup
182
- @lower_bound = collection.lower_bound
183
- @upper_bound = collection.upper_bound
184
- @row_limit = collection.row_limit
185
- self
186
- end
187
-
188
186
  private
189
187
  attr_reader :clazz
190
188
  def_delegators :clazz, :connection
191
189
 
192
- def scoped(record_set_class = self.class, &block)
193
- record_set_class.new(clazz).chain_from(self).tap(&block)
194
- end
195
-
196
190
  def next_key_column
197
191
  clazz.key_columns[scoped_key_values.length + 1]
198
192
  end
@@ -217,6 +211,7 @@ module Cequel
217
211
  fragment = construct_bound_fragment(upper_bound, '<')
218
212
  data_set = data_set.where(fragment, upper_bound.value)
219
213
  end
214
+ data_set = data_set.where(scoped_indexed_column) if scoped_indexed_column
220
215
  data_set
221
216
  end
222
217
 
@@ -225,43 +220,41 @@ module Cequel
225
220
  "TOKEN(#{range_key_name}) #{operator} TOKEN(?)"
226
221
  end
227
222
 
223
+ def scoped(new_attributes = {}, &block)
224
+ attributes_copy = Marshal.load(Marshal.dump(attributes))
225
+ attributes_copy.merge!(new_attributes)
226
+ attributes_copy.tap(&block) if block
227
+ RecordSet.create(clazz, attributes_copy)
228
+ end
229
+
228
230
  end
229
231
 
230
- class SortableRecordSet < RecordSet
232
+ class SinglePartitionRecordSet < RecordSet
231
233
 
232
- def initialize(clazz)
233
- super
234
- @reversed = false
235
- end
234
+ hattr_inquirer :attributes, :reversed
236
235
 
237
236
  def from(start_key)
238
- scoped do |record_set|
239
- record_set.lower_bound = Bound.new(start_key, true)
240
- end
237
+ scoped(lower_bound: Bound.new(start_key, true))
241
238
  end
242
239
 
243
240
  def upto(end_key)
244
- scoped do |record_set|
245
- record_set.upper_bound = Bound.new(end_key, true)
246
- end
241
+ scoped(upper_bound: Bound.new(end_key, true))
247
242
  end
248
243
 
249
244
  def reverse
250
- scoped { |scope| scope.reversed = !reversed? }
245
+ scoped(reversed: !reversed?)
251
246
  end
252
247
 
253
248
  def last
254
249
  reverse.first
255
250
  end
256
251
 
257
- def chain_from(collection)
258
- super
259
- @reversed = collection.reversed?
260
- self
252
+ # @api private
253
+ def next_batch_from(row)
254
+ reversed? ? before(row[range_key_name]) : super
261
255
  end
262
256
 
263
257
  protected
264
- attr_writer :reversed
265
258
 
266
259
  def construct_data_set
267
260
  data_set = super
@@ -269,10 +262,6 @@ module Cequel
269
262
  data_set
270
263
  end
271
264
 
272
- def reversed?
273
- @reversed
274
- end
275
-
276
265
  def construct_bound_fragment(bound, base_operator)
277
266
  operator = bound.inclusive ? "#{base_operator}=" : base_operator
278
267
  "#{range_key_name} #{operator} ?"
@@ -9,7 +9,8 @@ module Cequel
9
9
  module ClassMethods
10
10
  extend Forwardable
11
11
 
12
- def_delegators :table_schema, :key_columns, :key_column_names
12
+ def_delegators :table_schema, :key_columns, :key_column_names,
13
+ :partition_key_columns, :clustering_columns
13
14
 
14
15
  def synchronize_schema
15
16
  Cequel::Schema::TableSynchronizer.
@@ -9,7 +9,7 @@ module Cequel
9
9
  def_delegators :current_scope, *RecordSet.instance_methods(false)
10
10
 
11
11
  def current_scope
12
- RecordSet.new(self)
12
+ RecordSet.create(self)
13
13
  end
14
14
 
15
15
  end
@@ -0,0 +1,31 @@
1
+ module Cequel
2
+
3
+ module Model
4
+
5
+ module SecondaryIndexes
6
+
7
+ def column(name, type, options = {})
8
+ super
9
+ name = name.to_sym
10
+ if options[:index]
11
+ instance_eval <<-RUBY, __FILE__, __LINE__+1
12
+ def with_#{name}(value)
13
+ all.where(#{name.inspect}, value)
14
+ end
15
+
16
+ def find_by_#{name}(value)
17
+ with_#{name}(value).first
18
+ end
19
+
20
+ def find_all_by_#{name}(value)
21
+ with_#{name}(value).to_a
22
+ end
23
+ RUBY
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -14,7 +14,7 @@ module Cequel
14
14
 
15
15
  module ClassMethods
16
16
 
17
- def create!(attributes, &block)
17
+ def create!(attributes = {}, &block)
18
18
  new(attributes, &block).save!
19
19
  end
20
20
 
@@ -10,6 +10,22 @@ module Cequel
10
10
  @name, @type = name, type
11
11
  end
12
12
 
13
+ def key?
14
+ partition_key? || clustering_column?
15
+ end
16
+
17
+ def partition_key?
18
+ false
19
+ end
20
+
21
+ def clustering_column?
22
+ false
23
+ end
24
+
25
+ def data_column?
26
+ !key?
27
+ end
28
+
13
29
  def to_cql
14
30
  "#{@name} #{@type}"
15
31
  end
@@ -53,8 +69,8 @@ module Cequel
53
69
  "#{@name} #{@clustering_order}"
54
70
  end
55
71
 
56
- def partition_key?
57
- false
72
+ def clustering_column?
73
+ true
58
74
  end
59
75
 
60
76
  end
@@ -8,7 +8,7 @@ module Cequel
8
8
 
9
9
  attr_reader :name,
10
10
  :columns,
11
- :partition_keys,
11
+ :partition_key_columns,
12
12
  :clustering_columns,
13
13
  :data_columns,
14
14
  :properties
@@ -16,13 +16,13 @@ module Cequel
16
16
 
17
17
  def initialize(name)
18
18
  @name = name
19
- @partition_keys, @clustering_columns, @data_columns = [], [], []
19
+ @partition_key_columns, @clustering_columns, @data_columns = [], [], []
20
20
  @columns, @columns_by_name = [], {}
21
21
  @properties = ActiveSupport::HashWithIndifferentAccess.new
22
22
  end
23
23
 
24
24
  def add_key(name, type, clustering_order = nil)
25
- if @partition_keys.empty?
25
+ if @partition_key_columns.empty?
26
26
  unless clustering_order.nil?
27
27
  raise ArgumentError,
28
28
  "Can't set clustering order for partition key #{name}"
@@ -35,7 +35,7 @@ module Cequel
35
35
 
36
36
  def add_partition_key(name, type)
37
37
  column = PartitionKey.new(name, type(type))
38
- @partition_keys << add_column(column)
38
+ @partition_key_columns << add_column(column)
39
39
  end
40
40
 
41
41
  def add_clustering_column(name, type, clustering_order = nil)
@@ -71,7 +71,7 @@ module Cequel
71
71
  end
72
72
 
73
73
  def key_columns
74
- @partition_keys + @clustering_columns
74
+ @partition_key_columns + @clustering_columns
75
75
  end
76
76
 
77
77
  def key_column_names
@@ -79,7 +79,7 @@ module Cequel
79
79
  end
80
80
 
81
81
  def partition_key(name)
82
- @partition_keys.find { |column| column.name == name }
82
+ @partition_key_columns.find { |column| column.name == name }
83
83
  end
84
84
 
85
85
  def clustering_column(name)
@@ -86,15 +86,15 @@ module Cequel
86
86
  end
87
87
 
88
88
  def each_key_pair(&block)
89
- if existing.partition_keys.length != updated.partition_keys.length
89
+ if existing.partition_key_columns.length != updated.partition_key_columns.length
90
90
  raise InvalidSchemaMigration,
91
- "Existing partition keys #{existing.partition_keys.map { |key| key.name }.join(',')} differ from specified partition keys #{updated.partition_keys.map { |key| key.name }.join(',')}"
91
+ "Existing partition keys #{existing.partition_key_columns.map { |key| key.name }.join(',')} differ from specified partition keys #{updated.partition_key_columns.map { |key| key.name }.join(',')}"
92
92
  end
93
93
  if existing.clustering_columns.length != updated.clustering_columns.length
94
94
  raise InvalidSchemaMigration,
95
95
  "Existing clustering keys #{existing.clustering_columns.map { |key| key.name }.join(',')} differ from specified clustering keys #{updated.clustering_columns.map { |key| key.name }.join(',')}"
96
96
  end
97
- existing.partition_keys.zip(updated.partition_keys, &block)
97
+ existing.partition_key_columns.zip(updated.partition_key_columns, &block)
98
98
  existing.clustering_columns.zip(updated.clustering_columns, &block)
99
99
  end
100
100
 
@@ -51,7 +51,8 @@ module Cequel
51
51
  end
52
52
 
53
53
  def keys_cql
54
- partition_cql = table.partition_keys.map { |key| key.name }.join(', ')
54
+ partition_cql = table.partition_key_columns.
55
+ map { |key| key.name }.join(', ')
55
56
  if table.clustering_columns.any?
56
57
  nonpartition_cql =
57
58
  table.clustering_columns.map { |key| key.name }.join(', ')
@@ -0,0 +1,46 @@
1
+ module Cequel
2
+
3
+ module Util
4
+
5
+ module HashAccessors
6
+
7
+ def hattr_reader(hash, *attributes)
8
+ attributes.each do |attribute|
9
+ module_eval <<-RUBY, __FILE__, __LINE__+1
10
+ def #{attribute}
11
+ #{hash}[#{attribute.to_sym.inspect}]
12
+ end
13
+ RUBY
14
+ end
15
+ end
16
+
17
+ def hattr_inquirer(hash, *attributes)
18
+ attributes.each do |attribute|
19
+ module_eval <<-RUBY, __FILE__, __LINE__+1
20
+ def #{attribute}?
21
+ !!#{hash}[#{attribute.to_sym.inspect}]
22
+ end
23
+ RUBY
24
+ end
25
+ end
26
+
27
+ def hattr_writer(hash, *attributes)
28
+ attributes.each do |attribute|
29
+ module_eval <<-RUBY, __FILE__, __LINE__+1
30
+ def #{attribute}=(value)
31
+ #{hash}[#{attribute.to_sym.inspect}] = value
32
+ end
33
+ RUBY
34
+ end
35
+ end
36
+
37
+ def hattr_accessor(hash, *attributes)
38
+ hattr_reader(hash, *attributes)
39
+ hattr_writer(hash, *attributes)
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -1,3 +1,3 @@
1
1
  module Cequel
2
- VERSION = '1.0.0.pre.1'
2
+ VERSION = '1.0.0.pre.2'
3
3
  end
@@ -0,0 +1,68 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Cequel::Model::Dirty do
4
+ model :Post do
5
+ key :permalink, :text
6
+ column :title, :text
7
+ set :categories, :text
8
+ end
9
+
10
+ context 'loaded model' do
11
+ let(:post) do
12
+ Post.create!(
13
+ permalink: 'cequel',
14
+ title: 'Cequel',
15
+ categories: Set['Libraries']
16
+ )
17
+ end
18
+
19
+ it 'should not have changed attributes by default' do
20
+ post.changed_attributes.should be_empty
21
+ end
22
+
23
+ it 'should have changed attributes if attributes change' do
24
+ post.title = 'Cequel ORM'
25
+ post.changed_attributes.
26
+ should == {:title => 'Cequel'}.with_indifferent_access
27
+ end
28
+
29
+ it 'should not have changed attributes if attribute set to the same thing' do
30
+ post.title = 'Cequel'
31
+ post.changed_attributes.should be_empty
32
+ end
33
+
34
+ it 'should support *_changed? method' do
35
+ post.title = 'Cequel ORM'
36
+ post.title_changed?.should be_true
37
+ end
38
+
39
+ it 'should not have changed attributes after save' do
40
+ post.title = 'Cequel ORM'
41
+ post.save
42
+ post.changed_attributes.should be_empty
43
+ end
44
+
45
+ it 'should have previous changes after save' do
46
+ post.title = 'Cequel ORM'
47
+ post.save
48
+ post.previous_changes.
49
+ should == { :title => ['Cequel', 'Cequel ORM'] }.with_indifferent_access
50
+ end
51
+
52
+ it 'should detect changes to collections' do
53
+ post.categories << 'Gems'
54
+ post.changes.should ==
55
+ {categories: [Set['Libraries'], Set['Libraries', 'Gems']]}.
56
+ with_indifferent_access
57
+ end
58
+ end
59
+
60
+ context 'unloaded model' do
61
+ let(:post) { Post['cequel'] }
62
+
63
+ it 'should not track changes' do
64
+ post.title = 'Cequel'
65
+ post.changes.should be_empty
66
+ end
67
+ end
68
+ end
@@ -12,13 +12,15 @@ describe Cequel::Model::RecordSet do
12
12
  key :permalink, :text
13
13
  column :title, :text
14
14
  column :body, :text
15
- column :author_id, :uuid
15
+ column :author_id, :uuid, :index => true
16
+ column :author_name, :text, :index => true
16
17
  list :tags, :text
17
18
  set :categories, :text
18
19
  map :shares, :text, :int
19
20
  end
20
21
 
21
22
  let(:subdomains) { [] }
23
+ let(:uuids) { Array.new(2) { CassandraCQL::UUID.new }}
22
24
 
23
25
  before do
24
26
  cequel.batch do
@@ -36,7 +38,8 @@ describe Cequel::Model::RecordSet do
36
38
  :blog_subdomain => 'cassandra',
37
39
  :permalink => "cequel#{i}",
38
40
  :title => "Cequel #{i}",
39
- :body => "Post number #{i}"
41
+ :body => "Post number #{i}",
42
+ :author_id => uuids[i%2]
40
43
  )
41
44
  cequel[:posts].insert(
42
45
  :blog_subdomain => 'postgres',
@@ -276,6 +279,34 @@ describe Cequel::Model::RecordSet do
276
279
  end
277
280
  end
278
281
 
282
+ describe '#where' do
283
+ it 'should correctly query for secondary indexed columns' do
284
+ Post.where(:author_id, uuids.first).map(&:permalink).
285
+ should == %w(cequel0 cequel2 cequel4)
286
+ end
287
+
288
+ it 'should raise ArgumentError if column is not recognized' do
289
+ expect { Post.where(:bogus, 'Business') }.
290
+ to raise_error(ArgumentError)
291
+ end
292
+
293
+ it 'should raise ArgumentError if column is not indexed' do
294
+ expect { Post.where(:title, 'Cequel 0') }.
295
+ to raise_error(ArgumentError)
296
+ end
297
+
298
+ it 'should raise ArgumentError if column is a key' do
299
+ expect { Post.where(:permalink, 'cequel0') }.
300
+ to raise_error(ArgumentError)
301
+ end
302
+
303
+ it 'should raise IllegalQuery if applied twice' do
304
+ expect { Post.where(:author_id, uuids.first).
305
+ where(:author_name, 'Mat Brown') }.
306
+ to raise_error(Cequel::Model::IllegalQuery)
307
+ end
308
+ end
309
+
279
310
  describe '#count' do
280
311
  it 'should count records' do
281
312
  Blog.count.should == 3
@@ -20,7 +20,7 @@ describe Cequel::Model::Schema do
20
20
  context 'new model with simple primary key' do
21
21
  before { model.synchronize_schema }
22
22
 
23
- its(:partition_keys) { should == [Cequel::Schema::Column.new(:permalink, :text)] }
23
+ its(:partition_key_columns) { should == [Cequel::Schema::Column.new(:permalink, :text)] }
24
24
  its(:data_columns) { should include(Cequel::Schema::Column.new(:title, :text)) }
25
25
  its(:data_columns) { should include(Cequel::Schema::List.new(:categories, :text)) }
26
26
  its(:data_columns) { should include(Cequel::Schema::Set.new(:tags, :text)) }
@@ -0,0 +1,45 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Cequel::Model::SecondaryIndexes do
4
+ model :Post do
5
+ key :blog_subdomain, :text
6
+ key :permalink, :text
7
+ column :title, :text
8
+ column :author_id, :uuid, :index => true
9
+ end
10
+
11
+ let(:uuids) { Array.new(2) { CassandraCQL::UUID.new }}
12
+
13
+ let!(:posts) do
14
+ 3.times.map do |i|
15
+ Post.create! do |post|
16
+ post.blog_subdomain = 'bigdata'
17
+ post.permalink = "cequel#{i}"
18
+ post.title = "Cequel #{i}"
19
+ post.author_id = uuids[i%2]
20
+ end
21
+ end
22
+ end
23
+
24
+ it 'should create secondary index in schema' do
25
+ cequel.schema.read_table(:posts).data_columns.
26
+ find { |column| column.name == :author_id }.index_name.
27
+ should be
28
+ end
29
+
30
+ it 'should expose scope to query by secondary index' do
31
+ Post.with_author_id(uuids.first).map(&:permalink).
32
+ should == %w(cequel0 cequel2)
33
+ end
34
+
35
+ it 'should expose method to retrieve first result by secondary index' do
36
+ Post.find_by_author_id(uuids.first).should == posts.first
37
+ end
38
+
39
+ it 'should expose method to eagerly retrieve all results by secondary index' do
40
+ posts = Post.find_all_by_author_id(uuids.first)
41
+ disallow_queries!
42
+ posts.map(&:permalink).should == %w(cequel0 cequel2)
43
+ end
44
+
45
+ end
@@ -1,2 +1 @@
1
1
  require File.expand_path('../../spec_helper', __FILE__)
2
- require 'cequel/model'
@@ -14,11 +14,11 @@ describe Cequel::Schema::TableReader do
14
14
  end
15
15
 
16
16
  it 'should read name correctly' do
17
- table.partition_keys.first.name.should == :permalink
17
+ table.partition_key_columns.first.name.should == :permalink
18
18
  end
19
19
 
20
20
  it 'should read type correctly' do
21
- table.partition_keys.first.type.should be_a(Cequel::Type::Text)
21
+ table.partition_key_columns.first.type.should be_a(Cequel::Type::Text)
22
22
  end
23
23
 
24
24
  it 'should have no nonpartition keys' do
@@ -38,11 +38,11 @@ describe Cequel::Schema::TableReader do
38
38
  end
39
39
 
40
40
  it 'should read partition key name' do
41
- table.partition_keys.map(&:name).should == [:blog_subdomain]
41
+ table.partition_key_columns.map(&:name).should == [:blog_subdomain]
42
42
  end
43
43
 
44
44
  it 'should read partition key type' do
45
- table.partition_keys.map(&:type).should == [Cequel::Type::Text.instance]
45
+ table.partition_key_columns.map(&:type).should == [Cequel::Type::Text.instance]
46
46
  end
47
47
 
48
48
  it 'should read non-partition key name' do
@@ -124,11 +124,11 @@ describe Cequel::Schema::TableReader do
124
124
  end
125
125
 
126
126
  it 'should read partition key names' do
127
- table.partition_keys.map(&:name).should == [:blog_subdomain, :permalink]
127
+ table.partition_key_columns.map(&:name).should == [:blog_subdomain, :permalink]
128
128
  end
129
129
 
130
130
  it 'should read partition key types' do
131
- table.partition_keys.map(&:type).
131
+ table.partition_key_columns.map(&:type).
132
132
  should == [Cequel::Type::Text.instance, Cequel::Type::Ascii.instance]
133
133
  end
134
134
 
@@ -153,11 +153,11 @@ describe Cequel::Schema::TableReader do
153
153
  end
154
154
 
155
155
  it 'should read partition key names' do
156
- table.partition_keys.map(&:name).should == [:blog_subdomain, :permalink]
156
+ table.partition_key_columns.map(&:name).should == [:blog_subdomain, :permalink]
157
157
  end
158
158
 
159
159
  it 'should read partition key types' do
160
- table.partition_keys.map(&:type).
160
+ table.partition_key_columns.map(&:type).
161
161
  should == [Cequel::Type::Text.instance, Cequel::Type::Ascii.instance]
162
162
  end
163
163
 
@@ -19,11 +19,11 @@ describe Cequel::Schema::TableWriter do
19
19
  end
20
20
 
21
21
  it 'should create key alias' do
22
- table.partition_keys.map(&:name).should == [:permalink]
22
+ table.partition_key_columns.map(&:name).should == [:permalink]
23
23
  end
24
24
 
25
25
  it 'should set key validator' do
26
- table.partition_keys.map(&:type).should == [Cequel::Type[:ascii]]
26
+ table.partition_key_columns.map(&:type).should == [Cequel::Type[:ascii]]
27
27
  end
28
28
 
29
29
  it 'should set non-key columns' do
@@ -42,11 +42,11 @@ describe Cequel::Schema::TableWriter do
42
42
  end
43
43
 
44
44
  it 'should create key alias' do
45
- table.partition_keys.map(&:name).should == [:blog_subdomain]
45
+ table.partition_key_columns.map(&:name).should == [:blog_subdomain]
46
46
  end
47
47
 
48
48
  it 'should set key validator' do
49
- table.partition_keys.map(&:type).should == [Cequel::Type[:ascii]]
49
+ table.partition_key_columns.map(&:type).should == [Cequel::Type[:ascii]]
50
50
  end
51
51
 
52
52
  it 'should create non-partition key components' do
@@ -68,11 +68,11 @@ describe Cequel::Schema::TableWriter do
68
68
  end
69
69
 
70
70
  it 'should create all partition key components' do
71
- table.partition_keys.map(&:name).should == [:blog_subdomain, :permalink]
71
+ table.partition_key_columns.map(&:name).should == [:blog_subdomain, :permalink]
72
72
  end
73
73
 
74
74
  it 'should set key validators' do
75
- table.partition_keys.map(&:type).
75
+ table.partition_key_columns.map(&:type).
76
76
  should == [Cequel::Type[:ascii], Cequel::Type[:ascii]]
77
77
  end
78
78
  end
@@ -88,12 +88,12 @@ describe Cequel::Schema::TableWriter do
88
88
  end
89
89
 
90
90
  it 'should create all partition key components' do
91
- table.partition_keys.map(&:name).
91
+ table.partition_key_columns.map(&:name).
92
92
  should == [:blog_subdomain, :permalink]
93
93
  end
94
94
 
95
95
  it 'should set key validators' do
96
- table.partition_keys.map(&:type).
96
+ table.partition_key_columns.map(&:type).
97
97
  should == [Cequel::Type[:ascii], Cequel::Type[:ascii]]
98
98
  end
99
99
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.1
4
+ version: 1.0.0.pre.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mat Brown
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2013-09-13 00:00:00.000000000 Z
14
+ date: 2013-09-21 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -126,9 +126,10 @@ dependencies:
126
126
  - !ruby/object:Gem::Version
127
127
  version: '0.6'
128
128
  description: |
129
- Cequel is a lightweight query abstraction layer for Cassandra's CQL language. It
130
- also provides Cequel::Model, which is an ActiveModel-compliant object-row mapper
131
- for Cassandra. Cequel is heavily inspired by the Sequel library.
129
+ Cequel is an ActiveRecord-like domain model layer for Cassandra that exposes
130
+ the robust data modeling capabilities of CQL3, including parent-child
131
+ relationships via compound primary keys and in-memory atomic manipulation of
132
+ collection columns.
132
133
  email: mat.a.brown@gmail.com
133
134
  executables: []
134
135
  extensions: []
@@ -154,6 +155,7 @@ files:
154
155
  - lib/cequel/model/belongs_to_association.rb
155
156
  - lib/cequel/model/callbacks.rb
156
157
  - lib/cequel/model/collection.rb
158
+ - lib/cequel/model/dirty.rb
157
159
  - lib/cequel/model/errors.rb
158
160
  - lib/cequel/model/has_many_association.rb
159
161
  - lib/cequel/model/mass_assignment.rb
@@ -163,6 +165,7 @@ files:
163
165
  - lib/cequel/model/record_set.rb
164
166
  - lib/cequel/model/schema.rb
165
167
  - lib/cequel/model/scoped.rb
168
+ - lib/cequel/model/secondary_indexes.rb
166
169
  - lib/cequel/model/validations.rb
167
170
  - lib/cequel/model.rb
168
171
  - lib/cequel/new_relic_instrumentation.rb
@@ -178,12 +181,14 @@ files:
178
181
  - lib/cequel/schema/update_table_dsl.rb
179
182
  - lib/cequel/schema.rb
180
183
  - lib/cequel/type.rb
184
+ - lib/cequel/util.rb
181
185
  - lib/cequel/version.rb
182
186
  - lib/cequel.rb
183
187
  - spec/environment.rb
184
188
  - spec/examples/metal/data_set_spec.rb
185
189
  - spec/examples/model/associations_spec.rb
186
190
  - spec/examples/model/callbacks_spec.rb
191
+ - spec/examples/model/dirty_spec.rb
187
192
  - spec/examples/model/list_spec.rb
188
193
  - spec/examples/model/map_spec.rb
189
194
  - spec/examples/model/mass_assignment_spec.rb
@@ -192,6 +197,7 @@ files:
192
197
  - spec/examples/model/properties_spec.rb
193
198
  - spec/examples/model/record_set_spec.rb
194
199
  - spec/examples/model/schema_spec.rb
200
+ - spec/examples/model/secondary_index_spec.rb
195
201
  - spec/examples/model/serialization_spec.rb
196
202
  - spec/examples/model/set_spec.rb
197
203
  - spec/examples/model/spec_helper.rb
@@ -240,11 +246,12 @@ rubyforge_project:
240
246
  rubygems_version: 2.0.3
241
247
  signing_key:
242
248
  specification_version: 4
243
- summary: Query abstraction layer and object-row mapper for Cassandra and CQL
249
+ summary: Full-featured, ActiveModel-compliant ORM for Cassandra using CQL3
244
250
  test_files:
245
251
  - spec/examples/metal/data_set_spec.rb
246
252
  - spec/examples/model/associations_spec.rb
247
253
  - spec/examples/model/callbacks_spec.rb
254
+ - spec/examples/model/dirty_spec.rb
248
255
  - spec/examples/model/list_spec.rb
249
256
  - spec/examples/model/map_spec.rb
250
257
  - spec/examples/model/mass_assignment_spec.rb
@@ -253,6 +260,7 @@ test_files:
253
260
  - spec/examples/model/properties_spec.rb
254
261
  - spec/examples/model/record_set_spec.rb
255
262
  - spec/examples/model/schema_spec.rb
263
+ - spec/examples/model/secondary_index_spec.rb
256
264
  - spec/examples/model/serialization_spec.rb
257
265
  - spec/examples/model/set_spec.rb
258
266
  - spec/examples/model/spec_helper.rb