File Coverage

blib/lib/DBIx/DataModel/Schema/Generator.pm
Criterion Covered Total %
statement 122 197 61.9
branch 15 50 30.0
condition 14 47 29.7
subroutine 17 23 73.9
pod 9 9 100.0
total 177 326 54.2


line stmt bran cond sub pod time code
1             #----------------------------------------------------------------------
2             package DBIx::DataModel::Schema::Generator;
3             #----------------------------------------------------------------------
4              
5             # see POD doc at end of file
6             # version : see DBIx::DataModel
7              
8 2     2   7065 use strict;
  2         7  
  2         84  
9 2     2   11 use warnings;
  2         5  
  2         121  
10 2     2   12 no warnings 'uninitialized';
  2         5  
  2         100  
11 2     2   532 use DBIx::DataModel::Carp;
  2         16  
  2         19  
12 2     2   180 use List::Util qw/max/;
  2         4  
  2         268  
13 2     2   15 use Exporter qw/import/;
  2         5  
  2         78  
14 2     2   8700 use DBI;
  2         53679  
  2         214  
15 2     2   730 use Try::Tiny;
  2         2860  
  2         147  
16 2     2   16 use Module::Load ();
  2         4  
  2         94  
17              
18              
19             our @EXPORT = qw/fromDBIxClass fromDBI/;
20              
21              
22 2     2   11 use constant CASCADE => 0; # see L
  2         6  
  2         7426  
23              
24             #----------------------------------------------------------------------
25             # front methods
26             #----------------------------------------------------------------------
27              
28             sub new {
29 2     2 1 366968 my ($class, @args) = @_;
30 2         7 my $self = bless {@args}, $class;
31 2   50     12 $self->{-schema} ||= "My::Schema";
32 2   50     14 $self->{tables} ||= [];
33 2   50     10 $self->{assoc} ||= [];
34 2         45 return $self;
35             }
36              
37              
38             sub fromDBI {
39             # may be called as ordinary sub or as method
40 0 0   0 1 0 my $self = ref $_[0] eq __PACKAGE__ ? shift : __PACKAGE__->new(@ARGV);
41              
42 0         0 $self->parse_DBI(@_);
43 0         0 print $self->perl_code;
44             }
45              
46              
47             sub fromDBIxClass {
48             # may be called as ordinary sub or as method
49 0 0   0 1 0 my $self = ref $_[0] eq __PACKAGE__ ? shift : __PACKAGE__->new(@ARGV);
50              
51 0         0 $self->parse_DBIx_Class(@_);
52 0         0 print $self->perl_code;
53             }
54              
55             # other name for this method
56             *fromDBIC = \&fromDBIxClass;
57              
58              
59              
60             # support for SQL::Translator::Producer
61             sub produce {
62 0     0 1 0 my $tr = shift;
63              
64 0 0       0 my $self = __PACKAGE__->new(%{$tr->{producer_args} || {}});
  0         0  
65 0         0 $self->parse_SQL_Translator($tr);
66 0         0 return $self->perl_code;
67             }
68              
69              
70             sub load {
71 0     0 1 0 my $self = shift;
72 0         0 eval $self->perl_code;
73             }
74              
75              
76             #----------------------------------------------------------------------
77             # build internal data from external sources
78             #----------------------------------------------------------------------
79              
80             sub parse_DBI {
81 2     2 1 12 my $self = shift;
82              
83             # dbh connection
84 2 50       8 my $arg1 = shift or croak "missing arg (dsn for DBI->connect(..))";
85 2 50 33     17 my $dbh = ref $arg1 && $arg1->isa('DBI::db') ? $arg1 : do {
86 0   0     0 my $user = shift || "";
87 0   0     0 my $passwd = shift || "";
88 0   0     0 my $options = shift || {RaiseError => 1};
89 0 0       0 DBI->connect($arg1, $user, $passwd, $options)
90             or croak "DBI->connect failed ($DBI::errstr)";
91             };
92              
93             # get list of tables
94 2         2 my %args;
95 2         5 $args{catalog} = shift;
96 2         4 $args{schema} = shift;
97 2   50     8 $args{type} = shift || "TABLE";
98 2         17 my $tables_sth = $dbh->table_info(@args{qw/catalog schema table type/});
99 2         751 my $tables = $tables_sth->fetchall_arrayref({});
100              
101             TABLE:
102 2         183 foreach my $table (@$tables) {
103              
104             # get primary key info
105 7         411 my @table_id = @{$table}{qw/TABLE_CAT TABLE_SCHEM TABLE_NAME/};
  7         27  
106 7   100     56 my $pkey = join(" ", $dbh->primary_key(@table_id)) || "unknown_pk";
107              
108             my $table_info = {
109             classname => _table2class($table->{TABLE_NAME}),
110             tablename => $table->{TABLE_NAME},
111             pkey => $pkey,
112             remarks => $table->{REMARKS},
113 7         21167 };
114              
115             # insert into list of tables
116 7         17 push @{$self->{tables}}, $table_info;
  7         20  
117              
118              
119             # get association info (in an eval because unimplemented by some drivers)
120 7     7   269 my $fkey_sth = try {$dbh->foreign_key_info(@table_id,
121             undef, undef, undef)}
122 7 50       49 or next TABLE;
123              
124 7         23462 while (my $fk_row = $fkey_sth->fetchrow_hashref) {
125              
126             # hack for unifying "ODBC" or "SQL/CLI" column names (see L)
127 5   33     284 $fk_row->{"UK_$_"} ||= $fk_row->{"PK$_"} for qw/TABLE_NAME COLUMN_NAME/;
128 5   33     39 $fk_row->{"FK_$_"} ||= $fk_row->{"FK$_"} for qw/TABLE_NAME COLUMN_NAME/;
129              
130 5         12 my $del_rule = $fk_row->{DELETE_RULE};
131              
132             my @assoc = (
133             { table => _table2class($fk_row->{UK_TABLE_NAME}),
134             col => $fk_row->{UK_COLUMN_NAME},
135             role => _table2role($fk_row->{UK_TABLE_NAME}),
136             mult_min => 1, #0/1 (TODO: depend on is_nullable on other side)
137             mult_max => 1,
138             },
139             { table => _table2class($fk_row->{FK_TABLE_NAME}),
140             col => $fk_row->{FK_COLUMN_NAME},
141 5   66     18 role => _table2role($fk_row->{FK_TABLE_NAME}, "s"),
142             mult_min => 0,
143             mult_max => '*',
144             is_cascade => defined $del_rule && $del_rule == CASCADE,
145             }
146             );
147              
148 5         14 push @{$self->{assoc}}, \@assoc;
  5         136  
149             }
150             }
151             }
152              
153              
154             sub parse_DBIx_Class {
155 0     0 1 0 my $self = shift;
156              
157 0 0       0 my $dbic_schema = shift or croak "missing arg (DBIC schema name)";
158              
159             # load the DBIx::Class schema
160 0 0       0 eval {Module::Load::load $dbic_schema; 1} or croak $@;
  0         0  
  0         0  
161              
162             # global hash to hold assoc. info (because we must collect info from
163             # both tables to get both directions of the association)
164 0         0 my %associations;
165              
166             # foreach DBIC table class ("moniker" : short class name)
167 0         0 foreach my $moniker ($dbic_schema->sources) {
168 0         0 my $source = $dbic_schema->source($moniker); # full DBIC class
169              
170             # table info
171 0         0 my $table_info = {
172             classname => $moniker,
173             tablename => $source->from,
174             pkey => join(" ", $source->primary_columns),
175             };
176              
177             # inflated columns
178 0         0 foreach my $col ($source->columns) {
179 0         0 my $column_info = $source->column_info($col);
180             my $inflate_info = $column_info->{_inflate_info}
181 0 0       0 or next;
182              
183             # don't care about inflators for related objects
184 0 0       0 next if $source->relationship_info($col);
185              
186 0         0 my $data_type = $column_info->{data_type};
187 0         0 push @{$self->{column_types}{$data_type}{$moniker}}, $col;
  0         0  
188             }
189              
190             # insert into list of tables
191 0         0 push @{$self->{tables}}, $table_info;
  0         0  
192              
193             # association info
194 0         0 foreach my $relname ($source->relationships) {
195 0         0 my $relinfo = $source->relationship_info($relname);
196              
197             # extract join keys from $relinfo->{cond} (which
198             # is of shape {"foreign.k1" => "self.k2"})
199 0         0 my ($fk, $pk) = map /\.(.*)/, %{$relinfo->{cond}};
  0         0  
200              
201             # moniker of the other side of the relationship
202 0         0 my $relmoniker = $source->related_source($relname)->source_name;
203              
204             # info structure
205             my %info = (
206             table => $relmoniker,
207             col => $fk,
208             role => $relname,
209              
210             # compute multiplicities
211             mult_min => $relinfo->{attrs}{join_type} eq 'LEFT' ? 0 : 1,
212 0 0       0 mult_max => $relinfo->{attrs}{accessor} eq 'multi' ? "*" : 1,
    0          
213             );
214              
215             # store assoc info into global hash; since both sides of the assoc must
216             # ultimately be joined, we compute a unique key from alphabetic ordering
217 0 0 0     0 my ($key, $index) = ($moniker cmp $relmoniker || $fk cmp $pk) < 0
218             ? ("$moniker/$relmoniker/$fk/$pk", 0)
219             : ("$relmoniker/$moniker/$pk/$fk", 1);
220 0         0 $associations{$key}[$index] = \%info;
221              
222             # info on other side of the association
223 0         0 my $other_index = 1 - $index;
224 0   0     0 my $other_assoc = $associations{$key}[1 - $index] ||= {};
225 0   0     0 $other_assoc->{table} ||= $moniker;
226 0   0     0 $other_assoc->{col} ||= $pk;
227 0 0       0 defined $other_assoc->{mult_min} or $other_assoc->{mult_min} = 1;
228 0 0       0 defined $other_assoc->{mult_max} or $other_assoc->{mult_max} = 1;
229             }
230             }
231              
232 0         0 $self->{assoc} = [values %associations];
233             }
234              
235              
236             sub parse_SQL_Translator {
237 0     0 1 0 my ($self, $tr) = @_;
238              
239 0         0 my $schema = $tr->schema;
240 0         0 foreach my $table ($schema->get_tables) {
241 0         0 my $tablename = $table->name;
242 0         0 my $classname = _table2class($tablename);
243 0         0 my $pk = $table->primary_key;
244 0 0       0 my @pkey = $pk ? ($pk->field_names) : qw/unknown_pk/;
245              
246 0         0 my $table_info = {
247             classname => $classname,
248             tablename => $tablename,
249             pkey => join(" ", @pkey),
250             remarks => join("\n", $table->comments),
251             };
252 0         0 push @{$self->{tables}}, $table_info;
  0         0  
253              
254             my @foreign_keys
255 0         0 = grep {$_->type eq 'FOREIGN KEY'} ($table->get_constraints);
  0         0  
256              
257 0         0 my $role = _table2role($tablename, "s");
258 0         0 foreach my $fk (@foreign_keys) {
259 0         0 my $ref_table = $fk->reference_table;
260 0         0 my @ref_fields = $fk->reference_fields;
261              
262             my @assoc = (
263             { table => _table2class($ref_table),
264             col => $table_info->{pkey},
265 0         0 role => _table2role($ref_table),
266             mult_min => 1, #0/1 (TODO: depend on is_nullable on other side)
267             mult_max => 1,
268             },
269             { table => $classname,
270             col => join(" ", $fk->fields),
271             role => $role,
272             mult_min => 0,
273             mult_max => '*',
274             }
275             );
276 0         0 push @{$self->{assoc}}, \@assoc;
  0         0  
277             }
278             }
279             }
280              
281              
282             #----------------------------------------------------------------------
283             # emit perl code
284             #----------------------------------------------------------------------
285              
286             sub perl_code {
287 2     2 1 165 my ($self) = @_;
288              
289             # check that we have some data
290 2 50       2 @{$self->{tables}}
  2         8  
291             or croak "can't generate schema: no data. "
292             . "Call parse_DBI() or parse_DBIx_Class() before";
293              
294             # make sure there is no duplicate role on the same table
295 2         4 my %seen_role;
296 2         3 foreach my $assoc (@{$self->{assoc}}) {
  2         6  
297 5         5 my $count;
298 5         17 $count = ++$seen_role{$assoc->[0]{table}}{$assoc->[1]{role}};
299 5 100       10 $assoc->[1]{role} .= "_$count" if $count > 1;
300 5         11 $count = ++$seen_role{$assoc->[1]{table}}{$assoc->[0]{role}};
301 5 100       11 $assoc->[0]{role} .= "_$count" if $count > 1;
302             }
303              
304             # compute max length of various fields (for prettier source alignment)
305 2         3 my %l;
306 2         4 foreach my $field (qw/classname tablename pkey/) {
307 6         8 $l{$field} = max map {length $_->{$field}} @{$self->{tables}};
  21         42  
  6         9  
308             }
309 2         3 foreach my $field (qw/col role mult/) {
310 6         8 $l{$field} = max map {length $_->{$field}} map {(@$_)} @{$self->{assoc}};
  30         41  
  15         18  
  6         13  
311             }
312 2         8 $l{mult} = max ($l{mult}, 4);
313              
314             # start emitting code
315 2         7 my $code = <<__END_OF_CODE__;
316             use strict;
317             use warnings;
318             use DBIx::DataModel;
319              
320             DBIx::DataModel # no semicolon (intentional)
321              
322             #---------------------------------------------------------------------#
323             # SCHEMA DECLARATION #
324             #---------------------------------------------------------------------#
325             ->Schema('$self->{-schema}')
326              
327             #---------------------------------------------------------------------#
328             # TABLE DECLARATIONS #
329             #---------------------------------------------------------------------#
330             __END_OF_CODE__
331              
332 2         8 my $colsizes = "%-$l{classname}s %-$l{tablename}s %-$l{pkey}s";
333 2         3 my $format = "->Table(qw/$colsizes/)\n";
334              
335 2         16 $code .= sprintf("# $colsizes\n", qw/Class Table PK/)
336             . sprintf("# $colsizes\n", qw/===== ===== ==/);
337              
338 2         3 foreach my $table (@{$self->{tables}}) {
  2         5  
339 7 50       13 if ($table->{remarks}) {
340 0         0 $table->{remarks} =~ s/^/# /gm;
341 0         0 $code .= "\n$table->{remarks}\n";
342             }
343 7         10 $code .= sprintf $format, @{$table}{qw/classname tablename pkey/};
  7         23  
344             }
345              
346              
347 2         8 $colsizes = "%-$l{classname}s %-$l{role}s %-$l{mult}s %-$l{col}s";
348 2         2 $format = " [qw/$colsizes/]";
349              
350 2         7 $code .= <<__END_OF_CODE__;
351              
352             #---------------------------------------------------------------------#
353             # ASSOCIATION DECLARATIONS #
354             #---------------------------------------------------------------------#
355             __END_OF_CODE__
356              
357 2         18 $code .= sprintf("# $colsizes\n", qw/Class Role Mult Join/)
358             . sprintf("# $colsizes", qw/===== ==== ==== ====/);
359              
360 2         3 foreach my $a (@{$self->{assoc}}) {
  2         5  
361              
362             # for prettier output, make sure that multiplicity "1" is first
363             @$a = reverse @$a if $a->[1]{mult_max} eq "1"
364 5 50 33     18 && $a->[0]{mult_max} eq "*";
365              
366             # complete association info
367 5         12 for my $i (0, 1) {
368 10   50     20 $a->[$i]{role} ||= "---";
369 10         20 my $mult = "$a->[$i]{mult_min}..$a->[$i]{mult_max}";
370 10   33     38 $a->[$i]{mult} = {"0..*" => "*", "1..1" => "1"}->{$mult} || $mult;
371             }
372              
373             # association or composition
374 5 100       12 my $relationship = $a->[1]{is_cascade} ? 'Composition' : 'Association';
375              
376             $code .= "\n->$relationship(\n"
377 5         14 . sprintf($format, @{$a->[0]}{qw/table role mult col/})
378             . ",\n"
379 5         7 . sprintf($format, @{$a->[1]}{qw/table role mult col/})
  5         72  
380             . ")\n";
381             }
382 2         4 $code .= "\n;\n";
383              
384             # column types
385 2         7 $code .= <<__END_OF_CODE__;
386              
387             #---------------------------------------------------------------------#
388             # COLUMN TYPES #
389             #---------------------------------------------------------------------#
390             # $self->{-schema}->ColumnType(ColType_Example =>
391             # fromDB => sub {...},
392             # toDB => sub {...});
393              
394             # $self->{-schema}::SomeTable->ColumnType(ColType_Example =>
395             # qw/column1 column2 .../);
396              
397             __END_OF_CODE__
398              
399 2 50       4 while (my ($type, $targets) = each %{$self->{column_types} || {}}) {
  2         18  
400 0         0 $code .= <<__END_OF_CODE__;
401             # $type
402             $self->{-schema}->ColumnType($type =>
403             fromDB => sub {}, # SKELETON .. PLEASE FILL IN
404             toDB => sub {});
405             __END_OF_CODE__
406              
407 0         0 while (my ($table, $cols) = each %$targets) {
408             $code .= sprintf("%s::%s->ColumnType($type => qw/%s/);\n",
409 0         0 $self->{-schema}, $table, join(" ", @$cols));
410             }
411 0         0 $code .= "\n";
412             }
413              
414             # end of module
415 2         6 $code .= "\n\n1;\n";
416              
417 2         14 return $code;
418             }
419              
420             #----------------------------------------------------------------------
421             # utility methods/functions
422             #----------------------------------------------------------------------
423              
424             # generate a Perl classname from a database table name
425             sub _table2class{
426 17     17   41 my ($tablename) = @_;
427              
428 17         206 my $classname = join '', map ucfirst, split /[\W_]+/, lc $tablename;
429             }
430              
431             # singular / plural inflection. Start with simple-minded defaults,
432             # and try to more sophisticated use Lingua::Inflect if module is installed
433             my $to_S = sub {(my $r = $_[0]) =~ s/s$//i; $r};
434             my $to_PL = sub {$_[0] . "s"};
435 2     2   3116 eval "use Lingua::EN::Inflect::Phrase qw/to_S to_PL/;"
  2         166542  
  2         143  
436             . "\$to_S = \\&to_S; \$to_PL = \\&to_PL;"
437             or warn "Lingua::EN::Inflect::Phrase is recommended; please install it to "
438             . "generate better names for associations";
439             ;
440              
441             # generate a rolename from a database table name
442             sub _table2role{
443 10     10   23 my ($tablename, $plural) = @_;
444              
445 10 100       27 my $inflect = $plural ? $to_PL : $to_S;
446             # my ($first, @other) = map {$inflect->($_)} split /[\W_]+/, lc $tablename;
447             # my $role = join '_', $first, @other;
448 10         38 my $role = $inflect->(lc $tablename);
449 10         297284 return $role;
450             }
451              
452              
453             1;
454              
455             __END__