Failing test for prefetch bug: M -> 1 -> M with prefetch on 3rd rs, with order_by...
[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 IO::File;
9
10 plan tests => 10;
11
12 my $schema = DBICTest->init_schema();
13 my $sdebug = $schema->storage->debug;
14
15
16 # once the following TODO is complete, remove the 2 warning tests immediately
17 # after the TODO block
18 # (the TODO block itself contains tests ensuring that the warns are removed)
19 TODO: {
20     local $TODO = 'Prefetch of multiple has_many rels at the same level (currently warn to protect the clueless git)';
21
22     #( 1 -> M + M )
23     my $cd_rs = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' });
24     my $pr_cd_rs = $cd_rs->search ({}, {
25         prefetch => [qw/tracks tags/],
26     });
27
28     my $tracks_rs = $cd_rs->first->tracks;
29     my $tracks_count = $tracks_rs->count;
30
31     my ($pr_tracks_rs, $pr_tracks_count);
32
33     my $queries = 0;
34     $schema->storage->debugcb(sub { $queries++ });
35     $schema->storage->debug(1);
36
37     my $o_mm_warn;
38     {
39         local $SIG{__WARN__} = sub { $o_mm_warn = shift };
40         $pr_tracks_rs = $pr_cd_rs->first->tracks;
41     };
42     $pr_tracks_count = $pr_tracks_rs->count;
43
44     ok(! $o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (1 -> M + M)');
45
46     is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
47     $schema->storage->debugcb (undef);
48     $schema->storage->debug ($sdebug);
49
50     is($pr_tracks_count, $tracks_count, 'equal count of prefetched relations over several same level has_many\'s (1 -> M + M)');
51
52     for ($pr_tracks_rs, $tracks_rs) {
53         $_->result_class ('DBIx::Class::ResultClass::HashRefInflator');
54     }
55
56     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)');
57
58     #( M -> 1 -> M + M )
59     my $note_rs = $schema->resultset('LinerNotes')->search ({ notes => 'Buy Whiskey!' });
60     my $pr_note_rs = $note_rs->search ({}, {
61         prefetch => {
62             cd => [qw/tags tracks/]
63         },
64     });
65
66     my $tags_rs = $note_rs->first->cd->tags;
67     my $tags_count = $tags_rs->count;
68
69     my ($pr_tags_rs, $pr_tags_count);
70
71     $queries = 0;
72     $schema->storage->debugcb(sub { $queries++ });
73     $schema->storage->debug(1);
74
75     my $m_o_mm_warn;
76     {
77         local $SIG{__WARN__} = sub { $m_o_mm_warn = shift };
78         $pr_tags_rs = $pr_note_rs->first->cd->tags;
79     };
80     $pr_tags_count = $pr_tags_rs->count;
81
82     ok(! $m_o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (M -> 1 -> M + M)');
83
84     is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
85     $schema->storage->debugcb (undef);
86     $schema->storage->debug ($sdebug);
87
88     is($pr_tags_count, $tags_count, 'equal count of prefetched relations over several same level has_many\'s (M -> 1 -> M + M)');
89
90     for ($pr_tags_rs, $tags_rs) {
91         $_->result_class ('DBIx::Class::ResultClass::HashRefInflator');
92     }
93
94     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)');
95 }
96
97 # remove this closure once the TODO above is working
98 {
99     my $warn_re = qr/will explode the number of row objects retrievable via/;
100
101     my (@w, @dummy);
102     local $SIG{__WARN__} = sub { $_[0] =~ $warn_re ? push @w, @_ : warn @_ };
103
104     my $rs = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' }, { prefetch => [qw/tracks tags/] });
105     @w = ();
106     @dummy = $rs->first;
107     is (@w, 1, 'warning on attempt prefetching several same level has_manys (1 -> M + M)');
108
109     my $rs2 = $schema->resultset('LinerNotes')->search ({ notes => 'Buy Whiskey!' }, { prefetch => { cd => [qw/tags tracks/] } });
110     @w = ();
111     @dummy = $rs2->first;
112     is (@w, 1, 'warning on attempt prefetching several same level has_manys (M -> 1 -> M + M)');
113 }
114
115 __END__
116 The solution is to rewrite ResultSet->_collapse_result() and
117 ResultSource->resolve_prefetch() to focus on the final results from the collapse
118 of the data. Right now, the code doesn't treat the columns from the various
119 tables as grouped entities. While there is a concept of hierarchy (so that
120 prefetching down relationships does work as expected), there is no idea of what
121 the final product should look like and how the various columns in the row would
122 play together. So, the actual prefetch datastructure from the search would be
123 very useful in working through this problem. We already have access to the PKs
124 and sundry for those. So, when collapsing the search result, we know we are
125 looking for 1 cd object. We also know we're looking for tracks and tags records
126 -independently- of each other. So, we can grab the data for tracks and data for
127 tags separately, uniqueing on the PK as appropriate. Then, when we're done with
128 the given cd object's datastream, we know we're good. This should work for all
129 the various scenarios.
130
131 My reccommendation is the row's data is preprocessed first, breaking it up into
132 the data for each of the component tables. (This could be done in the single
133 table case, too, but probably isn't necessary.) So, starting with something
134 like:
135   my $row = {
136     t1.col1 => 1,
137     t1.col2 => 2,
138     t2.col1 => 3,
139     t2.col2 => 4,
140     t3.col1 => 5,
141     t3.col2 => 6,
142   };
143 it is massaged to look something like:
144   my $row_massaged = {
145     t1 => { col1 => 1, col2 => 2 },
146     t2 => { col1 => 3, col2 => 4 },
147     t3 => { col1 => 5, col2 => 6 },
148   };
149 At this point, find the stuff that's different is easy enough to do and slotting
150 things into the right spot is, likewise, pretty straightforward. Instead of
151 storing things in a AoH, store them in a HoH keyed on the PKs of the the table,
152 then convert to an AoH after all collapsing is done.
153
154 This implies that the collapse attribute can probably disappear or, at the
155 least, be turned into a boolean (which is how it's used in every other place).