File Coverage

blib/lib/HTTP/ClickHouse.pm
Criterion Covered Total %
statement 48 138 34.7
branch 3 38 7.8
condition n/a
subroutine 14 29 48.2
pod 9 10 90.0
total 74 215 34.4


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