Skip to content

Commit 3177114

Browse files
authored
RUBY-332: Add support for duration type (#262)
1 parent f72ac3e commit 3177114

File tree

14 files changed

+472
-61
lines changed

14 files changed

+472
-61
lines changed

Rakefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ end
4343

4444
Rake::TestTask.new(integration: :compile) do |t|
4545
t.libs.push 'lib'
46+
t.libs.push 'support'
47+
t.libs.push 'integration'
4648
t.test_files = FileList['integration/*_test.rb',
4749
'integration/security/*_test.rb',
4850
'integration/load_balancing/*_test.rb',

integration/integration_test_case.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
# limitations under the License.
1717
#++
1818

19-
require File.dirname(__FILE__) + '/../support/ccm.rb'
20-
require File.dirname(__FILE__) + '/../support/retry.rb'
21-
require File.dirname(__FILE__) + '/schema_change_listener.rb'
22-
require 'minitest/unit'
19+
require 'ccm.rb'
20+
require 'retry.rb'
21+
require 'schema_change_listener.rb'
2322
require 'minitest/autorun'
23+
require 'minitest/unit'
2424
require 'cassandra'
2525
require 'delorean'
2626
require 'ansi/code'

integration/types/duration_test.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# encoding: utf-8
2+
3+
#--
4+
# Copyright DataStax, Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#++
18+
19+
require 'integration_test_case'
20+
21+
class DurationTest < IntegrationTestCase
22+
23+
def setup
24+
@@ccm_cluster.setup_schema("CREATE KEYSPACE foo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}")
25+
end
26+
27+
def test_can_insert_duration
28+
skip("Duration type was added in DSE 5.1/C* 3.10") if (CCM.dse_version < '5.1' || CCM.cassandra_version < '3.10')
29+
30+
cluster = Cassandra.cluster
31+
session = cluster.connect "foo"
32+
33+
durations = {
34+
'week' => Cassandra::Types.duration.new(0,7,0),
35+
'year' => Cassandra::Types.duration.new(12,0,0),
36+
'day' => Cassandra::Types.duration.new(0,1,0)
37+
}
38+
session.execute 'CREATE TABLE bar ("name" varchar, "dur" duration, primary key ("name"))'
39+
40+
insert = Retry.with_attempts(5) { session.prepare "INSERT INTO foo.bar (name,dur) VALUES (?,?)" }
41+
durations.each_pair do |key,val|
42+
Retry.with_attempts(5) { session.execute insert, arguments: [key,val] }
43+
end
44+
45+
result = session.execute("SELECT * FROM foo.bar")
46+
result.each do |row|
47+
duration = row["dur"]
48+
duration_name = row["name"]
49+
assert_equal durations[duration_name], duration
50+
end
51+
ensure
52+
cluster && cluster.close
53+
end
54+
end

lib/cassandra.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,8 @@ def self.validate_and_massage_options(options)
777777
require 'cassandra/time'
778778

779779
require 'cassandra/types'
780+
require 'cassandra/custom_data'
781+
require 'cassandra/duration'
780782

781783
require 'cassandra/errors'
782784
require 'cassandra/compression'
@@ -788,7 +790,6 @@ def self.validate_and_massage_options(options)
788790
require 'cassandra/executors'
789791
require 'cassandra/future'
790792
require 'cassandra/cluster'
791-
require 'cassandra/custom_data'
792793
require 'cassandra/driver'
793794
require 'cassandra/host'
794795
require 'cassandra/session'

lib/cassandra/cluster/schema/cql_type_parser.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def lookup_type(node, types)
6060
when 'smallint' then Cassandra::Types.smallint
6161
when 'time' then Cassandra::Types.time
6262
when 'tinyint' then Cassandra::Types.tinyint
63+
when 'duration' then Cassandra::Types.duration
6364
when 'map' then
6465
Cassandra::Types.map(*node.children.map { |t| lookup_type(t, types)})
6566
when 'set' then

lib/cassandra/duration.rb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# encoding: utf-8
2+
3+
#--
4+
# Copyright DataStax, Inc.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#++
18+
module Cassandra
19+
module Types
20+
21+
class Duration < Type
22+
include CustomData
23+
24+
@@four_byte_max = 2 ** 32
25+
@@eight_byte_max = 2 ** 64
26+
27+
# @private
28+
attr_reader :months, :days, :nanos
29+
30+
# @private
31+
def initialize(months, days, nanos)
32+
super(:duration)
33+
@months = months
34+
@days = days
35+
@nanos = nanos
36+
end
37+
38+
def new(*values)
39+
Util.assert_size(3, values, "Duration type expects three values, #{values.size} were provided")
40+
values.each { |v| Util.assert_type(Int, v) }
41+
Util.assert (Util.encode_zigzag32(values[0]) < @@four_byte_max), "Months value must be a valid 32-bit integer"
42+
Util.assert (Util.encode_zigzag32(values[1]) < @@four_byte_max), "Days value must be a valid 32-bit integer"
43+
Util.assert (Util.encode_zigzag64(values[2]) < @@eight_byte_max), "Nanos value must be a valid 64-bit integer"
44+
all_positive = values.all? {|i| i >= 0 }
45+
all_negative = values.all? {|i| i <= 0 }
46+
Util.assert (all_positive or all_negative), "Values in a duration must be uniformly positive or negative"
47+
Duration.new *values
48+
end
49+
50+
def assert(value, message = nil, &block)
51+
Util.assert_instance_of(Duration, value, message, &block)
52+
end
53+
54+
def to_s
55+
"Duration: months => #{@months}, days => #{@days}, nanos => #{@nanos}"
56+
end
57+
58+
def hash
59+
@hash ||= begin
60+
h = 17
61+
h = 31 * h + @months.hash
62+
h = 31 * h + @days.hash
63+
h = 31 * h + @nanos.hash
64+
h
65+
end
66+
end
67+
68+
def eql?(other)
69+
other.is_a?(Duration) &&
70+
@months == other.months &&
71+
@days == other.days &&
72+
@nanos == other.nanos
73+
end
74+
75+
alias == eql?
76+
77+
def self.cql_type
78+
Type.new(@kind)
79+
end
80+
81+
# Requirements for CustomData module
82+
def self.deserialize(bytestr)
83+
buffer = Cassandra::Protocol::CqlByteBuffer.new.append(bytestr)
84+
Cassandra::Types::Duration.new(buffer.read_signed_vint,buffer.read_signed_vint,buffer.read_signed_vint)
85+
end
86+
87+
def self.type
88+
Cassandra::Types::Custom.new('org.apache.cassandra.db.marshal.DurationType')
89+
end
90+
91+
def serialize
92+
rv = Cassandra::Protocol::CqlByteBuffer.new
93+
rv.append_signed_vint32(@months)
94+
rv.append_signed_vint32(@days)
95+
rv.append_signed_vint64(@nanos)
96+
rv
97+
end
98+
end
99+
end
100+
end

lib/cassandra/protocol/cql_byte_buffer.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,29 @@ def read_tinyint
287287
"Not enough bytes available to decode a tinyint: #{e.message}", e.backtrace
288288
end
289289

290+
def read_vint
291+
n = read_byte
292+
293+
# Bits are indexed in Integer in little-endian order
294+
bytes_to_read = 7.downto(0).take_while {|i| n[i] == 1}.size
295+
return n unless bytes_to_read > 0
296+
297+
rv = n & (0xff >> bytes_to_read)
298+
1.upto(bytes_to_read) do |idx|
299+
new_byte = read_byte
300+
rv <<= 8
301+
rv |= (new_byte & 0xff)
302+
end
303+
304+
rv
305+
rescue RangeError => e
306+
raise Errors::DecodingError, e.message, e.backtrace
307+
end
308+
309+
def read_signed_vint
310+
Util.decode_zigzag(read_vint)
311+
end
312+
290313
def append_tinyint(n)
291314
append([n].pack(Formats::CHAR_FORMAT))
292315
end
@@ -409,6 +432,25 @@ def append_float(n)
409432
append([n].pack(Formats::FLOAT_FORMAT))
410433
end
411434

435+
def append_vint(n)
436+
send_bytes = Util.to_min_byte_array(n)
437+
send_cnt = send_bytes.length
438+
439+
raise Errors::EncodingError, "Too many bytes (#{bytes_to_send.length}) to send!" if send_cnt > 8
440+
441+
send_cnt_byte = (0xff << (8 - send_cnt)) & 0xff
442+
append([send_cnt_byte].pack(Formats::BYTES_FORMAT))
443+
append(send_bytes.pack(Formats::BYTES_FORMAT))
444+
end
445+
446+
def append_signed_vint32(n)
447+
append_vint(Util.encode_zigzag32(n))
448+
end
449+
450+
def append_signed_vint64(n)
451+
append_vint(Util.encode_zigzag64(n))
452+
end
453+
412454
def eql?(other)
413455
other.eql?(to_str)
414456
end

lib/cassandra/protocol/v4.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def write_parameters(buffer, params, types, names = EMPTY_LIST)
6161

6262
class Decoder
6363
def initialize(handler, compressor = nil, custom_type_handlers = {})
64+
# In v4 the duration type is represented as a custom type so we always want to have
65+
# a handler included here. This handler can be overridden via the connection options.
66+
custom_type_handlers[Cassandra::Types::Duration.type] ||= Cassandra::Types::Duration
6467
@handler = handler
6568
@compressor = compressor
6669
@state = :initial

lib/cassandra/types.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,5 +1700,9 @@ def udt(keyspace, name, *fields)
17001700
def custom(name)
17011701
Custom.new(name)
17021702
end
1703+
1704+
def duration
1705+
Duration.new 0,0,0
1706+
end
17031707
end
17041708
end

lib/cassandra/util.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,26 @@ def assert_equal(expected, actual, message = nil, &block)
302302
end
303303
end
304304

305+
def to_byte_array(n)
306+
[n].pack("Q>").unpack("C*")
307+
end
308+
309+
def to_min_byte_array(n)
310+
to_byte_array(n).drop_while {|i| i == 0}
311+
end
312+
313+
def decode_zigzag(n)
314+
(n >> 1) ^ -(n & 1)
315+
end
316+
317+
def encode_zigzag32(n)
318+
(n >> 31) ^ (n << 1)
319+
end
320+
321+
def encode_zigzag64(n)
322+
(n >> 63) ^ (n << 1)
323+
end
324+
305325
# @private
306326
LOWERCASE_REGEXP = /[[:lower:]\_]*/
307327
# @private

0 commit comments

Comments
 (0)