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
@@ -25,9 +25,12 @@ module Cequel
25
25
  # @see Keyspace#batch
26
26
  #
27
27
  def initialize(keyspace, options = {})
28
+ options.assert_valid_keys(:auto_apply, :unlogged, :consistency)
28
29
  @keyspace = keyspace
29
30
  @auto_apply = options[:auto_apply]
30
31
  @unlogged = options.fetch(:unlogged, false)
32
+ @consistency = options.fetch(:consistency,
33
+ keyspace.default_consistency)
31
34
  reset
32
35
  end
33
36
 
@@ -54,7 +57,8 @@ module Cequel
54
57
  @statement.prepend(begin_statement)
55
58
  @statement.append("APPLY BATCH\n")
56
59
  end
57
- @keyspace.execute(*@statement.args)
60
+ @keyspace.execute_with_consistency(
61
+ @statement.args.first, @statement.args.drop(1), @consistency)
58
62
  end
59
63
 
60
64
  #
@@ -74,6 +78,17 @@ module Cequel
74
78
  !unlogged?
75
79
  end
76
80
 
81
+ # @private
82
+ def execute_with_consistency(cql, bind_vars, query_consistency)
83
+ if query_consistency && query_consistency != @consistency
84
+ raise ArgumentError,
85
+ "Attempting to perform query with consistency " \
86
+ "#{query_consistency.to_s.upcase} in batch with consistency " \
87
+ "#{@consistency.upcase}"
88
+ end
89
+ execute(cql, *bind_vars)
90
+ end
91
+
77
92
  private
78
93
 
79
94
  def reset
@@ -44,10 +44,12 @@ module Cequel
44
44
  attr_reader :sort_order
45
45
  # @return [Integer] maximum number of rows to return, `nil` if no limit
46
46
  attr_reader :row_limit
47
+ # @return [Symbol] what consistency level queries from this data set will
48
+ # use
49
+ # @since 1.1.0
50
+ attr_reader :query_consistency
47
51
 
48
- def_delegator :keyspace, :execute, :execute_cql
49
- private :execute_cql
50
- def_delegator :keyspace, :write
52
+ def_delegator :keyspace, :write_with_consistency
51
53
 
52
54
  #
53
55
  # @param table_name [Symbol] column family for this data set
@@ -79,7 +81,7 @@ module Cequel
79
81
  # CQL documentation for INSERT
80
82
  #
81
83
  def insert(data, options = {})
82
- inserter(options) { insert(data) }.execute
84
+ inserter { insert(data) }.execute(options)
83
85
  end
84
86
 
85
87
  #
@@ -125,10 +127,10 @@ module Cequel
125
127
  #
126
128
  def update(*args, &block)
127
129
  if block
128
- updater(args.extract_options!, &block).execute
130
+ updater(&block).execute(args.extract_options!)
129
131
  else
130
132
  data = args.shift
131
- updater(args.extract_options!) { set(data) }.execute
133
+ updater { set(data) }.execute(args.extract_options!)
132
134
  end
133
135
  end
134
136
 
@@ -151,7 +153,7 @@ module Cequel
151
153
  # CQL documentation for counter columns
152
154
  #
153
155
  def increment(deltas, options = {})
154
- incrementer(options) { increment(deltas) }.execute
156
+ incrementer { increment(deltas) }.execute(options)
155
157
  end
156
158
  alias_method :incr, :increment
157
159
 
@@ -168,7 +170,7 @@ module Cequel
168
170
  # @since 0.5.0
169
171
  #
170
172
  def decrement(deltas, options = {})
171
- incrementer(options) { decrement(deltas) }.execute
173
+ incrementer { decrement(deltas) }.execute(options)
172
174
  end
173
175
  alias_method :decr, :decrement
174
176
 
@@ -193,7 +195,7 @@ module Cequel
193
195
  # @see #update
194
196
  #
195
197
  def list_prepend(column, elements, options = {})
196
- updater(options) { list_prepend(column, elements) }.execute
198
+ updater { list_prepend(column, elements) }.execute(options)
197
199
  end
198
200
 
199
201
  #
@@ -216,7 +218,7 @@ module Cequel
216
218
  # @since 1.0.0
217
219
  #
218
220
  def list_append(column, elements, options = {})
219
- updater(options) { list_append(column, elements) }.execute
221
+ updater { list_append(column, elements) }.execute(options)
220
222
  end
221
223
 
222
224
  #
@@ -238,7 +240,7 @@ module Cequel
238
240
  # @since 1.0.0
239
241
  #
240
242
  def list_replace(column, index, value, options = {})
241
- updater(options) { list_replace(column, index, value) }.execute
243
+ updater { list_replace(column, index, value) }.execute(options)
242
244
  end
243
245
 
244
246
  #
@@ -260,7 +262,7 @@ module Cequel
260
262
  # @since 1.0.0
261
263
  #
262
264
  def list_remove(column, value, options = {})
263
- updater(options) { list_remove(column, value) }.execute
265
+ updater { list_remove(column, value) }.execute(options)
264
266
  end
265
267
 
266
268
  #
@@ -284,7 +286,7 @@ module Cequel
284
286
  #
285
287
  def list_remove_at(column, *positions)
286
288
  options = positions.extract_options!
287
- deleter(options) { list_remove_at(column, *positions) }.execute
289
+ deleter { list_remove_at(column, *positions) }.execute(options)
288
290
  end
289
291
 
290
292
  #
@@ -307,7 +309,7 @@ module Cequel
307
309
  #
308
310
  def map_remove(column, *keys)
309
311
  options = keys.extract_options!
310
- deleter(options) { map_remove(column, *keys) }.execute
312
+ deleter { map_remove(column, *keys) }.execute(options)
311
313
  end
312
314
 
313
315
  #
@@ -328,7 +330,7 @@ module Cequel
328
330
  # @since 1.0.0
329
331
  #
330
332
  def set_add(column, values, options = {})
331
- updater(options) { set_add(column, values) }.execute
333
+ updater { set_add(column, values) }.execute(options)
332
334
  end
333
335
 
334
336
  #
@@ -349,7 +351,7 @@ module Cequel
349
351
  # @since 1.0.0
350
352
  #
351
353
  def set_remove(column, value, options = {})
352
- updater(options) { set_remove(column, value) }.execute
354
+ updater { set_remove(column, value) }.execute(options)
353
355
  end
354
356
 
355
357
  #
@@ -370,7 +372,7 @@ module Cequel
370
372
  # @since 1.0.0
371
373
  #
372
374
  def map_update(column, updates, options = {})
373
- updater(options) { map_update(column, updates) }.execute
375
+ updater { map_update(column, updates) }.execute(options)
374
376
  end
375
377
 
376
378
  #
@@ -422,11 +424,11 @@ module Cequel
422
424
  def delete(*columns, &block)
423
425
  options = columns.extract_options!
424
426
  if block
425
- deleter(options, &block).execute
427
+ deleter(&block).execute(options)
426
428
  elsif columns.empty?
427
- deleter(options) { delete_row }.execute
429
+ deleter { delete_row }.execute(options)
428
430
  else
429
- deleter(options) { delete_columns(*columns) }.execute
431
+ deleter { delete_columns(*columns) }.execute(options)
430
432
  end
431
433
  end
432
434
 
@@ -545,6 +547,25 @@ module Cequel
545
547
  end
546
548
  end
547
549
 
550
+ # rubocop:disable LineLength
551
+
552
+ #
553
+ # Change the consistency for queries performed by this data set
554
+ #
555
+ # @param consistency [Symbol] a consistency level
556
+ # @return [DataSet] new data set tuned to the given consistency
557
+ #
558
+ # @see http://www.datastax.com/documentation/cassandra/2.0/cassandra/dml/dml_config_consistency_c.html
559
+ # @since 1.1.0
560
+ #
561
+ def consistency(consistency)
562
+ clone.tap do |data_set|
563
+ data_set.query_consistency = consistency
564
+ end
565
+ end
566
+
567
+ # rubocop:enable LineLength
568
+
548
569
  #
549
570
  # Enumerate over rows in this data set. Along with #each, all other
550
571
  # Enumerable methods are implemented.
@@ -560,14 +581,15 @@ module Cequel
560
581
  #
561
582
  def each
562
583
  return enum_for(:each) unless block_given?
563
- execute_cql(*cql).fetch { |row| yield Row.from_result_row(row) }
584
+ result = execute_cql(*cql)
585
+ result.each { |row| yield Row.from_result_row(row) }
564
586
  end
565
587
 
566
588
  #
567
589
  # @return [Hash] the first row in this data set
568
590
  #
569
591
  def first
570
- row = execute_cql(*limit(1).cql).fetch_row
592
+ row = execute_cql(*limit(1).cql).first
571
593
  Row.from_result_row(row)
572
594
  end
573
595
 
@@ -575,7 +597,7 @@ module Cequel
575
597
  # @return [Fixnum] the number of rows in this data set
576
598
  #
577
599
  def count
578
- execute_cql(*count_cql).fetch_row['count']
600
+ execute_cql(*count_cql).first['count']
579
601
  end
580
602
 
581
603
  #
@@ -606,7 +628,7 @@ module Cequel
606
628
  #
607
629
  def inspect
608
630
  "#<#{self.class.name}: " \
609
- "#{CassandraCQL::Statement.sanitize(cql.first, cql[1..-1])}>"
631
+ "#{Keyspace.sanitize(cql.first, cql.drop(1))}>"
610
632
  end
611
633
 
612
634
  #
@@ -630,28 +652,30 @@ module Cequel
630
652
  end
631
653
  end
632
654
 
633
- # @private
634
- def updater(options = {}, &block)
635
- Updater.new(self, options, &block)
636
- end
637
-
638
- # @private
639
- def deleter(options = {}, &block)
640
- Deleter.new(self, options, &block)
641
- end
642
-
643
655
  protected
644
656
 
645
- attr_writer :row_limit
657
+ attr_writer :row_limit, :query_consistency
646
658
 
647
659
  private
648
660
 
649
- def inserter(options = {}, &block)
650
- Inserter.new(self, options, &block)
661
+ def execute_cql(cql, *bind_vars)
662
+ keyspace.execute_with_consistency(cql, bind_vars, query_consistency)
663
+ end
664
+
665
+ def inserter(&block)
666
+ Inserter.new(self, &block)
667
+ end
668
+
669
+ def incrementer(&block)
670
+ Incrementer.new(self, &block)
671
+ end
672
+
673
+ def updater(&block)
674
+ Updater.new(self, &block)
651
675
  end
652
676
 
653
- def incrementer(options = {}, &block)
654
- Incrementer.new(self, options, &block)
677
+ def deleter(&block)
678
+ Deleter.new(self, &block)
655
679
  end
656
680
 
657
681
  def initialize_copy(source)
@@ -61,7 +61,7 @@ module Cequel
61
61
 
62
62
  private
63
63
 
64
- def write_to_statement(statement)
64
+ def write_to_statement(statement, options)
65
65
  if @delete_row
66
66
  statement.append("DELETE FROM #{table_name}")
67
67
  elsif statements.empty?
@@ -71,7 +71,7 @@ module Cequel
71
71
  .append(statements.join(','), *bind_vars)
72
72
  .append(" FROM #{table_name}")
73
73
  end
74
- statement.append(generate_upsert_options)
74
+ statement.append(generate_upsert_options(options))
75
75
  end
76
76
 
77
77
  def empty?
@@ -35,10 +35,10 @@ module Cequel
35
35
 
36
36
  private
37
37
 
38
- def write_to_statement(statement)
38
+ def write_to_statement(statement, options)
39
39
  statement
40
40
  .append("UPDATE #{table_name}")
41
- .append(generate_upsert_options)
41
+ .append(generate_upsert_options(options))
42
42
  .append(
43
43
  " SET " << statements.join(', '),
44
44
  *bind_vars
@@ -11,7 +11,7 @@ module Cequel
11
11
  #
12
12
  # (see Writer#initialize)
13
13
  #
14
- def initialize(data_set, options = {})
14
+ def initialize(data_set)
15
15
  @row = {}
16
16
  super
17
17
  end
@@ -19,10 +19,12 @@ module Cequel
19
19
  #
20
20
  # (see Writer#execute)
21
21
  #
22
- def execute
22
+ def execute(options = {})
23
23
  statement = Statement.new
24
- write_to_statement(statement)
25
- data_set.write(*statement.args)
24
+ consistency = options.fetch(:consistency, data_set.query_consistency)
25
+ write_to_statement(statement, options)
26
+ data_set.write_with_consistency(
27
+ statement.cql, statement.bind_vars, consistency)
26
28
  end
27
29
 
28
30
  #
@@ -55,12 +57,12 @@ module Cequel
55
57
  end
56
58
  end
57
59
 
58
- def write_to_statement(statement)
60
+ def write_to_statement(statement, options)
59
61
  statement.append("INSERT INTO #{table_name}")
60
62
  statement.append(
61
63
  " (#{column_names.join(', ')}) VALUES (#{statements.join(', ')}) ",
62
64
  *bind_vars)
63
- statement.append(generate_upsert_options)
65
+ statement.append(generate_upsert_options(options))
64
66
  end
65
67
  end
66
68
  end
@@ -1,4 +1,6 @@
1
1
  # -*- encoding : utf-8 -*-
2
+ require 'set'
3
+
2
4
  module Cequel
3
5
  module Metal
4
6
  #
@@ -9,29 +11,72 @@ module Cequel
9
11
  class Keyspace
10
12
  extend Forwardable
11
13
  include Logging
14
+ include MonitorMixin
12
15
 
13
16
  # @return [Hash] configuration options for this keyspace
14
17
  attr_reader :configuration
15
18
  # @return [String] name of the keyspace
16
19
  attr_reader :name
20
+ # @return [Array<String>] list of hosts to connect to
21
+ attr_reader :hosts
22
+ # @return Integer port to connect to Cassandra nodes on
23
+ attr_reader :port
24
+ # @return [Symbol] the default consistency for queries in this keyspace
25
+ # @since 1.1.0
26
+ attr_writer :default_consistency
17
27
 
18
28
  #
19
29
  # @!method write(statement, *bind_vars)
20
30
  #
21
- # Write data to this keyspace using a CQL query. Will be included the
22
- # current batch operation if one is present.
31
+ # Write data to this keyspace using a CQL query. Will be included the
32
+ # current batch operation if one is present.
23
33
  #
24
- # @param (see #execute)
25
- # @return [void]
34
+ # @param (see #execute)
35
+ # @return [void]
26
36
  #
27
37
  def_delegator :write_target, :execute, :write
28
38
 
39
+ # @!method write_with_consistency(statement, bind_vars, consistency)
40
+ #
41
+ # Write data to this keyspace using a CQL query at the given
42
+ # consistency. Will be included the current batch operation if one is
43
+ # present.
44
+ #
45
+ # @param (see #execute_with_consistency)
46
+ # @return [void]
47
+ #
48
+ def_delegator :write_target, :execute_with_consistency,
49
+ :write_with_consistency
50
+
29
51
  #
30
52
  # @!method batch
31
53
  # (see Cequel::Metal::BatchManager#batch)
32
54
  #
33
55
  def_delegator :batch_manager, :batch
34
56
 
57
+ #
58
+ # Combine a statement with bind vars into a fully-fledged CQL query. This
59
+ # will no longer be needed once the CQL driver supports bound values
60
+ # natively.
61
+ #
62
+ # @param statement [String] CQL statement with ? placeholders for bind
63
+ # vars
64
+ # @param bind_vars [Array] bind variables corresponding to ? in the
65
+ # statement
66
+ # @return [String] CQL statement with quoted values in place of bind
67
+ # variables
68
+ #
69
+ def self.sanitize(statement, bind_vars)
70
+ each_bind_var = bind_vars.each
71
+ statement.gsub('?') { Type.quote(each_bind_var.next) }
72
+ end
73
+
74
+ #
75
+ # @!method sanitize
76
+ # (see Cequel::Metal::Keyspace.sanitize)
77
+ #
78
+ def_delegator 'self.class', :sanitize
79
+
35
80
  #
36
81
  # @api private
37
82
  # @param configuration [Options]
@@ -40,13 +85,14 @@ module Cequel
40
85
  #
41
86
  def initialize(configuration={})
42
87
  configure(configuration)
88
+ @lock = Monitor.new
43
89
  end
44
90
 
45
91
  #
46
92
  # Configure this keyspace from a hash of options
47
93
  #
48
94
  # @param configuration [Options] configuration options
49
- # @option configuration [String] :host ('127.0.0.1:9160') host/port of
95
+ # @option configuration [String] :host ('127.0.0.1:9042') host/port of
50
96
  # single Cassandra instance to connect to
51
97
  # @option configuration [Array<String>] :hosts list of Cassandra
52
98
  # instances to connect to
@@ -59,10 +105,14 @@ module Cequel
59
105
  # @return [void]
60
106
  #
61
107
  def configure(configuration = {})
108
+ if configuration.key?(:thrift)
109
+ warn "Cequel no longer uses the Thrift transport to communicate " \
110
+ "with Cassandra. The :thrift option is deprecated and ignored."
111
+ end
62
112
  @configuration = configuration
63
- @hosts = configuration.fetch(
64
- :host, configuration.fetch(:hosts, '127.0.0.1:9160'))
65
- @thrift_options = configuration[:thrift].try(:symbolize_keys) || {}
113
+
114
+ @hosts, @port = extract_hosts_and_port(configuration)
115
+
66
116
  @name = configuration[:keyspace]
67
117
  # reset the connections
68
118
  clear_active_connections!
@@ -83,18 +133,42 @@ module Cequel
83
133
  DataSet.new(table_name.to_sym, self)
84
134
  end
85
135
 
136
+ #
137
+ # @return [Cql::Client::Client] the low-level client provided by the
138
+ # adapter
139
+ # @api private
140
+ #
141
+ def client
142
+ synchronize { @client ||= build_client }
143
+ end
144
+
86
145
  #
87
146
  # Execute a CQL query in this keyspace
88
147
  #
89
148
  # @param statement [String] CQL string
90
149
  # @param bind_vars [Object] values for bind variables
91
- # @return [void]
150
+ # @return [Enumerable] the results of the query
151
+ #
152
+ # @see #execute_with_consistency
92
153
  #
93
154
  def execute(statement, *bind_vars)
155
+ execute_with_consistency(statement, bind_vars, default_consistency)
156
+ end
157
+
158
+ #
159
+ # Execute a CQL query in this keyspace with the given consistency
160
+ #
161
+ # @param statement [String] CQL string
162
+ # @param bind_vars [Array] array of values for bind variables
163
+ # @param consistency [Symbol] consistency at which to execute query
164
+ # @return [Enumerable] the results of the query
165
+ #
166
+ # @since 1.1.0
167
+ #
168
+ def execute_with_consistency(statement, bind_vars, consistency)
94
169
  log('CQL', statement, *bind_vars) do
95
- with_connection do |conn|
96
- conn.execute(statement, *bind_vars)
97
- end
170
+ client.execute(sanitize(statement, bind_vars),
171
+ consistency || default_consistency)
98
172
  end
99
173
  end
100
174
 
@@ -104,47 +178,66 @@ module Cequel
104
178
  # @return [void]
105
179
  #
106
180
  def clear_active_connections!
107
- if defined? @connection_pool
108
- remove_instance_variable(:@connection_pool)
181
+ if defined? @client
182
+ remove_instance_variable(:@client)
109
183
  end
110
184
  end
111
185
 
186
+ #
187
+ # @return [Symbol] the default consistency for queries in this keyspace
188
+ # @since 1.1.0
189
+ #
190
+ def default_consistency
191
+ @default_consistency || :quorum
192
+ end
193
+
112
194
  private
113
195
 
114
- def_delegator :connection_pool, :with, :with_connection
115
- private :with_connection
196
+ attr_reader :lock
116
197
 
117
198
  def_delegator :batch_manager, :current_batch
118
199
  private :current_batch
119
200
 
120
- def build_connection
121
- options = {cql_version: '3.0.0'}
122
- options[:keyspace] = name if name
123
- CassandraCQL::Database.new(
124
- @hosts,
125
- options,
126
- @thrift_options
127
- )
128
- end
201
+ def_delegator :lock, :synchronize
202
+ private :lock
129
203
 
130
- def connection_pool
131
- return @connection_pool if defined? @connection_pool
132
- options = {
133
- size: @configuration.fetch(:pool, 1),
134
- timeout: @configuration.fetch(:pool_timeout, 0)
135
- }
136
- @connection_pool = ConnectionPool.new(options) do
137
- build_connection
204
+ def build_client
205
+ Cql::Client.connect(hosts: hosts, port: port).tap do |client|
206
+ client.use(name) if name
138
207
  end
139
208
  end
140
209
 
141
210
  def batch_manager
142
- @batch_manager ||= BatchManager.new(self)
211
+ synchronize { @batch_manager ||= BatchManager.new(self) }
143
212
  end
144
213
 
145
214
  def write_target
146
215
  current_batch || self
147
216
  end
217
+
218
+ def extract_hosts_and_port(configuration)
219
+ hosts, ports = [], Set[]
220
+ ports << configuration[:port] if configuration.key?(:port)
221
+ Array.wrap(configuration.fetch(
222
+ :host, configuration.fetch(:hosts, '127.0.0.1'))).each do |host_port|
223
+
224
+ host, port = host_port.split(':')
225
+ hosts << host
226
+ if port
227
+ warn "Specifying a hostname as host:port is deprecated. Specify " \
228
+ "only the host IP or hostname in :hosts, and specify a " \
229
+ "port for all nodes using the :port option."
230
+ ports << port.to_i
231
+ end
232
+ end
233
+
234
+ if ports.size > 1
235
+ fail ArgumentError, "All Cassandra nodes must listen on the same " \
236
+ "port; specified multiple ports #{ports.join(', ')}"
237
+ end
238
+
239
+ [hosts, ports.first || 9042]
240
+ end
148
241
  end
149
242
  end
150
243
  end