new dev release
[dbsrgits/DBIx-Class-Schema-Loader.git] / lib / DBIx / Class / Schema / Loader / Base.pm
index 516a402..ba188d8 100644 (file)
@@ -2,7 +2,7 @@ package DBIx::Class::Schema::Loader::Base;
 
 use strict;
 use warnings;
-use base qw/Class::Accessor::Fast Class::C3::Componentised/;
+use base qw/Class::Accessor::Grouped Class::C3::Componentised/;
 use Class::C3;
 use Carp::Clan qw/^DBIx::Class/;
 use DBIx::Class::Schema::Loader::RelBuilder;
@@ -16,9 +16,9 @@ use File::Temp qw//;
 use Class::Unload;
 require DBIx::Class;
 
-our $VERSION = '0.04999_12';
+our $VERSION = '0.04999_14';
 
-__PACKAGE__->mk_ro_accessors(qw/
+__PACKAGE__->mk_group_ro_accessors('simple', qw/
                                 schema
                                 schema_class
 
@@ -30,6 +30,7 @@ __PACKAGE__->mk_ro_accessors(qw/
                                 components
                                 resultset_components
                                 skip_relationships
+                                skip_load_external
                                 moniker_map
                                 inflect_singular
                                 inflect_plural
@@ -37,12 +38,13 @@ __PACKAGE__->mk_ro_accessors(qw/
                                 dump_directory
                                 dump_overwrite
                                 really_erase_my_files
-                                use_namespaces
-                                result_namespace
                                 resultset_namespace
                                 default_resultset_class
                                 schema_base_class
                                 result_base_class
+                               overwrite_modifications
+
+                                relationship_attrs
 
                                 db_schema
                                 _tables
@@ -51,12 +53,18 @@ __PACKAGE__->mk_ro_accessors(qw/
                                 monikers
                                 dynamic
                                 naming
-                             /);
+/);
+
 
-__PACKAGE__->mk_accessors(qw/
+__PACKAGE__->mk_group_accessors('simple', qw/
                                 version_to_dump
                                 schema_version_to_dump
                                 _upgrading_from
+                                _upgrading_from_load_classes
+                                _downgrading_to_load_classes
+                                _rewriting_result_namespace
+                                use_namespaces
+                                result_namespace
 /);
 
 =head1 NAME
@@ -82,6 +90,11 @@ L<DBIx::Class::Schema::Loader/loader_options>.  Available constructor options ar
 Skip setting up relationships.  The default is to attempt the loading
 of relationships.
 
+=head2 skip_load_external
+
+Skip loading of other classes in @INC. The default is to merge all other classes
+with the same name found in @INC into the schema file we are creating.
+
 =head2 naming
 
 Static schemas (ones dumped to disk) will, by default, use the new-style 0.05XXX
@@ -145,6 +158,26 @@ next major version upgrade:
 
     __PACKAGE__->naming('v5');
 
+=head2 relationship_attrs
+
+Hashref of attributes to pass to each generated relationship, listed
+by type.  Also supports relationship type 'all', containing options to
+pass to all generated relationships.  Attributes set for more specific
+relationship types override those set in 'all'.
+
+For example:
+
+  relationship_attrs => {
+    all      => { cascade_delete => 0 },
+    has_many => { cascade_delete => 1 },
+  },
+
+will set the C<cascade_delete> option to 0 for all generated relationships,
+except for C<has_many>, which will have cascade_delete as 1.
+
+NOTE: this option is not supported if v4 backward-compatible naming is
+set either globally (naming => 'v4') or just for relationships.
+
 =head2 debug
 
 If set to true, each constructive L<DBIx::Class> statement the loader
@@ -183,7 +216,7 @@ the chunks back together.  Examples:
     ---------------------------
     luser       | Luser
     luser_group | LuserGroup
-    luser-opts  | LuserOpts
+    luser-opts  | LuserOpt
 
 =head2 inflect_plural
 
@@ -233,6 +266,9 @@ C<components> list if this option is set.
 
 =head2 use_namespaces
 
+This is now the default, to go back to L<DBIx::Class::Schema/load_classes> pass
+a C<0>.
+
 Generate result class names suitable for
 L<DBIx::Class::Schema/load_namespaces> and call that instead of
 L<DBIx::Class::Schema/load_classes>. When using this option you can also
@@ -284,6 +320,19 @@ You should really be using version control on your schema classes (and all
 of the rest of your code for that matter).  Don't blame me if a bug in this
 code wipes something out when it shouldn't have, you've been warned.
 
+=head2 overwrite_modifications
+
+Default false.  If false, when updating existing files, Loader will
+refuse to modify any Loader-generated code that has been modified
+since its last run (as determined by the checksum Loader put in its
+comment lines).
+
+If true, Loader will discard any manual modifications that have been
+made to Loader-generated code.
+
+Again, you should be using version control on your schema classes.  Be
+careful with this option.
+
 =head1 METHODS
 
 None of these methods are intended for direct invocation by regular
@@ -369,6 +418,8 @@ sub new {
 
     $self->_check_back_compat;
 
+    $self->use_namespaces(1) unless defined $self->use_namespaces;
+
     $self;
 }
 
@@ -388,6 +439,8 @@ Dynamic schema detected, will run in 0.04006 mode.
 Set the 'naming' attribute or the SCHEMA_LOADER_BACKCOMPAT environment variable
 to disable this warning.
 
+Also consider setting 'use_namespaces => 1' if/when upgrading.
+
 See perldoc DBIx::Class::Schema::Loader::Manual::UpgradingFromV4 for more
 details.
 EOF
@@ -399,6 +452,13 @@ EOF
         $self->naming->{relationships} ||= 'v4';
         $self->naming->{monikers}      ||= 'v4';
 
+        if ($self->use_namespaces) {
+            $self->_upgrading_from_load_classes(1);
+        }
+        else {
+            $self->use_namespaces(0);
+        }
+
         return;
     }
 
@@ -409,15 +469,56 @@ EOF
     open(my $fh, '<', $filename)
         or croak "Cannot open '$filename' for reading: $!";
 
+    my $load_classes     = 0;
+    my $result_namespace = '';
+
     while (<$fh>) {
-        if (/^# Created by DBIx::Class::Schema::Loader v((\d+)\.(\d+))/) {
-            my $real_ver = $1;
+        if (/^__PACKAGE__->load_classes;/) {
+            $load_classes = 1;
+        } elsif (/result_namespace => '([^']+)'/) {
+            $result_namespace = $1;
+        } elsif (my ($real_ver) =
+                /^# Created by DBIx::Class::Schema::Loader v(\d+\.\d+)/) {
+
+            if ($load_classes && (not defined $self->use_namespaces)) {
+                warn <<"EOF"  unless $ENV{SCHEMA_LOADER_BACKCOMPAT};
+
+'load_classes;' static schema detected, turning off 'use_namespaces'.
+
+Set the 'use_namespaces' attribute or the SCHEMA_LOADER_BACKCOMPAT environment
+variable to disable this warning.
+
+See perldoc DBIx::Class::Schema::Loader::Manual::UpgradingFromV4 for more
+details.
+EOF
+                $self->use_namespaces(0);
+            }
+            elsif ($load_classes && $self->use_namespaces) {
+                $self->_upgrading_from_load_classes(1);
+            }
+            elsif ((not $load_classes) && defined $self->use_namespaces
+                                       && (not $self->use_namespaces)) {
+                $self->_downgrading_to_load_classes(
+                    $result_namespace || 'Result'
+                );
+            }
+            elsif ((not defined $self->use_namespaces)
+                   || $self->use_namespaces) {
+                if (not $self->result_namespace) {
+                    $self->result_namespace($result_namespace || 'Result');
+                }
+                elsif ($result_namespace ne $self->result_namespace) {
+                    $self->_rewriting_result_namespace(
+                        $result_namespace || 'Result'
+                    );
+                }
+            }
 
             # XXX when we go past .0 this will need fixing
             my ($v) = $real_ver =~ /([1-9])/;
             $v = "v$v";
 
-            last if $v eq CURRENT_V;
+            last if $v eq CURRENT_V || $real_ver =~ /^0\.\d\d999/;
 
             if (not %{ $self->naming }) {
                 warn <<"EOF" unless $ENV{SCHEMA_LOADER_BACKCOMPAT};
@@ -433,6 +534,7 @@ EOF
             }
             else {
                 $self->_upgrading_from($v);
+                last;
             }
 
             $self->naming->{relationships} ||= $v;
@@ -453,7 +555,7 @@ sub _find_file_in_inc {
         my $fullpath = File::Spec->catfile($prefix, $file);
         return $fullpath if -f $fullpath
             and Cwd::abs_path($fullpath) ne
-                Cwd::abs_path(File::Spec->catfile($self->dump_directory, $file)) || '';
+               (Cwd::abs_path(File::Spec->catfile($self->dump_directory, $file)) || '');
     }
 
     return;
@@ -475,16 +577,43 @@ sub _find_class_in_inc {
     return $self->_find_file_in_inc($self->_class_path($class));
 }
 
+sub _rewriting {
+    my $self = shift;
+
+    return $self->_upgrading_from
+        || $self->_upgrading_from_load_classes
+        || $self->_downgrading_to_load_classes
+        || $self->_rewriting_result_namespace
+    ;
+}
+
+sub _rewrite_old_classnames {
+    my ($self, $code) = @_;
+
+    return $code unless $self->_rewriting;
+
+    my %old_classes = reverse %{ $self->_upgrading_classes };
+
+    my $re = join '|', keys %old_classes;
+    $re = qr/\b($re)\b/;
+
+    $code =~ s/$re/$old_classes{$1} || $1/eg;
+
+    return $code;
+}
+
 sub _load_external {
     my ($self, $class) = @_;
 
+    return if $self->{skip_load_external};
+
     # so that we don't load our own classes, under any circumstances
     local *INC = [ grep $_ ne $self->dump_directory, @INC ];
 
     my $real_inc_path = $self->_find_class_in_inc($class);
 
     my $old_class = $self->_upgrading_classes->{$class}
-        if $self->_upgrading_from;
+        if $self->_rewriting;
 
     my $old_real_inc_path = $self->_find_class_in_inc($old_class)
         if $old_class && $old_class ne $class;
@@ -498,32 +627,36 @@ sub _load_external {
 
         open(my $fh, '<', $real_inc_path)
             or croak "Failed to open '$real_inc_path' for reading: $!";
+        my $code = do { local $/; <$fh> };
+        close($fh)
+            or croak "Failed to close $real_inc_path: $!";
+        $code = $self->_rewrite_old_classnames($code);
+
+        if ($self->dynamic) { # load the class too
+            # kill redefined warnings
+            my $warn_handler = $SIG{__WARN__} || sub { warn @_ };
+            local $SIG{__WARN__} = sub {
+                $warn_handler->(@_)
+                    unless $_[0] =~ /^Subroutine \S+ redefined/;
+            };
+            eval $code;
+            die $@ if $@;
+        }
+
         $self->_ext_stmt($class,
           qq|# These lines were loaded from '$real_inc_path' found in \@INC.\n|
          .qq|# They are now part of the custom portion of this file\n|
          .qq|# for you to hand-edit.  If you do not either delete\n|
          .qq|# this section or remove that file from \@INC, this section\n|
          .qq|# will be repeated redundantly when you re-create this\n|
-         .qq|# file again via Loader!\n|
+         .qq|# file again via Loader!  See skip_load_external to disable\n|
+         .qq|# this feature.\n|
         );
-        while(<$fh>) {
-            chomp;
-            $self->_ext_stmt($class, $_);
-        }
+        chomp $code;
+        $self->_ext_stmt($class, $code);
         $self->_ext_stmt($class,
             qq|# End of lines loaded from '$real_inc_path' |
         );
-        close($fh)
-            or croak "Failed to close $real_inc_path: $!";
-
-        if ($self->dynamic) { # load the class too
-            # kill redefined warnings
-            local $SIG{__WARN__} = sub {
-                warn @_ unless $_[0] =~ /^Subroutine \S+ redefined/;
-            };
-            do $real_inc_path;
-            die $@ if $@;
-        }
     }
 
     if ($old_real_inc_path) {
@@ -531,10 +664,17 @@ sub _load_external {
             or croak "Failed to open '$old_real_inc_path' for reading: $!";
         $self->_ext_stmt($class, <<"EOF");
 
-# These lines were loaded from '$old_real_inc_path', based on the Result class
-# name that would have been created by an 0.04006 version of the Loader. For a
-# static schema, this happens only once during upgrade.
+# These lines were loaded from '$old_real_inc_path',
+# based on the Result class name that would have been created by an 0.04006
+# version of the Loader. For a static schema, this happens only once during
+# upgrade. See skip_load_external to disable this feature.
 EOF
+
+        my $code = do {
+            local ($/, @ARGV) = (undef, $old_real_inc_path); <>
+        };
+        $code = $self->_rewrite_old_classnames($code);
+
         if ($self->dynamic) {
             warn <<"EOF";
 
@@ -545,27 +685,20 @@ been used by an 0.04006 version of the Loader.
 new name of the Result.
 EOF
             # kill redefined warnings
+            my $warn_handler = $SIG{__WARN__} || sub { warn @_ };
             local $SIG{__WARN__} = sub {
-                warn @_ unless $_[0] =~ /^Subroutine \S+ redefined/;
-            };
-            my $code = do {
-                local ($/, @ARGV) = (undef, $old_real_inc_path); <>
+                $warn_handler->(@_)
+                    unless $_[0] =~ /^Subroutine \S+ redefined/;
             };
-            $code =~ s/$old_class/$class/g;
             eval $code;
             die $@ if $@;
         }
 
-        while(<$fh>) {
-            chomp;
-            $self->_ext_stmt($class, $_);
-        }
+        chomp $code;
+        $self->_ext_stmt($class, $code);
         $self->_ext_stmt($class,
             qq|# End of lines loaded from '$old_real_inc_path' |
         );
-
-        close($fh)
-            or croak "Failed to close $old_real_inc_path: $!";
     }
 }
 
@@ -629,8 +762,11 @@ sub _relbuilder {
             );
     }
 
-    $self->{relbuilder} ||= DBIx::Class::Schema::Loader::RelBuilder->new(
-        $self->schema, $self->inflect_plural, $self->inflect_singular
+    $self->{relbuilder} ||= DBIx::Class::Schema::Loader::RelBuilder->new (
+        $self->schema,
+        $self->inflect_plural,
+        $self->inflect_singular,
+        $self->relationship_attrs,
     );
 }
 
@@ -736,10 +872,11 @@ sub _reload_class {
     delete $INC{ $class_path };
 
 # kill redefined warnings
+    my $warn_handler = $SIG{__WARN__} || sub { warn @_ };
     local $SIG{__WARN__} = sub {
-        warn @_ unless $_[0] =~ /^Subroutine \S+ redefined/;
+        $warn_handler->(@_)
+            unless $_[0] =~ /^Subroutine \S+ redefined/;
     };
-
     eval "require $class;";
 }
 
@@ -803,7 +940,7 @@ sub _dump_to_dir {
 
     {
         local $self->{version_to_dump} = $self->schema_version_to_dump;
-        $self->_write_classfile($schema_class, $schema_text);
+        $self->_write_classfile($schema_class, $schema_text, 1);
     }
 
     my $result_base_class = $self->result_base_class || 'DBIx::Class::Core';
@@ -819,6 +956,23 @@ sub _dump_to_dir {
         $self->_write_classfile($src_class, $src_text);
     }
 
+    # remove Result dir if downgrading from use_namespaces, and there are no
+    # files left.
+    if (my $result_ns = $self->_downgrading_to_load_classes
+                        || $self->_rewriting_result_namespace) {
+        my $result_namespace = $self->_result_namespace(
+            $schema_class,
+            $result_ns,
+        );
+
+        (my $result_dir = $result_namespace) =~ s{::}{/}g;
+        $result_dir = $self->dump_directory . '/' . $result_dir;
+
+        unless (my @files = glob "$result_dir/*") {
+            rmdir $result_dir;
+        }
+    }
+
     warn "Schema dump completed.\n" unless $self->{dynamic} or $self->{quiet};
 
 }
@@ -832,7 +986,7 @@ sub _sig_comment {
 }
 
 sub _write_classfile {
-    my ($self, $class, $text) = @_;
+    my ($self, $class, $text, $is_schema) = @_;
 
     my $filename = $self->_get_dump_filename($class);
     $self->_ensure_dump_subdirs($class);
@@ -845,27 +999,25 @@ sub _write_classfile {
 
     my ($custom_content, $old_md5, $old_ver, $old_ts) = $self->_get_custom_content($class, $filename);
 
-    if ($self->_upgrading_from) {
-        my $old_class = $self->_upgrading_classes->{$class};
-
-        if ($old_class && ($old_class ne $class)) {
-            my $old_filename = $self->_get_dump_filename($old_class);
-
-            my ($old_custom_content) = $self->_get_custom_content(
-                $old_class, $old_filename, 0 # do not add default comment
-            );
+    if (my $old_class = $self->_upgrading_classes->{$class}) {
+        my $old_filename = $self->_get_dump_filename($old_class);
 
-            $old_custom_content =~ s/\n\n# You can replace.*\n1;\n//;
+        my ($old_custom_content) = $self->_get_custom_content(
+            $old_class, $old_filename, 0 # do not add default comment
+        );
 
-            if ($old_custom_content) {
-                $custom_content =
-                    "\n" . $old_custom_content . "\n" . $custom_content;
-            }
+        $old_custom_content =~ s/\n\n# You can replace.*\n1;\n//;
 
-            unlink $old_filename;
+        if ($old_custom_content) {
+            $custom_content =
+                "\n" . $old_custom_content . "\n" . $custom_content;
         }
+
+        unlink $old_filename;
     }
 
+    $custom_content = $self->_rewrite_old_classnames($custom_content);
+
     $text .= qq|$_\n|
         for @{$self->{_dump_storage}->{$class} || []};
 
@@ -877,7 +1029,7 @@ sub _write_classfile {
       
 
       if (Digest::MD5::md5_base64($compare_to) eq $old_md5) {
-        return;
+        return unless $self->_upgrading_from && $is_schema;
       }
     }
 
@@ -933,8 +1085,8 @@ sub _get_custom_content {
             ($ver, $ts) = $buffer =~ m/# Created by DBIx::Class::Schema::Loader v(.*?) @ (.*?)$/s;
 
             $buffer .= $line;
-            croak "Checksum mismatch in '$filename'"
-                if Digest::MD5::md5_base64($buffer) ne $md5;
+            croak "Checksum mismatch in '$filename', the auto-generated part of the file has been modified outside of this loader.  Aborting.\nIf you want to overwrite these modifications, set the 'overwrite_modifications' loader option.\n"
+                if !$self->overwrite_modifications && Digest::MD5::md5_base64($buffer) ne $md5;
 
             $buffer = '';
         }
@@ -973,6 +1125,22 @@ sub _inject {
     $self->_raw_stmt($target, "use base qw/ $blist /;") if @_;
 }
 
+sub _result_namespace {
+    my ($self, $schema_class, $ns) = @_;
+    my @result_namespace;
+
+    if ($ns =~ /^\+(.*)/) {
+        # Fully qualified namespace
+        @result_namespace = ($1)
+    }
+    else {
+        # Relative namespace
+        @result_namespace = ($schema_class, $ns);
+    }
+
+    return wantarray ? @result_namespace : join '::', @result_namespace;
+}
+
 # Create class with applicable bases, setup monikers, etc
 sub _make_src_class {
     my ($self, $table) = @_;
@@ -984,24 +1152,40 @@ sub _make_src_class {
     my @result_namespace = ($schema_class);
     if ($self->use_namespaces) {
         my $result_namespace = $self->result_namespace || 'Result';
-        if ($result_namespace =~ /^\+(.*)/) {
-            # Fully qualified namespace
-            @result_namespace =  ($1)
-        }
-        else {
-            # Relative namespace
-            push @result_namespace, $result_namespace;
-        }
+        @result_namespace = $self->_result_namespace(
+            $schema_class,
+            $result_namespace,
+        );
     }
     my $table_class = join(q{::}, @result_namespace, $table_moniker);
 
-    if (my $upgrading_v = $self->_upgrading_from) {
-        local $self->naming->{monikers} = $upgrading_v;
+    if ((my $upgrading_v = $self->_upgrading_from)
+            || $self->_rewriting) {
+        local $self->naming->{monikers} = $upgrading_v
+            if $upgrading_v;
+
+        my @result_namespace = @result_namespace;
+        if ($self->_upgrading_from_load_classes) {
+            @result_namespace = ($schema_class);
+        }
+        elsif (my $ns = $self->_downgrading_to_load_classes) {
+            @result_namespace = $self->_result_namespace(
+                $schema_class,
+                $ns,
+            );
+        }
+        elsif ($ns = $self->_rewriting_result_namespace) {
+            @result_namespace = $self->_result_namespace(
+                $schema_class,
+                $ns,
+            );
+        }
 
         my $old_class = join(q{::}, @result_namespace,
             $self->_table2moniker($table));
 
-        $self->_upgrading_classes->{$table_class} = $old_class;
+        $self->_upgrading_classes->{$table_class} = $old_class
+            unless $table_class eq $old_class;
     }
 
     my $table_normalized = lc $table;
@@ -1172,9 +1356,29 @@ sub _tables_list { croak "ABSTRACT METHOD" }
 
 # Execute a constructive DBIC class method, with debug/dump_to_dir hooks.
 sub _dbic_stmt {
-    my $self = shift;
-    my $class = shift;
+    my $self   = shift;
+    my $class  = shift;
+    my $method = shift;
+
+    # generate the pod for this statement, storing it with $self->_pod
+    $self->_make_pod( $class, $method, @_ );
+
+    my $args = dump(@_);
+    $args = '(' . $args . ')' if @_ < 2;
+    my $stmt = $method . $args . q{;};
+
+    warn qq|$class\->$stmt\n| if $self->debug;
+    $self->_raw_stmt($class, '__PACKAGE__->' . $stmt);
+    return;
+}
+
+# generates the accompanying pod for a DBIC class method statement,
+# storing it with $self->_pod
+sub _make_pod {
+    my $self   = shift;
+    my $class  = shift;
     my $method = shift;
+
     if ( $method eq 'table' ) {
         my ($table) = @_;
         $self->_pod( $class, "=head1 NAME" );
@@ -1188,14 +1392,27 @@ sub _dbic_stmt {
         $self->_pod_cut( $class );
     } elsif ( $method eq 'add_columns' ) {
         $self->_pod( $class, "=head1 ACCESSORS" );
-        my $i = 0;
-        foreach ( @_ ) {
-            $i++;
-            next unless $i % 2;
-            $self->_pod( $class, '=head2 ' . $_  );
-            my $comment;
-            $comment = $self->_column_comment( $self->{_class2table}{$class}, ($i - 1) / 2 + 1  ) if $self->can('_column_comment');
-            $self->_pod( $class, $comment ) if $comment;
+        my $col_counter = 0;
+       my @cols = @_;
+        while( my ($name,$attrs) = splice @cols,0,2 ) {
+           $col_counter++;
+            $self->_pod( $class, '=head2 ' . $name  );
+           $self->_pod( $class,
+                        join "\n", map {
+                            my $s = $attrs->{$_};
+                            $s = !defined $s      ? 'undef'          :
+                                 length($s) == 0  ? '(empty string)' :
+                                                     $s;
+
+                            "  $_: $s"
+                        } sort keys %$attrs,
+                      );
+
+           if( $self->can('_column_comment')
+               and my $comment = $self->_column_comment( $self->{_class2table}{$class}, $col_counter)
+             ) {
+               $self->_pod( $class, $comment );
+           }
         }
         $self->_pod_cut( $class );
     } elsif ( $method =~ /^(belongs_to|has_many|might_have)$/ ) {
@@ -1207,13 +1424,6 @@ sub _dbic_stmt {
         $self->_pod_cut( $class );
         $self->{_relations_started} { $class } = 1;
     }
-    my $args = dump(@_);
-    $args = '(' . $args . ')' if @_ < 2;
-    my $stmt = $method . $args . q{;};
-
-    warn qq|$class\->$stmt\n| if $self->debug;
-    $self->_raw_stmt($class, '__PACKAGE__->' . $stmt);
-    return;
 }
 
 # Stores a POD documentation