File Coverage

blib/lib/Couch/DB.pm
Criterion Covered Total %
statement 57 272 20.9
branch 0 130 0.0
condition 0 122 0.0
subroutine 19 56 33.9
pod 20 21 95.2
total 96 601 15.9


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Couch-DB version 0.201.
2             # The POD got stripped from this file by OODoc version 3.06.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2024-2026 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11              
12             package Couch::DB;{
13             our $VERSION = '0.201';
14             }
15              
16              
17 1     1   1430 use strict;
  1         2  
  1         44  
18 1     1   6 use warnings;
  1         3  
  1         54  
19 1     1   7 use version;
  1         2  
  1         7  
20              
21 1     1   88 use Log::Report 'couch-db';
  1         3  
  1         7  
22              
23 1     1   388 use Couch::DB::Client ();
  1         3  
  1         20  
24 1     1   5 use Couch::DB::Cluster ();
  1         1  
  1         27  
25 1     1   7 use Couch::DB::Database ();
  1         2  
  1         19  
26 1     1   4 use Couch::DB::Node ();
  1         2  
  1         36  
27 1     1   5 use Couch::DB::Util qw/flat/;
  1         3  
  1         16  
28              
29 1     1   9 use DateTime ();
  1         4  
  1         21  
30 1     1   7 use DateTime::Format::ISO8601 ();
  1         2  
  1         28  
31 1     1   6 use DateTime::Format::Mail ();
  1         1  
  1         30  
32 1     1   1000 use JSON qw/encode_json/;
  1         6016  
  1         4  
33 1     1   166 use List::Util qw/first min/;
  1         2  
  1         134  
34 1     1   51 use Scalar::Util qw/blessed/;
  1         3  
  1         64  
35 1     1   7 use Storable qw/dclone/;
  1         1  
  1         46  
36 1     1   7 use URI ();
  1         3  
  1         25  
37 1     1   6 use URI::Escape qw/uri_escape uri_unescape/;
  1         2  
  1         63  
38              
39             use constant {
40 1         5409 DEFAULT_SERVER => 'http://127.0.0.1:5984',
41 1     1   6 };
  1         2  
42              
43             #--------------------
44              
45             sub new(%)
46 0     0 1   { my ($class, %args) = @_;
47 0 0         $class ne __PACKAGE__
48             or panic "You have to instantiate extensions of this class";
49              
50 0           (bless {}, $class)->init(\%args);
51             }
52              
53             sub init($)
54 0     0 0   { my ($self, $args) = @_;
55              
56 0 0         my $v = delete $args->{api} or panic "Parameter 'api' is required";
57 0 0 0       $self->{CD_api} = blessed $v && $v->isa('version') ? $v : version->parse($v);
58 0           $self->{CD_clients} = [];
59              
60             # explicit undef for server means: do not create
61 0   0       my $create_client = ! exists $args->{server} || defined $args->{server};
62 0           my $server = delete $args->{server};
63 0           my $external = $ENV{PERL_COUCH_DB_SERVER};
64 0   0       my %auth = ( auth => delete $args->{auth} || 'BASIC' );
65              
66 0 0 0       if($server || ! $external)
    0          
67 0           { $auth{username} = delete $args->{username};
68 0           $auth{password} = delete $args->{password};
69             }
70             elsif($external)
71 0           { my $ext = URI->new($external);
72 0 0         if(my $userinfo = $ext->userinfo)
73 0           { my ($username, $password) = split /:/, $userinfo;
74 0           $auth{username} = uri_unescape $username;
75 0           $auth{password} = uri_unescape $password;
76 0           $ext->userinfo(undef);
77             }
78 0           $server = "$ext";
79             }
80 0           $self->{CD_auth} = \%auth;
81              
82 0 0 0       $self->createClient(server => $server || DEFAULT_SERVER, name => '_local')
83             if $create_client;
84              
85 0   0       $self->{CD_toperl} = delete $args->{to_perl} || {};
86 0   0       $self->{CD_tojson} = delete $args->{to_json} || {};
87 0   0       $self->{CD_toquery} = delete $args->{to_query} || {};
88 0           $self;
89             }
90              
91             #--------------------
92              
93 0     0 1   sub api() { $_[0]->{CD_api} }
94              
95             #--------------------
96              
97             sub createClient(%)
98 0     0 1   { my ($self, %args) = @_;
99 0           my $client = Couch::DB::Client->new(couch => $self, %{$self->{CD_auth}}, %args);
  0            
100 0 0         $client ? $self->addClient($client) : undef;
101             }
102              
103              
104             sub db($%)
105 0     0 1   { my ($self, $name, %args) = @_;
106 0           Couch::DB::Database->new(name => $name, couch => $self, %args);
107             }
108              
109              
110             sub node($)
111 0     0 1   { my ($self, $name) = @_;
112 0   0       $self->{CD_nodes}{$name} ||= Couch::DB::Node->new(name => $name, couch => $self);
113             }
114              
115              
116 0   0 0 1   sub cluster() { $_[0]->{CD_cluster} ||= Couch::DB::Cluster->new(couch => $_[0]) }
117              
118             #--------------------
119              
120             #XXX the API-doc might be mistaken, calling the "analyzer" parameter "field".
121              
122             sub searchAnalyze($%)
123 0     0 1   { my ($self, $config, %args) = @_;
124 0 0         exists $config->{analyzer} or panic "No analyzer specified.";
125 0 0         exists $config->{text} or panic "No text to inspect specified.";
126              
127 0           $self->call(POST => '/_search_analyze',
128             introduced => '3.0',
129             send => $config,
130             $self->_resultsConfig(\%args),
131             );
132             }
133              
134              
135             sub requestUUIDs($%)
136 0     0 1   { my ($self, $count, %args) = @_;
137              
138 0           $self->call(GET => '/_uuids',
139             introduced => '2.0.0',
140             query => { count => $count },
141             $self->_resultsConfig(\%args),
142             );
143             }
144              
145              
146             sub freshUUIDs($%)
147 0     0 1   { my ($self, $count, %args) = @_;
148 0   0       my $stock = $self->{CDC_uuids} || [];
149 0   0       my $bulk = delete $args{bulk} || 50;
150              
151 0           while($count > @$stock)
152 0 0         { my $result = $self->requestUUIDs($bulk, delay => 0) or last;
153 0 0         push @$stock, @{$result->values->{uuids} || []};
  0            
154             }
155              
156 0           splice @$stock, 0, $count;
157             }
158              
159              
160 0     0 1   sub freshUUID(%) { my $s = shift; ($s->freshUUIDs(1, @_))[0] }
  0            
161              
162             #--------------------
163              
164             sub addClient($)
165 0     0 1   { my ($self, $client) = @_;
166 0 0         $client or return $self;
167              
168 0 0         $client->isa('Couch::DB::Client') or panic;
169 0           push @{$self->{CD_clients}}, $client;
  0            
170 0           $self;
171             }
172              
173              
174             sub clients(%)
175 0     0 1   { my ($self, %args) = @_;
176 0           my $clients = $self->{CD_clients};
177              
178 0           my $role = delete $args{role};
179 0 0         $role ? grep $_->canRole($role), @$clients : @$clients;
180             }
181              
182              
183             sub client($)
184 0     0 1   { my ($self, $name) = @_;
185 0 0         $name = "$name" if blessed $name;
186 0     0     first { $_->name eq $name } $self->clients; # never many: no HASH needed
  0            
187             }
188              
189              
190              
191             sub call($$%)
192 0     0 1   { my ($self, $method, $path, %args) = @_;
193 0           $args{method} = $method;
194 0           $args{path} = $path;
195 0   0       $args{query} ||= my $query = {};
196              
197 0   0       my $headers = $args{headers} ||= {};
198 0   0       $headers->{Accept} ||= 'application/json';
199 0   0       $headers->{'Content-Type'} ||= 'application/json';
200              
201             #use Data::Dumper;
202             #warn "CALL ", Dumper \%args;
203              
204 0           my $send = $args{send};
205 0 0 0       defined $send || ($method ne 'POST' && $method ne 'PUT')
      0        
206             or panic "No send in $method $path";
207              
208 0           my $introduced = $args{introduced};
209             $self->check(exists $args{$_}, $_ => delete $args{$_}, "Endpoint '$method $path'")
210 0           for qw/removed introduced deprecated/;
211              
212             ### On this level, we pick a client. Extensions implement the transport.
213              
214 0           my $paging = $args{paging};
215 0 0 0       if($paging && (my $client = $paging->{client}))
216             { # No free choices for clients once we are on page 2
217 0           $args{client} = $client;
218 0           delete $args{clients};
219             }
220              
221 0           my @clients;
222 0 0         if(my $client = delete $args{client})
    0          
223 0 0         { @clients = blessed $client ? $client : $self->client($client);
224             }
225             elsif(my $c = delete $args{clients})
226 0 0         { @clients = ref $c eq 'ARRAY' ? @$c : $self->clients(role => $c);
227             }
228             else
229 0           { @clients = $self->clients;
230             }
231 0 0         @clients or error __x"No clients can run {method} {path}.", method => $method, path => $path;
232              
233             my $result = Couch::DB::Result->new(
234             couch => $self,
235             on_values => $args{on_values},
236             on_error => $args{on_error},
237             on_final => $args{on_final},
238             on_chain => $args{on_chain},
239             on_row => $args{on_row},
240 0           paging => $paging,
241             );
242              
243             CLIENT:
244 0           foreach my $client (@clients)
245             {
246 0 0 0       ! $introduced || $client->version >= $introduced
247             or next CLIENT; # server release too old
248              
249 0 0         if($paging)
250             { do
251 0           { # Merge paging setting into the request
252 0           $self->_pageRequest($paging, $method, $query, $send);
253              
254 0           $self->_callClient($result, $client, %args);
255 0 0         $result
256             or next CLIENT; # fail
257              
258             } while $result->pageIsPartial;
259              
260 0           last CLIENT;
261             }
262             else
263             { # Non-paging commands are simple
264 0 0         $self->_callClient($result, $client, %args)
265             and last CLIENT;
266             }
267             }
268              
269             # The error from the last try will remain.
270 0           $result;
271             }
272              
273 0     0     sub _callClient { panic "must be extended" }
274              
275             # Described in the DETAILS below, non-paging commands
276             sub _resultsConfig($%)
277 0     0     { my ($self, $args, @more) = @_;
278 0           my %config;
279              
280 0           unshift @more, %$args;
281 0           while(@more)
282 0           { my ($key, $value) = (shift @more, shift @more);
283 0 0         if($key eq 'headers')
    0          
284             { # Headers are added, as default only
285 0   0       my $headers = $config{headers} ||= {};
286 0   0       exists $headers->{$_} or ($headers->{$_} = $value->{$_}) for keys %$value;
287             }
288             elsif($key =~ /^on_/)
289             { # User specified additional events
290 0 0         push @{$config{$key}}, $value if defined $value;
  0            
291             }
292             else
293             { # Other parameters used as default
294 0 0         exists $config{$key} or $config{$key} = $value;
295             }
296             }
297              
298 0 0 0       $config{paging} && !$config{on_row} and panic "paging without on_row";
299              
300 0           delete @{$args}{qw/delay client clients/};
  0            
301 0           delete @{$args}{grep /^on_/, keys %$args};
  0            
302 0 0         keys %$args and warn "Unused call parameters: ", join ', ', sort keys %$args;
303              
304 0           %config;
305             }
306              
307             # Described in the DETAILS below, paging commands
308             sub _resultsPaging($%)
309 0     0     { my ($self, $args, @more) = @_;
310              
311 0           my %state = (harvested => []);
312 0           my $succ; # successor
313 0 0         if(my $succeeds = delete $args->{succeed})
314 0           { delete $args->{clients}; # no client switching within paging
315              
316 0 0 0       if(blessed $succeeds && $succeeds->isa('Couch::DB::Result'))
317             { # continue from living previous result
318 0           $succ = $succeeds->nextPageSettings;
319 0           $args->{client} = $succeeds->client;
320             }
321             else
322             { # continue from resurrected from Result->pagingState()
323             my $h = $succeeds->{harvester}
324 0 0         or panic "_succeed does not contain data from pagingState() nor is a Result object.";
325              
326             $h eq 'DEFAULT' || $args->{harvester}
327 0 0 0       or panic "Harvester does not survive pagingState(), resupply.";
328              
329             $succeeds->{map} eq 'NONE' || $args->{map}
330 0 0 0       or panic "Map does not survive pagingState(), resupply.";
331              
332 0           $succ = $succeeds;
333 0           $args->{client} = $succeeds->{client};
334             }
335             }
336              
337 0   0       $state{start} = $succ->{start} || 0;
338 0   0       $state{skip} = delete $args->{skip} || 0;
339 0   0       $state{all} = delete $args->{all} || 0;
340 0   0       $state{map} = my $map = delete $args->{map} || $succ->{map};
341 0   0       $state{harvester} = my $harvester = delete $args->{harvester} || $succ->{harvester};
342 0   0       $state{page_size} = my $size = delete $args->{page_size} || $succ->{page_size};
343 0   0       $state{req_rows} = delete $args->{limit} || $succ->{req_rows} || 100;
344 0   0       $state{page_mode} = !! ($state{all} || $size);
345 0   0       $state{stop} = my $stop = delete $args->{stop} || $succ->{stop} || 'EMPTY';
346              
347 0           my $page;
348 0 0         if($page = delete $args->{page})
349 0 0         { defined $size or panic "page parameter only usefull with page_size.";
350 0           $state{start} = ($page - 1) * $size;
351             }
352 0   0       $state{pagenr} = delete $args->{pagenr} // $succ->{pagenr} // $page // 1;
      0        
      0        
353              
354 0   0       $state{bookmarks} = $succ->{bookmarks} ||= { };
355 0 0         if(my $bm = delete $args->{bookmark})
356 0           { $state{bookmarks}{$state{start}} = $bm;
357             }
358              
359 0   0 0     $harvester ||= sub { $_[0]->_rowsRef(0) };
  0            
360             my $harvest = sub {
361 0 0   0     my $result = shift or return;
362 0           my @found = flat $harvester->($result);
363 0 0         @found = map $map->($result, $_), @found if $map;
364              
365             # The answer does not tell me that we are on the last page.
366 0           $result->_pageAdd($result->answer->{bookmark}, \@found); # also call with 0
367 0           };
368              
369 0 0         if(ref $stop ne 'CODE')
370 0 0         { if($stop eq 'EMPTY')
    0          
    0          
371             { # we always stop when there were no rows returned
372 0     0     $state{stop} = sub { 0 };
  0            
373             }
374             elsif($stop eq 'SMALLER')
375 0           { my $first;
376             $state{stop} = sub {
377 0 0   0     return $_[0]->numberOfRows < $first if defined $first;
378 0           $first = $_[0]->numberOfRows;
379 0           0;
380 0           };
381             }
382             elsif($stop =~ m/^UPTO\((\d+)\)$/)
383 0           { my $upto = $1;
384 0     0     $state{stop} = sub { $_[0]->numberOfRows <= $upto };
  0            
385             }
386             else
387 0           { panic "Unknown stop value `$stop`";
388             }
389             }
390              
391 0           $self->_resultsConfig($args, @more, on_final => $harvest, paging => \%state),
392             }
393              
394             sub _pageRequest($$$$)
395 0     0     { my ($self, $paging, $method, $query, $send) = @_;
396 0 0         my $params = $method eq 'GET' ? $query : $send;
397 0           my $progress = @{$paging->{harvested}}; # within the page
  0            
398 0           my $start = $paging->{start};
399              
400             $params->{limit}
401             = $paging->{page_size}
402             ? (min $paging->{page_size} - $progress, $paging->{req_rows})
403 0 0         : $paging->{req_rows};
404              
405 0 0         if(my $bookmark = $paging->{bookmarks}{$start + $progress})
406 0           { $params->{bookmark} = $bookmark;
407 0           $params->{skip} = $paging->{skip};
408             }
409             else
410 0           { delete $params->{bookmark};
411 0           $params->{skip} = $start + $paging->{skip} + $progress;
412             }
413             }
414              
415              
416             my %default_toperl = ( # sub ($couch, $name, $datum) returns value/object
417             abs_uri => sub { URI->new($_[2]) },
418             epoch => sub { DateTime->from_epoch(epoch => $_[2]) },
419             isotime => sub { DateTime::Format::ISO8601->parse_datetime($_[2]) },
420             mailtime => sub { DateTime::Format::Mail->parse_datetime($_[2]) }, # smart choice by CouchDB?
421             version => sub { version->parse($_[2]) },
422             node => sub { $_[0]->node($_[2]) },
423             );
424              
425             sub _toPerlHandler($)
426 0     0     { my ($self, $type) = @_;
427 0 0         $self->{CD_toperl}{$type} || $default_toperl{$type};
428             }
429              
430             sub toPerl($$@)
431 0     0 1   { my ($self, $data, $type) = (shift, shift, shift);
432 0 0         my $conv = $self->_toPerlHandler($type) or return $self;
433              
434             exists $data->{$_} && ($data->{$_} = $conv->($self, $_, $data->{$_}))
435 0   0       for @_;
436              
437 0           $self;
438             }
439              
440              
441             sub listToPerl
442 0     0 1   { my ($self, $name, $type) = (shift, shift, shift);
443 0 0         my $conv = $self->_toPerlHandler($type) or return flat @_;
444 0           grep defined, map $conv->($self, $name, $_), flat @_;
445             }
446              
447              
448             my %default_tojson = ( # sub ($couch, $name, $datum) returns JSON
449             # All known backends support these booleans
450             bool => sub { $_[2] ? $JSON::true : $JSON::false },
451              
452             # All known URL implementations correctly overload stringify
453             uri => sub { "$_[2]" },
454              
455             node => sub { my $n = $_[2]; blessed $n ? $n->name : $n },
456              
457             # In Perl, the int might come from text (for instance a configuration
458             # file. In that case, the JSON::XS will write "6". But the server-side
459             # JSON is type sensitive and may crash.
460             int => sub { defined $_[2] ? int($_[2]) : undef },
461             );
462              
463             sub _toJsonHandler($)
464 0     0     { my ($self, $type) = @_;
465 0 0         $self->{CD_tojson}{$type} || $default_tojson{$type};
466             }
467              
468             sub toJSON($@)
469 0     0 1   { my ($self, $data, $type) = (shift, shift, shift);
470 0 0         my $conv = $self->_toJsonHandler($type) or return $self;
471              
472             exists $data->{$_} && ($data->{$_} = $conv->($self, $_, $data->{$_}))
473 0   0       for @_;
474              
475 0           $self;
476             }
477              
478              
479             # Extend/override the list of toJSON converters
480             my %default_toquery = (
481             bool => sub { $_[2] ? 'true' : 'false' },
482             json => sub { encode_json $_[2] },
483             );
484              
485             sub _toQueryHandler($)
486 0     0     { my ($self, $type) = @_;
487             $self->{CD_toquery}{$type} || $default_toquery{$type}
488 0 0 0       || $self->{CD_tojson}{$type} || $default_tojson{$type};
      0        
489             }
490              
491             sub toQuery($@)
492 0     0 1   { my ($self, $data, $type) = (shift, shift, shift);
493 0 0         my $conv = $self->_toQueryHandler($type) or return $self;
494              
495             exists $data->{$_} && ($data->{$_} = $conv->($self, $_, $data->{$_}))
496 0   0       for @_;
497              
498 0           $self;
499             }
500              
501              
502             sub jsonText($%)
503 0     0 1   { my ($self, $json, %args) = @_;
504 0           JSON->new->pretty(not $args{compact})->encode($json);
505             }
506              
507              
508             my (%surpress_depr, %surpress_intro);
509              
510             sub check($$$$)
511 0 0   0 1   { $_[1] or return $_[0];
512 0           my ($self, $condition, $change, $version, $what) = @_;
513              
514             # API-doc versions are sometimes without 3rd part.
515 0           my $cv = version->parse($version);
516              
517 0 0         if($change eq 'removed')
    0          
    0          
518 0 0         { $self->api < $cv
519             or error __x"{what} got removed in {release}, but you specified api {api}.",
520             what => $what, release => $version, api => $self->api;
521             }
522             elsif($change eq 'introduced')
523 0 0 0       { $self->api >= $cv || $surpress_intro{$what}++
524             or warning __x"{what} was introduced in {release}, but you specified api {api}.",
525             what => $what, release => $version, api => $self->api;
526             }
527             elsif($change eq 'deprecated')
528 0 0 0       { $self->api >= $cv || $surpress_depr{$what}++
529             or warning __x"{what} got deprecated in api {release}.",
530             what => $what, release => $version;
531             }
532 0           else { panic "$change $cv $what" }
533              
534 0           $self;
535             }
536              
537             #### Extension which perform some tasks which are framework object specific.
538              
539             # Returns the JSON structure which is part of the response by the CouchDB
540             # server. Usually, this is the body of the response. In multipart
541             # responses, it is the first part.
542 0     0     sub _extractAnswer($) { panic "must be extended" }
543              
544             # The the decoded named extension from the multipart message
545 0     0     sub _attachment($$) { panic "must be extended" }
546              
547             # Extract the decoded body of the message
548 0     0     sub _messageContent($) { panic "must be extended" }
549              
550             1;
551              
552             #--------------------