File Coverage

blib/lib/Couch/DB/Document.pm
Criterion Covered Total %
statement 24 183 13.1
branch 0 86 0.0
condition 0 42 0.0
subroutine 8 58 13.7
pod 32 33 96.9
total 64 402 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::Document;{
13             our $VERSION = '0.201';
14             }
15              
16              
17 1     1   1132 use warnings;
  1         2  
  1         64  
18 1     1   6 use strict;
  1         2  
  1         28  
19              
20 1     1   4 use Couch::DB::Util;
  1         2  
  1         7  
21              
22 1     1   8 use Log::Report 'couch-db';
  1         3  
  1         9  
23 1     1   363 use Scalar::Util qw/weaken/;
  1         3  
  1         83  
24 1     1   8 use MIME::Base64 qw/decode_base64/;
  1         2  
  1         83  
25 1     1   8 use Devel::GlobalDestruction qw/in_global_destruction/;
  1         12  
  1         12  
26              
27             #--------------------
28              
29 0     0 1   sub new(@) { my ($class, %args) = @_; (bless {}, $class)->init(\%args) }
  0            
30              
31             sub init($)
32 0     0 0   { my ($self, $args) = @_;
33 0           $self->{CDD_id} = delete $args->{id};
34 0           $self->{CDD_db} = my $db = delete $args->{db};
35 0           $self->{CDD_info} = {};
36 0 0         $self->{CDD_batch} = exists $args->{batch} ? delete $args->{batch} : $db->batch;
37 0           $self->{CDD_revs} = my $revs = {};
38 0           $self->{CDD_local} = delete $args->{local};
39              
40 0           $self->{CDD_couch} = $db->couch;
41 0           weaken $self->{CDD_couch};
42              
43 0 0         if(my $content = delete $args->{content})
44 0           { $revs->{_new} = $content;
45             }
46              
47             # The Document is (for now) not linked to its Result source, because
48             # that might consume a lot of memory. Although it may help debugging.
49             # weaken $self->{CDD_result} = my $result = delete $args->{result};
50              
51 0           $self->row(delete $args->{row});
52 0           $self;
53             }
54              
55             sub DESTROY()
56 0     0     { my $self = shift;
57 0 0 0       $self->{CDD_revs}{_new} || ! in_global_destruction
58             or panic "Unsaved new document.";
59             }
60              
61             sub _consume($$)
62 0     0     { my ($self, $result, $data) = @_;
63 0           my $id = $self->{CDD_id} = delete $data->{_id};
64 0           my $rev = delete $data->{_rev};
65              
66             # Add all received '_' labels to the existing info.
67 0   0       my $info = $self->{CDD_info} ||= {};
68             $info->{$_} = delete $data->{$_}
69 0           for grep /^_/, keys %$data;
70              
71 0   0       my $attdata = $self->{CDD_atts} ||= {};
72 0 0         if(my $atts = $info->{_attachments})
73 0           { foreach my $name (keys %$atts)
74 0           { my $details = $atts->{$name};
75             $attdata->{$name} = $self->couch->_attachment($result->response, $name)
76 0 0         if $details->{follows};
77              
78             # Remove sometimes large data
79             $attdata->{$name} = decode_base64 delete $details->{data} #XXX need decompression?
80 0 0         if defined $details->{data};
81             }
82             }
83 0           $self->{CDD_revs}{$rev} = $data;
84 0           $self;
85             }
86              
87              
88             sub fromResult($$$%)
89 0     0 1   { my ($class, $result, $data, %args) = @_;
90 0           $class->new(%args, result => $result)->_consume($result, { %$data });
91             }
92              
93             #--------------------
94              
95 0     0 1   sub id() { $_[0]->{CDD_id} }
96 0     0 1   sub db() { $_[0]->{CDD_db} }
97 0     0 1   sub batch() { $_[0]->{CDD_batch} }
98 0     0 1   sub couch() { $_[0]->{CDD_couch} }
99              
100             sub _pathToDoc(;$)
101 0     0     { my ($self, $path) = @_;
102 0 0         if($self->isLocal)
103 0 0         { $path and panic "Local documents not supported with path '$path'";
104 0           return $self->db->_pathToDB('_local/' . $self->id);
105             }
106 0 0         $self->db->_pathToDB($self->id . (defined $path ? "/$path" : ''));
107             }
108              
109             sub _deleted($)
110 0     0     { my ($self, $rev) = @_;
111 0           $self->{CDD_revs}{$rev} = {};
112 0           $self->{CDD_deleted} = 1;
113             }
114              
115             sub _saved($$;$)
116 0     0     { my ($self, $id, $rev, $data) = @_;
117 0   0       $self->{CDD_id} ||= $id;
118 0   0       $self->{CDD_revs}{$rev} = $data || delete $self->{CDD_revs}{_new};
119             }
120              
121              
122             sub row(;$)
123 0     0 1   { my $self = shift;
124 0 0         @_ or return $self->{CDD_row};
125              
126 0           $self->{CDD_row} = shift;
127 0           weaken($self->{CDD_row});
128 0           $self->{CDD_row};
129             }
130              
131             #--------------------
132              
133 0     0 1   sub isLocal() { $_[0]->{CDD_local} }
134              
135              
136 0     0 1   sub isDeleted() { $_[0]->{CDD_deleted} }
137              
138              
139 0     0 1   sub revision($) { $_[0]->{CDD_revs}{$_[1]} }
140              
141              
142 0     0 1   sub latest() { $_[0]->revision(($_[0]->revisions)[0]) }
143              
144              
145              
146             sub revisions()
147 0     0 1   { my $revs = $_[0]->{CDD_revs};
148 1     1   1643 no warnings 'numeric'; # forget the "-hex" part of the rev
  1         2  
  1         2969  
149 0           sort {$b <=> $a} keys %$revs;
  0            
150             }
151              
152              
153 0     0 1   sub rev() { ($_[0]->revisions)[0] }
154              
155             #--------------------
156              
157 0 0   0     sub _info() { $_[0]->{CDD_info} or panic "no info yet." }
158              
159              
160 0 0   0 1   sub conflicts() { @{ $_[0]->_info->{_conflicts} || [] } }
  0            
161 0 0   0 1   sub deletedConflicts() { @{ $_[0]->_info->{_deleted_conflicts} || [] } }
  0            
162 0     0 1   sub updateSequence() { $_[0]->_info->{_local_seq} }
163              
164              
165             sub revisionsInfo()
166 0     0 1   { my $self = shift;
167 0 0         return $self->{CDD_revinfo} if $self->{CDD_revinfo};
168              
169             my $c = $self->_info->{_revs_info}
170 0 0         or error __x"you have requested the open_revs detail for the document yet.";
171              
172 0           $self->{CDD_revinfo} = +{ map +($_->{rev} => $_), @$c };
173             }
174              
175              
176 0     0 1   sub revisionInfo($) { $_[0]->revisionsInfo->{$_[1]} }
177              
178             #--------------------
179              
180             sub exists(%)
181 0     0 1   { my ($self, %args) = @_;
182              
183 0           $self->couch->call(HEAD => $self->_pathToDoc,
184             $self->couch->_resultsConfig(\%args),
185             );
186             }
187              
188              
189             sub __created($$)
190 0     0     { my ($self, $result, $data) = @_;
191 0 0         $result or return;
192              
193 0           my $v = $result->values;
194 0 0         $v->{ok} or return;
195              
196 0           delete $data->{_id}; # do not polute the data
197 0           $self->_saved($v->{id}, $v->{rev}, $data);
198             }
199              
200             sub create($%)
201 0     0 1   { my ($self, $data, %args) = @_;
202 0 0         ref $data eq 'HASH' or panic "Attempt to create document without data.";
203              
204 0           my %query;
205             $query{batch} = 'ok'
206 0 0         if exists $args{batch} ? delete $args{batch} : $self->batch;
    0          
207              
208             # When the _id is (accidentally) undef, no new one will be picked
209 0   0       $data->{_id} ||= $self->id;
210 0 0         defined $data->{_id} or delete $data->{_id};
211              
212             $self->couch->call(POST => $self->db->_pathToDB, # !!
213             send => $data,
214             query => \%query,
215             $self->couch->_resultsConfig(\%args,
216 0     0     on_final => sub { $self->__created($_[0], $data) },
217 0           ),
218             );
219             }
220              
221              
222             sub update($%)
223 0     0 1   { my ($self, $data, %args) = @_;
224 0 0         ref $data eq 'HASH' or panic "Attempt to update the document without data.";
225              
226 0           my $couch = $self->couch;
227              
228 0           my %query;
229 0 0         $query{batch} = 'ok' if exists $args{batch} ? delete $args{batch} : $self->batch;
    0          
230 0   0       $query{rev} = delete $args{rev} || $self->rev;
231 0 0         $query{new_edits} = delete $args{new_edits} if exists $args{new_edits};
232 0           $couch->toQuery(\%query, bool => qw/new_edits/);
233              
234             $couch->call(PUT => $self->_pathToDoc,
235             query => \%query,
236             send => $data,
237 0     0     $couch->_resultsConfig(\%args, on_final => sub { $self->__created($_[0], $data) }),
  0            
238             );
239             }
240              
241              
242             sub __get($$)
243 0     0     { my ($self, $result, $flags) = @_;
244 0 0         $result or return; # do nothing on unsuccessful access
245 0           $self->_consume($result, $result->answer);
246              
247             # meta is a shortcut for other flags
248             $flags->{conflicts} = $flags->{deleted_conflicts} = $flags->{revs_info} = 1
249 0 0         if $flags->{meta};
250              
251 0           $self->{CDD_flags} = $flags;
252             }
253              
254             sub get(%)
255 0     0 1   { my ($self, $flags, %args) = @_;
256 0           my $couch = $self->couch;
257              
258 0 0         my %query = $flags ? %$flags : ();
259 0           $couch->toQuery(\%query,
260             bool => qw/attachments att_encoding_info conflicts deleted_conflicts latest local_seq meta revs revs_info/);
261              
262             $couch->call(GET => $self->_pathToDoc,
263             query => \%query,
264             $couch->_resultsConfig(\%args,
265 0     0     on_final => sub { $self->__get($_[0], $flags) },
266 0 0         _headers => { Accept => $args{attachments} ? 'multipart/related' : 'application/json' },
267             ),
268             );
269             }
270              
271              
272             sub __delete($)
273 0     0     { my ($self, $result) = @_;
274 0 0         $result or return;
275              
276 0           my $v = $result->values;
277 0 0         $self->_deleted($v->{rev}) if $v->{ok};
278             }
279              
280             sub delete(%)
281 0     0 1   { my ($self, %args) = @_;
282 0           my $couch = $self->couch;
283              
284 0           my %query;
285 0 0         $query{batch} = 'ok' if exists $args{batch} ? delete $args{batch} : $self->batch;
    0          
286 0   0       $query{rev} = delete $args{rev} || $self->rev;
287              
288             $couch->call(DELETE => $self->_pathToDoc,
289             query => \%query,
290 0     0     $couch->_resultsConfig(\%args, on_final => sub { $self->__delete($_[0]) }),
  0            
291             );
292             }
293              
294              
295             # Not yet implemented. I don't like chaning the headers of my generic UA.
296             sub cloneInto($%)
297 0     0 1   { my ($self, $to, %args) = @_;
298 0           my $couch = $self->couch;
299              
300 0           my %query;
301 0 0         $query{batch} = 'ok' if exists $args{batch} ? delete $args{batch} : $self->batch;
    0          
302 0   0       $query{rev} = delete $args{rev} || $self->rev;
303              
304             #XXX still work to do on updating the admin in 'to'
305             $couch->call(COPY => $self->_pathToDoc,
306             query => \%query,
307             $couch->_resultsConfig(\%args,
308 0     0     on_final => sub { $self->__delete($_[0]) },
309 0           _headers => +{ Destination => $to->id },
310             ),
311             );
312             }
313              
314              
315             sub appendTo($%)
316 0     0 1   { my ($self, $to, %args) = @_;
317 0           my $couch = $self->couch;
318              
319 0           my %query;
320 0 0         $query{batch} = 'ok' if exists $args{batch} ? delete $args{batch} : $self->batch;
    0          
321 0   0       $query{rev} = delete $args{rev} || $self->rev;
322              
323             #XXX still work to do on updating the admin in 'to'
324 0 0         my $dest_rev = $to->rev or panic "No revision for destination document.";
325              
326             $couch->call(COPY => $self->_pathToDoc,
327             query => \%query,
328             $couch->_resultsConfig(\%args,
329 0     0     on_final => sub { $self->__delete($_[0]) },
330 0           _headers => +{ Destination => $to->id . "?rev=$dest_rev" },
331             ),
332             );
333             }
334              
335              
336             #--------------------
337              
338 0     0 1   sub attInfo($) { $_[0]->_info->{_attachments}{$_[1]} }
339 0     0 1   sub attachments() { keys %{$_[0]->_info->{_attachments}} }
  0            
340 0     0 1   sub attachment($) { $_[0]->{CDD_atts}{$_[1]} }
341              
342              
343             sub attExists($%)
344 0     0 1   { my ($self, $name, %args) = @_;
345 0   0       my %query = ( rev => delete $args{rev} || $self->rev );
346              
347 0           $self->couch->call(HEAD => $self->_pathToDoc($name),
348             query => \%query,
349             $self->couch->_resultsConfig(\%args),
350             );
351             }
352              
353              
354             sub __attLoad($$)
355 0     0     { my ($self, $result, $name) = @_;
356 0 0         $result or return;
357 0           my $data = $self->couch->_messageContent($result->response);
358 0           $self->_info->{_attachments}{$name} = { length => length $data };
359 0           $self->{CDD_atts}{$name} = $data;
360             }
361              
362             sub attLoad($%)
363 0     0 1   { my ($self, $name, %args) = @_;
364 0   0       my %query = (rev => delete $args{rev} || $self->rev);
365              
366             $self->couch->call(GET => $self->_pathToDoc($name),
367             query => \%query,
368             $self->couch->_resultsConfig(\%args,
369 0     0     on_final => sub { $self->__attLoad($_[0], $name) },
370 0           ),
371             );
372             }
373              
374              
375             sub attSave($$%)
376 0     0 1   { my ($self, $name, $data, %args) = @_;
377              
378 0   0       my $type = delete $args{type} || 'application/octet-stream';
379 0   0       my %query = (rev => delete $args{rev} || $self->rev);
380 0 0         $query{batch} = 'ok' if exists $args{batch} ? delete $args{batch} : $self->batch;
    0          
381              
382 0           $self->couch->call(PUT => $self->_pathToDoc($name),
383             query => \%query,
384             send => $data,
385             $self->couch->_resultsConfig(\%args,
386             _headers => { 'Content-Type' => $type },
387             ),
388             );
389             }
390              
391              
392             sub attDelete($$$%)
393 0     0 1   { my ($self, $name, %args) = @_;
394 0   0       my %query = (rev => delete $args{rev} || $self->rev);
395 0 0         $query{batch} = 'ok' if exists $args{batch} ? delete $args{batch} : $self->batch;
    0          
396              
397 0           $self->couch->call(DELETE => $self->_pathToDoc($name),
398             query => \%query,
399             $self->couch->_resultsConfig(\%args),
400             );
401             }
402              
403             1;