nearly there with conversion of source_tree
Arthur Axel 'fREW' Schmidt [Sun, 13 May 2012 19:08:11 +0000 (14:08 -0500)]
lib/DBIx/Class/Schema.pm
t/schema/source-tree.t [new file with mode: 0644]

index 2550cb5..f7dc646 100644 (file)
@@ -1536,6 +1536,150 @@ sub compose_connection {
   return $schema;
 }
 
+sub source_tree {
+   my $self = shift;
+   my $args = shift;
+
+   my %limit_sources = do {
+      my $l = $args->{limit_sources};
+      my $ref = ref $l;
+
+      $ref eq 'HASH' ?
+         %$l
+         : map { $_ => 1} @$l
+   };
+
+   my %table_monikers =
+      map { $_ => 1 }
+      grep { $self->source($_)->isa('DBIx::Class::ResultSource::Table') }
+      grep { !$limit_sources{$_} }
+      $self->sources;
+
+   my %tables;
+   foreach my $moniker (sort keys %table_monikers) {
+       my $source = $self->source($moniker);
+       my $table_name = $source->name;
+
+       # FIXME - this isn't the right way to do it, but sqlt does not
+       # support quoting properly to be signaled about this
+       $table_name = $$table_name if ref $table_name eq 'SCALAR';
+
+       # It's possible to have multiple DBIC sources using the same table
+       next if $tables{$table_name};
+
+       $tables{$table_name}{source} = $source;
+
+       foreach my $rel (sort $source->relationships) {
+           my $rel_info = $source->relationship_info($rel);
+
+           # FIXME - we can probably do better, at least check if it's a
+           #         coderef that returns a plain hash w/ fk-fk
+           # Ignore any rel cond that isn't a straight hash
+           next unless ref $rel_info->{cond} eq 'HASH';
+
+           my $relsource = try { $source->related_source($rel) };
+           next unless $relsource;
+
+           # related sources might be excluded via a {sources} filter or might be views
+           next unless exists $table_monikers{$relsource->source_name};
+
+           my $rel_table = $relsource->name;
+
+           # FIXME - this isn't the right way to do it, but sqlt does not
+           # support quoting properly to be signaled about this
+           $rel_table = $$rel_table if ref $rel_table eq 'SCALAR';
+
+           # Force the order of @cond to match the order of ->add_columns
+           my $idx;
+           my %other_columns_idx = map {'foreign.'.$_ => ++$idx } $relsource->columns;
+           my @cond = sort { $other_columns_idx{$a} cmp $other_columns_idx{$b} } keys(%{$rel_info->{cond}});
+
+           # Get the key information, mapping off the foreign/self markers
+           my @keys = map {$rel_info->{cond}->{$_} =~ /^\w+\.(\w+)$/} @cond;
+
+           # determine if this relationship is a self.fk => foreign.pk (i.e. belongs_to)
+           my $fk_constraint;
+
+           #first it can be specified explicitly
+           if ( exists $rel_info->{attrs}{is_foreign_key_constraint} ) {
+               $fk_constraint = $rel_info->{attrs}{is_foreign_key_constraint};
+           }
+           # it can not be multi
+           elsif ( $rel_info->{attrs}{accessor}
+                   && $rel_info->{attrs}{accessor} eq 'multi' ) {
+               $fk_constraint = 0;
+           }
+           # if indeed single, check if all self.columns are our primary keys.
+           # this is supposed to indicate a has_one/might_have...
+           # where's the introspection!!?? :)
+           else {
+               $fk_constraint = not $source->_compare_relationship_keys(
+                  \@keys, [$source->primary_columns]);
+           }
+
+           $tables{$table_name}{foreign_table_deps}{$rel_table}++
+              if $fk_constraint && @keys
+                 # calculate dependencies: do not consider deferrable constraints and
+                 # self-references for dependency calculations
+                 && !$rel_info->{attrs}{is_deferrable}
+                 && $rel_table && $rel_table ne $table_name
+
+       }
+   }
+
+   return {
+     map { $_ => $self->_resolve_deps ($_, \%tables) } (keys %tables)
+   }
+}
+
+sub _resolve_deps {
+    my ( $self, $question, $answers, $seen ) = @_;
+    my $ret = {};
+    $seen ||= {};
+    my @deps;
+
+    # copy and bump all deps by one (so we can reconstruct the chain)
+    my %seen = map { $_ => $seen->{$_} + 1 } ( keys %$seen );
+    if ( blessed($question)
+        && $question->isa('DBIx::Class::ResultSource::View') )
+    {
+        $seen{ $question->result_class } = 1;
+        @deps = keys %{ $question->{deploy_depends_on} };
+    }
+    else {
+        $seen{$question} = 1;
+        @deps = keys %{ $answers->{$question}{foreign_table_deps} };
+    }
+
+    for my $dep (@deps) {
+        if ( $seen->{$dep} ) {
+            return {};
+        }
+        my $next_dep;
+
+        if ( blessed($question)
+            && $question->isa('DBIx::Class::ResultSource::View') )
+        {
+            no warnings 'uninitialized';
+            my ($next_dep_source_name) =
+              grep {
+                $question->schema->source($_)->result_class eq $dep
+                  && !( $question->schema->source($_)
+                    ->isa('DBIx::Class::ResultSource::Table') )
+              } @{ [ $question->schema->sources ] };
+            return {} unless $next_dep_source_name;
+            $next_dep = $question->schema->source($next_dep_source_name);
+        }
+        else {
+            $next_dep = $dep;
+        }
+        my $subdeps = $self->_resolve_deps( $next_dep, $answers, \%seen );
+        $ret->{$_} += $subdeps->{$_} for ( keys %$subdeps );
+        ++$ret->{$dep};
+    }
+    return $ret;
+}
+
 1;
 
 =head1 AUTHORS
diff --git a/t/schema/source-tree.t b/t/schema/source-tree.t
new file mode 100644 (file)
index 0000000..5fd1bb8
--- /dev/null
@@ -0,0 +1,323 @@
+use strict;
+use warnings;
+use Test::More;
+
+use lib qw(t/lib);
+use DBICTest;
+
+my $schema = DBICTest->init_schema( no_connect => 1, no_deploy => 1 );
+
+is_deeply($schema->source_tree, {
+  "  ( SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year\n  FROM artist a\n  JOIN cd ON cd.artist = a.artistid\n  WHERE cd.year = ?)\n" => {},
+  artist => {},
+  artist_undirected_map => {
+    artist => 1
+  },
+  artwork_to_artist => {
+    artist => 1,
+    cd => 1,
+    cd_artwork => 1,
+    genre => 1,
+    track => 1
+  },
+  bindtype_test => {},
+  bookmark => {
+    link => 1
+  },
+  books => {
+    owners => 1
+  },
+  cd => {
+    genre => 1,
+    track => 1
+  },
+  cd_artwork => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  cd_to_producer => {
+    cd => 1,
+    genre => 1,
+    producer => 1,
+    track => 1
+  },
+  collection => {},
+  collection_object => {
+    collection => 1,
+    typed_object => 1
+  },
+  dummy => {},
+  employee => {
+    encoded => 1
+  },
+  encoded => {},
+  event => {},
+  forceforeign => {
+    artist => 1
+  },
+  fourkeys => {},
+  fourkeys_to_twokeys => {
+    artist => 1,
+    cd => 1,
+    fourkeys => 1,
+    genre => 1,
+    track => 1,
+    twokeys => 1
+  },
+  genre => {},
+  images => {
+    cd => 1,
+    cd_artwork => 1,
+    genre => 1,
+    track => 1
+  },
+  liner_notes => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  link => {},
+  lyric_versions => {
+    cd => 1,
+    lyrics => 1,
+    track => 1
+  },
+  lyrics => {
+    cd => 1,
+    track => 1
+  },
+  money_test => {},
+  noprimarykey => {},
+  onekey => {},
+  owners => {},
+  producer => {},
+  self_ref => {},
+  self_ref_alias => {
+    self_ref => 1
+  },
+  sequence_test => {},
+  serialized => {},
+  tags => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  timestamp_primary_key_test => {},
+  track => {
+    cd => 1
+  },
+  treelike => {},
+  twokeys => {
+    artist => 1,
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  twokeytreelike => {},
+  typed_object => {},
+}, 'got correct source tree');
+
+is_deeply($schema->source_tree({ limit_sources => ['TwoKeys'] }), {
+  "  ( SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year\n  FROM artist a\n  JOIN cd ON cd.artist = a.artistid\n  WHERE cd.year = ?)\n" => {},
+  artist => {},
+  artist_undirected_map => {
+    artist => 1
+  },
+  artwork_to_artist => {
+    artist => 1,
+    cd => 1,
+    cd_artwork => 1,
+    genre => 1,
+    track => 1
+  },
+  bindtype_test => {},
+  bookmark => {
+    link => 1
+  },
+  books => {
+    owners => 1
+  },
+  cd => {
+    genre => 1,
+    track => 1
+  },
+  cd_artwork => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  cd_to_producer => {
+    cd => 1,
+    genre => 1,
+    producer => 1,
+    track => 1
+  },
+  collection => {},
+  collection_object => {
+    collection => 1,
+    typed_object => 1
+  },
+  dummy => {},
+  employee => {
+    encoded => 1
+  },
+  encoded => {},
+  event => {},
+  forceforeign => {
+    artist => 1
+  },
+  fourkeys => {},
+  fourkeys_to_twokeys => {
+    fourkeys => 1
+  },
+  genre => {},
+  images => {
+    cd => 1,
+    cd_artwork => 1,
+    genre => 1,
+    track => 1
+  },
+  liner_notes => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  link => {},
+  lyric_versions => {
+    cd => 1,
+    lyrics => 1,
+    track => 1
+  },
+  lyrics => {
+    cd => 1,
+    track => 1
+  },
+  money_test => {},
+  noprimarykey => {},
+  onekey => {},
+  owners => {},
+  producer => {},
+  self_ref => {},
+  self_ref_alias => {
+    self_ref => 1
+  },
+  sequence_test => {},
+  serialized => {},
+  tags => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  timestamp_primary_key_test => {},
+  track => {
+    cd => 1
+  },
+  treelike => {},
+  twokeytreelike => {},
+  typed_object => {}
+}, 'got correct source tree with limit_sources => [ ... ]');
+
+is_deeply($schema->source_tree({ limit_sources => { TwoKeys => 1 } }), {
+  "  ( SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year\n  FROM artist a\n  JOIN cd ON cd.artist = a.artistid\n  WHERE cd.year = ?)\n" => {},
+  artist => {},
+  artist_undirected_map => {
+    artist => 1
+  },
+  artwork_to_artist => {
+    artist => 1,
+    cd => 1,
+    cd_artwork => 1,
+    genre => 1,
+    track => 1
+  },
+  bindtype_test => {},
+  bookmark => {
+    link => 1
+  },
+  books => {
+    owners => 1
+  },
+  cd => {
+    genre => 1,
+    track => 1
+  },
+  cd_artwork => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  cd_to_producer => {
+    cd => 1,
+    genre => 1,
+    producer => 1,
+    track => 1
+  },
+  collection => {},
+  collection_object => {
+    collection => 1,
+    typed_object => 1
+  },
+  dummy => {},
+  employee => {
+    encoded => 1
+  },
+  encoded => {},
+  event => {},
+  forceforeign => {
+    artist => 1
+  },
+  fourkeys => {},
+  fourkeys_to_twokeys => {
+    fourkeys => 1
+  },
+  genre => {},
+  images => {
+    cd => 1,
+    cd_artwork => 1,
+    genre => 1,
+    track => 1
+  },
+  liner_notes => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  link => {},
+  lyric_versions => {
+    cd => 1,
+    lyrics => 1,
+    track => 1
+  },
+  lyrics => {
+    cd => 1,
+    track => 1
+  },
+  money_test => {},
+  noprimarykey => {},
+  onekey => {},
+  owners => {},
+  producer => {},
+  self_ref => {},
+  self_ref_alias => {
+    self_ref => 1
+  },
+  sequence_test => {},
+  serialized => {},
+  tags => {
+    cd => 1,
+    genre => 1,
+    track => 1
+  },
+  timestamp_primary_key_test => {},
+  track => {
+    cd => 1
+  },
+  treelike => {},
+  twokeytreelike => {},
+  typed_object => {}
+}, 'got correct source tree with limit_sources => { ... }');
+
+# We probably also want a "collapsed" tree
+
+done_testing;