use strict;
use warnings;
-use base qw/Class::Accessor::Fast/;
+use base qw/Class::Accessor::Fast Class::C3::Componentised/;
use Class::C3;
use Carp::Clan qw/^DBIx::Class/;
-use UNIVERSAL::require;
use DBIx::Class::Schema::Loader::RelBuilder;
use Data::Dump qw/ dump /;
use POSIX qw//;
use File::Spec qw//;
use Cwd qw//;
use Digest::MD5 qw//;
+use Lingua::EN::Inflect::Number qw//;
+use File::Temp qw//;
+use Class::Unload;
require DBIx::Class;
-our $VERSION = '0.04999_05';
+our $VERSION = '0.04999_12';
__PACKAGE__->mk_ro_accessors(qw/
schema
result_namespace
resultset_namespace
default_resultset_class
+ schema_base_class
+ result_base_class
db_schema
_tables
As L</inflect_plural> above, but for singularizing relationship names.
Default behavior is to utilize L<Lingua::EN::Inflect::Number/to_S>.
+=head2 schema_base_class
+
+Base class for your schema classes. Defaults to 'DBIx::Class::Schema'.
+
+=head2 result_base_class
+
+Base class for your table classes (aka result classes). Defaults to
+'DBIx::Class::Core'.
+
=head2 additional_base_classes
List of additional base classes all of your table classes will use.
. " DBIx::Class::Schema::Loader::Base documentation"
if $self->{dump_overwrite};
+ $self->{dynamic} = ! $self->{dump_directory};
+ $self->{temp_directory} ||= File::Temp::tempdir( 'dbicXXXX',
+ TMPDIR => 1,
+ CLEANUP => 1,
+ );
+
+ $self->{dump_directory} ||= $self->{temp_directory};
+
$self->{relbuilder} = DBIx::Class::Schema::Loader::RelBuilder->new(
- $self->schema_class, $self->inflect_plural, $self->inflect_singular
+ $self->schema, $self->inflect_plural, $self->inflect_singular
) if !$self->{skip_relationships};
$self;
my ($self, $file) = @_;
foreach my $prefix (@INC) {
- my $fullpath = $prefix . '/' . $file;
- return $fullpath if -f $fullpath;
+ 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)) || '';
}
return;
$class_path =~ s{::}{/}g;
$class_path .= '.pm';
- my $inc_path = $self->_find_file_in_inc($class_path);
+ my $real_inc_path = $self->_find_file_in_inc($class_path);
- return if !$inc_path;
-
- my $real_dump_path = $self->dump_directory
- ? Cwd::abs_path(
- File::Spec->catfile($self->dump_directory, $class_path)
- ) || ''
- : '';
- my $real_inc_path = Cwd::abs_path($inc_path);
- return if $real_inc_path eq $real_dump_path;
-
- $class->require;
- croak "Failed to load external class definition"
- . " for '$class': $@"
- if $@;
+ return if !$real_inc_path;
# If we make it to here, we loaded an external definition
warn qq/# Loaded external class definition for '$class'\n/
if $self->debug;
- # Make sure ResultSetManager picks up any :ResultSet methods from
- # the external definition
- $class->table($class->table);
-
- # The rest is only relevant when dumping
- return if !$self->dump_directory;
-
croak 'Failed to locate actual external module file for '
. "'$class'"
if !$real_inc_path;
my ($self, $schema) = @_;
$self->{schema} = $schema;
+ $self->{relbuilder}{schema} = $schema;
my @created;
my @current = $self->_tables_list;
$self->{_tables}->{$_} = 1;
}
- # Set up classes/monikers
- {
- no warnings 'redefine';
- local *Class::C3::reinitialize = sub { };
- use warnings;
-
- $self->_make_src_class($_) for @tables;
- }
-
- Class::C3::reinitialize;
-
+ $self->_make_src_class($_) for @tables;
$self->_setup_src_meta($_) for @tables;
if(!$self->skip_relationships) {
+ # The relationship loader needs a working schema
+ $self->{quiet} = 1;
+ local $self->{dump_directory} = $self->{temp_directory};
+ $self->_reload_classes(@tables);
$self->_load_relationships($_) for @tables;
+ $self->{quiet} = 0;
+
+ # Remove that temp dir from INC so it doesn't get reloaded
+ @INC = grep { $_ ne $self->{dump_directory} } @INC;
}
$self->_load_external($_)
for map { $self->classes->{$_} } @tables;
- $self->_dump_to_dir if $self->dump_directory;
+ $self->_reload_classes(@tables);
# Drop temporary cache
delete $self->{_cache};
return \@tables;
}
+sub _reload_classes {
+ my ($self, @tables) = @_;
+
+ # so that we don't repeat custom sections
+ @INC = grep $_ ne $self->dump_directory, @INC;
+
+ $self->_dump_to_dir(map { $self->classes->{$_} } @tables);
+
+ unshift @INC, $self->dump_directory;
+
+ my @to_register;
+ my %have_source = map { $_ => $self->schema->source($_) }
+ $self->schema->sources;
+
+ for my $table (@tables) {
+ my $moniker = $self->monikers->{$table};
+ my $class = $self->classes->{$table};
+
+ {
+ no warnings 'redefine';
+ local *Class::C3::reinitialize = sub {};
+ use warnings;
+
+ Class::Unload->unload($class);
+ my ($source, $resultset_class);
+ if (
+ ($source = $have_source{$moniker})
+ && ($resultset_class = $source->resultset_class)
+ && ($resultset_class ne 'DBIx::Class::ResultSet')
+ ) {
+ my $has_file = Class::Inspector->loaded_filename($resultset_class);
+ Class::Unload->unload($resultset_class);
+ $self->ensure_class_loaded($resultset_class) if $has_file;
+ }
+ $self->ensure_class_loaded($class);
+ }
+ push @to_register, [$moniker, $class];
+ }
+
+ Class::C3->reinitialize;
+ for (@to_register) {
+ $self->schema->register_class(@$_);
+ }
+}
+
sub _get_dump_filename {
my ($self, $class) = (@_);
}
sub _dump_to_dir {
- my ($self) = @_;
-
- my $target_dir = $self->dump_directory;
+ my ($self, @classes) = @_;
my $schema_class = $self->schema_class;
+ my $schema_base_class = $self->schema_base_class || 'DBIx::Class::Schema';
- croak "Must specify target directory for dumping!" if ! $target_dir;
-
- warn "Dumping manual schema for $schema_class to directory $target_dir ...\n";
+ my $target_dir = $self->dump_directory;
+ warn "Dumping manual schema for $schema_class to directory $target_dir ...\n"
+ unless $self->{dynamic} or $self->{quiet};
my $schema_text =
qq|package $schema_class;\n\n|
+ . qq|# Created by DBIx::Class::Schema::Loader\n|
+ . qq|# DO NOT MODIFY THE FIRST PART OF THIS FILE\n\n|
. qq|use strict;\nuse warnings;\n\n|
- . qq|use base 'DBIx::Class::Schema';\n\n|;
+ . qq|use base '$schema_base_class';\n\n|;
-
if ($self->use_namespaces) {
$schema_text .= qq|__PACKAGE__->load_namespaces|;
my $namespace_options;
}
else {
$schema_text .= qq|__PACKAGE__->load_classes;\n|;
-
}
$self->_write_classfile($schema_class, $schema_text);
- foreach my $src_class (sort keys %{$self->{_dump_storage}}) {
+ my $result_base_class = $self->result_base_class || 'DBIx::Class::Core';
+
+ foreach my $src_class (@classes) {
my $src_text =
qq|package $src_class;\n\n|
+ . qq|# Created by DBIx::Class::Schema::Loader\n|
+ . qq|# DO NOT MODIFY THE FIRST PART OF THIS FILE\n\n|
. qq|use strict;\nuse warnings;\n\n|
- . qq|use base 'DBIx::Class';\n\n|;
+ . qq|use base '$result_base_class';\n\n|;
$self->_write_classfile($src_class, $src_text);
}
- warn "Schema dump completed.\n";
+ warn "Schema dump completed.\n" unless $self->{dynamic} or $self->{quiet};
+
+}
+
+sub _sig_comment {
+ my ($self, $version, $ts) = @_;
+ return qq|\n\n# Created by DBIx::Class::Schema::Loader|
+ . qq| v| . $version
+ . q| @ | . $ts
+ . qq|\n# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:|;
}
sub _write_classfile {
if (-f $filename && $self->really_erase_my_files) {
warn "Deleting existing file '$filename' due to "
- . "'really_erase_my_files' setting\n";
+ . "'really_erase_my_files' setting\n" unless $self->{quiet};
unlink($filename);
}
- my $custom_content = $self->_get_custom_content($class, $filename);
-
- $custom_content ||= qq|\n\n# You can replace this text with custom|
- . qq| content, and it will be preserved on regeneration|
- . qq|\n1;\n|;
+ my ($custom_content, $old_md5, $old_ver, $old_ts) = $self->_get_custom_content($class, $filename);
$text .= qq|$_\n|
for @{$self->{_dump_storage}->{$class} || []};
- $text .= qq|\n\n# Created by DBIx::Class::Schema::Loader|
- . qq| v| . $DBIx::Class::Schema::Loader::VERSION
- . q| @ | . POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime)
- . qq|\n# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:|;
+ # Check and see if the dump is infact differnt
+
+ my $compare_to;
+ if ($old_md5) {
+ $compare_to = $text . $self->_sig_comment($old_ver, $old_ts);
+
+
+ if (Digest::MD5::md5_base64($compare_to) eq $old_md5) {
+ return;
+ }
+ }
+
+ $text .= $self->_sig_comment(
+ $DBIx::Class::Schema::Loader::VERSION,
+ POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime)
+ );
open(my $fh, '>', $filename)
or croak "Cannot open '$filename' for writing: $!";
print $fh qq|$_\n|
for @{$self->{_ext_storage}->{$class} || []};
+ # Write out any custom content the user has added
print $fh $custom_content;
close($fh)
- or croak "Cannot close '$filename': $!";
+ or croak "Error closing '$filename': $!";
+}
+
+sub _default_custom_content {
+ return qq|\n\n# You can replace this text with custom|
+ . qq| content, and it will be preserved on regeneration|
+ . qq|\n1;\n|;
}
sub _get_custom_content {
my ($self, $class, $filename) = @_;
- return if ! -f $filename;
+ return ($self->_default_custom_content) if ! -f $filename;
+
open(my $fh, '<', $filename)
or croak "Cannot open '$filename' for reading: $!";
my $mark_re =
qr{^(# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:)([A-Za-z0-9/+]{22})\n};
- my $found = 0;
my $buffer = '';
+ my ($md5, $ts, $ver);
while(<$fh>) {
- if(!$found && /$mark_re/) {
- $found = 1;
- $buffer .= $1;
+ if(!$md5 && /$mark_re/) {
+ $md5 = $2;
+ my $line = $1;
+
+ # Pull out the previous version and timestamp
+ ($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 $2;
+ if Digest::MD5::md5_base64($buffer) ne $md5;
$buffer = '';
}
croak "Cannot not overwrite '$filename' without 'really_erase_my_files',"
. " it does not appear to have been generated by Loader"
- if !$found;
+ if !$md5;
+
+ # Default custom content:
+ $buffer ||= $self->_default_custom_content;
- return $buffer;
+ return ($buffer, $md5, $ver, $ts);
}
sub _use {
my $self = shift;
my $target = shift;
- my $evalstr;
foreach (@_) {
warn "$target: use $_;" if $self->debug;
$self->_raw_stmt($target, "use $_;");
- $_->require or croak ($_ . "->require: $@");
- $evalstr .= "package $target; use $_;";
}
- eval $evalstr if $evalstr;
- croak $@ if $@;
}
sub _inject {
my $target = shift;
my $schema_class = $self->schema_class;
- my $blist = join(q{ }, map "+$_", @_);
- warn "$target: __PACKAGE__->load_components( qw/ $blist / );" if $self->debug && @_;
- $self->_raw_stmt($target, "__PACKAGE__->load_components( qw/ $blist / );") if @_;
- foreach (@_) {
- $_->require or croak ($_ . "->require: $@");
- $schema_class->inject_base($target, $_);
- }
+ my $blist = join(q{ }, @_);
+ warn "$target: use base qw/ $blist /;" if $self->debug && @_;
+ $self->_raw_stmt($target, "use base qw/ $blist /;") if @_;
}
# Create class with applicable bases, setup monikers, etc
$self->monikers->{$table} = $table_moniker;
$self->monikers->{$table_normalized} = $table_moniker;
- { no strict 'refs'; @{"${table_class}::ISA"} = qw/DBIx::Class/ }
-
$self->_use ($table_class, @{$self->additional_classes});
- $self->_inject($table_class, @{$self->additional_base_classes});
+ $self->_inject($table_class, @{$self->left_base_classes});
- $self->_dbic_stmt($table_class, 'load_components', @{$self->components}, 'Core');
+ if (my @components = @{ $self->components }) {
+ $self->_dbic_stmt($table_class, 'load_components', @components);
+ }
$self->_dbic_stmt($table_class, 'load_resultset_components', @{$self->resultset_components})
if @{$self->resultset_components};
- $self->_inject($table_class, @{$self->left_base_classes});
+ $self->_inject($table_class, @{$self->additional_base_classes});
}
-# Set up metadata (cols, pks, etc) and register the class with the schema
+# Set up metadata (cols, pks, etc)
sub _setup_src_meta {
my ($self, $table) = @_;
my $table_class = $self->classes->{$table};
my $table_moniker = $self->monikers->{$table};
- $self->_dbic_stmt($table_class,'table',$table);
+ my $table_name = $table;
+ my $name_sep = $self->schema->storage->sql_maker->name_sep;
+
+ if ($name_sep && $table_name =~ /\Q$name_sep\E/) {
+ $table_name = \ $self->_quote_table_name($table_name);
+ }
+
+ $self->_dbic_stmt($table_class,'table',$table_name);
my $cols = $self->_table_columns($table);
my $col_info;
$self->_dbic_stmt($table_class,'add_columns',@$cols);
}
else {
- my %col_info_lc = map { lc($_), $col_info->{$_} } keys %$col_info;
+ if ($self->_is_case_sensitive) {
+ for my $col (keys %$col_info) {
+ $col_info->{$col}{accessor} = lc $col
+ if $col ne lc($col);
+ }
+ } else {
+ $col_info = { map { lc($_), $col_info->{$_} } keys %$col_info };
+ }
+
my $fks = $self->_table_fk_info($table);
+
for my $fkdef (@$fks) {
for my $col (@{ $fkdef->{local_columns} }) {
- $col_info_lc{$col}->{is_foreign_key} = 1;
+ $col_info->{$col}{is_foreign_key} = 1;
}
}
$self->_dbic_stmt(
$table_class,
'add_columns',
- map { $_, ($col_info_lc{$_}||{}) } @$cols
+ map { $_, ($col_info->{$_}||{}) } @$cols
);
}
+ my %uniq_tag; # used to eliminate duplicate uniqs
+
my $pks = $self->_table_pk_info($table) || [];
@$pks ? $self->_dbic_stmt($table_class,'set_primary_key',@$pks)
: carp("$table has no primary key");
+ $uniq_tag{ join("\0", @$pks) }++ if @$pks; # pk is a uniq
my $uniqs = $self->_table_uniq_info($table) || [];
- $self->_dbic_stmt($table_class,'add_unique_constraint',@$_) for (@$uniqs);
+ for (@$uniqs) {
+ my ($name, $cols) = @$_;
+ next if $uniq_tag{ join("\0", @$cols) }++; # skip duplicates
+ $self->_dbic_stmt($table_class,'add_unique_constraint', $name, $cols);
+ }
- $schema_class->register_class($table_moniker, $table_class);
- $schema->register_class($table_moniker, $table_class) if $schema ne $schema_class;
}
=head2 tables
}
# Make a moniker from a table
+sub _default_table2moniker {
+ my ($self, $table) = @_;
+
+ return join '', map ucfirst, split /[\W_]+/,
+ Lingua::EN::Inflect::Number::to_S(lc $table);
+}
+
sub _table2moniker {
my ( $self, $table ) = @_;
$moniker = $self->moniker_map->($table);
}
- $moniker ||= join '', map ucfirst, split /[\W_]+/, lc $table;
+ $moniker ||= $self->_default_table2moniker($table);
return $moniker;
}
my $self = shift;
my $class = shift;
my $method = shift;
-
- if(!$self->debug && !$self->dump_directory) {
- $class->$method(@_);
- return;
+ if ( $method eq 'table' ) {
+ my ($table) = @_;
+ $self->_pod( $class, "=head1 NAME" );
+ my $table_descr = $class;
+ if ( $self->can('_table_comment') ) {
+ my $comment = $self->_table_comment($table);
+ $table_descr .= " - " . $comment if $comment;
+ }
+ $self->{_class2table}{ $class } = $table;
+ $self->_pod( $class, $table_descr );
+ $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;
+ }
+ $self->_pod_cut( $class );
+ } elsif ( $method =~ /^(belongs_to|has_many|might_have)$/ ) {
+ $self->_pod( $class, "=head1 RELATIONS" ) unless $self->{_relations_started} { $class } ;
+ my ( $accessor, $rel_class ) = @_;
+ $self->_pod( $class, "=head2 $accessor" );
+ $self->_pod( $class, 'Type: ' . $method );
+ $self->_pod( $class, "Related object: L<$rel_class>" );
+ $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;
- $class->$method(@_);
$self->_raw_stmt($class, '__PACKAGE__->' . $stmt);
+ return;
+}
+
+# Stores a POD documentation
+sub _pod {
+ my ($self, $class, $stmt) = @_;
+ $self->_raw_stmt( $class, "\n" . $stmt );
+}
+
+sub _pod_cut {
+ my ($self, $class ) = @_;
+ $self->_raw_stmt( $class, "\n=cut\n" );
}
+
# Store a raw source line for a class (for dumping purposes)
sub _raw_stmt {
my ($self, $class, $stmt) = @_;
- push(@{$self->{_dump_storage}->{$class}}, $stmt) if $self->dump_directory;
+ push(@{$self->{_dump_storage}->{$class}}, $stmt);
}
# Like above, but separately for the externally loaded stuff
sub _ext_stmt {
my ($self, $class, $stmt) = @_;
- push(@{$self->{_ext_storage}->{$class}}, $stmt) if $self->dump_directory;
+ push(@{$self->{_ext_storage}->{$class}}, $stmt);
+}
+
+sub _quote_table_name {
+ my ($self, $table) = @_;
+
+ my $qt = $self->schema->storage->sql_maker->quote_char;
+
+ return $table unless $qt;
+
+ if (ref $qt) {
+ return $qt->[0] . $table . $qt->[1];
+ }
+
+ return $qt . $table . $qt;
}
+sub _is_case_sensitive { 0 }
+
=head2 monikers
Returns a hashref of loaded table to moniker mappings. There will
L<DBIx::Class::Schema::Loader>
+=head1 AUTHOR
+
+See L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
+
+=head1 LICENSE
+
+This library is free software; you can redistribute it and/or modify it under
+the same terms as Perl itself.
+
=cut
1;