cequel 1.10.0 → 2.0.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 (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