set up v8 relbuilder, strip _ref as well as _id
[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 ();
cc4f11a2 8use DBIx::Class::Schema::Loader::Utils 'split_name';
996be9ee 9
f3cfe4e6 10our $VERSION = '0.08000';
32f784fc 11
996be9ee 12=head1 NAME
13
14DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
15
16=head1 SYNOPSIS
17
19b7d71c 18See L<DBIx::Class::Schema::Loader> and L<DBIx::Class::Schema::Loader::Base>.
996be9ee 19
20=head1 DESCRIPTION
21
22This class builds relationships for L<DBIx::Class::Schema::Loader>. This
23is module is not (yet) for external use.
24
25=head1 METHODS
26
27=head2 new
28
e8ad6491 29Arguments: schema_class (scalar), inflect_plural, inflect_singular
996be9ee 30
31C<$schema_class> should be a schema class name, where the source
32classes have already been set up and registered. Column info, primary
33key, and unique constraints will be drawn from this schema for all
34of the existing source monikers.
35
996be9ee 36Options inflect_plural and inflect_singular are optional, and are better documented
37in L<DBIx::Class::Schema::Loader::Base>.
38
39=head2 generate_code
40
e8ad6491 41Arguments: local_moniker (scalar), fk_info (arrayref)
42
43This generates the code for the relationships of a given table.
44
45C<local_moniker> is the moniker name of the table which had the REFERENCES
46statements. The fk_info arrayref's contents should take the form:
47
48 [
49 {
50 local_columns => [ 'col2', 'col3' ],
51 remote_columns => [ 'col5', 'col7' ],
52 remote_moniker => 'AnotherTableMoniker',
53 },
54 {
55 local_columns => [ 'col1', 'col4' ],
56 remote_columns => [ 'col1', 'col2' ],
57 remote_moniker => 'YetAnotherTableMoniker',
58 },
59 # ...
60 ],
61
62This method will return the generated relationships as a hashref keyed on the
63class names. The values are arrayrefs of hashes containing method name and
64arguments, like so:
996be9ee 65
66 {
67 'Some::Source::Class' => [
b97c2c1e 68 { method => 'belongs_to', arguments => [ 'col1', 'Another::Source::Class' ],
69 { method => 'has_many', arguments => [ 'anothers', 'Yet::Another::Source::Class', 'col15' ],
996be9ee 70 ],
71 'Another::Source::Class' => [
72 # ...
73 ],
74 # ...
75 }
8f9d7ce5 76
996be9ee 77=cut
78
79sub new {
c8c27020 80 my ( $class, $schema, $inflect_pl, $inflect_singular, $rel_attrs ) = @_;
996be9ee 81
82 my $self = {
83 schema => $schema,
996be9ee 84 inflect_plural => $inflect_pl,
85 inflect_singular => $inflect_singular,
c8c27020 86 relationship_attrs => $rel_attrs,
996be9ee 87 };
88
c8c27020 89 # validate the relationship_attrs arg
90 if( defined $self->{relationship_attrs} ) {
91 ref($self->{relationship_attrs}) eq 'HASH'
92 or croak "relationship_attrs must be a hashref";
93 }
996be9ee 94
c8c27020 95 return bless $self => $class;
996be9ee 96}
97
98
99# pluralize a relationship name
100sub _inflect_plural {
ecf930e6 101 my ($self, $relname) = @_;
996be9ee 102
39ef3bfe 103 return '' if !defined $relname || $relname eq '';
104
996be9ee 105 if( ref $self->{inflect_plural} eq 'HASH' ) {
106 return $self->{inflect_plural}->{$relname}
107 if exists $self->{inflect_plural}->{$relname};
108 }
109 elsif( ref $self->{inflect_plural} eq 'CODE' ) {
110 my $inflected = $self->{inflect_plural}->($relname);
111 return $inflected if $inflected;
112 }
113
ecf930e6 114 return $self->_to_PL($relname);
996be9ee 115}
116
117# Singularize a relationship name
118sub _inflect_singular {
ecf930e6 119 my ($self, $relname) = @_;
996be9ee 120
39ef3bfe 121 return '' if !defined $relname || $relname eq '';
122
996be9ee 123 if( ref $self->{inflect_singular} eq 'HASH' ) {
124 return $self->{inflect_singular}->{$relname}
125 if exists $self->{inflect_singular}->{$relname};
126 }
127 elsif( ref $self->{inflect_singular} eq 'CODE' ) {
128 my $inflected = $self->{inflect_singular}->($relname);
129 return $inflected if $inflected;
130 }
131
ecf930e6 132 return $self->_to_S($relname);
c496748b 133}
134
135sub _to_PL {
136 my ($self, $name) = @_;
137
138 $name =~ s/_/ /g;
39b22ca9 139 my $plural = Lingua::EN::Inflect::Phrase::to_PL($name);
c496748b 140 $plural =~ s/ /_/g;
141
142 return $plural;
143}
144
c496748b 145sub _to_S {
146 my ($self, $name) = @_;
147
39b22ca9 148 $name =~ s/_/ /g;
149 my $singular = Lingua::EN::Inflect::Phrase::to_S($name);
150 $singular =~ s/ /_/g;
151
152 return $singular;
996be9ee 153}
154
53ef681d 155sub _default_relationship_attrs { +{
156 has_many => {
157 cascade_delete => 0,
158 cascade_copy => 0,
159 },
160 might_have => {
161 cascade_delete => 0,
162 cascade_copy => 0,
163 },
164 belongs_to => {
165 on_delete => 'CASCADE',
166 on_update => 'CASCADE',
6818ce9f 167# is_deferrable => 1,
53ef681d 168 },
169} }
170
c8c27020 171# accessor for options to be passed to each generated relationship
172# type. take single argument, the relationship type name, and returns
173# either a hashref (if some options are set), or nothing
174sub _relationship_attrs {
175 my ( $self, $reltype ) = @_;
176 my $r = $self->{relationship_attrs};
c8c27020 177
53ef681d 178 my %composite = (
179 %{ $self->_default_relationship_attrs->{$reltype} || {} },
180 %{ $r->{all} || {} }
181 );
182
c8c27020 183 if( my $specific = $r->{$reltype} ) {
184 while( my ($k,$v) = each %$specific ) {
185 $composite{$k} = $v;
186 }
187 }
188 return \%composite;
189}
190
f3cfe4e6 191sub _strip__id {
192 my ($self, $name) = @_;
193
194 $name =~ s/_(?:id|ref)\z//;
195
196 return $name;
197}
198
26f1c8c9 199sub _array_eq {
ecf930e6 200 my ($self, $a, $b) = @_;
26f1c8c9 201
202 return unless @$a == @$b;
203
204 for (my $i = 0; $i < @$a; $i++) {
205 return unless $a->[$i] eq $b->[$i];
206 }
207 return 1;
208}
209
c39e403e 210sub _remote_attrs {
c496748b 211 my ($self, $local_moniker, $local_cols) = @_;
c39e403e 212
c496748b 213 # get our base set of attrs from _relationship_attrs, if present
214 my $attrs = $self->_relationship_attrs('belongs_to') || {};
c8c27020 215
c496748b 216 # If the referring column is nullable, make 'belongs_to' an
217 # outer join, unless explicitly set by relationship_attrs
218 my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} } @$local_cols;
219 $attrs->{join_type} = 'LEFT' if $nullable && !defined $attrs->{join_type};
c39e403e 220
c496748b 221 return $attrs;
c39e403e 222}
223
414c61a0 224sub _sanitize_name {
225 my ($self, $name) = @_;
226
227 if (ref $name) {
228 # scalar ref for weird table name (like one containing a '.')
229 ($name = $$name) =~ s/\W+/_/g;
230 }
231 else {
232 # remove 'schema.' prefix if any
233 $name =~ s/^[^.]+\.//;
234 }
235
236 return $name;
237}
238
19b7d71c 239sub _normalize_name {
240 my ($self, $name) = @_;
241
414c61a0 242 $name = $self->_sanitize_name($name);
243
cc4f11a2 244 my @words = split_name $name;
19b7d71c 245
246 return join '_', map lc, @words;
247}
248
f2fc8d01 249sub _remote_relname {
250 my ($self, $remote_table, $cond) = @_;
251
252 my $remote_relname;
253 # for single-column case, set the remote relname to the column
254 # name, to make filter accessors work, but strip trailing _id
255 if(scalar keys %{$cond} == 1) {
256 my ($col) = values %{$cond};
19b7d71c 257 $col = $self->_normalize_name($col);
f3cfe4e6 258 $col = $self->_strip__id($col);
f2fc8d01 259 $remote_relname = $self->_inflect_singular($col);
260 }
261 else {
19b7d71c 262 $remote_relname = $self->_inflect_singular($self->_normalize_name($remote_table));
f2fc8d01 263 }
264
265 return $remote_relname;
266}
267
996be9ee 268sub generate_code {
26f1c8c9 269 my ($self, $local_moniker, $rels, $uniqs) = @_;
996be9ee 270
271 my $all_code = {};
272
e8ad6491 273 my $local_class = $self->{schema}->class($local_moniker);
057fbb08 274
e8ad6491 275 my %counters;
276 foreach my $rel (@$rels) {
277 next if !$rel->{remote_source};
278 $counters{$rel->{remote_source}}++;
279 }
280
281 foreach my $rel (@$rels) {
057fbb08 282 my $remote_moniker = $rel->{remote_source}
283 or next;
284
285 my $remote_class = $self->{schema}->class($remote_moniker);
286 my $remote_obj = $self->{schema}->source($remote_moniker);
287 my $remote_cols = $rel->{remote_columns} || [ $remote_obj->primary_columns ];
288
289 my $local_cols = $rel->{local_columns};
e8ad6491 290
291 if($#$local_cols != $#$remote_cols) {
292 croak "Column count mismatch: $local_moniker (@$local_cols) "
293 . "$remote_moniker (@$remote_cols)";
996be9ee 294 }
295
e8ad6491 296 my %cond;
297 foreach my $i (0 .. $#$local_cols) {
298 $cond{$remote_cols->[$i]} = $local_cols->[$i];
299 }
996be9ee 300
057fbb08 301 my ( $local_relname, $remote_relname, $remote_method ) =
39ef3bfe 302 $self->_relnames_and_method( $local_moniker, $rel, \%cond, $uniqs, \%counters );
7dba7c70 303
e8ad6491 304 push(@{$all_code->{$local_class}},
305 { method => 'belongs_to',
306 args => [ $remote_relname,
307 $remote_class,
308 \%cond,
c39e403e 309 $self->_remote_attrs($local_moniker, $local_cols),
e8ad6491 310 ],
996be9ee 311 }
e8ad6491 312 );
313
057fbb08 314 my %rev_cond = reverse %cond;
315 for (keys %rev_cond) {
316 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
317 delete $rev_cond{$_};
318 }
319
e8ad6491 320 push(@{$all_code->{$remote_class}},
26f1c8c9 321 { method => $remote_method,
e8ad6491 322 args => [ $local_relname,
323 $local_class,
324 \%rev_cond,
c8c27020 325 $self->_relationship_attrs($remote_method),
e8ad6491 326 ],
327 }
328 );
996be9ee 329 }
330
331 return $all_code;
332}
333
39ef3bfe 334sub _relnames_and_method {
057fbb08 335 my ( $self, $local_moniker, $rel, $cond, $uniqs, $counters ) = @_;
e9c09ed9 336
057fbb08 337 my $remote_moniker = $rel->{remote_source};
338 my $remote_obj = $self->{schema}->source( $remote_moniker );
339 my $remote_class = $self->{schema}->class( $remote_moniker );
ecf930e6 340 my $remote_relname = $self->_remote_relname( $remote_obj->from, $cond);
fa6f8d4e 341
057fbb08 342 my $local_cols = $rel->{local_columns};
343 my $local_table = $self->{schema}->source($local_moniker)->from;
344
345 # If more than one rel between this pair of tables, use the local
346 # col names to distinguish
ecf930e6 347 my ($local_relname, $local_relname_uninflected);
057fbb08 348 if ( $counters->{$remote_moniker} > 1) {
19b7d71c 349 my $colnames = q{_} . $self->_normalize_name(join '_', @$local_cols);
057fbb08 350 $remote_relname .= $colnames if keys %$cond > 1;
351
19b7d71c 352 $local_relname = $self->_normalize_name($local_table . $colnames);
f3cfe4e6 353 $local_relname = $self->_strip__id($local_relname);
c496748b 354
355 $local_relname_uninflected = $local_relname;
19b7d71c 356 $local_relname = $self->_inflect_plural($local_relname);
057fbb08 357 } else {
19b7d71c 358 $local_relname_uninflected = $self->_normalize_name($local_table);
359 $local_relname = $self->_inflect_plural($self->_normalize_name($local_table));
057fbb08 360 }
fa6f8d4e 361
057fbb08 362 my $remote_method = 'has_many';
363
364 # If the local columns have a UNIQUE constraint, this is a one-to-one rel
365 my $local_source = $self->{schema}->source($local_moniker);
ecf930e6 366 if ($self->_array_eq([ $local_source->primary_columns ], $local_cols) ||
367 grep { $self->_array_eq($_->[1], $local_cols) } @$uniqs) {
057fbb08 368 $remote_method = 'might_have';
c496748b 369 $local_relname = $self->_inflect_singular($local_relname_uninflected);
057fbb08 370 }
fa6f8d4e 371
057fbb08 372 return ( $local_relname, $remote_relname, $remote_method );
fa6f8d4e 373}
374
be80bba7 375=head1 AUTHOR
376
9cc8e7e1 377See L<DBIx::Class::Schema::Loader/AUTHOR> and L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
be80bba7 378
379=head1 LICENSE
380
381This library is free software; you can redistribute it and/or modify it under
382the same terms as Perl itself.
383
384=cut
385
996be9ee 3861;
19b7d71c 387# vim:et sts=4 sw=4 tw=0: