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 +4 -4
- data/lib/cequel.rb +2 -0
- data/lib/cequel/model.rb +2 -0
- data/lib/cequel/model/base.rb +10 -0
- data/lib/cequel/model/collection.rb +5 -1
- data/lib/cequel/model/dirty.rb +62 -0
- data/lib/cequel/model/errors.rb +1 -0
- data/lib/cequel/model/record_set.rb +61 -72
- data/lib/cequel/model/schema.rb +2 -1
- data/lib/cequel/model/scoped.rb +1 -1
- data/lib/cequel/model/secondary_indexes.rb +31 -0
- data/lib/cequel/model/validations.rb +1 -1
- data/lib/cequel/schema/column.rb +18 -2
- data/lib/cequel/schema/table.rb +6 -6
- data/lib/cequel/schema/table_synchronizer.rb +3 -3
- data/lib/cequel/schema/table_writer.rb +2 -1
- data/lib/cequel/util.rb +46 -0
- data/lib/cequel/version.rb +1 -1
- data/spec/examples/model/dirty_spec.rb +68 -0
- data/spec/examples/model/record_set_spec.rb +33 -2
- data/spec/examples/model/schema_spec.rb +1 -1
- data/spec/examples/model/secondary_index_spec.rb +45 -0
- data/spec/examples/model/spec_helper.rb +0 -1
- data/spec/examples/schema/table_reader_spec.rb +8 -8
- data/spec/examples/schema/table_writer_spec.rb +8 -8
- metadata +14 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7888f93ffd3d74bdfef143a9c8d18c6c9f2f4ba1
|
4
|
+
data.tar.gz: 831e5a90a03cf1c22ddf08421284107254aa59a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41a8585f9c78171c34944551914e4e06b44fabe615b9192901782b6653be345314c63ad4e4b5b048818bb10e1d4085aad24006285a9bd573489153b24719ccc3
|
7
|
+
data.tar.gz: 90297a4b794c2eb265ffdc7659f80c1b90ef8870c1d04ff910d4d015e425a47395b8151fa72c8cf85266538e5e7fe3c495b9433751776eb727ee5983f986314c
|
data/lib/cequel.rb
CHANGED
data/lib/cequel/model.rb
CHANGED
@@ -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
|
|
data/lib/cequel/model/base.rb
CHANGED
@@ -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?
|
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
|
data/lib/cequel/model/errors.rb
CHANGED
@@ -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
|
13
|
-
|
14
|
-
|
15
|
-
|
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 { |
|
36
|
+
scoped { |attributes| attributes[:select_columns].concat(columns) }
|
25
37
|
end
|
26
38
|
|
27
39
|
def limit(count)
|
28
|
-
scoped
|
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
|
-
|
33
|
-
|
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
|
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
|
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
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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
|
232
|
+
class SinglePartitionRecordSet < RecordSet
|
231
233
|
|
232
|
-
|
233
|
-
super
|
234
|
-
@reversed = false
|
235
|
-
end
|
234
|
+
hattr_inquirer :attributes, :reversed
|
236
235
|
|
237
236
|
def from(start_key)
|
238
|
-
scoped
|
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
|
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
|
245
|
+
scoped(reversed: !reversed?)
|
251
246
|
end
|
252
247
|
|
253
248
|
def last
|
254
249
|
reverse.first
|
255
250
|
end
|
256
251
|
|
257
|
-
|
258
|
-
|
259
|
-
|
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} ?"
|
data/lib/cequel/model/schema.rb
CHANGED
@@ -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.
|
data/lib/cequel/model/scoped.rb
CHANGED
@@ -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
|
data/lib/cequel/schema/column.rb
CHANGED
@@ -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
|
57
|
-
|
72
|
+
def clustering_column?
|
73
|
+
true
|
58
74
|
end
|
59
75
|
|
60
76
|
end
|
data/lib/cequel/schema/table.rb
CHANGED
@@ -8,7 +8,7 @@ module Cequel
|
|
8
8
|
|
9
9
|
attr_reader :name,
|
10
10
|
:columns,
|
11
|
-
:
|
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
|
-
@
|
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 @
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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.
|
89
|
+
if existing.partition_key_columns.length != updated.partition_key_columns.length
|
90
90
|
raise InvalidSchemaMigration,
|
91
|
-
"Existing partition keys #{existing.
|
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.
|
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.
|
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(', ')
|
data/lib/cequel/util.rb
ADDED
@@ -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
|
data/lib/cequel/version.rb
CHANGED
@@ -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(:
|
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
|
@@ -14,11 +14,11 @@ describe Cequel::Schema::TableReader do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'should read name correctly' do
|
17
|
-
table.
|
17
|
+
table.partition_key_columns.first.name.should == :permalink
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'should read type correctly' do
|
21
|
-
table.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
22
|
+
table.partition_key_columns.map(&:name).should == [:permalink]
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'should set key validator' do
|
26
|
-
table.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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-
|
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
|
130
|
-
|
131
|
-
|
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:
|
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
|