File Coverage

blib/lib/DBIx/CouchLike.pm
Criterion Covered Total %
statement 27 356 7.5
branch 0 132 0.0
condition 0 34 0.0
subroutine 9 45 20.0
pod 3 22 13.6
total 39 589 6.6


line stmt bran cond sub pod time code
1             package DBIx::CouchLike;
2              
3 1     1   12 use 5.8.1;
  1         4  
  1         44  
4 1     1   5 use strict;
  1         2  
  1         38  
5 1     1   4 use warnings;
  1         1  
  1         32  
6 1     1   5 use Carp;
  1         1  
  1         104  
7 1     1   963 use JSON 2.0 ();
  1         29002  
  1         41  
8 1     1   944 use UNIVERSAL::require;
  1         1826  
  1         14  
9 1     1   39 use base qw/ Class::Accessor::Fast /;
  1         3  
  1         1131  
10 1     1   3850 use DBIx::CouchLike::Iterator;
  1         5  
  1         14  
11 1     1   760 use DBIx::CouchLike::Sth;
  1         3  
  1         10  
12              
13             our $VERSION = '0.16';
14             our $RD;
15             __PACKAGE__->mk_accessors(qw/ dbh table utf8 _json trace versioning /);
16              
17             sub new {
18 0     0 1   my $class = shift;
19 0           my $self = $class->SUPER::new(@_);
20 0 0         $self->{utf8} = 1 unless defined $self->{utf8};
21              
22 0           $self->{_json} = JSON->new;
23 0           $self->{_json}->utf8( $self->{utf8} );
24 0 0 0       _setup_downgrade() if !$self->{utf8} && !$RD;
25 0           $self;
26             }
27              
28             sub _setup_downgrade {
29 0     0     require Unicode::RecursiveDowngrade;
30 0           $RD = Unicode::RecursiveDowngrade->new;
31             $Unicode::RecursiveDowngrade::DowngradeFunc
32 0     0     = sub { utf8::downgrade($_[0]); $_[0] };
  0            
  0            
33             }
34              
35             sub utf8 {
36 0     0 0   my $self = shift;
37 0 0         if (@_) {
38 0           $self->{utf8} = shift;
39 0           $self->{_json}->utf8( $self->{utf8} );
40 0 0 0       _setup_downgrade() if !$self->{utf8} && !$RD;
41             }
42 0           $self->{utf8};
43             }
44              
45             sub to_json {
46 0     0 0   my $self = shift;
47 0           my $json = $self->{_json}->encode(shift);
48 0 0         utf8::downgrade($json) unless $self->{utf8};
49 0           $json;
50             }
51              
52             sub from_json {
53 0     0 0   my $self = shift;
54 0           my $json = shift;
55 0 0         $self->{utf8} ? $self->{_json}->decode($json)
56             : $RD->downgrade( $self->{_json}->decode($json) );
57             }
58              
59             sub id_generator {
60 0     0 0   my $self = shift;
61 0 0         if (@_) {
62 0           $self->{id_generator} = shift;
63             }
64 0   0       $self->{id_generator} ||= do {
65 0           my $gen;
66 0           eval {
67 0           require Data::YUID::Generator;
68 0           $gen = Data::YUID::Generator->new;
69             };
70 0 0         unless ($gen) {
71 0           require DBIx::CouchLike::IdGenerator;
72 0           $gen = DBIx::CouchLike::IdGenerator->new;
73             }
74 0           $gen;
75             };
76             }
77              
78             sub sub_class {
79 0     0 0   my $self = shift;
80 0           return ref($self) . "::" . $self->dbh->{Driver}->{Name};
81             }
82              
83             sub create_table {
84 0     0 0   my $self = shift;
85 0 0 0       return if !$self->table or !$self->dbh;
86              
87 0           my $sub_class = $self->sub_class;
88 0           eval {
89 0           $sub_class->require;
90 0           $sub_class->create_table( $self->dbh, $self->table, $self->{versioning} );
91             };
92 0 0         if ($@) {
93 0           carp( "$@ Unsupported Driver: "
94             . $self->dbh->{Driver}->{Name}
95             . " to create_table()"
96             );
97             }
98 0           1;
99             }
100              
101             sub prepare_sql {
102 0     0 0   my $self = shift;
103 0           my $sql = shift;
104 0           $sql =~ s{_DATA_}{ $self->table . "_data" }eg;
  0            
105 0           $sql =~ s{_MAP_}{ $self->table . "_map" }eg;
  0            
106             return DBIx::CouchLike::Sth->new({
107             trace => $self->{trace},
108             sth => $self->dbh->prepare($sql),
109             sql => $sql,
110 0     0     quote => $self->{trace} ? sub { $self->dbh->quote($_[0]) } : undef,
111 0 0         });
112             }
113              
114             sub get {
115 0     0 1   my $self = shift;
116 0           my $id = shift;
117 0           $self->get_multi($id);
118             }
119              
120             sub get_multi {
121 0     0 0   my $self = shift;
122 0           my @id = @_;
123 0           my $pf = join(",", map {"?"} @id);
  0            
124              
125 0           my $versioning = $self->{versioning};
126 0 0         my $sql = $versioning
127             ? qq{SELECT id, value, version FROM _DATA_ WHERE id IN($pf)}
128             : qq{SELECT id, value FROM _DATA_ WHERE id IN($pf)};
129 0           my $sth = $self->prepare_sql($sql);
130 0           $sth->execute(@id);
131 0           my @res;
132 0           while ( my $r = $sth->fetchrow_arrayref ) {
133 0           my $res = $self->from_json($r->[1]);
134 0           $res->{_id} = $r->[0];
135 0 0         $res->{_version} = $r->[2] if $versioning;
136 0           push @res, $res;
137             }
138 0 0         return wantarray ? @res : $res[0];
139             }
140              
141             sub post {
142 0     0 0   my $self = shift;
143 0 0 0       my ( $id, $value_ref )
144             = ref $_[0] ? ( delete $_[0]->{_id} || $self->id_generator->get_id, $_[0] )
145             : ( $_[0], $_[1] );
146 0           $value_ref->{_id} = $id;
147 0           $self->post_multi($value_ref);
148             }
149              
150             sub post_multi {
151 0     0 0   my $self = shift;
152 0           $self->_post_multi([], @_);
153             }
154              
155             sub _post_multi {
156 0     0     my $self = shift;
157 0           my $views = shift;
158 0           my @value_ref = @_;
159 0           my $versioning = $self->{versioning};
160 0 0         my $sql = $versioning
161             ? q{INSERT INTO _DATA_ (id, value, version) VALUES(?, ?, ?)}
162             : q{INSERT INTO _DATA_ (id, value) VALUES(?, ?)};
163 0           my $sth = $self->prepare_sql($sql);
164 0           my @id;
165 0           for my $value_ref (@value_ref) {
166 0   0       my $id = delete($value_ref->{_id}) || $self->id_generator->get_id;
167 0 0 0       my $version = $versioning ? ( $value_ref->{_version} ||= 0 ) : undef;
168 0           my $json = $self->to_json($value_ref);
169 0           my @param = ($id, $json);
170 0 0         push @param, $version if $versioning;
171 0           $sth->execute(@param);
172              
173 0           $value_ref->{_id} = $id;
174 0 0         $value_ref->{_version} = $version if $versioning;
175 0           push @id, $id;
176             }
177 0           $self->update_views( $views, @value_ref );
178 0 0         return wantarray ? @id : $id[0];
179             }
180              
181             sub put {
182 0     0 0   my $self = shift;
183 0 0         my ( $id, $value_ref )
184             = ref $_[0] ? ( delete $_[0]->{_id}, $_[0] )
185             : ( $_[0], $_[1] );
186 0           $value_ref->{_id} = $id;
187 0           $self->put_multi($value_ref);
188             }
189              
190             sub put_multi {
191 0     0 0   my $self = shift;
192 0           $self->_put_multi([], @_);
193             }
194              
195             sub put_with_views {
196 0     0 0   my $self = shift;
197 0           my @views = map { "_design/$_" } @_;
  0            
198             sub {
199 0     0     $self->_put_multi(\@views, @_);
200 0           };
201             }
202              
203             sub _put_multi {
204 0     0     my $self = shift;
205 0           my $views = shift;
206 0           my @value_ref = @_;
207 0           my $versioning = $self->{versioning};
208 0 0         my $sql = $versioning
209             ? q{UPDATE _DATA_ SET value=?, version=version+1 WHERE id=? AND version=?}
210             : q{UPDATE _DATA_ SET value=? WHERE id=?};
211 0           my $sth = $self->prepare_sql($sql);
212              
213 0           my @post;
214             my @put;
215 0           my @id;
216 0           for my $value_ref (@value_ref) {
217 0           my $id = delete $value_ref->{_id};
218 0           my $put = defined $value_ref->{_version};
219 0 0 0       my $version = $versioning ? ($value_ref->{_version} ||= 0) : undef;
220 0           my $json = $self->to_json($value_ref);
221              
222 0           $value_ref->{_id} = $id;
223 0           my @param = ($json, $id);
224 0 0         push @param, $version if $versioning;
225 0           my $r = $sth->execute(@param);
226 0 0         if ( $r == 0 ) {
227 0 0 0       if ($versioning && $put) {
228 0           croak("Can't put id: $id, version: $version");
229             }
230             else {
231 0           push @post, $value_ref;
232             }
233             }
234             else {
235 0           push @id, $id;
236 0           push @put, $value_ref;
237             }
238             }
239 0           my @new_id;
240 0 0         @new_id = $self->post_multi(@post) if @post;
241 0           $self->update_views( $views, @put );
242 0 0 0       return wantarray ? (@id, @new_id) : $id[0] || $new_id[0];
243             }
244              
245             sub delete {
246 0     0 0   my $self = shift;
247 0           my $id = shift;
248 0           my $sth = $self->prepare_sql(q{DELETE FROM _DATA_ WHERE id=?});
249 0           my $res = $sth->execute($id);
250              
251 0 0         if ( $id =~ qr{^_design/} ) {
252 0           my ($part, @value) = $self->_start_with( design_id => $id );
253 0           my $del_sth = $self->prepare_sql(
254             q{DELETE FROM _MAP_ WHERE } . $part
255             );
256 0           $del_sth->execute(@value);
257             }
258             else {
259 0           my $del_sth = $self->prepare_sql(
260             q{DELETE FROM _MAP_ WHERE id=?}
261             );
262 0           $del_sth->execute($id);
263             };
264 0           return $res;
265             }
266              
267             sub view {
268 0     0 1   my $self = shift;
269 0           my $target = shift;
270 0   0       my $query = shift || {};
271              
272 0 0         $target = "_design/$target"
273             unless $target =~ qr{^_design/};
274              
275 0           my ( undef, $design_id, $name ) = split "/", $target;
276 0 0         my $design = $self->get("_design/$design_id")
277             or return;
278              
279 0           my @param = ($target);
280 0           my $sql = q{
281             SELECT m.id, m.key, m.value _COL_
282             FROM _MAP_ AS m _JOIN_
283             WHERE m.design_id=?};
284 0 0         if ( exists $query->{key} ) {
    0          
    0          
285 0 0         if ( ref $query->{key} eq 'ARRAY' ) {
    0          
    0          
286 0           $sql .= " AND m.key IN ("
287 0           . join(",", map { "?" } @{ $query->{key} })
  0            
288             . ")";
289 0           push @param, @{ $query->{key} };
  0            
290             }
291             elsif ( ref $query->{key} eq 'HASH' ) {
292 0           my %k = %{ $query->{key} };
  0            
293 0           $sql .= sprintf " AND m.key %s ? ", (keys %k)[0];
294 0           push @param, (values %k)[0];
295             }
296             elsif ( ref $query->{key} eq 'SCALAR' ) {
297 0           $sql .= sprintf " AND m.key %s", ${ $query->{key} };
  0            
298             }
299             else {
300 0           $sql .= q{ AND m.key=? };
301 0           push @param, $query->{key};
302             }
303             }
304             elsif ( exists $query->{key_like} ) {
305 0           $sql .= q{ AND m.key LIKE ? };
306 0           push @param, $query->{key_like};
307             }
308             elsif ( exists $query->{key_start_with} ) {
309 0           my ($part, @value)
310             = $self->_start_with("m.key" => $query->{key_start_with});
311 0           $sql .= q{ AND } . $part;
312 0           push @param, @value;
313             }
314              
315 0 0         if ( $query->{include_docs} ) {
316 0           $sql =~ s{_COL_}{, d.value};
317 0           $sql =~ s{_JOIN_}{JOIN _DATA_ AS d USING(id)};
318             }
319             else {
320 0           $sql =~ s{(?:_COL_|_JOIN_)}{}g;
321             }
322              
323 0 0         $sql .= sprintf(
    0          
324             " ORDER BY m.key %s, m.value %s, m.id ",
325             $query->{key_reverse} ? "DESC" : "",
326             $query->{value_reverse} ? "DESC" : "",
327             );
328              
329 0           $sql = $self->_offset_limit_sql( $sql, $query, \@param );
330              
331 0           my $sth = $self->prepare_sql($sql);
332 0           $sth->execute(@param);
333              
334 0           my $itr = DBIx::CouchLike::Iterator->new({
335             sth => $sth,
336             query => $query,
337             reduce => $design->{views}->{$name}->{reduce},
338             couch => $self,
339             });
340 0 0         return wantarray ? $itr->all()
341             : $itr;
342             }
343              
344             sub all_designs {
345 0     0 0   my $self = shift;
346              
347 0           my $sql = "SELECT id, NULL, value FROM _DATA_ WHERE ";
348 0           my ($part, @value) = $self->_start_with( id => "_design/" );
349 0           $sql .= $part . " ORDER BY id";
350              
351 0           my $sth = $self->prepare_sql($sql);
352 0           $sth->execute(@value);
353              
354 0           my $itr = DBIx::CouchLike::Iterator->new({
355             sth => $sth,
356             query => {},
357             couch => $self,
358             });
359 0 0         return wantarray ? $itr->all()
360             : $itr;
361             }
362              
363             sub all {
364 0     0 0   my $self = shift;
365 0   0       my $query = shift || {};
366              
367 0           my @param;
368 0           my $sql = q{SELECT id, NULL, value FROM _DATA_};
369 0 0         if ($query->{id_like}) {
    0          
    0          
    0          
370 0           $sql .= " WHERE id LIKE ?";
371 0           push @param, $query->{id_like};
372             }
373             elsif ($query->{id_start_with}) {
374 0           my ($part, @value)
375             = $self->_start_with( id => $query->{id_start_with} );
376 0           $sql .= " WHERE $part";
377 0           push @param, @value;
378             }
379             elsif ($query->{id_in}) {
380 0           my @id = @{ $query->{id_in} };
  0            
381 0           $sql .= " WHERE id IN (" . join(",", map { "?" } @id) . ")";
  0            
382 0           push @param, @id;
383             }
384             elsif ($query->{exclude_designs}) {
385 0           my ($part, @value) = $self->_start_with( id => "_design/" );
386 0           $sql .= " WHERE NOT $part";
387 0           push @param, @value;
388             }
389              
390 0           $sql .= " ORDER BY id";
391 0 0         $sql .= " DESC" if $query->{reverse};
392              
393 0           $sql = $self->_offset_limit_sql( $sql, $query, \@param );
394 0           my $sth = $self->prepare_sql($sql);
395 0           $sth->execute(@param);
396              
397 0           my $itr = DBIx::CouchLike::Iterator->new({
398             sth => $sth,
399             query => $query,
400             couch => $self,
401             });
402 0 0         return wantarray ? $itr->all()
403             : $itr;
404             }
405              
406             sub post_with_views {
407 0     0 0   my $self = shift;
408 0           my @views = map { "_design/$_" } @_;
  0            
409             sub {
410 0     0     $self->_post_multi(\@views, @_);
411 0           };
412             }
413              
414             sub _start_with {
415 0     0     my $self = shift;
416 0           my $sub_class = $self->sub_class;
417 0           $sub_class->require;
418 0           $sub_class->_start_with(@_);
419             }
420              
421             sub _offset_limit_sql {
422 0     0     my $self = shift;
423              
424 0           my $sub;
425 0 0         if ( $sub = $self->{_offset_limit_sql_sub} ) {
426 0           return $sub->(@_);
427             }
428 0           my $sub_class = $self->sub_class;
429 0           eval {
430 0           $sub_class->require;
431             $sub = $self->{_offset_limit_sql_sub} = sub {
432 0     0     $sub_class->_offset_limit_sql(@_);
433 0           };
434             };
435 0 0         if ( $sub ) {
436 0           return $sub->(@_);
437             }
438              
439 0           carp( "Unsupported Driver: "
440             . $self->dbh->{Driver}->{Name}
441             . " for using limit/offset"
442             );
443             }
444              
445             sub _select_all {
446 0     0     my $self = shift;
447 0           my $sub = shift;
448 0           my $sth = $self->prepare_sql(q{SELECT id, value FROM _DATA_});
449 0           $sth->execute();
450 0           while ( my $r = $sth->fetchrow_arrayref ) {
451 0 0         next if $r->[0] =~ qr{^_design/};
452 0           my $id = $r->[0];
453 0           my $value_ref = $self->from_json($r->[1]);
454 0           $value_ref->{_id} = $id;
455 0 0         $sub->( $id, $value_ref ) if $sub;
456             }
457             }
458              
459             sub create_view {
460 0     0 0   my $self = shift;
461 0           my $design_val = shift;
462 0           my $dbh = $self->dbh;
463              
464 0           my $design_id = delete $design_val->{_id};
465              
466 0           my ($part, @value) = $self->_start_with( design_id => $design_id );
467 0           my $del_sth = $self->prepare_sql(
468             q{DELETE FROM _MAP_ WHERE } . $part
469             );
470 0           $del_sth->execute(@value);
471 0           $design_val->{_id} = $design_id;
472              
473 0 0         my $views = $design_val->{views} or return 1;
474 0           my $index_sth = $self->prepare_sql(
475             q{INSERT INTO _MAP_ (design_id, id, key, value) VALUES (?,?,?,?)}
476             );
477              
478             VIEW:
479 0           for my $name ( keys %$views ) {
480 0 0         my $code = $views->{$name}->{map} or next VIEW;
481 0           my $sub = eval $code; ## no critic
482 0 0         if ($@) {
483 0           warn $@;
484 0           next VIEW;
485             }
486             $self->_select_all(
487             sub {
488 0     0     $self->_data_to_map({
489             sub => $sub,
490             id => $_[0],
491             data_val => $_[1],
492             sth => $index_sth,
493             design_id => "$design_id/$name",
494             });
495             }
496 0           );
497             }
498 0           return 1;
499             }
500              
501             sub update_views {
502 0     0 0   my $self = shift;
503 0           my $dbh = $self->dbh;
504 0   0       my $views = shift || [];
505 0           my @views = @$views;
506              
507 0           my @data_val;
508             my @id;
509 0           for my $data_val (@_) {
510 0 0         if ( $data_val->{_id} =~ qr{^_design/} ) {
511 0           $self->create_view( $data_val );
512             }
513             else {
514 0           push @data_val, $data_val;
515 0           push @id, $data_val->{_id};
516             }
517             }
518 0 0         return 1 unless @data_val;
519              
520 0           my $pf = join(",", map {"?"} @id);
  0            
521 0 0         my $df = @views ? join(",", map {"?"} @views) : "";
  0            
522              
523 0 0         if (@views) {
524 0           my $del_sql = qq{DELETE FROM _MAP_ WHERE id IN ($pf)};
525 0           my @value;
526             my @cond;
527 0           for my $v (@views) {
528 0           my ($part, @v) = $self->_start_with( design_id => $v );
529 0           push @cond, $part;
530 0           push @value, @v;
531             }
532 0           $del_sql .= " AND (" . join(" OR ", @cond). ")";
533 0           $self->prepare_sql($del_sql)
534             ->execute(@id, @value);
535             }
536             else {
537 0           $self->prepare_sql(qq{DELETE FROM _MAP_ WHERE id IN ($pf)})
538             ->execute(@id);
539             }
540              
541 0           my $index_sth = $self->prepare_sql(
542             q{INSERT INTO _MAP_ (design_id, id, key, value) VALUES (?,?,?,?)}
543             );
544              
545 0           my $sth;
546 0 0         if (@views) {
547 0           $sth = $self->prepare_sql(
548             qq{SELECT id, value FROM _DATA_ WHERE id IN($df)}
549             );
550 0           $sth->execute(@views);
551             }
552             else {
553 0           my ($part, @value) = $self->_start_with( id => '_design/' );
554 0           $sth = $self->prepare_sql(
555             q{SELECT id, value FROM _DATA_ WHERE } . $part
556             );
557 0           $sth->execute(@value);
558             }
559              
560             DESIGN:
561 0           while ( my $r = $sth->fetchrow_arrayref ) {
562 0           my $design_id = $r->[0];
563 0           my $val = $self->from_json($r->[1]);
564 0 0         my $views = $val->{views} or next DESIGN;
565              
566             VIEW:
567 0           for my $name ( keys %$views ) {
568 0 0         my $code = $views->{$name}->{map} or next VIEW;
569 0           my $sub = eval $code; ## no critic
570 0 0         if ($@) {
571 0           warn $@;
572 0           next VIEW;
573             }
574 0           for my $data_val (@data_val) {
575 0           $self->_data_to_map({
576             sub => $sub,
577             id => $data_val->{_id},
578             data_val => $data_val,
579             sth => $index_sth,
580             design_id => "$design_id/$name",
581             });
582             }
583             }
584             }
585 0           return 1;
586             }
587              
588             sub _data_to_map {
589 0     0     my $self = shift;
590 0           my $args = shift;
591              
592 0           my @pair;
593             my $emit = sub {
594 0     0     my ( $k, $v ) = @_;
595 0 0         push @pair, [
596             $k,
597             ref $v ? $self->to_json($v) : $v,
598             ];
599 0           };
600 0           eval { $args->{sub}->( $args->{data_val}, $emit ) };
  0            
601 0 0         if ($@) {
602 0           warn $@;
603 0           return;
604             }
605             PAIR:
606 0           for my $p (@pair) {
607 0 0         next PAIR unless defined $p->[0];
608 0           $args->{sth}->execute(
609             $args->{design_id}, $args->{id},
610             $p->[0], $p->[1],
611             );
612             }
613             }
614              
615              
616             1;
617             __END__