File Coverage

blib/lib/Protocol/CassandraCQL/Type.pm
Criterion Covered Total %
statement 229 238 96.2
branch 42 54 77.7
condition 4 4 100.0
subroutine 79 84 94.0
pod 2 4 50.0
total 356 384 92.7


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2013 -- leonerd@leonerd.org.uk
5              
6             package Protocol::CassandraCQL::Type;
7              
8 6     6   70100 use strict;
  6         22  
  6         244  
9 6     6   28 use warnings;
  6         10  
  6         297  
10              
11             our $VERSION = '0.12';
12              
13 6     6   28 use Carp;
  6         8  
  6         354  
14              
15 6     6   761 use Encode ();
  6         10095  
  6         130  
16              
17 6     6   361 use Protocol::CassandraCQL qw( :types );
  6         6  
  6         1207  
18 6     6   477 use Protocol::CassandraCQL::Frame; # collection types use it for encoding/decoding
  6         9  
  6         292  
19              
20 6     6   28 use constant HAVE_INT64 => eval { pack( "q>", 1 ) eq "\0\0\0\0\0\0\0\1" };
  6         9  
  6         29  
  6         2454  
21              
22             =head1 NAME
23              
24             C - represents a Cassandra CQL data type
25              
26             =head1 DESCRIPTION
27              
28             Objects in this class represent distinct types that may be found in Cassandra
29             CQL3, either as columns in query result rows, or as bind parameters to
30             prepared statements. It is used by L.
31              
32             =cut
33              
34             =head1 CONSTRUCTOR
35              
36             =head2 $type = Protocol::CassandraCQL::Type->from_frame( $frame )
37              
38             Returns a new type object initialised by parsing the type information in the
39             given message frame.
40              
41             =cut
42              
43             sub from_frame
44             {
45 26     26 1 30 shift; # ignore
46 26         30 my ( $frame ) = @_;
47              
48 26         60 my $typeid = $frame->unpack_short;
49 26         70 my $class = "Protocol::CassandraCQL::Type::" . Protocol::CassandraCQL::typename( $typeid );
50              
51 26 100       373 if( $class->can( "from_frame" ) != \&from_frame ) {
    50          
52 3         10 return $class->from_frame( @_ );
53             }
54             elsif( $class->can( "new" ) ) {
55 23         57 return $class->new;
56             }
57              
58 0         0 die "TODO: Unrecognised typeid $typeid";
59             }
60              
61             # Just for unit testing
62             # This and the LIST/MAP from_name methods form a simple incremental parser
63             sub from_name
64             {
65 95     95 0 9586 shift;
66              
67 95         357 $_[0] =~ s/^([^<,>]+)//;
68 95         179 my $name = $1;
69 95         134 my $class = "Protocol::CassandraCQL::Type::$name";
70              
71 95 100       1100 if( $class->can( "from_name" ) != \&from_name ) {
    50          
72 14         31 return $class->from_name( @_ );
73             }
74             elsif( $class->can( "new" ) ) {
75 81         206 return $class->new;
76             }
77              
78 0         0 die "Unrecognised type name '$name'";
79             }
80              
81             sub new
82             {
83 104     104 0 119 my $class = shift;
84 104         414 return bless [], $class;
85             }
86              
87             =head1 METHODS
88              
89             =cut
90              
91             =head2 $name = $type->name
92              
93             Returns a string representation of the type name.
94              
95             =cut
96              
97             sub name
98             {
99 19     19 1 30 my $self = shift;
100 19         241 return +( ( ref $self ) =~ m/::([^:]+)$/ )[0];
101             }
102              
103             =head2 $bytes = $type->encode( $v )
104              
105             Encodes the given perl data into a bytestring.
106              
107             =head2 $v = $type->decode( $bytes )
108              
109             Decodes the given bytestring into perl data.
110              
111             =cut
112              
113             =head2 $message = $type->validate( $v )
114              
115             Validates whether the given perl data is valid for this type. If so, returns
116             false. Otherwise, returns an error message explaining why.
117              
118             =cut
119              
120             # if( $typeid == TYPE_CUSTOM ) {
121             # push @col, $frame->unpack_string;
122             # }
123              
124             # my ( $typeid, $custom ) = @{ $self->{columns}[$idx] }[4,5];
125             # return $custom if $typeid == TYPE_CUSTOM;
126              
127             # Now the codecs
128              
129             package
130             Protocol::CassandraCQL::Type::_numeric;
131 6     6   43 use base qw( Protocol::CassandraCQL::Type );
  6         11  
  6         653  
132 6     6   46 use Scalar::Util qw( looks_like_number );
  6         9  
  6         765  
133 24 100   24   258 sub validate { !looks_like_number($_[1]) ? "not a number" : undef }
134              
135             package
136             Protocol::CassandraCQL::Type::_integral;
137 6     6   28 use base qw( Protocol::CassandraCQL::Type::_numeric );
  6         30  
  6         3516  
138 22 100   22   78 sub validate { $_[0]->SUPER::validate($_[1]) or
    100          
139             $_[1] != int($_[1]) ? "not an integer" : undef }
140              
141             # ASCII-only bytes
142             package Protocol::CassandraCQL::Type::ASCII;
143 6     6   39 use base qw( Protocol::CassandraCQL::Type );
  6         8  
  6         812  
144 2 100   2   24 sub validate { $_[1] =~ m/[^\x00-\x7f]/ ? "non-ASCII" : undef }
145 1     1   8 sub encode { $_[1] }
146 1     1   10 sub decode { $_[1] }
147              
148             # 64-bit integer
149             package Protocol::CassandraCQL::Type::BIGINT;
150 6     6   26 use base qw( Protocol::CassandraCQL::Type::_integral );
  6         7  
  6         2315  
151             if( Protocol::CassandraCQL::Type::HAVE_INT64 ) {
152 2     2   18 *encode = sub { pack "q>", $_[1] };
153 2     2   24 *decode = sub { unpack "q>", $_[1] };
154             }
155             else {
156             require Math::Int64;
157             *encode = sub { Math::Int64::int64_to_net( $_[1] ) };
158             *decode = sub { Math::Int64::net_to_int64( $_[1] ) };
159             }
160              
161             # blob
162             package Protocol::CassandraCQL::Type::BLOB;
163 6     6   29 use base qw( Protocol::CassandraCQL::Type );
  6         7  
  6         684  
164 0     0   0 sub validate { undef }
165 1     1   7 sub encode { $_[1] }
166 1     1   5 sub decode { $_[1] }
167              
168             # true/false byte
169             package Protocol::CassandraCQL::Type::BOOLEAN;
170 6     6   28 use base qw( Protocol::CassandraCQL::Type );
  6         6  
  6         806  
171 0     0   0 sub validate { undef }
172 2     2   15 sub encode { pack "C", !!$_[1] }
173 2     2   15 sub decode { !!unpack "C", $_[1] }
174              
175             # counter is a 64-bit integer
176             package Protocol::CassandraCQL::Type::COUNTER;
177 6     6   30 use base qw( Protocol::CassandraCQL::Type::BIGINT );
  6         8  
  6         1914  
178              
179             # Not clearly docmuented, but this appears to be an INT decimal shift followed
180             # by a VARINT
181             package Protocol::CassandraCQL::Type::DECIMAL;
182 6     6   30 use base qw( Protocol::CassandraCQL::Type::_numeric );
  6         8  
  6         1673  
183 6     6   32 use Scalar::Util qw( blessed );
  6         8  
  6         1589  
184             sub encode {
185 3     3   1116 require Math::BigFloat;
186 3 100       17363 my $shift = $_[1] =~ m/\.(\d*)$/ ? length $1 : 0;
187 3 50       23 my $n = blessed $_[1] ? $_[1] : Math::BigFloat->new( $_[1] );
188 3         1223 return pack( "L>", $shift ) . Protocol::CassandraCQL::Type::VARINT->encode( $n->blsft($shift, 10) );
189             }
190             sub decode {
191 3     3   16 require Math::BigFloat;
192 3         10 my $shift = unpack "L>", $_[1];
193 3         14 my $n = Protocol::CassandraCQL::Type::VARINT->decode( substr $_[1], 4 );
194 3         291 return scalar Math::BigFloat->new($n)->brsft($shift, 10);
195             }
196              
197             # IEEE double
198             package Protocol::CassandraCQL::Type::DOUBLE;
199 6     6   37 use base qw( Protocol::CassandraCQL::Type::_numeric );
  6         10  
  6         2079  
200 1     1   9 sub encode { pack "d>", $_[1] }
201 1     1   10 sub decode { unpack "d>", $_[1] }
202              
203             # IEEE single
204             package Protocol::CassandraCQL::Type::FLOAT;
205 6     6   35 use base qw( Protocol::CassandraCQL::Type::_numeric );
  6         10  
  6         1944  
206 1     1   6 sub encode { pack "f>", $_[1] }
207 1     1   8 sub decode { unpack "f>", $_[1] }
208              
209             # 32-bit integer
210             package Protocol::CassandraCQL::Type::INT;
211 6     6   34 use base qw( Protocol::CassandraCQL::Type::_integral );
  6         11  
  6         2089  
212 13     13   54 sub encode { pack "l>", $_[1] }
213 15     15   79 sub decode { unpack "l>", $_[1] }
214              
215             # UTF-8 text
216             package Protocol::CassandraCQL::Type::VARCHAR;
217 6     6   32 use base qw( Protocol::CassandraCQL::Type );
  6         11  
  6         807  
218 8     8   37 sub validate { undef } # TODO: maybe we can check for invalid codepoints?
219 13     13   42 sub encode { Encode::encode_utf8 $_[1] }
220 17     17   49 sub decode { Encode::decode_utf8 $_[1] }
221              
222             # 'text' seems to come back as 'varchar'
223             package Protocol::CassandraCQL::Type::TEXT;
224 6     6   29 use base qw( Protocol::CassandraCQL::Type::VARCHAR );
  6         9  
  6         1806  
225              
226             # miliseconds since UNIX epoch as 64bit uint
227             package Protocol::CassandraCQL::Type::TIMESTAMP;
228 6     6   28 use base qw( Protocol::CassandraCQL::Type::_integral );
  6         8  
  6         2664  
229             if( Protocol::CassandraCQL::Type::HAVE_INT64 ) {
230 1     1   8 *encode = sub { pack "Q>", ($_[1] * 1000) };
231 1     1   9 *decode = sub { (unpack "Q>", $_[1]) / 1000 };
232             }
233             else {
234             require Math::Int64;
235             *encode = sub { Math::Int64::uint64_to_net( $_[1] * 1000 ) };
236             *decode = sub { (Math::Int64::net_to_uint64( $_[1] )) / 1000 };
237             }
238              
239             # UUID is just a hex string - accept 32 hex digits, hypens optional
240             package Protocol::CassandraCQL::Type::UUID;
241 6     6   37 use base qw( Protocol::CassandraCQL::Type );
  6         9  
  6         1245  
242 2     2   7 sub validate { ( my $hex = $_[1] ) =~ s/-//g;
243 2 100       16 $hex !~ m/^[0-9A-F]{32}$/i ? "expected 32 hex digits" : undef }
244 1     1   5 sub encode { ( my $hex = $_[1] ) =~ s/-//g; pack "H32", $hex }
  1         8  
245 1     1   8 sub decode { join "-", unpack "H8 H4 H4 H4 H12", $_[1] }
246              
247             package Protocol::CassandraCQL::Type::TIMEUUID;
248 6     6   37 use base qw( Protocol::CassandraCQL::Type::UUID );
  6         9  
  6         2012  
249              
250             # Arbitrary-precision 2s-complement signed integer
251             # Math::BigInt doesn't handle signed, but we can mangle it
252             package Protocol::CassandraCQL::Type::VARINT;
253 6     6   33 use base qw( Protocol::CassandraCQL::Type::_integral );
  6         9  
  6         1724  
254 6     6   31 use Scalar::Util qw( blessed );
  6         6  
  6         1678  
255             sub encode {
256 11     11   1279 require Math::BigInt;
257 11 100       69 my $n = blessed $_[1] ? $_[1] : Math::BigInt->new($_[1]); # upgrade to a BigInt
258              
259 11         307 my $bytes;
260 11 100       39 if( $n < 0 ) {
261 3         296 my $hex = substr +(-$n-1)->as_hex, 2;
262 3 100       599 $hex = "0$hex" if length($hex) % 2;
263 3         13 $bytes = ~(pack "H*", $hex);
264             # Sign-extend if required to avoid appearing positive
265 3 100       21 $bytes = "\xff$bytes" if unpack( "C", $bytes ) < 0x80;
266             }
267             else {
268 8         1102 my $hex = substr $n->as_hex, 2; # trim 0x
269 8 100       440 $hex = "0$hex" if length($hex) % 2;
270 8         33 $bytes = pack "H*", $hex;
271             # Zero-extend if required to avoid appearing negative
272 8 100       34 $bytes = "\0$bytes" if unpack( "C", $bytes ) >= 0x80;
273             }
274 11         59 $bytes;
275             }
276             sub decode {
277 11     11   42 require Math::BigInt;
278              
279 11 100       30 if( unpack( "C", $_[1] ) >= 0x80 ) {
280 3         18 return -Math::BigInt->from_hex( "0x" . unpack "H*", ~$_[1] ) - 1;
281             }
282             else {
283 8         41 return Math::BigInt->from_hex( "0x" . unpack "H*", $_[1] );
284             }
285             }
286              
287             # 4 (AF_INET) or 16 (AF_INET6) byte address
288             package Protocol::CassandraCQL::Type::INET;
289 6     6   28 use base qw( Protocol::CassandraCQL::Type );
  6         8  
  6         962  
290 0 0   0   0 sub validate { length($_[1]) == 4 and return;
291 0 0       0 length($_[1]) == 16 and return;
292 0         0 "expected 4 bytes (AF_INET) or 16 bytes (AF_INET6)" }
293 0     0   0 sub encode { $_[1] }
294 0     0   0 sub decode { $_[1] }
295              
296             =head1 COLLECTION TYPES
297              
298             =head2 $etype = $type->element_type
299              
300             Returns the type of the elements in the list or set, for C and C
301             types.
302              
303             =head2 $ktype = $type->key_type
304              
305             =head2 $vtype = $type->value_type
306              
307             Returns the type of the keys and values in the map, for C types.
308              
309             =cut
310              
311             package Protocol::CassandraCQL::Type::LIST;
312 6     6   32 use base qw( Protocol::CassandraCQL::Type );
  6         8  
  6         3062  
313             sub from_frame {
314 2     2   4 my $class = shift;
315 2         8 my $etype = Protocol::CassandraCQL::Type->from_frame( @_ );
316 2         9 bless [ $etype ], $class;
317             }
318             sub from_name {
319 7     7   9 my $class = shift;
320 7 50       27 $_[0] =~ s/^
321 7         18 my $etype = Protocol::CassandraCQL::Type->from_name( @_ );
322 7 50       27 $_[0] =~ s/^>// or die "Expected '>' following collection element type\n";
323 7         38 bless [ $etype ], $class;
324             }
325 2     2   14 sub element_type { $_[0][0] }
326 2     2   11 sub name { $_[0]->SUPER::name . "<" . $_[0][0]->name . ">" }
327             sub validate {
328 6     6   10 my $l = $_[1];
329 6 100       9 eval { @$l } or return "not an ARRAY";
  6         36  
330 5         9 my $etype = $_[0][0];
331 5   100     7 my $e; $e = $etype->validate( $l->[$_] ) and return "[$_]: $e" for 0 .. $#$l;
  5         30  
332 3         16 undef;
333             }
334             sub encode {
335 4     4   9 my $l = $_[1];
336 4         20 my $etype = $_[0][0];
337 4         30 my $f = Protocol::CassandraCQL::Frame->new
338             ->pack_short( scalar @$l );
339 4         13 foreach my $i ( 0 .. $#$l ) {
340 12         30 $f->pack_short_bytes( $etype->encode( $l->[$i] ) );
341             }
342             $f->bytes
343 4         17 }
344             sub decode {
345 2     2   2 local $_;
346 2         5 my $etype = $_[0][0];
347 2         8 my $f = Protocol::CassandraCQL::Frame->new( $_[1] );
348 2         8 my $n = $f->unpack_short;
349 2         6 return [ map { $etype->decode( $f->unpack_short_bytes ) } 1 .. $n ]
  6         42  
350             }
351              
352             package Protocol::CassandraCQL::Type::MAP;
353 6     6   34 use base qw( Protocol::CassandraCQL::Type );
  6         57  
  6         3830  
354             sub from_frame {
355 1     1   2 my $class = shift;
356 1         4 my $ktype = Protocol::CassandraCQL::Type->from_frame( @_ );
357 1         5 my $vtype = Protocol::CassandraCQL::Type->from_frame( @_ );
358 1         5 bless [ $ktype, $vtype ], $class;
359             }
360             sub from_name {
361 7     7   9 my $class = shift;
362 7 50       28 $_[0] =~ s/^
363 7         17 my $ktype = Protocol::CassandraCQL::Type->from_name( @_ );
364 7 50       28 $_[0] =~ s/^,// or die "Expected ',' following collection key type\n";
365 7         15 my $vtype = Protocol::CassandraCQL::Type->from_name( @_ );
366 7 50       26 $_[0] =~ s/^>// or die "Expected '>' following collection value type\n";
367 7         29 bless [ $ktype, $vtype ], $class;
368             }
369 1     1   6 sub key_type { $_[0][0] }
370 1     1   6 sub value_type { $_[0][1] }
371 1     1   10 sub name { $_[0]->SUPER::name . "<" . $_[0][0]->name . "," . $_[0][1]->name . ">" }
372             sub validate {
373 4     4   7 my $m = $_[1];
374 4 100       6 eval { %$m } or return "not a HASH";
  4         30  
375 3         12 my $vtype = $_[0][1];
376 3   100     5 my $e; $e = $vtype->validate( $m->{$_} ) and return "{$_}: $e" for keys %$m;
  3         20  
377 2         13 undef;
378             }
379             sub encode {
380 3     3   6 my $m = $_[1];
381 3         10 my $ktype = $_[0][0];
382 3         7 my $vtype = $_[0][1];
383 3         12 my $f = Protocol::CassandraCQL::Frame->new
384             ->pack_short( scalar keys %$m );
385 3         11 foreach my $k ( keys %$m ) {
386 5         10 $f->pack_short_bytes( $ktype->encode( $k ) );
387 5         20 $f->pack_short_bytes( $vtype->encode( $m->{$k} ) );
388             }
389             $f->bytes
390 3         10 }
391             sub decode {
392 2     2   3 local $_;
393 2         5 my $ktype = $_[0][0];
394 2         3 my $vtype = $_[0][1];
395 2         6 my $f = Protocol::CassandraCQL::Frame->new( $_[1] );
396 2         6 my $n = $f->unpack_short;
397 2         5 return { map { $ktype->decode( $f->unpack_short_bytes ),
  4         8  
398             $vtype->decode( $f->unpack_short_bytes ) } 1 .. $n }
399             }
400              
401             # We just represent a SET as a LIST - use an ARRAY of elements
402             package Protocol::CassandraCQL::Type::SET;
403 6     6   36 use base qw( Protocol::CassandraCQL::Type::LIST );
  6         12  
  6         2077  
404              
405             =head1 DATA ENCODINGS
406              
407             The following encodings to and from perl data are supported:
408              
409             =head2 ASCII
410              
411             To or from a string scalar, which must contain only US-ASCII codepoints (i.e.
412             C <= 127).
413              
414             =head2 BIGINT, BOOLEAN, COUNTER, DECIMAL, FLOAT, INT
415              
416             To or from a numeric scalar.
417              
418             =head2 BLOB
419              
420             To or from an opaque string scalar of bytes.
421              
422             =head2 DECIMAL
423              
424             To or from an instance of L, or from a regular numeric scalar.
425              
426             =head2 TIMESTAMP
427              
428             To or from a numeric scalar, representing a UNIX epoch timestamp as a float to
429             the nearest milisecond.
430              
431             =head2 UUID, TIMEUUID
432              
433             To or from a string containing hex digits and hyphens, in the form
434             C.
435              
436             =head2 VARCHAR
437              
438             To or from a string scalar containing Unicode characters.
439              
440             =head2 VARINT
441              
442             To or from an instance of L, or from a regular numeric scalar.
443              
444             =head2 LIST, SET
445              
446             To or from an C reference containing elements.
447              
448             =head2 MAP
449              
450             To or from a C reference, where the keys used must be of some string
451             type.
452              
453             =cut
454              
455             =head1 SPONSORS
456              
457             This code was paid for by
458              
459             =over 2
460              
461             =item *
462              
463             Perceptyx L
464              
465             =item *
466              
467             Shadowcat Systems L
468              
469             =back
470              
471             =head1 AUTHOR
472              
473             Paul Evans
474              
475             =cut
476              
477             0x55AA;