no longer using set_up_table method. it incurs an overhead penalty by making
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / ClassDBI.pm
1 package SQL::Translator::Producer::ClassDBI;
2
3 # -------------------------------------------------------------------
4 # $Id: ClassDBI.pm,v 1.29 2003-08-07 03:51:48 allenday Exp $
5 # -------------------------------------------------------------------
6 # Copyright (C) 2003 Allen Day <allenday@ucla.edu>,
7 #                    Ying Zhang <zyolive@yahoo.com>
8 #
9 # This program is free software; you can redistribute it and/or
10 # modify it under the terms of the GNU General Public License as
11 # published by the Free Software Foundation; version 2.
12 #
13 # This program is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 # General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
21 # 02111-1307  USA
22 # -------------------------------------------------------------------
23
24 use strict;
25 use vars qw[ $VERSION $DEBUG ];
26 $VERSION = sprintf "%d.%02d", q$Revision: 1.29 $ =~ /(\d+)\.(\d+)/;
27 $DEBUG   = 1 unless defined $DEBUG;
28
29 use SQL::Translator::Schema::Constants;
30 use SQL::Translator::Utils qw(header_comment);
31 use Data::Dumper;
32
33 my %CDBI_auto_pkgs = (
34     MySQL      => 'mysql',
35     PostgreSQL => 'Pg',
36     Oracle     => 'Oracle',
37 );
38
39 # -------------------------------------------------------------------
40 sub produce {
41     my $t             = shift;
42         my $create        = undef;
43     local $DEBUG      = $t->debug;
44     my $no_comments   = $t->no_comments;
45     my $schema        = $t->schema;
46     my $args          = $t->producer_args;
47     my $db_user       = $args->{'db_user'} || '';
48     my $db_pass       = $args->{'db_pass'} || '';
49     my $main_pkg_name = $t->format_package_name('DBI');
50     my $header        = header_comment(__PACKAGE__, "# ");
51     my $parser_type   = ( split /::/, $t->parser_type )[-1];
52     my $from          = $CDBI_auto_pkgs{ $parser_type } || '';
53     my $dsn           = $args->{'dsn'} || sprintf( 'dbi:%s:_',
54                             $CDBI_auto_pkgs{ $parser_type }
55                             ? $CDBI_auto_pkgs{ $parser_type } : $parser_type
56                         );
57     my $sep           = '# ' . '-' x 67;
58
59     #
60     # Identify "link tables" (have only PK and FK fields).
61     #
62     my %linkable;
63     my %linktable;
64     foreach my $table ( $schema->get_tables ) {
65         my $is_link = 1;
66         foreach my $field ( $table->get_fields ) {
67             unless ( $field->is_primary_key or $field->is_foreign_key ) {
68                 $is_link = 0; 
69                 last;
70             }
71         }
72
73         next unless $is_link;
74       
75         foreach my $left ( $table->get_fields ) {
76             next unless $left->is_foreign_key;
77             my $lfk           = $left->foreign_key_reference or next;
78             my $lr_table      = $schema->get_table( $lfk->reference_table )
79                                  or next;
80             my $lr_field_name = ($lfk->reference_fields)[0];
81             my $lr_field      = $lr_table->get_field($lr_field_name);
82             next unless $lr_field->is_primary_key;
83
84             foreach my $right ( $table->get_fields ) {
85                 next if $left->name eq $right->name;
86         
87                 my $rfk      = $right->foreign_key_reference or next;
88                 my $rr_table = $schema->get_table( $rfk->reference_table )
89                                or next;
90                 my $rr_field_name = ($rfk->reference_fields)[0];
91                 my $rr_field      = $rr_table->get_field($rr_field_name);
92                 next unless $rr_field->is_primary_key;
93         
94                 $linkable{ $lr_table->name }{ $rr_table->name } = $table;
95                 $linkable{ $rr_table->name }{ $lr_table->name } = $table;
96                 $linktable{ $table->name } = $table;
97             }
98         }
99     }
100
101     #
102     # Iterate over all tables
103     #
104     my ( %packages, $order );
105     for my $table ( $schema->get_tables ) {
106         my $table_name = $table->name or next;
107
108         my $table_pkg_name = $t->format_package_name($table_name);
109         $packages{ $table_pkg_name } = {
110             order     => ++$order,
111             pkg_name  => $table_pkg_name,
112             base      => $main_pkg_name,
113             table     => $table_name,
114         };
115
116         #
117         # Primary key may have a differenct accessor method name
118         #
119         if ( my $pk_xform = $t->format_pk_name ) {
120             if ( my $constraint = $table->primary_key ) {
121                 my $field          = ($constraint->fields)[0];
122                 my $pk_name        = $pk_xform->($table_pkg_name, $field);
123
124                                 $packages{ $table_pkg_name }{'_columns_primary'} = $field;
125
126                 $packages{ $table_pkg_name }{'pk_accessor'} = 
127                     "#\n# Primary key accessor\n#\n".
128                     "sub $pk_name {\n    shift->$field\n}\n\n"
129                 ;
130             }
131         }
132
133         my $is_data = 0;
134         foreach my $field ( $table->get_fields ) {
135                   if ( !$field->is_foreign_key and !$field->is_primary_key ) {
136                         push @{ $packages{ $table_pkg_name }{'_columns_essential'} }, $field->name;
137                         $is_data++;
138                   } elsif ( !$field->is_primary_key ) {
139                         push @{ $packages{ $table_pkg_name }{'_columns_others'} }, $field->name;
140                   }
141                 }
142
143                 my %linked;
144                 if ( $is_data ) {
145                   foreach my $link ( keys %{ $linkable{ $table_name } } ) {
146                         my $linkmethodname;
147
148                         if ( my $fk_xform = $t->format_fk_name ) {
149                           # ADD CALLBACK FOR PLURALIZATION MANGLING HERE
150                           $linkmethodname = $fk_xform->($linkable{$table_name}{$link}->name,
151                                                                                         ($schema->get_table($link)->primary_key->fields)[0]).'s';
152                         } else {
153                           # ADD CALLBACK FOR PLURALIZATION MANGLING HERE
154                           $linkmethodname = $linkable{$table_name}{$link}->name.'_'.
155                                 ($schema->get_table($link)->primary_key->fields)[0].'s';
156                         }
157
158                         my @rk_fields = ();
159                         my @lk_fields = ();
160                         foreach my $field ($linkable{$table_name}{$link}->get_fields) {
161                           next unless $field->is_foreign_key;
162
163                           next unless(
164                                                   $field->foreign_key_reference->reference_table eq $table_name
165                                                   ||
166                                                   $field->foreign_key_reference->reference_table eq $link
167                                                  );
168                           push @lk_fields, ($field->foreign_key_reference->reference_fields)[0]
169                                 if $field->foreign_key_reference->reference_table eq $link;
170                           push @rk_fields, $field->name
171                                 if $field->foreign_key_reference->reference_table eq $table_name;
172                         }
173
174                         #if one possible traversal via link table
175                         if (scalar(@rk_fields) == 1 and scalar(@lk_fields) == 1) {
176                           foreach my $rk_field (@rk_fields) {
177                                 push @{ $packages{ $table_pkg_name }{'has_many'}{ $link } },
178                                   "sub ".$linkmethodname." { my \$self = shift; ".
179                                         "return map \$_->".
180                                           ($schema->get_table($link)->primary_key->fields)[0].
181                                                 ", \$self->".$linkable{$table_name}{$link}->name.
182                                                   "_".$rk_field." }\n\n";
183                           }
184                           #else there is more than one way to traverse it.  ack!
185                           #let's treat these types of link tables as a many-to-one (easier)
186                           #
187                           #NOTE: we need to rethink the link method name, as the cardinality
188                           #has shifted on us.
189                         } elsif (scalar(@rk_fields) == 1) {
190                           foreach my $rk_field (@rk_fields) {
191                                 # ADD CALLBACK FOR PLURALIZATION MANGLING HERE
192                                 push @{ $packages{ $table_pkg_name }{'has_many'}{ $link } },
193                                   "sub " . $linkable{$table_name}{$link}->name .
194                                         "s { my \$self = shift; return \$self->" .
195                                           $linkable{$table_name}{$link}->name . "_" .
196                                                 $rk_field . "(\@_) }\n\n";
197                           }
198                         } elsif (scalar(@lk_fields) == 1) {
199                           #these will be taken care of on the other end...
200                         } else {
201                           #many many many.  need multiple iterations here, data structure revision
202                           #to handle N FK sources.  This code has not been tested and likely doesn't
203                           #work here
204                           foreach my $rk_field (@rk_fields) {
205                                 # ADD CALLBACK FOR PLURALIZATION MANGLING HERE
206                                 push @{ $packages{ $table_pkg_name }{'has_many'}{ $link } },
207                                   "sub " . $linkable{$table_name}{$link}->name . "_" . $rk_field .
208                                         "s { my \$self = shift; return \$self->" .
209                                           $linkable{$table_name}{$link}->name . "_" .
210                                                 $rk_field . "(\@_) }\n\n";
211                           }
212                         }
213                   }
214         }
215
216
217         #
218         # Use foreign keys to set up "has_a/has_many" relationships.
219         #
220         foreach my $field ( $table->get_fields ) {
221             if ( $field->is_foreign_key ) {
222                 my $table_name = $table->name;
223                 my $field_name = $field->name;
224                 my $fk_method  = $t->format_fk_name($table_name, $field_name);
225                 my $fk         = $field->foreign_key_reference;
226                 my $ref_table  = $fk->reference_table;
227                 my $ref_pkg    = $t->format_package_name($ref_table);
228                 my $ref_field  = ($fk->reference_fields)[0];
229
230                 push @{ $packages{ $table_pkg_name }{'has_a'} },
231                     "$table_pkg_name->has_a(\n".
232                     "    $field_name => '$ref_pkg'\n);\n\n".
233                     "sub $fk_method {\n".
234                     "    return shift->$field_name\n}\n\n"
235                 ;
236
237                 #
238                 # If this table "has a" to the other, then it follows 
239                 # that the other table "has many" of this one, right?
240                 #
241                                 # No... there is the possibility of 1-1 cardinality
242
243                                 #if there weren't M-M relationships via the has_many
244                                 #being set up here, create nice pluralized method alias
245                                 #rather for user as alt. to ugly tablename_fieldname name
246                                 if(! $packages{ $ref_pkg }{ 'has_many' }{ $table_name } ){
247                                   # ADD CALLBACK FOR PLURALIZATION MANGLING HERE
248                                   push @{ $packages{ $ref_pkg }{'has_many'}{ $table_name } },
249                                         "sub $table_name\s {\n    return shift->$table_name\_$field_name\n}\n\n";
250
251                                 #else ugly
252                                 } else {
253                                 }
254
255                                 push @{ $packages{ $ref_pkg }{'has_many'}{ $table_name } },
256                                   "$ref_pkg->has_many(\n    '${table_name}_${field_name}', ".
257                                   "'$table_pkg_name' => '$field_name'\n);\n\n";
258
259             }
260                 }
261         }
262
263     #
264     # Now build up text of package.
265     #
266     my $base_pkg = sprintf( 'Class::DBI%s', $from ? "::$from" : '' );
267     $create .= join("\n",
268       "package $main_pkg_name;\n",
269       $header,
270       "use strict;",
271       "use base '$base_pkg';\n",
272       "$main_pkg_name->set_db('Main', '$dsn', '$db_user', '$db_pass');\n\n",
273     ); 
274
275     for my $pkg_name ( 
276         sort { $packages{ $a }{'order'} <=> $packages{ $b }{'order'} }
277         keys %packages
278     ) {
279         my $pkg = $packages{ $pkg_name };
280
281         $create .= join("\n",
282             $sep,
283             "package ".$pkg->{'pkg_name'}.";",
284             "use base '".$pkg->{'base'}."';",
285             "use Class::DBI::Pager;\n\n",
286         );    
287
288 #        if ( $from ) {
289 #            $create .= 
290 #                $pkg->{'pkg_name'}."->set_up_table('".$pkg->{'table'}."');\n\n";
291 #        }
292 #        else {
293 #            my $table       = $schema->get_table( $pkg->{'table'} );
294 #            my @field_names = map { $_->name } $table->get_fields;
295 #
296 #            $create .= join("\n",
297 #                $pkg_name."->table('".$pkg->{'table'}."');\n",
298 #                $pkg_name."->columns(All => qw/".
299 #                join(' ', @field_names)."/);\n\n",
300 #            );
301 #        }
302
303
304                 #the approach here is to do lazy loading on the expensive columns
305                 #(expensive defined as those columns which require construction of a referenced object)
306                 #fields which are strictly data (ie, not references) are treated as essential b/c they
307                 #don't require much time to set up.
308
309                 $create .= $pkg_name."->table('".$pkg->{'table'}."');\n";
310
311                 #set up primary key field
312                 if( $pkg->{'_columns_primary'} ) {
313                   $create .= $pkg_name."->columns(Primary   => qw/".              $pkg->{'_columns_primary'}      ."/);\n";
314                 } else {
315                   die "Class::DBI isn't going to like that you don't have a primary key field for table ".$pkg->{'table'};
316                 }
317
318                 #set up non-FK fields to be populated at construction
319                 if( $pkg->{'_columns_essential'} ) {
320                   $create .= $pkg_name."->columns(Essential => qw/". join(' ', @{ $pkg->{'_columns_essential'} }) ."/);\n";
321                 }
322
323                 #set up FK fields for lazy loading on request
324                 if( $pkg->{'_columns_others'} ) {
325                   $create .= $pkg_name."->columns(Others    => qw/".    join(' ', @{ $pkg->{'_columns_others'}    }) ."/);\n";
326                 }
327
328                 $create .= "\n";
329
330         if ( my $pk = $pkg->{'pk_accessor'} ) {
331             $create .= $pk;
332         }
333
334         if ( my @has_a = @{ $pkg->{'has_a'} || [] } ) {
335             $create .= $_ for @has_a;
336         }
337
338                 foreach my $has_many_key (keys %{ $pkg->{'has_many'} }){
339                   if ( my @has_many = @{ $pkg->{'has_many'}{ $has_many_key } || [] } ) {
340             $create .= $_ for @has_many;
341                   }
342                 }
343     }
344
345     $create .= "1;\n";
346
347     return $create;
348 }
349
350 1;
351
352 # -------------------------------------------------------------------
353
354 =pod
355
356 =head1 NAME
357
358 SQL::Translator::Producer::ClassDBI - create Class::DBI classes from schema
359
360 =head1 SYNOPSIS
361
362 Use this producer as you would any other from SQL::Translator.  See
363 L<SQL::Translator> for details.
364
365 This package utilizes SQL::Translator's formatting methods
366 format_package_name(), format_pk_name(), format_fk_name(), and
367 format_table_name() as it creates classes, one per table in the schema
368 provided.  An additional base class is also created for database connectivity
369 configuration.  See L<Class::DBI> for details on how this works.
370
371 =head1 AUTHORS
372
373 Allen Day E<lt>allenday@ucla.eduE<gt>
374 Ying Zhang E<lt>zyolive@yahoo.comE<gt>,
375 Ken Y. Clark E<lt>kclark@cpan.org<gt>.