Move misplaced changelog entries to the right version.
[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
457c3335 8our $VERSION = '0.04999_07';
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
996be9ee 161sub generate_code {
26f1c8c9 162 my ($self, $local_moniker, $rels, $uniqs) = @_;
996be9ee 163
164 my $all_code = {};
165
e8ad6491 166 my $local_table = $self->{schema}->source($local_moniker)->from;
167 my $local_class = $self->{schema}->class($local_moniker);
996be9ee 168
e8ad6491 169 my %counters;
170 foreach my $rel (@$rels) {
171 next if !$rel->{remote_source};
172 $counters{$rel->{remote_source}}++;
173 }
174
175 foreach my $rel (@$rels) {
176 next if !$rel->{remote_source};
177 my $local_cols = $rel->{local_columns};
178 my $remote_cols = $rel->{remote_columns};
179 my $remote_moniker = $rel->{remote_source};
180 my $remote_obj = $self->{schema}->source($remote_moniker);
181 my $remote_class = $self->{schema}->class($remote_moniker);
182 my $remote_table = $remote_obj->from;
183 $remote_cols ||= [ $remote_obj->primary_columns ];
184
185 if($#$local_cols != $#$remote_cols) {
186 croak "Column count mismatch: $local_moniker (@$local_cols) "
187 . "$remote_moniker (@$remote_cols)";
996be9ee 188 }
189
e8ad6491 190 my %cond;
191 foreach my $i (0 .. $#$local_cols) {
192 $cond{$remote_cols->[$i]} = $local_cols->[$i];
193 }
996be9ee 194
e8ad6491 195 my $local_relname;
e8ad6491 196 my $remote_relname;
996be9ee 197
54700b71 198 # for single-column case, set the remote relname to the column
e7886624 199 # name, to make filter accessors work, but strip trailing _id
e8ad6491 200 if(scalar keys %cond == 1) {
e7886624 201 my ($col) = values %cond;
202 $col =~ s/_id$//;
203 $remote_relname = $self->_inflect_singular($col);
e8ad6491 204 }
205 else {
206 $remote_relname = $self->_inflect_singular(lc $remote_table);
207 }
996be9ee 208
54700b71 209 # If more than one rel between this pair of tables, use the local
210 # col names to distinguish
211 if($counters{$remote_moniker} > 1) {
212 my $colnames = q{_} . join(q{_}, @$local_cols);
213 $local_relname = $self->_inflect_plural(
214 lc($local_table) . $colnames
215 );
216 $remote_relname .= $colnames if keys %cond > 1;
217 } else {
218 $local_relname = $self->_inflect_plural(lc $local_table);
219 }
996be9ee 220
e8ad6491 221 my %rev_cond = reverse %cond;
996be9ee 222
e8ad6491 223 for (keys %rev_cond) {
224 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
225 delete $rev_cond{$_};
226 }
996be9ee 227
c39e403e 228 my ($remote_method);
26f1c8c9 229
c39e403e 230 ($remote_method, $local_relname) = $self->_uniq_fk_rel($local_moniker, $local_relname, $local_cols, $uniqs);
7dba7c70 231
e8ad6491 232 push(@{$all_code->{$local_class}},
233 { method => 'belongs_to',
234 args => [ $remote_relname,
235 $remote_class,
236 \%cond,
c39e403e 237 $self->_remote_attrs($local_moniker, $local_cols),
e8ad6491 238 ],
996be9ee 239 }
e8ad6491 240 );
241
242 push(@{$all_code->{$remote_class}},
26f1c8c9 243 { method => $remote_method,
e8ad6491 244 args => [ $local_relname,
245 $local_class,
246 \%rev_cond,
247 ],
248 }
249 );
996be9ee 250 }
251
252 return $all_code;
253}
254
2551;