dev release
[dbsrgits/DBIx-Class-Schema-Loader.git] / lib / DBIx / Class / Schema / Loader / RelBuilder.pm
1 package DBIx::Class::Schema::Loader::RelBuilder;
2
3 use strict;
4 use warnings;
5 use Carp::Clan qw/^DBIx::Class/;
6 use Lingua::EN::Inflect::Number ();
7
8 our $VERSION = '0.04999_08';
9
10 =head1 NAME
11
12 DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
13
14 =head1 SYNOPSIS
15
16 See L<DBIx::Class::Schema::Loader>
17
18 =head1 DESCRIPTION
19
20 This class builds relationships for L<DBIx::Class::Schema::Loader>.  This
21 is module is not (yet) for external use.
22
23 =head1 METHODS
24
25 =head2 new
26
27 Arguments: schema_class (scalar), inflect_plural, inflect_singular
28
29 C<$schema_class> should be a schema class name, where the source
30 classes have already been set up and registered.  Column info, primary
31 key, and unique constraints will be drawn from this schema for all
32 of the existing source monikers.
33
34 Options inflect_plural and inflect_singular are optional, and are better documented
35 in L<DBIx::Class::Schema::Loader::Base>.
36
37 =head2 generate_code
38
39 Arguments: local_moniker (scalar), fk_info (arrayref)
40
41 This generates the code for the relationships of a given table.
42
43 C<local_moniker> is the moniker name of the table which had the REFERENCES
44 statements.  The fk_info arrayref's contents should take the form:
45
46     [
47         {
48             local_columns => [ 'col2', 'col3' ],
49             remote_columns => [ 'col5', 'col7' ],
50             remote_moniker => 'AnotherTableMoniker',
51         },
52         {
53             local_columns => [ 'col1', 'col4' ],
54             remote_columns => [ 'col1', 'col2' ],
55             remote_moniker => 'YetAnotherTableMoniker',
56         },
57         # ...
58     ],
59
60 This method will return the generated relationships as a hashref keyed on the
61 class names.  The values are arrayrefs of hashes containing method name and
62 arguments, like so:
63
64   {
65       'Some::Source::Class' => [
66           { method => 'belongs_to', arguments => [ 'col1', 'Another::Source::Class' ],
67           { method => 'has_many', arguments => [ 'anothers', 'Yet::Another::Source::Class', 'col15' ],
68       ],
69       'Another::Source::Class' => [
70           # ...
71       ],
72       # ...
73   }
74
75 =cut
76
77 sub new {
78     my ( $class, $schema, $inflect_pl, $inflect_singular ) = @_;
79
80     my $self = {
81         schema => $schema,
82         inflect_plural => $inflect_pl,
83         inflect_singular => $inflect_singular,
84     };
85
86     bless $self => $class;
87
88     $self;
89 }
90
91
92 # pluralize a relationship name
93 sub _inflect_plural {
94     my ($self, $relname) = @_;
95
96     if( ref $self->{inflect_plural} eq 'HASH' ) {
97         return $self->{inflect_plural}->{$relname}
98             if exists $self->{inflect_plural}->{$relname};
99     }
100     elsif( ref $self->{inflect_plural} eq 'CODE' ) {
101         my $inflected = $self->{inflect_plural}->($relname);
102         return $inflected if $inflected;
103     }
104
105     return Lingua::EN::Inflect::Number::to_PL($relname);
106 }
107
108 # Singularize a relationship name
109 sub _inflect_singular {
110     my ($self, $relname) = @_;
111
112     if( ref $self->{inflect_singular} eq 'HASH' ) {
113         return $self->{inflect_singular}->{$relname}
114             if exists $self->{inflect_singular}->{$relname};
115     }
116     elsif( ref $self->{inflect_singular} eq 'CODE' ) {
117         my $inflected = $self->{inflect_singular}->($relname);
118         return $inflected if $inflected;
119     }
120
121     return Lingua::EN::Inflect::Number::to_S($relname);
122 }
123
124 sub _array_eq {
125     my ($a, $b) = @_;
126
127     return unless @$a == @$b;
128
129     for (my $i = 0; $i < @$a; $i++) {
130         return unless $a->[$i] eq $b->[$i];
131     }
132     return 1;
133 }
134
135 sub _uniq_fk_rel {
136     my ($self, $local_moniker, $local_relname, $local_cols, $uniqs) = @_;
137
138     my $remote_method = 'has_many';
139
140     # If the local columns have a UNIQUE constraint, this is a one-to-one rel
141     my $local_source = $self->{schema}->source($local_moniker);
142     if (_array_eq([ $local_source->primary_columns ], $local_cols) ||
143             grep { _array_eq($_->[1], $local_cols) } @$uniqs) {
144         $remote_method = 'might_have';
145         $local_relname = $self->_inflect_singular($local_relname);
146     }
147
148     return ($remote_method, $local_relname);
149 }
150
151 sub _remote_attrs {
152         my ($self, $local_moniker, $local_cols) = @_;
153
154         # If the referring column is nullable, make 'belongs_to' an outer join:
155         my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} }
156                 @$local_cols;
157
158         return $nullable ? { join_type => 'LEFT' } : ();
159 }
160
161 sub _remote_relname {
162     my ($self, $remote_table, $cond) = @_;
163
164     my $remote_relname;
165     # for single-column case, set the remote relname to the column
166     # name, to make filter accessors work, but strip trailing _id
167     if(scalar keys %{$cond} == 1) {
168         my ($col) = values %{$cond};
169         $col =~ s/_id$//;
170         $remote_relname = $self->_inflect_singular($col);
171     }
172     else {
173         $remote_relname = $self->_inflect_singular(lc $remote_table);
174     }
175
176     return $remote_relname;
177 }
178
179 sub generate_code {
180     my ($self, $local_moniker, $rels, $uniqs) = @_;
181
182     my $all_code = {};
183
184     my $local_table = $self->{schema}->source($local_moniker)->from;
185     my $local_class = $self->{schema}->class($local_moniker);
186         
187     my %counters;
188     foreach my $rel (@$rels) {
189         next if !$rel->{remote_source};
190         $counters{$rel->{remote_source}}++;
191     }
192
193     foreach my $rel (@$rels) {
194         next if !$rel->{remote_source};
195         my $local_cols = $rel->{local_columns};
196         my $remote_cols = $rel->{remote_columns};
197         my $remote_moniker = $rel->{remote_source};
198         my $remote_obj = $self->{schema}->source($remote_moniker);
199         my $remote_class = $self->{schema}->class($remote_moniker);
200         my $remote_table = $remote_obj->from;
201         $remote_cols ||= [ $remote_obj->primary_columns ];
202
203         if($#$local_cols != $#$remote_cols) {
204             croak "Column count mismatch: $local_moniker (@$local_cols) "
205                 . "$remote_moniker (@$remote_cols)";
206         }
207
208         my %cond;
209         foreach my $i (0 .. $#$local_cols) {
210             $cond{$remote_cols->[$i]} = $local_cols->[$i];
211         }
212
213         my $local_relname;
214         my $remote_relname = $self->_remote_relname($remote_table, \%cond);
215
216         # If more than one rel between this pair of tables, use the local
217         # col names to distinguish
218         if($counters{$remote_moniker} > 1) {
219             my $colnames = q{_} . join(q{_}, @$local_cols);
220             $local_relname = $self->_inflect_plural(
221                 lc($local_table) . $colnames
222             );
223             $remote_relname .= $colnames if keys %cond > 1;
224         } else {
225             $local_relname = $self->_inflect_plural(lc $local_table);
226         }
227
228         my %rev_cond = reverse %cond;
229
230         for (keys %rev_cond) {
231             $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
232             delete $rev_cond{$_};
233         }
234
235         my ($remote_method);
236
237         ($remote_method, $local_relname) = $self->_uniq_fk_rel($local_moniker, $local_relname, $local_cols, $uniqs);
238
239         push(@{$all_code->{$local_class}},
240             { method => 'belongs_to',
241               args => [ $remote_relname,
242                         $remote_class,
243                         \%cond,
244                         $self->_remote_attrs($local_moniker, $local_cols),
245               ],
246             }
247         );
248
249         push(@{$all_code->{$remote_class}},
250             { method => $remote_method,
251               args => [ $local_relname,
252                         $local_class,
253                         \%rev_cond,
254               ],
255             }
256         );
257     }
258
259     return $all_code;
260 }
261
262 1;