Bump version for 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_07';
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 generate_code {
136     my ($self, $local_moniker, $rels, $uniqs) = @_;
137
138     my $all_code = {};
139
140     my $local_table = $self->{schema}->source($local_moniker)->from;
141     my $local_class = $self->{schema}->class($local_moniker);
142         
143     my %counters;
144     foreach my $rel (@$rels) {
145         next if !$rel->{remote_source};
146         $counters{$rel->{remote_source}}++;
147     }
148
149     foreach my $rel (@$rels) {
150         next if !$rel->{remote_source};
151         my $local_cols = $rel->{local_columns};
152         my $remote_cols = $rel->{remote_columns};
153         my $remote_moniker = $rel->{remote_source};
154         my $remote_obj = $self->{schema}->source($remote_moniker);
155         my $remote_class = $self->{schema}->class($remote_moniker);
156         my $remote_table = $remote_obj->from;
157         $remote_cols ||= [ $remote_obj->primary_columns ];
158
159         if($#$local_cols != $#$remote_cols) {
160             croak "Column count mismatch: $local_moniker (@$local_cols) "
161                 . "$remote_moniker (@$remote_cols)";
162         }
163
164         my %cond;
165         foreach my $i (0 .. $#$local_cols) {
166             $cond{$remote_cols->[$i]} = $local_cols->[$i];
167         }
168
169         my $local_relname;
170         my $remote_relname;
171
172         # for single-column case, set the remote relname to the column
173         # name, to make filter accessors work, but strip trailing _id
174         if(scalar keys %cond == 1) {
175             my ($col) = values %cond;
176             $col =~ s/_id$//;
177             $remote_relname = $self->_inflect_singular($col);
178         }
179         else {
180             $remote_relname = $self->_inflect_singular(lc $remote_table);
181         }
182
183         # If more than one rel between this pair of tables, use the local
184         # col names to distinguish
185         if($counters{$remote_moniker} > 1) {
186             my $colnames = q{_} . join(q{_}, @$local_cols);
187             $local_relname = $self->_inflect_plural(
188                 lc($local_table) . $colnames
189             );
190             $remote_relname .= $colnames if keys %cond > 1;
191         } else {
192             $local_relname = $self->_inflect_plural(lc $local_table);
193         }
194
195         my %rev_cond = reverse %cond;
196
197         for (keys %rev_cond) {
198             $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
199             delete $rev_cond{$_};
200         }
201
202         my $remote_method = 'has_many';
203
204         # If the local columns have a UNIQUE constraint, this is a one-to-one rel
205         my $local_source = $self->{schema}->source($local_moniker);
206         if (_array_eq([ $local_source->primary_columns ], $local_cols) ||
207             grep { _array_eq($_->[1], $local_cols) } @$uniqs) {
208             $remote_method = 'might_have';
209             $local_relname = $self->_inflect_singular($local_relname);
210         }
211
212         # If the referring column is nullable, make 'belongs_to' an outer join:
213         my $nullable = grep { $local_source->column_info($_)->{is_nullable} }
214           @$local_cols;
215
216         push(@{$all_code->{$local_class}},
217             { method => 'belongs_to',
218               args => [ $remote_relname,
219                         $remote_class,
220                         \%cond,
221                         $nullable ? { join_type => 'LEFT' } : ()
222               ],
223             }
224         );
225
226         push(@{$all_code->{$remote_class}},
227             { method => $remote_method,
228               args => [ $local_relname,
229                         $local_class,
230                         \%rev_cond,
231               ],
232             }
233         );
234     }
235
236     return $all_code;
237 }
238
239 1;