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.035000';
3             # ABSTRACT: Do set operations with DBIx::Class
4              
5 56     56   28450 use strict;
  56         147  
  56         1612  
6 56     56   298 use warnings;
  56         179  
  56         1482  
7              
8 56     56   282 use parent 'DBIx::Class::ResultSet';
  56         137  
  56         334  
9              
10             # cribbed from perlfaq4
11             sub _compare_arrays {
12 38     38   101 my ($self, $first, $second) = @_;
13              
14 56     56   4970 no warnings; # silence spurious -w undef complaints
  56         152  
  56         36863  
15 38 100       186 return 0 unless @$first == @$second;
16 37         111 for (my $i = 0; $i < @$first; $i++) {
17 74 50       233 return 0 if $first->[$i] ne $second->[$i];
18             }
19 37         90 return 1;
20             }
21              
22             sub union {
23 7     7 1 938 shift->_set_operation( UNION => @_ );
24             }
25              
26             sub union_all {
27 2     2 1 247 shift->_set_operation( "UNION ALL" => @_ );
28             }
29              
30             sub intersect {
31 5     5 1 3612 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   13 my $self = shift;
40              
41 6 50 66     36 $self->{_except_keyword} ||= ( $self->result_source->schema->storage->sqlt_type eq 'Oracle' ? "MINUS" : "EXCEPT" );
42             }
43              
44             sub except {
45 6     6 1 3236 my ( $self, @args ) = @_;
46 6         24 $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   365 my ( $self, $operation, $other ) = @_;
56              
57 20         44 my @sql;
58             my @params;
59              
60 20         75 my $as = $self->_resolved_attrs->{as};
61              
62 20 50       1138 my @operands = ( $self, ref $other eq 'ARRAY' ? @$other : $other );
63              
64 20         60 for (@operands) {
65 40 100       147 $self->throw_exception("ResultClass of ResultSets do not match!")
66             unless $self->result_class eq $_->result_class;
67              
68 38         540 my $attrs = $_->_resolved_attrs;
69              
70             $self->throw_exception('ResultSets do not all have the same selected columns!')
71 38 100       1199 unless $self->_compare_arrays($as, $attrs->{as});
72              
73 37         68 my ($sql, @bind) = @{${$_->as_query}};
  37         60  
  37         139  
74 37         41139 $sql =~ s/^\s*\((.*)\)\s*$/$1/;
75              
76 37         99 push @sql, $sql;
77 37         105 push @params, @bind;
78             }
79              
80 17         73 my $query = q<(> . join(" $operation ", @sql). q<)>;
81              
82 17         69 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         284 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) 2020 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