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

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: 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)