new dev release
[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
1fa18849 9our $VERSION = '0.04999_13';
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 _uniq_fk_rel {
160 my ($self, $local_moniker, $local_relname, $local_cols, $uniqs) = @_;
161
162 my $remote_method = 'has_many';
163
164 # If the local columns have a UNIQUE constraint, this is a one-to-one rel
165 my $local_source = $self->{schema}->source($local_moniker);
166 if (_array_eq([ $local_source->primary_columns ], $local_cols) ||
167 grep { _array_eq($_->[1], $local_cols) } @$uniqs) {
168 $remote_method = 'might_have';
169 $local_relname = $self->_inflect_singular($local_relname);
170 }
171
172 return ($remote_method, $local_relname);
173}
174
175sub _remote_attrs {
176 my ($self, $local_moniker, $local_cols) = @_;
177
c8c27020 178 # get our base set of attrs from _relationship_attrs, if present
179 my $attrs = $self->_relationship_attrs('belongs_to') || {};
180
181 # If the referring column is nullable, make 'belongs_to' an
182 # outer join, unless explicitly set by relationship_attrs
c39e403e 183 my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} }
184 @$local_cols;
c8c27020 185 $attrs->{join_type} = 'LEFT'
186 if $nullable && !defined $attrs->{join_type};
c39e403e 187
c8c27020 188 return $attrs;
c39e403e 189}
190
f2fc8d01 191sub _remote_relname {
192 my ($self, $remote_table, $cond) = @_;
193
194 my $remote_relname;
195 # for single-column case, set the remote relname to the column
196 # name, to make filter accessors work, but strip trailing _id
197 if(scalar keys %{$cond} == 1) {
198 my ($col) = values %{$cond};
199 $col =~ s/_id$//;
200 $remote_relname = $self->_inflect_singular($col);
201 }
202 else {
203 $remote_relname = $self->_inflect_singular(lc $remote_table);
204 }
205
206 return $remote_relname;
207}
208
996be9ee 209sub generate_code {
26f1c8c9 210 my ($self, $local_moniker, $rels, $uniqs) = @_;
996be9ee 211
212 my $all_code = {};
213
e8ad6491 214 my $local_table = $self->{schema}->source($local_moniker)->from;
215 my $local_class = $self->{schema}->class($local_moniker);
996be9ee 216
e8ad6491 217 my %counters;
218 foreach my $rel (@$rels) {
219 next if !$rel->{remote_source};
220 $counters{$rel->{remote_source}}++;
221 }
222
223 foreach my $rel (@$rels) {
224 next if !$rel->{remote_source};
225 my $local_cols = $rel->{local_columns};
226 my $remote_cols = $rel->{remote_columns};
227 my $remote_moniker = $rel->{remote_source};
228 my $remote_obj = $self->{schema}->source($remote_moniker);
229 my $remote_class = $self->{schema}->class($remote_moniker);
230 my $remote_table = $remote_obj->from;
231 $remote_cols ||= [ $remote_obj->primary_columns ];
232
233 if($#$local_cols != $#$remote_cols) {
234 croak "Column count mismatch: $local_moniker (@$local_cols) "
235 . "$remote_moniker (@$remote_cols)";
996be9ee 236 }
237
e8ad6491 238 my %cond;
239 foreach my $i (0 .. $#$local_cols) {
240 $cond{$remote_cols->[$i]} = $local_cols->[$i];
241 }
996be9ee 242
e8ad6491 243 my $local_relname;
f2fc8d01 244 my $remote_relname = $self->_remote_relname($remote_table, \%cond);
996be9ee 245
54700b71 246 # If more than one rel between this pair of tables, use the local
247 # col names to distinguish
248 if($counters{$remote_moniker} > 1) {
249 my $colnames = q{_} . join(q{_}, @$local_cols);
250 $local_relname = $self->_inflect_plural(
251 lc($local_table) . $colnames
252 );
253 $remote_relname .= $colnames if keys %cond > 1;
254 } else {
255 $local_relname = $self->_inflect_plural(lc $local_table);
256 }
996be9ee 257
e8ad6491 258 my %rev_cond = reverse %cond;
996be9ee 259
e8ad6491 260 for (keys %rev_cond) {
261 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
262 delete $rev_cond{$_};
263 }
996be9ee 264
c39e403e 265 my ($remote_method);
26f1c8c9 266
c39e403e 267 ($remote_method, $local_relname) = $self->_uniq_fk_rel($local_moniker, $local_relname, $local_cols, $uniqs);
7dba7c70 268
e8ad6491 269 push(@{$all_code->{$local_class}},
270 { method => 'belongs_to',
271 args => [ $remote_relname,
272 $remote_class,
273 \%cond,
c39e403e 274 $self->_remote_attrs($local_moniker, $local_cols),
e8ad6491 275 ],
996be9ee 276 }
e8ad6491 277 );
278
279 push(@{$all_code->{$remote_class}},
26f1c8c9 280 { method => $remote_method,
e8ad6491 281 args => [ $local_relname,
282 $local_class,
283 \%rev_cond,
c8c27020 284 $self->_relationship_attrs($remote_method),
e8ad6491 285 ],
286 }
287 );
996be9ee 288 }
289
290 return $all_code;
291}
292
be80bba7 293=head1 AUTHOR
294
9cc8e7e1 295See L<DBIx::Class::Schema::Loader/AUTHOR> and L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
be80bba7 296
297=head1 LICENSE
298
299This library is free software; you can redistribute it and/or modify it under
300the same terms as Perl itself.
301
302=cut
303
996be9ee 3041;