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 DBIx::Class::Exception ();
12 use Scalar::Util qw( blessed refaddr );
17 DBIx::Class::Schema::SanityChecker - Extensible "critic" for your Schema class hierarchy
21 package MyApp::Schema;
22 use base 'DBIx::Class::Schema';
24 # this is the default setting
25 __PACKAGE__->schema_sanity_checker('DBIx::Class::Schema::SanityChecker');
30 This is the default implementation of the Schema and related classes
31 L<validation framework|DBIx::Class::Schema/schema_sanity_checker>.
33 The validator is B<enabled by default>. See L</Performance considerations>
34 for discussion of the runtime effects.
36 Use of this class begins by invoking L</perform_schema_sanity_checks>
37 (usually via L<DBIx::Class::Schema/connection>), which in turn starts
38 invoking validators I<C<check_$checkname()>> in the order listed in
39 L</available_checks>. For each set of returned errors (if any)
40 I<C<format_$checkname_errors()>> is called and the resulting strings are
41 passed to L</emit_errors>, where final headers are prepended and the entire
42 thing is printed on C<STDERR>.
44 The class does not provide a constructor, due to the lack of state to be
45 passed around: object orientation was chosen purely for the ease of
46 overriding parts of the chain of events as described above. The general
47 pattern of communicating errors between the individual methods (both
48 before and after formatting) is an arrayref of hash references.
52 DBIC existed for more than a decade without any such setup validation
53 fanciness, let alone something that is enabled by default (which in turn
54 L<isn't free|/Performance considerations>). The reason for this relatively
55 drastic change is a set of revamps within the metadata handling framework,
56 in order to resolve once and for all problems like
57 L<RT#107462|https://rt.cpan.org/Ticket/Display.html?id=107462>,
58 L<RT#114440|https://rt.cpan.org/Ticket/Display.html?id=114440>, etc. While
59 DBIC internals are now way more robust than they were before, this comes at
60 a price: some non-issues in code that has been working for a while, will
61 now become hard to explain, or if you are unlucky: B<silent breakages>.
63 Thus, in order to protect existing codebases to the fullest extent possible,
64 the executive decision (and substantial effort) was made to introduce this
65 on-by-default setup validation framework. A massive amount of work has been
66 invested ensuring that none of the builtin checks emit a false-positive:
67 each and every complaint made by these checks B<should be investigated>.
69 =head2 Performance considerations
71 First of all - after your connection has been established - there is B<no
72 runtime penalty> whenever the checks are enabled.
74 By default the checks are triggered every time
75 L<DBIx::Class::Schema/connection> is called. Thus there is a
76 noticeable startup slowdown, most notably during testing (each test is
77 effectively a standalone program connecting anew). As an example the test
78 execution phase of the L<DBIx::Class::Helpers> C<v2.032002> distribution
79 suffers a consistent slowdown of about C<16%>. This is considered a relatively
80 small price to pay for the benefits provided.
82 Nevertheless, there are valid cases for disabling the checks during
83 day-to-day development, and having them run only during CI builds. In fact
84 the test suite of DBIC does exactly this as can be seen in
85 F<t/lib/DBICTest/BaseSchema.pm>:
87 ~/dbic_repo$ git show 39636786 | perl -ne "print if 16..61"
89 Whatever you do, B<please do not disable the checks entirely>: it is not
94 The situation with perl interpreters before C<v5.10.0> is sadly more
95 complicated: due to lack of built-in L<pluggable mro support|mro>, the
96 mechanism used to interrogate various classes is
97 L<< B<much> slower|https://github.com/dbsrgits/dbix-class/commit/296248c3 >>.
98 As a result the very same version of L<DBIx::Class::Helpers>
99 L<mentioned above|/Performance considerations> takes a C<B<220%>> hit on its
100 test execution time (these numbers are observed with the speedups of
101 L<Class::C3::XS> available, without them the slowdown reaches the whopping
104 It is the author's B<strongest> recommendation to find a way to run the
105 checks on your codebase continuously, even if it takes much longer. Refer to
106 the last paragraph of L</Performance considerations> above for an example how
107 to do this during CI builds only.
109 =head2 Validations provided by this module
111 =head3 no_indirect_method_overrides
113 There are many methods within DBIC which are
114 L<"strictly sugar"|DBIx::Class::MethodAttributes/DBIC_method_is_indirect_sugar>
115 and should never be overridden by your application (e.g. see warnings at the
116 end of L<DBIx::Class::ResultSet/create> and L<DBIx::Class::Schema/connect>).
117 Starting with C<v0.082900> DBIC is much more aggressive in calling the
118 underlying non-sugar methods directly, which in turn means that almost all
119 user-side overrides of sugar methods are never going to be invoked. These
120 situations are now reliably detected and reported individually (you may
121 end up with a lot of output on C<STDERR> due to this).
123 Note: B<ANY AND ALL ISSUES> reported by this check B<*MUST*> be resolved
124 before upgrading DBIC in production. Malfunctioning business logic and/or
125 B<SEVERE DATA LOSS> may result otherwise.
127 =head3 valid_c3_composition
129 Looks through everything returned by L</all_schema_related_classes>, and
130 for any class that B<does not> already utilize L<c3 MRO|mro/The C3 MRO> a
131 L<method shadowing map|App::Isa::Splain/SYNOPSIS> is calculated and then
132 compared to the shadowing map as if C<c3 MRO> was requested in the first place.
133 Any discrepancies are reported in order to clearly identify L<hard to explain
134 bugs|https://blog.afoolishmanifesto.com/posts/mros-and-you> especially when
135 encountered within complex inheritance hierarchies.
137 =head3 no_inheritance_crosscontamination
139 Checks that every individual L<Schema|DBIx::Class::Schema>,
140 L<Storage|DBIx::Class::Storage>, L<ResultSource|DBIx::Class::ResultSource>,
141 L<ResultSet|DBIx::Class::ResultSet>
142 and L<Result|DBIx::Class::Manual::ResultClass> class does not inherit from
143 an unexpected DBIC base class: e.g. an error will be raised if your
144 C<MyApp::Schema> inherits from both C<DBIx::Class::Schema> and
145 C<DBIx::Class::ResultSet>.
149 =head2 perform_schema_sanity_checks
153 =item Arguments: L<$schema|DBIx::Class::Schema>
155 =item Return Value: unspecified (ignored by caller)
159 The entry point expected by the
160 L<validation framework|DBIx::Class::Schema/schema_sanity_checker>. See
161 L</DESCRIPTION> for details.
165 sub perform_schema_sanity_checks {
166 my ($self, $schema) = @_;
168 local $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'} = {}
170 # does not make a measurable difference on 5.10+
171 DBIx::Class::_ENV_::OLD_MRO
173 # the callstack shouldn't really be recursive, but for completeness...
174 ! $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'}
177 my (@errors_found, $schema_desc);
178 for my $ch ( @{ $self->available_checks } ) {
180 my $err = $self->${\"check_$ch"} ( $schema );
182 push @errors_found, map
186 formatted_error => $_,
187 schema_desc => ( $schema_desc ||=
188 ( length ref $schema )
195 $self->${\"format_${ch}_errors"} ( $err )
202 $self->emit_errors(\@errors_found)
206 =head2 available_checks
210 =item Arguments: none
212 =item Return Value: \@list_of_check_names
216 The list of checks L</perform_schema_sanity_checks> will perform on the
217 provided L<$schema|DBIx::Class::Schema> object. For every entry returned
218 by this method, there must be a pair of I<C<check_$checkname()>> and
219 I<C<format_$checkname_errors()>> methods available.
221 Override this method to add checks to the
222 L<currently available set|/Validations provided by this module>.
226 sub available_checks { [qw(
228 no_inheritance_crosscontamination
229 no_indirect_method_overrides
236 =item Arguments: \@list_of_formatted_errors
238 =item Return Value: unspecified (ignored by caller)
242 Takes an array reference of individual errors returned by various
243 I<C<format_$checkname_errors()>> formatters, and outputs them on C<STDERR>.
245 This method is the most convenient integration point for a 3rd party logging
248 Each individual error is expected to be a hash reference with all values being
249 plain strings as follows:
252 schema_desc => $human_readable_description_of_the_passed_in_schema
253 check_name => $name_of_the_check_as_listed_in_available_checks()
254 formatted_error => $error_text_as_returned_by_format_$checkname_errors()
257 If the environment variable C<DBIC_ASSERT_NO_FAILING_SANITY_CHECKS> is set to
258 a true value this method will throw an exception with the same text. Those who
259 prefer to take no chances could set this variable permanently as part of their
264 # *NOT* using carp_unique and the warn framework - make
265 # it harder to accidentaly silence problems via $SIG{__WARN__}
267 #my ($self, $errs) = @_;
269 my @final_error_texts = map {
270 sprintf( "Schema %s failed the '%s' sanity check: %s\n",
271 @{$_}{qw( schema_desc check_name formatted_error )}
277 ) for @final_error_texts;
279 # Do not use the constant - but instead check the env every time
280 # This will allow people to start auditing their apps piecemeal
281 DBIx::Class::Exception->throw( join "\n", @final_error_texts, ' ' )
282 if $ENV{DBIC_ASSERT_NO_FAILING_SANITY_CHECKS};
285 =head2 all_schema_related_classes
289 =item Arguments: L<$schema|DBIx::Class::Schema>
291 =item Return Value: @sorted_list_of_unique_class_names
295 This is a convenience method providing a list (not an arrayref) of
296 "interesting classes" related to the supplied schema. The returned list
297 currently contains the following class names:
301 =item * The L<Schema|DBIx::Class::Schema> class itself
303 =item * The associated L<Storage|DBIx::Class::Schema/storage> class if any
305 =item * The classes of all L<registered ResultSource instances|DBIx::Class::Schema/sources> if any
307 =item * All L<Result|DBIx::Class::ResultSource/result_class> classes for all registered ResultSource instances
309 =item * All L<ResultSet|DBIx::Class::ResultSource/resultset_class> classes for all registered ResultSource instances
315 sub all_schema_related_classes {
316 my ($self, $schema) = @_;
319 ( not defined $_ ) ? ()
320 : ( defined blessed $_ ) ? ref $_
329 } map { $schema->source($_) } $schema->sources ),
334 sub format_no_indirect_method_overrides_errors {
335 # my ($self, $errors) = @_;
338 "Method(s) %s override the convenience shortcut %s::%s(): "
339 . 'it is almost certain these overrides *MAY BE COMPLETELY IGNORED* at '
340 . 'runtime. You MUST reimplement each override to hook a method from the '
341 . "chain of calls within the convenience shortcut as seen when running:\n "
342 . '~$ perl -M%2$s -MDevel::Dwarn -e "Ddie { %3$s => %2$s->can(q(%3$s)) }"',
343 join (', ', map { "$_()" } sort @{ $_->{by} } ),
344 $_->{overridden}{via_class},
345 $_->{overridden}{name},
349 sub check_no_indirect_method_overrides {
350 my ($self, $schema) = @_;
352 my( @err, $seen_shadowing_configurations );
355 for my $method_stack ( map {
356 values %{ describe_class_methods($_)->{methods_with_supers} || {} }
357 } $self->all_schema_related_classes($schema) ) {
359 my $nonsugar_methods;
361 for (@$method_stack) {
363 push @$nonsugar_methods, $_ and next
364 unless $_->{attributes}{DBIC_method_is_indirect_sugar};
370 # this way we report a much better Dwarn oneliner in the error
371 $_->{attributes}{DBIC_method_is_bypassable_resultsource_proxy}
372 ? 'DBIx::Class::ResultSource'
376 by => [ map { "$_->{via_class}::$_->{name}" } @$nonsugar_methods ],
380 ! $seen_shadowing_configurations->{
400 sub format_valid_c3_composition_errors {
401 # my ($self, $errors) = @_;
404 "Class '%s' %s using the '%s' MRO affecting the lookup order of the "
405 . "following method(s): %s. You MUST add the following line to '%1\$s' "
406 . "right after strict/warnings:\n use mro 'c3';",
408 ( ($_->{initial_mro} eq $_->{current_mro}) ? 'is' : 'was originally' ),
410 join (', ', map { "$_()" } sort keys %{$_->{affected_methods}} ),
416 map { $_ => 1 } @{mro::get_linear_isa("DBIx::Class")}
419 sub check_valid_c3_composition {
420 my ($self, $schema) = @_;
425 # A *very* involved check, to absolutely minimize false positives
426 # If this check returns an issue - it *better be* a real one
428 for my $class ( $self->all_schema_related_classes($schema) ) {
432 describe_class_methods({
434 ( ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"}
435 ? ( use_mro => ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"} )
441 # is there anything to check?
443 ! $desc->{mro}{is_c3}
445 $desc->{methods_with_supers}
447 my @potentially_problematic_method_stacks =
450 # at least 2 variants came via inheritance (not ours)
452 (grep { $_->{via_class} ne $class } @$_)
458 # last ditch effort to skip examining an alternative mro
459 # IFF the entire "foreign" stack is located in the "base isa"
461 # This allows for extra efficiency (as there are several
462 # with_supers methods that would always be there), but more
463 # importantly saves one from tripping on the nonsensical yet
464 # begrudgingly functional (as in - no adverse effects):
466 # use base 'DBIx::Class';
467 # use base 'DBIx::Class::Schema';
472 $_->{via_class} ne $class
474 # not from the base stack either
475 ! $base_ISA->{$_->{via_class}}
479 values %{ $desc->{methods_with_supers} }
482 my $affected_methods;
484 for my $stack (@potentially_problematic_method_stacks) {
486 # If we got so far - we need to see what the class would look
487 # like under c3 and compare, sigh
489 # Note that if the hierarchy is *really* fucked (like the above
490 # double-base e.g.) then recalc under 'c3' WILL FAIL, hence the
491 # extra eval: if we fail we report things as "jumbled up"
493 $affected_methods->{$stack->[0]{name}} = [
494 map { $_->{via_class} } @$stack
495 ] unless dbic_internal_try {
500 describe_class_methods({ class => $class, use_mro => 'c3' })
502 ->{$stack->[0]{name}}
510 initial_mro => $desc->{mro}{type},
511 current_mro => mro::get_mro($class),
512 affected_methods => $affected_methods,
513 } if $affected_methods;
520 sub format_no_inheritance_crosscontamination_errors {
521 # my ($self, $errors) = @_;
524 "Class '%s' registered in the role of '%s' unexpectedly inherits '%s': "
525 . 'you must resolve this by either removing an erroneous `use base` call '
526 . "or switching to Moo(se)-style delegation (i.e. the 'handles' keyword)",
529 $_->{unexpectedly_inherits},
533 sub check_no_inheritance_crosscontamination {
534 my ($self, $schema) = @_;
539 Schema => [ $schema ],
540 Storage => [ $schema->storage ],
541 ResultSource => [ map { $schema->source($_) } $schema->sources ],
544 $to_check->{ResultSet} = [
545 map { $_->resultset_class } @{$to_check->{ResultSource}}
548 $to_check->{Core} = [
549 map { $_->result_class } @{$to_check->{ResultSource}}
552 # Reduce everything to a unique sorted list of class names
553 $_ = [ sort( uniq( map {
554 ( not defined $_ ) ? ()
555 : ( defined blessed $_ ) ? ref $_
557 } @$_ ) ) ] for values %$to_check;
559 for my $group ( sort keys %$to_check ) {
560 for my $class ( @{ $to_check->{$group} } ) {
561 for my $foreign_base (
562 map { "DBIx::Class::$_" } sort grep { $_ ne $group } keys %$to_check
567 type => ( $group eq 'Core' ? 'ResultClass' : $group ),
568 unexpectedly_inherits => $foreign_base
569 } if $class->isa($foreign_base);
581 =head1 FURTHER QUESTIONS?
583 Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
585 =head1 COPYRIGHT AND LICENSE
587 This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
588 by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
589 redistribute it and/or modify it under the same terms as the
590 L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.