1 package DBIx::Class::Schema::SanityChecker;
6 use DBIx::Class::_Util qw(
7 dbic_internal_try refdesc uniq serialize
8 describe_class_methods emit_loud_diag
11 use Scalar::Util qw( blessed refaddr );
16 DBIx::Class::Schema::SanityChecker - Extensible "critic" for your Schema class hierarchy
20 package MyApp::Schema;
21 use base 'DBIx::Class::Schema';
23 # this is the default setting
24 __PACKAGE__->schema_sanity_checker('DBIx::Class::Schema::SanityChecker');
29 This is the default implementation of the Schema and related classes
30 L<validation framework|DBIx::Class::Schema/schema_sanity_checker>.
32 The validator is B<enabled by default>. See L</Performance considerations>
33 for discussion of the runtime effects.
35 Use of this class begins by invoking L</perform_schema_sanity_checks>
36 (usually via L<DBIx::Class::Schema/connection>), which in turn starts
37 invoking validators I<C<check_$checkname()>> in the order listed in
38 L</available_checks>. For each set of returned errors (if any)
39 I<C<format_$checkname_errors()>> is called and the resulting strings are
40 passed to L</emit_errors>, where final headers are prepended and the entire
41 thing is printed on C<STDERR>.
43 The class does not provide a constructor, due to the lack of state to be
44 passed around: object orientation was chosen purely for the ease of
45 overriding parts of the chain of events as described above. The general
46 pattern of communicating errors between the individual methods (both
47 before and after formatting) is an arrayref of hash references.
51 DBIC existed for more than a decade without any such setup validation
52 fanciness, let alone something that is enabled by default (which in turn
53 L<isn't free|/Performance considerations>). The reason for this relatively
54 drastic change is a set of revamps within the metadata handling framework,
55 in order to resolve once and for all problems like
56 L<RT#107462|https://rt.cpan.org/Ticket/Display.html?id=107462>,
57 L<RT#114440|https://rt.cpan.org/Ticket/Display.html?id=114440>, etc. While
58 DBIC internals are now way more robust than they were before, this comes at
59 a price: some non-issues in code that has been working for a while, will
60 now become hard to explain, or if you are unlucky: B<silent breakages>.
62 Thus, in order to protect existing codebases to the fullest extent possible,
63 the executive decision (and substantial effort) was made to introduce this
64 on-by-default setup validation framework. A massive amount of work has been
65 invested ensuring that none of the builtin checks emit a false-positive:
66 each and every complaint made by these checks B<should be investigated>.
68 =head2 Performance considerations
70 First of all - after your connection has been established - there is B<no
71 runtime penalty> whenever the checks are enabled.
73 By default the checks are triggered every time
74 L<DBIx::Class::Schema/connection> is called. Thus there is a
75 noticeable startup slowdown, most notably during testing (each test is
76 effectively a standalone program connecting anew). As an example the test
77 execution phase of the L<DBIx::Class::Helpers> C<v2.032002> distribution
78 suffers a consistent slowdown of about C<16%>. This is considered a relatively
79 small price to pay for the benefits provided.
81 Nevertheless, there are valid cases for disabling the checks during
82 day-to-day development, and having them run only during CI builds. In fact
83 the test suite of DBIC does exactly this as can be seen in
84 F<t/lib/DBICTest/BaseSchema.pm>:
86 ~/dbic_repo$ git show 39636786 | perl -ne "print if 16..61"
88 Whatever you do, B<please do not disable the checks entirely>: it is not
93 The situation with perl interpreters before C<v5.10.0> is sadly more
94 complicated: due to lack of built-in L<pluggable mro support|mro>, the
95 mechanism used to interrogate various classes is
96 L<< B<much> slower|https://github.com/dbsrgits/dbix-class/commit/296248c3 >>.
97 As a result the very same version of L<DBIx::Class::Helpers>
98 L<mentioned above|/Performance considerations> takes a C<B<220%>> hit on its
99 test execution time (these numbers are observed with the speedups of
100 L<Class::C3::XS> available, without them the slowdown reaches the whopping
103 It is the author's B<strongest> recommendation to find a way to run the
104 checks on your codebase continuously, even if it takes much longer. Refer to
105 the last paragraph of L</Performance considerations> above for an example how
106 to do this during CI builds only.
108 =head2 Validations provided by this module
110 =head3 no_indirect_method_overrides
112 There are many methods within DBIC which are
113 L<"strictly sugar"|DBIx::Class::MethodAttributes/DBIC_method_is_indirect_sugar>
114 and should never be overridden by your application (e.g. see warnings at the
115 end of L<DBIx::Class::ResultSet/create> and L<DBIx::Class::Schema/connect>).
116 Starting with C<v0.082900> DBIC is much more aggressive in calling the
117 underlying non-sugar methods directly, which in turn means that almost all
118 user-side overrides of sugar methods are never going to be invoked. These
119 situations are now reliably detected and reported individually (you may
120 end up with a lot of output on C<STDERR> due to this).
122 Note: B<ANY AND ALL ISSUES> reported by this check B<*MUST*> be resolved
123 before upgrading DBIC in production. Malfunctioning business logic and/or
124 B<SEVERE DATA LOSS> may result otherwise.
126 =head3 valid_c3_composition
128 Looks through everything returned by L</all_schema_related_classes>, and
129 for any class that B<does not> already utilize L<c3 MRO|mro/The C3 MRO> a
130 L<method shadowing map|App::Isa::Splain/SYNOPSIS> is calculated and then
131 compared to the shadowing map as if C<c3 MRO> was requested in the first place.
132 Any discrepancies are reported in order to clearly identify L<hard to explain
133 bugs|https://blog.afoolishmanifesto.com/posts/mros-and-you> especially when
134 encountered within complex inheritance hierarchies.
136 =head3 no_inheritance_crosscontamination
138 Checks that every individual L<Schema|DBIx::Class::Schema>,
139 L<Storage|DBIx::Class::Storage>, L<ResultSource|DBIx::Class::ResultSource>,
140 L<ResultSet|DBIx::Class::ResultSet>
141 and L<Result|DBIx::Class::Manual::ResultClass> class does not inherit from
142 an unexpected DBIC base class: e.g. an error will be raised if your
143 C<MyApp::Schema> inherits from both C<DBIx::Class::Schema> and
144 C<DBIx::Class::ResultSet>.
148 =head2 perform_schema_sanity_checks
152 =item Arguments: L<$schema|DBIx::Class::Schema>
154 =item Return Value: unspecified (ignored by caller)
158 The entry point expected by the
159 L<validation framework|DBIx::Class::Schema/schema_sanity_checker>. See
160 L</DESCRIPTION> for details.
164 sub perform_schema_sanity_checks {
165 my ($self, $schema) = @_;
167 local $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'} = {}
169 # does not make a measurable difference on 5.10+
170 DBIx::Class::_ENV_::OLD_MRO
172 # the callstack shouldn't really be recursive, but for completeness...
173 ! $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'}
176 my (@errors_found, $schema_desc);
177 for my $ch ( @{ $self->available_checks } ) {
179 my $err = $self->${\"check_$ch"} ( $schema );
181 push @errors_found, map
185 formatted_error => $_,
186 schema_desc => ( $schema_desc ||=
187 ( length ref $schema )
194 $self->${\"format_${ch}_errors"} ( $err )
201 $self->emit_errors(\@errors_found)
205 =head2 available_checks
209 =item Arguments: none
211 =item Return Value: \@list_of_check_names
215 The list of checks L</perform_schema_sanity_checks> will perform on the
216 provided L<$schema|DBIx::Class::Schema> object. For every entry returned
217 by this method, there must be a pair of I<C<check_$checkname()>> and
218 I<C<format_$checkname_errors()>> methods available.
220 Override this method to add checks to the
221 L<currently available set|/Validations provided by this module>.
225 sub available_checks { [qw(
227 no_inheritance_crosscontamination
228 no_indirect_method_overrides
235 =item Arguments: \@list_of_formatted_errors
237 =item Return Value: unspecified (ignored by caller)
241 Takes an array reference of individual errors returned by various
242 I<C<format_$checkname_errors()>> formatters, and outputs them on C<STDERR>.
244 This method is the most convenient integration point for a 3rd party logging
247 Each individual error is expected to be a hash reference with all values being
248 plain strings as follows:
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()
256 If the environment variable C<DBIC_ASSERT_NO_FAILING_SANITY_CHECKS> is set to
257 a true value this method will throw an exception with the same text. Those who
258 prefer to take no chances could set this variable permanently as part of their
263 # *NOT* using carp_unique and the warn framework - make
264 # it harder to accidentaly silence problems via $SIG{__WARN__}
266 #my ($self, $errs) = @_;
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 )}
276 ) for @final_error_texts;
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};
284 =head2 all_schema_related_classes
288 =item Arguments: L<$schema|DBIx::Class::Schema>
290 =item Return Value: @sorted_list_of_unique_class_names
294 This is a convenience method providing a list (not an arrayref) of
295 "interesting classes" related to the supplied schema. The returned list
296 currently contains the following class names:
300 =item * The L<Schema|DBIx::Class::Schema> class itself
302 =item * The associated L<Storage|DBIx::Class::Schema/storage> class if any
304 =item * The classes of all L<registered ResultSource instances|DBIx::Class::Schema/sources> if any
306 =item * All L<Result|DBIx::Class::ResultSource/result_class> classes for all registered ResultSource instances
308 =item * All L<ResultSet|DBIx::Class::ResultSource/resultset_class> classes for all registered ResultSource instances
314 sub all_schema_related_classes {
315 my ($self, $schema) = @_;
318 ( not defined $_ ) ? ()
319 : ( defined blessed $_ ) ? ref $_
328 } map { $schema->source($_) } $schema->sources ),
333 sub format_no_indirect_method_overrides_errors {
334 # my ($self, $errors) = @_;
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} } ),
343 $_->{overridden}{via_class},
344 $_->{overridden}{name},
348 sub check_no_indirect_method_overrides {
349 my ($self, $schema) = @_;
351 my( @err, $seen_shadowing_configurations );
354 for my $method_stack ( map {
355 values %{ describe_class_methods($_)->{methods_with_supers} || {} }
356 } $self->all_schema_related_classes($schema) ) {
358 my $nonsugar_methods;
360 for (@$method_stack) {
362 push @$nonsugar_methods, $_ and next
364 $_->{attributes}{DBIC_method_is_indirect_sugar}
366 $_->{attributes}{DBIC_method_is_generated_from_resultsource_metadata}
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'
379 by => [ map { "$_->{via_class}::$_->{name}" } @$nonsugar_methods ],
383 ! $seen_shadowing_configurations->{
403 sub format_valid_c3_composition_errors {
404 # my ($self, $errors) = @_;
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';",
411 ( ($_->{initial_mro} eq $_->{current_mro}) ? 'is' : 'was originally' ),
413 join (', ', map { "$_()" } sort keys %{$_->{affected_methods}} ),
419 map { $_ => 1 } @{mro::get_linear_isa("DBIx::Class")}
422 sub check_valid_c3_composition {
423 my ($self, $schema) = @_;
428 # A *very* involved check, to absolutely minimize false positives
429 # If this check returns an issue - it *better be* a real one
431 for my $class ( $self->all_schema_related_classes($schema) ) {
435 describe_class_methods({
437 ( ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"}
438 ? ( use_mro => ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"} )
444 # is there anything to check?
446 ! $desc->{mro}{is_c3}
448 $desc->{methods_with_supers}
450 my @potentially_problematic_method_stacks =
453 # at least 2 variants came via inheritance (not ours)
455 (grep { $_->{via_class} ne $class } @$_)
461 # last ditch effort to skip examining an alternative mro
462 # IFF the entire "foreign" stack is located in the "base isa"
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):
469 # use base 'DBIx::Class';
470 # use base 'DBIx::Class::Schema';
475 $_->{via_class} ne $class
477 # not from the base stack either
478 ! $base_ISA->{$_->{via_class}}
482 values %{ $desc->{methods_with_supers} }
485 my $affected_methods;
487 for my $stack (@potentially_problematic_method_stacks) {
489 # If we got so far - we need to see what the class would look
490 # like under c3 and compare, sigh
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"
496 $affected_methods->{$stack->[0]{name}} = [
497 map { $_->{via_class} } @$stack
498 ] unless dbic_internal_try {
503 describe_class_methods({ class => $class, use_mro => 'c3' })
505 ->{$stack->[0]{name}}
512 initial_linear_isa => $desc->{linear_isa},
513 current_linear_isa => do { (undef, my @isa) = @{ mro::get_linear_isa($class) }; \@isa },
514 initial_mro => $desc->{mro}{type},
515 current_mro => mro::get_mro($class),
516 affected_methods => $affected_methods,
517 } if $affected_methods;
524 sub format_no_inheritance_crosscontamination_errors {
525 # my ($self, $errors) = @_;
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)",
533 $_->{unexpectedly_inherits},
537 sub check_no_inheritance_crosscontamination {
538 my ($self, $schema) = @_;
543 Schema => [ $schema ],
544 Storage => [ $schema->storage ],
545 ResultSource => [ map { $schema->source($_) } $schema->sources ],
548 $to_check->{ResultSet} = [
549 map { $_->resultset_class } @{$to_check->{ResultSource}}
552 $to_check->{Core} = [
553 map { $_->result_class } @{$to_check->{ResultSource}}
556 # Reduce everything to a unique sorted list of class names
557 $_ = [ sort( uniq( map {
558 ( not defined $_ ) ? ()
559 : ( defined blessed $_ ) ? ref $_
561 } @$_ ) ) ] for values %$to_check;
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
571 type => ( $group eq 'Core' ? 'ResultClass' : $group ),
572 unexpectedly_inherits => $foreign_base
573 } if $class->isa($foreign_base);
585 =head1 FURTHER QUESTIONS?
587 Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
589 =head1 COPYRIGHT AND LICENSE
591 This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
592 by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
593 redistribute it and/or modify it under the same terms as the
594 L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.