Illustration for rob
[dbsrgits/DBIx-Class.git] / t / prefetch / multiple_hasmany.t
1 use strict;
2 use warnings;  
3
4 use Test::More;
5 use Test::Exception;
6 use lib qw(t/lib);
7 use DBICTest;
8 use Data::Dumper;
9
10 my $schema = DBICTest->init_schema();
11
12 my $orig_debug = $schema->storage->debug;
13
14 use IO::File;
15
16 BEGIN {
17     eval "use DBD::SQLite";
18     plan $@
19         ? ( skip_all => 'needs DBD::SQLite for testing' )
20 #        : ( tests => 16 );
21         : 'no_plan';
22 }
23
24 # once the following TODO is complete, remove the 2 warning tests immediately
25 # after the TODO block
26 # (the TODO block itself contains tests ensuring that the warns are removed)
27 TODO: {
28     local $TODO = 'Prefetch of multiple has_many rels at the same level (currently warn to protect the clueless git)';
29
30     #( 1 -> M + M )
31     my $cd_rs = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' });
32     my $pr_cd_rs = $cd_rs->search ({}, {
33         prefetch => [qw/tracks tags/],
34     });
35
36     my $tracks_rs = $cd_rs->first->tracks;
37     my $tracks_count = $tracks_rs->count;
38
39     my ($pr_tracks_rs, $pr_tracks_count);
40
41     my $queries = 0;
42     $schema->storage->debugcb(sub { $queries++ });
43     $schema->storage->debug(1);
44
45     my $o_mm_warn;
46     {
47         local $SIG{__WARN__} = sub { $o_mm_warn = shift };
48         $pr_tracks_rs = $pr_cd_rs->first->tracks;
49     };
50     $pr_tracks_count = $pr_tracks_rs->count;
51
52     ok(! $o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (1 -> M + M)');
53
54     is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
55     is($pr_tracks_count, $tracks_count, 'equal count of prefetched relations over several same level has_many\'s (1 -> M + M)');
56
57     for ($pr_tracks_rs, $tracks_rs) {
58         $_->result_class ('DBIx::Class::ResultClass::HashRefInflator');
59     }
60
61     is_deeply ([$pr_tracks_rs->all], [$tracks_rs->all], 'same structure returned with and without prefetch over several same level has_many\'s (1 -> M + M)');
62
63     #( M -> 1 -> M + M )
64     my $note_rs = $schema->resultset('LinerNotes')->search ({ notes => 'Buy Whiskey!' });
65     my $pr_note_rs = $note_rs->search ({}, {
66         prefetch => {
67             cd => [qw/tags tracks/]
68         },
69     });
70
71     my $tags_rs = $note_rs->first->cd->tags;
72     my $tags_count = $tags_rs->count;
73
74     my ($pr_tags_rs, $pr_tags_count);
75
76     $queries = 0;
77     $schema->storage->debugcb(sub { $queries++ });
78     $schema->storage->debug(1);
79
80     my $m_o_mm_warn;
81     {
82         local $SIG{__WARN__} = sub { $m_o_mm_warn = shift };
83         $pr_tags_rs = $pr_note_rs->first->cd->tags;
84     };
85     $pr_tags_count = $pr_tags_rs->count;
86
87     ok(! $m_o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (M -> 1 -> M + M)');
88
89     is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
90
91     is($pr_tags_count, $tags_count, 'equal count of prefetched relations over several same level has_many\'s (M -> 1 -> M + M)');
92
93     for ($pr_tags_rs, $tags_rs) {
94         $_->result_class ('DBIx::Class::ResultClass::HashRefInflator');
95     }
96
97     is_deeply ([$pr_tags_rs->all], [$tags_rs->all], 'same structure returned with and without prefetch over several same level has_many\'s (M -> 1 -> M + M)');
98 }
99
100 # remove this closure once the TODO above is working
101 my $w;
102 {
103     local $SIG{__WARN__} = sub { $w = shift };
104
105     my $rs = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' }, { prefetch => [qw/tracks tags/] });
106     for (qw/all count next first/) {
107         undef $w;
108         my @stuff = $rs->search()->$_;
109         like ($w, qr/will currently disrupt both the functionality of .rs->count\(\), and the amount of objects retrievable via .rs->next\(\)/,
110             "warning on ->$_ attempt prefetching several same level has_manys (1 -> M + M)");
111     }
112     my $rs2 = $schema->resultset('LinerNotes')->search ({ notes => 'Buy Whiskey!' }, { prefetch => { cd => [qw/tags tracks/] } });
113     for (qw/all count next first/) {
114         undef $w;
115         my @stuff = $rs2->search()->$_;
116         like ($w, qr/will currently disrupt both the functionality of .rs->count\(\), and the amount of objects retrievable via .rs->next\(\)/,
117             "warning on ->$_ attempt prefetching several same level has_manys (M -> 1 -> M + M)");
118     }
119 }
120
121
122 # Illustration purposes only
123
124 {
125   package Inf::Dump;
126   sub inflate_result {
127     return [ @_[2,3] ];
128   }
129 }
130
131 my $cd = $schema->resultset ('CD')->create ({
132   artist => 1,
133   title => 'bad cd',
134   year => 1313,
135   tags => [ map { { tag => "bad tag $_" } } (1 .. 3) ],
136   tracks => [
137     { title => 'bad track 1', cd_single => {
138       artist => 1,
139       title => 'bad_single',
140       year => 1313,
141     }},
142     map { { title => "bad track $_" } } (2 .. 3),
143   ],
144 });
145
146 my $rs = $schema->resultset ('CD')->search (
147   { 'me.cdid' => $cd->id },
148   { prefetch => [ 'tags', { tracks => 'cd_single' } ], result_class => 'Inf::Dump' },
149 );
150
151 use Text::Table;
152 my $query = ${$rs->as_query}->[0];
153 my ($cols) = ( $query =~ /SELECT (.+) FROM/);
154 my $tb = Text::Table->new (map { $_ => \ ' | ' } (split /,\s*/, $cols) );
155
156 my $c = $rs->cursor;
157 while (my @stuff = $c->next) {
158   $tb->add (map { defined $_ ? $_ : 'NULL' } (@stuff) );
159 }
160
161 $rs->reset;
162 note Dumper [
163   "\n$query",
164   "\n$tb",
165   $rs->next
166 ];
167
168
169
170
171 __END__
172 The solution is to rewrite ResultSet->_collapse_result() and
173 ResultSource->resolve_prefetch() to focus on the final results from the collapse
174 of the data. Right now, the code doesn't treat the columns from the various
175 tables as grouped entities. While there is a concept of hierarchy (so that
176 prefetching down relationships does work as expected), there is no idea of what
177 the final product should look like and how the various columns in the row would
178 play together. So, the actual prefetch datastructure from the search would be
179 very useful in working through this problem. We already have access to the PKs
180 and sundry for those. So, when collapsing the search result, we know we are
181 looking for 1 cd object. We also know we're looking for tracks and tags records
182 -independently- of each other. So, we can grab the data for tracks and data for
183 tags separately, uniqueing on the PK as appropriate. Then, when we're done with
184 the given cd object's datastream, we know we're good. This should work for all
185 the various scenarios.
186
187 My reccommendation is the row's data is preprocessed first, breaking it up into
188 the data for each of the component tables. (This could be done in the single
189 table case, too, but probably isn't necessary.) So, starting with something
190 like:
191   my $row = {
192     t1.col1 => 1,
193     t1.col2 => 2,
194     t2.col1 => 3,
195     t2.col2 => 4,
196     t3.col1 => 5,
197     t3.col2 => 6,
198   };
199 it is massaged to look something like:
200   my $row_massaged = {
201     t1 => { col1 => 1, col2 => 2 },
202     t2 => { col1 => 3, col2 => 4 },
203     t3 => { col1 => 5, col2 => 6 },
204   };
205 At this point, find the stuff that's different is easy enough to do and slotting
206 things into the right spot is, likewise, pretty straightforward.
207
208 This implies that the collapse attribute can probably disappear or, at the
209 least, be turned into a boolean (which is how it's used in every other place).