fix bug in qualify_objects that would add schema to relnames
[dbsrgits/DBIx-Class-Schema-Loader.git] / lib / DBIx / Class / Schema / Loader / RelBuilder.pm
CommitLineData
996be9ee 1package DBIx::Class::Schema::Loader::RelBuilder;
2
3use strict;
4use warnings;
7824616e 5use Class::C3;
fa994d3c 6use Carp::Clan qw/^DBIx::Class/;
39b22ca9 7use Lingua::EN::Inflect::Phrase ();
cc4f11a2 8use DBIx::Class::Schema::Loader::Utils 'split_name';
996be9ee 9
6b1d4f76 10our $VERSION = '0.07001';
32f784fc 11
996be9ee 12=head1 NAME
13
14DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
15
16=head1 SYNOPSIS
17
19b7d71c 18See L<DBIx::Class::Schema::Loader> and L<DBIx::Class::Schema::Loader::Base>.
996be9ee 19
20=head1 DESCRIPTION
21
22This class builds relationships for L<DBIx::Class::Schema::Loader>. This
23is module is not (yet) for external use.
24
25=head1 METHODS
26
27=head2 new
28
e8ad6491 29Arguments: schema_class (scalar), inflect_plural, inflect_singular
996be9ee 30
31C<$schema_class> should be a schema class name, where the source
32classes have already been set up and registered. Column info, primary
33key, and unique constraints will be drawn from this schema for all
34of the existing source monikers.
35
996be9ee 36Options inflect_plural and inflect_singular are optional, and are better documented
37in L<DBIx::Class::Schema::Loader::Base>.
38
39=head2 generate_code
40
e8ad6491 41Arguments: local_moniker (scalar), fk_info (arrayref)
42
43This generates the code for the relationships of a given table.
44
45C<local_moniker> is the moniker name of the table which had the REFERENCES
46statements. The fk_info arrayref's contents should take the form:
47
48 [
49 {
50 local_columns => [ 'col2', 'col3' ],
51 remote_columns => [ 'col5', 'col7' ],
52 remote_moniker => 'AnotherTableMoniker',
53 },
54 {
55 local_columns => [ 'col1', 'col4' ],
56 remote_columns => [ 'col1', 'col2' ],
57 remote_moniker => 'YetAnotherTableMoniker',
58 },
59 # ...
60 ],
61
62This method will return the generated relationships as a hashref keyed on the
63class names. The values are arrayrefs of hashes containing method name and
64arguments, like so:
996be9ee 65
66 {
67 'Some::Source::Class' => [
b97c2c1e 68 { method => 'belongs_to', arguments => [ 'col1', 'Another::Source::Class' ],
69 { method => 'has_many', arguments => [ 'anothers', 'Yet::Another::Source::Class', 'col15' ],
996be9ee 70 ],
71 'Another::Source::Class' => [
72 # ...
73 ],
74 # ...
75 }
8f9d7ce5 76
996be9ee 77=cut
78
79sub new {
c8c27020 80 my ( $class, $schema, $inflect_pl, $inflect_singular, $rel_attrs ) = @_;
996be9ee 81
82 my $self = {
83 schema => $schema,
996be9ee 84 inflect_plural => $inflect_pl,
85 inflect_singular => $inflect_singular,
c8c27020 86 relationship_attrs => $rel_attrs,
996be9ee 87 };
88
c8c27020 89 # validate the relationship_attrs arg
90 if( defined $self->{relationship_attrs} ) {
91 ref($self->{relationship_attrs}) eq 'HASH'
92 or croak "relationship_attrs must be a hashref";
93 }
996be9ee 94
c8c27020 95 return bless $self => $class;
996be9ee 96}
97
98
99# pluralize a relationship name
100sub _inflect_plural {
ecf930e6 101 my ($self, $relname) = @_;
996be9ee 102
39ef3bfe 103 return '' if !defined $relname || $relname eq '';
104
996be9ee 105 if( ref $self->{inflect_plural} eq 'HASH' ) {
106 return $self->{inflect_plural}->{$relname}
107 if exists $self->{inflect_plural}->{$relname};
108 }
109 elsif( ref $self->{inflect_plural} eq 'CODE' ) {
110 my $inflected = $self->{inflect_plural}->($relname);
111 return $inflected if $inflected;
112 }
113
ecf930e6 114 return $self->_to_PL($relname);
996be9ee 115}
116
117# Singularize a relationship name
118sub _inflect_singular {
ecf930e6 119 my ($self, $relname) = @_;
996be9ee 120
39ef3bfe 121 return '' if !defined $relname || $relname eq '';
122
996be9ee 123 if( ref $self->{inflect_singular} eq 'HASH' ) {
124 return $self->{inflect_singular}->{$relname}
125 if exists $self->{inflect_singular}->{$relname};
126 }
127 elsif( ref $self->{inflect_singular} eq 'CODE' ) {
128 my $inflected = $self->{inflect_singular}->($relname);
129 return $inflected if $inflected;
130 }
131
ecf930e6 132 return $self->_to_S($relname);
c496748b 133}
134
135sub _to_PL {
136 my ($self, $name) = @_;
137
138 $name =~ s/_/ /g;
39b22ca9 139 my $plural = Lingua::EN::Inflect::Phrase::to_PL($name);
c496748b 140 $plural =~ s/ /_/g;
141
142 return $plural;
143}
144
c496748b 145sub _to_S {
146 my ($self, $name) = @_;
147
39b22ca9 148 $name =~ s/_/ /g;
149 my $singular = Lingua::EN::Inflect::Phrase::to_S($name);
150 $singular =~ s/ /_/g;
151
152 return $singular;
996be9ee 153}
154
53ef681d 155sub _default_relationship_attrs { +{
156 has_many => {
157 cascade_delete => 0,
158 cascade_copy => 0,
159 },
160 might_have => {
161 cascade_delete => 0,
162 cascade_copy => 0,
163 },
164 belongs_to => {
165 on_delete => 'CASCADE',
166 on_update => 'CASCADE',
6818ce9f 167# is_deferrable => 1,
53ef681d 168 },
169} }
170
c8c27020 171# accessor for options to be passed to each generated relationship
172# type. take single argument, the relationship type name, and returns
173# either a hashref (if some options are set), or nothing
174sub _relationship_attrs {
175 my ( $self, $reltype ) = @_;
176 my $r = $self->{relationship_attrs};
c8c27020 177
53ef681d 178 my %composite = (
179 %{ $self->_default_relationship_attrs->{$reltype} || {} },
180 %{ $r->{all} || {} }
181 );
182
c8c27020 183 if( my $specific = $r->{$reltype} ) {
184 while( my ($k,$v) = each %$specific ) {
185 $composite{$k} = $v;
186 }
187 }
188 return \%composite;
189}
190
26f1c8c9 191sub _array_eq {
ecf930e6 192 my ($self, $a, $b) = @_;
26f1c8c9 193
194 return unless @$a == @$b;
195
196 for (my $i = 0; $i < @$a; $i++) {
197 return unless $a->[$i] eq $b->[$i];
198 }
199 return 1;
200}
201
c39e403e 202sub _remote_attrs {
c496748b 203 my ($self, $local_moniker, $local_cols) = @_;
c39e403e 204
c496748b 205 # get our base set of attrs from _relationship_attrs, if present
206 my $attrs = $self->_relationship_attrs('belongs_to') || {};
c8c27020 207
c496748b 208 # If the referring column is nullable, make 'belongs_to' an
209 # outer join, unless explicitly set by relationship_attrs
210 my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} } @$local_cols;
211 $attrs->{join_type} = 'LEFT' if $nullable && !defined $attrs->{join_type};
c39e403e 212
c496748b 213 return $attrs;
c39e403e 214}
215
414c61a0 216sub _sanitize_name {
217 my ($self, $name) = @_;
218
219 if (ref $name) {
220 # scalar ref for weird table name (like one containing a '.')
221 ($name = $$name) =~ s/\W+/_/g;
222 }
223 else {
224 # remove 'schema.' prefix if any
225 $name =~ s/^[^.]+\.//;
226 }
227
228 return $name;
229}
230
19b7d71c 231sub _normalize_name {
232 my ($self, $name) = @_;
233
414c61a0 234 $name = $self->_sanitize_name($name);
235
cc4f11a2 236 my @words = split_name $name;
19b7d71c 237
238 return join '_', map lc, @words;
239}
240
f2fc8d01 241sub _remote_relname {
242 my ($self, $remote_table, $cond) = @_;
243
244 my $remote_relname;
245 # for single-column case, set the remote relname to the column
246 # name, to make filter accessors work, but strip trailing _id
247 if(scalar keys %{$cond} == 1) {
248 my ($col) = values %{$cond};
19b7d71c 249 $col = $self->_normalize_name($col);
f2fc8d01 250 $col =~ s/_id$//;
251 $remote_relname = $self->_inflect_singular($col);
252 }
253 else {
19b7d71c 254 $remote_relname = $self->_inflect_singular($self->_normalize_name($remote_table));
f2fc8d01 255 }
256
257 return $remote_relname;
258}
259
996be9ee 260sub generate_code {
26f1c8c9 261 my ($self, $local_moniker, $rels, $uniqs) = @_;
996be9ee 262
263 my $all_code = {};
264
e8ad6491 265 my $local_class = $self->{schema}->class($local_moniker);
057fbb08 266
e8ad6491 267 my %counters;
268 foreach my $rel (@$rels) {
269 next if !$rel->{remote_source};
270 $counters{$rel->{remote_source}}++;
271 }
272
273 foreach my $rel (@$rels) {
057fbb08 274 my $remote_moniker = $rel->{remote_source}
275 or next;
276
277 my $remote_class = $self->{schema}->class($remote_moniker);
278 my $remote_obj = $self->{schema}->source($remote_moniker);
279 my $remote_cols = $rel->{remote_columns} || [ $remote_obj->primary_columns ];
280
281 my $local_cols = $rel->{local_columns};
e8ad6491 282
283 if($#$local_cols != $#$remote_cols) {
284 croak "Column count mismatch: $local_moniker (@$local_cols) "
285 . "$remote_moniker (@$remote_cols)";
996be9ee 286 }
287
e8ad6491 288 my %cond;
289 foreach my $i (0 .. $#$local_cols) {
290 $cond{$remote_cols->[$i]} = $local_cols->[$i];
291 }
996be9ee 292
057fbb08 293 my ( $local_relname, $remote_relname, $remote_method ) =
39ef3bfe 294 $self->_relnames_and_method( $local_moniker, $rel, \%cond, $uniqs, \%counters );
7dba7c70 295
e8ad6491 296 push(@{$all_code->{$local_class}},
297 { method => 'belongs_to',
298 args => [ $remote_relname,
299 $remote_class,
300 \%cond,
c39e403e 301 $self->_remote_attrs($local_moniker, $local_cols),
e8ad6491 302 ],
996be9ee 303 }
e8ad6491 304 );
305
057fbb08 306 my %rev_cond = reverse %cond;
307 for (keys %rev_cond) {
308 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
309 delete $rev_cond{$_};
310 }
311
e8ad6491 312 push(@{$all_code->{$remote_class}},
26f1c8c9 313 { method => $remote_method,
e8ad6491 314 args => [ $local_relname,
315 $local_class,
316 \%rev_cond,
c8c27020 317 $self->_relationship_attrs($remote_method),
e8ad6491 318 ],
319 }
320 );
996be9ee 321 }
322
323 return $all_code;
324}
325
39ef3bfe 326sub _relnames_and_method {
057fbb08 327 my ( $self, $local_moniker, $rel, $cond, $uniqs, $counters ) = @_;
e9c09ed9 328
057fbb08 329 my $remote_moniker = $rel->{remote_source};
330 my $remote_obj = $self->{schema}->source( $remote_moniker );
331 my $remote_class = $self->{schema}->class( $remote_moniker );
ecf930e6 332 my $remote_relname = $self->_remote_relname( $remote_obj->from, $cond);
fa6f8d4e 333
057fbb08 334 my $local_cols = $rel->{local_columns};
335 my $local_table = $self->{schema}->source($local_moniker)->from;
336
337 # If more than one rel between this pair of tables, use the local
338 # col names to distinguish
ecf930e6 339 my ($local_relname, $local_relname_uninflected);
057fbb08 340 if ( $counters->{$remote_moniker} > 1) {
19b7d71c 341 my $colnames = q{_} . $self->_normalize_name(join '_', @$local_cols);
057fbb08 342 $remote_relname .= $colnames if keys %$cond > 1;
343
19b7d71c 344 $local_relname = $self->_normalize_name($local_table . $colnames);
c496748b 345 $local_relname =~ s/_id$//;
346
347 $local_relname_uninflected = $local_relname;
19b7d71c 348 $local_relname = $self->_inflect_plural($local_relname);
057fbb08 349 } else {
19b7d71c 350 $local_relname_uninflected = $self->_normalize_name($local_table);
351 $local_relname = $self->_inflect_plural($self->_normalize_name($local_table));
057fbb08 352 }
fa6f8d4e 353
057fbb08 354 my $remote_method = 'has_many';
355
356 # If the local columns have a UNIQUE constraint, this is a one-to-one rel
357 my $local_source = $self->{schema}->source($local_moniker);
ecf930e6 358 if ($self->_array_eq([ $local_source->primary_columns ], $local_cols) ||
359 grep { $self->_array_eq($_->[1], $local_cols) } @$uniqs) {
057fbb08 360 $remote_method = 'might_have';
c496748b 361 $local_relname = $self->_inflect_singular($local_relname_uninflected);
057fbb08 362 }
fa6f8d4e 363
057fbb08 364 return ( $local_relname, $remote_relname, $remote_method );
fa6f8d4e 365}
366
be80bba7 367=head1 AUTHOR
368
9cc8e7e1 369See L<DBIx::Class::Schema::Loader/AUTHOR> and L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
be80bba7 370
371=head1 LICENSE
372
373This library is free software; you can redistribute it and/or modify it under
374the same terms as Perl itself.
375
376=cut
377
996be9ee 3781;
19b7d71c 379# vim:et sts=4 sw=4 tw=0: