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;
fa994d3c 5use Carp::Clan qw/^DBIx::Class/;
996be9ee 6use Lingua::EN::Inflect::Number ();
7
c3fb509f 8our $VERSION = '0.04999_09';
32f784fc 9
996be9ee 10=head1 NAME
11
12DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
13
14=head1 SYNOPSIS
15
16See L<DBIx::Class::Schema::Loader>
17
18=head1 DESCRIPTION
19
20This class builds relationships for L<DBIx::Class::Schema::Loader>. This
21is module is not (yet) for external use.
22
23=head1 METHODS
24
25=head2 new
26
e8ad6491 27Arguments: schema_class (scalar), inflect_plural, inflect_singular
996be9ee 28
29C<$schema_class> should be a schema class name, where the source
30classes have already been set up and registered. Column info, primary
31key, and unique constraints will be drawn from this schema for all
32of the existing source monikers.
33
996be9ee 34Options inflect_plural and inflect_singular are optional, and are better documented
35in L<DBIx::Class::Schema::Loader::Base>.
36
37=head2 generate_code
38
e8ad6491 39Arguments: local_moniker (scalar), fk_info (arrayref)
40
41This generates the code for the relationships of a given table.
42
43C<local_moniker> is the moniker name of the table which had the REFERENCES
44statements. 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
60This method will return the generated relationships as a hashref keyed on the
61class names. The values are arrayrefs of hashes containing method name and
62arguments, like so:
996be9ee 63
64 {
65 'Some::Source::Class' => [
b97c2c1e 66 { method => 'belongs_to', arguments => [ 'col1', 'Another::Source::Class' ],
67 { method => 'has_many', arguments => [ 'anothers', 'Yet::Another::Source::Class', 'col15' ],
996be9ee 68 ],
69 'Another::Source::Class' => [
70 # ...
71 ],
72 # ...
73 }
8f9d7ce5 74
996be9ee 75=cut
76
77sub new {
e8ad6491 78 my ( $class, $schema, $inflect_pl, $inflect_singular ) = @_;
996be9ee 79
80 my $self = {
81 schema => $schema,
996be9ee 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
93sub _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
d3af7a07 105 return Lingua::EN::Inflect::Number::to_PL($relname);
996be9ee 106}
107
108# Singularize a relationship name
109sub _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
d3af7a07 121 return Lingua::EN::Inflect::Number::to_S($relname);
996be9ee 122}
123
26f1c8c9 124sub _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
c39e403e 135sub _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
151sub _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
f2fc8d01 161sub _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
996be9ee 179sub generate_code {
26f1c8c9 180 my ($self, $local_moniker, $rels, $uniqs) = @_;
996be9ee 181
182 my $all_code = {};
183
e8ad6491 184 my $local_table = $self->{schema}->source($local_moniker)->from;
185 my $local_class = $self->{schema}->class($local_moniker);
996be9ee 186
e8ad6491 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)";
996be9ee 206 }
207
e8ad6491 208 my %cond;
209 foreach my $i (0 .. $#$local_cols) {
210 $cond{$remote_cols->[$i]} = $local_cols->[$i];
211 }
996be9ee 212
e8ad6491 213 my $local_relname;
f2fc8d01 214 my $remote_relname = $self->_remote_relname($remote_table, \%cond);
996be9ee 215
54700b71 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 }
996be9ee 227
e8ad6491 228 my %rev_cond = reverse %cond;
996be9ee 229
e8ad6491 230 for (keys %rev_cond) {
231 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
232 delete $rev_cond{$_};
233 }
996be9ee 234
c39e403e 235 my ($remote_method);
26f1c8c9 236
c39e403e 237 ($remote_method, $local_relname) = $self->_uniq_fk_rel($local_moniker, $local_relname, $local_cols, $uniqs);
7dba7c70 238
e8ad6491 239 push(@{$all_code->{$local_class}},
240 { method => 'belongs_to',
241 args => [ $remote_relname,
242 $remote_class,
243 \%cond,
c39e403e 244 $self->_remote_attrs($local_moniker, $local_cols),
e8ad6491 245 ],
996be9ee 246 }
e8ad6491 247 );
248
249 push(@{$all_code->{$remote_class}},
26f1c8c9 250 { method => $remote_method,
e8ad6491 251 args => [ $local_relname,
252 $local_class,
253 \%rev_cond,
254 ],
255 }
256 );
996be9ee 257 }
258
259 return $all_code;
260}
261
2621;