File Coverage

blib/lib/Class/ReluctantORM/Driver/PostgreSQL.pm
Criterion Covered Total %
statement 54 368 14.6
branch 0 104 0.0
condition 0 41 0.0
subroutine 18 49 36.7
pod 7 8 87.5
total 79 570 13.8


line stmt bran cond sub pod time code
1             package Class::ReluctantORM::Driver::PostgreSQL;
2              
3             =head1 NAME
4              
5             Class::ReluctantORM::Driver::PostgreSQL - PG driver for TB
6              
7             =head1 SYNOPSIS
8              
9             # See Class::ReluctantORM::Driver
10              
11              
12             =head1 DRIVER IDIOSYNCRACIES
13              
14             This driver supports PostgreSQL 8+. It may work with 7.4, but this has not been tested.
15              
16             This driver supports parsing. See Class::ReluctantORM::Driver::PostgreSQL::Parsing for details.
17              
18             =cut
19              
20 1     1   5 use strict;
  1         2  
  1         31  
21 1     1   4 use warnings;
  1         2  
  1         22  
22              
23 1     1   6 use DBI::Const::GetInfoType;
  1         2  
  1         125  
24             our $DEBUG = 0;
25              
26 1     1   6 use Data::Dumper;
  1         2  
  1         56  
27 1     1   911 use Data::Diff;
  1         9987  
  1         66  
28              
29 1     1   11 use Scalar::Util qw(looks_like_number);
  1         2  
  1         65  
30 1     1   8 use base 'Class::ReluctantORM::Driver';
  1         3  
  1         108  
31 1     1   7 use Class::ReluctantORM::Exception;
  1         2  
  1         25  
32              
33 1     1   796 use Class::ReluctantORM::SchemaCache;
  1         3  
  1         29  
34 1     1   7 use Class::ReluctantORM::SQL::Aliases;
  1         2  
  1         145  
35              
36 1     1   6 use constant USE_EXPLICIT_TABLE_NAME => 0;
  1         2  
  1         60  
37 1     1   5 use constant USE_TABLE_ALIAS => 1;
  1         2  
  1         40  
38 1     1   5 use constant CREATE_TABLE_ALIAS => 2;
  1         2  
  1         38  
39 1     1   5 use constant CREATE_COLUMN_ALIAS => 3;
  1         2  
  1         38  
40 1     1   5 use constant USE_BARE_COLUMN => 4;
  1         2  
  1         72  
41              
42             our %FUNCTION_RENDERERS;
43             our %COLUMN_CACHE; # Keyed by schema name, then table name, then column name, then hash of column info
44              
45 1     1   792 use Class::ReluctantORM::Driver::PostgreSQL::Functions;
  1         4  
  1         30  
46 1     1   770 use Class::ReluctantORM::Driver::PostgreSQL::Parsing;
  1         5  
  1         38  
47 1     1   724 use Class::ReluctantORM::FetchDeep::Results qw(fd_inflate);
  1         4  
  1         4380  
48              
49             sub init {
50 0     0 1   my $self = shift;
51 0           $self->{open_quote} = '"'; # $self->cro_dbh->get_info($GetInfoType{SQL_IDENTIFIER_QUOTE_CHAR}); # This is an expensive call for something that never changes
52 0           $self->{close_quote} = $self->{open_quote};
53             }
54              
55 0     0 1   sub supports_namespaces { return 1; }
56              
57             sub aptitude {
58 0     0 1   my ($class, $brand, $version) = @_;
59              
60 0           my $score = 0;
61 0 0         if ($brand eq 'PostgreSQL') { $score += .8; }
  0            
62              
63 0           my ($maj, $min, $rel) = map { $_ + 0 } split /\./, $version;
  0            
64              
65 0 0 0       if ($maj == 7 || $maj == 8) {
    0 0        
66 0           $score += .2;
67             } elsif ($maj == 6 || $maj == 9) {
68 0           $score += .1;
69             }
70              
71 0           return $score;
72             }
73              
74             sub find_primary_key_columns {
75 0     0 1   my ($driver, $schema_name, $table_name) = @_;
76              
77 0           my $schema_cache = Class::ReluctantORM::SchemaCache->instance();
78 0           my $pks = $schema_cache->read_primary_keys_for_table($schema_name, $table_name);
79 0 0         if ($pks) { return $pks; }
  0            
80 0           $pks = $driver->dbi_dbh->primary_keys(undef, $schema_name, $table_name);
81 0           $schema_cache->store_primary_keys_for_table($schema_name, $table_name, $pks);
82 0           return $pks;
83             }
84              
85             sub read_fields {
86 0     0 1   my $self = shift;
87 0           my $schema_name = shift;
88 0           my $table_name = shift;
89              
90              
91             # We use two-layer caching - first layer is the whole-db cache file
92 0           my $schema_cache = Class::ReluctantORM::SchemaCache->instance();
93 0           my $fieldmap = $schema_cache->read_columns_for_table($schema_name, $table_name);
94 0 0         if ($fieldmap) {
95 0           return $fieldmap;
96             }
97              
98             # OK, cache miss on the whole-db file. Now fetch all columns in the namespace (PG schema).
99             # (this lets us run one query per schema, not per table)
100 0           $self->__populate_column_cache_for_schema($schema_name);
101              
102 0 0         unless (exists $COLUMN_CACHE{$schema_name}{$table_name}) {
103 0           Class::ReluctantORM::Exception::Param::BadValue->croak
104             (
105             error => "Could not list columns for table '$schema_name.$table_name' - does it exist?",
106             param => ' table or schema name',
107             value => '$schema_name.$table_name',
108             );
109             }
110              
111 0           my @column_names;
112 0           foreach my $col_info (values %{$COLUMN_CACHE{$schema_name}{$table_name}}) {
  0            
113 0 0         if ($DEBUG > 1) { print STDERR __PACKAGE__ . ":" . __LINE__ . " - have column profile values:\n" . Dumper($col_info); }
  0            
114 0           $fieldmap->{lc($col_info->{COLUMN_NAME})} = $col_info->{COLUMN_NAME};
115             }
116              
117 0           $schema_cache->store_columns_for_table($schema_name, $table_name, $fieldmap);
118              
119 0           return $fieldmap;
120             }
121              
122             # Cache on a per-schema basis
123             sub __populate_column_cache_for_schema {
124 0     0     my $self = shift;
125 0           my $schema_name = shift;
126 0 0         unless (exists $COLUMN_CACHE{$schema_name}) {
127 0 0         if ($DEBUG) { print STDERR __PACKAGE__ . ":" . __LINE__ . " - column cache MISS for schema '$schema_name'"; }
  0            
128             # Cache is empty - run column_info for all tables in that schema
129 0           my $sth = $self->cro_dbh->column_info(undef, $schema_name, '%', '%');
130 0           while (my $col_info = $sth->fetchrow_hashref()) {
131 0           my $table = $col_info->{TABLE_NAME};
132 0   0       $COLUMN_CACHE{$schema_name}{$table} ||= {};
133 0           $COLUMN_CACHE{$schema_name}{$table}{lc($col_info->{COLUMN_NAME})} = $col_info;
134             }
135             }
136             }
137              
138 0     0 0   sub purge_column_cache { %COLUMN_CACHE = (); }
139              
140             # inherit sub run_sql
141              
142             sub render {
143 0     0 1   my $driver = shift;
144 0           my $sql = shift;
145 0           my $hints = shift;
146              
147 0 0         if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . " - Have run_sql operation: " . $sql->operation . "\n"; }
  0            
148              
149 0 0         unless ($hints->{already_transformed}) {
150 0           $driver->_monitor_render_begin(sql_obj => $sql);
151 0           $sql->annotate();
152 0           $sql->reconcile();
153 0           $driver->_monitor_render_transform(sql_obj => $sql);
154             }
155              
156 0           my %dispatcher = (
157             INSERT => \&__pg_render_insert,
158             SELECT => \&__pg_render_select,
159             DELETE => \&__pg_render_delete,
160             UPDATE => \&__pg_render_update,
161             );
162 0           my $str = $dispatcher{$sql->operation()}->($driver, $sql, $hints);
163              
164 0           $driver->_monitor_render_finish(sql_obj => $sql, sql_str => $str);
165 0           $sql->_sql_string($str);
166 0           $sql->_execution_driver($driver);
167              
168 0           return $str;
169             }
170              
171              
172              
173              
174             sub _pg_table_name {
175 0     0     my $driver = shift;
176 0           my $table = shift;
177 0           my $tn = $driver->_pg_quoted($table->table);
178 0 0         if ($table->schema) {
179 0           $tn = $driver->_pg_quoted($table->schema) . '.' . $tn;
180             }
181 0           return $tn;
182             }
183              
184             sub _pg_quoted {
185 0     0     my $driver = shift;
186 0           my $text = shift;
187 0           return '"' . $text . '"';
188             }
189              
190             sub execute_fetch_deep {
191 0     0 1   my $driver = shift;
192 0           my $sql = shift;
193 0           my $with = shift;
194              
195 0   0       $with->{__upper_table} ||= $sql->base_table();
196              
197             # Transform SQL
198 0           $driver->__pg_fd_transform_sql($sql);
199              
200             # Return results
201 0           return fd_inflate($sql, $with, {already_transformed => 1});
202             }
203              
204             #=============================================================================#
205             # SQL Rendering (Postgres Dialect)
206             #=============================================================================#
207              
208             sub __pg_render_insert {
209 0     0     my $driver = shift;
210 0           my $sql = shift;
211 0           my $str = '';
212              
213 0           $str .= 'INSERT INTO ';
214 0           $str .= $driver->_pg_table_name($sql->table);
215 0           $str .= ' (';
216 0           $str .= join ',', map { $driver->_pg_quoted($_->{column}->column) } $sql->inputs();
  0            
217 0           $str .= ') ';
218 0 0         if ($sql->input_subquery()) {
219 0           $str .= $driver->__pg_render_select($sql->input_subquery->statement);
220             } else {
221 0           $str .= ' VALUES (';
222 0           $str .= join ',', map { '?' } $sql->inputs();
  0            
223 0           $str .= ')';
224             }
225              
226             # Build RETURNING clause if needed
227 0 0         if ($sql->output_columns()) {
228 0           $sql->set_default_output_aliases();
229 0           $str .= ' RETURNING ';
230 0           $str .= join ',', map { $driver->__pg_render_output_column($_, USE_EXPLICIT_TABLE_NAME) } $sql->output_columns();
  0            
231             }
232              
233 0           return $str;
234             }
235              
236             sub __pg_render_update {
237 0     0     my $driver = shift;
238 0           my $sql = shift;
239 0           my $str = '';
240              
241 0           $str .= 'UPDATE ';
242 0           $str .= $driver->__pg_render_table_name(
243             $sql->table,
244             #( ($driver->server_version >= 8.2) ? CREATE_TABLE_ALIAS : USE_EXPLICIT_TABLE_NAME)
245             );
246 0           $str .= ' SET ';
247              
248 0           $str .= join ',', map {
249 0           $driver->__pg_render_column_name($_->{column},USE_BARE_COLUMN,0,0)
250             . ' = '
251             . '?'
252             } $sql->inputs();
253              
254 0           $str .= ' WHERE ';
255 0           $str .= $driver->__pg_render_where_clause($sql, USE_EXPLICIT_TABLE_NAME);
256              
257             # Build RETURNING clause if needed
258 0 0         if ($sql->output_columns()) {
259 0           $sql->set_default_output_aliases();
260 0           $str .= ' RETURNING ';
261 0           $str .= join ',', map { $driver->__pg_render_output_column($_, USE_EXPLICIT_TABLE_NAME) } $sql->output_columns();
  0            
262             }
263              
264 0           return $str;
265             }
266              
267             sub __pg_render_delete {
268 0     0     my $driver = shift;
269 0           my $sql = shift;
270 0           my $str = '';
271              
272 0           $str .= 'DELETE FROM ';
273 0           $str .= $driver->__pg_render_table_name($sql->table);
274              
275 0           $str .= ' WHERE ';
276 0           $str .= $driver->__pg_render_where_clause($sql, USE_EXPLICIT_TABLE_NAME);
277              
278 0           return $str;
279             }
280              
281              
282             sub __pg_fd_transform_sql {
283 0     0     my $driver = shift;
284 0           my $sql = shift;
285              
286 0           $driver->_monitor_render_begin(sql_obj => $sql);
287 0           $sql->make_inflatable(auto_reconcile => 1, auto_annotate => 1);
288              
289 0 0         if ($sql->limit) {
290 0           $driver->__pg_transform_sql_fold_limit_for_deep($sql);
291             }
292              
293 0           $driver->_monitor_render_transform(sql_obj => $sql);
294             }
295              
296             sub __pg_render_select {
297 0     0     my $driver = shift;
298 0           my $sql = shift;
299 0           my $str = "SELECT \n";
300 0           $str .= $driver->__pg_render_output_column_list($sql->output_columns);
301 0           $str .= "\n FROM \n";
302 0           $str .= $driver->__pg_render_from_clause($sql->from);
303 0           $str .= "\n WHERE \n";
304 0           $str .= $driver->__pg_render_where_clause($sql, USE_TABLE_ALIAS);
305 0 0         if ($sql->order_by->columns) {
306 0           $str .= "\n ORDER BY \n";
307 0           $str .= $driver->__pg_render_order_by_clause($sql->order_by);
308             }
309 0 0         if (defined ($sql->limit())) {
310 0           $str .= " LIMIT " . $sql->limit() ."\n";
311 0 0         if (defined ($sql->offset())) {
312 0           $str .= " OFFSET " . $sql->offset() ."\n";
313             }
314             }
315 0           return $str;
316             }
317              
318             sub __pg_render_output_column {
319 0     0     my $driver = shift;
320 0           my $oc = shift;
321 0   0       my $use_table_aliases = shift || USE_EXPLICIT_TABLE_NAME;
322 0           my $str = $driver->__pg_render_expression($oc->expression, $use_table_aliases);
323 0 0         if ($oc->alias) {
324 0           $str .= ' AS ' . $oc->alias;
325             }
326 0           return $str;
327             }
328              
329              
330             sub __pg_render_column_name {
331 0     0     my $driver = shift;
332 0           my $col = shift;
333 0   0       my $use_table_alias = shift || USE_EXPLICIT_TABLE_NAME;
334 0   0       my $use_column_alias = shift || 0;
335 0   0       my $make_column_alias = shift || 0;
336              
337 0           my $table = $col->table;
338              
339 0           my $name = '';
340              
341 0 0 0       if ($use_column_alias && $col->alias) {
    0          
    0          
342 0           $name = $col->alias;
343             } elsif ($table) {
344 0 0         if ($use_table_alias == USE_TABLE_ALIAS) {
    0          
    0          
345 0           $name .= $table->alias . '.';
346             } elsif ($use_table_alias == USE_EXPLICIT_TABLE_NAME) {
347 0           $name = $driver->__pg_render_table_name($table) . '.';
348             } elsif ($use_table_alias == USE_BARE_COLUMN) {
349             # Do nothing
350             }
351 0           $name .= $driver->_pg_quoted($col->column);
352 0 0 0       if ($make_column_alias && $col->alias) {
353 0           $name .= ' AS ' . $col->alias;
354             }
355             } elsif ($use_table_alias == USE_BARE_COLUMN) {
356 0           $name .= $driver->_pg_quoted($col->column);
357 0 0 0       if ($make_column_alias && $col->alias) {
358 0           $name .= ' AS ' . $col->alias;
359             }
360             }
361              
362 0           return $name;
363             }
364              
365             sub __pg_render_table_name {
366 0     0     my $driver = shift;
367 0           my $table = shift;
368 0   0       my $alias_mode = shift || USE_EXPLICIT_TABLE_NAME;
369 0           my $name = '';
370              
371 0 0 0       if (($alias_mode == USE_TABLE_ALIAS) && $table->alias) {
372 0           return $table->alias();
373             }
374              
375 0 0         if ($table->schema) {
376 0           $name .= $driver->_pg_quoted($table->schema) . '.';
377             }
378 0           $name .= $driver->_pg_quoted($table->table);
379 0           return $name;
380             }
381              
382             sub __pg_render_output_column_list {
383 0     0     my $driver = shift;
384 0           my @cols = @_;
385 0           my $str = join ', ',
386             map {
387 0           $driver->__pg_render_output_column($_, USE_TABLE_ALIAS);
388             } @cols;
389 0           return $str;
390             }
391              
392             sub __pg_render_order_by_clause {
393 0     0     my $driver = shift;
394 0           my $ob = shift;
395 0           my $str = join ', ',
396             map {
397 0           $driver->__pg_render_column_name($_->[0], 1, 0, 1)
398             . ' '
399             . $_->[1]
400             } $ob->columns_with_directions;
401 0           return $str;
402             }
403              
404             sub __pg_render_from_clause {
405 0     0     my $driver = shift;
406 0           my $from = shift;
407 0           my $rel = $from->root_relation();
408 0           return $driver->__pg_render_relation($rel);
409             }
410              
411             sub __pg_render_where_clause {
412 0     0     my $driver = shift;
413 0           my $sql = shift;
414 0   0       my $alias_mode = shift || USE_EXPLICIT_TABLE_NAME;
415 0 0         if ($sql->raw_where) {
416 0 0         unless ($sql->_cooked_where()) {
417 0           Class::ReluctantORM::Exception::Call::ExpectationFailure->croak
418             ('SQL has raw_where but no _cooked_where - did reconcile fail?');
419             }
420 0           return $sql->_cooked_where(); # Anything else needed? TODO - apply alias_mode?
421             } else {
422 0           my $where = $sql->where();
423 0 0         unless ($where) { return '1=1'; }
  0            
424 0           my $crit = $where->root_criterion();
425 0           return $driver->__pg_render_criterion($crit, $alias_mode);
426             }
427             }
428              
429             sub __pg_render_relation {
430 0     0     my $driver = shift;
431 0           my $rel = shift;
432 0   0       my $alias_mode = shift || USE_EXPLICIT_TABLE_NAME;
433 0           my $sql = '';
434              
435 0 0         if ($rel->is_leaf_relation) {
436 0 0         if ($rel->is_table) {
437 0           $sql = $driver->__pg_render_table_name($rel, $alias_mode);
438             } else {
439             # Don't know how to handle this
440 0           Class::ReluctantORM::Exception::Call::NotImplemented->croak(__PACKAGE__ . ' does not know how to render a non-table leaf relation');
441             }
442             } else {
443 0 0         if ($rel->is_join) {
444 0           $sql = '(' . $driver->__pg_render_relation($rel->left_relation, $alias_mode);
445 0           $sql .= ' ' . $driver->__pg_render_join_type($rel->type) . ' ';
446 0           $sql .= $driver->__pg_render_relation($rel->right_relation, $alias_mode);
447              
448             # Always use table alias in ON criteria - PG requires it
449 0           $sql .= ' ON ' . $driver->__pg_render_criterion($rel->criterion, USE_TABLE_ALIAS) . ')';
450             } else {
451 0           Class::ReluctantORM::Exception::Call::NotImplemented->croak(__PACKAGE__ . ' does not know how to render a non-join non-leaf relation');
452             }
453             }
454              
455 0 0         if ($rel->alias) {
456 0           $sql .= ' ' . $rel->alias;
457             }
458              
459 0           return $sql;
460             }
461              
462              
463             sub __pg_render_join_type {
464 0     0     my $driver = shift;
465 0           my $raw_type = shift;
466 0           return $raw_type . ' JOIN';
467             }
468              
469             sub __pg_render_expression {
470 0     0     my $driver = shift;
471 0           my $exp = shift;
472 0   0       my $use_table_aliases = shift || USE_EXPLICIT_TABLE_NAME;
473              
474 0 0         if ($exp->is_param) {
    0          
    0          
    0          
    0          
475 0           return '?';
476             } elsif ($exp->is_column) {
477 0           return $driver->__pg_render_column_name($exp, $use_table_aliases, 0, 0);
478             } elsif ($exp->is_literal) {
479 0           return $driver->__pg_render_literal($exp);
480             # Criterion case now handled by Function Call
481             #} elsif ($exp->is_criterion) {
482             # return $driver->__pg_render_criterion($exp, $use_table_aliases);
483             } elsif ($exp->is_function_call) {
484 0           return $driver->__pg_render_function_call($exp, $use_table_aliases);
485             } elsif ($exp->is_subquery()) {
486 0           return $driver->__pg_render_subquery_as_expresion($exp);
487             } else {
488             # Don't know how to handle this
489 0           my $type = ref($exp);
490 0           Class::ReluctantORM::Exception::NotImplemented->croak(__PACKAGE__ . " does not know how to render a $type");
491             }
492             }
493              
494             sub __pg_render_literal {
495 0     0     my $driver = shift;
496 0           my $literal = shift;
497              
498 0           my $val = $literal->value();
499 0           my $dt = $literal->data_type();
500              
501 0 0         if (0) {
    0          
    0          
502 0           } elsif ($dt eq 'NULL') {
503 0           return 'NULL'; # Not quoted
504             } elsif ($dt eq 'BOOLEAN') {
505 0 0         return $literal->is_equivalent(Literal->TRUE) ? 'TRUE' : 'FALSE'; # Not quoted
506             } elsif (looks_like_number($val)) {
507 0           return $val;
508             } else {
509 0           return "'$val'";
510             }
511             }
512              
513             sub __pg_render_subquery_as_expresion {
514 0     0     my $driver = shift;
515 0           my $subquery = shift;
516 0           return '(' . $driver->__pg_render_select($subquery->statement()) . ')';
517             }
518              
519              
520             # This is currently an alias for function_call
521 0     0     sub __pg_render_criterion { return __pg_render_function_call(@_); }
522              
523             sub __pg_render_function_call {
524 0     0     my $driver = shift;
525 0           my $criterion = shift;
526 0   0       my $use_table_aliases = shift || USE_EXPLICIT_TABLE_NAME;
527              
528             # Avoid $_
529 0           my @args;
530 0           foreach my $arg ($criterion->arguments) {
531 0           push @args, $driver->__pg_render_expression($arg, $use_table_aliases);
532             }
533              
534 0           my $sql;
535 0           my $func = $criterion->function();
536 0 0         if (exists $FUNCTION_RENDERERS{$func->name()}) {
537 0           $sql = $FUNCTION_RENDERERS{$func->name()}->(@args);
538             } else {
539 0           Class::ReluctantORM::Exception::NotImplemented->croak(__PACKAGE__ . " does not know how to render a function call for function " . $func->name());
540             }
541 0           return $sql;
542             }
543              
544              
545             #=============================================================================#
546             # Fetch Deep SQL Transformation
547             #=============================================================================#
548              
549             =begin devnotes
550              
551             =head2 Transformations on Limits, Offsets, and Ordering In Joins
552              
553             Given:
554             Ship->fetch_deep(
555             where =>
556             with => { pirates => {}},
557             limit => ,
558             order_by => ,
559             offset => ,
560             );
561              
562             Initial SQL looks like:
563             SELECT
564             FROM
565             TABLE (Ship)
566             LEFT OUTER JOIN TABLE (Pirate)
567             WHERE
568             ORDER BY
569             LIMIT
570             OFFSET
571              
572             This is wrong - the limit will apply to the ship-pirate join,
573             when it should apply only to ships.
574              
575             Transform to:
576             SELECT
577             FROM
578             TABLE (Ship)
579             LEFT OUTER JOIN TABLE (Pirate)
580             WHERE CompositePK(Ship) IN (
581             SUBSELECT CompositePK(Ship)
582             FROM Ship
583             WHERE
584             ORDER BY
585             LIMIT
586             OFFSET
587             )
588             ORDER BY
589              
590             with the additional constraints that:
591             - ORDER1 may only refer to Ship
592             - WHERE1 may only refer to Ship
593             - WHERE1 and ORDER1 must be re-aliasable
594              
595              
596             =cut
597              
598             sub __pg_transform_sql_fold_limit_for_deep {
599 0     0     my $driver = shift;
600 0           my $sql = shift;
601              
602             # Determine the base table
603 0           my $base_table = $sql->base_table();
604 0 0         unless ($base_table->is_table()) {
605 0           Class::ReluctantORM::Exception::NotImplemented->croak(__PACKAGE__ . " doesn't know what to do with a non-table base relation");
606             }
607              
608             # Check that order clause only refers to base table
609 0           foreach my $table ($sql->order_by->tables()) {
610 0 0         unless ($base_table->is_the_same_table($table)) {
611 0           Class::ReluctantORM::Exception::SQL::TooComplex->croak(__PACKAGE__ . " can't handle a ORDER BY clause on a fetch_deep that refers to anything other than the base table.");
612             }
613             }
614              
615             # Check that where clause only refers to base table
616 0 0         if (!$sql->raw_where) {
617 0           foreach my $table ($sql->where->tables()) {
618 0 0         unless ($base_table->is_the_same_table($table)) {
619 0           Class::ReluctantORM::Exception::SQL::TooComplex->croak(__PACKAGE__ . " can't handle a WHERE clause on a fetch_deep-with-limit that refers to anything other than the base table.");
620             }
621             }
622             }
623              
624             # Create new SELECT statement, with re-aliased base references
625 0           my $select = Class::ReluctantORM::SQL->new('select');
626 0           my $alias = $sql->new_table_alias();
627 0           my $table_copy = Table->new($base_table->class());
628 0           $table_copy->alias($alias);
629 0           $select->from(From->new($table_copy));
630              
631             # Extract and move where clause
632 0 0         if ($sql->_cooked_where) {
633             # Move outer where into inner
634              
635             # TODO - may wish we re-aliased things!
636 0           $select->raw_where($sql->raw_where);
637 0           $select->_cooked_where($sql->_cooked_where);
638 0           $select->_raw_where_execargs($sql->_raw_where_execargs);
639 0           $select->_raw_where_params([ $sql->_raw_where_params ]);
640              
641             # Clear outer where
642 0           $sql->raw_where(undef);
643 0           $sql->_cooked_where(undef);
644 0           $sql->_raw_where_execargs([]);
645 0           $sql->_raw_where_params([]);
646              
647             } else {
648 0           my $inner_where = $sql->where();
649 0           $sql->where(undef); # Clear outer where
650 0           foreach my $col ($inner_where->columns) {
651             # Force columns referenced in the where clause to refer to new, re-aliased copy of table
652 0           $col->table($table_copy);
653             }
654 0           $select->where($inner_where);
655             }
656              
657             # Copy order by clause, re-alias, and attach to select statement
658 0           my $inner_ob = OrderBy->new();
659 0           foreach my $crit ($sql->order_by->columns_with_directions) {
660 0           my ($outer_col, $direction) = @$crit;
661 0           my $inner_col = Column->new(column => $outer_col->column, table => $table_copy);
662 0           $inner_ob->add($inner_col, $direction);
663             }
664 0           $select->order_by($inner_ob);
665              
666             # Move limit and offset clauses to inner select
667 0           $select->limit($sql->limit());
668 0           $sql->limit(undef);
669 0           $select->offset($sql->offset());
670 0           $sql->offset(undef);
671              
672             # Alter SELECT statement to return composite PK
673 0           my $oc = OutputColumn->new(
674             expression =>
675             FunctionCall->new('key_compositor_inside_subquery', $table_copy->primary_key_columns()),
676             alias => '',
677             );
678 0           $select->add_output($oc);
679              
680              
681             # Replace top-level WHERE with single criteria, seeking a composite key in the subselect
682 0           my $new_top_where = Where->new(
683             Criterion->new(
684             'in',
685             FunctionCall->new('key_compositor_outside_subquery', $base_table->primary_key_columns()),
686             SubQuery->new($select),
687             )
688             );
689 0           $sql->where($new_top_where);
690              
691              
692             }
693              
694             1;