File Coverage

blib/lib/DBIx/Class/Helper/ResultSet/SetOperations.pm
Criterion Covered Total %
statement 41 43 95.3
branch 9 12 75.0
condition 2 3 66.6
subroutine 11 13 84.6
pod 6 6 100.0
total 69 77 89.6


line stmt bran cond sub pod time code
1             package DBIx::Class::Helper::ResultSet::SetOperations;
2             $DBIx::Class::Helper::ResultSet::SetOperations::VERSION = '2.034002';
3             # ABSTRACT: Do set operations with DBIx::Class
4              
5 55     55   21480 use strict;
  55         119  
  55         1357  
6 55     55   298 use warnings;
  55         109  
  55         1170  
7              
8 55     55   255 use parent 'DBIx::Class::ResultSet';
  55         114  
  55         272  
9              
10             # cribbed from perlfaq4
11             sub _compare_arrays {
12 38     38   97 my ($self, $first, $second) = @_;
13              
14 55     55   3955 no warnings; # silence spurious -w undef complaints
  55         115  
  55         28467  
15 38 100       155 return 0 unless @$first == @$second;
16 37         84 for (my $i = 0; $i < @$first; $i++) {
17 74 50       181 return 0 if $first->[$i] ne $second->[$i];
18             }
19 37         76 return 1;
20             }
21              
22             sub union {
23 7     7 1 722 shift->_set_operation( UNION => @_ );
24             }
25              
26             sub union_all {
27 2     2 1 157 shift->_set_operation( "UNION ALL" => @_ );
28             }
29              
30             sub intersect {
31 5     5 1 2536 shift->_set_operation( INTERSECT => @_ );
32             }
33              
34             sub intersect_all {
35 0     0 1 0 shift->_set_operation( "INTERSECT ALL" => @_ );
36             }
37              
38             sub _except_keyword {
39 6     6   10 my $self = shift;
40              
41 6 50 66     33 $self->{_except_keyword} ||= ( $self->result_source->schema->storage->sqlt_type eq 'Oracle' ? "MINUS" : "EXCEPT" );
42             }
43              
44             sub except {
45 6     6 1 2856 my ( $self, @args ) = @_;
46 6         19 $self->_set_operation( $self->_except_keyword => @args );
47             }
48              
49             sub except_all {
50             # not supported on most DBs
51 0     0 1 0 shift->_set_operation( "EXCEPT ALL" => @_ );
52             }
53              
54             sub _set_operation {
55 20     20   312 my ( $self, $operation, $other ) = @_;
56              
57 20         31 my @sql;
58             my @params;
59              
60 20         59 my $as = $self->_resolved_attrs->{as};
61              
62 20 50       798 my @operands = ( $self, ref $other eq 'ARRAY' ? @$other : $other );
63              
64 20         44 for (@operands) {
65 40 100       119 $self->throw_exception("ResultClass of ResultSets do not match!")
66             unless $self->result_class eq $_->result_class;
67              
68 38         420 my $attrs = $_->_resolved_attrs;
69              
70             $self->throw_exception('ResultSets do not all have the same selected columns!')
71 38 100       905 unless $self->_compare_arrays($as, $attrs->{as});
72              
73 37         52 my ($sql, @bind) = @{${$_->as_query}};
  37         54  
  37         102  
74 37         31323 $sql =~ s/^\s*\((.*)\)\s*$/$1/;
75              
76 37         78 push @sql, $sql;
77 37         85 push @params, @bind;
78             }
79              
80 17         52 my $query = q<(> . join(" $operation ", @sql). q<)>;
81              
82 17         51 my $attrs = $self->_resolved_attrs;
83             return $self->result_source->resultset->search(undef, {
84             alias => $self->current_source_alias,
85             from => [{
86             $self->current_source_alias => \[ $query, @params ],
87             -alias => $self->current_source_alias,
88             -source_handle => $self->result_source->handle,
89             }],
90             columns => $attrs->{as},
91 17         217 result_class => $self->result_class,
92             });
93             }
94              
95             1;
96              
97             __END__
98              
99             =pod
100              
101             =head1 NAME
102              
103             DBIx::Class::Helper::ResultSet::SetOperations - Do set operations with DBIx::Class
104              
105             =head1 SYNOPSIS
106              
107             package MyApp::Schema::ResultSet::Foo;
108              
109             __PACKAGE__->load_components(qw{Helper::ResultSet::SetOperations});
110              
111             ...
112              
113             1;
114              
115             And then elsewhere, like in a controller:
116              
117             my $rs1 = $rs->search({ foo => 'bar' });
118             my $rs2 = $rs->search({ baz => 'biff' });
119             for ($rs1->union($rs2)->all) { ... }
120              
121             =head1 DESCRIPTION
122              
123             This component allows you to use various set operations with your ResultSets.
124             See L<DBIx::Class::Helper::ResultSet/NOTE> for a nice way to apply it to your
125             entire schema.
126              
127             Component throws exceptions if ResultSets have different ResultClasses or
128             different "Columns Specs."
129              
130             The basic idea here is that in SQL if you use a set operation they must be
131             selecting the same columns names, so that the results will all match. The deal
132             with the ResultClasses is that DBIC needs to inflate the results the same for
133             the entire ResultSet, so if one were to try to apply something like a union in
134             a table with the same column name but different classes DBIC wouldn't be doing
135             what you would expect.
136              
137             A nice way to use this is with L<DBIx::Class::ResultClass::HashRefInflator>.
138              
139             You might have something like the following sketch autocompletion code:
140              
141             my $rs1 = $schema->resultset('Album')->search({
142             name => { -like => "$input%" }
143             }, {
144             columns => [qw( id name ), {
145             tablename => \['?', [{} => 'album']],
146             }],
147             });
148              
149             my $rs2 = $schema->resultset('Artist')->search({
150             name => { -like => "$input%" }
151             }, {
152             columns => [qw( id name ), {
153             tablename => \['?', [{} => 'artist']],
154             }],
155             });
156              
157             my $rs3 = $schema->resultset('Song')->search({
158             name => { -like => "$input%" }
159             }, {
160             columns => [qw( id name ), {
161             tablename => \['?', [{} => 'song']],
162             }],
163             });
164              
165             $_->result_class('DBIx::Class::ResultClass::HashRefInflator')
166             for ($rs1, $rs2, $rs3);
167              
168             my $data = [$rs1->union([$rs2, $rs3])->all];
169              
170             =head1 METHODS
171              
172             =head2 union
173              
174             =head2 union_all
175              
176             =head2 intersect
177              
178             =head2 intersect_all
179              
180             =head2 except
181              
182             =head2 except_all
183              
184             All of these methods take a single ResultSet or an ArrayRef of ResultSets as
185             the parameter only parameter.
186              
187             On Oracle C<except> will issue a C<MINUS> operation.
188              
189             =head1 AUTHOR
190              
191             Arthur Axel "fREW" Schmidt <frioux+cpan@gmail.com>
192              
193             =head1 COPYRIGHT AND LICENSE
194              
195             This software is copyright (c) 2019 by Arthur Axel "fREW" Schmidt.
196              
197             This is free software; you can redistribute it and/or modify it under
198             the same terms as the Perl 5 programming language system itself.
199              
200             =cut