cassandra 0.12.1 → 0.13.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.
@@ -0,0 +1,118 @@
1
+
2
+ class Cassandra
3
+ class Composite
4
+ include ::Comparable
5
+ attr_reader :parts
6
+ attr_reader :column_slice
7
+
8
+ def initialize(*parts)
9
+ options = {}
10
+ if parts.last.is_a?(Hash)
11
+ options = parts.pop
12
+ end
13
+ @column_slice = options[:slice]
14
+ raise ArgumentError if @column_slice != nil && ![:before, :after].include?(@column_slice)
15
+
16
+ if parts.length == 1 && parts[0].instance_of?(self.class)
17
+ @column_slice = parts[0].column_slice
18
+ @parts = parts[0].parts
19
+ elsif parts.length == 1 && parts[0].instance_of?(String) && @column_slice.nil? && valid_packed_composite?(parts[0])
20
+ unpack(parts[0])
21
+ else
22
+ @parts = parts
23
+ end
24
+ end
25
+
26
+ def [](*args)
27
+ return @parts[*args]
28
+ end
29
+
30
+ def pack
31
+ packed = @parts.map do |part|
32
+ [part.length].pack('n') + part + "\x00"
33
+ end
34
+ if @column_slice
35
+ part = @parts[-1]
36
+ packed[-1] = [part.length].pack('n') + part + slice_end_of_component
37
+ end
38
+ return packed.join('')
39
+ end
40
+
41
+ def to_s
42
+ return pack
43
+ end
44
+
45
+ def <=>(other)
46
+ if !other.instance_of?(self.class)
47
+ return @parts.first <=> other
48
+ end
49
+ eoc = slice_end_of_component.unpack('c')[0]
50
+ other_eoc = other.slice_end_of_component.unpack('c')[0]
51
+ @parts.zip(other.parts).each do |a, b|
52
+ next if a == b
53
+ if a.nil? && b.nil?
54
+ return eoc <=> other_eoc
55
+ end
56
+
57
+ if a.nil?
58
+ return @column_slice == :after ? 1 : -1
59
+ end
60
+ if b.nil?
61
+ return other.column_slice == :after ? -1 : 1
62
+ end
63
+ return -1 if a < b
64
+ return 1 if a > b
65
+ end
66
+ return 0
67
+ end
68
+
69
+ def inspect
70
+ return "#<Composite:#{@column_slice} #{@parts.inspect}>"
71
+ end
72
+
73
+ def slice_end_of_component
74
+ return "\x01" if @column_slice == :after
75
+ return "\xFF" if @column_slice == :before
76
+ return "\x00"
77
+ end
78
+
79
+ private
80
+ def unpack(packed_string)
81
+ parts = []
82
+ end_of_component = nil
83
+ while packed_string.length > 0
84
+ length = packed_string.slice(0, 2).unpack('n')[0]
85
+ parts << packed_string.slice(2, length)
86
+ end_of_component = packed_string.slice(2 + length, 1)
87
+
88
+ packed_string = packed_string.slice(3 + length, packed_string.length)
89
+ end
90
+ @column_slice = :after if end_of_component == "\x01"
91
+ @column_slice = :before if end_of_component == "\xFF"
92
+ @parts = parts
93
+ end
94
+
95
+ def valid_packed_composite?(packed_string)
96
+ while packed_string.length > 0
97
+ length = packed_string.slice(0, 2).unpack('n')[0]
98
+ return false if length.nil? || length + 3 > packed_string.length
99
+
100
+ end_of_component = packed_string.slice(2 + length, 1)
101
+ if length + 3 != packed_string.length
102
+ return false if end_of_component != "\x00"
103
+ end
104
+
105
+ packed_string = packed_string.slice(3 + length, packed_string.length)
106
+ end
107
+ return true
108
+ end
109
+
110
+ def hash
111
+ return to_s.hash
112
+ end
113
+
114
+ def eql?(other)
115
+ return to_s == other.to_s
116
+ end
117
+ end
118
+ end
@@ -393,11 +393,16 @@ class Cassandra
393
393
  if (start_key.nil? || key >= start_key) && (finish_key.nil? || key <= finish_key)
394
394
  if columns
395
395
  #ret[key] = columns.inject(OrderedHash.new){|hash, column_name| hash[column_name] = cf(column_family)[key][column_name]; hash;}
396
- ret[key] = columns_to_hash(column_family, cf(column_family)[key].select{|k,v| columns.include?(k)})
396
+ selected_hash = OrderedHash.new
397
+ cf(column_family)[key].each do |k, v|
398
+ selected_hash.[]=(k, v, cf(column_family)[key].timestamps[k]) if columns.include?(k)
399
+ end
400
+ ret[key] = columns_to_hash(column_family, selected_hash)
397
401
  ret[key] = apply_count(ret[key], count, reversed)
398
402
  blk.call(key,ret[key]) unless blk.nil?
399
403
  else
400
404
  #ret[key] = apply_range(cf(column_family)[key], column_family, start, finish, !is_super(column_family))
405
+ start, finish = finish, start if reversed
401
406
  ret[key] = apply_range(columns_to_hash(column_family, cf(column_family)[key]), column_family, start, finish)
402
407
  ret[key] = apply_count(ret[key], count, reversed)
403
408
  blk.call(key,ret[key]) unless blk.nil?
@@ -446,7 +451,12 @@ class Cassandra
446
451
 
447
452
  new_stuff = new_stuff.to_a.inject({}){|h,k| h[k[0].to_s] = k[1]; h }
448
453
 
449
- OrderedHash[old_stuff.merge(new_stuff).sort{|a,b| a[0] <=> b[0]}]
454
+ new_stuff.each { |k,v| old_stuff.[]=(k, v, (Time.now.to_f * 1000000).to_i) }
455
+ hash = OrderedHash.new
456
+ old_stuff.sort{ |a,b| a[0] <=> b[0] }.each do |k, v|
457
+ hash.[]=(k, v, old_stuff.timestamps[k])
458
+ end
459
+ hash
450
460
  end
451
461
 
452
462
  def columns_to_hash(column_family, columns)
@@ -454,15 +464,17 @@ class Cassandra
454
464
  output = OrderedHash.new
455
465
 
456
466
  columns.each do |column_name, value|
467
+ timestamp = columns.timestamps[column_name]
457
468
  column = column_class.new(column_name)
458
469
 
459
470
  if [Hash, OrderedHash].include?(value.class)
460
471
  output[column] ||= OrderedHash.new
461
472
  value.each do |sub_column, sub_column_value|
462
- output[column][sub_column_class.new(sub_column)] = sub_column_value
473
+ timestamp = value.timestamps[sub_column]
474
+ output[column].[]=(sub_column_class.new(sub_column), sub_column_value, timestamp)
463
475
  end
464
476
  else
465
- output[column_class.new(column_name)] = value
477
+ output.[]=(column_class.new(column_name), value, timestamp)
466
478
  end
467
479
  end
468
480
 
@@ -475,7 +487,7 @@ class Cassandra
475
487
  keys = keys.reverse if reversed
476
488
  keys = keys[0...count]
477
489
  keys.inject(OrderedHash.new) do |memo, key|
478
- memo[key] = row[key]
490
+ memo.[]=(key, row[key], row.timestamps[key])
479
491
  memo
480
492
  end
481
493
  else
@@ -489,7 +501,7 @@ class Cassandra
489
501
  ret = OrderedHash.new
490
502
  row.keys.each do |key|
491
503
  if (start.nil? || key >= start) && (finish.nil? || key <= finish)
492
- ret[key] = row[key]
504
+ ret.[]=(key, row[key], row.timestamps[key])
493
505
  end
494
506
  end
495
507
  ret
@@ -22,6 +22,12 @@ class CassandraMockTest < CassandraTest
22
22
 
23
23
  @uuids = (0..6).map {|i| SimpleUUID::UUID.new(Time.at(2**(24+i))) }
24
24
  @longs = (0..6).map {|i| Long.new(Time.at(2**(24+i))) }
25
+ @composites = [
26
+ Cassandra::Composite.new([5].pack('N'), "zebra"),
27
+ Cassandra::Composite.new([5].pack('N'), "aardvark"),
28
+ Cassandra::Composite.new([1].pack('N'), "elephant"),
29
+ Cassandra::Composite.new([10].pack('N'), "kangaroo"),
30
+ ]
25
31
  end
26
32
 
27
33
  def test_setup
@@ -57,6 +63,24 @@ class CassandraMockTest < CassandraTest
57
63
  end
58
64
  end
59
65
 
66
+ def test_get_range_reversed_slice
67
+ data = 4.times.map { |i| ["body-#{i.to_s}", "v"] }
68
+ hash = Cassandra::OrderedHash[data]
69
+ sliced_hash = Cassandra::OrderedHash[data.reverse[1..-1]]
70
+
71
+ @twitter.insert(:Statuses, "all-keys", hash)
72
+
73
+ columns = @twitter.get_range(
74
+ :Statuses,
75
+ :start => sliced_hash.keys.first,
76
+ :reversed => true
77
+ )["all-keys"]
78
+
79
+ columns.each do |column|
80
+ assert_equal sliced_hash.shift, column
81
+ end
82
+ end
83
+
60
84
  def test_get_range_count
61
85
  data = 3.times.map { |i| ["body-#{i.to_s}", "v"] }
62
86
  hash = Cassandra::OrderedHash[data]
@@ -76,4 +100,23 @@ class CassandraMockTest < CassandraTest
76
100
  @twitter.insert(:UserRelationships, 'a', ['u1','u2'])
77
101
  }
78
102
  end
103
+
104
+ def test_column_timestamps
105
+ base_time = Time.now
106
+ @twitter.insert(:Statuses, "time-key", { "body" => "value" })
107
+
108
+ results = @twitter.get(:Statuses, "time-key")
109
+ assert(results.timestamps["body"] / 1000000 >= base_time.to_i)
110
+ end
111
+
112
+ def test_supercolumn_timestamps
113
+ base_time = Time.now
114
+ @twitter.insert(:StatusRelationships, "time-key", { "super" => { @uuids[1] => "value" }})
115
+
116
+ results = @twitter.get(:StatusRelationships, "time-key")
117
+ assert_nil(results.timestamps["super"])
118
+
119
+ columns = results["super"]
120
+ assert(columns.timestamps[@uuids[1]] / 1000000 >= base_time.to_i)
121
+ end
79
122
  end
@@ -21,6 +21,12 @@ class CassandraTest < Test::Unit::TestCase
21
21
 
22
22
  @uuids = (0..6).map {|i| SimpleUUID::UUID.new(Time.at(2**(24+i))) }
23
23
  @longs = (0..6).map {|i| Long.new(Time.at(2**(24+i))) }
24
+ @composites = [
25
+ Cassandra::Composite.new([5].pack('N'), "zebra"),
26
+ Cassandra::Composite.new([5].pack('N'), "aardvark"),
27
+ Cassandra::Composite.new([1].pack('N'), "elephant"),
28
+ Cassandra::Composite.new([10].pack('N'), "kangaroo"),
29
+ ]
24
30
  end
25
31
 
26
32
  def test_inspect
@@ -43,7 +49,7 @@ class CassandraTest < Test::Unit::TestCase
43
49
  end
44
50
 
45
51
  def test_get_key
46
-
52
+
47
53
  @twitter.insert(:Users, key, {'body' => 'v', 'user' => 'v'})
48
54
  assert_equal({'body' => 'v', 'user' => 'v'}, @twitter.get(:Users, key))
49
55
  assert_equal(['body', 'user'].sort, @twitter.get(:Users, key).timestamps.keys.sort)
@@ -280,35 +286,46 @@ class CassandraTest < Test::Unit::TestCase
280
286
 
281
287
  def test_get_range_block
282
288
  k = key
289
+
290
+ values = {}
283
291
  5.times do |i|
284
- @twitter.insert(:Statuses, k+i.to_s, {"body-#{i.to_s}" => 'v'})
292
+ values[k+i.to_s] = {"body-#{i.to_s}" => 'v'}
285
293
  end
286
294
 
287
- values = (0..4).collect{|n| { :key => "test_get_range_block#{n}", :columns => { "body-#{n}" => "v" }} }.reverse
295
+ values.each {|key, columns| @twitter.insert(:Statuses, key, columns) }
288
296
 
289
297
  returned_value = @twitter.get_range(:Statuses, :start_key => k.to_s, :key_count => 5) do |key,columns|
290
- expected = values.pop
291
- assert_equal expected[:key], key
292
- assert_equal expected[:columns], columns
298
+ expected = values.delete(key)
299
+ assert_equal expected, columns
293
300
  end
294
301
 
295
- assert_equal [], values
302
+ assert values.length < 5
296
303
  assert_nil returned_value
297
304
  end
298
-
305
+
299
306
  def test_get_range_reversed
300
307
  data = 3.times.map { |i| ["body-#{i.to_s}", "v"] }
301
308
  hash = Cassandra::OrderedHash[data]
302
309
  reversed_hash = Cassandra::OrderedHash[data.reverse]
303
-
310
+
304
311
  @twitter.insert(:Statuses, "all-keys", hash)
305
-
312
+
306
313
  columns = @twitter.get_range(:Statuses, :reversed => true)["all-keys"]
307
314
  columns.each do |column|
308
315
  assert_equal reversed_hash.shift, column
309
316
  end
310
317
  end
311
318
 
319
+ def test_get_range_with_start_key_and_key_count
320
+ hash = {"name" => "value"}
321
+ @twitter.insert(:Statuses, "a-key", hash)
322
+ @twitter.insert(:Statuses, "b-key", hash)
323
+ @twitter.insert(:Statuses, "c-key", hash)
324
+
325
+ results = @twitter.get_range(:Statuses, :start_key => "b-key", :key_count => 1)
326
+ assert_equal ["b-key"], results.keys
327
+ end
328
+
312
329
  def test_each_key
313
330
  k = key
314
331
  keys_yielded = []
@@ -490,12 +507,12 @@ class CassandraTest < Test::Unit::TestCase
490
507
 
491
508
  def test_count_columns
492
509
  columns = (1..200).inject(Hash.new){|h,v| h['column' + v.to_s] = v.to_s; h;}
493
-
510
+
494
511
  @twitter.insert(:Statuses, key, columns)
495
512
  assert_equal 200, @twitter.count_columns(:Statuses, key, :count => 200)
496
- assert_equal 100, @twitter.count_columns(:Statuses, key)
513
+ assert_equal 100, @twitter.count_columns(:Statuses, key)
497
514
  assert_equal 55, @twitter.count_columns(:Statuses, key, :count => 55)
498
-
515
+
499
516
  end
500
517
 
501
518
  def test_count_super_columns
@@ -539,40 +556,43 @@ class CassandraTest < Test::Unit::TestCase
539
556
  @twitter.insert(:Users, k + '3', {'body' => 'bogus', 'user' => 'v3'})
540
557
  @twitter.insert(:Users, k + '3', {'body' => 'v3', 'location' => 'v3'})
541
558
  @twitter.insert(:Statuses, k + '3', {'body' => 'v'})
559
+ @twitter.add(:UserCounters, 'bob', 5, 'tweet_count') if CASSANDRA_VERSION.to_f >= 0.8
542
560
 
543
561
  assert_equal({'delete_me' => 'v0', 'keep_me' => 'v0'}, @twitter.get(:Users, k + '0')) # Written
544
562
  assert_equal({'body' => 'v1', 'user' => 'v1'}, @twitter.get(:Users, k + '1')) # Written
545
563
  assert_equal({}, @twitter.get(:Users, k + '2')) # Not yet written
546
564
  assert_equal({}, @twitter.get(:Statuses, k + '3')) # Not yet written
565
+ assert_equal({}, @twitter.get(:UserCounters, 'bob')) if CASSANDRA_VERSION.to_f >= 0.8 # Not yet written
547
566
 
548
- @twitter.remove(:Users, k + '1') # Full row
567
+ @twitter.remove(:Users, k + '1') # Full row
549
568
  assert_equal({'body' => 'v1', 'user' => 'v1'}, @twitter.get(:Users, k + '1')) # Not yet removed
550
569
 
551
570
  @twitter.remove(:Users, k + '0', 'delete_me') # A single column of the row
552
571
  assert_equal({'delete_me' => 'v0', 'keep_me' => 'v0'}, @twitter.get(:Users, k + '0')) # Not yet removed
553
-
572
+
554
573
  @twitter.remove(:Users, k + '4')
555
574
  @twitter.insert(:Users, k + '4', {'body' => 'v4', 'user' => 'v4'})
556
575
  assert_equal({}, @twitter.get(:Users, k + '4')) # Not yet written
557
576
 
558
577
  # SuperColumns
559
578
  # Add and delete new sub columns to the user timeline supercolumn
560
- @twitter.insert(:StatusRelationships, k, {'user_timelines' => new_subcolumns })
579
+ @twitter.insert(:StatusRelationships, k, {'user_timelines' => new_subcolumns })
561
580
  @twitter.remove(:StatusRelationships, k, 'user_timelines' , subcolumn_to_delete ) # Delete the first of the initial_subcolumns from the user_timeline supercolumn
562
581
  assert_equal(initial_subcolumns, @twitter.get(:StatusRelationships, k, 'user_timelines')) # No additions or deletes reflected yet
563
- # Delete a complete supercolumn
582
+ # Delete a complete supercolumn
564
583
  @twitter.remove(:StatusRelationships, k, 'dummy_supercolumn' ) # Delete the full dummy supercolumn
565
- assert_equal({@uuids[5] => 'value'}, @twitter.get(:StatusRelationships, k, 'dummy_supercolumn')) # dummy supercolumn not yet deleted
584
+ assert_equal({@uuids[5] => 'value'}, @twitter.get(:StatusRelationships, k, 'dummy_supercolumn')) # dummy supercolumn not yet deleted
566
585
  end
567
586
 
568
587
  assert_equal({'body' => 'v2', 'user' => 'v2'}, @twitter.get(:Users, k + '2')) # Written
569
588
  assert_equal({'body' => 'v3', 'user' => 'v3', 'location' => 'v3'}, @twitter.get(:Users, k + '3')) # Written and compacted
570
589
  assert_equal({'body' => 'v4', 'user' => 'v4'}, @twitter.get(:Users, k + '4')) # Written
571
590
  assert_equal({'body' => 'v'}, @twitter.get(:Statuses, k + '3')) # Written
591
+ assert_equal({'tweet_count' => 5}, @twitter.get(:UserCounters, 'bob')) if CASSANDRA_VERSION.to_f >= 0.8 # Written
572
592
  assert_equal({}, @twitter.get(:Users, k + '1')) # Removed
573
-
593
+
574
594
  assert_equal({ 'keep_me' => 'v0'}, @twitter.get(:Users, k + '0')) # 'delete_me' column removed
575
-
595
+
576
596
 
577
597
  assert_equal({'body' => 'v2', 'user' => 'v2'}.keys.sort, @twitter.get(:Users, k + '2').timestamps.keys.sort) # Written
578
598
  assert_equal({'body' => 'v3', 'user' => 'v3', 'location' => 'v3'}.keys.sort, @twitter.get(:Users, k + '3').timestamps.keys.sort) # Written and compacted
@@ -582,7 +602,7 @@ class CassandraTest < Test::Unit::TestCase
582
602
  # Final result: initial_subcolumns - initial_subcolumns.first + new_subcolumns
583
603
  resulting_subcolumns = initial_subcolumns.merge(new_subcolumns).reject{|k2,v| k2 == subcolumn_to_delete }
584
604
  assert_equal(resulting_subcolumns, @twitter.get(:StatusRelationships, key, 'user_timelines'))
585
- assert_equal({}, @twitter.get(:StatusRelationships, key, 'dummy_supercolumn')) # dummy supercolumn deleted
605
+ assert_equal({}, @twitter.get(:StatusRelationships, key, 'dummy_supercolumn')) # dummy supercolumn deleted
586
606
 
587
607
  end
588
608
 
@@ -823,6 +843,75 @@ class CassandraTest < Test::Unit::TestCase
823
843
  assert_equal(1, @twitter.get(:UserCounterAggregates, 'bob', 'DAU', 'today'))
824
844
  assert_equal(2, @twitter.get(:UserCounterAggregates, 'bob', 'DAU', 'tomorrow'))
825
845
  end
846
+
847
+ def test_reading_rows_with_super_column_counter
848
+ assert_nil @twitter.add(:UserCounterAggregates, 'bob', 1, 'DAU', 'today')
849
+ assert_nil @twitter.add(:UserCounterAggregates, 'bob', 2, 'DAU', 'tomorrow')
850
+ result = @twitter.get(:UserCounterAggregates, 'bob')
851
+ assert_equal(1, result.size)
852
+ assert_equal(2, result.first.size)
853
+ assert_equal("DAU", result.first[0])
854
+ assert_equal(1, result.first[1]["today"])
855
+ assert_equal(2, result.first[1]["tomorrow"])
856
+ end
857
+
858
+ def test_composite_column_type_conversion
859
+ columns = {}
860
+ @composites.each_with_index do |c, index|
861
+ columns[c] = "value-#{index}"
862
+ end
863
+ @type_conversions.insert(:CompositeColumnConversion, key, columns)
864
+ columns_in_order = [
865
+ Cassandra::Composite.new([1].pack('N'), "elephant"),
866
+ Cassandra::Composite.new([5].pack('N'), "aardvark"),
867
+ Cassandra::Composite.new([5].pack('N'), "zebra"),
868
+ Cassandra::Composite.new([10].pack('N'), "kangaroo"),
869
+ ]
870
+ assert_equal(columns_in_order, @type_conversions.get(:CompositeColumnConversion, key).keys)
871
+
872
+ column_slice = @type_conversions.get(:CompositeColumnConversion, key,
873
+ :start => Cassandra::Composite.new([1].pack('N')),
874
+ :finish => Cassandra::Composite.new([10].pack('N')),
875
+ ).keys
876
+ assert_equal(columns_in_order[0..-2], column_slice)
877
+
878
+ column_slice = @type_conversions.get(:CompositeColumnConversion, key,
879
+ :start => Cassandra::Composite.new([5].pack('N')),
880
+ :finish => Cassandra::Composite.new([5].pack('N'), :slice => :after),
881
+ ).keys
882
+ assert_equal(columns_in_order[1..2], column_slice)
883
+
884
+ column_slice = @type_conversions.get(:CompositeColumnConversion, key,
885
+ :start => Cassandra::Composite.new([5].pack('N'), :slice => :after).to_s,
886
+ ).keys
887
+ assert_equal([columns_in_order[-1]], column_slice)
888
+
889
+ column_slice = @type_conversions.get(:CompositeColumnConversion, key,
890
+ :finish => Cassandra::Composite.new([10].pack('N'), :slice => :before).to_s,
891
+ ).keys
892
+ assert_equal(columns_in_order[0..-2], column_slice)
893
+
894
+ assert_equal('value-2', @type_conversions.get(:CompositeColumnConversion, key, columns_in_order.first))
895
+ end
896
+ end
897
+
898
+ def test_column_timestamps
899
+ base_time = Time.now
900
+ @twitter.insert(:Statuses, "time-key", { "body" => "value" })
901
+
902
+ results = @twitter.get(:Statuses, "time-key")
903
+ assert(results.timestamps["body"] / 1000000 >= base_time.to_i)
904
+ end
905
+
906
+ def test_supercolumn_timestamps
907
+ base_time = Time.now
908
+ @twitter.insert(:StatusRelationships, "time-key", { "super" => { @uuids[1] => "value" }})
909
+
910
+ results = @twitter.get(:StatusRelationships, "time-key")
911
+ assert_nil(results.timestamps["super"])
912
+
913
+ columns = results["super"]
914
+ assert(columns.timestamps[@uuids[1]] / 1000000 >= base_time.to_i)
826
915
  end
827
916
 
828
917
  private