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