handle CamelCase columns for making relnames
[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/;
39b22ca9 7use Lingua::EN::Inflect::Phrase ();
996be9ee 8
9990e58f 9our $VERSION = '0.07000';
32f784fc 10
996be9ee 11=head1 NAME
12
13DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
14
15=head1 SYNOPSIS
16
19b7d71c 17See L<DBIx::Class::Schema::Loader> and L<DBIx::Class::Schema::Loader::Base>.
996be9ee 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 my ( $class, $schema, $inflect_pl, $inflect_singular, $rel_attrs ) = @_;
996be9ee 80
81 my $self = {
82 schema => $schema,
996be9ee 83 inflect_plural => $inflect_pl,
84 inflect_singular => $inflect_singular,
c8c27020 85 relationship_attrs => $rel_attrs,
996be9ee 86 };
87
c8c27020 88 # validate the relationship_attrs arg
89 if( defined $self->{relationship_attrs} ) {
90 ref($self->{relationship_attrs}) eq 'HASH'
91 or croak "relationship_attrs must be a hashref";
92 }
996be9ee 93
c8c27020 94 return bless $self => $class;
996be9ee 95}
96
97
98# pluralize a relationship name
99sub _inflect_plural {
ecf930e6 100 my ($self, $relname) = @_;
996be9ee 101
39ef3bfe 102 return '' if !defined $relname || $relname eq '';
103
996be9ee 104 if( ref $self->{inflect_plural} eq 'HASH' ) {
105 return $self->{inflect_plural}->{$relname}
106 if exists $self->{inflect_plural}->{$relname};
107 }
108 elsif( ref $self->{inflect_plural} eq 'CODE' ) {
109 my $inflected = $self->{inflect_plural}->($relname);
110 return $inflected if $inflected;
111 }
112
ecf930e6 113 return $self->_to_PL($relname);
996be9ee 114}
115
116# Singularize a relationship name
117sub _inflect_singular {
ecf930e6 118 my ($self, $relname) = @_;
996be9ee 119
39ef3bfe 120 return '' if !defined $relname || $relname eq '';
121
996be9ee 122 if( ref $self->{inflect_singular} eq 'HASH' ) {
123 return $self->{inflect_singular}->{$relname}
124 if exists $self->{inflect_singular}->{$relname};
125 }
126 elsif( ref $self->{inflect_singular} eq 'CODE' ) {
127 my $inflected = $self->{inflect_singular}->($relname);
128 return $inflected if $inflected;
129 }
130
ecf930e6 131 return $self->_to_S($relname);
c496748b 132}
133
134sub _to_PL {
135 my ($self, $name) = @_;
136
137 $name =~ s/_/ /g;
39b22ca9 138 my $plural = Lingua::EN::Inflect::Phrase::to_PL($name);
c496748b 139 $plural =~ s/ /_/g;
140
141 return $plural;
142}
143
c496748b 144sub _to_S {
145 my ($self, $name) = @_;
146
39b22ca9 147 $name =~ s/_/ /g;
148 my $singular = Lingua::EN::Inflect::Phrase::to_S($name);
149 $singular =~ s/ /_/g;
150
151 return $singular;
996be9ee 152}
153
53ef681d 154sub _default_relationship_attrs { +{
155 has_many => {
156 cascade_delete => 0,
157 cascade_copy => 0,
158 },
159 might_have => {
160 cascade_delete => 0,
161 cascade_copy => 0,
162 },
163 belongs_to => {
164 on_delete => 'CASCADE',
165 on_update => 'CASCADE',
4f19ef97 166 is_deferrable => 1,
53ef681d 167 },
168} }
169
c8c27020 170# accessor for options to be passed to each generated relationship
171# type. take single argument, the relationship type name, and returns
172# either a hashref (if some options are set), or nothing
173sub _relationship_attrs {
174 my ( $self, $reltype ) = @_;
175 my $r = $self->{relationship_attrs};
c8c27020 176
53ef681d 177 my %composite = (
178 %{ $self->_default_relationship_attrs->{$reltype} || {} },
179 %{ $r->{all} || {} }
180 );
181
c8c27020 182 if( my $specific = $r->{$reltype} ) {
183 while( my ($k,$v) = each %$specific ) {
184 $composite{$k} = $v;
185 }
186 }
187 return \%composite;
188}
189
26f1c8c9 190sub _array_eq {
ecf930e6 191 my ($self, $a, $b) = @_;
26f1c8c9 192
193 return unless @$a == @$b;
194
195 for (my $i = 0; $i < @$a; $i++) {
196 return unless $a->[$i] eq $b->[$i];
197 }
198 return 1;
199}
200
c39e403e 201sub _remote_attrs {
c496748b 202 my ($self, $local_moniker, $local_cols) = @_;
c39e403e 203
c496748b 204 # get our base set of attrs from _relationship_attrs, if present
205 my $attrs = $self->_relationship_attrs('belongs_to') || {};
c8c27020 206
c496748b 207 # If the referring column is nullable, make 'belongs_to' an
208 # outer join, unless explicitly set by relationship_attrs
209 my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} } @$local_cols;
210 $attrs->{join_type} = 'LEFT' if $nullable && !defined $attrs->{join_type};
c39e403e 211
c496748b 212 return $attrs;
c39e403e 213}
214
19b7d71c 215sub _normalize_name {
216 my ($self, $name) = @_;
217
218 my @words = split /(?<=[[:lower:]])[\W_]*(?=[[:upper:]])|[\W_]+/, $name;
219
220 return join '_', map lc, @words;
221}
222
f2fc8d01 223sub _remote_relname {
224 my ($self, $remote_table, $cond) = @_;
225
226 my $remote_relname;
227 # for single-column case, set the remote relname to the column
228 # name, to make filter accessors work, but strip trailing _id
229 if(scalar keys %{$cond} == 1) {
230 my ($col) = values %{$cond};
19b7d71c 231 $col = $self->_normalize_name($col);
f2fc8d01 232 $col =~ s/_id$//;
233 $remote_relname = $self->_inflect_singular($col);
234 }
235 else {
19b7d71c 236 $remote_relname = $self->_inflect_singular($self->_normalize_name($remote_table));
f2fc8d01 237 }
238
239 return $remote_relname;
240}
241
996be9ee 242sub generate_code {
26f1c8c9 243 my ($self, $local_moniker, $rels, $uniqs) = @_;
996be9ee 244
245 my $all_code = {};
246
e8ad6491 247 my $local_class = $self->{schema}->class($local_moniker);
057fbb08 248
e8ad6491 249 my %counters;
250 foreach my $rel (@$rels) {
251 next if !$rel->{remote_source};
252 $counters{$rel->{remote_source}}++;
253 }
254
255 foreach my $rel (@$rels) {
057fbb08 256 my $remote_moniker = $rel->{remote_source}
257 or next;
258
259 my $remote_class = $self->{schema}->class($remote_moniker);
260 my $remote_obj = $self->{schema}->source($remote_moniker);
261 my $remote_cols = $rel->{remote_columns} || [ $remote_obj->primary_columns ];
262
263 my $local_cols = $rel->{local_columns};
e8ad6491 264
265 if($#$local_cols != $#$remote_cols) {
266 croak "Column count mismatch: $local_moniker (@$local_cols) "
267 . "$remote_moniker (@$remote_cols)";
996be9ee 268 }
269
e8ad6491 270 my %cond;
271 foreach my $i (0 .. $#$local_cols) {
272 $cond{$remote_cols->[$i]} = $local_cols->[$i];
273 }
996be9ee 274
057fbb08 275 my ( $local_relname, $remote_relname, $remote_method ) =
39ef3bfe 276 $self->_relnames_and_method( $local_moniker, $rel, \%cond, $uniqs, \%counters );
7dba7c70 277
e8ad6491 278 push(@{$all_code->{$local_class}},
279 { method => 'belongs_to',
280 args => [ $remote_relname,
281 $remote_class,
282 \%cond,
c39e403e 283 $self->_remote_attrs($local_moniker, $local_cols),
e8ad6491 284 ],
996be9ee 285 }
e8ad6491 286 );
287
057fbb08 288 my %rev_cond = reverse %cond;
289 for (keys %rev_cond) {
290 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
291 delete $rev_cond{$_};
292 }
293
e8ad6491 294 push(@{$all_code->{$remote_class}},
26f1c8c9 295 { method => $remote_method,
e8ad6491 296 args => [ $local_relname,
297 $local_class,
298 \%rev_cond,
c8c27020 299 $self->_relationship_attrs($remote_method),
e8ad6491 300 ],
301 }
302 );
996be9ee 303 }
304
305 return $all_code;
306}
307
39ef3bfe 308sub _relnames_and_method {
057fbb08 309 my ( $self, $local_moniker, $rel, $cond, $uniqs, $counters ) = @_;
e9c09ed9 310
057fbb08 311 my $remote_moniker = $rel->{remote_source};
312 my $remote_obj = $self->{schema}->source( $remote_moniker );
313 my $remote_class = $self->{schema}->class( $remote_moniker );
ecf930e6 314 my $remote_relname = $self->_remote_relname( $remote_obj->from, $cond);
fa6f8d4e 315
057fbb08 316 my $local_cols = $rel->{local_columns};
317 my $local_table = $self->{schema}->source($local_moniker)->from;
318
319 # If more than one rel between this pair of tables, use the local
320 # col names to distinguish
ecf930e6 321 my ($local_relname, $local_relname_uninflected);
057fbb08 322 if ( $counters->{$remote_moniker} > 1) {
19b7d71c 323 my $colnames = q{_} . $self->_normalize_name(join '_', @$local_cols);
057fbb08 324 $remote_relname .= $colnames if keys %$cond > 1;
325
19b7d71c 326 $local_relname = $self->_normalize_name($local_table . $colnames);
c496748b 327 $local_relname =~ s/_id$//;
328
329 $local_relname_uninflected = $local_relname;
19b7d71c 330 $local_relname = $self->_inflect_plural($local_relname);
057fbb08 331 } else {
19b7d71c 332 $local_relname_uninflected = $self->_normalize_name($local_table);
333 $local_relname = $self->_inflect_plural($self->_normalize_name($local_table));
057fbb08 334 }
fa6f8d4e 335
057fbb08 336 my $remote_method = 'has_many';
337
338 # If the local columns have a UNIQUE constraint, this is a one-to-one rel
339 my $local_source = $self->{schema}->source($local_moniker);
ecf930e6 340 if ($self->_array_eq([ $local_source->primary_columns ], $local_cols) ||
341 grep { $self->_array_eq($_->[1], $local_cols) } @$uniqs) {
057fbb08 342 $remote_method = 'might_have';
c496748b 343 $local_relname = $self->_inflect_singular($local_relname_uninflected);
057fbb08 344 }
fa6f8d4e 345
057fbb08 346 return ( $local_relname, $remote_relname, $remote_method );
fa6f8d4e 347}
348
be80bba7 349=head1 AUTHOR
350
9cc8e7e1 351See L<DBIx::Class::Schema::Loader/AUTHOR> and L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
be80bba7 352
353=head1 LICENSE
354
355This library is free software; you can redistribute it and/or modify it under
356the same terms as Perl itself.
357
358=cut
359
996be9ee 3601;
19b7d71c 361# vim:et sts=4 sw=4 tw=0: