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