File Coverage

blib/lib/Protocol/CassandraCQL/Type.pm
Criterion Covered Total %
statement 225 234 96.1
branch 42 54 77.7
condition 4 4 100.0
subroutine 78 83 93.9
pod 2 4 50.0
total 351 379 92.6


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