cassandra 0.12.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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