Upped version numbers, cleaned up code, fixed my name.
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Diff.pm
CommitLineData
51ffe5ee 1package SQL::Translator::Diff;
4d438549 2
51ffe5ee 3## SQLT schema diffing code
4use strict;
5use warnings;
da5a1bae 6use Data::Dumper;
51ffe5ee 7use SQL::Translator::Schema::Constants;
8
4d438549 9use base 'Class::Accessor::Fast';
10
ba506e52 11use vars qw[ $VERSION ];
12
13$VERSION = 1.60;
14
4d438549 15# Input/option accessors
16__PACKAGE__->mk_accessors(qw/
17 ignore_index_names ignore_constraint_names ignore_view_sql
e30b71b8 18 ignore_proc_sql output_db source_schema target_schema
7467c458 19 case_insensitive no_batch_alters ignore_missing_methods producer_options
4d438549 20/);
21
22my @diff_arrays = qw/
23 tables_to_drop
24 tables_to_create
25/;
26
27my @diff_hash_keys = qw/
28 constraints_to_create
29 constraints_to_drop
30 indexes_to_create
31 indexes_to_drop
32 fields_to_create
33 fields_to_alter
34 fields_to_rename
35 fields_to_drop
36 table_options
46bf5655 37 table_renamed_from
4d438549 38/;
39
40__PACKAGE__->mk_accessors(@diff_arrays, 'table_diff_hash');
41
42sub schema_diff {
da5a1bae 43 # use Data::Dumper;
44 ## we are getting instructions on how to turn the source into the target
45 ## source == original, target == new (hmm, if I need to comment this, should I rename the vars again ??)
46 ## _schema isa SQL::Translator::Schema
47 ## _db is the name of the producer/db it came out of/into
48 ## results are formatted to the source preferences
49
e30b71b8 50 my ($source_schema, $source_db, $target_schema, $output_db, $options) = @_;
4d438549 51 $options ||= {};
da5a1bae 52
4d438549 53 my $obj = SQL::Translator::Diff->new( {
54 %$options,
55 source_schema => $source_schema,
4d438549 56 target_schema => $target_schema,
e30b71b8 57 output_db => $output_db
4d438549 58 } );
da5a1bae 59
4d438549 60 $obj->compute_differences->produce_diff_sql;
61}
51ffe5ee 62
4d438549 63sub new {
64 my ($class, $values) = @_;
65 $values->{$_} ||= [] foreach @diff_arrays;
66 $values->{table_diff_hash} = {};
67
7467c458 68 $values->{producer_options} ||= {};
4d438549 69 $values->{output_db} ||= $values->{source_db};
70 return $class->SUPER::new($values);
71}
72
73sub compute_differences {
74 my ($self) = @_;
51ffe5ee 75
4d438549 76 my $target_schema = $self->target_schema;
77 my $source_schema = $self->source_schema;
78
9ab59f87 79 my $producer_class = "SQL::Translator::Producer::@{[$self->output_db]}";
80 eval "require $producer_class";
81 die $@ if $@;
82
83 if (my $preprocess = $producer_class->can('preprocess_schema')) {
934e1b39 84 $preprocess->($source_schema);
85 $preprocess->($target_schema);
9ab59f87 86 }
87
46bf5655 88 my %src_tables_checked = ();
4d438549 89 my @tar_tables = sort { $a->name cmp $b->name } $target_schema->get_tables;
da5a1bae 90 ## do original/source tables exist in target?
4d438549 91 for my $tar_table ( @tar_tables ) {
da5a1bae 92 my $tar_table_name = $tar_table->name;
46bf5655 93
94 my $src_table;
95
96 $self->table_diff_hash->{$tar_table_name} = {
97 map {$_ => [] } @diff_hash_keys
98 };
99
100 if (my $old_name = $tar_table->extra('renamed_from')) {
101 $src_table = $source_schema->get_table( $old_name, $self->case_insensitive );
102 if ($src_table) {
103 $self->table_diff_hash->{$tar_table_name}{table_renamed_from} = [ [$src_table, $tar_table] ];
104 } else {
105 delete $tar_table->extra->{renamed_from};
106 warn qq#Renamed table can't find old table "$old_name" for renamed table\n#;
107 }
108 } else {
109 $src_table = $source_schema->get_table( $tar_table_name, $self->case_insensitive );
110 }
da5a1bae 111
da5a1bae 112 unless ( $src_table ) {
da5a1bae 113 ## table is new
114 ## add table(s) later.
4d438549 115 push @{$self->tables_to_create}, $tar_table;
da5a1bae 116 next;
117 }
51ffe5ee 118
46bf5655 119 my $src_table_name = $src_table->name;
120 $src_table_name = lc $src_table_name if $self->case_insensitive;
121 $src_tables_checked{$src_table_name} = 1;
122
4d438549 123
124 $self->diff_table_options($src_table, $tar_table);
51ffe5ee 125
da5a1bae 126 ## Compare fields, their types, defaults, sizes etc etc
4d438549 127 $self->diff_table_fields($src_table, $tar_table);
51ffe5ee 128
4d438549 129 $self->diff_table_indexes($src_table, $tar_table);
130 $self->diff_table_constraints($src_table, $tar_table);
51ffe5ee 131
4d438549 132 } # end of target_schema->get_tables loop
51ffe5ee 133
4d438549 134 for my $src_table ( $source_schema->get_tables ) {
135 my $src_table_name = $src_table->name;
51ffe5ee 136
46bf5655 137 $src_table_name = lc $src_table_name if $self->case_insensitive;
51ffe5ee 138
46bf5655 139 push @{ $self->tables_to_drop}, $src_table
140 unless $src_tables_checked{$src_table_name};
4d438549 141 }
142
143 return $self;
144}
145
146sub produce_diff_sql {
147 my ($self) = @_;
148
149 my $target_schema = $self->target_schema;
150 my $source_schema = $self->source_schema;
151 my $tar_name = $target_schema->name;
152 my $src_name = $source_schema->name;
153
154 my $producer_class = "SQL::Translator::Producer::@{[$self->output_db]}";
155 eval "require $producer_class";
156 die $@ if $@;
157
158 # Map of name we store under => producer method name
159 my %func_map = (
160 constraints_to_create => 'alter_create_constraint',
161 constraints_to_drop => 'alter_drop_constraint',
162 indexes_to_create => 'alter_create_index',
163 indexes_to_drop => 'alter_drop_index',
164 fields_to_create => 'add_field',
165 fields_to_alter => 'alter_field',
166 fields_to_rename => 'rename_field',
167 fields_to_drop => 'drop_field',
46bf5655 168 table_options => 'alter_table',
169 table_renamed_from => 'rename_table',
4d438549 170 );
171 my @diffs;
172
173 if (!$self->no_batch_alters &&
174 (my $batch_alter = $producer_class->can('batch_alter_table')) )
175 {
176 # Good - Producer supports batch altering of tables.
177 foreach my $table ( sort keys %{$self->table_diff_hash} ) {
178 my $tar_table = $target_schema->get_table($table)
179 || $source_schema->get_table($table);
da5a1bae 180
4d438549 181 push @diffs, $batch_alter->($tar_table,
182 { map {
183 $func_map{$_} => $self->table_diff_hash->{$table}{$_}
184 } keys %func_map
7467c458 185 },
186 $self->producer_options
4d438549 187 );
da5a1bae 188 }
4d438549 189 } else {
51ffe5ee 190
46bf5655 191 # If we have any table renames we need to do those first;
4d438549 192 my %flattened_diffs;
193 foreach my $table ( sort keys %{$self->table_diff_hash} ) {
194 my $table_diff = $self->table_diff_hash->{$table};
195 for (@diff_hash_keys) {
196 push( @{ $flattened_diffs{ $func_map{$_} } ||= [] }, @{ $table_diff->{$_} } );
51ffe5ee 197 }
4d438549 198 }
da5a1bae 199
4d438549 200 push @diffs, map( {
46bf5655 201 if (@{ $flattened_diffs{$_} || [] }) {
4d438549 202 my $meth = $producer_class->can($_);
203
7467c458 204 $meth ? map {
205 my $sql = $meth->( (ref $_ eq 'ARRAY' ? @$_ : $_), $self->producer_options );
24d9fe69 206 $sql ? ("$sql") : ();
7467c458 207 } @{ $flattened_diffs{$_} }
4d438549 208 : $self->ignore_missing_methods
209 ? "-- $producer_class cant $_"
210 : die "$producer_class cant $_";
211 } else { () }
51ffe5ee 212
46bf5655 213 } qw/rename_table
214 alter_drop_constraint
4d438549 215 alter_drop_index
216 drop_field
217 add_field
218 alter_field
219 rename_field
220 alter_create_index
221 alter_create_constraint
222 alter_table/),
da5a1bae 223 }
51ffe5ee 224
4d438549 225 if (my @tables = @{ $self->tables_to_create } ) {
226 my $translator = new SQL::Translator(
227 producer_type => $self->output_db,
228 add_drop_table => 0,
229 no_comments => 1,
230 # TODO: sort out options
7467c458 231 %{ $self->producer_options }
4d438549 232 );
f9c96971 233 $translator->producer_args->{no_transaction} = 1;
4d438549 234 my $schema = $translator->schema;
51ffe5ee 235
4d438549 236 $schema->add_table($_) for @tables;
51ffe5ee 237
4d438549 238 unshift @diffs,
239 # Remove begin/commit here, since we wrap everything in one.
24d9fe69 240 grep { $_ !~ /^(?:COMMIT|START(?: TRANSACTION)?|BEGIN(?: TRANSACTION)?)/ } $producer_class->can('produce')->($translator);
7ac784ff 241 }
242
4d438549 243 if (my @tables_to_drop = @{ $self->{tables_to_drop} || []} ) {
244 my $meth = $producer_class->can('drop_table');
245
24d9fe69 246 push @diffs, $meth ? ( map { $meth->($_, $self->producer_options) } @tables_to_drop)
4d438549 247 : $self->ignore_missing_methods
248 ? "-- $producer_class cant drop_table"
249 : die "$producer_class cant drop_table";
250 }
7ac784ff 251
4d438549 252 if (@diffs) {
24d9fe69 253 unshift @diffs, "BEGIN";
254 push @diffs, "\nCOMMIT";
4d438549 255 } else {
24d9fe69 256 @diffs = ("-- No differences found");
51ffe5ee 257 }
51ffe5ee 258
da5a1bae 259 if ( @diffs ) {
3406fd5b 260 if ( $self->output_db !~ /^(?:MySQL|SQLite|PostgreSQL)$/ ) {
e30b71b8 261 unshift(@diffs, "-- Output database @{[$self->output_db]} is untested/unsupported!!!");
51ffe5ee 262 }
c12a81eb 263
264 my @return =
265 map { $_ ? ( $_ =~ /;$/xms ? $_ : "$_;\n\n" ) : "\n" }
266 ("-- Convert schema '$src_name' to '$tar_name':", @diffs);
267
268 return wantarray ? @return : join('', @return);
51ffe5ee 269 }
da5a1bae 270 return undef;
4d438549 271
272}
273
274sub diff_table_indexes {
275 my ($self, $src_table, $tar_table) = @_;
276
277 my (%checked_indices);
278 INDEX_CREATE:
279 for my $i_tar ( $tar_table->get_indices ) {
280 for my $i_src ( $src_table->get_indices ) {
281 if ( $i_tar->equals($i_src, $self->case_insensitive, $self->ignore_index_names) ) {
282 $checked_indices{$i_src} = 1;
283 next INDEX_CREATE;
284 }
285 }
286 push @{$self->table_diff_hash->{$tar_table}{indexes_to_create}}, $i_tar;
287 }
288
289 INDEX_DROP:
290 for my $i_src ( $src_table->get_indices ) {
291 next if !$self->ignore_index_names && $checked_indices{$i_src};
292 for my $i_tar ( $tar_table->get_indices ) {
293 next INDEX_DROP if $i_src->equals($i_tar, $self->case_insensitive, $self->ignore_index_names);
294 }
295 push @{$self->table_diff_hash->{$tar_table}{indexes_to_drop}}, $i_src;
296 }
297}
298
299
300sub diff_table_constraints {
301 my ($self, $src_table, $tar_table) = @_;
302
303 my(%checked_constraints);
304 CONSTRAINT_CREATE:
305 for my $c_tar ( $tar_table->get_constraints ) {
306 for my $c_src ( $src_table->get_constraints ) {
46bf5655 307
308 # This is a bit of a hack - needed for renaming tables to work
309 local $c_src->{table} = $tar_table;
310
4d438549 311 if ( $c_tar->equals($c_src, $self->case_insensitive, $self->ignore_constraint_names) ) {
312 $checked_constraints{$c_src} = 1;
313 next CONSTRAINT_CREATE;
314 }
315 }
316 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_create} }, $c_tar;
317 }
318
319
320 CONSTRAINT_DROP:
321 for my $c_src ( $src_table->get_constraints ) {
46bf5655 322
323 # This is a bit of a hack - needed for renaming tables to work
324 local $c_src->{table} = $tar_table;
325
4d438549 326 next if !$self->ignore_constraint_names && $checked_constraints{$c_src};
327 for my $c_tar ( $tar_table->get_constraints ) {
328 next CONSTRAINT_DROP if $c_src->equals($c_tar, $self->case_insensitive, $self->ignore_constraint_names);
329 }
330
331 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_drop} }, $c_src;
51ffe5ee 332 }
51ffe5ee 333
4d438549 334}
335
336sub diff_table_fields {
337 my ($self, $src_table, $tar_table) = @_;
338
339 # List of ones ew've renamed from so we dont drop them
340 my %renamed_source_fields;
341
342 for my $tar_table_field ( $tar_table->get_fields ) {
343 my $f_tar_name = $tar_table_field->name;
344
345 if (my $old_name = $tar_table_field->extra->{renamed_from}) {
346 my $src_table_field = $src_table->get_field( $old_name, $self->case_insensitive );
46bf5655 347 unless ($src_table_field) {
348 warn qq#Renamed column can't find old column "@{[$src_table->name]}.$old_name" for renamed column\n#;
349 delete $tar_table_field->extra->{renamed_from};
350 } else {
351 push @{$self->table_diff_hash->{$tar_table}{fields_to_rename} }, [ $src_table_field, $tar_table_field ];
352 $renamed_source_fields{$old_name} = 1;
353 next;
354 }
4d438549 355 }
356
357 my $src_table_field = $src_table->get_field( $f_tar_name, $self->case_insensitive );
358
359 unless ( $src_table_field ) {
360 push @{$self->table_diff_hash->{$tar_table}{fields_to_create}}, $tar_table_field;
361 next;
362 }
363
07d6e5f7 364 # field exists, something changed. This is a bit complex. Parsers can
365 # normalize types, but only some of them do, so compare the normalized and
366 # parsed types for each field to each other
367 if ( !$tar_table_field->equals($src_table_field, $self->case_insensitive) &&
368 !$tar_table_field->equals($src_table_field->parsed_field, $self->case_insensitive) &&
369 !$tar_table_field->parsed_field->equals($src_table_field, $self->case_insensitive) &&
370 !$tar_table_field->parsed_field->equals($src_table_field->parsed_field, $self->case_insensitive) ) {
4d438549 371
372 # Some producers might need src field to diff against
373 push @{$self->table_diff_hash->{$tar_table}{fields_to_alter}}, [ $src_table_field, $tar_table_field ];
374 next;
375 }
376 }
377
378
379 # Now check to see if any fields from src_table need to be dropped
380 for my $src_table_field ( $src_table->get_fields ) {
381 my $f_src_name = $src_table_field->name;
382 next if $renamed_source_fields{$f_src_name};
383
384 my $tar_table_field = $tar_table->get_field( $f_src_name, $self->case_insensitive );
385
386 unless ( $tar_table_field ) {
387 push @{$self->table_diff_hash->{$tar_table}{fields_to_drop}}, $src_table_field;
388 next;
389 }
390 }
391}
392
393sub diff_table_options {
394 my ($self, $src_table, $tar_table) = @_;
395
7725e1e6 396 my $cmp = sub {
397 my ($a_name, undef, $b_name, undef) = ( %$a, %$b );
398 $a_name cmp $b_name;
399 };
400 # Need to sort the options so we dont get supruious diffs.
401 my (@src_opts, @tar_opts);
402 @src_opts = sort $cmp $src_table->options;
403 @tar_opts = sort $cmp $tar_table->options;
404
4d438549 405
4d438549 406 # If there's a difference, just re-set all the options
407 push @{ $self->table_diff_hash->{$tar_table}{table_options} }, $tar_table
7725e1e6 408 unless $src_table->_compare_objects( \@src_opts, \@tar_opts );
4d438549 409}
410
51ffe5ee 4111;
4d438549 412
413__END__
414
415=head1 NAME
416
417SQL::Translator::Diff
418
419=head1 DESCRIPTION
420
421Takes two input SQL::Translator::Schemas (or SQL files) and produces ALTER
422statments to make them the same
423
424=head1 SNYOPSIS
425
426Simplest usage:
427
428 use SQL::Translator::Diff;
429 my $sql = SQL::Translator::Diff::schema_diff($source_schema, 'MySQL', $target_schema, 'MySQL', $options_hash)
430
431OO usage:
432
433 use SQL::Translator::Diff;
434 my $diff = SQL::Translator::Diff->new({
435 output_db => 'MySQL',
436 source_schema => $source_schema,
437 target_schema => $target_schema,
438 %$options_hash,
439 })->compute_differences->produce_diff_sql;
440
441=head1 OPTIONS
442
443=over
444
445=item B<ignore_index_names>
446
447Match indexes based on types and fields, ignoring name.
448
449=item B<ignore_constraint_names>
450
451Match constrains based on types, fields and tables, ignoring name.
452
453=item B<output_db>
454
455Which producer to use to produce the output.
456
457=item B<case_insensitive>
458
459Ignore case of table, field, index and constraint names when comparing
460
461=item B<no_batch_alters>
462
463Produce each alter as a distinct C<ALTER TABLE> statement even if the producer
464supports the ability to do all alters for a table as one statement.
465
466=item B<ignore_missing_methods>
467
468If the diff would need a method that is missing from the producer, just emit a
469comment showing the method is missing, rather than dieing with an error
470
471=back
472
473=head1 PRODUCER FUNCTIONS
474
475The following producer functions should be implemented for completeness. If
476any of them are needed for a given diff, but not found, an error will be
477thrown.
478
479=over
480
481=item * C<alter_create_constraint($con)>
482
483=item * C<alter_drop_constraint($con)>
484
485=item * C<alter_create_index($idx)>
486
487=item * C<alter_drop_index($idx)>
488
489=item * C<add_field($fld)>
490
491=item * C<alter_field($old_fld, $new_fld)>
492
493=item * C<rename_field($old_fld, $new_fld)>
494
495=item * C<drop_field($fld)>
496
497=item * C<alter_table($table)>
498
499=item * C<drop_table($table)>
500
46bf5655 501=item * C<rename_table($old_table, $new_table)> (optional)
502
4d438549 503=item * C<batch_alter_table($table, $hash)> (optional)
504
4d438549 505If the producer supports C<batch_alter_table>, it will be called with the
506table to alter and a hash, the keys of which will be the method names listed
507above; values will be arrays of fields or constraints to operate on. In the
508case of the field functions that take two arguments this will appear as a hash.
509
510I.e. the hash might look something like the following:
511
512 {
513 alter_create_constraint => [ $constraint1, $constraint2 ],
514 add_field => [ $field ],
515 alter_field => [ [$old_field, $new_field] ]
516 }
517
934e1b39 518
519=item * C<preprocess_schema($class, $schema)> (optional)
520
521C<preprocess_schema> is called by the Diff code to allow the producer to
522normalize any data it needs to first. For example, the MySQL producer uses
523this method to ensure that FK contraint names are unique.
524
525Basicaly any changes that need to be made to produce the SQL file for the
526schema should be done here, so that a diff between a parsed SQL file and (say)
527a parsed DBIx::Class::Schema object will be sane.
528
529(As an aside, DBIx::Class, for instance, uses the presence of a
530C<preprocess_schema> function on the producer to know that it can diff between
531the previous SQL file and its own internal representation. Without this method
532on th producer it will diff the two SQL files which is slower, but known to
533work better on old-style producers.)
534
535=back
536
537
4d438549 538=head1 AUTHOR
539
540Original Author(s) unknown.
541
934e1b39 542Refactor/re-write and more comprehensive tests by Ash Berlin C<< ash@cpan.org >>.
4d438549 543
544Redevelopment sponsored by Takkle Inc.
545
546=cut