cassandra 0.13.0 → 0.14.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.
@@ -16,12 +16,37 @@ class Cassandra
16
16
  @sub_column_name_class[column_family] ||= column_name_class_for_key(column_family, "subcomparator_type")
17
17
  end
18
18
 
19
+ def column_name_maker(column_family)
20
+ @column_name_maker[column_family] ||=
21
+ begin
22
+ klass = column_name_class(column_family)
23
+ if klass == Composite
24
+ lambda {|name| klass.new_from_packed(name) }
25
+ else
26
+ lambda {|name| klass.new(name) }
27
+ end
28
+ end
29
+ end
30
+
31
+ def sub_column_name_maker(column_family)
32
+ @sub_column_name_maker[column_family] ||=
33
+ begin
34
+ klass = sub_column_name_class(column_family)
35
+ if klass == Composite
36
+ lambda {|name| klass.new_from_packed(name) }
37
+ else
38
+ lambda {|name| klass.new(name) }
39
+ end
40
+ end
41
+ end
42
+
19
43
  def column_name_class_for_key(column_family, comparator_key)
20
44
  property = column_family_property(column_family, comparator_key)
21
45
  property =~ /[^(]*\.(.*?)$/
22
46
  case $1
23
47
  when "LongType" then Long
24
48
  when "LexicalUUIDType", "TimeUUIDType" then SimpleUUID::UUID
49
+ when /^DynamicCompositeType\(/ then DynamicComposite
25
50
  when /^CompositeType\(/ then Composite
26
51
  else
27
52
  String # UTF8, Ascii, Bytes, anything else
@@ -64,26 +89,26 @@ class Cassandra
64
89
  end
65
90
 
66
91
  def columns_to_hash(column_family, columns)
67
- columns_to_hash_for_classes(columns, column_name_class(column_family), sub_column_name_class(column_family))
92
+ columns_to_hash_for_classes(columns, column_name_maker(column_family), sub_column_name_maker(column_family))
68
93
  end
69
94
 
70
95
  def sub_columns_to_hash(column_family, columns)
71
- columns_to_hash_for_classes(columns, sub_column_name_class(column_family))
96
+ columns_to_hash_for_classes(columns, sub_column_name_maker(column_family))
72
97
  end
73
98
 
74
- def columns_to_hash_for_classes(columns, column_name_class, sub_column_name_class = nil)
99
+ def columns_to_hash_for_classes(columns, column_name_maker, sub_column_name_maker = nil)
75
100
  hash = OrderedHash.new
76
101
  Array(columns).each do |c|
77
102
  c = c.super_column || c.column || c.counter_column || c.counter_super_column if c.is_a?(CassandraThrift::ColumnOrSuperColumn)
78
103
  case c
79
- when CassandraThrift::CounterSuperColumn
80
- hash.[]=(column_name_class.new(c.name), columns_to_hash_for_classes(c.columns, sub_column_name_class)) # Pop the class stack, and recurse
81
104
  when CassandraThrift::SuperColumn
82
- hash.[]=(column_name_class.new(c.name), columns_to_hash_for_classes(c.columns, sub_column_name_class)) # Pop the class stack, and recurse
105
+ hash.[]=(column_name_maker.call(c.name), columns_to_hash_for_classes(c.columns, sub_column_name_maker)) # Pop the class stack, and recurse
83
106
  when CassandraThrift::Column
84
- hash.[]=(column_name_class.new(c.name), c.value, c.timestamp)
107
+ hash.[]=(column_name_maker.call(c.name), c.value, c.timestamp)
85
108
  when CassandraThrift::CounterColumn
86
- hash.[]=(column_name_class.new(c.name), c.value, 0)
109
+ hash.[]=(column_name_maker.call(c.name), c.value, 0)
110
+ when CassandraThrift::CounterSuperColumn
111
+ hash.[]=(column_name_maker.call(c.name), columns_to_hash_for_classes(c.columns, sub_column_name_maker)) # Pop the class stack, and recurse
87
112
  end
88
113
  end
89
114
  hash
@@ -103,7 +128,7 @@ class Cassandra
103
128
  end
104
129
 
105
130
  def _super_insert_mutation(column_family, super_column_name, sub_columns, timestamp, ttl = nil)
106
- CassandraThrift::Mutation.new(:column_or_supercolumn =>
131
+ CassandraThrift::Mutation.new(:column_or_supercolumn =>
107
132
  CassandraThrift::ColumnOrSuperColumn.new(
108
133
  :super_column => CassandraThrift::SuperColumn.new(
109
134
  :name => column_name_class(column_family).new(super_column_name).to_s,
@@ -122,12 +147,12 @@ class Cassandra
122
147
 
123
148
  # General info about a deletion object within a mutation
124
149
  # timestamp - required. If this is the only param, it will cause deletion of the whole key at that TS
125
- # supercolumn - opt. If passed, the deletes will only occur within that supercolumn (only subcolumns
150
+ # supercolumn - opt. If passed, the deletes will only occur within that supercolumn (only subcolumns
126
151
  # will be deleted). Otherwise the normal columns will be deleted.
127
- # predicate - opt. Defines how to match the columns to delete. if supercolumn passed, the slice will
152
+ # predicate - opt. Defines how to match the columns to delete. if supercolumn passed, the slice will
128
153
  # be scoped to subcolumns of that supercolumn.
129
-
130
- # Deletes a single column from the containing key/CF (and possibly supercolumn), at a given timestamp.
154
+
155
+ # Deletes a single column from the containing key/CF (and possibly supercolumn), at a given timestamp.
131
156
  # Although mutations (as opposed to 'remove' calls) support deleting slices and lists of columns in one shot, this is not implemented here.
132
157
  # The main reason being that the batch function takes removes, but removes don't have that capability...so we'd need to change the remove
133
158
  # methods to use delete mutation calls...although that might have performance implications. We'll leave that refactoring for later.
@@ -1,4 +1,3 @@
1
-
2
1
  class Cassandra
3
2
  class Composite
4
3
  include ::Comparable
@@ -6,6 +5,8 @@ class Cassandra
6
5
  attr_reader :column_slice
7
6
 
8
7
  def initialize(*parts)
8
+ return if parts.empty?
9
+
9
10
  options = {}
10
11
  if parts.last.is_a?(Hash)
11
12
  options = parts.pop
@@ -16,13 +17,19 @@ class Cassandra
16
17
  if parts.length == 1 && parts[0].instance_of?(self.class)
17
18
  @column_slice = parts[0].column_slice
18
19
  @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])
20
+ elsif parts.length == 1 && parts[0].instance_of?(String) && @column_slice.nil? && try_packed_composite(parts[0])
21
+ @hash = parts[0].hash
21
22
  else
22
23
  @parts = parts
23
24
  end
24
25
  end
25
26
 
27
+ def self.new_from_packed(packed)
28
+ obj = new
29
+ obj.fast_unpack(packed)
30
+ return obj
31
+ end
32
+
26
33
  def [](*args)
27
34
  return @parts[*args]
28
35
  end
@@ -67,7 +74,7 @@ class Cassandra
67
74
  end
68
75
 
69
76
  def inspect
70
- return "#<Composite:#{@column_slice} #{@parts.inspect}>"
77
+ return "#<#{self.class}:#{@column_slice} #{@parts.inspect}>"
71
78
  end
72
79
 
73
80
  def slice_end_of_component
@@ -76,27 +83,31 @@ class Cassandra
76
83
  return "\x00"
77
84
  end
78
85
 
79
- private
80
- def unpack(packed_string)
81
- parts = []
82
- end_of_component = nil
86
+ def fast_unpack(packed_string)
87
+ @hash = packed_string.hash
88
+
89
+ @parts = []
90
+ end_of_component = packed_string.slice(packed_string.length-1, 1)
83
91
  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)
92
+ length = packed_string.unpack('n')[0]
93
+ @parts << packed_string.slice(2, length)
87
94
 
88
- packed_string = packed_string.slice(3 + length, packed_string.length)
95
+ packed_string.slice!(0, length+3)
89
96
  end
97
+
90
98
  @column_slice = :after if end_of_component == "\x01"
91
99
  @column_slice = :before if end_of_component == "\xFF"
92
- @parts = parts
93
100
  end
94
101
 
95
- def valid_packed_composite?(packed_string)
102
+ private
103
+ def try_packed_composite(packed_string)
104
+ parts = []
105
+ end_of_component = nil
96
106
  while packed_string.length > 0
97
107
  length = packed_string.slice(0, 2).unpack('n')[0]
98
108
  return false if length.nil? || length + 3 > packed_string.length
99
109
 
110
+ parts << packed_string.slice(2, length)
100
111
  end_of_component = packed_string.slice(2 + length, 1)
101
112
  if length + 3 != packed_string.length
102
113
  return false if end_of_component != "\x00"
@@ -104,11 +115,16 @@ class Cassandra
104
115
 
105
116
  packed_string = packed_string.slice(3 + length, packed_string.length)
106
117
  end
118
+
119
+ @column_slice = :after if end_of_component == "\x01"
120
+ @column_slice = :before if end_of_component == "\xFF"
121
+ @parts = parts
122
+
107
123
  return true
108
124
  end
109
125
 
110
126
  def hash
111
- return to_s.hash
127
+ return @hash ||= pack.hash
112
128
  end
113
129
 
114
130
  def eql?(other)
@@ -116,3 +132,9 @@ class Cassandra
116
132
  end
117
133
  end
118
134
  end
135
+
136
+ begin
137
+ require "cassandra_native"
138
+ rescue LoadError
139
+ puts "Unable to load cassandra_native extension. Defaulting to pure Ruby libraries."
140
+ end
@@ -0,0 +1,96 @@
1
+ class Cassandra
2
+ class DynamicComposite < Composite
3
+ attr_accessor :types
4
+
5
+ def initialize(*parts)
6
+ return if parts.empty?
7
+
8
+ options = {}
9
+ if parts.last.is_a?(Hash)
10
+ options = parts.pop
11
+ end
12
+ @column_slice = options[:slice]
13
+ raise ArgumentError if @column_slice != nil && ![:before, :after].include?(@column_slice)
14
+
15
+ if parts.length == 1 && parts[0].instance_of?(self.class)
16
+ @column_slice = parts[0].column_slice
17
+ @parts = parts[0].parts
18
+ @types = parts[0].types
19
+ elsif parts.length == 1 && parts[0].instance_of?(String) && @column_slice.nil? && try_packed_composite(parts[0])
20
+ @hash = parts[0].hash
21
+ else
22
+ @types, @parts = parts.transpose
23
+ end
24
+ end
25
+
26
+ def pack
27
+ packed_parts = @parts.map do |part|
28
+ [part.length].pack('n') + part + "\x00"
29
+ end
30
+
31
+ if @column_slice
32
+ part = @parts[-1]
33
+ packed_parts[-1] = [part.length].pack('n') + part + slice_end_of_component
34
+ end
35
+
36
+ packed_types = @types.map do |type|
37
+ if type.length == 1
38
+ [0x8000 | type.ord].pack('n')
39
+ else
40
+ [type.length].pack('n') + type
41
+ end
42
+ end
43
+
44
+ return packed_types.zip(packed_parts).flatten.join('')
45
+ end
46
+
47
+ def fast_unpack(packed_string)
48
+ result = try_packed_composite(packed_string)
49
+ raise ArgumentError.new("Invalid DynamicComposite column") if !result
50
+ @hash = packed_string.hash
51
+ end
52
+
53
+ private
54
+ def try_packed_composite(packed_string)
55
+ types = []
56
+ parts = []
57
+ end_of_component = nil
58
+ offset = 0
59
+
60
+ read_bytes = proc do |length|
61
+ return false if offset + length > packed_string.length
62
+ out = packed_string.slice(offset, length)
63
+ offset += length
64
+ out
65
+ end
66
+
67
+ while offset < packed_string.length
68
+ header = read_bytes.call(2).unpack('n')[0]
69
+ is_alias = header & 0x8000 != 0
70
+ if is_alias
71
+ alias_char = (header & 0xFF).chr
72
+ types << alias_char
73
+ else
74
+ length = header
75
+ return false if length.nil? || length + offset > packed_string.length
76
+ type = read_bytes.call(length)
77
+ types << type
78
+ end
79
+ length = read_bytes.call(2).unpack('n')[0]
80
+ return false if length.nil? || length + offset > packed_string.length
81
+ parts << read_bytes.call(length)
82
+ end_of_component = read_bytes.call(1)
83
+ if offset < packed_string.length
84
+ return false if end_of_component != "\x00"
85
+ end
86
+ end
87
+ @column_slice = :after if end_of_component == "\x01"
88
+ @column_slice = :before if end_of_component == "\xFF"
89
+ @types = types
90
+ @parts = parts
91
+ @hash = packed_string.hash
92
+
93
+ return true
94
+ end
95
+ end
96
+ end
@@ -20,6 +20,8 @@ class Cassandra
20
20
  @keyspace = keyspace
21
21
  @column_name_class = {}
22
22
  @sub_column_name_class = {}
23
+ @column_name_maker = {}
24
+ @sub_column_name_maker = {}
23
25
  @indexes = {}
24
26
  @schema = schema[keyspace]
25
27
  clear_keyspace!
@@ -293,10 +295,13 @@ class Cassandra
293
295
 
294
296
  def get_indexed_slices(column_family, idx_clause, *columns_and_options)
295
297
  column_family, columns, _, options =
296
- extract_and_validate_params_for_real(column_family, [], columns_and_options, READ_DEFAULTS.merge(:key_count => 100, :key_start => ""))
298
+ extract_and_validate_params_for_real(column_family, [], columns_and_options,
299
+ READ_DEFAULTS.merge(:key_count => 100, :start_key => nil, :key_start => nil))
300
+
301
+ start_key = options[:start_key] || options[:key_start] || ""
297
302
 
298
303
  unless [Hash, OrderedHash].include?(idx_clause.class) && idx_clause[:type] == :index_clause
299
- idx_clause = create_index_clause(idx_clause, options[:key_start], options[:key_count])
304
+ idx_clause = create_index_clause(idx_clause, start_key, options[:key_count])
300
305
  end
301
306
 
302
307
  ret = {}
@@ -319,20 +324,24 @@ class Cassandra
319
324
  end
320
325
 
321
326
  def add(column_family, key, value, *columns_and_options)
322
- column_family, column, sub_column, options = extract_and_validate_params_for_real(column_family, key, columns_and_options, WRITE_DEFAULTS)
323
-
324
- if is_super(column_family)
325
- cf(column_family)[key] ||= OrderedHash.new
326
- cf(column_family)[key][column] ||= OrderedHash.new
327
- cf(column_family)[key][column][sub_column] ||= 0
328
- cf(column_family)[key][column][sub_column] += value
327
+ if @batch
328
+ @batch << [:add, column_family, key, value, *columns_and_options]
329
329
  else
330
- cf(column_family)[key] ||= OrderedHash.new
331
- cf(column_family)[key][column] ||= 0
332
- cf(column_family)[key][column] += value
333
- end
330
+ column_family, column, sub_column, options = extract_and_validate_params_for_real(column_family, key, columns_and_options, WRITE_DEFAULTS)
334
331
 
335
- nil
332
+ if is_super(column_family)
333
+ cf(column_family)[key] ||= OrderedHash.new
334
+ cf(column_family)[key][column] ||= OrderedHash.new
335
+ cf(column_family)[key][column][sub_column] ||= 0
336
+ cf(column_family)[key][column][sub_column] += value
337
+ else
338
+ cf(column_family)[key] ||= OrderedHash.new
339
+ cf(column_family)[key][column] ||= 0
340
+ cf(column_family)[key][column] += value
341
+ end
342
+
343
+ nil
344
+ end
336
345
  end
337
346
 
338
347
  def column_families
@@ -28,6 +28,12 @@ class CassandraMockTest < CassandraTest
28
28
  Cassandra::Composite.new([1].pack('N'), "elephant"),
29
29
  Cassandra::Composite.new([10].pack('N'), "kangaroo"),
30
30
  ]
31
+ @dynamic_composites = [
32
+ Cassandra::DynamicComposite.new(['i', [5].pack('N')], ['UTF8Type', "zebra"]),
33
+ Cassandra::DynamicComposite.new(['i', [5].pack('N')], ['UTF8Type', "aardvark"]),
34
+ Cassandra::DynamicComposite.new(['IntegerType', [1].pack('N')], ['s', "elephant"]),
35
+ Cassandra::DynamicComposite.new(['IntegerType', [10].pack('N')], ['s', "kangaroo"]),
36
+ ]
31
37
  end
32
38
 
33
39
  def test_setup
@@ -3,17 +3,21 @@ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
3
3
  class CassandraTest < Test::Unit::TestCase
4
4
  include Cassandra::Constants
5
5
 
6
+ def assert_has_keys(keys, hash)
7
+ assert_equal Set.new(keys), Set.new(hash.keys)
8
+ end
9
+
6
10
  def setup
7
- @twitter = Cassandra.new('Twitter', "127.0.0.1:9160", :retries => 2, :connect_timeout => 0.1, :exception_classes => [])
11
+ @twitter = Cassandra.new('Twitter', "127.0.0.1:9160", :retries => 2, :connect_timeout => 0.1, :timeout => 5, :exception_classes => [])
8
12
  @twitter.clear_keyspace!
9
13
 
10
- @blogs = Cassandra.new('Multiblog', "127.0.0.1:9160", :retries => 2, :connect_timeout => 0.1, :exception_classes => [])
14
+ @blogs = Cassandra.new('Multiblog', "127.0.0.1:9160", :retries => 2, :connect_timeout => 0.1, :timeout => 5, :exception_classes => [])
11
15
  @blogs.clear_keyspace!
12
16
 
13
- @blogs_long = Cassandra.new('MultiblogLong', "127.0.0.1:9160", :retries => 2, :connect_timeout => 0.1, :exception_classes => [])
17
+ @blogs_long = Cassandra.new('MultiblogLong', "127.0.0.1:9160", :retries => 2, :connect_timeout => 0.1, :timeout => 5, :exception_classes => [])
14
18
  @blogs_long.clear_keyspace!
15
19
 
16
- @type_conversions = Cassandra.new('TypeConversions', "127.0.0.1:9160", :retries => 2, :connect_timeout => 0.1, :exception_classes => [])
20
+ @type_conversions = Cassandra.new('TypeConversions', "127.0.0.1:9160", :retries => 2, :connect_timeout => 0.1, :timeout => 5, :exception_classes => [])
17
21
  @type_conversions.clear_keyspace!
18
22
 
19
23
  Cassandra::WRITE_DEFAULTS[:consistency] = Cassandra::Consistency::ONE
@@ -27,6 +31,12 @@ class CassandraTest < Test::Unit::TestCase
27
31
  Cassandra::Composite.new([1].pack('N'), "elephant"),
28
32
  Cassandra::Composite.new([10].pack('N'), "kangaroo"),
29
33
  ]
34
+ @dynamic_composites = [
35
+ Cassandra::DynamicComposite.new(['i', [5].pack('N')], ['UTF8Type', "zebra"]),
36
+ Cassandra::DynamicComposite.new(['i', [5].pack('N')], ['UTF8Type', "aardvark"]),
37
+ Cassandra::DynamicComposite.new(['IntegerType', [1].pack('N')], ['s', "elephant"]),
38
+ Cassandra::DynamicComposite.new(['IntegerType', [10].pack('N')], ['s', "kangaroo"]),
39
+ ]
30
40
  end
31
41
 
32
42
  def test_inspect
@@ -510,9 +520,8 @@ class CassandraTest < Test::Unit::TestCase
510
520
 
511
521
  @twitter.insert(:Statuses, key, columns)
512
522
  assert_equal 200, @twitter.count_columns(:Statuses, key, :count => 200)
513
- assert_equal 100, @twitter.count_columns(:Statuses, key)
514
- assert_equal 55, @twitter.count_columns(:Statuses, key, :count => 55)
515
-
523
+ assert_equal 100, @twitter.count_columns(:Statuses, key) if CASSANDRA_VERSION.to_f >= 0.8
524
+ assert_equal 55, @twitter.count_columns(:Statuses, key, :count => 55) if CASSANDRA_VERSION.to_f >= 0.8
516
525
  end
517
526
 
518
527
  def test_count_super_columns
@@ -562,7 +571,7 @@ class CassandraTest < Test::Unit::TestCase
562
571
  assert_equal({'body' => 'v1', 'user' => 'v1'}, @twitter.get(:Users, k + '1')) # Written
563
572
  assert_equal({}, @twitter.get(:Users, k + '2')) # Not yet written
564
573
  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
574
+ assert_equal({}, @twitter.get(:UserCounters, 'bob')) if CASSANDRA_VERSION.to_f >= 0.8 # Written
566
575
 
567
576
  @twitter.remove(:Users, k + '1') # Full row
568
577
  assert_equal({'body' => 'v1', 'user' => 'v1'}, @twitter.get(:Users, k + '1')) # Not yet removed
@@ -715,69 +724,361 @@ class CassandraTest < Test::Unit::TestCase
715
724
 
716
725
  if CASSANDRA_VERSION.to_f >= 0.7
717
726
  def test_creating_and_dropping_new_index
718
- @twitter.create_index('Twitter', 'Statuses', 'column_name', 'LongType')
719
- assert_nil @twitter.create_index('Twitter', 'Statuses', 'column_name', 'LongType')
727
+ @twitter.create_index('Twitter', 'Statuses', 'column_name', 'BytesType')
728
+ assert_nil @twitter.create_index('Twitter', 'Statuses', 'column_name', 'BytesType')
720
729
 
721
730
  @twitter.drop_index('Twitter', 'Statuses', 'column_name')
722
731
  assert_nil @twitter.drop_index('Twitter', 'Statuses', 'column_name')
723
732
 
724
733
  # Recreating and redropping the same index should not error either.
725
- @twitter.create_index('Twitter', 'Statuses', 'column_name', 'LongType')
734
+ @twitter.create_index('Twitter', 'Statuses', 'column_name', 'BytesType')
726
735
  @twitter.drop_index('Twitter', 'Statuses', 'column_name')
727
736
  end
728
737
 
729
738
  def test_get_indexed_slices
730
- @twitter.create_index('Twitter', 'Statuses', 'x', 'LongType')
739
+ @twitter.insert(:Statuses, 'row_a', {
740
+ 'tags' => 'a',
741
+ 'y' => 'foo'
742
+ })
743
+ @twitter.insert(:Statuses, 'row_b', {
744
+ 'tags' => 'b',
745
+ 'y' => 'foo'
746
+ })
747
+ [1,2].each do |i|
748
+ @twitter.insert(:Statuses, "row_c_#{i}", {
749
+ 'tags' => 'c',
750
+ 'y' => 'a'
751
+ })
752
+ end
753
+ [3,4].each do |i|
754
+ @twitter.insert(:Statuses, "row_c_#{i}", {
755
+ 'tags' => 'c',
756
+ 'y' => 'b'
757
+ })
758
+ end
759
+ @twitter.insert(:Statuses, 'row_d', {
760
+ 'tags' => 'd',
761
+ 'y' => 'foo'
762
+ })
763
+ @twitter.insert(:Statuses, 'row_e', {
764
+ 'tags' => 'e',
765
+ 'y' => 'bar'
766
+ })
767
+
768
+ # Test == operator (single clause)
769
+ rows = @twitter.get_indexed_slices(:Statuses, [
770
+ {:column_name => 'tags',
771
+ :value => 'c',
772
+ :comparison => "=="}
773
+ ])
774
+ assert_has_keys %w[row_c_1 row_c_2 row_c_3 row_c_4], rows
775
+
776
+ # Test other operators
777
+ # Currently (as of Cassandra 1.1) you can't use anything but == as the
778
+ # primary query -- you must match on == first, and subsequent clauses are
779
+ # then applied as filters -- so that's what we are doing here.
780
+
781
+ # Test > operator
782
+ rows = @twitter.get_indexed_slices(:Statuses, [
783
+ {:column_name => 'tags',
784
+ :value => 'c',
785
+ :comparison => "=="},
786
+ {:column_name => 'y',
787
+ :value => 'a',
788
+ :comparison => ">"}
789
+ ])
790
+ assert_has_keys %w[row_c_3 row_c_4], rows
791
+
792
+ # Test >= operator
793
+ rows = @twitter.get_indexed_slices(:Statuses, [
794
+ {:column_name => 'tags',
795
+ :value => 'c',
796
+ :comparison => "=="},
797
+ {:column_name => 'y',
798
+ :value => 'a',
799
+ :comparison => ">="}
800
+ ])
801
+ assert_has_keys %w[row_c_1 row_c_2 row_c_3 row_c_4], rows
802
+
803
+ # Test < operator
804
+ rows = @twitter.get_indexed_slices(:Statuses, [
805
+ {:column_name => 'tags',
806
+ :value => 'c',
807
+ :comparison => "=="},
808
+ {:column_name => 'y',
809
+ :value => 'b',
810
+ :comparison => "<"}
811
+ ])
812
+ assert_has_keys %w[row_c_1 row_c_2], rows
813
+
814
+ # Test <= operator
815
+ rows = @twitter.get_indexed_slices(:Statuses, [
816
+ {:column_name => 'tags',
817
+ :value => 'c',
818
+ :comparison => "=="},
819
+ {:column_name => 'y',
820
+ :value => 'b',
821
+ :comparison => "<="}
822
+ ])
823
+ assert_has_keys %w[row_c_1 row_c_2 row_c_3 row_c_4], rows
824
+
825
+ # Test query on a non-indexed column
826
+ unless self.is_a?(CassandraMockTest)
827
+ assert_raises(CassandraThrift::InvalidRequestException) do
828
+ @twitter.get_indexed_slices(:Statuses, [
829
+ {:column_name => 'y',
830
+ :value => 'foo',
831
+ :comparison => "=="}
832
+ ])
833
+ end
834
+ end
731
835
 
732
- @twitter.insert(:Statuses, 'row1', { 'x' => [0,10].pack("NN") })
836
+ # Test start key
837
+ rows = @twitter.get_indexed_slices(:Statuses, [
838
+ {:column_name => 'tags',
839
+ :value => 'c',
840
+ :comparison => "=="},
841
+ ], :start_key => 'row_c_2')
842
+ assert_equal 'row_c_2', rows.keys.first
843
+ # <- can't test any keys after that since it's going to be random
844
+
845
+ # Test key_start option
846
+ rows = @twitter.get_indexed_slices(:Statuses, [
847
+ {:column_name => 'tags',
848
+ :value => 'c',
849
+ :comparison => "=="},
850
+ ], :key_start => 'row_c_2')
851
+ assert_equal 'row_c_2', rows.keys.first
852
+ # <- can't test any keys after that since it's going to be random
853
+
854
+ # Test key count
855
+ rows = @twitter.get_indexed_slices(:Statuses, [
856
+ {:column_name => 'tags',
857
+ :value => 'c',
858
+ :comparison => "=="}
859
+ ], :key_count => 2)
860
+ assert_equal 2, rows.length
861
+ # <- can't test which keys are present since it's going to be random
862
+ end
733
863
 
734
- (2..10).to_a.each do |i|
735
- @twitter.insert(:Statuses, 'row' + i.to_s, { 'x' => [0,20].pack("NN"), 'non_indexed' => [i].pack('N*') })
864
+ def test_get_indexed_slices_with_IndexClause_objects
865
+ @twitter.insert(:Statuses, 'row_a', {
866
+ 'tags' => 'a',
867
+ 'y' => 'foo'
868
+ })
869
+ @twitter.insert(:Statuses, 'row_b', {
870
+ 'tags' => 'b',
871
+ 'y' => 'foo'
872
+ })
873
+ [1,2].each do |i|
874
+ @twitter.insert(:Statuses, "row_c_#{i}", {
875
+ 'tags' => 'c',
876
+ 'y' => 'a'
877
+ })
878
+ end
879
+ [3,4].each do |i|
880
+ @twitter.insert(:Statuses, "row_c_#{i}", {
881
+ 'tags' => 'c',
882
+ 'y' => 'b'
883
+ })
884
+ end
885
+ @twitter.insert(:Statuses, 'row_d', {
886
+ 'tags' => 'd',
887
+ 'y' => 'foo'
888
+ })
889
+ @twitter.insert(:Statuses, 'row_e', {
890
+ 'tags' => 'e',
891
+ 'y' => 'bar'
892
+ })
893
+
894
+ # Test == operator (single clause)
895
+ index_clause = @twitter.create_index_clause([
896
+ @twitter.create_index_expression('tags', 'c', '==')
897
+ ])
898
+ rows = @twitter.get_indexed_slices(:Statuses, index_clause)
899
+ assert_has_keys %w[row_c_1 row_c_2 row_c_3 row_c_4], rows
900
+
901
+ # Test other operators
902
+ # Currently (as of Cassandra 1.1) you can't use anything but == as the
903
+ # primary query -- you must match on == first, and subsequent clauses are
904
+ # then applied as filters -- so that's what we are doing here.
905
+
906
+ # Test > operator
907
+ index_clause = @twitter.create_index_clause([
908
+ @twitter.create_index_expression('tags', 'c', '=='),
909
+ @twitter.create_index_expression('y', 'a', '>'),
910
+ ])
911
+ rows = @twitter.get_indexed_slices(:Statuses, index_clause)
912
+ assert_has_keys %w[row_c_3 row_c_4], rows
913
+
914
+ # Test >= operator
915
+ index_clause = @twitter.create_index_clause([
916
+ @twitter.create_index_expression('tags', 'c', '=='),
917
+ @twitter.create_index_expression('y', 'a', '>=')
918
+ ])
919
+ rows = @twitter.get_indexed_slices(:Statuses, index_clause)
920
+ assert_has_keys %w[row_c_1 row_c_2 row_c_3 row_c_4], rows
921
+
922
+ # Test < operator
923
+ index_clause = @twitter.create_index_clause([
924
+ @twitter.create_index_expression('tags', 'c', '=='),
925
+ @twitter.create_index_expression('y', 'b', '<')
926
+ ])
927
+ rows = @twitter.get_indexed_slices(:Statuses, index_clause)
928
+ assert_has_keys %w[row_c_1 row_c_2], rows
929
+
930
+ # Test <= operator
931
+ index_clause = @twitter.create_index_clause([
932
+ @twitter.create_index_expression('tags', 'c', '=='),
933
+ @twitter.create_index_expression('y', 'b', '<=')
934
+ ])
935
+ rows = @twitter.get_indexed_slices(:Statuses, index_clause)
936
+ assert_has_keys %w[row_c_1 row_c_2 row_c_3 row_c_4], rows
937
+
938
+ # Test query on a non-indexed column
939
+ unless self.is_a?(CassandraMockTest)
940
+ assert_raises(CassandraThrift::InvalidRequestException) do
941
+ index_clause = @twitter.create_index_clause([
942
+ @twitter.create_index_expression('y', 'foo', '==')
943
+ ])
944
+ @twitter.get_indexed_slices(:Statuses, index_clause)
945
+ end
736
946
  end
737
947
 
738
- @twitter.insert(:Statuses, 'row11', { 'x' => [0,30].pack("NN") })
948
+ # Test start key
949
+ index_clause = @twitter.create_index_clause([
950
+ @twitter.create_index_expression('tags', 'c', '==')
951
+ ], 'row_c_2')
952
+ rows = @twitter.get_indexed_slices(:Statuses, index_clause)
953
+ assert_equal 'row_c_2', rows.keys.first
954
+ # <- can't test any keys after that since it's going to be random
955
+
956
+ # Test key count
957
+ index_clause = @twitter.create_index_clause([
958
+ @twitter.create_index_expression('tags', 'c', '==')
959
+ ], "", 2)
960
+ rows = @twitter.get_indexed_slices(:Statuses, index_clause)
961
+ assert_equal 2, rows.length
962
+ # <- can't test which keys are present since it's going to be random
963
+ end
739
964
 
740
- expressions = [{:column_name => 'x', :value => [0,20].pack("NN"), :comparison => "=="}]
965
+ def test_create_index_clause
966
+ return if self.is_a?(CassandraMockTest)
741
967
 
742
- # verify multiples will be returned
743
- assert_equal 9, @twitter.get_indexed_slices(:Statuses, expressions).length
968
+ ie = CassandraThrift::IndexExpression.new(
969
+ :column_name => 'foo',
970
+ :value => 'x',
971
+ :op => '=='
972
+ )
744
973
 
745
- # verify that GT and LT queries perform properly
746
- expressions = [
747
- {:column_name => 'x', :value => [0,20].pack("NN"), :comparison => "=="},
748
- {:column_name => 'non_indexed', :value => [5].pack("N*"), :comparison => ">"}
749
- ]
750
- assert_equal(5, @twitter.get_indexed_slices(:Statuses, expressions).length)
974
+ ic = @twitter.create_index_clause([ie], 'aaa', 250)
975
+ assert_instance_of CassandraThrift::IndexClause, ic
976
+ assert_equal 'aaa', ic.start_key
977
+ assert_equal ie, ic.expressions[0]
978
+ assert_equal 250, ic.count
979
+
980
+ # test alias
981
+ ic = @twitter.create_idx_clause([ie], 'aaa', 250)
982
+ assert_instance_of CassandraThrift::IndexClause, ic
983
+ assert_equal 'aaa', ic.start_key
984
+ assert_equal ie, ic.expressions[0]
985
+ assert_equal 250, ic.count
751
986
  end
752
987
 
753
- def test_old_get_indexed_slices
754
- @twitter.create_index('Twitter', 'Statuses', 'x', 'LongType')
988
+ def test_create_index_expression
989
+ return if self.is_a?(CassandraMockTest)
755
990
 
756
- @twitter.insert(:Statuses, 'row1', { 'x' => [0,10].pack("NN") })
757
-
758
- (2..10).to_a.each do |i|
759
- @twitter.insert(:Statuses, 'row' + i.to_s, { 'x' => [0,20].pack("NN"), 'non_indexed' => [i].pack('N*') })
991
+ # EQ operator
992
+ [nil, "EQ", "eq", "=="].each do |op|
993
+ ie = @twitter.create_index_expression('foo', 'x', op)
994
+ assert_instance_of CassandraThrift::IndexExpression, ie
995
+ assert_equal 'foo', ie.column_name
996
+ assert_equal 'x', ie.value
997
+ assert_equal CassandraThrift::IndexOperator::EQ, ie.op
998
+ end
999
+ # alias
1000
+ [nil, "EQ", "eq", "=="].each do |op|
1001
+ ie = @twitter.create_idx_expr('foo', 'x', op)
1002
+ assert_instance_of CassandraThrift::IndexExpression, ie
1003
+ assert_equal 'foo', ie.column_name
1004
+ assert_equal 'x', ie.value
1005
+ assert_equal CassandraThrift::IndexOperator::EQ, ie.op
760
1006
  end
761
1007
 
762
- @twitter.insert(:Statuses, 'row11', { 'x' => [0,30].pack("NN") })
1008
+ # GTE operator
1009
+ ["GTE", "gte", ">="].each do |op|
1010
+ ie = @twitter.create_index_expression('foo', 'x', op)
1011
+ assert_instance_of CassandraThrift::IndexExpression, ie
1012
+ assert_equal 'foo', ie.column_name
1013
+ assert_equal 'x', ie.value
1014
+ assert_equal CassandraThrift::IndexOperator::GTE, ie.op
1015
+ end
1016
+ # alias
1017
+ ["GTE", "gte", ">="].each do |op|
1018
+ ie = @twitter.create_idx_expr('foo', 'x', op)
1019
+ assert_instance_of CassandraThrift::IndexExpression, ie
1020
+ assert_equal 'foo', ie.column_name
1021
+ assert_equal 'x', ie.value
1022
+ assert_equal CassandraThrift::IndexOperator::GTE, ie.op
1023
+ end
763
1024
 
764
- idx_expr = @twitter.create_idx_expr('x', [0,20].pack("NN"), "==")
1025
+ # GT operator
1026
+ ["GT", "gt", ">"].each do |op|
1027
+ ie = @twitter.create_index_expression('foo', 'x', op)
1028
+ assert_instance_of CassandraThrift::IndexExpression, ie
1029
+ assert_equal 'foo', ie.column_name
1030
+ assert_equal 'x', ie.value
1031
+ assert_equal CassandraThrift::IndexOperator::GT, ie.op
1032
+ end
1033
+ # alias
1034
+ ["GT", "gt", ">"].each do |op|
1035
+ ie = @twitter.create_idx_expr('foo', 'x', op)
1036
+ assert_instance_of CassandraThrift::IndexExpression, ie
1037
+ assert_equal 'foo', ie.column_name
1038
+ assert_equal 'x', ie.value
1039
+ assert_equal CassandraThrift::IndexOperator::GT, ie.op
1040
+ end
765
1041
 
766
- # verify count is observed
767
- idx_clause = @twitter.create_idx_clause([idx_expr], "", 1)
768
- assert_equal 1, @twitter.get_indexed_slices(:Statuses, idx_clause).length
1042
+ # LTE operator
1043
+ ["LTE", "lte", "<="].each do |op|
1044
+ ie = @twitter.create_index_expression('foo', 'x', op)
1045
+ assert_instance_of CassandraThrift::IndexExpression, ie
1046
+ assert_equal 'foo', ie.column_name
1047
+ assert_equal 'x', ie.value
1048
+ assert_equal CassandraThrift::IndexOperator::LTE, ie.op
1049
+ end
1050
+ # alias
1051
+ ["LTE", "lte", "<="].each do |op|
1052
+ ie = @twitter.create_idx_expr('foo', 'x', op)
1053
+ assert_instance_of CassandraThrift::IndexExpression, ie
1054
+ assert_equal 'foo', ie.column_name
1055
+ assert_equal 'x', ie.value
1056
+ assert_equal CassandraThrift::IndexOperator::LTE, ie.op
1057
+ end
769
1058
 
770
- # verify multiples will be returned
771
- idx_clause = @twitter.create_idx_clause([idx_expr])
772
- assert_equal 9, @twitter.get_indexed_slices(:Statuses, idx_clause).length
1059
+ # LT operator
1060
+ ["LT", "lt", "<"].each do |op|
1061
+ ie = @twitter.create_index_expression('foo', 'x', op)
1062
+ assert_instance_of CassandraThrift::IndexExpression, ie
1063
+ assert_equal 'foo', ie.column_name
1064
+ assert_equal 'x', ie.value
1065
+ assert_equal CassandraThrift::IndexOperator::LT, ie.op
1066
+ end
1067
+ # alias
1068
+ ["LT", "lt", "<"].each do |op|
1069
+ ie = @twitter.create_idx_expr('foo', 'x', op)
1070
+ assert_instance_of CassandraThrift::IndexExpression, ie
1071
+ assert_equal 'foo', ie.column_name
1072
+ assert_equal 'x', ie.value
1073
+ assert_equal CassandraThrift::IndexOperator::LT, ie.op
1074
+ end
773
1075
 
774
- # verify that GT and LT queries perform properly
775
- idx_expr = [
776
- @twitter.create_idx_expr('x', [0,20].pack("NN"), "=="),
777
- @twitter.create_idx_expr('non_indexed', [5].pack("N*"), ">")
778
- ]
779
- idx_clause = @twitter.create_idx_clause(idx_expr)
780
- assert_equal(5, @twitter.get_indexed_slices(:Statuses, idx_clause).length)
1076
+ # unknown operator
1077
+ ie = @twitter.create_index_expression('foo', 'x', '~$')
1078
+ assert_equal nil, ie.op
1079
+ # alias
1080
+ ie = @twitter.create_idx_expr('foo', 'x', '~$')
1081
+ assert_equal nil, ie.op
781
1082
  end
782
1083
 
783
1084
  def test_column_family_mutation
@@ -843,7 +1144,7 @@ class CassandraTest < Test::Unit::TestCase
843
1144
  assert_equal(1, @twitter.get(:UserCounterAggregates, 'bob', 'DAU', 'today'))
844
1145
  assert_equal(2, @twitter.get(:UserCounterAggregates, 'bob', 'DAU', 'tomorrow'))
845
1146
  end
846
-
1147
+
847
1148
  def test_reading_rows_with_super_column_counter
848
1149
  assert_nil @twitter.add(:UserCounterAggregates, 'bob', 1, 'DAU', 'today')
849
1150
  assert_nil @twitter.add(:UserCounterAggregates, 'bob', 2, 'DAU', 'tomorrow')
@@ -853,8 +1154,8 @@ class CassandraTest < Test::Unit::TestCase
853
1154
  assert_equal("DAU", result.first[0])
854
1155
  assert_equal(1, result.first[1]["today"])
855
1156
  assert_equal(2, result.first[1]["tomorrow"])
856
- end
857
-
1157
+ end
1158
+
858
1159
  def test_composite_column_type_conversion
859
1160
  columns = {}
860
1161
  @composites.each_with_index do |c, index|
@@ -893,6 +1194,46 @@ class CassandraTest < Test::Unit::TestCase
893
1194
 
894
1195
  assert_equal('value-2', @type_conversions.get(:CompositeColumnConversion, key, columns_in_order.first))
895
1196
  end
1197
+
1198
+ def test_dynamic_composite_column_type_conversion
1199
+ columns = {}
1200
+ @dynamic_composites.each_with_index do |c, index|
1201
+ columns[c] = "value-#{index}"
1202
+ end
1203
+ @type_conversions.insert(:DynamicComposite, key, columns)
1204
+
1205
+ columns_in_order = [
1206
+ Cassandra::DynamicComposite.new(['IntegerType', [1].pack('N')], ['s', "elephant"]),
1207
+ Cassandra::DynamicComposite.new(['i', [5].pack('N')], ['UTF8Type', "aardvark"]),
1208
+ Cassandra::DynamicComposite.new(['i', [5].pack('N')], ['UTF8Type', "zebra"]),
1209
+ Cassandra::DynamicComposite.new(['IntegerType', [10].pack('N')], ['s', "kangaroo"]),
1210
+ ]
1211
+ assert_equal(columns_in_order, @type_conversions.get(:DynamicComposite, key).keys)
1212
+
1213
+ column_slice = @type_conversions.get(:DynamicComposite, key,
1214
+ :start => Cassandra::DynamicComposite.new(['i', [1].pack('N')]),
1215
+ :finish => Cassandra::DynamicComposite.new(['i', [10].pack('N')]),
1216
+ ).keys
1217
+ assert_equal(columns_in_order[0..-2], column_slice)
1218
+
1219
+ column_slice = @type_conversions.get(:DynamicComposite, key,
1220
+ :start => Cassandra::DynamicComposite.new(['IntegerType', [5].pack('N')]),
1221
+ :finish => Cassandra::DynamicComposite.new(['IntegerType', [5].pack('N')], :slice => :after),
1222
+ ).keys
1223
+ assert_equal(columns_in_order[1..2], column_slice)
1224
+
1225
+ column_slice = @type_conversions.get(:DynamicComposite, key,
1226
+ :start => Cassandra::DynamicComposite.new(['i', [5].pack('N')], :slice => :after).to_s,
1227
+ ).keys
1228
+ assert_equal([columns_in_order[-1]], column_slice)
1229
+
1230
+ column_slice = @type_conversions.get(:DynamicComposite, key,
1231
+ :finish => Cassandra::DynamicComposite.new(['i', [10].pack('N')], :slice => :before).to_s,
1232
+ ).keys
1233
+ assert_equal(columns_in_order[0..-2], column_slice)
1234
+
1235
+ assert_equal('value-2', @type_conversions.get(:DynamicComposite, key, columns_in_order.first))
1236
+ end
896
1237
  end
897
1238
 
898
1239
  def test_column_timestamps