Fix warning messages
[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
16 ignore_proc_sql output_db source_schema source_db target_schema target_db
17 case_insensitive no_batch_alters ignore_missing_methods
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
51ffe5ee 48 my ($source_schema, $source_db, $target_schema, $target_db, $options) = @_;
4d438549 49 $options ||= {};
da5a1bae 50
4d438549 51 my $obj = SQL::Translator::Diff->new( {
52 %$options,
53 source_schema => $source_schema,
54 source_db => $source_db,
55 target_schema => $target_schema,
56 target_db => $target_db
57 } );
da5a1bae 58
4d438549 59 $obj->compute_differences->produce_diff_sql;
60}
51ffe5ee 61
4d438549 62sub new {
63 my ($class, $values) = @_;
64 $values->{$_} ||= [] foreach @diff_arrays;
65 $values->{table_diff_hash} = {};
66
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')) {
82 $producer_class->$preprocess($source_schema);
9ab59f87 83 $producer_class->$preprocess($target_schema);
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
51ffe5ee 183 }
4d438549 184 );
da5a1bae 185 }
4d438549 186 } else {
51ffe5ee 187
46bf5655 188 # If we have any table renames we need to do those first;
4d438549 189 my %flattened_diffs;
190 foreach my $table ( sort keys %{$self->table_diff_hash} ) {
191 my $table_diff = $self->table_diff_hash->{$table};
192 for (@diff_hash_keys) {
193 push( @{ $flattened_diffs{ $func_map{$_} } ||= [] }, @{ $table_diff->{$_} } );
51ffe5ee 194 }
4d438549 195 }
da5a1bae 196
4d438549 197 push @diffs, map( {
46bf5655 198 if (@{ $flattened_diffs{$_} || [] }) {
4d438549 199 my $meth = $producer_class->can($_);
200
201 $meth ? map { my $sql = $meth->(ref $_ eq 'ARRAY' ? @$_ : $_); $sql ? ("$sql;") : () } @{ $flattened_diffs{$_} }
202 : $self->ignore_missing_methods
203 ? "-- $producer_class cant $_"
204 : die "$producer_class cant $_";
205 } else { () }
51ffe5ee 206
46bf5655 207 } qw/rename_table
208 alter_drop_constraint
4d438549 209 alter_drop_index
210 drop_field
211 add_field
212 alter_field
213 rename_field
214 alter_create_index
215 alter_create_constraint
216 alter_table/),
da5a1bae 217 }
51ffe5ee 218
4d438549 219 if (my @tables = @{ $self->tables_to_create } ) {
220 my $translator = new SQL::Translator(
221 producer_type => $self->output_db,
222 add_drop_table => 0,
223 no_comments => 1,
224 # TODO: sort out options
225 quote_table_names => 0,
226 quote_field_names => 0,
227 );
228 my $schema = $translator->schema;
51ffe5ee 229
4d438549 230 $schema->add_table($_) for @tables;
51ffe5ee 231
4d438549 232 unshift @diffs,
233 # Remove begin/commit here, since we wrap everything in one.
46bf5655 234 grep { $_ !~ /^(?:COMMIT|START(?: TRANSACTION)?|BEGIN(?: TRANSACTION)?);/ } $producer_class->can('produce')->($translator);
7ac784ff 235 }
236
4d438549 237 if (my @tables_to_drop = @{ $self->{tables_to_drop} || []} ) {
238 my $meth = $producer_class->can('drop_table');
239
240 push @diffs, $meth ? map( { $meth->($_) } @tables_to_drop )
241 : $self->ignore_missing_methods
242 ? "-- $producer_class cant drop_table"
243 : die "$producer_class cant drop_table";
244 }
7ac784ff 245
4d438549 246 if (@diffs) {
1664676a 247 unshift @diffs, "BEGIN;\n";
4d438549 248 push @diffs, "\nCOMMIT;\n";
249 } else {
250 @diffs = ("-- No differences found\n\n");
51ffe5ee 251 }
51ffe5ee 252
da5a1bae 253 if ( @diffs ) {
4d438549 254 if ( $self->target_db !~ /^(?:MySQL|SQLite)$/ ) {
255 unshift(@diffs, "-- Target database @{[$self->target_db]} is untested/unsupported!!!");
51ffe5ee 256 }
4d438549 257 return join( "\n", "-- Convert schema '$src_name' to '$tar_name':\n", @diffs);
51ffe5ee 258 }
da5a1bae 259 return undef;
4d438549 260
261}
262
263sub diff_table_indexes {
264 my ($self, $src_table, $tar_table) = @_;
265
266 my (%checked_indices);
267 INDEX_CREATE:
268 for my $i_tar ( $tar_table->get_indices ) {
269 for my $i_src ( $src_table->get_indices ) {
270 if ( $i_tar->equals($i_src, $self->case_insensitive, $self->ignore_index_names) ) {
271 $checked_indices{$i_src} = 1;
272 next INDEX_CREATE;
273 }
274 }
275 push @{$self->table_diff_hash->{$tar_table}{indexes_to_create}}, $i_tar;
276 }
277
278 INDEX_DROP:
279 for my $i_src ( $src_table->get_indices ) {
280 next if !$self->ignore_index_names && $checked_indices{$i_src};
281 for my $i_tar ( $tar_table->get_indices ) {
282 next INDEX_DROP if $i_src->equals($i_tar, $self->case_insensitive, $self->ignore_index_names);
283 }
284 push @{$self->table_diff_hash->{$tar_table}{indexes_to_drop}}, $i_src;
285 }
286}
287
288
289sub diff_table_constraints {
290 my ($self, $src_table, $tar_table) = @_;
291
292 my(%checked_constraints);
293 CONSTRAINT_CREATE:
294 for my $c_tar ( $tar_table->get_constraints ) {
295 for my $c_src ( $src_table->get_constraints ) {
46bf5655 296
297 # This is a bit of a hack - needed for renaming tables to work
298 local $c_src->{table} = $tar_table;
299
4d438549 300 if ( $c_tar->equals($c_src, $self->case_insensitive, $self->ignore_constraint_names) ) {
301 $checked_constraints{$c_src} = 1;
302 next CONSTRAINT_CREATE;
303 }
304 }
305 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_create} }, $c_tar;
306 }
307
308
309 CONSTRAINT_DROP:
310 for my $c_src ( $src_table->get_constraints ) {
46bf5655 311
312 # This is a bit of a hack - needed for renaming tables to work
313 local $c_src->{table} = $tar_table;
314
4d438549 315 next if !$self->ignore_constraint_names && $checked_constraints{$c_src};
316 for my $c_tar ( $tar_table->get_constraints ) {
317 next CONSTRAINT_DROP if $c_src->equals($c_tar, $self->case_insensitive, $self->ignore_constraint_names);
318 }
319
320 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_drop} }, $c_src;
51ffe5ee 321 }
51ffe5ee 322
4d438549 323}
324
325sub diff_table_fields {
326 my ($self, $src_table, $tar_table) = @_;
327
328 # List of ones ew've renamed from so we dont drop them
329 my %renamed_source_fields;
330
331 for my $tar_table_field ( $tar_table->get_fields ) {
332 my $f_tar_name = $tar_table_field->name;
333
334 if (my $old_name = $tar_table_field->extra->{renamed_from}) {
335 my $src_table_field = $src_table->get_field( $old_name, $self->case_insensitive );
46bf5655 336 unless ($src_table_field) {
337 warn qq#Renamed column can't find old column "@{[$src_table->name]}.$old_name" for renamed column\n#;
338 delete $tar_table_field->extra->{renamed_from};
339 } else {
340 push @{$self->table_diff_hash->{$tar_table}{fields_to_rename} }, [ $src_table_field, $tar_table_field ];
341 $renamed_source_fields{$old_name} = 1;
342 next;
343 }
4d438549 344 }
345
346 my $src_table_field = $src_table->get_field( $f_tar_name, $self->case_insensitive );
347
348 unless ( $src_table_field ) {
349 push @{$self->table_diff_hash->{$tar_table}{fields_to_create}}, $tar_table_field;
350 next;
351 }
352
07d6e5f7 353 # field exists, something changed. This is a bit complex. Parsers can
354 # normalize types, but only some of them do, so compare the normalized and
355 # parsed types for each field to each other
356 if ( !$tar_table_field->equals($src_table_field, $self->case_insensitive) &&
357 !$tar_table_field->equals($src_table_field->parsed_field, $self->case_insensitive) &&
358 !$tar_table_field->parsed_field->equals($src_table_field, $self->case_insensitive) &&
359 !$tar_table_field->parsed_field->equals($src_table_field->parsed_field, $self->case_insensitive) ) {
4d438549 360
361 # Some producers might need src field to diff against
362 push @{$self->table_diff_hash->{$tar_table}{fields_to_alter}}, [ $src_table_field, $tar_table_field ];
363 next;
364 }
365 }
366
367
368 # Now check to see if any fields from src_table need to be dropped
369 for my $src_table_field ( $src_table->get_fields ) {
370 my $f_src_name = $src_table_field->name;
371 next if $renamed_source_fields{$f_src_name};
372
373 my $tar_table_field = $tar_table->get_field( $f_src_name, $self->case_insensitive );
374
375 unless ( $tar_table_field ) {
376 push @{$self->table_diff_hash->{$tar_table}{fields_to_drop}}, $src_table_field;
377 next;
378 }
379 }
380}
381
382sub diff_table_options {
383 my ($self, $src_table, $tar_table) = @_;
384
385
4d438549 386 # If there's a difference, just re-set all the options
387 push @{ $self->table_diff_hash->{$tar_table}{table_options} }, $tar_table
9ab59f87 388 unless $src_table->_compare_objects( scalar $src_table->options, scalar $tar_table->options );
4d438549 389}
390
51ffe5ee 3911;
4d438549 392
393__END__
394
395=head1 NAME
396
397SQL::Translator::Diff
398
399=head1 DESCRIPTION
400
401Takes two input SQL::Translator::Schemas (or SQL files) and produces ALTER
402statments to make them the same
403
404=head1 SNYOPSIS
405
406Simplest usage:
407
408 use SQL::Translator::Diff;
409 my $sql = SQL::Translator::Diff::schema_diff($source_schema, 'MySQL', $target_schema, 'MySQL', $options_hash)
410
411OO usage:
412
413 use SQL::Translator::Diff;
414 my $diff = SQL::Translator::Diff->new({
415 output_db => 'MySQL',
416 source_schema => $source_schema,
417 target_schema => $target_schema,
418 %$options_hash,
419 })->compute_differences->produce_diff_sql;
420
421=head1 OPTIONS
422
423=over
424
425=item B<ignore_index_names>
426
427Match indexes based on types and fields, ignoring name.
428
429=item B<ignore_constraint_names>
430
431Match constrains based on types, fields and tables, ignoring name.
432
433=item B<output_db>
434
435Which producer to use to produce the output.
436
437=item B<case_insensitive>
438
439Ignore case of table, field, index and constraint names when comparing
440
441=item B<no_batch_alters>
442
443Produce each alter as a distinct C<ALTER TABLE> statement even if the producer
444supports the ability to do all alters for a table as one statement.
445
446=item B<ignore_missing_methods>
447
448If the diff would need a method that is missing from the producer, just emit a
449comment showing the method is missing, rather than dieing with an error
450
451=back
452
453=head1 PRODUCER FUNCTIONS
454
455The following producer functions should be implemented for completeness. If
456any of them are needed for a given diff, but not found, an error will be
457thrown.
458
459=over
460
461=item * C<alter_create_constraint($con)>
462
463=item * C<alter_drop_constraint($con)>
464
465=item * C<alter_create_index($idx)>
466
467=item * C<alter_drop_index($idx)>
468
469=item * C<add_field($fld)>
470
471=item * C<alter_field($old_fld, $new_fld)>
472
473=item * C<rename_field($old_fld, $new_fld)>
474
475=item * C<drop_field($fld)>
476
477=item * C<alter_table($table)>
478
479=item * C<drop_table($table)>
480
46bf5655 481=item * C<rename_table($old_table, $new_table)> (optional)
482
4d438549 483=item * C<batch_alter_table($table, $hash)> (optional)
484
46bf5655 485
4d438549 486=back
487
488If the producer supports C<batch_alter_table>, it will be called with the
489table to alter and a hash, the keys of which will be the method names listed
490above; values will be arrays of fields or constraints to operate on. In the
491case of the field functions that take two arguments this will appear as a hash.
492
493I.e. the hash might look something like the following:
494
495 {
496 alter_create_constraint => [ $constraint1, $constraint2 ],
497 add_field => [ $field ],
498 alter_field => [ [$old_field, $new_field] ]
499 }
500
501=head1 AUTHOR
502
503Original Author(s) unknown.
504
505Refactor and more comprehensive tests by Ash Berlin C<< ash@cpan.org >>.
506
507Redevelopment sponsored by Takkle Inc.
508
509=cut