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