fixed test failure
[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/;
996be9ee 7use Lingua::EN::Inflect::Number ();
8
83b38372 9our $VERSION = '0.05002';
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
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 {
101 my ($self, $relname) = @_;
102
103 if( ref $self->{inflect_plural} eq 'HASH' ) {
104 return $self->{inflect_plural}->{$relname}
105 if exists $self->{inflect_plural}->{$relname};
106 }
107 elsif( ref $self->{inflect_plural} eq 'CODE' ) {
108 my $inflected = $self->{inflect_plural}->($relname);
109 return $inflected if $inflected;
110 }
111
d3af7a07 112 return Lingua::EN::Inflect::Number::to_PL($relname);
996be9ee 113}
114
115# Singularize a relationship name
116sub _inflect_singular {
117 my ($self, $relname) = @_;
118
119 if( ref $self->{inflect_singular} eq 'HASH' ) {
120 return $self->{inflect_singular}->{$relname}
121 if exists $self->{inflect_singular}->{$relname};
122 }
123 elsif( ref $self->{inflect_singular} eq 'CODE' ) {
124 my $inflected = $self->{inflect_singular}->($relname);
125 return $inflected if $inflected;
126 }
127
d3af7a07 128 return Lingua::EN::Inflect::Number::to_S($relname);
996be9ee 129}
130
c8c27020 131# accessor for options to be passed to each generated relationship
132# type. take single argument, the relationship type name, and returns
133# either a hashref (if some options are set), or nothing
134sub _relationship_attrs {
135 my ( $self, $reltype ) = @_;
136 my $r = $self->{relationship_attrs};
137 return unless $r && ( $r->{all} || $r->{$reltype} );
138
139 my %composite = %{ $r->{all} || {} };
140 if( my $specific = $r->{$reltype} ) {
141 while( my ($k,$v) = each %$specific ) {
142 $composite{$k} = $v;
143 }
144 }
145 return \%composite;
146}
147
26f1c8c9 148sub _array_eq {
149 my ($a, $b) = @_;
150
151 return unless @$a == @$b;
152
153 for (my $i = 0; $i < @$a; $i++) {
154 return unless $a->[$i] eq $b->[$i];
155 }
156 return 1;
157}
158
c39e403e 159sub _remote_attrs {
160 my ($self, $local_moniker, $local_cols) = @_;
161
c8c27020 162 # get our base set of attrs from _relationship_attrs, if present
163 my $attrs = $self->_relationship_attrs('belongs_to') || {};
164
165 # If the referring column is nullable, make 'belongs_to' an
166 # outer join, unless explicitly set by relationship_attrs
c39e403e 167 my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} }
168 @$local_cols;
c8c27020 169 $attrs->{join_type} = 'LEFT'
170 if $nullable && !defined $attrs->{join_type};
c39e403e 171
c8c27020 172 return $attrs;
c39e403e 173}
174
f2fc8d01 175sub _remote_relname {
176 my ($self, $remote_table, $cond) = @_;
177
178 my $remote_relname;
179 # for single-column case, set the remote relname to the column
180 # name, to make filter accessors work, but strip trailing _id
181 if(scalar keys %{$cond} == 1) {
182 my ($col) = values %{$cond};
183 $col =~ s/_id$//;
184 $remote_relname = $self->_inflect_singular($col);
185 }
186 else {
187 $remote_relname = $self->_inflect_singular(lc $remote_table);
188 }
189
190 return $remote_relname;
191}
192
996be9ee 193sub generate_code {
26f1c8c9 194 my ($self, $local_moniker, $rels, $uniqs) = @_;
996be9ee 195
196 my $all_code = {};
197
e8ad6491 198 my $local_class = $self->{schema}->class($local_moniker);
057fbb08 199
e8ad6491 200 my %counters;
201 foreach my $rel (@$rels) {
202 next if !$rel->{remote_source};
203 $counters{$rel->{remote_source}}++;
204 }
205
206 foreach my $rel (@$rels) {
057fbb08 207 my $remote_moniker = $rel->{remote_source}
208 or next;
209
210 my $remote_class = $self->{schema}->class($remote_moniker);
211 my $remote_obj = $self->{schema}->source($remote_moniker);
212 my $remote_cols = $rel->{remote_columns} || [ $remote_obj->primary_columns ];
213
214 my $local_cols = $rel->{local_columns};
e8ad6491 215
216 if($#$local_cols != $#$remote_cols) {
217 croak "Column count mismatch: $local_moniker (@$local_cols) "
218 . "$remote_moniker (@$remote_cols)";
996be9ee 219 }
220
e8ad6491 221 my %cond;
222 foreach my $i (0 .. $#$local_cols) {
223 $cond{$remote_cols->[$i]} = $local_cols->[$i];
224 }
996be9ee 225
057fbb08 226 my ( $local_relname, $remote_relname, $remote_method ) =
227 $self->_relnames_and_methods( $local_moniker, $rel, \%cond, $uniqs, \%counters );
7dba7c70 228
e8ad6491 229 push(@{$all_code->{$local_class}},
230 { method => 'belongs_to',
231 args => [ $remote_relname,
232 $remote_class,
233 \%cond,
c39e403e 234 $self->_remote_attrs($local_moniker, $local_cols),
e8ad6491 235 ],
996be9ee 236 }
e8ad6491 237 );
238
057fbb08 239 my %rev_cond = reverse %cond;
240 for (keys %rev_cond) {
241 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
242 delete $rev_cond{$_};
243 }
244
e8ad6491 245 push(@{$all_code->{$remote_class}},
26f1c8c9 246 { method => $remote_method,
e8ad6491 247 args => [ $local_relname,
248 $local_class,
249 \%rev_cond,
c8c27020 250 $self->_relationship_attrs($remote_method),
e8ad6491 251 ],
252 }
253 );
996be9ee 254 }
255
256 return $all_code;
257}
258
057fbb08 259sub _relnames_and_methods {
260 my ( $self, $local_moniker, $rel, $cond, $uniqs, $counters ) = @_;
e9c09ed9 261
057fbb08 262 my $remote_moniker = $rel->{remote_source};
263 my $remote_obj = $self->{schema}->source( $remote_moniker );
264 my $remote_class = $self->{schema}->class( $remote_moniker );
265 my $remote_relname = $self->_remote_relname( $remote_obj->from, $cond);
fa6f8d4e 266
057fbb08 267 my $local_cols = $rel->{local_columns};
268 my $local_table = $self->{schema}->source($local_moniker)->from;
269
270 # If more than one rel between this pair of tables, use the local
271 # col names to distinguish
272 my $local_relname;
273 if ( $counters->{$remote_moniker} > 1) {
274 my $colnames = q{_} . join(q{_}, @$local_cols);
275 $remote_relname .= $colnames if keys %$cond > 1;
276
277 my $old_relname = #< TODO: remove me after 0.05003 release
ff098bf3 278 $local_relname = lc($local_table) . $colnames;
057fbb08 279 my $stripped_id = $local_relname =~ s/_id$//; #< strip off any trailing _id
280 $local_relname = $self->_inflect_plural( $local_relname );
281
282 # TODO: remove me after 0.05003 release
283 $old_relname = $self->_inflect_plural( $old_relname );
284 warn __PACKAGE__." $VERSION: warning, stripping trailing _id from ${remote_class} relation '$old_relname', renaming to '$local_relname'. This behavior is new as of 0.05003.\n"
285 if $stripped_id;
286
287 } else {
288 $local_relname = $self->_inflect_plural(lc $local_table);
289 }
fa6f8d4e 290
057fbb08 291 my $remote_method = 'has_many';
292
293 # If the local columns have a UNIQUE constraint, this is a one-to-one rel
294 my $local_source = $self->{schema}->source($local_moniker);
295 if (_array_eq([ $local_source->primary_columns ], $local_cols) ||
296 grep { _array_eq($_->[1], $local_cols) } @$uniqs) {
297 $remote_method = 'might_have';
298 $local_relname = $self->_inflect_singular($local_relname);
299 }
fa6f8d4e 300
057fbb08 301 return ( $local_relname, $remote_relname, $remote_method );
fa6f8d4e 302}
303
be80bba7 304=head1 AUTHOR
305
9cc8e7e1 306See L<DBIx::Class::Schema::Loader/AUTHOR> and L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
be80bba7 307
308=head1 LICENSE
309
310This library is free software; you can redistribute it and/or modify it under
311the same terms as Perl itself.
312
313=cut
314
996be9ee 3151;