cassandra 0.13.0 → 0.14.0

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