File Coverage

blib/lib/SQL/Translator/Parser/DBIx/Class.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package SQL::Translator::Parser::DBIx::Class;
2              
3             # AUTHOR: Jess Robinson
4              
5             # Some mistakes the fault of Matt S Trout
6              
7             # Others the fault of Ash Berlin
8              
9 2     2   1311 use strict;
  2         3  
  2         48  
10 2     2   7 use warnings;
  2         3  
  2         112  
11             our ($DEBUG, $VERSION, @EXPORT_OK);
12             $VERSION = '1.10';
13             $DEBUG = 0 unless defined $DEBUG;
14              
15 2     2   7 use Exporter;
  2         2  
  2         66  
16 2     2   392 use SQL::Translator::Utils qw(debug normalize_name);
  0            
  0            
17             use DBIx::Class::Carp qw/^SQL::Translator|^DBIx::Class|^Try::Tiny/;
18             use DBIx::Class::Exception;
19             use Scalar::Util 'blessed';
20             use Try::Tiny;
21             use namespace::clean;
22              
23             use base qw(Exporter);
24              
25             @EXPORT_OK = qw(parse);
26              
27             # -------------------------------------------------------------------
28             # parse($tr, $data)
29             #
30             # setting parser_args => { add_fk_index => 0 } will prevent
31             # the auto-generation of an index for each FK.
32             #
33             # Note that $data, in the case of this parser, is not useful.
34             # We're working with DBIx::Class Schemas, not data streams.
35             # -------------------------------------------------------------------
36             sub parse {
37             my ($tr, $data) = @_;
38             my $args = $tr->parser_args;
39              
40             my $dbicschema = $data || $args->{dbic_schema};
41              
42             for (qw(DBIx::Class::Schema DBIx::Schema package)) {
43             if (defined (my $s = delete $args->{$_} )) {
44             carp_unique("Supplying a schema via ... parser_args => { '$_' => \$schema } is deprecated. Please use parser_args => { dbic_schema => \$schema } instead");
45              
46             # move it from the deprecated to the proper $args slot
47             unless ($dbicschema) {
48             $args->{dbic_schema} = $dbicschema = $s;
49             }
50             }
51             }
52              
53             DBIx::Class::Exception->throw('No DBIx::Class::Schema') unless ($dbicschema);
54              
55             if (!ref $dbicschema) {
56             eval "require $dbicschema"
57             or DBIx::Class::Exception->throw("Can't load $dbicschema: $@");
58             }
59              
60             if (
61             ref $args->{dbic_schema}
62             and
63             $args->{dbic_schema}->storage
64             ) {
65             # we have a storage-holding $schema instance in $args
66             # we need to dissociate it from that $storage
67             # otherwise SQLT insanity may ensue due to how some
68             # serializing producers treat $args (crazy crazy shit)
69             local $args->{dbic_schema}{storage};
70             $args->{dbic_schema} = $args->{dbic_schema}->clone;
71             }
72              
73             my $schema = $tr->schema;
74             my $table_no = 0;
75              
76             $schema->name( ref($dbicschema) . " v" . ($dbicschema->schema_version || '1.x'))
77             unless ($schema->name);
78              
79             my @monikers = sort $dbicschema->sources;
80             if (my $limit_sources = $args->{'sources'}) {
81             my $ref = ref $limit_sources || '';
82             $dbicschema->throw_exception ("'sources' parameter must be an array or hash ref")
83             unless( $ref eq 'ARRAY' || ref eq 'HASH' );
84              
85             # limit monikers to those specified in
86             my $sources;
87             if ($ref eq 'ARRAY') {
88             $sources->{$_} = 1 for (@$limit_sources);
89             } else {
90             $sources = $limit_sources;
91             }
92             @monikers = grep { $sources->{$_} } @monikers;
93             }
94              
95              
96             my(%table_monikers, %view_monikers);
97             for my $moniker (@monikers){
98             my $source = $dbicschema->source($moniker);
99             if ( $source->isa('DBIx::Class::ResultSource::Table') ) {
100             $table_monikers{$moniker}++;
101             } elsif( $source->isa('DBIx::Class::ResultSource::View') ){
102             next if $source->is_virtual;
103             $view_monikers{$moniker}++;
104             }
105             }
106              
107             my %tables;
108             foreach my $moniker (sort keys %table_monikers)
109             {
110             my $source = $dbicschema->source($moniker);
111             my $table_name = $source->name;
112              
113             # FIXME - this isn't the right way to do it, but sqlt does not
114             # support quoting properly to be signaled about this
115             $table_name = $$table_name if ref $table_name eq 'SCALAR';
116              
117             # It's possible to have multiple DBIC sources using the same table
118             next if $tables{$table_name};
119              
120             $tables{$table_name}{source} = $source;
121             my $table = $tables{$table_name}{object} = SQL::Translator::Schema::Table->new(
122             name => $table_name,
123             type => 'TABLE',
124             );
125             foreach my $col ($source->columns)
126             {
127             # assuming column_info in dbic is the same as DBI (?)
128             # data_type is a number, column_type is text?
129             my %colinfo = (
130             name => $col,
131             size => 0,
132             is_auto_increment => 0,
133             is_foreign_key => 0,
134             is_nullable => 0,
135             %{$source->column_info($col)}
136             );
137             if ($colinfo{is_nullable}) {
138             $colinfo{default} = '' unless exists $colinfo{default};
139             }
140             my $f = $table->add_field(%colinfo)
141             || $dbicschema->throw_exception ($table->error);
142             }
143              
144             my @primary = $source->primary_columns;
145              
146             $table->primary_key(@primary) if @primary;
147              
148             my %unique_constraints = $source->unique_constraints;
149             foreach my $uniq (sort keys %unique_constraints) {
150             if (!$source->_compare_relationship_keys($unique_constraints{$uniq}, \@primary)) {
151             $table->add_constraint(
152             type => 'unique',
153             name => $uniq,
154             fields => $unique_constraints{$uniq}
155             );
156             }
157             }
158              
159             my @rels = $source->relationships();
160              
161             my %created_FK_rels;
162              
163             # global add_fk_index set in parser_args
164             my $add_fk_index = (exists $args->{add_fk_index} && ! $args->{add_fk_index}) ? 0 : 1;
165              
166             REL:
167             foreach my $rel (sort @rels) {
168              
169             my $rel_info = $source->relationship_info($rel);
170              
171             # Ignore any rel cond that isn't a straight hash
172             next unless ref $rel_info->{cond} eq 'HASH';
173              
174             my $relsource = try { $source->related_source($rel) };
175             unless ($relsource) {
176             carp "Ignoring relationship '$rel' on '$moniker' - related resultsource '$rel_info->{class}' is not registered with this schema\n";
177             next;
178             };
179              
180             # related sources might be excluded via a {sources} filter or might be views
181             next unless exists $table_monikers{$relsource->source_name};
182              
183             my $rel_table = $relsource->name;
184              
185             # FIXME - this isn't the right way to do it, but sqlt does not
186             # support quoting properly to be signaled about this
187             $rel_table = $$rel_table if ref $rel_table eq 'SCALAR';
188              
189             # Force the order of @cond to match the order of ->add_columns
190             my $idx;
191             my %other_columns_idx = map {'foreign.'.$_ => ++$idx } $relsource->columns;
192              
193             for ( keys %{$rel_info->{cond}} ) {
194             unless (exists $other_columns_idx{$_}) {
195             carp "Ignoring relationship '$rel' on '$moniker' - related resultsource '@{[ $relsource->source_name ]}' does not contain one of the specified columns: '$_'\n";
196             next REL;
197             }
198             }
199              
200             my @cond = sort { $other_columns_idx{$a} <=> $other_columns_idx{$b} } keys(%{$rel_info->{cond}});
201              
202             # Get the key information, mapping off the foreign/self markers
203             my @refkeys = map {/^\w+\.(\w+)$/} @cond;
204             my @keys = map {$rel_info->{cond}->{$_} =~ /^\w+\.(\w+)$/} @cond;
205              
206             # determine if this relationship is a self.fk => foreign.pk (i.e. belongs_to)
207             my $fk_constraint;
208              
209             #first it can be specified explicitly
210             if ( exists $rel_info->{attrs}{is_foreign_key_constraint} ) {
211             $fk_constraint = $rel_info->{attrs}{is_foreign_key_constraint};
212             }
213             # it can not be multi
214             elsif ( $rel_info->{attrs}{accessor}
215             && $rel_info->{attrs}{accessor} eq 'multi' ) {
216             $fk_constraint = 0;
217             }
218             # if indeed single, check if all self.columns are our primary keys.
219             # this is supposed to indicate a has_one/might_have...
220             # where's the introspection!!?? :)
221             else {
222             $fk_constraint = not $source->_compare_relationship_keys(\@keys, \@primary);
223             }
224              
225             my ($otherrelname, $otherrelationship) = %{ $source->reverse_relationship_info($rel) };
226              
227             my $cascade;
228             for my $c (qw/delete update/) {
229             if (exists $rel_info->{attrs}{"on_$c"}) {
230             if ($fk_constraint) {
231             $cascade->{$c} = $rel_info->{attrs}{"on_$c"};
232             }
233             elsif ( $rel_info->{attrs}{"on_$c"} ) {
234             carp "SQLT attribute 'on_$c' was supplied for relationship '$moniker/$rel', which does not appear to be a foreign constraint. "
235             . "If you are sure that SQLT must generate a constraint for this relationship, add 'is_foreign_key_constraint => 1' to the attributes.\n";
236             }
237             }
238             elsif (defined $otherrelationship and $otherrelationship->{attrs}{$c eq 'update' ? 'cascade_copy' : 'cascade_delete'}) {
239             $cascade->{$c} = 'CASCADE';
240             }
241             }
242              
243             if($rel_table) {
244             # Constraints are added only if applicable
245             next unless $fk_constraint;
246              
247             # Make sure we don't create the same foreign key constraint twice
248             my $key_test = join("\x00", sort @keys);
249             next if $created_FK_rels{$rel_table}->{$key_test};
250              
251             if (scalar(@keys)) {
252             $created_FK_rels{$rel_table}->{$key_test} = 1;
253              
254             my $is_deferrable = $rel_info->{attrs}{is_deferrable};
255              
256             # calculate dependencies: do not consider deferrable constraints and
257             # self-references for dependency calculations
258             if (! $is_deferrable and $rel_table ne $table_name) {
259             $tables{$table_name}{foreign_table_deps}{$rel_table}++;
260             }
261              
262             # trim schema before generating constraint/index names
263             (my $table_abbrev = $table_name) =~ s/ ^ [^\.]+ \. //x;
264              
265             $table->add_constraint(
266             type => 'foreign_key',
267             name => join('_', $table_abbrev, 'fk', @keys),
268             fields => \@keys,
269             reference_fields => \@refkeys,
270             reference_table => $rel_table,
271             on_delete => uc ($cascade->{delete} || ''),
272             on_update => uc ($cascade->{update} || ''),
273             (defined $is_deferrable ? ( deferrable => $is_deferrable ) : ()),
274             );
275              
276             # global parser_args add_fk_index param can be overridden on the rel def
277             my $add_fk_index_rel = (exists $rel_info->{attrs}{add_fk_index}) ? $rel_info->{attrs}{add_fk_index} : $add_fk_index;
278              
279             # Check that we do not create an index identical to the PK index
280             # (some RDBMS croak on this, and it generally doesn't make much sense)
281             # NOTE: we do not sort the key columns because the order of
282             # columns is important for indexes and two indexes with the
283             # same cols but different order are allowed and sometimes
284             # needed
285             next if join("\x00", @keys) eq join("\x00", @primary);
286              
287             if ($add_fk_index_rel) {
288             (my $idx_name = $table_name) =~ s/ ^ [^\.]+ \. //x;
289             my $index = $table->add_index(
290             name => join('_', $table_abbrev, 'idx', @keys),
291             fields => \@keys,
292             type => 'NORMAL',
293             );
294             }
295             }
296             }
297             }
298              
299             }
300              
301             # attach the tables to the schema in dependency order
302             my $dependencies = {
303             map { $_ => _resolve_deps ($_, \%tables) } (keys %tables)
304             };
305              
306             for my $table (sort
307             {
308             keys %{$dependencies->{$a} || {} } <=> keys %{ $dependencies->{$b} || {} }
309             ||
310             $a cmp $b
311             }
312             (keys %tables)
313             ) {
314             $schema->add_table ($tables{$table}{object});
315             $tables{$table}{source} -> _invoke_sqlt_deploy_hook( $tables{$table}{object} );
316              
317             # the hook might have already removed the table
318             if ($schema->get_table($table) && $table =~ /^ \s* \( \s* SELECT \s+/ix) {
319             carp <<'EOW';
320              
321             Custom SQL through ->name(\'( SELECT ...') is DEPRECATED, for more details see
322             "Arbitrary SQL through a custom ResultSource" in DBIx::Class::Manual::Cookbook
323             or http://search.cpan.org/dist/DBIx-Class/lib/DBIx/Class/Manual/Cookbook.pod
324              
325             EOW
326              
327             # remove the table as there is no way someone might want to
328             # actually deploy this
329             $schema->drop_table ($table);
330             }
331             }
332              
333             my %views;
334             my @views = map { $dbicschema->source($_) } keys %view_monikers;
335              
336             my $view_dependencies = {
337             map {
338             $_ => _resolve_deps( $dbicschema->source($_), \%view_monikers )
339             } ( keys %view_monikers )
340             };
341              
342             my @view_sources =
343             sort {
344             keys %{ $view_dependencies->{ $a->source_name } || {} } <=>
345             keys %{ $view_dependencies->{ $b->source_name } || {} }
346             || $a->source_name cmp $b->source_name
347             }
348             map { $dbicschema->source($_) }
349             keys %view_monikers;
350              
351             foreach my $source (@view_sources)
352             {
353             my $view_name = $source->name;
354              
355             # FIXME - this isn't the right way to do it, but sqlt does not
356             # support quoting properly to be signaled about this
357             $view_name = $$view_name if ref $view_name eq 'SCALAR';
358              
359             # Skip custom query sources
360             next if ref $view_name;
361              
362             # Its possible to have multiple DBIC source using same table
363             next if $views{$view_name}++;
364              
365             $dbicschema->throw_exception ("view $view_name is missing a view_definition")
366             unless $source->view_definition;
367              
368             my $view = $schema->add_view (
369             name => $view_name,
370             fields => [ $source->columns ],
371             $source->view_definition ? ( 'sql' => $source->view_definition ) : ()
372             ) || $dbicschema->throw_exception ($schema->error);
373              
374             $source->_invoke_sqlt_deploy_hook($view);
375             }
376              
377              
378             if ($dbicschema->can('sqlt_deploy_hook')) {
379             $dbicschema->sqlt_deploy_hook($schema);
380             }
381              
382             return 1;
383             }
384              
385             #
386             # Quick and dirty dependency graph calculator
387             #
388             sub _resolve_deps {
389             my ( $question, $answers, $seen ) = @_;
390             my $ret = {};
391             $seen ||= {};
392             my @deps;
393              
394             # copy and bump all deps by one (so we can reconstruct the chain)
395             my %seen = map { $_ => $seen->{$_} + 1 } ( keys %$seen );
396             if ( blessed($question)
397             && $question->isa('DBIx::Class::ResultSource::View') )
398             {
399             $seen{ $question->result_class } = 1;
400             @deps = keys %{ $question->{deploy_depends_on} };
401             }
402             else {
403             $seen{$question} = 1;
404             @deps = keys %{ $answers->{$question}{foreign_table_deps} };
405             }
406              
407             for my $dep (@deps) {
408             if ( $seen->{$dep} ) {
409             return {};
410             }
411             my $next_dep;
412              
413             if ( blessed($question)
414             && $question->isa('DBIx::Class::ResultSource::View') )
415             {
416             no warnings 'uninitialized';
417             my ($next_dep_source_name) =
418             grep {
419             $question->schema->source($_)->result_class eq $dep
420             && !( $question->schema->source($_)
421             ->isa('DBIx::Class::ResultSource::Table') )
422             } @{ [ $question->schema->sources ] };
423             return {} unless $next_dep_source_name;
424             $next_dep = $question->schema->source($next_dep_source_name);
425             }
426             else {
427             $next_dep = $dep;
428             }
429             my $subdeps = _resolve_deps( $next_dep, $answers, \%seen );
430             $ret->{$_} += $subdeps->{$_} for ( keys %$subdeps );
431             ++$ret->{$dep};
432             }
433             return $ret;
434             }
435              
436             1;
437              
438             =head1 NAME
439              
440             SQL::Translator::Parser::DBIx::Class - Create a SQL::Translator schema
441             from a DBIx::Class::Schema instance
442              
443             =head1 SYNOPSIS
444              
445             ## Via DBIx::Class
446             use MyApp::Schema;
447             my $schema = MyApp::Schema->connect("dbi:SQLite:something.db");
448             $schema->create_ddl_dir();
449             ## or
450             $schema->deploy();
451              
452             ## Standalone
453             use MyApp::Schema;
454             use SQL::Translator;
455              
456             my $schema = MyApp::Schema->connect;
457             my $trans = SQL::Translator->new (
458             parser => 'SQL::Translator::Parser::DBIx::Class',
459             parser_args => {
460             dbic_schema => $schema,
461             add_fk_index => 0,
462             sources => [qw/
463             Artist
464             CD
465             /],
466             },
467             producer => 'SQLite',
468             ) or die SQL::Translator->error;
469             my $out = $trans->translate() or die $trans->error;
470              
471             =head1 DESCRIPTION
472              
473             This class requires L installed to work.
474              
475             C reads a DBIx::Class schema,
476             interrogates the columns, and stuffs it all in an $sqlt_schema object.
477              
478             Its primary use is in deploying database layouts described as a set
479             of L classes, to a database. To do this, see
480             L.
481              
482             This can also be achieved by having DBIx::Class export the schema as a
483             set of SQL files ready for import into your database, or passed to
484             other machines that need to have your application installed but don't
485             have SQL::Translator installed. To do this see
486             L.
487              
488             =head1 PARSER OPTIONS
489              
490             =head2 dbic_schema
491              
492             The DBIx::Class schema (either an instance or a class name) to be parsed.
493             This argument is in fact optional - instead one can supply it later at
494             translation time as an argument to L. In
495             other words both of the following invocations are valid and will produce
496             conceptually identical output:
497              
498             my $yaml = SQL::Translator->new(
499             parser => 'SQL::Translator::Parser::DBIx::Class',
500             parser_args => {
501             dbic_schema => $schema,
502             },
503             producer => 'SQL::Translator::Producer::YAML',
504             )->translate;
505              
506             my $yaml = SQL::Translator->new(
507             parser => 'SQL::Translator::Parser::DBIx::Class',
508             producer => 'SQL::Translator::Producer::YAML',
509             )->translate(data => $schema);
510              
511             =head2 add_fk_index
512              
513             Create an index for each foreign key.
514             Enabled by default, as having indexed foreign key columns is normally the
515             sensible thing to do.
516              
517             =head2 sources
518              
519             =over 4
520              
521             =item Arguments: \@class_names
522              
523             =back
524              
525             Limit the amount of parsed sources by supplying an explicit list of source names.
526              
527             =head1 SEE ALSO
528              
529             L, L
530              
531             =head1 FURTHER QUESTIONS?
532              
533             Check the list of L.
534              
535             =head1 COPYRIGHT AND LICENSE
536              
537             This module is free software L
538             by the L. You can
539             redistribute it and/or modify it under the same terms as the
540             L.