File Coverage

blib/lib/Couch/DB/Database.pm
Criterion Covered Total %
statement 27 239 11.3
branch 0 86 0.0
condition 0 35 0.0
subroutine 9 63 14.2
pod 32 33 96.9
total 68 456 14.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             #oorestyle: not found P for method saveBulk(%details)
12              
13             package Couch::DB::Database;{
14             our $VERSION = '0.201';
15             }
16              
17              
18 1     1   1434 use warnings;
  1         2  
  1         66  
19 1     1   8 use strict;
  1         1  
  1         34  
20              
21 1     1   6 use Log::Report 'couch-db';
  1         2  
  1         8  
22              
23 1     1   392 use Couch::DB::Util qw/flat/;
  1         3  
  1         10  
24 1     1   9 use Couch::DB::Document ();
  1         3  
  1         32  
25 1     1   6 use Couch::DB::Design ();
  1         1  
  1         32  
26              
27 1     1   6 use Scalar::Util qw/weaken blessed/;
  1         1  
  1         123  
28 1     1   13 use HTTP::Status qw/HTTP_OK HTTP_NOT_FOUND/;
  1         2  
  1         79  
29 1     1   7 use JSON::PP ();
  1         3  
  1         6015  
30              
31             #--------------------
32              
33              
34 0     0 1   sub new(@) { my ($class, %args) = @_; (bless {}, $class)->init(\%args) }
  0            
35              
36             sub init($)
37 0     0 0   { my ($self, $args) = @_;
38              
39 0 0         my $name = $self->{CDD_name} = delete $args->{name} or panic "Requires name";
40 0 0         $name =~ m!^[a-z][a-z0-9_$()+/-]*$!
41             or error __x"Illegal database name '{name}'.", name => $name;
42              
43 0 0         $self->{CDD_couch} = delete $args->{couch} or panic "Requires couch";
44 0           weaken $self->{CDD_couch};
45              
46 0           $self->{CDD_batch} = delete $args->{batch};
47 0           $self;
48             }
49              
50             #--------------------
51              
52 0     0 1   sub name() { $_[0]->{CDD_name} }
53 0     0 1   sub couch() { $_[0]->{CDD_couch} }
54 0     0 1   sub batch() { $_[0]->{CDD_batch} }
55              
56 0 0   0     sub _pathToDB(;$) { '/' . $_[0]->name . (defined $_[1] ? '/' . $_[1] : '') }
57              
58             #--------------------
59              
60             sub ping(%)
61 0     0 1   { my ($self, %args) = @_;
62              
63 0           $self->couch->call(HEAD => $self->_pathToDB,
64             $self->couch->_resultsConfig(\%args),
65             );
66             }
67              
68              
69             sub exists()
70 0     0 1   { my $self = shift;
71 0           my $result = $self->ping(delay => 0);
72              
73 0 0         $result->code eq HTTP_NOT_FOUND ? 0
    0          
74             : $result->code eq HTTP_OK ? 1
75             : undef; # will probably die in the next step
76             }
77              
78              
79             sub __detailsValues($$)
80 0     0     { my ($self, $result, $raw) = @_;
81 0           my %values = %$raw; # deep not needed;
82 0           $self->couch->toPerl(\%values, epoch => qw/instance_start_time/);
83 0           \%values;
84             }
85              
86             sub details(%)
87 0     0 1   { my ($self, %args) = @_;
88 0           my $part = delete $args{partition};
89              
90             #XXX Value instance_start_time is now always zero, useful to convert if not
91             #XXX zero in old nodes?
92              
93             $self->couch->call(GET => $self->_pathToDB($part ? '_partition/'.uri_escape($part) : undef),
94             $self->couch->_resultsConfig(\%args,
95 0     0     on_values => sub { $self->__detailsValues(@_) },
96 0 0         ),
97             );
98             }
99              
100              
101             sub create(%)
102 0     0 1   { my ($self, %args) = @_;
103 0           my $couch = $self->couch;
104              
105 0           my %query;
106 0   0       exists $args{$_} && ($query{$_} = delete $args{$_}) for qw/partitioned q n/;
107 0           $couch->toQuery(\%query, bool => qw/partitioned/);
108 0           $couch->toQuery(\%query, int => qw/q n/);
109              
110 0           $couch->call(PUT => $self->_pathToDB,
111             query => \%query,
112             send => { },
113             $self->couch->_resultsConfig(\%args),
114             );
115             }
116              
117              
118             sub remove(%)
119 0     0 1   { my ($self, %args) = @_;
120              
121 0           $self->couch->call(DELETE => $self->_pathToDB,
122             $self->couch->_resultsConfig(\%args),
123             );
124             }
125              
126              
127             sub userRoles(%)
128 0     0 1   { my ($self, %args) = @_;
129              
130 0           $self->couch->call(GET => $self->_pathToDB('_security'),
131             $self->couch->_resultsConfig(\%args),
132             );
133             }
134              
135              
136             sub userRolesChange(%)
137 0     0 1   { my ($self, %args) = @_;
138             my %send = (
139             admin => delete $args{admin} || [],
140 0   0       members => delete $args{members} || [],
      0        
141             );
142              
143 0           $self->couch->call(PUT => $self->_pathToDB('_security'),
144             send => \%send,
145             $self->couch->_resultsConfig(\%args),
146             );
147             }
148              
149              
150 0     0 1   sub changes($%) { ... }
151              
152              
153             sub compactViews(%)
154 0     0 1   { my ($self, %args) = @_;
155 0           $self->couch->call(POST => $self->_pathToDB('_compact'),
156             send => +{},
157             $self->couch->_resultsConfig(\%args),
158             );
159             }
160              
161              
162             sub __ensure($$)
163 0     0     { my ($self, $result, $raw) = @_;
164 0 0         return $raw unless $raw->{instance_start_time}; # exists && !=0
165 0           my $v = { %$raw };
166 0           $self->couch->toPerl($v, epoch => qw/instance_start_time/);
167 0           $v;
168             }
169              
170             sub ensureFullCommit(%)
171 0     0 1   { my ($self, %args) = @_;
172              
173             $self->couch->call(POST => $self->_pathToDB('_ensure_full_commit'),
174             deprecated => '3.0.0',
175             send => { },
176             $self->couch->_resultsConfig(\%args,
177 0     0     on_values => sub { $self->__ensureValues(@_) },
178 0           ),
179             );
180             }
181              
182              
183             sub purgeDocs($%)
184 0     0 1   { my ($self, $plan, %args) = @_;
185              
186             #XXX looking for smarter behavior here, to construct a plan.
187 0           my $send = $plan;
188              
189 0           $self->couch->call(POST => $self->_pathToDB('_purge'),
190             $self->couch->_resultsConfig(\%args),
191             );
192             }
193              
194              
195             #XXX seems not really a useful method.
196              
197             sub purgedRecordsLimit(%)
198 0     0 1   { my ($self, %args) = @_;
199              
200 0           $self->couch->call(GET => $self->_pathToDB('_purged_infos_limit'),
201             $self->couch->_resultsConfig(\%args),
202             );
203             }
204              
205              
206             #XXX attribute of database creation
207              
208             sub purgedRecordsLimitSet($%)
209 0     0 1   { my ($self, $value, %args) = @_;
210              
211 0           $self->couch->call(PUT => $self->_pathToDB('_purged_infos_limit'),
212             send => int($value),
213             $self->couch->_resultsConfig(\%args),
214             );
215             }
216              
217              
218             sub purgeUnusedViews(%)
219 0     0 1   { my ($self, %args) = @_;
220              
221             #XXX nothing to send?
222 0           $self->couch->call(POST => $self->_pathToDB('_view_cleanup'),
223             $self->couch->_resultsConfig(\%args),
224             );
225             }
226              
227              
228             sub revisionsMissing($%)
229 0     0 1   { my ($self, $plan, %args) = @_;
230              
231             #XXX needs extra features
232 0           $self->couch->call(POST => $self->_pathToDB('_missing_revs'),
233             send => $plan,
234             $self->couch->_resultsConfig(\%args),
235             );
236             }
237              
238              
239             sub revisionsDiff($%)
240 0     0 1   { my ($self, $plan, %args) = @_;
241              
242             #XXX needs extra features
243 0           $self->couch->call(POST => $self->_pathToDB('_revs_diff'),
244             send => $plan,
245             $self->couch->_resultsConfig(\%args),
246             );
247             }
248              
249              
250             #XXX seems not really a useful method.
251              
252             sub revisionLimit(%)
253 0     0 1   { my ($self, %args) = @_;
254              
255 0           $self->couch->call(GET => $self->_pathToDB('_revs_limit'),
256             $self->couch->_resultsConfig(\%args),
257             );
258             }
259              
260              
261             #XXX attribute of database creation
262              
263             sub revisionLimitSet($%)
264 0     0 1   { my ($self, $value, %args) = @_;
265              
266 0           $self->couch->call(PUT => $self->_pathToDB('_revs_limit'),
267             send => int($value),
268             $self->couch->_resultsConfig(\%args),
269             );
270             }
271              
272             #--------------------
273              
274             sub design($)
275 0     0 1   { my ($self, $which) = @_;
276              
277 0 0 0       return $which if blessed $which && $which->isa('Couch::DB::Design');
278 0           Couch::DB::Design->new(id => $which, db => $self);
279             }
280              
281              
282             sub __designsPrepare($$$)
283 0     0     { my ($self, $method, $data, $where) = @_;
284 0 0         $method eq 'POST' or panic;
285 0           my $s = +{ %$data };
286              
287             # Very close to a view search, but not equivalent. At least: according to the
288             # API documentation :-(
289 0           $self->couch
290             ->toJSON($s, bool => qw/conflicts descending include_docs inclusive_end update_seq/)
291             ->toJSON($s, int => qw/limit skip/);
292 0           $s;
293             }
294              
295             sub __designsRow($$%)
296 0     0     { my ($self, $result, $index, %args) = @_;
297 0 0         my $answer = $result->answer->{rows}[$index] or return;
298 0           my $values = $result->values->{rows}[$index];
299              
300             ( answer => $answer,
301             values => $values,
302             ddocdata => $values->{doc},
303 0           docparams => { db => $self },
304             );
305             }
306              
307             sub designs(;$%)
308 0     0 1   { my ($self, $search, %args) = @_;
309 0           my $couch = $self->couch;
310 0           my @search = flat $search;
311              
312 0           my ($method, $path, $send) = (GET => $self->_pathToDB('_design_docs'), undef);
313 0 0         if(@search)
314 0           { $method = 'POST';
315 0           my @s = map $self->__designsPrepare($method, $_), @search;
316              
317 0 0         if(@search==1)
318 0           { $send = $s[0];
319             }
320             else
321 0           { $send = +{ queries => \@s };
322 0           $path .= '/queries';
323             }
324             }
325              
326             $self->couch->call($method => $path,
327             ($send ? (send => $send) : ()),
328             $couch->_resultsConfig(\%args,
329 0     0     on_row => sub { $self->__designsRow(@_, queries => scalar(@search)) },
330 0 0         ),
331             );
332             }
333              
334              
335             sub __indexesRow($$%)
336 0     0     { my ($self, $result, $index, %args) = @_;
337 0 0         my $answer = $result->answer->{indexes}[$index] or return ();
338              
339             ( answer => $answer,
340 0           values => $result->values->{indexes}[$index],
341             );
342             }
343              
344             sub __indexesValues()
345 0     0     { my ($self, $raw) = @_;
346 0           my %values = %$raw; # deep not needed (yes)
347 0           $self->couch->toPerl(\%values, bool => qw/partitioned/);
348 0 0         $values{design} = $self->design($values{ddoc}) if $values{ddoc};
349 0           \%values;
350             }
351              
352             sub indexes(%)
353 0     0 1   { my ($self, %args) = @_;
354              
355             $self->couch->call(GET => $self->_pathToDB('_index'),
356             $self->couch->_resultsConfig(\%args,
357 0     0     on_values => sub { $self->__indexesValues(@_) },
358 0     0     on_row => sub { $self->__indexesRow(@_) },
359 0           ),
360             );
361             }
362              
363              
364             sub search($$;$%)
365 0     0 1   { my ($self, $ddoc, $index, $search, %args) = @_;
366 0           $self->design($ddoc)->search($index, $search, %args);
367             }
368              
369             #--------------------
370              
371             sub doc($%)
372 0     0 1   { my ($self, $id) = @_;
373 0           Couch::DB::Document->new(id => $id, db => $self, @_);
374             }
375              
376              
377             sub __bulk($$$$)
378 0     0     { my ($self, $result, $saves, $deletes, $issues) = @_;
379 0 0         $result or return;
380              
381 0           my %saves = map +($_->id => $_), @$saves;
382 0           my %deletes = map +($_->id => $_), @$deletes;
383              
384 0           foreach my $report (@{$result->values})
  0            
385 0           { my $id = $report->{id};
386 0           my $delete = exists $deletes{$id};
387 0 0 0       my $doc = delete $deletes{$id} || delete $saves{$id}
388             or panic "missing report for updated $id";
389              
390 0 0         if($report->{ok})
391 0           { $doc->_saved($id, $report->{rev});
392 0 0         $doc->_deleted($report->{rev}) if $delete;
393             }
394             else
395 0           { $issues->($result, $doc, +{ %$report, delete => $delete });
396             }
397             }
398              
399             $issues->($result, $saves{$_},
400             +{ error => 'missing', reason => "The server did not report back on saving $_." }
401 0           ) for keys %saves;
402              
403             $issues->($result, $deletes{$_},
404             +{ error => 'missing', reason => "The server did not report back on deleting $_.", delete => 1 }
405 0           ) for keys %deletes;
406             }
407              
408             sub saveBulk($%)
409 0     0 1   { my ($self, $docs, %args) = @_;
410 0           my $couch = $self->couch;
411 0   0 0     my $issues = delete $args{issues} || sub {};
412              
413 0           my @plan;
414 0           foreach my $doc (@$docs)
415 0           { my $rev = $doc->rev;
416 0           my %plan = %{$doc->revision($rev)};
  0            
417 0           $plan{_id} = $doc->id;
418 0 0         $plan{_rev} = $rev if $rev ne '_new';
419 0           push @plan, \%plan;
420             }
421              
422 0           my @deletes = flat delete $args{delete};
423 0           foreach my $del (@deletes)
424 0           { push @plan, +{ _id => $del->id, _rev => $del->rev, _deleted => JSON::PP::true };
425 0           $couch->toJSON($plan[-1], bool => qw/_delete/);
426             }
427              
428 0 0         @plan or error __x"need at least on document for bulk processing.";
429 0           my $send = +{ docs => \@plan };
430              
431 0 0         $send->{new_edits} = delete $args{new_edits} if exists $args{new_edits}; # default true
432 0           $couch->toJSON($send, bool => qw/new_edits/);
433              
434             $couch->call(POST => $self->_pathToDB('_bulk_docs'),
435             send => $send,
436             $couch->_resultsConfig(\%args,
437 0     0     on_final => sub { $self->__bulk($_[0], $docs, \@deletes, $issues) },
438 0           ),
439             );
440             }
441              
442              
443             sub inspectDocs($%)
444 0     0 1   { my ($self, $docs, %args) = @_;
445 0           my $couch = $self->couch;
446              
447 0           my $query;
448 0 0         $query->{revs} = delete $args{revs} if exists $args{revs};
449 0           $couch->toQuery($query, bool => qw/revs/);
450              
451 0 0         @$docs or error __x"need at least one document for bulk query.";
452              
453             #XXX what does "conflicted documents mean?
454             #XXX what does "a": 1 mean in its response?
455              
456 0           $self->couch->call(POST => $self->_pathToDB('_bulk_get'),
457             query => $query,
458             send => { docs => $docs },
459             $couch->_resultsConfig(\%args),
460             );
461             }
462              
463              
464             sub __allDocsRow($$%)
465 0     0     { my ($self, $result, $index, %args) = @_;
466 0 0         my $answer = $result->answer->{rows}[$index] or return ();
467 0           my $values = $result->values->{rows}[$index];
468              
469             ( answer => $answer,
470             values => $values,
471             docdata => $values->{doc},
472 0           docparams => { local => $args{local}, db => $self },
473             );
474             }
475              
476             sub allDocs(;$%)
477 0     0 1   { my ($self, $search, %args) = @_;
478 0           my $couch = $self->couch;
479              
480 0           my @search = flat $search;
481 0           my $part = delete $args{partition};
482 0           my $local = delete $args{local};
483 0           my $view = delete $args{view};
484 0           my $ddoc = delete $args{design};
485 0 0         my $ddocid = blessed $ddoc ? $ddoc->id : $ddoc;
486              
487             #XXX The API shows some difference in the parameter combinations, which do not
488             #XXX need to be there. For now, we produce an error for these cases.
489 0 0 0       !$view || $ddoc or panic "allDocs(view) requires design document.";
490 0 0 0       !$local || !$part or panic "allDocs(local) cannot be combined with partition.";
491 0 0 0       !$local || !$view or panic "allDocs(local) cannot be combined with a view.";
492 0 0 0       !$part || @search < 2 or panic "allDocs(partition) cannot work with multiple searches.";
493              
494 0 0         my $set
    0          
    0          
495             = $local ? '_local_docs'
496             : ($part ? '_partition/'. uri_escape($part) . '/' : '')
497             . ($view ? "_design/$ddocid/_view/". uri_escape($view) : '_all_docs');
498              
499 0 0 0       my $method = !@search || $part ? 'GET' : 'POST';
500 0           my $path = $self->_pathToDB($set);
501              
502             # According to the spec, _all_docs is just a special view.
503 0           my @send = map $self->_viewPrepare($method, $_, "docs search"), @search;
504              
505 0           my @params;
506 0 0         if($method eq 'GET')
    0          
507 0 0         { @send < 2 or panic "Only one search with docs(GET)";
508 0           @params = (query => $send[0]);
509             }
510             elsif(@send==1)
511 0           { @params = (send => $send[0]);
512             }
513             else
514 0           { $couch->check(1, introduced => '2.2.0', 'Bulk queries');
515 0           @params = (send => +{ queries => \@send });
516 0           $path .= '/queries';
517             }
518              
519             $couch->call($method => $path,
520             @params,
521             $couch->_resultsPaging(\%args,
522 0     0     on_row => sub { $self->__allDocsRow(@_, local => $local, queries => scalar(@search)) },
523 0           ),
524             );
525             }
526              
527             my @docview_bools = qw/
528             conflicts descending group include_docs attachments att_encoding_info
529             inclusive_end reduce sorted stable update_seq
530             /;
531              
532             # Handles standard view/_all_docs/_local_docs queries.
533             sub _viewPrepare($$$)
534 0     0     { my ($self, $method, $data, $where) = @_;
535 0           my $s = +{ %$data };
536 0           my $couch = $self->couch;
537              
538             # Main doc in 1.5.4. /{db}/_design/{ddoc}/_view/{view}
539 0 0         if($method eq 'GET')
540 0           { $couch
541             ->toQuery($s, bool => @docview_bools)
542             ->toQuery($s, json => qw/endkey end_key key keys start_key startkey/);
543             }
544             else
545 0           { $couch
546             ->toJSON($s, bool => @docview_bools)
547             ->toJSON($s, int => qw/group_level limit skip/);
548             }
549              
550             $couch
551             ->check($s->{attachments}, introduced => '1.6.0', 'Search attribute "attachments"')
552             ->check($s->{att_encoding_info}, introduced => '1.6.0', 'Search attribute "att_encoding_info"')
553             ->check($s->{sorted}, introduced => '2.0.0', 'Search attribute "sorted"')
554             ->check($s->{stable}, introduced => '2.1.0', 'Search attribute "stable"')
555 0           ->check($s->{update}, introduced => '2.1.0', 'Search attribute "update"');
556              
557 0           $s;
558             }
559              
560              
561             sub __findRow($$%)
562 0     0     { my ($self, $result, $index, %args) = @_;
563 0 0         my $answer = $result->answer->{docs}[$index] or return ();
564 0           my $values = $result->values->{docs}[$index];
565              
566             ( answer => $answer,
567             values => $values,
568             docdata => $values,
569 0           docparams => { local => $args{local}, db => $self },
570             );
571             }
572              
573             sub find($%)
574 0     0 1   { my ($self, $search, %args) = @_;
575              
576 0           my $part = delete $args{partition};
577 0   0       $search->{selector} ||= {};
578              
579 0           my $path = $self->_pathToDB;
580 0 0         $path .= '/_partition/'. uri_escape($part) if $part;
581              
582             $self->couch->call(POST => "$path/_find",
583             send => $self->_findPrepare(POST => $search),
584             $self->couch->_resultsPaging(\%args,
585 0     0     on_row => sub { $self->__findRow(@_) },
586 0           ),
587             );
588             }
589              
590             sub _findPrepare($$)
591 0     0     { my ($self, $method, $data, $where) = @_;
592 0           my $s = +{ %$data }; # no nesting
593              
594 0 0         $method eq 'POST' or panic;
595              
596             $self->couch
597             ->toJSON($s, bool => qw/conflicts update stable execution_stats/)
598             ->toJSON($s, int => qw/limit sip r/)
599             #XXX Undocumented when this got deprecated
600 0           ->check(exists $s->{stale}, deprecated => '3.0.0', 'Database find(stale)');
601              
602 0           $s;
603             }
604              
605              
606             sub findExplain(%)
607 0     0 1   { my ($self, $search, %args) = @_;
608 0           my $part = delete $args{partition};
609 0   0       $search->{selector} ||= {};
610              
611 0           my $path = $self->_pathToDB;
612 0 0         $path .= '/_partition/' . uri_escape($part) if $part;
613              
614 0           $self->couch->call(POST => "$path/_explain",
615             send => $self->_findPrepare(POST => $search),
616             $self->couch->_resultsConfig(\%args),
617             );
618             }
619              
620             1;