cequel 1.10.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +93 -65
  5. data/README.md +26 -5
  6. data/Vagrantfile +2 -2
  7. data/lib/cequel/errors.rb +2 -0
  8. data/lib/cequel/instrumentation.rb +5 -4
  9. data/lib/cequel/metal/batch.rb +21 -18
  10. data/lib/cequel/metal/data_set.rb +17 -28
  11. data/lib/cequel/metal/inserter.rb +3 -2
  12. data/lib/cequel/metal/keyspace.rb +56 -33
  13. data/lib/cequel/metal/request_logger.rb +22 -8
  14. data/lib/cequel/metal/row_specification.rb +9 -8
  15. data/lib/cequel/metal/statement.rb +23 -7
  16. data/lib/cequel/metal/updater.rb +12 -10
  17. data/lib/cequel/metal/writer.rb +5 -13
  18. data/lib/cequel/record/association_collection.rb +6 -33
  19. data/lib/cequel/record/collection.rb +2 -1
  20. data/lib/cequel/record/errors.rb +6 -0
  21. data/lib/cequel/record/persistence.rb +2 -2
  22. data/lib/cequel/record/record_set.rb +3 -4
  23. data/lib/cequel/record/validations.rb +5 -5
  24. data/lib/cequel/schema/table.rb +3 -5
  25. data/lib/cequel/schema/table_reader.rb +73 -111
  26. data/lib/cequel/schema/table_updater.rb +9 -15
  27. data/lib/cequel/version.rb +1 -1
  28. data/spec/examples/metal/data_set_spec.rb +34 -46
  29. data/spec/examples/metal/keyspace_spec.rb +8 -6
  30. data/spec/examples/record/associations_spec.rb +8 -18
  31. data/spec/examples/record/persistence_spec.rb +6 -6
  32. data/spec/examples/record/record_set_spec.rb +39 -12
  33. data/spec/examples/record/timestamps_spec.rb +12 -5
  34. data/spec/examples/schema/keyspace_spec.rb +13 -37
  35. data/spec/examples/schema/table_reader_spec.rb +4 -1
  36. data/spec/examples/schema/table_updater_spec.rb +22 -7
  37. data/spec/examples/schema/table_writer_spec.rb +2 -3
  38. data/spec/examples/spec_helper.rb +1 -0
  39. data/spec/examples/spec_support/preparation_spec.rb +14 -7
  40. metadata +7 -8
@@ -37,16 +37,17 @@ module Cequel
37
37
 
38
38
  define_method(:"__data_for_#{method_name}_instrumentation", &data_proc)
39
39
 
40
- module_eval <<-METH
41
- def #{method_name}_with_instrumentation(*args)
40
+ mod = Module.new
41
+ mod.module_eval <<-METH
42
+ def #{method_name}(*args)
42
43
  instrument("#{topic}",
43
44
  __data_for_#{method_name}_instrumentation(self)) do
44
- #{method_name}_without_instrumentation(*args)
45
+ super(*args)
45
46
  end
46
47
  end
47
48
  METH
48
49
 
49
- alias_method_chain method_name, "instrumentation"
50
+ prepend mod
50
51
  end
51
52
  end
52
53
 
@@ -39,10 +39,9 @@ module Cequel
39
39
  #
40
40
  # @param (see Keyspace#execute)
41
41
  #
42
- def execute(cql, *bind_vars)
43
- @statement.append("#{cql}\n", *bind_vars)
44
- @statement_count += 1
45
- if @auto_apply && @statement_count >= @auto_apply
42
+ def execute(statement)
43
+ @statements << statement
44
+ if @auto_apply && @statements.size >= @auto_apply
46
45
  apply
47
46
  reset
48
47
  end
@@ -52,13 +51,20 @@ module Cequel
52
51
  # Send the batch to Cassandra
53
52
  #
54
53
  def apply
55
- return if @statement_count.zero?
56
- if @statement_count > 1
57
- @statement.prepend(begin_statement)
58
- @statement.append("APPLY BATCH\n")
54
+ return if @statements.empty?
55
+
56
+ statement = @statements.first
57
+ if @statements.size > 1
58
+ statement =
59
+ if logged?
60
+ keyspace.client.logged_batch
61
+ else
62
+ keyspace.client.unlogged_batch
63
+ end
64
+ @statements.each { |s| statement.add(s.prepare(keyspace), arguments: s.bind_vars) }
59
65
  end
60
- @keyspace.execute_with_consistency(
61
- @statement.args.first, @statement.args.drop(1), @consistency)
66
+
67
+ keyspace.execute_with_options(statement, consistency: @consistency)
62
68
  execute_on_complete_hooks
63
69
  end
64
70
 
@@ -84,30 +90,27 @@ module Cequel
84
90
  end
85
91
 
86
92
  # @private
87
- def execute_with_consistency(cql, bind_vars, query_consistency)
93
+ def execute_with_options(statement, options)
94
+ query_consistency = options.fetch(:consistency)
88
95
  if query_consistency && query_consistency != @consistency
89
96
  raise ArgumentError,
90
97
  "Attempting to perform query with consistency " \
91
98
  "#{query_consistency.to_s.upcase} in batch with consistency " \
92
99
  "#{@consistency.upcase}"
93
100
  end
94
- execute(cql, *bind_vars)
101
+ execute(statement)
95
102
  end
96
103
 
97
104
  private
98
105
 
99
- attr_reader :on_complete_hooks
106
+ attr_reader :on_complete_hooks, :keyspace
100
107
 
101
108
  def reset
102
- @statement = Statement.new
109
+ @statements = []
103
110
  @statement_count = 0
104
111
  @on_complete_hooks = []
105
112
  end
106
113
 
107
- def begin_statement
108
- "BEGIN #{"UNLOGGED " if unlogged?}BATCH\n"
109
- end
110
-
111
114
  def execute_on_complete_hooks
112
115
  on_complete_hooks.each { |hook| hook.call }
113
116
  end
@@ -51,7 +51,7 @@ module Cequel
51
51
  attr_reader :query_page_size
52
52
  attr_reader :query_paging_state
53
53
 
54
- def_delegator :keyspace, :write_with_consistency
54
+ def_delegator :keyspace, :write_with_options
55
55
 
56
56
  #
57
57
  # @param table_name [Symbol] column family for this data set
@@ -189,8 +189,8 @@ module Cequel
189
189
  # @example
190
190
  # posts.list_prepend(:categories, ['CQL', 'ORMs'])
191
191
  #
192
- # @note If multiple elements are passed, they will appear in the list in
193
- # reverse order.
192
+ # @note A bug (CASSANDRA-8733) exists in Cassandra versions 0.3.0-2.0.12 and 2.1.0-2.1.2 which
193
+ # will make elements appear in REVERSE ORDER in the list.
194
194
  # @note If a enclosed in a Keyspace#batch block, this method will be
195
195
  # executed as part of the batch.
196
196
  # @see #list_append
@@ -611,15 +611,16 @@ module Cequel
611
611
  Row.from_result_row(row)
612
612
  end
613
613
 
614
- #
615
- # @return [Fixnum] the number of rows in this data set
616
- #
614
+ # @raise [DangerousQueryError] to prevent loading the entire record set
615
+ # to be counted
617
616
  def count
618
- execute_cql(*count_cql).first['count']
617
+ raise Cequel::Record::DangerousQueryError.new
619
618
  end
619
+ alias_method :length, :count
620
+ alias_method :size, :count
620
621
 
621
622
  #
622
- # @return [String] CQL `SELECT` statement encoding this data set's scope.
623
+ # @return [Statement] CQL `SELECT` statement encoding this data set's scope.
623
624
  #
624
625
  def cql
625
626
  statement = Statement.new
@@ -628,25 +629,13 @@ module Cequel
628
629
  .append(*row_specifications_cql)
629
630
  .append(sort_order_cql)
630
631
  .append(limit_cql)
631
- .args
632
- end
633
-
634
- #
635
- # @return [String] CQL statement to get count of rows in this data set
636
- #
637
- def count_cql
638
- Statement.new
639
- .append("SELECT COUNT(*) FROM #{table_name}")
640
- .append(*row_specifications_cql)
641
- .append(limit_cql).args
642
632
  end
643
633
 
644
634
  #
645
635
  # @return [String]
646
636
  #
647
637
  def inspect
648
- "#<#{self.class.name}: " \
649
- "#{Keyspace.sanitize(cql.first, cql.drop(1))}>"
638
+ "#<#{self.class.name}: #{cql.inspect}>"
650
639
  end
651
640
 
652
641
  #
@@ -677,15 +666,15 @@ module Cequel
677
666
  private
678
667
 
679
668
  def results
680
- @results ||= execute_cql(*cql)
669
+ @results ||= execute_cql(cql)
681
670
  end
682
671
 
683
- def execute_cql(cql, *bind_vars)
684
- keyspace.execute_with_options(cql, bind_vars, {
685
- consistency: query_consistency,
686
- page_size: query_page_size,
687
- paging_state: query_paging_state
688
- })
672
+ def execute_cql(cql_stmt)
673
+ keyspace.execute_with_options(cql_stmt,
674
+ consistency: query_consistency,
675
+ page_size: query_page_size,
676
+ paging_state: query_paging_state
677
+ )
689
678
  end
690
679
 
691
680
  def inserter(&block)
@@ -23,8 +23,9 @@ module Cequel
23
23
  statement = Statement.new
24
24
  consistency = options.fetch(:consistency, data_set.query_consistency)
25
25
  write_to_statement(statement, options)
26
- data_set.write_with_consistency(
27
- statement.cql, statement.bind_vars, consistency)
26
+ data_set.write_with_options(statement,
27
+ consistency: consistency
28
+ )
28
29
  end
29
30
 
30
31
  #
@@ -46,7 +46,7 @@ module Cequel
46
46
  #
47
47
  def_delegator :write_target, :execute, :write
48
48
 
49
- # @!method write_with_consistency(statement, bind_vars, consistency)
49
+ # @!method write_with_options(statement, bind_vars, consistency)
50
50
  #
51
51
  # Write data to this keyspace using a CQL query at the given
52
52
  # consistency. Will be included the current batch operation if one is
@@ -55,8 +55,8 @@ module Cequel
55
55
  # @param (see #execute_with_consistency)
56
56
  # @return [void]
57
57
  #
58
- def_delegator :write_target, :execute_with_consistency,
59
- :write_with_consistency
58
+ def_delegator :write_target, :execute_with_options,
59
+ :write_with_options
60
60
 
61
61
  #
62
62
  # @!method batch
@@ -187,16 +187,33 @@ module Cequel
187
187
  # @see #execute_with_consistency
188
188
  #
189
189
  def execute(statement, *bind_vars)
190
- execute_with_consistency(statement, bind_vars, default_consistency)
190
+ execute_with_options(Statement.new(statement, bind_vars), { consistency: default_consistency })
191
191
  end
192
192
 
193
- def execute_with_options(statement, bind_vars, options={})
193
+ #
194
+ # Execute a CQL query in this keyspace with the given options
195
+ #
196
+ # @param statement [String,Statement,Batch] statement to execute
197
+ # @param options [Options] options for statement execution
198
+ # @return [Enumerable] the results of the query
199
+ #
200
+ # @since 1.1.0
201
+ #
202
+ def execute_with_options(statement, options={})
194
203
  options[:consistency] ||= default_consistency
195
204
 
196
205
  retries = max_retries
197
- log('CQL', statement, *bind_vars) do
206
+ cql, options = *case statement
207
+ when Statement
208
+ [client.prepare(statement.cql),
209
+ {arguments: statement.bind_vars}.merge(options)]
210
+ when Cassandra::Statements::Batch
211
+ [statement, options]
212
+ end
213
+
214
+ log('CQL', statement) do
198
215
  begin
199
- client.execute(sanitize(statement, bind_vars), options)
216
+ client.execute(cql, options)
200
217
  rescue Cassandra::Errors::NoHostsAvailable,
201
218
  Ione::Io::ConnectionError => e
202
219
  clear_active_connections!
@@ -206,20 +223,6 @@ module Cequel
206
223
  retry
207
224
  end
208
225
  end
209
-
210
- end
211
- #
212
- # Execute a CQL query in this keyspace with the given consistency
213
- #
214
- # @param statement [String] CQL string
215
- # @param bind_vars [Array] array of values for bind variables
216
- # @param consistency [Symbol] consistency at which to execute query
217
- # @return [Enumerable] the results of the query
218
- #
219
- # @since 1.1.0
220
- #
221
- def execute_with_consistency(statement, bind_vars, consistency)
222
- execute_with_options(statement, bind_vars, {consistency: consistency || default_consistency})
223
226
  end
224
227
 
225
228
  #
@@ -250,14 +253,38 @@ module Cequel
250
253
 
251
254
  # @return [Boolean] true if the keyspace exists
252
255
  def exists?
256
+ cluster.has_keyspace?(name)
257
+ end
258
+
259
+ # @return [String] Cassandra version number
260
+ def cassandra_version
261
+ return @cassandra_version if @cassandra_version
262
+
253
263
  statement = <<-CQL
254
- SELECT keyspace_name
255
- FROM system.schema_keyspaces
256
- WHERE keyspace_name = ?
264
+ SELECT release_version
265
+ FROM system.local
257
266
  CQL
258
267
 
259
- log('CQL', statement, [name]) do
260
- client_without_keyspace.execute(sanitize(statement, [name])).any?
268
+ log('CQL', statement) do
269
+ @cassandra_version = client_without_keyspace.execute(statement).first['release_version']
270
+ end
271
+ end
272
+
273
+ # return true if Cassandra server version is known to include bug CASSANDRA-8733
274
+ def bug8733_version?
275
+ version_file = File.expand_path('../../../../.cassandra-versions', __FILE__)
276
+ @all_versions ||= File.read(version_file).split("\n").map(&:strip)
277
+
278
+ # bug exists in versions 0.3.0-2.0.12 and 2.1.0-2.1.2
279
+ @bug8733_versions ||= @all_versions[0..@all_versions.index('2.0.12')] +
280
+ @all_versions[@all_versions.index('2.1.0')..@all_versions.index('2.1.2')]
281
+
282
+ @bug8733_versions.include?(cassandra_version)
283
+ end
284
+
285
+ def cluster
286
+ synchronize do
287
+ @cluster ||= Cassandra.cluster(client_options)
261
288
  end
262
289
  end
263
290
 
@@ -271,11 +298,6 @@ module Cequel
271
298
  def_delegator :lock, :synchronize
272
299
  private :lock
273
300
 
274
- def cluster
275
- synchronize do
276
- @cluster ||= Cassandra.cluster(client_options)
277
- end
278
- end
279
301
 
280
302
  def client_without_keyspace
281
303
  synchronize do
@@ -301,7 +323,7 @@ module Cequel
301
323
 
302
324
  def extract_hosts_and_port(configuration)
303
325
  hosts, ports = [], Set[]
304
- ports << configuration[:port] if configuration.key?(:port)
326
+ ports << Integer(configuration[:port]) if configuration.key?(:port)
305
327
  host_or_hosts =
306
328
  configuration.fetch(:host, configuration.fetch(:hosts, '127.0.0.1'))
307
329
  Array.wrap(host_or_hosts).each do |host_port|
@@ -311,7 +333,7 @@ module Cequel
311
333
  warn "Specifying a hostname as host:port is deprecated. Specify " \
312
334
  "only the host IP or hostname in :hosts, and specify a " \
313
335
  "port for all nodes using the :port option."
314
- ports << port.to_i
336
+ ports << Integer(port)
315
337
  end
316
338
  end
317
339
 
@@ -345,6 +367,7 @@ module Cequel
345
367
  ssl_config.each { |key, value| ssl_config.delete(key) unless value }
346
368
  ssl_config
347
369
  end
370
+
348
371
  end
349
372
  end
350
373
  end
@@ -22,18 +22,17 @@ module Cequel
22
22
  # Log a CQL statement
23
23
  #
24
24
  # @param label [String] a logical label for this statement
25
- # @param statement [String] the CQL statement to log
26
- # @param bind_vars bind variables for the CQL statement
25
+ # @param statement [String,Statement,Batch] the CQL statement to log
27
26
  # @return [void]
28
27
  #
29
- def log(label, statement, *bind_vars)
28
+ def log(label, statement)
30
29
  return yield if logger.nil?
31
30
 
32
31
  response = nil
33
32
  begin
34
33
  time = Benchmark.ms { response = yield }
35
34
  generate_message = lambda do
36
- format_for_log(label, "#{time.round.to_i}ms", statement, bind_vars)
35
+ format_for_log(label, "#{time.round.to_i}ms", statement)
37
36
  end
38
37
 
39
38
  if time >= slowlog_threshold
@@ -42,7 +41,7 @@ module Cequel
42
41
  logger.debug(&generate_message)
43
42
  end
44
43
  rescue Exception => e
45
- logger.error { format_for_log(label, 'ERROR', statement, bind_vars) }
44
+ logger.error { format_for_log(label, 'ERROR', statement) }
46
45
  raise
47
46
  end
48
47
  response
@@ -50,9 +49,24 @@ module Cequel
50
49
 
51
50
  private
52
51
 
53
- def format_for_log(label, timing, statement, bind_vars)
54
- bind_vars = bind_vars.map{|it| String === it ? limit_length(it) : it }
55
- format('%s (%s) %s', label, timing, sanitize(statement, bind_vars))
52
+ def format_for_log(label, timing, statement)
53
+ cql_for_log =
54
+ case statement
55
+ when String
56
+ statement
57
+ when Statement
58
+ sanitize(statement.cql, limit_value_length(statement.bind_vars))
59
+ when Cassandra::Statements::Batch
60
+ batch_stmt = "BEGIN #{'UNLOGGED ' if statement.type == :unlogged}BATCH"
61
+ statement.statements.each { |s| batch_stmt << "\n" << sanitize(s.cql, limit_value_length(s.params)) }
62
+ batch_stmt << "END BATCH"
63
+ end
64
+
65
+ format('%s (%s) %s', label, timing, cql_for_log)
66
+ end
67
+
68
+ def limit_value_length(bind_vars)
69
+ bind_vars.map { |it| String === it ? limit_length(it) : it }
56
70
  end
57
71
 
58
72
  def limit_length(str)
@@ -35,15 +35,16 @@ module Cequel
35
35
  # @return [String] row specification as CQL fragment
36
36
  #
37
37
  def cql
38
- case @value
39
- when Array
40
- if @value.length == 1
41
- ["#{@column} = ?", @value.first]
42
- else
43
- ["#{@column} IN (?)", @value]
44
- end
38
+ value = if Enumerable === @value && @value.count == 1
39
+ @value.first
40
+ else
41
+ @value
42
+ end
43
+
44
+ if Array === value
45
+ ["#{@column} IN ?", value]
45
46
  else
46
- ["#{@column} = ?", @value]
47
+ ["#{@column} = ?", value]
47
48
  end
48
49
  end
49
50
  end
@@ -10,16 +10,30 @@ module Cequel
10
10
  class Statement
11
11
  # @return [Array] bind variables for CQL string
12
12
  attr_reader :bind_vars
13
+ # @return [Array] cassandra type hints for bind variables
13
14
 
14
- def initialize
15
- @cql, @bind_vars = [], []
15
+ def initialize(cql_or_prepared='', bind_vars=[])
16
+ cql, prepared = *case cql_or_prepared
17
+ when Cassandra::Statements::Prepared
18
+ [cql_or_prepared.cql, cql_or_prepared]
19
+ else
20
+ [cql_or_prepared.to_s, nil]
21
+ end
22
+
23
+ @cql, @prepared, @bind_vars = cql, prepared, bind_vars
16
24
  end
17
25
 
18
26
  #
19
27
  # @return [String] CQL statement
20
28
  #
21
- def cql
22
- @cql.join
29
+ def to_s
30
+ @cql
31
+ end
32
+ alias_method :cql, :to_s
33
+
34
+ # @return [Cassandra::Statements::Prepared] prepared version of this statement
35
+ def prepare(keyspace)
36
+ @prepared ||= keyspace.client.prepare(cql)
23
37
  end
24
38
 
25
39
  #
@@ -30,7 +44,7 @@ module Cequel
30
44
  # @return [void]
31
45
  #
32
46
  def prepend(cql, *bind_vars)
33
- @cql.unshift(cql)
47
+ @cql.prepend(cql)
34
48
  @bind_vars.unshift(*bind_vars)
35
49
  end
36
50
 
@@ -43,8 +57,10 @@ module Cequel
43
57
  # @return [void]
44
58
  #
45
59
  def append(cql, *bind_vars)
46
- @cql << cql
47
- @bind_vars.concat(bind_vars)
60
+ unless cql.nil?
61
+ @cql << cql
62
+ @bind_vars.concat(bind_vars)
63
+ end
48
64
  self
49
65
  end
50
66