Mark forgotten ::Row::id() method as indirect_sugar
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Schema / SanityChecker.pm
CommitLineData
12e7015a 1package DBIx::Class::Schema::SanityChecker;
2
3use strict;
4use warnings;
5
6use DBIx::Class::_Util qw(
7 dbic_internal_try refdesc uniq serialize
8 describe_class_methods emit_loud_diag
9);
10use DBIx::Class ();
12e7015a 11use Scalar::Util qw( blessed refaddr );
12use namespace::clean;
13
14=head1 NAME
15
16DBIx::Class::Schema::SanityChecker - Extensible "critic" for your Schema class hierarchy
17
18=head1 SYNOPSIS
19
20 package MyApp::Schema;
21 use base 'DBIx::Class::Schema';
22
58c8eea0 23 # this is the default setting
12e7015a 24 __PACKAGE__->schema_sanity_checker('DBIx::Class::Schema::SanityChecker');
25 ...
26
27=head1 DESCRIPTION
28
29This is the default implementation of the Schema and related classes
30L<validation framework|DBIx::Class::Schema/schema_sanity_checker>.
31
58c8eea0 32The validator is B<enabled by default>. See L</Performance considerations>
33for discussion of the runtime effects.
12e7015a 34
35Use of this class begins by invoking L</perform_schema_sanity_checks>
36(usually via L<DBIx::Class::Schema/connection>), which in turn starts
37invoking validators I<C<check_$checkname()>> in the order listed in
38L</available_checks>. For each set of returned errors (if any)
39I<C<format_$checkname_errors()>> is called and the resulting strings are
40passed to L</emit_errors>, where final headers are prepended and the entire
41thing is printed on C<STDERR>.
42
43The class does not provide a constructor, due to the lack of state to be
44passed around: object orientation was chosen purely for the ease of
45overriding parts of the chain of events as described above. The general
46pattern of communicating errors between the individual methods (both
47before and after formatting) is an arrayref of hash references.
48
49=head2 WHY
50
51DBIC existed for more than a decade without any such setup validation
52fanciness, let alone something that is enabled by default (which in turn
53L<isn't free|/Performance considerations>). The reason for this relatively
54drastic change is a set of revamps within the metadata handling framework,
55in order to resolve once and for all problems like
56L<RT#107462|https://rt.cpan.org/Ticket/Display.html?id=107462>,
57L<RT#114440|https://rt.cpan.org/Ticket/Display.html?id=114440>, etc. While
58DBIC internals are now way more robust than they were before, this comes at
59a price: some non-issues in code that has been working for a while, will
60now become hard to explain, or if you are unlucky: B<silent breakages>.
61
62Thus, in order to protect existing codebases to the fullest extent possible,
63the executive decision (and substantial effort) was made to introduce this
64on-by-default setup validation framework. A massive amount of work has been
65invested ensuring that none of the builtin checks emit a false-positive:
66each and every complaint made by these checks B<should be investigated>.
67
68=head2 Performance considerations
69
70First of all - after your connection has been established - there is B<no
71runtime penalty> whenever the checks are enabled.
72
73By default the checks are triggered every time
74L<DBIx::Class::Schema/connection> is called. Thus there is a
75noticeable startup slowdown, most notably during testing (each test is
76effectively a standalone program connecting anew). As an example the test
77execution phase of the L<DBIx::Class::Helpers> C<v2.032002> distribution
78suffers a consistent slowdown of about C<16%>. This is considered a relatively
79small price to pay for the benefits provided.
80
81Nevertheless, there are valid cases for disabling the checks during
82day-to-day development, and having them run only during CI builds. In fact
83the test suite of DBIC does exactly this as can be seen in
84F<t/lib/DBICTest/BaseSchema.pm>:
85
86 ~/dbic_repo$ git show 39636786 | perl -ne "print if 16..61"
87
88Whatever you do, B<please do not disable the checks entirely>: it is not
89worth the risk.
90
91=head3 Perl5.8
92
93The situation with perl interpreters before C<v5.10.0> is sadly more
94complicated: due to lack of built-in L<pluggable mro support|mro>, the
95mechanism used to interrogate various classes is
96L<< B<much> slower|https://github.com/dbsrgits/dbix-class/commit/296248c3 >>.
97As a result the very same version of L<DBIx::Class::Helpers>
98L<mentioned above|/Performance considerations> takes a C<B<220%>> hit on its
99test execution time (these numbers are observed with the speedups of
100L<Class::C3::XS> available, without them the slowdown reaches the whopping
101C<350%>).
102
12e7015a 103It is the author's B<strongest> recommendation to find a way to run the
104checks on your codebase continuously, even if it takes much longer. Refer to
105the last paragraph of L</Performance considerations> above for an example how
106to do this during CI builds only.
107
108=head2 Validations provided by this module
109
110=head3 no_indirect_method_overrides
111
112There are many methods within DBIC which are
113L<"strictly sugar"|DBIx::Class::MethodAttributes/DBIC_method_is_indirect_sugar>
114and should never be overridden by your application (e.g. see warnings at the
115end of L<DBIx::Class::ResultSet/create> and L<DBIx::Class::Schema/connect>).
116Starting with C<v0.082900> DBIC is much more aggressive in calling the
117underlying non-sugar methods directly, which in turn means that almost all
118user-side overrides of sugar methods are never going to be invoked. These
119situations are now reliably detected and reported individually (you may
120end up with a lot of output on C<STDERR> due to this).
121
122Note: B<ANY AND ALL ISSUES> reported by this check B<*MUST*> be resolved
123before upgrading DBIC in production. Malfunctioning business logic and/or
124B<SEVERE DATA LOSS> may result otherwise.
125
126=head3 valid_c3_composition
127
128Looks through everything returned by L</all_schema_related_classes>, and
129for any class that B<does not> already utilize L<c3 MRO|mro/The C3 MRO> a
130L<method shadowing map|App::Isa::Splain/SYNOPSIS> is calculated and then
131compared to the shadowing map as if C<c3 MRO> was requested in the first place.
132Any discrepancies are reported in order to clearly identify L<hard to explain
133bugs|https://blog.afoolishmanifesto.com/posts/mros-and-you> especially when
134encountered within complex inheritance hierarchies.
135
136=head3 no_inheritance_crosscontamination
137
138Checks that every individual L<Schema|DBIx::Class::Schema>,
139L<Storage|DBIx::Class::Storage>, L<ResultSource|DBIx::Class::ResultSource>,
140L<ResultSet|DBIx::Class::ResultSet>
141and L<Result|DBIx::Class::Manual::ResultClass> class does not inherit from
142an unexpected DBIC base class: e.g. an error will be raised if your
143C<MyApp::Schema> inherits from both C<DBIx::Class::Schema> and
144C<DBIx::Class::ResultSet>.
145
146=head1 METHODS
147
148=head2 perform_schema_sanity_checks
149
150=over
151
152=item Arguments: L<$schema|DBIx::Class::Schema>
153
154=item Return Value: unspecified (ignored by caller)
155
156=back
157
158The entry point expected by the
159L<validation framework|DBIx::Class::Schema/schema_sanity_checker>. See
160L</DESCRIPTION> for details.
161
162=cut
163
164sub perform_schema_sanity_checks {
165 my ($self, $schema) = @_;
166
167 local $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'} = {}
168 if
169 # does not make a measurable difference on 5.10+
170 DBIx::Class::_ENV_::OLD_MRO
171 and
172 # the callstack shouldn't really be recursive, but for completeness...
173 ! $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'}
174 ;
175
176 my (@errors_found, $schema_desc);
177 for my $ch ( @{ $self->available_checks } ) {
178
179 my $err = $self->${\"check_$ch"} ( $schema );
180
181 push @errors_found, map
182 {
183 {
184 check_name => $ch,
185 formatted_error => $_,
186 schema_desc => ( $schema_desc ||=
187 ( length ref $schema )
188 ? refdesc $schema
189 : "'$schema'"
190 ),
191 }
192 }
193 @{
194 $self->${\"format_${ch}_errors"} ( $err )
195 ||
196 []
197 }
198 if @$err;
199 }
200
201 $self->emit_errors(\@errors_found)
202 if @errors_found;
203}
204
205=head2 available_checks
206
207=over
208
209=item Arguments: none
210
211=item Return Value: \@list_of_check_names
212
213=back
214
215The list of checks L</perform_schema_sanity_checks> will perform on the
216provided L<$schema|DBIx::Class::Schema> object. For every entry returned
217by this method, there must be a pair of I<C<check_$checkname()>> and
218I<C<format_$checkname_errors()>> methods available.
219
220Override this method to add checks to the
221L<currently available set|/Validations provided by this module>.
222
223=cut
224
225sub available_checks { [qw(
226 valid_c3_composition
227 no_inheritance_crosscontamination
228 no_indirect_method_overrides
229)] }
230
231=head2 emit_errors
232
233=over
234
235=item Arguments: \@list_of_formatted_errors
236
237=item Return Value: unspecified (ignored by caller)
238
239=back
240
241Takes an array reference of individual errors returned by various
242I<C<format_$checkname_errors()>> formatters, and outputs them on C<STDERR>.
243
244This method is the most convenient integration point for a 3rd party logging
245framework.
246
247Each individual error is expected to be a hash reference with all values being
248plain strings as follows:
249
250 {
251 schema_desc => $human_readable_description_of_the_passed_in_schema
252 check_name => $name_of_the_check_as_listed_in_available_checks()
253 formatted_error => $error_text_as_returned_by_format_$checkname_errors()
254 }
255
256If the environment variable C<DBIC_ASSERT_NO_FAILING_SANITY_CHECKS> is set to
257a true value this method will throw an exception with the same text. Those who
258prefer to take no chances could set this variable permanently as part of their
259deployment scripts.
260
261=cut
262
263# *NOT* using carp_unique and the warn framework - make
264# it harder to accidentaly silence problems via $SIG{__WARN__}
265sub emit_errors {
266 #my ($self, $errs) = @_;
267
268 my @final_error_texts = map {
269 sprintf( "Schema %s failed the '%s' sanity check: %s\n",
270 @{$_}{qw( schema_desc check_name formatted_error )}
271 );
272 } @{$_[1]};
273
274 emit_loud_diag(
275 msg => $_
276 ) for @final_error_texts;
277
278 # Do not use the constant - but instead check the env every time
279 # This will allow people to start auditing their apps piecemeal
280 DBIx::Class::Exception->throw( join "\n", @final_error_texts, ' ' )
281 if $ENV{DBIC_ASSERT_NO_FAILING_SANITY_CHECKS};
282}
283
284=head2 all_schema_related_classes
285
286=over
287
288=item Arguments: L<$schema|DBIx::Class::Schema>
289
290=item Return Value: @sorted_list_of_unique_class_names
291
292=back
293
294This is a convenience method providing a list (not an arrayref) of
295"interesting classes" related to the supplied schema. The returned list
296currently contains the following class names:
297
298=over
299
300=item * The L<Schema|DBIx::Class::Schema> class itself
301
302=item * The associated L<Storage|DBIx::Class::Schema/storage> class if any
303
304=item * The classes of all L<registered ResultSource instances|DBIx::Class::Schema/sources> if any
305
306=item * All L<Result|DBIx::Class::ResultSource/result_class> classes for all registered ResultSource instances
307
308=item * All L<ResultSet|DBIx::Class::ResultSource/resultset_class> classes for all registered ResultSource instances
309
310=back
311
312=cut
313
314sub all_schema_related_classes {
315 my ($self, $schema) = @_;
316
317 sort( uniq( map {
318 ( not defined $_ ) ? ()
319 : ( defined blessed $_ ) ? ref $_
320 : $_
321 } (
322 $schema,
323 $schema->storage,
324 ( map {
325 $_,
326 $_->result_class,
327 $_->resultset_class,
328 } map { $schema->source($_) } $schema->sources ),
329 )));
330}
331
332
333sub format_no_indirect_method_overrides_errors {
334 # my ($self, $errors) = @_;
335
336 [ map { sprintf(
337 "Method(s) %s override the convenience shortcut %s::%s(): "
338 . 'it is almost certain these overrides *MAY BE COMPLETELY IGNORED* at '
339 . 'runtime. You MUST reimplement each override to hook a method from the '
340 . "chain of calls within the convenience shortcut as seen when running:\n "
341 . '~$ perl -M%2$s -MDevel::Dwarn -e "Ddie { %3$s => %2$s->can(q(%3$s)) }"',
342 join (', ', map { "$_()" } sort @{ $_->{by} } ),
2781bf35 343 $_->{overridden}{via_class},
344 $_->{overridden}{name},
12e7015a 345 )} @{ $_[1] } ]
346}
347
348sub check_no_indirect_method_overrides {
349 my ($self, $schema) = @_;
350
351 my( @err, $seen_shadowing_configurations );
352
353 METHOD_STACK:
354 for my $method_stack ( map {
355 values %{ describe_class_methods($_)->{methods_with_supers} || {} }
356 } $self->all_schema_related_classes($schema) ) {
357
358 my $nonsugar_methods;
359
360 for (@$method_stack) {
361
362 push @$nonsugar_methods, $_ and next
97940e36 363 unless(
364 $_->{attributes}{DBIC_method_is_indirect_sugar}
365 or
366 $_->{attributes}{DBIC_method_is_generated_from_resultsource_metadata}
367 );
12e7015a 368
369 push @err, {
2781bf35 370 overridden => {
12e7015a 371 name => $_->{name},
28ef9468 372 via_class => (
373 # this way we report a much better Dwarn oneliner in the error
374 $_->{attributes}{DBIC_method_is_bypassable_resultsource_proxy}
375 ? 'DBIx::Class::ResultSource'
376 : $_->{via_class}
377 ),
12e7015a 378 },
379 by => [ map { "$_->{via_class}::$_->{name}" } @$nonsugar_methods ],
380 } if (
381 $nonsugar_methods
382 and
383 ! $seen_shadowing_configurations->{
384 join "\0",
385 map
386 { refaddr $_ }
387 (
388 $_,
389 @$nonsugar_methods,
390 )
391 }++
392 )
393 ;
394
395 next METHOD_STACK;
396 }
397 }
398
399 \@err
400}
401
402
403sub format_valid_c3_composition_errors {
404 # my ($self, $errors) = @_;
405
406 [ map { sprintf(
407 "Class '%s' %s using the '%s' MRO affecting the lookup order of the "
408 . "following method(s): %s. You MUST add the following line to '%1\$s' "
409 . "right after strict/warnings:\n use mro 'c3';",
410 $_->{class},
411 ( ($_->{initial_mro} eq $_->{current_mro}) ? 'is' : 'was originally' ),
412 $_->{initial_mro},
413 join (', ', map { "$_()" } sort keys %{$_->{affected_methods}} ),
414 )} @{ $_[1] } ]
415}
416
417
418my $base_ISA = {
419 map { $_ => 1 } @{mro::get_linear_isa("DBIx::Class")}
420};
421
422sub check_valid_c3_composition {
423 my ($self, $schema) = @_;
424
425 my @err;
426
427 #
428 # A *very* involved check, to absolutely minimize false positives
429 # If this check returns an issue - it *better be* a real one
430 #
431 for my $class ( $self->all_schema_related_classes($schema) ) {
432
433 my $desc = do {
434 no strict 'refs';
435 describe_class_methods({
436 class => $class,
437 ( ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"}
438 ? ( use_mro => ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"} )
439 : ()
440 ),
441 })
442 };
443
444 # is there anything to check?
445 next unless (
446 ! $desc->{mro}{is_c3}
447 and
448 $desc->{methods_with_supers}
449 and
450 my @potentially_problematic_method_stacks =
451 grep
452 {
453 # at least 2 variants came via inheritance (not ours)
454 (
455 (grep { $_->{via_class} ne $class } @$_)
456 >
457 1
458 )
459 and
460 #
461 # last ditch effort to skip examining an alternative mro
462 # IFF the entire "foreign" stack is located in the "base isa"
463 #
464 # This allows for extra efficiency (as there are several
465 # with_supers methods that would always be there), but more
466 # importantly saves one from tripping on the nonsensical yet
467 # begrudgingly functional (as in - no adverse effects):
468 #
469 # use base 'DBIx::Class';
470 # use base 'DBIx::Class::Schema';
471 #
472 (
473 grep {
474 # not ours
475 $_->{via_class} ne $class
476 and
477 # not from the base stack either
478 ! $base_ISA->{$_->{via_class}}
479 } @$_
480 )
481 }
482 values %{ $desc->{methods_with_supers} }
483 );
484
485 my $affected_methods;
486
487 for my $stack (@potentially_problematic_method_stacks) {
488
489 # If we got so far - we need to see what the class would look
490 # like under c3 and compare, sigh
491 #
492 # Note that if the hierarchy is *really* fucked (like the above
493 # double-base e.g.) then recalc under 'c3' WILL FAIL, hence the
494 # extra eval: if we fail we report things as "jumbled up"
495 #
496 $affected_methods->{$stack->[0]{name}} = [
497 map { $_->{via_class} } @$stack
498 ] unless dbic_internal_try {
499
500 serialize($stack)
501 eq
502 serialize(
503 describe_class_methods({ class => $class, use_mro => 'c3' })
504 ->{methods}
505 ->{$stack->[0]{name}}
506 )
507 };
508 }
509
510 push @err, {
511 class => $class,
3aa25d8b 512 initial_linear_isa => $desc->{linear_isa},
513 current_linear_isa => do { (undef, my @isa) = @{ mro::get_linear_isa($class) }; \@isa },
12e7015a 514 initial_mro => $desc->{mro}{type},
515 current_mro => mro::get_mro($class),
516 affected_methods => $affected_methods,
517 } if $affected_methods;
518 }
519
520 \@err;
521}
522
523
524sub format_no_inheritance_crosscontamination_errors {
525 # my ($self, $errors) = @_;
526
527 [ map { sprintf(
528 "Class '%s' registered in the role of '%s' unexpectedly inherits '%s': "
529 . 'you must resolve this by either removing an erroneous `use base` call '
530 . "or switching to Moo(se)-style delegation (i.e. the 'handles' keyword)",
531 $_->{class},
532 $_->{type},
533 $_->{unexpectedly_inherits},
534 )} @{ $_[1] } ]
535}
536
537sub check_no_inheritance_crosscontamination {
538 my ($self, $schema) = @_;
539
540 my @err;
541
542 my $to_check = {
543 Schema => [ $schema ],
544 Storage => [ $schema->storage ],
545 ResultSource => [ map { $schema->source($_) } $schema->sources ],
546 };
547
548 $to_check->{ResultSet} = [
549 map { $_->resultset_class } @{$to_check->{ResultSource}}
550 ];
551
552 $to_check->{Core} = [
553 map { $_->result_class } @{$to_check->{ResultSource}}
554 ];
555
556 # Reduce everything to a unique sorted list of class names
557 $_ = [ sort( uniq( map {
558 ( not defined $_ ) ? ()
559 : ( defined blessed $_ ) ? ref $_
560 : $_
561 } @$_ ) ) ] for values %$to_check;
562
563 for my $group ( sort keys %$to_check ) {
564 for my $class ( @{ $to_check->{$group} } ) {
565 for my $foreign_base (
566 map { "DBIx::Class::$_" } sort grep { $_ ne $group } keys %$to_check
567 ) {
568
569 push @err, {
570 class => $class,
571 type => ( $group eq 'Core' ? 'ResultClass' : $group ),
572 unexpectedly_inherits => $foreign_base
573 } if $class->isa($foreign_base);
574 }
575 }
576 }
577
578 \@err;
579}
580
5811;
582
583__END__
584
585=head1 FURTHER QUESTIONS?
586
587Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
588
589=head1 COPYRIGHT AND LICENSE
590
591This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
592by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
593redistribute it and/or modify it under the same terms as the
594L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.