Illustration for rob
[dbsrgits/DBIx-Class.git] / t / prefetch / multiple_hasmany.t
CommitLineData
e9bd1473 1use strict;
2use warnings;
3
4use Test::More;
5use Test::Exception;
6use lib qw(t/lib);
7use DBICTest;
8use Data::Dumper;
9
10my $schema = DBICTest->init_schema();
11
12my $orig_debug = $schema->storage->debug;
13
14use IO::File;
15
16BEGIN {
17 eval "use DBD::SQLite";
18 plan $@
19 ? ( skip_all => 'needs DBD::SQLite for testing' )
28fddc39 20# : ( tests => 16 );
21 : 'no_plan';
e9bd1473 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)
27TODO: {
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
101my $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}
87333f6c 120
28fddc39 121
122# Illustration purposes only
123
124{
125 package Inf::Dump;
126 sub inflate_result {
127 return [ @_[2,3] ];
128 }
129}
130
131my $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
146my $rs = $schema->resultset ('CD')->search (
147 { 'me.cdid' => $cd->id },
148 { prefetch => [ 'tags', { tracks => 'cd_single' } ], result_class => 'Inf::Dump' },
149);
150
151use Text::Table;
152my $query = ${$rs->as_query}->[0];
153my ($cols) = ( $query =~ /SELECT (.+) FROM/);
154my $tb = Text::Table->new (map { $_ => \ ' | ' } (split /,\s*/, $cols) );
155
156my $c = $rs->cursor;
157while (my @stuff = $c->next) {
158 $tb->add (map { defined $_ ? $_ : 'NULL' } (@stuff) );
159}
160
161$rs->reset;
162note Dumper [
163 "\n$query",
164 "\n$tb",
165 $rs->next
166];
167
168
169
170
87333f6c 171__END__
172The solution is to rewrite ResultSet->_collapse_result() and
173ResultSource->resolve_prefetch() to focus on the final results from the collapse
174of the data. Right now, the code doesn't treat the columns from the various
175tables as grouped entities. While there is a concept of hierarchy (so that
176prefetching down relationships does work as expected), there is no idea of what
177the final product should look like and how the various columns in the row would
178play together. So, the actual prefetch datastructure from the search would be
179very useful in working through this problem. We already have access to the PKs
180and sundry for those. So, when collapsing the search result, we know we are
181looking 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
183tags separately, uniqueing on the PK as appropriate. Then, when we're done with
184the given cd object's datastream, we know we're good. This should work for all
185the various scenarios.
186
187My reccommendation is the row's data is preprocessed first, breaking it up into
188the data for each of the component tables. (This could be done in the single
189table case, too, but probably isn't necessary.) So, starting with something
190like:
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 };
199it 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 };
205At this point, find the stuff that's different is easy enough to do and slotting
206things into the right spot is, likewise, pretty straightforward.
207
208This implies that the collapse attribute can probably disappear or, at the
209least, be turned into a boolean (which is how it's used in every other place).