cequel 1.0.0.pre.4 → 1.0.0.pre.5

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: af518e804a2ce5cf3e021b7a2ba92302011c0cd4
4
- data.tar.gz: dadafdee945a8c961ac58fcf29ed3a76f84e616e
3
+ metadata.gz: f830646acc29101f50ff14afb06933e15fa88e35
4
+ data.tar.gz: c3d9d72fbf56847758e5de1191e7f48200b03423
5
5
  SHA512:
6
- metadata.gz: f90d210ad3a9ab787fcd6e70bba0e950f5983ade9d41f5f17f160b91eab470a84e4f5ca4f828be938db04ccb4bafda1f1be5fba1ea2db75667a2a82ee0d6043b
7
- data.tar.gz: 22436304a692c25bd34ce86b58d317630b81af527a28234e3d4bc4884617699cb0fc1092138e7c1a54867059f8118b183e8c4263bef175f735041948a5a53a93
6
+ metadata.gz: 981a73d5f5f4a6b5a07374df3f96ccdd599642927ea29aa652a13249234ffd18a40921aaa675345355de052072b94ecd8e8f2b63d47e16c2b1bdabbb7c7150e1
7
+ data.tar.gz: dc9035f9e642cdffac8f3b83ab9ec2abacb3498580cdfe4e3b6e5b370ec8a70fe22ab4e5f20a9f702fc5d8eacda436098a73fb04592d7858a73f64bd1720e1c6
data/lib/cequel/errors.rb CHANGED
@@ -2,4 +2,5 @@ module Cequel
2
2
  Error = Class.new(StandardError)
3
3
  EmptySubquery = Class.new(Error)
4
4
  InvalidSchemaMigration = Class.new(Error)
5
+ MissingKeyError = Class.new(Error)
5
6
  end
data/lib/cequel/record.rb CHANGED
@@ -7,6 +7,7 @@ require 'cequel/record/properties'
7
7
  require 'cequel/record/collection'
8
8
  require 'cequel/record/persistence'
9
9
  require 'cequel/record/record_set'
10
+ require 'cequel/record/bound'
10
11
  require 'cequel/record/scoped'
11
12
  require 'cequel/record/secondary_indexes'
12
13
  require 'cequel/record/associations'
@@ -0,0 +1,101 @@
1
+ module Cequel
2
+
3
+ module Record
4
+
5
+ class Bound
6
+ attr_reader :column, :value
7
+
8
+ def self.create(column, gt, inclusive, value)
9
+ implementation =
10
+ if column.partition_key?
11
+ PartitionKeyBound
12
+ elsif column.type?(Type::Timeuuid) && !value.is_a?(CassandraCQL::UUID)
13
+ TimeuuidBound
14
+ else
15
+ ClusteringColumnBound
16
+ end
17
+
18
+ implementation.new(column, gt, inclusive, value)
19
+ end
20
+
21
+ def initialize(column, gt, inclusive, value)
22
+ @column, @gt, @inclusive, @value = column, gt, inclusive, value
23
+ end
24
+
25
+ def to_cql_with_bind_variables
26
+ [to_cql, bind_value]
27
+ end
28
+
29
+
30
+ def gt?
31
+ !!@gt
32
+ end
33
+
34
+ def lt?
35
+ !gt?
36
+ end
37
+
38
+ def inclusive?
39
+ !!@inclusive
40
+ end
41
+
42
+ def exclusive?
43
+ !inclusive?
44
+ end
45
+
46
+ protected
47
+
48
+ def bind_value
49
+ column.cast(value)
50
+ end
51
+
52
+ def operator
53
+ exclusive? ? base_operator : "#{base_operator}="
54
+ end
55
+
56
+ def base_operator
57
+ lt? ? '<' : '>'
58
+ end
59
+
60
+ end
61
+
62
+ class PartitionKeyBound < Bound
63
+ def to_cql
64
+ "TOKEN(#{column.name}) #{operator} TOKEN(?)"
65
+ end
66
+ end
67
+
68
+ class ClusteringColumnBound < Bound
69
+ def to_cql
70
+ "#{column.name} #{operator} ?"
71
+ end
72
+ end
73
+
74
+ class TimeuuidBound < Bound
75
+ def to_cql
76
+ "#{column.name} #{operator} #{function}(?)"
77
+ end
78
+
79
+ protected
80
+
81
+ def operator
82
+ base_operator
83
+ end
84
+
85
+ def bind_value
86
+ cast_value = Type::Timestamp.instance.cast(value)
87
+ if inclusive?
88
+ lt? ? cast_value + 0.001 : cast_value - 0.001
89
+ else
90
+ cast_value
91
+ end
92
+ end
93
+
94
+ def function
95
+ lt? ^ exclusive? ? 'maxTimeuuid' : 'minTimeuuid'
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -10,10 +10,9 @@ module Cequel
10
10
  extend Forwardable
11
11
 
12
12
  def_delegators :@model, :loaded?, :updater, :deleter
13
+ def_delegator :@column, :name, :column_name
13
14
  def_delegators :__getobj__, :clone, :dup
14
15
 
15
- attr_reader :column_name
16
-
17
16
  included do
18
17
  private
19
18
  define_method(
@@ -21,8 +20,8 @@ module Cequel
21
20
  BasicObject.instance_method(:method_missing))
22
21
  end
23
22
 
24
- def initialize(model, column_name)
25
- @model, @column_name = model, column_name
23
+ def initialize(model, column)
24
+ @model, @column = model, column
26
25
  end
27
26
 
28
27
  def inspect
@@ -40,8 +39,8 @@ module Cequel
40
39
  protected
41
40
 
42
41
  def __getobj__
43
- @model.__send__(:read_attribute, @column_name) ||
44
- @model.__send__(:write_attribute, @column_name, self.class.empty)
42
+ model.__send__(:read_attribute, column_name) ||
43
+ model.__send__(:write_attribute, column_name, self.class.empty)
45
44
  end
46
45
 
47
46
  def __setobj__(obj)
@@ -49,10 +48,14 @@ module Cequel
49
48
  end
50
49
 
51
50
  private
51
+ attr_reader :model, :column
52
+ def_delegator :column, :cast, :cast_collection
53
+ def_delegator 'column.type', :cast, :cast_element
54
+ private :cast_collection, :cast_element
52
55
 
53
56
  def to_modify(&block)
54
57
  if loaded?
55
- @model.__send__("#{@column_name}_will_change!")
58
+ model.__send__("#{column_name}_will_change!")
56
59
  block.()
57
60
  else modifications << block
58
61
  end
@@ -98,11 +101,17 @@ module Cequel
98
101
  if Range === position then first, count = position.first, position.count
99
102
  else first, count = position, args[-2]
100
103
  end
101
- element = args[-1]
104
+
105
+ element = args[-1] =
106
+ if args[-1].is_a?(Array) then cast_collection(args[-1])
107
+ else cast_element(args[-1])
108
+ end
109
+
102
110
  if first < 0
103
111
  raise ArgumentError,
104
112
  "Bad index #{position}: CQL lists do not support negative indices"
105
113
  end
114
+
106
115
  if count.nil?
107
116
  updater.list_replace(column_name, first, element)
108
117
  else
@@ -124,11 +133,13 @@ module Cequel
124
133
  end
125
134
 
126
135
  def concat(array)
136
+ array = cast_collection(array)
127
137
  updater.list_append(column_name, array)
128
138
  to_modify { super }
129
139
  end
130
140
 
131
141
  def delete(object)
142
+ object = cast_element(object)
132
143
  updater.list_remove(column_name, object)
133
144
  to_modify { super }
134
145
  end
@@ -139,17 +150,20 @@ module Cequel
139
150
  end
140
151
 
141
152
  def push(object)
153
+ object = cast_element(object)
142
154
  updater.list_append(column_name, object)
143
155
  to_modify { super }
144
156
  end
145
157
  alias_method :<<, :push
146
158
 
147
159
  def replace(array)
160
+ array = cast_collection(array)
148
161
  updater.set(column_name => array)
149
162
  to_modify { super }
150
163
  end
151
164
 
152
165
  def unshift(*objs)
166
+ objs.map!(&method(:cast_element))
153
167
  updater.list_prepend(column_name, objs.reverse)
154
168
  to_modify { super }
155
169
  end
@@ -175,6 +189,7 @@ module Cequel
175
189
  each { |method| undef_method(method) if method_defined? method }
176
190
 
177
191
  def add(object)
192
+ object = cast_element(object)
178
193
  updater.set_add(column_name, object)
179
194
  to_modify { super }
180
195
  end
@@ -186,11 +201,13 @@ module Cequel
186
201
  end
187
202
 
188
203
  def delete(object)
204
+ object = cast_element(object)
189
205
  updater.set_remove(column_name, object)
190
206
  to_modify { super }
191
207
  end
192
208
 
193
209
  def replace(set)
210
+ set = cast_collection(set)
194
211
  updater.set(column_name => set)
195
212
  to_modify { super }
196
213
  end
@@ -200,6 +217,7 @@ module Cequel
200
217
  class Map < DelegateClass(::Hash)
201
218
 
202
219
  include Collection
220
+ extend Forwardable
203
221
 
204
222
  NON_ATOMIC_MUTATORS = [
205
223
  :default,
@@ -226,6 +244,7 @@ module Cequel
226
244
  each { |method| undef_method(method) if method_defined? method }
227
245
 
228
246
  def []=(key, value)
247
+ key = cast_key(key)
229
248
  updater.map_update(column_name, key => value)
230
249
  to_modify { super }
231
250
  end
@@ -237,21 +256,28 @@ module Cequel
237
256
  end
238
257
 
239
258
  def delete(key)
259
+ key = cast_key(key)
240
260
  deleter.map_remove(column_name, key)
241
261
  to_modify { super }
242
262
  end
243
263
 
244
264
  def merge!(hash)
265
+ hash = cast_collection(hash)
245
266
  updater.map_update(column_name, hash)
246
267
  to_modify { super }
247
268
  end
248
269
  alias_method :update, :merge!
249
270
 
250
271
  def replace(hash)
272
+ hash = cast_collection(hash)
251
273
  updater.set(column_name => hash)
252
274
  to_modify { super }
253
275
  end
254
276
 
277
+ private
278
+ def_delegator 'column.key_type', :cast, :cast_key
279
+ private :cast_key
280
+
255
281
  end
256
282
 
257
283
  end
@@ -41,6 +41,7 @@ module Cequel
41
41
  alias :exist? :exists?
42
42
 
43
43
  def load
44
+ assert_keys_present!
44
45
  unless loaded?
45
46
  row = metal_scope.first
46
47
  hydrate(row) unless row.nil?
@@ -77,6 +78,7 @@ module Cequel
77
78
  end
78
79
 
79
80
  def destroy
81
+ assert_keys_present!
80
82
  metal_scope.delete
81
83
  transient!
82
84
  self
@@ -105,12 +107,14 @@ module Cequel
105
107
  end
106
108
 
107
109
  def create
110
+ assert_keys_present!
108
111
  metal_scope.insert(attributes.reject { |attr, value| value.nil? })
109
112
  loaded!
110
113
  persisted!
111
114
  end
112
115
 
113
116
  def update
117
+ assert_keys_present!
114
118
  connection.batch do
115
119
  updater.execute
116
120
  deleter.execute
@@ -135,13 +139,21 @@ module Cequel
135
139
  super
136
140
  end
137
141
 
138
- def write_attribute(attribute, value)
142
+ def write_attribute(name, value)
143
+ column = self.class.reflect_on_column(name)
144
+ raise UnknownAttributeError, "unknown attribute: #{name}" unless column
145
+ value = column.cast(value) unless value.nil?
146
+
139
147
  super.tap do
140
148
  unless new_record?
149
+ if key_attributes.keys.include?(name)
150
+ raise ArgumentError, "Can't update key #{name} on persisted record"
151
+ end
152
+
141
153
  if value.nil?
142
- deleter.delete_columns(attribute)
154
+ deleter.delete_columns(name)
143
155
  else
144
- updater.set(attribute => value)
156
+ updater.set(name => value)
145
157
  end
146
158
  end
147
159
  end
@@ -178,6 +190,14 @@ module Cequel
178
190
  @attributes_for_deletion ||= []
179
191
  end
180
192
 
193
+ def assert_keys_present!
194
+ missing_keys = key_attributes.select { |k, v| v.nil? }
195
+ if missing_keys.any?
196
+ raise MissingKeyError,
197
+ "Missing required key values: #{missing_keys.keys.join(', ')}"
198
+ end
199
+ end
200
+
181
201
  end
182
202
 
183
203
  end
@@ -72,13 +72,13 @@ module Cequel
72
72
  end
73
73
 
74
74
  def def_reader(name)
75
- module_eval <<-RUBY
75
+ module_eval <<-RUBY, __FILE__, __LINE__+1
76
76
  def #{name}; read_attribute(#{name.inspect}); end
77
77
  RUBY
78
78
  end
79
79
 
80
80
  def def_writer(name)
81
- module_eval <<-RUBY
81
+ module_eval <<-RUBY, __FILE__, __LINE__+1
82
82
  def #{name}=(value); write_attribute(#{name.inspect}, value); end
83
83
  RUBY
84
84
  end
@@ -89,7 +89,7 @@ module Cequel
89
89
  end
90
90
 
91
91
  def def_collection_reader(name, collection_proxy_class)
92
- module_eval <<-RUBY
92
+ module_eval <<-RUBY, __FILE__, __LINE__+1
93
93
  def #{name}
94
94
  proxy_collection(#{name.inspect}, #{collection_proxy_class})
95
95
  end
@@ -97,7 +97,7 @@ module Cequel
97
97
  end
98
98
 
99
99
  def def_collection_writer(name)
100
- module_eval <<-RUBY
100
+ module_eval <<-RUBY, __FILE__, __LINE__+1
101
101
  def #{name}=(value)
102
102
  reset_collection_proxy(#{name.inspect})
103
103
  write_attribute(#{name.inspect}, value)
@@ -163,16 +163,14 @@ module Cequel
163
163
  end
164
164
 
165
165
  def write_attribute(name, value)
166
- column = self.class.reflect_on_column(name)
167
- raise UnknownAttributeError,
168
- "unknown attribute: #{name}" unless column
169
- @attributes[name] = value.nil? ? nil : column.cast(value)
166
+ @attributes[name] = value
170
167
  end
171
168
 
172
169
  private
173
170
 
174
- def proxy_collection(name, proxy_class)
175
- collection_proxies[name] ||= proxy_class.new(self, name)
171
+ def proxy_collection(column_name, proxy_class)
172
+ column = self.class.reflect_on_column(column_name)
173
+ collection_proxies[column_name] ||= proxy_class.new(self, column)
176
174
  end
177
175
 
178
176
  def reset_collection_proxy(name)
@@ -8,8 +8,6 @@ module Cequel
8
8
  extend Cequel::Util::HashAccessors
9
9
  include Enumerable
10
10
 
11
- Bound = Struct.new(:value, :inclusive)
12
-
13
11
  def self.default_attributes
14
12
  {:scoped_key_values => [], :select_columns => []}
15
13
  end
@@ -34,7 +32,7 @@ module Cequel
34
32
  end
35
33
 
36
34
  def where(column_name, value)
37
- column = clazz.table_schema.column(column_name)
35
+ column = clazz.reflect_on_column(column_name)
38
36
  raise IllegalQuery,
39
37
  "Can't scope by more than one indexed column in the same query" if scoped_indexed_column
40
38
  raise ArgumentError,
@@ -43,17 +41,21 @@ module Cequel
43
41
  "Use the `at` method to restrict scope by primary key" unless column.data_column?
44
42
  raise ArgumentError,
45
43
  "Can't scope by non-indexed column #{column_name}" unless column.indexed?
46
- scoped(scoped_indexed_column: {column_name => value})
44
+ scoped(scoped_indexed_column: {column_name => column.cast(value)})
47
45
  end
48
46
 
49
47
  def at(*scoped_key_values)
50
48
  scoped do |attributes|
51
- attributes[:scoped_key_values].concat(scoped_key_values)
49
+ type_cast_key_values = scoped_key_values.zip(unscoped_key_columns).
50
+ map { |value, column| column.cast(value) }
51
+ attributes[:scoped_key_values].concat(type_cast_key_values)
52
52
  end
53
53
  end
54
54
 
55
55
  def [](scoped_key_value)
56
- if next_key_column
56
+ scoped_key_value = cast_range_key(scoped_key_value)
57
+
58
+ if next_range_key_column
57
59
  at(scoped_key_value)
58
60
  else
59
61
  attributes = {}
@@ -74,17 +76,17 @@ module Cequel
74
76
  end
75
77
 
76
78
  def after(start_key)
77
- scoped(lower_bound: Bound.new(start_key, false))
79
+ scoped(lower_bound: bound(true, false, start_key))
78
80
  end
79
81
 
80
82
  def before(end_key)
81
- scoped(upper_bound: Bound.new(end_key, false))
83
+ scoped(upper_bound: bound(false, false, end_key))
82
84
  end
83
85
 
84
86
  def in(range)
85
87
  scoped(
86
- lower_bound: Bound.new(range.first, true),
87
- upper_bound: Bound.new(range.last, !range.exclude_end?)
88
+ lower_bound: bound(true, true, range.first),
89
+ upper_bound: bound(false, !range.exclude_end?, range.last)
88
90
  )
89
91
  end
90
92
 
@@ -93,7 +95,7 @@ module Cequel
93
95
  raise IllegalQuery,
94
96
  "Can't construct exclusive range on partition key #{range_key_name}"
95
97
  end
96
- scoped(lower_bound: Bound.new(start_key, true))
98
+ scoped(lower_bound: bound(true, true, start_key))
97
99
  end
98
100
 
99
101
  def upto(end_key)
@@ -101,7 +103,7 @@ module Cequel
101
103
  raise IllegalQuery,
102
104
  "Can't construct exclusive range on partition key #{range_key_name}"
103
105
  end
104
- scoped(upper_bound: Bound.new(end_key, true))
106
+ scoped(upper_bound: bound(false, true, end_key))
105
107
  end
106
108
 
107
109
  def reverse
@@ -186,7 +188,7 @@ module Cequel
186
188
  end
187
189
 
188
190
  def find_nested_batches_from(row, options, &block)
189
- if next_key_column
191
+ if next_range_key_column
190
192
  at(row[range_key_name]).
191
193
  next_batch_from(row).
192
194
  find_rows_in_batches(options, &block)
@@ -204,43 +206,57 @@ module Cequel
204
206
  end
205
207
  end
206
208
 
209
+ def scoped_key_names
210
+ scoped_key_columns.map { |column| column.name }
211
+ end
212
+
213
+ def scoped_key_columns
214
+ clazz.key_columns.first(scoped_key_values.length)
215
+ end
216
+
217
+ def unscoped_key_columns
218
+ clazz.key_columns.drop(scoped_key_values.length)
219
+ end
220
+
221
+ def unscoped_key_names
222
+ unscoped_key_columns.map { |column| column.name }
223
+ end
224
+
207
225
  def range_key_column
208
- clazz.key_columns[scoped_key_values.length]
226
+ unscoped_key_columns.first
209
227
  end
210
228
 
211
229
  def range_key_name
212
230
  range_key_column.name
213
231
  end
214
232
 
215
- def scoped_key_columns
216
- clazz.key_columns.first(scoped_key_values.length)
233
+ def next_range_key_column
234
+ unscoped_key_columns.second
217
235
  end
218
236
 
219
- def scoped_key_names
220
- scoped_key_columns.map { |column| column.name }
237
+ def next_range_key_name
238
+ next_range_key_column.try(:name)
221
239
  end
222
240
 
223
241
  def single_partition?
224
242
  scoped_key_values.length >= clazz.partition_key_columns.length
225
243
  end
226
244
 
245
+ # Try to order results by the first clustering column. Fall back to partition key if none exist.
246
+ def order_by_column
247
+ clazz.clustering_columns.first.name if clazz.clustering_columns.any?
248
+ end
249
+
227
250
  private
228
251
  attr_reader :clazz
229
252
  def_delegators :clazz, :connection
230
- private :connection
253
+ def_delegator :range_key_column, :cast, :cast_range_key
254
+ private :connection, :cast_range_key
231
255
 
232
256
  def method_missing(method, *args, &block)
233
257
  clazz.with_scope(self) { super }
234
258
  end
235
259
 
236
- def next_key_column
237
- clazz.key_columns[scoped_key_values.length + 1]
238
- end
239
-
240
- def next_key_name
241
- next_key_column.name if next_key_column
242
- end
243
-
244
260
  def construct_data_set
245
261
  data_set = connection[clazz.table_name]
246
262
  data_set = data_set.limit(row_limit) if row_limit
@@ -250,24 +266,26 @@ module Cequel
250
266
  data_set = data_set.where(key_conditions)
251
267
  end
252
268
  if lower_bound
253
- fragment = construct_bound_fragment(lower_bound, '>')
254
- data_set = data_set.where(fragment, lower_bound.value)
269
+ data_set = data_set.where(*lower_bound.to_cql_with_bind_variables)
255
270
  end
256
271
  if upper_bound
257
- fragment = construct_bound_fragment(upper_bound, '<')
258
- data_set = data_set.where(fragment, upper_bound.value)
272
+ data_set = data_set.where(*upper_bound.to_cql_with_bind_variables)
259
273
  end
260
- data_set = data_set.order(range_key_name => :desc) if reversed?
274
+ data_set = data_set.order(order_by_column => :desc) if reversed?
261
275
  data_set = data_set.where(scoped_indexed_column) if scoped_indexed_column
262
276
  data_set
263
277
  end
264
278
 
265
- def construct_bound_fragment(bound, base_operator)
266
- operator = bound.inclusive ? "#{base_operator}=" : base_operator
267
- single_partition? ?
268
- "#{range_key_name} #{operator} ?" :
269
- "TOKEN(#{range_key_name}) #{operator} TOKEN(?)"
279
+ def bound(gt, inclusive, value)
280
+ Bound.create(range_key_column, gt, inclusive, value)
281
+ end
270
282
 
283
+ def cast_range_key_for_bound(value)
284
+ if range_key_column.type?(Type::Timeuuid) && !value.is_a?(CassandraCQL::UUID)
285
+ Type::Timestamp.instance.cast(value)
286
+ else
287
+ cast_range_key(value)
288
+ end
271
289
  end
272
290
 
273
291
  def scoped(new_attributes = {}, &block)