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 )