cequel 1.0.4 → 1.1.0

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.
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