DBIx-Class-Schema-Diff

 view release on metacpan or  search on metacpan

lib/DBIx/Class/Schema/Diff.pm  view on Meta::CPAN

 $D = DBIx::Class::Schema::Diff->new(
   old_schema => $schema1,
   new_schema => $schema2
 );
 
 # Dump current schema data to a json file for later use:
 $D->old_schema->dump_json_file('/tmp/my_schema1_data.json');
 
 # Or
 DBIx::Class::Schema::Diff::SchemaData->new(
   schema => 'My::Schema1'
 )->dump_json_file('/tmp/my_schema1_data.json');
 
 # Create new diff object using previously saved 
 # schema data + current schema class:
 $D = DBIx::Class::Schema::Diff->new(
   old_schema => '/tmp/my_schema1_data.json',
   new_schema => 'My::Schema1'
 );
 
 # Git a checksum/fingerprint of the diff data:
 my $checksum = $D->fingerprint;
 

Filtering the diff:

 
 # Get all differences (hash structure):
 my $hash = $D->diff;
 
 # Only column differences:
 $hash = $D->filter('columns')->diff;
 
 # Only things named 'Artist' or 'CD':
 $hash = $D->filter(qw/Artist CD/)->diff;
 
 # Things named 'Artist', *columns* named 'CD' and *relationships* named 'columns':
 $hash = $D->filter(qw(Artist columns/CD relationships/columns))->diff;
 
 # Sources named 'Artist', excluding column changes:
 $hash = $D->filter('Artist:')->filter_out('columns')->diff;
 
 if( $D->filter('Artist:columns/name.size')->diff ) {
  # Do something only if there has been a change in 'size' (i.e. in column_info)
  # to the 'name' column in the 'Artist' source
  # ...
 }
 
 # Names of all sources which exist in new_schema but not in old_schema:
 my @sources = keys %{ 
   $D->filter({ source_events => 'added' })->diff || {}
 };
 
 # All changes to existing unique_constraints (ignoring added or deleted)
 # excluding those named or within sources named Album or Genre:
 $hash = $D->filter_out({ events => [qw(added deleted)] })
           ->filter_out('Album','Genre')
           ->filter('constraints')
           ->diff;
 
 # All changes to relationship attrs except for 'cascade_delete' in 
 # relationships named 'artists':
 $hash = $D->filter_out('relationships/artists.attrs.cascade_delete')
           ->filter('relationships/*.attrs')
           ->diff;


=head1 DESCRIPTION

General-purpose schema differ for L<DBIx::Class> to identify changes between two DBIC Schemas. 
Currently tracks added/deleted/changed events and deep diffing across 5 named types of source data:

=over

=item *

columns

=item *

relationships

=item *

constraints

=item *

table_name

=item *

isa

=back

The changes which are detected are stored in a HashRef which can be accessed by calling 
L<diff|DBIx::Class::Schema::Diff#diff>. This data packet, which has a format that is specific to 
this module, can either be inspected directly, or I<filtered> to be able to check for specific 
changes as boolean test(s), making it unnecessary to know the internal diff structure for many 
use-cases (since if there are no changes, or no changes left after being filtered, C<diff> returns 
false/undef - see the L<FILTERING|DBIx::Class::Schema::Diff#FILTERING> section for more info).

This tool attempts to be simple and flexible with a straightforward, "DWIM" API. It is meant
to be used programmatically in dynamic scenarios where schema changes are occurring but are not well
suited for L<DBIx::Class::Migration> or L<DBIx::Class::DeploymentHandler> for whatever reasons, or
some other event/action needs to take place based on certain types of changes (note that this tool 
is NOT meant to be a replacement for Migrations/DH). 

It is also useful as a general debugging/development tool, and was designed with this in mind to 
be "handy" and not need a lot of setup/RTFM to use.

This tool is different from L<SQL::Translator::Diff> in that it compares DBIC schemas at the 
I<class/code> level, not the underlying DDL, nor does it attempt to modify one schema to match
the other (although, it could certainly be used to write a tool that did).

=head1 METHODS

=head2 new

Create a new DBIx::Class::Schema::Diff instance. The following build options are supported:

=over 4

=item old_schema

The "old" (or left-side) schema to be compared. 

Can be supplied as a L<DBIx::Class::Schema> class name, connected schema object instance, 
or previously saved L<SchemaData|DBIx::Class::Schema::Diff::SchemaData> which can be 
supplied as an object, HashRef, or a path to a file containing serialized JSON data (as 
produced by L<DBIx::Class::Schema::Diff::SchemaData#dump_json_file>)

See the SYNOPSIS and L<DBIx::Class::Schema::Diff::SchemaData> for more info.

=item new_schema

The "new" (or right-side) schema to be compared. Accepts the same dynamic type options 
as C<old_schema>.

=back

=head2 diff

Returns the differences between the two schemas as a HashRef structure, or C<undef> if there are 
none.

The HashRef is divided first by source name, then by type, with the special C<_event> key 
identifying the kind of modification (added, deleted or changed) at both the source and the type 
level. For 'changed' events within types, a deeper, type-specific diff HashRef is provided (with 
column_info/relationship_info diffs generated using L<Hash::Diff>).

Here is an example of what a diff packet (with a sampling of lots of different kinds of changes) 
might look like:

 # Example diff with sample of all 3 kinds of events and all 5 types:
 {
   Address => {
     _event => "changed",
     isa => [
       "-Some::Removed::Component",
       "+Test::DummyClass"
     ],
     relationships => {
       customers2 => {
         _event => "added"
       },
       staffs => {
         _event => "changed",
         diff => {
           attrs => {
             cascade_delete => 1
           }
         }
       }
     }
   },
   City => {
     _event => "changed",
     table_name => "city1"
   },
   FilmCategory => {
     _event => "changed",
     columns => {
       last_update => {
         _event => "changed",
         diff => {
           is_nullable => 1
         }
       }
     }
   },
   FooBar => {
     _event => "added"
   },
   FooBaz => {
     _event => "deleted"
   },
   Store => {
     _event => "changed",
     constraints => {
       idx_unique_store_manager => {
         _event => "added"
       }
     }
   }
 }

=head2 filter

Accepts filter argument(s) to restrict the differences to consider and returns a new C<Schema::Diff> 
instance, making it chainable (much like L<ResultSets|DBIx::Class::ResultSet#search_rs>).

See L<FILTERING|DBIx::Class::Schema::Diff#FILTERING> for filter argument syntax.

=head2 filter_out

Works like C<filter()> but the arguments exclude differences rather than restrict/limit to them.

See L<FILTERING|DBIx::Class::Schema::Diff#FILTERING> for filter argument syntax.

=head2 fingerprint

Returns a SHA1 checksum (as a 15 character string) of the diff data.

=head1 FILTERING

The L<filter|DBIx::Class::Schema::Diff#filter> (and inverse 
L<filter_out|DBIx::Class::Schema::Diff#filter_out>) method is analogous to ResultSet's 
L<search_rs|DBIx::Class::ResultSet#search_rs> in that it is chainable (i.e. returns a new object 
instance) and each call further restricts the data considered. But, instead of building up an SQL 
query, it filters the data in the HashRef returned by L<diff|DBIx::Class::Schema::Diff#diff>. 

The filter argument(s) define an expression which matches specific parts of the C<diff> packet. In 
the case of C<filter()>, all data that B<does not> match the expression is removed from the diff 
HashRef (of the returned, new object), while in the case of C<filter_out()>, all data that B<does> 
match the expression is removed.

The filter expression is designed to be simple and declarative. It can be supplied as a list of 
strings which match schema data either broadly or narrowly. A filter string argument follows this 
general pattern:

 '<source>:<type>/<id>'

Where C<source> is the name of a specific source in the schema (either side), C<type> is the 
I<type> of data, which is currently one of five (5) supported, predefined types: I<'columns'>, 
I<'relationships'>, I<'constraints'>, I<'isa'> and I<'table_name'>, and C<id> is the name of an 
item, specific to that type, if applicable. 

For instance, this expression would match only the I<column> named 'timestamp' in the source 
named 'Artist':

 'Artist:columns/timestamp'

Not all types have sub-items (only I<columns>, I<relationships> and I<constraints>). The I<isa> and 
I<table_name> types are source-global. So, for example, to see changes to I<isa> (i.e. differences 
in inheritance and/or loaded components in the result class) you could use the following:

 'Artist:isa'

On the other hand, not only are there multiple I<columns> and I<relationships> within each source, 
but each can have specific changes to their attributes (column_info/relationship_info) which can 
also be targeted selectively. For instance, to match only changes in C<size> of a specific column:

 'Artist:columns/timestamp.size'

Attributes with sub hashes can be matched as well. For example, to match only changes in C<list> 
I<within> C<extra> (which is where DBIC puts the list of possible values for enum columns):

 'Artist:columns/my_enum.extra.list'

The structure is specific to the type. The dot-separated path applies to the data returned by L<column_info|DBIx::Class::ResultSource#column_info> for columns and
L<relationship_info|DBIx::Class::ResultSource#relationship_info> for relationships. For instance, 
the following matches changes to C<cascade_delete> of a specific relationship named 'some_rel' 
in the 'Artist' source:

 'Artist:relationships/some_rel.attrs.cascade_delete'

Filter arguments can also match I<broadly> using the wildcard asterisk character (C<*>). For 
instance, to match I<'isa'> changes in any source:

 '*:isa'

The system also accepts ambiguous/partial match strings and tries to "DWIM". So, the above can also 
be written simply as:

 'isa'

This is possible because 'isa' is understood/known as a I<type> keyword. Additionally, the system 
knows the names of all the sources in advance, so the following filter string argument would match 
everything in the 'Artist' source:

 'Artist'

Sub-item names are automatically resolved, too. The following would match any column, relationship, 
or constraint named C<'code'> in any source:

 'code'

When you have schemas with overlapping names, such as a column named 'isa', you simply need to 
supply more specific match strings, as ambiguous names are resolved with left-precedence. So, to 
match any column, relationship, or constraint named 'isa', you could use the following:

 # Matches column, relationship, or constraints named 'isa':
 '*:*/isa'

Different delimiter characters are used for the source level (C<':'>) and the type level (C<'/'>) 
so you can do things like match any column/relationship/constraint of a specific source, such as:

 Artist:code

The above is equivalent to:

 Artist:*/code

You can also supply a delimiter character to match a specific level explicitly. So, if you wanted to
match all changes to a I<source> named 'isa':

 # Matches a source (poorly) named 'isa'
 'isa:'

The same works at the type level. The following are all equivalent

 # Each of the following 3 filter strings are equivalent:
 'columns/'
 '*:columns/*'
 'columns'

Internally, L<Hash::Layout> is used to process the filter arguments.

=head2 event filtering

Besides matching specific parts of the schema, you can also filter by I<event>, which is either 
I<'added'>, I<'deleted'> or I<'changed'> at both the source and type level (i.e. the event of a 
new column is 'added' at the type level, but 'changed' at the source level).

Filtering by event requires passing a HashRef argument to filter/filter_out, with the special



( run in 0.717 second using v1.01-cache-2.11-cpan-5623c5533a1 )