use base qw/Class::Accessor::Grouped Class::C3::Componentised/;
use mro 'c3';
use Carp::Clan qw/^DBIx::Class/;
-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 Lingua::EN::Inflect::Phrase qw//;
-use File::Temp qw//;
+use DBIx::Class::Schema::Loader::RelBuilder ();
+use Data::Dump 'dump';
+use POSIX ();
+use File::Spec ();
+use Cwd ();
+use Digest::MD5 ();
+use Lingua::EN::Inflect::Number ();
+use Lingua::EN::Inflect::Phrase ();
+use String::ToIdentifier::EN ();
+use String::ToIdentifier::EN::Unicode ();
+use File::Temp ();
use Class::Unload;
use Class::Inspector ();
use Scalar::Util 'looks_like_number';
use DBIx::Class ();
use Encode qw/encode decode/;
use List::MoreUtils qw/all firstidx/;
-use IPC::Open2;
-use Symbol 'gensym';
+use File::Temp 'tempfile';
use namespace::clean;
-our $VERSION = '0.07010';
+our $VERSION = '0.07012';
__PACKAGE__->mk_group_ro_accessors('simple', qw/
schema
result_base_class
result_roles
use_moose
+ only_autoclean
overwrite_modifications
relationship_attrs
=head1 SYNOPSIS
-See L<DBIx::Class::Schema::Loader>
+See L<DBIx::Class::Schema::Loader>.
=head1 DESCRIPTION
The option also takes a hashref:
- naming => { relationships => 'v7', monikers => 'v7' }
+ naming => {
+ relationships => 'v8',
+ monikers => 'v8',
+ column_accessors => 'v8',
+ force_ascii => 1,
+ }
+
+or
+
+ naming => { ALL => 'v8', force_ascii => 1 }
The keys are:
=over 4
+=item ALL
+
+Set L</relationships>, L</monikers> and L</column_accessors> to the specified
+value.
+
=item relationships
How to name relationship accessors.
How to name column accessors in Result classes.
+=item force_ascii
+
+For L</v8> mode and later, uses L<String::ToIdentifier::EN> instead of
+L<String::ToIdentifier::EM::Unicode> to force monikers and other identifiers to
+ASCII.
+
=back
The values can be:
If you don't have any CamelCase table or column names, you can upgrade without
breaking any of your code.
+=item v8
+
+(EXPERIMENTAL)
+
+The default mode is L</v7>, to get L</v8> mode, you have to specify it in
+L</naming> explictly until C<0.08> comes out.
+
+L</monikers> and L</column_accessors> are created using
+L<String::ToIdentifier::EN::Unicode> or L<String::ToIdentifier::EN> if
+L</force_ascii> is set; this is only significant for names with non-C<\w>
+characters such as C<.>.
+
+CamelCase identifiers with words in all caps, e.g. C<VLANValidID> are supported
+correctly in this mode.
+
+For relationships, belongs_to accessors are made from column names by stripping
+postfixes other than C<_id> as well, for example just C<Id>, C<_?ref>, C<_?cd>,
+C<_?code> and C<_?num>, case insensitively.
+
=item preserve
For L</monikers>, this option does not inflect the table names but makes
Comment metadata can be stored in two ways.
The first is that you can create two tables named C<table_comments> and
-C<column_comments> respectively. They both need to have columns named
-C<table_name> and C<comment_text>. The second one needs to have a column
-named C<column_name>. Then data stored in these tables will be used as a
-source of metadata about tables and comments.
+C<column_comments> respectively. These tables must exist in the same database
+and schema as the tables they describe. They both need to have columns named
+C<table_name> and C<comment_text>. The second one needs to have a column named
+C<column_name>. Then data stored in these tables will be used as a source of
+metadata about tables and comments.
(If you wish you can change the name of these tables with the parameters
L</table_comments_table> and L</column_comments_table>.)
The table to look for comments about tables in. By default C<table_comments>.
See L</generate_pod> for details.
+This must not be a fully qualified name, the table will be looked for in the
+same database and schema as the table whose comment is being retrieved.
+
=head2 column_comments_table
The table to look for comments about columns in. By default C<column_comments>.
See L</generate_pod> for details.
+This must not be a fully qualified name, the table will be looked for in the
+same database and schema as the table/column whose comment is being retrieved.
+
=head2 relationship_attrs
Hashref of attributes to pass to each generated relationship, listed
=head2 moniker_map
-Overrides the default table name to moniker translation. Can be either
-a hashref of table keys and moniker values, or a coderef for a translator
-function taking a single scalar table name argument and returning
-a scalar moniker. If the hash entry does not exist, or the function
-returns a false value, the code falls back to default behavior
-for that table name.
+Overrides the default table name to moniker translation. Can be either a
+hashref of table keys and moniker values, or a coderef for a translator
+function taking a L<table object|DBIx::Class::Schema::Loader::Table> argument
+and returning a scalar moniker. If the hash entry does not exist, or the
+function returns a false value, the code falls back to default behavior for
+that table name.
The default behavior is to split on case transition and non-alphanumeric
boundaries, singularize the resulting phrase, then join the titlecased words
{
table_class => name of the DBIC class we are building,
table_moniker => calculated moniker for this table (after moniker_map if present),
- table_name => name of the database table,
+ table => table object of interface DBIx::Class::Schema::Loader::Table,
full_table_name => schema-qualified name of the database table (RDBMS specific),
schema_class => name of the schema class we are building,
column_info => hashref of column info (data_type, is_nullable, etc),
Base class for your schema classes. Defaults to 'DBIx::Class::Schema'.
+=head2 schema_components
+
+List of components to load into the Schema class.
+
=head2 result_base_class
Base class for your table classes (aka result classes). Defaults to
List of additional classes which all of your table classes will use.
-=head2 schema_components
-
-List of components to load into the Schema class.
-
=head2 components
List of additional components to be loaded into all of your Result
Must be a coderef that returns a hashref with the extra attributes.
-Receives the table name, column name and column_info.
+Receives the L<table object|DBIx::Class::Schema::Loader::Table>, column name
+and column_info.
For example:
custom_column_info => sub {
- my ($table_name, $column_name, $column_info) = @_;
+ my ($table, $column_name, $column_info) = @_;
if ($column_name eq 'dog' && $column_info->{default_value} eq 'snoopy') {
return { is_snoopy => 1 };
=head2 preserve_case
-Usually column names are lowercased, to make them easier to work with in
-L<DBIx::Class>. This option lets you turn this behavior off, if the driver
-supports it.
+Normally database names are lowercased and split by underscore, use this option
+if you have CamelCase database names.
Drivers for case sensitive databases like Sybase ASE or MSSQL with a
case-sensitive collation will turn this option on unconditionally.
-Currently the drivers for SQLite, mysql, MSSQL and Firebird/InterBase support
-setting this option.
+B<NOTE:> L</naming> = C<v8> is highly recommended with this option as the
+semantics of this mode are much improved for CamelCase database names.
+
+L</naming> = C<v7> or greater is required with this option.
=head2 qualify_objects
Set to true to prepend the L</db_schema> to table names for C<<
__PACKAGE__->table >> calls, and to some other things like Oracle sequences.
+This attribute is automatically set to true for multi db_schema configurations.
+
=head2 use_moose
Creates Schema and Result classes that use L<Moose>, L<MooseX::NonMoose> and
-L<namespace::autoclean>. The default content after the md5 sum also makes the
-classes immutable.
+L<MooseX::MarkAsMethods> (or L<namespace::autoclean>, see below). The default
+content after the md5 sum also makes the classes immutable.
It is safe to upgrade your existing Schema to this option.
+=head2 only_autoclean
+
+By default, we use L<MooseX::MarkAsMethods> to remove imported functions from
+your generated classes. It uses L<namespace::autoclean> to do this, after
+telling your object's metaclass that any operator L<overload>s in your class
+are methods, which will cause namespace::autoclean to spare them from removal.
+
+This prevents the "Hey, where'd my overloads go?!" effect.
+
+If you don't care about operator overloads, enabling this option falls back to
+just using L<namespace::autoclean> itself.
+
+If none of the above made any sense, or you don't have some pressing need to
+only use L<namespace::autoclean>, leaving this set to the default is
+recommended.
+
=head2 col_collision_map
This option controls how accessors for column names which collide with perl
column_accessors => $naming_ver,
};
}
+ elsif (ref $self->naming eq 'HASH' && exists $self->naming->{ALL}) {
+ my $val = delete $self->naming->{ALL};
+
+ $self->naming->{$_} = $val
+ foreach qw/relationships monikers column_accessors/;
+ }
if ($self->naming) {
- for (values %{ $self->naming }) {
- $_ = $CURRENT_V if $_ eq 'current';
+ foreach my $key (qw/relationships monikers column_accessors/) {
+ $self->naming->{$key} = $CURRENT_V if ($self->naming->{$key}||'') eq 'current';
}
}
$self->{naming} ||= {};
return if $self->{skip_relationships};
return $self->{relbuilder} ||= do {
-
- no warnings 'uninitialized';
my $relbuilder_suff =
{qw{
v4 ::Compat::v0_040
v5 ::Compat::v0_05
v6 ::Compat::v0_06
+ v7 ::Compat::v0_07
}}
- ->{ $self->naming->{relationships}};
+ ->{$self->naming->{relationships}||$CURRENT_V} || '';
my $relbuilder_class = 'DBIx::Class::Schema::Loader::RelBuilder'.$relbuilder_suff;
$self->ensure_class_loaded($relbuilder_class);
- $relbuilder_class->new( $self );
-
+ $relbuilder_class->new($self);
};
}
if (@clashes) {
die 'Unable to load schema - chosen moniker/class naming style results in moniker clashes. '
- . 'Either change the naming style, or supply an explicit moniker_map: '
+ . 'In multi db_schema configurations you may need to set moniker_parts, '
+ . 'otherwise change the naming style, or supply an explicit moniker_map: '
. join ('; ', @clashes)
. "\n"
;
unless $self->dynamic or $self->quiet;
my $schema_text =
- qq|package $schema_class;\n\n|
+ qq|use utf8;\n|
+ . 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|;
+ my $autoclean
+ = $self->only_autoclean
+ ? 'namespace::autoclean'
+ : 'MooseX::MarkAsMethods autoclean => 1'
+ ;
+
if ($self->use_moose) {
- $schema_text.= qq|use Moose;\nuse namespace::autoclean;\nextends '$schema_base_class';\n\n|;
+
+ $schema_text.= qq|use Moose;\nuse $autoclean;\nextends '$schema_base_class';\n\n|;
}
else {
$schema_text .= qq|use strict;\nuse warnings;\n\nuse base '$schema_base_class';\n\n|;
foreach my $src_class (@classes) {
my $src_text =
- qq|package $src_class;\n\n|
+ qq|use utf8;\n|
+ . 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|;
unless $result_base_class eq 'DBIx::Class::Core';
if ($self->use_moose) {
- $src_text.= qq|use Moose;\nuse MooseX::NonMoose;\nuse namespace::autoclean;|;
+ $src_text.= qq|use Moose;\nuse MooseX::NonMoose;\nuse $autoclean;|;
# these options 'use base' which is compile time
if (@{ $self->left_base_classes } || @{ $self->additional_base_classes }) {
my $custom_content = $old_custom || '';
- # prepend extra custom content from a *renamed* class (singularization effect)
+ # Use custom content from a renamed class, the class names in it are
+ # rewritten below.
if (my $renamed_class = $self->_upgrading_classes->{$class}) {
my $old_filename = $self->_get_dump_filename($renamed_class);
if (-f $old_filename) {
- my $extra_custom = ($self->_parse_generated_file ($old_filename))[4];
-
- $extra_custom =~ s/\n\n# You can replace.*\n1;\n//;
-
- $custom_content = join ("\n", '', $extra_custom, $custom_content)
- if $extra_custom;
+ $custom_content = ($self->_parse_generated_file ($old_filename))[4];
unlink $old_filename;
}
);
}
else {
- my ($out, $in) = (gensym, gensym);
-
- my $pid = open2($out, $in, $filter)
- or croak "Could not open pipe to $filter: $!";
+ my ($fh, $temp_file) = tempfile();
- print $in $text;
+ binmode $fh, ':encoding(UTF-8)';
+ print $fh $text;
+ close $fh;
- close $in;
+ open my $out, qq{$filter < "$temp_file"|}
+ or croak "Could not open pipe to $filter: $!";
$text = decode('UTF-8', do { local $/; <$out> });
$text =~ s/$CR?$LF/\n/g;
- waitpid $pid, 0;
+ close $out;
my $exit_code = $? >> 8;
+ unlink $temp_file
+ or croak "Could not remove temporary file '$temp_file': $!";
+
if ($exit_code != 0) {
croak "filter '$filter' exited non-zero: $exit_code";
}
sub _default_column_accessor_name {
my ( $self, $column_name ) = @_;
- my $accessor_name = $column_name;
- $accessor_name =~ s/\W+/_/g;
+ my $preserve = ($self->naming->{column_accessors}||'') eq 'preserve';
+
+ my $v = $self->_get_naming_v('column_accessors');
+
+ my $accessor_name = $preserve ?
+ $self->_to_identifier('column_accessors', $column_name) # assume CamelCase
+ :
+ $self->_to_identifier('column_accessors', $column_name, '_');
+
+ $accessor_name =~ s/\W+/_/g; # only if naming < v8, otherwise to_identifier
+ # takes care of it
- if ((($self->naming->{column_accessors}||'') =~ /(\d+)/ && $1 < 7) || (not $self->preserve_case)) {
+ if ($preserve) {
+ return $accessor_name;
+ }
+ elsif ($v < 7 || (not $self->preserve_case)) {
# older naming just lc'd the col accessor and that's all.
return lc $accessor_name;
}
- elsif (($self->naming->{column_accessors}||'') eq 'preserve') {
- return $accessor_name;
- }
- return join '_', map lc, split_name $column_name;
+ return join '_', map lc, split_name $column_name, $v;
}
sub _make_column_accessor_name {
return values %{$self->_tables};
}
+sub _get_naming_v {
+ my ($self, $naming_key) = @_;
+
+ my $v;
+
+ if (($self->naming->{$naming_key}||'') =~ /^v(\d+)\z/) {
+ $v = $1;
+ }
+ else {
+ ($v) = $CURRENT_V =~ /^v(\d+)\z/;
+ }
+
+ return $v;
+}
+
+sub _to_identifier {
+ my ($self, $naming_key, $name, $sep_char, $force) = @_;
+
+ my $v = $self->_get_naming_v($naming_key);
+
+ my $to_identifier = $self->naming->{force_ascii} ?
+ \&String::ToIdentifier::EN::to_identifier
+ : \&String::ToIdentifier::EN::Unicode::to_identifier;
+
+ return $v >= 8 || $force ? $to_identifier->($name, $sep_char) : $name;
+}
+
# Make a moniker from a table
sub _default_table2moniker {
- no warnings 'uninitialized';
my ($self, $table) = @_;
+ my $v = $self->_get_naming_v('monikers');
+
my @name_parts = map $table->$_, @{ $self->moniker_parts };
my $name_idx = firstidx { $_ eq 'name' } @{ $self->moniker_parts };
- if ($self->naming->{monikers} eq 'v4') {
- return join '', map ucfirst, map split(/[\W_]+/, lc $_), @name_parts;
- }
- elsif ($self->naming->{monikers} eq 'v5') {
- my @parts = map lc, @name_parts;
- $parts[$name_idx] = Lingua::EN::Inflect::Number::to_S($parts[$name_idx]);
+ my @all_parts;
- return join '', map ucfirst, map split(/[\W_]+/, $_), @parts;
- }
- elsif ($self->naming->{monikers} eq 'v6') {
- (my $as_phrase = join '', map lc, @name_parts) =~ s/_+/ /g;
- my $inflected = Lingua::EN::Inflect::Phrase::to_S($as_phrase);
+ foreach my $i (0 .. $#name_parts) {
+ my $part = $name_parts[$i];
- return join '', map ucfirst, split /\W+/, $inflected;
- }
+ if ($i != $name_idx || $v >= 8) {
+ $part = $self->_to_identifier('monikers', $part, '_', 1);
+ }
- my @words = map lc, map split_name $_, @name_parts;
- my $as_phrase = join ' ', @words;
+ if ($i == $name_idx && $v == 5) {
+ $part = Lingua::EN::Inflect::Number::to_S($part);
+ }
- my $inflected = $self->naming->{monikers} eq 'plural' ?
- Lingua::EN::Inflect::Phrase::to_PL($as_phrase)
- :
- $self->naming->{monikers} eq 'preserve' ?
- $as_phrase
- :
- Lingua::EN::Inflect::Phrase::to_S($as_phrase);
+ my @part_parts = map lc, $v > 6 ?
+ # use v8 semantics for all moniker parts except name
+ ($i == $name_idx ? split_name $part, $v : split_name $part)
+ : split /[\W_]+/, $part;
- return join '', map ucfirst, split /\W+/, $inflected;
+ if ($i == $name_idx && $v >= 6) {
+ my $as_phrase = join ' ', @part_parts;
+
+ my $inflected = ($self->naming->{monikers}||'') eq 'plural' ?
+ Lingua::EN::Inflect::Phrase::to_PL($as_phrase)
+ :
+ ($self->naming->{monikers}||'') eq 'preserve' ?
+ $as_phrase
+ :
+ Lingua::EN::Inflect::Phrase::to_S($as_phrase);
+
+ @part_parts = split /\s+/, $inflected;
+ }
+
+ push @all_parts, map ucfirst, @part_parts;
+ }
+
+ return join '', @all_parts;
}
sub _table2moniker {
contain multiple entries per table for the original and normalized table
names, as above in L</monikers>.
+=head1 NON-ENGLISH DATABASES
+
+If you use the loader on a database with table and column names in a language
+other than English, you will want to turn off the English language specific
+heuristics.
+
+To do so, use something like this in your loader options:
+
+ naming => { monikers => 'v4' },
+ inflect_singular => sub { "$_[0]_rel" },
+ inflect_plural => sub { "$_[0]_rel" },
+
=head1 COLUMN ACCESSOR COLLISIONS
Occasionally you may have a column name that collides with a perl method, such
=head1 SEE ALSO
-L<DBIx::Class::Schema::Loader>
+L<DBIx::Class::Schema::Loader>, L<dbicdump>
=head1 AUTHOR