File Coverage

blib/lib/HTTP/ClickHouse.pm
Criterion Covered Total %
statement 53 160 33.1
branch 5 46 10.8
condition 0 3 0.0
subroutine 15 30 50.0
pod 9 10 90.0
total 82 249 32.9


line stmt bran cond sub pod time code
1             package HTTP::ClickHouse;
2              
3 1     1   22372 use 5.010000;
  1         2  
4 1     1   3 use strict;
  1         1  
  1         22  
5 1     1   3 use warnings FATAL => 'all';
  1         1  
  1         48  
6 1     1   3 use Carp;
  1         2  
  1         77  
7              
8             =head1 NAME
9              
10             HTTP::ClickHouse - Client library for Clickhouse OLAP database
11              
12             =head1 SYNOPSIS
13              
14             HTTP::ClickHouse - Perl interface to Clickhouse Database via HTTP.
15              
16             =head1 EXAMPLE
17              
18             use HTTP::ClickHouse;
19            
20             my $chdb = HTTP::ClickHouse->new(
21             host => '127.0.0.1',
22             user => 'Harry',
23             password => 'Alohomora',
24             );
25              
26             $chdb->do("create table test (id UInt8, f1 String, f2 String) engine = Memory");
27              
28             $chdb->do("INSERT INTO my_table (id, field_1, field_2) VALUES",
29             [1, "Gryffindor", "a546825467 1861834657416875469"],
30             [2, "Hufflepuff", "a18202568975170758 46717657846"],
31             [3, "Ravenclaw", "a678 2527258545746575410210547"],
32             [4, "Slytherin", "a1068267496717456 878134788953"]
33             );
34              
35             my $rows = $chdb->selectall_array("SELECT count(*) FROM my_table");
36             unless (@$rows) { $rows->[0]->[0] = 0; } # the query returns an empty string instead of 0
37             print $rows->[0]->[0]."\n";
38              
39              
40             if ($chdb->select_array("SELECT id, field_1, field_2 FROM my_table")) {
41             my $rows = $chdb->fetch_array();
42             foreach my $row (@$rows) {
43             # Do something with your row
44             foreach my $col (@$row) {
45             # Do something
46             print $col."\t";
47             }
48             print "\n";
49             }
50             }
51              
52             $rows = $chdb->selectall_hash("SELECT count(*) as count FROM my_table");
53             foreach my $row (@$rows) {
54             foreach my $key (keys %{$row}){
55             # Do something
56             print $key." = ".$row->{$key}."\t";
57             }
58             print "\n";
59             }
60            
61             ...
62              
63             disconnect $chdb;
64              
65             =head1 DESCRIPTION
66              
67             This module implements HTTP driver for Clickhouse OLAP database
68              
69             =cut
70              
71 1     1   3 use Data::Dumper;
  1         2  
  1         33  
72 1     1   454 use Net::HTTP::NB;
  1         35892  
  1         10  
73 1     1   1042 use IO::Select;
  1         1186  
  1         58  
74 1     1   590 use Storable qw(nfreeze thaw);
  1         2410  
  1         81  
75 1     1   7 use URI;
  1         2  
  1         48  
76 1     1   452 use URI::QueryParam;
  1         540  
  1         27  
77 1     1   4 use Scalar::Util qw/looks_like_number/;
  1         1  
  1         73  
78              
79 1     1   5 use constant READ_BUFFER_LENGTH => 4096;
  1         1  
  1         1063  
80              
81             =head1 VERSION
82              
83             Version 0.04
84              
85             =cut
86              
87             our $VERSION = '0.04';
88             our $AUTOLOAD;
89              
90             #our @EXPORT = qw(new select do);
91              
92             =head1 SUBROUTINES/METHODS
93              
94             =head2 new
95              
96             Create a new connection object with auto reconnect socket if disconnected.
97              
98             my $chdb = HTTP::ClickHouse->new(
99             host => '127.0.0.1',
100             user => 'Harry',
101             password => 'Alohomora'
102             );
103              
104             =head3 options:
105              
106             new(
107             host => 'hogwards.mag', # optional, default value '127.0.0.1'
108             port => 8123, # optional, default value 8123
109             user => 'Harry', # optional, default value 'default'
110             password => 'Alohomora', # optional
111             database => 'database_name', # optional, default name "default"
112             nb_timeout => 10 # optional, default value 25 second
113             keep_alive => 1 # optional, default 1 (1 or 0)
114             debug => 1 # optional, default 0
115             );
116              
117             =cut
118              
119             sub new {
120 1     1 1 1109 my $class = shift;
121 1         4 my $self = { @_ };
122 1         3 $self = bless $self, $class;
123 1         4 $self->_init();
124 1         4 $self->_connect();
125 1         3461 return $self;
126             }
127              
128             sub _init {
129 1     1   1 my $self = shift;
130 1         8 my %_attrs = (
131             host => '127.0.0.1',
132             port => 8123,
133             database => 'default',
134             user => undef,
135             password => undef,
136             keep_alive => 1,
137             nb_timeout => 25,
138             debug => 0
139             );
140 1         4 foreach my $_key ( keys %_attrs ) {
141 8 100       16 unless ($self->{$_key}){
142 5         7 $self->{$_key} = $_attrs{$_key};
143             }
144             }
145            
146             }
147              
148             sub _connect {
149 1     1   1 my $self = shift;
150              
151 1         8 my $_uri = URI->new("/");
152 1 50       3889 $_uri->query_param('user' => $self->{user}) if $self->{user};
153 1 50       5 $_uri->query_param('password' => $self->{password}) if $self->{password};
154 1         9 $_uri->query_param('database' => $self->{database});
155 1         142 $self->{_uri} = nfreeze($_uri);
156              
157             $self->{socket} = Net::HTTP::NB->new(
158             Host => $self->{host},
159             PeerPort => $self->{port},
160             HTTPVersion => '1.1',
161             KeepAlive => $self->{keep_alive}
162 1 50       109 ) or carp "Error. Can't connect to ClickHouse host: $!";
163             }
164              
165             sub uri {
166 0     0 0   my $self = shift;
167 0           return thaw($self->{_uri});
168             }
169              
170             sub _status {
171 0     0     my $self = shift;
172 0           my $select = IO::Select->new($self->{socket});
173 0           my ($code, $mess, %h);
174              
175 0           my $_status = eval {
176             READHEADER: {
177 0 0         die "Get header timeout" unless $select->can_read($self->{nb_timeout});
  0            
178 0           ($code, $mess, %h) = $self->{socket}->read_response_headers;
179 0 0         redo READHEADER unless $code;
180             }
181             };
182              
183 0 0         $_status = $code if ($code);
184 0 0         if ($@) {
185 0 0         carp($@) if $self->{debug};
186 0           $_status = 500;
187             }
188 0           return $_status;
189             }
190              
191             sub _read {
192 0     0     my $self = shift;
193 0           my @_response;
194             READBODY: {
195 0           my $_bufer;
  0            
196 0           my $l = $self->{socket}->read_entity_body($_bufer, READ_BUFFER_LENGTH);
197 0 0         last unless $l;
198 0           push @_response, split (/\n/, $_bufer);
199 0           redo READBODY;
200             }
201 0           return [ map { [ split (/\t/) ] } @_response ];
  0            
202             }
203              
204             sub _query {
205 0     0     my $self = shift;
206 0           my $method = shift;
207 0           my $query = shift;
208 0           my $data = shift;
209              
210 0           delete $self->{response};
211              
212 0           my $t = $self->_ping();
213 0 0         if ($t == 0) { $self->_connect(); carp('Reconnect socket') if $self->{debug}; }
  0 0          
  0            
214              
215 0           my $uri = $self->uri();
216 0           $uri->query_param('query' => $query);
217              
218 0           my @qparam;
219 0           push @qparam, $method => $uri->as_string();
220 0 0         push @qparam, $data if ($method eq 'POST');
221              
222 0           $self->{socket}->write_request(@qparam);
223              
224 0           my $_status = $self->_status();
225 0           my $body = $self->_read(); # By default, data is returned in TabSeparated format.
226 0           $self->{response} = $body;
227              
228 0 0         if ($_status ne '200') {
229 0 0         carp(join(" ".$body)) if $self->{debug};
230 0 0         carp(Dumper($body)) if $self->{debug};
231 0           return 0;
232             }
233 0           return 1;
234             }
235              
236             =head2 select_array & fetch_array
237              
238             First step - select data from the table (readonly). It returns 1 if query completed without errors or 0.
239             Don't set FORMAT in query. TabSeparated is used by default in the HTTP interface.
240              
241             Second step - fetch data.
242             It returns a reference to an array containing a reference to an array for each row of data fetched.
243              
244             if ($chdb->select_array("SELECT id, field_1, field_2 FROM my_table")) {
245             my $rows = $chdb->fetch_array();
246             foreach my $row (@$rows) {
247             # Do something with your row
248             foreach my $col (@$row) {
249             # Do something
250             print $col."\t";
251             }
252             print "\n";
253             }
254             }
255              
256             =cut
257              
258             sub select_array {
259 0     0 1   my $self = shift;
260 0           my $query = shift;
261 0           $query .= ' FORMAT TabSeparated';
262 0           return $self->_query('GET', $query); # When using the GET method, 'readonly' is set.
263             }
264              
265             sub fetch_array {
266 0     0 1   my $self = shift;
267 0           my $_responce = $self->{response};
268             # unless (@$_responce) { $_responce->[0]->[0] = 0; } # the query returns an empty string instead of 0
269 0           return $_responce;
270             }
271              
272             =head2 selectall_array
273              
274             Fetch data from the table (readonly).
275             It returns a reference to an array containing a reference to an array for each row of data fetched.
276              
277             my $rows = $chdb->selectall_array("SELECT count(*) FROM my_table");
278             unless (@$rows) { $rows->[0]->[0] = 0; } # the query returns an empty string instead of 0
279             print $rows->[0]->[0]."\n";
280              
281             =cut
282              
283             sub selectall_array {
284 0     0 1   my $self = shift;
285 0           my $query = shift;
286 0           $self->select_array($query);
287 0           return $self->fetch_array;
288             }
289              
290             =head2 select_hash & fetch_hash
291              
292             First step - select data from the table (readonly). It returns 1 if query completed without errors or 0.
293             Don't set FORMAT in query.
294              
295             Second step - fetch data.
296             It returns a reference to an array containing a reference to an array for each row of data fetched.
297              
298             if ($chdb->select_hash("SELECT id, field_1, field_2 FROM my_table")) {
299             my $rows = $chdb->fetch_hash();
300             foreach my $row (@$rows) {
301             # Do something with your row
302             foreach my $key (sort(keys %{$row})){
303             # Do something
304             print $key." = ".$row->{$key}."\t";
305             }
306             print "\n";
307             }
308             }
309              
310             =cut
311              
312             sub select_hash {
313 0     0 1   my $self = shift;
314 0           my $query = shift;
315 0           $query .= ' FORMAT TabSeparatedWithNames';
316 0           return $self->_query('GET', $query); # When using the GET method, 'readonly' is set.
317             }
318              
319             sub fetch_hash {
320 0     0 1   my $self = shift;
321 0           my @_response = @{$self->{response}};
  0            
322 0           my $response;
323 0           my $key = shift @_response;
324 0           for (0..$#_response) {
325 0           my $row = $_;
326 0           for (0..$#{$_response[$row]}) {
  0            
327 0           my $col = $_;
328 0           $response->[$row]->{"".$key->[$col].""} = $_response[$row][$_];
329             }
330             }
331 0           return $response;
332             }
333              
334             =head2 selectall_hash
335              
336             Fetch data from the table (readonly).
337             It returns a reference to an array containing a reference to an hash for each row of data fetched.
338              
339             my $rows = $chdb->selectall_hash("SELECT id, field_1, field_2 FROM my_table");
340             foreach my $row (@$rows) {
341             # Do something with your row
342             foreach my $key (sort(keys %{$row})){
343             # Do something
344             print $key." = ".$row->{$key}."\t";
345             }
346             print "\n";
347             }
348              
349             =cut
350              
351             sub selectall_hash {
352 0     0 1   my $self = shift;
353 0           my $query = shift;
354 0           $self->select_hash($query);
355 0           return $self->fetch_hash;
356             }
357              
358             =head2 do
359              
360             Universal method for any queries inside the database, which modify data (insert data, create, alter, detach or drop table or partition).
361             It returns 1 if query completed without errors or 0.
362              
363             # drop
364             $chdb->do("drop table test if exist");
365              
366             # create
367             $chdb->do("create table test (id UInt8, f1 String, f2 String) engine = Memory");
368              
369             # insert
370             $chdb->do("INSERT INTO my_table (id, field_1, field_2) VALUES",
371             [1, "Gryffindor", "a546825467 1861834657416875469"],
372             [2, "Hufflepuff", "a18202568975170758 46717657846"],
373             [3, "Ravenclaw", "a678 2527258545746575410210547"],
374             [4, "Slytherin", "a1068267496717456 878134788953"]
375             );
376              
377             =cut
378              
379             sub do {
380 0     0 1   my $self = shift;
381 0           my $query = shift;
382 0           my @_rows = map { [@$_] } @_;
  0            
383 0           foreach my $row (@_rows) {
384 0           foreach my $val (@$row) {
385 0 0 0       unless (defined ($val)) {
    0          
    0          
386 0           $val = qq{''};
387             }
388             elsif (ref($val) eq 'ARRAY') {
389 0           $val = q{'}.join ("','", @$val).q{'};
390             }
391             elsif (defined ($val) && !looks_like_number($val)) {
392 0           $val =~ s/\\/\\\\/g;
393 0           $val =~ s/'/\\'/g;
394 0           $val = qq{'$val'};
395             }
396             }
397             }
398 0 0         my $data = scalar @_rows ? join ",", map { "(".join (",", @{ $_ }).")" } @_rows : "\n" ;
  0            
  0            
399 0           return $self->_query('POST', $query, $data);
400             }
401              
402             sub _ping {
403 0     0     my $self = shift;
404 0           $self->{socket}->write_request(GET => '/ping');
405 0           my $_status = $self->_status();
406 0 0         if ($_status == 200) {
407 0           my $body = $self->_read();
408 0 0         if ($body->[0]->[0] eq 'Ok.') {
409 0           return 1;
410             }
411             }
412 0           return 0;
413             }
414              
415             =head2 disconnect
416              
417             Disconnects http socket from the socket handle. Disconnect typically occures only used before exiting the program.
418              
419             disconnect $chdb;
420              
421             # or
422              
423             $chdb->disconnect;
424              
425             =cut
426              
427             sub disconnect {
428 0     0 1   my $self = shift;
429 0 0         $self->{socket}->keep_alive(0) if ($self->{socket});
430 0           $self->_ping();
431             }
432              
433              
434             =head1 SEE ALSO
435              
436             =over 4
437              
438             =item * ClickHouse official documentation
439              
440             L
441              
442             =back
443              
444             =head1 AUTHOR
445              
446             Maxim Motylkov
447              
448             =head1 TODO
449              
450             The closest plans are
451              
452             =over 4
453              
454             =item * Add json data format.
455              
456             =back
457              
458             =head1 LICENSE AND COPYRIGHT
459              
460             Copyright 2016 Maxim Motylkov
461              
462             This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
463              
464             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
465              
466             =cut
467              
468             sub HTTP::ClickHouse::AUTOLOAD {
469 0     0     croak "No such method: $AUTOLOAD";
470             }
471              
472       0     sub DESTROY {
473             }
474              
475             1;