Intermezzo checkin
[dbsrgits/DBIx-Class.git] / t / prefetch / standard.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 => 45 );
21 }
22
23 # figure out if we've got a version of sqlite that is older than 3.2.6, in
24 # which case COUNT(DISTINCT()) doesn't work
25 my $is_broken_sqlite = 0;
26 my ($sqlite_major_ver,$sqlite_minor_ver,$sqlite_patch_ver) =
27     split /\./, $schema->storage->dbh->get_info(18);
28 if( $schema->storage->dbh->get_info(17) eq 'SQLite' &&
29     ( ($sqlite_major_ver < 3) ||
30       ($sqlite_major_ver == 3 && $sqlite_minor_ver < 2) ||
31       ($sqlite_major_ver == 3 && $sqlite_minor_ver == 2 && $sqlite_patch_ver < 6) ) ) {
32     $is_broken_sqlite = 1;
33 }
34
35 my $queries = 0;
36 $schema->storage->debugcb(sub { $queries++; });
37 $schema->storage->debug(1);
38
39 my $search = { 'artist.name' => 'Caterwauler McCrae' };
40 my $attr = { prefetch => [ qw/artist liner_notes/ ],
41              order_by => 'me.cdid' };
42 my $search_str = Dumper($search);
43 my $attr_str = Dumper($attr);
44
45 my $rs = $schema->resultset("CD")->search($search, $attr);
46 my @cd = $rs->all;
47
48 is($cd[0]->title, 'Spoonful of bees', 'First record returned ok');
49
50 ok(!defined $cd[0]->liner_notes, 'No prefetch for NULL LEFT join');
51
52 is($cd[1]->{_relationship_data}{liner_notes}->notes, 'Buy Whiskey!', 'Prefetch for present LEFT JOIN');
53
54 is(ref $cd[1]->liner_notes, 'DBICTest::LinerNotes', 'Prefetch returns correct class');
55
56 is($cd[2]->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'Prefetch on parent object ok');
57
58 is($queries, 1, 'prefetch ran only 1 select statement');
59
60 $schema->storage->debug($orig_debug);
61 $schema->storage->debugobj->callback(undef);
62
63 # test for partial prefetch via columns attr
64 my $cd = $schema->resultset('CD')->find(1,
65     {
66       columns => [qw/title artist artist.name/], 
67       join => { 'artist' => {} }
68     }
69 );
70 ok(eval { $cd->artist->name eq 'Caterwauler McCrae' }, 'single related column prefetched');
71
72 # start test for nested prefetch SELECT count
73 $queries = 0;
74 $schema->storage->debugcb(sub { $queries++ });
75 $schema->storage->debug(1);
76
77 $rs = $schema->resultset('Tag')->search(
78   {},
79   {
80     prefetch => { cd => 'artist' }
81   }
82 );
83
84 my $tag = $rs->first;
85
86 is( $tag->cd->title, 'Spoonful of bees', 'step 1 ok for nested prefetch' );
87
88 is( $tag->cd->artist->name, 'Caterwauler McCrae', 'step 2 ok for nested prefetch');
89
90 # count the SELECTs
91 #$selects++ if /SELECT(?!.*WHERE 1=0.*)/;
92 is($queries, 1, 'nested prefetch ran exactly 1 select statement (excluding column_info)');
93
94 $queries = 0;
95
96 is($tag->search_related('cd')->search_related('artist')->first->name,
97    'Caterwauler McCrae',
98    'chained belongs_to->belongs_to search_related ok');
99
100 is($queries, 0, 'chained search_related after belontgs_to->belongs_to prefetch ran no queries');
101
102 $queries = 0;
103
104 $cd = $schema->resultset('CD')->find(1, { prefetch => 'artist' });
105
106 is($cd->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'artist prefetched correctly on find');
107
108 is($queries, 1, 'find with prefetch ran exactly 1 select statement (excluding column_info)');
109
110 $queries = 0;
111
112 $schema->storage->debugcb(sub { $queries++; });
113
114 $cd = $schema->resultset('CD')->find(1, { prefetch => { cd_to_producer => 'producer' } });
115
116 is($cd->producers->first->name, 'Matt S Trout', 'many_to_many accessor ok');
117
118 is($queries, 1, 'many_to_many accessor with nested prefetch ran exactly 1 query');
119
120 $queries = 0;
121
122 my $producers = $cd->search_related('cd_to_producer')->search_related('producer');
123
124 is($producers->first->name, 'Matt S Trout', 'chained many_to_many search_related ok');
125
126 is($queries, 0, 'chained search_related after many_to_many prefetch ran no queries');
127
128 $schema->storage->debug($orig_debug);
129 $schema->storage->debugobj->callback(undef);
130
131 $rs = $schema->resultset('Tag')->search(
132   {},
133   {
134     join => { cd => 'artist' },
135     prefetch => { cd => 'artist' }
136   }
137 );
138
139 cmp_ok( $rs->count, '>=', 0, 'nested prefetch does not duplicate joins' );
140
141 my ($artist) = $schema->resultset("Artist")->search({ 'cds.year' => 2001 },
142                  { order_by => 'artistid DESC', join => 'cds' });
143
144 is($artist->name, 'Random Boy Band', "Join search by object ok");
145
146 my @cds = $schema->resultset("CD")->search({ 'liner_notes.notes' => 'Buy Merch!' },
147                                { join => 'liner_notes' });
148
149 cmp_ok(scalar @cds, '==', 1, "Single CD retrieved via might_have");
150
151 is($cds[0]->title, "Generic Manufactured Singles", "Correct CD retrieved");
152
153 my @artists = $schema->resultset("Artist")->search({ 'tags.tag' => 'Shiny' },
154                                        { join => { 'cds' => 'tags' } });
155
156 cmp_ok( @artists, '==', 2, "two-join search ok" );
157
158 $rs = $schema->resultset("CD")->search(
159   {},
160   { group_by => [qw/ title me.cdid /] }
161 );
162
163 SKIP: {
164     skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
165         if $is_broken_sqlite;
166     cmp_ok( $rs->count, '==', 5, "count() ok after group_by on main pk" );
167 }
168
169 cmp_ok( scalar $rs->all, '==', 5, "all() returns same count as count() after group_by on main pk" );
170
171 $rs = $schema->resultset("CD")->search(
172   {},
173   { join => [qw/ artist /], group_by => [qw/ artist.name /] }
174 );
175
176 SKIP: {
177     skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
178         if $is_broken_sqlite;
179     cmp_ok( $rs->count, '==', 3, "count() ok after group_by on related column" );
180 }
181
182 $rs = $schema->resultset("Artist")->search(
183   {},
184       { join => [qw/ cds /], group_by => [qw/ me.name /], having =>{ 'MAX(cds.cdid)'=> \'< 5' } }
185 );
186
187 cmp_ok( $rs->all, '==', 2, "results ok after group_by on related column with a having" );
188
189 $rs = $rs->search( undef, {  having =>{ 'count(*)'=> \'> 2' }});
190
191 cmp_ok( $rs->all, '==', 1, "count() ok after group_by on related column with a having" );
192
193 $rs = $schema->resultset("Artist")->search(
194         { 'cds.title' => 'Spoonful of bees',
195           'cds_2.title' => 'Forkful of bees' },
196         { join => [ 'cds', 'cds' ] });
197
198 SKIP: {
199     skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
200         if $is_broken_sqlite;
201     cmp_ok($rs->count, '==', 1, "single artist returned from multi-join");
202 }
203
204 is($rs->next->name, 'Caterwauler McCrae', "Correct artist returned");
205
206 $cd = $schema->resultset('Artist')->first->create_related('cds',
207     {
208     title   => 'Unproduced Single',
209     year    => 2007
210 });
211
212 my $left_join = $schema->resultset('CD')->search(
213     { 'me.cdid' => $cd->cdid },
214     { prefetch => { cd_to_producer => 'producer' } }
215 );
216
217 cmp_ok($left_join, '==', 1, 'prefetch with no join record present');
218
219 $queries = 0;
220 $schema->storage->debugcb(sub { $queries++ });
221 $schema->storage->debug(1);
222
223 my $tree_like =
224      $schema->resultset('TreeLike')->find(5,
225        { join     => { parent => { parent => 'parent' } },
226          prefetch => { parent => { parent => 'parent' } } });
227
228 is($tree_like->name, 'quux', 'Bottom of tree ok');
229 $tree_like = $tree_like->parent;
230 is($tree_like->name, 'baz', 'First level up ok');
231 $tree_like = $tree_like->parent;
232 is($tree_like->name, 'bar', 'Second level up ok');
233 $tree_like = $tree_like->parent;
234 is($tree_like->name, 'foo', 'Third level up ok');
235
236 $schema->storage->debug($orig_debug);
237 $schema->storage->debugobj->callback(undef);
238
239 cmp_ok($queries, '==', 1, 'Only one query run');
240
241 $tree_like = $schema->resultset('TreeLike')->search({'me.id' => 2});
242 $tree_like = $tree_like->search_related('children')->search_related('children')->search_related('children')->first;
243 is($tree_like->name, 'quux', 'Tree search_related ok');
244
245 $tree_like = $schema->resultset('TreeLike')->search_related('children',
246     { 'children.id' => 3, 'children_2.id' => 4 },
247     { prefetch => { children => 'children' } }
248   )->first;
249 is(eval { $tree_like->children->first->children->first->name }, 'quux',
250    'Tree search_related with prefetch ok');
251
252 $tree_like = eval { $schema->resultset('TreeLike')->search(
253     { 'children.id' => 3, 'children_2.id' => 6 }, 
254     { join => [qw/children children/] }
255   )->search_related('children', { 'children_4.id' => 7 }, { prefetch => 'children' }
256   )->first->children->first; };
257 is(eval { $tree_like->name }, 'fong', 'Tree with multiple has_many joins ok');
258
259 # test that collapsed joins don't get a _2 appended to the alias
260
261 my $sql = '';
262 $schema->storage->debugcb(sub { $sql = $_[1] });
263 $schema->storage->debug(1);
264
265 eval {
266   my $row = $schema->resultset('Artist')->search_related('cds', undef, {
267     join => 'tracks',
268     prefetch => 'tracks',
269   })->search_related('tracks')->first;
270 };
271
272 like( $sql, qr/^SELECT tracks_2\.trackid/, "join not collapsed for search_related" );
273
274 $schema->storage->debug($orig_debug);
275 $schema->storage->debugobj->callback(undef);
276
277 $rs = $schema->resultset('Artist');
278 $rs->create({ artistid => 4, name => 'Unknown singer-songwriter' });
279 $rs->create({ artistid => 5, name => 'Emo 4ever' });
280 @artists = $rs->search(undef, { prefetch => 'cds', order_by => 'artistid' });
281 is(scalar @artists, 5, 'has_many prefetch with adjacent empty rows ok');
282
283 # -------------
284 #
285 # Tests for multilevel has_many prefetch
286
287 # artist resultsets - with and without prefetch
288 my $art_rs = $schema->resultset('Artist');
289 my $art_rs_pr = $art_rs->search(
290     {},
291     {
292         join     => [ { cds => ['tracks'] } ],
293         prefetch => [ { cds => ['tracks'] } ],
294         cache    => 1 # last test needs this
295     }
296 );
297
298 # This test does the same operation twice - once on a
299 # set of items fetched from the db with no prefetch of has_many rels
300 # The second prefetches 2 levels of has_many
301 # We check things are the same by comparing the name or title
302 # we build everything into a hash structure and compare the one
303 # from each rs to see what differs
304
305 sub make_hash_struc {
306     my $rs = shift;
307
308     my $struc = {};
309     foreach my $art ( $rs->all ) {
310         foreach my $cd ( $art->cds ) {
311             foreach my $track ( $cd->tracks ) {
312                 $struc->{ $art->name }{ $cd->title }{ $track->title }++;
313             }
314         }
315     }
316     return $struc;
317 }
318
319 $queries = 0;
320 $schema->storage->debugcb(sub { $queries++ });
321 $schema->storage->debug(1);
322
323 my $prefetch_result = make_hash_struc($art_rs_pr);
324
325 is($queries, 1, 'nested prefetch across has_many->has_many ran exactly 1 query');
326
327 my $nonpre_result   = make_hash_struc($art_rs);
328
329 is_deeply( $prefetch_result, $nonpre_result,
330     'Compare 2 level prefetch result to non-prefetch result' );
331
332 $queries = 0;
333
334 is($art_rs_pr->search_related('cds')->search_related('tracks')->first->title,
335    'Fowlin',
336    'chained has_many->has_many search_related ok'
337   );
338
339 is($queries, 0, 'chained search_related after has_many->has_many prefetch ran no queries');
340