File Coverage

blib/lib/Class/ReluctantORM/Driver/SQLite.pm
Criterion Covered Total %
statement 48 400 12.0
branch 0 110 0.0
condition 0 44 0.0
subroutine 16 49 32.6
pod 8 8 100.0
total 72 611 11.7


line stmt bran cond sub pod time code
1             package Class::ReluctantORM::Driver::SQLite;
2              
3             =head1 NAME
4              
5             Class::ReluctantORM::Driver::SQLite - SQLite driver for CRO
6              
7             =head1 SYNOPSIS
8              
9             # See Class::ReluctantORM::Driver
10              
11             =head1 DRIVER IDIOSYNCRACIES
12              
13             Supports any version of sqlite supported by DBD::SQLite.
14              
15             SQLite is itself fairly idiosyncratic. Here are how they impact this driver:
16              
17             =over
18              
19             =item No RETURNING support on INSERT/UPDATE/DELETE
20              
21             Since SQLite doesn't support this SQL extension, and DBD::SQLite doesn't support using output binds to read them (a hack the Oracle DBD provides), you can't use RETURNING to get columns from INSERT/UPDATE/DELETE statements you provide in SQL, nor can you add output columns to SQL objects performing those operations. Worse, if your model uses refresh_on_update columns, they must be fetched by a separate SELECT (handled internally by the Driver, but you need to be aware of the penalty).
22              
23             =item Column names forced to be lowercase
24              
25              
26             =back
27              
28              
29             =cut
30              
31              
32             # How to obtain a primary key value with SQLite
33             # 1. If the table has a single-column integer PK, SQLite will effectively auto-increment,
34             # and the value returned by DBI->las_insert_id will be the new value for that column.
35             # 2. If the table has amultiple-column PK, or non-integer, SQLite will return a bigint
36             # rowid as the last_insert_id, which can then be used to SELECT the new primary key values.
37              
38              
39 1     1   6 use strict;
  1         4  
  1         35  
40 1     1   6 use warnings;
  1         2  
  1         28  
41              
42 1     1   6 use DBI::Const::GetInfoType;
  1         3  
  1         150  
43             our $DEBUG = 0;
44              
45 1     1   6 use Data::Dumper;
  1         2  
  1         55  
46 1     1   5 use Data::Diff;
  1         1  
  1         39  
47              
48 1     1   7 use Scalar::Util qw(looks_like_number);
  1         3  
  1         52  
49 1     1   6 use base 'Class::ReluctantORM::Driver';
  1         8  
  1         80  
50 1     1   6 use Class::ReluctantORM::Exception;
  1         3  
  1         29  
51              
52 1     1   6 use Class::ReluctantORM::SQL::Aliases;
  1         2  
  1         141  
53              
54 1     1   5 use constant USE_EXPLICIT_TABLE_NAME => 0;
  1         2  
  1         75  
55 1     1   6 use constant USE_TABLE_ALIAS => 1;
  1         2  
  1         42  
56 1     1   5 use constant CREATE_TABLE_ALIAS => 2;
  1         3  
  1         51  
57 1     1   6 use constant CREATE_COLUMN_ALIAS => 3;
  1         1  
  1         46  
58 1     1   5 use constant USE_BARE_COLUMN => 4;
  1         1  
  1         59  
59              
60             our %FUNCTION_RENDERERS;
61              
62 1     1   653 use Class::ReluctantORM::Driver::SQLite::Functions;
  1         4  
  1         27  
63 1     1   6 use Class::ReluctantORM::FetchDeep::Results qw(fd_inflate);
  1         3  
  1         4893  
64              
65             sub init {
66 0     0 1   my $self = shift;
67 0           $self->{open_quote} = $self->dbh->get_info($GetInfoType{SQL_IDENTIFIER_QUOTE_CHAR});
68 0           $self->{close_quote} = $self->dbh->get_info($GetInfoType{SQL_IDENTIFIER_QUOTE_CHAR});
69             }
70              
71 0     0 1   sub supports_namespaces { return 1; } # Awkwardly, yes
72              
73             sub aptitude {
74 0     0 1   my ($class, $brand, $version) = @_;
75              
76 0           my $score = 0;
77 0 0         if ($brand eq 'SQLite') { $score += .8; }
  0            
78              
79             # $version is like 3.7.2
80 0           my ($maj, $min, $rel) = map { $_ + 0 } split /\./, $version;
  0            
81              
82 0 0         if ($maj == 3) {
    0          
83 0           $score += .2;
84             } elsif ($maj == 2) {
85 0           $score += .1;
86             }
87              
88 0           return $score;
89             }
90              
91             sub read_fields {
92 0     0 1   my $self = shift;
93 0           my $schema = shift;
94 0           my $table = shift;
95              
96 0           my $sth = $self->dbh->column_info(undef, $schema, $table, '%');
97              
98 0           my @column_names;
99 0           while (my $col_info = $sth->fetchrow_hashref()) {
100 0 0         if ($DEBUG > 1) { print STDERR __PACKAGE__ . ":" . __LINE__ . " - have column profile values:\n" . Dumper($col_info); }
  0            
101 0           push @column_names, $col_info->{COLUMN_NAME};
102             }
103              
104 0           my %fieldmap = map { (lc($_) => lc($_)) } @column_names;
  0            
105              
106 0           return \%fieldmap;
107             }
108              
109             sub find_primary_key_columns {
110 0     0 1   my ($driver, $schema, $table) = @_;
111 0           return $driver->dbi_dbh->primary_keys(undef, $schema, $table);
112             }
113              
114              
115             sub run_sql {
116 0     0 1   my $driver = shift;
117 0           my $sql = shift;
118 0           my $hints = shift;
119              
120             # Figure out if we need to split
121 0 0         if ($driver->_sl_must_split_sql($sql, $hints)) {
122 0           return $driver->_sl_split_and_run_sql($sql, $hints);
123             } else {
124 0           return $driver->_sl_run_single_sql($sql, $hints);
125             }
126             }
127              
128             sub _sl_must_split_sql {
129 0     0     my $driver = shift;
130 0           my $sql = shift;
131 0           my $hints = shift;
132              
133 0 0         if ($sql->operation eq 'SELECT') {
    0          
134 0           return 0; # Can always run a SELECT directly
135              
136             } elsif (0 == $sql->output_columns ) {
137 0           return 0; # Can always run one if there are no outputs (may happen with secondary Audit inserts)
138             } else {
139             # Can run directly if there are no non-pk output columns, AND the base table has a single-column primary key
140 0           my $class = $sql->base_table->class();
141 0           my @pk_columns = $class->primary_key_columns;
142              
143 0 0         if ($sql->operation eq 'INSERT') {
144             # In this case, we have to go get the SQLite rowID,
145             # then use it to read out the primary key columns with a SELECT.
146 0 0         if (@pk_columns > 1) { return 1; }
  0            
147              
148             # If we have a single-column PK, we'll read it using DBI's last insert id,
149             # so no need for a SELECT (unless we have non-pk columns in the output)
150             }
151              
152             # OK, otherwise, we can do it in one query, so long as all output columns in the PK
153             # (all PK columns get put on the refresh-on-update list, so if something isn't
154             # on that list, it really is a refresh. PKs themselves are supposed to be immutable in CRO.)
155 0           foreach my $oc ($sql->output_columns()) {
156 0 0 0       unless ($oc->expression->is_column && $oc->is_primary_key) {
157             # Funky refresh column - must split
158 0           return 1;
159             }
160             }
161              
162             # All clear
163 0           return 0;
164             }
165             }
166              
167              
168             sub _sl_split_and_run_sql {
169 0     0     my $driver = shift;
170 0           my $original_statement = shift;
171 0           my $hints = shift;
172              
173             #my $primary_statement = $original_sql->clone();
174              
175             # Start building up the secondary statement
176 0           my $secondary_statement = SQL->new('SELECT');
177 0           my $base_table = $original_statement->base_table()->clone();
178 0           $secondary_statement->from(From->new($base_table));
179              
180             # Copy output columns
181 0           foreach my $oc ($original_statement->output_columns) {
182 0           $secondary_statement->add_output($oc->clone());
183             }
184              
185             # Figure out where clause
186 0           my $where;
187 0 0         if ($original_statement->operation eq 'INSERT') {
188             # We'd only get here if the table has multi-column PKs OR has refresh-on-updates
189 0           $where = Where->new();
190 0           $where->and(
191             Criterion->new(
192             '=',
193             Column->new(table => $base_table, column => 'oid'),
194             Param->new(),
195             ),
196             );
197              
198             } else {
199 0           $where = $original_statement->where->clone();
200             }
201 0           $secondary_statement->where($where);
202              
203             # Build primary statement
204 0           my $primary_statement = $original_statement->clone();
205 0           $primary_statement->remove_all_outputs();
206              
207             # Run primary
208 0           $driver->_sl_run_single_sql($primary_statement, $hints);
209              
210             # Collect params for secondary
211 0           my @binds;
212 0 0         if ($original_statement->operation eq 'INSERT') {
213             # use SQLite OID fetcher
214 0           @binds = (
215             $driver->dbi_dbh->last_insert_id('', $base_table->schema, $base_table->table, ''),
216             );
217             } else {
218 0 0         @binds = $original_statement->where ? map { $_->bind_value } $original_statement->where->params : ();
  0            
219             }
220 0           $secondary_statement->set_bind_values(@binds);
221              
222             # Run secondary
223 0           $driver->_sl_run_single_sql($secondary_statement, $hints);
224              
225             # Copy output values from secondary to original
226             # Note: since the original never got rendered, it will have
227             # fewer columns than the secondary. We can't just replace them,
228             # because the caller may have saved references to the OCs.
229              
230 0 0         my %secondary_outputs =
231 0           map { ($_->expression->is_column ? $_->expression->column : $_->alias) => $_ }
232             $secondary_statement->output_columns();
233              
234             # Copy over any values for columns that existed
235 0           foreach my $original_output ($original_statement->output_columns()) {
236 0 0         my $moniker = $original_output->expression->is_column ?
237             $original_output->expression->column :
238             $original_output->alias();
239 0           $original_output->output_value($secondary_outputs{$moniker}->output_value);
240 0           delete $secondary_outputs{$moniker};
241             }
242              
243             # Clone any columns that were in secondary but not original
244 0           foreach my $new_secondary (values %secondary_outputs) {
245 0           $original_statement->add_output($new_secondary->clone);
246             }
247              
248 0           return 1;
249             }
250              
251              
252             sub _sl_run_single_sql {
253 0     0     my $driver = shift;
254 0           my $sql = shift;
255 0           my $hints = shift;
256              
257 0           $driver->prepare($sql, $hints);
258 0           my $sth = $sql->_sth();
259 0           my $str = $sql->_sql_string();
260              
261             # OK, run the query
262 0           my @binds = $sql->get_bind_values();
263 0           $driver->_monitor_execute_begin(sql_obj => $sql, sql_str => $str, binds => \@binds, sth => $sth);
264 0           $driver->_pre_execute_hook($sql);
265 0           $sth->execute(@binds);
266 0           $driver->_post_execute_hook($sql);
267 0           $driver->_monitor_execute_finish(sql_obj => $sql, sql_str => $str, binds => \@binds, sth => $sth);
268              
269             # Fetch the result, if any
270 0 0         if ($sql->output_columns) {
271 0           while (my $row = $sth->fetchrow_hashref()) {
272 0           $driver->_monitor_fetch_row(sql_obj => $sql, sql_str => $str, binds => \@binds, sth => $sth, row => $row);
273 0           $sql->set_single_row_results($row);
274             }
275             }
276 0           $sth->finish();
277 0           $driver->_monitor_finish(sql_obj => $sql, sql_str => $str, sth => $sth);
278              
279 0           return 1;
280             }
281              
282              
283             sub render {
284 0     0 1   my $driver = shift;
285 0           my $sql = shift;
286 0           my $hints = shift;
287              
288 0 0         if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . " - Have run_sql operation: " . $sql->operation . "\n"; }
  0            
289              
290 0 0         unless ($hints->{already_transformed}) {
291 0           $driver->_monitor_render_begin(sql_obj => $sql);
292 0           $sql->annotate();
293 0           my $should_add_output_columns = $sql->operation eq 'SELECT';
294 0           $sql->reconcile(add_output_columns => $should_add_output_columns);
295 0           $driver->_monitor_render_transform(sql_obj => $sql);
296             }
297              
298 0           my %dispatcher = (
299             INSERT => \&__sl_render_insert,
300             SELECT => \&__sl_render_select,
301             DELETE => \&__sl_render_delete,
302             UPDATE => \&__sl_render_update,
303             );
304 0           my $str = $dispatcher{$sql->operation()}->($driver, $sql, $hints);
305              
306 0           $driver->_monitor_render_finish(sql_obj => $sql, sql_str => $str);
307 0           $sql->_sql_string($str);
308 0           $sql->_execution_driver($driver);
309              
310 0           return $str;
311             }
312              
313             sub _post_execute_hook {
314 0     0     my $driver = shift;
315 0           my $sql = shift;
316 0   0       my $hints = $sql->execute_hints() || {};
317              
318 0 0 0       if ($sql->operation eq 'INSERT' && $hints->{set_pk_from_last_insert_id}) {
319 0           my $table = $sql->base_table();
320 0           my ($oc) = $sql->output_columns(); # Should only be 1
321 0           my $last_insert_id = $driver->dbi_dbh()->last_insert_id
322             (
323             undef, # sqlite doesn't use catalogs
324             $table->schema,
325             $table->table,
326             $oc->expression->column,
327             );
328              
329 0           $oc->output_value($last_insert_id);
330             }
331             }
332              
333              
334             sub _sl_table_name {
335 0     0     my $driver = shift;
336 0           my $table = shift;
337 0           my $tn = $driver->_sl_quoted($table->table);
338 0 0         if ($table->schema) {
339 0           $tn = $driver->_sl_quoted($table->schema) . '.' . $tn;
340             }
341 0           return $tn;
342             }
343              
344             sub _sl_quoted {
345 0     0     my $driver = shift;
346 0           my $text = shift;
347 0           return '"' . $text . '"';
348             }
349              
350             sub execute_fetch_deep {
351 0     0 1   my $driver = shift;
352 0           my $sql = shift;
353 0           my $with = shift;
354              
355 0   0       $with->{__upper_table} ||= $sql->base_table();
356              
357             # Transform SQL
358 0           $driver->__sl_fd_transform_sql($sql);
359              
360             # Return results
361 0           return fd_inflate($sql, $with, {already_transformed => 1});
362             }
363              
364             #=============================================================================#
365             # SQL Rendering (SQLite Dialect)
366             #=============================================================================#
367              
368             sub __sl_render_update {
369 0     0     my $driver = shift;
370 0           my $sql = shift;
371 0           my $str = '';
372              
373 0 0         if ($sql->output_columns()) {
374 0           Class::ReluctantORM::Exception::Param::BadValue->croak
375             (
376             error => "The SQLite driver does not permit UPDATE statements to have output columns (no RETURNING support)",
377             param => 'sql_obj',
378             value => $sql,
379             );
380             }
381              
382              
383 0           $str .= 'UPDATE ';
384 0           $str .= $driver->__sl_render_table_name($sql->table);
385 0           $str .= ' SET ';
386              
387 0           $str .= join ',', map {
388 0           $driver->__sl_render_column_name($_->{column},USE_BARE_COLUMN,0,0)
389             . ' = '
390             . '?'
391             } $sql->inputs();
392              
393 0           $str .= ' WHERE ';
394 0           $str .= $driver->__sl_render_where_clause($sql->where, USE_EXPLICIT_TABLE_NAME);
395              
396 0           return $str;
397             }
398              
399              
400             sub __sl_render_insert {
401 0     0     my $driver = shift;
402 0           my $sql = shift;
403 0           my $str = '';
404              
405 0           my $set_pk_from_last_insert_id = 0;
406 0           my @outputs = $sql->output_columns();
407              
408 0 0 0       if (@outputs == 1 && $outputs[0]->is_primary_key()) {
    0          
409 0           $set_pk_from_last_insert_id = 1;
410             } elsif (@outputs == 0) {
411 0           $set_pk_from_last_insert_id = 0;
412             } else {
413 0           Class::ReluctantORM::Exception::Param::BadValue->croak
414             (
415             error => "The SQLite driver does not permit INSERT statements to have output columns (no RETURNING support)",
416             param => 'sql_obj',
417             value => $sql,
418             );
419             }
420              
421 0           $str .= 'INSERT INTO ';
422 0           $str .= $driver->_sl_table_name($sql->table);
423 0           $str .= ' (';
424 0           $str .= join ',', map { $driver->_sl_quoted($_->{column}->column) } $sql->inputs();
  0            
425 0           $str .= ') ';
426 0 0         if ($sql->input_subquery()) {
427 0           $str .= $driver->__sl_render_select($sql->input_subquery->statement);
428             } else {
429 0           $str .= ' VALUES (';
430 0           $str .= join ',', map { '?' } $sql->inputs();
  0            
431 0           $str .= ')';
432             }
433              
434 0           $sql->execute_hints({ set_pk_from_last_insert_id => $set_pk_from_last_insert_id });
435              
436 0           return $str;
437             }
438              
439             sub __sl_render_delete {
440 0     0     my $driver = shift;
441 0           my $sql = shift;
442 0           my $str = '';
443              
444 0 0         if ($sql->output_columns()) {
445 0           Class::ReluctantORM::Exception::Param::BadValue->croak
446             (
447             error => "The SQLite driver does not permit DELETE statements to have output columns (no RETURNING support)",
448             param => 'sql_obj',
449             value => $sql,
450             );
451             }
452              
453              
454 0           $str .= 'DELETE FROM ';
455 0           $str .= $driver->__sl_render_table_name($sql->table);
456              
457 0           $str .= ' WHERE ';
458 0           $str .= $driver->__sl_render_where_clause($sql->where, USE_EXPLICIT_TABLE_NAME);
459              
460 0           return $str;
461             }
462              
463             sub __sl_render_select {
464 0     0     my $driver = shift;
465 0           my $sql = shift;
466 0           my $str = "SELECT \n";
467 0           $str .= $driver->__sl_render_output_column_list($sql->output_columns);
468 0           $str .= "\n FROM \n";
469 0           $str .= $driver->__sl_render_from_clause($sql->from);
470 0           $str .= "\n WHERE \n";
471 0           $str .= $driver->__sl_render_where_clause($sql->where, USE_TABLE_ALIAS);
472 0 0         if ($sql->order_by->columns) {
473 0           $str .= "\n ORDER BY \n";
474 0           $str .= $driver->__sl_render_order_by_clause($sql->order_by);
475             }
476 0 0         if (defined ($sql->limit())) {
477 0           $str .= " LIMIT " . $sql->limit() ."\n";
478 0 0         if (defined ($sql->offset())) {
479 0           $str .= " OFFSET " . $sql->offset() ."\n";
480             }
481             }
482 0           return $str;
483             }
484              
485              
486             #=================================================================================#
487             # SQL Clause Rendering
488             #=================================================================================#
489              
490              
491             sub __sl_render_output_column {
492 0     0     my $driver = shift;
493 0           my $oc = shift;
494 0   0       my $use_table_aliases = shift || USE_EXPLICIT_TABLE_NAME;
495 0           my $str = $driver->__sl_render_expression($oc->expression, $use_table_aliases);
496 0 0         if ($oc->alias) {
497 0           $str .= ' AS ' . $oc->alias;
498             }
499 0           return $str;
500             }
501              
502             sub __sl_render_output_column_list {
503 0     0     my $driver = shift;
504 0           my @cols = @_;
505 0           my $str = join ', ',
506             map {
507 0           $driver->__sl_render_output_column($_, USE_TABLE_ALIAS);
508             } @cols;
509 0           return $str;
510             }
511              
512             sub __sl_render_order_by_clause {
513 0     0     my $driver = shift;
514 0           my $ob = shift;
515 0           my $str = join ', ',
516             map {
517 0           $driver->__sl_render_column_name($_->[0], 1, 0, 1)
518             . ' '
519             . $_->[1]
520             } $ob->columns_with_directions;
521 0           return $str;
522             }
523              
524             sub __sl_render_from_clause {
525 0     0     my $driver = shift;
526 0           my $from = shift;
527 0           my $rel = $from->root_relation();
528 0           return $driver->__sl_render_relation($rel);
529             }
530              
531             sub __sl_render_where_clause {
532 0     0     my $driver = shift;
533 0           my $where = shift;
534 0 0         unless ($where) { return '1=1'; }
  0            
535 0   0       my $use_table_aliases = shift || USE_EXPLICIT_TABLE_NAME;
536 0           my $crit = $where->root_criterion();
537 0           return $driver->__sl_render_criterion($crit, $use_table_aliases);
538             }
539              
540              
541              
542             #=================================================================================#
543             # SQL Expression Rendering
544             #=================================================================================#
545              
546             sub __sl_render_expression {
547 0     0     my $driver = shift;
548 0           my $exp = shift;
549 0   0       my $use_table_aliases = shift || USE_EXPLICIT_TABLE_NAME;
550              
551 0 0         if ($exp->is_param) {
    0          
    0          
    0          
    0          
552 0           return '?';
553             } elsif ($exp->is_column) {
554 0           return $driver->__sl_render_column_name($exp, $use_table_aliases, 0, 0);
555             } elsif ($exp->is_literal) {
556 0           my $val = $exp->value;
557 0 0         if (looks_like_number($val)) {
558 0           return $val;
559             } else {
560 0           return "'$val'";
561             }
562             # Criterion case now handled by Function Call
563             #} elsif ($exp->is_criterion) {
564             # return $driver->__sl_render_criterion($exp, $use_table_aliases);
565             } elsif ($exp->is_function_call) {
566 0           return $driver->__sl_render_function_call($exp, $use_table_aliases);
567             } elsif ($exp->is_subquery()) {
568 0           return $driver->__sl_render_subquery_as_expresion($exp);
569             } else {
570             # Don't know how to handle this
571 0           my $type = ref($exp);
572 0           Class::ReluctantORM::Exception::NotImplemented->croak(__PACKAGE__ . " does not know how to render a $type");
573             }
574             }
575              
576             sub __sl_render_column_name {
577 0     0     my $driver = shift;
578 0           my $col = shift;
579 0   0       my $use_table_alias = shift || USE_EXPLICIT_TABLE_NAME;
580 0   0       my $use_column_alias = shift || 0;
581 0   0       my $make_column_alias = shift || 0;
582              
583 0           my $table = $col->table;
584              
585 0           my $name = '';
586              
587 0 0 0       if ($use_column_alias && $col->alias) {
    0          
    0          
588 0           $name = $col->alias;
589             } elsif ($table) {
590 0 0         if ($use_table_alias == USE_TABLE_ALIAS) {
    0          
    0          
591 0           $name .= $table->alias . '.';
592             } elsif ($use_table_alias == USE_EXPLICIT_TABLE_NAME) {
593 0           $name = $driver->__sl_render_table_name($table) . '.';
594             } elsif ($use_table_alias == USE_BARE_COLUMN) {
595             # Do nothing
596             }
597 0           $name .= $driver->_sl_quoted($col->column);
598 0 0 0       if ($make_column_alias && $col->alias) {
599 0           $name .= ' AS ' . $col->alias;
600             }
601             } elsif ($use_table_alias == USE_BARE_COLUMN) {
602 0           $name .= $driver->_sl_quoted($col->column);
603 0 0 0       if ($make_column_alias && $col->alias) {
604 0           $name .= ' AS ' . $col->alias;
605             }
606             }
607              
608 0           return $name;
609             }
610              
611             sub __sl_render_table_name {
612 0     0     my $driver = shift;
613 0           my $table = shift;
614 0   0       my $alias_mode = shift || USE_EXPLICIT_TABLE_NAME;
615 0           my $name = '';
616              
617 0 0 0       if (($alias_mode == USE_TABLE_ALIAS) && $table->alias) {
618 0           return $table->alias();
619             }
620              
621 0 0         if ($table->schema) {
622 0           $name .= $driver->_sl_quoted($table->schema) . '.';
623             }
624 0           $name .= $driver->_sl_quoted($table->table);
625 0           return $name;
626             }
627              
628              
629             sub __sl_render_relation {
630 0     0     my $driver = shift;
631 0           my $rel = shift;
632 0   0       my $alias_mode = shift || USE_EXPLICIT_TABLE_NAME;
633 0           my $sql = '';
634              
635 0 0         if ($rel->is_leaf_relation) {
636 0 0         if ($rel->is_table) {
637 0           $sql = $driver->__sl_render_table_name($rel, $alias_mode);
638             } else {
639             # Don't know how to handle this
640 0           Class::ReluctantORM::Exception::Call::NotImplemented->croak(__PACKAGE__ . ' does not know how to render a non-table leaf relation');
641             }
642             } else {
643 0 0         if ($rel->is_join) {
644 0           $sql = '(' . $driver->__sl_render_relation($rel->left_relation, $alias_mode);
645 0           $sql .= ' ' . $driver->__sl_render_join_type($rel->type) . ' ';
646 0           $sql .= $driver->__sl_render_relation($rel->right_relation, $alias_mode);
647              
648             # Always use table alias in ON criteria - PG requires it
649 0           $sql .= ' ON ' . $driver->__sl_render_criterion($rel->criterion, USE_TABLE_ALIAS) . ')';
650             } else {
651 0           Class::ReluctantORM::Exception::Call::NotImplemented->croak(__PACKAGE__ . ' does not know how to render a non-join non-leaf relation');
652             }
653             }
654              
655 0 0         if ($rel->alias) {
656 0           $sql .= ' ' . $rel->alias;
657             }
658              
659 0           return $sql;
660             }
661              
662              
663             sub __sl_render_join_type {
664 0     0     my $driver = shift;
665 0           my $raw_type = shift;
666 0           return $raw_type . ' JOIN';
667             }
668              
669             sub __sl_render_subquery_as_expresion {
670 0     0     my $driver = shift;
671 0           my $subquery = shift;
672 0           return '(' . $driver->__sl_render_select($subquery->statement()) . ')';
673             }
674              
675              
676             # This is currently an alias for function_call
677 0     0     sub __sl_render_criterion { return __sl_render_function_call(@_); }
678              
679             sub __sl_render_function_call {
680 0     0     my $driver = shift;
681 0           my $criterion = shift;
682 0   0       my $use_table_aliases = shift || USE_EXPLICIT_TABLE_NAME;
683              
684             # Avoid $_
685 0           my @args;
686 0           foreach my $arg ($criterion->arguments) {
687 0           push @args, $driver->__sl_render_expression($arg, $use_table_aliases);
688             }
689              
690 0           my $sql;
691 0           my $func = $criterion->function();
692 0 0         if (exists $FUNCTION_RENDERERS{$func->name()}) {
693 0           $sql = $FUNCTION_RENDERERS{$func->name()}->(@args);
694             } else {
695 0           Class::ReluctantORM::Exception::NotImplemented->croak(__PACKAGE__ . " does not know how to render a function call for function " . $func->name());
696             }
697 0           return $sql;
698             }
699              
700              
701             sub __sl_fd_transform_sql {
702 0     0     my $driver = shift;
703 0           my $sql = shift;
704              
705 0           $driver->_monitor_render_begin(sql_obj => $sql);
706 0           $sql->make_inflatable(auto_reconcile => 1, auto_annotate => 1);
707              
708 0 0         if ($sql->limit) {
709 0           $driver->__sl_transform_sql_fold_limit_for_deep($sql);
710             }
711              
712 0           $driver->_monitor_render_transform(sql_obj => $sql);
713             }
714              
715              
716             #=============================================================================#
717             # Fetch Deep SQL Transformation
718             #=============================================================================#
719              
720             =begin devnotes
721              
722             =head2 Transformations on Limits, Offsets, and Ordering In Joins
723              
724             Given:
725             Ship->fetch_deep(
726             where =>
727             with => { pirates => {}},
728             limit => ,
729             order_by => ,
730             offset => ,
731             );
732              
733             Initial SQL looks like:
734             SELECT
735             FROM
736             TABLE (Ship)
737             LEFT OUTER JOIN TABLE (Pirate)
738             WHERE
739             ORDER BY
740             LIMIT
741             OFFSET
742              
743             This is wrong - the limit will apply to the ship-pirate join,
744             when it should apply only to ships.
745              
746             Transform to:
747             SELECT
748             FROM
749             TABLE (Ship)
750             LEFT OUTER JOIN TABLE (Pirate)
751             WHERE CompositePK(Ship) IN (
752             SUBSELECT CompositePK(Ship)
753             FROM Ship
754             WHERE
755             ORDER BY
756             LIMIT
757             OFFSET
758             )
759             ORDER BY
760              
761             with the additional constraints that:
762             - ORDER1 may only refer to Ship
763             - WHERE1 may only refer to Ship
764             - WHERE1 and ORDER1 must be re-aliasable
765              
766              
767             =cut
768              
769             sub __sl_transform_sql_fold_limit_for_deep {
770 0     0     my $driver = shift;
771 0           my $sql = shift;
772              
773             # Determine the base table
774 0           my $base_table = $sql->base_table();
775 0 0         unless ($base_table->is_table()) {
776 0           Class::ReluctantORM::Exception::NotImplemented->croak(__PACKAGE__ . " doesn't know what to do with a non-table base relation");
777             }
778              
779             # Check that order clause only refers to base table
780 0           foreach my $table ($sql->order_by->tables()) {
781 0 0         unless ($base_table->is_the_same_table($table)) {
782 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.");
783             }
784             }
785              
786             # Check that where clause only refers to base table
787 0           foreach my $table ($sql->where->tables()) {
788 0 0         unless ($base_table->is_the_same_table($table)) {
789 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.");
790             }
791             }
792              
793             # Create new SELECT statement, with re-aliased base references
794 0           my $select = Class::ReluctantORM::SQL->new('select');
795 0           my $alias = $sql->new_table_alias();
796 0           my $table_copy = Table->new($base_table->class());
797 0           $table_copy->alias($alias);
798 0           $select->from(From->new($table_copy));
799              
800             # Extract and move where clause
801 0           my $inner_where = $sql->where();
802 0           $sql->where(undef); # Clear outer where
803 0           foreach my $col ($inner_where->columns) {
804             # Force columns referenced in the where clause to refer to new, re-aliased copy of table
805 0           $col->table($table_copy);
806             }
807 0           $select->where($inner_where);
808              
809             # Copy order by clause, re-alias, and attach to select statement
810 0           my $inner_ob = OrderBy->new();
811 0           foreach my $crit ($sql->order_by->columns_with_directions) {
812 0           my ($outer_col, $direction) = @$crit;
813 0           my $inner_col = Column->new(column => $outer_col->column, table => $table_copy);
814 0           $inner_ob->add($inner_col, $direction);
815             }
816 0           $select->order_by($inner_ob);
817              
818             # Move limit and offset clauses to inner select
819 0           $select->limit($sql->limit());
820 0           $sql->limit(undef);
821 0           $select->offset($sql->offset());
822 0           $sql->offset(undef);
823              
824             # Alter SELECT statement to return composite PK
825 0           my $oc = OutputColumn->new(
826             expression =>
827             FunctionCall->new('key_compositor_inside_subquery', $table_copy->primary_key_columns()),
828             alias => '',
829             );
830 0           $select->add_output($oc);
831              
832              
833             # Replace top-level WHERE with single criteria, seeking a composite key in the subselect
834 0           my $new_top_where = Where->new(
835             Criterion->new(
836             'in',
837             FunctionCall->new('key_compositor_outside_subquery', $base_table->primary_key_columns()),
838             SubQuery->new($select),
839             )
840             );
841 0           $sql->where($new_top_where);
842              
843              
844             }
845              
846             1;