cequel 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +3 -0
  3. data/CHANGELOG.md +7 -0
  4. data/CONTRIBUTING.md +1 -1
  5. data/Gemfile +5 -4
  6. data/Gemfile.lock +215 -22
  7. data/README.md +19 -6
  8. data/Vagrantfile +6 -1
  9. data/lib/cequel.rb +3 -2
  10. data/lib/cequel/metal/batch.rb +16 -1
  11. data/lib/cequel/metal/data_set.rb +63 -39
  12. data/lib/cequel/metal/deleter.rb +2 -2
  13. data/lib/cequel/metal/incrementer.rb +2 -2
  14. data/lib/cequel/metal/inserter.rb +8 -6
  15. data/lib/cequel/metal/keyspace.rb +127 -34
  16. data/lib/cequel/metal/logger.rb +1 -1
  17. data/lib/cequel/metal/row.rb +3 -4
  18. data/lib/cequel/metal/updater.rb +2 -2
  19. data/lib/cequel/metal/writer.rb +18 -12
  20. data/lib/cequel/record/associations.rb +7 -1
  21. data/lib/cequel/record/bound.rb +1 -1
  22. data/lib/cequel/record/callbacks.rb +10 -6
  23. data/lib/cequel/record/data_set_builder.rb +13 -2
  24. data/lib/cequel/record/persistence.rb +14 -12
  25. data/lib/cequel/record/properties.rb +3 -3
  26. data/lib/cequel/record/railtie.rb +1 -1
  27. data/lib/cequel/record/record_set.rb +12 -12
  28. data/lib/cequel/record/schema.rb +5 -1
  29. data/lib/cequel/schema/keyspace.rb +1 -1
  30. data/lib/cequel/schema/table_property.rb +1 -1
  31. data/lib/cequel/type.rb +44 -10
  32. data/lib/cequel/uuids.rb +46 -0
  33. data/lib/cequel/version.rb +1 -1
  34. data/spec/examples/metal/data_set_spec.rb +93 -0
  35. data/spec/examples/metal/keyspace_spec.rb +74 -0
  36. data/spec/examples/record/associations_spec.rb +84 -0
  37. data/spec/examples/record/persistence_spec.rb +23 -0
  38. data/spec/examples/record/properties_spec.rb +1 -1
  39. data/spec/examples/record/record_set_spec.rb +12 -3
  40. data/spec/examples/record/schema_spec.rb +1 -1
  41. data/spec/examples/record/secondary_index_spec.rb +1 -1
  42. data/spec/examples/schema/table_reader_spec.rb +2 -3
  43. data/spec/examples/spec_helper.rb +8 -0
  44. data/spec/examples/type_spec.rb +7 -5
  45. data/spec/examples/uuids_spec.rb +22 -0
  46. data/spec/support/helpers.rb +25 -19
  47. data/templates/config/cequel.yml +5 -5
  48. metadata +40 -33
@@ -47,7 +47,7 @@ module Cequel
47
47
 
48
48
  private
49
49
 
50
- def_delegator 'CassandraCQL::Statement', :sanitize
50
+ def_delegator 'Cequel::Metal::Keyspace', :sanitize
51
51
  end
52
52
 
53
53
  #
@@ -9,9 +9,9 @@ module Cequel
9
9
  #
10
10
  class Row < DelegateClass(ActiveSupport::HashWithIndifferentAccess)
11
11
  #
12
- # Encapsulate a row from CassandraCQL
12
+ # Encapsulate a result row from the driver
13
13
  #
14
- # @param result_row [CassandraCQL::Row] row from underlying driver
14
+ # @param result_row [Hash] row from underlying driver
15
15
  # @return [Row] encapsulated row
16
16
  #
17
17
  # @api private
@@ -19,8 +19,7 @@ module Cequel
19
19
  def self.from_result_row(result_row)
20
20
  if result_row
21
21
  new.tap do |row|
22
- names, values = result_row.column_names, result_row.column_values
23
- names.zip(values) do |name, value|
22
+ result_row.each_pair do |name, value|
24
23
  if name =~ /^(ttl|writetime)\((.+)\)$/
25
24
  if $1 == 'ttl' then row.set_ttl($2, value)
26
25
  else row.set_writetime($2, value)
@@ -143,10 +143,10 @@ module Cequel
143
143
  super && column_updates.empty?
144
144
  end
145
145
 
146
- def write_to_statement(statement)
146
+ def write_to_statement(statement, options)
147
147
  prepare_column_updates
148
148
  statement.append("UPDATE #{table_name}")
149
- .append(generate_upsert_options)
149
+ .append(generate_upsert_options(options))
150
150
  .append(" SET ")
151
151
  .append(statements.join(', '), *bind_vars)
152
152
  end
@@ -15,13 +15,8 @@ module Cequel
15
15
 
16
16
  #
17
17
  # @param data_set [DataSet] data set to write to
18
- # @param options [Options] options
19
- # @option options [Integer] :ttl time-to-live in seconds for the written
20
- # data
21
- # @option options [Time,Integer] :timestamp the timestamp associated with
22
- # the column values
23
18
  #
24
- def initialize(data_set, options = {}, &block)
19
+ def initialize(data_set, &block)
25
20
  @data_set, @options, @block = data_set, options, block
26
21
  @statements, @bind_vars = [], []
27
22
  SimpleDelegator.new(self).instance_eval(&block) if block
@@ -30,14 +25,24 @@ module Cequel
30
25
  #
31
26
  # Execute the statement as a write operation
32
27
  #
28
+ # @param options [Options] options
29
+ # @opiton options [Symbol] :consistency what consistency level to use for
30
+ # the operation
31
+ # @option options [Integer] :ttl time-to-live in seconds for the written
32
+ # data
33
+ # @option options [Time,Integer] :timestamp the timestamp associated with
34
+ # the column values
33
35
  # @return [void]
34
36
  #
35
- def execute
37
+ def execute(options = {})
38
+ options.assert_valid_keys(:timestamp, :ttl, :consistency)
36
39
  return if empty?
37
40
  statement = Statement.new
38
- write_to_statement(statement)
41
+ consistency = options.fetch(:consistency, data_set.query_consistency)
42
+ write_to_statement(statement, options)
39
43
  statement.append(*data_set.row_specifications_cql)
40
- data_set.write(*statement.args)
44
+ data_set.write_with_consistency(
45
+ statement.cql, statement.bind_vars, consistency)
41
46
  end
42
47
 
43
48
  private
@@ -63,12 +68,13 @@ module Cequel
63
68
  #
64
69
  # Generate CQL option statement for inserts and updates
65
70
  #
66
- def generate_upsert_options
67
- if options.empty?
71
+ def generate_upsert_options(options)
72
+ upsert_options = options.slice(:timestamp, :ttl)
73
+ if upsert_options.empty?
68
74
  ''
69
75
  else
70
76
  ' USING ' <<
71
- options.map do |key, value|
77
+ upsert_options.map do |key, value|
72
78
  serialized_value =
73
79
  case key
74
80
  when :timestamp then (value.to_f * 1_000_000).to_i
@@ -82,6 +82,10 @@ module Cequel
82
82
  # has a primary key `(subdomain)`, this will declare a key column
83
83
  # `blog_subdomain` of the same type.
84
84
  #
85
+ # If the parent class has multiple keys, e.g. it belongs to a parent
86
+ # class, defining a `partition: true` option will declare all of the
87
+ # parent's keys as partition key columns for this class.
88
+ #
85
89
  # Parent associations are read/write, so declaring `belongs_to :blog`
86
90
  # will define a `blog` getter and `blog=` setter, which will update the
87
91
  # underlying key column. Note that a record's parent cannot be changed
@@ -104,12 +108,14 @@ module Cequel
104
108
  "belongs_to association must be declared before declaring " \
105
109
  "key(s)"
106
110
  end
111
+
112
+ key_options = options.extract!(:partition)
107
113
 
108
114
  self.parent_association =
109
115
  BelongsToAssociation.new(self, name.to_sym, options)
110
116
 
111
117
  parent_association.association_key_columns.each do |column|
112
- key :"#{name}_#{column.name}", column.type
118
+ key :"#{name}_#{column.name}", column.type, key_options
113
119
  end
114
120
  def_parent_association_accessors
115
121
  end
@@ -28,7 +28,7 @@ module Cequel
28
28
  implementation =
29
29
  if column.partition_key?
30
30
  PartitionKeyBound
31
- elsif column.type?(:timeuuid) && !value.is_a?(CassandraCQL::UUID)
31
+ elsif column.type?(:timeuuid) && !Cequel.uuid?(value)
32
32
  TimeuuidBound
33
33
  else
34
34
  ClusteringColumnBound
@@ -31,21 +31,25 @@ module Cequel
31
31
 
32
32
  # (see Persistence#save)
33
33
  def save(options = {})
34
- connection.batch { run_callbacks(:save) { super } }
34
+ connection.batch(options.slice(:consistency)) do
35
+ run_callbacks(:save) { super }
36
+ end
35
37
  end
36
38
 
37
- # (see Persistence#save)
38
- def destroy
39
- connection.batch { run_callbacks(:destroy) { super } }
39
+ # (see Persistence#destroy)
40
+ def destroy(options = {})
41
+ connection.batch(options.slice(:consistency)) do
42
+ run_callbacks(:destroy) { super }
43
+ end
40
44
  end
41
45
 
42
46
  protected
43
47
 
44
- def create
48
+ def create(*)
45
49
  run_callbacks(:create) { super }
46
50
  end
47
51
 
48
- def update
52
+ def update(*)
49
53
  run_callbacks(:update) { super }
50
54
  end
51
55
  end
@@ -14,7 +14,7 @@ module Cequel
14
14
  # Build a data set for the given record set
15
15
  #
16
16
  # @param (see #initialize)
17
- # @return [Metal::DataSet] a DataSet exposing the rows for the record set
17
+ # @return (see #build)
18
18
  #
19
19
  def self.build_for(record_set)
20
20
  new(record_set).build
@@ -30,12 +30,16 @@ module Cequel
30
30
  end
31
31
  private_class_method :new
32
32
 
33
+ #
34
+ # @return [Metal::DataSet] a DataSet exposing the rows for the record set
35
+ #
33
36
  def build
34
37
  add_limit
35
38
  add_select_columns
36
39
  add_where_statement
37
40
  add_bounds
38
41
  add_order
42
+ set_consistency
39
43
  data_set
40
44
  end
41
45
 
@@ -46,7 +50,8 @@ module Cequel
46
50
  def_delegators :record_set, :row_limit, :select_columns,
47
51
  :scoped_key_names, :scoped_key_values,
48
52
  :scoped_indexed_column, :lower_bound,
49
- :upper_bound, :reversed?, :order_by_column
53
+ :upper_bound, :reversed?, :order_by_column,
54
+ :query_consistency
50
55
 
51
56
  private
52
57
 
@@ -82,6 +87,12 @@ module Cequel
82
87
  def add_order
83
88
  self.data_set = data_set.order(order_by_column => :desc) if reversed?
84
89
  end
90
+
91
+ def set_consistency
92
+ if query_consistency
93
+ self.data_set = data_set.consistency(query_consistency)
94
+ end
95
+ end
85
96
  end
86
97
  end
87
98
  end
@@ -171,9 +171,9 @@ module Cequel
171
171
  # @see Validations#save!
172
172
  #
173
173
  def save(options = {})
174
- options.assert_valid_keys
175
- if new_record? then create
176
- else update
174
+ options.assert_valid_keys(:consistency)
175
+ if new_record? then create(options)
176
+ else update(options)
177
177
  end
178
178
  @new_record = false
179
179
  true
@@ -199,9 +199,10 @@ module Cequel
199
199
  #
200
200
  # @return [Record] self
201
201
  #
202
- def destroy
202
+ def destroy(options = {})
203
+ options.assert_valid_keys(:consistency)
203
204
  assert_keys_present!
204
- metal_scope.delete
205
+ metal_scope.delete(options)
205
206
  transient!
206
207
  self
207
208
  end
@@ -252,28 +253,29 @@ module Cequel
252
253
  self
253
254
  end
254
255
 
255
- def create
256
+ def create(options = {})
256
257
  assert_keys_present!
257
- metal_scope.insert(attributes.reject { |attr, value| value.nil? })
258
+ metal_scope
259
+ .insert(attributes.reject { |attr, value| value.nil? }, options)
258
260
  loaded!
259
261
  persisted!
260
262
  end
261
263
 
262
- def update
264
+ def update(options = {})
263
265
  assert_keys_present!
264
266
  connection.batch do
265
- updater.execute
266
- deleter.execute
267
+ updater.execute(options)
268
+ deleter.execute(options)
267
269
  @updater, @deleter = nil
268
270
  end
269
271
  end
270
272
 
271
273
  def updater
272
- @updater ||= metal_scope.updater
274
+ @updater ||= Metal::Updater.new(metal_scope)
273
275
  end
274
276
 
275
277
  def deleter
276
- @deleter ||= metal_scope.deleter
278
+ @deleter ||= Metal::Deleter.new(metal_scope)
277
279
  end
278
280
 
279
281
  private
@@ -101,7 +101,7 @@ module Cequel
101
101
  unless Type[type].is_a?(Cequel::Type::Uuid)
102
102
  fail ArgumentError, ":auto option only valid for UUID columns"
103
103
  end
104
- default = -> { CassandraCQL::UUID.new } if options[:auto]
104
+ default = -> { Cequel.uuid } if options[:auto]
105
105
  end
106
106
  set_attribute_default(name, default)
107
107
  end
@@ -300,8 +300,8 @@ module Cequel
300
300
  #
301
301
  def inspect
302
302
  inspected_attributes = attributes.each_pair.map do |attr, value|
303
- inspected_value = value.is_a?(CassandraCQL::UUID) ?
304
- value.to_guid :
303
+ inspected_value = Cequel.uuid?(value) ?
304
+ value.to_s :
305
305
  value.inspect
306
306
  "#{attr}: #{inspected_value}"
307
307
  end
@@ -20,7 +20,7 @@ module Cequel
20
20
  config = YAML.load(ERB.new(IO.read(config_path)).result)[Rails.env]
21
21
  .deep_symbolize_keys
22
22
  else
23
- config = {host: '127.0.0.1:9160'}
23
+ config = {host: '127.0.0.1:9042'}
24
24
  end
25
25
  config.reverse_merge!(keyspace: "#{Railtie.app_name}_#{Rails.env}")
26
26
  connection = Cequel.connect(config)
@@ -454,6 +454,16 @@ module Cequel
454
454
  scoped(reversed: !reversed?)
455
455
  end
456
456
 
457
+ #
458
+ # Set the consistency at which to read records into the record set.
459
+ #
460
+ # @param consistency [Symbol] consistency for reads
461
+ # @return [RecordSet] record set tuned to given consistency
462
+ #
463
+ def consistency(consistency)
464
+ scoped(query_consistency: consistency)
465
+ end
466
+
457
467
  #
458
468
  # @overload first
459
469
  # @return [Record] the first record in this record set
@@ -634,9 +644,9 @@ module Cequel
634
644
  attr_reader :attributes
635
645
  hattr_reader :attributes, :select_columns, :scoped_key_values,
636
646
  :row_limit, :lower_bound, :upper_bound,
637
- :scoped_indexed_column
647
+ :scoped_indexed_column, :query_consistency
638
648
  protected :select_columns, :scoped_key_values, :row_limit, :lower_bound,
639
- :upper_bound, :scoped_indexed_column
649
+ :upper_bound, :scoped_indexed_column, :query_consistency
640
650
  hattr_inquirer :attributes, :reversed
641
651
  protected :reversed?
642
652
 
@@ -790,16 +800,6 @@ module Cequel
790
800
  Bound.create(range_key_column, gt, inclusive, value)
791
801
  end
792
802
 
793
- def cast_range_key_for_bound(value)
794
- if range_key_column.type?(Type::Timeuuid) &&
795
- !value.is_a?(CassandraCQL::UUID)
796
-
797
- Type::Timestamp.instance.cast(value)
798
- else
799
- cast_range_key(value)
800
- end
801
- end
802
-
803
803
  def load!
804
804
  fail ArgumentError, "Not all primary key columns have specified values"
805
805
  end
@@ -48,6 +48,9 @@ module Cequel
48
48
  # @!attribute [r] partition_key_columns
49
49
  # (see Cequel::Schema::Table#partition_key_columns)
50
50
  #
51
+ # @!attribute [r] partition_key_column_names
52
+ # (see Cequel::Schema::Table#partition_key_column_names)
53
+ #
51
54
  # @!attribute [r] clustering_columns
52
55
  # (see Cequel::Schema::Table#clustering_columns)
53
56
  #
@@ -56,7 +59,8 @@ module Cequel
56
59
  #
57
60
  def_delegators :table_schema, :columns, :key_columns,
58
61
  :key_column_names, :partition_key_columns,
59
- :clustering_columns, :compact_storage?
62
+ :partition_key_column_names, :clustering_columns,
63
+ :compact_storage?
60
64
  #
61
65
  # @!method reflect_on_column(name)
62
66
  # (see Cequel::Schema::Table#column)
@@ -43,7 +43,7 @@ module Cequel
43
43
  options[:replication_factor] ||= 1
44
44
  end
45
45
  options_strs = options.map do |name, value|
46
- "'#{name}': #{CassandraCQL::Statement.quote(value)}"
46
+ "'#{name}': #{Cequel::Type.quote(value)}"
47
47
  end
48
48
 
49
49
  bare_connection.execute(<<-CQL)
@@ -57,7 +57,7 @@ module Cequel
57
57
  end
58
58
 
59
59
  def quote(value)
60
- CassandraCQL::Statement.quote(value)
60
+ Cequel::Type.quote(value)
61
61
  end
62
62
  end
63
63
 
data/lib/cequel/type.rb CHANGED
@@ -73,12 +73,39 @@ module Cequel
73
73
  raise UnknownType, "Unrecognized internal type #{internal_name.inspect}"
74
74
  end
75
75
 
76
+ #
77
+ # Quote an arbitrary value for use in a CQL statement by inferring the
78
+ # equivalent CQL type to the value's Ruby type
79
+ #
80
+ # @return [String] quoted value
81
+ #
82
+ def self.quote(value)
83
+ if value.is_a?(Array)
84
+ return value.map { |element| quote(element) }.join(',')
85
+ end
86
+ case value
87
+ when ::String
88
+ if value.encoding == Encoding::ASCII_8BIT
89
+ "0x#{value}"
90
+ else
91
+ "'#{value.gsub("'", "''")}'"
92
+ end
93
+ when Date, Time, ActiveSupport::TimeWithZone
94
+ quote(value.to_i * 1000 + value.usec / 1000)
95
+ when Numeric, true, false, Cql::Uuid
96
+ value.to_s
97
+ else
98
+ quote(value.to_s)
99
+ end
100
+ end
101
+
76
102
  #
77
103
  # The base class for all type objects. Types are singletons.
78
104
  #
79
105
  # @abstract Subclasses should implement {#cast}, and may implement
80
- # {#internal_names} if it cannot be inferred from the class name. The
81
- # name of the type class should be the camel-cased CQL name of the type
106
+ # {#internal_names} if it cannot be inferred from the class name.
107
+ # The name of the type class should be the camel-cased CQL name of the
108
+ # type
82
109
  #
83
110
  class Base
84
111
  include Singleton
@@ -367,10 +394,9 @@ module Cequel
367
394
  register Timestamp.instance
368
395
 
369
396
  #
370
- # `uuid` columns store type 1 and type 4 UUIDs. Cequel uses the
371
- # `CassandraCQL::UUID` type to represent UUIDs in Ruby, since this is what
372
- # the underlying `cassandra-cql` library expects. Other UUID formats are
373
- # supported as inputs.
397
+ # `uuid` columns store type 1 and type 4 UUIDs. New UUID instances can be
398
+ # created using the {Cequel.uuid} method, and a value can be checked to see
399
+ # if it is a UUID recognized by Cequel using the {Cequel.uuid?} method.
374
400
  #
375
401
  # @see http://cassandra.apache.org/doc/cql3/CQL.html#types
376
402
  # CQL3 data type documentation
@@ -381,10 +407,14 @@ module Cequel
381
407
  end
382
408
 
383
409
  def cast(value)
384
- case value
385
- when CassandraCQL::UUID then value
386
- when SimpleUUID::UUID then CassandraCQL::UUID.new(value.to_s)
387
- else CassandraCQL::UUID.new(value)
410
+ if value.is_a? Cql::Uuid then value
411
+ elsif defined?(SimpleUUID::UUID) && value.is_a?(SimpleUUID::UUID)
412
+ Cql::Uuid.new(value.to_i)
413
+ elsif value.is_a?(::Integer) || value.is_a?(::String)
414
+ Cql::Uuid.new(value)
415
+ else
416
+ fail ArgumentError,
417
+ "Don't know how to cast #{value.inspect} to a UUID"
388
418
  end
389
419
  end
390
420
  end
@@ -401,6 +431,10 @@ module Cequel
401
431
  # CQL3 data type documentation
402
432
  #
403
433
  class Timeuuid < Uuid
434
+ def cast(value)
435
+ Cql::TimeUuid.new(super.value)
436
+ end
437
+
404
438
  def internal_names
405
439
  ['org.apache.cassandra.db.marshal.TimeUUIDType']
406
440
  end