Changes + Reverts for 0.11000, see Changes file for info
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Diff.pm
CommitLineData
51ffe5ee 1package SQL::Translator::Diff;
4d438549 2
11ad2df9 3
51ffe5ee 4## SQLT schema diffing code
5use strict;
6use warnings;
11ad2df9 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 }
c12a81eb 261
262 my @return =
263 map { $_ ? ( $_ =~ /;$/xms ? $_ : "$_;\n\n" ) : "\n" }
264 ("-- Convert schema '$src_name' to '$tar_name':", @diffs);
265
266 return wantarray ? @return : join('', @return);
51ffe5ee 267 }
da5a1bae 268 return undef;
4d438549 269
270}
271
272sub diff_table_indexes {
273 my ($self, $src_table, $tar_table) = @_;
274
275 my (%checked_indices);
276 INDEX_CREATE:
277 for my $i_tar ( $tar_table->get_indices ) {
278 for my $i_src ( $src_table->get_indices ) {
279 if ( $i_tar->equals($i_src, $self->case_insensitive, $self->ignore_index_names) ) {
280 $checked_indices{$i_src} = 1;
281 next INDEX_CREATE;
282 }
283 }
284 push @{$self->table_diff_hash->{$tar_table}{indexes_to_create}}, $i_tar;
285 }
286
287 INDEX_DROP:
288 for my $i_src ( $src_table->get_indices ) {
289 next if !$self->ignore_index_names && $checked_indices{$i_src};
290 for my $i_tar ( $tar_table->get_indices ) {
291 next INDEX_DROP if $i_src->equals($i_tar, $self->case_insensitive, $self->ignore_index_names);
292 }
293 push @{$self->table_diff_hash->{$tar_table}{indexes_to_drop}}, $i_src;
294 }
295}
296
297
298sub diff_table_constraints {
299 my ($self, $src_table, $tar_table) = @_;
300
301 my(%checked_constraints);
302 CONSTRAINT_CREATE:
303 for my $c_tar ( $tar_table->get_constraints ) {
304 for my $c_src ( $src_table->get_constraints ) {
46bf5655 305
306 # This is a bit of a hack - needed for renaming tables to work
307 local $c_src->{table} = $tar_table;
308
4d438549 309 if ( $c_tar->equals($c_src, $self->case_insensitive, $self->ignore_constraint_names) ) {
310 $checked_constraints{$c_src} = 1;
311 next CONSTRAINT_CREATE;
312 }
313 }
314 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_create} }, $c_tar;
315 }
316
317
318 CONSTRAINT_DROP:
319 for my $c_src ( $src_table->get_constraints ) {
46bf5655 320
321 # This is a bit of a hack - needed for renaming tables to work
322 local $c_src->{table} = $tar_table;
323
4d438549 324 next if !$self->ignore_constraint_names && $checked_constraints{$c_src};
325 for my $c_tar ( $tar_table->get_constraints ) {
326 next CONSTRAINT_DROP if $c_src->equals($c_tar, $self->case_insensitive, $self->ignore_constraint_names);
327 }
328
329 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_drop} }, $c_src;
51ffe5ee 330 }
51ffe5ee 331
4d438549 332}
333
334sub diff_table_fields {
335 my ($self, $src_table, $tar_table) = @_;
336
337 # List of ones ew've renamed from so we dont drop them
338 my %renamed_source_fields;
339
340 for my $tar_table_field ( $tar_table->get_fields ) {
341 my $f_tar_name = $tar_table_field->name;
342
343 if (my $old_name = $tar_table_field->extra->{renamed_from}) {
344 my $src_table_field = $src_table->get_field( $old_name, $self->case_insensitive );
46bf5655 345 unless ($src_table_field) {
346 warn qq#Renamed column can't find old column "@{[$src_table->name]}.$old_name" for renamed column\n#;
347 delete $tar_table_field->extra->{renamed_from};
348 } else {
349 push @{$self->table_diff_hash->{$tar_table}{fields_to_rename} }, [ $src_table_field, $tar_table_field ];
350 $renamed_source_fields{$old_name} = 1;
351 next;
352 }
4d438549 353 }
354
355 my $src_table_field = $src_table->get_field( $f_tar_name, $self->case_insensitive );
356
357 unless ( $src_table_field ) {
358 push @{$self->table_diff_hash->{$tar_table}{fields_to_create}}, $tar_table_field;
359 next;
360 }
361
07d6e5f7 362 # field exists, something changed. This is a bit complex. Parsers can
363 # normalize types, but only some of them do, so compare the normalized and
364 # parsed types for each field to each other
365 if ( !$tar_table_field->equals($src_table_field, $self->case_insensitive) &&
366 !$tar_table_field->equals($src_table_field->parsed_field, $self->case_insensitive) &&
367 !$tar_table_field->parsed_field->equals($src_table_field, $self->case_insensitive) &&
368 !$tar_table_field->parsed_field->equals($src_table_field->parsed_field, $self->case_insensitive) ) {
4d438549 369
370 # Some producers might need src field to diff against
371 push @{$self->table_diff_hash->{$tar_table}{fields_to_alter}}, [ $src_table_field, $tar_table_field ];
372 next;
373 }
374 }
375
376
377 # Now check to see if any fields from src_table need to be dropped
378 for my $src_table_field ( $src_table->get_fields ) {
379 my $f_src_name = $src_table_field->name;
380 next if $renamed_source_fields{$f_src_name};
381
382 my $tar_table_field = $tar_table->get_field( $f_src_name, $self->case_insensitive );
383
384 unless ( $tar_table_field ) {
385 push @{$self->table_diff_hash->{$tar_table}{fields_to_drop}}, $src_table_field;
386 next;
387 }
388 }
389}
390
391sub diff_table_options {
392 my ($self, $src_table, $tar_table) = @_;
393
7725e1e6 394 my $cmp = sub {
395 my ($a_name, undef, $b_name, undef) = ( %$a, %$b );
396 $a_name cmp $b_name;
397 };
398 # Need to sort the options so we dont get supruious diffs.
399 my (@src_opts, @tar_opts);
400 @src_opts = sort $cmp $src_table->options;
401 @tar_opts = sort $cmp $tar_table->options;
402
4d438549 403
4d438549 404 # If there's a difference, just re-set all the options
405 push @{ $self->table_diff_hash->{$tar_table}{table_options} }, $tar_table
7725e1e6 406 unless $src_table->_compare_objects( \@src_opts, \@tar_opts );
4d438549 407}
408
51ffe5ee 4091;
4d438549 410
411__END__
412
413=head1 NAME
414
415SQL::Translator::Diff
416
417=head1 DESCRIPTION
418
419Takes two input SQL::Translator::Schemas (or SQL files) and produces ALTER
420statments to make them the same
421
422=head1 SNYOPSIS
423
424Simplest usage:
425
426 use SQL::Translator::Diff;
427 my $sql = SQL::Translator::Diff::schema_diff($source_schema, 'MySQL', $target_schema, 'MySQL', $options_hash)
428
429OO usage:
430
431 use SQL::Translator::Diff;
432 my $diff = SQL::Translator::Diff->new({
433 output_db => 'MySQL',
434 source_schema => $source_schema,
435 target_schema => $target_schema,
436 %$options_hash,
437 })->compute_differences->produce_diff_sql;
438
439=head1 OPTIONS
440
441=over
442
443=item B<ignore_index_names>
444
445Match indexes based on types and fields, ignoring name.
446
447=item B<ignore_constraint_names>
448
449Match constrains based on types, fields and tables, ignoring name.
450
451=item B<output_db>
452
453Which producer to use to produce the output.
454
455=item B<case_insensitive>
456
457Ignore case of table, field, index and constraint names when comparing
458
459=item B<no_batch_alters>
460
461Produce each alter as a distinct C<ALTER TABLE> statement even if the producer
462supports the ability to do all alters for a table as one statement.
463
464=item B<ignore_missing_methods>
465
466If the diff would need a method that is missing from the producer, just emit a
467comment showing the method is missing, rather than dieing with an error
468
469=back
470
471=head1 PRODUCER FUNCTIONS
472
473The following producer functions should be implemented for completeness. If
474any of them are needed for a given diff, but not found, an error will be
475thrown.
476
477=over
478
479=item * C<alter_create_constraint($con)>
480
481=item * C<alter_drop_constraint($con)>
482
483=item * C<alter_create_index($idx)>
484
485=item * C<alter_drop_index($idx)>
486
487=item * C<add_field($fld)>
488
489=item * C<alter_field($old_fld, $new_fld)>
490
491=item * C<rename_field($old_fld, $new_fld)>
492
493=item * C<drop_field($fld)>
494
495=item * C<alter_table($table)>
496
497=item * C<drop_table($table)>
498
46bf5655 499=item * C<rename_table($old_table, $new_table)> (optional)
500
4d438549 501=item * C<batch_alter_table($table, $hash)> (optional)
502
4d438549 503If the producer supports C<batch_alter_table>, it will be called with the
504table to alter and a hash, the keys of which will be the method names listed
505above; values will be arrays of fields or constraints to operate on. In the
506case of the field functions that take two arguments this will appear as a hash.
507
508I.e. the hash might look something like the following:
509
510 {
511 alter_create_constraint => [ $constraint1, $constraint2 ],
512 add_field => [ $field ],
513 alter_field => [ [$old_field, $new_field] ]
514 }
515
934e1b39 516
517=item * C<preprocess_schema($class, $schema)> (optional)
518
519C<preprocess_schema> is called by the Diff code to allow the producer to
520normalize any data it needs to first. For example, the MySQL producer uses
521this method to ensure that FK contraint names are unique.
522
523Basicaly any changes that need to be made to produce the SQL file for the
524schema should be done here, so that a diff between a parsed SQL file and (say)
525a parsed DBIx::Class::Schema object will be sane.
526
527(As an aside, DBIx::Class, for instance, uses the presence of a
528C<preprocess_schema> function on the producer to know that it can diff between
529the previous SQL file and its own internal representation. Without this method
530on th producer it will diff the two SQL files which is slower, but known to
531work better on old-style producers.)
532
533=back
534
535
4d438549 536=head1 AUTHOR
537
538Original Author(s) unknown.
539
934e1b39 540Refactor/re-write and more comprehensive tests by Ash Berlin C<< ash@cpan.org >>.
4d438549 541
542Redevelopment sponsored by Takkle Inc.
543
544=cut