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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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