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