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