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