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.036000';
3             # ABSTRACT: Do set operations with DBIx::Class
4              
5 56     56   25382 use strict;
  56         130  
  56         1490  
6 56     56   275 use warnings;
  56         112  
  56         1376  
7              
8 56     56   268 use parent 'DBIx::Class::ResultSet';
  56         124  
  56         282  
9              
10             # cribbed from perlfaq4
11             sub _compare_arrays {
12 38     38   72 my ($self, $first, $second) = @_;
13              
14 56     56   4894 no warnings; # silence spurious -w undef complaints
  56         143  
  56         34125  
15 38 100       154 return 0 unless @$first == @$second;
16 37         84 for (my $i = 0; $i < @$first; $i++) {
17 74 50       174 return 0 if $first->[$i] ne $second->[$i];
18             }
19 37         77 return 1;
20             }
21              
22             sub union {
23 7     7 1 730 shift->_set_operation( UNION => @_ );
24             }
25              
26             sub union_all {
27 2     2 1 159 shift->_set_operation( "UNION ALL" => @_ );
28             }
29              
30             sub intersect {
31 5     5 1 2460 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   12 my $self = shift;
40              
41 6 50 66     30 $self->{_except_keyword} ||= ( $self->result_source->schema->storage->sqlt_type eq 'Oracle' ? "MINUS" : "EXCEPT" );
42             }
43              
44             sub except {
45 6     6 1 2417 my ( $self, @args ) = @_;
46 6         18 $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   306 my ( $self, $operation, $other ) = @_;
56              
57 20         32 my @sql;
58             my @params;
59              
60 20         50 my $as = $self->_resolved_attrs->{as};
61              
62 20 50       814 my @operands = ( $self, ref $other eq 'ARRAY' ? @$other : $other );
63              
64 20         38 for (@operands) {
65 40 100       107 $self->throw_exception("ResultClass of ResultSets do not match!")
66             unless $self->result_class eq $_->result_class;
67              
68 38         449 my $attrs = $_->_resolved_attrs;
69              
70             $self->throw_exception('ResultSets do not all have the same selected columns!')
71 38 100       823 unless $self->_compare_arrays($as, $attrs->{as});
72              
73 37         54 my ($sql, @bind) = @{${$_->as_query}};
  37         45  
  37         102  
74 37         30807 $sql =~ s/^\s*\((.*)\)\s*$/$1/;
75              
76 37         81 push @sql, $sql;
77 37         85 push @params, @bind;
78             }
79              
80 17         50 my $query = q<(> . join(" $operation ", @sql). q<)>;
81              
82 17         48 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) 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