File Coverage

blib/lib/CouchDB/Client/DB.pm
Criterion Covered Total %
statement 18 179 10.0
branch 0 82 0.0
condition 0 21 0.0
subroutine 6 26 23.0
pod 20 20 100.0
total 44 328 13.4


line stmt bran cond sub pod time code
1              
2             package CouchDB::Client::DB;
3              
4 3     3   1491 use strict;
  3         6  
  3         108  
5 3     3   16 use warnings;
  3         4  
  3         144  
6              
7             our $VERSION = $CouchDB::Client::VERSION;
8              
9 3     3   15 use Carp qw(confess);
  3         5  
  3         149  
10 3     3   13 use URI::Escape qw(uri_escape_utf8);
  3         6  
  3         149  
11 3     3   1249 use CouchDB::Client::Doc;
  3         8  
  3         84  
12 3     3   1268 use CouchDB::Client::DesignDoc;
  3         7  
  3         9870  
13              
14             sub new {
15 0     0 1   my $class = shift;
16 0 0         my %opt = @_ == 1 ? %{$_[0]} : @_;
  0            
17              
18 0 0         $opt{name} || confess "CouchDB database requires a name.";
19 0 0         $opt{client} || confess "CouchDB database requires a client.";
20              
21 0           return bless \%opt, $class;
22             }
23              
24             sub validName {
25 0     0 1   shift;
26 0           my $name = shift;
27 0           return $name =~ m{^[a-z0-9_\$\(\)\+/-]+/$};
28             }
29              
30             sub uriName {
31 0     0 1   my $self = shift;
32 0           my $sn = uri_escape_utf8($self->{name});
33 0           return "$sn";
34             }
35              
36             sub dbInfo {
37 0     0 1   my $self = shift;
38 0           my $res = $self->{client}->req('GET', $self->uriName);
39 0 0         return $res->{json} if $res->{success};
40 0           confess("Connection error: $res->{msg}");
41             }
42              
43             sub create {
44 0     0 1   my $self = shift;
45 0           my $res = $self->{client}->req('PUT', $self->uriName);
46 0 0 0       return $self if $res->{success} and $res->{json}->{ok};
47 0 0         confess("Database '$self->{name}' exists: $res->{msg}") if $res->{status} == 409;
48 0           confess("Connection error: $res->{msg}");
49             }
50              
51             sub delete {
52 0     0 1   my $self = shift;
53 0           my $res = $self->{client}->req('DELETE', $self->uriName);
54 0 0 0       return 1 if $res->{success} and $res->{json}->{ok};
55 0 0         confess("Database '$self->{name}' not found: $res->{msg}") if $res->{status} == 404;
56 0           confess("Connection error: $res->{msg}");
57             }
58              
59             sub replicate {
60 0     0 1   my $self = shift;
61 0           my %args = @_;
62              
63 0           my $name = $self->{name};
64 0           $name =~ s/\/$//;
65              
66 0           my $json;
67 0 0 0       if (defined($args{source}) && defined($args{target})) {
    0          
    0          
68 0           confess("Source and target can't be used at the sametime.");
69             }
70             elsif (defined($args{source})) {
71 0           $json->{source} = $args{source}; # pull replication
72 0           $json->{target} = $name;
73             }
74             elsif (defined($args{target})) {
75 0           $json->{source} = $name; # push replication
76 0           $json->{target} = $args{target};
77             }
78             else {
79 0           confess("Either source or target is required.");
80             }
81              
82 0           my @flags = ('continuous');
83              
84 0           my ($M,$m,undef) = split(/\./,$self->{client}->serverInfo()->{version});
85 0 0         if ($m > 10) {
86             # This flag was added after v0.10
87 0           push(@flags,'create_target');
88             }
89              
90 0           foreach (@flags) {
91 0 0 0       $json->{$_} = (defined($args{$_}) && $args{$_})?$self->{client}->{json}->true:$self->{client}->{json}->false;
92             }
93              
94 0           my $res = $self->{client}->req('POST','_replicate',$json);
95 0 0         confess("Error replicating database: $res->{msg}") if $res->{status} >= 300;
96              
97 0           return $res->{json};
98             }
99              
100             sub newDoc {
101 0     0 1   my $self = shift;
102 0           my $id = shift;
103 0           my $rev = shift;
104 0           my $data = shift;
105 0           my $att = shift;
106 0           return CouchDB::Client::Doc->new(id => $id, rev => $rev, data => $data, attachments => $att, db => $self);
107             }
108              
109             sub listDocIdRevs {
110 0     0 1   my $self = shift;
111 0           my %args = @_;
112 0 0         my $qs = %args ? $self->argsToQuery(%args) : '';
113 0           my $res = $self->{client}->req('GET', $self->uriName . '/_all_docs' . $qs);
114 0 0         confess("Connection error: $res->{msg}") unless $res->{success};
115             return [
116 0 0         map {
117 0           {
118             id => $_->{id},
119             rev => ($_->{value}->{rev})? # The correct key may be version specific;
120             $_->{value}->{rev}: # v0.10.1 returns rev under this key,
121             $_->{value}->{_rev} # older versions may return it here.
122             }
123 0           } @{$res->{json}->{rows}}];
124             }
125              
126             sub listDocs {
127 0     0 1   my $self = shift;
128 0           my %args = @_;
129 0           return [ map { $self->newDoc($_->{id}, $_->{rev}) } @{$self->listDocIdRevs(%args)} ];
  0            
  0            
130             }
131              
132             sub docExists {
133 0     0 1   my $self = shift;
134 0           my $id = shift;
135 0           my $rev = shift;
136 0 0         if ($rev) {
137 0 0         return (grep { $_->{id} eq $id and $_->{rev} eq $rev } @{$self->listDocIdRevs}) ? 1 : 0;
  0 0          
  0            
138             }
139             else {
140 0 0         return (grep { $_->{id} eq $id } @{$self->listDocIdRevs}) ? 1 : 0;
  0            
  0            
141             }
142             }
143              
144             sub newDesignDoc {
145 0     0 1   my $self = shift;
146 0           my $id = shift;
147 0           my $rev = shift;
148 0           my $data = shift;
149 0           return CouchDB::Client::DesignDoc->new(id => $id, rev => $rev, data => $data, db => $self);
150             }
151              
152             sub listDesignDocIdRevs {
153 0     0 1   my $self = shift;
154 0           my %args = @_;
155 0           return [grep { $_->{id} =~ m{^_design/} } @{$self->listDocIdRevs(%args)}];
  0            
  0            
156             }
157              
158             sub listDesignDocs {
159 0     0 1   my $self = shift;
160 0           my %args = @_;
161 0           return [ map { $self->newDesignDoc($_->{id}, $_->{rev}) } @{$self->listDesignDocIdRevs(%args)} ];
  0            
  0            
162             }
163              
164             sub designDocExists {
165 0     0 1   my $self = shift;
166 0           my $id = shift;
167 0           my $rev = shift;
168 0 0         $id = "_design/$id" unless $id =~ m{^_design/};
169 0 0         if ($rev) {
170 0 0         return (grep { $_->{id} eq $id and $_->{rev} eq $rev } @{$self->listDesignDocIdRevs}) ? 1 : 0;
  0 0          
  0            
171             }
172             else {
173 0 0         return (grep { $_->{id} eq $id } @{$self->listDesignDocIdRevs}) ? 1 : 0;
  0            
  0            
174             }
175             }
176              
177             sub tempView {
178 0     0 1   my $self = shift;
179 0           my $view = shift;
180 0           my $res = $self->{client}->req('POST', $self->uriName . '/_temp_view', $view);
181 0 0         return $res->{json} if $res->{success};
182 0           confess("Connection error: $res->{msg}");
183             }
184              
185             sub bulkStore {
186 0     0 1   my $self = shift;
187 0           my $docs = shift;
188 0           my $json = { docs => [map { $_->contentForSubmit } @$docs] };
  0            
189 0           my $res = $self->{client}->req('POST', $self->uriName . '/_bulk_docs', $json);
190 0 0         confess("Connection error: $res->{msg}") unless $res->{success};
191 0           my $i = 0;
192              
193             # versions prior to 0.9 returned the results under the new_revs key,
194             # newer versions just return an array.
195 0 0         my $list = (ref($res->{json}) eq "ARRAY")?$res->{json}:$res->{json}->{new_revs};
196 0           for my $ok (@{$list}) {
  0            
197 0           my $doc = $docs->[$i];
198 0 0         $doc->{id} = $ok->{id} unless $doc->{id};
199 0           $doc->{rev} = $ok->{rev};
200 0           $i++;
201             }
202 0 0         return $res->{json} if $res->{success};
203             }
204              
205             sub bulkDelete {
206 0     0 1   my $self = shift;
207 0           my $docs = shift;
208 0           my $json = { docs => [map { my $cnt = $_->contentForSubmit; $cnt->{_deleted} = $self->{client}->{json}->true; $cnt; } @$docs] };
  0            
  0            
  0            
209 0           my $res = $self->{client}->req('POST', $self->uriName . '/_bulk_docs', $json);
210 0 0         confess("Connection error: $res->{msg}") unless $res->{success};
211 0           my $i = 0;
212              
213             # versions prior to 0.9 returned the results under the new_revs key,
214             # newer versions just return an array.
215 0 0         my $list = (ref($res->{json}) eq "ARRAY")?$res->{json}:$res->{json}->{new_revs};
216 0           for my $ok (@{$list}) {
  0            
217 0           my $doc = $docs->[$i];
218 0           $doc->{deletion_stub_rev} = $ok->{rev};
219 0           $doc->{rev} = '';
220 0           $doc->data({});
221 0           $doc->attachments({});
222 0           $i++;
223             }
224 0 0         return $res->{json} if $res->{success};
225             }
226              
227             # from docs
228             # key=keyvalue
229             # startkey=keyvalue
230             # startkey_docid=docid
231             # endkey=keyvalue
232             # count=max rows to return
233             # update=false
234             # descending=true
235             # skip=rows to skip
236             sub fixViewArgs {
237 0     0 1   my $self = shift;
238 0           my %args = @_;
239              
240 0           for my $k (keys %args) {
241 0 0 0       if ($k eq 'key' or $k eq 'startkey' or $k eq 'endkey') {
    0 0        
    0          
242 0 0 0       if (ref($args{$k}) eq 'ARRAY' or ref($args{$k}) eq 'HASH') {
243 0           $args{$k} = $self->{client}->{json}->encode($args{$k});
244             }
245             else {
246 0 0         unless ($args{$k} =~ /^\d+(?:\.\d+)*$/s) {
247 0           $args{$k} = '"' . $args{$k} . '"';
248             }
249             }
250             }
251             elsif ($k eq 'descending') {
252 0 0         if ($args{$k}) {
253 0           $args{$k} = 'true';
254             }
255             else {
256 0           delete $args{$k};
257             }
258             }
259             elsif ($k eq 'update') {
260 0 0         if ($args{$k}) {
261 0           delete $args{$k};
262             }
263             else {
264 0           $args{$k} = 'false';
265             }
266             }
267             }
268 0           return %args;
269             }
270              
271             sub argsToQuery {
272 0     0 1   my $self = shift;
273 0           my %args = @_;
274 0           %args = $self->fixViewArgs(%args);
275 0           return '?' .
276             join '&',
277 0           map { uri_escape_utf8($_) . '=' . uri_escape_utf8($args{$_}) }
278             keys %args;
279             }
280              
281             1;
282              
283             =pod
284              
285             =head1 NAME
286              
287             CouchDB::Client::DB - CouchDB::Client database
288              
289             =head1 SYNOPSIS
290              
291             use CouchDB::Client;
292             my $c = CouchDB::Client->new(uri => 'https://dbserver:5984/');
293             my $db = $c->newDB('my-stuff')->create;
294             $db->dbInfo;
295             my $doc = $db->newDoc('dahut.svg', undef, { foo => 'bar' })->create;
296             my $dd = $db->newDesignDoc('dahut.svg', undef, $myViews)->create;
297             #...
298             $db->delete;
299              
300             =head1 DESCRIPTION
301              
302             This module represents databases in the CouchDB database.
303              
304             We don't currently handle the various options available on listing all documents.
305              
306             =head1 METHODS
307              
308             =over 8
309              
310             =item new
311              
312             Constructor. Takes a hash or hashref of options, both of which are required:
313             C being the name of the DB (do not escape it, that is done internally,
314             however the name isn't validated, you can use C for that) and C
315             being a reference to the parent C. It is not expected that
316             you would use this constructor directly, but rather that would would go through
317             C<<< Couch::Client->newDB >>>.
318              
319             =item validName $NAME
320              
321             Returns true if the name is a valid CouchDB database name, false otherwise.
322              
323             =item dbInfo
324              
325             Returns metadata that CouchDB maintains about its databases as a Perl structure.
326             It will throw an exception if it can't connect. Typically it will look like:
327              
328             {
329             db_name => "dj",
330             doc_count => 5,
331             doc_del_count => 0,
332             update_seq => 13,
333             compact_running => 0,
334             disk_size => 16845,
335             }
336              
337             =item create
338              
339             Performs the actual creation of a database. Returns the object itself upon success.
340             Throws an exception if it already exists, or for connection problems.
341              
342             =item delete
343              
344             Deletes the database. Returns true on success. Throws an exception if
345             the DB can't be found, or for connection problems.
346              
347             =item replicate %ARGS
348              
349             Sets up replication between two databases. Setting C to a database url (either local or remote)
350             replicates this database into one specified by the url. Conversely, setting C to a database url
351             replicates that database into the current one. In CouchDB terminology, C and C, respectively,
352             set up "push" and "pull" replication.
353              
354             Either C or C is required; both can't be used at the same time.
355              
356             By default, replication is a one time event. New modifications to the origin database do not automatically
357             appear in the replicated database. Setting C to a true value will cause new changes in
358             the origin database to be reflected in the replicated one.
359              
360             Note: Support for the C flag (which was added after version 0.10) is included, but untested.
361              
362             =item newDoc $ID?, $REV?, $DATA?, $ATTACHMENTS?
363              
364             Returns a new C object, optionally with the given ID, revision, data,
365             and attachments. Note that this does not create the actual document, simply the object. For
366             constraints on these fields please look at C<<new>>>
367              
368             =item listDocIdRevs %ARGS?
369              
370             Returns an arrayref containing the ID and revision of all documents in this DB as hashrefs
371             with C and C keys. Throws an exception if there's a problem. Takes an optional hash
372             of arguments matching those understood by CouchDB queries.
373              
374             =item listDocs %ARGS?
375              
376             The same as above, but returns an arrayref of C objects.
377             Takes an optional hash of arguments matching those understood by CouchDB queries.
378              
379             =item docExists $ID, $REV?
380              
381             Takes an ID and an optional revision and returns true if there is a document with that ID
382             in this DB, false otherwise. If the revision is provided, note that this will match only if
383             there is a document with the given ID B its latest revision is the same as the given
384             one.
385              
386             =item newDesignDoc $ID?, $REV?, $DATA?
387              
388             Same as above, but instantiates design documents.
389              
390             =item listDesignDocIdRevs %ARGS?
391              
392             Same as above, but only matches design documents.
393              
394             =item listDesignDocs %ARGS?
395              
396             Same as above, but only matches design documents.
397              
398             =item designDocExists $ID, $REV?
399              
400             Same as above, but only matches design documents.
401              
402             =item tempView $VIEW
403              
404             Given a view (defined as a hash with the fields that CouchDB expects from the corresponding
405             JSON), will run it and return the CouchDB resultset. Throws an exception if there is a
406             connection error.
407              
408             =item bulkStore \@DOCS
409              
410             Takes an arrayref of Doc objects and stores them on the server (creating or updating them
411             depending on whether they exist or not). Returns the data structure that CouchDB returns
412             on success (which is of limited interest as this client already updates all documents so
413             that their ID and revisions are correct after this operation), and throws an exception
414             upon failure.
415              
416             =item bulkDelete \@DOCS
417              
418             Same as above but performs mass deletion of documents. Note that using bulkStore you could
419             also obtain the same effect by setting a C<_deleted> field to true on your objects but
420             that is not recommended as fields that begin with an underscore are reserved by CouchDB.
421              
422             =item uriName
423              
424             Returns the name of the database escaped.
425              
426             =item fixViewArgs %ARGS
427              
428             Takes a hash of view parameters expressed in a Perlish fashion (e.g. 1 for true or an arrayref
429             for multi-valued keys) and returns a hash with the same options turned into what CouchDB
430             understands.
431              
432             =item argsToQuery %ARGS
433              
434             Takes a hash of view parameters, runs them through C, and returns a query
435             string (complete with leading '?') to pass on to CouchDB.
436              
437             =back
438              
439             =head1 AUTHOR
440              
441             Robin Berjon,
442             Maverick Edwards, (current maintainer)
443              
444             =head1 BUGS
445              
446             Please report any bugs or feature requests to bug-couchdb-client at rt.cpan.org, or through the
447             web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CouchDB-Client.
448              
449             =head1 COPYRIGHT & LICENSE
450              
451             Copyright 2008 Robin Berjon, all rights reserved.
452              
453             This library is free software; you can redistribute it and/or modify it under the same terms as
454             Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may
455             have available.
456              
457             =cut