5af94879910b09ba90d794e8f5853040d56df421
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / SQLMaker / LimitDialects.pm
1 package DBIx::Class::SQLMaker::LimitDialects;
2
3 use warnings;
4 use strict;
5
6 use List::Util 'first';
7 use namespace::clean;
8
9 # constants are used not only here, but also in comparison tests
10 sub __rows_bindtype () {
11   +{ sqlt_datatype => 'integer' }
12 }
13 sub __offset_bindtype () {
14   +{ sqlt_datatype => 'integer' }
15 }
16 sub __total_bindtype () {
17   +{ sqlt_datatype => 'integer' }
18 }
19
20 =head1 NAME
21
22 DBIx::Class::SQLMaker::LimitDialects - SQL::Abstract::Limit-like functionality for DBIx::Class::SQLMaker
23
24 =head1 DESCRIPTION
25
26 This module replicates a lot of the functionality originally found in
27 L<SQL::Abstract::Limit>. While simple limits would work as-is, the more
28 complex dialects that require e.g. subqueries could not be reliably
29 implemented without taking full advantage of the metadata locked within
30 L<DBIx::Class::ResultSource> classes. After reimplementation of close to
31 80% of the L<SQL::Abstract::Limit> functionality it was deemed more
32 practical to simply make an independent DBIx::Class-specific limit-dialect
33 provider.
34
35 =head1 SQL LIMIT DIALECTS
36
37 Note that the actual implementations listed below never use C<*> literally.
38 Instead proper re-aliasing of selectors and order criteria is done, so that
39 the limit dialect are safe to use on joined resultsets with clashing column
40 names.
41
42 Currently the provided dialects are:
43
44 =head2 LimitOffset
45
46  SELECT ... LIMIT $limit OFFSET $offset
47
48 Supported by B<PostgreSQL> and B<SQLite>
49
50 =cut
51 sub _LimitOffset {
52     my ( $self, $sql, $rs_attrs, $rows, $offset ) = @_;
53     $sql .= $self->_parse_rs_attrs( $rs_attrs ) . " LIMIT ?";
54     push @{$self->{limit_bind}}, [ $self->__rows_bindtype => $rows ];
55     if ($offset) {
56       $sql .= " OFFSET ?";
57       push @{$self->{limit_bind}}, [ $self->__offset_bindtype => $offset ];
58     }
59     return $sql;
60 }
61
62 =head2 LimitXY
63
64  SELECT ... LIMIT $offset $limit
65
66 Supported by B<MySQL> and any L<SQL::Statement> based DBD
67
68 =cut
69 sub _LimitXY {
70     my ( $self, $sql, $rs_attrs, $rows, $offset ) = @_;
71     $sql .= $self->_parse_rs_attrs( $rs_attrs ) . " LIMIT ";
72     if ($offset) {
73       $sql .= '?, ';
74       push @{$self->{limit_bind}}, [ $self->__offset_bindtype => $offset ];
75     }
76     $sql .= '?';
77     push @{$self->{limit_bind}}, [ $self->__rows_bindtype => $rows ];
78
79     return $sql;
80 }
81
82 =head2 RowNumberOver
83
84  SELECT * FROM (
85   SELECT *, ROW_NUMBER() OVER( ORDER BY ... ) AS RNO__ROW__INDEX FROM (
86    SELECT ...
87   )
88  ) WHERE RNO__ROW__INDEX BETWEEN ($offset+1) AND ($limit+$offset)
89
90
91 ANSI standard Limit/Offset implementation. Supported by B<DB2> and
92 B<< MSSQL >= 2005 >>.
93
94 =cut
95 sub _RowNumberOver {
96   my ($self, $sql, $rs_attrs, $rows, $offset ) = @_;
97
98   # get selectors, and scan the order_by (if any)
99   my $sq_attrs = $self->_subqueried_limit_attrs ( $sql, $rs_attrs );
100
101   # make up an order if none exists
102   my $requested_order = (delete $rs_attrs->{order_by}) || $self->_rno_default_order;
103   my $rno_ord = $self->_order_by ($requested_order);
104
105   # this is the order supplement magic
106   my $mid_sel = $sq_attrs->{selection_outer};
107   if (my $extra_order_sel = $sq_attrs->{order_supplement}) {
108     for my $extra_col (sort
109       { $extra_order_sel->{$a} cmp $extra_order_sel->{$b} }
110       keys %$extra_order_sel
111     ) {
112       $sq_attrs->{selection_inner} .= sprintf (', %s AS %s',
113         $extra_col,
114         $extra_order_sel->{$extra_col},
115       );
116     }
117   }
118
119   # and this is order re-alias magic
120   for ($sq_attrs->{order_supplement}, $sq_attrs->{outer_renames}) {
121     for my $col (keys %$_) {
122       my $re_col = quotemeta ($col);
123       $rno_ord =~ s/$re_col/$_->{$col}/;
124     }
125   }
126
127   # whatever is left of the order_by (only where is processed at this point)
128   my $group_having = $self->_parse_rs_attrs($rs_attrs);
129
130   my $qalias = $self->_quote ($rs_attrs->{alias});
131   my $idx_name = $self->_quote ('rno__row__index');
132
133   push @{$self->{limit_bind}}, [ $self->__offset_bindtype => $offset + 1], [ $self->__total_bindtype => $offset + $rows ];
134
135   return <<EOS;
136
137 SELECT $sq_attrs->{selection_outer} FROM (
138   SELECT $mid_sel, ROW_NUMBER() OVER( $rno_ord ) AS $idx_name FROM (
139     SELECT $sq_attrs->{selection_inner} $sq_attrs->{query_leftover}${group_having}
140   ) $qalias
141 ) $qalias WHERE $idx_name >= ? AND $idx_name <= ?
142
143 EOS
144
145 }
146
147 # some databases are happy with OVER (), some need OVER (ORDER BY (SELECT (1)) )
148 sub _rno_default_order {
149   return undef;
150 }
151
152 =head2 SkipFirst
153
154  SELECT SKIP $offset FIRST $limit * FROM ...
155
156 Suported by B<Informix>, almost like LimitOffset. According to
157 L<SQL::Abstract::Limit> C<... SKIP $offset LIMIT $limit ...> is also supported.
158
159 =cut
160 sub _SkipFirst {
161   my ($self, $sql, $rs_attrs, $rows, $offset) = @_;
162
163   $sql =~ s/^ \s* SELECT \s+ //ix
164     or $self->throw_exception("Unrecognizable SELECT: $sql");
165
166   return sprintf ('SELECT %s%s%s%s',
167     $offset
168       ? do {
169          push @{$self->{pre_select_bind}}, [ $self->__offset_bindtype => $offset];
170          'SKIP ? '
171       }
172       : ''
173     ,
174     do {
175        push @{$self->{pre_select_bind}}, [ $self->__rows_bindtype => $rows ];
176        'FIRST ? '
177     },
178     $sql,
179     $self->_parse_rs_attrs ($rs_attrs),
180   );
181 }
182
183 =head2 FirstSkip
184
185  SELECT FIRST $limit SKIP $offset * FROM ...
186
187 Supported by B<Firebird/Interbase>, reverse of SkipFirst. According to
188 L<SQL::Abstract::Limit> C<... ROWS $limit TO $offset ...> is also supported.
189
190 =cut
191 sub _FirstSkip {
192   my ($self, $sql, $rs_attrs, $rows, $offset) = @_;
193
194   $sql =~ s/^ \s* SELECT \s+ //ix
195     or $self->throw_exception("Unrecognizable SELECT: $sql");
196
197   return sprintf ('SELECT %s%s%s%s',
198     do {
199        push @{$self->{pre_select_bind}}, [ $self->__rows_bindtype => $rows ];
200        'FIRST ? '
201     },
202     $offset
203       ? do {
204          push @{$self->{pre_select_bind}}, [ $self->__offset_bindtype => $offset];
205          'SKIP ? '
206       }
207       : ''
208     ,
209     $sql,
210     $self->_parse_rs_attrs ($rs_attrs),
211   );
212 }
213
214
215 =head2 RowNum
216
217 Depending on the resultset attributes one of:
218
219  SELECT * FROM (
220   SELECT *, ROWNUM rownum__index FROM (
221    SELECT ...
222   ) WHERE ROWNUM <= ($limit+$offset)
223  ) WHERE rownum__index >= ($offset+1)
224
225 or
226
227  SELECT * FROM (
228   SELECT *, ROWNUM rownum__index FROM (
229     SELECT ...
230   )
231  ) WHERE rownum__index BETWEEN ($offset+1) AND ($limit+$offset)
232
233 or
234
235  SELECT * FROM (
236     SELECT ...
237   ) WHERE ROWNUM <= ($limit+1)
238
239 Supported by B<Oracle>.
240
241 =cut
242 sub _RowNum {
243   my ( $self, $sql, $rs_attrs, $rows, $offset ) = @_;
244
245   my $sq_attrs = $self->_subqueried_limit_attrs ($sql, $rs_attrs);
246
247   my $qalias = $self->_quote ($rs_attrs->{alias});
248   my $idx_name = $self->_quote ('rownum__index');
249   my $order_group_having = $self->_parse_rs_attrs($rs_attrs);
250
251
252   # if no offset (e.g. first page) - we can skip one of the subqueries
253   if (! $offset) {
254     push @{$self->{limit_bind}}, [ $self->__rows_bindtype => $rows ];
255
256     return <<EOS;
257 SELECT $sq_attrs->{selection_outer} FROM (
258   SELECT $sq_attrs->{selection_inner} $sq_attrs->{query_leftover}${order_group_having}
259 ) $qalias WHERE ROWNUM <= ?
260 EOS
261   }
262
263   #
264   # There are two ways to limit in Oracle, one vastly faster than the other
265   # on large resultsets: https://decipherinfosys.wordpress.com/2007/08/09/paging-and-countstopkey-optimization/
266   # However Oracle is retarded and does not preserve stable ROWNUM() values
267   # when called twice in the same scope. Therefore unless the resultset is
268   # ordered by a unique set of columns, it is not safe to use the faster
269   # method, and the slower BETWEEN query is used instead
270   #
271   # FIXME - this is quite expensive, and does not perform caching of any sort
272   # as soon as some of the DQ work becomes viable consider switching this
273   # over
274   if (
275     $rs_attrs->{order_by}
276       and
277     $rs_attrs->{_rsroot_rsrc}->storage->_order_by_is_stable(
278       $rs_attrs->{from}, $rs_attrs->{order_by}
279     )
280   ) {
281     push @{$self->{limit_bind}}, [ $self->__total_bindtype => $offset + $rows ], [ $self->__offset_bindtype => $offset + 1 ];
282
283     return <<EOS;
284 SELECT $sq_attrs->{selection_outer} FROM (
285   SELECT $sq_attrs->{selection_outer}, ROWNUM $idx_name FROM (
286     SELECT $sq_attrs->{selection_inner} $sq_attrs->{query_leftover}${order_group_having}
287   ) $qalias WHERE ROWNUM <= ?
288 ) $qalias WHERE $idx_name >= ?
289 EOS
290   }
291   else {
292     push @{$self->{limit_bind}}, [ $self->__offset_bindtype => $offset + 1 ], [ $self->__total_bindtype => $offset + $rows ];
293
294     return <<EOS;
295 SELECT $sq_attrs->{selection_outer} FROM (
296   SELECT $sq_attrs->{selection_outer}, ROWNUM $idx_name FROM (
297     SELECT $sq_attrs->{selection_inner} $sq_attrs->{query_leftover}${order_group_having}
298   ) $qalias
299 ) $qalias WHERE $idx_name BETWEEN ? AND ?
300 EOS
301   }
302 }
303
304 # used by _Top and _FetchFirst below
305 sub _prep_for_skimming_limit {
306   my ( $self, $sql, $rs_attrs ) = @_;
307
308   # get selectors
309   my $sq_attrs = $self->_subqueried_limit_attrs ($sql, $rs_attrs);
310
311   my $requested_order = delete $rs_attrs->{order_by};
312   $sq_attrs->{order_by_requested} = $self->_order_by ($requested_order);
313   $sq_attrs->{grpby_having} = $self->_parse_rs_attrs ($rs_attrs);
314
315   # make up an order unless supplied or sanity check what we are given
316   my $inner_order;
317   if ($sq_attrs->{order_by_requested}) {
318     $self->throw_exception (
319       'Unable to safely perform "skimming type" limit with supplied unstable order criteria'
320     ) unless $rs_attrs->{_rsroot_rsrc}->schema->storage->_order_by_is_stable(
321       $rs_attrs->{from},
322       $requested_order
323     );
324
325     $inner_order = $requested_order;
326   }
327   else {
328     $inner_order = [ map
329       { "$rs_attrs->{alias}.$_" }
330       ( @{
331         $rs_attrs->{_rsroot_rsrc}->_identifying_column_set
332           ||
333         $self->throw_exception(sprintf(
334           'Unable to auto-construct stable order criteria for "skimming type" limit '
335         . "dialect based on source '%s'", $rs_attrs->{_rsroot_rsrc}->name) );
336       } )
337     ];
338   }
339
340   # localise as we already have all the bind values we need
341   local $self->{order_bind};
342
343   $sq_attrs->{order_by_inner} = $self->_order_by ($inner_order);
344
345   my @out_chunks;
346   for my $ch ($self->_order_by_chunks ($inner_order)) {
347     $ch = $ch->[0] if ref $ch eq 'ARRAY';
348
349     $ch =~ s/\s+ ( ASC|DESC ) \s* $//ix;
350     my $dir = uc ($1||'ASC');
351
352     push @out_chunks, \join (' ', $ch, $dir eq 'ASC' ? 'DESC' : 'ASC' );
353   }
354
355   $sq_attrs->{quoted_rs_alias} = $self->_quote ($rs_attrs->{alias});
356   $sq_attrs->{order_by_middle} = $self->_order_by (\@out_chunks);
357   $sq_attrs->{selection_middle} = $sq_attrs->{selection_outer};
358
359   # this is the order supplement magic
360   if (my $extra_order_sel = $sq_attrs->{order_supplement}) {
361     for my $extra_col (sort
362       { $extra_order_sel->{$a} cmp $extra_order_sel->{$b} }
363       keys %$extra_order_sel
364     ) {
365       $sq_attrs->{selection_inner} .= sprintf (', %s AS %s',
366         $extra_col,
367         $extra_order_sel->{$extra_col},
368       );
369
370       $sq_attrs->{selection_middle} .= ', ' . $extra_order_sel->{$extra_col};
371     }
372
373     # Whatever order bindvals there are, they will be realiased and
374     # reselected, and need to show up at end of the initial inner select
375     push @{$self->{select_bind}}, @{$self->{order_bind}};
376
377     # if this is a part of something bigger, we need to add back all
378     # the extra order_by's, as they may be relied upon by the outside
379     # of a prefetch or something
380     if ($rs_attrs->{_is_internal_subuery}) {
381       $sq_attrs->{selection_outer} .= sprintf ", $extra_order_sel->{$_} AS $_"
382         for sort
383           { $extra_order_sel->{$a} cmp $extra_order_sel->{$b} }
384             grep { $_ !~ /[^\w\-]/ }  # ignore functions
385             keys %$extra_order_sel
386       ;
387     }
388   }
389
390   # and this is order re-alias magic
391   for my $map ($sq_attrs->{order_supplement}, $sq_attrs->{outer_renames}) {
392     for my $col (sort { $map->{$a} cmp $map->{$b} } keys %{$map||{}}) {
393       my $re_col = quotemeta ($col);
394       $_ =~ s/$re_col/$map->{$col}/
395         for ($sq_attrs->{order_by_middle}, $sq_attrs->{order_by_requested});
396     }
397   }
398
399   $sq_attrs;
400 }
401
402 =head2 Top
403
404  SELECT * FROM
405
406  SELECT TOP $limit FROM (
407   SELECT TOP $limit FROM (
408    SELECT TOP ($limit+$offset) ...
409   ) ORDER BY $reversed_original_order
410  ) ORDER BY $original_order
411
412 Unreliable Top-based implementation, supported by B<< MSSQL < 2005 >>.
413
414 =head3 CAVEAT
415
416 Due to its implementation, this limit dialect returns B<incorrect results>
417 when $limit+$offset > total amount of rows in the resultset.
418
419 =cut
420
421 sub _Top {
422   my ( $self, $sql, $rs_attrs, $rows, $offset ) = @_;
423
424   my $lim = $self->_prep_for_skimming_limit($sql, $rs_attrs);
425
426   $sql = sprintf ('SELECT TOP %u %s %s %s %s',
427     $rows + ($offset||0),
428     $lim->{selection_inner},
429     $lim->{query_leftover},
430     $lim->{grpby_having},
431     $lim->{order_by_inner},
432   );
433
434   $sql = sprintf ('SELECT TOP %u %s FROM ( %s ) %s %s',
435     $rows,
436     $lim->{selection_middle},
437     $sql,
438     $lim->{quoted_rs_alias},
439     $lim->{order_by_middle},
440   ) if $offset;
441
442   $sql = sprintf ('SELECT TOP %u %s FROM ( %s ) %s %s',
443     $rows,
444     $lim->{selection_outer},
445     $sql,
446     $lim->{quoted_rs_alias},
447     $lim->{order_by_requested},
448   ) if $offset and (
449     $lim->{order_by_requested} or $lim->{selection_middle} ne $lim->{selection_outer}
450   );
451
452   return $sql;
453 }
454
455 =head2 FetchFirst
456
457  SELECT * FROM
458  (
459  SELECT * FROM (
460   SELECT * FROM (
461    SELECT * FROM ...
462   ) ORDER BY $reversed_original_order
463     FETCH FIRST $limit ROWS ONLY
464  ) ORDER BY $original_order
465    FETCH FIRST $limit ROWS ONLY
466  )
467
468 Unreliable FetchFirst-based implementation, supported by B<< IBM DB2 <= V5R3 >>.
469
470 =head3 CAVEAT
471
472 Due to its implementation, this limit dialect returns B<incorrect results>
473 when $limit+$offset > total amount of rows in the resultset.
474
475 =cut
476
477 sub _FetchFirst {
478   my ( $self, $sql, $rs_attrs, $rows, $offset ) = @_;
479
480   my $lim = $self->_prep_for_skimming_limit($sql, $rs_attrs);
481
482   $sql = sprintf ('SELECT %s %s %s %s FETCH FIRST %u ROWS ONLY',
483     $lim->{selection_inner},
484     $lim->{query_leftover},
485     $lim->{grpby_having},
486     $lim->{order_by_inner},
487     $rows + ($offset||0),
488   );
489
490   $sql = sprintf ('SELECT %s FROM ( %s ) %s %s FETCH FIRST %u ROWS ONLY',
491     $lim->{selection_middle},
492     $sql,
493     $lim->{quoted_rs_alias},
494     $lim->{order_by_middle},
495     $rows,
496   ) if $offset;
497
498
499   $sql = sprintf ('SELECT %s FROM ( %s ) %s %s FETCH FIRST %u ROWS ONLY',
500     $lim->{selection_outer},
501     $sql,
502     $lim->{quoted_rs_alias},
503     $lim->{order_by_requested},
504     $rows,
505   ) if $offset and (
506     $lim->{order_by_requested} or $lim->{selection_middle} ne $lim->{selection_outer}
507   );
508
509   return $sql;
510 }
511
512 =head2 RowCountOrGenericSubQ
513
514 This is not exactly a limit dialect, but more of a proxy for B<Sybase ASE>.
515 If no $offset is supplied the limit is simply performed as:
516
517  SET ROWCOUNT $limit
518  SELECT ...
519  SET ROWCOUNT 0
520
521 Otherwise we fall back to L</GenericSubQ>
522
523 =cut
524
525 sub _RowCountOrGenericSubQ {
526   my $self = shift;
527   my ($sql, $rs_attrs, $rows, $offset) = @_;
528
529   return $self->_GenericSubQ(@_) if $offset;
530
531   return sprintf <<"EOF", $rows, $sql;
532 SET ROWCOUNT %d
533 %s
534 SET ROWCOUNT 0
535 EOF
536 }
537
538 =head2 GenericSubQ
539
540  SELECT * FROM (
541   SELECT ...
542  )
543  WHERE (
544   SELECT COUNT(*) FROM $original_table cnt WHERE cnt.id < $original_table.id
545  ) BETWEEN $offset AND ($offset+$rows-1)
546
547 This is the most evil limit "dialect" (more of a hack) for I<really> stupid
548 databases. It works by ordering the set by some unique column, and calculating
549 the amount of rows that have a less-er value (thus emulating a L</RowNum>-like
550 index). Of course this implies the set can only be ordered by a single unique
551 column. Also note that this technique can be and often is B<excruciatingly
552 slow>.
553
554 Currently used by B<Sybase ASE>, due to lack of any other option.
555
556 =cut
557 sub _GenericSubQ {
558   my ($self, $sql, $rs_attrs, $rows, $offset) = @_;
559
560   my $root_rsrc = $rs_attrs->{_rsroot_rsrc};
561   my $root_tbl_name = $root_rsrc->name;
562
563   my ($first_order_by) = do {
564     local $self->{quote_char};
565     map { ref $_ ? $_->[0] : $_ } $self->_order_by_chunks ($rs_attrs->{order_by})
566   } or $self->throw_exception (
567     'Generic Subquery Limit does not work on resultsets without an order. Provide a single, '
568   . 'unique-column order criteria.'
569   );
570
571   $first_order_by =~ s/\s+ ( ASC|DESC ) \s* $//ix;
572   my $direction = lc ($1 || 'asc');
573
574   my ($first_ord_alias, $first_ord_col) = $first_order_by =~ /^ (?: ([^\.]+) \. )? ([^\.]+) $/x;
575
576   $self->throw_exception(sprintf
577     "Generic Subquery Limit order criteria can be only based on the root-source '%s'"
578   . " (aliased as '%s')", $root_rsrc->source_name, $rs_attrs->{alias},
579   ) if ($first_ord_alias and $first_ord_alias ne $rs_attrs->{alias});
580
581   $first_ord_alias ||= $rs_attrs->{alias};
582
583   $self->throw_exception(
584     "Generic Subquery Limit first order criteria '$first_ord_col' must be unique"
585   ) unless $root_rsrc->_identifying_column_set([$first_ord_col]);
586
587   my $sq_attrs = $self->_subqueried_limit_attrs ($sql, $rs_attrs);
588
589   my $cmp_op = $direction eq 'desc' ? '>' : '<';
590   my $count_tbl_alias = 'rownum__emulation';
591
592   my $order_sql = $self->_order_by (delete $rs_attrs->{order_by});
593   my $group_having_sql = $self->_parse_rs_attrs($rs_attrs);
594
595   my $in_sel = $sq_attrs->{selection_inner};
596
597   # add the order supplement (if any) as this is what will be used for the outer WHERE
598   $in_sel .= ", $_" for keys %{$sq_attrs->{order_supplement}};
599
600   my $rownum_cond;
601   if ($offset) {
602     $rownum_cond = 'BETWEEN ? AND ?';
603
604     push @{$self->{limit_bind}},
605       [ $self->__offset_bindtype => $offset ],
606       [ $self->__total_bindtype => $offset + $rows - 1]
607     ;
608   }
609   else {
610     $rownum_cond = '< ?';
611
612     push @{$self->{limit_bind}},
613       [ $self->__rows_bindtype => $rows ]
614     ;
615   }
616
617   return sprintf ("
618 SELECT $sq_attrs->{selection_outer}
619   FROM (
620     SELECT $in_sel $sq_attrs->{query_leftover}${group_having_sql}
621   ) %s
622 WHERE ( SELECT COUNT(*) FROM %s %s WHERE %s $cmp_op %s ) $rownum_cond
623 $order_sql
624   ", map { $self->_quote ($_) } (
625     $rs_attrs->{alias},
626     $root_tbl_name,
627     $count_tbl_alias,
628     "$count_tbl_alias.$first_ord_col",
629     "$first_ord_alias.$first_ord_col",
630   ));
631 }
632
633
634 # !!! THIS IS ALSO HORRIFIC !!! /me ashamed
635 #
636 # Generates inner/outer select lists for various limit dialects
637 # which result in one or more subqueries (e.g. RNO, Top, RowNum)
638 # Any non-root-table columns need to have their table qualifier
639 # turned into a column alias (otherwise names in subqueries clash
640 # and/or lose their source table)
641 #
642 # Returns mangled proto-sql, inner/outer strings of SQL QUOTED selectors
643 # with aliases (to be used in whatever select statement), and an alias
644 # index hashref of QUOTED SEL => QUOTED ALIAS pairs (to maybe be used
645 # for string-subst higher up).
646 # If an order_by is supplied, the inner select needs to bring out columns
647 # used in implicit (non-selected) orders, and the order condition itself
648 # needs to be realiased to the proper names in the outer query. Thus we
649 # also return a hashref (order doesn't matter) of QUOTED EXTRA-SEL =>
650 # QUOTED ALIAS pairs, which is a list of extra selectors that do *not*
651 # exist in the original select list
652 sub _subqueried_limit_attrs {
653   my ($self, $proto_sql, $rs_attrs) = @_;
654
655   $self->throw_exception(
656     'Limit dialect implementation usable only in the context of DBIC (missing $rs_attrs)'
657   ) unless ref ($rs_attrs) eq 'HASH';
658
659   # mangle the input sql as we will be replacing the selector entirely
660   unless (
661     $rs_attrs->{_selector_sql}
662       and
663     $proto_sql =~ s/^ \s* SELECT \s* \Q$rs_attrs->{_selector_sql}//ix
664   ) {
665     $self->throw_exception("Unrecognizable SELECT: $proto_sql");
666   }
667
668   my ($re_sep, $re_alias) = map { quotemeta $_ } ( $self->{name_sep}, $rs_attrs->{alias} );
669
670   # insulate from the multiple _recurse_fields calls below
671   local $self->{select_bind};
672
673   # correlate select and as, build selection index
674   my (@sel, $in_sel_index);
675   for my $i (0 .. $#{$rs_attrs->{select}}) {
676
677     my $s = $rs_attrs->{select}[$i];
678     my $sql_sel = $self->_recurse_fields ($s);
679     my $sql_alias = (ref $s) eq 'HASH' ? $s->{-as} : undef;
680
681     push @sel, {
682       sql => $sql_sel,
683       unquoted_sql => do {
684         local $self->{quote_char};
685         $self->_recurse_fields ($s);
686       },
687       as =>
688         $sql_alias
689           ||
690         $rs_attrs->{as}[$i]
691           ||
692         $self->throw_exception("Select argument $i ($s) without corresponding 'as'")
693       ,
694     };
695
696     $in_sel_index->{$sql_sel}++;
697     $in_sel_index->{$self->_quote ($sql_alias)}++ if $sql_alias;
698
699     # record unqualified versions too, so we do not have
700     # to reselect the same column twice (in qualified and
701     # unqualified form)
702     if (! ref $s && $sql_sel =~ / $re_sep (.+) $/x) {
703       $in_sel_index->{$1}++;
704     }
705   }
706
707
708   # re-alias and remove any name separators from aliases,
709   # unless we are dealing with the current source alias
710   # (which will transcend the subqueries as it is necessary
711   # for possible further chaining)
712   my ($sel, $renamed);
713   for my $node (@sel) {
714     if (
715       $node->{as} =~ / (?<! ^ $re_alias ) \. /x
716         or
717       $node->{unquoted_sql} =~ / (?<! ^ $re_alias ) $re_sep /x
718     ) {
719       $node->{as} = $self->_unqualify_colname($node->{as});
720       my $quoted_as = $self->_quote($node->{as});
721       push @{$sel->{inner}}, sprintf '%s AS %s', $node->{sql}, $quoted_as;
722       push @{$sel->{outer}}, $quoted_as;
723       $renamed->{$node->{sql}} = $quoted_as;
724     }
725     else {
726       push @{$sel->{inner}}, $node->{sql};
727       push @{$sel->{outer}}, $self->_quote ($node->{as});
728     }
729   }
730
731   # see if the order gives us anything
732   my $extra_order_sel;
733   for my $chunk ($self->_order_by_chunks ($rs_attrs->{order_by})) {
734     # order with bind
735     $chunk = $chunk->[0] if (ref $chunk) eq 'ARRAY';
736     $chunk =~ s/\s+ (?: ASC|DESC ) \s* $//ix;
737
738     next if $in_sel_index->{$chunk};
739
740     $extra_order_sel->{$chunk} ||= $self->_quote (
741       'ORDER__BY__' . scalar keys %{$extra_order_sel||{}}
742     );
743   }
744
745   return {
746     query_leftover => $proto_sql,
747     (map {( "selection_$_" => join (', ', @{$sel->{$_}} ) )} keys %$sel ),
748     outer_renames => $renamed,
749     order_supplement => $extra_order_sel,
750   };
751 }
752
753 sub _unqualify_colname {
754   my ($self, $fqcn) = @_;
755   $fqcn =~ s/ \. /__/xg;
756   return $fqcn;
757 }
758
759 1;
760
761 =head1 AUTHORS
762
763 See L<DBIx::Class/CONTRIBUTORS>.
764
765 =head1 LICENSE
766
767 You may distribute this code under the same terms as Perl itself.
768
769 =cut