Back out constructor/prefetch rewrite introduced mainly by 43245ada4a
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBIHacks.pm
CommitLineData
c443438f 1package #hide from PAUSE
2 DBIx::Class::Storage::DBIHacks;
d28bb90d 3
4#
5# This module contains code that should never have seen the light of day,
6# does not belong in the Storage, or is otherwise unfit for public
6a6394f1 7# display. The arrival of SQLA2 should immediately obsolete 90% of this
d28bb90d 8#
9
10use strict;
11use warnings;
12
13use base 'DBIx::Class::Storage';
14use mro 'c3';
15
6298a324 16use List::Util 'first';
17use Scalar::Util 'blessed';
ea5c7509 18use Sub::Name 'subname';
6298a324 19use namespace::clean;
d28bb90d 20
21#
052e8431 22# This code will remove non-selecting/non-restricting joins from
4b1b5ea3 23# {from} specs, aiding the RDBMS query optimizer
052e8431 24#
25sub _prune_unused_joins {
ea95892e 26 my $self = shift;
4b1b5ea3 27 my ($from, $select, $where, $attrs) = @_;
052e8431 28
ea95892e 29 return $from unless $self->_use_join_optimizer;
30
052e8431 31 if (ref $from ne 'ARRAY' || ref $from->[0] ne 'HASH' || ref $from->[1] ne 'ARRAY') {
32 return $from; # only standard {from} specs are supported
33 }
34
4b1b5ea3 35 my $aliastypes = $self->_resolve_aliastypes_from_select_args(@_);
36
37 # a grouped set will not be affected by amount of rows. Thus any
38 # {multiplying} joins can go
39 delete $aliastypes->{multiplying} if $attrs->{group_by};
40
052e8431 41 my @newfrom = $from->[0]; # FROM head is always present
42
a4812caa 43 my %need_joins;
44 for (values %$aliastypes) {
45 # add all requested aliases
46 $need_joins{$_} = 1 for keys %$_;
47
48 # add all their parents (as per joinpath which is an AoH { table => alias })
49 $need_joins{$_} = 1 for map { values %$_ } map { @$_ } values %$_;
50 }
052e8431 51 for my $j (@{$from}[1..$#$from]) {
539ffe87 52 push @newfrom, $j if (
4b1b5ea3 53 (! $j->[0]{-alias}) # legacy crap
539ffe87 54 ||
55 $need_joins{$j->[0]{-alias}}
56 );
052e8431 57 }
58
59 return \@newfrom;
60}
61
052e8431 62#
d28bb90d 63# This is the code producing joined subqueries like:
8273e845 64# SELECT me.*, other.* FROM ( SELECT me.* FROM ... ) JOIN other ON ...
d28bb90d 65#
66sub _adjust_select_args_for_complex_prefetch {
67 my ($self, $from, $select, $where, $attrs) = @_;
68
69 $self->throw_exception ('Nothing to prefetch... how did we get here?!')
36fd7f07 70 if not @{$attrs->{_prefetch_selector_range}};
d28bb90d 71
72 $self->throw_exception ('Complex prefetches are not supported on resultsets with a custom from attribute')
73 if (ref $from ne 'ARRAY' || ref $from->[0] ne 'HASH' || ref $from->[1] ne 'ARRAY');
74
d28bb90d 75 # generate inner/outer attribute lists, remove stuff that doesn't apply
76 my $outer_attrs = { %$attrs };
77 delete $outer_attrs->{$_} for qw/where bind rows offset group_by having/;
78
186ba34c 79 my $inner_attrs = { %$attrs };
c9733800 80 delete $inner_attrs->{$_} for qw/for collapse _prefetch_selector_range _collapse_order_by select as/;
81
82 # bring over all non-collapse-induced order_by into the inner query (if any)
83 # the outer one will have to keep them all
84 delete $inner_attrs->{order_by};
85 if (my $ord_cnt = @{$outer_attrs->{order_by}} - @{$outer_attrs->{_collapse_order_by}} ) {
86 $inner_attrs->{order_by} = [
87 @{$outer_attrs->{order_by}}[ 0 .. $ord_cnt - 1]
88 ];
89 }
946f6260 90
d28bb90d 91 # generate the inner/outer select lists
92 # for inside we consider only stuff *not* brought in by the prefetch
93 # on the outside we substitute any function for its alias
94 my $outer_select = [ @$select ];
95 my $inner_select = [];
36fd7f07 96
27e0370d 97 my ($root_source, $root_source_offset);
98
99 for my $i (0 .. $#$from) {
100 my $node = $from->[$i];
101 my $h = (ref $node eq 'HASH') ? $node
102 : (ref $node eq 'ARRAY' and ref $node->[0] eq 'HASH') ? $node->[0]
103 : next
104 ;
105
106 if ( ($h->{-alias}||'') eq $attrs->{alias} and $root_source = $h->{-rsrc} ) {
107 $root_source_offset = $i;
108 last;
109 }
110 }
111
112 $self->throw_exception ('Complex prefetches are not supported on resultsets with a custom from attribute')
113 unless $root_source;
114
115 # use the heavy duty resolver to take care of aliased/nonaliased naming
116 my $colinfo = $self->_resolve_column_info($from);
117 my $selected_root_columns;
118
36fd7f07 119 my ($p_start, $p_end) = @{$outer_attrs->{_prefetch_selector_range}};
120 for my $i (0 .. $p_start - 1, $p_end + 1 .. $#$outer_select) {
d28bb90d 121 my $sel = $outer_select->[$i];
122
123 if (ref $sel eq 'HASH' ) {
124 $sel->{-as} ||= $attrs->{as}[$i];
125 $outer_select->[$i] = join ('.', $attrs->{alias}, ($sel->{-as} || "inner_column_$i") );
126 }
27e0370d 127 elsif (! ref $sel and my $ci = $colinfo->{$sel}) {
128 $selected_root_columns->{$ci->{-colname}} = 1;
129 }
d28bb90d 130
131 push @$inner_select, $sel;
bb9bffea 132
133 push @{$inner_attrs->{as}}, $attrs->{as}[$i];
d28bb90d 134 }
135
27e0370d 136 # We will need to fetch all native columns in the inner subquery, which may be a part
137 # of an *outer* join condition. We can not just fetch everything because a potential
138 # has_many restricting join collapse *will not work* on heavy data types.
139 # Time for more horrible SQL parsing, aughhhh
140
141 # MASSIVE FIXME - in fact when we are fully transitioned to DQ and the support is
142 # is sane - we will need to trim the select list to *only* fetch stuff that is
143 # necessary to build joins. In the current implementation if I am selecting a blob
144 # and the group_by kicks in - we are fucked, and all the user can do is not select
145 # that column. This is silly!
146
147 my $retardo_sqla_cache = {};
148 for my $cond ( map { $_->[1] } @{$from}[$root_source_offset + 1 .. $#$from] ) {
149 for my $col (@{$self->_extract_condition_columns($cond, $retardo_sqla_cache)}) {
150 my $ci = $colinfo->{$col};
151 if (
152 $ci
153 and
154 $ci->{-source_alias} eq $attrs->{alias}
155 and
156 ! $selected_root_columns->{$ci->{-colname}}++
157 ) {
158 # adding it to both to keep limits not supporting dark selectors happy
159 push @$inner_select, $ci->{-fq_colname};
160 push @{$inner_attrs->{as}}, $ci->{-fq_colname};
161 }
162 }
163 }
164
ea95892e 165 # construct the inner $from and lock it in a subquery
48580715 166 # we need to prune first, because this will determine if we need a group_by below
53c29913 167 # the fake group_by is so that the pruner throws away all non-selecting, non-restricting
168 # multijoins (since we def. do not care about those inside the subquery)
ea95892e 169
6395604e 170 my $inner_subq = do {
ea95892e 171
172 # must use it here regardless of user requests
173 local $self->{_use_join_optimizer} = 1;
174
175 my $inner_from = $self->_prune_unused_joins ($from, $inner_select, $where, {
176 group_by => ['dummy'], %$inner_attrs,
177 });
178
887a0aef 179 my $inner_aliastypes =
180 $self->_resolve_aliastypes_from_select_args( $inner_from, $inner_select, $where, $inner_attrs );
181
a4812caa 182 # we need to simulate collapse in the subq if a multiplying join is pulled
183 # by being a non-selecting restrictor
0a3441ee 184 if (
185 ! $inner_attrs->{group_by}
186 and
887a0aef 187 first {
188 $inner_aliastypes->{restricting}{$_}
189 and
190 ! $inner_aliastypes->{selecting}{$_}
191 } ( keys %{$inner_aliastypes->{multiplying}||{}} )
0a3441ee 192 ) {
14e26c5f 193 my $unprocessed_order_chunks;
194 ($inner_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection (
0a3441ee 195 $inner_from, $inner_select, $inner_attrs->{order_by}
196 );
14e26c5f 197
198 $self->throw_exception (
199 'A required group_by clause could not be constructed automatically due to a complex '
200 . 'order_by criteria. Either order_by columns only (no functions) or construct a suitable '
201 . 'group_by by hand'
202 ) if $unprocessed_order_chunks;
0a3441ee 203 }
d28bb90d 204
ea95892e 205 # we already optimized $inner_from above
206 local $self->{_use_join_optimizer} = 0;
d28bb90d 207
ea95892e 208 # generate the subquery
6395604e 209 $self->_select_args_to_query (
ea95892e 210 $inner_from,
211 $inner_select,
212 $where,
213 $inner_attrs,
214 );
d28bb90d 215 };
216
217 # Generate the outer from - this is relatively easy (really just replace
218 # the join slot with the subquery), with a major caveat - we can not
219 # join anything that is non-selecting (not part of the prefetch), but at
220 # the same time is a multi-type relationship, as it will explode the result.
221 #
222 # There are two possibilities here
223 # - either the join is non-restricting, in which case we simply throw it away
224 # - it is part of the restrictions, in which case we need to collapse the outer
225 # result by tackling yet another group_by to the outside of the query
226
27e0370d 227 # work on a shallow copy
052e8431 228 $from = [ @$from ];
052e8431 229
d28bb90d 230 my @outer_from;
53c29913 231
27e0370d 232 # we may not be the head
233 if ($root_source_offset) {
234 # first generate the outer_from, up to the substitution point
235 @outer_from = splice @$from, 0, $root_source_offset;
6395604e 236
27e0370d 237 my $root_node = shift @$from;
238
239 push @outer_from, [
240 {
241 -alias => $attrs->{alias},
242 -rsrc => $root_node->[0]{-rsrc},
243 $attrs->{alias} => $inner_subq,
244 },
245 @{$root_node}[1 .. $#$root_node],
246 ];
247 }
248 else {
249 my $root_node = shift @$from;
250
251 @outer_from = {
252 -alias => $attrs->{alias},
253 -rsrc => $root_node->{-rsrc},
254 $attrs->{alias} => $inner_subq,
255 };
d28bb90d 256 }
257
ea95892e 258 # scan the *remaining* from spec against different attributes, and see which joins are needed
052e8431 259 # in what role
260 my $outer_aliastypes =
539ffe87 261 $self->_resolve_aliastypes_from_select_args( $from, $outer_select, $where, $outer_attrs );
052e8431 262
a4812caa 263 # unroll parents
264 my ($outer_select_chain, $outer_restrict_chain) = map { +{
265 map { $_ => 1 } map { values %$_} map { @$_ } values %{ $outer_aliastypes->{$_} || {} }
266 } } qw/selecting restricting/;
267
d28bb90d 268 # see what's left - throw away if not selecting/restricting
a4812caa 269 # also throw in a group_by if a non-selecting multiplier,
270 # to guard against cross-join explosions
36fd7f07 271 my $need_outer_group_by;
d28bb90d 272 while (my $j = shift @$from) {
273 my $alias = $j->[0]{-alias};
274
a4812caa 275 if (
276 $outer_select_chain->{$alias}
277 ) {
278 push @outer_from, $j
d28bb90d 279 }
a4812caa 280 elsif ($outer_restrict_chain->{$alias}) {
d28bb90d 281 push @outer_from, $j;
a4812caa 282 $need_outer_group_by ||= $outer_aliastypes->{multiplying}{$alias} ? 1 : 0;
d28bb90d 283 }
284 }
285
36fd7f07 286 if ($need_outer_group_by and ! $outer_attrs->{group_by}) {
287
288 my $unprocessed_order_chunks;
289 ($outer_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection (
290 \@outer_from, $outer_select, $outer_attrs->{order_by}
291 );
292
293 $self->throw_exception (
294 'A required group_by clause could not be constructed automatically due to a complex '
295 . 'order_by criteria. Either order_by columns only (no functions) or construct a suitable '
296 . 'group_by by hand'
297 ) if $unprocessed_order_chunks;
298
299 }
300
d28bb90d 301 # This is totally horrific - the $where ends up in both the inner and outer query
302 # Unfortunately not much can be done until SQLA2 introspection arrives, and even
303 # then if where conditions apply to the *right* side of the prefetch, you may have
304 # to both filter the inner select (e.g. to apply a limit) and then have to re-filter
305 # the outer select to exclude joins you didin't want in the first place
306 #
307 # OTOH it can be seen as a plus: <ash> (notes that this query would make a DBA cry ;)
308 return (\@outer_from, $outer_select, $where, $outer_attrs);
309}
310
1a736efb 311#
312# I KNOW THIS SUCKS! GET SQLA2 OUT THE DOOR SO THIS CAN DIE!
313#
ad630f4b 314# Due to a lack of SQLA2 we fall back to crude scans of all the
315# select/where/order/group attributes, in order to determine what
316# aliases are neded to fulfill the query. This information is used
317# throughout the code to prune unnecessary JOINs from the queries
318# in an attempt to reduce the execution time.
319# Although the method is pretty horrific, the worst thing that can
1a736efb 320# happen is for it to fail due to some scalar SQL, which in turn will
321# result in a vocal exception.
539ffe87 322sub _resolve_aliastypes_from_select_args {
052e8431 323 my ( $self, $from, $select, $where, $attrs ) = @_;
546f1cd9 324
ad630f4b 325 $self->throw_exception ('Unable to analyze custom {from}')
326 if ref $from ne 'ARRAY';
546f1cd9 327
ad630f4b 328 # what we will return
964a3c71 329 my $aliases_by_type;
546f1cd9 330
ad630f4b 331 # see what aliases are there to work with
332 my $alias_list;
539ffe87 333 for (@$from) {
334 my $j = $_;
ad630f4b 335 $j = $j->[0] if ref $j eq 'ARRAY';
539ffe87 336 my $al = $j->{-alias}
337 or next;
338
339 $alias_list->{$al} = $j;
a4812caa 340 $aliases_by_type->{multiplying}{$al} ||= $j->{-join_path}||[] if (
341 # not array == {from} head == can't be multiplying
342 ( ref($_) eq 'ARRAY' and ! $j->{-is_single} )
343 or
344 # a parent of ours is already a multiplier
345 ( grep { $aliases_by_type->{multiplying}{$_} } @{ $j->{-join_path}||[] } )
346 );
546f1cd9 347 }
546f1cd9 348
1a736efb 349 # get a column to source/alias map (including unqualified ones)
350 my $colinfo = $self->_resolve_column_info ($from);
351
ad630f4b 352 # set up a botched SQLA
353 my $sql_maker = $self->sql_maker;
07f31d19 354
4c2b30d6 355 # these are throw away results, do not pollute the bind stack
4c2b30d6 356 local $sql_maker->{select_bind};
0542ec57 357 local $sql_maker->{where_bind};
358 local $sql_maker->{group_bind};
359 local $sql_maker->{having_bind};
3f5b99fe 360
361 # we can't scan properly without any quoting (\b doesn't cut it
362 # everywhere), so unless there is proper quoting set - use our
363 # own weird impossible character.
364 # Also in the case of no quoting, we need to explicitly disable
365 # name_sep, otherwise sorry nasty legacy syntax like
366 # { 'count(foo.id)' => { '>' => 3 } } will stop working >:(
367 local $sql_maker->{quote_char} = $sql_maker->{quote_char};
368 local $sql_maker->{name_sep} = $sql_maker->{name_sep};
369
370 unless (defined $sql_maker->{quote_char} and length $sql_maker->{quote_char}) {
e493ecb2 371 $sql_maker->{quote_char} = ["\x00", "\xFF"];
372 # if we don't unset it we screw up retarded but unfortunately working
373 # 'MAX(foo.bar)' => { '>', 3 }
3f5b99fe 374 $sql_maker->{name_sep} = '';
375 }
376
377 my ($lquote, $rquote, $sep) = map { quotemeta $_ } ($sql_maker->_quote_chars, $sql_maker->name_sep);
07f31d19 378
1a736efb 379 # generate sql chunks
380 my $to_scan = {
381 restricting => [
382 $sql_maker->_recurse_where ($where),
a7e643b1 383 $sql_maker->_parse_rs_attrs ({
1a736efb 384 map { $_ => $attrs->{$_} } (qw/group_by having/)
385 }),
386 ],
387 selecting => [
1a736efb 388 $sql_maker->_recurse_fields ($select),
bac358c9 389 ( map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker) ),
1a736efb 390 ],
391 };
07f31d19 392
1a736efb 393 # throw away empty chunks
394 $_ = [ map { $_ || () } @$_ ] for values %$to_scan;
07f31d19 395
1a736efb 396 # first loop through all fully qualified columns and get the corresponding
397 # alias (should work even if they are in scalarrefs)
ad630f4b 398 for my $alias (keys %$alias_list) {
1a736efb 399 my $al_re = qr/
3f5b99fe 400 $lquote $alias $rquote $sep
1a736efb 401 |
3f5b99fe 402 \b $alias \.
1a736efb 403 /x;
404
1a736efb 405 for my $type (keys %$to_scan) {
406 for my $piece (@{$to_scan->{$type}}) {
a4812caa 407 $aliases_by_type->{$type}{$alias} ||= $alias_list->{$alias}{-join_path}||[]
408 if ($piece =~ $al_re);
1a736efb 409 }
ad630f4b 410 }
1a736efb 411 }
412
413 # now loop through unqualified column names, and try to locate them within
414 # the chunks
415 for my $col (keys %$colinfo) {
3f5b99fe 416 next if $col =~ / \. /x; # if column is qualified it was caught by the above
1a736efb 417
3f5b99fe 418 my $col_re = qr/ $lquote $col $rquote /x;
07f31d19 419
1a736efb 420 for my $type (keys %$to_scan) {
421 for my $piece (@{$to_scan->{$type}}) {
a4812caa 422 if ($piece =~ $col_re) {
423 my $alias = $colinfo->{$col}{-source_alias};
424 $aliases_by_type->{$type}{$alias} ||= $alias_list->{$alias}{-join_path}||[];
425 }
1a736efb 426 }
07f31d19 427 }
428 }
429
430 # Add any non-left joins to the restriction list (such joins are indeed restrictions)
ad630f4b 431 for my $j (values %$alias_list) {
07f31d19 432 my $alias = $j->{-alias} or next;
a4812caa 433 $aliases_by_type->{restricting}{$alias} ||= $j->{-join_path}||[] if (
07f31d19 434 (not $j->{-join_type})
435 or
436 ($j->{-join_type} !~ /^left (?: \s+ outer)? $/xi)
437 );
438 }
439
964a3c71 440 return $aliases_by_type;
07f31d19 441}
442
bac358c9 443# This is the engine behind { distinct => 1 }
0a3441ee 444sub _group_over_selection {
445 my ($self, $from, $select, $order_by) = @_;
446
447 my $rs_column_list = $self->_resolve_column_info ($from);
448
449 my (@group_by, %group_index);
450
36fd7f07 451 # the logic is: if it is a { func => val } we assume an aggregate,
452 # otherwise if \'...' or \[...] we assume the user knows what is
453 # going on thus group over it
0a3441ee 454 for (@$select) {
455 if (! ref($_) or ref ($_) ne 'HASH' ) {
456 push @group_by, $_;
457 $group_index{$_}++;
458 if ($rs_column_list->{$_} and $_ !~ /\./ ) {
459 # add a fully qualified version as well
460 $group_index{"$rs_column_list->{$_}{-source_alias}.$_"}++;
461 }
07f31d19 462 }
463 }
ad630f4b 464
0a3441ee 465 # add any order_by parts that are not already present in the group_by
466 # we need to be careful not to add any named functions/aggregates
bac358c9 467 # i.e. order_by => [ ... { count => 'foo' } ... ]
14e26c5f 468 my @leftovers;
bac358c9 469 for ($self->_extract_order_criteria($order_by)) {
0a3441ee 470 # only consider real columns (for functions the user got to do an explicit group_by)
14e26c5f 471 if (@$_ != 1) {
472 push @leftovers, $_;
473 next;
474 }
bac358c9 475 my $chunk = $_->[0];
14e26c5f 476 my $colinfo = $rs_column_list->{$chunk} or do {
477 push @leftovers, $_;
478 next;
479 };
0a3441ee 480
481 $chunk = "$colinfo->{-source_alias}.$chunk" if $chunk !~ /\./;
482 push @group_by, $chunk unless $group_index{$chunk}++;
483 }
484
14e26c5f 485 return wantarray
486 ? (\@group_by, (@leftovers ? \@leftovers : undef) )
487 : \@group_by
488 ;
07f31d19 489}
490
d28bb90d 491sub _resolve_ident_sources {
492 my ($self, $ident) = @_;
493
494 my $alias2source = {};
495 my $rs_alias;
496
497 # the reason this is so contrived is that $ident may be a {from}
498 # structure, specifying multiple tables to join
6298a324 499 if ( blessed $ident && $ident->isa("DBIx::Class::ResultSource") ) {
d28bb90d 500 # this is compat mode for insert/update/delete which do not deal with aliases
501 $alias2source->{me} = $ident;
502 $rs_alias = 'me';
503 }
504 elsif (ref $ident eq 'ARRAY') {
505
506 for (@$ident) {
507 my $tabinfo;
508 if (ref $_ eq 'HASH') {
509 $tabinfo = $_;
510 $rs_alias = $tabinfo->{-alias};
511 }
512 if (ref $_ eq 'ARRAY' and ref $_->[0] eq 'HASH') {
513 $tabinfo = $_->[0];
514 }
515
4376a157 516 $alias2source->{$tabinfo->{-alias}} = $tabinfo->{-rsrc}
517 if ($tabinfo->{-rsrc});
d28bb90d 518 }
519 }
520
521 return ($alias2source, $rs_alias);
522}
523
524# Takes $ident, \@column_names
525#
526# returns { $column_name => \%column_info, ... }
527# also note: this adds -result_source => $rsrc to the column info
528#
09e14fdc 529# If no columns_names are supplied returns info about *all* columns
530# for all sources
d28bb90d 531sub _resolve_column_info {
532 my ($self, $ident, $colnames) = @_;
533 my ($alias2src, $root_alias) = $self->_resolve_ident_sources($ident);
534
52416317 535 my (%seen_cols, @auto_colnames);
d28bb90d 536
537 # compile a global list of column names, to be able to properly
538 # disambiguate unqualified column names (if at all possible)
539 for my $alias (keys %$alias2src) {
540 my $rsrc = $alias2src->{$alias};
541 for my $colname ($rsrc->columns) {
542 push @{$seen_cols{$colname}}, $alias;
3f5b99fe 543 push @auto_colnames, "$alias.$colname" unless $colnames;
d28bb90d 544 }
545 }
546
09e14fdc 547 $colnames ||= [
548 @auto_colnames,
549 grep { @{$seen_cols{$_}} == 1 } (keys %seen_cols),
550 ];
551
52416317 552 my (%return, $colinfos);
d28bb90d 553 foreach my $col (@$colnames) {
52416317 554 my ($source_alias, $colname) = $col =~ m/^ (?: ([^\.]+) \. )? (.+) $/x;
d28bb90d 555
52416317 556 # if the column was seen exactly once - we know which rsrc it came from
557 $source_alias ||= $seen_cols{$colname}[0]
558 if ($seen_cols{$colname} and @{$seen_cols{$colname}} == 1);
d28bb90d 559
52416317 560 next unless $source_alias;
561
562 my $rsrc = $alias2src->{$source_alias}
563 or next;
564
565 $return{$col} = {
6395604e 566 %{
567 ( $colinfos->{$source_alias} ||= $rsrc->columns_info )->{$colname}
568 ||
569 $self->throw_exception(
570 "No such column '$colname' on source " . $rsrc->source_name
571 );
572 },
d28bb90d 573 -result_source => $rsrc,
52416317 574 -source_alias => $source_alias,
81bf295c 575 -fq_colname => $col eq $colname ? "$source_alias.$col" : $col,
576 -colname => $colname,
d28bb90d 577 };
81bf295c 578
579 $return{"$source_alias.$colname"} = $return{$col} if $col eq $colname;
d28bb90d 580 }
581
582 return \%return;
583}
584
289ac713 585# The DBIC relationship chaining implementation is pretty simple - every
586# new related_relationship is pushed onto the {from} stack, and the {select}
587# window simply slides further in. This means that when we count somewhere
588# in the middle, we got to make sure that everything in the join chain is an
589# actual inner join, otherwise the count will come back with unpredictable
590# results (a resultset may be generated with _some_ rows regardless of if
591# the relation which the $rs currently selects has rows or not). E.g.
592# $artist_rs->cds->count - normally generates:
593# SELECT COUNT( * ) FROM artist me LEFT JOIN cd cds ON cds.artist = me.artistid
594# which actually returns the number of artists * (number of cds || 1)
595#
596# So what we do here is crawl {from}, determine if the current alias is at
597# the top of the stack, and if not - make sure the chain is inner-joined down
598# to the root.
599#
31a8aaaf 600sub _inner_join_to_node {
289ac713 601 my ($self, $from, $alias) = @_;
602
603 # subqueries and other oddness are naturally not supported
604 return $from if (
605 ref $from ne 'ARRAY'
606 ||
607 @$from <= 1
608 ||
609 ref $from->[0] ne 'HASH'
610 ||
611 ! $from->[0]{-alias}
612 ||
7eb76996 613 $from->[0]{-alias} eq $alias # this last bit means $alias is the head of $from - nothing to do
289ac713 614 );
615
616 # find the current $alias in the $from structure
617 my $switch_branch;
618 JOINSCAN:
619 for my $j (@{$from}[1 .. $#$from]) {
620 if ($j->[0]{-alias} eq $alias) {
621 $switch_branch = $j->[0]{-join_path};
622 last JOINSCAN;
623 }
624 }
625
7eb76996 626 # something else went quite wrong
289ac713 627 return $from unless $switch_branch;
628
629 # So it looks like we will have to switch some stuff around.
630 # local() is useless here as we will be leaving the scope
631 # anyway, and deep cloning is just too fucking expensive
8273e845 632 # So replace the first hashref in the node arrayref manually
289ac713 633 my @new_from = ($from->[0]);
faeb2407 634 my $sw_idx = { map { (values %$_), 1 } @$switch_branch }; #there's one k/v per join-path
289ac713 635
636 for my $j (@{$from}[1 .. $#$from]) {
637 my $jalias = $j->[0]{-alias};
638
639 if ($sw_idx->{$jalias}) {
640 my %attrs = %{$j->[0]};
641 delete $attrs{-join_type};
642 push @new_from, [
643 \%attrs,
644 @{$j}[ 1 .. $#$j ],
645 ];
646 }
647 else {
648 push @new_from, $j;
649 }
650 }
651
652 return \@new_from;
653}
654
ea5c7509 655# yet another atrocity: attempt to extract all columns from a
656# where condition by hooking _quote
657sub _extract_condition_columns {
27e0370d 658 my ($self, $cond, $sql_maker_cache) = @_;
ea5c7509 659
660 return [] unless $cond;
661
27e0370d 662 my $sm = $sql_maker_cache->{condparser} ||= $self->{_sql_ident_capturer} ||= do {
ea5c7509 663 # FIXME - replace with a Moo trait
664 my $orig_sm_class = ref $self->sql_maker;
665 my $smic_class = "${orig_sm_class}::_IdentCapture_";
666
667 unless ($smic_class->isa('SQL::Abstract')) {
668
669 no strict 'refs';
670 *{"${smic_class}::_quote"} = subname "${smic_class}::_quote" => sub {
671 my ($self, $ident) = @_;
672 if (ref $ident eq 'SCALAR') {
673 $ident = $$ident;
674 my $storage_quotes = $self->sql_quote_char || '"';
675 my ($ql, $qr) = map
676 { quotemeta $_ }
677 (ref $storage_quotes eq 'ARRAY' ? @$storage_quotes : ($storage_quotes) x 2 )
678 ;
679
680 while ($ident =~ /
681 $ql (\w+) $qr
682 |
683 ([\w\.]+)
684 /xg) {
685 $self->{_captured_idents}{$1||$2}++;
686 }
bac6c4fb 687 }
037e8dca 688 else {
ea5c7509 689 $self->{_captured_idents}{$ident}++;
037e8dca 690 }
ea5c7509 691 return $ident;
692 };
693
694 *{"${smic_class}::_get_captured_idents"} = subname "${smic_class}::_get_captures" => sub {
695 (delete shift->{_captured_idents}) || {};
696 };
697
698 $self->inject_base ($smic_class, $orig_sm_class);
699
bac6c4fb 700 }
ea5c7509 701
702 $smic_class->new();
703 };
704
27e0370d 705 $sm->_recurse_where($cond);
ea5c7509 706
27e0370d 707 return [ sort keys %{$sm->_get_captured_idents} ];
ea5c7509 708}
709
bac358c9 710sub _extract_order_criteria {
1a736efb 711 my ($self, $order_by, $sql_maker) = @_;
c0748280 712
1a736efb 713 my $parser = sub {
714 my ($sql_maker, $order_by) = @_;
c0748280 715
1a736efb 716 return scalar $sql_maker->_order_by_chunks ($order_by)
717 unless wantarray;
c0748280 718
1a736efb 719 my @chunks;
bac358c9 720 for ($sql_maker->_order_by_chunks ($order_by) ) {
721 my $chunk = ref $_ ? $_ : [ $_ ];
722 $chunk->[0] =~ s/\s+ (?: ASC|DESC ) \s* $//ix;
1a736efb 723 push @chunks, $chunk;
bac6c4fb 724 }
1a736efb 725
726 return @chunks;
727 };
728
729 if ($sql_maker) {
730 return $parser->($sql_maker, $order_by);
bac6c4fb 731 }
732 else {
1a736efb 733 $sql_maker = $self->sql_maker;
734 local $sql_maker->{quote_char};
735 return $parser->($sql_maker, $order_by);
bac6c4fb 736 }
bac6c4fb 737}
738
7cec4356 739sub _order_by_is_stable {
5f11e54f 740 my ($self, $ident, $order_by, $where) = @_;
c0748280 741
5f11e54f 742 my $colinfo = $self->_resolve_column_info($ident, [
743 (map { $_->[0] } $self->_extract_order_criteria($order_by)),
744 $where ? @{$self->_extract_fixed_condition_columns($where)} :(),
745 ]);
c0748280 746
7cec4356 747 return undef unless keys %$colinfo;
748
749 my $cols_per_src;
750 $cols_per_src->{$_->{-source_alias}}{$_->{-colname}} = $_ for values %$colinfo;
751
752 for (values %$cols_per_src) {
753 my $src = (values %$_)[0]->{-result_source};
754 return 1 if $src->_identifying_column_set($_);
c0748280 755 }
756
7cec4356 757 return undef;
758}
759
5f11e54f 760# returns an arrayref of column names which *definitely* have som
761# sort of non-nullable equality requested in the given condition
762# specification. This is used to figure out if a resultset is
763# constrained to a column which is part of a unique constraint,
764# which in turn allows us to better predict how ordering will behave
765# etc.
766#
767# this is a rudimentary, incomplete, and error-prone extractor
768# however this is OK - it is conservative, and if we can not find
769# something that is in fact there - the stack will recover gracefully
770# Also - DQ and the mst it rode in on will save us all RSN!!!
771sub _extract_fixed_condition_columns {
772 my ($self, $where, $nested) = @_;
773
774 return unless ref $where eq 'HASH';
775
776 my @cols;
777 for my $lhs (keys %$where) {
778 if ($lhs =~ /^\-and$/i) {
779 push @cols, ref $where->{$lhs} eq 'ARRAY'
780 ? ( map { $self->_extract_fixed_condition_columns($_, 1) } @{$where->{$lhs}} )
781 : $self->_extract_fixed_condition_columns($where->{$lhs}, 1)
782 ;
783 }
784 elsif ($lhs !~ /^\-/) {
785 my $val = $where->{$lhs};
786
787 push @cols, $lhs if (defined $val and (
788 ! ref $val
789 or
790 (ref $val eq 'HASH' and keys %$val == 1 and defined $val->{'='})
791 ));
792 }
793 }
794 return $nested ? @cols : \@cols;
c0748280 795}
bac6c4fb 796
d28bb90d 7971;